Skip to content

Architecture

Fighter90 edited this page Jul 9, 2026 · 7 revisions

Architecture

The five-minute tour. The canonical source is docs/architecture/ in the repo (OVERVIEW.md, SERVER.md, FRONTEND.md, API.md, DATA-FLOWS.md).

Top-level shape

Browser (SPA)                          Express server (Node ≥18)              Parent career-ops project
────────────                           ─────────────────────────              ─────────────────────────
public/index.html                      server/index.mjs (~130 LOC             cv.md  config/profile.yml
  └► js/app.js                           orchestrator)                        portals.yml  data/*.md
      └► js/router.js  ──HTTP+SSE──►     ├─ server/lib/routes/*.mjs (31)  ──►  reports/*.md  modes/*.md
          └► js/views/*.js                ├─ server/lib/*.mjs (helpers)         jds/*.txt  output/*.pdf
      └► js/api.js                        └─ server/lib/sources/*.mjs (59)      scan.mjs  doctor.mjs  …
                                        127.0.0.1:4317

Server (server/)

  • index.mjs — a createApp() factory, ~130 LOC after the P-2 refactor split (was 1230 LOC at v1.7.x). It is a pure orchestrator: wires middleware, calls register<Topic>Routes(app) for each of the 31 route modules, mounts static serving from public/ and the SPA catch-all. No inline route handlers remain.
    • Middleware: JSON/text body parsing, unconditional security headers (CSP is always on since v1.58.4), the activity-logging middleware, static file serving.
  • server/lib/*.mjs — shared helpers. Highlights:
    • paths.mjs — the single source of truth for parent-project paths (resolveProjectRoot(), PATHS.*). Resolves once per process at import time.
    • security.mjs — the one-and-only sanitizers: isValidJobUrl, stripDangerousMarkdown, sanitizeJobDescription, sanitizePathName, isPubliclyExposed.
    • safe-fetch.mjs — DNS-rebind-safe safeGet (one lookup, pinned TCP connect, per-hop redirect revalidation, byte cap).
    • rate-limit.mjsllmRateLimit (no-op on loopback; 10 req/min/IP on public bind).
    • file-lock.mjswithFileLock(path, fn) per-path async mutex for read-modify-write on applications.md / pipeline.md.
    • llm-dispatch.mjs — the shared provider cascade (runActiveProvider / providerAvailable) over anthropic.mjs, openai.mjs (OpenAI/Qwen/OpenRouter), gemini.mjs.
    • prompts.mjsbundleProjectContext and the prompt builders that inline parent files into LLM requests.
    • store.mjs — defensive readers (safeReadApps/Pipeline/Reports) + ensureRussianPortalsDefaults.
    • parent-relay.mjs — the fail-soft shell-out relay contract (used by followup/patterns/lifetime/salary-gap).
    • llm-usage.mjs / llm-pricing.mjs — usage rollup + editable price table for #/usage and the sidebar HUD.
    • ru-scanner.mjs / en-scanner.mjs — the two in-process scanners.
    • runner.mjsrunNodeScript (buffered) + streamNodeScript (SSE) for spawning parent .mjs scripts.

The 31 route modules (server/lib/routes/)

Each exports register<Topic>Routes(app). New routes go into server/lib/routes/<topic>.mjs, never back into index.mjs.

activity  auto-pipeline  batch  career-plan  cli-detect  config  content
cv-studio  docs-assistant  export  followup  health  help  interview  jds
llm  logos  market  memory  networking  openrouter  orientation  pipeline
portals  reports  runners  scan  stats  tracker  two-pager  usage

See API Reference for the endpoint inventory each one owns, and Features for the views they back.

SPA (public/)

  • index.html — one page, ~150 LOC. Loads CSS + scripts via <script src> (no bundler), mounts #content, renders the sidebar nav + footer.
  • js/app.js — boot: loads /api/health, renders the language switcher, starts the router, wires global shortcuts (Ctrl+K, Esc), the mobile drawer, and the notifications drawer that re-surfaces the per-tab toast journal.
  • js/router.js — hash-router. Router.register('name', renderer) per view; renderers return a DOM node or HTML string. Aliases keep URLs stable across renames; dedicated 404 view.
  • js/api.jsAPI.get/post/put/delete + API.stream (SSE). Wraps fetch, normalizes errors, manages the connection-error banner.
  • js/views/*.js — one file per route. Pure render-and-wire functions; no client-side state library.
  • js/lib/*.js — self-contained widgets and helpers: i18n.js (locale loader + data-i18n walker), usage-hud.js (sidebar usage meter), docs-fab.js (floating Ask-the-docs launcher), bug-report.js + logbuf.js (in-app bug reporter), cv-diagnostics.js, cv-privacy.js, company-logo.js, report-export.js (Markdown/PDF/DOCX export).

Styling is hand-written CSS with docs-style tokens in public/css/app.css, theme-aware (light/dark) and RTL-mirrored ([dir="rtl"]) for Arabic. CSP forbids inline scripts — every handler is addEventListener, never inline onclick=.

The two scanner registries (important)

Adding an EN job board touches two registries — this is deliberate:

  1. server/lib/sources/<slug>.mjs — the source meta registry. Exports export const meta = { value, label, region, configKey? }. server/lib/sources/registry.mjs readdirSync-scans the folder at boot and dynamically import()s every *.mjs, collecting each meta block (P-14, v1.69.0). This drives GET /api/scan/sources and the #/scan source dropdown. 59 files = 54 EN + 5 RU.
  2. server/lib/portals/adapters/<slug>.mjs + server/lib/portals/registry.mjs (ALL_ADAPTERS) — the fetch-walk registry. 54 EN adapters that actually perform the HTTP fetch/parse walk. ALL_ADAPTERS.length === 54 is asserted by tests/adapter-registry.test.mjs with the exact sorted id list.

RU sources additionally need a RU_DISPATCH row in ru-scanner.mjs. See Scanner Providers for the full walkthrough.

Parent read-only contract

  • Reads are unrestricted — the UI reads cv.md, config/profile.yml, portals.yml, data/*, reports/*, modes/*, jds/*, output/*, .env.
  • Writes are explicit-user-action only — every write corresponds to a documented HTTP action initiated by a UI control. The complete list lives in docs/architecture/DATA-FLOWS.md:
    • PUT /api/cvcv.md (sanitized, 1 MB cap)
    • POST /api/trackerdata/applications.md (dedup, file-locked)
    • POST/DELETE /api/pipelinedata/pipeline.md (URL-gated, file-locked)
    • scan runs → data/scan-history.tsv + data/last-scan.json (append/atomic replace)
    • POST /api/jds / /api/evaluatejds/*.txt
    • POST /api/deep {run:true}interview-prep/<company>-<role>.md
    • POST /api/config → parent .env (only KNOWN_KEYS)
    • user-layer config writes: config/career-plan.md, config/memory.md, config/two-pager.yml, networking/net-*.md, data/role-stats.jsonl, data/follow-ups.md
    • the single auto-write: ensureRussianPortalsDefaults() appends russian_portals: to portals.yml on first boot (idempotent).
  • Boundaries never crossed: no write outside PROJECT_ROOT, no symlink writes, no execution of arbitrary user-supplied scripts (runners invoke only a hardcoded list of .mjs filenames).

Key invariants

  1. Parent layout discovery is dynamic — always PATHS.<thing>, never literal ...
  2. CSP excludes 'unsafe-inline'/'unsafe-eval' from script-src.
  3. Writes to the parent are explicit user actions only.
  4. Sanitizers are never duplicated — one isValidJobUrl, one stripDangerousMarkdown, one sanitizeJobDescription.

Clone this wiki locally