Replaces single-MP4 + range-request streaming with HLS multi-bitrate
segments to fix video stutter through the Newt tunnel. Range-request
bursts were the root cause; HLS chunks are small and tunnel-friendly,
plus the player adapts bitrate to bandwidth.
Backend
- New BullMQ `hls-transcode` queue (in-process worker, concurrency 1)
- FFmpeg single-pass transcode → 360p/720p/1080p variants with aligned
keyframes; output at /media/local/hls/{id}/master.m3u8
- New /api/{videos|public}/{id}/hls/* routes serving signed manifests
and segments (URLs emitted as /media/* so nginx rewrites to media-api)
- Prisma: HlsStatus enum + 6 fields on Video + index, migration
- Upload + yt-dlp fetch paths enqueue transcode jobs
- ENABLE_HLS_TRANSCODE flag (default off; gates enqueue only)
- Backfill script: `npm run backfill:hls`
- media-api bumped to 4 CPU / 2G for FFmpeg headroom
Frontend
- New useHls hook: lazy-imports hls.js (kept out of main bundle),
native HLS on Safari/iOS, gives up after 2 NETWORK_ERRORs so MP4
fallback engages cleanly
- VideoPlayer, VideoViewerModal, ShortsPage, ProductDetailPage now
prefer HLS when ready; MP4 fallback is automatic
- ShortsPage prefetches next-3 master manifests via <link rel="prefetch">
- PublicVideoCard hover preview stays MP4 (avoids hls.js init latency)
Bunker Admin
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
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
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).
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.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
- 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
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
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
((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
- 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
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