5 Commits
Author SHA1 Message Date
gitprov ada26ec40a Merge remote-tracking branch 'origin/main' 2026-08-22 23:43:33 +02:00
gitprov 1a9b6124c2 Merge remote-tracking branch 'origin/main' 2026-08-22 23:31:23 +02:00
gitprovandClaude Opus 4.8 5eab67dd5d Remove narrative-todo.md (audited done / superseded)
Its live items migrated into story-graph.md and persistent-boards.md; the
rest was shipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 23:31:23 +02:00
gitprovandClaude Opus 4.8 072e82f253 Prod: public origin gupi.glitch.university, internal services over HTTP
CORS_ORIGIN defaults to https://gupi.${DOMAIN} (the TLS frontend served by the
proxy); the app still reaches Postgres/MinIO by their docker hostnames over
plain HTTP on the shared network. Clarify the split in .env.prod.example and
fix the deploy success URL.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 23:09:43 +02:00
gitprovandClaude Opus 4.8 37c9fa1ebe Deploy: target /opt/gupi with its own .env.prod
Point deploy.sh at /opt/gupi (its own dir + .env.prod) so it uploads, builds
on the server, migrates, and runs `docker compose -f
/opt/gupi/docker-compose.prod.yml --env-file /opt/gupi/.env.prod up -d`.
The app-only prod stack still joins the shared network and uses gu_common's
Postgres/MinIO. Adds .env.prod.example as the fill-in template.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 22:54:18 +02:00
5 changed files with 58 additions and 125 deletions
+46
View File
@@ -0,0 +1,46 @@
# GUPI production environment TEMPLATE. Copy to /opt/gupi/.env.prod on the server and
# fill in real values there (never commit real secrets). GUPI runs ONLY its own app
# container; Postgres + MinIO come from the shared gu_common stack (must already be up).
#
# Endpoints:
# - Public (frontend, TLS via the proxy): https://gupi.glitch.university
# - Internal (app -> services, plain HTTP): docker hostnames gnommo-db / gnommo-minio
# (those hostnames live in docker-compose.prod.yml; here you only set credentials).
#
# Deploy with: ./deploy.sh (server-side it runs, from /opt/gupi:)
# docker compose -f /opt/gupi/docker-compose.prod.yml --env-file /opt/gupi/.env.prod up -d
# --- Shared PostgreSQL (gu_common; reachable as gnommo-db on the shared network) ---
POSTGRES_USER=gupi
POSTGRES_PASSWORD=CHANGE_ME
POSTGRES_DB=gupi
# --- Shared MinIO / S3 (gu_common; reachable as gnommo-minio) ---
MINIO_ROOT_USER=CHANGE_ME
MINIO_ROOT_PASSWORD=CHANGE_ME
S3_REGION=us-east-1
OSINT_S3_BUCKET=gupi-osint
# --- App ---
# DOMAIN drives the default public origin (https://gupi.${DOMAIN}); override CORS_ORIGIN
# directly if the frontend is served somewhere else.
DOMAIN=glitch.university
# CORS_ORIGIN=https://gupi.glitch.university
JWT_SECRET=CHANGE_ME_LONG_RANDOM_SECRET
LEVEL_EDITING_ENABLED=false
MAX_DOCUMENT_BYTES=26214400
# --- OCR (evidence text extraction) ---
OCR_ENABLED=true
OCR_LANGUAGES=nor+eng
OCR_TIMEOUT_MS=20000
MAX_OCR_BYTES=15728640
MAX_EXTRACTED_TEXT_CHARACTERS=200000
# --- Evidence LLM judge (optional; disabled by default) ---
EVIDENCE_JUDGE_PROVIDER=disabled
EVIDENCE_JUDGE_MODEL=
EVIDENCE_JUDGE_VERSION=evidence_claim_v1
EVIDENCE_JUDGE_TIMEOUT_MS=10000
EVIDENCE_JUDGE_MAX_CHARACTERS=20000
ANTHROPIC_API_KEY=
+3
View File
@@ -1,6 +1,9 @@
node_modules/ node_modules/
dist/ dist/
.env .env
.env.prod
.env.local
.env.*.local
.DS_Store .DS_Store
*.tsbuildinfo *.tsbuildinfo
playwright-report/ playwright-report/
+6 -5
View File
@@ -9,8 +9,9 @@ if [ "$1" = "--skip-pull" ]; then
fi fi
SERVER="${DEPLOY_SERVER:-root@76.13.144.52}" SERVER="${DEPLOY_SERVER:-root@76.13.144.52}"
REMOTE_DIR="${DEPLOY_DIR:-/opt/osint-board}" REMOTE_DIR="${DEPLOY_DIR:-/opt/gupi}"
COMPOSE="docker compose -f ${REMOTE_DIR}/docker-compose.prod.yml --env-file /opt/gu_common/.env.prod" # GUPI runs from its own dir with its own env; the shared services come from gu_common.
COMPOSE="docker compose -f ${REMOTE_DIR}/docker-compose.prod.yml --env-file ${REMOTE_DIR}/.env.prod"
TARGET_HOST=$(echo "${SERVER}" | sed 's/.*@//') TARGET_HOST=$(echo "${SERVER}" | sed 's/.*@//')
OWN_IP=$(curl -sf --max-time 3 ifconfig.me 2>/dev/null || echo "unknown") OWN_IP=$(curl -sf --max-time 3 ifconfig.me 2>/dev/null || echo "unknown")
@@ -54,8 +55,8 @@ rsync -avz --delete \
--exclude '.DS_Store' \ --exclude '.DS_Store' \
./ "${SERVER}:${REMOTE_DIR}/" ./ "${SERVER}:${REMOTE_DIR}/"
echo "==> Verifying gu_common configuration..." echo "==> Verifying GUPI environment..."
ssh "$SERVER" "test -f /opt/gu_common/.env.prod || { echo 'ERROR: /opt/gu_common/.env.prod is missing'; exit 1; }" ssh "$SERVER" "test -f ${REMOTE_DIR}/.env.prod || { echo 'ERROR: ${REMOTE_DIR}/.env.prod is missing (copy .env.prod.example and fill it in)'; exit 1; }"
echo "==> Ensuring shared network exists..." echo "==> Ensuring shared network exists..."
ssh "$SERVER" "docker network create gnommo 2>/dev/null || true" ssh "$SERVER" "docker network create gnommo 2>/dev/null || true"
@@ -97,4 +98,4 @@ for i in $(seq 1 24); do
fi fi
done done
echo "==> Done! https://osint.glitch.university" echo "==> Done! https://gupi.glitch.university"
+3 -1
View File
@@ -8,8 +8,10 @@ services:
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 8787 PORT: 8787
# Internal: reach gu_common services by their docker hostnames over plain HTTP.
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@gnommo-db:5432/${POSTGRES_DB} DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@gnommo-db:5432/${POSTGRES_DB}
CORS_ORIGIN: https://osint.${DOMAIN} # Public: the frontend is served over TLS at gupi.glitch.university by the proxy.
CORS_ORIGIN: ${CORS_ORIGIN:-https://gupi.${DOMAIN}}
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
LEVEL_EDITING_ENABLED: ${LEVEL_EDITING_ENABLED:-false} LEVEL_EDITING_ENABLED: ${LEVEL_EDITING_ENABLED:-false}
MAX_DOCUMENT_BYTES: ${MAX_DOCUMENT_BYTES:-26214400} MAX_DOCUMENT_BYTES: ${MAX_DOCUMENT_BYTES:-26214400}
-119
View File
@@ -1,119 +0,0 @@
# Narrative layer: campaigns, NPC cutscenes, and authoring
This roadmap covers the narrative layer end to end: the **campaign** that chains
levels into a mystery, the **NPCs / poses / cutscenes** the player watches
between levels, and the **admin authoring panel** that lets a game designer build
all of it in-app. Work generally proceeds top to bottom.
**Design intent.** These mysteries are real-world scam cases that must actually be
investigated to be understood. Such cases cannot be simplified, they
can only be staged for didactic discovery into levels.
The LLM is not a decoration on the cutscenes — it
is a **cognitive shim**: the assistant that keeps a player oriented as case
complexity grows. A player can use this feature many times, but some very bright
players might get it right on the first go.
This is what lets a mystery be as intricate as the real case
demands without the player getting lost. The scripted narrative layer below is the
delivery channel and the fallback; the LLM is the layer that scales comprehension.
Section 9 is deferred in build order but primary in intent, so the model is shaped
now to accommodate it (the `generated` step kind, the read-only context contract,
and the authored case model).
## Working agreement
- All narrative content — campaigns, NPCs, poses, cutscenes, dialogue text — is
authored template data frozen through supported operations. None of it is
hard-coded into React components or SQL seed literals. The exception to this rule is
custom cutscenes (react components) which are registered as components and referenced by the node.
- The admin panel is a GUI over the **same** operations available to the manifest
importer; both paths freeze the same immutable template data.
- A cutscene never mutates a board. The professor's scene *narrates* the new
document and goal; those exhibits and the updated brief already live in the
next chapter's template.
- A narrative behavior is complete only when its PostgreSQL representation, API
behavior, frontend presentation, persistence, and focused tests agree.
- Pose portraits are immutable shared bytes, stored and cloned exactly like
document image assets (MinIO + `objectStorage`); scenes reference assets, they
never duplicate them.
## First vertical slice: "The Glass Harbour Diversion"
Build the smallest end-to-end narrative loop before generalizing. Ship these in
order; each is playable on its own.
- [ ] Add the **splash screen** — "PRINCIPAL INVESTIGATOR", Glitch University —
with a single **New Game** action that creates a playthrough and launches the
first scene (sections 1, 6, 7).
- [ ] Create a mystery named **The Glass Harbour Diversion** as a one-chapter
campaign wrapping the existing Glass Harbor level template (sections 12).
- [ ] Add a **briefing NPC** (the Glitch University professor) and a
`mystery_intro` cutscene that briefs the player, using at least two poses to
prove pose-per-utterance (sections 23, 7).
- [ ] Wire the briefing to play once on load and mark itself seen, then reveal the
first level's board (sections 1, 6, 7).
- [ ] Author **two `level_debrief` end-scenes** the player reaches by reporting
back: one that **sends them back to the board** and one that **concludes the
mystery**. Model these as two end-of-scene outcomes (buttons), not branching.
- [ ] Leave the back-to-board scene as **scripted** for now, but author it as a
`generated`-ready step (section 9) so the professor's hint can later be produced
from the player's Case Report explanation.
## 1. Campaign / progression backbone
- [ ] Define a **cutscene** node as something a) references a custom react component.
That react can use potential **utterances** such that for example, it can play
an ordered sequence utterance that brief the player: `(speaker NPC, pose, text)`.
A node can be marked has_utterances which permits the admin user to add utterances in order.
[ ] A dialogue is another type of node that invokes the standard NPC dialogue component. This
has utterances, and consist of a graph where utterances either are spoken by the NPC or available for selection.
Example : if the NPC utters "Are you ready?" this has two child utterances marked "player" which could be "yes" and no. The user may select these. "No" could in principle point back to the same utterance and "yes" to the next. If an utterance has a non NULL terminal id, then the game advances to the node pointed to by that terminal. Available terminals are only those who have the current dialogue node as it parent.
- [ ] There exists "det_gate" nodes and "llm_gate" nodes. We begin with the deterministic gate only. The end result of a level is sent to a "det_gate". The det gate can inspect the output of the level and determine if the story should advance through one of its terminals. For now, the det-gate always returns the happy path terminal leading to the mystery being solved.
## 2. NPC and pose catalog
- [ ] Add an **NPC** entity (display name, short role e.g. "Glitch University
professor", default pose) owned by the mystery/template family, so casts are
authored rather than global magic strings.
- [ ] Add **poses** as named portrait variants of an NPC (`pose_key` such as
`neutral`, `concerned`, `wry`, `pointing`), each backed by one immutable image
asset via the existing `assets` table + MinIO path.
- [ ] Clone NPCs and pose→asset references (asset bytes reused, not copied) during
template freeze and instantiation, mirroring document image cloning.
- [ ] Enforce that every dialogue step names an NPC that exists in the mystery's
cast; a **missing pose is never an error**.
- [ ] Resolve a step's portrait at render time with graceful fallback: the
requested `pose_key`, else the NPC's **`default`** pose, else **no artwork**
(speaker name + text only). This lets authors add poses incrementally and keeps
the first slice playable with zero uploaded art.
## 6. API contract
- [ ] **New Game / session:** add `POST …/playthroughs` (create for the current
`user_id`, per section 1) and `GET …/playthroughs/current` so the splash can
offer New Game or Resume; scope every playthrough read/write to the caller's
`user_id` so one player cannot touch another's game state.
- [ ] **Play mode:** return a compact `pendingCutscene` payload (slot, ordered
steps with resolved NPC name + pose asset URL) when one is due and unseen; add
`POST …/playthroughs/:id/cutscenes/:cutsceneId/seen` (idempotent) and
`POST …/playthroughs/:id/advance` implementing section 1's transactional advance.
- [ ] **Admin mode:** add authenticated CRUD for mysteries, chapter ordering,
NPCs and poses, and dialogue scenes/steps, plus the campaign freeze operation —
all behind the existing admin JWT and `edit=1` gate.
- [ ] Keep authoring-only fields (raw pose keys, expected-solution data, unfrozen
drafts) out of play-mode responses, consistent with how brief concept
`expectedPartyKind` is already hidden in play mode.
## Definition of done
A game designer can, in the admin panel, create a mystery, select existing levels
as ordered chapters, create NPCs and upload named poses, craft dialogue scenes
choosing a pose per step, and attach those scenes to chapter slots — then freeze
it. A player lands on the "PRINCIPAL INVESTIGATOR" splash, chooses New Game to
create a playthrough bound to their identity, watches the professor speak
line-by-line with a
changing portrait, investigates each board, reports back to advance a chapter that
introduces a new document and goal, and reloads at any point without replaying
seen scenes — with no dialogue text hard-coded in React and no cutscene mutating a
board.