Files
recurring-checklist/deployment-guide.html
Waters ChurchandClaude Opus 5 469db5bc60 Bring the checklist app under version control
The project has lived only as a folder on the Mac Studio and, briefly, as a
transfer tarball. This is the first commit: the Express server and its web
front end, the Capacitor iOS and Android wrappers, and the release and backup
scripts.

The repo is the source, not a backup of the machine. Left out on purpose, by
the .gitignore files that were already in the tree: node_modules, build output,
mobile/www (regenerated from server/public on every build), and server/data,
which holds the live checklists and belongs on the NAS rather than in git.

Every credential is excluded too — server/.env, the Play upload keystore and
its properties file, the App Store Connect key and ios-signing.config. The one
secret that was not in an ignored file was the NAS SSH password, hard-coded in
build-deploy.sh and grepped back out of it by backup-to-drive.sh. Both now read
it from the login keychain (service checklist-nas, account Christian), falling
back to NAS_PASS in the environment, so rotating it is one keychain entry
rather than a history rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 13:00:57 -04:00

449 lines
20 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Recurring Checklist — Deployment & Maintenance Guide</title>
<style>
:root {
--ink: #1a1a1a;
--muted: #555;
--accent: #2c5d9c;
--accent-2: #f1c40f;
--bg: #ffffff;
--code-bg: #f5f7fa;
--rule: #d9dde3;
}
* { box-sizing: border-box; }
html, body { background: var(--bg); color: var(--ink); }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.55;
font-size: 12pt;
margin: 0;
padding: 24pt 28pt;
max-width: 7.5in;
}
h1 {
font-size: 24pt;
margin: 0 0 4pt;
color: var(--ink);
}
h1 + .subtitle {
color: var(--muted);
margin: 0 0 18pt;
font-size: 12pt;
}
h2 {
font-size: 16pt;
margin: 28pt 0 8pt;
padding-bottom: 4pt;
border-bottom: 2px solid var(--accent);
color: var(--accent);
page-break-after: avoid;
}
h3 {
font-size: 13pt;
margin: 18pt 0 6pt;
color: var(--ink);
page-break-after: avoid;
}
h4 {
font-size: 12pt;
margin: 14pt 0 4pt;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
}
p, ul, ol, table { margin: 6pt 0; }
ul, ol { padding-left: 22pt; }
li { margin-bottom: 3pt; }
code, pre {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 10.5pt;
}
code {
background: var(--code-bg);
padding: 1pt 5pt;
border-radius: 3pt;
border: 1px solid var(--rule);
}
pre {
background: var(--code-bg);
border: 1px solid var(--rule);
border-radius: 4pt;
padding: 10pt 12pt;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
pre code { background: transparent; border: none; padding: 0; }
table {
border-collapse: collapse;
width: 100%;
margin: 8pt 0;
}
th, td {
border: 1px solid var(--rule);
padding: 6pt 9pt;
text-align: left;
vertical-align: top;
font-size: 11pt;
}
th {
background: var(--code-bg);
font-weight: 600;
}
hr {
border: none;
border-top: 1px solid var(--rule);
margin: 18pt 0;
}
.callout {
border-left: 4px solid var(--accent-2);
background: #fff8db;
padding: 8pt 12pt;
margin: 10pt 0;
border-radius: 0 4pt 4pt 0;
}
.callout.note { border-left-color: var(--accent); background: #eaf2fb; }
.callout.warn { border-left-color: #d04a4a; background: #fcecec; }
.callout strong { display: block; margin-bottom: 4pt; }
.toc {
background: var(--code-bg);
border: 1px solid var(--rule);
border-radius: 6pt;
padding: 12pt 18pt;
margin: 12pt 0 24pt;
}
.toc ol { margin: 0; padding-left: 18pt; }
.toc li { margin-bottom: 2pt; }
.toc a { color: var(--ink); text-decoration: none; }
.kicker {
display: inline-block;
background: var(--accent);
color: white;
padding: 2pt 8pt;
font-size: 10pt;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
border-radius: 3pt;
margin-bottom: 8pt;
}
.small { font-size: 10pt; color: var(--muted); }
@media print {
body { padding: 0.6in 0.6in; max-width: none; }
h2 { page-break-after: avoid; }
h3 { page-break-after: avoid; }
pre, table, .callout { page-break-inside: avoid; }
}
</style>
</head>
<body>
<span class="kicker">Internal documentation</span>
<h1>Recurring Checklist</h1>
<p class="subtitle">Deployment &amp; maintenance guide — self-hosted web app on Synology DSM.</p>
<div class="toc">
<strong>Contents</strong>
<ol>
<li><a href="#overview">Overview</a></li>
<li><a href="#environment">Environment summary</a></li>
<li><a href="#deploy">Initial deployment</a></li>
<li><a href="#updates">Pushing app updates</a></li>
<li><a href="#backup">Backups</a></li>
<li><a href="#dns">DDNS, reverse proxy &amp; HTTPS</a></li>
<li><a href="#troubleshoot">Troubleshooting</a></li>
<li><a href="#references">Reference</a></li>
</ol>
</div>
<h2 id="overview">1. Overview</h2>
<p>
The Recurring Checklist is a small Node.js web application that lives in a Docker container on the
Synology NAS. Multi-location operators (Woonsocket, North Attleboro, etc.) open it in a web browser,
identify themselves once per device, and tick items off through the week. Every Sunday at 3:00 PM the
server emails a digest of who completed what (and what was missed) to a single recipient, then resets
all checkboxes for the following week.
</p>
<h4>Architecture</h4>
<pre><code>Users (LAN or internet)
│
▼
https://check.watersmedia.org (Boba-Fett DSM, port 443)
│
│ reverse proxy
▼
http://10.1.1.1:3000 (Chronicles-NA DSM, Docker container)
│
▼
node:20-alpine + Express + node-cron + nodemailer
│
├── data volume → /volume1/docker/recurring-checklist/data/checklists.json
│ (backed up nightly to Google Drive via Hyper Backup)
│
└── Sunday 3:00 PM cron → Gmail SMTP → media@waterschurch.org</code></pre>
<h2 id="environment">2. Environment summary</h2>
<table>
<tr><th>Item</th><th>Value</th></tr>
<tr><td>App URL (external)</td><td><code>https://check.watersmedia.org</code></td></tr>
<tr><td>App URL (LAN, direct)</td><td><code>http://10.1.1.1:3000</code></td></tr>
<tr><td>Public TLS host</td><td>Boba-Fett (10.1.1.4), DSM reverse proxy + Let's Encrypt cert</td></tr>
<tr><td>Container host</td><td>Chronicles-NA (10.1.1.1), Container Manager project <code>recurring-checklist</code></td></tr>
<tr><td>Project folder on NAS</td><td><code>/volume1/docker/recurring-checklist/</code></td></tr>
<tr><td>Persisted data file</td><td><code>/volume1/docker/recurring-checklist/data/checklists.json</code></td></tr>
<tr><td>Backups</td><td>Hyper Backup → Google Drive (<code>recurring-checklist_server-backup/Chronicles-NA_1</code>), daily 23:30</td></tr>
<tr><td>Email digest schedule</td><td>Sunday 15:00 America/New_York</td></tr>
<tr><td>Email recipient</td><td><code>media@waterschurch.org</code></td></tr>
<tr><td>SMTP relay</td><td><code>smtp.gmail.com:587</code> (Google Workspace app password)</td></tr>
<tr><td>Admin password</td><td>Set in <code>.env</code> as <code>ADMIN_PASSWORD</code></td></tr>
</table>
<h2 id="deploy">3. Initial deployment</h2>
<p>The container was created using the following procedure. Use this section as the disaster-recovery /
rebuild reference.</p>
<h3>3.1 Prerequisites</h3>
<ul>
<li>Synology DSM 7.2 or newer on the host NAS (Chronicles-NA)</li>
<li>Container Manager package installed</li>
<li>SSH or File Station access to <code>/volume1/docker/</code></li>
<li>Google Workspace account with an App Password (for the digest sender)</li>
<li>The deployment tarball: <code>checklist-deploy.tgz</code> (produced from the project's <code>server/</code> directory)</li>
</ul>
<h3>3.2 Upload the bundle</h3>
<ol>
<li>Open File Station on the NAS.</li>
<li>Inside <code>/volume1/docker/</code>, create a folder named <code>recurring-checklist</code>.</li>
<li>Upload <code>checklist-deploy.tgz</code> into it.</li>
<li>Right-click → Extract → Extract here. Repeat once on the resulting <code>.tar</code> (Synology's
File Station unwraps in two passes).</li>
<li>Delete the <code>.tgz</code>, <code>.tar</code>, and the <code>PaxHeader</code> folder that gets produced. The
folder should be left with:
<pre><code>server.js public/ Dockerfile
package.json data/ docker-compose.yml
package-lock.json README.md .env .env.example .dockerignore .gitignore</code></pre>
</li>
</ol>
<h3>3.3 Configure environment</h3>
<p>The deployment ships with a working <code>.env</code> file. Verify the following keys are populated:</p>
<table>
<tr><th>Variable</th><th>Purpose</th></tr>
<tr><td><code>ADMIN_PASSWORD</code></td><td>Required for any list-management operation in the UI.</td></tr>
<tr><td><code>REPORT_EMAIL</code></td><td>Where the Sunday digest is delivered.</td></tr>
<tr><td><code>REPORT_CRON</code></td><td>cron schedule for the digest. Default <code>0 15 * * 0</code> (Sun 3:00 PM).</td></tr>
<tr><td><code>REPORT_TZ</code></td><td>IANA timezone for the cron (default <code>America/New_York</code>).</td></tr>
<tr><td><code>RESET_AFTER_DIGEST</code></td><td><code>true</code> = clear all checks once the digest is sent. <code>false</code> to disable.</td></tr>
<tr><td><code>SMTP_HOST</code></td><td><code>smtp.gmail.com</code></td></tr>
<tr><td><code>SMTP_PORT</code></td><td><code>587</code></td></tr>
<tr><td><code>SMTP_USER</code></td><td>Gmail / Google Workspace address that sends the digest.</td></tr>
<tr><td><code>SMTP_PASS</code></td><td>16-character app password from <code>myaccount.google.com/apppasswords</code>.</td></tr>
<tr><td><code>SMTP_FROM</code></td><td>Friendly <code>From:</code> header, e.g. <code>Recurring Checklist &lt;media@waterschurch.org&gt;</code></td></tr>
</table>
<div class="callout warn">
<strong>Security note</strong>
The <code>.env</code> file contains credentials. Do not place it in a publicly-shared folder. Hyper
Backup is encrypted in transit; client-side encryption is not enabled because it complicates restore.
</div>
<h3>3.4 Create the Container Manager project</h3>
<ol>
<li>Open Container Manager → <strong>Project</strong> → <strong>Create</strong>.</li>
<li><strong>Project name:</strong> <code>recurring-checklist</code></li>
<li><strong>Path:</strong> browse to <code>/volume1/docker/recurring-checklist</code> → Select.</li>
<li><strong>Source:</strong> "Use existing docker-compose.yml" (auto-selected). Click <strong>OK</strong>.</li>
<li>On the <em>Web portal settings</em> screen, leave the checkbox unchecked. Click <strong>Next</strong>.</li>
<li>Confirm the summary and click <strong>Done</strong>.</li>
</ol>
<p>Container Manager will build the image (~60–90 seconds on first run) and start the container. The
project should report <code>Exit Code: 0</code> at the end of the build log and the project status indicator
should turn green.</p>
<h3>3.5 Verify</h3>
<ol>
<li>From a browser on the LAN, open <code>http://10.1.1.1:3000</code>. The "Who are you?" prompt should appear.</li>
<li>Click <strong>Admin</strong> (top right) and authenticate with the value of <code>ADMIN_PASSWORD</code>.</li>
<li>Use <strong>Send Digest Now</strong> in the admin drawer to verify SMTP connectivity. A test email should arrive within a few seconds.</li>
</ol>
<h2 id="updates">4. Pushing app updates</h2>
<p>Updates are delivered as a refreshed deployment tarball. Application data is mounted from the host's
<code>data/</code> directory, so rebuilding the image preserves all checklists, locations, and check states.</p>
<h3>4.1 Receive the update</h3>
<p>The developer produces a fresh <code>checklist-deploy.tgz</code> from the latest project tree. It will
arrive as a single file (currently ~35 KB).</p>
<h3>4.2 Apply on the NAS</h3>
<ol>
<li>Open File Station → navigate to <code>/volume1/docker/recurring-checklist/</code>.</li>
<li>Upload the new <code>checklist-deploy.tgz</code> into this folder.</li>
<li>Right-click → Extract → Extract here. Choose <strong>Overwrite</strong> when prompted (this replaces
<code>server.js</code>, the <code>public/</code> folder, etc.; it does <em>not</em> touch <code>data/</code>).</li>
<li>Repeat the extract on the resulting <code>.tar</code>.</li>
<li>Delete the <code>.tgz</code>, <code>.tar</code>, and <code>PaxHeader</code> when done.</li>
</ol>
<h3>4.3 Rebuild the container</h3>
<ol>
<li>Open Container Manager → <strong>Project</strong> → click the <code>recurring-checklist</code> row.</li>
<li>Click <strong>Action</strong> → <strong>Build</strong> (rebuilds the image from the updated source).</li>
<li>Wait for the build log to finish with <code>Exit Code: 0</code>.</li>
<li>Click <strong>Action</strong> → <strong>Stop</strong>, then <strong>Action</strong> → <strong>Start</strong> if the build did not auto-restart.</li>
</ol>
<div class="callout note">
<strong>What gets preserved across updates</strong>
Everything in <code>./data/</code> is volume-mounted. Lists, items, per-location check states, and
<code>lastCheckDate</code> values are persisted. Configuration in <code>.env</code> is also untouched by an update unless
the new tarball explicitly ships a replacement.
</div>
<h3>4.4 Rollback</h3>
<p>If an update misbehaves:</p>
<ol>
<li>Recover the previous <code>checklist-deploy.tgz</code> (kept by the developer) or restore from Hyper Backup.</li>
<li>Replace the files in <code>/volume1/docker/recurring-checklist/</code> as in §4.2.</li>
<li>Rebuild the project as in §4.3.</li>
</ol>
<h2 id="backup">5. Backups</h2>
<h3>5.1 What's backed up</h3>
<p>A Hyper Backup task named <strong>Recurring Checklist</strong> backs up the entire <code>docker</code>
shared folder nightly at 23:30 to Google Drive. Destination:</p>
<pre><code>Google Drive: recurring-checklist_server-backup / Chronicles-NA_1</code></pre>
<p>This captures both the application source (in <code>recurring-checklist/</code>) and its data folder.</p>
<h3>5.2 Restoring</h3>
<p>To restore the data file only:</p>
<ol>
<li>Open Hyper Backup → Recurring Checklist task → <strong>Restore</strong> → <strong>Data</strong>.</li>
<li>Pick a version. Browse into
<code>recurring-checklist/data/</code> and restore <code>checklists.json</code>.</li>
<li>Container Manager → Project → Stop, then Start the <code>recurring-checklist</code> project to pick up the restored file.</li>
</ol>
<h3>5.3 Backup verification</h3>
<p>An integrity check runs weekly on Saturday at 01:30. Failed integrity checks generate a DSM notification.</p>
<h2 id="dns">6. DDNS, reverse proxy &amp; HTTPS</h2>
<h3>6.1 DDNS record</h3>
<ul>
<li><strong>Hostname:</strong> <code>check.watersmedia.org</code></li>
<li><strong>Provider:</strong> Synology (free)</li>
<li><strong>Account:</strong> Registered to the Synology account on Chronicles-NA.</li>
</ul>
<h3>6.2 Reverse proxy rule (on Boba-Fett, 10.1.1.4)</h3>
<p>Located at <em>Control Panel → Login Portal → Advanced → Reverse Proxy → "Recurring Checklist"</em>.</p>
<table>
<tr><th>Side</th><th>Protocol</th><th>Hostname</th><th>Port</th></tr>
<tr><td>Source</td><td>HTTPS</td><td><code>check.watersmedia.org</code></td><td>443</td></tr>
<tr><td>Destination</td><td>HTTP</td><td><code>10.1.1.1</code></td><td>3000</td></tr>
</table>
<h3>6.3 TLS certificate</h3>
<ul>
<li><strong>Type:</strong> Let's Encrypt</li>
<li><strong>Bound to:</strong> the <code>check.watersmedia.org</code> service in Control Panel → Security → Certificate → Settings</li>
<li><strong>Auto-renewal:</strong> handled by DSM. The cert renews ~30 days before expiry as long as
ports 80/443 inbound continue to reach Boba-Fett.</li>
</ul>
<h3>6.4 Router (UniFi)</h3>
<p>Existing port-forward of TCP 443 to Boba-Fett (10.1.1.4) is the only inbound rule required. No
new forwarding is needed for the Recurring Checklist app.</p>
<h2 id="troubleshoot">7. Troubleshooting</h2>
<table>
<tr><th>Symptom</th><th>Likely cause &amp; fix</th></tr>
<tr>
<td>App returns 502 / Bad Gateway from <code>check.watersmedia.org</code></td>
<td>The Docker container on Chronicles-NA is stopped. Open Container Manager → Project → click
<code>recurring-checklist</code> → <strong>Action → Start</strong>. If it fails to start, open
the build log for errors (commonly a malformed <code>.env</code>).</td>
</tr>
<tr>
<td>Browser shows "Not Secure" on the public URL</td>
<td>Certificate either expired or not bound. Check Control Panel → Security → Certificate on Boba-Fett.
The <code>check.watersmedia.org</code> cert should be green and bound under
<strong>Settings</strong>.</td>
</tr>
<tr>
<td>Sunday email did not arrive</td>
<td>Open Container Manager → Container → <code>recurring-checklist</code> → <strong>Details → Log</strong>.
Look for lines beginning with <code>[digest]</code>. Common causes: expired Google App Password,
Google Workspace 2FA disabled on the sending account, outbound TCP 587 blocked by firewall.</td>
</tr>
<tr>
<td>"This list is reserved for X" error</td>
<td>Expected. The list is location-scoped; a user signed in as a different location cannot tick it
off. Either change the user's location to match the list or remove the list's location to make it
shared.</td>
</tr>
<tr>
<td>Checks reappear after a user signs out and back in</td>
<td>Per-location checks are stored on the server keyed by the user's typed name (case-insensitive,
whitespace-collapsed). If a user enters a different name, they are treated as a different
location and see different state.</td>
</tr>
<tr>
<td>Container won't build</td>
<td>Usually a stale or partial extraction. Re-upload <code>checklist-deploy.tgz</code> and extract
both <code>.tgz</code> and the intermediate <code>.tar</code> step. Confirm <code>server.js</code> and the <code>public/</code> folder
are present at the project root.</td>
</tr>
</table>
<h2 id="references">8. Reference</h2>
<h3>8.1 Files in <code>/volume1/docker/recurring-checklist/</code></h3>
<table>
<tr><th>Path</th><th>Purpose</th></tr>
<tr><td><code>server.js</code></td><td>Express app + scheduler + email logic.</td></tr>
<tr><td><code>public/</code></td><td>Static front-end (HTML, CSS, JS).</td></tr>
<tr><td><code>Dockerfile</code></td><td>Image definition (node:20-alpine).</td></tr>
<tr><td><code>docker-compose.yml</code></td><td>Container Manager project definition.</td></tr>
<tr><td><code>package.json</code> / <code>package-lock.json</code></td><td>npm manifest, locked dependency versions.</td></tr>
<tr><td><code>.env</code></td><td>Runtime config (passwords, SMTP, schedule). Not in version control.</td></tr>
<tr><td><code>.env.example</code></td><td>Template for a fresh deploy.</td></tr>
<tr><td><code>data/checklists.json</code></td><td>Persistent state. Volume-mounted; backed up nightly.</td></tr>
</table>
<h3>8.2 Useful container actions</h3>
<table>
<tr><th>Action</th><th>Where</th></tr>
<tr><td>View live logs</td><td>Container Manager → Container → <code>recurring-checklist</code> → Details → Log</td></tr>
<tr><td>Restart container</td><td>Container Manager → Project → recurring-checklist → Action → Stop, then Start</td></tr>
<tr><td>Rebuild image after update</td><td>Container Manager → Project → recurring-checklist → Action → Build</td></tr>
<tr><td>Edit env vars</td><td>Edit <code>/volume1/docker/recurring-checklist/.env</code> via File Station, then restart the project</td></tr>
</table>
<h3>8.3 Server endpoints (for diagnostics)</h3>
<table>
<tr><th>Method</th><th>Path</th><th>Purpose</th></tr>
<tr><td>GET</td><td><code>/api/state</code></td><td>Returns current lists, items, and per-location check map (read-only).</td></tr>
<tr><td>POST</td><td><code>/api/admin/verify</code></td><td>Validates the admin password (used by the UI).</td></tr>
<tr><td>POST</td><td><code>/api/admin/state</code></td><td>Replaces full state. Requires <code>X-Admin-Password</code>.</td></tr>
<tr><td>POST</td><td><code>/api/admin/send-digest</code></td><td>Manually triggers the email digest. Requires admin.</td></tr>
<tr><td>POST</td><td><code>/api/check</code></td><td>Toggle a single item's checked state for a location.</td></tr>
</table>
<hr>
<p class="small">
Document version 1 · Generated for the Recurring Checklist deployment on Chronicles-NA / Boba-Fett.
Maintained alongside the project source.
</p>
</body>
</html>