Skip to content

Deploy Fizzy and Super Productivity, with SP syncing to copyparty - #74

Merged
mathewcsims merged 3 commits into
mainfrom
deploy-fizzy-and-super-productivity
Aug 8, 2026
Merged

Deploy Fizzy and Super Productivity, with SP syncing to copyparty#74
mathewcsims merged 3 commits into
mainfrom
deploy-fizzy-and-super-productivity

Conversation

@mathewcsims

Copy link
Copy Markdown
Owner

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/--acam with no control over Access-Control-Allow-Headers, and SP sends Depth and If-Match, which aren't CORS-safelisted — preflight would fail, and SP has a dedicated PotentialCorsError for exactly that. Same-origin removes the problem rather than configuring around it.

Path is never rewritten (handle, not handle_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:

Finding
--daw Set as a volflag. Globally it would change overwrite semantics for the entire file server; without it every save invents a new filename and sync never converges
vague-403 Kept. The old note said remove it for any WebDAV volume — that assumed challenge-based clients. SP authenticates preemptively, and unauthenticated PROPFIND returns 401 not 404, measured
dav-port Not needed. --ua-nodav's Mozilla default governs GET-vs-HTML, not method dispatch — one fewer port and firewall rule
ETag copyparty returns none, and it doesn't matter: SP's parser does etag: lastModified

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.

Fizzy

  • Runs as uid 1000 → Thruster can't bind port 80 and crash-loops. Fixed with HTTP_PORT=8080, not by running as root
  • TLS_DOMAIN/DISABLE_SSL both unset — DISABLE_SSL would kill assume_ssl and force_ssl together, losing HSTS and secure cookies
  • Signups close permanently once the first account exists (upstream default) — MULTI_TENANT must never be set

Mail — first working SMTP in this repo

Uses the shared "Proton SMTP" Pass item. Field names don't line up (SMTP_SERVER/SMTP_TOKEN vs Rails' SMTP_ADDRESS/SMTP_PASSWORD), so compose maps them, keeping the item reusable. Credentials verified to authenticate against Proton without sending anything.

pass-deploy.sh now 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

  • Kuma monitors 38 + 39, both linked to Discord (a monitor with no notification row is silent). Both UP, integrity ok, nothing else down across the restart
  • Kopia: fizzy/storage and copyparty/sp-sync — the latter is the only durable copy of SP's tasks
  • dump-databases.sh dumps production.sqlite3 only; the other three are Solid Queue/Cache infrastructure
  • pf-lockdown extended 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 a pfctl reload

Also corrects one of my own errors

The claim I added to SETUP.md this morning — that curling 10.0.1.14:3923 from 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

mathewcsims and others added 3 commits August 8, 2026 14:22
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>
@mathewcsims
mathewcsims merged commit 6b9758f into main Aug 8, 2026
7 checks passed
@mathewcsims
mathewcsims deleted the deploy-fizzy-and-super-productivity branch August 8, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant