Every change we ship to katura1999.com — features, fixes, security patches, the lot. Pulled straight from our private GitHub repository so you can see exactly what was built and when.
By the numbers
Lines of code
943,828
Web platform — TypeScript, React, Prisma, CSS
iOS
79,607
Swift + SwiftUI lines
Android
3,117
Kotlin + Jetpack Compose lines
All platforms
1,026,552
Web + iOS + Android combined (51.0× the King James Bible)
Characters written
40.17M
40,168,559 total characters
Updates pushed
2,265
exact commit count on main
Current version
v1.22.65
build 2265 · d89cabc
Database models
420
across 47 schema files — most SaaS platforms have 20–50
API endpoints
1,052
individually routed — Stripe's public API has ~400
Translated strings
90,960
every string, in 24 languages
System permutations
10^316
2^1,052 endpoint combinations — more than atoms in the observable universe (10^80)
Project age
9mo 16d
since Dec 14, 2025
Pre-AI dev hours
34.2K hrs
1,026,552 lines ÷ 30 LOC/hr — equivalent to 16.5 years (senior engineer, no AI)
With-AI dev hours
8.6K hrs
4× AI productivity multiplier (2024–2026 studies) — equivalent to 1,069 days
Equivalent firm cost
$8,963,537
live ticker · Katura rate: $60 USD/hr
Hours estimated from source line count at 30 LOC/hr (industry benchmark for production-quality TypeScript/React without AI assistance), with a 4× multiplier for AI-assisted development per published 2024–2026 enterprise studies. Equivalent Firm Cost uses a $250/hr loaded billable rate reflecting a premium engineering firm building enterprise-grade SaaS — and ticks up live, because the project is still being actively built.
Commit history
2,265 updates pushed
Showing page 5 of 40 · 201–250 of 2,000 fetched
Thursday, August 20, 2026
7 updates pushed
8:11 PM · ZRosserMcIntosh
jeweler services page at pitch.katura1999.com
Service cards render from SERVICES in lib/k99/capabilities, which declares itself the single source of truth and forbids present-tense promises for anything "rolling_out". The status chip renders next to every card, so the page cannot drift from it. K99 Business Platform correctly shows "Rolling out"; the six sellable services show "Available now".
Prices render from OFFERS in lib/prospecting/config — the same productized catalogue already used in proposals sent to prospects, with real figures, scope and boundaries. Do not hand-write prices on the page; change them there and both stay in step.
The assessment deliverables are derived from the actual generator: evidence categories and sources from JewelerEvidence, findings / quickWins / plan90Day / independentFactCheck from GeneratedProposalV2, and the copy-ready asset + implementation + verify-after steps from ComplimentaryAction, which is gated so no brief ships without at least one verified free action. If the generator changes, change that list.
The four animated demos recreate software that ships today (live meeting translation, CAD shared in-call, in-call invoice + e-signature, and the project workspace using its real status values). Each is labelled an illustration rather than a live session.
throttle the translation tail — the other 5x of the DeepL burn
af432921 stopped the minutes page re-billing whole transcripts. This is the
live-meeting half of the same problem, plus the latency defects found while
measuring it.
DeepL hit 282,000 characters in one day, exhausting the quota and tripping
the circuit breaker nine minutes into a call. Root cause was not the
provider: the same speech was billed five to six times over, because the
incremental translator retranslated the volatile tail on EVERY Deepgram
interim (~3-6/sec). Measured on one 49-minute meeting: 828 chunk requests
for 128 sentences, 21,556 chars of tail churn against 4,674 chars of actual
speech. And that is one browser — each participant translates independently,
so a two-person bilingual call runs two of these.
Tail throttle (incremental-translate.ts): identical tails are deduped,
one request in flight at a time, issues spaced by tailMinIntervalMs
(700ms default), and only the newest pending tail is ever sent. Committed
chunks are untouched, so captions still progress at full cadence — only the
speculative last few words lag. Waives the cooldown right after a chunk
commits, since commits are bounded by speech cadence, not interim cadence.
Finals de-blocked (translate.ts): the MeetingTranslationCache INSERT was
awaited before responding, holding every final caption behind a
cross-region Postgres write (iad1 -> us-west-2). At matched character counts
finals cost a flat +1,450ms over chunks — even 0-15 char finals ran 1384ms
vs 2845ms. Now runs via after(). This matters more since af432921 raised the
persist cap to 1000 chars: 8x more rows written, all of it blocking.
Stale finals can no longer corrupt the live sentence
(meeting-room-client.tsx): finals resolve at p50 2.9s / max 5.7s, by which
time the speaker has moved on. The handler called incrementalRef.reset() and
nulled translationSinkRef unconditionally, wiping the translator mid-way
through the CURRENT sentence and spilling its output into the transient
overlay. 16% of requests (153/956) landed out of order. Now guarded by a
segment token; the transcript update still always runs, since it is
addressed by line id and can only correct its own sentence.
7:44 PM · ZRosserMcIntosh
piece previews in admin + campaign-grade public collection pages
every card now previews its actual pieces — first 8 live products as thumbnails (hover = name/price, click-through to the product) with an honest live count (the old count included drafts/archived)
assignment sheet: each product row shows metal · gemstone and, when the piece lives in another collection, an amber 'in <Collection> — will move here' warning (collectionId is a single FK; moves were silent before)
product search now matches metalType and gemstone ('pearl' finds pearls)
create/edit form exposes what the API always accepted: Featured toggle (labeled as the campaign-target switch) + SEO title/description
renders a real hero: collection name, curator's description, piece count, hero image with overlay when set (before: NO copy at all — an h1 over a bare grid, unfit for paid traffic)
Sterling Silver — the material collection, optimized to the max
queried by Product.metalType (NOT a DB Collection row — collectionId is a single FK, so a Collection would pull the silver Chasing Ring / Starfish / Pearl pieces OUT of their home collections; a material query lets every sterling piece live in both places)
localized metadata in all 24 locales (CATEGORY_SEO 'sterling-silver') + nav/footer/category copy in all 24 message files
ItemList + BreadcrumbList (via CategoryPage) PLUS CollectionPage schema and the first visible FAQ + FAQPage schema on any shop page
CategoryPage extended with a metalType filter mode (material categories span every jewelryType; take raised to 60 for them)
header nav, footer shop column, legacy siteConfig nav all link it
sitemap: /creations/sterling-silver at 0.8 weekly; STATIC_CONTENT_DATE bumped to 2026-08-20
/collections index: Sterling Silver mosaic tile (live images + count) linking to the landing page, included in the ItemList schema
llms.txt Shop section: Sterling Silver line, worded to disambiguate retail silver jewelry from the /precious-metals buying service
llms-full.txt: new '## Collections' section (all DB collections + the material collection) so models can recommend browsable pages, not just individual pieces
one Country control — full Central + South America currency coverage
Product/Collection JSON-LD priceCurrency is now truthfully USD — it was labeling the unconverted USD price with the locale's currency ($5,000 ring marked €5,000 for German visitors; Merchant Center mismatch)
additive state-wide discovery + physical mail as a first-class channel
discovery response now returns before/after prospect totals — the console proves every run is purely additive (discovery has never deleted, but the operator couldn't see that)
Entire-state mode: pick a state, the console sweeps its metros one search at a time (all 50 states + DC curated in metros.ts), each new prospect auto-assigned to the metro it was found under; per-metro progress log, partial-failure tolerant, board refreshes as results land
JewelerMailPiece model + SQL migration (supabase/migrations/20260819)
print edition PDF = the v2 proposal + a navy closing card page with the piece's unique QR ("We found something.")
landing page /proposal/[token] — "Prepared exclusively for <business>", call CTA + digital proposal; QR scans, views, and CTA clicks all recorded as ProspectingEvents (offline mail becomes an attributable channel)
lifecycle: DRAFT → submit (test_sk_ dry-runs free; live needs POSTGRID_LIVE_APPROVED="true", fails closed) → webhook tracks printing/ mailed/delivered; webhook treats payloads as hints and re-fetches every letter from the API before acting
delivery writes the timed follow-up call onto the prospect's nextStep ("delivered 47 minutes ago" call), exactly once; manual mark-mailed/ mark-delivered escape hatches for hand-assembled PRESENTATION/EXECUTIVE kits
same integrity gates as the stage-2 proposal email (v2 + fact-check + validity window); unlike email, physical may be a first touch by design
P0 fixes verified in code: client-controlled mobile-checkout shipping/ gift-balance removed (server flat-rate table; zero-value orders rejected), tenant API/MCP routes bound to the authenticated tenant, fail-closed auth (isActive + tenant membership, session invalidation), centralized SaaS builder authorization, Mailgun/EasyPost/ShipEngine/ Brevo/Slack/Telegram/log-drain webhook verification, inbound-email HTML sanitization (parser-based allowlist + sandboxed iframe), edge-level cron gate with header-only credentials, upload MIME/magic-byte hardening, production rate limiting fails closed without Redis.
Focused suites 50/50 green; scoped tsc over all 72 changed/new TS files (the gate the original run couldn't finish): every error except one maps to PRE-EXISTING debt in unchanged lines/imported files; the one genuine new-code error (JWK typing in shipengine-signature.ts) fixed here, verifier tests still 2/2.
Founder/platform accounts confirmed compatible with fail-closed auth (tenant_katura, active, platform role intact). The single would-be locked-out staff account (CAD manager) was backfilled to tenant_katura in production per the ledger's own manual action.
Remaining dangerouslySetInnerHTML in compose-dialog renders the admin's own signature (self-authored), not untrusted inbound content.
contract-grade in-house e-sign — the PandaDoc replacement
Tuesday, August 18, 2026
12 updates pushed
11:54 PM · ZRosserMcIntosh
honest stats, real search, atomic money math, working cache busts
The "Processing" stat card read stats.processing — a key the API never sends (it sends in_production) — so the card rendered blank forever. The client state now mirrors the API payload and shows In Production.
"Shopify Orders" / "Katura Website" counted the current PAGE's 20 rows while labelled as totals. Now a real groupBy over the table.
The search box filtered the 20 already-loaded rows client-side while the API's search went unused — a search that looked global missed everything not on the current page. Now debounced to the server; filters and search reset to page 1 (staying on page 3 of a one-page filter rendered empty).
total = subtotal + shipping + tax − discount is now enforced whenever any component changes. Before: patching shipping/tax/discount alone never recomputed total (silent drift), and patching WITH items rebuilt total treating omitted shipping/tax as zero (wiping real values).
Item replacement ran deleteMany then a separate update — a failure between them left the order stripped of every line item. Now one transaction (verified: the extended tenant client supports $transaction on Prisma 6).
Invalid status values now 400 instead of 500ing inside Prisma.
invalidateCache is exact-key DEL, but list cache keys include the full filter combination — so PATCH's hardcoded trio only ever busted the unfiltered page-1 view, and cancel/mark-paid/ship/refund/DELETE busted nothing: every action served the old list for the TTL ("the button didn't work"). New SCAN-based invalidateCachePrefix, called from all six writers.
Send actually sends: the invite email fires on Send (fail-soft to the manual link), audited as EMAIL_SENT.
Real sealed PDF: @react-pdf/renderer document (body, typed/drawn signature, countersignature, E-SIGN consent recital, full audit trail) uploaded to the PRIVATE signatures bucket, with tamper-evidence: pdfsha256 of the exact bytes + HMAC seal (SIGNATURE_SEAL_SECRET or NEXTAUTH_SECRET) + a KS-XXXX-XXXX verification code printed in the footer. Public GET /api/signatures/verify/[code] lets anyone holding a copy authenticate it byte-for-byte — the thing "genius sign" can't do.
Completion emails to both parties, the signer's with the executed PDF attached and the paper-copy-on-request notice.
Template variables: the create form renders one input per {{placeholder}} and the server refuses any document with an unfilled blank (variablessnapshot recorded).
Countersigning end-to-end: signer signs -> COUNTERSIGN_REQUIRED -> admin notified -> countersign box on the detail page -> both-signature PDF regenerates -> both parties emailed. Re-signing blocked while pending.
E-SIGN consent: strengthened disclosure on the signing page; consent is a required server-side field recorded in the audit trail.
New template seeded (repo + prod): "Growth Services Agreement — 90-Day Retainer" with countersign required — matches the prospecting proposal's page-5 terms, ready for the first marketing client.
Dead 0-byte branding.ts removed; status/audit type unions aligned with the live DB enums; seed script ON CONFLICT fixed (id, not slug).
Feeds the previously-dead 14-signal seo-audit.ts: assessSeo() now runs on real fetched signals — the "free SEO audit" door-opener is finally backed by data, and its quick wins quote what was actually observed.
Absences are dated + scoped ("We did not identify X in our fetch on <date>"), never absolute; commerce-reality evidence deliberately feeds the "lacks e-commerce is contradicted" Luxor rule.
Refetch supersedes prior machine-captured rows (marked STALE, never deleted — historical proposals keep their sources); human-entered evidence untouched.
Optional PageSpeed Insights (mobile score + LCP) when PAGESPEED_API_KEY / GOOGLE_PAGESPEED_API_KEY is set; fail-soft.
3 new tests (fixture HTML through stubbed fetch): rich-store persona, bare-brochure absence phrasing, unreachable-site fail-soft. Full prospecting suite: 78/78.
/admin/analytics/conversions|traffic|orders rendered TWO stacked layouts: the (marketing) route group's layout was a near-verbatim copy of the outer one — its own context, its own module caches, its own KPI card grid, header, and timeframe picker, plus a second tab bar overlapping the outer eight. Every visit double-fetched the 21-query conversions endpoint and showed two "days" pickers that could disagree about the period on one screen. The inner layout is now a pass-through that re-exports useMarketingContext from the real provider; the three pages needed zero changes.
The hub nav is now grouped: Overview · Acquisition (conversions, traffic, campaigns, hero, affiliates) · Sales (orders, products, customers, reviews) · Team (team, time-tracking), rendered as a primary row plus the active group's sub-views. Every existing URL is preserved; hero, affiliates and time-tracking — pages that existed but appeared in no navigation at all — finally have a home.
The hub's period selector now lives in the URL (?days=) and every nav link carries it, so the window follows you across tabs, survives refresh, and is shareable. Previously each surface defaulted independently.
/admin/analytics/campaigns: smart links and QR codes in one sortable table (7d / 30d / all-time), each row clicking through to its operational page; cross-links to email campaigns and media performance. Comparing campaign assets previously meant flipping between two separate trees.
The Marketing sidebar entry "Conversions" was a duplicate pointer into the hub — removed; its keywords merged into the single Analytics entry.
Two dead /admin/stella/* links fixed (analytics layout, CRM marketing stub), same rot family as the Under Development drift.
Stella Marketing Hub dropped from Under Development: it is launched.
paper-archive: client matching, historical orders on the books, honest dates
Client matching: each card is checked against User accounts, CRM contacts, and other already-linked cards (email near-certain, phone strong, name token overlap). The review panel shows "Linked to customer · N orders" or states plainly "as far as we know, this is not currently a Katura client" with one-tap Create client record (CRM contact, source=PAPER_ARCHIVE). Linked clients flow into invoice promotion (customerType USER/CRM_CONTACT instead of ANONYMOUS).
Record as historical order: a CONFIRMED receipt becomes a real Order — the entity every revenue dashboard and the cap-table valuation compute from — dated to the CARD via customOrderDate, tagged source=PAPER_ARCHIVE (filterable like Shopify imports), created as DELIVERED history with no fulfillment/email side effects. Line items as written; deposit noted in internalNotes; synthetic archive+ email only when the card has none (and flagged).
Honest dates: recordDatePrecision (exact/~month/~year/unknown) set in review; order promotion requires at least a year so revenue periods stay defensible.
auth/profile: session-aware logins, self-service profile, es locale saves
Both /employee-login and /login showed the password form to people who were already signed in — while any direct /admin link sailed straight through — so signing in twice felt mandatory. Both pages now check the session server-side and redirect to the callbackUrl (or /admin, /account).
Settings → Profile rendered a self photo-upload widget for every employee, but the API it posts to was MANAGER/OWNER-only: a control the UI hands you that always 403s. Self-upload is now allowed; managing OTHERS' photos still requires manager/owner.
Even after an upload, the JWT pinned name/image from the moment of login ("reload to refresh session" does nothing for a JWT). The existing 5-minute role re-check now refreshes name/image too.
Nobody could change their OWN password while signed in — the PUT handled password only in the manage-others branch, so managers could set employee passwords but self-service meant the public forgot-password email flow. Own-profile password change added (min 8 chars) plus a Change Password section in Settings → Profile (en/pt/es).
Choosing Español saved the cookie but the server preference 400'd: every adminLocale allowlist said ['en','pt'] while the admin ships es. All three allowlists now accept es, so the language survives a new device.
The provider declared `settings` TWICE (interface, en, and pt objects) — a small orphaned block shadowed the real one in the type, which is why every t.settings.* on the settings page has been a type error masked by ignoreBuildErrors. Duplicates removed (es had the same problem); ~10 pre-existing type errors gone.
Claude Opus 5 vision extraction (official SDK, zodOutputFormat-enforced structure): verbatim transcription + typed fields with per-field confidence; the model must flag ambiguous digits ("5850 or 6850") instead of silently guessing.
Arithmetic is checked DETERMINISTICALLY in code (price−deposit=balance, balance+tax+shipping=total, line-item sums) — failed checks and low-confidence fields are highlighted in the review UI next to the original card image; confirm/correct/reject per record, with reviewer corrections logged for future few-shot use.
PAYMENT_PROOF records get deterministic match suggestions against the card they most likely settle (amount vs total/balance/deposit, name overlap, date proximity) and one-tap linking.
Search across everything written on any card (ILIKE over flattened extraction). Mobile-first UI so cards can be photographed and reviewed from a phone. Listed under /admin/under-development.
New PaperRecord model; migration already applied to Supabase and recorded in _prisma_migrations (resolve-style, matching repo practice).
admin dashboard: fail-closed scoping, coherent stat cards, store-local days
Every dashboard query is scoped to Katura's storefronts so SaaS tenants' orders can't contaminate the numbers — but the scope lookup fell back to [] on a DB error, and `ids.length > 0 ? {...} : {}` then dropped the scope entirely. A transient failure silently widened revenue to every tenant. The lookup now returns null on failure and the page fails closed (empty stats beat wrong stats). It also runs once per request instead of three times.
The orders card showed a 12-month count with a 30-day delta captioned "from last month", with a 7-day sparkline underneath — four windows on one card. Each card now has ONE window, titled with it (Revenue · 12 mo, Orders · 30d), and the caption names the comparison (vs prior 12 months / vs prior 30 days). Sparklines carry a "7d" tag. New i18n keys in en/pt/es.
Accent colors were positional, so hiding the revenue card for non-financial users shifted every color one card left. Keyed by stat id.
Revenue/order sums now come from aggregate queries instead of fetching every order row for 24 months; the sparkline is 2 range queries bucketed in JS instead of 14 per-day queries.
startOfDay(new Date()) ran in server UTC, so from 8pm New York the appointments card showed the wrong day. New src/lib/store-time.ts computes store-local (America/New_York — the codebase's canonical zone) day bounds, DST-aware, with tests. Sparkline buckets use the same store days.
Recent Orders and Recent Leads rows were the only dashboard cards with no click-through and no "view all". Rows now link to /admin/orders/[id] and /admin/leads/[id] (full ids threaded through; short ids still displayed).
"Activity" was a literal string among nav.* keys — the one sidebar item that never translated. Now nav.activity with en/pt/es strings.
Conversions (an analytics page) carried an image icon; now TrendingUp.
Command-palette search matched raw translation keys, so typing the label you see on screen only worked by keyword luck — worse in PT/ES. Search now matches resolved labels via the palette's translator.
The Under Development page had drifted both ways: it listed pages promoted to the sidebar months ago (Goals, Wholesale, CAD Design, Media) as hidden, and three entries pointed at dead /admin/stella/* paths. Stale hrefs fixed, and the list now derives against admin-navigation.ts at load — anything reachable from sidebar or palette is pruned automatically.
minutes reuse live translations — stop re-billing whole transcripts
New lookupCachedTranslations(): one indexed query returning every already-paid translation for the transcript, plus free passthrough for lines already in the target language (half the lines in a bilingual meeting).
minutes-translation POST now fills transcript lines from the cache and sends ONLY genuine misses to translateBatch; response reports transcriptCacheHits vs transcriptTranslated so the saving is visible.
Live cache persistence cap raised 120 -> 1000 chars for finals: the cap was a write optimization that discarded exactly the expensive lines.
Defensive speaker-name prefix strip on reused cache entries.
sendable proposal PDF — stage-2 email path, tiered page 5, press-cited page 6, hard integrity gates
New POST prospects/[id]/proposal-email drafts the stage-2 COMMERCIAL_PROPOSAL email with the PDF attached, gated on: fact-check passed, proposal within its 30-day validity window, prior outreach on record (never a cold attachment). "Draft proposal email" button on the deal page; send.ts also refuses to attach an expired proposal.
New katura-proof.ts: the approved whyKatura allowlist now carries real proof — press citations (JCK, Garden & Gun, Flower, Atlanta Magazine) as clickable nominative links, needing no logo rights.
Page 5 is a decision, not a number: 2-3 priced tier cards from OFFERS with the recommended tier highlighted, plus explicit terms & acceptance (90-day term stated plainly, monthly billing, validity date).
Integrity: engagement.price forced from the offer catalog (never model-authored); detectForbiddenLeaps now scans every prospect-facing proposal string and fails the audit on any hit; Stage-1 validation verdicts persisted onto JewelerEvidence (console badges + consultation "verified" strength finally work).
PDF polish: broken cover watermark removed, validity date on cover, unknown audit tiles replaced by one honest line, buyer-facing confidence numbers dropped, method-and-limits box, versioned footers.
displayed price/shipping = charged price/shipping, autofill, visible errors, ring size on orders
create-payment-intent now mirrors format-cart.ts pricing exactly (variant price -> promotion -> native sale -> base), so a promotion can never silently disappear between cart and charge.
New src/lib/shipping/flat-rates.ts is the single shipping table; the checkout page displays it and the server charges it (the client no longer shows a live carrier quote the server ignored).
variantId/variantName written to OrderItem — ring size is preserved.
autoComplete/inputMode on every checkout field; validation failures scroll to and focus the field, error echoed at the submit button.
Success page renders the order optimistically (purchase tracking still gated on paid status); poll backoff capped at 8s instead of 24s.
checkRateLimit('checkout') on create-payment-intent; PaymentForm debug console.logs removed; cart totals copy no longer promises free international shipping; bag count matches the header badge.
scope grids to Katura, honest buy-box states, real cart errors, category SEO + journal link
Every browse surface (creations, categories, collections, homepage) now filters by storefrontId — Rosser products can no longer leak into the Katura grid and 404 on click. PDP id/handle lookups require ACTIVE too.
PDP buy box honors purchaseMode: INQUIRY_ONLY/SHOWCASE route to the consultation CTA instead of an Add to Bag the cart API 403s.
addItem returns the server's failure reason; the toast shows "out of stock" / "not available for direct purchase" instead of "try again".
generateMetadata for engagement-rings, wedding-bands, bespoke (new CATEGORY_SEO bespoke entry; the other two existed but were never called).
Katura Journal (/blog) gets its first internal link, in the footer.
A business name is not a person's name. Discovery output is untrusted, so personalisation now requires a full human name WITH a cited source (or an explicit admin attestation). Stops "Rand Jeweler" being turned into a "Hi Rand" salutation.
normalizeFullPersonName() requires first + last, unicode-letter parts only; citedContactIdentity() drops any identity the model supplied without a real http(s) source.
Recipient must be a source-verified contact email.
PROSPECTING_POSTAL_ADDRESS must contain a real address (CAN-SPAM footer was otherwise shipping the literal "[SET PROSPECTING_POSTAL_ADDRESS]").
Live provider sending stays closed until PROSPECTING_PROVIDER_OUTREACH_APPROVED=true is set following provider / compliance sign-off, per Brevo's consent-based recipient policy.
First-touch emails are validated send-ready before dispatch.
Every send now writes a PROSPECTING_EVENT audit row; proposal status only flips to SENT when the attachment was actually allowed.
fix duplicate captions, whole-sentence pops, and the DeepL double-hop
translate.ts: DeepL circuit breaker. 456/429 holds 10min, 401/403 holds 30min, 3 consecutive soft failures hold 30s; half-open on expiry, closes on first success. New tryDeepL() helper centralises the bookkeeping. Spend is now attributed to the provider that actually served (everything was booked as "deepl", including every OpenAI fallback).
All 4 final sites now retire the live caption. New translationSinkRef routes onFinal()'s provisional tail translation to the confirmed line instead of the retired overlay, so the translation column fills immediately rather than sitting on "..." until the whole-sentence request returns.
Streaming was dead code: stream:true was only sent on the NON-incremental path, but the incremental flag is on in prod. Wired streaming through the incremental path via a new optional onPartial on IncrementalTranslator's translate contract (backwards-compatible; 7 existing tests still pass).
Both translate routes dropped `context` on the streaming branch, and the streaming DeepL call ignored it — incremental seams lost grammatical agreement whenever streaming engaged.
Streaming path called lruSet() even for context-dependent results, which the non-streaming path correctly refuses to cache (cache poisoning).
subtitle-panel.tsx had a JSX block spliced into its header doc-comment by a bad search-replace in an earlier session (committed in HEAD). It compiled, but it had swallowed the autoGrow render logic — the prop was accepted and silently ignored. Comment restored, logic reinstated.
New src/lib/build-version.ts + next.config env block so each meeting's latency report records WHICH build produced it (commit SHA, branch, env, immutable deployment URL) — previously unanswerable outside Sentry.
Copy button on the latency report emitting a paste-ready text report with build identity, per-kind and per-role breakdowns, and raw samples.
Guests recorded no latency at all; samples now carry role: host|guest.
Latency chart time axis floored to whole minutes, so every label read "0m" on any call under 2 minutes. Now m:ss.
latency Task 5 — streaming OpenAI fallback for live subtitles
translate.ts: new translateSegmentStreaming() — same decision tree as translateSegment (LRU hit / same-lang / DeepL success return atomic 'complete' outcomes; only the OpenAI fallback streams). Parses OpenAI SSE frames, forwards raw token text, LRU-caches + records spend on finish.
Both translate routes: opt-in body.stream flag; streaming responses are marked X-Translate-Streaming: 1, everything else stays JSON (all existing callers unaffected).
Both room clients: interim path sends stream:true, reads the body progressively, updates the caption per chunk, respects the interimTranslateTokenRef race (cancels the reader mid-stream when a newer interim wins), and feeds the Task-6 phrase cache on completion.
Finals keep the non-streaming path (with its wordOverlapRatio retry guard) — only interims stream.