# 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