180 Commits

Author SHA1 Message Date
2ae7d8b968 Bug fixes for video serving and updats to documentation for mobile use screenshots 2026-04-30 14:17:50 -06:00
aba935c8ac fix(gitea): healthz probe + DB-first token + honest banner copy
Three related bugs surfaced by the Gitea Setup wizard:

1. checkStatus probed /api/v1/version unauthenticated, which returns 403
   under REQUIRE_SIGNIN_VIEW=true (Gitea's default). Result: giteaOnline
   and installComplete always read false on any REQUIRE_SIGNIN_VIEW=true
   install, producing a green "Setup Complete" banner over two red-X
   foundational rows.

   Fix: use /api/healthz (public, exempt from sign-in requirement).
   Bonus: split installComplete from giteaOnline — online now honestly
   means "process responding", installComplete means "admin user exists"
   (verified either by DB flag or by Basic Auth probe on /user). Status
   code is now logged on healthz failure for debuggability.

2. docs-history.service.ts read the API token from env only, bypassing
   the DB. After the setup wizard wrote the token to siteSettings,
   docs-history still saw env.GITEA_API_TOKEN empty and silently did
   nothing. Same file also had a second /api/v1/version bug in
   isAvailable().

   Fix: route token lookup through giteaClient.getConfig() (DB-first,
   env fallback — same pattern as the rest of the codebase). Switch
   isAvailable() to /api/healthz.

3. UI banner confidently claimed "Gitea is not running" on any
   non-200 response, including the 403 case above. Misleading, and
   dangerous to point users at `docker compose up -d gitea` when the
   container is already running.

   Fix: softer "not reachable" copy that points users at the API log
   for the actual status code.

Bunker Admin
v2.9.12
2026-04-23 11:47:02 -06:00
4ccc433eb9 fix(gitea): fall back to INITIAL_ADMIN_PASSWORD for gitea-init
Previously both compose files defaulted GITEA_ADMIN_PASSWORD to empty,
and scripts/gitea-init.sh silently skipped admin creation on blank input.
If config.sh failed to propagate the password (e.g. Docs Comments not
enabled, or --admin-password omitted), fresh installs ended up with a
Gitea container running but zero users — and the admin GUI's Gitea setup
wizard had no token to progress.

Changes:
- docker-compose.yml + docker-compose.prod.yml: GITEA_ADMIN_PASSWORD now
  falls back to INITIAL_ADMIN_PASSWORD when unset
- .env.example: declare GITEA_ADMIN_PASSWORD= with explanatory comment so
  users discover the override
- scripts/gitea-init.sh: silent skip becomes a loud WARN so a broken
  config is visible in compose logs

Bunker Admin
v2.9.11
2026-04-23 10:54:24 -06:00
94451f9aa0 A bunch of documentation updates 2026-04-19 16:32:38 -06:00
6d562da4b2 Merge branch 'chore/squash-prisma-migrations' v2.9.10 2026-04-16 17:36:36 -06:00
3f8c064649 chore(prisma): squash 50 migrations into single init
Replaces all 50 prior migrations with one fresh init.sql generated via
`prisma migrate diff --from-empty --to-schema-datamodel`. Schema is
unchanged; this is purely a consolidation of migration history.

Result:
- 50 → 1 migration (50 dirs deleted, 1 new init created)
- 6304 → 6075 lines of migration SQL (-229; redundant ALTERs collapsed)
- Fresh installs apply one transaction instead of 50 chained
- 192 tables / 103 enums / 516 indexes — verified against current schema

Verified locally:
- prisma migrate reset → applied cleanly, seed succeeded
- 193 tables in public schema (192 models + _prisma_migrations)
- /api/health → database+redis ok
- Login as admin@bnkops.ca → SUPER_ADMIN role returned
- Authenticated GET /api/settings + /api/users → working

Existing deployments need `prisma migrate reset` (acceptable: no
production data of consequence currently).
2026-04-16 17:34:07 -06:00
5082fe7b76 chore: untrack api/dist + tsbuildinfo build artifacts
api/dist/ (468 files, 11MB) and admin/tsconfig.tsbuildinfo were committed
before being added to .gitignore — the rule had no effect on the existing
tracked copies. Untrack them now so future Docker rebuilds stop showing
spurious diffs. Files stay on disk; rebuild regenerates everything.

Also add *.tsbuildinfo to .gitignore so future tsc incremental caches stay
out of git.
2026-04-16 16:54:55 -06:00
3a528d9a49 chore: gitignore hygiene + untrack stale artifacts
- Add gitignore rules for code-server data/, mkdocs cache,
  scheduled_tasks.lock, and archive/ (closes routine git status noise)
- Widen data/upgrade ignore from *.json to all files
- Delete archive/ (3.6GB of legacy V3.x release zips)
- Untrack .playwright-mcp/ logs and mkdocs.yml.bak (already in .gitignore;
  predate the rules)
- Remove empty root package.json/package-lock.json stubs
2026-04-16 16:38:45 -06:00
8a2b82a4e8 docs(blog): v2.9.8 + v2.9.9 post — "A smoother fresh install"
High-level recap of the install-UX sprint for the MkDocs blog. Covers:
- What the three-round fresh-install test surfaced
- Install-day improvements that shipped in v2.9.8 (host-port
  preflight, Pangolin smoke test, admin password file,
  --enable-all installing systemd units, Next Steps verify
  pointer)
- Teardown scripts (pangolin-teardown.sh, ccp-deregister.sh) and
  CCP's deleteInstance tunnel cleanup
- CCP registration polish (slug-conflict 409, poll-rate-limit
  split, agent exponential backoff)
- Release hygiene in v2.9.9 (--replace safety, whitelist parity,
  install.sh extract preservation on TTY failure)
- Brief "what's next / deferred" section

Placed under mkdocs/docs/blog/posts/ with the date-prefix filename
convention used by 2026-03-27-test-blog-post.md.

Bunker Admin
2026-04-16 16:11:08 -06:00
5968df5b42 release: scripts/*.sh whitelist parity check in build-release.sh
Replace the implicit runtime-scripts for-loop with two explicit arrays
(RUNTIME_SCRIPTS + DEV_ONLY_SCRIPTS) and a pre-build assertion that
aborts if any scripts/*.sh isn't classified.

Motivation: during the v2.9.8 sprint we added three new scripts
(pangolin-teardown.sh, ccp-deregister.sh, validate-env.sh) and had to
manually remember to include each in the whitelist. Missing one is
silent — the script just doesn't ship, and the release tarball is
subtly broken in a way you only notice when the docs reference the
missing script.

The parity check surfaces the decision: every new scripts/*.sh must
be deliberately classified as ship (RUNTIME_SCRIPTS) or don't-ship
(DEV_ONLY_SCRIPTS). Adding a script and forgetting aborts the build
with a clear message naming the unclassified file.

Side-effect fixes from this audit:
  - register-with-ccp.sh is now shipped (was only in source; needed
    for retrofitting CCP onto existing installs)
  - update-env.sh is now shipped (safe .env updater used by upgrade
    flows)

Bunker Admin
v2.9.9
2026-04-16 15:17:54 -06:00
824f3cce99 install: preserve extracted dir when config wizard can't start
scripts/install.sh cleanup trap previously removed $INSTALL_DIR on
any non-zero exit if .env wasn't written yet. That made sense for
half-extracted state, but also bit us when config.sh failed at
/dev/tty (the common "curl | ssh bash" non-interactive case) — the
15MB tarball had already extracted cleanly and the user was forced
to re-download to retry on a console.

New EXTRACT_COMPLETE state flag:
  - Set to true after the tar xzf step verifies docker-compose.yml.
  - cleanup() distinguishes "extract OK, config wizard didn't run"
    from "extraction never completed":
      * First case: preserve the dir, print a resumption hint
        (cd $INSTALL_DIR && bash config.sh on an interactive console).
      * Second case: unchanged behaviour — remove the partial dir.

Typical SSH-without-tty recovery path now costs zero re-download.

Bunker Admin
2026-04-16 14:25:53 -06:00
450b5ad4ba docs: sync getting-started + README with install UX improvements
Updates the user-facing docs to match the install flow after the
friction fixes landed:

README.md
  Quick Start block now reflects reality: install.sh host-port
  check, test-deployment.sh verify step, password file location,
  and the useful-tools block (validate-env, test-deployment,
  pangolin-teardown, ccp-deregister).

mkdocs prerequisites.md
  New warning block under Linux Server covering the cockpit-on-9090
  class of port collisions, pointing at the installer's ss-based
  preflight and validate-env.sh for manual checks. Checklist gains
  a host-port line.

mkdocs installation.md
  "What install.sh does" now enumerates the new port check and disk
  check. Configuration Wizard Step 4 notes the
  data/admin-credentials.txt persistence for auto-generated
  passwords. "Verifying Installation" rewritten around
  test-deployment.sh. New "Clean reset before reinstall" block with
  the teardown sequence.

mkdocs first-steps.md
  Log In step tells users where to find the generated password when
  they ran config.sh -y without --admin-password.

mkdocs control-panel.md
  New "Registering an Existing Install (Phone-Home)" section
  covering invite code, --ccp-* flags, approval, rate-limit + backoff
  behaviour, and the ccp-deregister.sh teardown path with the
  slug-conflict rationale.

Bunker Admin
v2.9.8
2026-04-16 13:21:44 -06:00
d2da13929a ccp: split /register and /poll rate limits; agent backoff on 429
Problem: the agent polled /poll every 30s while waiting for admin
approval. At 10 req/15min, the 11th poll hit 429 after ~5 min and
every subsequent one also failed — recovery required an agent
restart. A human-paced approval SLA is longer than 5 minutes.

CCP side (agents.routes.ts):
  Split the one-size-fits-all agentRegistrationLimiter into two.
  /register stays tight (10/15min — invite-code brute force is the
  real attack surface). /poll gets a new agentPollLimiter at 180/15min
  (one poll per ~5s upper bound), scoped to registrationId+slug so
  blast radius is bounded.

Agent side (server.ts):
  Replaced fixed 30s setInterval with a self-scheduling setTimeout
  loop that backs off exponentially on HTTP 429 (30s → 60s → 120s →
  300s cap) and resets to 30s on any 2xx. Stop-flag protects against
  re-entry after approval. Fixes the "agent wedged at 429, restart to
  recover" workaround.

Bunker Admin
2026-04-16 13:11:39 -06:00
6504598752 ccp: surface slug-collision as 409, not raw Prisma error
agents.routes.ts approve handler: wrap prisma.instance.create in
try/catch. When a PrismaClientKnownRequestError P2002 with target
including 'slug' is thrown, convert to a 409 AppError with a clear
message directing the admin at DELETE /api/instances/:id or the new
scripts/ccp-deregister.sh on the target host.

Before this, re-registering a previously-registered host (after the
operator tore down the underlying stack without cleaning CCP's DB
row) returned 500 with a raw Prisma error string — the operator had
to read a stack trace to understand the cause. Now the error is
self-describing and points at the fix.

Matches the pattern other CCP error paths use.

Bunker Admin
2026-04-16 13:08:23 -06:00
ce8c5aaf1f install: add scripts/ccp-deregister.sh + ship in tarball
Pairs with pangolin-teardown.sh. For instances that were phone-home
registered with a CCP, this script removes the CCP-side Instance row
during teardown so the slug is freed for re-registration.

Without it, tearing down a CCP-registered instance leaves a stale
Instance row in CCP's DB. The next phone-home-registration with the
same slug hits the unique-constraint violation we saw in marcelle
testing.

Matching: by agentUrl (default from .env CCP_AGENT_URL), --slug, or
--instance-id. Dry-run by default; --yes to execute. CCP admin token
via --token or CCP_ADMIN_TOKEN env var.

Added to build-release.sh whitelist so release tarballs include it
alongside pangolin-teardown.sh and validate-env.sh.

Bunker Admin
2026-04-16 13:07:12 -06:00
c2f12aa2bf release: refuse upload over existing tag unless --replace
scripts/build-release.sh --upload now checks for an existing release
at the given tag before POSTing a new one. If found and --replace is
not set, errors out with a clear message.

This prevents the silent-overwrite problem: a user on v2.9.7 running
./scripts/upgrade.sh sees "no update available" when the v2.9.7
release's tarball contents have silently changed. Version tags should
be immutable once published.

--replace is still available for deliberate test-bench iteration
(DELETEs the existing release, then POSTs). Documented as destructive
in the --help output and DEV_WORKFLOW.md.

Bunker Admin
2026-04-16 13:06:06 -06:00
6e01d580b2 install: persist generated admin password + Pangolin credential smoke test
config.sh: two more pieces of first-time-user UX polish surfaced
during fresh-install testing.

- When configure_admin() auto-generates the admin password (no
  --admin-password given), also write a locked-down
  data/admin-credentials.txt (mode 0600) containing email + password
  + timestamp. Users who pipe config.sh output or miss the single
  stdout print no longer lose the password forever. The file is only
  written on the auto-generate path — explicit --admin-password
  leaves it alone.

- configure_pangolin() now smoke-tests --pangolin-api-key +
  --pangolin-org-id against /org/:id/resources before writing them to
  .env. Catches typos, revoked keys, or wrong org IDs while recovery
  is cheap (rather than later, when Newt fails to connect and
  symptoms look like a tunnel outage). New flag:
  --skip-pangolin-check for offline bootstrap scenarios.

Bunker Admin
2026-04-16 12:59:10 -06:00
dbbff8adc9 install: host-port preflight in install.sh + surface verify/teardown tools
scripts/install.sh: inline ss -Htln check before tarball download so
cockpit-on-9090 (and friends) fail early instead of breaking the stack
mid-compose-up. Culprit-specific hints for :9090 (cockpit.socket) and
:80/:443. Gracefully skipped if iproute2 not installed.

config.sh: Next Steps in release mode now surfaces
  - test-deployment.sh --wait 60 (verify step)
  - validate-env.sh (re-check ports/.env)
  - pangolin-teardown.sh (clean reset before reinstall)
Also documents the ~3min first-pull + ~90s stabilization window so
brief "unhealthy" statuses don't panic new users.

Bunker Admin
2026-04-16 12:56:55 -06:00
f9d566bd84 install: preflight + teardown tooling + CCP tunnel cleanup on delete
Fixes surfaced by three rounds of fresh-install testing on marcelle:

- config.sh: add host-port preflight check (ss -tln) to catch
  cockpit-on-9090 style collisions before compose up; add
  --skip-port-check escape hatch; add --install-watcher /
  --no-install-watcher / --install-backup-timer /
  --no-install-backup-timer flags; -y --enable-all now installs both
  systemd units by default (previously silently skipped); print
  resolved admin email in Configuration Complete block.

- scripts/validate-env.sh: new section 5b "Host Port Availability"
  using ss-based detection, with process-name surfacing when run as
  root.

- scripts/pangolin-teardown.sh: new wrapper. Reads credentials from
  .env or takes --api-url/--api-key/--org-id flags. Dry-run by
  default; --yes to execute. Deletes resources before sites (avoids
  orphans). --keep-site-ids for safety.

- scripts/build-release.sh: include validate-env.sh and
  pangolin-teardown.sh in release tarball whitelist.

- CCP instances.service.ts: deleteInstance() now calls
  teardownTunnel() before composeDown when pangolinSiteId is set.
  Previously an admin clicking "Delete Instance" orphaned the
  Pangolin site + all its resources. Best-effort with try/catch
  matching the existing Docker-cleanup tolerance pattern.

- CLAUDE.md: sync drift — 44 → 50 migrations, 186 → 192 models,
  40 → 44 modules.

Bunker Admin
2026-04-16 12:50:48 -06:00
13513aeca5 Fix VERSION promotion regression: don't gate on soft health-check warnings
Prior commit (ac901c9e, Fix B) gated VERSION.pending promotion behind
VERIFY_FAILED=false, but VERIFY_FAILED is a soft warning signal — it
also fires when the admin container's 30s verify budget is tight
(which was the cry-wolf case Fix 3 addressed in the same commit).

Observed on marcelle during v2.9.5 → v2.9.6: the upgrade completed
successfully (tarball extracted, containers pulled and running new
image), but because the admin healthcheck warned at 30s (still using
v2.9.5's upgrade.sh with its 30s budget), VERIFY_FAILED=true pinned
VERSION back to v2.9.5 despite everything else having advanced. result.json showed success=true but newCommit=v2.9.5.

Hard failures still prevent promotion via on_failure's rm -f of
VERSION.pending before the promotion site is reached. Reaching the
promotion site means Phase 7 completed without exit-code or trap —
that's the correct gate.

Bunker Admin
v2.9.7
2026-04-15 18:33:13 -06:00
ac901c9e53 Update system hardening: breaking-release gate + release-mode rollback + health budgets + success archival
Four fixes building on the prior upgrade-path work. All observed on
marcelle across today's v2.9.2 → v2.9.5 cycles and addressed here.

- Fix 1 (breaking-release gate). upgrade-check.sh now parses the first
  line of each Gitea release body for `BREAKING: <reason>` and threads
  `breaking`/`breakingReason` through status.json into the API status
  response. Admin UI renders a red Alert with a typed-tag confirmation
  input and gates the Start Upgrade button. auto-upgrade.service.ts
  refuses to apply breaking releases, logging a skip and holding off
  until the operator confirms manually.

- Fix 2 (release-mode rollback). print_rollback_help and the --rollback
  flow both used `git checkout`, which silently fails in release
  installs (no .git). Added INSTALL_MODE branches: release mode
  downloads the prior tarball from Gitea using a new VERSION.rollback
  marker seeded at Phase 3 start. Source mode retains the existing
  git-based flow.

- Fix 3 (Phase 7 health budgets). admin verify_service_health budget
  30s → 90s (matches the admin container's start_period from commit
  47704667). Gancio + MkDocs switched from one-shot to the existing
  verify_service_health retry wrapper. Cuts the cry-wolf
  "services may still be starting" warning from every upgrade result.

- Fix 4 (symmetric success archival). Bash archive_failure_to_history
  already logs failures on exit; added a matching archive_success_to_
  history called after write_result on the success path. API-side
  archiveResult now dedupes on completedAt so double-recording (bash
  + post-restart handler) can't land twice in history.json.

Release the bundle as v2.9.6.

Bunker Admin
v2.9.6
2026-04-15 16:57:13 -06:00
47704667b1 Upgrade failure visibility + atomic VERSION + external smoke test
Three fixes to harden the admin-UI upgrade path, all in scripts/upgrade.sh.
Root-caused by yesterday's v2.9.2 → v2.9.3 on marcelle which was killed by
systemd mid-Phase-4 and left the system in a misleading half-upgraded state
(VERSION bumped, container pre-upgrade, result.json stale from 24h prior).

- Fix A (failure visibility): stop silencing stderr on the five docker
  compose pull sites so timeouts / auth failures / network errors flow
  into upgrade-watcher.log. Add explicit SIGTERM/SIGINT traps alongside
  the existing EXIT trap. Track CURRENT_PHASE_NAME globally so the
  failure message reports "during Phase 4: Container Rebuild" rather
  than just an exit code. Introduce write_result_force (bypasses
  API_MODE guard) + archive_failure_to_history so a killed upgrade
  always leaves a truthful result.json + history.json entry, and the
  progress.json is cleared so the admin UI stops showing a phantom
  in-progress phase.

- Fix B (atomic VERSION): Phase 3 rsync now --excludes VERSION and
  stashes the new one at data/upgrade/VERSION.pending. Phase 7 promotes
  it to VERSION only after VERIFY_FAILED stays false. on_failure deletes
  the pending file. upgrade-check.sh needs no changes — its head -1
  VERSION read sees actual state instead of a mid-upgrade promise.

- Fix C (external smoke): after Phase 7 localhost checks, curl
  https://api.${DOMAIN}/api/health with --max-time 10 and warn (not
  fail) on non-200. Catches Pangolin resource misassignments that the
  localhost-only checks miss. Appends to UPGRADE_WARNINGS so the admin
  UI surfaces it in result.json.

Bunker Admin
v2.9.5
2026-04-15 16:13:04 -06:00
12708e5824 Bump upgrade watcher TimeoutStartSec 900s → 3600s
Admin-UI-triggered v2.9.2 → v2.9.3 upgrade on marcelle was killed by
systemd at 15min mid `docker compose pull` because Gitea registry
pulls over the Pangolin tunnel routinely exceed that budget on a
cold cache. 1hr gives enough headroom for slow pulls without
hiding genuinely hung upgrades.

Bunker Admin
v2.9.4
2026-04-15 15:40:59 -06:00
23df6a8b52 Fresh-install + upgrade-path hardening bundle
Six independent fixes surfaced during the v2.9.1 → v2.9.2 admin-UI
upgrade validation today. Together they make a clean install on a new
box work end-to-end without in-session patching.

- Fix 1: scripts/validate-compose-parity.sh + build-release.sh hook —
  fail release builds when api/admin/media-api/nginx healthcheck
  blocks drift between docker-compose.yml and docker-compose.prod.yml.
  Previous boot-race fix had to be applied to both files manually.

- Fix 2: scripts/systemd/install.sh chowns logs/ to the install user
  (the API container creates subdirs there as root, locking the
  host-side watcher out), pre-creates logs/upgrade-watcher.log, and
  changemaker-upgrade.service adds StartLimitIntervalSec=0 so a
  single transient failure can't wedge the .path unit permanently.

- Fix 3: /api/upgrade/status now returns a `watcher` sub-object that
  flags the host systemd watcher as stalled when trigger.json has
  been pending >30s. Admin SettingsPage SystemUpgradeTab renders a
  warning Alert with the systemctl recovery command when unhealthy.

- Fix 4: scripts/upgrade.sh write_result() — prefer head -1 VERSION
  over `git rev-parse HEAD` so release-mode upgrades report the new
  tag in result.json instead of "unknown".

- Fix 5: admin container healthcheck start_period 20s → 60s in both
  compose files, same class as the earlier api fix. Matches Gancio
  convention.

- Fix 7: /api/pangolin/sync now detects resources bound to a stale
  siteId (common after --pangolin-site new rotations), deletes and
  recreates them against the current site, and reports them under
  a new `reassigned` response field.

Bunker Admin
v2.9.3
2026-04-15 11:57:50 -06:00
5115c65691 Fix nginx/newt boot race by raising API healthcheck start_period to 120s
On a fresh install, the api container's docker-entrypoint.sh does
migrations + seed + MaxMind GeoLite2 download synchronously before
binding port 4000, which routinely exceeds the previous 75s tolerance
(start_period 30s + 3x15s retries). When the API flipped to unhealthy,
nginx and newt (which depend on api: condition: service_healthy)
aborted, leaving the tunnel offline and requiring a manual
'docker compose up -d nginx newt' second pass.

Bumping start_period to 120s gives first-boot enough slack, matches
convention (Rocket.Chat 90s, Gancio 60s), and leaves steady-state
restarts unaffected. Change applied to both dev (docker-compose.yml)
and prod (docker-compose.prod.yml) since the release tarball ships
the latter.

Bunker Admin
2026-04-15 10:05:20 -06:00
e55bc07eb6 Security hardening: red-team remediation + CCP/WIP updates
## Security (red-team audit 2026-04-12)

Public data exposure (P0):
- Public map converted to server-side heatmap, 2-decimal (~1.1km) bucketing,
  no addresses/support-levels/sign-info returned
- Petition signers endpoint strips displayName/signerComment/geoCity/geoCountry
- Petition public-stats drops recentSigners entirely
- Response wall strips userComment + submittedByName
- Campaign createdByUserEmail + moderation fields gated to SUPER_ADMIN

Access control (P1):
- Campaign findById/update/delete/email-stats enforce owner === req.user.id
  (SUPER_ADMIN bypasses), return 404 to avoid enumeration
- GPS tracking session route restricted to session owner or SUPER_ADMIN
- Canvass volunteer stats restricted to self or SUPER_ADMIN
- People household endpoints restricted to INFLUENCE + MAP roles (was ADMIN*)
- CCP upgrade.service.ts + certificate.service.ts gate user-controlled
  shell inputs (branch, path, slug, SAN hostname) behind regex validators

Token security (P2):
- Query-param JWT auth replaced with HMAC-signed short-lived URLs
  (utils/signed-url.ts + /api/media/sign endpoint); legacy ?token= removed
  from media streaming, photos, chat-notifications, and social SSE
- GITEA_SSO_SECRET + SERVICE_PASSWORD_SALT now REQUIRED (min 32 chars);
  JWT_ACCESS_SECRET fallback removed — BREAKING for existing deployments
- Refresh tokens bound to device fingerprint (UA + /24 IP) via `df` JWT
  claim; mismatch revokes all user sessions
- Refresh expiry reduced 7d → 24h
- Refresh/logout via request body removed — httpOnly cookie only
- Password-reset + verification-resend rate limits now keyed on (IP, email)
  composite to prevent both IP rotation and email enumeration

Defense-in-depth (P3):
- DOMPurify sanitization applied to GrapesJS landing page HTML/CSS
- /api/health?detailed=true disk-space leak removed
- Password-reset/verification token log lines no longer include userId

## Deployment

- docker-compose.yml + docker-compose.prod.yml: media-api now receives
  GITEA_SSO_SECRET + SERVICE_PASSWORD_SALT; empty fallbacks removed
- CCP templates/env.hbs adds both new secrets; refresh expiry → 24h
- CCP secret-generator.ts generates giteaSsoSecret + servicePasswordSalt
- leaflet.heat added to admin/package.json for heatmap rendering

## Operator action required on existing installs

Run `./config.sh` once (idempotent — only fills empty values) or manually
add GITEA_SSO_SECRET + SERVICE_PASSWORD_SALT to .env via
`openssl rand -hex 32`. Startup fails with a clear Zod error otherwise.

See SECURITY_REDTEAM_2026-04-12.md for full audit and verification matrix.

## Other

Includes in-flight CCP work: instance schema tweaks, agent server updates,
health service, tunnel service, DEV_WORKFLOW doc updates, and new migration
dropping composeProject uniqueness.

Bunker Admin
v2.9.1 v2.9.2
2026-04-12 15:17:00 -06:00
26ec925d9b CCP restore/tunnel/upgrade + upgrade.sh release-mode fixes + volunteer dashboard polish
- Add instance restore model, routes, and agent backup/restore endpoints
- Add Pangolin tunnel service (subdomain prefix, teardown action, CCP client)
- Add slug mutex for concurrent operation safety in agent
- Expand upgrade service with remote driver orchestration
- Fix upgrade.sh to properly handle release-mode installs (no git operations)
- Add CCP registration flags to config.sh (--ccp-url, --ccp-invite-code, --ccp-agent-url)
- Auto-detect JVB advertise IP in non-interactive mode
- Polish volunteer dashboard ActionStepsList with highlighted step component
- Add ticketed event description field + volunteer dashboard query refinements

Bunker Admin
v2.9.0
2026-04-12 11:09:46 -06:00
29d1f3998a Visual polish of volunteer dashboard components to match FAFC reference
TakeActionCard: replaced standard Card with a bold red gradient panel
(linear-gradient #e74c3c → #c0392b) with white text, uppercase header,
and a white CTA button — matches the FAFC "Take Action" visual weight.

ActionCampaignCard: tightened padding, thicker progress bar (10px),
compact card header with gold trophy icon, and a "Next:" hint with
the upcoming step label separated by a subtle divider.

ActionStepsList: replaced Antd List with manual flex rows for tighter
control. Each row shows a kind prefix label (Email:, Sign:, RSVP:)
in muted text above the step label. "Take Action" buttons are type=link
for visual lightness. Completed steps show a compact "Done" tag.

TrainingList: added count badge ("2 Upcoming" in green) in the card
header extra slot. Each row uses a format tag ("In Person" / "Virtual")
matching FAFC's colored badges. RSVP buttons use danger variant (red)
to match FAFC's prominent red RSVP buttons.

ActivityCard: centered hero layout with large gold point number and
"points earned" subtitle. Achievement count only shown when > 0,
separated by a subtle divider.

MyEventsCard: consistent row layout with flex instead of Antd List.
Details button uses type=link for visual consistency with the steps
list.

ResourcesGrid: renamed to "Tools & Resources". Resource cards have
taller thumbnails (120px), cleaner padding, removed the type label
to reduce clutter — the icon in the placeholder already communicates
the type.

All cards now use consistent header styling (14px bold, 12px padding,
subtle 6% white bottom border) for visual rhythm across the page.

Bunker Admin
2026-04-11 21:35:58 -06:00
054902b9f9 Restructure volunteer dashboard to FAFC two-column layout
Matches the For Alberta For Canada reference: profile + goal progress
on the left, take-action CTA + action steps on the right. Specifics:

- ProfileCard now embeds the referral link (copy button + code) below
  the stats row, eliminating the standalone ReferralCard from the grid.

- ActionCampaignCard slimmed to progress-only: title, description,
  progress bar, reward text, and a "Next: {step}" hint. The steps
  list is extracted into a new ActionStepsList component.

- ActionStepsList renders as a compact card with kind-prefixed labels
  (Email:, Sign:, RSVP:) and Take Action / Mark done buttons, matching
  the FAFC action list on the right column.

- Dashboard page uses lg=12/12 for the top two-column section. Falls
  back to full-width single column when no campaign and no featured
  event exist. Mobile stacks vertically as before. The 3-up middle
  row (trainings / my events / activity) and full-width resources grid
  remain unchanged.

Bunker Admin
2026-04-11 21:27:58 -06:00
82db26fcef Auto-mint referral codes + action-based points for volunteer dashboard
Referral: if the user has no active invite code when the dashboard
loads, one is auto-created via referralService.createInviteCode.
Every volunteer now sees a ready-to-share referral link on first
visit without needing to manually create one.

Points: replaced the media-engagement placeholder (videos watched +
upvotes + comments) with a weighted tally of actual volunteer actions:
shift signups (10pts), influence campaign emails (5pts), verified
petition signatures (5pts), event tickets (10pts), action step
completions (5pts), and achievements (15pts). This reflects
organizing work rather than gallery browsing.

Bunker Admin
2026-04-11 21:15:36 -06:00
df65b1b72e Add documents volume mount + harden compose health checks
Mount ${MEDIA_ROOT}/local/documents:/media/local/documents:rw on the
media-api service so the document upload route can write PDFs to
disk. Without this mount, uploads fail on fresh deployments.

Also hardens several depends_on entries from service_started to
service_healthy (redis for media-api, api for admin, listmonk-app
for listmonk-init, gitea-db for gitea, nginx for newt) so
containers wait for actual readiness, not just process start.

Bunker Admin
2026-04-11 21:05:01 -06:00
80321f04e7 Link event staffing shifts to ticketed events with auto pre-fill
Shift.ticketedEventId (nullable FK to TicketedEvent) persists the
link between a staffing shift and its parent event. The shift list
response now includes the linked event (id, slug, title, date) so
the admin UI can show context without a second request.

The Create/Edit Shift drawer gains a conditional Event picker that
only appears when kind is EVENT_STAFFING. Picking an event pre-fills
the form's date, startTime, endTime, and location (venue name +
address) — and seeds the title with "Staff: {event.title}" only if
the title field is still empty, so hand-typed overrides aren't
stomped. Switching kind away from EVENT_STAFFING clears the link.

The shifts table's Kind tag wraps in a tooltip showing the linked
event title when one is present, so organizers can see at a glance
which staffing shifts belong to which event without opening the
drawer.

Bunker Admin
2026-04-11 11:45:15 -06:00
96ff2a85d6 Expose ShiftKind in admin panel with Dropdown.Button picker
Shift.kind existed in the schema and on the volunteer dashboard's
training filter but there was no way to create or edit a shift's
kind from the admin UI — every shift landed as the default CANVASS.
This wires the full loop:

Backend: createShiftSchema / updateShiftSchema / listShiftsSchema
and their series counterparts now accept a kind field. The shifts
service passes it through on create and filters by it on list.
Series shift templates propagate kind to every generated shift
instance so a training series produces training shifts.

Admin UI: the Create Shift button becomes a Dropdown.Button. The
main action creates a Canvass shift (default); the menu offers
Training, Event Staffing, Phone Bank, and Other. Each menu item
pre-fills the form's kind field. A kind Select appears at the top
of the form so admins can change it mid-creation or on edit. The
shifts table gets a color-coded Kind column and the toolbar gets
a kind filter.

Bunker Admin
2026-04-11 11:09:23 -06:00
76fd3c7065 Fix action-campaigns list shape + widen volunteer dashboard on desktop
The admin list endpoint returned a bare array but the frontend list
page expected { items: [] }. Newly-created campaigns were saving
successfully but rendering as an empty list. Wrap the response to
match the contract that the frontend was already coded against.

VolunteerLayout capped content at 800px wide, which suits the
phone-first volunteer tool pages but left most of the desktop screen
empty on the new dashboard. Bump the cap to 1280px for /volunteer
only so other volunteer pages keep their narrow sizing.

Bunker Admin
2026-04-11 10:37:40 -06:00
c00b4432d7 Add volunteer dashboard page + ActionCampaigns admin editor
VolunteerDashboardPage replaces the old direct-to-map landing at
/volunteer with a personalized action hub modeled on the For Alberta
For Canada layout: profile + referral on top, action campaign goal
tile next to a featured event CTA, training shifts + my events +
points + resources. The map moves to /volunteer/map as a fullscreen
route outside VolunteerLayout. CutRedirect updated to match.

VolunteerFooterNav and VolunteerLayout drawer get Home/Map split
tabs. AppLayout sidebar gets an Action Campaigns link under the
Advocacy menu.

ActionCampaignsPage lists campaigns; ActionCampaignEditorPage edits
metadata + steps with type-aware target pickers per ActionStepKind
(video picker, petition picker, ticketed-event picker, etc).
CUSTOM/VISIT_LINK steps get a free-form target URL. Reorder via
up/down buttons.

Bunker Admin
2026-04-11 10:21:10 -06:00
ae5a90d8d4 Add Document model upload + download routes (PDFs as first-class media)
Documents are a separate media type from Video/Photo because the
Photo pipeline assumes raster images (sharp metadata, EXIF, variant
generation). The new routes mirror the photo upload pattern but
target the Document Prisma model and serve files with
Content-Disposition: attachment so browsers download instead of
inline-rendering. Tag-based categorization (e.g. 'volunteer-resource')
lets the volunteer dashboard surface curated downloads alongside
videos and photos.

Admin Library page gets a Documents tab for upload/list/edit/delete
with the same affordances as the existing photo and video tabs.

Bunker Admin
2026-04-11 10:20:54 -06:00
ed011a762b Add action-campaigns module + volunteer-dashboard aggregator
ActionCampaign service exposes admin CRUD plus a per-user composer
(getActiveForUser) that fans out a per-step completion check against
the existing per-user model for each ActionStepKind: VideoView for
WATCH_VIDEO, CampaignEmail for SUBMIT_INFLUENCE, PetitionSignature
for SIGN_PETITION (matched by signer email), Ticket for RSVP_EVENT,
ShiftSignup for SIGNUP_SHIFT, ChallengeTeamMember for JOIN_CHALLENGE.
CUSTOM and VISIT_LINK steps complete only via explicit self-report.
An existing ActionStepCompletion row also short-circuits the check
so manual marking and idempotency both work.

Volunteer dashboard aggregator at GET /api/volunteer/dashboard
composes the active campaign with the user's profile, referral info,
upcoming featured event, training shifts (Shift.kind='TRAINING'),
ticketed events the user holds, an engagement-counter point total
(placeholder until the Redis engagement score is wired in), and
resources tagged 'volunteer-resource' across Document/Video/Photo.

Bunker Admin
2026-04-11 10:20:26 -06:00
3fc67cd81a Add ActionCampaign + Document models for volunteer dashboard
Foundation schema for the FAFC-style volunteer dashboard. Adds:
- ActionCampaign / ActionStep / ActionStepCompletion (stacked-action
  mini-campaigns where steps reference existing entities like videos,
  petitions, ticketed events; completion is detected at query time
  against the per-user model for each step kind)
- Document model for downloadable resources (PDFs etc.) — Photo's
  EXIF/sharp pipeline can't host non-image files
- Shift.kind discriminator (ShiftKind enum) so training shifts can
  surface separately on the dashboard
- TicketedEvent.featured for the "Take Action" CTA tile

Bunker Admin
2026-04-10 21:26:56 -06:00
5f0ae6bc5a Revert NocoDB auto sign-in, keep CSP fix for embed proxy
NocoDB v2 stores auth tokens in-memory (Pinia store), not in cookies
accessible to external pages. The auth bridge approach can't inject
tokens into NocoDB's SPA state. Reverted to the original banner
approach ("sign in to NocoDB in a new tab").

Kept: CSP fix (frame-ancestors http://localhost:* instead of just
localhost, which only matched port 80).

Bunker Admin
v2.8.2
2026-04-09 14:01:02 -06:00
aa69048024 Fix Gitea init: must-change-password flag syntax + auth check
- Use --must-change-password=false (equals syntax) in gitea CLI
  The space-separated form was parsed as boolean flag + extra arg
- Fix auto-setup readiness check: use Basic Auth instead of
  unauthenticated /version endpoint (blocked by REQUIRE_SIGNIN_VIEW)
- Increase retries to 8 to accommodate gitea-init container startup

Bunker Admin
2026-04-09 13:32:44 -06:00
c180bb5ace Rework Gitea init to use separate init container pattern
The previous approach (custom CMD on gitea-app) failed because:
- Gitea's entrypoint generates app.ini as root, then drops to git user
- Overriding CMD ran our script before app.ini was generated
- su-exec to git user lost access to the entrypoint-generated config

Now uses the same pattern as nocodb-init: a separate container that
depends on gitea-app being healthy, shares the gitea-data volume
(which has app.ini), and runs gitea admin user create.

Bunker Admin
2026-04-09 13:25:56 -06:00
c5209887cc Fix gitea-init.sh running as root — drop to git user via su-exec
The Gitea Docker entrypoint sets up directories as root then exec's
the CMD still as root. Gitea refuses to run as root, so our init
script must re-exec itself as the 'git' user via su-exec before
running any gitea commands.

Bunker Admin
2026-04-09 13:14:48 -06:00
ca446136a1 Fix set -e crash in pangolin_create_resources arithmetic
((created++)) returns exit code 1 when created=0 (post-increment
evaluates to 0, which is falsy), killing the script under set -e.
Use x=$((x + 1)) instead.

Bunker Admin
2026-04-09 13:03:12 -06:00
0510420772 Fix pangolin_create_site blocking on read in non-interactive mode
The site name prompt used read -rp which blocks when stdin is piped.
Now uses default name automatically when NON_INTERACTIVE=true.

Bunker Admin
2026-04-09 12:57:54 -06:00
36b709b911 Automate Gitea init, NocoDB auto sign-in, and fix prod compose
- Add scripts/gitea-init.sh: runs migrations + creates admin user on
  first boot, replacing the manual installation wizard
- Set GITEA__security__INSTALL_LOCK=true in both compose files
- Add NocoDB auth bridge (nginx) + /api/services/nocodb-auth proxy
  endpoint so the admin iframe auto-authenticates
- Update NocoDBPage.tsx to fetch token and use auth bridge flow
- Fix docker-compose.prod.yml missing Gitea env vars for API container
  (GITEA_URL, GITEA_API_TOKEN, GITEA_ADMIN_PASSWORD, etc.)
- Pass NC_ADMIN_EMAIL/PASSWORD to API for NocoDB auth proxy
- Increase Gitea auto-setup retries from 3 to 6 with admin auth check
- Update config.sh non-interactive mode to set GITEA_ADMIN_USER
- Include gitea-init.sh in release tarball (build-release.sh)

Bunker Admin
2026-04-09 12:49:33 -06:00
0a8e1fe46b Remove deployment report from repo (moved to gitignored docs/)
Bunker Admin
2026-04-09 12:08:15 -06:00
f8c8a939d7 Add full non-interactive mode to config.sh
New CLI flags for scripted deployments:
  --smtp-host/port/user/pass   Production SMTP configuration
  --pangolin-api-url/key/org-id/endpoint/site  Full Pangolin tunnel setup
  --mapbox-key                 Mapbox API key
  --maxmind-account-id/license-key  MaxMind GeoIP credentials

With --pangolin-site=new, config.sh creates a Pangolin site, fetches
Newt credentials, and creates all resources+targets automatically.
With --pangolin-site=existing, it connects to the first available site.

Bunker Admin
2026-04-09 12:08:05 -06:00
bca4cb8227 Fix Pangolin site creation: omit address field from payload
The clientAddress from pickSiteDefaults lacks CIDR notation and gets
rejected by the Pangolin API. Omitting it lets Pangolin auto-assign.

Bunker Admin
2026-04-09 11:58:14 -06:00
f0d994074d Update admin modals and page components for mobile responsiveness
Bunker Admin
v2.8.1
2026-04-09 11:43:23 -06:00
849dea7ce2 Fix config.sh Pangolin setup and MongoDB init for fresh deployments
- Fix Pangolin endpoint: ask separately from API URL (different hostnames)
- Add pangolin_create_resources() to create resources + targets during setup
- Set all resources as public (no SSO/blockAccess) automatically
- Fix API health check URL to use actual org endpoint
- Fix MongoDB entrypoint: delegate to docker-entrypoint.sh so INITDB user
  creation works on fresh volumes (was bypassing Docker's init sequence)

Bunker Admin
2026-04-09 11:43:13 -06:00