Files
recurring-checklist/DEPLOYMENT_GUIDE.html
T
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

325 lines
20 KiB
HTML
Raw 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 Guide</title>
<style>
@page { margin: 0.7in; }
html, body {
font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
color: #1a1a1a;
line-height: 1.55;
font-size: 11pt;
background: #fff;
margin: 0;
padding: 32px 40px 56px;
max-width: 820px;
}
h1, h2, h3, h4 { color: #1f2937; line-height: 1.25; }
h1 { font-size: 26pt; margin: 0 0 4px; }
h2 { font-size: 16pt; margin: 28px 0 8px; padding-bottom: 4px; border-bottom: 2px solid #3498db; }
h3 { font-size: 13pt; margin: 18px 0 6px; color: #2c3e50; }
h4 { font-size: 11pt; margin: 12px 0 4px; color: #2c3e50; }
p { margin: 6px 0 8px; }
ul, ol { margin: 6px 0 10px 22px; padding: 0; }
li { margin: 3px 0; }
code {
background: #f1f3f5;
padding: 1px 5px;
border-radius: 3px;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 10pt;
color: #c7254e;
}
pre {
background: #1f2937;
color: #f8f9fa;
padding: 12px 14px;
border-radius: 6px;
overflow-x: auto;
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 9.5pt;
line-height: 1.4;
page-break-inside: avoid;
}
pre code { background: transparent; color: inherit; padding: 0; font-size: inherit; }
table { width: 100%; border-collapse: collapse; margin: 8px 0 14px; font-size: 10pt; }
th, td { border: 1px solid #d1d5db; padding: 6px 9px; text-align: left; vertical-align: top; }
th { background: #f3f4f6; font-weight: 600; }
.lede { color: #4b5563; font-size: 12pt; margin: 4px 0 14px; }
.meta { color: #6b7280; font-size: 9pt; margin-top: 2px; }
.callout {
border-left: 4px solid #3498db;
background: #ecf6fd;
padding: 8px 12px;
margin: 10px 0 12px;
border-radius: 0 4px 4px 0;
font-size: 10pt;
}
.callout.warn { border-color: #f39c12; background: #fff7e6; }
.callout.danger { border-color: #e74c3c; background: #fdecea; }
.callout strong { display: block; margin-bottom: 2px; }
hr { border: 0; border-top: 1px solid #e5e7eb; margin: 28px 0; }
.toc { background: #f9fafb; border: 1px solid #e5e7eb; padding: 14px 18px; border-radius: 6px; margin: 16px 0 24px; }
.toc h3 { margin-top: 0; }
.toc ol { margin: 6px 0 0 22px; }
section { page-break-inside: auto; }
h2 { page-break-after: avoid; }
pre, table, .callout { page-break-inside: avoid; }
.footer { margin-top: 36px; padding-top: 12px; border-top: 1px solid #e5e7eb; color: #6b7280; font-size: 9pt; }
</style>
</head>
<body>
<h1>Recurring Checklist</h1>
<p class="lede">Self-hosted deployment guide — Synology + Docker + Gmail SMTP</p>
<p class="meta">Document version 1 · Generated for IT handoff · <em>Edit any value in this document to match your environment.</em></p>
<div class="toc">
<h3>Contents</h3>
<ol>
<li>What this is &amp; what was deployed</li>
<li>Network architecture</li>
<li>Deployment steps (already completed)</li>
<li>How users access it</li>
<li>How admin manages it</li>
<li>Weekly email digest</li>
<li>Backups (Hyper Backup → Google Drive)</li>
<li>Updating the app code</li>
<li>Troubleshooting / common ops</li>
<li>Credentials &amp; references</li>
</ol>
</div>
<hr/>
<h2>1. What this is</h2>
<p>A small Node.js web application running in a Docker container on the Synology NAS named <strong>Chronicles-NA</strong>. It hosts a multi-location checklist that staff complete each Sunday for startup and shutdown procedures. Every Sunday at 3:00 PM the server emails a single digest to <code>media@waterschurch.org</code> summarising what each location checked off and what they missed, then resets the list for the next week.</p>
<h3>Components</h3>
<table>
<tr><th>Piece</th><th>Where</th><th>Purpose</th></tr>
<tr><td>Web app (Node.js + Express)</td><td>Container on Chronicles-NA, port 3000</td><td>Serves the UI and stores check states</td></tr>
<tr><td>JSON state file</td><td><code>/volume1/docker/recurring-checklist/data/checklists.json</code></td><td>Source of truth for lists, items, and per-location checks</td></tr>
<tr><td>Reverse proxy</td><td>Boba-Fett (DSM → Control Panel → Login Portal → Advanced → Reverse Proxy)</td><td>Terminates HTTPS for the friendly URL and forwards to Chronicles-NA</td></tr>
<tr><td>Let’s Encrypt certificate</td><td>Boba-Fett (DSM → Control Panel → Security → Certificate)</td><td>Issued for <code>check.watersmedia.org</code>; auto-renews every 90 days</td></tr>
<tr><td>Scheduler (node-cron)</td><td>Inside the container</td><td>Fires <code>0 15 * * 0</code> in <code>America/New_York</code> to send the digest and reset state</td></tr>
<tr><td>Hyper Backup task</td><td>Chronicles-NA → Google Drive</td><td>Daily 23:30 backup of the <code>docker</code> shared folder</td></tr>
</table>
<hr/>
<h2>2. Network architecture</h2>
<pre>
Internet
│
▼ (https://check.watersmedia.org → public IP)
Router ─ port 443 forwarded to ─► Boba-Fett (10.1.1.4)
│
│ Reverse-proxy rule:
│ check.watersmedia.org
│ → http://10.1.1.1:3000
▼
Chronicles-NA (10.1.1.1)
│
│ Docker container "recurring-checklist"
│ listens on :3000
▼
Node.js / Express
│
├─► /volume1/docker/recurring-checklist/data/ (state file)
└─► SMTP smtp.gmail.com:587 (digest)
</pre>
<h3>Hosts &amp; URLs</h3>
<table>
<tr><th>Hostname</th><th>Role</th></tr>
<tr><td><code>https://check.watersmedia.org</code></td><td>Public URL for the app (TLS via Boba-Fett)</td></tr>
<tr><td><code>http://10.1.1.1:3000</code></td><td>Direct LAN access to the app (bypasses reverse proxy)</td></tr>
<tr><td><code>chronicles-na</code> (10.1.1.1)</td><td>Synology that runs the container</td></tr>
<tr><td><code>boba-fett</code> (10.1.1.4)</td><td>Synology that owns the public IP &amp; reverse proxy</td></tr>
</table>
<hr/>
<h2>3. Deployment steps (already completed)</h2>
<p>The system is already deployed. The steps below document exactly how it was built so it can be reproduced on another NAS if needed.</p>
<h3>3.1 Prepare the source bundle</h3>
<p>The application source lives in this repository under <code>server/</code>. A deploy tarball is produced from that folder (excluding <code>node_modules</code>):</p>
<pre><code>tar --exclude='node_modules' --exclude='.DS_Store' \
-czf checklist-deploy.tgz -C server .</code></pre>
<h3>3.2 Upload to the NAS</h3>
<ol>
<li>Open <strong>File Station</strong> on Chronicles-NA.</li>
<li>Navigate to the <code>docker</code> shared folder (created automatically when Container Manager is installed).</li>
<li>Create folder <code>recurring-checklist</code> inside <code>docker</code>.</li>
<li>Upload <code>checklist-deploy.tgz</code> into the new folder.</li>
<li>Right-click → <strong>Extract</strong> → Extract here. Synology unpacks <code>.tgz</code> in two passes (it leaves an intermediate <code>.tar</code>) — right-click that <code>.tar</code> and extract again.</li>
<li>Delete <code>checklist-deploy.tgz</code>, <code>checklist-deploy.tar</code>, and the <code>PaxHeader</code> folder.</li>
</ol>
<h3>3.3 Configure <code>.env</code></h3>
<p>The bundle ships a working <code>.env</code>. The values that matter are listed below. Edit through File Station → right-click → <strong>Open with → Text Editor</strong> (or any text editor).</p>
<table>
<tr><th>Key</th><th>Value used</th><th>Notes</th></tr>
<tr><td><code>PORT</code></td><td>3000</td><td>Container-internal port. Don’t change unless also editing <code>docker-compose.yml</code>.</td></tr>
<tr><td><code>ADMIN_PASSWORD</code></td><td>(set in <code>.env</code>)</td><td>Password used to enter “Admin” mode in the web UI.</td></tr>
<tr><td><code>REPORT_EMAIL</code></td><td><code>media@waterschurch.org</code></td><td>Recipient of the Sunday digest.</td></tr>
<tr><td><code>REPORT_CRON</code></td><td><code>0 15 * * 0</code></td><td>Sunday at 3:00 PM in <code>REPORT_TZ</code>.</td></tr>
<tr><td><code>REPORT_TZ</code></td><td><code>America/New_York</code></td><td>IANA timezone for the cron.</td></tr>
<tr><td><code>RESET_AFTER_DIGEST</code></td><td><code>true</code></td><td>Server clears all check states after the digest is sent.</td></tr>
<tr><td><code>SMTP_HOST</code></td><td><code>smtp.gmail.com</code></td><td></td></tr>
<tr><td><code>SMTP_PORT</code></td><td><code>587</code></td><td>STARTTLS port.</td></tr>
<tr><td><code>SMTP_USER</code></td><td><code>media@waterschurch.org</code></td><td>Sender — a Google Workspace account.</td></tr>
<tr><td><code>SMTP_PASS</code></td><td><em>Gmail App Password (16 chars)</em></td><td>Generated at <code>myaccount.google.com/apppasswords</code>. NOT the regular Gmail password — requires 2-Step Verification.</td></tr>
<tr><td><code>SMTP_FROM</code></td><td><code>Recurring Checklist &lt;media@waterschurch.org&gt;</code></td><td></td></tr>
</table>
<h3>3.4 Build and start the container</h3>
<ol>
<li>Install <strong>Container Manager</strong> from Package Center on Chronicles-NA (creates the <code>docker</code> shared folder automatically).</li>
<li>Open Container Manager → left sidebar <strong>Project</strong> → <strong>Create</strong>.</li>
<li>Project name: <code>recurring-checklist</code>. Path: <code>/volume1/docker/recurring-checklist</code>. Source: “Use existing docker-compose.yml”.</li>
<li>Skip the “Set up web portal via Web Station” option (not needed).</li>
<li>Click Done. Container Manager runs <code>docker compose up</code>; first build takes ~60 seconds. Final log line should be <code>Exit Code: 0</code>.</li>
<li>Verify: <code>http://10.1.1.1:3000</code> serves the app.</li>
</ol>
<h3>3.5 Public hostname + TLS</h3>
<ol>
<li><strong>DDNS</strong> (Chronicles-NA → Control Panel → External Access → DDNS → Add): Synology provider, hostname <code>check.watersmedia.org</code>, External Address Auto. Status should be Normal.</li>
<li><strong>Reverse proxy</strong> on <em>Boba-Fett</em> (Control Panel → Login Portal → Advanced → Reverse Proxy → Create):
<ul>
<li>Source: <code>HTTPS</code> · <code>check.watersmedia.org</code> · port <code>443</code></li>
<li>Destination: <code>HTTP</code> · <code>10.1.1.1</code> · port <code>3000</code></li>
</ul>
</li>
<li><strong>Let’s Encrypt</strong> on Boba-Fett (Control Panel → Security → Certificate → Add → Get a certificate from Let’s Encrypt):
<ul>
<li>Domain: <code>check.watersmedia.org</code></li>
<li>Email: <code>media@waterschurch.org</code></li>
</ul>
</li>
<li>Bind the cert: <strong>Settings</strong> button in the Certificate tab → set the <code>check.watersmedia.org</code> row to use the new cert → OK.</li>
</ol>
<div class="callout">
<strong>Why does the reverse proxy live on Boba-Fett, not on Chronicles-NA?</strong>
The router forwards port 443 to Boba-Fett. Putting the proxy there avoids any router changes. Boba-Fett receives <code>check.watersmedia.org</code> traffic, terminates TLS with the Let’s Encrypt cert, and forwards plain HTTP to Chronicles-NA:3000 over the LAN.
</div>
<hr/>
<h2>4. How users access it</h2>
<p>Any browser on any device:</p>
<pre><code>https://check.watersmedia.org</code></pre>
<p>First visit on each device asks for a name. That name is the <strong>location identifier</strong> (e.g. <code>Woonsocket</code>, <code>North Attleboro</code>). Case and whitespace are normalised — <code>woonsocket</code>, <code>WOONSOCKET</code>, and <code>Woonsocket</code> all collapse to the same canonical name. The name is stored in the browser’s <code>localStorage</code> and can be changed by clicking the name pill in the top-right.</p>
<p>Each location ticks off items independently. Unassigned lists are visible to all locations but checks are per-location. Lists with a specific location are only visible to (and writeable by) that location.</p>
<hr/>
<h2>5. How admin manages it</h2>
<ol>
<li>Click the <strong>🔒 Admin</strong> button in the top-right and enter <code>ADMIN_PASSWORD</code>.</li>
<li>A horizontal “Menu” bar appears under the list tabs. Click or swipe it to expand:
<ul>
<li><strong>+ Add List</strong> / <strong>Rename List</strong> / <strong>Delete List</strong> — each list has an optional “Assign to location” field. Leave blank to show the list to every location.</li>
<li><strong>Export JSON</strong> — downloads the current state file as a backup snapshot.</li>
<li><strong>Import JSON</strong> — appends imported lists into the current state, auto-tagged with the current logged-in location.</li>
<li><strong>Send Digest Now</strong> — sends a one-off (manual) copy of the Sunday report immediately. Useful to verify SMTP. Does <em>not</em> reset state.</li>
</ul>
</li>
<li>Within a list (admin mode), each item has Edit / Move / Delete buttons. Drag tabs sideways to reorder lists; drag items up/down within the To Do or Done column to reorder tasks.</li>
<li>Click the yellow <strong>🔓 Admin (on)</strong> button again to leave admin mode. All management UI disappears for non-admins.</li>
</ol>
<hr/>
<h2>6. Weekly email digest</h2>
<p>A <code>node-cron</code> job fires at <code>0 15 * * 0</code> in <code>America/New_York</code> (Sunday 3:00 PM local). It:</p>
<ol>
<li>Reads the current state from <code>checklists.json</code>.</li>
<li>Builds a per-location report: one section per location, each list within shown as a green/red table of ✓/✗ items with the who+when stamp.</li>
<li>Sends the email via <code>smtp.gmail.com:587</code> using the configured app password, to <code>REPORT_EMAIL</code> only. The subject is <code>Checklist Report — &lt;weekday, month, day&gt;</code>.</li>
<li>If <code>RESET_AFTER_DIGEST=true</code>, the entire check-states map is cleared so the next week starts clean. Items themselves are preserved.</li>
</ol>
<p>To change the schedule, edit <code>REPORT_CRON</code> in <code>.env</code> and rebuild the container (see §8).</p>
<hr/>
<h2>7. Backups</h2>
<p>Hyper Backup task <strong>“Recurring Checklist”</strong> on Chronicles-NA:</p>
<ul>
<li>Destination: Google Drive (signed in as <code>media@waterschurch.org</code>).</li>
<li>Source: <code>docker</code> shared folder (includes everything under <code>/volume1/docker</code>).</li>
<li>Schedule: Daily 23:30; integrity check Saturday 01:30.</li>
<li>Compression on. Client-side encryption off.</li>
</ul>
<p>Restoring an entire run is as simple as: open Hyper Backup → select task → Restore Data → pick a version → restore to <code>/volume1/docker/recurring-checklist/data/checklists.json</code>. The container picks up the new file on next request (no restart needed).</p>
<hr/>
<h2>8. Updating the app code</h2>
<p>The state file (<code>data/checklists.json</code>) and the configuration file (<code>.env</code>) are preserved between updates because they live in a Docker bind-mount and on disk. The update procedure is:</p>
<ol>
<li>Developer produces a new <code>checklist-deploy.tgz</code> from the latest <code>server/</code> source.</li>
<li>Upload the new tarball into <code>/volume1/docker/recurring-checklist/</code> via File Station.</li>
<li><strong>Important:</strong> before extracting, rename or move the existing <code>.env</code> aside so it’s not overwritten (the bundle ships an example <code>.env</code>, but if the deploy script excludes it, this step is unnecessary — confirm with the developer per release).</li>
<li>Extract the new tarball (in two passes, as in §3.2). Replace existing files when prompted.</li>
<li>Delete the leftover <code>.tgz</code> / <code>.tar</code> / <code>PaxHeader</code>.</li>
<li>Open Container Manager → Project → <strong>recurring-checklist</strong> → <strong>Action</strong> menu → <strong>Build</strong> (or Stop, then Start with Build). This rebuilds the image with the new code and restarts the container.</li>
<li>Verify: <code>http://10.1.1.1:3000</code> still loads, and any new functionality from the release is visible.</li>
</ol>
<p>Rollback: keep the previous <code>checklist-deploy.tgz</code> archived. To roll back, re-extract the older bundle, rebuild. State file is untouched.</p>
<hr/>
<h2>9. Troubleshooting / common ops</h2>
<table>
<tr><th>Symptom</th><th>Where to look</th><th>Fix</th></tr>
<tr><td>App URL times out</td><td>Container Manager → recurring-checklist → status</td><td>If stopped: Action → Start. If crashing: open container Log, look for errors.</td></tr>
<tr><td>Digest email never arrived</td><td>Container Manager → container Log, search <code>[digest]</code></td><td>Look for <code>Failed: ...</code>. Most often Gmail app password revoked or rotated; regenerate at <code>myaccount.google.com/apppasswords</code> and update <code>SMTP_PASS</code> in <code>.env</code>, then rebuild.</td></tr>
<tr><td>Cert renewal failed</td><td>Boba-Fett → Control Panel → Security → Certificate</td><td>Right-click the cert → Renew. Requires port 80 reachable on Boba-Fett.</td></tr>
<tr><td>State got corrupted / accidental wipe</td><td>Hyper Backup → Recurring Checklist task</td><td>Restore Data → pick latest known-good version → restore the <code>data/</code> folder only.</td></tr>
<tr><td>“Wrong password” on admin login</td><td><code>.env</code> on the NAS</td><td>Compare <code>ADMIN_PASSWORD</code>. If changed, rebuild container (env vars are read on container start).</td></tr>
<tr><td>Reverse proxy returns Boba-Fett DSM instead of the app</td><td>Boba-Fett → Control Panel → Login Portal → Advanced → Reverse Proxy</td><td>Confirm rule for <code>check.watersmedia.org</code> exists and routes to <code>10.1.1.1:3000</code>. Cert must be bound (see §3.5).</td></tr>
</table>
<h3>Useful one-liners (SSH into Chronicles-NA)</h3>
<pre><code># See the running container
sudo docker ps | grep recurring
# Tail logs
sudo docker logs -f recurring-checklist
# Manually restart the container
sudo docker compose -f /volume1/docker/recurring-checklist/docker-compose.yml restart
# Look at the live state file
cat /volume1/docker/recurring-checklist/data/checklists.json | head</code></pre>
<hr/>
<h2>10. Credentials &amp; references</h2>
<ul>
<li><strong>Admin password</strong>: set in <code>.env</code> on the NAS (key <code>ADMIN_PASSWORD</code>). Rotate by editing <code>.env</code> and rebuilding the container.</li>
<li><strong>Gmail app password</strong>: generated at <code>https://myaccount.google.com/apppasswords</code> using the <code>media@waterschurch.org</code> Google Workspace account. Stored in <code>.env</code> as <code>SMTP_PASS</code>. To rotate: revoke the existing app password in Google, generate a new one, paste into <code>.env</code>, rebuild.</li>
<li><strong>Synology DDNS</strong>: <code>check.watersmedia.org</code> tied to the Synology Account used during DDNS setup. Renewal is automatic.</li>
<li><strong>Let’s Encrypt cert</strong>: 90-day validity, auto-renewal handled by DSM 7. Confirm under Control Panel → Security → Certificate.</li>
<li><strong>Source repository</strong>: the application source is the <code>server/</code> directory of <code>recurring-checklist-app</code>, maintained by the application developer.</li>
</ul>
<div class="callout warn">
<strong>Security notes</strong>
The app uses a shared password for admin actions (no per-user accounts). Anyone on the open URL can check items off using any name, so don’t treat the contents of the checklist as confidential. The admin password is the only gate for adding/editing/deleting lists.
</div>
<div class="footer">
Recurring Checklist deployment runbook · Generated for Waters Church IT handoff.
</div>
</body>
</html>