# SYSADMIN CHRONICLES — PROJECT MAP
> Living document. Update when files are added, moved, removed, or when architecture changes.
> Version 5.1 | Living document — update when files are added, moved, or removed.

---

## ROOT STRUCTURE

```
sysadmin-chronicles/
│
├── server/                     ← NEW: Node.js game server
│   ├── src/
│   │   ├── index.js            Entry point — Express + WebSocket
│   │   ├── routes/             auth, state, tickets, mail, docs, sage, vms
│   │   ├── services/           ContentLoader, QuestEngine, TicketService,
│   │   │                       ValidationEngine, VMManager, TrustSystem,
│   │   │                       ProgressionSystem, EmailService, SageService,
│   │   │                       ShiftTimer, IncidentScheduler, ShiftReviewService,
│   │   │                       CertificationService, SaveState
│   │   └── lib/                ssh.js, virsh.js, command.js, eventBus.js, session.js
│   └── package.json
│
├── frontend/                   ← NEW: Svelte web HUD
│   ├── src/
│   │   ├── App.svelte          Root component, WebSocket, panel routing
│   │   ├── components/         TicketsPanel, MailPanel, DocsPanel, SagePanel,
│   │   │                       VmsPanel, ProfilePanel, HeaderBar, SidebarTabs
│   │   ├── lib/api.js          REST API fetch wrapper
│   │   └── main.js
│   ├── dist/                   Built output (served by game server)
│   └── package.json
│
├── scripts/
│   └── start-game.sh           One-shot: start server + open SPICE workstation viewer
│
├── docs/
│   ├── ARCHITECTURE.md               System architecture
│   ├── CHARACTERS.md                 All characters — bios, relationships, story hooks
│   ├── COMPANY_LORE.md               World, company, products, tone guidelines
│   ├── INSTALLER_PLAN.md             Installer design and packaging
│   ├── PRESSURE_PROFILES.md          Time-pressure escalation schema and authoring guide
│   ├── PROJECT_MAP.md                ← this file
│   ├── ROADMAP.md                    Development phases and content status
│   ├── RUNTIME_DEPENDENCIES.md       Host dependencies and version requirements
│   ├── SAVE_SYSTEM.md                Save model, VM persistence policy, recovery flows
│   ├── SNAPSHOT_CHAIN.md             VM snapshot chain and baseline management
│   ├── STORY_DESIGN_CONTEXT.md       How story works — narrative arc, quest model, design constraints
│   ├── VM_BUILD_SYSTEM.md            VM build and provisioning system
│   ├── WORKSTATION_POLISH_BACKLOG.md Outstanding UX polish items
│   └── codex-specs/
│
├── content/                    ← data-driven content loaded by Node.js server
│   ├── quests/         quest JSON files (being reworked — see STORY_DESIGN_CONTEXT.md)
│   ├── tickets/        ticket JSON files (being reworked)
│   ├── incidents/      incident JSON files (being reworked)
│   ├── pressure_profiles/  escalation profiles (schema in PRESSURE_PROFILES.md)
│   ├── dialogue/       character dialogue JSON files (being reworked)
│   ├── world_flags/    world_flags.json (central registry)
│   ├── docs/           onboarding, sage_content, internal_docs, etc.
│   ├── progression/    trust_unlocks.json, access_tiers.json
│   └── vm_profiles/    workstation.json, web_server.json, build_machine.json
│
├── tools/
│   ├── setup/          check-host.sh, seed-vms.sh, first-run-setup.sh, uninstall.sh
│   ├── vm/             build-vm.sh, build-*.sh, snapshot-all.sh, suppress-maintenance-noise.sh
│   │   ├── profiles/   workstation.sh, web-server.sh, build-machine.sh
│   │   └── quest-prep/ Q001–Q008 prep/post scripts
│   └── content/        validate-content.js (zero-error gate), verify-clue-fingerprints.js
│
├── company-website/            Axiom Works public website (static HTML/CSS)
│   ├── index.html              Home — hero, product highlights, stats
│   ├── about.html              Company story, values, contact
│   ├── people.html             Team page — Dave, Marcus, Priya, Sarah + filler staff
│   ├── products.html           AxiomFlow, AxiomDash, AxiomSync product pages
│   ├── style.css               Shared corporate CSS (navy/blue scheme)
│   └── assets/                 logo.png, portrait photos for each NPC
│
├── vm/                         images/, snapshots/, cloud-init/, probes/
├── package.json
└── README.md
```



---

## COMPANY WEBSITE

Static HTML/CSS site serving as the public-facing Axiom Works company website, accessible from the workstation VM.

**URL inside the VM:** `http://www.axiomworks.corp/` (no port)

**How it works:**
- The game server serves `company-website/` at `/company/` (port 3000)
- nginx is installed in the workstation VM and proxies `axiomworks.io` and `www.axiomworks.io` (port 80) → game server port 3000 at `/company/`
- `/etc/hosts` in the workstation maps both hostnames to `127.0.0.1` (localhost → nginx)
- Result: the player sees a clean `http://www.axiomworks.io/` URL in Chromium with no port number

**Pages:** Home (`index.html`), About (`about.html`), Our Team (`people.html`), Products (`products.html`)

**Team page portraits:** NPC photos live in `company-website/assets/`. The player is not featured on the website.

**Domain note:** `axiomworks.corp` uses the IANA-reserved `.corp` TLD (reserved 2024, can never be publicly delegated). No registration needed — it will never resolve on the real internet. The in-VM `/etc/hosts` + nginx approach is sufficient for any build.

**Player portraits** (for the HUD profile panel) are separate from the website portraits. They live in `server/public/portraits/` and are served at `/public/portraits/`. The player selects one via the Profile panel; the choice persists in `save.json` as `player_portrait`.

---

## BOOT FLOW (Node.js Server)

```
bash scripts/start-game.sh
  ↓
node server/src/index.js
  1. ContentLoader.load()      — reads all content/**/*.json into memory
  2. SaveState.load()          — reads ~/.local/share/sysadmin-chronicles/save.json
                                  or creates fresh save
  3. TrustSystem.initialize()  — hydrates trust score + unlock state
  4. ProgressionSystem.initialize()
  5. QuestEngine.initialize()  — restores quest states from save
  6. TicketService.initialize()
  7. EmailService.initialize() — restores inbox, seeds T001 email on fresh save
  8. ShiftTimer.start()        — starts shift clock
  9. IncidentScheduler.start() — begins pressure tick loop (every 30s)
  10. VMManager.ensureWorkstationLive() — virsh start sc-workstation if needed
  ↓
Express + WebSocket listening on PORT (default 3000)
  ↓
remote-viewer opens SPICE connection to sc-workstation
Player sees XFCE desktop → Chromium opens HUD → game is live
```

---

## TICKET COMPLETION FLOW

```
Player clicks "Mark Complete" on ticket in HUD
  ↓
POST /api/tickets/:id/complete
  ↓
TicketService.markComplete(ticketId)
  → load ticket + linked quest JSON
  → for each solution_branch (sorted by priority DESC):
      ValidationEngine.check(vmId, branch.validation.rules)
        → VMManager.getIP(vmId)
        → SSH as opsbridge using sc_host_key
        → run each rule check (file_exists, service_state, etc.)
      if all rules pass → winning branch found
  → TrustSystem.adjust(branch.trust_delta)
  → WorldFlags.set(branch.world_flags)
  → QuestEngine.completeQuest(questId)
  → EmailService.send(follow-up NPC email if negative branch)
  → SaveState.write()
  → broadcast trust:changed, mail:new via WebSocket
  ↓
Response: { passed, branch, trust_delta, failures }
HUD shows success toast or failure details
```

---

## VM IDENTITY TABLE

| vm_id | SC constant | libvirt domain | hostname | distro | ssh_user | mgmt_user | always_live | Quests |
|-------|-------------|----------------|----------|--------|----------|-----------|-------------|--------|
| `workstation` | `SC.VM_WORKSTATION` | `sc-workstation` | `ares` | Debian 12 | `player` | `opsbridge` | yes | Q001 |
| `web_server` | `SC.VM_WEB_SERVER` | `sc-web-server` | `hermes` | Debian 12 | `player` | — | no | Q002–Q005, Q007 |
| `build_machine` | `SC.VM_BUILD_MACHINE` | `sc-build-machine` | `vulcan` | Arch Linux | `player` | — | no | Q006, Q008 |

See `docs/VM_BUILD_SYSTEM.md` for full build system documentation and profile authoring guide.

**SSH key**: all host→guest connections use `~/.ssh/sc_host_key` (BatchMode, no password).

**Baseline snapshots**:
- workstation: `baseline.day-one`
- web_server, build_machine: `baseline.clean`

---

## TERMINAL ARCHITECTURE

The player uses a real **Tilix** terminal inside the workstation VM (sc-workstation / ares).
No terminal simulation. SSH to target VMs is real SSH. There is no in-game terminal widget.

```
Player opens Tilix on the workstation XFCE desktop
  → types: ssh hermes
  → real SSH to sc-web-server using player's authorized_keys
  → works directly on the target VM

Host-side validation (triggered by "Mark Complete" in HUD):
  ValidationEngine.js SSHes as 'opsbridge' → sudo -H -i -u player
  Runs rule checks (file_exists, service_state, etc.)
  Returns pass/fail to game server
```

Host SSH options (used by ValidationEngine.js and VMManager.js):
```
-o StrictHostKeyChecking=no
-o BatchMode=yes
-o ConnectTimeout=5
-o LogLevel=ERROR
-i ~/.ssh/sc_host_key
```

---

## SERVICE DEPENDENCY GRAPH (Node.js server)

```
eventBus.js (Node.js EventEmitter — no deps)
  └─ consumed by: all services

ContentLoader
  └─ consumed by: QuestEngine, TicketService, ValidationEngine, TrustSystem,
                  ProgressionSystem, IncidentScheduler, EmailService, SageService

VMManager
  ← wraps virsh.js + ssh.js
  ← called by QuestEngine (start required VMs on quest activation)
  ← called by ValidationEngine (get VM IP for SSH)

ValidationEngine
  ← calls VMManager.getIP(vmId)
  ← SSHes as opsbridge → runs rule checks (file_exists, service_state, etc.)
  ← called by TicketService on mark-complete

QuestEngine
  ← calls VMManager to start required VMs
  ← calls ValidationEngine via TicketService
  ← calls TrustSystem, WorldFlags, EmailService on resolution
  → emits via eventBus: quest:activated, quest:resolved, ticket:received

IncidentScheduler
  ← reads WorldFlags for trigger conditions
  ← tick drives escalation step advancement
  → emits via eventBus: incident:activated, incident:escalated, incident:resolved

TrustSystem
  ← called by QuestEngine on branch resolution
  ← called by IncidentScheduler for ignored incident penalties
  → emits via eventBus: trust:changed

SaveState
  ← called by QuestEngine, TrustSystem, ProgressionSystem
  ← reads/writes ~/.local/share/sysadmin-chronicles/save.json
```

---

## KEY MODULES

### Server (`server/src/`)

| Module | File | Responsibility |
|--------|------|----------------|
| Entry point | index.js | Express + WS, service wiring, static serving |
| ContentLoader | services/ContentLoader.js | Load all content/ JSON at startup |
| QuestEngine | services/QuestEngine.js | Quest state machine |
| TicketService | services/TicketService.js | Ticket state, mark-complete, branch resolution |
| ValidationEngine | services/ValidationEngine.js | SSH rule evaluation (all rule types) |
| VMManager | services/VMManager.js | virsh wrappers, IP resolution |
| TrustSystem | services/TrustSystem.js | Score, unlocks, revocation |
| ProgressionSystem | services/ProgressionSystem.js | Unlocked VMs, docs, access |
| EmailService | services/EmailService.js | Inbox, follow-ups, reply options |
| SageService | services/SageService.js | Rule-based dialogue / KB |
| ShiftTimer | services/ShiftTimer.js | Shift clock, 30s tick broadcasts |
| IncidentScheduler | services/IncidentScheduler.js | Pressure tick, incident injection |
| ShiftReviewService | services/ShiftReviewService.js | End-of-shift review email |
| CertificationService | services/CertificationService.js | Cert awards after quest chains |
| SaveState | services/SaveState.js | Read/write save.json |
| ssh.js | lib/ssh.js | Promisified SSH execution |
| virsh.js | lib/virsh.js | virsh command wrappers |
| eventBus.js | lib/eventBus.js | Node.js EventEmitter for service coordination |

### Frontend (`frontend/src/`)

| Component | File | Responsibility |
|-----------|------|----------------|
| Root | App.svelte | Panel routing, WebSocket connection |
| Tickets | TicketsPanel.svelte | List, detail, mark-complete |
| Mail | MailPanel.svelte | Inbox, message, reply buttons |
| Docs | DocsPanel.svelte | Trust-gated doc viewer |
| Sage | SagePanel.svelte | Chat / KB search |
| VMs | VmsPanel.svelte | Live VM status indicators |
| Header | HeaderBar.svelte | Trust, shift timer, mail badge |
| API | lib/api.js | REST fetch wrapper |

---

## CONTENT DOMAINS

| Domain | Purpose |
|--------|---------|
| `quests/` | Objective chains, clue fingerprints, validation rules, branch priorities |
| `tickets/` | Player-facing problem statements with initial/current priority |
| `incidents/` | Dynamic pressure events with blast_radius and escalation steps |
| `dialogue/` | Workplace messages, hints, follow-ups, series threads |
| `pressure_profiles/` | Reusable escalation templates referenced by quest branches |
| `world_flags/` | Central registry — all world state flags declared here |
| `docs/` | Internal documentation + Sage/help content (trust-gated) |
| `progression/` | Trust thresholds, unlocks, revocation rules, access tiers |
| `vm_profiles/` | Domain names, hostnames, snapshots, networks, resource budgets |

---

## FILE NAMING CONVENTIONS

- Quest files: `Q{NNN}-{kebab-case-title}.json`
- Ticket files: `T{NNN}.json`
- Incident files: `I{NNN}-{kebab-case-title}.json`
- Dialogue files: `{character}-Q{NNN}.json` or `{character}-Q{NNN}-{variant}.json`
- Quest prep scripts: `Q{NNN}-prep.sh`
- VM profiles: `{snake_case}.json`

---

## CONTENT VALIDATION CHECKS

Run: `node tools/content/validate-content.js` — must exit 0 (zero errors).

| Check | Rule |
|-------|------|
| JSON well-formed | All content files parse without error |
| No duplicate IDs | Unique across quests, tickets, incidents, pressure profiles, dialogue |
| World flags | Every referenced flag exists in `world_flags/world_flags.json` |
| required_vms | Every entry maps to a valid VM profile |
| blast_radius | Every entry maps to an existing incident file |
| linked_quest | Every ticket's linked_quest maps to an existing quest |
| ticket_id | Every quest's ticket_id maps to an existing ticket |
| Branch priority | Priorities unique per quest (no ties) |
| follow_up_incident | Maps to an existing incident file |
| pressure_profile | Maps to an existing pressure profile file |
| series_id | Every series_id has at least two dialogue members |
| revokes | Trust unlock revoke entries reference valid unlock strings |
| clue_fingerprint | Evidence rule types are valid |

---

## KNOWN GAPS (Post-Redesign)

These are gaps in the v4.0 Node.js + Svelte implementation.
All content is authored, validator-clean, and reused unchanged.

### P0 — Blocking for first playable shift

| Gap | Notes |
|-----|-------|
| Phase 7 workstation VM verification | Confirm SPICE display, Chromium autostart, Tilix as default work end-to-end on a freshly seeded VM |
| Phase 10 full playtest | Boot all VMs, play Q001→Q002, validate full server→SSH→HUD loop |

### P1 — Required before broader testing

| Gap | Notes |
|-----|-------|
| Clue quality as system degrades | Evidence should remain legible as incidents escalate (I001/I002/I003 escalation pass) |
| Viewer smoothness | `remote-viewer` SPICE path is functional but not final-UX smooth; lower priority with real XFCE desktop |

### P2 — Polish / completeness

| Gap | Notes |
|-----|-------|
| WORKSTATION_POLISH_BACKLOG.md items | See that file for outstanding desktop UX polish |

---

## GENERATED / LARGE ASSETS

Created by CLI tooling, not hand-managed:

- `vm/images/*.qcow2`
- Imported libvirt domain XML
- Baseline snapshot exports or manifests
- Shift checkpoint snapshots
- Packaged Linux build artifacts
