dfc27fd5a2a51a84bd32386e5ebce776163273e8
97
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dfc27fd5a2 |
Shortens deploy_production label, adds a deploy_test toggle for manual runs, and skips backend/frontend gates when that side of the repo has no changes
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / changes (pull_request) Successful in 21s
Continuous Integration / backend-build (pull_request) Successful in 5m17s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m30s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m32s
Continuous Integration / backend-test (pull_request) Successful in 5m31s
Continuous Integration / frontend-build (pull_request) Successful in 2m10s
Continuous Integration / frontend-test (pull_request) Successful in 4m26s
Continuous Integration / frontend-lint (pull_request) Successful in 2m12s
Continuous Integration / publish-test (pull_request) Canceled after 0s
Continuous Integration / publish-production (pull_request) Canceled after 0s
Continuous Integration / deploy-test (pull_request) Canceled after 0s
Continuous Integration / deploy-production (pull_request) Canceled after 0s
Push-to-master keeps auto-deploying to test unchanged. A new deploy_test input (default false) lets a manual workflow_dispatch run opt into a test deploy instead of it firing by default. A new changes job (dorny/paths-filter) gates the backend and frontend build/test/lint jobs on whether their side of the repo actually changed, shortening runs that only touch one side; publish jobs still run whenever either side changed, and manual runs always run everything since there's no commit to diff against. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VhySVFARKAP89WqQ3Kff8M |
||
|
|
66c0d768cf |
Merge pull request 'Fixes mobile sidebar collapsing to content height instead of filling the overlay' (#3) from fix/mobile-sidebar-height into master
Continuous Integration / config (push) Successful in 10s
Continuous Integration / backend-build (push) Successful in 4m56s
Continuous Integration / vulnerability-scan (push) Successful in 4m23s
Continuous Integration / frontend-prepare (push) Successful in 1m51s
Continuous Integration / backend-test (push) Successful in 5m23s
Continuous Integration / frontend-build (push) Successful in 2m20s
Continuous Integration / frontend-test (push) Successful in 4m24s
Continuous Integration / frontend-lint (push) Successful in 2m6s
Continuous Integration / publish-production (push) Skipped
Continuous Integration / deploy-production (push) Skipped
Continuous Integration / publish-test (push) Successful in 6m45s
Deploy (SCP) / deploy (push) Successful in 1m22s
Continuous Integration / deploy-test (push) Successful in 1m24s
Reviewed-on: #3 |
||
|
|
bc4de27b97 |
Fixes mobile sidebar collapsing to content height instead of filling the overlay
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 5m9s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m28s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m53s
Continuous Integration / backend-test (pull_request) Successful in 5m29s
Continuous Integration / frontend-build (pull_request) Successful in 2m16s
Continuous Integration / frontend-test (pull_request) Successful in 4m42s
Continuous Integration / frontend-lint (pull_request) Successful in 1m54s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m37s
Continuous Integration / deploy-test (pull_request) Skipped
The aside had no explicit height, so it stretched to full height on desktop only because its flex-row parent applies align-items:stretch. In the mobile SidebarOverlay it sits in a plain (non-flex) h-full div, so it shrank to fit its nav items, pushing settings/profile up under the nav instead of staying pinned to the bottom. |
||
|
|
5dfe92b61c |
Merge pull request 'Adds Monitoring Setup docs and deploy-scp troubleshooting/debug fixes' (#2) from feature/gitea-deployment-workflow into master
Continuous Integration / config (push) Successful in 10s
Continuous Integration / backend-build (push) Successful in 5m6s
Continuous Integration / vulnerability-scan (push) Successful in 4m40s
Continuous Integration / frontend-prepare (push) Successful in 1m51s
Continuous Integration / backend-test (push) Successful in 5m33s
Continuous Integration / frontend-build (push) Successful in 2m8s
Continuous Integration / frontend-test (push) Successful in 4m26s
Continuous Integration / frontend-lint (push) Successful in 1m58s
Continuous Integration / publish-production (push) Skipped
Continuous Integration / deploy-production (push) Skipped
Continuous Integration / publish-test (push) Successful in 6m27s
Deploy (SCP) / deploy (push) Successful in 1m24s
Continuous Integration / deploy-test (push) Successful in 1m25s
Reviewed-on: #2 |
||
|
|
084b941204 |
Pins actions/setup-node to v3, skips DB backup when no database exists yet
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m57s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m47s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m51s
Continuous Integration / backend-test (pull_request) Successful in 5m17s
Continuous Integration / frontend-build (pull_request) Successful in 2m15s
Continuous Integration / frontend-test (pull_request) Successful in 4m24s
Continuous Integration / frontend-lint (pull_request) Successful in 2m10s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m19s
Continuous Integration / deploy-test (pull_request) Skipped
actions/setup-node@v4's Post step attempts a cache-save that fails hard on this self-hosted Gitea Actions runner (no GHES cache API support), marking frontend-prepare as failed and blocking its dependents even though the actual install/build work succeeds. v3's older cache implementation doesn't hit this. Also cleans up two stale comments left over from the /admin MSBuild-target removal. Also updates the documented backup script to skip cleanly (exit 0) when the target database doesn't exist yet (MariaDB error 1049) rather than failing the whole deploy - expected on a brand-new environment's first production run, before migrations have ever had a chance to create it. Still fails hard on any other error, so a real backup failure against an existing database still blocks the deploy as intended. |
||
|
|
f10d7b983e |
Reverts deploy-test to push/workflow_dispatch only, drops the pull_request trigger
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 5m22s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m44s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m39s
Continuous Integration / backend-test (pull_request) Successful in 5m11s
Continuous Integration / frontend-build (pull_request) Successful in 2m10s
Continuous Integration / frontend-test (pull_request) Successful in 4m27s
Continuous Integration / frontend-lint (pull_request) Successful in 1m56s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m24s
Continuous Integration / deploy-test (pull_request) Skipped
The pull_request trigger was a temporary convenience to test the deploy pipeline itself against the real test environment on every PR push. Now that the pipeline is verified working end to end, going back to only deploying on a master push or manual dispatch avoids concurrent PRs silently overwriting the single shared test environment. |
||
|
|
22bc5c5cde |
Doubles frontend test findBy* timeouts to reduce CI flakiness
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m48s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m33s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m47s
Continuous Integration / backend-test (pull_request) Successful in 5m17s
Continuous Integration / frontend-build (pull_request) Successful in 2m13s
Continuous Integration / frontend-test (pull_request) Successful in 4m22s
Continuous Integration / frontend-lint (pull_request) Successful in 2m0s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m18s
Deploy (SCP) / deploy (pull_request) Successful in 1m28s
Continuous Integration / deploy-test (pull_request) Successful in 1m29s
5000ms was occasionally too tight on the self-hosted Actions runner for lazy-loaded routes gated behind an async setup-status check (observed on SettingsPage.test.tsx, passed reliably locally). Bumps to 10000ms across all nine test files using that pattern, and raises vite.config.ts's global testTimeout from 15000 to 20000 to keep headroom above it. |
||
|
|
7c1d2aa520 |
Fixes /admin 404 by building the admin SPA before dotnet publish, not via an MSBuild target
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m52s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m50s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m44s
Continuous Integration / backend-test (pull_request) Successful in 5m37s
Continuous Integration / frontend-build (pull_request) Successful in 2m11s
Continuous Integration / frontend-test (pull_request) Successful in 4m33s
Continuous Integration / frontend-lint (pull_request) Successful in 2m2s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m14s
Deploy (SCP) / deploy (pull_request) Successful in 1m20s
Continuous Integration / deploy-test (pull_request) Successful in 1m21s
The BuildAndCopyAdminFrontend MSBuild target (BeforeTargets=Publish) never actually worked: files it created after project evaluation were silently absent from the publish output, and forcing them in via an explicit Content item collided with the SDK's own static-web-asset resolution. Verified locally with a clean obj/bin and a fresh node_modules - every deploy so far genuinely shipped without wwwroot/admin. Moves the frontend build + copy into its own CI step ahead of dotnet publish, so the SDK's ordinary wwwroot handling picks it up with no custom MSBuild involved. Also fixes unreadable <code> badges on the website placeholder page (no explicit text color, relying on inherited body color pairing unreliably with the badge background). |
||
|
|
9b44a5e85e |
Adds a shared/modules symlink mechanism for optional plugin modules
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 5m4s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m23s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m40s
Continuous Integration / backend-test (pull_request) Successful in 5m46s
Continuous Integration / frontend-build (pull_request) Successful in 2m18s
Continuous Integration / frontend-test (pull_request) Successful in 4m33s
Continuous Integration / frontend-lint (pull_request) Successful in 2m0s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m11s
Deploy (SCP) / deploy (pull_request) Successful in 1m25s
Continuous Integration / deploy-test (pull_request) Successful in 1m25s
ModuleOrchestrator already discovers SlpModularCms.Modules.*.dll from disk at startup via reflection, so this needed no code change — only a persistent location (mirroring shared/wwwroot-web) that survives release swaps, and a deploy-scp.yaml step to symlink its contents into each new release before restart. |
||
|
|
d51285d9dd |
Removes the secret-diagnosis debug step, fixes wwwroot symlink parent dir
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m34s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m39s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m45s
Continuous Integration / backend-test (pull_request) Successful in 5m15s
Continuous Integration / frontend-build (pull_request) Successful in 2m14s
Continuous Integration / frontend-test (pull_request) Successful in 4m27s
Continuous Integration / frontend-lint (pull_request) Successful in 1m57s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 5m51s
Deploy (SCP) / deploy (pull_request) Failing after 11m46s
Continuous Integration / deploy-test (pull_request) Failing after 11m47s
The PI_MAIN_USERNAME/PASSWORD debug step served its purpose (found the
shell-metacharacter password bug fixed in
|
||
|
|
51488d6d94 |
Routes PI_MAIN_* secrets through env vars instead of inline interpolation
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 4m57s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m21s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m28s
Continuous Integration / backend-test (pull_request) Successful in 5m3s
Continuous Integration / frontend-build (pull_request) Successful in 2m10s
Continuous Integration / frontend-test (pull_request) Successful in 4m42s
Continuous Integration / frontend-lint (pull_request) Successful in 1m56s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m30s
Deploy (SCP) / deploy (pull_request) Failing after 57s
Continuous Integration / deploy-test (pull_request) Failing after 58s
Every ssh/scp step interpolated ${{ secrets.PI_MAIN_PASSWORD }} and
${{ secrets.PI_MAIN_USERNAME }} directly into the run: shell text.
Gitea/GitHub Actions substitutes that as literal text before bash ever
sees it, so any shell-metacharacter in the password ($, `, ", \) gets
re-interpreted by bash instead of passed through - silently changing
what sshpass actually receives. This plausibly explains a persistent
"Permission denied" even after the username and password length were
both confirmed correct via the debug step (which used env: and so
never hit this).
All five steps now receive SSH_USER/SSH_PASS/SSH_PORT/SSH_HOST via
env:, which bash treats as opaque values with no re-parsing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FffvxxJp5wG34Ru48GBig
|
||
|
|
3776c0f27b |
Adds a trimmed-length check to the PI_MAIN_PASSWORD debug step
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 4m39s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m15s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m47s
Continuous Integration / backend-test (pull_request) Successful in 5m15s
Continuous Integration / frontend-build (pull_request) Successful in 2m11s
Continuous Integration / frontend-test (pull_request) Successful in 4m40s
Continuous Integration / frontend-lint (pull_request) Successful in 1m58s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m25s
Deploy (SCP) / deploy (pull_request) Failing after 56s
Continuous Integration / deploy-test (pull_request) Failing after 57s
Username is now confirmed correct (14 chars, exact match). This checks whether the password secret carries leading/trailing whitespace or a stray newline picked up when it was pasted into Gitea - the length differs from the trimmed length if so, without ever logging the password's actual characters. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FffvxxJp5wG34Ru48GBig |
||
|
|
c3070b4bfb |
Merge branch 'master' into feature/gitea-deployment-workflow
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m19s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m0s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m39s
Continuous Integration / backend-test (pull_request) Successful in 5m1s
Continuous Integration / frontend-build (pull_request) Successful in 2m9s
Continuous Integration / frontend-test (pull_request) Successful in 4m41s
Continuous Integration / frontend-lint (pull_request) Successful in 2m0s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 6m16s
Deploy (SCP) / deploy (pull_request) Failing after 56s
Continuous Integration / deploy-test (pull_request) Failing after 57s
|
||
|
|
92547efd47 |
Allows deploy-test to run on pull_request events too
Continuous Integration / config (pull_request) Canceled after 0s
Continuous Integration / backend-build (pull_request) Canceled after 0s
Continuous Integration / backend-test (pull_request) Canceled after 0s
Continuous Integration / vulnerability-scan (pull_request) Canceled after 0s
Continuous Integration / frontend-prepare (pull_request) Canceled after 0s
Continuous Integration / frontend-build (pull_request) Canceled after 0s
Continuous Integration / frontend-test (pull_request) Canceled after 0s
Continuous Integration / frontend-lint (pull_request) Canceled after 0s
Continuous Integration / publish-test (pull_request) Canceled after 0s
Continuous Integration / publish-production (pull_request) Canceled after 0s
Continuous Integration / deploy-test (pull_request) Canceled after 0s
Continuous Integration / deploy-production (pull_request) Canceled after 0s
So PR pushes deploy straight to the test environment, without a manual workflow_dispatch each time. Only one test environment exists, so this means concurrent PRs would overwrite each other's deploy - acceptable with a single active PR, worth revisiting once that's no longer true. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015FffvxxJp5wG34Ru48GBig |
||
|
|
56f4f6fe0f |
Adds Monitoring Setup docs and deploy-scp troubleshooting/debug fixes
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 4m27s
Continuous Integration / vulnerability-scan (pull_request) Successful in 4m10s
Continuous Integration / backend-test (pull_request) Canceled after 0s
Continuous Integration / frontend-build (pull_request) Canceled after 0s
Continuous Integration / frontend-test (pull_request) Canceled after 0s
Continuous Integration / frontend-lint (pull_request) Canceled after 0s
Continuous Integration / publish-test (pull_request) Canceled after 0s
Continuous Integration / publish-production (pull_request) Canceled after 0s
Continuous Integration / deploy-test (pull_request) Canceled after 0s
Continuous Integration / deploy-production (pull_request) Canceled after 0s
Continuous Integration / frontend-prepare (pull_request) Canceled after 50s
Monitoring Setup: operations/plans/monitoring-setup-plan.md and
operations/monitoring/monitoring-instructions.md, covering Sentry alert
rules on the security_event tag, UptimeRobot's 6 liveness monitors, and
the two new Umami website entries for the admin SPA.
deployment-instructions.md gains a missing Observability__Environment
host var (without it, both environments would tag Sentry events as
"Production"), the nginx client_max_body_size fix for the 413 seen on
publish-test/production artifact uploads, and two troubleshooting notes
on Gitea Actions re-run behaviour: re-running deploy-test/production
alone loses the run's uploaded artifact, and re-running all jobs on an
existing (rather than a brand new) run can replay stale secrets.
deploy-scp.yaml: step names no longer show literal unresolved
${{ inputs.* }} text (Gitea doesn't interpolate that context in step
names), and a temporary debug step logs PI_MAIN_USERNAME/PASSWORD
length plus a username equality check to diagnose a persistent
Permission denied during the SSH steps, without ever logging the
secret values themselves.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015FffvxxJp5wG34Ru48GBig
|
||
|
|
2d0372ad78 |
Merge pull request 'Feature/gitea deployment workflow' (#1) from feature/gitea-deployment-workflow into master
Continuous Integration / config (push) Successful in 9s
Continuous Integration / backend-build (push) Successful in 4m20s
Continuous Integration / vulnerability-scan (push) Successful in 4m24s
Continuous Integration / frontend-prepare (push) Successful in 1m34s
Continuous Integration / backend-test (push) Successful in 5m0s
Continuous Integration / frontend-build (push) Successful in 2m7s
Continuous Integration / frontend-test (push) Successful in 4m28s
Continuous Integration / frontend-lint (push) Successful in 1m53s
Continuous Integration / publish-production (push) Skipped
Continuous Integration / deploy-production (push) Skipped
Continuous Integration / publish-test (push) Successful in 5m50s
Deploy (SCP) / deploy (push) Failing after 54s
Continuous Integration / deploy-test (push) Failing after 55s
Reviewed-on: #1 |
||
|
|
d81168b7a9 |
Aligns mock API_BASE with the real ApiClient's origin fallback
Continuous Integration / config (pull_request) Successful in 9s
Continuous Integration / backend-build (pull_request) Successful in 4m2s
Continuous Integration / backend-test (pull_request) Successful in 4m35s
Continuous Integration / vulnerability-scan (pull_request) Successful in 3m41s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m35s
Continuous Integration / frontend-build (pull_request) Successful in 2m7s
Continuous Integration / frontend-test (pull_request) Successful in 4m22s
Continuous Integration / frontend-lint (pull_request) Successful in 1m51s
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Continuous Integration / publish-test (pull_request) Successful in 5m17s
Continuous Integration / deploy-test (pull_request) Skipped
The mocks re-derived VITE_API_BASE_URL with their own fallback instead of reusing getAppConfig().apiBaseUrl, so the two silently diverged whenever the env var was unset (as in CI, where no local .env.local is present) — every MSW-intercepted request then had no matching handler. |
||
|
|
ff0fc66b10 |
Adds the missing packages field to pnpm-workspace.yaml
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m1s
Continuous Integration / vulnerability-scan (pull_request) Successful in 3m38s
Continuous Integration / frontend-prepare (pull_request) Successful in 1m22s
Continuous Integration / backend-test (pull_request) Successful in 4m39s
Continuous Integration / frontend-build (pull_request) Successful in 2m6s
Continuous Integration / frontend-test (pull_request) Failing after 6m22s
Continuous Integration / frontend-lint (pull_request) Successful in 1m58s
Continuous Integration / publish-test (pull_request) Skipped
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
Pre-existing file from the original frontend setup - had allowBuilds and confirmModulesPurge but no packages list, which pnpm 9+ requires the moment a workspace file exists at all, even for a single package. Surfaced by the first real pipeline run (frontend-prepare failed with "packages field missing or empty"). |
||
|
|
2cd8c4aa88 |
Renames the backup script to match the instance, not the software
Continuous Integration / config (pull_request) Successful in 10s
Continuous Integration / backend-build (pull_request) Successful in 4m6s
Continuous Integration / vulnerability-scan (pull_request) Successful in 3m39s
Continuous Integration / frontend-prepare (pull_request) Failing after 58s
Continuous Integration / frontend-build (pull_request) Skipped
Continuous Integration / frontend-test (pull_request) Skipped
Continuous Integration / frontend-lint (pull_request) Skipped
Continuous Integration / backend-test (pull_request) Successful in 4m26s
Continuous Integration / publish-test (pull_request) Skipped
Continuous Integration / publish-production (pull_request) Skipped
Continuous Integration / deploy-test (pull_request) Skipped
Continuous Integration / deploy-production (pull_request) Skipped
backup-slpmodularcms-db.sh named the CMS; this backs up slpsoftware specifically, same as every other instance-scoped path already does. Hardcoded in deploy-scp.yaml itself, so this is the actual pipeline code, not just docs describing it. |
||
|
|
459c347c12 |
Labels every command with who runs it and where
Four contexts got tangled together across the doc as it grew: pi-main as root, pi-main as gitea-workflow, the separate proxy Pi, and Gitea's own web UI. Added a legend up front and tagged every single command block so it's no longer something the reader has to infer. |
||
|
|
c438ec8b1d |
Warns against copying the reference project's nginx Sentry tunnel
The backend already serves /sentry-tunnel itself with its own rate limit, payload cap, and DSN-derived destination - exactly what that nginx block exists to avoid needing. Proxying the same path at nginx intercepts it first and silently drops all three protections. |
||
|
|
50c1a5106d |
Fixes deployment docs to reverse-proxy on a separate Pi, not pi-main
nginx and certbot were designed assuming they lived on the same host as the app. They don't - a dedicated proxy Pi terminates TLS and forwards plain HTTP over the LAN. Kestrel now binds 0.0.0.0 instead of localhost, the whole certbot procedure moved to the proxy Pi's side, and pi-main gets a firewall rule restricting the backend ports to just the proxy Pi's address - otherwise binding all interfaces would let anything on the LAN skip the proxy's TLS entirely. |
||
|
|
d1b0d06568 |
Splits nginx routing into a from-scratch procedure plus the example
New section walks through adding any domain from nothing - plain HTTP block first (certbot's nginx plugin needs one to validate against), then the certbot command, then a verification step. The existing slpsoftware.nl blocks stay as the worked result of that procedure rather than a separate one-off. |
||
|
|
743c173257 |
Names su - specifically as a confirmed cause, not just sudo -u
Traced live on the actual Pi: su - gitea-workflow left XDG_RUNTIME_DIR unset even with the user manager already running and the bus socket already there. None of su/sudo -i/sudo -u reliably go through pam_systemd on Debian - only a real SSH login or sshd itself does, which is what the deploy workflow already uses. |
||
|
|
6882eda03f |
Adds systemd --user bus-connection troubleshooting
Enabling lingering doesn't start the user manager retroactively, and sudo -u often drops XDG_RUNTIME_DIR - both produce the same "Failed to connect to bus" error when setting this up by hand. |
||
|
|
33d18ebbf8 |
Switches the database from SQL Server to MariaDB
The target Pi only has MariaDB, and SQL Server has no ARM64 build at all - not a config problem, a real gap discovered during deployment setup. Swapped the EF Core provider, regenerated every migration, updated connection strings and the backup script everywhere they appear. Took two tries to land on a provider that actually works: Pomelo builds fine against this project's EF Core 10 packages but fails at runtime (it's compiled against 9's internal API surface, which moved in 10 wherever Identity/DataProtection force the newer packages). Oracle's official provider builds and migrates fine but has a real MariaDB bug in its own migration-lock code, reproduced against a live database. Kept Oracle's provider and worked around just that one broken method - everything else it does is correct - rather than give up more of the stack to chase a workaround. Verified against a real local MariaDB end to end: all three migrations applied, both hosts start clean, full suite still green. |
||
|
|
579e0ceaac |
Fixes a wrong placeholder and explains the optional env fields
AllowedConnectOrigins had a sentry-ingest-origin placeholder that was simply incorrect - the Sentry tunnel exists precisely so that exception is never needed. Both SecurityHeaders fields are Umami-only now, with inline comments on when to set them versus leave them out, and a note that MasterModule__MasterUrl degrades to a warning rather than a startup failure when unset. |
||
|
|
97f18d00e5 |
Names the deployed instance slpsoftware, not the software itself
SlpModularCms is the product; slpsoftware.nl is the customer running it - the first one, and a fine stand-in for what a real deployment looks like. Renamed the directories, systemd units, and backup paths accordingly. Left the backup script's own filename alone since the already-committed deploy-scp.yaml calls it by that exact name. |
||
|
|
843253888e |
Nests environments under one slpmodularcms folder instead of siblings
Applies to both the app's own release tree and the website upload path - slpmodularcms/test and slpmodularcms/production side by side under one parent, not two separately-named directories. Service unit names stay hyphenated; those aren't folders. |
||
|
|
25a3de0f1f |
Stops mixing gitea-workflow with the generic deploy-user placeholder
Same account, one name throughout instead of switching partway in. |
||
|
|
366e912e73 |
Renames the example deploy account to gitea-workflow
pi-deploy restated the obvious (it's on the Pi) without saying what the account is actually for. |
||
|
|
960760c24e |
Makes the deploy account generic instead of per-project
No reason to mint a new system account for every project on the same Pi - one shared, host-wide account works, namespaced by directory per project underneath it. |
||
|
|
2f243f23c3 |
Fixes the account model deploy setup got wrong
webadmin turned out to be FTP-only and belongs to whoever uploads customer websites, not to this pipeline. Switched to a separate deploy account with its own home-directory paths, well away from where the other sites live, and made the persistent website symlink cross into webadmin's territory instead of the other way around. |
||
|
|
cf80f827ae |
Deployment setup - the parts the workflow deliberately left out
Host setup checklist, real domains and ports, the database backup script the deploy workflow only ever invokes, and a rollback plan. Also drafted the FTPS switch procedure for whenever production moves off the Pi, with a note that shared hosting is likely IIS-based - that's a bigger change than swapping the transport. |
||
|
|
9ab30fe2a6 |
Closes construction - build and test across all seven units
Unit suites already passed individually; this is the full solution together plus what none of them could check alone: two real hosts talking to each other, a slave API key encrypted by one process and decrypted by the next one after a restart, and headers landing on actual static files instead of a simulated response. |
||
|
|
73126f5a9c |
Closes round 4 - all seven units generated
U7 was the last one. State doc corrected: it had U7 down for an infrastructure design pass that never actually happened, because by the time units were carved up U7 had shrunk to documentation only. |
||
|
|
88770c5bd0 |
U7 — tells a website builder what they need to know
WEBSITE_WORKSPACE.md, plus the parts of the README that were still describing the old layout or a manual step the code doesn't need anymore (the key-ring paragraph, mainly - that one was actively wrong now, not just stale). |
||
|
|
9f4ae475e7 |
U5 — the gate everything else has to pass
continuous_integration.yaml: six blocking checks, then a separate publish per environment so a Vite build never gets tagged for the wrong one, then a call into last commit's deploy workflow. Along the way: the lint list had drifted (two problems not in the requirement, one already fixed), and the Umami-origin gate needed a variable pair of its own since the backend's side of that comparison lives on the host, not in CI. Pinned the two vulnerable packages while at it. |
||
|
|
bd2a963498 |
U6 — the deploy workflow itself
deploy-scp.yaml: download, back up the production database before touching anything, upload into a timestamped release, symlink the persistent website in, switch current and restart, verify /health with retries, prune old releases only once that check passes. No sudo, no container actions, one shared interface U5 will call next. |
||
|
|
9a77eec0f1 |
Designs where the deploy workflow puts things on the Pi
Infrastructure design for U6: single Pi split by directory not port, systemd --user services instead of sudo, a releases/current/shared layout that keeps the customer website outside every atomic switch, and a health check through the existing reverse proxy before pruning anything. Flags that user-level systemd needs enable-linger or the service dies the moment the deploy SSH session closes. |
||
|
|
247d8be241 |
Closes round 2 of the deployment feature
Records the stage as complete and logs the two defects that local testing turned up after U3 and U4 were generated: ciphertext predating the key-ring move, and an integrity check that could not tell an unreachable slave from one that does not recognise the master. Both were fixed in this branch rather than filed. The second is master/slave domain behaviour rather than deployment work, so the state file says so plainly — it sits here by decision, not because it belongs to the feature. Verified at close: build 0 errors, 372 backend tests, 237 frontend tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
6957ec7c60 |
Tells apart a slave that is down from one that does not know us
The integrity check mapped four outcomes onto a single null: no response, a 404, a rejected key, and a genuine answer. Only "rejected key" is recoverable, and it was being reported as "unreachable" and never repaired — so an instance registered against the wrong URL stayed broken until someone edited the database by hand. That is exactly what happened locally. GetRegisteredMasterUrlAsync now returns an outcome alongside the URL. Unauthorized triggers registration; 404 is reported as "this host does not serve the master/slave protocol", which names the actual mistake instead of hiding it behind a generic contact failure; unreachable and server errors behave as before. Registering on a rejected key cannot hijack a slave that belongs to another master: the slave accepts a registration only when it has none, and refuses any key that does not match an existing one. So it succeeds exactly in the case worth recovering and fails harmlessly otherwise. That guarantee lives on the slave, so the test asserting the refusal now says out loud that the master depends on it. Found while diagnosing a status push that failed against a frontend URL. Small and contained, so fixed here rather than filed as tech debt. 372 tests pass, up from 366. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
980dc80701 |
Records that the key-ring move leaves older ciphertext unreadable
Starting both hosts after U2 fails to decrypt a stored slave API key: the row was encrypted under the old file-based key ring, and the database key ring holds a different, freshly generated key. The key ring working correctly is exactly why the old key is not found. U2 moved the ring without a path for ciphertext that predates it. Confirmed with the user that nothing is deployed yet, so no migration ships and the first deploy meets an empty database. ASM-08 records the condition and makes it blocking for U6 if it ever stops holding. Also notes that the master lets the CryptographicException escape while the slave catches it and reports "master API key rejected" instead — the ambiguity U4's event was designed around, showing up on day one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
8e79a72340 |
Makes the application say what it is doing and when it fails
U4. Console logging plus Sentry, a same-origin tunnel so ad blockers cannot silence browser errors, Umami on the admin SPA, and six security events that alert rules can actually be built on. The correlation id is the W3C trace id from the ambient Activity, enabled by one line of ActivityTrackingOptions so every entry from every category carries it without touching a call site. It propagates across the master/slave boundary via traceparent, which TraceIdentifier cannot do at all, and it is the same value ProblemDetails already returns to the browser. The security events use source-generated LoggerMessage with constant templates. Sentry groups log events by message, so interpolating an email address would give every address its own issue and "more than 20 failed logins in five minutes" could never fire — the events would arrive, be visible, be tagged, and the alerting would silently be impossible. A test asserts the rendered message is identical across argument values. Scrubbing happens in-process, before transmission, and covers Set-Cookie as well as Cookie: the login response issues the refreshToken there, so scrubbing only the request side would protect nothing. Transactions are scrubbed too, because they carry request data and are the channel nobody thinks of. The tunnel derives its destination from the DSN once at startup and reads nothing from the request, which is what separates a tunnel from a server-side request forgery primitive. Size is capped by a bounded read rather than by trusting Content-Length, and the endpoint is rate limited. Two things found along the way. Zod 4's url() hands the value to the URL constructor, which accepts any scheme — so the existing frontend validation would have accepted the exact "htp://" typo BR-U4-24 names, and the SPA would have called a nonexistent origin. Now constrained to http(s). And the new appsettings comments are verified against the real configuration provider, because the failure mode if it rejected them is both hosts refusing to start after a release switch. One deviation. IAdminTokenValidator was meant to gain a reason-reporting overload; implemented that way, a substitute returning false by default silently inverted the access decision while both methods compiled. Two methods whose difference is invisible at the call site is the defect, so it is now a single Validate returning AdminTokenResult. Touches two files from already-committed units: DatabaseMigrationExtensions (U2) gains a flush before the rethrow, or the one Critical event in the system dies with the process; AdminTokenValidator (U1) classifies why a bypass was refused. Build 0 errors; 366 backend tests pass, up from 315, and 237 frontend tests, up from 213. tsc clean, eslint clean on every changed file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
a122548454 |
Sends the security headers from the application instead of the proxy
U3. These headers normally come from nginx, but the deployment target does
not allow server configuration, so the application emits them itself. That
changes the failure mode: a bad nginx config fails loudly at reload, while a
middleware that never runs sends nothing and says nothing.
Two policies, defined in code. Strict for /admin, /api/v1 and /health;
relaxed for the public website, which is authored elsewhere by someone who
has never seen this policy. Configuration decides where a policy applies and
which external origins are permitted; it cannot invent a policy that is
subtly permissive.
script-src 'self' under Strict has no 'unsafe-inline' and no 'unsafe-eval',
asserted by a test so that loosening it means deleting a test that says why.
style-src does carry 'unsafe-inline' and cannot not: Radix positions its
overlays with inline style attributes, which nonces cannot reach at all.
Two traps handled explicitly. StartsWithSegments rather than string
StartsWith, because "/administrator".StartsWith("/admin") is true and a
public page would silently lose its inline scripts with no server-side trace.
And all decision logic sits in a static writer rather than in the middleware,
because DefaultHttpContext.Response.OnStarting is a no-op — the obvious
middleware test observes nothing and an assertion that nothing was written
passes for entirely the wrong reason.
An unknown policy name fails ValidateOnStart, so the process exits rather
than quietly serving /admin under the relaxed policy. Origin format is
validated too, beyond what the design asked: a CSP source list silently
ignores a malformed source, so a URL with a path would look configured and
block the script anyway.
BR-U3-22's Umami startup warning is withdrawn (REF-U3-01) — the backend
never sees VITE_UMAMI_WEBSITE_ID. It becomes a blocking CI gate in U5.
Build 0 errors; 315 tests pass, up from 253.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
|
||
|
|
5102f8668b |
Settles how the headers and observability units get built
NFR Design for U3 and U4. Two decisions the earlier stages had deliberately left open, plus four risks the functional design did not name. OPEN-01 closed: the correlation ID is the W3C trace ID from the ambient Activity, with TraceIdentifier as the fallback. It propagates across the master/slave boundary via traceparent, which TraceIdentifier cannot do at all, and it is the same value ProblemDetails already returns to the client. REF-U3-01 raised: BR-U3-22's Umami-origin startup warning cannot work. The backend never sees VITE_UMAMI_WEBSITE_ID, so the check would either always warn or never warn. Withdrawn from U3 and replaced by a blocking U5 CI gate that compares the frontend build variable against that environment's CSP origins, where both values are visible. Four additions beyond the functional design: - Set-Cookie added to the scrub list; the login response issues the refreshToken there, so scrubbing only the request cookie protects nothing - SetBeforeSendTransaction alongside SetBeforeSend; transactions carry request data too - OnRejected on the rate limiter; today a 429 leaves no trace anywhere - FlushAsync before the migration-failure rethrow, or the one Critical event in the system dies with the process Two traps recorded with tests attached: Sentry groups log events by message template, so interpolated messages make FR-19's rate-based alert rules unimplementable while appearing to work; and DefaultHttpContext.Response .OnStarting is a no-op, so the obvious middleware test asserts nothing. Three values chosen rather than escalated, each one line to change and all three listed for review at the end of U4's pattern document: JSON console outside Development, TracesSampleRate 0.1, tunnel cap 200 KB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
589146a167 |
Removes the sample database password from the README
Replaces the literal SA password in the container command and the example connection string with a placeholder. This change was already in the working tree at the start of the session and is unrelated to the deployment feature; it is committed separately so it stays easy to identify. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
357d395629 |
Designs the security headers and observability units
Records the functional design for the two remaining application units, before any of their code exists. Security headers have to come from the application, because relying on nginx or IIS configuration is exactly what this deployment model rules out. Strict applies to /admin, /api/v1 and /health; a relaxed policy applies to the public website, which this repository does not author. The strict policy needs style-src 'unsafe-inline'. That is not a shortcut: Radix positions dropdowns and dialogs with inline style attributes recalculated per click and scroll position, and CSP nonces apply only to style elements, never to style attributes. No nonce- or hash-based variant leaves the admin UI working. The exception is bounded to styles — script-src stays closed, which is where XSS actually lives. The website's policy is enforcing rather than absent, so every HTML-serving path carries a CSP and no exception has to be recorded. It still blocks external script origins, so it remains a real boundary. HSTS is skipped in development: browsers remember it per host and localhost is shared with unrelated projects. Every other header applies locally, so a CSP violation surfaces while developing. For observability, browser error reports tunnel through the API rather than going to Sentry directly. Ad blockers block Sentry domains, which loses errors precisely for the users most likely to have browser oddities. The tunnel forwards only to the host derived from the configured DSN — a caller-supplied destination would turn an anonymous endpoint into a request-forgery primitive. Two consequences of the chosen options are recorded rather than left implicit: Enabling SendDefaultPii attaches request headers, and this application carries two standing credentials in them. Besides the refreshToken cookie, X-Master-Api-Key would have been sent to a third party on every error raised during a master/slave call. The scrub list removes the whole Cookie header, Authorization, X-Master-Api-Key and the request body. Console logging at Information plus structured logging to Sentry would, taken literally, mean one Sentry event per request — exhausting the free plan within hours and burying real errors in request noise. The thresholds are split: console keeps Information, Sentry takes warnings and above as events with Information as breadcrumbs, so every event arrives carrying the trail that led to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
5f3eda2680 |
Makes a redeploy safe for the key ring and the schema
Nothing here is visible in normal operation. Its whole purpose is that swapping the release directory on deploy cannot silently destroy state. Data Protection secures the API keys that authenticate master/slave communication. Two separate defaults would each have destroyed them: keys are held on the filesystem, which a release swap discards, and the application discriminator is derived from the content root path, which changes with every release directory — so even keys stored in a database would have stopped being derivable. Keys now live in ApplicationDbContext and the discriminator is a fixed constant. Losing them produces no error. It produces stored keys that no longer decrypt, which presents as an apparent network fault between a Master and its slaves and is easily misdiagnosed. That is also why the tests assert the resulting configuration rather than the registration: the XmlRepository must be the EF one and the discriminator must be the constant, plus a round-trip proving a value encrypted before a deploy is readable after one. A test that only checked "Data Protection is registered" would have passed in the broken case too. Both modules previously called AddDataProtection() themselves. Module registration runs after the host's, so those calls re-registered the configuration chain and would have overridden the persistent store while IDataProtector still resolved. They are removed, with a comment at each site — the deletion otherwise looks like a regression. Each module's own test project now guards against it being reintroduced. ApplicationDbContext also migrates itself at startup. Deploy targets offer no CLI, so migrations cannot be a manual step on the server. Failures are classified rather than treated alike: a connection failure means the database is not up yet, normal when the app and the database start together after a reboot, and is retried with backoff; a migration failure means something is broken and fails at once. Either way the process does not start, which is what makes the liveness health check trustworthy — an application that cannot reach its schema never answers /health, so monitoring goes red instead of reporting a healthy instance that cannot serve a request. The cost of migrating without a human gate is that migrations must stay forward-compatible and non-destructive, since rollback is "redeploy the previous release". The new migration is purely additive. Also wires this and the preceding hosting commit into both hosts, as they touch the same lines of Program.cs. Two constraints are enforced by documentation rather than code, and belong in the deployment instructions: the key table must never be pruned, and only one instance may migrate a given database at a time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |
||
|
|
29a93ef873 |
Separates website and admin roots, adds /health, hardens the availability gate
Prepares the single-host layout for deployment. The customer's public website moves from wwwroot/ to wwwroot/web/, so a CMS deploy can no longer overwrite content it does not own: with the website in its own directory, the release directory can be swapped without touching it. Each front-end gets its own file provider, and both tolerate a missing directory at startup — a fresh deployment has no website until a separate workspace deploys one, and the CMS must still serve /admin and the API. When the website's index.html is absent, an embedded placeholder is served instead of a 404, which also doubles as proof the CMS itself is running. The placeholder is embedded in the assembly rather than shipped into wwwroot/web/, because that directory is owned and overwritten by the website workspace. Adds GET /health for uptime monitoring. It reports infrastructure liveness only and is deliberately NOT the same thing as /api/v1/Availability/status or /api/v1/System/capabilities: those are CMS domain state that also serve the master/slave protocol. A healthy instance can be switched off by design, and a switched-on instance can be unhealthy, so conflating them would alert on business state and stay silent on real outages. /health is on the availability gate's bypass list for the same reason. Fixes a real defect in the gate's admin bypass. It parsed the bearer token with ReadJwtToken, which reads claims without verifying the signature, so an unauthenticated caller could forge an unsigned token carrying an Owner role claim and bypass the gate that suspends a customer's site. Protected endpoints still rejected them, so nothing leaked — but the gate itself was bypassable. The token is now fully validated against the same parameters as the bearer scheme, resolved from one shared source so the two cannot drift apart. Host wiring for these changes lands with the data-durability commit, since both units touch the same lines of Program.cs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw |