This commit is contained in:
Dmytro Tkachenko
2026-08-29 11:59:28 +03:00
commit 28d817ebe9
147 changed files with 17534 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
# 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_<id>_<secret>`,
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 <token>`. 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).