Feat/global widget enhancements 1#118
Conversation
Three hand-crafted first-touch Q&As on the homepage empty-state: - What does keepsimple actually make? - How is this project completely free? - Where do I start if I'm new here? Each renders a finished Turn locally with pre-canned answer + 3-4 nominated cards — no LLM call, no retrieval. Carve-out documented in docs/widget-architecture.md; the normal concierge pipeline keeps running everywhere else (free-form questions, follow-ups, non-home pages). Widget's in-panel UXCAT "Begin Test" CTA now mirrors the in-page CTA: when anonymous, dispatches ks-aux-request-login event instead of bouncing through /uxcat/start-test → /uxcat. UXCatLayout listens and opens its LogInModal directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Picked-card visual emphasis (orange border, tinted background, "Your pick" badge) duplicated the host-page glow that already fires on the same click. Removed the rendering + orphan CSS rules. The c.picked data stays in flight — the server still gets last-pick context. Curated UX Core blurb in pageIdentity.ts claimed "200+ entries"; the real number is 100+. Updated en + ru so concierge landing turns stop overstating it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Starter chips were dumping the full pre-canned answer in one frame — read as obviously fake. Now: 750ms think-beat with the caret, then the answer types in chunks (~3 chars / 14ms), cards land at the end. Matches the feel of the real concierge stream so the carve-out reads like the agent thinking, not a static FAQ. Bold text inside the answer (e.g. "UX Core" in the new starters) was inheriting #1f1d1a from .ks-aux-a strong, invisible on the dark panel background. Added a dark-mode override using the same cream emphasis tone used elsewhere. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
750ms read short compared to the real concierge round-trip; 1.5s lines the carve-out up with live LLM tempo so the visitor can't tell which path served them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously only the homepage starters faked a typing stream; the real concierge answer and the landing turns either burst (server chunks arrive in chunks, not chars) or popped in fully formed. Visitor could tell which path served them — starters felt alive, everything else felt static. Extracted createTypewriter() inside the component — accepts a target via push() (grow as chunks arrive, or set once for pre-canned text) and finalises via finish() once displayed catches up. Routed through it: - runQuery: server chunks land on push(), finish() attaches cards. - runStarter: refactored to use the same helper (one tempo source). - fireOrganicLanding + pendingLanding effect: text reveals at the same typewriter tempo instead of slamming in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Q1 starter ended with a sentence enumerating every sub-project — too much text on first contact. Trimmed to the flagship UX Core line in both en and ru. Starter think-beat 1.5s → 2.2s. Matches the real concierge round-trip better when the visitor has just clicked something for the first time. Card hover (light + dark): dropped the orange border + box-shadow + translateX. The host-page glow on the matching tile already says "this is the destination" without the widget echoing the same accent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
findHostMatches did a document-wide querySelectorAll for matching hrefs, which swept in the widget's own card <a> elements and got them painted with the host-page glow class (orange border + breathing shadow). The visitor saw all of their own cards lit up as if they were "destinations on this page" — which they aren't; they're the nav surface that points off-widget. Added isInsideWidget(el) — walks parents until any class starting with `ks-aux-` is hit — and filters both anchor passes through it. Card anchors stay calm; real host-page links keep getting the glow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tore these out chasing a phantom — thought the orange highlight on all cards was hover/picked bleed when it was actually the host-page highlighter (findHostMatches) sweeping the widget's own anchors. Real root cause already fixed in c256772. Restoring: - .ks-aux-card:hover (light + dark) — clickable cue. - .ks-aux-card-picked + badge (light + dark) — shows the last card the visitor actually clicked. - yourPick TEXT key (en) + JSX rendering of the badge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds PAGE_LANDINGS table — hand-crafted message + cards for /uxcore, /tools/longevity-protocol, /tools, /ai-atlas, /articles. When the visitor lands on one of these pages (organic nav OR card-click) and hasn't seen its curated landing this session, the widget renders the local turn instead of hitting /api/concierge-landing. Server landing keeps running on every other page. Once-per-session via sessionStorage keyed off canonical pathname — revisits in the same tab get nothing (not curated, not server) so the visitor isn't nagged. Card click into an already-fired curated page silently drops the placeholder. Same typewriter throttle + 2.2s thinking pause as the homepage starters, so the carve-out reads at the same tempo as a live LLM round-trip. Cards retain host-page highlight (findHostMatches still finds matching tiles, including hash anchors like /ai-atlas#terminal). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On UXCG (and any UX Core family page), a "what do I do here" SPATIAL
turn was getting cards to AI Atlas / Longevity / Articles in the
recommendation slot — the LLM picked them because the candidate pool
shipped every surface card regardless of intent, and UXCG-internal
library hits were thin so the off-family surfaces won by default.
Added a family map (UX Core family = uxcore, uxcg, uxcp, uxcat,
uxcore-api; standalones each their own) and a SPATIAL-only filter on
buildCandidates that drops any surface OR library candidate whose top
namespace isn't in the visitor's family. Empty pool → LLM returns 0
cards, which is the right behaviour on a "go deeper here" turn.
GLOBAL turns ("show me articles", "Longevity") are untouched — visitor
intent still wins and cross-project pivots happen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…is")
detectIntent only returned 'spatial' when the visitor named the
current section explicitly ("more biases" on /uxcore). The most common
spatial signals — "what should I do here", "what's this", "show me
around", "это" / "что тут" / "глубже про это" — fell through to
'neutral', which meant the SPATIAL filter on the candidate pool
never fired and AI Atlas / Longevity surface cards kept showing up
on /uxcg "what do I do" turns (Wolf flagged earlier today).
Added GENERIC_SPATIAL regex bank — en + ru, covering this/here/where
am I/what should I do/show me/explain this/walk me through/глубже про
это family. Only returns 'spatial' when the page has a known section
(here !== null), so unknown-page neutrals stay neutral.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On a UXCG question page LightRAG retrieval is often sparse — the page
itself is the answer, neighbouring questions live as separate Strapi
entries that may or may not be indexed. The candidate pool would end
up empty after the family filter, and the visitor got no "go deeper
inside UXCG" path.
New module src/lib/widget/uxcgBridge.ts fetches the full 63-question
catalog from Strapi once per process (en + ru), inverts the
relatedQuestions JSON field into a slug → siblings map, and caches
in-process. Concierge awaits getUxcgBridgeEntry when the visitor is
on /uxcg/<slug> and injects up to 2 siblings as high-score library
candidates so the LLM has real picks even on a cold LightRAG.
Phase 2 will add the question → underlying bias mapping (currently
ambiguous in answer text {{N}} refs and skipped for now).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Panel header now shows "Copilot" wordmark next to the pulsing dot. - Returning-visitor pill copy: "Your copilot is here" / "Ваш copilot тут" — replaces the older "I'm always here" / "Я всегда тут". - Host-page tile highlight: outline trimmed from 6px → 2px; breathe + flash + glow shadow ranges roughly halved. Was overpowering the underlying tile; now reads as a hint, not a megaphone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ngual
Brand-critical "about us" questions ("what is keepsimple", "is it
free", "where do I start", "who's Wolf", "why open-source", "300k
users credibility", "how can I contribute", "how do you make money")
now render a hand-crafted answer locally — no LLM round-trip, no
drift. Same think-pause (2.2s) + typewriter as homepage starters so
the canned path reads like a live response.
Each cluster has a bilingual regex bank + en + ru canned answer + 3
nominated cards. Fires on any page; misses fall through to the real
concierge as before.
Funding cluster reflects Wolf's exact framing: keepsimple is funded
solely from his own pocket. No ads, no paid tier, no investor
pressure.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previous tempo (~215 chars/sec from 3 chars / 14ms) read as firehose, not as someone composing. Dropped to 1 char per 22ms ≈ 45 chars/sec — smooth char-by-char reveal across starters, landing turns, and the real concierge stream. Settle hold bumped to 200ms so cards land slightly later than the last typed character. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When the visitor's query is a how-to-use-the-chat meta question, a
one-word ack ("ok", "thanks"), or pure conversational filler, ship
the prose with no cards. Cards on these turns push the visitor
sideways out of what they're already engaging with.
Two layers:
- LLM-side: explicit ZERO-CARDS rule in EN + RU system prompts.
- Server-side: bilingual keyword detector hard-gates the card-
resolution block (and the bias-mention safety net) when the
query matches a meta pattern, regardless of what the LLM
nominated.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The CTA was gated on `isCurrentSpatial`, so any follow-up nav turn (spurious title swap, in-page hash change, modal route) bumped the "most recent spatial idx" off the curated UXCAT landing and the Begin-Test button disappeared, even though the visitor was still sitting on /uxcat. Stamp `landingKey` on curated landing turns and gate the CTA on `turn.landingKey === '/uxcat' && onUxcatRoot`. The button now stays with the turn that earned it, for as long as the visitor is on the Awareness Test page. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirror every Copilot session into our existing Strapi: - Two new collections, `copilot-sessions` (one row per visitor) and `copilot-turns` (one row per event). Spec lives at `docs/copilot-analytics-strapi-spec.md` — Strapi admin paste-and- create. Both prefixed `copilot-*` and written via a write-only token (`STRAPI_COPILOT_TOKEN`) so existing content is untouched. - Q&A turns logged server-side from inside /api/concierge after the response is built. Fire-and-forget; visitor never waits. - CLEARs and card-clicks posted by the widget to /api/copilot/event via sendBeacon (survives unload), forwarded to Strapi from there. - Auth-link detection on every turn: server reads NextAuth JWT via getToken; on first sighting it stamps linkedUser/linkedAt on the session row AND emits a kind=auth turn at that exact moment, so we can see when in the conversation the visitor signed up. - Thread id: client-side localStorage, rotated on CLEAR, so transcripts naturally split into per-conversation blocks under the same sid. - Inert when STRAPI_COPILOT_TOKEN is unset (local dev). Never queried at build time, so a Strapi outage never blocks deploy. Wolf reads it in Strapi admin, filtered by env=prod for live calibration. No custom admin UI in v1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds src/lib/copilotSafety.ts and wires it into /api/concierge plus the widget-event endpoint. All gates run BEFORE retrieval/LLM so blocked or at-capacity turns cost us nothing. 1. Daily cost ceiling — COPILOT_DAILY_BUDGET_USD (default $5) gates every Q&A turn. Over the cap → polite "at capacity" reply, no LLM spend. UTC midnight reset. In-memory counter (single-replica Contabo container today). 2. Abuse moderation — one free OpenAI omni-moderation call per question. Flagged → polite refusal, no LLM spend, blocked turn logged with meta.blocked=true. Fails open when the key is missing or the API is down. 3. Prompt-injection hardening — visitor / page / history blocks fenced as <question>/<page>/<pageContent>/<history>; new INSTRUCTION SAFETY rule in EN + RU system prompts treats anything inside those fences as DATA, never instructions. 4. PII scrub on Strapi log — emails, phone-shaped runs, long digit sequences masked before they reach Strapi from /api/concierge or /api/copilot/event. All four are env-tunable and degrade gracefully when dependencies (OpenAI key, Strapi token, budget vars) are missing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Long-form draft on building the Copilot widget — four criteria (high fidelity, friendly, dirt-cheap, unique) and how LightRAG + Sonnet 4.6 landed there. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gres Strapi was wrong for this — at our user base copilot-turn would balloon past 100k rows per week and the admin panel would become unreadable. Move the analytics sink to the copilot-events sibling service (Postgres 16, HTTP ingest at /track), and expand the event taxonomy to cover visitor MOVEMENTS, not only Q&A. - src/lib/copilotAnalytics.ts: thin HTTP client to copilot-events POST /track; uses COPILOT_EVENTS_URL + COPILOT_EVENTS_WRITE_TOKEN; same public exports (ensureSession, logTurn, markAuthLink, bumpThread) so call sites in /api/concierge + /api/copilot/event are untouched beyond comment refresh. Inert when env unset. - src/pages/api/copilot/event.ts: accept new kinds (page_view, dwell, outbound_click) alongside existing clear / card_click / nav. - widget: every page entry fires page_view; every exit fires dwell with ms-on-page (sealed=true on unload, uses sendBeacon); every outbound anchor click fires outbound_click with href + anchor text + target. - docs: delete copilot-analytics-strapi-spec.md, add copilot-analytics-spec.md (Postgres-shaped); rewrite Analytics section of widget-architecture.md SSOT. KeepSimple repo gains zero Postgres deps — DB lives in sibling container; we're a thin HTTP client. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Wolf wanted a way to actually see what's landing in the copilot-events store without a Postgres CLI. Two server-rendered pages, hard-gated by NEXT_PUBLIC_ENV — staging and prod return 404, period. - /admin/copilot-sessions: recent sessions table (env tabs dev/staging/ prod, last 100), columns: started, sid, lang, event count, thread count, linked user, first URL. - /admin/copilot-sessions/[sid]: per-session detail — header metadata + chronological event ribbon with human-readable bodies per kind (question/answer transcripts, card-click target, outbound href, dwell seconds, page entries, auth, clear). - src/lib/copilotEventsRead.ts: thin server-side client to the copilot-events GET endpoints; uses a separate COPILOT_EVENTS_READ_TOKEN. Token never crosses to the browser — all fetches happen in getServerSideProps. Inert (404) when the token isn't set, so the absence of read access on staging/prod is the safe default. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Header grid had four columns (dot, brand, immersion, CLEAR) but the collapse chevron was a fifth child — grid spilled it onto a second row, eating vertical space the chat needs. Bump grid-template-columns to five and the chevron sits next to CLEAR as intended. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The detail page shows "Session not found" without saying why. When the list works but the per-sid endpoint fails, the cause is ambiguous (token? CF route? service error?). Surface the actual fetch outcome — URL hit, HTTP status, body snippet — under the empty card so the mismatch is debuggable from the page itself. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-session puzzle Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Next.js was silently dropping the session + events props because the object graph from the copilot-events service contained a value Next's prop serializer wouldn't accept (typically a Date or an undefined- nested field). Round-tripping through JSON.stringify yields a clean plain-object/array that the framework forwards unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Next.js kept dropping the session object prop between getServerSideProps and the page component even after a JSON.parse(JSON.stringify(...)) sanitize. Bypass the framework's prop serializer entirely by stringifying the whole result into a single 'payload' string prop, then parse on the page. Pre-build is happy, and the actual transcript renders. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The copilot-events service auto-creates the session row on the first event of any kind via INSERT ... ON CONFLICT COALESCE, so a dedicated session_start write was pure noise — every visitor was racking up dozens of them in the timeline. Carry lang / userAgent / firstUrl on every track call instead; the service still seeds the session-row metadata from whichever non-null value lands first. - session_start removed from EventKind union. - ensureSession() kept as a no-op shim for any future caller. - Question / answer / card_click / nav / page_view / dwell / outbound_click now all forward the session-row seed fields. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Writer side stopped emitting session_start (1a4bceb), but historic events still sit in the DB and were rendering as empty rows with just the timestamp + kind label. Filter the event list before render so the timeline is clean for sessions that pre-date the writer fix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dwell was firing wall-clock time on a tab that may have sat hidden in the background for hours, producing readings like "spent 7400s on Copilot sessions — admin". And it was double-firing on title-only DOM mutations + on both beforeunload and pagehide. Reshape: - Dwell now counts only the time document.visibilityState === 'visible'. Title-only changes no longer trigger a dwell (URL must actually change). Payload field renamed dwellMs → activeMs. - New event kind 'tab_close' replaces the old "sealed dwell" boolean. Single-fire guarded by a sealedRef so beforeunload + pagehide don't duplicate. - Admin renderer detects tab_close → next live event ≥ 60s and inserts a "↺ returned 2h 14m later" gap banner. - Legacy rows still render: dwell falls back to dwellMs when activeMs is absent. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The /api/copilot/event handler has an explicit switch over event kinds; tab_close was missing, so the widget's new tab_close events were 204'd but never forwarded to the copilot-events service. Add it next to dwell. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
@claude, review this PR. |
|
Claude finished @MaryWylde's task in 2m 52s —— View job PR Review: Feat/global widget enhancements 1Tasks
This is a substantial PR (4027 additions) that introduces a full analytics pipeline, safety layer, admin observability UI, and major widget enhancements. The overall architecture is solid and well-commented. Below are issues found, grouped by severity. 🔴 Bugs / Type Errors1.
export type CopilotEventKind =
| 'clear' | 'card_click' | 'nav' | 'page_view'
| 'dwell' | 'outbound_click' | 'auth_probe';But 🟠 Style/Lint Violations (repo rules)2. Wrong import order in The reorder in This is backwards and will likely fail the ESLint CI check. 3. Relative imports instead of
import { logTurn, markAuthLink } from '../../lib/copilotAnalytics';
import { ..., scrubPii } from '../../lib/copilotSafety';The repo requires path aliases for cross-folder imports. The new 🟡 Code Quality / Maintainability4.
5.
6.
7.
🔵 Architecture / Design Notes8. In-memory budget counter won't survive restarts
9. The file has grown substantially (was ~800 lines before this PR, now ~1900). The new functions 10. Cookie missing
`${COOKIE_NAME}=${sid}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${COOKIE_MAX_AGE}`The 11.
✅ What's Working Well
Summary: The critical fix needed before merging is the |
| }; | ||
| let inFlight: Promise<void> | null = null; | ||
|
|
||
| async function fetchQuestionsForLang( |
There was a problem hiding this comment.
@claude shouldn't this be in api/ folder?
There was a problem hiding this comment.
Claude finished @MaryWylde's task in 34s —— View job
No — src/lib/widget/uxcgBridge.ts is correctly placed. Here's why:
Why it shouldn't be in src/pages/api/
src/pages/api/ is reserved for Next.js API route handlers — files that export a default handler(req, res) function that the framework routes HTTP requests to. If uxcgBridge.ts were placed there, Next.js would try to expose it as an HTTP endpoint (e.g. GET /api/uxcgBridge) and serve a 500 because there's no default export handler.
uxcgBridge.ts is a server-side library module, not a route handler. It:
- Fetches and caches UXCG question data from Strapi at process startup
- Exports
getUxcgBridgeEntry(slug, lang)— a utility function consumed byconcierge.ts - Holds module-level in-memory state (
cached,inFlight) that would be wrong to expose as an HTTP endpoint
The right analogy: copilotAnalytics.ts and copilotSafety.ts live in src/lib/ for the same reason — they're server-side utilities consumed by API routes, not routes themselves.
Is src/lib/widget/ the right sub-folder?
That's a reasonable follow-up question. It's currently grouped with pageIdentity.ts under src/lib/widget/, which is fine since it's widget-context logic. An equally valid home would be src/lib/ root (alongside copilotAnalytics.ts) since the module serves the concierge route, not the widget bundle. Either location works — the important thing is it stays out of src/pages/api/.
| margin: 40px auto; | ||
| padding: 0 24px; | ||
| font-family: | ||
| -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
There was a problem hiding this comment.
@claude, what is this? Do we have this font?
There was a problem hiding this comment.
Claude finished @MaryWylde's task in 1m 6s —— View job
Tasks
- Gather context
- Read
index.module.scssto identify fonts at lines 6 and 96 - Check if those fonts exist elsewhere in the project
- Post findings
Fonts in index.module.scss
Line 6 — font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
This is the standard system UI font stack — not a custom font and not something that needs to be installed or imported. It resolves to whatever the OS provides:
| OS | Font rendered |
|---|---|
| macOS / iOS | San Francisco (-apple-system / BlinkMacSystemFont) |
| Windows | Segoe UI |
| Android / Linux | Roboto |
| Everything else | fallback sans-serif |
Zero new dependencies — no @font-face, no Google Fonts, no file download.
Line 96 — font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
Same idea — a system monospace font stack:
| OS | Font rendered |
|---|---|
| macOS (modern) | SF Mono (ui-monospace → SFMono-Regular) |
| macOS (older) | Menlo |
| Everything else | fallback monospace |
Again, zero new dependencies. Also used consistently at lines 131, 205, and 228 in the same file for monospace-formatted content (session IDs, event payloads, etc.).
Are these consistent with the project?
The main site uses custom loaded fonts (Sarabun, Aldrich, Tomorrow, Cormorant Garamond, Source Serif, etc.) registered via @font-face in globals.scss. The admin pages deliberately don't use those — system fonts are correct here because:
- The admin pages are dev-only internal tools (
isDevHost()gate) — they don't need to match the public brand aesthetic. - System fonts load instantly, have no FOUT risk, and are more readable at small UI sizes (13px table rows, 12px mono labels).
- This is the same pattern GitHub, Vercel, and most dev-tool UIs use for their dashboards.
Short answer: both fonts are fine and require nothing extra. They are intentionally different from the site's brand fonts, which is the right call for an admin tool.
| } | ||
|
|
||
| .mono { | ||
| font-family: ui-monospace, SFMono-Regular, Menlo, monospace; |
There was a problem hiding this comment.
@claude, do we have this font, or is it something extra added?
There was a problem hiding this comment.
Claude finished @MaryWylde's task in 1m 0s —— View job
Font check: index.module.scss lines 6 & 96
Tasks
- Read
index.module.scss - Check project font definitions
- Post findings
Both fonts referenced in this file are OS/browser system font stacks — no installation, @font-face declaration, or import is needed. They are not extra dependencies.
Line 6 (.wrap):
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;This is the standard system UI font stack — resolves to San Francisco on macOS/iOS, Segoe UI on Windows, Roboto on Android. It's a CSS convention, not a custom font.
Line 96 (.mono):
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;This is the standard system monospace stack — resolves to SF Mono on macOS, Cascadia Code or Consolas on Windows. Again, no file needed.
Neither font needs to be added to the project. They're zero-overhead system fonts.
That said, these are not from the keepsimple design system. The rest of the site uses custom fonts declared in src/styles/globals.scss (Sarabun, Source-Serif, IBM Plex Mono, etc.). The admin page bypasses those and uses raw system fonts — which is intentional since the admin pages are dev-only internal tools (isDevHost() guard) and don't need to match the public-facing design system.
If you'd prefer the admin pages to use the project's design tokens (e.g. IBM Plex Mono for monospace, Sarabun for body), that would be a style consistency improvement but is not a bug.
- widget/api.ts: add `tab_close` to CopilotEventKind, drop unused `auth_probe` ghost entry that had no handler. - concierge.ts: rewrite relative imports to use `@lib` aliases; extract `isMetaTurn`, `PROJECT_FAMILIES`, `topSegment`, `familyOf`, `inSameFamily` to `src/lib/widget/conciergeHelpers.ts`. - copilotAnalytics.ts: delete `ensureSession` no-op shim — it had no callers and silently misled anyone who tried to use it. - copilotEventsRead.ts: drop `READ_LIB_REVISION` debug constant and its admin-page usage; document `getSessionDetail` debug field as admin-only since it embeds the internal service URL. - copilotSafety.ts: document the daily budget cap as a *soft* in-process counter that resets on container restart. - copilot/event.ts + concierge.ts: add `Secure` flag to the session cookie when the request is HTTPS (detected via x-forwarded-proto or socket.encrypted); kept off for local http dev. - .gitignore: ignore `docker-compose.dev.yml`, `docker-compose.override.yml`, and `/docs/` going forward. Remove `docker-compose.dev.yml` from tracking (was committed by accident on this branch). - AGENTS.md: add "Commit Hygiene" section so future agents audit `git status` before pushing and drop personal/local files instead of carrying them into PRs. UXCatLayout import order: verified clean against `eslint-plugin-simple-import-sort` config (no reorder needed; review's claim was based on a different ordering than what the rule actually enforces). `pageIdentity.ts` "100+ biases" copy: matches the canonical description used elsewhere in the project, no further change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
No description provided.