Featuremeetings8:39 PM Β· ZRosserMcIntosh
full-screen room, robust camera/mic, broadcast subtitles, per-viewer minutes translation
- /admin/meetings/[id] now bypasses the admin shell (no sidebar/header squeezing the room). Minutes pages still render with the shell.
- 'Start now' opens the meeting in a NEW TAB so the operator's admin context isn't lost. List rows: LIVE β new tab; ENDED β /minutes inline.
- Camera/mic/screen toggles now wait for ConnectionState.Connected before publishing, and surface specific errors: NotAllowed β 'Permission denied. Check browser settings.' NotReadable β 'Device in use by another app.' NotFound β 'No camera/mic found.'
- Fixed translation response key mismatch (res.translated β res.text) so live subtitles actually appear in the room.
- HOST-BROADCAST SUBTITLES: when any participant clicks the Languages button, a LiveKit data-channel message tells every other participant to enable their own per-viewer subtitle translation. Each client translates into THEIR OWN preferredLanguage β nobody loses context.
- Summary always renders in the host's (meeting.defaultLanguage) tongue.
- New <MinutesTranslator/> client widget detects when the viewer's browser locale differs from the host's, and offers a one-click 'Translate for me' that translates summary + decisions + action items + open questions + risks + follow-ups in parallel.
- Repo is private β copy fixed: 'public GitHub repository' β 'private GitHub repository'. JSDoc updated to require a fine-grained PAT with read-only Contents access.
- 'Updated every 5 minutes' β 'Refreshed continuously'. ISR cache bumped from 300s to 3600s now we no longer need the unauth IP limit.
- Verified line-count: regenerated repo-stats.json reports 865,014 lines across 3,174 files. The ~19k delta vs Codex (884k) is exactly pnpm-lock.yaml, which we deliberately exclude (it's vendored noise, not 'work shipped'). prebuild hook ensures fresh stats every deploy.
- Summarize: Anthropic claude-sonnet-4-5 primary, OpenAI gpt-4o fallback.
- Translate: OpenAI gpt-4o-mini with MeetingTranslationCache for cost control. Cached results return in ~5 ms; first viewer pays the LLM cost, every subsequent viewer is free.
Featuremeetings8:22 PM Β· ZRosserMcIntosh
LiveKit webhook handler β room_finished auto-ends + AI minutes, participant_left stamps leftAt
Updatemeetings8:11 PM Β· ZRosserMcIntosh
restore meeting-room-client export (was empty after manual edit)
Featuremeetings7:56 PM Β· ZRosserMcIntosh
Apple FaceTime redesign + fix camera/disconnect bugs
- Complete room client rewrite: FaceTime-style dark UI, auto-hide controls
- Fix: remove video/audio props from LiveKitRoom (eliminates disconnect race)
- Fix: AI Notes off by default (eliminates Chrome/Edge error on join)
- Fix: custom CtrlBtn replaces LiveKit ControlBar (CSS conflict)
- Fix: endingRef pattern for stable onDisconnected closure
- Remove pre-meeting language dropdowns β browser auto-detect for both spoken + subtitle lang
- onDisconnected: 5s grace period + keepalive /leave POST
Featuremeetings7:20 PM Β· ZRosserMcIntosh
guest invite system + K99 API key structure
- MeetingInvite table (add-meeting-invites.sql) β signed tokens, 8h expiry
- POST /api/admin/meetings/[id]/invite β creates invite, sends Brevo email or returns link
- GET+POST /api/guest/meetings/[token]/join β public, no auth; validates token, issues LiveKit token
- /g/[token] β public branded guest room (pre-join name screen β LiveKit β left screen)
- meeting-room-client: 'Invite Guest' button opens modal; enter name + optional email; 'Send email invite' or 'Copy link'; guests join without any account
Featuremeetings7:09 PM Β· ZRosserMcIntosh
auto-terminate zombie rooms + force-end + invite link
- New /api/admin/meetings/[id]/leave: stamps leftAt on disconnect, auto-ENDS meeting + tears down LiveKit room when last participant leaves
- New /api/cron/meetings-zombie-sweep: runs every 5min, ends LIVE meetings with no remaining participants after 10min grace; hard-cap at 8h
- Meeting room client: onDisconnected now POSTs /leave with keepalive, so tab-close / nav-away no longer leaves rooms 'Live' forever
- Title bar: 'Invite' button copies meeting link to clipboard (this is how a second user joins β share the URL, they need admin access)
- Meetings list: 'End' button on LIVE rows for force-termination
- vercel.json: registered new cron
Featuremeetings6:39 PM Β· ZRosserMcIntosh
internal video calling infrastructure
- LiveKit room with composable GridLayout + ControlBar + ParticipantTile
- Screen sharing via LiveKit ControlBar (built-in, one click)
- AI Notes toggle: browser SpeechRecognition β LiveKit data channel β subtitles
- Per-viewer live translation via OpenAI gpt-4o-mini (cached per meeting/lang pair)
- Post-call AI minutes: Claude Sonnet 4 (primary), gpt-4o fallback
- 7 API routes: create, list, get, join, transcript, translate, end
- Meetings list page + room page + minutes page
- Sidebar nav wired (Business Tools group)
- EN + PT i18n strings
- Prisma schema (meetings.prisma) + idempotent SQL migration
- @anthropic-ai/sdk installed, env var placeholders added for Anthropic + Deepgram
Update5:32 PM Β· ZRosserMcIntosh
remove icons from metals page + overhaul resolution email template
- Remove Sparkles (amber) and Shield (stone-400) Lucide icons used as bullet markers β they read as decorative emoji noise on the page
- Replace with a typographic em-dash span (text-stone-300, select-none) consistent with the care-tips bullet style already used on the page
- Remove unused { Shield, Sparkles } from lucide-react import
- Remove β
emoji from subject line and email body entirely
- Change subject: '[T-REF] Your support ticket has been resolved' β '[T-REF] Your support enquiry has been resolved β Katura'
- Full HTML email redesign: cream background (#f5f1eb), white card with warm stone border, centered KATURA wordmark in Georgia serif at top, serif body copy, muted uppercase tracking labels, no emoji anywhere
- 'Additional information from our team' detail block restyled in warm stone tones (was a jarring green '#f0fdf4')
- Greeting now uses first name only (recipientName = name.split(' ')[0])
- Professional closing: 'The Katura Team' with support email inline
- Sender was already support@katura1999.com β confirmed correct
Feature5:13 PM Β· ZRosserMcIntosh
support escalation chain, bell notifications, settings page, stats page
- prisma/add-support-it-admin.sql: use public."User" (schema-qualified) β fixes 'relation User does not exist' error in Supabase SQL editor
- prisma/add-share-link-client-view.sql: same fix for JewelryProjectShareLink
- prisma/add-support-escalation.sql: adds isSupportItAdminOrder (INT) to User and escalationLevel (INT) + escalationSentAt (TIMESTAMPTZ) to support_tickets
- auth.prisma: added isSupportItAdminOrder field to User model
- support-tickets.prisma: added escalationLevel + escalationSentAt to SupportTicket
- Finds URGENT tickets still in NEW/OPEN with no assignee
- Emails next IT admin in order every 20 min until all admins contacted
- Includes ticket snippet, reporter, link, and escalation level in email
- Updates escalationLevel + escalationSentAt after each send
- Added to vercel.json crons schedule
- Changed urgent email from 'email all IT admins at once' to 'email only IT admin #1 (lowest isSupportItAdminOrder) immediately; escalation cron handles the rest'
- Added in-app bell notification for ALL staff (OWNER+MANAGER+EMPLOYEE) on every new ticket using createBulkNotifications β bell badge lights up for everyone
- GET: now returns isSupportItAdminOrder in response
- POST: auto-assigns order (appended to end) when enabling IT admin; resets to 0 when removing
- PATCH (new): swap order of two adjacent IT admins (direction: up | down)
- Shows numbered escalation order (1, 2, 3β¦) for each IT admin
- Up/down arrow buttons to reorder β uses new PATCH endpoint
- Amber info box explaining the 20-min escalation chain
- Separate sections: 'Escalation Order' and 'Add to Escalation Chain'
- Dedicated page for IT admin escalation management
- Accessible by OWNER and MANAGER roles only
- Removed ItAdminSettings card (moved to /settings)
- Added β settings icon button β /admin/support/settings
- Added π analytics icon button β /admin/support/stats
- KPI cards: total tickets, open, urgent open, avg resolution time
- Area chart: ticket volume by day with new/resolved/urgent lines + 7d/30d toggle
- Pie chart: priority breakdown (Normal / High / Urgent)
- Bar chart: daily resolved tickets
- Table: per-IT-admin stats (assigned, resolved, resolution rate, avg time)
Feature4:18 PM Β· ZRosserMcIntosh
priority support tickets + IT admin mgmt + labor edit/delete + SQL fix + title edit
- 1. Cmd+/ support form: Normal/Urgent priority selector
- 2. Urgent tickets: fire-and-forget email to all isSupportItAdmin users
- 3. /admin/support: IT Admin notification management panel (add/remove staff)
- 4. New API: GET/POST /api/admin/support/it-admins
- 5. New Prisma field: User.isSupportItAdmin (migration: add-support-it-admin.sql)
- 6. /admin/projects/[id] labor tab: inline edit + delete buttons per item
- 7. New API: PATCH/DELETE /api/admin/jewelry-projects/[id]/labor/[itemId]
- 8. /admin/projects/[id]: inline title edit (pencil icon in header)
- 9. Fix SQL migration: SET search_path = public in add-share-link-client-view.sql
- 10. /education/metals: confirmed zero emojis β page is clean (uses Lucide icons)
Feature3:50 PM Β· ZRosserMcIntosh
Client View Mode for project share links
- Add isClientView, clientName, clientEmail, requestApproval, approvalStatus, approvalNotes, approvalSubmittedAt columns to JewelryProjectShareLink (prisma schema + SQL migration)
- STLViewerCanvas: add initialShowGrid + hideGridToggle props
- Admin share dialog: Client View Mode toggle, client name/email inputs, requestApproval switch, Send Email button, approval status badges
- Public share GET API: return new client view fields
- Admin share-links POST API: accept new fields, force-disable download/upload/costs/customer when isClientView
- New approval endpoint: POST /api/share/projects/[token]/approve
- New send-client-email endpoint: POST /api/admin/.../share-links/[linkId]/send-client-email
- Public share page: ClientViewPage component (dark Katura-branded layout, no grid/costs/downloads, approval panel, comments)
Update3:04 PM Β· ZRosserMcIntosh
emails stuck as SCHEDULED β only treat scheduledAt >30s in future as a real schedule
Update2:59 PM Β· ZRosserMcIntosh
wrap email tracking tooltips in TooltipProvider β fixes sent-box crash
Security9:53 AM Β· ZRosserMcIntosh
harden public share-links and support intake
- New rate-limit buckets in src/lib/rate-limit.ts:
- share-view 30 reads/min (per IP+token)
- share-password 5 attempts/15min (per TOKEN β prevents IP rotation from bypassing the brute-force lock)
- share-comment 15 comments/hr (per IP+token)
- share-upload 5 uploads/hr (per IP+token)
- GET /api/share/projects/[token]
- Token shape sanity check (length 16-200) short-circuits junk before DB
- Per-IP+token rate limit on view (anti-scrape / anti-enumeration)
- Per-token brute-force lockout on wrong passwords (keyed by token so distributed attacks across rotating IPs still get capped)
- POST .../comments
- Content-Length pre-check (256 KB cap) and content-type guard
- Per-IP+token rate limit (anti-spam + anti-email-amplification: every comment fans out notifications to project subscribers)
- Honeypot field (`website`) β silently accept-and-discard so bots don't learn they were caught
- Length caps: 5K message / 120 name / 254 email / parent-id 64
- Strict attachment-shape validator: max 10, key allowlist ({url,name,mimeType,size}), numeric range checks; reject the whole request on any deviation rather than silently dropping fields
- Parent-comment lookup now also filters deletedAt to prevent threading replies under a tombstoned comment
- POST .../cad-files
- Token shape sanity check
- Content-Length pre-check (50 MB) BEFORE request.formData() β without this an attacker could stream a multi-GB body and pin a worker
- Per-IP+token rate limit (anti storage-cost-DoS)
- Extension allowlist (STL/STEP/OBJ/3DM/IGES/GLB/etc. + jpg/png/pdf/zip); blocks executable/script payloads (.exe, .html, .htm, .svg, .js, .sh, .bat, .ps1, .php) which would otherwise be served back via the Supabase public URL and enable HTML smuggling / stored XSS
- Filename sanitization: strip CR/LF/NUL, leading dots/slashes, and collapse path separators β defends against path traversal in both the storage key and the activity-log metadata
- Reject empty (size === 0) files
- Identity-spoofing fix: when the request is authenticated, force reporterEmail and fromName from the SESSION rather than the request body. Previously a logged-in user could submit a ticket claiming to be from victim@example.com, which would set Reply-To on the support notification email to that address β tricking internal staff into replying to the wrong person (or into a phishing thread).
Perf9:34 AM Β· ZRosserMcIntosh
cut build time β cpus: 2, optimizePackageImports (lucide-react etc), expand serverExternalPackages, Sentry widenClientFileUpload: false, .vercelignore
Feature9:30 AM Β· ZRosserMcIntosh
header β Education moved left, per-trigger dropdowns (viewport=false), frosted glass menus
Feature9:25 AM Β· ZRosserMcIntosh
software-updates enhanced hints β 'Fix' β 'Update', dynamic LOC-to-years, AI hours-to-days, Bible equivalent calculations
Feature9:18 AM Β· ZRosserMcIntosh
software-updates 3-row stats grid β DB models, API endpoints, translations, 10^234 permutations + $250/hr rate
Feature9:08 AM Β· ZRosserMcIntosh
software-updates reorder stats grid + add Characters card (32.7 Million format)
Update9:04 AM Β· ZRosserMcIntosh
software-updates show exact line count with commas, not abbreviated K notation + refresh stats
Feature9:00 AM Β· ZRosserMcIntosh
tasks Asana overhaul β dark mode, animations, inline editing, completed section
Feature8:52 AM Β· ZRosserMcIntosh
support UX β desktop notifications, dialog fixes, close+notify flow
Feature8:40 AM Β· ZRosserMcIntosh
support ticket security hardening + storage retention + redirect fix
- ticket-actions.tsx: resolve/close redirect β /admin/support (open queue) instead of /admin/support/archive
- /api/support/ticket: add 10 MB body-size guard (413), content-type guard (415 for unexpected types), dedicated 'support' rate-limit bucket (5 req/hr) for both per-IP and per-email axes
- rate-limit.ts: add 'support' bucket at 5 req/hr (stricter than shared 'contact' bucket at 10 req/hr, independent so tuning one doesn't affect the other)
- cron/support-cleanup: daily 03:00 UTC cron to enforce retention policy: screenshots purged 90d post-resolution (180d from creation if still open) ticket PII anonymised 2 years after closedAt / resolvedAt; row kept forever
- vercel.json: register /api/cron/support-cleanup at '0 3 * * *'
Feature8:33 AM Β· ZRosserMcIntosh
calendar reminders, visibility, animations, dark mode + Google Calendar plan
- calendar/page.tsx: add email reminder multi-select UI (15min/1hr/4hr/24hr preset chips, stored as [{method:'email',minutes:N}] JSON); add event visibility selector (Default/Public/Private) with lock icon; fix date/time input dark mode (dark:[color-scheme:dark]); beautify event chips with hover scale + shadow transitions; animate schedule view date groups with fade-in/slide-in stagger; richer loading state; popover gets fade+zoom entry animation and rounded-xl; lock icons on PRIVATE events in time grid and schedule view; reminder summary shown in EventPopover with sent-checkmark once processed by cron
- cron/calendar-reminders/route.ts: new cron route runs every 10 min; finds events where reminder fire-time just entered [now-11min,now+1min] window and reminder.sentAt is null; sends polished HTML email via Resend to creator + attendees; marks sentAt in the event JSON field (no migration needed); guards via CRON_SECRET or x-vercel-cron header
- vercel.json: add */10 * * * * cron for calendar-reminders
- docs/GOOGLE-CALENDAR-INTEGRATION.md: full plan covering GCP OAuth setup, schema additions, pull/push sync engine, conflict resolution, security (token encryption, rate limits, webhook alternative), and settings UI spec
Feature8:17 AM Β· ZRosserMcIntosh
Katura/K99 labels, messages URL routing, calendar management + privacy
- layout.tsx: replace Store/Rocket with Gem/Layers icons; label 'Katura' and 'K99'
- messages: add /admin/messages/[slug] route; wire initialSlug through MessagesPageClient β Messenger; selectChannel() syncs URL on every selection; auto-select channel from slug on initial load (@firstName for DMs, name for groups)
- calendar API: add DELETE /api/admin/calendar/calendars endpoint (owner or admin can delete; default calendar protected); add PRIVATE event privacy filter to events GET (only creator sees PRIVATE events)
- calendar UI: add deleteCalendar() + createSharedCalendar(); sidebar shows Γ delete buttons on hover; Settings dialog overhauled with personal/shared sections and Add-calendar forms
Feature7:56 AM Β· ZRosserMcIntosh
design page overhaul + support page perf
- Replace emoji folder icons with Google Drive-style SVG folders (blue, rounded body + tab, open animation on drag-over)
- New FileTypeIcon SVG component: color-coded by file type (purple for STL/OBJ/FBX, teal for STEP/IGES, red for PDF, amber for DWG/DXF etc.) with a rendered 3D cube for 3D formats instead of text label
- Image thumbnails: 3D-preview hover overlay (box icon) on image cards
- Inline rename: click the pencil icon on any file or folder to rename in-place; Enter confirms, Escape cancels; calls new PATCH /api/admin/cad/rename
- Internal drag-and-drop: file cards now have draggable=true; dragging a card onto a folder moves it (calls rename API) rather than uploading; external OS file drops still work as before
- Rename API: new /api/admin/cad/rename route using Supabase .move() for files and list+move for folder renames
- Add select{} to findMany so only displayed columns are fetched (removes loading of screenshot blobs, metadata JSON, and other unused heavy fields)
- Drop the double orderBy [status, createdAt] in favour of just createdAt desc (status is already filtered; simpler sort = faster)
- resolve redirect: router.push -> router.replace (cleaner history stack)
Update7:39 AM Β· ZRosserMcIntosh
email privacy, shared mailbox sorting, settings dialog, Cmd+/ on email pages
- Scope personal email accounts to the authenticated user only (no more cross-employee inbox leakage)
- Sort shared mailboxes by received message volume descending (busiest first)
- Replace non-functional settings gear (β legacy view) with a real EmailSettingsDialog: per-account display name, signature, auto-reply toggle/subject/body, and alias address listing; PATCH hits the existing /api/admin/email/accounts endpoint
- Guard the email-shell '/' keydown handler with metaKey/ctrlKey check so Cmd+/ always passes through to GlobalSupportHotkey
- Increase GlobalSupportHotkey screenshot timeout 3s β 6s so heavy pages (email thread view) have time to capture before the safety fallback fires
Feature7:19 AM Β· ZRosserMcIntosh
LiveKit VOIP + video calling β internal team + client consultations
- Video button now starts a LiveKit call (inline modal) instead of opening Jitsi
- Added call state: activeCall, incomingCall, isStartingCall
- Incoming call toast: shows for all channel members when someone starts a call
- CallModal is lazy-loaded (ssr:false) so it doesn't bloat the initial bundle
- handleStartCall: POST /api/admin/messenger/call action=start
- handleJoinIncomingCall: POST /api/admin/messenger/call action=join
- Supabase realtime subscription for livekit_call system messages
- Checks elapsed call time every 60s
- At 4h: shows StillHereModal with 5-minute countdown
- Any participant who doesn't click 'I'm still here' is disconnected via room.disconnect()
- Confirming resets the 4-hour clock from zero
- Admin: POST /api/admin/consult/create -> { consultUrl, roomName }
- Share the consultUrl with the client (email, WhatsApp, etc.)
- Client visits /consult/[roomToken] -> enters name -> joins call
- Admin starts same room from Messenger to connect
- No client account required. E2E encrypted via LiveKit WebRTC.
- Added livekit-server-sdk to serverExternalPackages (Node.js crypto APIs)
Update6:52 AM Β· ZRosserMcIntosh
dark mode star colors in reviews (admin + storefront)
- admin/analytics/reviews: stars now fill amber-500/400 instead of black
- admin/analytics/reviews: rating bar background uses stone-800 in dark, bar fill amber
- product-reviews.tsx: storefront star rating now uses amber instead of fill-black
Feature6:39 AM Β· ZRosserMcIntosh
BRL entity name, footer sort, admin welcome logo, support queue cleanup
- Show 'KATURA BRASIL LTDA' in copyright line when currency = BRL
- Sort all nav columns (Shop, Love & Engagement, Client Care, Company) by translated label character length at render time β shortest link first, longest last β works correctly for every locale
- Add useCurrency import
- Add Katura header logo above the 'WELCOME, ROSSER' greeting text using /brand/header-logo.png with fade-in animation before the decorative line
- Default tab changed from ALL β NEW (no ?status= param = show new tickets)
- STATUS_TABS reordered: New Β· Open Β· Waiting Β· All (ALL moved last)
- RESOLVED + CLOSED removed from the active queue filter tabs entirely
- Stat cards: RESOLVED + CLOSED merged into single 'Archive' card that links to /admin/support/archive (not a status filter)
- Filter badge row: 'Archive β' link badge added (green) alongside status tabs
- Remove unused CheckCircle2 import
- Both statuses mean the ticket is done β they live in /admin/support/archive
- RESOLVED = staff marked it solved (may auto-close after N days)
- CLOSED = final terminal state β no further action
- Neither belongs in the active queue view
Update6:10 AM Β· ZRosserMcIntosh
support archive crash + header nav reorder
- Remove select of 'subject' field (doesn't exist on SupportTicket model)
- Use 'message' field instead, truncated to 100 chars for display
- Fix groupBy _count: true β _count: { _all: true } (correct Prisma syntax)
- Fix countMap access: c._count β c._count._all (was returning object not number)
- Move Education dropdown from left nav to right nav (slice 0-2 left, 2+ right) β Left: Jewelry, Love & Engagement β Right: Education, Jewel Vox
- Reorder right-side icons: UserMenu β SearchDialog β Wishlist β ShoppingBag (was: Search β Wishlist β UserMenu β ShoppingBag)
Feature6:00 AM Β· ZRosserMcIntosh
footer i18n + expanded admin search
- Add softwareUpdates key to all 23 non-English locale files (fr, de, es, zh, ja, etc.)
- Add whyKatura section (label/promise/customDesign/freeRingSizer/financing) to all 24 locale files
- Replace hardcoded 'Why Katura' bar in footer.tsx with t() calls
- Add searchTeamMembers (staff users: EMPLOYEE, MANAGER, OWNER)
- Add searchServiceRequests (by requestNumber, customerName, itemDescription)
- Add searchCollections (by name, slug, description)
- Add searchEmailTemplates (by name, subject, category)
- Add searchCrmContacts (by firstName, lastName, email, account name)
- Update default types param to include all new content types
- Expand SearchResultItem type union with 5 new types
- Add TYPE_CONFIG entries for all 5 new result types with appropriate icons
- Import UserCog, Wrench, LayoutGrid, Mail, ContactRound, HeadphonesIcon
- Sync AdminSearchResult.type union to match API
- Update default types param string
Feature5:36 AM Β· ZRosserMcIntosh
support archive page, resolve+escalate actions, fix Cmd+/ conflict, support tickets in search
Feature5:25 AM Β· ZRosserMcIntosh
employee default-closed permissions + add support/yen/openclaw/blog/dashboard-financials to ADMIN_PAGES
Update5:10 AM Β· ZRosserMcIntosh
restore GlobalSupportHotkey (accidentally emptied)
Updatesupport4:52 AM Β· ZRosserMcIntosh
replace getDisplayMedia with html-to-image for screenshot capture
- No browser permission prompt
- No picker dialog
- Snapshot is guaranteed to be taken BEFORE setOpen(true) so the modal never appears in the attached screenshot
- Works identically in Chrome, Firefox, and Safari
Updatesupport4:38 AM Β· ZRosserMcIntosh
pin sender to support@, route customer replies to ticket timeline, add audit logs
Updatesupport4:26 AM Β· ZRosserMcIntosh
screenshot captures page behind modal, not the modal itself; fix Firefox
- Two bugs:
- 1. Modal appeared before getDisplayMedia resolved -- setOpen(true) was
- called synchronously in the keydown handler alongside the async capture,
- so by the time the user picked a tab the support-ticket dialog was
- already painted on top of the page. Moved setOpen(true) into the
- .finally() of captureWindowScreenshot() so the modal only mounts
- after the frame is grabbed (or after capture is denied/skipped).
- 2. Firefox threw NotSupportedError on the Chrome-specific options object
- ({ displaySurface, preferCurrentTab }) which made the whole capture
- path silently fail. Fixed with a try/catch that retries with plain
- { video: true, audio: false } -- works in Firefox and Safari.
Featuresupport3:46 AM Β· ZRosserMcIntosh
capture window screenshot on Cmd+/ hotkey and attach to ticket
- When a visitor hits the global Cmd+/ (Mac) or Ctrl+/ (Win) hotkey, we now
- synchronously call navigator.mediaDevices.getDisplayMedia inside the
- keydown handler so the browser allows the screen-share prompt. The user
- picks which window/tab to share (Chrome biases to the current tab via
- preferCurrentTab); we grab one frame via ImageCapture (Chrome) or a
- hidden <video>+canvas fallback (Safari/Firefox), convert to a PNG blob,
- and stop the track immediately.
- The blob is handed to <SupportTicketForm screenshotBlob=...> which now
- posts multipart/form-data when an attachment is present. /api/support/ticket
- detects the content-type, parses the FormData, and after persisting the
- ticket uploads the screenshot to the private Supabase 'support' bucket
- under screenshots/<ticketId>/<rand>.png. The storage path lives on the
- new support_tickets.screenshotUrl column.
- /admin/support/[id] mints a 1-hour signed URL via getSupportScreenshotSignedUrl
- and renders a clickable preview card. Bucket stays private so screenshots
- (which can contain order details, draft emails, sensitive UI) never leak
- via referrer headers or accidental link sharing.
- Failure modes are all silent fallbacks: denied permission, unsupported
- browser (mobile Safari), upload failure -- the ticket is still persisted
- without the attachment and the team can act on it normally.
- Migration: prisma/add-support-screenshot.sql adds the nullable column
- and creates the bucket. Already applied to production.
Updateadmin/support/[id]2:39 AM Β· ZRosserMcIntosh
same RSC fix as list page
- The ticket detail page hit the same crash (digest 3186334352) for the
- same two reasons as /admin/support: it passed icon={LifeBuoy} (a function)
- to PageHeader (a 'use client' component), and it ran a redundant auth
- check that the layout already enforces.
- Fix: drop PageHeader, drop auth/redirect imports, inline a server-safe
- header with breadcrumbs + back-to-queue button. Visual output is the same.
Updateadmin/support2:24 AM Β· ZRosserMcIntosh
inline header to fix RSC serialization crash (digest 3186334352)
- Root cause: PageHeader is a 'use client' component. The support page is a
- Server Component. React 19 / Next.js 16 throws when a function value (a
- Lucide icon component) is passed as a prop across the serverβclient
- boundary β RSC props must be serializable, functions are not.
- Every other admin page is 'use client' so they never hit this boundary.
- The support page was the only server component using PageHeader with an
- icon prop, which is why digest 3186334352 persisted through every other fix.
- Fix: remove PageHeader import and inline an equivalent <div> header that
- renders LifeBuoy directly inside the server component (no boundary
- crossing). Visual output is identical.
Updateadmin/support1:38 AM Β· ZRosserMcIntosh
remove redundant auth check that was crashing the page
- The admin layout (/admin/layout.tsx) already guards every /admin/* route
- with canAccessAdmin() + tenantId checks and redirects unauthenticated or
- unauthorised users before any page component runs. The support page was
- redundantly calling auth() + redirect() again inside its own server
- component.
- In Next.js 16, redirect() inside a server component is a React interrupt
- that cannot safely round-trip through try/catch β even re-throwing it
- causes the framework to surface it as an error boundary hit instead of
- a proper 307 response. This is why digest 3186334352 persisted across
- every attempted fix (migration, re-throw guard, etc.).
- Fix: remove auth() import, redirect() import, and the role check from
- the page entirely. The layout is the single source of truth for admin
- auth. The page now only does DB queries (with their own try/catch) and
- renders the ticket queue.
Perfsoftware-updates1:24 AM Β· ZRosserMcIntosh
paginate to 50/page + keyword search
- Replace single enormous render of all 1,500+ commits with server-side pagination (PER_PAGE=50) driven by ?page= URL param.
- Add keyword search (?q=) that filters across type, scope, title, body bullets, author name and commit SHA β AND-logic, all terms must match. Searches the full fetched history, not just the current page.
- GitHub data fetch keeps next: { revalidate: 300 } so the raw commit list is edge-cached for 5 min; only 50 cards are rendered per request instead of 2,000, making every page load ~30x faster.
- Pagination: Newer / Older links preserve the active search query.
- Search hint text: 'Search across all N updates by feature name, type (feat, fix, perfβ¦), scope, or keyword.'
- fix(admin/support): re-throw NEXT_REDIRECT errors in outer try/catch so Next.js 16 redirect() calls are never swallowed.
Updatesoftware-updates12:32 AM Β· ZRosserMcIntosh
remove burn rate card, no-cents ticker; fix(footer): explore button new tab, remove free trial CTA
Updatesoftware-updates12:01 AM Β· ZRosserMcIntosh
restore LOC-based hour estimates
- The previous commit accidentally changed the hours calculation from
- LOC-based to time-elapsed-based, which collapsed:
- Pre-AI dev hours: 28.4K β 2.5K (wrong)
- With-AI dev hours: 7.1K β 208 (wrong)
- Equivalent cost: ~4.3M β $898 (wrong)
- Correct methodology:
- Pre-AI hours = totalLines Γ· 30 LOC/hr (industry standard for senior
- TS/React engineers without AI tooling)
- With-AI hours = Pre-AI hours Γ· 4 (4Γ AI productivity multiplier,
- per published 2024β2026 enterprise studies)
- Firm cost = Pre-AI hours Γ $150/hr (base, LOC-derived)
- + live ongoing accrual since project start (ticker)
- At 852K lines:
- Pre-AI: 852,810 Γ· 30 = 28,427 hrs
- With-AI: 28,427 Γ· 4 = 7,107 hrs
- Cost: 28,427 Γ $150 = $4,264,050 base + live ticker