Deep Audit: Realtime, Bugs and Security
This Notice section is Sami's universal inbox: every important report on any topic is filed here in one sequence. Report #002 is the deep audit of North Deck itself, ordered on 15 November 2026, covering three questions: is the site's data actually realtime, what bugs exist, and what security flaws exist. Everything below was measured or tested live, not assumed. Fixes were deployed the same night (Pages deployment 1a1e3cfb). 1. REALTIME VERDICTS (claimed vs measured) - Sheet sync, hourly (nd-sheet-sync): CONFIRMED. sync_state shows every module (frb, sessions, studio, watchlist, health, vault) refreshed at 21:59, 22:59, 23:59 and 00:59 Dhaka. End-to-end lag for Sheet data is therefore up to about 1 hour plus one sync run, by design, not live. FRB payload matches the Tracker Sheet state as of the last run (24 classes, 56% complete). - Daily Briefing + Daily Digest + Focus, daily 00:05 (nd-daily): CONFIRMED. The 15 November run fired on time and wrote all three payloads at 00:06:51 Dhaka. The Daily payload is dated 2026-11-15 (correct Dhaka date) and is structured (news, science, quotes, brainstorming) with real source links. One quality note: two digest items share the same source URL, and one source (azcentral) answers with a paywall code. - Briefing + Focus midday refresh, 12:05 (nd-focus-midday): SCHEDULED, UNPROVEN. The job exists and is enabled; its first fire is today at 12:05, after this audit. Nothing claims it ran yet. - Messages inbox replies, hourly (nd-messages-hourly): RUNNING, WITH A FLAW. The job fires hourly and messages_last_seen advances (last_id 11). But its replies were pure acknowledgements and implemented nothing; one reply wrongly promised attachment removal after 18 hours (the correct figure is 12). Sami's messages 18 and 19 were still unseen when the audit began. Closed properly in section 5. - Football notices, every 15 minutes (nd-football-notice): CONFIRMED. Runs at :13/:28/:43/:58 succeeded; football_notices_seen updated 00:46 Dhaka; Updates rows for big-team starts and finishes are being inserted (Japan 3-2 Brazil FT, Bangladesh 0-1 Pakistan SAFF semifinal FT). - Football live on Home (ESPN): REALTIME while Home is open. The browser polls ESPN every 60 seconds and skips hidden tabs. SAFF and Bangladesh rows come from TheSportsDB through the 15-minute job, so those refresh at 15-minute cadence, not 60 seconds. That difference is real and was stated to Sami. - Home clock: 1 second, client-side. Confirmed in code. - Messages page: polls every 20 seconds. Confirmed in code. - To-Do done items: hidden 6 hours after completion via the todo_done_at map. Confirmed live: checking a test todo stamped the map, unchecking removed it. - Timezone (Asia/Dhaka): Daily payload date matches the Dhaka date; the daily workout is date-keyed, so yesterday's list never leaks into today (today correctly shows not set until filled); todo expiry math and the overdue-class flag use Dhaka dates. 2. SAMI'S FIVE REPORTED PROBLEMS (Messages, 14 November) - ALL FIXED AND VERIFIED 1. Messages page scrolled on mobile, with a scrollbar beside the chat box. Root cause: the page body used 100vh minimum height, which is taller than the visible area on mobile browsers with a URL bar. Fixed by switching the layout to dynamic viewport height (dvh). Verified on a 390px render: page scroll height equals the viewport, only the thread scrolls. 2. The keyboard return key sent the message on mobile. Fixed: on touch (coarse-pointer) devices Enter now inserts a newline and never sends; sending is the Send button only. Desktop keeps Enter-to-send with Shift+Enter for a newline. Verified in emulation both ways: mobile Enter produced a newline with zero sends, the Send tap sent once; desktop Enter sent once. 3. Sideways scrolling on mobile. Measured every page at 390px and 360px before and after: document width now equals viewport width on all 8 pages (zero horizontal overflow). The only element extending past the edge is Home's decorative background shape, which is clipped by design. 4. Attachments, any file type, working end to end. Added: the paperclip button beside the composer (as in the approved design), a private Supabase Storage bucket (message-attachments, 25 MB cap, any file type), upload through a server route, image previews in the thread, download chips for other files, downloads forced as attachments with nosniff (only images may render inline, for preview). Files are auto-removed after 12 hours (Sami's number; the hourly bot's 18 was wrong): the hourly sync now deletes expired objects and the thread shows an "Attachment removed" placeholder. Verified end to end locally and live: upload, listing, download (200, correct headers), expiry (410 after cleanup), placeholder render. All test files and messages were removed afterwards. 5. Closure: one consolidated reply was posted in the Messages thread listing these fixes, and messages_last_seen was advanced past message 19 so the hourly job does not post another generic acknowledgement. 3. OTHER BUGS FOUND AND FIXED IN THIS ROUND - Home Quick Links showed only 4 of the 6 settled links. Added Watchlist Sheet and Class Routine Sheet (routine sheet ID verified live through the Sheets API). All 6 now render. - Unsafe link handling: stored source URLs were rendered into hrefs without a scheme check, so a javascript: or data: URL could have become a clickable link. All parse points now accept only http/https (updates, reports, daily digest, watchlist, football fallback rows). - Missing input limits: message bodies, todo text, goal titles and notes, food and activity names now have generous length caps; class-status overrides must be one of the seven real statuses; study sessions must carry a real YYYY-MM-DD date and a duration of at most 1440 minutes. Invalid bodies return clean 400s, verified live (never a 500 or a stack trace). - Football followed nations expanded on Sami's orders: Germany, Norway, Japan, Uruguay, Netherlands, Turkey and Turkiye (both spellings, ESPN uses Turkiye), and Croatia were added in both the Home component and the notices script; the sync README was updated to match. Clubs and all coverage rules unchanged. - Security headers were entirely absent. Added via the app config: X-Content-Type-Options nosniff, Referrer-Policy, X-Frame-Options SAMEORIGIN, HSTS, and the X-Powered-By header removed. Verified live on the deployed site. 4. WRITE-PATH TESTS (live, with full cleanup) Every write path was exercised live in 55 checks: todos add/check/uncheck plus the 6-hour stamp, messages post (including Bengali text and HTML, which renders escaped), goals create/edit/delete, watchlist check moving an item into the Checklist section and uncheck restoring it, class-status override, study session logging, and all six health paths (food add/delete, activity add/delete, weight, workout, daily check, daily workout tick). Invalid bodies (missing fields, wrong types, over-limit text, bogus status, bad date) all returned clean 400s. All checks passed; the only first-run failures were wrong field-name assumptions in the test script itself, each re-run against the route's real contract and passing. Every test artifact was deleted and every touched payload was restored to its exact pre-test state. 5. SECURITY FINDINGS CRITICAL (open, needs Sami's decision): the site has no login gate yet, and every write API executes with the Supabase secret key, which bypasses row-level security. Anyone who learns the URL can read everything and write almost everything: post, edit or delete goals; add and check todos; post messages forged as Sami (the messages route hard-codes from_sami true); set class-status overrides; log study sessions; toggle watchlist items; add or delete food and activity entries; set weight, workout, daily check and daily workout state. The new attachment upload is likewise unauthenticated (25 MB cap, private bucket, files served only through the download route, auto-deleted after 12 hours). Full route inventory: POST /api/goals, PATCH and DELETE /api/goals, POST /api/todos, PATCH /api/todos/[id], POST /api/messages, POST /api/messages/attachment, POST /api/pending-overrides, POST /api/study-sessions, POST /api/watchlist-done, POST and DELETE /api/health/food, POST and DELETE /api/health/activity, POST /api/health/workout, POST /api/health/weight, POST /api/health/daily-check, POST /api/health/daily-workout. The fix is the Cloudflare Access gate, which is blocked on two actions only Sami can take: enable Zero Trust on his Cloudflare account, and name the Google account to allow. Until then the address must not be shared. HIGH (open, same root cause): all reads are equally public, including up to 200 messages, todos, goals and the aggregated search endpoint. MEDIUM: no rate limiting anywhere (length caps added this round reduce, but do not remove, spam and storage-exhaustion risk). Shared content payloads are updated read-modify-write without locking, so two writes in the same second can overwrite each other; no incident observed. A Content-Security-Policy header is still missing; it needs a careful pass against the app's inline styles before it can be added safely, so it was deliberately not bolted on. LOW: next.config still allows remote images from any hostname, but no next/image component is used anywhere (posters are plain img tags), so the rule is dormant. The Supabase project reference inside wrangler.jsonc is an identifier, not a key. CLEAN, verified: the Supabase secret key appears in no client-shipped JavaScript chunk and nowhere in the repository (all 14 live chunks and the repo were grepped; .env files are placeholders and gitignored). No PostgREST injection: filter values are hard-coded or encoded, and search filtering happens in memory. No dangerouslySetInnerHTML anywhere; all user text is React-escaped (proven live with a script-tag message). Error responses are fixed generic strings. Dependency audit could not be completed: the npm registry audit endpoint refuses this sandbox (403), and the offline audit has no advisory database. Versions are current (Next 16.4.6, React 19.3.0); re-running npm audit from an allowed network remains an open item. Nothing was upgraded blind. 6. LINK SPOT-CHECK 13 outbound links sampled: all 6 Quick Links, 3 watchlist IMDb links, 3 Daily source links, 1 football Updates link (Prothom Alo, 200). No dead links. Notes: three Google Sheets answer 401 to anonymous requests because they are private (they open normally when signed in); IMDb answers 403 to bots (the title IDs are valid and open in a browser); one Daily source is paywalled (402). 7. WHAT WAS DELIBERATELY NOT DONE The Access gate (Sami's two actions above), a CSP header (needs its own careful pass), rate limiting and write locking (both belong behind the gate decision), key rotation (not needed; no leakage found), and the dormant image-hostname rule (no surface uses it). The midday job's first real fire (today 12:05) and a full npm audit remain to be confirmed after this report.