import type { Project } from '@lib/schema'; /* InboxIntel case study. Source: F:\Documents\InboxIntel\InboxIntel (README + docs). */ export const inboxintel: Project = { id: 'inboxintel', name: 'InboxIntel', order: 2, status: 'in-development', template: 'case-study', stack: [ { name: '.NET 8', context: { en: 'ASP.NET Core', no: 'ASP.NET Core' } }, { name: 'PostgreSQL', context: { en: 'EF Core + Npgsql', no: 'EF Core + Npgsql' } }, { name: 'React 18', context: { en: 'Vite · Chart.js', no: 'Vite · Chart.js' } }, { name: 'Gmail API', context: { en: 'OAuth2 · Polly', no: 'OAuth2 · Polly' } }, { name: 'Serilog' }, { 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 (Api → Infrastructure → Application → Domain). A hosted background worker syncs the external Gmail API into PostgreSQL with Polly retry/backoff.', no: 'En React-app kaller et ASP.NET Core-API bygget som Clean Architecture (Api → Infrastructure → Application → Domain). En bakgrunnstjeneste synkroniserer det eksterne Gmail-API-et inn i PostgreSQL med Polly retry/backoff.', }, nodes: [ { id: 'spa', x: 40, y: 116, w: 170, h: 72, kind: 'internal', label: 'React SPA', sub: { en: 'Vite · Chart.js', no: 'Vite · Chart.js' }, }, { 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: 'pg', x: 580, y: 48, w: 180, h: 64, kind: 'internal', label: 'PostgreSQL', sub: { en: 'EF Core · Npgsql', no: 'EF Core · Npgsql' }, }, { 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: '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: 'placeholder:inboxintel-dashboard', width: 1600, height: 1000, alt: { en: 'InboxIntel draggable analytics dashboard with charts', no: 'InboxIntel dashbord med flyttbare analyse-widgets', }, caption: { en: 'Draggable analytics dashboard (react-grid-layout)', no: 'Dashbord med flyttbare widgets (react-grid-layout)', }, }, { src: 'placeholder:inboxintel-cleanup', width: 1600, height: 1000, alt: { en: 'Bulk cleanup preview screen requiring explicit confirmation', no: 'Forhåndsvisning av masseopprydding som krever bekreftelse', }, caption: { en: 'Cleanup preview — nothing is deleted without a confirmed flag', no: 'Oppryddings-forhåndsvisning — ingenting slettes uten bekreftelse', }, }, ], content: { en: { valueProp: 'Gmail analytics and safe bulk cleanup, built as a disciplined Clean Architecture.', cardTeaser: 'Gmail analytics and safe bulk cleanup — Clean Architecture, encrypted OAuth tokens, preview-before-delete by design.', tldr: { what: 'Gmail analytics, cleanup and unsubscribe management with a background sync worker.', why: 'To practise Clean Architecture properly and make destructive operations safe by design.', stack: '.NET 8 · PostgreSQL · React 18 · Gmail API · Serilog · Polly · Docker.', role: 'Sole architect and developer, front to back.', }, sections: [ { kind: 'problem', heading: 'Problem & context', anchorId: 'problem', blocks: [ { type: 'p', text: 'A busy inbox needs analytics and cleanup, but bulk operations on real mail are dangerous — one wrong filter can delete things you can’t get back. I built InboxIntel to explore two things at once: a clean, testable backend architecture, and destructive operations that are safe by construction.', }, ], }, { kind: 'architecture', heading: 'Architecture', anchorId: 'architecture', blocks: [ { type: 'p', text: 'A four-project Clean Architecture solution behind a React SPA. The dependency rule points inward: Api → Infrastructure → Application → Domain. Controllers hold no business logic — they delegate to Application-layer service interfaces resolved through DI. A hosted background worker syncs Gmail into PostgreSQL with Polly retry and backoff.', }, { type: 'ul', items: [ 'Domain: entities and enums, no external dependencies.', 'Application: service interfaces, DTOs, validators, the Gmail query parser.', 'Infrastructure: EF Core, the Gmail client, the sync worker, AI and export.', 'Api: ASP.NET Core controllers, auth, DI and Serilog — thin by design.', ], }, ], }, { kind: 'decisions', heading: 'Key decisions & trade-offs', anchorId: 'decisions', decisions: [ { n: 1, choice: 'Full Clean Architecture across four projects.', alternative: 'A single project would have shipped faster.', rationale: 'I wanted the dependency rule to be enforced by the project structure, not by discipline alone. It costs more ceremony up front and pays back in testability and clear boundaries.', }, { 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: [ 'Gmail scopes are read/modify only — no send scope is ever requested.', 'Polly provides retry and backoff against Gmail API rate limits and transient failures.', 'FluentValidation on inputs; Serilog structured logging that never records tokens.', 'Unit tests cover the query parser and unsubscribe extraction; integration tests boot the API host and assert authorization is enforced.', ], }, ], }, { kind: 'screenshots', heading: 'Screenshots', anchorId: 'screenshots', }, { kind: 'next', heading: 'Status & what’s next', anchorId: 'next', blocks: [ { type: 'p', text: 'InboxIntel is in active development: the architecture is complete and the layers compile and are wired end to end, with a few integration points (Gmail parsing edge cases, AI prompt tuning) left as clearly-marked extension points. Next up is hardening those edges and expanding the integration-test suite.', }, ], }, ], }, no: { valueProp: 'Gmail-analyse og trygg masseopprydding, bygget som en ryddig Clean Architecture.', cardTeaser: 'Gmail-analyse og trygg masseopprydding — Clean Architecture, krypterte OAuth-tokens, forhåndsvisning før sletting.', tldr: { what: 'Gmail-analyse, opprydding og avmeldingshåndtering med en bakgrunns-synk.', why: 'For å øve på Clean Architecture skikkelig og gjøre destruktive operasjoner trygge fra bunnen.', stack: '.NET 8 · PostgreSQL · React 18 · Gmail API · Serilog · Polly · Docker.', 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 trenger analyse og opprydding, men masseoperasjoner på ekte e-post er farlige — ett feil filter kan slette ting du ikke får tilbake. Jeg bygde InboxIntel for å utforske to ting samtidig: en ryddig, testbar backend-arkitektur, og destruktive operasjoner som er trygge fra bunnen.', }, ], }, { kind: 'architecture', heading: 'Arkitektur', anchorId: 'arkitektur', blocks: [ { type: 'p', text: 'En løsning med fire prosjekter i Clean Architecture bak en React-app. Avhengighetsregelen peker innover: Api → Infrastructure → Application → Domain. Kontrollerne har ingen forretningslogikk — de delegerer til tjenestegrensesnitt i Application-laget via DI. En bakgrunnstjeneste synkroniserer Gmail inn i PostgreSQL med Polly retry og backoff.', }, { type: 'ul', items: [ 'Domain: entiteter og enums, uten eksterne avhengigheter.', 'Application: tjenestegrensesnitt, DTO-er, validatorer og Gmail-søkeparser.', 'Infrastructure: EF Core, Gmail-klient, synk-tjeneste, AI og eksport.', 'Api: ASP.NET Core-kontrollere, auth, DI og Serilog — tynt med hensikt.', ], }, ], }, { kind: 'decisions', heading: 'Viktige valg og avveininger', anchorId: 'beslutninger', decisions: [ { n: 1, choice: 'Full Clean Architecture over fire prosjekter.', alternative: 'Ett prosjekt hadde vært raskere å levere.', rationale: 'Jeg ville at avhengighetsregelen skulle håndheves av prosjektstrukturen, ikke bare av disiplin. Det koster mer seremoni i starten og betaler seg i testbarhet og tydelige grenser.', }, { 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: [ 'Gmail-scopes er kun lese/endre — send-scope blir aldri etterspurt.', 'Polly gir retry og backoff mot Gmail-API-ets rategrenser og forbigående feil.', 'FluentValidation på input; strukturert Serilog-logging som aldri lagrer tokens.', 'Enhetstester dekker søkeparser og avmeldings-uttrekk; integrasjonstester starter API-verten og sjekker at autorisasjon håndheves.', ], }, ], }, { kind: 'screenshots', heading: 'Skjermbilder', anchorId: 'skjermbilder', }, { kind: 'next', heading: 'Status og veien videre', anchorId: 'videre', blocks: [ { type: 'p', text: 'InboxIntel er under aktiv utvikling: arkitekturen er komplett, lagene kompilerer og er koblet ende til ende, med noen integrasjonspunkter (kanttilfeller i Gmail-parsing, tuning av AI-prompter) igjen som tydelig markerte utvidelsespunkter. Neste steg er å herde disse kantene og utvide integrasjonstestene.', }, ], }, ], }, }, };