import type { Project } from '@lib/schema'; /* InboxIntel case study. Source: F:\Documents\InboxIntel\InboxIntel + current app mockups. */ export const inboxintel: Project = { id: 'inboxintel', name: 'InboxIntel', order: 2, status: 'in-development', template: 'case-study', stack: [ { name: '.NET 10', context: { en: 'ASP.NET Core', no: 'ASP.NET Core' } }, { name: 'React', context: { en: 'TypeScript SPA', no: 'TypeScript-SPA' } }, { name: 'PostgreSQL', context: { en: 'EF Core · pgvector', no: 'EF Core · pgvector' } }, { name: 'Ollama', context: { en: 'embeddings · summaries', no: 'embeddings · sammendrag' } }, { name: 'Clean Architecture' }, { name: 'Docker' }, ], links: [], diagram: { viewBox: '0 0 1120 300', title: { en: 'InboxIntel architecture', no: 'InboxIntel-arkitektur' }, desc: { en: 'A React SPA calls an ASP.NET Core API layered as Clean Architecture. A hosted background worker syncs the external Gmail API into PostgreSQL with pgvector, and a local Ollama model produces the embeddings and summaries that power semantic search.', no: 'En React-app kaller et ASP.NET Core-API bygget som Clean Architecture. En bakgrunnstjeneste synkroniserer det eksterne Gmail-API-et inn i PostgreSQL med pgvector, og en lokal Ollama-modell lager embeddings og sammendrag som driver semantisk søk.', }, nodes: [ { id: 'spa', x: 40, y: 116, w: 170, h: 72, kind: 'internal', label: 'React SPA', sub: { en: 'split-view · search', no: 'delt visning · søk' }, }, { id: 'api', x: 290, y: 116, w: 200, h: 72, kind: 'primary', label: 'ASP.NET Core API', sub: { en: 'Clean Architecture', no: 'Clean Architecture' }, }, { id: 'ollama', x: 290, y: 232, w: 200, h: 56, kind: 'internal', label: 'Ollama', sub: { en: 'embeddings · summaries', no: 'embeddings · sammendrag' }, }, { id: 'pg', x: 580, y: 48, w: 180, h: 64, kind: 'internal', label: 'PostgreSQL', sub: { en: 'EF Core · pgvector', no: 'EF Core · pgvector' }, }, { id: 'worker', x: 580, y: 160, w: 180, h: 64, kind: 'internal', label: 'Sync worker', sub: { en: 'hosted · Polly', no: 'hostet · Polly' }, }, { id: 'gmail', x: 900, y: 152, w: 180, h: 72, kind: 'external', label: 'Gmail API', sub: { en: 'external · OAuth2', no: 'eksternt · OAuth2' }, }, ], edges: [ { d: 'M210 152 H290', kind: 'flow', label: '/api', labelX: 240, labelY: 144 }, { d: 'M390 188 V232', kind: 'flow', label: 'AI', labelX: 408, labelY: 216 }, { d: 'M490 134 L580 92', kind: 'flow' }, { d: 'M490 170 L580 192', kind: 'flow' }, { d: 'M670 160 V112', kind: 'flow' }, { d: 'M760 192 H900', kind: 'external', label: 'sync', labelX: 820, labelY: 184 }, ], }, media: [ { src: '/assets/projects/inboxintel/inbox.webp', width: 1600, height: 1000, alt: { en: 'InboxIntel split view: a persistent message list beside a resizable reading pane with an AI summary', no: 'InboxIntel delt visning: en fast meldingsliste ved siden av en justerbar leserute med AI-sammendrag', }, caption: { en: 'Split view — read and triage without ever opening a new tab', no: 'Delt visning — les og sorter uten å åpne en ny fane', }, }, { src: '/assets/projects/inboxintel/dashboard.webp', width: 1600, height: 1000, alt: { en: 'InboxIntel analytics dashboard: inbox-health grade, emails by category, 90-day volume and top senders', no: 'InboxIntel dashbord: innboks-helse, e-post per kategori, 90-dagers volum og toppavsendere', }, caption: { en: 'Analytics — an inbox-health grade, categories, volume trend and top senders', no: 'Analyse — innboks-helse, kategorier, volumtrend og toppavsendere', }, }, { src: '/assets/projects/inboxintel/search.webp', width: 1600, height: 1000, alt: { en: 'InboxIntel search: hybrid semantic and full-text results with why-matched highlights and relevance scores', no: 'InboxIntel søk: hybrid semantisk og fulltekst med treff-forklaring og relevans-score', }, caption: { en: 'Search — hybrid semantic + full-text, with why-matched highlights and relevance', no: 'Søk — hybrid semantisk + fulltekst, med treff-forklaring og relevans', }, }, ], content: { en: { valueProp: 'A self-hosted inbox intelligence and cleanup tool — semantic search, AI summaries and inbox-health analytics over your mail.', cardTeaser: 'Self-hosted inbox intelligence — hybrid semantic + full-text search, AI summaries and inbox-health analytics, with safe cleanup by design.', tldr: { what: 'Inbox intelligence: split-view reading, semantic search, AI summaries, inbox-health analytics and safe cleanup.', why: 'To practise Clean Architecture properly, and make search useful and destructive operations safe by design.', stack: '.NET 10 · PostgreSQL + pgvector · React · Ollama · Docker · Clean Architecture.', role: 'Sole architect and developer, front to back.', }, sections: [ { kind: 'problem', heading: 'Problem & context', anchorId: 'problem', blocks: [ { type: 'p', text: 'A busy inbox is hard to search and dangerous to clean up — keyword search misses what you meant, and one wrong bulk filter deletes things you can’t get back. I built InboxIntel to make an inbox actually searchable (by meaning, not just words), understandable at a glance, and safe to tidy — all self-hosted, with a clean, testable backend.', }, ], }, { kind: 'architecture', heading: 'Architecture', anchorId: 'architecture', blocks: [ { type: 'p', text: 'A React SPA over an ASP.NET Core API built as Clean Architecture — the dependency rule points inward (Api → Infrastructure → Application → Domain), and controllers hold no business logic. A hosted background worker syncs Gmail into PostgreSQL; a local Ollama model generates embeddings (stored with pgvector) and message summaries, which power hybrid semantic + full-text search.', }, { type: 'ul', items: [ 'Reading: a persistent list beside a resizable pane with an AI summary — triage without opening a new tab.', 'Search: semantic (pgvector) + full-text, with why-matched highlights and a relevance score.', 'Analytics: an inbox-health grade, emails by category, a 90-day volume trend and top senders.', 'Cleanup: preview-then-confirm on every destructive action; the AI is advisory only.', ], }, ], }, { kind: 'decisions', heading: 'Key decisions & trade-offs', anchorId: 'decisions', decisions: [ { n: 1, choice: 'Local Ollama embeddings + pgvector for semantic search.', alternative: 'A hosted embeddings API and a vector database.', rationale: 'Mail is private, so embeddings stay on my own hardware and live right next to the data in Postgres via pgvector — one datastore, no third party, no per-call cost. The trade-off is running the model myself.', }, { n: 2, choice: 'Encrypt OAuth refresh tokens at rest and never log them.', alternative: 'Store them as plain columns.', rationale: 'Refresh tokens are long-lived keys to someone’s mailbox. They’re encrypted with the ASP.NET Core Data Protection API (AES), with keys persisted to a mounted volume — the single most important security decision in the app.', }, { n: 3, choice: 'Every destructive action is preview-then-confirm.', alternative: 'Delete immediately on request.', rationale: 'All cleanup and unsubscribe actions require a server-side preview and an explicit Confirmed flag. The AI layer is advisory only and can never trigger a deletion.', }, ], }, { kind: 'security', heading: 'Security & production notes', anchorId: 'security', blocks: [ { type: 'ul', items: [ 'Google-OAuth gated; Gmail scopes are read/modify only — no send scope is ever requested.', 'Embeddings and summaries are generated locally (Ollama); mail never leaves the box for a third-party model.', 'Polly retry/backoff against Gmail rate limits; Serilog structured logging that never records tokens.', 'Clean Architecture keeps the layers testable; integration tests boot the API host and assert authorization.', ], }, ], }, { kind: 'screenshots', heading: 'Screenshots', anchorId: 'screenshots', }, { kind: 'next', heading: 'Status & what’s next', anchorId: 'next', blocks: [ { type: 'p', text: 'In active development. Search, summaries and analytics work end to end; next up is tuning the semantic-recall thresholds, expanding the integration-test suite, and working toward connectors beyond Gmail so it becomes a genuinely universal inbox tool.', }, ], }, ], }, no: { valueProp: 'Et egendriftet verktøy for innboks-intelligens og opprydding — semantisk søk, AI-sammendrag og innboks-helse over e-posten din.', cardTeaser: 'Egendriftet innboks-intelligens — hybrid semantisk + fulltekst-søk, AI-sammendrag og innboks-helse, med trygg opprydding fra bunnen.', tldr: { what: 'Innboks-intelligens: delt visning, semantisk søk, AI-sammendrag, innboks-helse og trygg opprydding.', why: 'For å øve på Clean Architecture skikkelig, gjøre søk nyttig og destruktive operasjoner trygge fra bunnen.', stack: '.NET 10 · PostgreSQL + pgvector · React · Ollama · Docker · Clean Architecture.', role: 'Eneste arkitekt og utvikler, fra ende til ende.', }, sections: [ { kind: 'problem', heading: 'Problem og kontekst', anchorId: 'problem', blocks: [ { type: 'p', text: 'En travel innboks er vanskelig å søke i og farlig å rydde i — nøkkelordsøk bommer på det du mente, og ett feil massefilter sletter ting du ikke får tilbake. Jeg bygde InboxIntel for å gjøre en innboks faktisk søkbar (på mening, ikke bare ord), forståelig med ett blikk, og trygg å rydde i — alt egendriftet, med en ryddig og testbar backend.', }, ], }, { kind: 'architecture', heading: 'Arkitektur', anchorId: 'arkitektur', blocks: [ { type: 'p', text: 'En React-app over et ASP.NET Core-API bygget som Clean Architecture — avhengighetsregelen peker innover (Api → Infrastructure → Application → Domain), og kontrollerne har ingen forretningslogikk. En bakgrunnstjeneste synkroniserer Gmail inn i PostgreSQL; en lokal Ollama-modell lager embeddings (lagret med pgvector) og sammendrag, som driver hybrid semantisk + fulltekst-søk.', }, { type: 'ul', items: [ 'Lesing: en fast liste ved siden av en justerbar rute med AI-sammendrag — sorter uten å åpne en ny fane.', 'Søk: semantisk (pgvector) + fulltekst, med treff-forklaring og relevans-score.', 'Analyse: en innboks-helsekarakter, e-post per kategori, 90-dagers volumtrend og toppavsendere.', 'Opprydding: forhåndsvis-så-bekreft på hver destruktive handling; AI-en er kun rådgivende.', ], }, ], }, { kind: 'decisions', heading: 'Viktige valg og avveininger', anchorId: 'beslutninger', decisions: [ { n: 1, choice: 'Lokale Ollama-embeddings + pgvector for semantisk søk.', alternative: 'Et hostet embeddings-API og en egen vektordatabase.', rationale: 'E-post er privat, så embeddings blir på min egen maskin og ligger rett ved siden av dataene i Postgres via pgvector — én datalagring, ingen tredjepart, ingen kostnad per kall. Avveiningen er å drifte modellen selv.', }, { n: 2, choice: 'Kryptere OAuth-refresh-tokens i ro og aldri logge dem.', alternative: 'Lagre dem som vanlige kolonner.', rationale: 'Refresh-tokens er langlevde nøkler til noens innboks. De krypteres med ASP.NET Core Data Protection API (AES), med nøkler lagret på et montert volum — det viktigste sikkerhetsvalget i appen.', }, { n: 3, choice: 'Alle destruktive handlinger er forhåndsvis-så-bekreft.', alternative: 'Slette umiddelbart ved forespørsel.', rationale: 'All opprydding og avmelding krever en server-side forhåndsvisning og et eksplisitt Confirmed-flagg. AI-laget er kun rådgivende og kan aldri utløse en sletting.', }, ], }, { kind: 'security', heading: 'Sikkerhet og drift', anchorId: 'sikkerhet', blocks: [ { type: 'ul', items: [ 'Google-OAuth-beskyttet; Gmail-scopes er kun lese/endre — send-scope blir aldri etterspurt.', 'Embeddings og sammendrag lages lokalt (Ollama); e-post forlater aldri maskinen til en tredjepartsmodell.', 'Polly retry/backoff mot Gmails rategrenser; strukturert Serilog-logging som aldri lagrer tokens.', 'Clean Architecture holder lagene testbare; integrasjonstester starter API-verten og sjekker autorisasjon.', ], }, ], }, { kind: 'screenshots', heading: 'Skjermbilder', anchorId: 'skjermbilder', }, { kind: 'next', heading: 'Status og veien videre', anchorId: 'videre', blocks: [ { type: 'p', text: 'Under aktiv utvikling. Søk, sammendrag og analyse fungerer ende til ende; neste steg er å justere tersklene for semantisk gjenfinning, utvide integrasjonstestene, og jobbe mot koblinger utover Gmail slik at det blir et virkelig universelt innboks-verktøy.', }, ], }, ], }, }, };