Deploy Fizzy and Super Productivity, with SP syncing to copyparty - #74
Merged
Conversation
The three-tool task setup, minus Tasks.org which is phone-side and set up by
hand. Fizzy is the backlog (someday/now/done/never); Super Productivity is
day-to-day detailed tasks; both LAN/tailnet-gated at Caddy.
## Super Productivity syncs same-origin, deliberately
SP is local-first and syncs FROM THE BROWSER, so its WebDAV target shares
its hostname: sp.mathewcsims.uk/sp-sync/ routes to copyparty. copyparty
exposes only --acao/--acam with no control over Access-Control-Allow-Headers,
and SP sends Depth and If-Match, which are not CORS-safelisted — preflight
would fail, and SP has a dedicated PotentialCorsError for exactly that.
Same-origin removes the problem rather than configuring around it. The path
is never rewritten (handle, not handle_path) because copyparty puts absolute
paths in PROPFIND <D:href> and the client resolves against them.
Verified in a throwaway container and then end-to-end through Caddy, with a
browser user-agent and SP's exact method set: PROPFIND 207, MKCOL 201, PUT
201, overwrite 201, GET, DELETE 200; hrefs correct; unauthenticated and
wrong-password both 401.
Four things checked in source rather than assumed:
* --daw is a VOLFLAG, not global. Without it a PUT over an existing file
invents a new filename and sync never converges; set globally it would
change overwrite semantics for the entire file server. Needed because SP
does not send x-oc-mtime.
* vague-403 was KEPT. The old note said to remove it the moment a WebDAV
volume returned; that assumed challenge-based clients. SP authenticates
preemptively, and an unauthenticated PROPFIND returns 401 not 404 —
measured, which is the assertion the note actually demanded.
* No dav-port needed. --ua-nodav's Mozilla default governs GET-vs-HTML,
not method dispatch — one fewer published port and firewall rule.
* copyparty returns no ETag, and it does not matter: SP's own parser does
`etag: lastModified`, which copyparty always returns.
THE BIND MOUNT IS LOAD-BEARING. A [/sp-sync] volume with no matching mount
still "works" — copyparty logs type=overlay and writes to the container's
ephemeral layer, losing every task on the next recreation, silently. Caught
by reading that log line rather than trusting the volume block.
## Fizzy
Runs as uid 1000, so Thruster cannot bind port 80 and crash-loops with
"permission denied" — upstream's own example compose publishes 80/443 and
works only because plain Docker grants the capability. Fixed with
HTTP_PORT=8080 rather than running the container as root.
TLS_DOMAIN and DISABLE_SSL both left unset: DISABLE_SSL would switch off
assume_ssl AND force_ssl together, losing HSTS and secure cookies. Unset is
Rails' documented pattern behind a terminating proxy.
Registration hardening is upstream's default — signups close permanently
once the first account exists — so MULTI_TENANT must never be set.
## Mail — the first working SMTP in this repo
Fizzy needs it: sign-in is a magic link or 6-character code, and the
alternative is reading codes out of the container log forever. Uses the
shared "Proton SMTP" Pass item (self-hosted@mathewcsims.uk,
smtp.protonmail.ch:587 STARTTLS). Field names do not line up —
SMTP_SERVER/SMTP_TOKEN vs Rails' SMTP_ADDRESS/SMTP_PASSWORD — so compose maps
them, keeping the Pass item reusable by other apps. Credentials verified to
authenticate against Proton without sending anything.
pass-deploy.sh now accepts multiple item titles and merges them, which is
what makes that possible: the agent Pass token cannot UPDATE an item, so a
secret added later must live in its own item. Strictly additive — the
default and explicit-single-item paths were both retested, and a bad item
aborts with exit 1 before the eval rather than deploying with blanks.
The copyparty spsync account uses the same trick: its own Pass item rendered
to a second accounts file, because copyparty merges [accounts] across
auto-loaded *.conf files (verified in a throwaway container first).
## Monitoring, backups, firewall
* Uptime Kuma monitors 38 (Fizzy, /up) and 39 (Super Productivity, keyword)
added via the documented DB procedure, both linked to Discord — a monitor
with no notification row is monitored but silent. Both UP, integrity ok,
nothing else went down across the restart.
* Kopia sources: fizzy/storage and copyparty/sp-sync, initial snapshots
created. sp-sync is the ONLY durable copy of SP's tasks — everything else
is browser IndexedDB.
* dump-databases.sh dumps fizzy's production.sqlite3 only; the other three
files are Solid Cable/Cache/Queue infrastructure, worthless in a restore.
* pf-lockdown extended to 3600 and 3601. 3601 matters most: Super
Productivity has NO LOGIN AT ALL, so the LAN gate and this rule are its
entire access control. NEEDS A pfctl RELOAD to take effect.
Also corrects a claim I added to SETUP.md earlier today: curling
10.0.1.14:3923 from the Mac IS a valid negative test — it is blocked.
Measured this time (3923 blocked; 2368, 5230 and 3000, which pf does not
cover, all answer), which isolates pf rather than a podman loopback quirk.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Feedback on the handover: the instruction given for configuring SP's WebDAV sync was one line, for an app he had never used, from an agent that had already read the form definition and knew every field. Replaced with a numbered procedure using the real labels — including that "Server URL" and "App Password" are labelled for Nextcloud because the form is shared, which is exactly the kind of thing a one-liner leaves you guessing at. Also records two things found while writing it up. BROWSING /sp-sync/ IN A BROWSER LOOKS BROKEN, AND IS NOT. Super Productivity is a PWA whose service worker is registered at scope `/`, and its ngsw.json treats any path without a dot in the last segment as a navigation to serve the app shell for. `/sp-sync/` qualifies, so the shell loads and then requests chunk-*.js, main-*.js, manifest.json and fonts relative to /sp-sync/, which copyparty 404s. Found in Caddy's access log, not reported by anything. The sync itself is unaffected, and that was checked rather than assumed: sync-data.json contains a dot so it is excluded from navigation handling, and the only dataGroup (api-freshness) matches api.github.com, gitlab.com and *.atlassian.net — nothing under /sp-sync/ is cached or intercepted. Browse the files via cp.mathewcsims.uk instead. Note curl returns 200 for both, because curl has no service worker; only a real browser hits this, which is why it needed writing down. ENCRYPTION IS OFF AND SHOULD BE A DECISION. The synced blob is plaintext — a `pf_2__` prefix then readable JSON of every task. Left off because it already sits behind a LAN gate, a volume-scoped account and full-disk encryption, and Kopia encrypts it again before Backblaze, while a lost passphrase loses every task with no server-side recovery. Recorded so it is a choice rather than a default nobody looked at. Sync verified working end to end: sync-data.json, 31 KB, current timestamp. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caught reviewing this PR: ports 3600 and 3601 were NOT firewalled, despite the repo, the commit message and SETUP.md all saying they were. Measured — the Mac reaches 10.0.1.14:3601 with a 200. Cause is mine. reload.sh only ever reloaded /etc/pf.conf; the anchor itself had to be copied to /etc/pf.anchors/ separately. I included that `cp` in the instructions for the Vikunja port change earlier today and omitted it this time, giving only the reload and a `grep -c 3601` check framed as "expect 1" with no statement of what a 0 meant. Precisely the insufficient-detail failure raised one message earlier, repeated inside the same task. Fixing the instance would leave the trap. reload.sh now compares the live anchor with the repo copy alongside it and installs it when they differ, so editing the repo and running the script does what everyone already assumed it did. The repo becomes the source of truth exactly as it is for the Pi's Caddyfile, and a hand-edit of the live file is overwritten on the next run — the intended direction. Worth noting why this hid so well: the reload succeeds and reports success whether or not the anchor is current, so nothing anywhere contradicted the belief that the rules were live. That is the same shape as the pf.conf wipe, the empty-anchor check and the zero-container probe — something reporting success while the thing it is responsible for is not in effect. Fourth instance today; the script now self-heals both halves it depends on. Also verified while reviewing: both new containers run the exact pinned digests (podman's ImageName reports the tag alias, .Image resolves to the pinned digest for both), the Pi's Caddyfile matches the repo, and no existing caller of pass-deploy.sh passes extra arguments that its new multi-item handling would misread. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The three-tool task setup, minus Tasks.org (phone-side, set up by hand). Fizzy is the backlog; Super Productivity is day-to-day detailed tasks. Both LAN/tailnet-gated.
Super Productivity syncs same-origin, deliberately
SP is local-first and syncs from the browser, so its WebDAV target shares its hostname:
sp.mathewcsims.uk/sp-sync/routes to copyparty. copyparty exposes only--acao/--acamwith no control overAccess-Control-Allow-Headers, and SP sendsDepthandIf-Match, which aren't CORS-safelisted — preflight would fail, and SP has a dedicatedPotentialCorsErrorfor exactly that. Same-origin removes the problem rather than configuring around it.Path is never rewritten (
handle, nothandle_path) because copyparty puts absolute paths in PROPFIND<D:href>.Verified end-to-end through Caddy with a browser UA and SP's exact method set: PROPFIND 207 · MKCOL 201 · PUT 201 · overwrite 201 · GET · DELETE 200 · hrefs correct · unauthenticated and wrong-password both 401.
Four things checked in source, not assumed:
--dawvague-403dav-port--ua-nodav's Mozilla default governs GET-vs-HTML, not method dispatch — one fewer port and firewall ruleETagetag: lastModifiedThe bind mount is load-bearing. A
[/sp-sync]volume with no matching mount still "works" — copyparty logstype=overlayand writes to the container's ephemeral layer, losing every task on the next recreation, silently. Caught by reading that log line.Fizzy
HTTP_PORT=8080, not by running as rootTLS_DOMAIN/DISABLE_SSLboth unset —DISABLE_SSLwould killassume_sslandforce_ssltogether, losing HSTS and secure cookiesMULTI_TENANTmust never be setMail — first working SMTP in this repo
Uses the shared "Proton SMTP" Pass item. Field names don't line up (
SMTP_SERVER/SMTP_TOKENvs Rails'SMTP_ADDRESS/SMTP_PASSWORD), so compose maps them, keeping the item reusable. Credentials verified to authenticate against Proton without sending anything.pass-deploy.shnow merges multiple Pass items — needed because the agent token can't UPDATE an item, so later secrets need their own. Strictly additive: default and explicit-single paths retested, and a bad item exits 1 before the eval rather than deploying with blanks.Monitoring, backups, firewall
fizzy/storageandcopyparty/sp-sync— the latter is the only durable copy of SP's tasksdump-databases.shdumpsproduction.sqlite3only; the other three are Solid Queue/Cache infrastructurepf-lockdownextended to 3600/3601. 3601 matters most — SP has no login at all, so the LAN gate and this rule are its entire access control. Needs apfctlreloadAlso corrects one of my own errors
The claim I added to SETUP.md this morning — that curling
10.0.1.14:3923from the Mac bypasses the filter — is wrong. It's blocked. Measured: 3923 blocked, while 2368/5230/3000 (not in pf) all answer, isolating pf rather than a podman quirk.🤖 Generated with Claude Code