Files
ResumeSite/site/src/data/projects/inboxintel.ts
T
cesnimda 8132c202ba fix: mobile theme toggle + UI polish; refresh both case studies
- fix: theme toggle did nothing on mobile — duplicate id=theme-toggle bound only the
  desktop button; bind all [data-theme-toggle] instead (root cause)
- header: 2px gap top/bottom; TraceMotif svg 24->28 so the status dot isn't clipped
- remove private git.cesnimda.uk links (footer, contact, mobile nav, JSON-LD sameAs);
  soften homelab diagram label
- both projects now 'in development'; about wording: Gmail analytics -> inbox cleanup
- JobTrack: refreshed to current app (Kanban pipeline, deterministic CV match, assistive
  AI, EF Core) + new mockups; InboxIntel: .NET 10, pgvector + Ollama semantic search,
  inbox-health analytics, split-view + new mockups; meta descriptions updated
- new ATS CV PDFs (from the new docx, via Word)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:54:30 +02:00

351 lines
15 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 + 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 cant 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 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: [
'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 & whats 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.',
},
],
},
],
},
},
};