Every household now always has exactly one or more admins. Admins can invite new members (previously open to any parent -- now gated), promote/ demote other admins, remove parents, and directly set another parent's password (no email infra exists for a reset-link flow, so this is a direct admin-sets-the-value action). Every parent can change their own password with current-password confirmation. A sole admin can't remove themselves or demote until they promote someone else -- this falls out of a single "household must have >=1 admin" invariant rather than needing special-case code. Schema: parents.is_admin, added via a new idempotent ensureColumn() helper (SQLite has no ADD COLUMN IF NOT EXISTS, and this needed to run safely against the already-populated production parents table on next boot, not just fresh installs). A boot-time backfill promotes the earliest-created parent in any household with zero admins -- verified against a simulated copy of the real production schema/data (including the exact "spouse joined via invite" scenario), confirming correct promotion and clean idempotency across repeated boots. Fixed a real foreign-key landmine along the way: household_invites. used_by_parent_id had no ON DELETE clause, so deleting any parent who'd ever accepted an invite -- i.e. any spouse, in this app's normal usage -- would have thrown a constraint violation. src/lib/removeParent.js nulls that reference before deleting, wrapped in an explicit transaction (first use of manual BEGIN/COMMIT/ROLLBACK in this codebase, verified working with node:sqlite before relying on it). Verified extensively: every route's permission/edge cases via curl (cross-household isolation, sole-admin guards, password round-trips via real login), and the full UI flow (promote/demote/remove/both password modals/leave-with-error-toast) across two independent real browser sessions acting as admin and non-admin simultaneously. Full-app regression and a Docker build/boot check both pass with the new code in place.
173 lines
6.0 KiB
HTML
173 lines
6.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>Dashboard — Kids Calendar</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Baloo+2:wght@500;700;800&family=Nunito:wght@500;700;800&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="/css/shared.css">
|
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
<link rel="apple-touch-icon" href="/icons/apple-touch-icon-180.png">
|
|
<meta name="theme-color" content="#F1F5FB">
|
|
<meta name="mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
|
<meta name="apple-mobile-web-app-title" content="Calendar">
|
|
</head>
|
|
<body>
|
|
|
|
<header>
|
|
<div>
|
|
<h1 id="householdName">Loading…</h1>
|
|
<div class="subtitle" id="whoami"></div>
|
|
</div>
|
|
<div class="toolbar">
|
|
<button class="tool" id="inviteBtn">Invite spouse</button>
|
|
<button class="tool danger" id="logoutBtn">Log out</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="section">
|
|
<h2>Notifications</h2>
|
|
<div class="entity-card" id="notificationsCard">
|
|
<p class="meta" id="notificationsMessage">Checking notification support…</p>
|
|
<div>
|
|
<button class="tool primary" id="notificationsBtn" style="display:none;">Enable notifications</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>Household</h2>
|
|
<div id="parentsList" class="card-grid"></div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:12px;">
|
|
<h2 style="margin:0;">Children</h2>
|
|
<button class="tool primary" id="addChildBtn">+ Add child</button>
|
|
</div>
|
|
<div id="childrenList" class="card-grid"></div>
|
|
</div>
|
|
|
|
<!-- Invite modal -->
|
|
<div class="modal-backdrop hidden" id="inviteModal">
|
|
<div class="modal">
|
|
<h2>Invite your spouse</h2>
|
|
<p id="inviteLeadText">Share this link — it lets them create a login joined to your household. Expires in 7 days.</p>
|
|
<div class="field">
|
|
<input type="text" id="inviteLinkField" readonly>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="tool primary" id="copyInviteBtn">Copy link</button>
|
|
<button class="tool" id="closeInviteBtn">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add child modal -->
|
|
<div class="modal-backdrop hidden" id="addChildModal">
|
|
<div class="modal">
|
|
<h2>Add a child</h2>
|
|
<form id="addChildForm">
|
|
<div class="field">
|
|
<label for="childNameInput">Name</label>
|
|
<input type="text" id="childNameInput" required>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="tool primary">Add</button>
|
|
<button type="button" class="tool" id="cancelAddChildBtn">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New calendar modal -->
|
|
<div class="modal-backdrop hidden" id="newCalendarModal">
|
|
<div class="modal">
|
|
<h2>New calendar</h2>
|
|
<form id="newCalendarForm">
|
|
<input type="hidden" id="newCalChildId">
|
|
<div class="field">
|
|
<label for="newCalTitle">Title</label>
|
|
<input type="text" id="newCalTitle" placeholder="e.g. Week of Aug 24" required>
|
|
</div>
|
|
<div class="field">
|
|
<label class="opt" style="display:flex; align-items:center; gap:8px; font-weight:700;">
|
|
<input type="checkbox" id="newCalDuplicate">
|
|
Duplicate tasks from most recent calendar (checkmarks reset)
|
|
</label>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="tool primary">Create</button>
|
|
<button type="button" class="tool" id="cancelNewCalBtn">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change own password modal -->
|
|
<div class="modal-backdrop hidden" id="changeOwnPasswordModal">
|
|
<div class="modal">
|
|
<h2>Change my password</h2>
|
|
<form id="changeOwnPasswordForm">
|
|
<div class="field">
|
|
<label for="currentPasswordInput">Current password</label>
|
|
<input type="password" id="currentPasswordInput" required>
|
|
</div>
|
|
<div class="field">
|
|
<label for="newOwnPasswordInput">New password</label>
|
|
<input type="password" id="newOwnPasswordInput" minlength="8" required>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="tool primary">Change password</button>
|
|
<button type="button" class="tool" id="cancelChangeOwnPasswordBtn">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Admin sets another parent's password modal -->
|
|
<div class="modal-backdrop hidden" id="setParentPasswordModal">
|
|
<div class="modal">
|
|
<h2>Set password for <span id="setPasswordParentName"></span></h2>
|
|
<form id="setParentPasswordForm">
|
|
<input type="hidden" id="setPasswordParentId">
|
|
<div class="field">
|
|
<label for="newParentPasswordInput">New password</label>
|
|
<input type="password" id="newParentPasswordInput" minlength="8" required>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="submit" class="tool primary">Set password</button>
|
|
<button type="button" class="tool" id="cancelSetParentPasswordBtn">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Kiosk link modal -->
|
|
<div class="modal-backdrop hidden" id="kioskModal">
|
|
<div class="modal">
|
|
<h2>Tablet link for <span id="kioskChildName"></span></h2>
|
|
<p>Open this link on the tablet's browser and add it to the home screen. It only shows the active calendar and lets your child check items off — nothing else.</p>
|
|
<div class="field">
|
|
<input type="text" id="kioskLinkField" readonly>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button class="tool primary" id="copyKioskBtn">Copy link</button>
|
|
<button class="tool danger" id="regenKioskBtn">Regenerate link</button>
|
|
<button class="tool" id="closeKioskBtn">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<script src="/js/api.js"></script>
|
|
<script src="/js/push.js"></script>
|
|
<script src="/js/dashboard.js"></script>
|
|
|
|
</body>
|
|
</html>
|