import type { Project } from '@lib/schema';

/* JobTrack case study. Source: D:\Job tracker (full codebase + docs audit, 2026-07-12). */
export const jobtrack: Project = {
  id: 'jobtrack',
  name: 'JobTrack',
  order: 1,
  status: 'in-development',
  template: 'case-study',
  stack: [
    { name: 'ASP.NET Core', context: { en: '.NET 9 API', no: '.NET 9-API' } },
    { name: 'Next.js', context: { en: 'React 19 · TypeScript', no: 'React 19 · TypeScript' } },
    { name: 'EF Core', context: { en: 'SQLite · MySQL/MariaDB', no: 'SQLite · MySQL/MariaDB' } },
    { name: 'FastAPI + Ollama', context: { en: 'local AI service', no: 'lokal AI-tjeneste' } },
    {
      name: 'Multi-provider email',
      context: { en: 'Gmail · Microsoft · IMAP', no: 'Gmail · Microsoft · IMAP' },
    },
    { name: 'Docker' },
  ],
  links: [],
  diagram: {
    viewBox: '0 0 1120 300',
    title: { en: 'JobTrack architecture', no: 'JobTrack-arkitektur' },
    desc: {
      en: 'A Next.js single-page app talks to an ASP.NET Core API, which persists via EF Core with attachments on disk, calls a FastAPI/Ollama AI service, and syncs correspondence from Gmail, Microsoft Graph and IMAP through one provider abstraction.',
      no: 'En Next.js-app snakker med et ASP.NET Core-API som lagrer via EF Core med vedlegg på disk, kaller en FastAPI/Ollama AI-tjeneste, og synkroniserer korrespondanse fra Gmail, Microsoft Graph og IMAP gjennom én felles abstraksjon.',
    },
    nodes: [
      {
        id: 'react',
        x: 40,
        y: 112,
        w: 180,
        h: 72,
        kind: 'internal',
        label: 'Next.js SPA',
        sub: { en: 'nginx · CSR', no: 'nginx · CSR' },
      },
      {
        id: 'api',
        x: 300,
        y: 112,
        w: 200,
        h: 72,
        kind: 'primary',
        label: 'ASP.NET Core API',
        sub: { en: 'auth · rules · CV match', no: 'auth · regler · CV-match' },
      },
      {
        id: 'data',
        x: 600,
        y: 44,
        w: 180,
        h: 64,
        kind: 'internal',
        label: 'EF Core + files',
        sub: { en: 'data · attachments', no: 'data · vedlegg' },
      },
      {
        id: 'ai',
        x: 600,
        y: 156,
        w: 180,
        h: 64,
        kind: 'internal',
        label: 'AI service',
        sub: { en: 'FastAPI → Ollama', no: 'FastAPI → Ollama' },
      },
      {
        id: 'gmail',
        x: 900,
        y: 112,
        w: 180,
        h: 72,
        kind: 'external',
        label: 'Email providers',
        sub: { en: 'Gmail · MS Graph · IMAP', no: 'Gmail · MS Graph · IMAP' },
      },
    ],
    edges: [
      { d: 'M220 148 H300', kind: 'flow', label: '/api', labelX: 248, labelY: 140 },
      { d: 'M500 130 L600 88', kind: 'flow' },
      { d: 'M500 166 L600 200', kind: 'flow' },
      { d: 'M500 148 H900', kind: 'external', label: 'sync', labelX: 690, labelY: 140 },
    ],
  },
  media: [
    {
      src: '/assets/projects/jobtrack/workspace.webp',
      width: 1600,
      height: 1000,
      alt: {
        en: 'JobTrack job workspace: one application with its details, CV match and recruiter thread',
        no: 'JobTrack jobb-arbeidsrom: én søknad med detaljer, CV-match og rekruttør-tråd',
      },
      caption: {
        en: 'Job workspace — everything about one application in one place',
        no: 'Jobb-arbeidsrom — alt om én søknad på ett sted',
      },
    },
    {
      src: '/assets/projects/jobtrack/pipeline.webp',
      width: 1600,
      height: 1000,
      alt: {
        en: 'JobTrack Kanban pipeline with applications across Applied, Waiting, Interview and Offer',
        no: 'JobTrack Kanban-pipeline med søknader fordelt på stadiene',
      },
      caption: {
        en: 'Pipeline — drag applications between stages, from Applied to Offer',
        no: 'Pipeline — dra søknader mellom stadier, fra Søkt til Tilbud',
      },
    },
    {
      src: '/assets/projects/jobtrack/dashboard.webp',
      width: 1600,
      height: 1000,
      alt: {
        en: 'JobTrack dashboard with response rate, funnel and time-in-stage',
        no: 'JobTrack-dashbord med svarrate, trakt og tid-per-stadium',
      },
      caption: {
        en: 'Dashboard — response rates, funnel and skill demand across the search',
        no: 'Dashbord — svarrater, trakt og etterspurte ferdigheter',
      },
    },
  ],
  content: {
    en: {
      valueProp:
        'An AI-assisted job-search workspace — import a role, tailor your CV, and track every application from applied to offer.',
      cardTeaser:
        'Import a role, tailor your CV, and track every application — recruiter threads and a Kanban pipeline in one workspace.',
      tldr: {
        what: 'Full-stack job-search workspace: pipeline, deterministic CV match, multi-provider recruiter threads, AI drafting.',
        why: 'A real problem — my own job search needed production-grade tooling, not a spreadsheet.',
        stack:
          'Next.js + ASP.NET Core (EF Core) · FastAPI/Ollama local AI · Gmail/Microsoft/IMAP · Docker.',
        role: 'Everything: product, backend, frontend, ops and security.',
      },
      sections: [
        {
          kind: 'problem',
          heading: 'Problem & context',
          anchorId: 'problem',
          blocks: [
            {
              type: 'p',
              text: 'A serious job search spreads across spreadsheets, email threads, notes apps and scattered documents. Nothing shows you, at a glance, which applications need attention or what was said last, and every CV-tailoring tool either charges a subscription or hands back an opaque "match score" you can\'t interrogate. I wanted one focused workspace — from importing a role to the final offer — built to the standard I would ship at work, not as a throwaway.',
            },
          ],
        },
        {
          kind: 'solution',
          heading: 'Solution',
          anchorId: 'solution',
          blocks: [
            {
              type: 'p',
              text: 'JobTrack is a self-hosted workspace that carries one job application through its whole life: import the posting, see how your CV stacks up against it, track it through a pipeline, and keep every recruiter message attached to the right job — with an assistive AI layer that drafts but never sends anything on your behalf.',
            },
            {
              type: 'ul',
              items: [
                'Pipeline: a Kanban board — Applied, Waiting, Interview, Offer, Rejected, Ghosted — drag to update, with rule-based auto-transition to Ghosted after inactivity.',
                'CV match: deterministic keyword coverage (matched vs missing), not a black-box score.',
                'Correspondence: one unified thread view across Gmail, Microsoft/Outlook and generic IMAP, auto-linked to the right job.',
                'AI is assistive, never autonomous: it drafts CVs, cover letters and follow-ups — you always review and send.',
              ],
            },
          ],
        },
        {
          kind: 'features',
          heading: 'Feature showcase',
          anchorId: 'features',
          features: [
            {
              title: 'Deterministic CV↔job match score',
              value:
                'See exactly which keywords from a posting your CV covers and which are missing — a stable, reproducible number instead of a $50/month "AI fit score" you can\'t argue with.',
              technical:
                'A from-scratch weighted-keyword algorithm (JobCvMatchService): curated skill tags at weight 3 plus a title bonus, frequency-ranked stop-word-filtered posting terms at weight 1, matched with a hand-rolled word-boundary check so "go" never matches "goal". No AI call — same inputs always produce the same score, with an honest "not enough signal" flag instead of false confidence.',
            },
            {
              title: 'One thread, any provider',
              value:
                'Recruiter emails land in the right job automatically, whether they come through Gmail, Outlook or any other IMAP mailbox — no more digging through three inboxes to find what a recruiter said.',
              technical:
                'Correspondence import runs through a single IEmailProvider interface (SearchAsync / ListThreadMessagesAsync / GetMessageAsync) behind a provider registry, with a Provider discriminator column on each stored message. Gmail shipped first; Microsoft Graph and IMAP were added later as separate implementations of the same contract — no controller logic was duplicated per provider.',
            },
            {
              title: 'AI-assisted drafting, human-gated sending',
              value:
                'Get a tailored CV, cover letter or follow-up draft in seconds — but nothing is ever sent or auto-applied without you reading and approving it first.',
              technical:
                'CVs are OCR/text-extracted (FastAPI + Tesseract/PyMuPDF), classified into structured blocks by a local Ollama model (qwen2.5:7b), then rendered to PDF via a templated Playwright export. Prompts wrap untrusted CV text in explicit delimiters with an instruction to treat it as inert data, not commands — closing off prompt injection from a CV someone else wrote.',
            },
          ],
        },
        {
          kind: 'architecture',
          heading: 'Engineering highlights',
          anchorId: 'architecture',
          blocks: [
            {
              type: 'p',
              text: 'The backend is a modular monolith on ASP.NET Core (.NET 9) with six hosted background services — rules engine, follow-up reminders, scheduled exports, job enrichment, CV processing and an AI-readiness probe — running off the request path. EF Core targets SQLite by default and switches to MySQL/MariaDB via the Pomelo provider for larger deployments. Every tenant table is scoped by a global EF Core query filter (deny-on-null by design), so one database safely serves multiple users.',
            },
            {
              type: 'ul',
              items: [
                'Auth: a custom policy scheme routes Google-issued, Microsoft-issued and local JWTs to the right handler; CSRF double-submit on cookie sessions; rate-limited login and email-sending endpoints.',
                'CI/CD: Gitea Actions builds and tests the backend, builds the frontend, then SSHes into the production host and redeploys via Docker Compose on every push to main — no staging environment, by deliberate choice.',
                'i18n: full English + Norwegian Bokmål UI, a genuine differentiator against the mostly US-centric competitors (Teal, Huntr, Simplify).',
              ],
            },
          ],
        },
        {
          kind: 'decisions',
          heading: 'Challenges',
          anchorId: 'decisions',
          decisions: [
            {
              n: 1,
              choice:
                'Generalize Gmail-only import into a provider-neutral IEmailProvider abstraction, incrementally.',
              alternative:
                'Rewrite the correspondence layer once, up front, for every provider we might ever need.',
              rationale:
                'Import started Gmail-only; when Microsoft and IMAP support were requested later, I extracted the interface across several small PRs instead of a big-bang rewrite. Outcome: three working providers behind one contract, and no controller ever had to be duplicated per provider.',
            },
            {
              n: 2,
              choice: 'SSRF-harden the job-import URL fetcher before shipping it.',
              alternative: 'Trust the pasted URL and fetch it directly — it is only a job posting.',
              rationale:
                'Letting users paste an arbitrary URL for server-side scraping is a classic path into internal infrastructure. The fetcher resolves the hostname via DNS rather than trusting the literal host, then rejects the resolved IP against the full private/reserved-range table (RFC 1918, CGNAT, link-local, IPv6 unique-local) for both IPv4 and IPv6. Outcome: independently re-verified as fixed in a follow-up security pass.',
            },
            {
              n: 3,
              choice: 'Keep CV matching deterministic instead of an AI-generated score.',
              alternative: 'Let the local model rate the fit directly.',
              rationale:
                'A number a candidate can\'t interrogate is useless, and it\'s the exact "AI slop" complaint competitor research turned up against existing tools. Deterministic keyword coverage shows precisely which terms matched and which are missing, so the advice is honest and actionable — and reproducible.',
            },
          ],
        },
        {
          kind: 'security',
          heading: 'Security & production notes',
          anchorId: 'security',
          blocks: [
            {
              type: 'ul',
              items: [
                'Google and Microsoft sign-in (issuer-routed policy scheme) protect the API; every record is scoped to its owner via EF Core global query filters.',
                'File uploads are validated and stored per-application with ownership checks on every access.',
                'The AI layer is advisory only — it drafts, it never sends or auto-applies.',
                'Runs as a reproducible Docker Compose stack with a documented .env; JSON/CSV exports and automated database backups for data portability.',
              ],
            },
          ],
        },
        {
          kind: 'screenshots',
          heading: 'Screenshots',
          anchorId: 'screenshots',
        },
        {
          kind: 'next',
          heading: 'Status & roadmap',
          anchorId: 'next',
          blocks: [
            {
              type: 'p',
              text: 'Implemented today:',
            },
            {
              type: 'ul',
              items: [
                'Kanban pipeline with rule-based stage automation',
                'Deterministic CV↔job match scoring',
                'Multi-provider correspondence (Gmail, Microsoft Graph, IMAP)',
                'AI-assisted CV, cover-letter and follow-up drafting (local Ollama, optional cloud fallback)',
                'Attachments, reminders, JSON/CSV exports and automated backups',
              ],
            },
            {
              type: 'p',
              text: 'Planned next:',
            },
            {
              type: 'ul',
              items: [
                'An interview hub and lightweight recruiter/contacts CRM',
                'A second analytics pass and a proper PWA offline mode',
                'Wider integration-test coverage around correspondence-import edge cases',
                'Configurable per-stage follow-up rules (currently simple date logic)',
              ],
            },
          ],
        },
      ],
    },
    no: {
      valueProp:
        'Et AI-assistert arbeidsrom for jobbsøking — importer en stilling, tilpass CV-en, og følg hver søknad fra søkt til tilbud.',
      cardTeaser:
        'Importer en stilling, tilpass CV-en, og følg hver søknad — rekruttør-tråder og en Kanban-pipeline i ett arbeidsrom.',
      tldr: {
        what: 'Fullstack arbeidsrom for jobbsøking: pipeline, deterministisk CV-match, korrespondanse fra flere kanaler, AI-utkast.',
        why: 'Et ekte problem — min egen jobbsøking trengte skikkelig verktøy, ikke et regneark.',
        stack:
          'Next.js + ASP.NET Core (EF Core) · FastAPI/Ollama lokal AI · Gmail/Microsoft/IMAP · Docker.',
        role: 'Alt: produkt, backend, frontend, drift og sikkerhet.',
      },
      sections: [
        {
          kind: 'problem',
          heading: 'Problem og kontekst',
          anchorId: 'problem',
          blocks: [
            {
              type: 'p',
              text: 'En seriøs jobbsøking sprer seg over regneark, e-posttråder, notatapper og løse dokumenter. Ingenting viser deg med ett blikk hvilke søknader som trenger oppfølging, eller hva som sist ble sagt — og alle CV-tilpasningsverktøy krever enten abonnement eller gir deg en uigjennomsiktig "match-score" du ikke kan etterprøve. Jeg ville ha ett samlet arbeidsrom — fra import av en stilling til endelig tilbud — bygget til samme standard som jeg leverer på jobb.',
            },
          ],
        },
        {
          kind: 'solution',
          heading: 'Løsning',
          anchorId: 'losning',
          blocks: [
            {
              type: 'p',
              text: 'JobTrack er et egendriftet arbeidsrom som følger én søknad gjennom hele livsløpet: importer stillingen, se hvordan CV-en din står seg mot den, følg den gjennom en pipeline, og hold hver rekruttør-melding koblet til riktig jobb — med et assisterende AI-lag som skriver utkast, men aldri sender noe på dine vegne.',
            },
            {
              type: 'ul',
              items: [
                'Pipeline: et Kanban-brett — Søkt, Venter, Intervju, Tilbud, Avslått, Ghostet — dra for å oppdatere, med regelstyrt auto-overgang til Ghostet ved inaktivitet.',
                'CV-match: deterministisk nøkkelorddekning (treff vs. mangler), ikke en svart boks.',
                'Korrespondanse: én samlet tråd-visning på tvers av Gmail, Microsoft/Outlook og generisk IMAP, automatisk koblet til riktig jobb.',
                'AI-en er assisterende, aldri autonom: den skriver utkast til CV, søknadsbrev og oppfølging — du gjennomgår og sender alltid selv.',
              ],
            },
          ],
        },
        {
          kind: 'features',
          heading: 'Utvalgte funksjoner',
          anchorId: 'funksjoner',
          features: [
            {
              title: 'Deterministisk CV↔jobb-match',
              value:
                'Se nøyaktig hvilke nøkkelord fra stillingsannonsen CV-en din dekker, og hvilke som mangler — et stabilt, reproduserbart tall i stedet for en uigjennomsiktig "AI-score" til 50 dollar i måneden.',
              technical:
                'En egenutviklet, vektet nøkkelordalgoritme (JobCvMatchService): kuraterte kompetanse-tagger med vekt 3 pluss tittelbonus, frekvensrangerte, stoppord-filtrerte annonsetermer med vekt 1, matchet med en håndbygget ordgrense-sjekk så «go» aldri treffer «goal». Ingen AI-kall — samme input gir alltid samme score, med et ærlig «for lite signal»-flagg i stedet for falsk trygghet.',
            },
            {
              title: 'Én tråd, uansett leverandør',
              value:
                'Rekruttør-eposter havner automatisk på riktig jobb, uansett om de kommer via Gmail, Outlook eller en annen IMAP-postkasse — ikke flere tre-innbokser å lete gjennom for å finne hva en rekrutterer sa.',
              technical:
                'Import av korrespondanse går gjennom ett felles IEmailProvider-grensesnitt (SearchAsync / ListThreadMessagesAsync / GetMessageAsync) bak et leverandørregister, med en Provider-kolonne på hver lagrede melding. Gmail kom først; Microsoft Graph og IMAP ble lagt til senere som separate implementasjoner av samme kontrakt — ingen kontrollerlogikk ble duplisert per leverandør.',
            },
            {
              title: 'AI-assistert utkast, menneske-styrt sending',
              value:
                'Få et tilpasset CV-, søknadsbrev- eller oppfølgingsutkast på sekunder — men ingenting sendes eller søkes automatisk uten at du har lest og godkjent det først.',
              technical:
                'CV-er tekstutrekkes med OCR (FastAPI + Tesseract/PyMuPDF), klassifiseres i strukturerte blokker av en lokal Ollama-modell (qwen2.5:7b), og rendres til PDF via en malbasert Playwright-eksport. Prompter pakker inn utrygg CV-tekst i eksplisitte skilletegn med instruks om å behandle den som data, ikke kommandoer — som stenger av prompt-injeksjon fra en CV noen andre har skrevet.',
            },
          ],
        },
        {
          kind: 'architecture',
          heading: 'Tekniske høydepunkter',
          anchorId: 'arkitektur',
          blocks: [
            {
              type: 'p',
              text: 'Backend er en modulær monolitt på ASP.NET Core (.NET 9) med seks bakgrunnstjenester — regelmotor, oppfølgingspåminnelser, planlagte eksporter, jobb-berikelse, CV-prosessering og en AI-klarhetssjekk — som kjører utenfor forespørselsløpet. EF Core bruker SQLite som standard og bytter til MySQL/MariaDB via Pomelo-driveren for større driftssettinger. Hver leietaker-tabell er avgrenset av et globalt EF Core-spørringsfilter (nekt-ved-null by design), så én database trygt kan betjene flere brukere.',
            },
            {
              type: 'ul',
              items: [
                'Autentisering: et tilpasset policy-skjema ruter Google-utstedte, Microsoft-utstedte og lokale JWT-er til riktig håndterer; CSRF-dobbeltinnsending på cookie-økter; ratebegrenset innlogging og e-postutsending.',
                'CI/CD: Gitea Actions bygger og tester backend, bygger frontend, og SSH-er så inn på produksjonsserveren og ruller ut på nytt via Docker Compose ved hver push til main — ingen staging-miljø, et bevisst valg.',
                'i18n: fullt engelsk + norsk bokmål-grensesnitt, en reell differensiator mot de mest USA-sentrerte konkurrentene (Teal, Huntr, Simplify).',
              ],
            },
          ],
        },
        {
          kind: 'decisions',
          heading: 'Utfordringer',
          anchorId: 'utfordringer',
          decisions: [
            {
              n: 1,
              choice:
                'Generalisere Gmail-bare import til en leverandørnøytral IEmailProvider-abstraksjon, trinnvis.',
              alternative:
                'Skrive om korrespondanselaget én gang, på forhånd, for hver leverandør vi kanskje trenger.',
              rationale:
                'Import startet Gmail-bare; da Microsoft- og IMAP-støtte ble etterspurt senere, trakk jeg ut grensesnittet over flere små PR-er i stedet for en stor omskriving. Resultat: tre fungerende leverandører bak én kontrakt, og ingen kontroller måtte dupliseres per leverandør.',
            },
            {
              n: 2,
              choice: 'SSRF-sikre jobb-import-URL-henteren før den ble sluppet.',
              alternative:
                'Stole på den limte inn URL-en og hente den direkte — det er jo bare en stillingsannonse.',
              rationale:
                'Å la brukere lime inn en vilkårlig URL for server-side skraping er en klassisk vei inn i intern infrastruktur. Henteren slår opp vertsnavnet via DNS i stedet for å stole på den bokstavelige verten, og avviser så den oppløste IP-en mot hele det private/reserverte adresseområdet (RFC 1918, CGNAT, link-lokal, IPv6 unique-local) for både IPv4 og IPv6. Resultat: uavhengig verifisert som fikset i en påfølgende sikkerhetsgjennomgang.',
            },
            {
              n: 3,
              choice: 'Holde CV-matchen deterministisk i stedet for en AI-generert score.',
              alternative: 'La den lokale modellen vurdere treffet direkte.',
              rationale:
                'Et tall en kandidat ikke kan etterprøve er ubrukelig, og det er nøyaktig «AI-slop»-klagen konkurrentanalysen fant mot eksisterende verktøy. Deterministisk nøkkelorddekning viser nøyaktig hvilke ord som traff og hvilke som mangler, så rådet er ærlig, handlingsrettet — og reproduserbart.',
            },
          ],
        },
        {
          kind: 'security',
          heading: 'Sikkerhet og drift',
          anchorId: 'sikkerhet',
          blocks: [
            {
              type: 'ul',
              items: [
                'Google- og Microsoft-innlogging (utsteder-rutet policy-skjema) beskytter API-et; hver post er knyttet til sin eier via globale EF Core-spørringsfilter.',
                'Filopplastinger valideres og lagres per søknad, med eierskapssjekk ved hvert tilgangspunkt.',
                'AI-laget er kun rådgivende — det skriver utkast, det sender aldri og søker aldri automatisk.',
                'Kjører som en reproduserbar Docker Compose-stack med dokumentert .env; JSON/CSV-eksport og automatiske databasebackupper for dataportabilitet.',
              ],
            },
          ],
        },
        {
          kind: 'screenshots',
          heading: 'Skjermbilder',
          anchorId: 'skjermbilder',
        },
        {
          kind: 'next',
          heading: 'Status og veien videre',
          anchorId: 'videre',
          blocks: [
            {
              type: 'p',
              text: 'Implementert i dag:',
            },
            {
              type: 'ul',
              items: [
                'Kanban-pipeline med regelstyrt stadie-automasjon',
                'Deterministisk CV↔jobb-match',
                'Korrespondanse fra flere kanaler (Gmail, Microsoft Graph, IMAP)',
                'AI-assistert utkast til CV, søknadsbrev og oppfølging (lokal Ollama, valgfri sky-fallback)',
                'Vedlegg, påminnelser, JSON/CSV-eksport og automatiske backupper',
              ],
            },
            {
              type: 'p',
              text: 'Planlagt videre:',
            },
            {
              type: 'ul',
              items: [
                'Et intervjuknutepunkt og en enkel rekrutterer/kontakt-CRM',
                'En ny analyserunde og et skikkelig offline-modus for PWA-en',
                'Bredere integrasjonstestdekning rundt kanttilfeller i korrespondanse-import',
                'Konfigurerbare oppfølgingsregler per stadium (i dag enkel datologikk)',
              ],
            },
          ],
        },
      ],
    },
  },
};
