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>
This commit is contained in:
Waters Church
2026-09-24 13:00:57 -04:00
co-authored by Claude Opus 5
commit 469db5bc60
177 changed files with 33247 additions and 0 deletions
+798
View File
@@ -0,0 +1,798 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<!-- Stop iOS Safari's data detectors from auto-linking IPs/ports/credentials in
task descriptions as "phone numbers" — that drew a stray underline and hijacked
taps into the call/share sheet. Nothing in this app is ever a phone number. -->
<meta name="format-detection" content="telephone=no, date=no, address=no, email=no">
<title>Recurring Checklist</title>
<!-- Inter — modern productivity typeface (falls back to the system stack offline) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="styles.css">
<!-- PWA / installable web app -->
<link rel="manifest" href="/manifest.webmanifest">
<meta name="theme-color" content="#1d2026">
<meta name="color-scheme" content="dark">
<!-- iOS Safari support -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Checklist">
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png?v=2">
<!-- Favicons (v2 = new checkmark logo; query busts the browser's favicon cache) -->
<link rel="icon" type="image/svg+xml" href="/icons/icon.svg?v=2">
<link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32.png?v=2">
</head>
<body>
<div id="loadingOverlay" class="loading-overlay">
<div class="loading-spinner"></div>
<div class="loading-text">Loading checklist...</div>
</div>
<!-- Shown when offline or while queued check-offs are syncing. -->
<div id="offlineBadge" class="offline-badge" style="display:none;"></div>
<!-- First-launch setup, native apps only. The store build ships to anyone, so the
server is a per-install choice rather than something baked into the binary. -->
<div id="setupOverlay" class="setup-overlay" style="display:none;">
<div class="setup-card">
<div class="setup-logo" aria-hidden="true">
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs><linearGradient id="sg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#39a8ff"/><stop offset="1" stop-color="#2ecc71"/>
</linearGradient></defs>
<rect width="512" height="512" rx="112" fill="url(#sg)"/>
<path d="M150 262 L224 338 L366 178" fill="none" stroke="#fff" stroke-width="46"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h2>Set up Checklist</h2>
<p class="setup-sub">How do you want to use it? You can change this later.</p>
<div id="setupChoices">
<button type="button" class="setup-choice" data-setup="local">
<span class="setup-choice-title">Just on this device</span>
<span class="setup-choice-desc">Everything stays on your phone. No server,
no account, works offline &mdash; but only you see it.</span>
</button>
<button type="button" class="setup-choice" data-setup="join">
<span class="setup-choice-title">Join a server</span>
<span class="setup-choice-desc">Your church already runs one. Everyone on your
team shares the same lists.</span>
</button>
<button type="button" class="setup-choice" data-setup="host">
<span class="setup-choice-title">Host it yourself</span>
<span class="setup-choice-desc">Run it on your own computer or NAS. Your data
stays on your hardware.</span>
</button>
</div>
<!-- Address entry, shared by both choices above. -->
<div id="setupJoin" style="display:none;">
<label for="setupAddress">Server address</label>
<input id="setupAddress" type="url" inputmode="url" autocapitalize="off"
autocorrect="off" spellcheck="false" placeholder="checklist.yourchurch.org" />
<div id="setupError" class="setup-error" style="display:none;"></div>
<div class="setup-actions">
<button type="button" id="setupBack" class="btn btn-secondary btn-small">Back</button>
<button type="button" id="setupConnect" class="btn btn-primary btn-small">Connect</button>
</div>
</div>
<div id="setupHostInfo" style="display:none;">
<p class="setup-sub" style="text-align:left;">
Checklist runs as a small Docker service on any always-on machine — a NAS, a
spare computer, or a cheap VPS. Once it is running, come back and enter its
address.
</p>
<div class="setup-actions">
<button type="button" id="setupHostBack" class="btn btn-secondary btn-small">Back</button>
<button type="button" id="setupHostNext" class="btn btn-primary btn-small">I have it running</button>
</div>
</div>
</div>
</div>
<div id="loginModal" class="modal-overlay" style="display: none;">
<div class="modal">
<h3>Sign in</h3>
<div class="form-group">
<label for="loginLocation">Location</label>
<select id="loginLocation"></select>
</div>
<div class="form-group">
<label for="loginTeam">Team</label>
<select id="loginTeam"></select>
</div>
<div class="form-group" id="loginExecNameGroup" style="display:none;">
<label for="loginExecName">Who are you?</label>
<input id="loginExecName" type="text" autocomplete="off" autocapitalize="words" placeholder="Your name" />
</div>
<div class="form-group">
<label for="loginPassword">Password</label>
<div class="password-field">
<input id="loginPassword" type="password" autocomplete="current-password" />
<button id="loginPasswordToggle" class="password-toggle" type="button" aria-label="Show password" title="Show password">🐵</button>
</div>
</div>
<div id="loginError" class="modal-message danger" style="display:none;"></div>
<p style="font-size:11px;color:#888;margin:0;">Your location, team, and password. Editors: choose <code>Executive</code>. <a href="/privacy" target="_blank" rel="noopener noreferrer" style="color:#7c9cc4;">Privacy</a><span id="changeServerWrap" style="display:none;"> · <a href="#" id="changeServerLink" style="color:#7c9cc4;">Change server</a></span></p>
<div class="modal-actions">
<button id="loginSubmit" class="btn btn-primary btn-small" type="button">Sign in</button>
</div>
</div>
</div>
<div id="userMgmtModal" class="modal-overlay" style="display: none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>Manage Access</h3>
<button id="userMgmtClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<div id="userMgmtError" class="modal-message danger" style="display:none;"></div>
<div class="mgmt-grid">
<section class="mgmt-section">
<h4>Team logins</h4>
<input id="userMgmtSearch" class="mgmt-search" type="search" placeholder="🔍 Filter by location, team, or email…" />
<div id="userMgmtList" class="user-mgmt-list"></div>
<div class="mgmt-add">
<h5>Add a team login</h5>
<div class="user-mgmt-grid">
<select id="newUserLocation" aria-label="Location"></select>
<select id="newUserTeam" aria-label="Team"></select>
<input id="newUserPassword" type="password" placeholder="Password (4+ chars)" />
<input id="newUserEmails" type="text" placeholder="Report email(s), comma-separated — optional" />
<button id="newUserCreate" class="btn btn-primary btn-small" type="button">Create login</button>
</div>
<p class="mgmt-tip">One shared login per location + team. Report emails get the weekly digest.</p>
</div>
</section>
<section class="mgmt-section">
<h4>Executives</h4>
<input id="execSearch" class="mgmt-search" type="search" placeholder="🔍 Filter by name…" />
<div id="execList" class="user-mgmt-list"></div>
<div class="mgmt-add">
<h5>Add an executive</h5>
<div class="user-mgmt-grid">
<input id="newExecName" type="text" placeholder="Name (e.g. Jordan)" autocapitalize="words" />
<input id="newExecPassword" type="password" placeholder="Password (4+ chars)" />
<button id="newExecCreate" class="btn btn-primary btn-small" type="button">Create executive</button>
</div>
<p class="mgmt-tip">Sign in under &ldquo;Executive&rdquo; with their name. &ldquo;Edit access&rdquo; sets the locations/teams they can edit.</p>
</div>
</section>
<section class="mgmt-section mgmt-wide">
<h4>Locations &amp; teams</h4>
<div id="directoryEditor" class="directory-editor"></div>
</section>
</div>
</div>
</div>
<div id="listModal" class="modal-overlay" style="display: none;">
<div class="modal">
<h3 id="listModalTitle"></h3>
<div id="listModalMessage" class="modal-message" style="display:none;"></div>
<input id="listModalInput" type="text" placeholder="List name" />
<div id="listModalTemplateGroup" class="form-group" style="margin-top:8px; display:none;">
<label for="listModalTemplate" style="font-size:12px;color:#b0b0b0;">Start from</label>
<select id="listModalTemplate"></select>
</div>
<div id="listModalLocationGroup" class="form-group" style="margin-top:8px;">
<label for="listModalLocation" style="font-size:12px;color:#b0b0b0;">Assign to location</label>
<select id="listModalLocation"></select>
</div>
<div id="listModalTeamGroup" class="form-group" style="margin-top:8px;">
<label for="listModalTeam" style="font-size:12px;color:#b0b0b0;">Assign to team</label>
<select id="listModalTeam"></select>
</div>
<div id="listModalColorGroup" class="form-group" style="margin-top:8px;">
<label style="font-size:12px;color:#b0b0b0;">Color</label>
<div id="listModalColors" class="color-swatches">
<button type="button" class="swatch swatch-none" data-color="" title="No color"></button>
<button type="button" class="swatch" data-color="blue" title="Blue"></button>
<button type="button" class="swatch" data-color="green" title="Green"></button>
<button type="button" class="swatch" data-color="amber" title="Amber"></button>
<button type="button" class="swatch" data-color="red" title="Red"></button>
<button type="button" class="swatch" data-color="purple" title="Purple"></button>
<button type="button" class="swatch" data-color="teal" title="Teal"></button>
<button type="button" class="swatch" data-color="pink" title="Pink"></button>
<button type="button" class="swatch" data-color="slate" title="Slate"></button>
<button type="button" class="swatch" data-color="orange" title="Orange"></button>
<button type="button" class="swatch" data-color="yellow" title="Yellow"></button>
<button type="button" class="swatch" data-color="lime" title="Lime"></button>
<button type="button" class="swatch" data-color="cyan" title="Cyan"></button>
<button type="button" class="swatch" data-color="indigo" title="Indigo"></button>
<button type="button" class="swatch" data-color="brown" title="Brown"></button>
<label class="swatch swatch-custom" title="Custom color — pick your own">
<input type="color" id="listModalCustomColor" value="#3498db" aria-label="Custom color" />
</label>
</div>
</div>
<div id="listModalHiddenGroup" class="form-group" style="margin-top:10px;">
<label class="checkbox-label" style="font-size:13px;"><input type="checkbox" id="listModalHidden" /> Hidden — only editors see it</label>
<label class="checkbox-label" style="font-size:13px;margin-top:6px;"><input type="checkbox" id="listModalShowNotes" /> Show a Notes column — reference info, no checking off</label>
<label class="checkbox-label" style="font-size:13px;margin-top:6px;"><input type="checkbox" id="listModalNotesOnly" /> Notes only — hide the To&nbsp;Do and Done columns</label>
<label class="checkbox-label" style="font-size:13px;margin-top:6px;"><input type="checkbox" id="listModalShowFeedback" /> Show a Feedback column — anyone on the list can add input</label>
<div class="form-group" id="listModalSectionsGroup" style="margin-top:10px;">
<label style="font-size:13px;">Task sections — extra To&nbsp;Do columns (e.g. Monday, Tuesday). The first is the default and can be renamed.</label>
<div id="listModalSections"></div>
<button type="button" class="btn btn-secondary btn-small" id="listModalSectionAdd">+ Add section</button>
</div>
</div>
<div id="listModalRepeatGroup" class="form-group" style="margin-top:8px;">
<label for="listModalResetType" style="font-size:12px;color:#b0b0b0;">🔁 Repeat this list <span style="color:#7c828c;">— every item resets to To&nbsp;Do on this schedule</span></label>
<select id="listModalResetType">
<option value="">Don't repeat — reset manually</option>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
<option value="yearly">Yearly</option>
</select>
<div id="listModalResetEvery" class="reset-row" style="display:none;">
<span>Every</span>
<input type="number" id="listModalResetInterval" min="1" max="99" value="1" />
<span id="listModalResetUnit">weeks</span>
<span id="listModalResetHint" class="reset-hint"></span>
</div>
<div id="listModalResetStartRow" class="reset-row" style="display:none;">
<span>starting on</span>
<select id="listModalResetStartDow">
<option value="0">Sunday</option><option value="1">Monday</option><option value="2">Tuesday</option>
<option value="3">Wednesday</option><option value="4">Thursday</option><option value="5">Friday</option><option value="6">Saturday</option>
</select>
</div>
<div id="listModalResetDowRow" class="reset-row" style="display:none;">
<span>on</span>
<select id="listModalResetDow">
<option value="0">Sunday</option><option value="1">Monday</option><option value="2">Tuesday</option>
<option value="3">Wednesday</option><option value="4">Thursday</option><option value="5">Friday</option><option value="6">Saturday</option>
</select>
</div>
<div id="listModalResetMonthRow" class="reset-row" style="display:none;">
<span>in</span>
<select id="listModalResetMonth">
<option value="0">January</option><option value="1">February</option><option value="2">March</option>
<option value="3">April</option><option value="4">May</option><option value="5">June</option>
<option value="6">July</option><option value="7">August</option><option value="8">September</option>
<option value="9">October</option><option value="10">November</option><option value="11">December</option>
</select>
</div>
<div id="listModalResetDomRow" class="reset-row" style="display:none;">
<span>on day</span>
<input type="number" id="listModalResetDom" min="1" max="31" value="1" />
<span>of the month</span>
</div>
<div id="listModalResetTimesRow" class="reset-row reset-times-row" style="display:none;">
<span>at</span>
<div id="listModalResetTimesList" class="reset-times-list"></div>
<button type="button" id="listModalResetTimesAdd" class="btn btn-secondary btn-small reset-time-add">+ Add time</button>
<span class="reset-hint reset-times-hint">leave empty to reset once a day</span>
</div>
</div>
<div class="modal-actions">
<button id="listModalCancel" class="btn btn-secondary btn-small" type="button">Cancel</button>
<button id="listModalSave" class="btn btn-primary btn-small" type="button">Save</button>
</div>
</div>
</div>
<div id="editTaskModal" class="modal-overlay" style="display: none;">
<div class="modal wide">
<h3>Edit Task</h3>
<div class="modal-body">
<div class="form-group">
<label for="editTitle">Title</label>
<input id="editTitle" type="text" placeholder="Task title..." />
</div>
<div class="form-group" id="editGroupRow" style="display:none;">
<label for="editGroupSelect">Section</label>
<select id="editGroupSelect"></select>
</div>
<div class="form-group">
<label for="editDescription">Description</label>
<div class="md-toolbar" data-md-target="editDescription">
<button type="button" class="md-btn" data-md="bold" title="Bold"><b>B</b></button>
<button type="button" class="md-btn" data-md="italic" title="Italic"><i>I</i></button>
<button type="button" class="md-btn" data-md="h" title="Heading">H</button>
<button type="button" class="md-btn" data-md="ul" title="Bullet list">• List</button>
<button type="button" class="md-btn" data-md="ol" title="Numbered list">1. List</button>
<button type="button" class="md-btn" data-md="task" title="Checkbox — a step users tick off (the task auto-completes when all are checked)">☑ Step</button>
<button type="button" class="md-btn" data-md="link" title="Add a link (web page, Google Doc…)">🔗 Link</button>
<button type="button" class="md-btn" data-md="attach" title="Attach a document (PDF, Word, Excel, PowerPoint…) — uploads it and links it here">📎 File</button>
<button type="button" class="md-btn md-preview-btn" data-md="preview" aria-pressed="false" title="Toggle formatted markdown view">Markdown</button>
</div>
<textarea id="editDescription" placeholder="Description — format with the buttons above." style="display:none;"></textarea>
<div class="md-rich" contenteditable="true" data-md-rich-for="editDescription" data-placeholder="Description — format with the buttons above." role="textbox" aria-multiline="true"></div>
</div>
<div class="form-group">
<label class="checkbox-label"><input type="checkbox" id="editIsRecurring" /> Recurring Task</label>
</div>
<div class="form-group" id="editRecurrenceOptions" style="display: none;">
<div class="rec-editor"></div>
</div>
</div>
<div class="modal-actions">
<button id="editModalCancel" class="btn btn-secondary btn-small" type="button">Cancel</button>
<button id="editModalSave" class="btn btn-primary btn-small" type="button">Save</button>
</div>
</div>
</div>
<div id="moveItemModal" class="modal-overlay" style="display: none;">
<div class="modal">
<h3>Move Task</h3>
<div class="modal-body">
<div class="form-group">
<label for="moveItemSelect">Select destination list</label>
<select id="moveItemSelect"></select>
</div>
</div>
<div class="modal-actions">
<button id="moveItemCancel" class="btn btn-secondary btn-small" type="button">Cancel</button>
<button id="moveItemSave" class="btn btn-primary btn-small" type="button">Move</button>
</div>
</div>
</div>
<!-- Inline "Move to…" dropdown anchored to a task's Move button (replaces the modal flow). -->
<div id="moveMenuBackdrop" class="move-menu-backdrop" style="display:none;"></div>
<div id="moveMenu" class="move-menu" role="menu" aria-label="Move task to list" style="display:none;"></div>
<!-- Cross-department request (editors only): lands in the target team's Requests inbox. -->
<div id="requestModal" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header"><h3>✋ Request something</h3></div>
<div class="modal-body">
<div id="requestError" class="modal-message danger" style="display:none;"></div>
<div class="form-group">
<label for="requestTarget">From which department?</label>
<select id="requestTarget"></select>
</div>
<div class="form-group">
<label for="requestTitle">What do you need?</label>
<input id="requestTitle" type="text" maxlength="120" placeholder="e.g. Toilet paper restock in the office bathroom" />
</div>
<div class="form-group">
<label for="requestDetails">Details (optional)</label>
<textarea id="requestDetails" rows="3" maxlength="2000" placeholder="Where exactly, how urgent, anything helpful…"></textarea>
</div>
<div id="requestPhotos" class="equip-photos"></div>
<div class="equip-photo-row">
<label class="btn btn-secondary btn-small equip-upload-label">
📷 Add photo
<input id="requestPhotoFile" type="file" accept="image/*" hidden />
</label>
<span id="requestUploadStatus" class="equip-upload-status"></span>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px;">
<button id="requestCancel" class="btn btn-secondary" type="button">Cancel</button>
<button id="requestSend" class="btn btn-primary" type="button">Send request</button>
</div>
</div>
</div>
</div>
<!-- Equipment photo popup (shown when anyone taps an equipment word).
The image sits in a pan/zoom stage: pinch or scroll to zoom, drag to pan,
double-tap to reset. The note panel below scrolls independently. -->
<div id="equipModal" class="modal-overlay equip-lightbox" style="display: none;">
<div class="modal equip-modal">
<div class="equip-lb-head">
<h3 id="equipModalName"></h3>
<button id="equipReportBtn" class="equip-report-btn" type="button" title="Report a problem with this gear">⚠ Report a problem</button>
<button id="equipModalClose" class="equip-lb-close" type="button" aria-label="Close">✕</button>
</div>
<div id="equipStage" class="equip-stage">
<img id="equipModalImg" class="equip-img" alt="" draggable="false" />
<button id="equipPrev" class="equip-nav equip-nav-prev" type="button" aria-label="Previous photo">‹</button>
<button id="equipNext" class="equip-nav equip-nav-next" type="button" aria-label="Next photo">›</button>
<div id="equipZoomHint" class="equip-zoom-hint"></div>
<div class="equip-zoom-controls">
<button id="equipZoomOut" class="equip-zoom-btn" type="button" aria-label="Zoom out">−</button>
<button id="equipZoomReset" class="equip-zoom-btn" type="button" aria-label="Fit to screen">⤢</button>
<button id="equipZoomIn" class="equip-zoom-btn" type="button" aria-label="Zoom in">+</button>
</div>
</div>
<div id="equipCaption" class="equip-caption"></div>
<div id="equipThumbs" class="equip-thumbs"></div>
<div id="equipModalNote" class="equip-note"></div>
<div id="equipIssues" class="equip-issues"></div>
</div>
</div>
<!-- Equipment library manager (master only) -->
<div id="equipMgmtModal" class="modal-overlay" style="display: none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>Equipment Library</h3>
<button id="equipMgmtClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<div id="equipMgmtError" class="modal-message danger" style="display:none;"></div>
<p class="mgmt-tip">Add a photo for any gear — its name becomes tappable in descriptions for the locations/teams you pick. Each location can keep its own photo.</p>
<input id="equipSearch" class="mgmt-search" type="search" placeholder="🔍 Filter equipment…" />
<div id="equipMgmtList" class="user-mgmt-list"></div>
<div class="mgmt-add" id="equipAddSection">
<button type="button" class="mgmt-add-toggle" id="equipAddToggle" aria-expanded="false" aria-controls="equipAddBody">
<span class="mgmt-add-caret" aria-hidden="true">+</span>
<span id="equipFormTitle">Add equipment</span>
</button>
<div class="mgmt-add-body" id="equipAddBody">
<div class="user-mgmt-grid">
<input id="equipName" type="text" placeholder="Name" />
<input id="equipAliases" type="text" placeholder="Aliases, comma-separated (optional)" />
<textarea id="equipNote" class="equip-note-input" rows="2" placeholder="Note / troubleshooting steps (optional) — Markdown & line breaks supported"></textarea>
</div>
<div id="equipPhotos" class="equip-photos"></div>
<div class="equip-photo-row">
<label class="btn btn-secondary btn-small equip-upload-label">
📁 Add photo / video
<input id="equipFile" type="file" accept="image/*,video/mp4,video/webm,video/quicktime" hidden />
</label>
<button id="equipCamBtn" class="btn btn-secondary btn-small" type="button">📷 Take photo</button>
<span id="equipUploadStatus" class="equip-upload-status"></span>
</div>
<div class="equip-url-row">
<input id="equipImage" type="text" class="equip-url-input" placeholder="…or paste an image / video URL (JPG, PNG, MP4)" />
<button id="equipAddUrl" class="btn btn-secondary btn-small" type="button">Add</button>
</div>
<div id="equipDocs" class="equip-docs-editor"></div>
<div class="equip-photo-row">
<label class="btn btn-secondary btn-small equip-upload-label">
📎 Add document
<input id="equipDocFile" type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.rtf" hidden />
</label>
</div>
<div class="equip-url-row">
<input id="equipDocUrl" type="text" class="equip-url-input" placeholder="…or paste a link (Google Doc, manual, web page)" />
<button id="equipDocAdd" class="btn btn-secondary btn-small" type="button">Add link</button>
</div>
<div class="equip-scope">
<div id="equipScopeHelp" class="exec-access-help">Where it appears — check location(s) and/or team(s).</div>
<div class="dir-head">Locations</div>
<div id="equipLocRow" class="exec-cb-row"></div>
<div class="dir-head" style="margin-top:6px;">Teams</div>
<div id="equipTeamRow" class="exec-cb-row"></div>
</div>
<div class="equip-form-actions">
<button id="equipSave" class="btn btn-primary btn-small" type="button">Add equipment</button>
<button id="equipCancelEdit" class="btn btn-secondary btn-small" type="button" style="display:none;">Cancel edit</button>
</div>
</div><!-- /.mgmt-add-body -->
</div>
</div>
</div>
<!-- In-app camera capture (works on desktop + mobile via getUserMedia) -->
<div id="equipCamModal" class="modal-overlay" style="display:none; z-index:10002;">
<div class="modal equip-cam">
<div class="mgmt-head">
<h3>Take a photo</h3>
<button id="equipCamCancel" class="btn btn-secondary btn-small" type="button">Cancel</button>
</div>
<div class="equip-cam-stage">
<video id="equipCamVideo" playsinline autoplay muted></video>
</div>
<div id="equipCamError" class="modal-message danger" style="display:none;"></div>
<div class="equip-cam-actions">
<button id="equipCamCapture" class="btn btn-primary" type="button">📸 Capture</button>
<button id="equipCamSwitch" class="btn btn-secondary btn-small" type="button">🔄 Switch camera</button>
</div>
</div>
</div>
<!-- Overview: each team's progress today + recent activity (master + execs) -->
<div id="overviewModal" class="modal-overlay" style="display:none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>Today at a glance</h3>
<button id="overviewClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<div id="overviewBody" class="overview-body"></div>
</div>
</div>
<div id="activityModal" class="modal-overlay" style="display:none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>🛡 Activity Log</h3>
<button id="activityClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<input id="activitySearch" class="activity-search" type="search" placeholder="Filter by person, action, or list…" autocomplete="off" />
<div id="activityBody" class="activity-body"></div>
</div>
</div>
<!-- "Who's checking?" name picker (shared team logins only) -->
<div id="checkerModal" class="modal-overlay" style="display:none;">
<div class="modal">
<h3>Who's checking?</h3>
<p style="font-size:12px;color:#888;margin:0;">Pick your name so your check-offs are recorded as you — no password needed.</p>
<select id="checkerSelect" style="margin-top:8px;"></select>
<div id="checkerAddRow" style="display:none; gap:8px; margin-top:8px;">
<input id="checkerNewName" type="text" placeholder="Type your name" autocapitalize="words" autocomplete="off" style="flex:1;" />
<button id="checkerAddBtn" class="btn btn-primary btn-small" type="button">Add</button>
</div>
<div class="modal-actions">
<button id="checkerClose" class="btn btn-secondary btn-small" type="button">Done</button>
</div>
</div>
</div>
<!-- Team name rosters — manage who's on each team (master + execs) -->
<div id="rosterModal" class="modal-overlay" style="display:none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>Team names</h3>
<button id="rosterClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<p class="mgmt-tip">Names checkers can tap when signing in. Add or remove names for any team you manage.</p>
<input id="rosterSearch" class="mgmt-search" type="search" placeholder="🔍 Filter by location or team…" />
<div id="rosterBody" class="roster-body user-mgmt-list"></div>
</div>
</div>
<div class="app-container">
<header>
<div class="header-left">
<svg class="app-logo" viewBox="0 0 44 44" role="img" aria-label="Checklist" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="appLogoGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#39a8ff"/>
<stop offset="100%" stop-color="#2ecc71"/>
</linearGradient>
</defs>
<rect x="3" y="3" width="38" height="38" rx="12" fill="url(#appLogoGrad)"/>
<path class="app-logo-check" d="M13.5 22.5 L19.5 28.5 L30.5 15.5" fill="none" stroke="#fff" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="header-controls admin-only">
<div class="list-tabs-controls">
<button id="listViewToggle" class="lists-toggle" type="button" title="Switch between the tab strip and a Location → Team → List tree">🌳 Tree view</button>
<button id="listTabsToggle" class="lists-toggle" type="button" aria-expanded="true" title="Show/hide the lists">Hide lists</button>
<div class="list-filter master-only">
<select id="adminFilterLocation" aria-label="Filter by location"></select>
<select id="adminFilterTeam" aria-label="Filter by team"></select>
</div>
</div>
<div class="undo-redo admin-only" role="group" aria-label="Undo and redo">
<button id="undoBtn" class="icon-btn" type="button" title="Undo (Ctrl/⌘+Z)" aria-label="Undo" disabled>↶</button>
<button id="redoBtn" class="icon-btn" type="button" title="Redo (Ctrl/⌘+Shift+Z)" aria-label="Redo" disabled>↷</button>
</div>
<div id="listMenu" class="list-menu">
<button id="listMenuHandle" class="list-menu-btn" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="listMenuPanel" aria-label="List tools menu"><span class="lmh-icon" aria-hidden="true">☰</span><span class="lmh-label">Menu</span></button>
<div id="listMenuBackdrop" class="list-menu-backdrop"></div>
<div id="listMenuPanel" class="list-menu-panel" role="menu" aria-label="List tools">
<div class="menu-group-label">This list</div>
<button id="addTaskBtn" class="btn btn-primary btn-small">+ Add Task</button>
<button id="addNoteBtn" class="btn btn-secondary btn-small" title="Add a reference note (no checkbox)">+ Add Note</button>
<button id="renameListBtn" class="btn btn-secondary btn-small">✏️ Edit List</button>
<button id="duplicateListBtn" class="btn btn-secondary btn-small" title="Make a copy of this list (items copied, checkmarks fresh)">⧉ Duplicate List</button>
<button id="resetListBtn" class="btn btn-secondary btn-small" title="Move every item back to To Do">↺ Reset List</button>
<button id="deleteListBtn" class="btn btn-danger btn-small">🗑 Delete List</button>
<div class="menu-group-label">Lists</div>
<button id="addListBtn" class="btn btn-primary btn-small">+ Add List</button>
<div class="menu-group-label">Tools</div>
<button id="helpTipsBtn" class="btn btn-secondary btn-small" type="button" title="Quick tips on how this works">❔ How this works</button>
<button id="overviewBtn" class="btn btn-secondary btn-small admin-only" title="See each team's progress today">📊 Overview</button>
<button id="rosterBtn" class="btn btn-secondary btn-small admin-only" title="Manage the names checkers can pick">🧑 Team Names</button>
<button id="manageUsersBtn" class="btn btn-secondary btn-small master-only">👥 Manage Users</button>
<button id="activityBtn" class="btn btn-secondary btn-small master-only" title="See everything done in the app — who checked, created, edited, moved, or deleted">🛡 Activity Log</button>
<button id="incidentsBtn" class="btn btn-secondary btn-small admin-only" title="See and resolve reported gear problems in your area">⚠ Incidents<span id="incidentsBtnCount" class="menu-count" style="display:none;"></span></button>
<button id="reliabilityBtn" class="btn btn-secondary btn-small admin-only" title="Which gear breaks most — ranked by reported issues">📊 Reliability</button>
<button id="equipMgmtBtn" class="btn btn-secondary btn-small admin-only">🔧 Equipment</button>
<button id="sendDigestBtn" class="btn btn-secondary btn-small admin-only" title="Email the checklist report now to the configured recipients for your area">📧 Send Report</button>
<button id="exportJsonBtn" class="btn btn-secondary btn-small admin-only" title="Download a backup of your lists (master = full backup)">⬇ Export JSON</button>
<button id="importJsonBtn" class="btn btn-secondary btn-small admin-only">⬆ Import JSON</button>
<input id="importJsonInput" type="file" accept="application/json,.json" style="display:none;" />
</div>
</div>
</div>
<div class="header-right">
<button id="searchBtn" class="icon-btn search-bar-btn" type="button" title="Search gear, notes & tasks (press /)" aria-label="Search"><span class="sb-icon" aria-hidden="true">🔍</span><span class="sb-label">Search…</span></button>
<button id="reportIssueBtn" class="icon-btn report-issue-btn" type="button" title="Report a gear problem" aria-label="Report a gear problem">⚠</button>
<button id="requestsBadge" class="requests-badge" type="button" style="display:none;" title="Open requests waiting — tap to view" aria-label="Open requests">✋ <span id="requestsBadgeCount">0</span></button>
<button id="requestSendBtn" class="icon-btn request-send-btn admin-only" type="button" title="Request something from another department" aria-label="Request something from another department">✋</button>
<button id="checkerChip" class="checker-chip checker-only" type="button" title="Who's checking? Tap to pick your name">
<span class="checker-chip-icon">👤</span>
<span id="checkerChipName">Who's checking?</span>
</button>
<button id="userBadge" class="user-badge" type="button" title="Click to sign out">
<span class="user-badge-icon">👤</span>
<span id="userBadgeName">Sign in</span>
</button>
</div>
</header>
<div class="main-content">
<div id="tasksView" class="tab-view active">
<div class="list-tabs-row">
<div id="activeListName" class="active-list-name admin-only"></div>
<div id="listTabs" class="list-tabs"></div>
<div id="listTree" class="list-tree admin-only"></div>
</div>
<div class="checklists-section">
<div class="checklist-column" data-col="todo">
<h2><span class="col-title">To Do</span></h2>
<div id="todoList" class="checklist-list"></div>
</div>
<div class="checklist-column" data-col="done">
<h2><span class="col-title">Done</span></h2>
<div id="doneList" class="checklist-list"></div>
</div>
<div class="checklist-column notes-column" data-col="notes">
<h2><span class="col-title">Notes</span></h2>
<div id="notesList" class="checklist-list"></div>
</div>
<div class="checklist-column feedback-column" data-col="feedback">
<h2><span class="col-title">Feedback</span></h2>
<div id="feedbackList" class="checklist-list"></div>
</div>
</div>
</div>
</div>
<div id="searchOverlay" class="search-overlay" style="display:none;">
<div class="search-panel" role="dialog" aria-label="Search">
<div class="search-inputrow">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
<input id="globalSearchInput" type="search" placeholder="Search gear, notes, tasks & lists…" autocomplete="off" />
<button id="searchClose" type="button" class="icon-btn" aria-label="Close search">✕</button>
</div>
<div id="searchResults" class="search-results"></div>
</div>
</div>
<!-- Report a gear problem (any signed-in user) -->
<div id="incidentModal" class="modal-overlay" style="display: none;">
<div class="modal">
<div class="modal-header">
<h3>⚠ Report a problem</h3>
<button id="incidentClose" class="btn btn-secondary btn-small" type="button">Cancel</button>
</div>
<div class="modal-body">
<div id="incidentGearRow" class="incident-gear-row" style="display:none;">
<span class="incident-gear-label">Gear:</span>
<span id="incidentGearName" class="incident-gear-name"></span>
</div>
<div class="form-group">
<label>How bad is it?</label>
<div class="severity-toggle" id="incidentSeverity">
<button type="button" class="sev-btn sev-down" data-sev="down">
<span class="sev-emoji">🔴</span>
<span class="sev-title">It's down now</span>
<span class="sev-sub">Can't be used — alerts the team right away</span>
</button>
<button type="button" class="sev-btn sev-minor is-selected" data-sev="minor">
<span class="sev-emoji">🟡</span>
<span class="sev-title">Minor issue</span>
<span class="sev-sub">Still works — goes in the report</span>
</button>
</div>
</div>
<div class="form-group">
<label for="incidentWhat">What's wrong?</label>
<input type="text" id="incidentWhat" maxlength="120" placeholder="e.g. HDMI port loose, won't hold signal" />
</div>
<div class="form-group">
<label for="incidentDetails">Details <span class="label-hint">(optional)</span></label>
<textarea id="incidentDetails" rows="3" maxlength="2000" placeholder="Anything that helps whoever fixes it…"></textarea>
</div>
<div id="incidentReportingAs" class="incident-reporting-as" style="display:none;"></div>
<div class="modal-footer">
<button id="incidentSubmit" class="btn btn-primary" type="button">Send report</button>
</div>
</div>
</div>
</div>
<!-- Reported gear problems (editors) -->
<div id="incidentsModal" class="modal-overlay" style="display: none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>⚠ Gear Issues</h3>
<button id="incidentsClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<div class="incidents-controls">
<label class="incidents-openonly"><input type="checkbox" id="incidentsOpenOnly" checked /> Open only</label>
<input type="search" id="incidentsSearch" class="incidents-search" placeholder="Filter by gear, place, or text…" autocomplete="off" />
</div>
<div id="incidentsList" class="incidents-list"></div>
</div>
</div>
<!-- Reliability dashboard (editors) -->
<div id="reliabilityModal" class="modal-overlay" style="display: none;">
<div class="modal mgmt">
<div class="mgmt-head">
<h3>📊 Gear Reliability</h3>
<button id="reliabilityClose" class="btn btn-secondary btn-small" type="button">Close</button>
</div>
<div class="reliability-controls">
<label class="incidents-openonly"><input type="checkbox" id="reliabilityOpenFirst" checked /> Sort by open issues</label>
<span id="reliabilitySummary" class="reliability-summary"></span>
</div>
<div id="reliabilityList" class="reliability-list"></div>
</div>
</div>
<div id="addTaskModal" class="modal-overlay" style="display: none;">
<div class="modal wide">
<div class="add-header">
<h3 id="addModalTitle">Add Task</h3>
<div class="add-list-target">
<label for="addListSelect">Add to list:</label>
<select id="addListSelect"></select>
</div>
</div>
<div class="modal-body">
<div class="form-group">
<input type="text" id="itemTitle" placeholder="Task title..." />
</div>
<div class="form-group" id="itemGroupRow" style="display:none;">
<label for="itemGroupSelect">Section</label>
<select id="itemGroupSelect"></select>
</div>
<div class="form-group">
<div class="md-toolbar" data-md-target="itemDescription">
<button type="button" class="md-btn" data-md="bold" title="Bold"><b>B</b></button>
<button type="button" class="md-btn" data-md="italic" title="Italic"><i>I</i></button>
<button type="button" class="md-btn" data-md="h" title="Heading">H</button>
<button type="button" class="md-btn" data-md="ul" title="Bullet list">• List</button>
<button type="button" class="md-btn" data-md="ol" title="Numbered list">1. List</button>
<button type="button" class="md-btn" data-md="task" title="Checkbox — a step users tick off (the task auto-completes when all are checked)">☑ Step</button>
<button type="button" class="md-btn" data-md="link" title="Add a link (web page, Google Doc…)">🔗 Link</button>
<button type="button" class="md-btn" data-md="attach" title="Attach a document (PDF, Word, Excel, PowerPoint…) — uploads it and links it here">📎 File</button>
<button type="button" class="md-btn md-preview-btn" data-md="preview" aria-pressed="false" title="Toggle formatted markdown view">Markdown</button>
</div>
<textarea id="itemDescription" placeholder="Description (optional)" style="display:none;"></textarea>
<div class="md-rich" contenteditable="true" data-md-rich-for="itemDescription" data-placeholder="Description (optional)" role="textbox" aria-multiline="true"></div>
</div>
<div class="form-group" id="recurringFieldGroup">
<label class="checkbox-label">
<input type="checkbox" id="isRecurring" />
Recurring Task
</label>
</div>
<div class="form-group" id="recurrenceOptions" style="display: none;">
<div class="rec-editor"></div>
</div>
</div>
<div class="modal-actions">
<button id="addTaskCancel" class="btn btn-secondary btn-small" type="button">Close</button>
<button id="addItemBtn" class="btn btn-primary btn-small">Add Task</button>
</div>
</div>
</div>
</div>
<div id="toast" class="toast"></div>
<!-- Motion One (vendored, self-contained ~15KB): exposes window.Motion synchronously
before app.js so item FLIP, spring modals and the completion burst can use it.
If it ever fails to load, app.js falls back to the pure-CSS animations. -->
<script src="vendor/motion.global.js"></script>
<!-- Native shim: a no-op on the web; inside the iOS/Android app bundles it points
server-absolute URLs (/api/…, /equip-images/…) at the real server. The mobile
build injects native-config.js + native-shell.js ahead of it. -->
<script src="native-bridge.js"></script>
<script src="md.js"></script>
<script src="app.js"></script>
</body>
</html>