Files
ResumeSite/site/src/data/projects/inboxintel.ts
T
cesnimda 033c9ec315 test: vitest unit + playwright e2e suites; fix a11y, contrast, no-JS reveal
- vitest: dictionary parity, slug-map bijection, content-schema validation, JSON-LD (23)
- playwright: both locales, language-switch mapping (incl. no-JS), CV downloads,
  form happy-path + honeypot, axe a11y on 5 templates x 2 themes (23)
- fix: raise ink-faint + light accent to meet WCAG AA 4.5:1 (axe-verified)
- fix: gate reveal animations behind html.js so content is visible without JS
  (progressive enhancement - was hidden at opacity 0); scan reduced-motion path
- fix: validate contact form before the anti-bot time-trap (no false success)
- chore: ESLint node globals, typed diagram props, resvg fontFiles, prettier pass

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:14:18 +02:00

326 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 cant 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 someones mailbox. Theyre 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 & whats 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.',
},
],
},
],
},
},
};