# FORGE — setup & sync ## 1. Database FORGE needs a Postgres database named `forge`. Either: - **Bundled (compose):** `docker compose up -d db` starts `postgres:16-alpine` with database/user/password `forge` on a named volume. `DATABASE_URL` in `.env.example` already points at it. - **Your own Postgres:** create a `forge` database and set `DATABASE_URL=postgresql://USER:PASS@HOST:5432/forge` in `.env`. The schema self-bootstraps on boot (`initDB()` in `server/db.ts`) — no migrations. On the very first boot, if the `tickets` table is empty, the bundled archives in `server/data/` are seeded (idempotent upsert). ## Seed data The board ships pre-populated from `server/data/active_archive.json` + `closed_archive.json`, seeded on the first boot of an empty `tickets` table. Those archives are generated from a **Let it Snow `chrome.storage.local` dump** (the `sn_tickets`, `analytics_meta_cache`, and `jira_status_map` keys): ```bash # 1. drop the raw dump at repo root (gitignored — it contains real PII) # storage-dump.json node scripts/dump-to-archives.mjs # or: npm run dump-to-archives # → rewrites server/data/{active,closed}_archive.json # 2. reload the DB from the regenerated archives (destructive: TRUNCATE + reseed) npm run reseed ``` A ticket number can appear in both the active list and the closed cache; the loader applies closed first, then active, so the **live active board wins** over the stale closed snapshot. `npm run reseed` also works standalone whenever you want to reset the DB to the bundled archives. > The raw `storage-dump.json` is a dev artifact and is gitignored. The derived > `server/data/*.json` still contain real ticket text and are baked into the > Docker image — keep the repo internal, or point the seed at a mounted volume. ## 2. Run | | command | notes | |---|---|---| | Dev | `npm run dev` | server `:3000` + Vite client `:5173` (proxies `/api`) | | Build | `npm run build` | `tsc` + `client` production build | | Serve | `npm start` | serves the built client from the Express server | | Docker | `docker compose up -d --build` | app on host `:3099`, bundled DB | ## 3. Log in (read-API auth) The whole read UI (`/api/tickets`, `/api/stats`, …) is gated behind a username/password login; `/healthz` and the token-authed `/api/sync` are not. - Set `AUTH_USER` / `AUTH_PASS` and a `SESSION_SECRET` in `.env` (see `.env.example`). `SESSION_SECRET` is **required** in production. - On first boot the account is seeded into `app_users` with a **bcrypt** hash. Sessions are stored in Postgres (`user_sessions`, auto-created), so they survive restarts. Cookies are `httpOnly` + `secure` in production (HTTPS via the reverse proxy). - Changing `AUTH_PASS` later does **not** update an already-seeded account — delete the `app_users` row and reboot to re-seed, or update the hash directly. The database role is least-privilege: `DATABASE_URL` uses a `forge_app` role that owns the app's tables but is **not** a Postgres superuser. ## 4. Mint a sync token The Chrome extension authenticates with a bearer token (`fg__`, SHA-256 verifier stored server-side — the raw token is shown once). **CLI (recommended):** ```bash npx tsx server/mint-token.ts "my laptop" ``` **Over HTTP** (only if `ADMIN_KEY` is set in the environment): ```bash curl -X POST https://forge.mycloud.dp.ua/api/tokens \ -H "x-admin-key: $ADMIN_KEY" -H 'content-type: application/json' \ -d '{"label":"my laptop"}' ``` Copy the `fg_…` value — it is not recoverable later. ## 5. Install the Chrome extension 1. `chrome://extensions` → enable **Developer mode** → **Load unpacked** → select the `extension/` folder. 2. Open the extension's **options** (Server & token settings). Enter: - **Server URL** — e.g. `https://forge.mycloud.dp.ua` (or `http://localhost:3000`) - **Sync token** — the `fg_…` value from step 4 - Click **Test connection** (hits `/healthz`), then **Save**. Chrome will ask to grant access to the server origin — accept it. 3. Sign in to `https://rbassist.service-now.com` in the same browser. 4. Click the extension toolbar icon → **Sync now**. ### What the sync does The service worker runs a collector **in the ServiceNow page context**, so it reuses your live session cookie and CSRF token (`g_ck`). It pages the `sc_req_item` Table API for `active=true` tickets in *Marketing Web Presence* groups, maps them into FORGE's ticket shape, and `POST`s them to `/api/sync` in chunks of 100 with `Authorization: Bearer `. The server upserts by ticket number, so re-syncing is safe and never duplicates. ## 6. Deploy to the Synology NAS FORGE ships to the NAS as a docker-compose stack (bundled Postgres + app), fronted by the Synology reverse proxy (`forge.mycloud.dp.ua` → the published app port `3089`). Two scripts (ported from the Husky template): - **`scripts/push-to-nas.sh`** (local, `npm run deploy`) — preflight (pinned SSH key, `IdentitiesOnly`) → test gate → rsync the tree to the NAS (excludes node_modules/dist/secrets/`storage-dump.json`) → run `deploy.sh` over SSH. Falls back to tar-over-ssh if macOS's openrsync is the only rsync. - **`scripts/deploy.sh`** (on the NAS) — Synology PATH+sudo handling → optional `git pull` (`--pull`) → `compose build` → `up -d --remove-orphans` → poll the app container until Docker reports **healthy**. `--fresh` tears down first but **never passes `-v`**, so the `forge-db` volume (your ticket data) is preserved. **One-time, on the NAS:** create `${NAS_PATH}/.env` with the production secrets — `SESSION_SECRET`, `AUTH_USER`, `AUTH_PASS`, `POSTGRES_PASSWORD` (compose sets `DATABASE_URL`/`PORT`/`NODE_ENV` itself). See `.env.example`. Secrets live only on the NAS and are **never synced**. **Config** is baked into `push-to-nas.sh` (defaults: `NAS_HOST=mycloud.dp.ua`, `NAS_USER=d.tkachenko`, `NAS_PORT=2323`, `NAS_PATH=/volume1/docker/forge`, `NAS_KEY=~/.ssh/id_ed25519`). Override via env vars or an optional `.deploy.env` (see `.deploy.env.example`). Key-based SSH to the NAS must already work. ```bash npm run deploy # test → sync → build → up → health npm run deploy -- --fresh # recreate the stack (db volume kept) npm run deploy -- --pull # git pull on the NAS first SKIP_TESTS=1 npm run deploy # emergency bypass of the local test gate ``` The reverse-proxy mapping is configured once in DSM, not by the script. ## Security notes - `GATSBY_`-style client exposure does not apply here, but the same rule holds: the API token and `g_ck` are **never logged**. Only the SHA-256 of the token secret is stored; `token_id` is a non-secret locator. - `POST /api/sync` is rate-limited (60 req/min) and rejects any request without a valid, unrevoked, unexpired token (401). - `server/data/*.json` contains **real ticket data** (names, comments). It ships with the repo for the demo seed — treat the repo as internal, or delete the archives and rely on live syncs only (the app runs fine with an empty seed).