8132c202ba
- 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>
351 lines
15 KiB
TypeScript
351 lines
15 KiB
TypeScript
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.',
|
||
},
|
||
],
|
||
},
|
||
],
|
||
},
|
||
},
|
||
};
|