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>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { Project } from '@lib/schema';
|
||||
|
||||
/* InboxIntel case study. Source: F:\Documents\InboxIntel\InboxIntel (README + docs). */
|
||||
/* InboxIntel case study. Source: F:\Documents\InboxIntel\InboxIntel + current app mockups. */
|
||||
export const inboxintel: Project = {
|
||||
id: 'inboxintel',
|
||||
name: 'InboxIntel',
|
||||
@@ -8,11 +8,11 @@ export const inboxintel: Project = {
|
||||
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: '.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: [],
|
||||
@@ -20,8 +20,8 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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: [
|
||||
{
|
||||
@@ -32,7 +32,7 @@ export const inboxintel: Project = {
|
||||
h: 72,
|
||||
kind: 'internal',
|
||||
label: 'React SPA',
|
||||
sub: { en: 'Vite · Chart.js', no: 'Vite · Chart.js' },
|
||||
sub: { en: 'split-view · search', no: 'delt visning · søk' },
|
||||
},
|
||||
{
|
||||
id: 'api',
|
||||
@@ -44,6 +44,16 @@ export const inboxintel: Project = {
|
||||
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,
|
||||
@@ -52,7 +62,7 @@ export const inboxintel: Project = {
|
||||
h: 64,
|
||||
kind: 'internal',
|
||||
label: 'PostgreSQL',
|
||||
sub: { en: 'EF Core · Npgsql', no: 'EF Core · Npgsql' },
|
||||
sub: { en: 'EF Core · pgvector', no: 'EF Core · pgvector' },
|
||||
},
|
||||
{
|
||||
id: 'worker',
|
||||
@@ -77,6 +87,7 @@ export const inboxintel: Project = {
|
||||
],
|
||||
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' },
|
||||
@@ -85,42 +96,55 @@ export const inboxintel: Project = {
|
||||
},
|
||||
media: [
|
||||
{
|
||||
src: 'placeholder:inboxintel-dashboard',
|
||||
src: '/assets/projects/inboxintel/inbox.webp',
|
||||
width: 1600,
|
||||
height: 1000,
|
||||
alt: {
|
||||
en: 'InboxIntel draggable analytics dashboard with charts',
|
||||
no: 'InboxIntel dashbord med flyttbare analyse-widgets',
|
||||
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: 'Draggable analytics dashboard (react-grid-layout)',
|
||||
no: 'Dashbord med flyttbare widgets (react-grid-layout)',
|
||||
en: 'Split view — read and triage without ever opening a new tab',
|
||||
no: 'Delt visning — les og sorter uten å åpne en ny fane',
|
||||
},
|
||||
},
|
||||
{
|
||||
src: 'placeholder:inboxintel-cleanup',
|
||||
src: '/assets/projects/inboxintel/dashboard.webp',
|
||||
width: 1600,
|
||||
height: 1000,
|
||||
alt: {
|
||||
en: 'Bulk cleanup preview screen requiring explicit confirmation',
|
||||
no: 'Forhåndsvisning av masseopprydding som krever bekreftelse',
|
||||
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: 'Cleanup preview — nothing is deleted without a confirmed flag',
|
||||
no: 'Oppryddings-forhåndsvisning — ingenting slettes uten bekreftelse',
|
||||
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:
|
||||
'Gmail analytics and safe bulk cleanup, built as a disciplined Clean Architecture.',
|
||||
'A self-hosted inbox intelligence and cleanup tool — semantic search, AI summaries and inbox-health analytics over your mail.',
|
||||
cardTeaser:
|
||||
'Gmail analytics and safe bulk cleanup — Clean Architecture, encrypted OAuth tokens, preview-before-delete by design.',
|
||||
'Self-hosted inbox intelligence — hybrid semantic + full-text search, AI summaries and inbox-health analytics, with safe cleanup 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.',
|
||||
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: [
|
||||
@@ -131,7 +155,7 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -142,15 +166,15 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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: [
|
||||
'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.',
|
||||
'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.',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -162,10 +186,10 @@ export const inboxintel: Project = {
|
||||
decisions: [
|
||||
{
|
||||
n: 1,
|
||||
choice: 'Full Clean Architecture across four projects.',
|
||||
alternative: 'A single project would have shipped faster.',
|
||||
choice: 'Local Ollama embeddings + pgvector for semantic search.',
|
||||
alternative: 'A hosted embeddings API and a vector database.',
|
||||
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.',
|
||||
'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,
|
||||
@@ -191,10 +215,10 @@ export const inboxintel: Project = {
|
||||
{
|
||||
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.',
|
||||
'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.',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -211,20 +235,21 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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: 'Gmail-analyse og trygg masseopprydding, bygget som en ryddig Clean Architecture.',
|
||||
valueProp:
|
||||
'Et egendriftet verktøy for innboks-intelligens og opprydding — semantisk søk, AI-sammendrag og innboks-helse over e-posten din.',
|
||||
cardTeaser:
|
||||
'Gmail-analyse og trygg masseopprydding — Clean Architecture, krypterte OAuth-tokens, forhåndsvisning før sletting.',
|
||||
'Egendriftet innboks-intelligens — hybrid semantisk + fulltekst-søk, AI-sammendrag og innboks-helse, med trygg opprydding fra bunnen.',
|
||||
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.',
|
||||
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: [
|
||||
@@ -235,7 +260,7 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -246,15 +271,15 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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: [
|
||||
'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.',
|
||||
'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.',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -266,10 +291,10 @@ export const inboxintel: Project = {
|
||||
decisions: [
|
||||
{
|
||||
n: 1,
|
||||
choice: 'Full Clean Architecture over fire prosjekter.',
|
||||
alternative: 'Ett prosjekt hadde vært raskere å levere.',
|
||||
choice: 'Lokale Ollama-embeddings + pgvector for semantisk søk.',
|
||||
alternative: 'Et hostet embeddings-API og en egen vektordatabase.',
|
||||
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.',
|
||||
'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,
|
||||
@@ -295,10 +320,10 @@ export const inboxintel: Project = {
|
||||
{
|
||||
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.',
|
||||
'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.',
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -315,7 +340,7 @@ export const inboxintel: Project = {
|
||||
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.',
|
||||
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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user