From 851798265ac432a59d9e1dd559a5b1edc1774ab2 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 26 Aug 2026 09:52:20 -0400 Subject: [PATCH 1/5] Pause the room display's polling while backgrounded (#25) Traced via Supabase request logs (edge_logs, filtered to the node/service-role user agent): two spaces' /display/[spaceId] pages were each generating ~270 identical requests to spaces/space_bookings/space_blackouts over a 16-hour window, continuing straight through the night regardless of whether anyone could see the screen. That is exactly the profile in the reported Screen Time chart -- solid, continuous usage attributed to this site across 12AM-5AM. Both the 50s data poll and the 1s clock tick now check document.visibilityState before doing anything, so a display left open in a backgrounded tab (screen off, phone locked, tab switched away) stops generating traffic and CPU wake-ups. A visibilitychange listener fetches immediately on return so the display catches up rather than waiting out the rest of the interval. A kiosk whose screen genuinely stays on and visible the whole time -- the feature's actual use case -- is unaffected, since visibilityState stays 'visible' for as long as it's actually on screen. Co-Authored-By: Claude Sonnet 5 --- app/display/[spaceId]/page.tsx | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/app/display/[spaceId]/page.tsx b/app/display/[spaceId]/page.tsx index ebdc2bf..3600c2e 100644 --- a/app/display/[spaceId]/page.tsx +++ b/app/display/[spaceId]/page.tsx @@ -113,7 +113,10 @@ function DisplayContent({ spaceId }: { spaceId: string }) { const [loading, setLoading] = useState(true) useEffect(() => { - const interval = setInterval(() => setNow(new Date()), 1000) + // Skip the tick while backgrounded -- nothing reads `now` when the display isn't visible. + const interval = setInterval(() => { + if (document.visibilityState === 'visible') setNow(new Date()) + }, 1000) return () => clearInterval(interval) }, []) @@ -146,8 +149,27 @@ function DisplayContent({ spaceId }: { spaceId: string }) { } fetchData() - const interval = setInterval(fetchData, 50_000) - return () => clearInterval(interval) + + // Issue #25: request logs showed a display left open kept polling every 50s around the + // clock regardless of whether the tab was actually on screen -- exactly the profile of + // the reported overnight usage/battery spike. The interval keeps running (cheap), but the + // network request itself is skipped while backgrounded; a visibilitychange listener + // fetches immediately on return so the display catches up rather than waiting out the + // rest of the interval. A kiosk whose screen genuinely stays on and visible the whole + // time is unaffected -- this only stops polling once nobody could be looking at it. + const interval = setInterval(() => { + if (document.visibilityState === 'visible') fetchData() + }, 50_000) + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') fetchData() + } + document.addEventListener('visibilitychange', handleVisibilityChange) + + return () => { + clearInterval(interval) + document.removeEventListener('visibilitychange', handleVisibilityChange) + } }, [spaceId, key, accessDenied]) // Booking times are stored as UTC wall-clock (T17:00Z = 5pm local). From 9f51fa8292917af776758e0268a960c7e5780142 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 26 Aug 2026 09:52:35 -0400 Subject: [PATCH 2/5] Surface a request's booking scope in the UI (#23) Requests have carried scope/division/room_request_bodies since #19, but nothing rendered it: every request list showed only the originating body, with no way to tell a single-body request from a divisional or multi-body one, and no way to filter by it. - /api/administrator/requests and /api/me/requests now select scope, division, and room_request_bodies(body_id, bodies(name)) alongside what they already returned. - ScopeLabel (already used for bookings) now renders on every request surface: the admin Requests tab, the Fulfill Request modal's summary card, the Open Requests sidebar in all three admin creation forms, and leadership's own My Requests list. - A type filter (All / Single Body / Divisional / Multi-Body) is added alongside the Requests tab's existing status grouping, and to each Open Requests sidebar next to its existing per-body filter -- mirroring the scope selector's own option set so the two stay in sync. FulfillModal takes the owning body's name, scope, division and linked bodies as plain fields on `request` rather than the raw row, since its only source is the caller passing through what it already rendered -- avoids re-deriving ScopedRow shape in a component that isn't otherwise scope-aware. Co-Authored-By: Claude Sonnet 5 --- .../administrator/fulfill-modal.tsx | 11 ++++ .../administrator/one-time-form.tsx | 33 ++++++++-- .../administrator/requests-tab.tsx | 61 +++++++++++++++++-- .../administrator/tabling-form.tsx | 33 ++++++++-- app/(dashboard)/administrator/weekly-form.tsx | 33 ++++++++-- app/(dashboard)/request/page.tsx | 39 ++++++++---- app/api/administrator/requests/route.ts | 5 +- app/api/me/requests/route.ts | 5 +- 8 files changed, 182 insertions(+), 38 deletions(-) diff --git a/app/(dashboard)/administrator/fulfill-modal.tsx b/app/(dashboard)/administrator/fulfill-modal.tsx index c9ba68a..96115e8 100644 --- a/app/(dashboard)/administrator/fulfill-modal.tsx +++ b/app/(dashboard)/administrator/fulfill-modal.tsx @@ -2,6 +2,8 @@ import { useEffect, useState } from 'react' import BookingModal from './booking-modal' +import ScopeLabel from '@/app/_components/scope-label' +import type { BookingScope, Division } from '@/lib/booking-scope' interface Booking { id: string @@ -15,6 +17,10 @@ interface FulfillModalProps { type: string purpose: string body_id: string + bodyName: string + scope: BookingScope + division: Division | null + linkedBodies: { id: string; name: string }[] } onClose: () => void onSuccess: () => void @@ -79,6 +85,11 @@ export default function FulfillModal({ request, onClose, onSuccess }: FulfillMod

Request

{request.purpose}

{request.type}

+ {/* Booking dropdown */} diff --git a/app/(dashboard)/administrator/one-time-form.tsx b/app/(dashboard)/administrator/one-time-form.tsx index bc1a3a7..a251786 100644 --- a/app/(dashboard)/administrator/one-time-form.tsx +++ b/app/(dashboard)/administrator/one-time-form.tsx @@ -3,7 +3,8 @@ import { useState, useEffect } from 'react' import TimePicker from './time-picker' import BookingScopeSelector, { type BookingScopeValue } from '@/app/_components/booking-scope-selector' -import { DIVISIONS, type Division } from '@/lib/booking-scope' +import ScopeLabel from '@/app/_components/scope-label' +import { DIVISIONS, type Division, type BookingScope } from '@/lib/booking-scope' const STATUSES = [ 'Reserved', @@ -38,7 +39,10 @@ interface PendingRequest { status: string created_at: string body_id: string + scope: BookingScope + division: Division | null bodies: { name: string } | null + room_request_bodies: { body_id: string; bodies: { name: string } | null }[] | null room_request_details: Array<{ start_date: string | null end_date: string | null @@ -92,6 +96,7 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O const [saving, setSaving] = useState(false) const [error, setError] = useState('') const [pendingRequests, setPendingRequests] = useState([]) + const [requestTypeFilter, setRequestTypeFilter] = useState<'all' | BookingScope>('all') useEffect(() => { fetch('/api/administrator/requests') @@ -104,9 +109,9 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O .catch(() => {}) }, []) - const visibleRequests = scopeValue.body_id - ? pendingRequests.filter(r => r.body_id === scopeValue.body_id) - : pendingRequests + const visibleRequests = pendingRequests + .filter(r => !scopeValue.body_id || r.body_id === scopeValue.body_id) + .filter(r => requestTypeFilter === 'all' || r.scope === requestTypeFilter) const updateSession = (index: number, field: keyof OneTimeSession, value: string) => { setSessions(prev => prev.map((s, i) => i === index ? { ...s, [field]: value } : s)) @@ -306,7 +311,19 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O
-

Open Requests

+
+

Open Requests

+ +
{visibleRequests.length === 0 ? (

No open requests

) : ( @@ -314,7 +331,11 @@ export default function OneTimeForm({ bodies, semesters, onClose, onSuccess }: O {visibleRequests.map(r => (
- {r.bodies?.name ?? '—'} + ({ id: x.body_id, name: x.bodies?.name ?? '' }))} + className="text-sm font-semibold text-[#f0f6ff]" + /> {new Date(r.created_at).toLocaleDateString()}

{r.purpose}

diff --git a/app/(dashboard)/administrator/requests-tab.tsx b/app/(dashboard)/administrator/requests-tab.tsx index 85060e0..e5fa0fd 100644 --- a/app/(dashboard)/administrator/requests-tab.tsx +++ b/app/(dashboard)/administrator/requests-tab.tsx @@ -4,6 +4,8 @@ import { useEffect, useState } from 'react' import FulfillModal from './fulfill-modal' import DenyModal from './deny-modal' import { Skeleton } from '@/app/_components/skeleton' +import ScopeLabel from '@/app/_components/scope-label' +import type { BookingScope, Division } from '@/lib/booking-scope' function RequestsTabSkeleton() { const card = (wide: boolean) => ( @@ -62,8 +64,11 @@ interface RoomRequest { status: RequestStatus notes: string | null created_at: string + scope: BookingScope + division: Division | null bodies: { name: string } | null users: { full_name: string } | null + room_request_bodies: { body_id: string; bodies: { name: string } | null }[] | null room_request_details: { room_name: string | null start_date: string @@ -113,7 +118,12 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) { type: string purpose: string body_id: string + bodyName: string + scope: BookingScope + division: Division | null + linkedBodies: { id: string; name: string }[] } | null>(null) + const [typeFilter, setTypeFilter] = useState<'all' | BookingScope>('all') const fetchRequests = async () => { const [reqRes, revRes] = await Promise.all([ @@ -135,6 +145,15 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) { if (requests.length === 0 && revisions.length === 0) return
No requests found.
+ const filteredRequests = requests.filter(r => typeFilter === 'all' || r.scope === typeFilter) + + const TYPE_FILTERS: { value: 'all' | BookingScope; label: string }[] = [ + { value: 'all', label: 'All' }, + { value: 'single', label: 'Single Body' }, + { value: 'divisional', label: 'Divisional' }, + { value: 'multi', label: 'Multi-Body' }, + ] + return (
{revisions.length > 0 && ( @@ -175,13 +194,38 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) { {requests.length > 0 && (
- {revisions.length > 0 &&

Room Requests

} - {requests.map(r => ( +
+ {revisions.length > 0 && ( +

Room Requests

+ )} +
+ {TYPE_FILTERS.map(f => ( + + ))} +
+
+ {filteredRequests.length === 0 ? ( +

No requests match this filter.

+ ) : filteredRequests.map(r => (
{/* Header */}
- {r.bodies?.name || 'Unknown Body'} + ({ id: x.body_id, name: x.bodies?.name ?? '' }))} + className="font-semibold text-[#f0f6ff]" + /> · {r.type === 'One-Time Room' ? 'One-Time/Multiple Room' : r.type}
@@ -245,7 +289,16 @@ export default function RequestsTab({ onCountChange }: RequestsTabProps) { ) : (
))} -
@@ -469,7 +479,12 @@ export default function RequestPage() {
Body -

{req.bodies?.name ?? '—'}

+

+ ({ id: x.body_id, name: x.bodies?.name ?? '' }))} + /> +

Type @@ -615,21 +630,21 @@ export default function RequestPage() { setForm({ ...form, room_name: e.target.value })} className={inputCls} />
-
+
0 ? getMinDate(minDaysRoom) : undefined} onChange={e => setForm({ ...form, start_date: e.target.value })} className={inputCls} />
-
+
0 ? getMinDate(minDaysRoom) : undefined} onChange={e => setForm({ ...form, end_date: e.target.value })} className={inputCls} />
-
+
setForm({ ...form, start_time: v })} />
-
+
setForm({ ...form, end_time: v })} />
diff --git a/app/api/administrator/requests/route.ts b/app/api/administrator/requests/route.ts index c4e7e9e..829666e 100644 --- a/app/api/administrator/requests/route.ts +++ b/app/api/administrator/requests/route.ts @@ -23,11 +23,12 @@ export async function GET() { const { data: requests } = await supabase .from('room_requests') .select(` - id, type, purpose, status, notes, created_at, body_id, + id, type, purpose, status, notes, created_at, body_id, scope, division, bodies(name), users(full_name), room_request_details(room_name, start_date, start_time, end_time, end_date), - tabling_request_sessions(session_date, start_time, end_time) + tabling_request_sessions(session_date, start_time, end_time), + room_request_bodies(body_id, bodies(name)) `) .order('created_at', { ascending: false }) diff --git a/app/api/me/requests/route.ts b/app/api/me/requests/route.ts index 2163c30..a77bb29 100644 --- a/app/api/me/requests/route.ts +++ b/app/api/me/requests/route.ts @@ -21,11 +21,12 @@ export async function GET() { const { data: requests, error } = await adminSupabase .from('room_requests') .select(` - id, type, purpose, status, notes, created_at, + id, type, purpose, status, notes, created_at, body_id, scope, division, bodies(name), room_request_details(room_name, start_date, start_time, end_time, end_date), tabling_request_sessions(session_date, start_time, end_time), - user_alerts(denial_reason) + user_alerts(denial_reason), + room_request_bodies(body_id, bodies(name)) `) .eq('requested_by', user.id) .order('created_at', { ascending: false }) From d6b8fe3aee99c1a5d6d5a54d95a5c418476490e7 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 26 Aug 2026 09:52:51 -0400 Subject: [PATCH 3/5] Fix mobile layout clipping and overflow across five screens (#24) Each fix targets the specific reported symptom, no broader redesign: - Dashboard content now clears the fixed mobile hamburger button (top-4, ~40px tall, no reserved layout space) with pt-20 on small screens, restored to the normal pt-8 at md+. This was the root cause behind two of the reported screens, where a right-aligned control sat directly under it. - My Rooms' title/bell/filter-buttons header row now wraps instead of forcing the "Next 1/3/7 Days" buttons hard against the bell. - The notification dropdown's fixed w-80 overflowed off the right edge on mobile, since the bell sits near the left of that row -- clamped to w-[min(20rem,calc(100vw-2rem))]. - Settings' "Add a body"
-
+
setForm(f => ({ ...f, start_date: e.target.value }))} className={inputCls} required /> @@ -464,7 +464,7 @@ function AdminBlackoutsPanel({ spaces }: { spaces: Space[] }) { ) : blackouts.length === 0 ? (
No blackouts configured.
) : ( -
+
@@ -523,7 +523,7 @@ function AdminBlackoutsPanel({ spaces }: { spaces: Space[] }) { {spaces.map(s => )} -
+
setEditForm(f => f && ({ ...f, start_date: e.target.value }))} className={inputCls} required /> diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 0f69174..36729ff 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -296,7 +296,9 @@ export default function DashboardLayout({
-
+ {/* pt-20 on mobile clears the fixed hamburger button (top-4, ~40px tall) so page + content -- e.g. My Rooms' filter row -- doesn't render underneath it (issue #24) */} +
{/* Scoped to the content area so the sidebar paints immediately instead of the whole app staying blank during the auth check. */} diff --git a/app/(dashboard)/my-rooms/notification-bell.tsx b/app/(dashboard)/my-rooms/notification-bell.tsx index 78343c3..6cd5fc9 100644 --- a/app/(dashboard)/my-rooms/notification-bell.tsx +++ b/app/(dashboard)/my-rooms/notification-bell.tsx @@ -101,8 +101,10 @@ export default function NotificationBell() { )} + {/* Width clamped to the viewport (issue #24) -- a fixed w-80 overflowed off the right + edge on mobile since the bell sits near the left of its header row. */} {open && ( -
+
Notifications {alerts.length > 0 && ( diff --git a/app/(dashboard)/my-rooms/page.tsx b/app/(dashboard)/my-rooms/page.tsx index 216a980..2935718 100644 --- a/app/(dashboard)/my-rooms/page.tsx +++ b/app/(dashboard)/my-rooms/page.tsx @@ -237,7 +237,7 @@ export default function MyRoomsPage() {
{/* My Upcoming Spaces */}
-
+

My Upcoming Spaces

diff --git a/app/(dashboard)/settings-modal.tsx b/app/(dashboard)/settings-modal.tsx index 48c53b1..2e35684 100644 --- a/app/(dashboard)/settings-modal.tsx +++ b/app/(dashboard)/settings-modal.tsx @@ -244,7 +244,7 @@ export default function SettingsModal({ onClose, cachedSettings, onSettingsLoade ` fields themselves rendered wider than their bordered box and spilled past its right edge, regardless of width or min-width. Root cause: globals.css applies the site's custom font (Space Grotesk) to every input, including native date inputs. iOS's date-picker widget sizes its internal day/month/year segments using the input's own font metrics, and Space Grotesk's glyph widths differ enough from the system font that WebKit miscalculates and renders wider than the CSS box. Fixed with one rule: `input[type="date"]` falls back to the system font, letting iOS size it against the metrics its own picker expects. Confirmed via grep that every date field in the app (17 call sites across 10 files -- SGA Spaces blackouts, all three admin booking create/edit forms, and all three Request a Booking date fields) is a plain native `` with no local font override, so this single global rule covers every instance rather than needing a per-file fix. Co-Authored-By: Claude Sonnet 5 --- app/globals.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/globals.css b/app/globals.css index d5fb53c..14d9f34 100644 --- a/app/globals.css +++ b/app/globals.css @@ -41,3 +41,13 @@ option { input[type="date"]::-webkit-calendar-picker-indicator { filter: invert(1) brightness(0.7); } + +/* iOS Safari sizes a native date input's day/month/year segments using the + applied font's own glyph metrics. Space Grotesk's widths differ enough from + the system font that WebKit miscalculates and renders the control wider + than its CSS box, pushing it past the container's right edge regardless of + width/min-width (issue #24). Falling back to the system font for this one + input type lets iOS size it against the metrics its own picker expects. */ +input[type="date"] { + font-family: -apple-system, system-ui, sans-serif; +} From 67c04094fc2e2afadbdd66e44e7aae10d06f3694 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 26 Aug 2026 10:48:14 -0400 Subject: [PATCH 5/5] Add a temporary debug page for the date-input rendering issue (#24) Isolates the reported "Date field uncontained/blank" symptom into four minimal cases (empty w/ Space Grotesk, empty w/ system font, a filled value, and a plain text input for comparison) so it can be checked directly on the device where the bug reproduces, without needing an authenticated session. Not a fix -- remove once the real cause is confirmed. Co-Authored-By: Claude Sonnet 5 --- .claude/launch.json | 11 +++++++++ public/_debug-date-test.html | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 .claude/launch.json create mode 100644 public/_debug-date-test.html diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..3029be9 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "chambers-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + } + ] +} diff --git a/public/_debug-date-test.html b/public/_debug-date-test.html new file mode 100644 index 0000000..b33fda9 --- /dev/null +++ b/public/_debug-date-test.html @@ -0,0 +1,47 @@ + + + + + + + + +
+ + +
+
+ + +
^ is the mm/dd/yyyy placeholder visible above, or does it look blank?
+
+
+ + +
^ same question with the system-font fallback applied
+
+
+ + +
^ is "8/26/2026" clearly readable, or hard to see against the dark background?
+
+ +