KATURA
Your browser is not supported. Please update your iPad to the latest iOS version, or visit us on a newer device.
How to update your iPad Skip to main content JOIN THE WORLD OF KATURA Be the first to discover new collections, exclusive events, and the stories behind our legendary creations.
Katura—Crafting timeless treasures since 1999.
The technology behind Katura is built in-house. K99 is our jewelry business platform — available to other jewelers.
© 2026 KATURA. All rights reserved.
Live from GitHub · Refreshed continuously
Software Updates 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,498
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 4 of 40 · 151–200 of 2,000 fetched
Saturday, August 22, 2026 19 updates pushed
8:48 PM · ZRosserMcIntosh
the email must say who is writing (I banned that by accident) FORBIDDEN_OPENING applies to the FIRST SENTENCE only — the first line still belongs to the recipient, not to us. SELF_INTRODUCTION is REQUIRED in the first 60% of the body: a name, or a plainly stated role at Katura. The body must also name Katura at all. 8:40 PM · ZRosserMcIntosh
regeneration must retire the first-touch draft it replaces Regenerating a deck CREATED a new step-1 email and left the previous one DRAFT. Prospects had quietly accumulated up to four competing first-touch emails — Diamond Gems and Rand Jewelers had four each, Koorosh, Mitchell's, Premier, Skeie's, Underwood's and Viir three — every one of them still sendable, and all but the newest written to a superseded spec. The console renders the newest, so this was invisible. It was not harmless: any older row could still be opened, approved, or picked up by a bulk action, and the voice rewrite only reached the newest row per prospect, so 46 stale agency-voice drafts were sitting behind ready-to-send prospects. Creating a first touch now cancels the open ones it supersedes, inside the same transaction. SENT rows are never touched — those are the outreach record. Backfilled the same rule over existing data: 46 superseded drafts cancelled, leaving exactly one open first-touch per prospect (70/70). 209/209 tests, scoped tsc clean. 8:33 PM · ZRosserMcIntosh
make the two send buttons impossible to confuse, and prove it is red, not black, and prints the actual recipient on its face ("Send LIVE → owner@example.com") requires a confirm naming the address, business, subject, and whether the PDF rides along, and says plainly that it cannot be recalled Test send now reads "Test to my inbox only" and its success alert states that the prospect was not contacted. 8:24 PM · ZRosserMcIntosh
retry near-misses, and keep the machinery out of the inbox Two problems the first live rewrites exposed. 1. A genuinely bespoke email was being thrown away over a nine-word subject against an arbitrary eight-word cap — and the prospect got the canned fallback instead. The subject band was never part of the voice spec, so it is now 3–10; more importantly, a rejected draft now gets ONE retry with the deterministic failures handed back ("your subject is 9 words, fix it, keep everything that was working"). Most rejections are near-misses, and discarding them is precisely how 59 prospects end up with one identical template. 2. The first clean output read: "the validated dataset only surfaced homepage title work". That is internal pipeline vocabulary in a jeweler's inbox — it tells the reader a SYSTEM looked at their business, not a person, which undoes the entire premise of the email. Banned deterministically (validated dataset, evidence rows, structured metrics, storefront capture, fact-check, "our data shows") and called out in the writer rules. POSH Fine Jewelers before: rejected, generic fallback. After: 165 words, 4 paragraphs, opens on "4.8 stars with 100+ reviews as observed on 2026-08-22", states an opinion, gives away a specific homepage title fix, asks for twenty minutes. 203/203 tests. 8:14 PM · ZRosserMcIntosh
an API failure must not overwrite a bespoke email with the template Discovered while regenerating the ready-to-send emails: the OpenAI balance is exhausted, every rewrite call 429s — and rewriteFirstTouchEmail treated that exactly like a spec violation, writing the canned fallback over the existing draft. One sweep would have flattened all 59 individually-researched emails into the same generic paragraph, losing "504 Google reviews at 4.8" and every other specific the whole spec exists to surface. Now the two cases are separated. A model answer that VIOLATES the spec still earns the fallback; a model/API FAILURE returns outcome "failed" and leaves the stored draft untouched, so a transient 429 costs nothing. RewriteResult also carries `failures[]` now — the reason an attempt was rejected. Without it a fallback is indistinguishable from a rate limit, and "every prospect got the identical template" is exactly the failure this system must never hide. 203/203 tests. 8:11 PM · ZRosserMcIntosh
adopt the Virgil first-touch voice spec across every layer compose: greeting is now "Hi Farha —" / "Good afternoon —". It no longer introduces the sender — his name is in the signature, and the opening line is the only one with the reader's full attention. Greeting stays compose-time so the name can still only come from contact-safety's confirmed-and-cited check; the writer can never guess one. validator: 110–200 words (spec target 120–180), 3–6 paragraphs (mobile readability is part of the spec — the old emails were one block), 4–14 sentences. Plus four new deterministic bans: agency jargon (the full banned-phrase list), self-introducing openings, fabricated familiarity ("followed your company for years"), and faux-passive CTAs. A prompt alone does not hold a voice; the model drifts back to agency register under pressure, which is how the Farrah email shipped. writer + auditor prompts rewritten to the same spec, and the auditor is now told the opinionated operator voice IS the specification, not a defect, so it judges only whether factual claims are supported. rewriter now receives the CAPTURED EVIDENCE ROWS (exact ratings, counts, dates) rather than only the summary sentences — specificity is what makes these land — at temperature 0.75, since 59 near-identical emails read as a template. safe fallback rewritten to the same voice, and verified to satisfy the validator it is the fallback for. 5:56 PM · ZRosserMcIntosh
"never audited" is not "passed" — close the unaudited-send hole Found while verifying the post-sweep board: Luxor Fine Jewelry sat in READY with a drafted first-touch email and the proposal PDF attached, despite its deck carrying no fact check at all. Both the approve route and the send path gated on `passed === false`. A deck with NO independentFactCheck block satisfies "not failed", so every pre-gate legacy deck could be approved and emailed to a real jeweler with an unaudited PDF attached — precisely the outcome the fact-check gates exist to prevent. Now an affirmative pass is required, and the rule lives in ONE exported function (factCheckGate) that both the approve route and the send path call. Two hand-maintained copies of a rule is exactly how today's stale-auditor bug happened; this one gets a single truth and 7 regression tests instead. The console predicate matches the API: an unaudited deck now shows under "Needs fix" rather than promising a send the server would refuse. Blast radius verified against production: 59 audited-and-passed decks are unaffected; exactly 1 deck (Luxor, a V1-era proposal) moves to Needs fix, where the Reassess button regenerates it under current gates. 187/187 tests, scoped tsc clean. 5:48 PM · ZRosserMcIntosh
make dark mode actually dark, across every page hsl(var(--token)) is INVALID here — the tokens hold oklch/hex, so the declaration was dropped and Recharts fell back to white tooltips, #ccc grids and black axis ticks on navy. 30+ instances across charts. gradients (background-image beats the sheet's background-color) SVG stroke/fill literals, chart colour props, prose without dark:prose-invert email-body iframes whose sandboxed HTML sets dark text but no background 5:15 PM · ZRosserMcIntosh
heal the UK run — stale auditor rule, city-name collisions, rule drift validator banned "ninety-day/roadmap" mentions the auditor requires — ban now applies only in attachment-free mode validator/fallback now invert cleanly when FIRST_TOUCH_ATTACH=false (no more emails claiming an attachment that isn't there) send path: resolveEmailAttachment is now genuinely the single truth (real send, test send, preview), plus a words-vs-wire gate — a body claiming an attachment can never send without one stage-2 proposal email no longer writes its own greeting/sign-off (compose adds both; they were doubling on the wire) stale "NO ATTACHMENT" badge/comments across the console and routes 4:35 PM · ZRosserMcIntosh
territory tiers, jeweler-not-agency voice, previews, 6-up runs Metros are now RANKED by yes-probability — curated tiers combining spending power with win likelihood (affluent, independent-jeweler-rich, under-pitched markets like Scottsdale, Naples, Charleston rank 1; pitch-saturated megacities sit at 2; thin markets at 3 — documented as editable judgment). Everything consumes the ranking: pickers list best-first with T1/T3 badges, state sweeps and Autopilot rotation spend attention tier-1-first. Identity, everywhere the words are written: Katura is a JEWELRY BUSINESS that does everything in-house — never an agency. The rule leads the email prompt, the rewriter persona states it, and a deterministic validator ban rejects agency framing outright. In-site email preview: an Eye button on every row renders the EXACT composed email (greeting, signature, opt-out) in a sandboxed frame with the attachment truth — the PDF is really rendered so the chip shows its true size, and blocked attachments say exactly why. One shared resolveEmailAttachment is now the single truth for send, test-send, and preview. Console: up to six concurrent Autopilot runs in a three-across grid, and the entire admin nav collapses ChatGPT-style via a slim edge strip (persisted per browser) so every admin page can run full-width. 4:04 PM · ZRosserMcIntosh
the outreach email becomes a real letter, and duplicates die The test email exposed everything wrong at once: the deterministic fallback shipped a mid-clause fragment ('…a third-party store listing that states.'), zero context or introduction, no PDF, a bare signature, and the raw [SET PROSPECTING_POSTAL_ADDRESS] placeholder in the footer. Now: every email opens 'Good morning, {Name} — my name is Zack.' in the RECIPIENT's local time (state→timezone), the writer produces a fuller 70–130-word letter (one line of who-Zack-is context, the strongest verified observation, the strength worth keeping, a casual reference to the attached proposal, a soft CTA — no compliance hedging in the pitch voice), and the proposal PDF rides on the first email by owner decision (PROSPECTING_FIRST_TOUCH_ATTACH=false reverts). The signature mirrors the admin/mail card — wordmark, Zachary Rosser, gold EST. 1999 line, mailto — and the opt-out is one 10px italic line that hides the postal address until a real one is configured. The fallback is rewritten with clause-safe trimming (a fragment that doesn't fit whole is dropped, not cut), 'Regen all emails' refreshes every un-sent draft from its audited proposal content for ~1–3¢ each, and APPROVED drafts reset to DRAFT. Duplicate protection hardened on the mid-flight question: discovery dedupe is now GLOBAL (any campaign — concurrent jurisdiction runs made cross-campaign duplicates likely), and a send-time twin guard refuses to email a business any other prospect row already reached at the same domain or address. 3:44 PM · ZRosserMcIntosh
multi-run console, cost chips, auditor round-two fixes The reassess sweep exposed a second failure class: the auditor put advisory commentary ('this is acceptable… but would be cleaner') in unsupportedClaims, failing decks that passed every structured check. Pass/fail is now reconciled deterministically — structured ledger verdicts + contradictions are ground truth; prose advisory notes become non-blocking research guidance. Auditor prompt gains discipline rules (only genuine failures in unsupportedClaims; rhetorical transitions are framing); writer is barred from review counts in copy assets without validated evidence and from truncating proper nouns; site-fetch treats 4xx homepages as unreachable (a 403 bot-wall's title once scored as a passing tag). Console: the Autopilot card renders up to three stacked live run panels (per-run stop, activity, CASL/PECR territory notes, auto-distinct campaign tags), and every prospect row shows its attributed AI cost with tokens in the tooltip. 3:33 PM · ZRosserMcIntosh
concurrent autopilot runs, CA/UK jurisdictions, cost attribution Up to three Autopilot runs may now run at once, one jurisdiction each: budgets are metered per campaign (every AI spend path — discover, enrich, validate, write, audit — now tags the campaign in the usage ledger; the migration is applied to the live DB), so concurrent runs can never consume each other's budgets, and the runs API enforces distinct campaigns. Six Canadian provinces and the UK join the territory map as an experiment — with the compliance line drawn in code: CASL blocks cold email to Canadian prospects at the send gate (their decks feed the physical-mail channel), while UK B2B email is permitted under PECR corporate rules. The prospects list now ships per-prospect attributed AI cost (cents + tokens) from the ledger. 3:24 PM · ZRosserMcIntosh
prospecting console: tabs + per-row send actions The pipeline is now organized by what to DO next, not by raw status: Ready to email (verified channel, clean deck, drafted email) · Needs fix (fact-check failures, contradictions, needs-research) · Research · Mail only · In motion · All — live counts, group-by preserved within a tab. Every row carries 'Test → my inbox' (full-fidelity test send, no mutations) and 'Send real' / 'Approve & send' with confirm dialogs — server gates stay authoritative, errors surface verbatim. The list API now ships generationStatus so tab predicates read the real field. 3:14 PM · ZRosserMcIntosh
fix the fact-check's false-positive patterns Reviewed all 12 failed fact-checks in the database; five patterns, four of them auditor false-positives: (1) the auditor was never told the prospect's identity, so legitimate references to the known city/state (Underwood's 'Fayetteville', Porterfi's 'Rocky River') failed as unsupported — it now receives a PROSPECT CONTEXT FACTS block that is established ground truth; (2) properly date-hedged absence statements ('we did not identify X in our review on [date]') were demanded to have positive evidence, inverting their meaning — the auditor now judges them on scope discipline; (3) writer overreach on page layout ('only in navigation', 'above the fold') is now prohibited unless evidenced; (4) comparative industry flattery ('stronger than most independents') is banned; (5) site-fetch described a 404'd sitemap as 'present but weak', which the auditor rightly flagged — HTTP 4xx observations now phrase as absences. 8:27 AM · ZRosserMcIntosh
marketing site: audit fixes — the funnel now survives its own success The reply loop was the weakest link: both funnel emails inherited a BREVO_FROM_EMAIL whose historical default is a dead @katura.com address, and lead notifications defaulted to unmonitored marketing@. Emails now send from an explicit verified marketing@katura1999.com with replies routed to zack@ (which exists and is watched), notifications default to zack@ with reply-to set to the lead, submissions populate the indexed email/name columns, dedupe keys on the column and per-offer, and the tenant fallback uses tenant zero's real id instead of a nonexistent FK that 500'd on submit. Attribution: the EN/PT switch preserves the query string (the promised SwitchLocaleLink finally exists), the lead form captures attribution on mount instead of first focus, and proposal-PDF clicks carry utm_source=proposal — they were the funnel's one unattributable entry. Rendering and SEO: PT pages get html lang=pt via a pinned x-pathname in the marketing proxy branch (EN pages immune to stale locale cookies), OG/Twitter images added, six double-branded titles deduplicated, KPI cells align and the 5th card no longer leaves a gray hole on phones, FAQ heading localizes, storefront chrome (invoice popup, support hotkey) stays off the funnel, sitemap lastmod is stable, JSON-LD jobTitle matches 'Director of Brand & Growth', thank-you copy is offer-neutral, wa.me numbers sanitize to digits. Truth locks: CAMPAIGN_DURATION_DAYS derived from the dates (= 7) plus a test that fails if any prose surface ever says six-day/seis dias or frames the $498.39 campaign as a test again; the stale implementation brief carries a correction banner; PT terminology unified. 8:07 AM · ZRosserMcIntosh
marketing site: the Charleston campaign was seven days, and not a test June 1–7, 2026 — corrected at the single source of truth (case-study.ts campaignEnd + qualifiers) and in every en/pt copy surface that described Katura's own campaign as a 'six-day test'. The collection-launch service's testing methodology language is unchanged — that describes the offering, not our campaign. 7:24 AM · ZRosserMcIntosh
verify discovery-claimed emails automatically, honest badges Two mail-only states looked identical and one was fixable by machine: an email on file whose source was never cited (discovery-claimed) vs no email at all. Site capture now upgrades unverified addresses in place — published on the prospect's own site ⇒ published_source (Da Vinci Fine Jewelry verified this way); otherwise the cited source page itself is fetched and, when it visibly contains the address, third_party_verified. Bot-walled sources (J.P. Haase's allbiz 403) honestly stay unverified for the manual confirm flow. Badges split: amber '✉ UNVERIFIED EMAIL' with the fix in its tooltip vs stone '📮 MAIL ONLY'; the pipeline's mail-only notation now says which case it is. 7:13 AM · ZRosserMcIntosh
email-first economy, personal email format, reassess for old decks Reachability by VERIFIED email now dominates scoring: only a source-cited address counts (22/25 pts), an unverified one scores 10, and none at all caps the tier at C and routes the prospect to a mail-only bucket — notated on the record, no deck money spent (autopilot and Process All both honor the gate; single-prospect runs may override for the physical-mail channel). Site capture becomes the token-free email finder: addresses visibly published on the prospect's own pages are adopted as published_source contacts automatically. Outreach emails now read like a person wrote them: client-default typography, no logos or headers, greeting by the verified first name only, Zack's three-line signature (CTO, Katura 1999 / K99 Jeweler Services), and a small italic reply-'remove' line with the postal address instead of an unsubscribe button — bare 'remove' replies auto-suppress. A 'Send test to my inbox' button delivers the exact composition (PDF included per attachment policy) from DRAFT and in live mode, mutating nothing. Older decks are refreshable: 'Reassess with current engine' on any proposal, plus a bulk 'Reassess pre-gate decks' sweep for proposals that predate the claim-ledger gates. Friday, August 21, 2026 20 updates pushed
10:39 PM · ZRosserMcIntosh
evidence hygiene — quarantine, re-verify button, finer ledger A real contradiction (Hamra: an 'Instagram followers' row sourced from LinkedIn) blocked approval with no cleanup path, because rows stage-1 condemned as unsupported/contradicted/stale stayed in the pool and re-fed every regeneration. Now: deterministic hygiene quarantines such rows (REJECTED, never deleted — audit trail survives) and generation filters them out; platform-mismatch rows are refused at all three intake points (discovery, enrichment, manual add); and a 'Re-verify evidence & regenerate' button on the failure banner runs quarantine → site re-capture → clean regeneration in one click. Ledger refinements: absence-phrased audit tiles are judged as scoped inferences (an absence in our search is not a verifiable fact), and email bodies audit per sentence so one weak line pinpoints itself instead of failing the whole email. Hamra's data is fixed: 3 rows quarantined, regenerated clean — fact-check passed, zero contradictions, approval unblocked. 10:11 PM · ZRosserMcIntosh
one-click pipeline behind a verified-score gate The console flow needed three button presses per prospect (discover, fetch facts, generate) and generation candidates were ordered by the discovery model's self-reported score — a made-up number. Now: a 'Full pipeline' button per prospect and a bulk 'Process all' chain Thursday, August 20, 2026 11 updates pushed
11:16 PM · ZRosserMcIntosh
make the audit log survive the people it audits DELETE /api/admin/users/[id] deleted any account, ran auditLog.deleteMany({ userId }) first, and logged nothing. Anyone could erase a colleague's entire history and leave no trace of having done it. PUT /api/admin/permissions granted admin page permissions. Its role check only constrained MANAGER, so an EMPLOYEE fell straight through and could grant themselves anything. category counts came from a raw query with no tenant predicate, so the summary cards counted every tenant on the platform "Previous" reused a forward cursor against a skip:1 read and landed one row off from page three onward; paging is bidirectional now, with id as a tiebreaker so same-millisecond entries cannot repeat or vanish every keystroke refetched, running an unbounded GROUP BY over the whole table; search is debounced and filter options moved to a cached endpoint geolocation resolved on every read, with up to 3s of ip-api.com rate-limit sleeps on the request path; it resolves once at write time into a persistent IpGeoCache now, and reads never leave the process indexes now match the query shape (tenant + newest-first) 11:16 PM · ZRosserMcIntosh
fail-open rate limiting + full i18n of the checkout flow Production has no Upstash Redis, and rate-limit.ts failed CLOSED, so every endpoint using it 429'd on first touch: contact/register/newsletter since 2026-02-22, and checkout since 2026-08-18 when create-payment-intent gained
facts-first → deterministic lead score (reachability, decision-maker,
fixable pain… computed from stored fields and evidence, with the factor
breakdown shown) → generation only when the verified score clears a
minimum (default 55, tier B). Autopilot uses the same gate: candidates
are ranked and filtered by the verified score, never the model's
number; held-back prospects are counted and narrated in the activity
feed, and the run form takes a min-score input. Sending stays
human-approved everywhere. 9:40 PM · ZRosserMcIntosh
proposal pdf: layout budgets — the six-page contract survives any payload A real proposal (Hamra Jewelers) rendered 9 pages with three stranded fragments: two audit tiles alone on a page, the method box alone on a page, and the plan header divorced from its phase columns — because verbose writer output overflowed the fixed one-section-per-page design and react-pdf pushes whole keep-together blocks forward. Every variable-length field is now clamped at sentence/word boundaries by LAYOUT_BUDGET in pdf-data.ts, the plan gets height-aware fitting (estimated column lines, tail items dropped only in the worst case), and a maximum-verbosity regression test pins the render at exactly six pages. The writer prompt now demands tight plan bullets so future generations avoid truncation at the source. Stored proposals re-render correctly through the same builder — no regeneration needed. 9:22 PM · ZRosserMcIntosh
live activity feed, and a tighter budget ledger The console now shows what the engine is doing RIGHT NOW — the tick writes 'Generating deck — {business}' / 'Reading website — …' / 'Discovering — {metro}' to the run before each long operation and keeps a rolling 40-entry activity log, rendered as a live feed (open while running, collapsible on finished runs). A stale-tick warning appears if the cron hasn't fired in 12 minutes. Budget honesty: the spend cell is labeled as ledger-derived with the exact enforcement semantics in its tooltip, and discovery/enrichment calls now add a flat web-search-tool surcharge to the ledger so the cap can't be under-counted by per-call tool fees. 8:45 PM · ZRosserMcIntosh
zack@ exists as a Workspace account — runbook records it 8:07 PM · ZRosserMcIntosh
correct the runbook — Google holds MX, zack@ needs a Workspace alias Live DNS says katura1999.com's MX is smtp.google.com; the email-refactor brief's Mailgun-as-MX claim is stale, and the in-app inboxes have received nothing since 2026-06-07. Replies to zack@ therefore need a free alias on the existing Workspace user, not app-side routing. The runbook now documents the dormant webhook (reply-ingestion included), the harmless-but-ready mailbox rename, and what must be verified before ever moving MX back to Mailgun. 7:57 PM · ZRosserMcIntosh
zack@ mailbox is live — runbook reflects the rename ea_zachary renamed zachary@ → zack@katura1999.com in email_accounts (inbox history preserved), with zachary@ kept as an ALIAS into the same inbox. Replies to the prospecting sender now land at /admin/email/zack with no Google mailbox and no relay needed; the optional Gmail-copy relay stays documented. Gmail-inject addresses untouched. 7:50 PM · ZRosserMcIntosh
prospecting ops: reply ingestion for zack@, env contract, going-live runbook Inbound replies to the prospecting sender now mark prospects REPLIED (idempotent against Mailgun retries) and auto-suppress on opt-out phrases scanned only in unquoted text — the outbound footer itself says unsubscribe. PROSPECTING_* env vars join .env.example; the runbook documents going live, including that Google MX are removed so a Google Admin mailbox alone receives nothing (relay or EmailAccount row required). vercel.json gains the autopilot cron and honest maxDurations for prospecting, assessments, and the .tsx PDF routes. The cap-table diligence endpoint loses its diverged duplicate — the surviving route requires admin and serves short-lived signed URLs instead of permanent public ones. docs/marketing/PROSPECTING-ENGINE-PLAYS.md maps the engine onto six more direct-revenue plays, led by Paper Archive reactivation. 7:50 PM · ZRosserMcIntosh
the promised findings PDF now actually exists The services funnel promises an emailed assessment report — six areas, every finding sourced and dated, at least one copy-ready free fix, a 90-day plan, fact-checked — and nothing generated it. New report-grade PDF (honest unknowns included, entitlements restated in writing, no pricing tiers, no invented turnarounds) built from the prospecting pipeline via prepare/preview/approve-and-send actions on the admin assessments queue, emailed from the prospecting sender with replies invited. Form success copy, the confirmation email, and the site card now name the same six areas; the services-host footer links stop 404ing. 7:50 PM · ZRosserMcIntosh
autopilot — time-boxed autonomous discovery-to-deck runs Operator starts a run (duration, states/metros, target deck count, AI budget); a 5-minute cron tick discovers prospects, captures site evidence, and generates ranked proposal decks until time, target, or budget runs out. Per-tick caps (2 generations / 6 fetches / 1 discovery call), cost enforced against the AI-usage ledger before every generation, optimistic tick locking, and a console card with live progress and a ranked results table. The engine never sends anything — emails and mail stay human-approved. Generation and site-capture orchestration extracted into shared modules; ProspectingRun table migration applied to Supabase. 7:50 PM · ZRosserMcIntosh
wire the claim ledger + free-value gates, personalize page 6 Every atomic claim in a proposal now needs an auditor verdict (100% coverage or the fact-check fails), and quick wins must carry the shape the services site promises — steps, a copy-ready asset, verification — or generation degrades to needs_research before the audit call is spent. Page 6 gains a deterministic personal note built only from already- audited strings; press citations link only to article-level URLs (Flower and The Atlantan become unlinked mentions); the 7.77 paragraph states plainly that we run every offered capability for our own house first. 7:14 PM · ZRosserMcIntosh
30-day window; lab-grown and custom pieces are never returnable One policy everywhere: the window was 7 days on English pages, 14 on /terms, and 30 in most translations and the product structured data. All surfaces now say 30 days — pages, merchant feed (returnWindowDays), FAQ JSON-LD, trust copy, and the seeder scripts that would have re-introduced stale copy — and state plainly that lab-grown diamonds and custom made pieces are final sale under any circumstances. The EU 14-day cooling-off stays, framed inside the 30-day window. Canonical policy doc: docs/marketing/RETURN_POLICY.md. Google Merchant Center's console setting still needs a manual update to 30 days. 7:14 PM · ZRosserMcIntosh
account/orders: translate the page and gate the Pix hint on the Brazil store The orders list was hardcoded English with en-US dates regardless of locale, and flagged BRL invoices as 'Pix available' on every store. Page and order card now use the ordersPage namespace, dates follow the locale, and the Pix hint appears only on the Brazil storefront. 7:14 PM · ZRosserMcIntosh
country selector shows just the country; language follows it The trigger label drops the currency and language readout. Picking a country now applies its default language directly instead of offering it in a dialog; the dropdown's language submenu remains the manual override for shoppers who want a different language than their store. 7:14 PM · ZRosserMcIntosh
default to the store country and unblock CEP/zip autofill The address form now seeds its country from the KATURA_COUNTRY cookie (saved address still wins) instead of a currency guess that stranded logged-in shoppers on United States. ViaCEP/Zippopotam lookups were silently blocked by the CSP — both hosts join connect-src — and the lookup now fires as soon as the postal code is complete instead of racing the Continue button on blur. The account address form drops the same US hardcode. 7:14 PM · ZRosserMcIntosh
i18n: ordersPage namespace + 30-day returns copy across all 24 locales New ordersPage strings (orders list, order card, invoice statuses) in every locale, and the return-policy keys move from 7 to 30 days with an explicit final-sale exclusion for lab-grown diamonds and custom pieces. 12:14 PM · ZRosserMcIntosh
point the viewer's Enquire button at our own form Auralith's embed injects its own "Enquire" pill whose form POSTs to app.auralithvisuals.com/e/<key>/lead. Enquiries raised from our page were landing in their inbox, not ours. There is no supported way to switch that off, and this is not "we didn't find the flag" — the page source proves no flag can exist. The button is written into the server-rendered embed shell by an inline script whose only guard is parsing the embed key out of the pathname; it reads no query string, no data attribute, no config. 85 query-param variants return byte-identical HTML. /package is geometry and HDRIs with zero CTA fields, /presets and both /environments are empty arrays. The embed registers no `message` listener at all, so the parent cannot signal it either — the only postMessage traffic is child->parent analytics. Auralith publishes no documentation. So the corner is covered from our side. A cross-origin child cannot paint outside its iframe box and its stacking context is contained by it, so a positioned parent element wins over the child's z-index of 2147482000. The panel is painted in the viewer's own backdrop colour, which makes it vanish, and carries a Katura pill that scrolls to the free assessment form. Measured in a real browser rather than assumed: their pill is 89x31 CSS px inset 13.6px from the corner; the 160x64 panel contains it with 13.6px of margin on the two tight sides at 390px, 820px and 1280px viewports. Clicking ours sets #assessment and puts the form in the viewport. Two limits are written into the comment rather than papered over: tabbing into the iframe can still reach their button, and if they restyle that pill the cover stops lining up. The real fix is to have Auralith disable the CTA on the account. 12:06 PM · ZRosserMcIntosh
three rose gold solitaire renders in the gallery Publishes the first samples in the pitch page's render gallery, which has been hiding itself since it was built because the folder held nothing but its README. Filenames are the alt text — the gallery derives it from the basename — so the UUIDs these arrived with would have shipped as "2cf1a72c 7f9a 4dc6 9935 fb8bfa3de226" to every screen reader and every crawler. Renamed to describe what each one actually shows. Alphabetical order is gallery order, so the full view leads and the two macros follow it. Verified against the running dev server: /pitch returns 200, all three resolve through the image optimizer as JPEG, and they render between the CAD-vs-renders explainer and the wholesale section. 11:52 AM · ZRosserMcIntosh
interactive ring viewer, and renders follow the explainer Embeds the Auralith 3D viewer between the press strip and "What you can hire us for", so a jeweler drags a real ring around before reading a word about what we sell. The host also had to be added to frame-src in src/proxy.ts — this site sends a strict CSP, so an un-allowlisted iframe renders as nothing at all, with no console error a non-developer would find. Its max-width is capped against viewport height as well as width, so the 3:4 viewer can never grow taller than the screen on a phone held in portrait. Moves the renders gallery to sit directly under the "CAD and renders are not the same thing" note instead of above it. The paragraph explains what a render is; the gallery is the proof. Reading them in that order is the point, and it was backwards. Verified against a running dev server rather than by eye: /pitch returns 200, press strip -> viewer -> services in that order, the CSP header carries the new host, and with a temporary file in the renders folder the gallery renders between the explainer and the wholesale section. 11:49 AM · ZRosserMcIntosh
Merge branch 'worktree-prospecting-upgrade' into main Brings the jeweler services work onto main: the booth QR share button for services.katura1999.com, the three-step monthly ladder and tiered project pricing, the inbound-assessment fee waiver, plus collections/sterling-silver, Central + South America currency coverage, and the prospecting mail channel.
its rate-limit check — no customer could pay (verified live via curl).
Limiter now fails open to the per-instance memory store with a loud error
log; EXPIRE gets NX so windows don't slide. Provisioning Upstash restores
distributed limits.
Also de-hardcodes English across the checkout flow: 429/errors mapped to
translated messages client-side, CheckoutConfidence badges, PaymentForm
fallbacks, the entire success page, state labels, and the postal-code
autofill hint; 28 new checkoutPage keys across all 24 locales
(scripts/add-checkout-i18n-keys.mjs); pt zipLabel no longer says "CEP" for
non-BR countries. 10:29 PM · ZRosserMcIntosh
waive the onboarding fee for inbound assessment leads the API writes an "entitlements" block onto the submission at capture time, with expiresAt explicitly null the internal notification carries a flagged callout so whoever quotes sees it before pricing anything /admin/assessments shows it on every row — every lead in that queue arrived through the form, which is exactly the stated condition the jeweler's confirmation email states it in writing, so they hold it independently of our records 10:24 PM · ZRosserMcIntosh
Merge remote-tracking branch 'origin/main' into worktree-prospecting-upgrade 10:19 PM · ZRosserMcIntosh
three-step monthly ladder, tiered project pricing, booth QR Pricing was uneven rather than wrong: the cheapest way in was $2,000/mo + $1,250 onboarding + ad spend, which is a ~$4,750 first month for a jeweler whose whole ad budget is often $1,000-2,000. And a plain solitaire and an articulated mechanism were both quoted "from $325". Monthly ladder, rendered from OFFERS via a new recurringTier field: K99 Visibility $1,500/mo local search, GBP, reviews, AI search K99 Growth $2,500/mo + one ad platform <- flagged as the start K99 Partner $4,500/mo + Google and Meta, CRO, credits $1,500 over $1,499: every other K99 price is round ($3,500, $1,250, $325), so a charm price would be the only one on the page reading as retail discounting, and it has to be said out loud at a booth all day. It also sits $250 above standalone Local SEO while containing it, which makes the upgrade argument itself. Project work now has real ladders instead of one "starting at": Websites $3,500 sprint / $8,000-15,000 rebuild CAD $200 simple / $325 standard / from $550 complex CAD Desk $1,250 for 5 ($250 ea) / $2,250 for 10 ($225 ea) — was $1,500 for 5, an 8% discount for committing to monthly volume Renders $125 image / $400 four-pack / $300 360 spin / $600 launch — priced publicly for the first time local-seo and paid-traffic drop off the public page (they are components of the programs now) but stay in OFFERS, so proposals can still quote either on its own. PROPOSAL_TIERS now presents the same ladder outbound, so a proposal and the website never quote different numbers. Booth QR (src/app/pitch/share-qr.tsx): fixed top-right button opens a sheet with the branded code for /go/services-pitch — the attributed smart link, which already resolves. Verified by decoding the rendered screenshot: reads back the correct URL down to 25% scale. Lazy — nothing is generated until first open. Falls back to an unbranded code if the logo asset fails, because a plain QR that scans beats a pretty one that never appears. Mobile: no horizontal overflow at 390px (measured); card padding cut from p-8 to p-6 so text isn't squeezed into a ~270px well; wholesale grid goes single-column; hero gets right padding so the headline clears the QR button. Also: the Modern Luxury / The Atlantan press logo is white-on-transparent artwork and was rendering invisibly on the light press strip. Inverted via CSS so the original asset stays untouched. 10:16 PM · ZRosserMcIntosh
stop claiming the assessment is written by hand The confirmation email said "a real person goes through every submission by hand" and justified first-come-first-served with "because each one is done properly rather than generated." The report is generated and then reviewed and approved — so that was an overclaim, and it also implicitly denied that any AI is involved. Reworded to the part that is verifiable and still carries the weight: we look at the site, socials, search and AI visibility; every finding is tied to something we can point you at; the report is reviewed and approved before it goes out. First come first served is now stated as the policy it is — "requests are worked through in the order they arrive" — rather than justified by a mechanism that isn't true. Neither version says anything about how the report is produced, which is the right amount to say. The same claim was on the page ("go through your whole online presence by hand") and the in-browser success screen ("A real person here will go through your website…"), stated more strongly than the email. Fixing only the email would have left the page contradicting it, so all three now match. Both the API route and the form carry a COPY RULE in the header: claim neither extreme — not "written by hand", not "instant" — and promise the 48–72 hour window and the contents, not the process. Internal admin copy corrected the same way ("nothing goes out unapproved" rather than "a manual job"), since it was describing the pipeline wrongly too. Also removed the early-access checkbox from the form. It offered the project workspace, whose section came off the page earlier, so it was asking about "your system" with nothing on screen explaining what that meant. 9:59 PM · ZRosserMcIntosh
Merge remote-tracking branch 'origin/main' into worktree-prospecting-upgrade 9:34 PM · ZRosserMcIntosh
file every ad-hoc migration by domain and date-stamp them scripts/db-migrations/applied-sql/<domain>/ is the single home for hand-run SQL: 222 files across 32 domain folders, with a generated README index listing every file by date scripts/db-migrations/runners/ holds the 21 one-off Node/TS/shell runners (zero references anywhere, safe to relocate) supabase/storage-policies/ holds the 3 bucket-policy files that were loose at the supabase/ root every archived .sql now opens with its authoring date as a comment (-- 9 MAY 2026), taken from the filename timestamp where one exists, otherwise from the commit that introduced the file prisma/migrations/<ts>_<name>/migration.sql — Prisma checksums these in _prisma_migrations; editing one breaks migrate deploy on Vercel. Added a README there explaining that, and why loose .sql must not go back in. supabase/migrations/*.sql — the Supabase CLI owns that flat layout and every filename already carries its date. docs/**/*.sql — documentation artifacts, referenced by name from many docs. 9:13 PM · ZRosserMcIntosh
web dev section, colour press, $100 CAD credit, admin queue New Websites & Web Development block, given its own full-width layout with the stack named (Next.js/React/TypeScript, Postgres/Prisma/Supabase, Stripe and Shopify/WooCommerce migrations, Vercel, Core Web Vitals, structured data, 24 locales). It's the one service where the buyer — or the buyer's nephew who "does computers" — wants the actual stack before they'll believe it. Added at page level rather than to SERVICES, which is organised around jewelry craft and also feeds the homepage and /services grids. Press strip moved above the services grid and rendered in full colour. Every service CTA ("Audit My AI Visibility", "Build My Brand", …) now links to #assessment instead of a service detail page. They were the only dead ends on the page — someone clicking those wants us to do it, not to read. $100 new-client credit toward a first CAD design, under the price grid. Removed the "Apply for a wholesale account" CTA and the whole "people who'd actually do the work" section. Renders gallery moved to sit directly above "What it costs" (still hidden while its folder holds no images). The assessment intro now makes the follow-on explicit: the report already says what to work on, so there's no discovery phase to pay for and we can start immediately. Explicit data promise under the form: never sold, rented, or shared, used only to prepare the assessment, deleted on request. 9:00 PM · ZRosserMcIntosh
admin/collections: fix per-keystroke focus loss; make the page campaign-ready Real <form>: Enter submits, spinner while saving, no double-submit, disabled without a name Client slug generator now mirrors the server's ("Silver & Pearls" -> silver-pearls, not silver--pearls), with a live URL preview and blur-time normalization Featured is exclusive server-side: featuring one collection un-features the rest, so the /collections banner has one owner revalidatePath on every mutation route: admin edits and curation go live immediately instead of waiting out the 30-min ISR window Manage-products search matches metal/gemstone/collection name; 'Select all shown' bulk-select; 'N will move here / N will leave' summary before saving Delete uses a styled confirmation dialog with piece-count-aware copy instead of the native confirm() Featured badge overlays the card image; mosaic list gets a loading spinner; create dialog scrolls; deprecated onKeyPress replaced 8:33 PM · ZRosserMcIntosh
narrow to services sellable today; add renders gallery + press Removed the whole "approve your piece on a video call" section and the project-workspace early access section. The in-meeting tooling and the client workspace are how we WORK with clients, not something on sale, so they belong on the page only once they are. demos.tsx is kept, unrendered, for when that changes; the accuracy contract at the top of pitch-client says why. Casting & Production dropped from the service grid, and Bridal Growth System from the price list. Both are filtered at the page level rather than deleted from SERVICES / OFFERS, because those catalogues also drive the homepage grid, /services, and the outbound proposal generator, where they still belong. "Katura Wholesale — 7.77 Collection" shows as plain "Wholesale" with the 7.77 Collection called out in the body copy. Display-only override, so proposals keep the formal name. Identity is now explicitly K99 Jeweler Services throughout — hero eyebrow, metadata, footer.