JSON Feed Validator

Check whether your feed is valid. For more information about JSON Feed, see the specification. Find the validator source code on GitHub.

GET validation response in JSON format.

Feed source

{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Engineer — Portefølje og notater",
  "home_page_url": "https://engineer.company/nb/",
  "feed_url": "https://engineer.company/nb/feed.json",
  "description": "Engineer ApS — programvareutvikling & IT-rådgivning i København, Danmark. En portefølje av dokumenterte ingeniørresultater innen programvare, data, sky og IT.",
  "language": "nb",
  "icon": "https://engineer.company/assets/images/brand/card.webp",
  "favicon": "https://engineer.company/assets/icons/apple/apple-touch-icon.png",
  "authors": [
    {
      "name": "Engineer ApS",
      "url": "https://engineer.company/nb/"
    }
  ],
  "items": [
    {
      "id": "https://engineer.company/nb/portfolio/built-the-visual-identity-from-two-drawings-164/",
      "url": "https://engineer.company/nb/portfolio/built-the-visual-identity-from-two-drawings-164/",
      "title": "Bygde selskapets ikon- og favicon‑sett fra to håndlagde tegninger, med alle avledede filer regenerert av skript og en kontroll som feiler når en avledet fil ble committet før kilden.",
      "summary": "Én tegning gir nå hvert publisert ikon, faneikonet medregnet, å generere på nytt er én kommando, og en utdatert ressurs får byggingen til å feile.",
      "content_html": "<p><strong>Situasjon.</strong> Den visuelle identiteten til et lite selskap er som regel kjøpt, generert eller satt sammen fra bildebanker, og resultatet er en identitet som ikke tilhører noen. Det alternative problemet er verre: håndlaget grafikk som bare finnes som eksporterte filer, der kildetegningen er tapt, eksportene redigeres direkte, og der versjonene som er i bruk, innen et år er uenige med hverandre og det ikke finnes noen original igjen som kan avgjøre saken.</p>\n<p><strong>Oppgave.</strong> Identiteten måtte tegnes i stedet for å hentes inn, og kjeden fra tegning til publisert ressurs måtte være reproduserbar, slik at hver fil på nettstedet kan avledes og ikke bare oppbevares.</p>\n<p><strong>Handling.</strong> Den genererte grafikken begynte med to håndlagde tegninger: selskapsmerket, en rastertegning, og et tannhjul i vektor, tegnet for nettleserfanen ut fra tanken om at en kraftig silhuett holder seg ved 16 piksler. Vist side om side ved 16, 32 og 48 piksler på lyse og mørke fanelinjer forble merket gjenkjennelig, mens tannhjulet ble utydelig, så faneikonet gikk over til merket, og hvert ikon nettstedet publiserer, lages nå fra den ene tegningen. Fra den lager skript hver avledet form: favicon‑settet i de nødvendige størrelsene, touch‑ikonene og oppstartsbildene i hver enhetsstørrelse. Genereringen er en oppgave hvem som helst kan kjøre, så spørsmålet «hvor kom denne filen fra» har et svar som er en kommando. Sjekken for utdaterte ressurser er den delen som får det til å holde: den sammenligner commitdatoen til hver avledet ressurs med kilden og feiler når en avledet fil ble committet først, og det fanger nøyaktig den feilen denne utformingen finnes for å hindre — noen redigerer kilden, glemmer å generere på nytt, og det publiserte nettstedet fortsetter å vise grafikk som ikke lenger stemmer med originalen. Regelen om at avledede filer aldri redigeres for hånd, står der kildene ligger.</p>\n<p><strong>Resultat.</strong> Én tegning gir nå hvert publisert ikon, faneikonet medregnet, å generere på nytt er én kommando, og en utdatert ressurs får byggingen til å feile. Avveiningen er en hard avhengighet av verktøykjeden: de avledede filene er committet så nettstedet kan bygges hvor som helst, men å endre identiteten krever at genereringsverktøyene fortsatt fungerer, og et kildeformat som slutter å være lesbart, tar hele identiteten med seg.</p>\n",
      "date_published": "2026-10-18T10:08:26+02:00",
      "date_modified": "2026-10-18T10:08:26+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Merkevare og markedsføring",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Webutvikling",
        "Merkevare, markedsføring og SEO",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/",
      "url": "https://engineer.company/nb/portfolio/increased-brand-popularity-by-200x-through-successful-brand-1/",
      "title": "Økte merkevarens popularitet 200 ganger gjennom vellykket merkevareutvikling på plattformer både offline og online.",
      "summary": "Økte merkevarens rekkevidde rundt 200 ganger på tvers av fysiske og digitale plattformer, og gjorde en ukjent nykommer til et kjent navn.",
      "content_html": "<p><strong>Situasjon.</strong> Engineer ApS var et helt nytt konsulentselskap med reell teknisk dybde og nesten ingen som hadde hørt om det. Det er en helt spesiell form for frustrasjon: kompetansen er der, arbeidet ville blitt godt, men ingenting av det betyr noe hvis kundene og partnerne som ville ha ønsket det, ikke vet at du finnes. Et ungt selskap må bli sett før det kan vinne noe som helst.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å bygge en merkevare folk faktisk ville kjenne igjen — både offline og online — og å gjøre selskapets ingeniørfaglige troverdighet om til synlig tilstedeværelse i markedet, i stedet for å la den forbli en godt bevart hemmelighet.</p>\n<p><strong>Handling.</strong> Merkevaren ble bygget bevisst og holdt konsekvent. Det startet med en tydelig identitet — en stemme, et visuelt språk og en portefølje som løftet fram konkrete ingeniørresultater i stedet for det vage «vi leverer verdi»-språket alle andre bruker. Deretter ble den brukt i kanalene som betyr noe for et slikt selskap — nettstedet, LinkedIn, GitHub, fysiske arrangementer — der hvert kontaktpunkt sa det samme i stedet for at hvert enkelt drev av gårde på egen hånd. Den røde tråden var å lede med reelle kundecaser og reelle resultater, slik at troverdigheten var noe man kunne se bevis for, ikke bare en påstand.</p>\n<p><strong>Resultat.</strong> Merkevarens rekkevidde økte omtrent 200 ganger på tvers av offline- og onlineplattformene — en ukjent nykommer ble til noe folk faktisk kjente igjen. Og det var ikke rekkevidde for forfengelighetens skyld; synligheten begynte å gi en jevn strøm av innkommende samtaler og muligheter som rett og slett ikke hadde vært der før.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Interessenter og rapportering",
        "Merkevare og markedsføring",
        "Produkt og krav",
        "Merkevare, markedsføring og SEO",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/",
      "url": "https://engineer.company/nb/portfolio/drove-brand-engagement-and-loyalty-by-100-through-2/",
      "title": "Økte engasjementet og lojaliteten rundt merkevaren med 100 % gjennom analyse av markedstrender og innsikt i forbrukeratferd.",
      "summary": "Doblet engasjementet og lojaliteten rundt merkevaren (+100 %) med analyse av markedstrender og forbrukeratferd, og vant et publikum som kommer tilbake.",
      "content_html": "<p><strong>Situasjon.</strong> Etter hvert som synligheten økte, nådde Engineer ApS flere mennesker — men engasjementet var tynt. Folk la merke til selskapet og gikk videre; den tidlige interessen ble ikke til relasjoner som varte. Rekkevidde uten engasjement er bare støy, og merkevaren skapte mer støy enn forbindelser.</p>\n<p><strong>Oppgave.</strong> Målet var å styrke engasjementet og lojaliteten, og å gjøre det ved faktisk å se på hva markedet og publikum responderte på, i stedet for å stole på magefølelsen.</p>\n<p><strong>Handling.</strong> Derfor ble merkevaren datainformert i stedet for intuisjonsstyrt. Det betydde å se på markedstrendene og på hvordan publikum faktisk oppførte seg på tvers av kanalene — ikke hva noen antok at de ville like, men hva de beviselig engasjerte seg i. Det avdekket temaene og formatene som fikk reell oppmerksomhet, og innholdet og det utadrettede arbeidet ble styrt mot dem. Det viktige var å stramme inn sløyfen: se hva som traff, publisere mer av samme type neste gang, og la hver runde treffe litt bedre enn den forrige.</p>\n<p><strong>Resultat.</strong> Engasjementet og lojaliteten ble doblet — en forbedring på 100 % — med et publikum som kom tilbake og engasjerte seg i stedet for å kaste et blikk og forsvinne, og merkbart sterkere relasjoner til potensielle kunder og partnere. Merkevaren sluttet å kringkaste ut i tomrommet og begynte å bygge noe som ga noe tilbake.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dataanalyse",
        "Interessenter og rapportering",
        "Merkevare og markedsføring",
        "Produkt og krav",
        "Dataanalyse og BI‑dashbord",
        "Merkevare, markedsføring og SEO",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-a-comprehensive-infrastructure-framework-for-a-research-institute-3/",
      "url": "https://engineer.company/nb/portfolio/designed-a-comprehensive-infrastructure-framework-for-a-research-institute-3/",
      "title": "Designet et helhetlig infrastrukturrammeverk for et forskningsinstitutt som berørte 14 avdelinger, med fleksible moduler, samlede datapipelines og strukturerte støttestrategier for varig innføring.",
      "summary": "Designet et helhetlig infrastrukturrammeverk for et forskningsinstitutt med 14 avdelinger — modulært, med enhetlige datapipelines og støttestrategier.",
      "content_html": "<p><strong>Situasjon.</strong> Ved et forskningsinstitutt med 14 ulike forskningsgrupper arbeidet hver gruppe med forskjellige datakilder, formater, skalaer og programvareverktøy. Den tekniske kompetansen og de tilgjengelige IT‑ressursene varierte mye mellom gruppene. Mens noen få hadde klart å lage og produksjonssette egne IT‑løsninger, slet mange med kompleksiteten i behovene sine for datainfrastruktur, noe som tok verdifull tid og oppmerksomhet bort fra kjerneforskningen.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å utarbeide en løsning som ville la forskerne konsentrere seg om det vitenskapelige arbeidet i stedet for IT‑utfordringer. Målet var å designe og implementere en skalerbar datainfrastruktur for hele instituttet som kunne dekke de brede og ulike kravene til de fleste forskningsgruppene.</p>\n<p><strong>Handling.</strong> Det ble utviklet en robust og framtidsrettet infrastrukturplan som balanserte fleksibilitet og standardisering. Planen beskrev sentrale komponenter som modulær arkitektur, integrasjonsveier for ulike datakilder, brukervennlige grensesnitt tilpasset ulike tekniske ferdighetsnivåer, og skalerbare løsninger for lagring og prosessering. Den inneholdt også strategier for onboarding, støtte og styring for å sikre at løsningen ble tatt i bruk og var bærekraftig over tid.</p>\n<p><strong>Resultat.</strong> Den resulterende infrastrukturplanen var både teknisk solid og strategisk i tråd med instituttets forskningsmål. Den samlet visjonen for datahåndtering på tvers av organisasjonen, ga en tydelig vei mot mindre IT‑byrde for forskerne, og la grunnlaget for et felles, effektivt og framtidsklart miljø for forskningsdata. Planen ble godt mottatt for sin inkluderende tilnærming, tydelighet og tilpasningsevne, og satte en klar retning for omleggingen av instituttets datainfrastruktur.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Datapipelines (ETL/ELT)",
        "Datastyring",
        "Dokumentasjon",
        "Infrastruktur",
        "Interessenter og rapportering",
        "Løsningsarkitektur",
        "Plattformarkitektur",
        "Teknisk ledelse",
        "Datastyring og datakvalitet",
        "Plattform- og løsningsarkitektur",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/engineered-hourly-electricity-consumption-aggregation-pipeline-in-python-4/",
      "url": "https://engineer.company/nb/portfolio/engineered-hourly-electricity-consumption-aggregation-pipeline-in-python-4/",
      "title": "Utviklet en pipeline for timesaggregering av strømforbruk i Python / SQL / Bash + Jq som oppnådde 180 ms for datasett på 30 dager fra heterogene JSONL‑kilder.",
      "summary": "Bygde en datapipeline for strømforbruk i Python/SQL/Bash som aggregerer 30-dagers datasett på 180 ms fra heterogene JSONL-kilder.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet behandler store mengder heterogene strømdata fra flere datakilder, hver med sin egen rapporteringsfrekvens — fra timesintervaller til 15‑minuttersintervaller, og i noen tilfeller uregelmessige tidsstempler. Denne variasjonen gjør det utfordrende å lage et sammenhengende, sammenlignbart datasett. For å støtte presise energianalyser må disse dataene normaliseres til konsistente forbruksverdier per time, aggregert per sone.</p>\n<p><strong>Oppgave.</strong> Målet var å ta rå hendelsesdata fra et historisk datasett (i JSONL‑format) og konvertere dem til strømforbrukstall justert til hele timer, strukturert som én rad per time og per sone. Konkret innebar oppgaven:</p>\n<ul>\n<li>Justere tidsstemplede data til strenge timesintervaller,</li>\n<li>Aggregere total strømproduksjon innenfor hvert intervall ved å summere mix‑verdiene,</li>\n<li>Ta med utveksling over landegrensene ved å legge til import og trekke fra eksport,</li>\n<li>Levere de endelige verdiene i et strukturert, skalerbart format egnet for videre analyse.</li>\n<li>Løsningen måtte også være effektiv nok til å skalere til store tidsvinduer (30+ dager) og på tvers av flere land/soner.</li>\n</ul>\n<p><strong>Handling.</strong> For å løse oppgaven ble tre ulike varianter av løsningen implementert med Python, JQ (for JSON‑behandling på kommandolinjen) og SQL, hver optimalisert for ulike bruksområder:</p>\n<p>Python ble valgt for sin fleksibilitet, enkle datamanipulering og evne til å håndtere transformasjoner i minnet effektivt. Med rene Python‑funksjoner ble det bygget en pipeline som:</p>\n<ul>\n<li>Parset JSONL‑filene til strukturerte datarammer</li>\n<li>Samplet tidsseriedataene om til timesintervaller</li>\n<li>Aggregerte produksjonen og beregnet netto strømforbruk (produksjon + import − eksport)</li>\n<li>Eksporterte resultatene som CSV eller lastet dem inn i en lettvekts SQLite‑database for inspeksjon.</li>\n</ul>\n<p>JQ ble brukt til å lage en rask løsning med minimale avhengigheter for kommandolinjemiljøer, bygget som en kjede av JQ‑filtre.</p>\n<p>I PostgreSQL ble JSONL‑dataene importert, normaliserte tabeller opprettet og en serie SQL‑spørringer skrevet.</p>\n<p>For å evaluere ytelsen ble hver løsning benchmarket med historiske datasett på både 1 dag og 30 dager.</p>\n<p><strong>Resultat.</strong> Python‑implementasjonen viste seg å være den raskeste og mest skalerbare løsningen, og fullførte:</p>\n<ul>\n<li>Behandling av data for 1 dag på bare 34 ms</li>\n<li>Datasettet for 30 dager på 180 ms</li>\n</ul>\n<p>Dette bekreftet at den egnet seg til å håndtere lengre tidsrom og samtidig holde ytelsen under ett sekund. Den ga også tydelig, vedlikeholdbar kode som lett kunne utvides til behandling av flere soner eller integreres i en ETL‑pipeline.</p>\n<p>Samlet sett viste tilnærmingen med flere verktøy fleksibilitet i verktøybruken, sterk ytelsesoptimalisering og robust håndtering av utfordringer med tidsjustering og aggregering i reelle datapipelines for strømdata.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Dataanalyse",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "PostgreSQL",
        "Python",
        "SQL",
        "Ytelsesoptimalisering",
        "Dataanalyse og BI‑dashbord",
        "Optimalisering av databaseytelse",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/accelerated-geographical-data-pipeline-performance-by-50x-by-5/",
      "url": "https://engineer.company/nb/portfolio/accelerated-geographical-data-pipeline-performance-by-50x-by-5/",
      "title": "Økte ytelsen i geografiske datapipelines 50 ganger ved å forbedre SQL‑programmeringen og datamodelleringen i PostgreSQL, MS SQL og Google Cloud BigQuery.",
      "summary": "Gjorde en datapipeline for geodata 50 ganger raskere ved å finjustere SQL og datamodell i PostgreSQL, MS SQL og Google BigQuery.",
      "content_html": "<p><strong>Situasjon.</strong> Organisasjonen opplevde betydelige forsinkelser i behandlingen av store mengder geografiske data, noe som gikk ut over effektiviteten i datadrevne beslutningsprosesser og analytisk rapportering.</p>\n<p><strong>Oppgave.</strong> Målet var å optimalisere den geografiske datapipelinen for å forbedre ytelsen og redusere behandlingstiden, slik at dataanalyser kunne utføres mer effektivt og i sanntid.</p>\n<p><strong>Handling.</strong> De eksisterende strukturene for SQL‑programmering og datamodellering i PostgreSQL, MS SQL og Google Cloud BigQuery ble grundig analysert, noe som avdekket flaskehalser knyttet til ineffektive indekseringsstrategier, lite optimale spørringsdesign og mangel på hensiktsmessige integritetsregler. For å løse disse problemene:</p>\n<ul>\n<li>Datamodellene ble redesignet for å normalisere kritiske datasett, og partisjoneringsstrategier ble innført for å korte ned hentetidene for data.</li>\n<li>Tok i bruk avanserte indekseringsteknikker, blant annet B‑tree- og GiST‑indekser for PostgreSQL, filtrerte indekser i MS SQL og clustering i BigQuery.</li>\n<li>Optimaliserte komplekse SQL‑spørringer ved å refaktorere delspørringer, redusere antall join‑operasjoner og innføre materialiserte visninger der det var relevant.</li>\n<li>Etablerte integritetsregler for data, som fremmednøkler og CHECK‑regler, for å sikre datakonsistens uten å gå på bekostning av ytelsen.</li>\n</ul>\n<p><strong>Resultat.</strong> Disse omfattende optimaliseringene gjorde den geografiske datapipelinen 50 ganger raskere og reduserte behandlingstiden for data betydelig. Forbedringen muliggjorde sanntidsanalyse, styrket rapporteringsmulighetene og ga interessentene rettidig, datadrevet innsikt, noe som til syvende og sist bidro til bedre informerte strategiske beslutninger.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "Datastyring",
        "Datavarehus",
        "GIS / geodata",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Design av datavarehus",
        "GIS- og geodataløsninger",
        "Optimalisering av databaseytelse",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/improved-geographical-map-application-performance-by-10x-through-6/",
      "url": "https://engineer.company/nb/portfolio/improved-geographical-map-application-performance-by-10x-through-6/",
      "title": "Forbedret ytelsen i en geografisk kartapplikasjon 10 ganger gjennom et strategisk databaseskifte fra MSSQL til PostgreSQL, inkludert omleggingen av dataene, og optimaliserte samtidig behandlingen og datasikkerheten.",
      "summary": "Forbedret en GIS-kartapplikasjon 10 ganger ved å migrere fra MSSQL til PostgreSQL — romlige spørringer falt fra 2,5 s til under 250 ms.",
      "content_html": "<p><strong>Situasjon.</strong> Organisasjonens geografiske kartapplikasjon, som støttet romlige spørringer i sanntid for mange brukere, slet med alvorlige ytelsesflaskehalser. Latens i gjengivelsen av kartlag og i spørringer mot stedsbaserte data gikk ut over både brukeropplevelsen og påliteligheten til backend‑tjenesten. Systemet bygde på en eldre Microsoft SQL Server‑database (MSSQL) som manglet innebygd geospatial indeksering.</p>\n<p><strong>Oppgave.</strong> Forbedre ytelsen, skalerbarheten og sikkerheten i kartapplikasjonen, med et konkret mål om å redusere spørringslatensen og øke gjennomstrømningen for romlige operasjoner.</p>\n<p><strong>Handling.</strong> Ledet en strategisk migrering fra MSSQL til PostgreSQL med PostGIS‑utvidelsen for å få innebygd geospatial støtte.</p>\n<ul>\n<li>Designet et nytt skjema optimalisert for romlige data, med GiST- og SP‑GiST‑indekser på geometri- og geografikolonner for raskere spørringer.</li>\n<li>Definerte strenge fremmednøkler og CHECK‑regler for å sikre relasjonell integritet og håndheve valideringsregler for romlige koordinater.</li>\n<li>Migrerte over 50 millioner romlige poster via ETL‑pipelines med transformasjonssteg for å oppfylle nye SRID‑standarder (EPSG:4326).</li>\n<li>Justerte konfigurasjonsparametere i PostgreSQL (f.eks. work_mem, effective_cache_size) for optimal I/O‑ytelse ved samtidig tilgang.</li>\n<li>Implementerte rollebasert tilgangskontroll (RBAC) og sikkerhet på radnivå for å håndheve retningslinjer for databeskyttelse på tvers av flere brukergrupper.</li>\n</ul>\n<p><strong>Resultat.</strong> Oppnådde 10 ganger bedre ytelse for romlige spørringer, med en reduksjon i gjennomsnittlig svartid fra 2,5 sekunder til under 250 millisekunder. CPU‑belastningen i backend falt med 65 %, og systemets tilgjengelighet ble bedre i perioder med høy belastning. Sikkerheten ble også styrket med detaljerte tilgangsregler og valideringsregler for data, noe som reduserte risikoen for korrupte romlige data.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Data engineering",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "GIS / geodata",
        "Migrering og modernisering",
        "PostgreSQL",
        "Sikkerhet",
        "SQL",
        "Ytelsesoptimalisering",
        "Databasemigrering og modernisering",
        "GIS- og geodataløsninger",
        "Optimalisering av databaseytelse"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/resolved-1-000-issues-in-geographical-data-and-7/",
      "url": "https://engineer.company/nb/portfolio/resolved-1-000-issues-in-geographical-data-and-7/",
      "title": "Løste 1 000 problemer i geografiske data og tidsseriedata med GDAL, ArcGIS, PostGIS, Mapbox, QGIS, SQL (PL/pgSQL, Transact‑SQL) og Bash, og sikret behandling av stordata med høy kvalitet.",
      "summary": "Løste over 1 000 problemer i geografiske stordata og tidsseriedata med GDAL, PostGIS, ArcGIS, Mapbox, QGIS og SQL — 35 % raskere spørringer.",
      "content_html": "<p><strong>Situasjon.</strong> Under arbeidet med et storskala prosjekt for geospatial analyse støtte teamet på en rekke inkonsistenser og avvik i de geografiske datasettene og tidsseriedatasettene. Disse problemene gikk ut over nøyaktigheten i romlige analyser og beslutningsverktøy som ble brukt i flere avdelinger.</p>\n<p><strong>Oppgave.</strong> Ansvaret var å identifisere, løse og optimalisere over 1 000 datakvalitetsproblemer i disse komplekse datasettene for å sikre integriteten og ytelsen til nedstrøms applikasjoner og visualiseringer.</p>\n<p><strong>Handling.</strong> Romlige feil ble systematisk diagnostisert og rettet med en kombinasjon av verktøy, blant annet GDAL, QGIS og ArcGIS, og automatiserte arbeidsflyter i Bash‑skript forenklet gjentakende oppgaver med datavask. PostGIS håndterte avanserte romlige spørringer og romlig indeksering, og robuste prosedyrer ble skrevet i PL/pgSQL og Transact‑SQL for å håndtere og transformere både geografiske og tidsmessige data i PostgreSQL- og SQL Server‑databasene. I tillegg ble de rensede dataene integrert i interaktive visualiseringer med Mapbox, noe som gjorde dataene mer tilgjengelige for sluttbrukerne.</p>\n<p><strong>Resultat.</strong> Gjennom denne innsatsen ble over 1 000 kritiske problemer løst, noe som forbedret datanøyaktigheten og behandlingshastigheten betydelig. Dette bidro direkte til 35 % kortere kjøretid for romlige spørringer og ga teamet mer pålitelige romlige analyser. Arbeidet sørget for at bruksklare data av høy kvalitet alltid var tilgjengelige for analyse og rapportering, noe som støttet strategiske beslutninger i hele organisasjonen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "Datastyring",
        "GIS / geodata",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Datastyring og datakvalitet",
        "GIS- og geodataløsninger",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/",
      "url": "https://engineer.company/nb/portfolio/designed-implemented-and-administered-6-etl-elt-pipelines-8/",
      "title": "Designet, implementerte og administrerte 6 ETL/ELT‑pipelines med Google BigQuery, MSSQL, PostgreSQL, Shell‑skripting, PL/pgSQL og Transact‑SQL, og integrerte data for effektiv behandling i et Python‑API.",
      "summary": "Designet og drev 6 ETL/ELT-pipelines (BigQuery, MSSQL, PostgreSQL) og kortet en manuell jobb på 3 timer ned til under 20 minutter med daglig synk.",
      "content_html": "<p><strong>Situasjon.</strong> Organisasjonen trengte å samle og behandle store mengder strukturerte data fra et eksternt datavarehus som lå bak et IPsec‑VPN. Disse dataene var avgjørende for interne analysedashbord og eksterne Python‑API‑er som ble brukt av kunder og partnere.</p>\n<p><strong>Oppgave.</strong> Målet var å designe, implementere og vedlikeholde et sett med robuste og automatiserte ETL/ELT‑pipelines for å hente, transformere og laste data sikkert inn i Google BigQuery, med nøyaktige data, god ytelse og skalerbarhet på tvers av systemene.</p>\n<p><strong>Handling.</strong> 6 ende‑til‑ende ETL/ELT‑pipelines ble designet, implementert og administrert, på tvers av flere teknologier, blant annet Google BigQuery, MSSQL, PostgreSQL, Shell‑skripting, PL/pgSQL og Transact‑SQL.</p>\n<ul>\n<li>Etablerte sikre tilkoblinger til en ekstern server beskyttet av et IPsec‑VPN for å automatisere datainnhentingen.</li>\n<li>Planla og kjørte nedlastinger av komprimerte dataarkiver med Parquet-, CSV- og .bak‑filer.</li>\n<li>Utviklet Bash- og Python‑skript for å pakke ut og klassifisere filer etter type og skjema.</li>\n<li>For .bak‑filer ble sikkerhetskopiene gjenopprettet i en lokal MSSQL Server‑instans med RESTORE DATABASE‑arbeidsflyter, og skjemaintegriteten ble validert.</li>\n<li>Lastet strukturerte data inn i staging‑skjemaer i MSSQL og PostgreSQL med verktøyene bcp, psql og SSIS, avhengig av kildeformatet.</li>\n<li>Skrev modulære og gjenbrukbare PL/pgSQL- og T‑SQL‑prosedyrer for å rense, normalisere og berike dataene ut fra forretningslogikken.</li>\n<li>Eksporterte kuraterte datasett og tabeller til mellomformater, komprimerte dem med gzip og overførte dem sikkert til en Google Cloud Storage‑bucket.</li>\n<li>Automatiserte opplastingen og skjemamappingen til Google BigQuery med bq CLI og Python‑baserte skript for datainnlasting.</li>\n</ul>\n<p><strong>Resultat.</strong> Disse automatiserte pipelinene reduserte det manuelle merarbeidet og behandlingstiden betydelig — fra 3+ timer manuelt arbeid til under 20 minutter fra ende til ende, samtidig som dataenes ferskhet ble forbedret fra ukentlige til daglige synkroniseringer. Python‑API‑ene som brukte disse dataene, fikk 30 % bedre ytelse, og den bedre oversikten hjalp forretningsanalytikerne med å levere raskere innsikt til interessentene. Løsningen er fortsatt skalerbar og utvidbar for onboarding av nye datakilder etter hvert som virksomheten vokser.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "Datavarehus",
        "Nettverk og VPN",
        "PostgreSQL",
        "Python",
        "Skytjenester",
        "SQL",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Design av datavarehus",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/developed-and-launched-the-company-s-first-observability-9/",
      "url": "https://engineer.company/nb/portfolio/developed-and-launched-the-company-s-first-observability-9/",
      "title": "Utviklet og lanserte selskapets første dashbord for observerbarhet, med sanntidsinnsikt i systemytelsen og datavisualisering på den store TV‑skjermen på kontoret.",
      "summary": "Bygde selskapets første dashbord for observerbarhet med sanntidsinnsikt i systemene, slik at teamene oppdaget og løste hendelser 40 % raskere.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet hadde utfordringer med å overvåke systemytelsen i sanntid, noe som ofte førte til forsinket håndtering av hendelser og dårligere innsyn i infrastrukturens helse. Det fantes ingen sentral løsning der teamene kunne få innsikt i driftsmetrikker.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å utvikle en løsning som ville gjøre det mulig for både tekniske og ikke‑tekniske interessenter å overvåke sentrale systemmetrikker i sanntid, med vekt på tilgjengelighet, tydelighet og proaktiv oppdagelse av problemer.</p>\n<p><strong>Handling.</strong> Selskapets første dashbord for observerbarhet ble designet og implementert, og det samlet inn alle viktige systemmetrikker — som CPU, RAM, HDD, temperatur og mer — fra eksterne Linux‑servere via SSH. Til og med Docker‑containere ble overvåket på denne måten. Senere ble en andre versjon designet og implementert med Grafana og Prometheus for mer avansert visualisering og overvåking. I samarbeid med DevOps- og utviklingsteamene ble de kritiske metrikkene identifisert, som CPU‑utnyttelse, minnebruk, oppetid for tjenester og API‑latens. Datapipelines ble konfigurert for å hente inn og behandle ytelsesmetrikker fra ulike systemer, og dashbordet ble vist på en stor TV‑skjerm på kontoret for maksimal synlighet. Varslingsmekanismer for overskridelse av terskelverdier ble også integrert, slik at det kunne handles umiddelbart.</p>\n<p><strong>Resultat.</strong> Dashbordet ga betydelig bedre innsyn i systemene og kortere responstid på driftsproblemer. Teamene kunne oppdage og løse hendelser 40 % raskere. Det fremmet også en kultur med felles eierskap til systemenes helse ved å gjøre ytelsesdata tilgjengelige for alle på kontoret, og bidro til syvende og sist til et mer stabilt og effektivt produksjonsmiljø.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Containere (Docker/Kubernetes)",
        "Dataanalyse",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Dataanalyse og BI‑dashbord",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/delivered-8-power-bi-projects-with-comprehensive-manuals-10/",
      "url": "https://engineer.company/nb/portfolio/delivered-8-power-bi-projects-with-comprehensive-manuals-10/",
      "title": "Leverte 8 Power BI‑prosjekter med grundige brukerveiledninger, og integrerte Microsoft Power BI‑verktøy med et NodeJS‑API og Python FastAPI for effektiv dataanalyse og visualisering.",
      "summary": "Leverte 8 analyseprosjekter i Power BI (Node.js-API, Python FastAPI) med fullstendige brukerveiledninger og gjorde rapportgenereringen 60 % mer effektiv.",
      "content_html": "<p><strong>Situasjon.</strong> Organisasjonen trengte dynamisk, visuell innsikt i komplekse datasett om strømnettets ytelse og geografiske fordelingsmetrikker for å støtte beslutninger i både tekniske og strategiske team.</p>\n<p><strong>Oppgave.</strong> Utvikle interaktive dashbord og rapporteringsløsninger som på en effektiv måte kunne presentere både sanntidsdata og historiske geografiske og elektriske data, slik at interessentene raskt kunne identifisere trender, avvik og ytelsesindikatorer.</p>\n<p><strong>Handling.</strong> Integrerte Microsoft Power BI med en skreddersydd backend‑stack basert på et NodeJS‑API og Python FastAPI for å effektivisere datainnlasting, transformasjon og visualisering. Designet og implementerte dashbord med kartvisualiseringer, metrikker for energiforbruk, sporing av driftsavbrudd og indikatorer for nettets effektivitet. Utviklet gjenbrukbare maler og detaljert dokumentasjon for å støtte skalerbarhet og brukervennlighet.</p>\n<p><strong>Resultat.</strong> Leverte 8 dataanalyseprosjekter med suksess, gjorde rapportgenereringen 60 % mer effektiv og gjorde det mulig for tverrfaglige team å ta raskere, datadrevne beslutninger. Interessentene rapporterte en betydelig bedre forståelse av den regionale elektriske ytelsen og mer presis ressursplanlegging.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Data engineering",
        "Dataanalyse",
        "Dokumentasjon",
        "GIS / geodata",
        "Produkt og krav",
        "Python",
        "Backend- og API‑utvikling",
        "Dataanalyse og BI‑dashbord",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/released-500-electricity-and-gis-data-analysis-reports-11/",
      "url": "https://engineer.company/nb/portfolio/released-500-electricity-and-gis-data-analysis-reports-11/",
      "title": "Publiserte 500 analyserapporter om strøm- og GIS‑data, basert på grundige undersøkelser og feilsøking, for å sikre presis innsikt i geografiske stordata og tidsseriedata.",
      "summary": "Publiserte 500 analyserapporter om strøm- og GIS-data — et oppslagsverk på tvers av avdelingene for lastbalansering, effektivitet og avviksdeteksjon.",
      "content_html": "<p><strong>Situasjon.</strong> Teamet forvaltet enorme datasett generert av smartmålere installert i flere geografiske regioner. Disse smartmålerne produserte detaljerte tidsseriedata om strømforbruk som ble brukt av energianalytikere, ingeniører og regionale planleggere til operative og strategiske beslutninger.</p>\n<p><strong>Oppgave.</strong> Ansvaret var å lage transparente og reproduserbare analytiske rapporter av høy kvalitet som kunne avdekke mønstre i energiforbruket, oppdage avvik og identifisere regionale forbrukstrender, samtidig som ikke‑tekniske interessenter enkelt kunne tolke og gjenbruke funnene.</p>\n<p><strong>Handling.</strong> 500+ grundige dataanalyserapporter ble laget og levert, med ren SQL til all datauttrekking, transformasjon og analyse, direkte i skybaserte miljøer som PostgreSQL og BigQuery. Dataene omfattet geolokasjonskoordinater, måler‑ID‑er, tidsstemplet energibruk og miljømetadata. SQL‑skriptene brukte CTE‑er, vindusfunksjoner, delspørringer og geospatiale joins, noe som ga skalerbar og effektiv behandling.</p>\n<p>Hver rapport inneholdt kommentert SQL‑kode, slik at kolleger og samarbeidspartnere fullt ut kunne reprodusere og etterprøve undersøkelsene, noe som reduserte tiden som trengtes til oppfølgingsanalyser betydelig. Det ble også lagt til notater om feilsøking, og vanlige datakvalitetsproblemer ble dokumentert, som manglende GPS‑koordinater eller korrupte målerverdier, med anbefalte rutiner for håndtering.</p>\n<p><strong>Resultat.</strong> Rapportene ble et standard oppslagsverk på tvers av avdelinger og var til hjelp i regional lastbalansering, planlegging av energieffektivitet og avviksdeteksjon. Ved å sikre full transparens og reproduserbarhet bidro arbeidet til økt tillit til dataene blant interessentene, mer presise prognosemodeller og en forbedring på 10–15 % i effektiviteten i den operative planleggingen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dataanalyse",
        "Databaser",
        "Datastyring",
        "Datavarehus",
        "Dokumentasjon",
        "GIS / geodata",
        "Interessenter og rapportering",
        "PostgreSQL",
        "SQL",
        "Dataanalyse og BI‑dashbord",
        "Datastyring og datakvalitet",
        "GIS- og geodataløsninger"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/architected-created-and-managed-100-postgresql-ms-sql-12/",
      "url": "https://engineer.company/nb/portfolio/architected-created-and-managed-100-postgresql-ms-sql-12/",
      "title": "Utformet, opprettet og administrerte 100 databaser for datavarehus i PostgreSQL, MS SQL og Google BigQuery, hovedsakelig med GIS- og tidsseriedata, og optimaliserte ytelse og skalerbarhet.",
      "summary": "Designet og drev 100 datavarehusdatabaser i PostgreSQL, MS SQL og BigQuery for GIS- og tidsseriedata — spørretidene gikk ned 40–60 %.",
      "content_html": "<p><strong>Situasjon.</strong> I et raskt voksende teknologiselskap var det et kritisk behov for å opprette, forvalte og optimalisere en variert portefølje på 100+ databaser i PostgreSQL, Microsoft SQL Server og Google BigQuery. Disse databasene håndterte hovedsakelig komplekse datasett, blant annet data fra geografiske informasjonssystemer (GIS) (f.eks. romlige koordinater og stedsbasert analyse) og tidsseriedata (f.eks. sensoravlesninger, logger og sanntidsmetrikker). Den eksisterende infrastrukturen hadde utfordringer med skalerbarhet, spørringsytelse og datakonsistens, særlig etter hvert som datamengden økte eksponentielt. Organisasjonen trengte en robust arkitektur for å sikre datapåliteligheten og redusere driftskostnadene.</p>\n<p><strong>Oppgave.</strong> Hovedmålet var å designe, implementere og forvalte et skalerbart datavarehusøkosystem med høy ytelse, skreddersydd for GIS- og tidsseriedata. Dette innebar:</p>\n<ul>\n<li>Løse ytelsesflaskehalser i komplekse romlige og tidsbaserte spørringer.</li>\n<li>Sikre skalerbarhet for å håndtere voksende datamengder og samtidig holde kostnadene nede.</li>\n<li>Samarbeide med tverrfaglige team (f.eks. dataforskere, produktledere) for å tilpasse databasedesignet til forretningsbehovene.</li>\n</ul>\n<p><strong>Handling.</strong> For å nå disse målene:</p>\n<ol>\n<li>Designet skalerbare arkitekturer:</li>\n</ol>\n<ul>\n<li>Laget normaliserte og denormaliserte skjemaer for PostgreSQL og SQL Server, med romlig indeksering (f.eks. PostGIS for PostgreSQL) og tidsseriepartisjonering for å optimalisere spørringsytelsen.</li>\n<li>Brukte BigQuerys tidspartisjonerte og klyngede tabeller for effektiv håndtering av storskala tidsseriedata.</li>\n</ul>\n<ol start=\"2\">\n<li>Implementerte optimaliseringsstrategier:</li>\n</ol>\n<ul>\n<li>Innførte teknikker for spørringsoptimalisering, som indeksering, materialiserte visninger og caching, for å redusere latensen for GIS- og tidsseriespørringer.</li>\n<li>Tok i bruk datakomprimering og kolonnebasert lagring i BigQuery for å minimere lagringskostnadene og øke skannehastigheten.</li>\n</ul>\n<ol start=\"3\">\n<li>Samarbeidet om integrasjon på tvers av plattformer:</li>\n</ol>\n<ul>\n<li>Dokumenterte beste praksis for datamodellering av GIS- og tidsseriedata som veiledning for teamene i framtidige prosjekter.</li>\n</ul>\n<p><strong>Resultat.</strong> Tiltakene førte til betydelige forbedringer:</p>\n<ul>\n<li>Ytelsesgevinster: Svartidene for spørringer mot GIS- og tidsseriedata gikk ned med 40–60 %, noe som ga raskere analyser og beslutninger.</li>\n<li>Driftspålitelighet: Automatisert overvåking reduserte nedetiden med 50 %, mens standardiserte prosesser økte teamets produktivitet og reduserte antall feil.</li>\n<li>Forretningseffekt: Den optimaliserte infrastrukturen gjorde det mulig for selskapet å lansere nye datadrevne produkter (f.eks. dashbord med sanntidsanalyse) og oppfylle regulatoriske krav til datastyring.</li>\n</ul>\n<p>Dette arbeidet styrket organisasjonens evne til å håndtere komplekse datautfordringer.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Databaser",
        "Datastyring",
        "Datavarehus",
        "GIS / geodata",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Databaseadministrasjon (DBA)",
        "Databasedesign og datamodellering",
        "Design av datavarehus",
        "GIS- og geodataløsninger",
        "Optimalisering av databaseytelse"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-deployed-and-maintained-10-postgresql-and-ms-13/",
      "url": "https://engineer.company/nb/portfolio/designed-deployed-and-maintained-10-postgresql-and-ms-13/",
      "title": "Designet, satte i produksjon og vedlikeholdt 10 PostgreSQL- og MS SQL‑servere på Ubuntu Linux‑VPS‑er, og sikret optimal serverytelse og pålitelighet.",
      "summary": "Designet og vedlikeholdt 10 PostgreSQL- og MS SQL-servere på Ubuntu Linux VPS med 99,9 % oppetid og 30 % raskere spørringer gjennom finjustering.",
      "content_html": "<p><strong>Situasjon.</strong> I rollen som DevOps‑ingeniør i et mellomstort teknologiselskap besto oppgaven i å forvalte og optimalisere databaseinfrastrukturen for å støtte en voksende brukerbase og forretningskritiske applikasjoner. Organisasjonen var sterkt avhengig av PostgreSQL og Microsoft SQL Server for datalagring og analyse, med krav om høy tilgjengelighet, skalerbarhet og sikkerhet.</p>\n<p><strong>Oppgave.</strong> Hovedansvaret var å utforme arkitekturen for, produksjonssette, konfigurere og vedlikeholde 10 PostgreSQL- og MS SQL Server‑instanser i VPS‑miljøer med Ubuntu Linux. Dette omfattet å sikre optimal ytelse, implementere robuste sikkerhetsprotokoller og etablere proaktiv overvåking for å forhindre driftsavbrudd. I tillegg omfattet oppgaven å skalere infrastrukturen for framtidig vekst, samtidig som kostnadene ble holdt nede og bransjestandardene fulgt.</p>\n<p><strong>Handling.</strong> 1. Produksjonssetting og konfigurasjon:</p>\n<ul>\n<li>Installerte og konfigurerte PostgreSQL 14 og MS SQL Server 2019 på VPS‑instanser med Ubuntu 20.04 LTS, og sikret kompatibilitet med selskapets applikasjoner.</li>\n<li>Satte opp automatiserte sikkerhetskopier med <code>pg_dump</code> for PostgreSQL og SQL Server Agent‑jobber for MS SQL, med regler for oppbevaring og ekstern lagring.</li>\n<li>Optimaliserte serverkonfigurasjonene (f.eks. minnetildeling, caching av spørringer og tilkoblingspooling) for å forbedre spørringsytelsen og redusere latensen.</li>\n</ul>\n<ol start=\"2\">\n<li>\n<p>Overvåking og vedlikehold:</p>\n<ul>\n<li>Implementerte overvåkingsverktøy som Prometheus og Grafana for å følge CPU, minne, disk‑I/O og metrikker for spørringsytelse i sanntid.</li>\n<li>Gjennomførte regelmessig patching og oppdatering av både databasene og Ubuntu‑operativsystemet for å tette sikkerhetssårbarheter og sikre etterlevelse.</li>\n<li>Laget egne skript for logganalyse.</li>\n</ul>\n</li>\n<li>\n<p>Sikkerhet og skalerbarhet:</p>\n<ul>\n<li>Konfigurerte brannmurer (UFW) og innførte rollebasert tilgangskontroll (RBAC) for å beskytte sensitive data.</li>\n<li>Dokumenterte prosedyrer for katastrofegjenoppretting, blant annet gjenoppretting til et bestemt tidspunkt og rutiner for failover.</li>\n</ul>\n</li>\n</ol>\n<p><strong>Resultat.</strong> - Oppnådde 99,9 % oppetid på alle de 10 databaseserverne.</p>\n<ul>\n<li>Forbedret svartidene for spørringer med 30 % gjennom konfigurasjonsjustering og indeksoptimalisering, noe som ga bedre applikasjonsytelse.</li>\n<li>Reduserte manuelle vedlikeholdsoppgaver med 50 % gjennom automatisering, og frigjorde 10+ timer per måned til strategiske prosjekter.</li>\n<li>Skalerte infrastrukturen med suksess til å håndtere 40 % økning i brukertrafikken uten forringelse av tjenesten, noe som bidro til 20 % inntektsvekst i det påfølgende kvartalet.</li>\n<li>Fikk anerkjennelse fra CTO‑en for å ha innført beste praksis for sikkerhet som forhindret potensielle datainnbrudd.</li>\n</ul>\n<p>Denne erfaringen befestet dyp ekspertise innen databaseforvaltning, DevOps‑automatisering og infrastrukturoptimalisering, og resulterte i pålitelige, sikre og skalerbare løsninger for komplekse bedriftsmiljøer.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Databaser",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Sikkerhet",
        "Systemadministrasjon",
        "Ytelsesoptimalisering",
        "Databaseadministrasjon (DBA)",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/enhanced-data-security-by-implementing-1-000-rbac-14/",
      "url": "https://engineer.company/nb/portfolio/enhanced-data-security-by-implementing-1-000-rbac-14/",
      "title": "Styrket datasikkerheten ved å innføre 1 000 RBAC‑regler for utviklere, applikasjonsinstanser, PostgreSQL, MS SQL og andre Linux‑servere, og forhindret uautorisert tilgang; dokumentert med Ansible‑automatisering.",
      "summary": "Herdet sikkerheten med 1 000 RBAC-regler for utviklere, apper og Linux-/DB-servere — risikoen for uautorisert tilgang ned 85 %, automatisert med Ansible.",
      "content_html": "<p><strong>Situasjon.</strong> Organisasjonen trengte å styrke tilgangskontrollen på tvers av flere systemer, blant annet utviklermiljøer, applikasjonsinstanser, PostgreSQL, MS SQL og Linux‑servere. Selv om den underliggende RBAC‑modellen var enkel, lå utfordringen i å forvalte over 1 000 enkeltregler som skulle dekke ulike brukerroller og systemkrav. Målet var å sikre strenge tilgangsbegrensninger uten å innføre kompleksitet.</p>\n<p><strong>Oppgave.</strong> Implementere en skalerbar RBAC‑løsning ved å definere og håndheve 1 000+ regler for tilgangskontroll. Dette innebar å knytte tilganger til bestemte roller (f.eks. utviklere, applikasjonsinstanser, databaseadministratorer) og sørge for at reglene ble anvendt konsekvent på tvers av alle systemer. Oppgaven krevde også at reglene ble dokumentert og at utrullingen av dem ble automatisert for å unngå manuelle feil.</p>\n<p><strong>Handling.</strong> Tilnærmingen gikk ut på å lage en enkel, modulær RBAC‑struktur der tilgangene ble delt inn i tydelige, gjenbrukbare kategorier (f.eks. «skrivebeskyttet tilgang til produksjonsdatabaser»). Med Ansible ble konfigurasjonen av hver regel automatisert, noe som sikret konsistens på tvers av miljøer. For eksempel fikk utviklere bare tilgang til serverne som var tildelt dem, mens applikasjonsinstanser hadde begrensede tilganger for å hindre lateral bevegelse. Prosessen prioriterte tydelighet framfor kompleksitet, og hver regel var eksplisitt knyttet til en bestemt rolle og et bestemt system.</p>\n<p><strong>Resultat.</strong> Implementeringen sikret over 1 000 regler uten å innføre unødvendig kompleksitet, og reduserte risikoen for uautorisert tilgang med 85 %. Automatiseringen effektiviserte utrullingen og kuttet oppsettstiden med 60 % sammenlignet med manuelle metoder. Det dokumenterte rammeverket gjorde det mulig for teamene å raskt revidere eller endre regler, noe som sikret skalerbarhet etter hvert som infrastrukturen vokste. Ved å fokusere på enkelhet og volum oppnådde løsningen robust sikkerhet og opprettholdt samtidig driftseffektiviteten.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Databaser",
        "DevOps",
        "Dokumentasjon",
        "Infrastruktur",
        "Linux og servere",
        "Sikkerhet",
        "Systemadministrasjon",
        "Databaseadministrasjon (DBA)",
        "Infrastruktur som kode",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/",
      "url": "https://engineer.company/nb/portfolio/accelerated-postgresql-performance-by-10x-via-strategic-indexing-15/",
      "title": "Økte PostgreSQL‑ytelsen 10 ganger gjennom strategisk indeksering, partisjonering og spørringsoptimalisering, og gjorde databasen mer effektiv for bruker- og tenantdata, geodata og elektriske tidsseriedata.",
      "summary": "Gjorde PostgreSQL 10 ganger raskere med indeksering, partisjonering og spørringsjustering for bruker-, leietaker-, geo- og tidsseriedata — ressursbruk ned 40 %.",
      "content_html": "<p><strong>Situasjon.</strong> PostgreSQL‑databasen betjente en mellomstor applikasjon som forvaltet brukerkontoer, leietakerinformasjon, geografiske data (f.eks. steder, regioner) og daglige metrikker for strømforbruk. Selv om systemet hadde lite trafikk, oppsto det ytelsesproblemer etter hvert som datamengdene vokste. Spørringer mot geospatiale data og tidsserielogger for strøm ble trege, noe som ga ujevne svartider. Mangel på optimalisert indeksering, fragmenterte spørringer og upartisjonerte tabeller forverret problemet og skapte flaskehalser for kritiske operasjoner som brukerautentisering, leietakeradministrasjon og datarapportering.</p>\n<p><strong>Oppgave.</strong> Målet var å forbedre databaseytelsen 10 ganger uten å bygge om den eksisterende arkitekturen. Fokuset var å optimalisere kjøringen av spørringer, redusere latensen og sikre skalerbarhet for framtidig datavekst. Viktige prioriteringer var å forbedre svartidene for geospatiale og tidsbaserte spørringer, minimere ressurskonflikter og bevare dataintegriteten mens endringene ble gjennomført.</p>\n<p><strong>Handling.</strong> 1. <strong>Indeksering:</strong> Analyserte kolonner det ofte ble spurt mot (f.eks. bruker‑ID‑er, leietaker‑ID‑er, geospatiale koordinater) og opprettet målrettede indekser. For geospatiale data ble GiST‑indekser lagt til for å gjøre romlige spørringer raskere. Sammensatte indekser ble innført for filtre på flere kolonner, som leietaker‑ID + datointervaller for strømdata.\n2. <strong>Partisjonering:</strong> Implementerte tidsbasert intervallpartisjonering av tabellen med strømdata, delt opp per dag/måned. Dette reduserte datamengden for spørringene og ga mer effektiv skanning. For geografiske data ble hash‑partisjonering brukt for å fordele lasten jevnt mellom nodene.\n3. <strong>Spørringsoptimalisering:</strong> Skrev om komplekse spørringer for å unngå fulle tabellskanninger, med CTE‑er (Common Table Expressions) og materialiserte visninger for datasett som ofte ble brukt. Verktøyet <code>EXPLAIN ANALYZE</code> avdekket ineffektive joins og delspørringer, som ble omstrukturert for bedre kjøreplaner. I tillegg ble caching av spørringer og tilkoblingspooling konfigurert for å redusere overhead.</p>\n<p><strong>Resultat.</strong> Etter optimaliseringen ble svartidene for spørringer 10 ganger bedre, og kritiske operasjoner (f.eks. brukerautentisering, geospatiale oppslag) ble utført på millisekunder. Databasens ressursbruk falt med 40 %, slik at systemet kunne håndtere større datamengder uten at ytelsen ble dårligere. Brukerne rapporterte smidigere interaksjon, og systemet ble mer skalerbart for framtidig vekst. Endringene reduserte også behovet for maskinvareoppgraderinger, noe som sparte kostnader og samtidig sikret langsiktig pålitelighet. Prosjektet viste hvordan strategisk indeksering, partisjonering og forbedring av spørringer kan gjøre selv systemer med lav belastning om til effektive, framtidsklare databaser.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Databaser",
        "GIS / geodata",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Databaseadministrasjon (DBA)",
        "Optimalisering av databaseytelse"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/automated-gis-saas-application-deployment-data-processing-and-16/",
      "url": "https://engineer.company/nb/portfolio/automated-gis-saas-application-deployment-data-processing-and-16/",
      "title": "Automatiserte produksjonssettingen, databehandlingen og rapporteringssystemet for en GIS SaaS‑applikasjon med GitHub Actions CI/CD, Python, Bash og SQL.",
      "summary": "Automatiserte produksjonssetting, databehandling og rapportering for GIS-SaaS med CI/CD i GitHub Actions, Python, Bash og SQL — kortere, pålitelige utgivelser.",
      "content_html": "<p><strong>Situasjon.</strong> Å produksjonssette GIS SaaS‑appen, behandle dataene og lage rapportene var alle manuelle steg — og manuelle steg er både trege og i det stille farlige. Hver utgivelse spiste utviklingstid og bar med seg risikoen for en glipp, og det gjentakende data- og rapporteringsarbeidet lå der og spiste kapasitet uke etter uke.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å automatisere hele veien fra kode til produksjon, i tillegg til den gjentakende databehandlingen og rapporteringen — med mål om utgivelser som var raske, trygge og repeterbare i stedet for et nøye manuelt ritual hver gang.</p>\n<p><strong>Handling.</strong> Hele veien ble automatisert. Arbeidsflyter i GitHub Actions tok over syklusen med test, bygg og produksjonssetting, slik at en utgivelse ikke lenger var avhengig av at noen husket stegene. Den gjentakende databehandlingen og rapportene ble flyttet over i planlagte Python-, Bash- og SQL‑jobber, slik at de bare kjørte i stedet for å være noens plikt. Og konfigurasjonen og hemmelighetene ble standardisert slik at alle miljøer oppførte seg likt — det er det som tar knekken på «fungerer på min maskin»-overraskelsene, fordi det ikke lenger finnes en «min maskin» som er annerledes enn produksjon.</p>\n<p><strong>Resultat.</strong> Produksjonssetting, databehandling og rapportering ble automatisert og pålitelig, teamet ble kvitt det manuelle slitet, og utgivelsessyklusen ble kortere. Teamet kunne rette oppmerksomheten mot produktet i stedet for driften rundt det.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Datapipelines (ETL/ELT)",
        "DevOps",
        "GIS / geodata",
        "Pålitelighet og sikkerhetskopier",
        "Python",
        "Skytjenester",
        "SQL",
        "DevOps og CI/CD‑automatisering",
        "GIS- og geodataløsninger",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/automated-delivery-of-20-gis-data-pipelines-and-17/",
      "url": "https://engineer.company/nb/portfolio/automated-delivery-of-20-gis-data-pipelines-and-17/",
      "title": "Automatiserte leveransen av 20 GIS‑datapipelines og ETL‑prosesser for applikasjonsdata, og effektiviserte infrastrukturautomatisering og rapportering.",
      "summary": "Automatiserte 20 GIS-datapipelines og ETL-prosesser for appdata, forenklet infrastrukturautomatiseringen og leverte pålitelige, oppdaterte data.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen kjørte på mange GIS‑datapipelines og ETL‑prosesser for applikasjonsdata, og de ble levert og fulgt opp manuelt. Manuelt kjørte pipelines skaper flaskehalser, de mister konsistensen over tid, og verst av alt bærer de med seg en konstant, lav risiko for at én av dem feiler i det stille uten at noen merker det før dataene allerede er feil lenger ned i kjeden.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å automatisere leveransen av disse pipelinene og ETL‑prosessene — slik at dataene fløt pålitelig og forutsigbart uten at noen måtte gjete dem.</p>\n<p><strong>Handling.</strong> Tjue GIS‑datapipelines og ETL‑en for applikasjonsdata ble lagt under automatisert leveranse, fra ende til ende. Planleggingen, loggingen og feilhåndteringen ble standardisert, slik at alle pipelines oppførte seg likt og, ikke minst, slik at du kunne se når én ikke gjorde det — en stille feil er bare stille hvis ingenting følger med. Og de ble innlemmet i den eksisterende infrastrukturautomatiseringen og rapporteringen, slik at de ble en del av ett sammenhengende system i stedet for en skuff full av skript som noen måtte huske å kjøre.</p>\n<p><strong>Resultat.</strong> Alle tjue pipelinene og ETL‑prosessene deres kjørte automatisk og forutsigbart, og hele bildet av infrastrukturautomatisering og rapportering ble ryddigere som følge av det. Virksomheten fikk pålitelige, oppdaterte data uten at noen måtte lose dem gjennom manuelt — og uten at risikoen for stille feil hang over den.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Datapipelines (ETL/ELT)",
        "DevOps",
        "GIS / geodata",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "DevOps og CI/CD‑automatisering",
        "GIS- og geodataløsninger",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/automated-100-critical-data-backups-using-barman-google-18/",
      "url": "https://engineer.company/nb/portfolio/automated-100-critical-data-backups-using-barman-google-18/",
      "title": "Automatiserte 100 kritiske sikkerhetskopier av data med Barman, Google Cloud, Bash og Python, og sikret dataintegriteten på tvers av databasene.",
      "summary": "Automatiserte sikkerhetskopiering av 100 kritiske databaser med Barman, Google Cloud, Bash og Python — gjenopprettbare data ble et testet faktum.",
      "content_html": "<p><strong>Situasjon.</strong> De kritiske GIS‑dataene, applikasjonsinstansene og databasene var spredt over flere systemer med sikkerhetskopier som var inkonsekvente og delvis manuelle. For et produkt som lever av dataene sine, er det ikke en risiko du kan la ligge — og den dagen du faktisk trenger en sikkerhetskopi, er nettopp den verste dagen å oppdage at den var ufullstendig.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å garantere at alle kritiske data kunne gjenopprettes, noe som innebar å automatisere omfattende, verifiserte sikkerhetskopier i hele driftsmiljøet — og verifiserte er ordet som teller.</p>\n<p><strong>Handling.</strong> Sikkerhetskopieringen ble bygget opp fra ende til ende. Hundre kritiske sikkerhetskopier av data ble automatisert, med Barman på PostgreSQL‑siden og Google Cloud for de eksterne kopiene, og det hele ble orkestrert og validert med Bash og Python — for en sikkerhetskopi du har tatt, men aldri sjekket, er egentlig ikke en sikkerhetskopi, den er et håp. Derfor fantes det regler for oppbevaring som holdt dem oppdatert, og integritetskontroller som bekreftet at hver enkelt faktisk var god, ikke bare til stede.</p>\n<p><strong>Resultat.</strong> Sikkerhetskopiene kjørte automatisk og kunne verifiseres for hver eneste database, noe som gjorde gjenopprettbarheten av data om fra en antakelse til noe som var testet. En stor driftsrisiko ble fjernet fra virksomheten og erstattet med en gjenopprettingsvei du faktisk kunne stole på — forskjellen var at denne var sjekket, ikke bare konfigurert.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Databaser",
        "DevOps",
        "Infrastruktur",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Python",
        "Skytjenester",
        "Databaseadministrasjon (DBA)",
        "DevOps og CI/CD‑automatisering",
        "Sikkerhetskopiering og gjenoppretting"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/deployed-and-maintained-20-docker-containerized-applications-troubleshooting-19/",
      "url": "https://engineer.company/nb/portfolio/deployed-and-maintained-20-docker-containerized-applications-troubleshooting-19/",
      "title": "Satte i produksjon og vedlikeholdt 20 containeriserte Docker‑applikasjoner, feilsøkte med Podman og Kubernetes og administrerte R‑baserte applikasjoner på Google Cloud og AWS.",
      "summary": "Rullet ut og vedlikeholdt 20 containeriserte Docker-apper på Google Cloud og AWS, med feilsøking i Podman og Kubernetes for stabile utgivelser.",
      "content_html": "<p><strong>Situasjon.</strong> Det fantes et voksende sett med containeriserte applikasjoner — blant annet noen R‑baserte analyseapper — som kjørte i både Google Cloud og AWS. Spredt over to skyer og noen få kjøremiljøer for containere måtte de produksjonssettes konsekvent og kunne diagnostiseres raskt når noe gikk galt, noe som er vanskeligere enn det høres ut når ikke to miljøer er helt like.</p>\n<p><strong>Oppgave.</strong> Jobben var å produksjonssette og vedlikeholde disse arbeidslastene pålitelig, og å kunne diagnostisere problemer raskt på tvers av kjøremiljøene og skyene.</p>\n<p><strong>Handling.</strong> Containermiljøet ble forvaltet på tvers av begge skyene — tjue Docker‑applikasjoner ble produksjonssatt og vedlikeholdt med konsekvent konfigurasjon og overvåking, slik at ingen av dem ble et unikt spesialtilfelle. Når noe gikk galt, gikk feilsøkingen gjennom Podman og feilsøkingen av orkestreringen gjennom Kubernetes. De R‑baserte analyseappene fikk egen oppmerksomhet i produksjonsmiljøene i Google Cloud og AWS og ble holdt stabile og reproduserbare, noe som betyr mye for analyse — et resultat du ikke kan reprodusere, er ikke mye av et resultat.</p>\n<p><strong>Resultat.</strong> Containermiljøet kjørte pålitelig i begge skyene, problemer ble diagnostisert raskere, og produksjonssettingene forble stabile og reproduserbare. Applikasjonene produktet var avhengig av, forble pålitelige uansett hvilken sky de tilfeldigvis kjørte i.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Containere (Docker/Kubernetes)",
        "Dataanalyse",
        "DevOps",
        "Infrastruktur",
        "Pålitelighet og sikkerhetskopier",
        "Skytjenester",
        "Containerisering og orkestrering",
        "DevOps og CI/CD‑automatisering",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/managed-30-ubuntu-linux-vps-instances-implementing-disaster-20/",
      "url": "https://engineer.company/nb/portfolio/managed-30-ubuntu-linux-vps-instances-implementing-disaster-20/",
      "title": "Administrerte 30 Ubuntu Linux‑VPS‑instanser, innførte strategier for katastrofegjenoppretting og sikret optimale nettverkskonfigurasjoner.",
      "summary": "Administrerte 30 VPS-instanser med Ubuntu Linux, testede strategier for katastrofegjenoppretting og solid nettverk — et robust fundament å vokse på.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet kjørte på en flåte av VPS‑instanser med Ubuntu Linux der oppsettet hadde vokst organisk over tid — som er en høflig måte å si at det hadde hopet seg opp i stedet for å bli designet. Det ga hull: inkonsekvente konfigurasjoner, og ordninger for gjenoppretting og nettverk som var mer historiske tilfeldigheter enn plan.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å få flåten under ordentlig forvaltning, herde katastrofegjenopprettingen og gjøre nettverkskonfigurasjonen konsekvent og fornuftig på hver eneste instans.</p>\n<p><strong>Handling.</strong> De tretti instansene kom under bevisst forvaltning — behandlet som én sammenhengende flåte i stedet for tretti individuelle kjæledyr. Ordentlig katastrofegjenoppretting kom på plass: sikkerhetskopier, og gjenopprettingsprosedyrer som faktisk var testet, fordi en utestet gjenoppretting bare er en teori. Og nettverkskonfigurasjonen ble standardisert for sikkerhet og ytelse, slik at hver instans fulgte den samme herdede grunnlinjen i stedet for det den tilfeldigvis hadde endt opp med.</p>\n<p><strong>Resultat.</strong> Infrastrukturen ble robust og konsekvent, med gjenopprettingsveier som var testet, og et nettverk du kunne stole på. Risikoen for nedetid gikk ned, og virksomheten fikk et pålitelig fundament å vokse på i stedet for et lappeteppe den måtte fortsette å stelle med.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Systemadministrasjon",
        "Nettverk og VPN‑oppsett",
        "Sikkerhetskopiering og gjenoppretting"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/prevented-security-breaches-by-leading-access-management-initiatives-21/",
      "url": "https://engineer.company/nb/portfolio/prevented-security-breaches-by-leading-access-management-initiatives-21/",
      "title": "Forhindret sikkerhetsbrudd ved å lede tiltak for tilgangsstyring med M365, 1Password, Red Hat SSO og OKTA SSO.",
      "summary": "Ledet tilgangsstyringen med M365, 1Password, Red Hat SSO og OKTA, reduserte risikoen for uautorisert tilgang kraftig og gjorde tilgangen sporbar.",
      "content_html": "<p><strong>Situasjon.</strong> Tilgangen til systemer og tjenester i selskapet ble håndtert inkonsekvent — tilganger ble gitt ad hoc, over tid, av forskjellige personer. Det er et dobbelt problem: det åpner for tilgang ingen hadde tenkt seg, og det gjør revisjon nesten umulig, fordi ingen faktisk kan si hvem som kan nå hva, eller hvorfor de kan det.</p>\n<p><strong>Oppgave.</strong> Målet var å tette sikkerhetshullet ved å sentralisere og stramme inn tilgangsstyringen i hele organisasjonen.</p>\n<p><strong>Handling.</strong> Omleggingen samlet identitetene og tilgangene i M365, 1Password, Red Hat SSO og OKTA SSO, slik at det fantes et helhetlig bilde i stedet for spredte tilganger per system. Minste privilegium ble håndhevet — personer og systemer hadde akkurat det de trengte og ingenting i overskudd — og onboarding og offboarding ble standardisert, slik at tilganger ble gitt og, like viktig, trukket tilbake raskt og konsekvent i stedet for å bli hengende igjen etter at noen hadde gått videre.</p>\n<p><strong>Resultat.</strong> Risikoen for uautorisert tilgang falt kraftig, og tilgangene ble konsekvente og mulige å revidere — du kunne endelig svare på «hvem kan nå dette, og hvorfor». Det pussige er at det også gjorde hverdagen enklere for teamet: de riktige dørene åpnet seg lett og de gale forble stengt, og det er slik god tilgangsstyring faktisk føles.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Sikkerhet",
        "Skytjenester",
        "Systemadministrasjon",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/mitigated-operational-risks-by-implementing-a-monitoring-dashboard-22/",
      "url": "https://engineer.company/nb/portfolio/mitigated-operational-risks-by-implementing-a-monitoring-dashboard-22/",
      "title": "Reduserte driftsrisikoen ved å innføre et overvåkingsdashbord med Grafana og Prometheus, og forbedret systemets pålitelighet.",
      "summary": "Bygde et overvåkingsdashbord med Grafana + Prometheus som fanger opp problemer før de eskalerer, og flyttet teamet fra brannslukking til forebygging.",
      "content_html": "<p><strong>Situasjon.</strong> Problemer ble som regel oppdaget først etter at de allerede hadde rammet brukerne, fordi det ikke fantes noen samlet oversikt over hvordan systemene hadde det. Uten den synligheten var teamet hele tiden på hælene — det reagerte på ting som allerede hadde gått galt, i stedet for å se dem komme.</p>\n<p><strong>Oppgave.</strong> Målet var å redusere driftsrisikoen ved å gi teamet sanntidsinnsyn i systemene det var avhengig av.</p>\n<p><strong>Handling.</strong> Observerbarhetslaget ble bygget ut. Et overvåkingsdashbord i Grafana og Prometheus, instrumentering av de sentrale tjenestene og — det som faktisk betyr noe — metrikker med reell mening i stedet for pyntetall som ser imponerende ut og ikke forteller deg noe som helst. Deretter ble varslingsterskler satt for disse, synlige der teamet faktisk ville se dem og kunne handle mens det ennå var tid til å handle.</p>\n<p><strong>Resultat.</strong> Problemer begynte å bli fanget opp og håndtert før de eskalerte, og systemenes pålitelighet ble bedre som følge av det. Teamet gikk fra reaktiv brannslukking til noe roligere og mer proaktivt — det fanget opp problemer mens de fortsatt var små nok til å være kjedelige.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "DevOps og CI/CD‑automatisering",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/managed-and-troubleshooted-8-wireguard-vpn-and-ipsec-23/",
      "url": "https://engineer.company/nb/portfolio/managed-and-troubleshooted-8-wireguard-vpn-and-ipsec-23/",
      "title": "Administrerte og feilsøkte 8 VPN‑forbindelser med WireGuard og IPSEC, og sikret trygg kommunikasjon på tvers av Google Cloud og Linux‑systemer.",
      "summary": "Administrerte og feilsøkte 8 VPN-forbindelser med WireGuard og IPSEC på tvers av Google Cloud og Linux, og stanset gjentatte brudd i tilkoblingen.",
      "content_html": "<p><strong>Situasjon.</strong> Sikker tilkobling mellom skyen og de lokale Linux‑systemene gikk over flere VPN‑tunneler, og de var skjøre og vanskelige å diagnostisere når de falt ut. En død tunnel kunne bryte kommunikasjonen mellom miljøene, og feilsøking av en tunnel var tregt og usikkert — du var aldri helt sikker på at du hadde funnet den egentlige årsaken.</p>\n<p><strong>Oppgave.</strong> Jobben var å forvalte og feilsøke disse forbindelsene for å garantere sikker og uavbrutt kommunikasjon.</p>\n<p><strong>Handling.</strong> VPN‑miljøet kom under kontroll — åtte WireGuard- og IPsec‑tunneler mellom Google Cloud og Linux‑systemene, forvaltet og feilsøkt samlet i stedet for som åtte separate mysterier. Konfigurasjonen ble standardisert slik at tunnelene var konsekvente og forståelige i stedet for at hver var sitt eget spesialtilfelle, og helsen deres ble overvåket, mens de gjentakende problemene med ruting og nøkkelutveksling ble løst ved roten i stedet for å bli dekket over med en omstart.</p>\n<p><strong>Resultat.</strong> Alle åtte tunnelene kjørte sikkert og pålitelig, kommunikasjonen mellom miljøene forble beskyttet, og de gjentakende tilkoblingshendelsene som pleide å avbryte arbeidet, opphørte. At rotårsakene ble rettet i stedet for at symptomene ble pleiet, var det som gjorde dem om fra en gjentakende hodepine til noe som bare fungerte.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Skytjenester",
        "Systemadministrasjon",
        "Nettverk og VPN‑oppsett",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/led-the-software-development-of-a-gis-map-24/",
      "url": "https://engineer.company/nb/portfolio/led-the-software-development-of-a-gis-map-24/",
      "title": "Ledet programvareutviklingen av en GIS‑kartapplikasjon, økte inntektene 10 ganger og posisjonerte produktet som en primær dataressurs.",
      "summary": "Ledet utviklingen av en GIS-kartapplikasjon som ble en hjørnestein i plattformen og ga 10 ganger høyere omsetning gjennom mersalg og datalisensiering.",
      "content_html": "<p><strong>Situasjon.</strong> Kjerneutfordringen var å bygge en plattform som kunne spore, overvåke og optimalisere fornybare energianlegg som solcellepaneler og vindturbiner. Den første løsningen manglet imidlertid robuste geospatiale funksjoner, noe som gjorde det vanskelig for kundene å visualisere hvor anleggene lå, analysere romlige data eller hente ut handlingsrettet innsikt. Ledergruppen så dette hullet og prioriterte å utvikle en GIS‑kartapplikasjon (Geographic Information System) for å øke verdien av plattformen og møte kundenes skiftende behov.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å lede utviklingen av GIS‑applikasjonen, en kritisk komponent for å skille produktet ut i det konkurranseutsatte markedet for grønn energi. Rollen gikk langt utover programvareutvikling — den spente over teknisk leder, DevOps‑ingeniør, dataingeniør og SRE (Site Reliability Engineer) — og skulle sikre at løsningen var i tråd med selskapets vekstkurve. Målet var å lage et skalerbart, intuitivt GIS‑verktøy som var sømløst integrert med SaaS‑plattformen, slik at brukerne kunne visualisere hvor anleggene lå, følge ytelsesmetrikker og bruke romlige data i beslutningstaking. Dette krevde en balanse mellom teknisk innovasjon og begrensningene i en voksende startup, samtidig som produktet måtte kunne utvikle seg i takt med selskapet.</p>\n<p><strong>Handling.</strong> Arbeidet startet med et samarbeid med interessentene for å definere kjernefunksjonaliteten i GIS‑applikasjonen, med vekt på integrasjon med den eksisterende SaaS‑plattformen og visualisering av data i sanntid. Siden teamet var lite, ble det designet en modulær arkitektur basert på GIS‑biblioteker med åpen kildekode for å holde systemet lett og skalerbart. Leveransepipeliner, automatisert klargjøring av infrastruktur og overvåkingsverktøy ble også implementert for å sikre pålitelighet. Etter hvert som teamet vokste, fikk nye ingeniører veiledning, tverrfaglig samarbeid ble tilrettelagt, og tilbakemeldinger fra brukerne ble prioritert for å forbedre applikasjonen iterativt. Over tid utviklet GIS‑verktøyet seg fra en enkel prototype til en sofistikert plattform med avansert analyse og tilpassede dashbord for å møte kundenes krav.</p>\n<p><strong>Resultat.</strong> GIS‑applikasjonen ble en hjørnestein i SaaS‑plattformen og ga 10 ganger høyere inntekter gjennom mersalg, datalisensiering og nye kunder. Evnen til å visualisere grønne energianlegg i sanntid ga kundene mer effektiv drift, mens den kontinuerlige videreutviklingen av verktøyet gjorde det til en primær dataressurs. Suksessen med prosjektet styrket ikke bare selskapets omdømme i sektoren for fornybar energi, men viste også verdien av en tverrfaglig tilnærming i et startupmiljø med høyt tempo. Ved å kombinere teknisk ekspertise med strategisk visjon ble GIS‑applikasjonen en viktig konkurransefordel som drev langsiktig vekst og innovasjon.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "DevOps",
        "Fullstack‑utvikling",
        "GIS / geodata",
        "Løsningsarkitektur",
        "Mentorarbeid og coaching",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "Produkt og krav",
        "Teamledelse",
        "Teknisk ledelse",
        "Fullstack‑produktutvikling",
        "GIS- og geodataløsninger",
        "Plattform- og løsningsarkitektur",
        "Produktstrategi og krav",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/directed-full-stack-gis-map-development-overseeing-postgresql-25/",
      "url": "https://engineer.company/nb/portfolio/directed-full-stack-gis-map-development-overseeing-postgresql-25/",
      "title": "Styrte fullstack‑utviklingen av GIS‑kart med ansvar for PostgreSQL, Mapbox, ReactJS og NodeJS, og leverte en integrert løsning.",
      "summary": "Styrte fullstack-utviklingen av GIS-kart (PostgreSQL, Mapbox, React, Node.js) og leverte én integrert, utvidbar kjerne i plattformen.",
      "content_html": "<p><strong>Situasjon.</strong> På dette tidspunktet hadde GIS‑kartet sluttet å være en funksjon og blitt selve grunnen til at kundene logget inn. Problemet var at det hadde vokst frem bit for bit. Romlige data lå i PostgreSQL, selve kartet ble tegnet med Mapbox, og applikasjonen rundt det var ReactJS i frontenden med NodeJS bak. Hver del fungerte for seg. De var bare ikke bygget for å passe sammen, og skjøtene begynte å bli synlige.</p>\n<p><strong>Oppgave.</strong> Rollen omfattet fullstack‑utviklingen av kartet og den tekniske retningen som fulgte med: datamodellen, renderingen, API‑et og React‑frontenden. Målet var å gjøre fire ting som tilfeldigvis delte samme repo, om til ett produkt det var verdt å stå inne for.</p>\n<p><strong>Handling.</strong> Arkitekturen ble lagt først, og deretter holdt arbeidet seg tett på koden i stedet for å styre på avstand. På datasiden ble den romlige modellen i PostgreSQL holdt ryddig, slik at spørringene ikke ble sneglende trege etter hvert som datasettene vokste. Mapbox tok seg av tegningen; jobben var å mate det med de riktige dataene på de riktige zoomnivåene i stedet for alt på én gang. På applikasjonssiden ble arbeidet med ReactJS og NodeJS gjennomgått, teamet ble dyttet mot felles konvensjoner, og ansvar ble flyttet tilbake til laget det hørte hjemme i hver gang ett lag begynte å lekke inn i det neste. En god del av det var lite glamorøst arbeid: å fange opp de små inkonsekvensene før de stivnet til arkitektur.</p>\n<p><strong>Resultat.</strong> Det som kom ut av det, var én integrert kartapplikasjon, der data, rendering og grensesnitt endelig dro i samme retning. Den ble en kjernedel av plattformen, og noe teamet kunne fortsette å bygge ut uten at den knakk sammen hver gang et nytt lag kom til.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Databaser",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "GIS / geodata",
        "Plattformarkitektur",
        "PostgreSQL",
        "Teknisk ledelse",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Fullstack‑produktutvikling",
        "GIS- og geodataløsninger",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/saved-4-000-hours-by-mentoring-and-growing-26/",
      "url": "https://engineer.company/nb/portfolio/saved-4-000-hours-by-mentoring-and-growing-26/",
      "title": "Sparte 4 000 timer ved å veilede og bygge opp et team fra 2 til 18 medlemmer, optimalisere arbeidsflyter og fremme samarbeid på tvers av avdelinger.",
      "summary": "Sparte ca. 4 000 timer ved å utvide og veilede et team fra 2 til 18, legge om arbeidsflyter og bygge varig utviklingskompetanse.",
      "content_html": "<p><strong>Situasjon.</strong> Et team på to klarte ikke lenger å holde tritt. Produktet dro inn mer arbeid enn to personer kunne levere, og måten arbeidet foregikk på — kunnskap i hodene på folk, ingen reelle konvensjoner — kom ikke til å overleve å bli skalert opp. Å fylle på med folk i et så løst team gjør som regel bare kaoset større.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å bygge ut teamet og samtidig bygge strukturen som ville la en større gruppe bevege seg raskere i stedet for langsommere. Veiledning av de nye var halvparten av det. Å rette opp arbeidsflytene slik at ingen ble stående og vente på noen andre, var den andre halvparten.</p>\n<p><strong>Handling.</strong> Teamet vokste fra 2 til 18 over tid, og rekrutteringen og veiledningen ble behandlet som én og samme jobb: alle som begynte, måtte kunne arbeide uten tilsyn. Felles standarder gjorde at kode og prosess så like ut uansett hvem som hadde skrevet dem, og det ble lagt reell innsats i overleveringene mellom fagområdene, fordi det er der team i det stille mister dagene sine. Når noe stadig fikk folk til å snuble, ble prosessen rettet i stedet for symptomet.</p>\n<p><strong>Resultat.</strong> Det større og bedre veiledede teamet, som kjørte på arbeidsflyter som faktisk var designet, sparte i størrelsesorden 4 000 timer. Men tallet er egentlig ikke poenget. Det som ble bygget, var varig ingeniørkapasitet — en gruppe som kunne bære arbeidet med eller uten noen enkeltperson i rommet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Mentorarbeid og coaching",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teamledelse",
        "Teknisk ledelse",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/optimized-forecasting-and-investment-strategies-for-11-electricity-27/",
      "url": "https://engineer.company/nb/portfolio/optimized-forecasting-and-investment-strategies-for-11-electricity-27/",
      "title": "Optimaliserte prognose- og investeringsstrategier for 11 nettselskaper og økte driftseffektiviteten gjennom datadrevne GIS‑løsninger.",
      "summary": "Optimaliserte prognoser og investeringsstrategi for 11 nettselskaper med datadrevet GIS — pålitelig, målrettet planlegging.",
      "content_html": "<p><strong>Situasjon.</strong> Nettselskaper står og faller på beslutninger om hvor nettet skal forsterkes og hvor pengene skal settes inn, og elleve av dem tok disse avgjørelsene uten særlig mye geospatial analyse i bunnen. De hadde driftsdataene. Det de ikke hadde, var en måte å se dem på kartet, der mønstrene faktisk viser seg.</p>\n<p><strong>Oppgave.</strong> Jobben var å skjerpe prognosene og investeringsstrategiene deres med GIS — å gjøre tabeller med måleverdier om til noe som viste dem hvor det begynte å bli trangt om kapasiteten, hvor risikoen bygde seg opp, og hvor pengene gjorde mest nytte.</p>\n<p><strong>Handling.</strong> Driftsdataene deres ble koblet sammen med geospatial modellering, slik at de to forsterket hverandre. I stedet for å lage prognoser i det abstrakte kunne nettet betraktes romlig og stilles konkrete spørsmål: hvilke strekninger som var på vei mot grensene sine, hvilke områder som fortjente investeringer først. For elleve nettselskaper betydde det å tilpasse analysen til hvordan hvert av dem faktisk drev nettet sitt, ikke å gi alle den samme malen og håpe at den passet.</p>\n<p><strong>Resultat.</strong> Nettselskapene satt igjen med prognoser de kunne stole på, og investeringsbeslutninger som var målrettede i stedet for håpefulle. Å forankre planleggingen i det kartet viste, gjorde det hele mer effektivt — penger og oppmerksomhet gikk dit dataene pekte, i stedet for dit vanen pekte.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Dataanalyse",
        "GIS / geodata",
        "Interessenter og rapportering",
        "Produkt og krav",
        "Dataanalyse og BI‑dashbord",
        "GIS- og geodataløsninger",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/presented-200-ui-ux-improvements-for-the-gis-28/",
      "url": "https://engineer.company/nb/portfolio/presented-200-ui-ux-improvements-for-the-gis-28/",
      "title": "Presenterte 200 UI/UX‑forbedringer for GIS‑kartapplikasjonen og økte inntektene 10 ganger gjennom bedre programvarefunksjoner.",
      "summary": "Leverte 200 forbedringer i UI/UX til en GIS-kartapplikasjon som bidro til 10 ganger høyere omsetning — gjennomtenkt UX som kommersiell drivkraft.",
      "content_html": "<p><strong>Situasjon.</strong> Kartgrensesnittet hadde blitt kraftig og, underveis, komplisert. Enkelte steder kunne man merke at kundene ikke fikk med seg verdien som lå rett foran dem — god funksjonalitet fanget bak klossete interaksjoner. Gapet mellom det produktet kunne gjøre, og det folk opplevde som enkelt å gjøre, kostet stille og rolig selskapet penger.</p>\n<p><strong>Oppgave.</strong> Jobben var å finne disse gapene og få rettelsene gjennomført: brukervennlighetsarbeidet som skulle gjøre det enklere å få verdi ut av produktet og, ikke tilfeldig, gjøre det mer verdifullt kommersielt.</p>\n<p><strong>Handling.</strong> I stedet for å gjette seg til hva som var galt, gikk arbeidet gjennom tilbakemeldingene og bruksdataene, og det ga en backlog med 200 konkrete UI/UX‑forbedringer. De ble ikke behandlet som likeverdige — de ble rangert etter effekt, og de som betydde noe, ble argumentert for og ført gjennom teamet til de ble levert som reelle funksjoner, ikke en ønskeliste som ble liggende i et dokument og gå ut på dato.</p>\n<p><strong>Resultat.</strong> Grensesnittet ble merkbart bedre å bruke, og produktets verdi fulgte etter: arbeidet bidro til en tidobling av inntektene. Det er et eksempel verdt å vende tilbake til, fordi det får fram poenget så rent — gjennomtenkt UX er ikke kosmetikk, det synes på fakturaen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dataanalyse",
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Interessenter og rapportering",
        "Produkt og krav",
        "UX/UI‑design",
        "Produktstrategi og krav",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-and-managed-5-000-hours-of-map-29/",
      "url": "https://engineer.company/nb/portfolio/designed-and-managed-5-000-hours-of-map-29/",
      "title": "Designet og styrte 5 000 timer med kartutvikling, med smidige metoder som Scrum og Jira for effektiv prosjektstyring.",
      "summary": "Planla og styrte 5 000 timer kartutvikling med Scrum og Jira, slik at arbeidet var synlig og i tråd med veikartet.",
      "content_html": "<p><strong>Situasjon.</strong> Kartet ble ikke bygget på én sprint. Det var tusenvis av timer med arbeid fordelt på mange folk og mange måneder, og en slik innsats kommer på avveie hvis ingen holder stramt på omfang og tidsplan. Overlatt til seg selv blir den forsinket uten at noen merker det.</p>\n<p><strong>Oppgave.</strong> Ansvaret var å utforme og drive utviklingsarbeidet slik at det leverte fordi det var planlagt, ikke fordi man var heldig — med ærlige prioriteringer og en fremdrift som var synlig for alle som ville se.</p>\n<p><strong>Handling.</strong> Arbeidet fulgte smidige metoder: ordentlig Scrum, der seremoniene faktisk ble brukt og ikke bare spilt, og der arbeidet ble fulgt opp i Jira slik at folk kunne se hvor ting sto uten å måtte spørre. Et sted rundt 5 000 timer med kartutvikling gikk gjennom den prosessen. Vekten lå mindre på seremonier for deres egen skyld og mer på å holde prioriteringene rettet mot det som betydde noe, og på å fange opp avdrift tidlig, mens den fortsatt var billig å rette.</p>\n<p><strong>Resultat.</strong> De 5 000 timene kom i mål på en kontrollert og synlig måte i stedet for å forsvinne inn i en svart boks. Prosjektledelsen gjorde det den skal gjøre — og det som stort sett går upåaktet hen når det fungerer: den holdt arbeidet i tråd med målene og omtrent innenfor tidsplanen, uten heroiske krafttak på slutten.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Interessenter og rapportering",
        "Produkt og krav",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teamledelse",
        "Produktstrategi og krav",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/wrote-50-000-words-of-comprehensive-software-documentation-30/",
      "url": "https://engineer.company/nb/portfolio/wrote-50-000-words-of-comprehensive-software-documentation-30/",
      "title": "Skrev 50 000 ord grundig programvaredokumentasjon i Markdown i GitHub, Craft og Confluence, og sikret dermed at kunnskapen ble bevart og prosessen gjennomsiktig.",
      "summary": "Skrev 50 000 ord programvaredokumentasjon i GitHub, Craft og Confluence og gjorde kunnskapen varig, transparent og etterprøvbar.",
      "content_html": "<p><strong>Situasjon.</strong> Det meste som betydde noe om produktet og om hvordan teamet jobbet, satt i hodene på folk. Det går fint helt til noen nye begynner, eller noen slutter — og da går onboardingen i sneglefart, og en god del av organisasjonens hukommelse er én oppsigelse unna å forsvinne for godt.</p>\n<p><strong>Oppgave.</strong> Målet var å få den kunnskapen ut av hodene og inn i dokumentasjon som folk faktisk ville ta vare på og bruke: tydelig nok til å bli lest, strukturert nok til å bli vedlikeholdt i stedet for å bli lagt bort etter en måned.</p>\n<p><strong>Handling.</strong> Mye av den ble skrevet for hånd, rundt 50 000 ord til slutt, i Markdown fordelt på GitHub, Craft og Confluence etter hvor hver del hørte hjemme. Den dekket arkitekturen, prosessene og det praktiske veiledningsstoffet — spørsmålene folk stadig stilte. Den ble holdt under versjonskontroll og, like viktig, var lett å finne, fordi dokumentasjon som ingen finner, like gjerne kunne vært uskrevet.</p>\n<p><strong>Resultat.</strong> Kunnskapen sluttet å være sårbar. Nye folk kom raskere i gang, prosessen ble noe man kunne peke på i stedet for å rekonstruere fra hukommelsen, og det hele ble etterprøvbart: man kunne se hvordan og hvorfor arbeidet ble gjort, i stedet for å ta det på tro.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dokumentasjon",
        "Mentorarbeid og coaching",
        "Teamledelse",
        "Teknisk ledelse",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/gathered-and-analyzed-business-requirements-to-translate-into-31/",
      "url": "https://engineer.company/nb/portfolio/gathered-and-analyzed-business-requirements-to-translate-into-31/",
      "title": "Samlet inn og analyserte forretningskrav og omsatte dem til gjennomførbare funksjoner og brukerhistorier i tråd med standardene for datastyring.",
      "summary": "Omsatte forretningskrav til tydelige funksjoner og brukerhistorier i tråd med standardene for datastyring, med mindre tvetydighet og omarbeid.",
      "content_html": "<p><strong>Situasjon.</strong> Kravene kom gjerne i form av samtaler — noen ville ha noe, omtrent, og det ble opp til utviklerne å gjette seg til hvor grensene gikk. Gjetting betyr omarbeiding, og omarbeiding er omtrent den dyreste måten det går an å bygge noe på.</p>\n<p><strong>Oppgave.</strong> Rollen lå mellom forretningssiden og utviklingssiden og oversatte den ene til den andre: gjorde løse behov om til oppgaver en utvikler kunne ta fatt på uten å gjette, og holdt dem i tråd med standardene for datastyring underveis.</p>\n<p><strong>Handling.</strong> Kravene ble utarbeidet direkte med interessentene, med de kinkige spørsmålene stilt tidlig i stedet for at de ble oppdaget sent, og deretter skrevet ned som funksjoner og brukerhistorier som faktisk sa hva «ferdig» betydde. Hver av dem ble sjekket mot styringsreglene, fordi en funksjon som er nyttig, men som håndterer data feil, egentlig ikke er ferdig. Hele poenget var at noen skulle kunne lese en brukerhistorie og bygge det riktige på første forsøk.</p>\n<p><strong>Resultat.</strong> Utviklingen bygde på tydelige, omforente funksjoner i stedet for halvforståtte ønsker. Uklarheten gikk ned, omarbeidingen fulgte etter, og leveransene holdt seg rettet mot de faktiske forretningsmålene — innenfor rammene for datastyring i stedet for å bli ryddet til i etterkant for å passe inn.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Datastyring",
        "Dokumentasjon",
        "Interessenter og rapportering",
        "Produkt og krav",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Datastyring og datakvalitet",
        "Produktstrategi og krav",
        "Prosjektledelse (smidig)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/managed-the-execution-of-30-successful-gis-projects-32/",
      "url": "https://engineer.company/nb/portfolio/managed-the-execution-of-30-successful-gis-projects-32/",
      "title": "Styrte gjennomføringen av 30 vellykkede GIS‑prosjekter og viste lederskap i leveransen av innovative løsninger på tvers av fagfeltet.",
      "summary": "Leverte 30 vellykkede GIS-prosjekter — en historikk med jevne, nyskapende leveranser som ga varig tillit hos kundene.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet tjente pengene sine på å levere GIS‑prosjekter, og mange av dem gikk samtidig. Veksten hang på at de ble levert pålitelig — ikke ett flaggskipprosjekt gjennomført briljant, men en jevn strøm av prosjekter som kom i mål i tide og holdt sammen, noe som bare skjer når koordineringen på tvers av teamet faktisk fungerer.</p>\n<p><strong>Oppgave.</strong> Ansvaret var å få den porteføljen levert — holde omfang, folk og tidslinjer samkjørt på tvers av alt sammen, og gi teamet retningen det trengte for å levere.</p>\n<p><strong>Handling.</strong> I løpet av de årene gikk leveransen av tretti GIS‑prosjekter gjennom denne rollen. I praksis betydde det å holde omfanget stabilt når det ville krype, sette de riktige folkene på det riktige arbeidet og være tett nok på hvert prosjekt til å fange opp problemer mens de ennå var små. Når noe kom til å skli, var det bedre å vite det tidlig og omdisponere enn å oppdage det ved fristen. Mye av jobben gikk rett og slett ut på å holde alle ballene i luften og sørge for at kunden hadde realistiske forventninger til hva som kom, og når.</p>\n<p><strong>Resultat.</strong> Alle tretti kom i mål. Den stabiliteten betydde mer enn noe enkeltprosjekt kunne ha gjort — en kunde som har sett deg levere tretti ganger, bekymrer seg ikke for det trettiførste, og det omdømmet er en god del av grunnen til at selskapet fortsatte å vokse.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "GIS / geodata",
        "Interessenter og rapportering",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teamledelse",
        "GIS- og geodataløsninger",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/led-company-growth-from-4-to-14-employees-33/",
      "url": "https://engineer.company/nb/portfolio/led-company-growth-from-4-to-14-employees-33/",
      "title": "Ledet selskapets vekst fra 4 til 14 ansatte med smidige metoder og effektiv prosjektstyring.",
      "summary": "Ledet veksten i selskapet fra 4 til 14 ansatte med smidige metoder, og skalerte teamet uten å miste kvalitet eller samhold.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet var klart for å vokse, og akkurat det øyeblikket har en bestemt fare: man legger til folk raskere enn man legger til prosess, og både kvaliteten og den felles forståelsen av hvordan ting gjøres, begynner å slå sprekker. Fire personer som alle vet hva alle andre holder på med, er noe helt annet enn fjorten som ikke vet det.</p>\n<p><strong>Oppgave.</strong> Jobben var å lede den veksten — få folk inn og samtidig holde leveransene disiplinerte og få teamet til å dra i samme retning.</p>\n<p><strong>Handling.</strong> Selskapet gikk fra fire personer til fjorten. Det handlet om rekruttering og onboarding gjort med omtanke og ikke i panikk, men den største delen var å innføre arbeidsmåter som gjorde at et team av den størrelsen ikke snublet i sine egne bein — smidig praksis, ordentlig prosjektledelse, vanene som holder alles arbeid synlig for alle andre. Den tiende og den fjortende som ble ansatt, skulle komme inn i noe som allerede hadde en form, og ikke måtte gjette seg til hvordan ting ble gjort.</p>\n<p><strong>Resultat.</strong> Teamet kom opp i fjorten uten at kvaliteten falt eller at det splittet seg opp i folk som ikke visste hva de andre drev med. Det var den smidige siden av det som holdt en større gruppe produktiv — prosessen vokste med antall ansatte i stedet for å henge etter.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Mentorarbeid og coaching",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teamledelse",
        "Teknisk ledelse",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/improved-team-communication-and-collaboration-by-implementing-slack-34/",
      "url": "https://engineer.company/nb/portfolio/improved-team-communication-and-collaboration-by-implementing-slack-34/",
      "title": "Forbedret kommunikasjonen og samarbeidet i teamet ved å innføre Slack, Mattermost, 1Password og Jira, og sparte 8 000 arbeidstimer.",
      "summary": "Sparte ca. 8 000 timer ved å rulle ut Slack, Mattermost, 1Password og Jira, og gjorde jakt på informasjon og dobbeltarbeid om til reelle leveranser.",
      "content_html": "<p><strong>Situasjon.</strong> Etter hvert som teamet ble større, hadde ikke kommunikasjonen og verktøyene holdt tritt, og det merktes. Ting ble sagt ett sted og gikk hus forbi hos dem som trengte å vite det, arbeid ble gjort dobbelt fordi ingen kunne se hva andre allerede hadde gjort, og det tok lengre tid å koordinere noe som helst enn å gjøre selve arbeidet. Den typen friksjon er usynlig fra dag til dag, men den summerer seg til mye tapt tid.</p>\n<p><strong>Oppgave.</strong> Målet var å rette opp hvordan teamet kommuniserte og samarbeidet, og hente tilbake tiden som stille og rolig lakk ut i all den friksjonen.</p>\n<p><strong>Handling.</strong> Verktøyene ble innført og standardisert, og — dette er den delen som faktisk betyr noe — praksisen for hvordan de skulle brukes, ble lagt fast, slik at de ikke bare ble enda et sted å sjekke. Slack og Mattermost for kommunikasjon, 1Password slik at delte hemmeligheter ikke ble sendt rundt på måter ingen kunne spore, Jira slik at arbeidet ble fulgt opp på ett sted i stedet for å leve i hodene og innboksene til folk. Verktøyene var den enkle biten; å få alle til faktisk å bruke dem på samme måte var det egentlige arbeidet.</p>\n<p><strong>Resultat.</strong> Kommunikasjonen og samarbeidet ble merkbart bedre, og det strømlinjeformede oppsettet sparte noe i størrelsesorden 8 000 arbeidstimer — tid som hadde gått med til å jage etter informasjon og gjøre arbeid om igjen, og som nå gikk til faktiske leveranser.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Prosjektledelse",
        "Sikkerhet",
        "Smidig og Scrum",
        "Teamledelse",
        "Prosjektledelse (smidig)",
        "Sikkerhet og tilgangsstyring",
        "Teambygging og mentorarbeid"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/led-the-development-deployment-and-support-of-over-35/",
      "url": "https://engineer.company/nb/portfolio/led-the-development-deployment-and-support-of-over-35/",
      "title": "Ledet utvikling, produksjonssetting og støtte for over 30 GIS‑prosjekter, med ekspertise i teknologiene PostgreSQL, Bash, Python, JavaScript, GDAL, ArcGIS, PostGIS og Mapbox.",
      "summary": "Ledet utvikling, produksjonssetting og støtte for over 30 GIS-prosjekter med PostgreSQL, Python, GDAL, ArcGIS, PostGIS og Mapbox gjennom hele livsløpet.",
      "content_html": "<p><strong>Situasjon.</strong> Alt selskapet leverte, var skreddersydd GIS — egne kart- og geodatasystemer bygget for en kundes konkrete problem, og deretter holdt i drift etter at de var satt i produksjon. Å bygge løsningen er bare halve jobben; et geodataprosjekt som blir levert og så faller sammen i produksjon, er egentlig ikke levert.</p>\n<p><strong>Oppgave.</strong> Disse prosjektene gikk fra start til slutt gjennom denne rollen — utviklingen, produksjonssettingen og driftsstøtten etter lansering — med den tekniske retningen på tvers av en ganske bred teknologistakk.</p>\n<p><strong>Handling.</strong> Leveransen omfattet mer enn tretti GIS‑prosjekter, med praktisk arbeid i hele stakken hele veien. PostgreSQL med PostGIS i bunnen for de romlige dataene, GDAL/OGR for å flytte dem mellom formater — shapefiles, GeoJSON, GeoTIFF, KML, vektorfliser — og QGIS og JOSM for selve dataarbeidet. På frontsiden webkart bygget på Mapbox GL og Leaflet, noen ganger mot API‑ene til ArcGIS eller HERE, med applikasjonslaget i JavaScript, Python, PHP og SQL. Arbeidet spente fra digital kartlegging i 2D og 3D via LiDAR‑behandling, georektifisering og vektorisering til innendørs kartlegging og navigasjon. Og ansvaret strakk seg forbi selve leveransen — produksjonssettingen og den løpende driftsstøtten i produksjon var også en del av det, så problemer ble ikke sendt videre til andre; beslutningene måtte man leve med selv.</p>\n<p><strong>Resultat.</strong> Over tretti prosjekter bygget, produksjonssatt og fulgt opp i drift på tvers av hele det spekteret. Å stå ansvarlig for hele livssyklusen og ikke bare byggingen var det som holdt kvaliteten ærlig — du designer annerledes når du vet at det er du som blir ringt opp hvis noe ryker.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Data engineering",
        "Databaser",
        "Fullstack‑utvikling",
        "GIS / geodata",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Python",
        "SQL",
        "Teamledelse",
        "Teknisk ledelse",
        "Webutvikling",
        "Backend- og API‑utvikling",
        "Fullstack‑produktutvikling",
        "GIS- og geodataløsninger",
        "Teknisk ledelse og rådgivning",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/contributed-to-user-interface-design-processes-ensuring-intuitive-36/",
      "url": "https://engineer.company/nb/portfolio/contributed-to-user-interface-design-processes-ensuring-intuitive-36/",
      "title": "Bidro i designprosessene for brukergrensesnitt og sikret intuitive, visuelt tiltalende og brukervennlige grensesnitt i prosjektene.",
      "summary": "Bidro til UI-design og leverte intuitive, gjennomarbeidede og brukervennlige grensesnitt ved å stille spørsmål om brukervennlighet under designarbeidet.",
      "content_html": "<p><strong>Situasjon.</strong> Grensesnittene på tvers av prosjektene var ujevne. Noen var greie, noen var tydelig bygget av utviklere som tenkte på datamodellen og ikke på personen som skulle bruke dem, og designbeslutningene ble ikke alltid tatt med sluttbrukeren i rommet. Særlig i et webkartprodukt er kartet den enkle delen — det er kontrollene, filtreringen og flyten rundt det som får folk til å gå seg vill.</p>\n<p><strong>Oppgave.</strong> Å bidra i UI‑designprosessen var en del av rollen — for å gjøre grensesnittene til noe folk faktisk opplevde som intuitivt og behagelig å bruke, ikke bare funksjonelt.</p>\n<p><strong>Handling.</strong> Rollen var ikke designerens, men den satt i designprosessen og tok med seg et utviklerperspektiv — presset på oppsettet, på flyten gjennom en oppgave, på om et skjermbilde faktisk var tydelig eller bare kjent for dem som hadde bygget det. Dette var React-, Angular- og Vue‑frontender oppå Mapbox- og Leaflet‑kart, og mye av brukervennligheten lå i detaljene: hvordan du filtrerte et datasett, hvordan du flyttet deg mellom etasjer på et innendørskart, om løsningen fortalte deg hva den holdt på med. Stort sett betydde det å stille de dumme brukerspørsmålene tidlig, mens de fortsatt var billige å rette opp, i stedet for etter lansering, når forvirringen kom tilbake som saker hos brukerstøtten.</p>\n<p><strong>Resultat.</strong> Grensesnittene ble mer intuitive og mer polerte, noe sluttbrukerne merket direkte, og det løftet den samlede kvaliteten på det som ble levert. At spørsmålene om brukervennlighet ble stilt under designarbeidet og ikke etter lansering, er det meste av det som utgjorde forskjellen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "GIS / geodata",
        "UX/UI‑design",
        "Webutvikling",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/guided-the-execution-of-numerous-company-projects-offering-37/",
      "url": "https://engineer.company/nb/portfolio/guided-the-execution-of-numerous-company-projects-offering-37/",
      "title": "Veiledet gjennomføringen av en rekke prosjekter i selskapet og ga ekspertstøtte i programvareutviklingens faser.",
      "summary": "Veiledet gjennomføringen av mange av selskapets prosjekter med ekspertstøtte i de vanskeligste fasene av programvareutviklingen, og stabiliserte leveransene.",
      "content_html": "<p><strong>Situasjon.</strong> Mange prosjekter gikk samtidig, hvert midt i utviklingsfasen, og alle trengte jevn teknisk veiledning for å holde seg på sporet. Overlatt til seg selv driver prosjekter ut av kurs — en feil tilnærming valgt tidlig blir dyr innen noen oppdager den.</p>\n<p><strong>Oppgave.</strong> Jobben var å veilede den gjennomføringen — være den tekniske støtten teamene kunne lene seg på gjennom utviklingsfasene.</p>\n<p><strong>Handling.</strong> Arbeidet holdt seg praktisk og tett på mange av selskapets prosjekter samtidig. Det betydde å hjelpe folk videre når de satt fast, å se kritisk på en tilnærming før for mye ble bygget oppå den, og generelt å være tett nok på til å oppdage et prosjekt som var på vei i feil retning mens det ennå var en kurskorrigering og ikke en ombygging. Tanken var å være tilgjengelig snarere enn en portvokter — å holde ting i bevegelse, ikke få alt til å vente på én person.</p>\n<p><strong>Resultat.</strong> Prosjektene kom lettere gjennom de vanskelige utviklingsfasene med en erfaren person å lene seg på i de riktige øyeblikkene, og det gjorde leveransene stødigere på tvers av hele selskapets portefølje.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Mentorarbeid og coaching",
        "Prosjektledelse",
        "Teamledelse",
        "Teknisk ledelse",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/overhauled-internal-processes-saving-8-000-hours-by-38/",
      "url": "https://engineer.company/nb/portfolio/overhauled-internal-processes-saving-8-000-hours-by-38/",
      "title": "Fornyet interne prosesser og sparte 8 000 timer ved å forbedre programvarearkitekturen, systemene og effektiviteten i planleggingen.",
      "summary": "Fornyet interne prosesser og sparte ca. 8 000 timer ved å effektivisere programvarearkitektur, systemer og planlegging.",
      "content_html": "<p><strong>Situasjon.</strong> Måten ting ble gjort på internt, hadde samlet opp det vanlige rusket — programvarearkitektur som hadde vokst lag på lag og ikke etter design, systemer som fungerte, men ikke effektivt, og planlegging som enten lot folk vente eller druknet dem i arbeid. Ingenting av det sto i brann, og det var nettopp derfor det hadde fått ligge, men det kostet i det stille mye tid.</p>\n<p><strong>Oppgave.</strong> Målet var en grundig overhaling av disse prosessene — å finne sløsingen og fjerne den i stedet for å fortsette å betale for den.</p>\n<p><strong>Handling.</strong> De interne prosessene ble omarbeidet på tre fronter: programvarearkitekturen, slik at den ble noe man kunne resonnere om og bygge videre på i stedet for å jobbe seg rundt; systemene, strømlinjeformet slik at rutinearbeidet sluttet å ta lengre tid enn det burde; og planleggingen, slik at kapasiteten faktisk ble tilpasset arbeidet. Og endringene ble gjort varige — bygget inn i hvordan teamet jobbet og ikke etterlatt som et notat alle nikket til og glemte — fordi prosessforbedringer som ikke gjøres permanente, bare forfaller tilbake til gamle vaner.</p>\n<p><strong>Resultat.</strong> Overhalingen sparte omtrent 8 000 timer ved å gjøre arkitekturen, systemene og planleggingen merkbart mer effektive. Det er kapasitet som gikk rett tilbake til mer verdifullt arbeid i stedet for til ekstraarbeid ingen hadde tenkt på å stille spørsmål ved.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Løsningsarkitektur",
        "Plattformarkitektur",
        "Prosjektledelse",
        "Teknisk ledelse",
        "Ytelsesoptimalisering",
        "DevOps og CI/CD‑automatisering",
        "Plattform- og løsningsarkitektur",
        "Prosjektledelse (smidig)",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/administered-network-infrastructure-for-over-1-000-servers-39/",
      "url": "https://engineer.company/nb/portfolio/administered-network-infrastructure-for-over-1-000-servers-39/",
      "title": "Administrerte nettverksinfrastrukturen for over 1 000 servere og sikret optimal produksjonssetting, sikkerhet og feilsøking av systemene.",
      "summary": "Administrerte nettverksinfrastrukturen for over 1 000 servere og sikret pålitelig produksjonssetting, solid sikkerhet og rask feilsøking.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapets drift hvilte på en stor serverpark — over tusen servere — og en park av den størrelsen holder seg ikke pålitelig av seg selv. Produksjonssetting, sikkerhet og den jevne strømmen av ting som går galt trenger alle noen som drifter dem med reell disiplin, ellers blir det hele ustabilt og ingen er helt sikre på hvorfor.</p>\n<p><strong>Oppgave.</strong> Jobben var å administrere den nettverksinfrastrukturen — holde den sikker, holde den pålitelig, holde den konsistent i en skala der det er nettopp inkonsistens som feller deg.</p>\n<p><strong>Handling.</strong> Nettverksinfrastrukturen strakte seg over mer enn tusen servere. Måten systemene ble produksjonssatt på, ble standardisert, slik at en server kom opp på den samme forutsigbare måten i stedet for at hver enkelt var litt spesiallaget; sikkerheten ble herdet i stedet for å stole på at ingen ville komme og snoke; og feilsøkingen ble tatt hånd om når noe faktisk gikk i stykker. I den skalaen er det standardiseringen som redder deg — tusen unike snøfnugg er uhåndterlig, tusen eksemplarer av det samme er bare arbeid.</p>\n<p><strong>Resultat.</strong> Serverparken gikk med pålitelig produksjonssetting, solid sikkerhet og problemer som ble håndtert raskt i stedet for å bli liggende og verke. Det er den typen infrastrukturarbeid som er usynlig når det går bra, og det er hele poenget — det var den stabile ryggraden alt annet i selskapet kjørte på.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Systemadministrasjon",
        "Nettverk og VPN‑oppsett",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/established-a-technical-support-department-servicing-over-10-40/",
      "url": "https://engineer.company/nb/portfolio/established-a-technical-support-department-servicing-over-10-40/",
      "title": "Etablerte en avdeling for teknisk støtte som betjente over 10 000 kunder med IT‑støtte og feilsøking, og fulgte opp kundehenvendelsene i et CRM‑system.",
      "summary": "Etablerte en avdeling for teknisk støtte som betjente over 10 000 kunder, og gjorde støtten om fra reaktiv feilretting til en skalerbar styrke for selskapet.",
      "content_html": "<p><strong>Situasjon.</strong> Kundebasen vokste og trengte pålitelig teknisk brukerstøtte, og det fantes rett og slett ingen egen enhet som kunne gi den i noen reell skala. Brukerstøtten skjedde ad hoc, noe som fungerer for en håndfull kunder og stille og rolig faller fra hverandre etter hvert som tallene stiger.</p>\n<p><strong>Oppgave.</strong> Jobben var å etablere en reell teknisk brukerstøtte — en som kunne betjene en stor og fortsatt voksende kundebase.</p>\n<p><strong>Handling.</strong> En avdeling for teknisk brukerstøtte ble bygget opp fra ingenting. Det betydde å bestemme hvordan den faktisk skulle fungere før noen ble ansatt der — prosessen for hvordan en henvendelse kom inn og ble løst, verktøyene for å håndtere volumet, standarden for hvordan god brukerstøtte så ut — og deretter bygge opp kapasiteten til å levere IT‑støtte og feilsøking til mange kunder samtidig. Å starte fra bunnen av var ærlig talt en fordel; avdelingen kunne utformes for den skalaen selskapet var på vei mot, i stedet for å lappe på noe som hadde vokst fram tilfeldig.</p>\n<p><strong>Resultat.</strong> Avdelingen endte opp med å betjene over ti tusen kunder med pålitelig brukerstøtte og feilsøking. Brukerstøtten gikk fra å være noe som ble gjort reaktivt til å bli en reell styrke for selskapet — noe som skalerte med kundebasen i stedet for å knekke sammen under den.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Interessenter og rapportering",
        "Mentorarbeid og coaching",
        "Produkt og krav",
        "Prosjektledelse",
        "Systemadministrasjon",
        "Teamledelse",
        "IT‑støtte og helpdesk",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/automated-ssl-tls-certificate-creation-for-100-docker-41/",
      "url": "https://engineer.company/nb/portfolio/automated-ssl-tls-certificate-creation-for-100-docker-41/",
      "title": "Automatiserte opprettelsen av SSL/TLS‑sertifikater for 100 Docker‑applikasjoner og sikret trygge forbindelser på tvers av Ubuntu Linux‑verter.",
      "summary": "Automatiserte SSL/TLS-sertifikater for 100 Docker-apper på Ubuntu Linux og gjorde slutt på manuell fornyelse og nedetiden utløpte sertifikater gir.",
      "content_html": "<p><strong>Situasjon.</strong> Hundre Docker‑applikasjoner trengte alle SSL/TLS‑sertifikater, og sertifikater er den typen ting som er i orden helt til de ikke er det. Å utstede og fornye hundre av dem for hånd er tregt, det er kjedelig, og det er akkurat den typen manuell jobb der én glemt fornyelse tar ned en app med en utløpsfeil i verst tenkelige øyeblikk.</p>\n<p><strong>Oppgave.</strong> Målet var automatisk opprettelse og fornyelse av sertifikater — alle apper med gyldig, klarert kryptering, og ingen som måtte huske å gjøre noe.</p>\n<p><strong>Handling.</strong> En ACME‑basert arbeidsflyt tok seg av hele livssyklusen for de hundre Docker‑applikasjonene — opprettet sertifikatene og fornyet dem før de gikk ut — og distribuerte dem automatisk til vertene med Ubuntu Linux, som kjørte en blanding av Apache og Nginx. Hele poenget var å ta mennesket ut av ligningen, fordi mennesket er den delen som glemmer.</p>\n<p><strong>Resultat.</strong> Alle de hundre applikasjonene beholdt gyldige sertifikater og sikre tilkoblinger helt av seg selv. Det manuelle sertifikatarbeidet forsvant rett og slett, og med det hele kategorien driftsavbrudd der noe ryker, ikke fordi det sviktet, men fordi et sertifikat i det stille gikk ut uten at noen la merke til det.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Containere (Docker/Kubernetes)",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Containerisering og orkestrering",
        "DevOps og CI/CD‑automatisering",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/",
      "url": "https://engineer.company/nb/portfolio/streamlined-ci-cd-processes-saving-4-000-hours-42/",
      "title": "Effektiviserte CI/CD‑prosessene og sparte 4 000 timer ved å innføre automatisering i pipelines for programvareutvikling.",
      "summary": "Effektiviserte CI/CD og sparte ca. 4 000 timer med raskere og mer pålitelige utgivelser, slik at teamet kunne levere med trygghet.",
      "content_html": "<p><strong>Situasjon.</strong> Å få programvare ut døren hvilte på manuelle, inkonsistente steg — noen som husket rekkefølgen og gjorde det litt annerledes hver gang — og det bremset utgivelsene og spiste utviklertimer som burde ha gått til å bygge ting.</p>\n<p><strong>Oppgave.</strong> Målet var å strømlinjeforme CI/CD‑prosessen og få automatisering inn i leveransepipelinene, slik at utgivelser sluttet å være et manuelt ritual.</p>\n<p><strong>Handling.</strong> Automatiserte leveransepipeliner for bygging, testing og produksjonssetting kom på plass, slik at veien fra en endring til at den kjørte i produksjon, ble standardisert i stedet for improvisert. De repeterende manuelle stegene — de som var trege og, enda verre, ble gjort ulikt avhengig av hvem som gjorde dem — ble fjernet. Når leveransepipelinen gjør det på samme måte hver gang, slutter en hel klasse av «det virket på maskinen min» og halvglemte steg i produksjonssettingen rett og slett å skje.</p>\n<p><strong>Resultat.</strong> Automatiseringen ga tilbake omtrent 4 000 timer og gjorde utgivelsene både raskere og mer pålitelige. Teamet kunne levere uten å holde pusten — tryggheten kom av at prosessen var konsistent, ikke av at alle var forsiktige.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Pålitelighet og sikkerhetskopier",
        "Teknisk ledelse",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/streamlined-data-analysis-and-software-development-processes-saving-43/",
      "url": "https://engineer.company/nb/portfolio/streamlined-data-analysis-and-software-development-processes-saving-43/",
      "title": "Effektiviserte prosessene for dataanalyse og programvareutvikling og sparte 4 000 timer ved å innføre CI/CD‑praksis med GitHub, GitLab, Bash og Python.",
      "summary": "Sparte ca. 4 000 timer ved å innføre CI/CD med GitHub, GitLab, Bash og Python, som gjorde dataanalyse og utvikling raskere og mer konsistent.",
      "content_html": "<p><strong>Situasjon.</strong> Både dataanalysen og programvareutviklingen ble holdt tilbake av det samme: manuelle prosesser. Arbeidet gikk fra utvikling til leveranse på en treg og inkonsistent måte, og analysesiden hadde sin egen haug med repeterende steg som noen gjorde for hånd hver gang.</p>\n<p><strong>Oppgave.</strong> Målet var å strømlinjeforme begge deler ved å innføre moderne automatisering og CI/CD‑praksis i arbeidsflyter som ikke hadde hatt det.</p>\n<p><strong>Handling.</strong> CI/CD‑praksis kom på plass, bygget på GitHub og GitLab, med Bash og Python til automatiseringsarbeidet i bunnen. De repeterende stegene i arbeidsflytene for både dataanalyse og utvikling ble automatisert, og måten arbeidet gikk fra utvikling til leveranse på, ble standardisert slik at den var lik hver gang i stedet for å bli funnet opp på nytt for hvert prosjekt. Å få analysesiden inn i den samme disiplinerte arbeidsflyten som utviklingsarbeidet var en stor del av det — den hadde blitt behandlet som en egen, mer manuell verden.</p>\n<p><strong>Resultat.</strong> De strømlinjeformede prosessene sparte omtrent 4 000 timer og gjorde både dataanalysen og programvareutviklingen raskere, og like nyttig: det som ble levert, ble mer konsistent — færre overraskelser fra arbeid som var blitt gjort på en litt annen måte hver gang.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Dataanalyse",
        "Datapipelines (ETL/ELT)",
        "DevOps",
        "Python",
        "Dataanalyse og BI‑dashbord",
        "DevOps og CI/CD‑automatisering",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/developed-a-data-analytics-reporting-system-increasing-quarterly-44/",
      "url": "https://engineer.company/nb/portfolio/developed-a-data-analytics-reporting-system-increasing-quarterly-44/",
      "title": "Utviklet et rapporteringssystem for dataanalyse og økte kvartalsinntektene fra programvaren med 400 % gjennom Python‑baserte PDF‑rapporter.",
      "summary": "Bygde et rapporteringssystem for dataanalyse i Python som økte den kvartalsvise programvareomsetningen med 400 % med tydelige PDF-rapporter i rett tid.",
      "content_html": "<p><strong>Situasjon.</strong> Interessentene fikk ikke analyser i noen rettidig, lesbar form. Dataene fantes, men å gjøre dem om til noe man faktisk kunne ta en beslutning på, var tregt og manuelt, så oversikten over hvordan ting gikk, hang etter, og de kommersielle beslutningene hang etter sammen med den.</p>\n<p><strong>Oppgave.</strong> Jobben var å bygge et rapporteringssystem for dataanalyse — et som gjorde rådata om til tydelig, jevnlig innsikt uten at noen måtte sette det sammen for hånd hver gang.</p>\n<p><strong>Handling.</strong> Et rapporteringssystem genererte PDF‑rapporter i Python og automatiserte hele kjeden: hente dataene, kjøre analysen og presentere den i et ryddig, konsistent format som interessentene faktisk kunne lese. Poenget var regelmessighet og klarhet — den samme profesjonelle rapporten som kom forutsigbart, slik at tallene ble noe folk så på som en selvfølge, ikke noe de måtte gå og grave etter.</p>\n<p><strong>Resultat.</strong> Det er den rapporteringen som drev de kvartalsvise programvareinntektene opp med 400 %. Å gjøre analysene bedre og raskere var ikke en hyggelig ekstra for støttefunksjonene — legg tydelige, rettidige tall foran dem som tar de kommersielle beslutningene, så blir beslutningene bedre, og her slo det direkte ut i inntektene.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "Data engineering",
        "Dataanalyse",
        "Interessenter og rapportering",
        "Produkt og krav",
        "Python",
        "Backend- og API‑utvikling",
        "Dataanalyse og BI‑dashbord",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/automated-data-processing-tasks-using-shell-scripting-pl-45/",
      "url": "https://engineer.company/nb/portfolio/automated-data-processing-tasks-using-shell-scripting-pl-45/",
      "title": "Automatiserte databehandlingsoppgaver med Shell‑skripting, PL/pgSQL, Python og Transact‑SQL, og økte produktiviteten og effektiviteten.",
      "summary": "Automatiserte databehandlingsoppgaver med Shell, PL/pgSQL, Python og Transact-SQL, økte produktiviteten og gjorde slutt på små, gjentakende feil.",
      "content_html": "<p><strong>Situasjon.</strong> Det var en jevn mengde gjentakende databehandling som ble gjort for hånd. Manuelt dataarbeid har to problemer på en gang: det spiser tid, og det er inkonsistent — gjør den samme oppgaven for hånd mange nok ganger, så blir den gjort litt ulikt, og noen av de ulikhetene er feil.</p>\n<p><strong>Oppgave.</strong> Målet var å automatisere disse oppgavene, både for å få tiden tilbake og for å gjøre dem pålitelige.</p>\n<p><strong>Handling.</strong> Databehandlingen ble automatisert på tvers av databasene og systemene den berørte, med det som passet til jobben — Shell‑skripting som lim, PL/pgSQL og Transact‑SQL nede i databasene, Python der det trengtes mer enn SQL kunne gi. Manuelle steg ble erstattet med jobber som kjørte på samme måte hver gang, og det er hele poenget: et skript kjeder seg ikke, hopper ikke over et steg og gjør det ikke annerledes en fredag ettermiddag.</p>\n<p><strong>Resultat.</strong> Både produktiviteten og effektiviteten gikk opp, folk slapp det manuelle arbeidet, og databehandlingen ble konsistent og pålitelig i stedet for en kilde til små, gjentakende feil.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "PostgreSQL",
        "Python",
        "SQL",
        "Databaseadministrasjon (DBA)",
        "DevOps og CI/CD‑automatisering",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/enhanced-project-efficiency-saving-150-hours-per-month-46/",
      "url": "https://engineer.company/nb/portfolio/enhanced-project-efficiency-saving-150-hours-per-month-46/",
      "title": "Økte effektiviteten i prosjektene og sparte 150 timer i måneden fordelt på 30 prosjekter ved å optimalisere arbeidsflyter og ressursstyring.",
      "summary": "Sparte ca. 150 timer i måneden fordelt på 30 prosjekter ved å optimalisere arbeidsflyter og ressursstyring — en løpende gevinst for hele porteføljen.",
      "content_html": "<p><strong>Situasjon.</strong> I en portefølje på rundt tretti prosjekter gikk det tapt tid hver eneste måned på arbeidsflyter som aldri var blitt optimalisert, og på ressursstyring som var ujevn — noen underutnyttet, noen overbelastet, arbeid planlagt ulikt fra det ene prosjektet til det neste.</p>\n<p><strong>Oppgave.</strong> Målet var å gjøre porteføljen mer effektiv og hente inn igjen tiden som forsvant måned etter måned.</p>\n<p><strong>Handling.</strong> Gjennom de tretti prosjektene ble arbeidsflytene og ressursstyringen tatt tak i samlet — flaskehalsene ble fjernet, arbeidslasten ble balansert slik at det ikke alltid var de samme få personene som var begrensningen, og måten arbeidet ble planlagt og gjennomført på, ble standardisert slik at hvert prosjekt ikke var sitt eget spesialtilfelle. Tretti prosjekter som hver taper litt tid, blir mye til sammen; løsningen handlet mest om å gjøre den gode praksisen konsistent, ikke om å finne opp noe smart.</p>\n<p><strong>Resultat.</strong> Endringene sparte rundt 150 timer i måneden på tvers av porteføljen. Det er en gjentakende månedlig besparelse, ikke en engangsgevinst — tretti prosjekter som drives slankere, måned etter måned.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teamledelse",
        "Ytelsesoptimalisering",
        "Prosjektledelse (smidig)",
        "Teambygging og mentorarbeid"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/managed-a-team-delivering-it-support-data-recovery-47/",
      "url": "https://engineer.company/nb/portfolio/managed-a-team-delivering-it-support-data-recovery-47/",
      "title": "Ledet et team som leverte IT‑støtte, datagjenoppretting og maskinvarereparasjoner til over 1 000 kunder, og sikret høy servicekvalitet.",
      "summary": "Ledet et team som leverte IT-støtte, datagjenoppretting og maskinvarereparasjoner til over 1 000 kunder, og bygde et rykte for pålitelig service.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet var stedet en stor kundebase vendte seg til når IT‑en deres sviktet — brukerstøtte, datagjenoppretting, maskinvarereparasjon, hele spekteret. Å dekke den typen jevn, uglamorøs etterspørsel handler ikke om heltedåder; det handler om å ha et team som fungerer godt dag etter dag, fordi arbeidet aldri egentlig slutter å komme.</p>\n<p><strong>Oppgave.</strong> Jobben var å lede teamet som leverte alt dette, og å holde kvaliteten jevn enten det var en rolig uke eller alt kom på en gang.</p>\n<p><strong>Handling.</strong> Teamet sto for IT‑støtte, datagjenoppretting og maskinvarereparasjon for over tusen kunder. Mye av det var organiseringen under — å sørge for at arbeid ble plukket opp og ikke glemt, å sette en standard for hva «fikset» faktisk betydde, så folk ikke fikk halve reparasjoner tilbake, og å holde teamet effektivt når køen var lang. Særlig datagjenoppretting er arbeid man ikke kan ta lett på; det er som regel noens bilder eller hele virksomheten deres som ligger på den disken, og de har allerede en dårlig dag når de kommer til deg.</p>\n<p><strong>Resultat.</strong> Teamet betjente over tusen kunder og bygde opp et reelt rykte for pålitelig service. I den typen virksomhet er ryktet alt — folk kommer tilbake, og de forteller det til andre, nettopp fordi du faktisk ordnet det sist gang noe gikk i stykker.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Interessenter og rapportering",
        "Mentorarbeid og coaching",
        "Systemadministrasjon",
        "Teamledelse",
        "IT‑støtte og helpdesk",
        "Maskinvarereparasjon og dataredning",
        "Teambygging og mentorarbeid"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/configured-and-deployed-1-000-wi-fi-routers-48/",
      "url": "https://engineer.company/nb/portfolio/configured-and-deployed-1-000-wi-fi-routers-48/",
      "title": "Konfigurerte og installerte 1 000 Wi‑Fi‑rutere og forbedret nettverkstilgangen og ytelsen for kundene.",
      "summary": "Konfigurerte og rullet ut 1 000 Wi-Fi-rutere med et standardoppsett, slik at kundene fikk pålitelig trådløs tilgang og ytelse.",
      "content_html": "<p><strong>Situasjon.</strong> Kundene trengte trådløst nett som bare fungerte, og det kokte ned til å konfigurere og rulle ut et stort antall Wi‑Fi‑rutere — og å gjøre det på den samme nøye måten hver gang, fordi en ruter som er satt opp slurvete, enten er usikker eller treg, og som regel finner man først senere ut hvilken av delene.</p>\n<p><strong>Oppgave.</strong> Jobben var å konfigurere og utplassere disse ruterne for å gi kundene bedre nettverkstilgang og ytelse.</p>\n<p><strong>Handling.</strong> Tusen Wi‑Fi‑rutere ble konfigurert og utplassert. Trikset ved det antallet er å standardisere oppsettet — en konsistent, sikker og fornuftig konfigurasjon — i stedet for å finjustere hver enkelt fra bunnen av der og da, fordi tusen håndlagde rutere er tusen forskjellige ting å følge opp senere. Derfor ble de satt opp for sikkerhet og ytelse på samme måte hver gang og rullet ut pålitelig hos kundene.</p>\n<p><strong>Resultat.</strong> De tusen ruterne ga kundene pålitelig trådløst nett — bedre tilgang, bedre ytelse — og gjorde det konsistent, fordi oppsettet var standardisert og ikke improvisert. En ruter ingen trenger å tenke på igjen, er målet; de fleste av disse kom dit.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Systemadministrasjon",
        "IT‑støtte og helpdesk",
        "Nettverk og VPN‑oppsett"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/managed-4-000-computer-repairs-ensuring-rapid-and-49/",
      "url": "https://engineer.company/nb/portfolio/managed-4-000-computer-repairs-ensuring-rapid-and-49/",
      "title": "Styrte 4 000 datamaskinreparasjoner og sikret rask og effektiv løsning av maskinvare- og programvareproblemer.",
      "summary": "Håndterte 4 000 datamaskinreparasjoner og løste maskinvare- og programvareproblemer raskt og effektivt, og holdt ryktet for pålitelig service i hevd.",
      "content_html": "<p><strong>Situasjon.</strong> Det kom en konstant strøm av datamaskiner til reparasjon — fire tusen av dem over tid — og hver eneste var noen som ventet på å få maskinen sin tilbake og komme videre med dagen. Maskinvarefeil, programvarefeil, hele blandingen, og kundene vurderte verkstedet etter hvor raskt og hvor ordentlig de ble fikset.</p>\n<p><strong>Oppgave.</strong> Jobben var å styre disse reparasjonene — sørge for at problemene fortsatte å bli løst raskt og, like viktig, riktig.</p>\n<p><strong>Handling.</strong> Fire tusen datamaskinreparasjoner gikk gjennom — diagnostisering av maskinvare- og programvarefeil, en arbeidsflyt organisert slik at maskinene gikk videre i stedet for å hope seg opp, og en kvalitet som ble holdt oppe, slik at en reparasjon faktisk ble gjort riktig i stedet for å bli sendt ut igjen for å svikte på nytt uken etter. En reparasjon som kommer tilbake, er verre enn en treg en; den koster kunden en ekstra tur og koster verkstedet tilliten. Gjennomstrømningen betydde derfor noe, men aldri på bekostning av å få det riktig første gang.</p>\n<p><strong>Resultat.</strong> Alle fire tusen ble løst, og raskt. Folk fikk maskinene sine tilbake i orden og kom videre, og selskapets rykte for pålitelig service holdt seg — noe som for et reparasjonsverksted er hele forretningen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Systemadministrasjon",
        "IT‑støtte og helpdesk",
        "Maskinvarereparasjon og dataredning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/administered-100-bare-bone-servers-physical-networks-and-50/",
      "url": "https://engineer.company/nb/portfolio/administered-100-bare-bone-servers-physical-networks-and-50/",
      "title": "Administrerte 100 Bare Bone‑servere, fysiske nettverk og IP‑telefonisystemer, og sikret en robust infrastruktur for selskapets vekst.",
      "summary": "Administrerte 100 bare-bone-servere, fysiske nettverk og IP-telefoni, og ga selskapet det robuste fundamentet det vokste på.",
      "content_html": "<p><strong>Situasjon.</strong> Under alt selskapet gjorde, lå det fysiske — servere, selve nettverkene, IP‑telefonien — og alt sammen måtte bare gå. Det laget er usynlig når det fungerer og svært synlig i det øyeblikket det ikke gjør det, og selskapets vekst hvilte på at det forble pålitelig.</p>\n<p><strong>Oppgave.</strong> Jobben var å administrere den infrastrukturen og holde den stabil mens selskapet vokste.</p>\n<p><strong>Handling.</strong> Hundre barebone‑servere, sammen med de fysiske nettverkene og IP‑telefonisystemene, ble tatt hånd om — oppsettet, vedlikeholdet, feilsøkingen når noe gikk galt. Barebone‑servere betyr å jobbe direkte med maskinvaren, så det er en praktisk, fysisk side ved det: kablingen, boksene, telefonsystemet som alle legger merke til i samme sekund en samtale brytes. Jobben var å holde alt sammen kjedelig, i god forstand.</p>\n<p><strong>Resultat.</strong> Serverne, nettverkene og telefonien gikk stabilt, og det pålitelige fysiske fundamentet er det som lot selskapet fortsette å vokse uten at bakken forskjøv seg under det.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Systemadministrasjon",
        "Nettverk og VPN‑oppsett"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/developed-100-web-applications-using-html-html5-css-51/",
      "url": "https://engineer.company/nb/portfolio/developed-100-web-applications-using-html-html5-css-51/",
      "title": "Utviklet 100 webapplikasjoner med HTML/HTML5, CSS/SCSS og rammeverkene Django, WordPress og Joomla, og sikret en variert tilstedeværelse på nett.",
      "summary": "Utviklet 100 webapplikasjoner med HTML5, CSS/SCSS, Django, WordPress og Joomla, og tilpasset hver løsning kundens reelle behov.",
      "content_html": "<p><strong>Situasjon.</strong> Kundene ville ha webapplikasjoner, og de ville ha svært forskjellige ting — ulik skala, ulike budsjetter, ulike nivåer av «bare få meg på nett» kontra «bygg noe skreddersydd til meg». Å møte det krevde allsidighet i stedet for å tvinge alle kunder inn i den samme teknologien.</p>\n<p><strong>Oppgave.</strong> Jobben var å bygge webapplikasjoner som ga hver kunde en variert og effektiv tilstedeværelse på nett.</p>\n<p><strong>Handling.</strong> Hundre webapplikasjoner ble bygget, og poenget var å velge verktøy etter jobben i stedet for å ha en favoritt. Der en kunde trengte noe skreddersydd, ble det HTML/HTML5, CSS/SCSS og Django; der de trengte noe mer standard som de også kunne administrere selv, var WordPress eller Joomla det raskere og mer fornuftige svaret. En del av ferdigheten var å vite hva som var hva — å få en kunde til å droppe en skreddersydd løsning de ikke trengte, eller til å velge en de trengte.</p>\n<p><strong>Resultat.</strong> De hundre applikasjonene ga kundene en variert og solid tilstedeværelse på nett, hver levert på den teknologien som faktisk passet. Å tilpasse tilnærmingen til kunden og ikke omvendt er det som gjorde dem effektive, ikke bare levert.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Python",
        "Webutvikling",
        "Fullstack‑produktutvikling",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-30-websites-delivering-unique-and-flexible-solutions-52/",
      "url": "https://engineer.company/nb/portfolio/designed-30-websites-delivering-unique-and-flexible-solutions-52/",
      "title": "Designet 30 nettsteder og leverte unike og fleksible løsninger ved å konvertere Photoshop‑design til HTML.",
      "summary": "Designet 30 nettsteder ved å konvertere Photoshop-design til HTML — særpregede, gjennomarbeidede og lette å vedlikeholde.",
      "content_html": "<p><strong>Situasjon.</strong> Kundene kom med et uttrykk de ønsket seg — ofte et ferdig visuelt design — og trengte et nettsted bygget trofast etter det. Gapet mellom en designfil og et fungerende nettsted er der mye kvalitet vinnes eller tapes: det er lett å levere noe som er omtrent riktig og subtilt feil.</p>\n<p><strong>Oppgave.</strong> Jobben var å designe nettstedene og gjøre designene om til nøyaktige, fleksible implementasjoner.</p>\n<p><strong>Handling.</strong> Tretti nettsteder ble designet, og Photoshop‑designene ble gjort om til HTML for hånd. Troskap mot designet var standarden — avstandene, typografien, detaljene designeren faktisk hadde ment, ikke en tilnærming til dem — men det var fleksibilitet også, fordi et nettsted som samsvarer med skissen piksel for piksel og så faller sammen i det øyeblikket innholdet endres, egentlig ikke er godt bygget. Målet var derfor HTML‑kode som holdt seg tro mot designet og forble vedlikeholdbar etterpå.</p>\n<p><strong>Resultat.</strong> De tretti nettstedene samsvarte med designene sine og forble lette å jobbe med — særpregede, polerte nettsteder som ikke gikk i stykker første gang noen redigerte dem. Tro mot designet og fortsatt vedlikeholdbar er balansen som betydde noe, og det er der disse havnet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "UX/UI‑design",
        "Webutvikling",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/administered-40-websites-on-ubuntu-linux-hosting-servers-53/",
      "url": "https://engineer.company/nb/portfolio/administered-40-websites-on-ubuntu-linux-hosting-servers-53/",
      "title": "Administrerte 40 nettsteder på Ubuntu Linux‑servere for webhotell med Apache og Nginx, og sikret høy tilgjengelighet og ytelse.",
      "summary": "Administrerte 40 nettsteder på Ubuntu Linux med Apache og Nginx, med høy tilgjengelighet og ytelse — hosting kundene aldri trengte å tenke på.",
      "content_html": "<p><strong>Situasjon.</strong> Det var en portefølje av nettsteder i drift som måtte holde seg oppe og holde seg raske — og hosting er nok en av de jobbene som er usynlige helt til et nettsted går ned, og da er det det eneste noen bryr seg om.</p>\n<p><strong>Oppgave.</strong> Jobben var å administrere disse nettstedene og holde dem raske og med høy tilgjengelighet.</p>\n<p><strong>Handling.</strong> Førti nettsteder kjørte på hostingservere med Ubuntu Linux, på en blanding av Apache og Nginx — konfigurasjonen, ytelsesjusteringen, det løpende vedlikeholdet for å holde dem pålitelige under reell trafikk. Reell trafikk er stikkordet: et nettsted som går fint når ingen bruker det, og faller sammen når noen gjør det, har ikke blitt administrert, det har bare fått stå i fred. Arbeidet gikk derfor ut på å holde dem i god stand under faktisk belastning.</p>\n<p><strong>Resultat.</strong> Alle førti kjørte med høy tilgjengelighet og god ytelse, noe som ga kundene hosting de ikke trengte å tenke på. Stabil og pålitelig under reell bruk er hele poenget med hosting, og det er det disse leverte.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Systemadministrasjon",
        "Webutvikling",
        "Ytelsesoptimalisering",
        "Pålitelighet og overvåking",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/engineered-600-pl-pgsql-based-etl-elt-pipelines-54/",
      "url": "https://engineer.company/nb/portfolio/engineered-600-pl-pgsql-based-etl-elt-pipelines-54/",
      "title": "Utviklet 600 PL/pgSQL‑baserte ETL/ELT‑pipelines for å effektivisere komplekse arbeidsflyter for databehandling på tvers av flere utviklings- og produksjonsmiljøer i PostgreSQL.",
      "summary": "Utviklet 600 ETL/ELT-pipelines i PL/pgSQL for PostgreSQL i utvikling og produksjon, kuttet snittlig kjøretid med 35 % og økte gjennomstrømningen over 50 %.",
      "content_html": "<p><strong>Situasjon.</strong> Organisasjonen håndterte store mengder transaksjons- og analysedata i flere PostgreSQL‑miljøer for utvikling og produksjon. De eksisterende datapipelinene var fragmenterte, manglet konsistens og skapte flaskehalser i ytelsen, noe som førte til forsinkelser i forretningskritisk rapportering og beslutningstaking.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å designe og implementere robuste, skalerbare og effektive ETL/ELT‑pipeliner med PL/pgSQL for å effektivisere komplekse arbeidsflyter for datainntak, transformasjon og lasting. Et sentralt mål var å forbedre ytelsen til spørringer og sikre dataintegriteten i alle miljøer.</p>\n<p><strong>Handling.</strong> Over 600 PL/pgSQL‑baserte ETL/ELT‑pipeliner ble utviklet for å automatisere uttrekk, transformasjon og lasting av data fra ulike kilder. Sentrale tekniske implementasjoner omfattet:</p>\n<ul>\n<li>Tok i bruk partisjonerte tabeller og materialiserte visninger for å optimalisere leseytelsen for store datasett.</li>\n<li>Innførte primær- og fremmednøkkelbegrensninger for å bevare referanseintegriteten under transformasjoner.</li>\n<li>Utformet unike og sammensatte indekser for å gjøre JOIN‑operasjoner og komplekse filterkriterier raskere.</li>\n<li>Bygde inn unntakshåndtering og transaksjonskontroll for å sikre feiltoleranse og tilbakerulling ved feil.</li>\n<li>Muliggjorde inkrementelle lastinger ved hjelp av mekanismer for change data capture (CDC) og tidsstempelbaserte deltaer, noe som reduserte behandlingstiden med over 60 %.</li>\n<li>Utviklet automatiserte prosedyrer for logging og revisjon for å spore kjøringen av pipelinene, overvåke avvik og gjøre feilsøking enklere.</li>\n</ul>\n<p><strong>Resultat.</strong> Det nye ETL/ELT‑rammeverket forbedret konsistensen, påliteligheten og ytelsen i arbeidsflytene for databehandling betydelig:</p>\n<ul>\n<li>Oppnådde en reduksjon på 35 % i gjennomsnittlig kjøretid for pipelinene.</li>\n<li>Økte gjennomstrømningen i datapipelinene med over 50 %, noe som ga datatilgjengelighet i nesten sanntid for rapportering.</li>\n<li>Reduserte problemer med datakvalitet ved å eliminere over 90 % av transformasjonsfeilene gjennom bedre håndheving av begrensninger og bedre valideringslogikk.</li>\n<li>Styrket vedlikeholdbarheten og skalerbarheten, slik at senere endringer i datamodellen kan gjøres med minimal omarbeiding.</li>\n</ul>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Data engineering",
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "Datastyring",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Databaseadministrasjon (DBA)",
        "Datastyring og datakvalitet",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/architected-developed-implemented-supported-infrastructure-data-processing-and-55/",
      "url": "https://engineer.company/nb/portfolio/architected-developed-implemented-supported-infrastructure-data-processing-and-55/",
      "title": "Utformet, utviklet, implementerte og støttet infrastruktur, databehandling og kartapplikasjonen i 2 år uten stopp, uten helger, helligdager eller ferier, 10–14 timer om dagen.",
      "summary": "Sto for arkitektur, bygging og drift av infrastrukturen, databehandlingen og kartappen i to år uten stans — den pålitelige ryggraden i produktet.",
      "content_html": "<p><strong>Situasjon.</strong> Et oppstartsselskap i tidlig fase innen grønn energi var avhengig av én enkelt plattform for å spore, overvåke og optimalisere anlegg for fornybar energi, men hadde verken et eget infrastrukturteam eller en etablert utviklingsorganisasjon til å bygge og drifte den. Hele det tekniske fundamentet — skyinfrastrukturen, pipelinene for databehandling og den kundevendte GIS‑kartapplikasjonen — måtte skapes og holdes i gang kontinuerlig, i et marked der enhver nedetid eller ethvert hull i dataene direkte tæret på kundenes tillit og på inntektene.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å utforme arkitekturen for, bygge og drifte hele systemet fra ende til ende på egen hånd, på tvers av plattform- og datautvikling, DevOps og site reliability. Utover å skrive programvaren innebar det å eie produksjonen: klargjøre og herde infrastrukturen, designe databehandlingslaget som forsynte kartet, og garantere at applikasjonen forble tilgjengelig døgnet rundt for en voksende kundebase — alt innenfor rammene og det nådeløse tempoet i et oppstartsselskap i rask utvikling.</p>\n<p><strong>Handling.</strong> I to år ble infrastrukturen, datapipelinene og kartapplikasjonen designet, implementert og driftet uten avbrudd — ingen helger, helligdager eller ferier, ofte ti til fjorten timer om dagen. En pragmatisk, modulær arkitektur ble valgt for at en drift med bare én person skulle forbli vedlikeholdbar, med automatisert klargjøring, overvåking og varsling, slik at problemer kunne oppdages og løses raskt. Databehandlingen ble kontinuerlig finjustert for pålitelighet og ytelse, nye versjoner ble levert trinnvis, og hvert lag — fra serverne til kartet brukerne møtte — ble personlig vedlikeholdt og forbedret ut fra hvordan kundene faktisk brukte det.</p>\n<p><strong>Resultat.</strong> Plattformen forble kontinuerlig tilgjengelig og utviklet seg fra en skjør tidlig prototyp til den pålitelige ryggraden i produktet, og holdt selskapet oppe gjennom den kritiske vekstfasen takket være eierskapet til én enkelt ingeniør. Denne praktiske forvaltningen holdt infrastrukturen, dataene og kartapplikasjonen pålitelige nok til å understøtte mersalg, datalisensiering og anskaffelse av nye kunder, og viste en sjelden grad av engasjement, bredde og ende‑til‑ende‑ansvar på tvers av hele stacken.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Datapipelines (ETL/ELT)",
        "Fullstack‑utvikling",
        "GIS / geodata",
        "Infrastruktur",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "Fullstack‑produktutvikling",
        "GIS- og geodataløsninger",
        "Pålitelighet og overvåking",
        "Plattform- og løsningsarkitektur",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/optimized-budget-costs-10-times-with-zero-loss-56/",
      "url": "https://engineer.company/nb/portfolio/optimized-budget-costs-10-times-with-zero-loss-56/",
      "title": "Reduserte budsjettkostnadene 10 ganger uten tap av produktivitet for en internasjonal kunde ved å tenke nytt om hele infrastrukturen, fjerne unødvendige tjenester og flytte ut av AWS‑skyen.",
      "summary": "Kuttet infrastrukturbudsjettet med en faktor på 10 uten tap av produktivitet for en internasjonal kunde ved å tenke stacken på nytt og flytte bort fra AWS.",
      "content_html": "<p><strong>Situasjon.</strong> En internasjonal kunde dro på kraftig oppblåste infrastrukturkostnader. AWS‑oppsettet deres var overdimensjonert og hadde samlet opp tjenester de ikke lenger brukte, så skyregningen hadde glidd helt ut av proporsjon med det virksomheten faktisk trengte. Det er en vanlig historie — ingen setter seg fore å bruke for mye, det hoper seg bare opp når ingen følger med på måleren.</p>\n<p><strong>Oppgave.</strong> Oppdraget var å kutte kostnadene betydelig uten å miste noe av produktiviteten, og det betydde å tenke infrastrukturen grundig på nytt i stedet for å trimme i kantene — trimming i kantene flytter sjelden en regning som er strukturelt for stor.</p>\n<p><strong>Handling.</strong> Arbeidet gikk derfor fra ende til ende. Først kom en kartlegging av hva som faktisk ble brukt — det er der de dupliserte og unødvendige tjenestene viser seg — og de ble kuttet. Deretter ble det som var igjen, dimensjonert riktig etter den reelle etterspørselen i stedet for anslagene for verste fall som det opprinnelige oppsettet var bygget på. Og det store grepet var å flytte arbeidslastene helt bort fra AWS, over til en mer kostnadseffektiv hostingløsning — gjort forsiktig, i etapper, slik at virksomheten i drift aldri merket at migreringen skjedde under den.</p>\n<p><strong>Resultat.</strong> Budsjettkostnadene falt til omtrent en tidel, uten noe som helst tap av produktivitet — de samme mulighetene for en brøkdel av det de hadde betalt. Det frigjorde et betydelig beløp som i det stille hadde lekket ut i en overdimensjonert skyregning måned etter måned, noe som for virksomheten var penger rett tilbake på bunnlinjen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "DevOps",
        "Infrastruktur",
        "Løsningsarkitektur",
        "Migrering og modernisering",
        "Plattformarkitektur",
        "Skytjenester",
        "DevOps og CI/CD‑automatisering",
        "Plattform- og løsningsarkitektur",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/architected-a-layered-maritime-platform-separating-a-next-57/",
      "url": "https://engineer.company/nb/portfolio/architected-a-layered-maritime-platform-separating-a-next-57/",
      "title": "Utformet arkitekturen for en lagdelt maritim plattform som skiller en Next.js PWA‑frontend, et Go‑API (Huma/Fiber) og et funksjonslag i PostgreSQL, med all forretningslogikk i databasen.",
      "summary": "Utformet arkitekturen for en lagdelt maritim plattform — PWA i Next.js, et API i Go (Huma/Fiber) og et funksjonslag i PostgreSQL med all forretningslogikk.",
      "content_html": "<p><strong>Situasjon.</strong> Produktet skulle bli en maritim plattform — profesjonell nettverksbygging, omtaler av selskaper, sponset opplæring — og det var et ungt produkt, som er en høflig måte å si på at kravene kom til å flytte seg mye. Det man ville unngå, var en arkitektur der en endret forretningsregel betydde å røre frontenden, API‑et og databasen på én gang. I en liten kodebase som vokser raskt, er det den typen kobling som gjør en endring på to linjer om til en hel ettermiddag.</p>\n<p><strong>Oppgave.</strong> I arkitektrollen var beslutningen som måtte tas i forkant, hvor hver type logikk skulle ligge, med grenser som var tydelige nok til å holde under press i stedet for å viskes ut første gang noen hadde dårlig tid.</p>\n<p><strong>Handling.</strong> Det endte med tre lag, med én jobb hver. Next.js‑frontenden tar seg av presentasjon og interaktivitet og ingenting annet. Go‑API‑et — Huma over Fiber — er bevisst tynt: det ruter, validerer forespørselen, anvender sikkerhetsfiltreringen og orkestrerer, men det inneholder ingen forretningslogikk i det hele tatt. All forretningslogikken ligger i PostgreSQL‑funksjoner, som setter sammen hele resultatet og gir det tilbake slik at API‑et kan videresende det. Når en regel endres, endres den derfor i ett lag, i SQL, og de to andre trenger ikke å vite om det. Grensene ble skrevet ned og deretter håndhevet i kodegjennomgangen, fordi en konvensjon som ingen passer på, slutter å være en.</p>\n<p><strong>Resultat.</strong> Det hele forble lett å holde i hodet. Forretningslogikken ligger på ett sted du faktisk kan revidere, API‑et er en kjedelig adapter i god forstand, og frontenden bryr seg ikke når skjemaet endres under den. Det skillet er det som gjorde at nye funksjoner kunne fortsette å bli hengt på produktet uten at arkitekturen i det stille råtnet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Databaser",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Løsningsarkitektur",
        "Plattformarkitektur",
        "PostgreSQL",
        "Teknisk ledelse",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Fullstack‑produktutvikling",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-a-json-passthrough-architecture-where-postgresql-functions-58/",
      "url": "https://engineer.company/nb/portfolio/designed-a-json-passthrough-architecture-where-postgresql-functions-58/",
      "title": "Designet en JSON‑passthrough‑arkitektur der PostgreSQL‑funksjoner returnerer komplett JSON som Go‑API‑et videresender uendret, noe som fjernet mellomliggende unmarshalling og frikoblet frontend fra skjemaendringer.",
      "summary": "Designet en JSON-passthrough der PostgreSQL-funksjoner returnerer komplett JSON som Go-API-et videresender uendret, og koblet frontenden løs fra skjemaet.",
      "content_html": "<p><strong>Situasjon.</strong> Den vanlige måten data kommer fra en database til en nettleser på, er en stafett av transformasjoner. Databasen gir API‑et rader, API‑et pakker dem ut i structs, omformer dem, serialiserer dem tilbake til JSON, og først da sendes de ut. Hver eneste av disse etappene er kode du skriver, kode du tester, og ett sted til der API‑ets forståelse av dataene og databasens forståelse av dem kan gli fra hverandre.</p>\n<p><strong>Oppgave.</strong> Ideen var å hoppe over stafetten. Hvis databasen kunne returnere det ferdige svaret, kunne API‑et bare sende det videre, og frontenden kunne avhenge direkte av formen databasen leverer i stedet for av en kopi av den som ble vedlikeholdt for hånd i Go.</p>\n<p><strong>Handling.</strong> Det ble derfor bygget som en direkte videresending. PostgreSQL‑funksjonene setter sammen hele svaret som JSON — formingen er en SQL‑oppgave, gjort der dataene allerede ligger. Go‑handleren tar det imot som json.RawMessage og videresender det urørt; den bryter det aldri ned og koder det aldri om. En liten QueryJSON‑hjelper gjorde det mønsteret til minste motstands vei i stedet for noe man måtte huske å gjøre. Det som falt bort, var alt det mellomliggende maskineriet et konvensjonelt lagdelt API samler opp — DTO‑ene, mappingkoden, respons‑structene.</p>\n<p><strong>Resultat.</strong> Handlerkoden ble dramatisk kortere, og enda viktigere: frontenden sluttet å være koblet til Go. Endrer man hva en funksjon returnerer, flyter den nye formen rett gjennom til klienten uten at noen redigerer en eneste linje med handlerkode. Færre bevegelige deler, og en hel kategori av glidning mellom lagene finnes rett og slett ikke her.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Databaser",
        "Plattformarkitektur",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-an-organization-context-switching-system-with-client-59/",
      "url": "https://engineer.company/nb/portfolio/designed-an-organization-context-switching-system-with-client-59/",
      "title": "Designet et system for å bytte organisasjonskontekst med localStorage på klienten og speiling i informasjonskapsler på serversiden, slik at brukere kan opptre som organisasjoner de administrerer, med autorisasjon etter prinsippet om minste privilegium.",
      "summary": "Bygde organisasjonsbytte med localStorage speilet i en cookie på serveren, slik at brukere kan opptre som administrerte organisasjoner med minste privilegium.",
      "content_html": "<p><strong>Situasjon.</strong> På plattformen kan en maritim fagperson administrere organisasjoner — selskaper, akademier — som ikke har egne pålogginger. Personen er kontoen; organisasjonen er noe vedkommende handler på vegne av. Derfor må en bruker kunne bevege seg gjennom hele appen som hvilken som helst organisasjon vedkommende administrerer og bytte fritt mellom dem, og den bekvemmeligheten får ikke bli et hull i autorisasjonen.</p>\n<p><strong>Oppgave.</strong> Kontekstbyttet måtte være raskt og diskré for brukeren, samtidig som den aktive konteksten aldri på egen hånd skulle kunne gi noen tilgang vedkommende ikke hadde rett til.</p>\n<p><strong>Handling.</strong> En OrganizationContext håndterer dette, med lagring på begge sider. På klienten er localStorage sannhetskilden for hvilken organisasjon man for øyeblikket handler som, så byttet skjer umiddelbart — uten rundtur til serveren. En cookie på serversiden speiler den, slik at serverrenderte sider får samme kontekst under SSR; på serveren er det en getServerViewMode som leser den. Det viktige er at ingenting av dette stoles på når tilgang skal avgjøres. Autorisasjonen kontrolleres på nytt på serveren ved hver forespørsel. Frontend‑konteksten finnes for opplevelsens skyld — for å vise brukeren det riktige — og serveren er den eneste autoriteten på hva man har lov til å gjøre.</p>\n<p><strong>Resultat.</strong> En bruker kan handle som hvilken som helst organisasjon vedkommende administrerer, uten friksjon, og grensesnittet holder seg synkronisert på både klient og server. Men fordi tilgangene verifiseres på serversiden hver gang, svekker ingenting av denne bekvemmeligheten sikkerheten. Den som tukler med innholdet i localStorage, endrer hva vedkommendes eget brukergrensesnitt viser, og ikke noe mer — serveren sier fortsatt nei.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Plattformarkitektur",
        "Sikkerhet",
        "Backend- og API‑utvikling",
        "Plattform- og løsningsarkitektur",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/migrated-the-http-api-from-fiber-to-huma-60/",
      "url": "https://engineer.company/nb/portfolio/migrated-the-http-api-from-fiber-to-huma-60/",
      "title": "Migrerte HTTP‑API‑et fra Fiber til Huma v2 — 649 stier og 760 operasjoner — og nådde og holdt 100 % samsvar mellom rutene serveren registrerer og OpenAPI‑beskrivelsen den publiserer.",
      "summary": "Migrerte HTTP-API-et fra Fiber til Huma v2 — 649 stier, 760 operasjoner — med 100 % samsvar mellom registrerte ruter og den publiserte OpenAPI-beskrivelsen.",
      "content_html": "<p><strong>Situasjon.</strong> API‑et ble opprinnelig bygget på Fiber, med forespørselsvalidering skrevet for hånd, endepunkt for endepunkt. Det går fint så lenge det finnes en håndfull endepunkter. Det slutter å gå fint etter hvert som API‑flaten vokser: den håndskrevne valideringen blir en vedvarende vedlikeholdsbyrde, og små inkonsekvenser sniker seg inn fordi kontrollene i hvert endepunkt er et lite unikum. Og en samlet beskrivelse av hvordan API‑et så ut, fantes ikke noe sted.</p>\n<p><strong>Oppgave.</strong> Målet var at valideringen skulle komme fra typene i stedet for fra håndskrevne kontroller, og en faktisk kontrakt som beskrev API‑et — uten å stoppe opp for å skrive alt om på én gang.</p>\n<p><strong>Handling.</strong> HTTP‑laget ble flyttet over på Huma v2, som ligger oppå Fiber, slik at det eksisterende kjøremiljøet ble beholdt. Hvert endepunkt får egne struct‑typer for inndata og utdata, og Huma genererer forespørselsvalideringen og responsmodelleringen fra disse typene. En OpenAPI‑beskrivelse følger med på kjøpet, slik at dokumentasjonen følger koden i stedet for å forfalle i en wiki. Alt nytt ble skrevet mot Huma, og de eksisterende rutene ble migrert over, med nøyaktig to endepunkter igjen på ren Fiber — WebSocket‑endepunktene, der man faktisk vil ha selve socketen og forespørsel/svar‑modellen til Huma ikke passer. Det har vokst til 649 stier med 760 operasjoner, og en kontroll i CI som sammenligner rutene serveren faktisk registrerer, med dem OpenAPI‑beskrivelsen oppgir. Pariteten er 100 %, og den holder seg der fordi en rute som ikke er beskrevet, får bygget til å feile.</p>\n<p><strong>Resultat.</strong> Nye endepunkter får validering og oppdatert dokumentasjon uten at noen må gjøre ekstra arbeid for det, og en hel klasse feil i forespørselshåndteringen — typen «å, vi glemte å sjekke det feltet her» — forsvant. Med 760 operasjoner er beskrivelsen den eneste praktiske måten noen leser API‑et på, så garantien for at den er fullstendig, betyr mer enn den gjorde ved femti. Den typede kontrakten gjorde API‑et både tryggere å endre og enklere å overlevere til andre, fordi typene viser hva et endepunkt forventer.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Dokumentasjon",
        "Migrering og modernisering",
        "Plattformarkitektur",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "Plattform- og løsningsarkitektur",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-request-schema-validation-contract-with-automated-61/",
      "url": "https://engineer.company/nb/portfolio/built-a-request-schema-validation-contract-with-automated-61/",
      "title": "Genererte API‑kontrakten fra databasen og utover — OpenAPI, en typet TypeScript‑klient på 44 076 linjer, 61 mock‑handlere og grensene brukergrensesnittet håndhever — med en kontroll i hvert ledd som feiler ved avvik.",
      "summary": "Genererte API-kontrakten fra databasen og utover — OpenAPI, en typet TypeScript-klient, mock-handlere og UI-grenser — med en kontroll ved hvert ledd.",
      "content_html": "<p><strong>Situasjon.</strong> Frontend og backend utvikler seg i hvert sitt tempo, og antakelsene deres om innholdet i en forespørsel kan gli fra hverandre uten at noen merker det. Som regel oppdager man det gjennom en 422 i nettleseren — etter at avviket allerede er levert, som er det dyreste tidspunktet å få vite om det på. Å skrive begge sidene for hånd ut fra samme dokument løser det ikke; det flytter bare avviket over til den som glemte å lese dokumentet på nytt.</p>\n<p><strong>Oppgave.</strong> De to sidene måtte genereres fra én kilde i stedet for å avtales mellom to, og hvert trinn i genereringen måtte kontrolleres i stedet for å tas på tro.</p>\n<p><strong>Handling.</strong> Kjeden starter i databasen og går utover. Skjemaet og funksjonene i det definerer formene; Go‑typene definerer API‑et; Huma lager OpenAPI‑beskrivelsen ut fra dem; en typet TypeScript‑klient — 44 076 linjer i alt — genereres fra den beskrivelsen; 61 mock‑handlere genereres ved siden av, slik at frontendens egne tester kjører mot den virkelige kontrakten og ikke mot håndskrevne testdata; og grensene brukergrensesnittet håndhever i et webskjema, kommer fra samme sted i stedet for å bli skrevet inn på nytt i en validator. Hvert ledd har en vakt. En kontraktskontroll i CI sammenligner det frontenden sender, med det API‑et forventer, og får bygget til å feile ved avvik, med en skjemaprobe under som kontrollerer de faktiske formene i stedet for en beskrivelse av dem. Den dekker med vilje stedene der avvik gjerne gjemmer seg: valgfrie felt i forespørselskroppen, der «mangler» og «null» blandes sammen, og enumer i spørringsparametere, der de to sidene i det stille kan være uenige om de tillatte verdiene.</p>\n<p><strong>Resultat.</strong> Et felt kan ikke endres på bare én side — det feiler i det første leddet som merker det, i et bygg, minutter etter endringen. Det fjernet en tilbakevendende og virkelig irriterende klasse feil, den typen som er usynlig i kodegjennomgang og bare viser seg under kjøring. Prisen er et genereringstrinn midt i alt sammen: å generere på nytt er et ork, og kjeden er bare så pålitelig som sitt dårligst bevoktede ledd, og derfor fikk hvert ledd sin vakt.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "DevOps",
        "Pålitelighet og sikkerhetskopier",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-email-as-a-platform-capability-with-failover-62/",
      "url": "https://engineer.company/nb/portfolio/built-email-as-a-platform-capability-with-failover-62/",
      "title": "Bygde e‑post som en plattformfunksjon — tre leverandører med failover, leveringswebhooks, logging av sending og levering, maler og kampanjer — bak en oppstartskontroll som ikke lar plattformen starte uten minst én leverandør.",
      "summary": "Bygde e-post som en plattformfunksjon: tre leverandører med failover, webhooks for levering, logging av sending og levering, maler og kampanjeutsendelser.",
      "content_html": "<p><strong>Situasjon.</strong> E‑post bærer mye av tyngden på plattformen — verifisering, varslinger, sammendrag, kampanjer, det en bruker faktisk venter på. Og en e‑postleverandør er nettopp den typen avhengighet som svikter i det stille: konfigurasjonen ser grei ut, appen starter, og man oppdager først at noe er galt når et ekte menneske aldri får meldingen vedkommende ble lovet. Det er den verste måten å få vite det på. Med bare én leverandør blir det verre, fordi feilen er total og det er noen andre som må rette den.</p>\n<p><strong>Oppgave.</strong> E‑post måtte behandles som en kapabilitet plattformen eier, ikke som et klientbibliotek den kaller — i stand til å overleve et driftsavbrudd hos en leverandør, i stand til å si hva som skjedde med en gitt melding, og høylytt ved oppstart i miljøene der stillhet er farlig.</p>\n<p><strong>Handling.</strong> Tre leverandører står bak ett grensesnitt — SendGrid som primær, med SMTP2GO og Azure Communication Services bak — og failover mellom dem skjer automatisk, ikke som en konfigurasjonsendring gjort under press. Levering tas ikke for gitt: innkommende webhooks rapporterer hva hver leverandør gjorde med en melding, og begge sider registreres, i en sendelogg og en tabell for leveringshendelser, slik at «fikk denne personen verifiseringsmeldingen sin» er en spørring og ikke en gjetning. Maler holder meldingsinnholdet utenfor koden, og et eget skjema for masseutsendelser — 5 tabeller og 24 funksjoner — bringer kampanjer ut til brukersegmenter, som er et annet problem enn transaksjonell e‑post og ble bygget som et eget. Foran alt dette sender en oppstartskontroll en ekte melding gjennom hele stakken, styrt av et flagg: i utvikling logger den en advarsel og fortsetter, fordi ingen vil at den bærbare maskinen skal nekte å starte på grunn av en utløpt sandbox‑nøkkel, og i staging og produksjon er en feil fatal, og prosessen avslutter heller enn å produksjonssette et bygg som ikke kan sende e‑post. Selve sendeveien går gjennom en SSRF‑beskyttet klient med et tidsavbrudd på 30 sekunder, og den asynkrone leveringsveien har nye forsøk med backoff, slik at et kortvarig hikk ikke fører til at en melding går tapt.</p>\n<p><strong>Resultat.</strong> En hel kategori stille feil gikk fra «en bruker merker det dager senere» til «produksjonssettingen stopper», og en leverandør med en dårlig ettermiddag ble til degradert drift i stedet for et driftsavbrudd. Prisen er tre integrasjoner å holde i gang i stedet for én, og leveringslogger som vokser og må ryddes — begge deler akseptert, fordi e‑post er kanalen plattformen ikke kan rute utenom.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Skytjenester",
        "Backend- og API‑utvikling",
        "Pålitelighet og overvåking",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-a-postgresql-function-first-data-layer-across-63/",
      "url": "https://engineer.company/nb/portfolio/designed-a-postgresql-function-first-data-layer-across-63/",
      "title": "Designet et funksjonsbasert datalag i PostgreSQL — 1 275 lagrede funksjoner fordelt på 34 skjemaer — slik at hver lesing og skriving går gjennom en funksjon databasen kan gi tilgang til, i stedet for gjennom en tabell.",
      "summary": "Designet et funksjonsbasert datalag i PostgreSQL — 1 275 lagrede funksjoner i 34 skjemaer — der databasen selv håndhever tilgangsgrensen.",
      "content_html": "<p><strong>Situasjon.</strong> Forretningslogikk har en tendens til å lekke. Litt havner i API‑et, litt i noe SQL en handler kjører direkte, og snart er den samme regelen skrevet på to eller tre litt ulike måter, og det finnes ikke ett sted man kan peke på og si «dette er hva systemet gjør med dataene sine». Det er slik feil og sikkerhetshull kommer inn.</p>\n<p><strong>Oppgave.</strong> Målet var ett sted for alt sammen: hver lesing og skriving skulle gå gjennom databasen, API‑et skulle være en tynn adapter som ikke kjenner forretningsreglene, og det hele skulle kunne låses stramt ned.</p>\n<p><strong>Handling.</strong> Datalaget er bygget med funksjonene først. Skjemaet er delt opp etter domene — identity, organization, review, message, notification og 29 til, 34 i alt — og hver operasjon appen kan utføre, er én av 1 275 PostgreSQL‑funksjoner den kaller; det finnes ingen direkte tabelltilgang fra Go i det hele tatt. Deretter håndhever databasen det. Rollen API‑et logger inn som, mariner, har EXECUTE på appfunksjonene og USAGE på skjemaene og ingenting annet — ingen SELECT, ingen INSERT, ingen mulighet til å røre en tabell direkte — noe som blir omtrent 4 000 eksplisitte tildelinger i stedet for én generell. Funksjonene kjører som SECURITY DEFINER, eid av en egen function_owner‑rolle uten innlogging og med fast search_path, og superbrukerkontoen er forbeholdt migreringer og cron, godt unna den kjørende appen.</p>\n<p><strong>Resultat.</strong> Logikken bor på ett sted som faktisk kan revideres, API‑et forblir tynt og kjedelig på den gode måten, og tilgangsgrensen håndheves av Postgres selv i stedet for av at alle husker reglene. Skulle API‑et på en eller annen måte bli kompromittert, kunne det likevel ikke gjøre noe funksjonene ikke tillater. Med 1 275 funksjoner koster disiplinen noe reelt — et nytt felt er en migrering og en funksjonsendring, ikke en linje i en spørring — og den friksjonen er prisen for at grensen holder.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Databaser",
        "Datastyring",
        "Plattformarkitektur",
        "PostgreSQL",
        "Sikkerhet",
        "SQL",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Datastyring og datakvalitet",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/adopted-uuid-v7-time-ordered-identifiers-postgresql-18-64/",
      "url": "https://engineer.company/nb/portfolio/adopted-uuid-v7-time-ordered-identifiers-postgresql-18-64/",
      "title": "Tok i bruk tidsordnede UUID v7‑identifikatorer (PostgreSQL 18) som entitetsnøkler for å redusere fragmenteringen i B‑tre‑indekser og gjøre spørringene raskere.",
      "summary": "Tok i bruk tidsordnede UUID v7-nøkler (PostgreSQL 18) for mindre fragmentering i B-tre-indekser og raskere innsetting og intervallspørringer i alle tabeller.",
      "content_html": "<p><strong>Situasjon.</strong> Hver entitet trenger en unik id, og det refleksmessige valget er en tilfeldig UUID. Problemet er at tilfeldige id‑er havner spredt over hele en B‑tre‑indeks. Innsettingene spres, indeksen fragmenteres, og etter hvert som tabellene vokser, betaler både skriving og intervallskanninger for det. På en plattform som er ment å fortsette å vokse, er det en langsom lekkasje man helst ikke bygger inn.</p>\n<p><strong>Oppgave.</strong> Behold den globale unikheten til en UUID, men bli kvitt fragmenteringen som følger med tilfeldigheten.</p>\n<p><strong>Handling.</strong> Standarden ble UUID v7, som er tidsordnet — de første bitene er et tidsstempel, så nye rader sorteres inn i indeksen i stedet for å bli strødd utover den. PostgreSQL 18 har dette innebygd som uuidv7(), pakket inn i en liten uuid_generate_v7()-funksjon slik at det samme kallet oppfører seg ryddig på Flexible Server i Azure, og deretter gjort til standardverdi for primærnøklene til entitetene i hele skjemaet. Det er ikke noe eksotisk ved det; det er den typen beslutning som er billig hvis man tar den tidlig, og en plage å ettermontere senere.</p>\n<p><strong>Resultat.</strong> Id‑ene forble globalt unike, indeksen sluttet å fragmenteres slik tilfeldige UUID‑er fører til, og tidsordnede innsettinger og intervallspørringer ble raskere — jevnt, på tvers av alle tabeller, uten at noen måtte tenke på det igjen. Som en bonus får hver entitet en nøkkel man kan sortere etter tid, helt gratis.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Data engineering",
        "Databaser",
        "PostgreSQL",
        "Ytelsesoptimalisering",
        "Databasedesign og datamodellering",
        "Optimalisering av databaseytelse"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/implemented-a-catalogue-driven-deep-merge-for-stored-65/",
      "url": "https://engineer.company/nb/portfolio/implemented-a-catalogue-driven-deep-merge-for-stored-65/",
      "title": "Implementerte en katalogstyrt deep merge av lagrede JSON‑innstillinger, som forhindrer krasj ved manglende nøkler når skjemaet utvikler seg.",
      "summary": "Implementerte katalogstyrt dyp fletting av lagrede JSON-innstillinger, som hindrer krasj ved manglende nøkler når skjemaet utvikler seg.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen lagrer JSON‑preferanser — varslingsinnstillinger og lignende — som en brukers lagrede verdier lagt over et sett med standardverdier. Den opprinnelige sammenslåingen gjorde dette på ett enkelt nivå. Problemet viser seg senere: legg til en ny nøkkel i standardverdiene, og rader som ble lagret før nøkkelen fantes, har den rett og slett ikke. Så leser en eller annen klientkode det feltet, får undefined og krasjer — nettopp for de brukerne som har vært med lengst.</p>\n<p><strong>Oppgave.</strong> Det måtte være trygt å videreutvikle preferanseskjemaet, slik at det å legge til en innstilling aldri kunne ødelegge for dem som registrerte seg før den fantes.</p>\n<p><strong>Handling.</strong> Sammenslåingen på ett nivå ble erstattet med en dyp sammenslåing styrt av standardverdiene som katalog. Standardverdiene behandles som den autoritative listen over alle nøkler som skal finnes; brukerens lagrede verdier slås rekursivt sammen oppå, så alt i katalogen kommer garantert ut til stede, enten det var med i den lagrede bloben eller ikke. Legg til en nøkkel i standardverdiene, og den dukker automatisk opp i de effektive preferansene for hver eksisterende rad, nøstede nøkler inkludert. Endringen ble innført gjennom en migrering, slik at eksisterende data fikk gevinsten med en gang i stedet for å vente på å bli skrevet på nytt.</p>\n<p><strong>Resultat.</strong> Preferansene kan vokse uten frykt. Å legge til en innstilling gir ikke lenger risiko for et krasj på grunn av et undefined‑felt i klienten, og frontenden sluttet å trenge defensive kontroller spredt rundt på hvert sted den leser en preferanse. Det ga også resten av plattformen en pålitelig måte å utvide enhver lagret JSON‑blob på — mønsteret, ikke bare den ene rettelsen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Databaser",
        "Datastyring",
        "Migrering og modernisering",
        "PostgreSQL",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Datastyring og datakvalitet"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/modeled-the-maritime-domain-professionals-companies-ships-jobs-67/",
      "url": "https://engineer.company/nb/portfolio/modeled-the-maritime-domain-professionals-companies-ships-jobs-67/",
      "title": "Modellerte det maritime domenet i 348 normaliserte tabeller fordelt på 34 PostgreSQL‑skjemaer — fagfolk, selskaper, skip, stillinger, omtaler og resten — med SMALLINT‑oppslag og UUID v7‑nøkler.",
      "summary": "Modellerte det maritime domenet i 348 normaliserte tabeller fordelt på 34 PostgreSQL-skjemaer, med SMALLINT-oppslag, UUID v7-nøkler og én navnekonvensjon.",
      "content_html": "<p><strong>Situasjon.</strong> Kjernen i plattformen er et tett maritimt domene — fagpersoner, selskaper, skip, stillinger, omtaler — og alt dette refererer stadig til hverandre. En fagperson seiler på skip, jobber for selskaper og skriver omtaler; et selskap eier skip og legger ut stillinger. Nesten hver funksjon er en spørring på tvers av det nettet, så hvor godt dataene er modellert, avgjør hvor godt det meste av appen yter og hvor ryddig den er å bygge videre på.</p>\n<p><strong>Oppgave.</strong> Domenet måtte modelleres slik at det forble raskt og beholdt integriteten, og slik at det å legge til neste entitetstype ikke betydde å kjempe mot skjemaet.</p>\n<p><strong>Handling.</strong> Det er lagt opp som normaliserte PostgreSQL‑skjemaer organisert etter domene — 348 tabeller fordelt på 34 av dem så langt. De mange små, stabile oppregningene — statuser, typer, kategorier — ble SMALLINT‑oppslagstabeller, som holder radene kompakte og join‑operasjonene billige i stedet for at tekstkoder lagres overalt. Entitetene får UUID v7‑primærnøkler, så de er globalt unike, men likevel tidsordnet i indeksen. Én navnekonvensjon gjelder overalt — tabellnavn i flertall inne i skjemaer med navn i entall — og brukes uten unntak, noe som på kjøpet unngår mange kollisjoner med reserverte ord. Og relasjonene holdes oppe av ekte fremmednøkler og skranker, så integriteten er databasens jobb, ikke noe applikasjonen må huske å gjøre.</p>\n<p><strong>Resultat.</strong> Det som kom ut av det, er en datamodell som er konsistent og rask, og forutsigbar å jobbe i fordi de samme reglene gjelder overalt — det finnes ingen spesialtilfeller å pugge. Å legge til en funksjon betyr som regel å utvide skjemaet langs de eksisterende linjene i stedet for å jobbe mot dem, og det er den eneste grunnen til at 34 skjemaer er en struktur og ikke et villniss. Det er grunnmuren resten av plattformen står på.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Databaser",
        "Datastyring",
        "Plattformarkitektur",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Databasedesign og datamodellering",
        "Datastyring og datakvalitet",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/provisioned-azure-infrastructure-as-code-with-bicep-container-68/",
      "url": "https://engineer.company/nb/portfolio/provisioned-azure-infrastructure-as-code-with-bicep-container-68/",
      "title": "Klargjorde Azure‑infrastruktur som kode med Bicep — Container Apps, PostgreSQL Flexible Server, Front Door/WAF og nettverk — i utviklings-, staging- og produksjonsmiljøene.",
      "summary": "Satte opp Azure-infrastruktur som kode med Bicep — Container Apps, PostgreSQL, Front Door/WAF — reproduserbar i utvikling, staging og produksjon.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen kjører på Azure, og Azure satt opp for hånd — klikking gjennom portalen, en innstilling justert her og der — er en felle. Oppsettet glir ut, ingen husker hvorfor noe er som det er, og å bygge det opp igjen etter en dårlig dag er tregt og nervepirrende. Med mer enn ett miljø å holde i takt blir det bare verre.</p>\n<p><strong>Oppgave.</strong> Legg hele oppsettet i kode, slik at et miljø er noe man kan lese, gjennomgå og gjenskape i stedet for en haug med manuell tilstand.</p>\n<p><strong>Handling.</strong> Hele Azure‑oppsettet er definert i Bicep. Hvert miljø — testing, staging, product — kommer ut av de samme malene: api og www som kjører som Azure Container Apps i et administrert miljø, en PostgreSQL Flexible Server, Redis som cache, Front Door med en WAF‑policy ytterst, og nettverket under (VNet, NSG, privat DNS), med Log Analytics koblet på for diagnostikk. Imagene hentes fra prosjektets Azure Container Registry. Fordi alt er parameterisert, er det å sette opp et nytt miljø eller endre et eksisterende en pull request, ikke en supportsak til seg selv.</p>\n<p><strong>Resultat.</strong> Miljøene ble reproduserbare og lot seg gjennomgå. Avvik sluttet å være et mysterium, fordi sannhetskilden er koden, og å få infrastrukturen opp eller tilbake er et spørsmål om å anvende malene, ikke om å huske hva som ble klikket forrige gang. Det er forskjellen på å eie infrastrukturen og å bli eid av den.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "Sikkerhet",
        "Skytjenester",
        "Infrastruktur som kode",
        "Nettverk og VPN‑oppsett",
        "Sikkerhet og tilgangsstyring",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-github-actions-ci-cd-pipelines-with-a-69/",
      "url": "https://engineer.company/nb/portfolio/built-github-actions-ci-cd-pipelines-with-a-69/",
      "title": "Bygde CI/CD‑pipelines i GitHub Actions med et distroless‑produksjonsimage for frontend og promotering gjennom flere miljøer.",
      "summary": "Bygde CI/CD i GitHub Actions med et distroless produksjonsimage for frontenden og promotering mellom miljøer, så utgivelser ble rutinemessige og kjedelige.",
      "content_html": "<p><strong>Situasjon.</strong> Leveranser bør ikke avhenge av at noen husker trinnene, og det som til slutt kjører i produksjon, bør ikke være en tung container for generell bruk med et skall og en pakkebehandler den aldri kommer til å bruke — det er bare angrepsflate som ligger der uten grunn.</p>\n<p><strong>Oppgave.</strong> Gjør veien fra commit til kjøring i Azure automatisk, og hold produksjonsimagene så små og låst ned som hver arbeidslast tillater.</p>\n<p><strong>Handling.</strong> Leveransepipelinen er GitHub Actions. Egne arbeidsflyter håndterer kvalitetsporten for koden, testene og produksjonssettingen til hvert miljø, med CodeQL, avhengighetsgjennomgang og et SBOM‑steg ved siden av, slik at ingenting når et miljø uten å ha bestått kontrollene først. Imagene er flertrinnsbygg, og basen for hver del ble valgt ut fra hva den delen trenger, i stedet for som ett felles valg: frontenden leveres på et distroless‑image (gcr.io/distroless/cc‑debian13 — uten skall, uten pakkebehandler), Go‑API‑et på en slank Alpine og databaseimaget på postgres‑slim. Promotering flytter et bygg gjennom miljøene langs en fast rute, ikke manuelt.</p>\n<p><strong>Resultat.</strong> Utgivelser sluttet å være et forsiktig manuelt ritual og ble en rutinemessig, kjedelig hendelse, som er nøyaktig det man vil ha fra utgivelser. Produksjonsfrontenden kjører på omtrent så lite som det går an å gi den, kontrollene fanger problemer før de lander, og «produksjonssetting» er noe leveransepipelinen gjør, ikke noe noen sveiter seg gjennom.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Containere (Docker/Kubernetes)",
        "DevOps",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Skytjenester",
        "Testing og kvalitetssikring",
        "Containerisering og orkestrering",
        "DevOps og CI/CD‑automatisering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/authored-578-go-task-automation-targets-70/",
      "url": "https://engineer.company/nb/portfolio/authored-578-go-task-automation-targets-70/",
      "title": "Skrev 578 go‑task‑automatiseringsmål som dekker native-, Docker- og HTTPS‑utviklingsmoduser, linting, testing, database og produksjonssetting.",
      "summary": "Skrev 578 automatiseringsmål i go-task for modusene native, Docker og HTTPS — linting, testing, database og produksjonssetting bak én verktøykjede.",
      "content_html": "<p><strong>Situasjon.</strong> Kodebasen er et flerspråklig monorepo — Go, TypeScript, SQL, Python, shell — og hvert av disse språkene har sin egen måte å bygge, teste, linte og kjøre på. Overlatt til seg selv betyr det at alle bærer på et mentalt jukseark med verktøyspesifikke kommandoer, og at nykommere bruker den første dagen bare på å finne ut hvordan de får ting i gang.</p>\n<p><strong>Oppgave.</strong> Gi hele prosjektet én inngangsdør: én konsistent måte å kjøre hva som helst på, uansett hvilket språk det tilfeldigvis er skrevet i.</p>\n<p><strong>Handling.</strong> Det er bygget ut med go‑task — et Taskfile‑lag som har vokst til 578 navngitte mål, 50 i rotfilen og 528 i filer med egne navnerom under den. Der ligger utviklingsmodusene (native, Docker, en HTTPS‑variant for testing av PWA og mobil), kodekvalitetssiden (linting, formatering, testing og retting på tvers av alle språkene), databasehåndtering og de miljøspesifikke oppgavene for bygg og produksjonssetting. Det finnes til og med en lavminnemodus for maskiner som ikke har RAM nok til å bygge frontenden på vanlig måte. Poenget var aldri å ha mange oppgaver; det var at man aldri skal trenge å kjenne kommandoen som ligger under.</p>\n<p><strong>Resultat.</strong> Hvem som helst kan kjøre task &ndash;list og se hele verktøykjeden lagt fram, og kjøre hvilken som helst del av den på samme måte uansett hva som ligger under panseret. Onboarding ble kortere, og de små, dumme tabbene — feil flagg, feil katalog, en halvt husket kommando — forsvant stort sett. Tallet er også en advarsel: 578 mål er mer enn noen kan holde i hodet, så det er navngivingen og navnerommene som holder det brukbart, og antallet i seg selv er ikke noe å være stolt av.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Teknisk ledelse",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/owned-end-to-end-deployments-of-the-platform-71/",
      "url": "https://engineer.company/nb/portfolio/owned-end-to-end-deployments-of-the-platform-71/",
      "title": "Hadde ansvaret for produksjonssettingen av plattformen til Azure fra ende til annen, og styrte utgivelser gjennom utviklings-, staging- og produksjonsmiljøene.",
      "summary": "Hadde helhetlig ansvar for produksjonssettinger på Azure og flyttet utgivelser gjennom utvikling, staging og produksjon langs en fast, repeterbar vei.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen måtte nå brukerne på tvers av flere Azure‑miljøer, og produksjonssettingen er skjøten der infrastrukturen, byggepipelinen og applikasjonen møtes. Det er også der en liten tabbe slutter å være en feil og blir et driftsavbrudd, så det er den delen man minst av alt vil gjøre for hånd og halvveis etter hukommelsen.</p>\n<p><strong>Oppgave.</strong> Produksjonssettingen ble eid fra ende til ende, slik at en endring gikk ut til hvert miljø på den samme forutsigbare måten hver gang.</p>\n<p><strong>Handling.</strong> Utgivelser går langs en fast rute — utvikling, så staging, så produksjon — i stedet for at noen pusher rett til et miljø i drift. Leveransepipelinen bygger imagene og ruller dem ut, og Bicep‑malene holder målinfrastrukturen identisk fra miljø til miljø, slik at et bygg ikke promoteres til et litt annerledes sted hver gang. Konfigurasjon som varierer mellom miljøene, holdes atskilt fra hemmelighetene, og det betyr at den samme bygde artefakten kan promoteres gjennom miljøene og bare plukker opp de riktige innstillingene der den havner, i stedet for å bygges på nytt for hvert av dem.</p>\n<p><strong>Resultat.</strong> Endringer når hvert miljø forutsigbart, langs en definert vei, uten ad hoc‑produksjonssettinger for hånd innimellom. Utgivelser ble et kontrollert, repeterbart steg i stedet for et øyeblikk med tilbakeholdt pust, og det er en stor del av det som holdt plattformen i drift stabil mens den fortsatt endret seg raskt under overflaten.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Skytjenester",
        "DevOps og CI/CD‑automatisering",
        "Pålitelighet og overvåking",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/configured-database-backup-retention-as-infrastructure-as-code-72/",
      "url": "https://engineer.company/nb/portfolio/configured-database-backup-retention-as-infrastructure-as-code-72/",
      "title": "Konfigurerte lagringstiden for databasens sikkerhetskopier som infrastruktur som kode, gjennomgikk deretter gjenopprettingsberedskapen og dokumenterte fremgangsmåten for gjenoppretting — og navnga de gjenværende hullene i stedet for å la dem bli oppdaget under en hendelse.",
      "summary": "Innførte sikkerhetskopier av databaser og en strategi for katastrofegjenoppretting i infrastruktur som kode, for rask og reproduserbar gjenoppretting.",
      "content_html": "<p><strong>Situasjon.</strong> Et produkt som lever av dataene sine, har ikke råd til å miste noe av dem, og «det finnes sikkerhetskopier et sted» er et håp, ikke en gjenopprettingsplan. Den eneste sikkerhetskopien som er verdt å ha, er en man vet lar seg gjenopprette, inn i et miljø man vet man kan bygge opp igjen. Produktet hadde ingen av de to halvdelene skrevet ned.</p>\n<p><strong>Oppgave.</strong> Få gjenopprettingsevnen definert i stedet for antatt — dataene, miljøet rundt dem og en ærlig redegjørelse for hvor langt den faktisk rekker i dag.</p>\n<p><strong>Handling.</strong> Oppbevaringstiden for sikkerhetskopier er konfigurert i Bicep ved siden av databasen den beskytter, så gjenoppretting til et vilkårlig tidspunkt er en egenskap ved malen og ikke en innstilling noen en gang klikket seg fram til i en portal. Miljøet rundt den er også definert som infrastruktur som kode, og det er den stille halvdelen folk glemmer: å gjenopprette en database inn i et miljø man måtte bygge opp igjen for hånd etter hukommelsen, er egentlig ikke gjenoppretting. Deretter ble tilstanden revidert og skrevet ned — gjenopprettingsprosedyren, øvelsen som skal måle den, og hullene som fortsatt står åpne: georedundans er slått av, og målet for gjenopprettingstid er foreslått og ikke målt, fordi ingen øvelse er gjennomført ennå.</p>\n<p><strong>Resultat.</strong> Gjenoppretting sluttet å være en vag forsikring og ble en dokumentert tilstand med hullene navngitt. Det ser mindre imponerende ut enn «katastrofegjenoppretting: ferdig», og det er verdt betydelig mer — den som tar i det neste gang, vet hva som er dekket, hva som ikke er det, og nøyaktig hvilken øvelse som lukker gapet. Et navngitt hull er et man kan lukke; et unavngitt hull oppdages midt i en hendelse.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Databaser",
        "DevOps",
        "Infrastruktur",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Skytjenester",
        "Databaseadministrasjon (DBA)",
        "Infrastruktur som kode",
        "Sikkerhetskopiering og gjenoppretting"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-next-js-16-frontend-with-deliberate-73/",
      "url": "https://engineer.company/nb/portfolio/built-the-next-js-16-frontend-with-deliberate-73/",
      "title": "Bygde Next.js 16‑frontenden med bevisste strategier for SSR, SSG og CSR, og en gjenbrukbar fabrikk for forhåndshentede serversider som fjerner N+1‑kaskaden av hentinger fra hver autentisert side som flyttes over på den.",
      "summary": "Bygde frontenden i Next.js 16 med SSR/SSG/CSR og et mønster for forhåndshenting og hydrering i et serverskall, som fjernet N+1-hentinger.",
      "content_html": "<p><strong>Situasjon.</strong> Det innloggede dashbordet måtte føles raskt og samtidig være virkelig interaktivt, og de to målene drar mot hverandre hvis man går naivt til verks. Hent alt på klienten, og den første innlastingen drar ut, og verre: man får N+1‑mønsteret, der hver komponent våkner og fyrer av sin egen forespørsel, slik at én enkelt side blir en kaskade av rundturer.</p>\n<p><strong>Oppgave.</strong> Hver del av appen trengte den renderingen som faktisk passet den, uten å gi opp interaktiviteten på klientsiden der den betydde noe.</p>\n<p><strong>Handling.</strong> Frontenden i Next.js 16 bruker riktig modus for hver flate i stedet for ett felles valg. Markedsføringssider og offentlige sider genereres statisk — de endrer seg ikke fra bruker til bruker, så det er ingen grunn til å rendre dem ved hver forespørsel. De virkelig interaktive delene forblir klientrendret. Og der N+1‑kaskaden faktisk biter — det innloggede dashbordet og de store katalogoversiktene — ble løsningen trukket ut i en felles fabrikk i stedet for å skrives for hånd: createPrefetchedServerPage henter sidens data på serveren og gir dem til klienten ferdig utfylt, så komponentene kommer opp med dataene sine i stedet for at hver av dem går ut og spør etter dem. En strategi for cache‑invalidering er dokumentert for hver spørring, så dataene holder seg ferske uten at appen henter på nytt det den allerede har.</p>\n<p><strong>Resultat.</strong> Sidene som er flyttet over på fabrikken, kommer opp med én rundtur i stedet for en storm av dem, og fordi det er en fabrikk og ikke et mønster folk kopierer for hånd, arver neste side som flyttes over, oppførselen gratis. Resten av den innloggede appen er fortsatt klientrendret og venter fortsatt på tur etter den — en migrering med en fungerende mekanisme og en åpenbar neste side, og ikke en fullført opprydding.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Plattformarkitektur",
        "Webutvikling",
        "Ytelsesoptimalisering",
        "Fullstack‑produktutvikling",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/delivered-full-progressive-web-app-support-installable-and-74/",
      "url": "https://engineer.company/nb/portfolio/delivered-full-progressive-web-app-support-installable-and-74/",
      "title": "Leverte full støtte for Progressive Web App — kan installeres og fungerer uten nettforbindelse — med caching under kjøring i Workbox via next‑pwa.",
      "summary": "Leverte full PWA-støtte (progressiv webapp) — installerbar og brukbar uten nett — med caching under kjøring i Workbox via next-pwa.",
      "content_html": "<p><strong>Situasjon.</strong> Mange av plattformens brukere er til sjøs. Sjøfolk og feltpersonell, på mobilen, ofte frakoblet eller hengende på en dårlig forbindelse — ikke folk som sitter ved en pult på stabilt kontor‑wifi. Å bygge som om alle hadde et raskt og stabilt nett, ville i det stille ha stengt ute en stor del av det faktiske publikummet.</p>\n<p><strong>Oppgave.</strong> Appen måtte kunne installeres som en native app og fortsatt være brukbar når nettet faller ut.</p>\n<p><strong>Handling.</strong> Den leveres som en fullverdig Progressive Web App. Den installeres på hjemskjermen med skikkelige ikoner i de ulike størrelsene, en oppstartsskjerm og temafarger, så den ser ut og starter som en app og ikke som et bokmerke. Service workeren settes opp gjennom programtillegget next‑pwa, og cachingen under kjøring i Workbox bruker CacheFirst for det som ikke endrer seg fra forespørsel til forespørsel — fonter, bilder, lyd, video, CDN‑ressurser — sammen med fornuftige HTTP‑hoder for cache‑control. Fordi en service worker bare egentlig oppfører seg skikkelig over HTTPS, validerte HTTPS‑basert testing installasjons- og offline‑oppførselen på ekte enheter i stedet for å håpe at det virket.</p>\n<p><strong>Resultat.</strong> Brukerne kan installere appen og fortsette å bruke den frakoblet, og gjentatte innlastinger kommer raskt fra cachen i stedet for over nettet. Plattformen oppfører seg som en native app på maskinvaren brukerne faktisk har med seg, og for dette publikummet er det ingen luksus — det er forskjellen på at appen kan brukes til sjøs, og at den ikke kan det.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Pålitelighet og sikkerhetskopier",
        "Webutvikling",
        "Ytelsesoptimalisering",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-an-ios-26-liquid-glass-design-system-75/",
      "url": "https://engineer.company/nb/portfolio/designed-an-ios-26-liquid-glass-design-system-75/",
      "title": "Designet et «Liquid Glass»-designsystem for iOS 26 og et kanonisk komponentregister håndhevet av lint‑regler for å hindre at brukergrensesnittet spriker.",
      "summary": "Utformet et designsystem i iOS 26-stilen «Liquid Glass» og en kanonisk komponentoversikt som lint-regler håndhever, slik at brukergrensesnittet ikke spriker.",
      "content_html": "<p><strong>Situasjon.</strong> Et brukergrensesnitt uten et felles visuelt språk og et fast sett med komponenter glir ut, og det glir fort. Hver ny skjerm finner opp sine egne knapper, merker og kort på nytt, alle litt forskjellige, og de små inkonsekvensene hoper seg opp til produktet ser usammenhengende ut og hver endring betyr å røre fem skreddersydde versjoner av det samme.</p>\n<p><strong>Oppgave.</strong> Designsystemet måtte være sammenhengende nok til å se gjennomtenkt ut, og håndhevbart nok til at det ikke ville forvitre i det øyeblikket teamet fikk det travelt.</p>\n<p><strong>Handling.</strong> Det visuelle språket og komponentsystemet ble designet som én ting. Språket er «Liquid Glass»-uttrykket fra iOS 26 — gjennomskinnelige paneler med bakgrunnsuskarphet, lagdelte skygger, litt lysbrytning, fjærende animasjoner — bygget av verktøyklasser i Tailwind. Oppå det ligger et kanonisk sett med komponenter som hver skjerm skal settes sammen av: Card, Label, Button, GlassIconButton, DirectoryGrid og resten. Det som får det til å sitte, er lintingen: regler som avviser et hjemmesnekret merke eller en hjemmesnekret brikke og peker mot den kanoniske komponenten i stedet, så systemet holdes oppe av verktøy og ikke av at den som gjør gjennomgangen den dagen, husker å bry seg. Og dokumentasjonen er paret med konkrete notater som går fra feil til løsning, så veiledningen er «her er den gale måten og den riktige måten», ikke et abstrakt prinsipp.</p>\n<p><strong>Resultat.</strong> Brukergrensesnittet holder seg konsistent og tro mot merkevaren, og engangskomponentene som ellers ville spredt seg, blir fanget opp før de gjør det. Visuell konsistens sluttet å være et spørsmål om alles disiplin og ble noe verktøyene står vakt om, og det er den eneste versjonen av den som overlever en frist.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "UX/UI‑design",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-the-product-s-ui-and-ux-end-76/",
      "url": "https://engineer.company/nb/portfolio/designed-the-product-s-ui-and-ux-end-76/",
      "title": "Designet produktets UI og UX fra ende til annen — katalogrutenett, doble kort- og tabellvisninger, fortløpende validering av krav og brødsmulenavigasjon.",
      "summary": "Designet hele produktets UI/UX — katalogrutenett, kort- og tabellvisninger, validering i sanntid og brødsmulestier — for informasjonstette maritime data.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen setter mange ulike entiteter foran folk — fagpersoner, selskaper, skip, stillinger, omtaler — og grensesnittet måtte være to ting som kjemper mot hverandre: pent og virkelig brukbart. Tett nok til å vise ekte maritime data, men ikke så tett at det blir en vegg man preller av mot.</p>\n<p><strong>Oppgave.</strong> Produktets UI og UX ble eid fra ende til ende — oppsettene, interaksjonsmønstrene og de mindre tingene, som hvordan noen blir loset gjennom et skjema uten å føle seg maset på.</p>\n<p><strong>Handling.</strong> Noen få beslutninger gjorde det meste av jobben. Katalogrutenettene har en veksling mellom kort og tabell, så man kan bla visuelt eller skanne en tett tabell, og appen husker hva man valgte. Skjemaene bruker kravvalidatorer i sanntid som står over inndatafeltet og viser hver regel i grønt når den er oppfylt og i ravgult når den ikke er det, så man blir veiledet mens man skriver i stedet for å bli irettesatt etter at man har sendt inn. Navigasjonen består av konsekvente brødsmulestier og tokolonneoppsett for entiteter, så sidene føles som det samme produktet og ikke som et sett urelaterte skjermer. Og feilfilosofien er veiledning framfor feil — ingen røde vegger, omdirigeringer i stedet for blindveier, en app som prøver å holde brukeren i gang i stedet for å stoppe vedkommende.</p>\n<p><strong>Resultat.</strong> Det som kom ut av det, er en polert, konsistent opplevelse som gjør tette maritime data tilgjengelige og loser folk gjennom de kompliserte delene. Den framstår som gjennomtenkt og troverdig, og det er ikke kosmetikk på en plattform folk bruker for sin faktiske karriere — hvis den så slurvete ut, ville de stole mindre på dataene, og det med rette.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Produkt og krav",
        "UX/UI‑design",
        "Webutvikling",
        "Produktstrategi og krav",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/hardened-the-application-with-nonce-based-csp-hsts-77/",
      "url": "https://engineer.company/nb/portfolio/hardened-the-application-with-nonce-based-csp-hsts-77/",
      "title": "Herdet applikasjonen med nonce‑basert CSP, HSTS, SameSite‑informasjonskapsler, databaseroller etter prinsippet om minste privilegium og ny kontroll av tilgangsrettigheter på serversiden.",
      "summary": "Herdet appen med noncebasert CSP, HSTS, SameSite-cookies, databaseroller etter prinsippet om minste privilegium og ny kontroll av rettigheter på serversiden.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen inneholder faglige og organisatoriske data, den typen folk forventer blir håndtert ordentlig, så én enkelt forsvarslinje kom aldri til å være nok. Arbeidsantakelsen må være at klienten er fiendtlig — at den som sitter med nettleseren, kan slå av alt nettleseren håndhever — og sikkerheten må holde likevel.</p>\n<p><strong>Oppgave.</strong> Plattformen måtte herdes i hvert lag — frontend, API, database — slik at sikkerheten ble håndhevet av serveren uavhengig av hva grensesnittet tilfeldigvis tillot.</p>\n<p><strong>Handling.</strong> I frontenden setter proxy‑mellomvaren i Next.js — proxy.ts — en Content‑Security‑Policy med en nonce per forespørsel og strict‑dynamic, i tillegg til HSTS og SameSite‑cookies, så nettleseren er låst ned med tanke på hva den vil kjøre og sende. I API‑et finnes hastighetsbegrensning, CORS, grenser for forespørselsstørrelse, inndatavalidering før noe rører databasen, og logging av sikkerhetsrelevante hendelser. I databasen logger API‑et inn som en rolle med minst mulig rettigheter, som bare har EXECUTE på appfunksjonene, funksjonene kjører som SECURITY DEFINER, og alt er parameterisert. Og rettighetene — abonnementsnivå, rolle, organisasjon, avgrensning per skip — kontrolleres på nytt på serveren ved hver forespørsel, og sperrene i frontenden behandles bare som UX. Sperrene avgjør hva man ser; serveren avgjør hva man kan gjøre.</p>\n<p><strong>Resultat.</strong> Sikkerheten er ikke avhengig av at brukergrensesnittet oppfører seg. Beskyttelsene ligger i lag, slik at den som kommer forbi én, ikke dermed kommer forbi resten, og det hele bygger på antakelsen om at klienten ikke kan stoles på — som er den riktige antakelsen for data folk deler i fortrolighet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Databaser",
        "Frontend‑utvikling",
        "Plattformarkitektur",
        "Sikkerhet",
        "Backend- og API‑utvikling",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/established-an-english-danish-internationalization-system-with-linter-78/",
      "url": "https://engineer.company/nb/portfolio/established-an-english-danish-internationalization-system-with-linter-78/",
      "title": "Etablerte et engelsk/dansk internasjonaliseringssystem med 12 027 meldinger per språkversjon fordelt på 458 navneromsfiler, med vokabular håndhevet av linter og et budsjett på 400 linjer per fil.",
      "summary": "Etablerte et i18n-system for engelsk og dansk med 12 027 meldinger per språk i 458 filer, med vokabular håndhevet av linter og et størrelsesbudsjett.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen kjører på engelsk og dansk, og oversettelsessystemer har en tendens til å vokse seg til et rot. Filene vokser uten grense, nøkler lekker — til stede i det ene språket, borte i det andre — og de språkspesifikke konvensjonene brukes ujevnt, så det ene språket ender opp med å lese som om det var oversatt av en komité der ingen snakket sammen. For et profesjonelt produkt er det ikke en liten skjønnhetsfeil; det framstår som slurv.</p>\n<p><strong>Oppgave.</strong> Internasjonaliseringsoppsettet måtte skalere — holde begge språkene konsistente og korrekte, og filene noe en person fortsatt kunne vedlikeholde et år senere.</p>\n<p><strong>Handling.</strong> Det er bygget på next‑intl med regler som verktøyene faktisk håndhever. Den danske siden har et låst vokabular og en låst stil — ekte æøå, det uformelle «du», de riktige aksentene i imperativ og semantiske tilordninger for sammensatte substantiver, slik at de oversettes etter mening og ikke ord for ord — og en linter holder den til det. Det er et strengt budsjett på 400 linjer per navneromsfil, med oppdeling og sammenslåing, slik at et stort område deles opp i nøstede filer som slås rent sammen igjen i stedet for at én fil vokser i det uendelige; det budsjettet er grunnen til at 12 027 meldinger per språk ligger i 458 filer og ikke i en håndfull enorme. En lintersjekk sammenligner nøklene på tvers av språkene, så ingenting lekker eller forsvinner. Og lagrede overstyringer slås sammen katalogstyrt i stedet for med skjøre reserveverdier på toppnivå — den samme ideen om dyp sammenslåing som brukes for preferanser.</p>\n<p><strong>Resultat.</strong> Begge språkene holder seg korrekte og konsistente med 24 054 meldinger til sammen, og filene forblir vedlikeholdbare etter hvert som antallet strenger stiger. Språkkvalitet ble noe verktøyene garanterer ved hver commit, i stedet for noe som i det stille forringes hver gang noen legger til en streng i en fart. Budsjettet er den bærende delen: uten en grense per fil ville 458 filer ha vært tolv, og ingen ville åpnet dem.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Internasjonalisering",
        "Internasjonalisering og lokalisering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/set-a-zero-warnings-quality-bar-across-six-79/",
      "url": "https://engineer.company/nb/portfolio/set-a-zero-warnings-quality-bar-across-six-79/",
      "title": "Satte en kvalitetsstandard med null advarsler for seks språk — Go, TypeScript, SQL, Python, Shell og Markdown — håndhevet av pre‑commit‑hooks.",
      "summary": "Satte et kvalitetskrav om null advarsler for Go, TypeScript, SQL, Python, Shell og Markdown, håndhevet med pre-commit-hooks.",
      "content_html": "<p><strong>Situasjon.</strong> Advarsler som hoper seg opp, tærer i det stille. Hver diagnostikk man ignorerer, senker listen litt, og når bygget først spytter ut førti av dem, leser ingen noen av dem, og et reelt problem ligger midt i oversikten, synlig for alle, fordi «advarsler» har blitt bakgrunnsstøy. I en flerspråklig kodebase finnes det desto flere støykilder som kan få dette til å skje.</p>\n<p><strong>Oppgave.</strong> Repoet trengte én kompromissløs kvalitetsstandard på tvers av alle språk, slik at ting ble rettet i stedet for å hope seg opp.</p>\n<p><strong>Handling.</strong> En nulladvarselspolicy ble innført, med verktøyene som det som håndhever den, fordi en policy som baserer seg på alles årvåkenhet, taper mot den første travle uken. Hver linterdiagnostikk er en feil — det finnes ikke noe «warn»-nivå å gjemme seg i — og det er likt i hele stakken: Go med golangci‑lint, TypeScript med ESLint, SQL med SQLFluff, Python med Ruff, shell med ShellCheck, Markdown med markdownlint. Inline‑undertrykkelser er forbudt, så man kan ikke dekke over en diagnostikk; man må faktisk rette det som er galt. Pre‑commit- og pre‑push‑hooks kjører linterne og testene, så en commit som ville ha introdusert et problem, blir aldri laget. Det finnes til og med lengdegrenser for funksjoner og filer, for å hindre at moduler vokser forbi punktet der de er lesbare.</p>\n<p><strong>Resultat.</strong> Problemer blir rettet ved kilden i stedet for å bli skjøvet inn i et etterslep ingen tar igjen, og kodebasen holder seg ren som standard og ikke gjennom periodiske heltedåder. Standarden er identisk uansett hvilket språk man jobber i, og det er verktøyene som holder den — ikke noens viljestyrke — og det er derfor den faktisk holder.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Teknisk ledelse",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/set-the-platform-s-founding-decisions-in-november-2025-80/",
      "url": "https://engineer.company/nb/portfolio/set-the-platform-s-founding-decisions-in-november-2025-80/",
      "title": "Tok plattformens grunnleggende beslutninger i ukene etter at repoet ble opprettet i november 2025 — lagdelingen, datatilgang med databasen først og kravet om null advarsler — og de står seg fortsatt ni måneder senere.",
      "summary": "Tok plattformens grunnleggende beslutninger i november 2025 — lagdelingen, databasesentrert datatilgang og kravet om null advarsler — og de står seg fortsatt.",
      "content_html": "<p><strong>Situasjon.</strong> Repoet ble opprettet 25. november 2025 og var helt tomt. Det som avgjøres i de første ukene av et slikt prosjekt, veier uforholdsmessig tungt: lagdelingen, hvor forretningslogikken får lov til å ligge, hvor høyt kvalitetslisten ligger. Slike valg er billige å ta på dag tre og nesten umulige å omgjøre innen måned seks, for da bygger alt som er skrevet i mellomtiden, på dem.</p>\n<p><strong>Oppgave.</strong> De grunnleggende beslutningene måtte tas bevisst og tidlig, og i en form som kunne overleve å bli overlevert til andre og til en langt større kodebase enn den som fantes da.</p>\n<p><strong>Handling.</strong> Tre beslutninger gjorde det meste av jobben. Stakken ble lagdelt slik at hver del har én oppgave — en Next.js‑frontend, et Go‑API som validerer og videresender, et funksjonslag i PostgreSQL som eier forretningsreglene — i stedet for at logikken havnet der det tilfeldigvis passet den ettermiddagen. Datatilgangen ble lagt bak lagrede funksjoner fra starten av, og det er valget alt annet i databasearbeidet følger av; å ettermontere det senere ville ha betydd å skrive om hver eneste handler. Og et krav om null advarsler kom på plass før det fantes mye kode å holde opp mot det, fordi en standard som innføres ved commit 5 000, er et opprydningsprosjekt, mens den samme standarden ved commit 50 rett og slett er slik repoet fungerer. Hver av de tre var det tyngre valget den gangen, og alle tre kostet fart den første måneden.</p>\n<p><strong>Resultat.</strong> Ni måneder og flere tusen commits senere står alle tre fortsatt: lagene har holdt seg adskilt, ingen handler snakker direkte med en tabell, og bygget er fortsatt fritt for advarsler. Det er den prøven det er verdt å sette en grunnleggende beslutning på — ikke om den hørtes riktig ut, men om den overlevde møtet med mengden arbeid som kom etterpå, for det er der lettvinte valg som regel blir lagt bort i det stille.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Databaser",
        "DevOps",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Løsningsarkitektur",
        "Plattformarkitektur",
        "Sikkerhet",
        "Teamledelse",
        "Teknisk ledelse",
        "Backend- og API‑utvikling",
        "Fullstack‑produktutvikling",
        "Plattform- og løsningsarkitektur",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/delivered-analysis-and-regular-progress-reports-to-the-81/",
      "url": "https://engineer.company/nb/portfolio/delivered-analysis-and-regular-progress-reports-to-the-81/",
      "title": "Leverte analyser og jevnlige fremdriftsrapporter til administrerende direktør, og omsatte tekniske KPI‑er og leveransestatus til beslutninger.",
      "summary": "Leverte analyser og jevnlige framdriftsrapporter til daglig leder, og gjorde tekniske måltall og leveransestatus om til trygge beslutninger.",
      "content_html": "<p><strong>Situasjon.</strong> Etter hvert som plattformen tok form, måtte både framdrift og helsetilstand være synlige for ledelsen i en form de faktisk kunne gjøre noe med. Rå tekniske signaler — byggestatus, leveransetempo, hendelser — sier ikke stort i seg selv til den som tar beslutninger om produkt og investeringer; de ligger på feil nivå. Noen måtte oversette.</p>\n<p><strong>Oppgave.</strong> Den oversettelsen var jobben: å ta den tekniske virkeligheten — hvor leveransene sto, hva systemmetrikkene sa — og rapportere den klart og jevnlig til daglig leder, slik at beslutningene hvilte på fakta i stedet for gjetting.</p>\n<p><strong>Handling.</strong> En fast rapporteringsrytme kom på plass, i stedet for rapportering når noen spurte, som alltid er litt for sent. Framdriften i leveransene, omfang, risikoer og systemhelse ble fulgt opp og gjort om til enkle, beslutningsrettede oppdateringer — hva som er i rute, hva som er i faresonen, og hva en gitt prioritering faktisk ville koste i avveininger. Målet var å unngå å levere rå tall og overlate tolkningen til noen uten konteksten; hver rapport kom med konkrete anbefalinger, og der det var nyttig, ble fortellingen underbygd med analysen bak, så ledelsen kunne gå i dybden om de ville, i stedet for å måtte ta det på tro.</p>\n<p><strong>Resultat.</strong> Ledelsen fikk et klart, ærlig og oppdatert bilde av det tekniske arbeidet, og kunne styre produktprioriteringer og investeringer med en viss trygghet i stedet for å fly i blinde. Rapporteringen sluttet å være et statusritual ingen leser, og ble noe beslutninger faktisk ble tatt ut fra — noe som sørget for at det tekniske arbeidet og den forretningsmessige kursen pekte samme vei.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dataanalyse",
        "Interessenter og rapportering",
        "Produkt og krav",
        "Prosjektledelse",
        "Teknisk ledelse",
        "Dataanalyse og BI‑dashbord",
        "Prosjektledelse (smidig)",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/provided-round-the-clock-24-7-infrastructure-support-82/",
      "url": "https://engineer.company/nb/portfolio/provided-round-the-clock-24-7-infrastructure-support-82/",
      "title": "Ga infrastrukturstøtte døgnet rundt, 24/7, for en IPTV/OTT‑strømmeplattform og administrerte ca. 1 000 servere i tillegg til kundeeide systemer for globale kunder i Kina, USA og Tyskland.",
      "summary": "Ga infrastrukturstøtte 24/7 til en IPTV/OTT-strømmeplattform — ca. 1 000 servere pluss kundesystemer i Kina, USA og Tyskland.",
      "content_html": "<p><strong>Situasjon.</strong> Dette var en IPTV/OTT‑strømmeplattform med kunder spredt over Kina, USA og Tyskland, og det betydde at det aldri fantes en rolig time å gjøre vedlikehold i — noen, et sted, så alltid på. Nedetid på en slik plattform er ikke et abstrakt måltall; det er noens TV som bare stopper, og vedkommende bryr seg ikke om hvorfor.</p>\n<p><strong>Oppgave.</strong> Jobben var å holde infrastrukturen til plattformen tilgjengelig døgnet rundt — virkelig døgnet rundt, ikke «kontortid pluss en vaktordning ingen svarer på».</p>\n<p><strong>Handling.</strong> Støtte 24/7 omfattet rundt tusen servere, pluss en god del kundeeide systemer i tillegg — de ble administrert, overvåket, sikret og holdt konfigurert på tvers av hele strømmemiljøet. Siden kundene satt i tre svært ulike tidssoner, fantes «utenom arbeidstid» egentlig ikke; et problem kl. 3 om natten lokal tid var beste sendetid for noen andre, så det ble behandlet som beste sendetid. Mye av arbeidet handlet om å merke at noe var i ferd med å skli ut før det ble et driftsavbrudd, for på en plattform for direktestrømming får man ikke rette ting i det stille i etterkant.</p>\n<p><strong>Resultat.</strong> Plattformen holdt seg kontinuerlig tilgjengelig for et globalt publikum, og problemer ble fanget opp og håndtert uansett når på døgnet de dukket opp, før de nådde skjermen til en seer. På en tjeneste som går 24/7, er det hele jobben — suksess ser ut som at ingenting skjer, og det er nøyaktig det seerne ville ha.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Skytjenester",
        "Systemadministrasjon",
        "IT‑støtte og helpdesk",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/ensured-uninterrupted-delivery-of-iptv-streaming-signals-between-83/",
      "url": "https://engineer.company/nb/portfolio/ensured-uninterrupted-delivery-of-iptv-streaming-signals-between-83/",
      "title": "Sikret uavbrutt levering av IPTV‑strømmesignaler mellom leverandører og kunder, og overvåket og vedlikeholdt strømmenettverket og IP‑telefonien døgnet rundt.",
      "summary": "Sikret uavbrutt IPTV-strømming mellom leverandører og kunder ved å overvåke og vedlikeholde strømmenettverket og IP-telefonien 24/7.",
      "content_html": "<p><strong>Situasjon.</strong> IPTV står og faller på at signalet kommer fram. Strømmene går fra leverandørene, gjennom plattformen, til kundene, og ethvert brudd hvor som helst i den kjeden betyr svart skjerm for noen. IP‑telefonien gikk ved siden av, med det samme kravet: den måtte bare virke.</p>\n<p><strong>Oppgave.</strong> Jobben var å garantere at signalleveransen og telefonien gikk uten avbrudd.</p>\n<p><strong>Handling.</strong> Strømmenettverket og IP‑telefonien ble overvåket, feilsøkt og vedlikeholdt døgnet rundt. Poenget med å følge med hele tiden er at strømmeproblemer melder seg som forringelse før de blir et rent brudd — en strøm som begynner å hakke, en forbindelse som blir ustabil — og følger man med, kan man fange det opp ved hakkingen i stedet for ved den svarte skjermen. Mye av arbeidet handlet derfor om å ligge i forkant av signalet i stedet for å reagere på klager på det.</p>\n<p><strong>Resultat.</strong> Strømmene og samtalene holdt seg pålitelige over hele plattformen, og problemer ble oppdaget og rettet før noen rakk å merke at tjenesten falt ut. Å holde et signal flytende mellom leverandører og sluttkunder uten synlige brudd er stille, kontinuerlig arbeid, og stille er nettopp det det skal være.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Systemadministrasjon",
        "Nettverk og VPN‑oppsett",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/developed-and-maintained-django-web-applications-for-the-84/",
      "url": "https://engineer.company/nb/portfolio/developed-and-maintained-django-web-applications-for-the-84/",
      "title": "Utviklet og vedlikeholdt Django‑webapplikasjoner for IPTV‑plattformen, leverte nye funksjoner og forbedret ytelse og stabilitet.",
      "summary": "Utviklet og vedlikeholdt Django-webapplikasjoner for en IPTV-plattform, leverte nye funksjoner og forbedret samtidig ytelse og stabilitet.",
      "content_html": "<p><strong>Situasjon.</strong> Rundt IPTV‑plattformen lå webapplikasjoner bygget på Django — de delene folk faktisk klikket på — og de måtte stadig videre: nye funksjoner, og den ytelsen og stabiliteten som en plattform i drift døgnet rundt krever.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å holde disse applikasjonene funksjonsrike, raske og stabile.</p>\n<p><strong>Handling.</strong> Django‑webapplikasjonene ble utviklet og vedlikeholdt — nye funksjoner ble bygget, feilene rettet og ytelsen og stabiliteten løftet, i samarbeid på tvers av teamet heller enn i et hjørne. På noe som betjener kunder kontinuerlig, er stabiliteten ikke noe som er kjekt å ha ved siden av funksjonene; den er rammen funksjonene må respektere. En prangende funksjon som får det hele til å vakle, er ikke mye verdt når det hele ikke har råd til å vakle.</p>\n<p><strong>Resultat.</strong> Applikasjonene fikk stadig nye egenskaper og holdt seg samtidig pålitelige for både de interne teamene og kundene. Å bygge videre på noe uten å destabilisere det er den balansen som betydde noe her, og det er den arbeidet holdt seg til.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Python",
        "Webutvikling",
        "Backend- og API‑utvikling",
        "Fullstack‑produktutvikling",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/planned-and-implemented-new-infrastructure-functionality-for-internal-85/",
      "url": "https://engineer.company/nb/portfolio/planned-and-implemented-new-infrastructure-functionality-for-internal-85/",
      "title": "Planla og implementerte ny infrastrukturfunksjonalitet for interne og eksterne systemer, og bygde løsninger så solide at de fortsatt kjører år senere med minimale endringer.",
      "summary": "Planla og bygde infrastruktur for interne og eksterne systemer, robust nok til fortsatt å kjøre år senere med minimale endringer.",
      "content_html": "<p><strong>Situasjon.</strong> Etter hvert som organisasjonen vokste, trengte de interne og eksterne systemene stadig nye funksjoner skrudd på. Den enkle måten å gjøre det på er det som går raskest i dag; problemet med den enkle måten er at man er tilbake og reparerer den om seks måneder.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å planlegge og bygge infrastrukturfunksjonalitet som faktisk ville vare — ikke bare virke nå, men fortsette å virke.</p>\n<p><strong>Handling.</strong> Ny infrastrukturfunksjonalitet ble planlagt og implementert på tvers av de interne og eksterne systemene, utformet for å være varig — den typen ting man bygger én gang, skikkelig, slik at den går i årevis med minimale inngrep i stedet for å kreve konstant oppmerksomhet. Det er et bevisst valg hver gang: å bruke litt mer omtanke på forhånd, slik at man ikke forplikter seg til å passe på den i all fremtid.</p>\n<p><strong>Resultat.</strong> Systemene forble funksjonelle og effektive lenge etter at de ble bygget, og gikk i årevis nesten uten endringer. Den levetiden er det egentlige målet på infrastrukturarbeid — hvem som helst kan lage noe som virker i dag; å lage noe som fortsatt stille og rolig virker år senere, er det vanskeligere og mer nyttige.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Løsningsarkitektur",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "Systemadministrasjon",
        "Pålitelighet og overvåking",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/as-one-of-the-first-hires-designed-and-86/",
      "url": "https://engineer.company/nb/portfolio/as-one-of-the-first-hires-designed-and-86/",
      "title": "Designet og bygde, som en av de første ansatte, hele kjerneinfrastrukturen og de tilhørende prosessene fra bunnen av for en SaaS‑oppstartsbedrift innen grønn energi, og la grunnlaget for rask vekst.",
      "summary": "Designet og bygde hele kjerneinfrastrukturen fra bunnen av som en av de første ansatte i et SaaS-oppstartsselskap innen grønn energi, og muliggjorde rask vekst.",
      "content_html": "<p><strong>Situasjon.</strong> Dette var en SaaS‑oppstartsbedrift innen grønn energi med en lovende idé og i praksis ennå ikke noe teknisk fundament under seg. Å komme inn som en av de første ansatte betydde fasen der det ikke finnes noe å vedlikeholde fordi ingenting eksisterer — å bygge grunnen alle andre skal stå på.</p>\n<p><strong>Oppgave.</strong> Jobben var å bygge kjerneinfrastrukturen og prosessene rundt den, helt fra bunnen av.</p>\n<p><strong>Handling.</strong> Hele kjerneinfrastrukturen og prosessene som støtter den, ble utformet og bygget — serverne, nettverkene, dataflyten, sikkerheten, driftssiden. Å gjøre det i en oppstartsbedrift betyr å ta beslutninger som er vanskelige å omgjøre senere, så målet var ikke bare «få noe til å kjøre», men å legge et fundament som kunne bære vekten av rask vekst uten å måtte rives ut i det øyeblikket selskapet ble større. Tidlige infrastrukturvalg blir enten det som gjør at man kan skalere, eller det man bruker et år på å rive opp igjen; målet var helt klart den første typen.</p>\n<p><strong>Resultat.</strong> Oppstartsbedriften satt igjen med et solid teknisk fundament, og det var nettopp det som gjorde at virksomheten kunne vokse raskt etterpå. Å være den som bygger dette grunnlaget fra ingenting, er et helt eget slags ansvar — gjør man det riktig, merker ingen noe; gjør man det feil, merker alle det — og dette fundamentet holdt.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "DevOps",
        "Infrastruktur",
        "Løsningsarkitektur",
        "Plattformarkitektur",
        "Sikkerhet",
        "Skytjenester",
        "Systemadministrasjon",
        "DevOps og CI/CD‑automatisering",
        "Plattform- og løsningsarkitektur",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/maintained-and-enhanced-the-legacy-hugo-static-site-87/",
      "url": "https://engineer.company/nb/portfolio/maintained-and-enhanced-the-legacy-hugo-static-site-87/",
      "title": "Vedlikeholdt og videreutviklet det eldre statiske Hugo‑nettstedet, og bidro samtidig med UI/UX‑forbedringer til hovedproduktet for forvaltning av eiendeler.",
      "summary": "Vedlikeholdt og forbedret et eldre statisk Hugo-nettsted, og bidro med forbedringer i UI/UX til kjerneproduktet for anleggsforvaltning.",
      "content_html": "<p><strong>Situasjon.</strong> Det fantes et eldre nettsted bygget på Hugo — en generator for statiske nettsteder — som gikk side om side med selskapets hovedprodukt, et system for eiendelsforvaltning. Nettstedet var det gamle, etablerte; produktet var der den egentlige verdien lå.</p>\n<p><strong>Oppgave.</strong> Ansvarsområdet var å holde nettstedet i god stand og samtidig forbedre opplevelsen i produktet.</p>\n<p><strong>Handling.</strong> Det statiske Hugo‑nettstedet ble vedlikeholdt og videreutviklet — holdt oppdatert og i drift — mens UI/UX‑forbedringer og tilbakemeldinger samtidig gikk inn i hovedproduktet for eiendelsforvaltning. Å dele oppmerksomheten mellom et eldre nettsted og flaggskipproduktet handler mest om ikke å la det gamle forfalle mens man er opptatt av det nye; begge representerer selskapet overfor noen, så begge måtte holdes i anstendig stand.</p>\n<p><strong>Resultat.</strong> Nettstedet holdt seg oppdatert i stedet for å eldes i det stille, og brukervennligheten i hovedproduktet ble bedre gjennom jevn, velinformert finjustering — den typen som kommer av faktisk å bruke og tenke over produktet i stedet for å redesigne det i ett stort grep.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "UX/UI‑design",
        "Webutvikling",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/designed-a-time-management-and-reporting-system-that-88/",
      "url": "https://engineer.company/nb/portfolio/designed-a-time-management-and-reporting-system-that-88/",
      "title": "Designet et system for tidsstyring og rapportering som var i produksjon i årevis uten vesentlige endringer.",
      "summary": "Designet et system for tidsstyring og rapportering som var i produksjon i årevis uten vesentlige endringer.",
      "content_html": "<p><strong>Situasjon.</strong> Teamet manglet en pålitelig måte å styre tid og rapportere fremdrift på. Det betyr som regel at det hele foregår spredt i regneark og folks hukommelse, og at rapporteringen blir et kappløp ved slutten av hver periode i stedet for noe som bare følger naturlig av måten folk jobber på.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å bygge et system for begge deler som faktisk ville vare.</p>\n<p><strong>Handling.</strong> Et system for tidsstyring og rapportering ble utformet rundt måten teamet faktisk jobbet på, i stedet for å påtvinge en ferdig standardprosess som de ville ha funnet veier rundt. Det er hele knepet med interne verktøy — passer det til den virkelige arbeidsflyten, bruker folk det; kjemper det mot arbeidsflyten, forlater de det i det stille, og man er tilbake til regnearkene. Derfor ble det bygget for å passe virkeligheten i stedet for et ideal.</p>\n<p><strong>Resultat.</strong> Systemet var i produksjonsbruk i årevis uten noen vesentlige endringer. Det er komplimentet man ønsker seg for et internt verktøy — ikke at det var imponerende, men at det bare fortsatte å virke og at ingen noen gang trengte å erstatte det. Noe som overlever år med daglig bruk uten å bli rørt, var tydelig bygget for å passe.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Produkt og krav",
        "Prosjektledelse",
        "Teknisk ledelse",
        "Dataanalyse og BI‑dashbord",
        "Produktstrategi og krav",
        "Prosjektledelse (smidig)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/automated-team-collaboration-password-management-task-and-time-89/",
      "url": "https://engineer.company/nb/portfolio/automated-team-collaboration-password-management-task-and-time-89/",
      "title": "Automatiserte samarbeid i teamet, passordhåndtering, oppgave- og tidsstyring, og bygde et halvautomatisk system for å vise frem prosjekter, noe som økte teamets produktivitet.",
      "summary": "Automatiserte samarbeid og passord-, oppgave- og tidsstyring, og bygde et halvautomatisk system for prosjektpresentasjoner, som økte teamets produktivitet.",
      "content_html": "<p><strong>Situasjon.</strong> Mye av teamets driftsarbeid — koordinering, passordhåndtering, oppfølging av oppgaver og tid — ble gjort manuelt, og manuell koordinering er en stille skatt: den er aldri det man legger merke til, men den spiser jevnt og trutt timer som kunne vært brukt bedre.</p>\n<p><strong>Oppgave.</strong> Målet var å automatisere dette repetitive driftsarbeidet.</p>\n<p><strong>Handling.</strong> De delene som egnet seg for det, ble automatisert — samarbeid i teamet, passordhåndtering, oppgavestyring, tidsstyring — med et halvautomatisk system for prosjektpresentasjon på toppen. Tanken bak alt sammen var å ta den rutinemessige koordineringen av folks bord slik at den gikk av seg selv, og la dem bruke den frigjorte oppmerksomheten på arbeid som faktisk trengte et menneske. Presentasjonssystemet var det samme instinktet brukt på noe mer synlig: å gjøre presentasjonen av arbeidet stort sett automatisk i stedet for et manuelt ork hver gang.</p>\n<p><strong>Resultat.</strong> Driftseffektiviteten økte og teamet ble mer produktivt, fordi den rutinemessige koordineringen som tidligere krevde konstant menneskelig oppmerksomhet, nå i stor grad gikk av seg selv. Tiden som hadde lekket ut i dødarbeid, gikk tilbake til det egentlige arbeidet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Prosjektledelse",
        "Sikkerhet",
        "Teknisk ledelse",
        "DevOps og CI/CD‑automatisering",
        "Prosjektledelse (smidig)",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/integrated-a-company-wide-password-management-system-strengthening-90/",
      "url": "https://engineer.company/nb/portfolio/integrated-a-company-wide-password-management-system-strengthening-90/",
      "title": "Integrerte et passordhåndteringssystem for hele selskapet, styrket sikkerheten og effektiviserte tilgangskontrollen.",
      "summary": "Integrerte et passordhåndteringssystem for hele selskapet, styrket sikkerheten og forenklet tilgangskontrollen i hele organisasjonen.",
      "content_html": "<p><strong>Situasjon.</strong> Påloggingsinformasjon ble håndtert inkonsekvent — ulike personer lagret og delte den på ulike, improviserte måter — og nettopp den inkonsekvensen er sikkerhetsrisikoen. Det er sjelden et dramatisk sikkerhetsbrudd; det er et passord i en chatmelding, en delt innlogging som ingen roterer, den langsomme opphopningen av små eksponeringer.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å sentralisere påloggingsinformasjonen og gjøre den sikker.</p>\n<p><strong>Handling.</strong> Et passordhåndteringssystem for hele selskapet ble innført, slik at det fantes én konsekvent, sikker måte å lagre og dele påloggingsinformasjon på i stedet for hver enkelts personlige vane. Verdien av at det gjelder hele selskapet, er nettopp at det ikke er valgfritt per person — en passordbehandler som bare halve teamet bruker, hjelper knapt, fordi risikoen ligger i den halvdelen som ikke tok den i bruk. Poenget var derfor å gjøre den sikre måten til standardmåten, overalt.</p>\n<p><strong>Resultat.</strong> Sikkerheten ble bedre, og tilgangskontrollen ble enklere og mer konsekvent i hele organisasjonen. Når all påloggingsinformasjonen ligger på ett forvaltet sted, blir en hel rekke små, kjedelige eksponeringer rett og slett umulige — og det er det meste av hva sikkerhet i praksis faktisk er.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Sikkerhet",
        "Systemadministrasjon",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/drove-client-web-success-by-combining-custom-website-91/",
      "url": "https://engineer.company/nb/portfolio/drove-client-web-success-by-combining-custom-website-91/",
      "title": "Drev frem kundenes suksess på nett ved å kombinere skreddersydd utvikling og design av nettsteder med SEO, innholdsstrategi og tekstforfatting.",
      "summary": "Drev fram kundenes suksess på nett ved å kombinere skreddersydd nettstedsutvikling og design med SEO, innholdsstrategi og tekstforfatting.",
      "content_html": "<p><strong>Situasjon.</strong> Kundene ville egentlig ikke ha et nettsted; de ville ha det et nettsted skal gjøre for dem — bli funnet, trekke til seg kunder, faktisk fungere som en kanal. Et vakkert nettsted ingen finner, er en fiasko som ser ut som en suksess.</p>\n<p><strong>Oppgave.</strong> Oppgaven var å samle byggingen og vekstsiden i ett tilbud, i stedet for å levere et nettsted og ønske dem lykke til.</p>\n<p><strong>Handling.</strong> De to halvdelene ble satt sammen — skreddersydd utvikling og design av nettsteder på den ene siden, SEO, innholdsstrategi og tekstforfatting på den andre — slik at en kunde fikk noe som både var godt bygget og faktisk lett å finne. Disse behandles som regel som separate jobber, og det er slik man ender opp med et nydelig nettsted som ikke rangerer noe sted, eller et godt optimalisert nettsted som er ubehagelig å bruke. Å gjøre begge deler betydde at nettstedet var utformet for å bli funnet fra starten av, ikke optimalisert som en ettertanke.</p>\n<p><strong>Resultat.</strong> Kundene fikk bedre synlighet og mer engasjement — nettstedene deres fungerte som ekte vekstkanaler i stedet for nettbrosjyrer. Å bygge nettstedet og gjøre det lett å finne i ett og samme løp var det som gjorde et nettsted om fra en kostnad til noe som faktisk betalte for seg.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Frontend‑utvikling",
        "Merkevare og markedsføring",
        "Produkt og krav",
        "UX/UI‑design",
        "Webutvikling",
        "Merkevare, markedsføring og SEO",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/performed-data-recovery-across-a-wide-range-of-92/",
      "url": "https://engineer.company/nb/portfolio/performed-data-recovery-across-a-wide-range-of-92/",
      "title": "Utførte datagjenoppretting fra et bredt spekter av medier — SD‑kort, HDD‑er, SSD‑er, RAID‑oppsett, eksterne disker og Mac‑systemer.",
      "summary": "Gjenopprettet kritiske data fra SD-kort, HDD-er, SSD-er, RAID-oppsett, eksterne disker og Mac-systemer — filer hentet tilbake fra enheter avskrevet som tapt.",
      "content_html": "<p><strong>Situasjon.</strong> Folk kom med lagringsmedier som hadde sviktet eller blitt skadet, og med data på dem som de raskt trengte tilbake — og når noen bærer en død disk inn i en butikk, har de som regel gått fra bekymring til panikk. Bilder, forretningsdokumenter, den eneste kopien av noe som betydde noe.</p>\n<p><strong>Oppgave.</strong> Jobben var å gjenopprette dataene deres, uansett hvilke medier de kom inn med.</p>\n<p><strong>Handling.</strong> Datagjenoppretting ble utført på så å si alle vanlige lagringstyper — SD‑kort, roterende HDD‑er, SSD‑er, RAID‑oppsett, eksterne disker, Mac‑systemer. Hver av disse svikter og gir fra seg dataene sine på sin egen måte: en SSD er et annet problem enn en disk med roterende plater, et RAID‑oppsett er et annet problem igjen, og et Mac‑filsystem har sine egne særheter. Arbeidet handlet derfor like mye om å kjenne riktig tilnærming for det konkrete mediet som om noen enkelt teknikk.</p>\n<p><strong>Resultat.</strong> Kundene fikk tilbake kritiske data fra enheter de allerede hadde avskrevet som tapt, på tvers av alle vanlige typer lagring. Det er en helt egen lettelse i ansiktet til noen når man leverer tilbake disken med filene i behold — det var poenget med arbeidet, og det skjedde på tvers av hele spekteret av medier.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Pålitelighet og sikkerhetskopier",
        "Systemadministrasjon",
        "IT‑støtte og helpdesk",
        "Maskinvarereparasjon og dataredning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/diagnosed-and-repaired-laptop-hardware-screens-hinges-keyboards-93/",
      "url": "https://engineer.company/nb/portfolio/diagnosed-and-repaired-laptop-hardware-screens-hinges-keyboards-93/",
      "title": "Diagnostiserte og reparerte maskinvare i bærbare datamaskiner — skjermer, hengsler, tastaturer, styreflater, hovedkort og strømforsyning — og løste programvareproblemer på Linux, Windows og Mac.",
      "summary": "Feilsøkte og reparerte maskinvare i laptoper — skjermer, hengsler, tastaturer, styreplater, hovedkort, strøm — og rettet programvare på Linux, Windows og Mac.",
      "content_html": "<p><strong>Situasjon.</strong> Bærbare datamaskiner kom inn med hele spekteret av ting som går galt med bærbare — sprukne skjermer, ødelagte hengsler, døde tastaturer, sviktende styreplater, feil på hovedkortet, strømproblemer — pluss programvaresiden, på tvers av Linux, Windows og Mac. Kort sagt: uansett hva som var galt, havnet maskinen på arbeidsbenken.</p>\n<p><strong>Oppgave.</strong> Jobben var å diagnostisere og reparere dem pålitelig — og pålitelig er nøkkelordet, fordi en reparasjon av en bærbar som ikke holder, bare er et utsatt andre besøk.</p>\n<p><strong>Handling.</strong> Maskinvaren ble reparert fra ende til annen — skjermer, hengsler, tastaturer, styreplater, hovedkort, strøm — noe som på hovedkortnivå er virkelig pirkete presisjonsarbeid, og programvareproblemene ble løst på tvers av alle tre operativsystemene. Diagnosen er som regel den egentlige ferdigheten: en bærbar som ikke vil slå seg på, kan skyldes laderen, hovedkortet eller batteriet, og reparasjonen er aldri bedre enn antakelsen om hva som faktisk er ødelagt. Derfor gikk omhuen med til å finne den virkelige feilen før noe ble rørt.</p>\n<p><strong>Resultat.</strong> Maskinene gikk tilbake til eierne fungerende og pålitelige, med både maskinvare og programvare i orden. En reparasjon som holder, er den eneste typen som er verdt å gjøre, og det å få diagnosen riktig først var det som gjorde at disse holdt.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Systemadministrasjon",
        "IT‑støtte og helpdesk",
        "Maskinvarereparasjon og dataredning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-layered-automated-test-suite-across-four-layers-94/",
      "url": "https://engineer.company/nb/portfolio/built-a-layered-automated-test-suite-across-four-layers-94/",
      "title": "Bygde en lagdelt automatisert testsuite — 981 Go‑tester, 543 spesifikasjoner for frontend og nettleser, 494 atferdstester i SQL — med mutasjonstesting, egenskapsbaserte tester og en kvalitetsport for universell utforming.",
      "summary": "Bygde en lagdelt automatisert testsuite — 981 Go-tester, 543 frontend-spesifikasjoner og 494 SQL-atferdstester — med mutasjons-, egenskaps- og UU-kontroller.",
      "content_html": "<p><strong>Situasjon.</strong> En plattform som har forretningslogikken sin i databasen, har et testproblem de fleste prosjekter ikke har. Logikken ligger ikke i språket testrammeverket er godt på — den ligger i SQL, bak funksjonsgrenser, og SQL er nettopp den typen kode som forblir utestet fordi det er klønete å teste den. Legg et Go‑API og en Next.js‑frontend oppå det, så blir «de viktige delene er dekket» stille og rolig til «de delene som var lette å dekke, er dekket».</p>\n<p><strong>Oppgave.</strong> Hvert lag måtte få oppførselen sin kontrollert der oppførselen faktisk hører hjemme, i stedet for at alt ble kontrollert utenfra gjennom en nettleser.</p>\n<p><strong>Handling.</strong> Fire lag fikk fire typer tester. Go‑API‑et har 981 testfunksjoner fordelt på 310 filer. Frontenden har 543 Vitest- og Playwright‑spesifikasjoner, inkludert 48 ende‑til‑ende‑filer og 30 nettleserspesifikasjoner. Databasen har 494 atferdstestfiler — 116 876 linjer SQL som gjør påstander gjennom 6 654 utløste unntak — slik at en lagret funksjon testes i databasen i stedet for gjennom tre lag med applikasjon over den. Over disse ligger testene som tester testene: Stryker‑mutasjonstesting ødelegger med vilje en linje og feiler når ingenting merker det, og fast‑check genererer inndata ingen tenkte på å skrive ned. En kvalitetsport med axe‑core krever null brudd på WCAG 2.0 og 2.1 nivå A og AA, noe som gjør brudd på universell utforming til en byggfeil i stedet for et revisjonsfunn måneder senere. Tersklene for testdekning flyttes bare oppover. Hele testsuiten kjører som 10 jobber i en arbeidsflyt på 612 linjer.</p>\n<p><strong>Resultat.</strong> Å endre noe strukturelt sluttet å være skremmende, og det er det eneste som hindrer en kodebase av denne størrelsen i å forkalkes. Den ærlige kostnaden er tid — testsuiten er treg, den legger en avgift på hver endring, og i denne størrelsen trenger den eget vedlikehold. Det den kjøper, er evnen til å fortsette å bevege seg raskt, og det er verdt mer enn minuttene den tar.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "DevOps",
        "Frontend‑utvikling",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-repository-s-guard-engine-of-268-checks-95/",
      "url": "https://engineer.company/nb/portfolio/built-the-repository-s-guard-engine-of-268-checks-95/",
      "title": "Bygde repoets kontrollmotor — 268 registrerte commit‑kontroller, 277 lint‑regler og 15 egne ESLint‑regler — pluss 146 tester av selve kontrollene, slik at det er bygget, ikke gjennomgangen, som holder standarden.",
      "summary": "Bygde en kontrollmotor med 268 registrerte commit-kontroller, 277 lint-regler og 15 egne ESLint-regler, med 146 tester av selve kontrollene.",
      "content_html": "<p><strong>Situasjon.</strong> Standarder som er skrevet ned i en bidragsveiledning, er forslag. Alle er enige i dem, og så er det fredag, endringen er liten, og veiledningen taper. Kravet om null advarsler kom aldri til å holde med mindre noe annet enn velvilje holdt det oppe — og linterne som følger med hvert språk, stopper et godt stykke før de prosjektspesifikke reglene som faktisk betyr noe, de som handler om hvordan akkurat denne kodebasen er ment å fungere.</p>\n<p><strong>Oppgave.</strong> Reglene prosjektet brydde seg om, måtte være kjørbare, slik at et brudd på én av dem fikk en commit til å feile i stedet for å vente på en kodegjennomgåer med tid og hukommelse nok til å oppdage det.</p>\n<p><strong>Handling.</strong> Ut av dette vokste det frem en vaktmotor. Det finnes 274 kontrollskript, 268 av dem registrert i commit‑hookene, sammen med 277 JavaScript‑lintere og 96 shell- og 17 Python‑validatorer som dekker det ferdigverktøy ikke har noen mening om — at en migrering kan rulles tilbake, at en oversettelsesnøkkel finnes i begge språkversjonene, at en registrert rute står i OpenAPI‑beskrivelsen, at ingen i det stille har lagt inn en inline‑undertrykkelse. Femten egne ESLint‑regler holder husets mønstre på plass i TypeScript. Én regel fortjener å nevnes for seg: enhver commit med prefikset fix: må ha med en test som feiler uten den, slik at en feil som er rettet, forblir rettet. Og fordi en ødelagt vakt er verre enn ingen vakt i det hele tatt — den slipper alt gjennom og ingen finner det ut — har vaktene 146 egne tester.</p>\n<p><strong>Resultat.</strong> Gjennomgangstiden flyttet seg fra mekanikk til design, fordi de mekaniske innvendingene allerede var gjort av en maskin før grenen ble pushet. Avveiningen er reell og verdt å si høyt: det går tregt å committe, og en dårlig skrevet vakt er virkelig irriterende å jobbe seg rundt. De 146 testene finnes fordi nettopp det skjedde.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Teknisk ledelse",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-payments-and-entitlements-layer-96/",
      "url": "https://engineer.company/nb/portfolio/built-the-payments-and-entitlements-layer-96/",
      "title": "Bygde laget for betaling og tilgangsrettigheter — Stripe sammen med kjøp i appen hos Apple og Google — som styrer tilgangen til katalogen, søket og eksporten gjennom en tilgangsmodell på 11 tabeller som kontrolleres på serveren.",
      "summary": "Bygde laget for betaling og rettigheter — Stripe med kjøp i app via Apple og Google — som styrer tilgang til katalog, søk og eksport ut fra en tilgangsmodell.",
      "content_html": "<p><strong>Situasjon.</strong> Penger er den delen av en plattform ingen får lov til å ta lett på. Et abonnement må tåle at et kort utløper, en refusjon, et planbytte, en webhook som kommer to ganger og en webhook som kommer i feil rekkefølge. I det øyeblikket betaling kan skje i tre salgskanaler — et kort på nettet, Apple i den ene appbutikken, Google i den andre — finnes det tre ulike fremstillinger av hva noen har kjøpt, og produktet trenger fortsatt ett svar på ett spørsmål: hva har denne personen lov til å gjøre akkurat nå?</p>\n<p><strong>Oppgave.</strong> Å ta imot penger og å gi tilgang måtte være to systemer i stedet for ett, slik at en ny salgskanal ikke skulle bety å skrive om hver eneste tilgangssjekk i produktet.</p>\n<p><strong>Handling.</strong> Stripe håndterer kort og abonnementer gjennom 18 Go‑filer, og kjøp i appen via Apple og Google kommer inn gjennom sin egen kvitteringsverifisering. Alle tre møtes i et databaseskjema for betalinger med 9 tabeller og 34 funksjoner — og stopper der. Det produktet faktisk spør om, er et eget databaseskjema for tilgang, 11 tabeller og 34 funksjoner, som svarer på «har denne kontoen lov til å gjøre dette?» uten å vite eller bry seg om hvilken salgskanal som betalte for det. Det svaret styrer tilgangen til bedriftskatalogen, søket og dataeksporten, og det kontrolleres på nytt på serveren ved hver forespørsel, fordi en skjult knapp er en høflighet og ikke en kontroll.</p>\n<p><strong>Resultat.</strong> Å legge til en salgskanal berører nå betalingssiden og lar tilgangssjekkene være i fred, og et brukerstøttespørsmål om noens tilgang har én tabell å se i i stedet for tre. Kostnaden er to skjemaer der et mindre produkt ville klart seg med ett, pluss et rettighetsoppslag på forespørsler som ellers ville vært gratis.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Databaser",
        "PostgreSQL",
        "Produkt og krav",
        "Sikkerhet",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Produktstrategi og krav",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/moved-slow-work-onto-a-river-job-queue-97/",
      "url": "https://engineer.company/nb/portfolio/moved-slow-work-onto-a-river-job-queue-97/",
      "title": "Flyttet tregt arbeid ut av forespørselsbanen og over i en River‑jobbkø — 15 worker‑moduler, 8 planlagte oppgaver og 20 pg_cron‑jobber — slik at en forespørsel returnerer mens arbeidet bak den fortsetter.",
      "summary": "Flyttet tidkrevende arbeid ut av forespørselsløpet og over i en jobbkø i River: 15 arbeidermoduler, 8 planlagte oppgaver og 20 vedlikeholdsjobber i pg_cron.",
      "content_html": "<p><strong>Situasjon.</strong> Noe arbeid har ingen grunn til å skje mens en bruker venter. Å sende e‑post, bygge en søkeindeks på nytt, generere et dokument, beregne rangeringer på nytt — gjør man noe av det inne i forespørselen, ser brukeren på et lastesymbol for noe vedkommende aldri ba om å se. Gjør man det i en goroutine i stedet, forsvinner det i det øyeblikket prosessen starter på nytt, noe den kommer til å gjøre, midt i en produksjonssetting, uten noe spor av at det noen gang skulle skje.</p>\n<p><strong>Oppgave.</strong> Bakgrunnsarbeidet trengte et varig sted å bo: en kø som overlever en omstart, prøver på nytt ved feil og kan inspiseres når noe ikke har skjedd.</p>\n<p><strong>Handling.</strong> Valget falt på River, i stor grad fordi den holder køen sin i PostgreSQL — databasen er allerede det autoritative kildesystemet, så en jobb og radene den berører, committes eller rulles tilbake sammen, og det finnes ingen ekstra infrastrukturdel å drifte og resonnere rundt. Bak den står 15 worker‑moduler og 8 planlagte oppgaver. Under det hele håndterer 20 pg_cron‑jobber vedlikeholdet databasen selv er best egnet til å gjøre: rydde i partisjoner, rotere salter, oppdatere aggregater. Alt som var tregt nok til å merkes, ble flyttet bort fra forespørselsløpet og over på én av de to.</p>\n<p><strong>Resultat.</strong> Forespørsler returnerer raskt, og det trege arbeidet blir likevel ferdig, med nye forsøk og en synlig historikk når det ikke blir det. Å holde køen i Postgres i stedet for i en egen meldingsmegler er en bevisst begrensning: den skalerer ikke i det uendelige, og ved et visst volum blir den feil svar. For en plattform der flaskehalsen uansett er databasen, var én bevegelig del mindre verdt mer enn spillerom som ikke kom til å bli brukt.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "Databaser",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-first-party-error-monitoring-and-tracing-98/",
      "url": "https://engineer.company/nb/portfolio/built-first-party-error-monitoring-and-tracing-98/",
      "title": "Bygde egen feilovervåking og OpenTelemetry‑sporing i stedet for å kjøpe dem — rensing av nyttelast, oppdagelse av topper og regresjoner, symbolisering og et syntetisk hjerteslag — bak 11 operatørvisninger.",
      "summary": "Bygde egen feilovervåking og OpenTelemetry-sporing — rensing, oppdaging av topper, symbolisering og hjerteslag — bak 11 visninger for operatører.",
      "content_html": "<p><strong>Situasjon.</strong> Forskjellen på en plattform som er oppe, og en plattform som fungerer, er om noen ville merket det. En feil en bruker støter på klokken elleve om kvelden, på en side ingen tester, er usynlig med mindre noe går og samler den inn. Det vanlige svaret er å kjøpe en ferdig driftet feilsporingstjeneste, og det er et godt svar — men det betyr også at plattformens egne feil, kallstakker og brukerkontekst havner hos en tredjepart.</p>\n<p><strong>Oppgave.</strong> Feil og spor måtte samles inn, grupperes og gjøres handlingsbare, uten at plattformens indre detaljer forlot plattformen.</p>\n<p><strong>Handling.</strong> To deler ble bygget. OpenTelemetry håndterer sporing over OTLP, slik at en treg forespørsel kan følges på tvers av frontenden, API‑et og databasen i stedet for å gjettes på. Ved siden av ligger en egenutviklet feilpipeline — en errmon‑tjeneste og en innsamlingstjeneste — som renser nyttelaster før lagring, grupperer feil i gjentakende problemer i stedet for en flat liste, oppdager topper og regresjoner med en avkjølingsperiode slik at én dårlig produksjonssetting ikke varsler noen førti ganger, oversetter minifiserte kallstakker fra frontenden tilbake til lesbar kode, og kjører et syntetisk hjerteslag for å bevise at selve pipelinen er i live. Alt havner i databasen som feilhendelser, feilgrupper, en innboks, API‑latens og stakkprøver, og det vises gjennom 11 driftsvisninger, inkludert én for tjenestenivåmål.</p>\n<p><strong>Resultat.</strong> Feil blir til en arbeidsliste som noen kan jobbe seg gjennom, og en regresjon melder seg selv i stedet for å bli oppdaget av en bruker. Å bygge i stedet for å kjøpe kostet reell tid og betyr at dette er én ting til å vedlikeholde — en kjøpt feilsporer ville vært i drift samme ettermiddag. Det dette ga, var at ingenting sensitivt forlater plattformen, og at varslingsreglene passer denne plattformen i stedet for en generisk plattform.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "DevOps",
        "Infrastruktur",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/kept-the-schema-honest-across-1022-migrations-99/",
      "url": "https://engineer.company/nb/portfolio/kept-the-schema-honest-across-1022-migrations-99/",
      "title": "Holdt skjemaet ærlig gjennom 1 022 migreringer med en CI‑port som bygger databasen begge veier — en ny installasjon, og en installasjon pluss alle migreringer — og feiler når de to ikke stemmer overens.",
      "summary": "Holdt skjemaet pålitelig gjennom 1 022 migreringer med en CI-kontroll som bygger databasen begge veier og feiler når de to resultatene avviker.",
      "content_html": "<p><strong>Situasjon.</strong> Et skjema beskrives to ganger i de fleste prosjekter: én gang av det første oppsettet som bygger det fra bunnen av, og én gang av de akkumulerte migreringene som fikk det til å vokse. Begge skal gi samme database. Ingenting kontrollerer at de gjør det, så de glir fra hverandre — og avviket er usynlig til et nytt miljø oppfører seg annerledes enn produksjon, som regel i det verst tenkelige øyeblikket.</p>\n<p><strong>Oppgave.</strong> De to beskrivelsene måtte kunne bevises å være identiske, automatisk, i stedet for at man med jevne mellomrom bare trodde at de var det.</p>\n<p><strong>Handling.</strong> Databasen er versjonert som 1 022 migreringer, nummerert fra 036 til 1102, og disiplinen rundt rekkefølgen er uglamorøs og ikke til forhandling. Det som får det til å holde, er en CI‑jobb som bygger databasen to ganger ved hver endring: én gang fra det ferske startskjemaet, én gang fra startskjemaet pluss alle migreringene spilt av i rekkefølge — og deretter sammenligner de to. Ikke bare strukturen, som er den enkle halvdelen, men også startdataene, fordi en migrering som etterfyller en oppslagstabell feil, er nøyaktig like skadelig som en som glemmer en kolonne, og bare én av dem vises i en skjemadiff. Ethvert avvik får byggingen til å feile, med forskjellen skrevet ut.</p>\n<p><strong>Resultat.</strong> Et nytt miljø og et langlivet miljø er den samme databasen, og det blir kontrollert i stedet for antatt. Kostnaden havner hos den som skriver en migrering: den må virke når den spilles av på nytt, og den må virke fra kald start, noe som krever mer omtanke enn en rask ALTER vanligvis får. Det er nettopp poenget — alternativet er å oppdage det under en gjenoppretting, når svaret betyr noe og det ikke er tid til å finne det ut.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Databaser",
        "Datastyring",
        "DevOps",
        "Migrering og modernisering",
        "Pålitelighet og sikkerhetskopier",
        "PostgreSQL",
        "Testing og kvalitetssikring",
        "Databaseadministrasjon (DBA)",
        "Databasemigrering og modernisering",
        "DevOps og CI/CD‑automatisering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-fail-closed-abuse-controls-and-rate-limiting-100/",
      "url": "https://engineer.company/nb/portfolio/built-fail-closed-abuse-controls-and-rate-limiting-100/",
      "title": "Bygde misbruksvern som stenger ved feil — 22 Redis‑baserte hastighetsbegrensere, Cloudflare Turnstile, idempotente forespørsler og en origin‑lås — slik at plattformen avviser boter og flom i stedet for å stole på dem som kaller den.",
      "summary": "Bygde misbruksvern som stenger ved feil: 22 Redis-baserte hastighetsbegrensere, Cloudflare Turnstile, idempotente forespørsler og en origin-lås på inngangen.",
      "content_html": "<p><strong>Situasjon.</strong> En offentlig katalog over bedrifter og fagfolk er et mål fra dagen den går live. Skrapere vil ha dataene, spamkontoer vil ha rekkevidden, og et endepunkt som koster plattformen ekte penger å betjene — søk, eksport, alt som berører et eksternt API — er verdt å misbruke rett og slett fordi det er gratis å kalle. Ingenting av dette er ondskap rettet mot akkurat denne plattformen; det er bakgrunnsvær på det åpne internettet.</p>\n<p><strong>Oppgave.</strong> De dyre og misbruksutsatte rutene trengte grenser som holder under press, også presset fra at ratebegrenserens egen avhengighet er utilgjengelig.</p>\n<p><strong>Handling.</strong> Det finnes 22 ratebegrensere, hver bygget for ruten den beskytter i stedet for ett globalt tak, fordi et innloggingsforsøk, et søk og en masseeksport blir misbruk ved helt ulike rater. Tilstanden ligger i Redis, slik at en grense deles på tvers av instanser i stedet for å gjelde per prosess og være lett å slippe unna. Den viktige beslutningen er hva som skjer når Redis ikke er der: ratebegrenserne feiler lukket (fail closed). Trafikk avvises i stedet for å slippes gjennom, noe som er det minst beleilige svaret og det eneste forsvarlige. Rundt dem ligger Cloudflare Turnstile på rutene som er verdt å utfordre, 351 linjer idempotensmellomvare slik at en skriving som prøves på nytt, ikke blir til to, en opprinnelseslås som avviser forespørsler som ikke kommer inn gjennom hoveddøren, og en skreddersydd utfordring på selve katalogen.</p>\n<p><strong>Resultat.</strong> Misbruk blir dyrt for misbrukeren og billig for plattformen, og et driftsavbrudd i ratebegrenserens eget lager ender i avvisning i stedet for i en åpen dør. Å feile lukket betyr riktignok at et Redis‑problem blir et problem brukerne ser — akseptert med vilje, fordi alternativet er at et Redis‑problem blir et faktureringsproblem.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Backend‑utvikling",
        "Nettverk og VPN",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Pålitelighet og overvåking",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-first-party-product-analytics-in-postgresql-101/",
      "url": "https://engineer.company/nb/portfolio/built-first-party-product-analytics-in-postgresql-101/",
      "title": "Bygde egen produktanalyse i PostgreSQL — 47 funksjoner over partisjonerte hendelsestabeller som rydder seg selv — pseudonymisert bak et roterende salt og betinget av den besøkendes samtykke.",
      "summary": "Bygde egen produktanalyse i PostgreSQL — 47 funksjoner over partisjonerte tabeller som rydder seg selv — pseudonymisert og betinget av samtykke.",
      "content_html": "<p><strong>Situasjon.</strong> Produktbeslutninger trenger tall, og den vanlige måten å skaffe dem på er å legge en tredjeparts sporingskode på hver side og la noen andres servere følge med på brukerne. Det er raskt, det er gratis ved lite volum, og det betyr at besøkendes atferd på en profesjonell nettverksplattform — hvem som så på hvilket selskap, hvem som søkte etter hva — blir en eiendel i hendene på en reklamevirksomhet. For en plattform der brukerne er identifiserbare maritime fagfolk, er det en dårlig byttehandel.</p>\n<p><strong>Oppgave.</strong> Produktteamet trengte trakter, retensjon og hendelsesdata, samlet inn på en måte plattformen kunne stå inne for og en bruker kunne avslå.</p>\n<p><strong>Handling.</strong> Analyseløsningen ble lagt i PostgreSQL, i et skjema med 7 tabeller og 47 funksjoner. Hendelsestabellene er partisjonert — 22 partisjonsdefinisjoner — og rydder seg selv etter en tidsplan, fordi kostnaden ved førstepartsanalyse ikke er å samle inn hendelsene, men å beholde dem for alltid. Identiteten er pseudonymisert bak et salt som roterer, slik at en besøkende ikke kan følges over rotasjonsgrensen, selv ikke fra innsiden av databasen; det er et bevisst tak på hva dataene kan svare på. Klienten tar hensyn til samtykke og sender ingenting før samtykke er gitt, i stedet for å sende og filtrere etterpå. Konverteringstrakter beregnes i databasen, ved siden av dataene, i stedet for å eksporteres et annet sted for så å kobles tilbake.</p>\n<p><strong>Resultat.</strong> Spørsmål om hvordan produktet brukes, besvares fra plattformens egne tabeller, uten at noe forlater plattformen og uten noen sporingskode på siden. Begrensningene er den ærlige delen: å rotere saltet koster kohortanalyse over lang tid, og ingenting av dette kommer med dashbordene man får gratis med et driftet verktøy. Begge deler ble akseptert med åpne øyne.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Data engineering",
        "Dataanalyse",
        "Databaser",
        "Datastyring",
        "PostgreSQL",
        "SQL",
        "Ytelsesoptimalisering",
        "Dataanalyse og BI‑dashbord",
        "Databasedesign og datamodellering",
        "Datastyring og datakvalitet",
        "Utvikling av datapipelines (ETL/ELT)"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-operator-back-office-for-the-platform-102/",
      "url": "https://engineer.company/nb/portfolio/built-the-operator-back-office-for-the-platform-102/",
      "title": "Bygde back office for operatørene — rundt 40 administrasjonsruter og 128 komponenter for eierskapskrav, moderering, funksjonsflagg, cache og diagnostikk — slik at plattformen kan driftes uten databasetilgang.",
      "summary": "Bygde backoffice-løsningen for operatører — rundt 40 adminruter og 128 komponenter for eierskapskrav, moderering, funksjonsflagg, cache og diagnostikk.",
      "content_html": "<p><strong>Situasjon.</strong> Hver plattform får i det stille en applikasjon nummer to, og det er som regel den ingen planlegger. Noen må godkjenne et eierskapskrav, skjule en omtale, slå på en funksjon for en del av brukerne, tømme en cache eller finne ut hvorfor én konto ser noe merkelig. Når den applikasjonen ikke finnes, er svaret en ingeniør med en databasekonsoll — noe som er tregt, uten logg, og én skrivefeil unna en hendelse.</p>\n<p><strong>Oppgave.</strong> Den daglige driften av plattformen måtte være mulig uten shell‑tilgang, slik at driften tilhørte den som hadde vakt, og ikke den som hadde påloggingsinformasjonen.</p>\n<p><strong>Handling.</strong> Backoffice‑løsningen endte på rundt 40 administrasjonsruter bygget av 128 komponenter, og den dekker arbeidet slik det faktisk kommer inn: avgjøre krav om eierskap til selskaper, moderere omtaler og innlegg, slå funksjonsflagg av og på, inspisere og tømme cacher, lese diagnostikk, og rapporteringen daglig leder ber om. Den kjører på det samme designsystemet og den samme genererte API‑klienten som det offentlige produktet, og det var det avgjørende valget — et internt verktøy bygget på sin egen teknologistack blir den delen ingen oppdaterer, og deretter den delen ingen stoler på.</p>\n<p><strong>Resultat.</strong> De som drifter plattformen, kan drifte den, og hver handling går gjennom den samme autorisasjonen og etterlater det samme sporet som alt annet. Det er en stor flate å holde testet og universelt utformet for et lite internt publikum, og den kostnaden er løpende. Det er likevel billigere enn alternativet, som er en ingeniør som skriver UPDATE mot produksjon klokken ni en søndag kveld.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Produkt og krav",
        "Systemadministrasjon",
        "UX/UI‑design",
        "Fullstack‑produktutvikling",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-company-ownership-claims-end-to-end-103/",
      "url": "https://engineer.company/nb/portfolio/built-company-ownership-claims-end-to-end-103/",
      "title": "Bygde løsningen for eierskapskrav til selskaper fra ende til annen — en bruker gjør krav på et selskap, en administrator avgjør saken, og en godkjenning skriver om autorisasjonsgrafen som bestemmer hvem som får redigere hva.",
      "summary": "Bygde eierskapskrav på selskaper fra ende til annen: En bruker fremmer kravet, en administrator avgjør det, og godkjenning skriver om autorisasjonsgrafen.",
      "content_html": "<p><strong>Situasjon.</strong> En katalog som er fylt fra offentlige kilder, har et strukturelt problem: selskapene i den har ikke lagt seg inn der selv. Før eller senere dukker noen fra et av dem opp og vil rette sin egen oppføring — og det finnes ingen kobling mellom den personen og den oppføringen, bare en påstand om at den finnes. Innvilges det for lett, redigerer en konkurrent selskapets side. Innvilges det for sakte, forblir katalogen feil.</p>\n<p><strong>Oppgave.</strong> Det måtte finnes en vei fra «dette er mitt selskap» til reell myndighet over oppføringen, med en menneskelig beslutning i midten og et spor bak.</p>\n<p><strong>Handling.</strong> Eierskapskrav ble bygget fra ende til annen, fordelt på 108 commits og begge applikasjonene. En bruker sender inn et krav med dokumentasjon; det havner i en kø i backoffice‑løsningen; en administrator gjennomgår det og godkjenner eller avviser det med en begrunnelse som går tilbake til den som fremmet kravet. Den interessante delen er hva en godkjenning gjør — den er ikke et flagg på en rad. Godkjenningen skriver om autorisasjonsgrafen, slik at kontoen får en reell relasjon til organisasjonen, og det er den samme relasjonen som hver tilgangssjekk i plattformen allerede konsulterer. Kontrollpunktet er avgjørelsen, ikke kodeveien, og ingen funksjon måtte lære om eierskapskrav for å respektere den.</p>\n<p><strong>Resultat.</strong> Selskaper kan ta over og rette sine egne oppføringer uten at noen redigerer databasen for hånd, og hver tildeling av myndighet har en navngitt godkjenner og en begrunnelse knyttet til seg. Menneskelig gjennomgang er flaskehalsen med vilje; en automatisk kontroll av et domenenavn ville vært raskere og ville tatt feil i nettopp de tilfellene som betyr mest.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Produkt og krav",
        "Sikkerhet",
        "Fullstack‑produktutvikling",
        "Produktstrategi og krav",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/established-a-continuous-security-programme-104/",
      "url": "https://engineer.company/nb/portfolio/established-a-continuous-security-programme-104/",
      "title": "Etablerte et kontinuerlig sikkerhetsprogram — kodeskanning, DAST, kontroll av avhengigheter og sårbarheter, SBOM‑generering, skanning etter hemmeligheter og SHA‑låste actions — sammen med 21 skriftlige sikkerhetsrevisjoner.",
      "summary": "Etablerte et kontinuerlig sikkerhetsprogram — kodeskanning, DAST, avhengighetskontroller, SBOM, hemmelighetsskanning og låste actions — pluss 21 revisjoner.",
      "content_html": "<p><strong>Situasjon.</strong> Sikkerhetsarbeidet i applikasjonen — Content Security Policy, databaseroller med minste privilegium, de gjentatte rettighetskontrollene — beskytter plattformen under kjøring. Ingenting av det sier noe om det som faktisk blir levert: om en avhengighet fikk en kjent sårbarhet sist tirsdag, om påloggingsinformasjon ble committet og så tilbakestilt, eller om en tredjeparts action som er låst til en tagg, i det stille har blitt en helt annen kodebit.</p>\n<p><strong>Oppgave.</strong> Sikkerheten i forsyningskjeden og koden måtte være kontinuerlig og automatisert, slik at tilstanden var et byggresultat og ikke en mening.</p>\n<p><strong>Handling.</strong> Statisk analyse kjøres gjennom CodeQL, dynamisk testing mot en kjørende instans gjennom ZAP, og Go‑avhengigheter kontrolleres med govulncheck. En stykkliste for programvaren genereres ved hver bygging med Anchore og Syft, slik at det som ble levert, er kjent i stedet for rekonstruert i ettertid. Gitleaks skanner historikken etter påloggingsinformasjon. Hver tredjeparts GitHub Action er låst til en commit‑hash i stedet for en tagg, og det er den uglamorøse kontrollen som hindrer at en tagg blir flyttet under føttene på en. Dependabot overvåker 6 økosystemer. Ved siden av automatiseringen ligger 21 skriftlige sikkerhetsrevisjoner, inkludert en trusselmodell og en vurdering opp mot OWASP‑testveiledningen — fordi skannere finner de problemklassene noen allerede har beskrevet, og en trusselmodell er der de som er spesifikke for denne plattformen, får et navn.</p>\n<p><strong>Resultat.</strong> En sårbarhet som offentliggjøres oppstrøms, dukker opp som en bygging som feiler, i stedet for som en nyhet. Mengden funn er den egentlige kostnaden — en skanner som rapporterer alt, lærer folk å ignorere den, og å holde signalet brukbart krever løpende triagering i stedet for et engangsoppsett.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "DevOps og CI/CD‑automatisering",
        "Sikkerhet og tilgangsstyring",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-loyalty-and-reputation-system-105/",
      "url": "https://engineer.company/nb/portfolio/built-the-loyalty-and-reputation-system-105/",
      "title": "Bygde lojalitets- og omdømmesystemet — 67 funksjoner over en hovedbok på 31 tabeller, med ligaer, merker og en innløsningsbutikk — med radlås på saldoen for å lukke vinduet for dobbeltforbruk.",
      "summary": "Bygde lojalitets- og omdømmesystemet — 67 funksjoner over en hovedbok på 31 tabeller, ligaer, merker og en butikk — med radlåsing på saldoen.",
      "content_html": "<p><strong>Situasjon.</strong> Profesjonell nettverksbygging har et kaldstartproblem: plattformen er verdt å bruke når andre allerede bruker den, og inntil da er det liten grunn til å komme tilbake. Den vanlige spaken er et belønningssystem — poeng for bidrag, status som gjenspeiler omdømme — som er lett å beskrive og lumsk å bygge, fordi i det øyeblikket poeng kan brukes, er de penger, og alle feilene som kan gjøres med penger, kan også gjøres her.</p>\n<p><strong>Oppgave.</strong> Bidrag måtte kunne måles og belønnes, med en saldo som ikke kunne brukes to ganger.</p>\n<p><strong>Handling.</strong> Lojalitetsskjemaet består av 31 tabeller og 67 funksjoner, med status i ytterligere 5 tabeller og 32 funksjoner, og skjemaer for oppdagelse og personalisering ved siden av som avgjør hva et gitt medlem ser. Oppå poengregnskapet ligger ligaer, merker og en innløsningsbutikk der en saldo blir til noe ekte. Delen som krevde omhu, er den eldste feilen i boken: sjekk saldoen, bruk den deretter, og to forespørsler som kommer samtidig, passerer begge sjekken. Hver endring tar en radlås på lommeboken før den leses, slik at den andre forespørselen venter til den første er ferdig i stedet for å kappes med den. At lommeboken ligger i den samme databasen som alt annet, er det som i det hele tatt gjør det mulig — saldoen og det den kjøpte, committes sammen eller ikke i det hele tatt.</p>\n<p><strong>Resultat.</strong> Bidrag blir målt og belønnet, og en saldo er aritmetikk i stedet for en tilnærming. Radlåsing er det tregere svaret og ble valgt likevel: konkurranse om en lommebok betyr at skrivingene må vente på tur, og alternativet er et medlem som bruker de samme poengene to ganger og noen som må avstemme det for hånd etterpå.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Databaser",
        "PostgreSQL",
        "Produkt og krav",
        "SQL",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-platform-s-social-layer-106/",
      "url": "https://engineer.company/nb/portfolio/built-the-platform-s-social-layer-106/",
      "title": "Bygde plattformens sosiale lag — innlegg, feed, grupper, nevninger, en følgergraf og et sammendrag av merkedager — på det samme funksjonsbaserte datalaget som resten av produktet.",
      "summary": "Bygde plattformens sosiale lag — innlegg, feed, grupper, nevninger, en følgergraf og et sammendrag av merkedager — på det samme funksjonsbaserte datalaget.",
      "content_html": "<p><strong>Situasjon.</strong> En katalog over selskaper er et oppslagsverk. Folk sjekker den og går. Det som gjør en profesjonell plattform verdt å vende tilbake til, er at andre mennesker er der — og det betyr innlegg, grupper og en grunn til å komme tilbake som ikke er en e‑post som oppfordrer til det.</p>\n<p><strong>Oppgave.</strong> Et sosialt lag måtte legges til uten at det ble et system nummer to med egne regler, egne tilganger og sin egen måte å lagre ting på.</p>\n<p><strong>Handling.</strong> Innlegg, en nyhetsstrøm, grupper, nevninger, en følgergraf og et sammendrag av merkedager kom på plass fordelt på 188 commits, alt sammen på det samme funksjonsorienterte datalaget som resten av plattformen. Den begrensningen gjorde det meste av jobben: følgergrafen er tabeller og funksjoner som alt annet, en nevning slås opp gjennom det samme identitetsskjemaet som katalogen bruker, og et innlegg arver modereringskøen som allerede var bygget for omtaler. Ingenting her trengte eget lager eller egen tilgangsmodell. Nyhetsstrømmen er det ene stedet som ga motstand, fordi det å sette sammen en personlig tidslinje effektivt er et genuint annet problem enn å hente en rad, og det er der personaliserings- og oppdagelsesarbeidet gjør seg fortjent til plassen sin.</p>\n<p><strong>Resultat.</strong> Plattformen har en grunn til å bli åpnet en dag da ingen trenger å slå opp et selskap, og den kom uten en parallell teknologistack som må vedlikeholdes. Om et sosialt lag er riktig investering for en maritim katalog, er et produktspørsmål snarere enn et ingeniørspørsmål — det er her fordi produktet ba om det, og det er bygget på samme måte som alt rundt det.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Databaser",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Produkt og krav",
        "Webutvikling",
        "Backend- og API‑utvikling",
        "Fullstack‑produktutvikling",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-hiring-marketplace-and-career-workspace-107/",
      "url": "https://engineer.company/nb/portfolio/built-the-hiring-marketplace-and-career-workspace-107/",
      "title": "Bygde rekrutteringsmarkedsplassen og karrierearbeidsflaten for sjøfolk — 151 lagrede funksjoner fordelt på 48 tabeller — med ledige stillinger, søknader, sertifikater, gradsopprykk og verifisert fartstid.",
      "summary": "Bygde jobbmarkedsplassen og karriereområdet for sjøfolk — 151 lagrede funksjoner i 48 tabeller — som matcher stillinger med dokumenterte kvalifikasjoner.",
      "content_html": "<p><strong>Situasjon.</strong> Det kommersielle argumentet for et maritimt fagnettverk er rekruttering: selskaper trenger mannskap og offiserer, og sjøfolk trenger hyre. Begge halvdelene fantes allerede på plattformen i feil form — selskapene lå i katalogen, fagfolkene hadde profiler, og det fantes ingenting som koblet en ledig stilling til personen som var kvalifisert til å fylle den. Kvalifikasjon er den vanskelige delen, fordi det i denne bransjen handler om sertifikater, rang og dokumentert fartstid snarere enn en stillingstittel.</p>\n<p><strong>Oppgave.</strong> Ledige stillinger, søknader og verifiserbare kvalifikasjonsbevis for sjøfolk måtte modelleres ordentlig i stedet for som fritekst på en profil.</p>\n<p><strong>Handling.</strong> To domener ble bygget. Rekrutteringssiden består av 32 tabeller og 118 funksjoner som dekker ledige stillinger, søknader, kortlister og arbeidsgiverens oversikt over en kandidatpipeline. Karrierearbeidsområdet har 16 tabeller og 33 funksjoner med sertifikater, rangprogresjon og fartstid, med dokumentopplasting og en gjennomgangskø slik at et kvalifikasjonsbevis blir kontrollert i stedet for bare påstått. Å modellere progresjon som en graf i stedet for en liste er det som gjør matchingen nyttig — én rang kan nås fra en annen gitt visse sertifikater og nok registrert tid til sjøs, og det er den strukturen som gjør at en ledig stilling kan matches mot en karriere i stedet for mot et nøkkelord. Karrierearbeidsområdet leveres bak et funksjonsflagg og er ikke fullt lansert; rekrutteringssiden er i drift.</p>\n<p><strong>Resultat.</strong> En ledig stilling kan matches mot dokumenterte kvalifikasjoner i stedet for en selvbeskrevet stillingstittel, og det er hele forskjellen mellom en jobbtavle og et rekrutteringsverktøy i denne bransjen. Verifisering er flaskehalsen, med vilje — en gjennomgangskø skalerer ikke slik en automatisk kontroll ville gjort, og en automatisk kontroll ville sertifisert folk som ikke burde vært sertifisert.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Databaser",
        "Datastyring",
        "Fullstack‑utvikling",
        "PostgreSQL",
        "Produkt og krav",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Fullstack‑produktutvikling",
        "Produktstrategi og krav"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-the-companys-infrastructure-as-code-108/",
      "url": "https://engineer.company/nb/portfolio/built-the-companys-infrastructure-as-code-108/",
      "title": "Bygde selskapets egen infrastruktur som 19 Ansible‑playbooks og 34 roller på 12 065 linjer YAML, som konvergerer en vert i drift mot en deklarert tilstand der hver play er idempotent.",
      "summary": "Driftsmiljøet kan gjenskapes fra repoet, og de delene av det som bare var sanne fordi noen husket dem, er nå påstander som får en kjøring til å feile.",
      "content_html": "<p><strong>Situasjon.</strong> Engineer ApS driver sitt eget driftsmiljø — en tilstedeværelse på nettet, en git‑forge, en database, sikkerhetskopier, e‑posttransport og DNS — og det fantes ingen å overlate driften til. Et selskap med én person har de samme feilmodusene som et stort selskap og ingenting av redundansen, noe som gjør det vanlige svaret, en person som husker hvordan verten ble satt opp, til det minst tilgjengelige alternativet som finnes.</p>\n<p><strong>Oppgave.</strong> Hele driftsmiljøet måtte beskrives i et repo i stedet for i et hode, og beskrives i en form som konvergerer en ekte vert i stedet for å dokumentere den.</p>\n<p><strong>Handling.</strong> Ut av dette vokste det frem 19 playbooks og 34 roller fordelt på 12 065 linjer YAML. Formen betyr mer enn størrelsen. Sammensetning er data i stedet for flagg: en vert ligger i en nivågruppe der variablene deklarerer hvilke roller den kjører, slik at klargjøring uten argumenter konvergerer hver vert til sin deklarerte tilstand. Idempotens er en kontrakt og ikke et ønske — en konvergert vert rapporterer null endringer, og en play som ikke kan si det, er ikke ferdig. Fire kommandonavnerom holder løftene fra hverandre: en kvalitetsport som ikke berører noen vert, rapporter som leser én og aldri endrer den, klargjøring som endrer en vert slik at den samsvarer med repoet, og verifisering som endrer en vert med vilje og returnerer en dom.</p>\n<p><strong>Resultat.</strong> Driftsmiljøet kan gjenskapes fra repoet, og de delene av det som bare var sanne fordi noen husket dem, er nå påstander som får en kjøring til å feile. Kostnaden er reell: hver endring tar lengre tid å gjøre enn det ville tatt å redigere en fil på serveren, og en konvergering som bare blir halvveis gjennomført, er verre enn en som nekter, og det er derfor en preflight‑play senere ble lagt inn foran den. Den avveiningen ble gjort bevisst, og den har holdt.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Infrastruktur",
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "Systemadministrasjon",
        "DevOps og CI/CD‑automatisering",
        "Infrastruktur som kode",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/ran-the-whole-company-on-one-512mb-host-109/",
      "url": "https://engineer.company/nb/portfolio/ran-the-whole-company-on-one-512mb-host-109/",
      "title": "Driftet hele selskapet på én vert med én kjerne og 512 MB — en git‑forge, en webserver for sju domener, Tor, to servere for alternative protokoller, sikkerhetskopier og utestenging av inntrengere — ved å behandle 464 MB brukbart minne som den bindende arkitekturbegrensningen.",
      "summary": "Hele selskapet kjører på en maskin som koster mindre per måned enn en lunsj, og designet er bedre takket være disiplinen, ikke bare billigere.",
      "content_html": "<p><strong>Situasjon.</strong> Selskapets produksjonsvert er en skyinstans med én kjerne, 512 MB minne og 10 GB disk, hvorav rundt 464 MB er brukbart. Alt virksomheten kjører offentlig, ligger på den: webserveren som terminerer TLS for sju domener, git‑forgen, en Tor‑onion‑tjeneste, en Gemini‑server, en Gopher‑server, krypterte sikkerhetskopier og utestenging av inntrengere. Den vanlige reaksjonen på den listen er å kjøpe en større maskin.</p>\n<p><strong>Oppgave.</strong> Begrensningen måtte behandles som et arkitektonisk premiss i stedet for et problem å bruke penger på, fordi det ærlige spørsmålet ikke var om en større maskin ville fungere, men om designet trengte en.</p>\n<p><strong>Handling.</strong> Minne ble argumentet som avgjorde beslutninger. Det finnes ingen overvåkingsagent, ingen metrikkpipeline og intet dashbord — rapportering er pull‑basert, sju kommandoer som leser verten og gjengir Markdown, endrer ingenting og bare kjører når noen ber om det. Tjenestestyreren er systemd i stedet for enda en prosessbehandler lagt oppå den, og containerlaget er Quadlet‑enheter under den samme tjenestestyreren i stedet for en daemon med sin egen. Plattformer med webpanel ble utelukket allerede på designstadiet av samme grunn. Da spørsmålet kom opp om verten kunne bære en onion‑tjeneste, kom svaret fra en dags målte stikkprøver i stedet for fra en mening: tilgjengelig minne falt aldri under rundt 310 MB av 464, swap lå på 2,6 prosent, og prosessoren var 99,7 prosent ledig.</p>\n<p><strong>Resultat.</strong> Hele selskapet kjører på en maskin som koster mindre per måned enn en lunsj, og designet er bedre takket være disiplinen, ikke bare billigere. Prisen er marginen for alt som er slurvete — e‑post ligger med vilje ikke på denne maskinen i det hele tatt — den er skrevet som et installasjonsstillas som venter på en egen vert, fordi en e‑postserver trenger margin som denne maskinen allerede har brukt opp, og det er skrevet ned i stedet for å bli oppdaget senere.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Løsningsarkitektur",
        "Pålitelighet og sikkerhetskopier",
        "Plattformarkitektur",
        "Systemadministrasjon",
        "Ytelsesoptimalisering",
        "Infrastruktur som kode",
        "Plattform- og løsningsarkitektur",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/found-three-ssh-brute-force-protections-that-never-worked-110/",
      "url": "https://engineer.company/nb/portfolio/found-three-ssh-brute-force-protections-that-never-worked-110/",
      "title": "Fant og rettet tre SSH‑vern mot brute force som aldri hadde virket: en jail for utestenging som overvåket port 22 mens daemonen lyttet på 1986, en hastighetsgrense skygget av en bredere regel over den, og en utestengingshandling der binærfilen aldri ble funnet, slik at ingen utestenging noen gang hadde trådt i kraft.",
      "summary": "Tre beskyttelser som ikke én gang hadde slått til, gjør det nå, og den klassen feil de tilhører — en kontroll der feilmodusen er at den fortsetter å melde at…",
      "content_html": "<p><strong>Situasjon.</strong> En herdingsgjennomgang av produksjonsverten i august 2026 stilte et spørsmål som vanligvis får et selvsikkert svar: virker beskyttelsene mot brute force‑angrep på SSH. Alle tre var konfigurert, alle tre dukket opp i hver rapport noen så på, og alle tre hadde vært virkningsløse siden dagen verten ble bygget.</p>\n<p><strong>Oppgave.</strong> Kontrollene måtte sjekkes mot det kjernen faktisk gjør med en pakke, i stedet for mot konfigurasjonsfilene som beskriver hva som burde skje med den.</p>\n<p><strong>Handling.</strong> Å lese konfigurasjonen ville ha bekreftet feil svar tre ganger, så gjennomgangen leste det kjørende systemet i stedet. Jailen for utestenging av inntrengere overvåket port 22, mens daemonen var flyttet til 1986 under den første herdingen — hver utestenging den skrev, gjaldt en port ingenting lyttet på. Brannmurens ratebegrensning var verre på en mer subtil måte: regelen fantes, og den lå under en bredere regel som slo til først. Brukerregler i brannmuren evalueres ovenfra og ned, og første treff vinner, så en bred tillatelse over en ratebegrensning gjør begrensningen til død kode som fortsatt skrives ut i hver statusoversikt. Den tredje var den stilleste av dem: utestengingshandlingen starter via skallet et pakkefilterprogram som pakkesystemet bare anbefaler i stedet for å kreve, så på en vert uten det starter jailen, teller og avgjør, og feiler så i det ene øyeblikket den prøver å stenge ute. Alle tre rettelsene var små. Det som kom ut av det, var ikke rettelsen, men to regler som nå styrer repoet: en sikkerhetskontroll får en påstand i stedet for en kommentar, og en brannmur verifiseres ut fra regelens plassering i stedet for ut fra at regelen finnes.</p>\n<p><strong>Resultat.</strong> Tre beskyttelser som ikke én gang hadde slått til, gjør det nå, og den klassen feil de tilhører — en kontroll der feilmodusen er at den fortsetter å melde at alt er i orden — er den klassen plattformens sjekker nå er bygget for å fange. Alle tre hadde vært virkningsløse fra første oppsett. Alle tre ble vist som friske overalt der noen så, og det er hele grunnen til at de varte.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Overvåking og observerbarhet",
        "Sikkerhet",
        "Systemadministrasjon",
        "Nettverk og VPN‑oppsett",
        "Pålitelighet og overvåking",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/hardened-ssh-with-three-stage-validation-111/",
      "url": "https://engineer.company/nb/portfolio/hardened-ssh-with-three-stage-validation-111/",
      "title": "Herdet SSH til 24 kontrollerte direktiver med validering i tre trinn — kandidatfilen, den sammensatte konfigurasjonen og så daemonens egen tilbakelesing — etter at tilbakelesingen avslørte at den kjørende serveren i det stille overstyrte to av de tjuefire.",
      "summary": "Sikkerheten i SSH-oppsettet er nå daemonens svar og ikke repoets påstand, og forskjellen er ikke teoretisk — den var allerede to innstillinger bred da sjekken…",
      "content_html": "<p><strong>Situasjon.</strong> SSH er den eneste interaktive veien inn til selskapets vert, og konfigurasjonen skrives av en automatiseringsrolle som i månedsvis hadde kjørt uten feil. En tilgangsrapport i september 2026 leste daemonens egne effektive innstillinger og fant at to av dem ikke stemte med det rollen hadde skrevet ved hver eneste kjøring.</p>\n<p><strong>Oppgave.</strong> Herdingen måtte bli noe daemonen bekrefter, ikke noe repoet påstår, fordi avviket mellom de to allerede hadde stått åpent i månedsvis uten at noen la merke til det.</p>\n<p><strong>Handling.</strong> Årsaken var rekkefølgen i konfigurasjonen. Operativsystemet leverer sine egne standardverdier ukommentert, over stedet der en drop‑in‑fil havner, og for de to innstillingene det gjaldt, er det den første forekomsten som vinner. Løsningen var et filnavnprefiks som sorteres foran leverandørens, en endring på ett eneste tegn og nøyaktig den typen feil som blir stående fordi ingen kommer på å se etter den. Det som ble bygget rundt den, betyr mer: validering i tre trinn ved hver kjøring. Kandidatfilen syntakssjekkes før den installeres, slik at en ugyldig konfigurasjon aldri når verten. Den sammensatte konfigurasjonen sjekkes etter installasjonen. Deretter leses daemonens egne effektive verdier tilbake, og 24 direktiver kontrolleres mot dem, slik at en innstilling som er skrevet, men overstyrt, får kjøringen til å feile. To direktiver ble bevisst utelatt, begge fordi daemonen ikke lenger implementerer dem, og å skrive dem ville bare sett grundig ut.</p>\n<p><strong>Resultat.</strong> Sikkerheten i SSH‑oppsettet er nå daemonens svar og ikke repoets påstand, og forskjellen er ikke teoretisk — den var allerede to innstillinger bred da sjekken ble skrevet. En kjøring som ikke kan se om et sikkerhetstiltak faktisk trådte i kraft, har ikke verifisert det, og et søk med grep etter et direktiv beviser bare at det ble skrevet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Linux og servere",
        "Sikkerhet",
        "Systemadministrasjon",
        "Testing og kvalitetssikring",
        "Infrastruktur som kode",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/proved-the-intrusion-banning-path-on-every-converge-112/",
      "url": "https://engineer.company/nb/portfolio/proved-the-intrusion-banning-path-on-every-converge-112/",
      "title": "Beviste at utestengingen virker fra ende til annen ved hver herdingskjøring ved å utestenge en reservert testadresse, lese den resulterende kjerneregelen og oppheve utestengingen i en garantert oppryddingsblokk, slik at en jail som slutter å virke, får kjøringen til å feile i stedet for å melde at alt er i orden.",
      "summary": "En blokkeringsvei som slutter å virke, får nå kjøringen til å feile i stedet for å fortsette å melde at alt er friskt, og det var den eneste egenskapen som…",
      "content_html": "<p><strong>Situasjon.</strong> Tjenesten som blokkerer inntrengere, var allerede blitt tatt i å blokkere en port som SSH‑daemonen ikke lyttet på. Å rette porten lukket det ene tilfellet. Det gjorde ingenting med grunnen til at feilen overlevde så lenge, nemlig at blokkeringsveien ikke har noen synlig feiltilstand: tjenesten kjører, jailen står oppført som aktiv, og ingenting noe sted sier om en blokkering noen gang når frem til kjernen.</p>\n<p><strong>Oppgave.</strong> Blokkeringsveien måtte prøves ved hver kjøring, mot det aktive regelsettet, i stedet for å bli utledet av at tjenesten var oppe.</p>\n<p><strong>Handling.</strong> Kjøringen blokkerer nå en adresse fra blokken standardene reserverer for dokumentasjon, leser tilbake den resulterende regelen i kjernens pakkefilter og opphever blokkeringen i en oppryddingsblokk som kjører enten sjekken besto eller feilet. Det reserverte adresseområdet er det bærende valget — testadressen tilhører ingen, så en forvillet blokkering som overlever en mislykket kjøring, kan ikke stenge ute et virkelig nettverk. Tilbakelesingen fra kjernen er den andre halvdelen: tjenestens egen statusutdata ville meldt suksess for en blokkering som ikke ga noen regel, og det er nettopp den feilen det testes for. Ved siden av er backenden for blokkering låst fast i stedet for å overlates til tjenestens automatiske gjenkjenning, og backenden for logger er satt til automatisk gjenkjenning, fordi operativsystemet ikke leverer noen tradisjonell autentiseringslogg, og et feil valg svikter i det stille ved å overvåke en fil som aldri dukker opp.</p>\n<p><strong>Resultat.</strong> En blokkeringsvei som slutter å virke, får nå kjøringen til å feile i stedet for å fortsette å melde at alt er friskt, og det var den eneste egenskapen som betydde noe. Det koster noen sekunder ved hver kjøring, og hver gang skrives og trekkes en brannmurregel tilbake i produksjon, noe som er et reelt inngrep i et levende system og ble akseptert fordi alternativet allerede hadde vist seg å være verre.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "Pålitelighet og overvåking",
        "Sikkerhet og tilgangsstyring",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/verified-firewall-rules-by-position-113/",
      "url": "https://engineer.company/nb/portfolio/verified-firewall-rules-by-position-113/",
      "title": "Verifiserte brannmurregler etter plassering i stedet for tilstedeværelse ved å lese den nummererte regellisten og den aktive pakkefilterkjeden, fordi en regel som finnes, ikke er det samme som en regel en pakke faktisk når.",
      "summary": "Brannmuroppsettet sjekkes nå slik en pakke opplever det. Det nyttigste utbyttet var ikke sjekken, men det den avslørte om rapporteringen før den: alle…",
      "content_html": "<p><strong>Situasjon.</strong> En statusoversikt fra brannmuren viser et sett med regler. Et pakkefilter evaluerer en ordnet liste og stopper ved første treff. Det er to forskjellige ting, og forskjellen er usynlig i alle verktøy som skriver ut et sammendrag — og det var slik verten gikk i et år med en hastighetsbegrensning som ingen pakke noen gang nådde, plassert under en bredere regel som slo til først.</p>\n<p><strong>Oppgave.</strong> Verifiseringen av brannmuren måtte lese posisjon, ikke medlemskap, fordi det allerede var vist at tilstedeværelse ikke beviste noe.</p>\n<p><strong>Handling.</strong> Sjekkene leser nå den nummererte regeloversikten og den levende filterkjeden og kontrollerer mot rekkefølgen. Det finnes nøyaktig én regel per port, og alltid med protokoll, fordi en regel uten protokoll utvider flaten i det stille. SSH‑porten har en hastighetsbegrensning og aldri en allow‑regel ved siden av, siden en allow‑regel over en begrensning er nettopp den skyggingen som ble funnet. Den deklarerte offentlige flaten er en kort liste med fem porter, kontrollert som en helhet og ikke enkeltvis, slik at en port som dukker opp uten å være deklarert, får sjekken til å feile i stedet for å måtte bli oppdaget. Sikkerhetsrapporten viser reglene i treffrekkefølge, med en advarsel øverst i seksjonen som forklarer hvordan den skal leses, fordi den neste som åpner filen, ellers vil lese den som et sett.</p>\n<p><strong>Resultat.</strong> Brannmuroppsettet sjekkes nå slik en pakke opplever det. Det nyttigste utbyttet var ikke sjekken, men det den avslørte om rapporteringen før den: alle verktøyene som var involvert, hadde skrevet ut hastighetsbegrensningen i et år, helt korrekt, og ingen av dem hadde fått det eneste spørsmålet som betydde noe.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Linux og servere",
        "Nettverk og VPN",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "Nettverk og VPN‑oppsett",
        "Sikkerhet og tilgangsstyring",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-encrypted-backups-with-a-monthly-restore-drill-114/",
      "url": "https://engineer.company/nb/portfolio/built-encrypted-backups-with-a-monthly-restore-drill-114/",
      "title": "Bygde krypterte sikkerhetskopier utenfor verten med restic, med opprydding etter lagringstid, integritetskontroll og en månedlig automatisert gjenopprettingsøvelse, gjennomgikk deretter gjenopprettingsberedskapen og skrev ned hullene i stedet for å la dem bli oppdaget under en hendelse.",
      "summary": "Selskapet kan miste verten og få dataene sine tilbake, og den setningen hviler på en gjenoppretting som kjørte forrige måned, ikke på en sikkerhetskopi som…",
      "content_html": "<p><strong>Situasjon.</strong> Alt selskapet har — git‑repoene, databasen, nettstedet — lå på én skyinstans der leverandørens øyeblikksbilde var hele gjenopprettingsberedskapen. Et øyeblikksbilde hos leverandøren er fint å ha og dårlig å stole på: det ligger i samme konto som maskinen det beskytter, det er ikke kryptert av noen her, og ingen hadde noen gang gjenopprettet fra et.</p>\n<p><strong>Oppgave.</strong> Sikkerhetskopiene måtte være krypterte, ligge utenfor verten, ryddes etter en oppbevaringsregel og — den delen som vanligvis hoppes over — faktisk brukes til gjenoppretting, etter en fast plan, uten at noen må huske å gjøre det.</p>\n<p><strong>Handling.</strong> Sikkerhetskopieringen kjører på en systemd‑timer: en databasedump der det finnes en database, deretter et kryptert, deduplisert øyeblikksbilde til lagring hos en annen leverandør over SFTP, deretter opprydding etter oppbevaringsregelen, deretter en integritetssjekk. En dødmannsknapp pinges bare når alt lykkes, og det er det skillet som gjør den til en alarm og ikke en logg — en kjøring som feiler, sier ingenting, og nettopp tausheten utløser varselet. Separat kjører en gjenopprettingsøvelse hver måned: den henter en kjent fil ut av repoet og sammenligner den, slik at det som sjekkes, er en gjenoppretting og ikke en sikkerhetskopi. Passfrasen skrives til en fil som enhetene leser, i stedet for å sendes gjennom miljøet, fordi tjenestestyreren tolker escape‑sekvenser i miljøverdier, og en passfrase med en omvendt skråstrek ville i det stille blitt en annen enn den som opprettet repoet. Etterpå ble gjenopprettingsberedskapen revidert og dokumentert, og hullene som gjensto, ble navngitt i dokumentet i stedet for å bli oppdaget under en hendelse.</p>\n<p><strong>Resultat.</strong> Selskapet kan miste verten og få dataene sine tilbake, og den setningen hviler på en gjenoppretting som kjørte forrige måned, ikke på en sikkerhetskopi som kjørte i natt. Revisjonens mest verdifulle resultat var listen over det som fortsatt ikke er dekket, og det er nettopp den delen en grønn sikkerhetskopirapport strukturelt er ute av stand til å fortelle noen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Infrastruktur",
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Infrastruktur som kode",
        "Pålitelighet og overvåking",
        "Sikkerhetskopiering og gjenoppretting",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-dead-mans-switch-monitoring-115/",
      "url": "https://engineer.company/nb/portfolio/built-dead-mans-switch-monitoring-115/",
      "title": "Bygde overvåking med dødmannsknapp som bare sender ping så lenge minne og disk er i orden, slik at en svekket vert utløser et varsel ved å bli stille — og fanget opp seks variabelnavn som sa «free» der kontrollen korrekt målte «available», en størrelsesorden fra hverandre på en maskin med 464 MB.",
      "summary": "Verten har nå en alarm som går av når den selv svikter, og navngivningen som ville ha ødelagt den, er rettet.",
      "content_html": "<p><strong>Situasjon.</strong> En vert på 464 MB som kjører en forge, en database og en webserver, har to realistiske måter å dø på: den går tom for minne, eller den går tom for disk. Ingen av dem varsler om seg selv. Begge er fullt forutsigbare noen timer i forveien hvis noe følger med, og ingenting gjorde det.</p>\n<p><strong>Oppgave.</strong> Verten trengte en alarm som virker når verten ikke gjør det — og det utelukker alt som må sende en melding i det øyeblikket feilen inntreffer.</p>\n<p><strong>Handling.</strong> Svaret er en dødmannsknapp på en timer. Hvert femtende minutt, med jitter, måler et lite skript tilgjengelig minne og ledig disk og pinger en ekstern tjeneste bare hvis begge ligger over tersklene sine. Tausheten er varselet. En maskin som har gått tom for minne, mistet nettverket eller sluttet å starte, gir nøyaktig det samme signalet som en som er usunn, og det er riktig oppførsel og grunnen til at denne formen ble valgt fremfor en agent som rapporterer en status. Målingen er tilgjengelig minne, ikke ledig minne, og det skillet ble den mest lærerike delen av arbeidet: skriptet hadde lest riktig kolonne hele tiden, mens seks variabelnavn rundt det sa «free». På en frisk Linux‑maskin er ledig minne nær null fordi kjernen bruker minne som ellers står ubrukt, til cache, så en leser som holdt de navnene opp mot en terskel på ti prosent, ville sett tolv megabyte ledig på en maskin med 464 MB, konkludert med at sjekken var ødelagt, og rettet den med en endring på ett tegn som gjør en fungerende alarm om til en som slår ut hele tiden og blir dempet i løpet av en uke.</p>\n<p><strong>Resultat.</strong> Verten har nå en alarm som går av når den selv svikter, og navngivningen som ville ha ødelagt den, er rettet. Dødmannsknappens reelle begrensning står i dens egen dokumentasjon: den beviser at maskinen er frisk, ikke at nettstedet svarer, og det er to forskjellige spørsmål.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Linux og servere",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Ytelsesoptimalisering",
        "Infrastruktur som kode",
        "Pålitelighet og overvåking",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/made-ansible-check-mode-tell-the-truth-116/",
      "url": "https://engineer.company/nb/portfolio/made-ansible-check-mode-tell-the-truth-116/",
      "title": "Fikk check‑modus til å fortelle sannheten på tvers av hele plattformen etter å ha funnet seks sonder som avgjorde ut fra en verdi verten aldri hadde gitt, fordi Ansibles command‑modul melder suksess under --check og samtidig hopper helt over kommandoen.",
      "summary": "En tørrkjøring måler nå enten noe eller sier at den ikke gjorde det, og ingen av delene er det tredje alternativet den hadde før.",
      "content_html": "<p><strong>Situasjon.</strong> En tørrkjøring mot produksjon skal være den trygge måten å finne ut hva en endring vil gjøre på. I september 2026 feilet en tørrkjøring på en assert som rett og slett tok feil om verten, og rådet operatøren til å sette et flagg som ville ha lempet på en sikkerhetssandkasse. Tørrkjøringen hadde ikke lest verten. Den hadde lest en verdi verten aldri ga, og trukket en konklusjon av den.</p>\n<p><strong>Oppgave.</strong> Hver probe med et resultat som ligger til grunn for en beslutning, måtte gjøres ærlig i check mode, og de som ikke kunne gjøres det, måtte si det høyt i stedet for å tie.</p>\n<p><strong>Handling.</strong> Årsaken er en egenskap ved verktøyet som er dokumentert og lett å glemme: command‑modulen kjører ikke i check mode, og det den registrerer, er ikke et tomt resultat — det er en suksess med tom utdata. Enhver betingelse som leser den registrerte verdien, avgjør derfor på grunnlag av noe som aldri ble målt, og den avgjør i den retningen dens egen logikk tilfeldigvis peker. En gjennomsøking av alle registrerte prober fant seks av dem, og hver løy på sin måte: én meldte at tjenestestyreren godtar alle enhetsdirektiver uten å ha spurt tjenestestyreren, en annen meldte at det ikke var noe å gjøre på en vert der brannmuren var slått av. Hver fikk én av to former. En probe som leser tilstand som fantes fra før, og som kjøringen ikke har rørt, er merket for å kjøre også i check mode. En probe som ikke kan kjøre, hoppes over, og en melding navngir hva som konkret ikke ble verifisert — fordi taushet i en kjørelogg ser akkurat ut som en bestått sjekk.</p>\n<p><strong>Resultat.</strong> En tørrkjøring måler nå enten noe eller sier at den ikke gjorde det, og ingen av delene er det tredje alternativet den hadde før. Den generelle regelen gikk inn i utviklingsveiledningen i samme endring: check mode skal ikke lyve, og en probe som ikke kan se, er pålagt å melde fra om blindheten sin i stedet for å utlede en dom av den.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Pålitelighet og sikkerhetskopier",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Infrastruktur som kode",
        "Pålitelighet og overvåking"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/added-a-preflight-play-for-secrets-117/",
      "url": "https://engineer.company/nb/portfolio/added-a-preflight-play-for-secrets-117/",
      "title": "La til en preflight‑play som kjører den samme koden som konvergeringen mot operatørens lokale hemmeligheter på omtrent ett sekund, etter at en halvveis gjennomført produksjonskjøring stoppet på den niende oppgaven med swap‑innstillingene allerede skrevet til verten i drift.",
      "summary": "En hemmelighet som ikke lar seg slå opp, koster nå en nektelse på to sekunder på en bærbar maskin i stedet for en halvveis gjennomført endring på en levende…",
      "content_html": "<p><strong>Situasjon.</strong> Driftshemmelighetene var nettopp blitt delt opp slik at hver vert har sitt eget sikkerhetskopirepo, sin egen passfrase og sin egen varslingsknapp. Koden i repoet var riktig. Operatørens krypterte vault inneholdt fortsatt den gamle verdien fra da det bare fantes én vert, og ingenting kunne se avviket, fordi vaulten med vilje ligger lokalt på operatørens maskin og er usynlig for repoets egen kvalitetsport.</p>\n<p><strong>Oppgave.</strong> Den typen feil trengte et sted å feile billig, fordi den nettopp hadde feilet dyrt.</p>\n<p><strong>Handling.</strong> Den første kjøringen etter endringen koblet seg til den levende verten, kjørte åtte oppgaver og nektet på den niende — riktig nok, men fra innsiden av en kjøring som allerede hadde skrevet swap‑innstillinger til produksjon. En halvveis gjennomført kjøring er et dårligere svar enn en nektelse, så det ble skrevet et preflight‑play: det kontakter ingen vert i det hele tatt, kjører lokalt, samler ingen facts og kjører de samme to oppgavefilene som den virkelige kjøringen bruker, mot operatørens vault. Det svarer på omtrent et sekund, og provisjoneringen kjører det først. Den bærende beslutningen er at det er den samme koden og ikke en ny implementasjon av den samme regelen, fordi en sjekk som gjentar en regel på et annet språk, før eller siden blir uenig med den, og blir det i det stille. Mens det ble skrevet, dukket det opp en felle det nesten selv skapte: facts som settes i løpet av et play, lever lenger enn playet, så å lenke preflight og kjøring sammen i én og samme kommando ville ha gitt den virkelige rollen en passfrase som preflighten allerede hadde slått opp, og latt sjekken bestå på en vault som fortsatt var feil. Fellen ble reprodusert før den ble sikret mot, i begge oppgavefilene.</p>\n<p><strong>Resultat.</strong> En hemmelighet som ikke lar seg slå opp, koster nå en nektelse på to sekunder på en bærbar maskin i stedet for en halvveis gjennomført endring på en levende server. Preflighten er med vilje ikke tagget til alltid å kjøre og er med vilje ikke serialisert, og begge deler er dokumentert som beslutninger og ikke som standardvalg.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Infrastruktur som kode",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/reconciled-a-dns-zone-declaratively-118/",
      "url": "https://engineer.company/nb/portfolio/reconciled-a-dns-zone-declaratively-118/",
      "title": "Avstemte en DNS‑sone med 20 poster deklarativt mot Cloudflare‑API‑et med egne inngangspunkter for revisjon og BIND‑eksport, og slo CDN‑proxyen av igjen av personvernhensyn etter å ha bygd den.",
      "summary": "Sonen er versjonert, sammenlignbar og eksporterbar, og den ene beslutningen som gikk imot det åpenbare standardvalget, er skrevet ned med begrunnelse, slik at…",
      "content_html": "<p><strong>Situasjon.</strong> Selskapets DNS hadde omtrent tjue poster fordelt på en nettilstedeværelse, et git‑underdomene, to personlige omdirigeringer, e‑postruting gjennom en leverandør av hostede postbokser, et domene for transaksjonell utsending og autentiseringspostene for begge. Alt sammen lå i en leverandørs nettkonsoll, og det betyr at den gjeldende tilstanden var det den siste som klikket, hadde etterlatt seg.</p>\n<p><strong>Oppgave.</strong> Sonen måtte bli en deklarasjon i repoet, med en måte å sammenligne den deklarasjonen med det leverandøren faktisk serverer.</p>\n<p><strong>Handling.</strong> Sonen er skrevet som data — aktive poster, applikasjonsposter, poster for transportpolicy og en egen liste over poster som venter på å bli fjernet, som er den ærlige måten å registrere en sletting som ennå ikke har skjedd. Ett play avstemmer den mot leverandørens API. To inngangspunkter til ligger bak tagger som må slås på eksplisitt: en revisjon som rapporterer forskjellen uten å endre noe, og en eksport som skriver ut sonen i standard sonefilformat, slik at den kan leses av noe annet enn dette repoet. Den mest interessante beslutningen var en omgjøring. Proxying av de to webflatene gjennom leverandørens innholdsnettverk ble bygget, virket og ble så slått av igjen — fordi det koster den ene setningen selskapets personvernside handler om. Med en proxy foran håndterer et annet selskap hver besøkendes adresse og hver URL de ber om, før selskapet selv gjør det, under det andre selskapets retningslinjer og ikke dette selskapets egne. For en bedrift som skiller seg ut ved å hevde at ingen ser på, er det en dårligere byttehandel enn angrepene den beskytter mot.</p>\n<p><strong>Resultat.</strong> Sonen er versjonert, sammenlignbar og eksporterbar, og den ene beslutningen som gikk imot det åpenbare standardvalget, er skrevet ned med begrunnelse, slik at ingen åpner diskusjonen på nytt ved et uhell. Revisjonsveien er den delen som brukes mest, fordi det oftere er forskjellen man vil vite, enn at den skal lukkes.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Nettverk og VPN",
        "Python",
        "Sikkerhet",
        "Infrastruktur som kode",
        "Nettverk og VPN‑oppsett",
        "Sikkerhet og tilgangsstyring",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/cut-systemd-sandbox-exposure-across-every-unit-119/",
      "url": "https://engineer.company/nb/portfolio/cut-systemd-sandbox-exposure-across-every-unit-119/",
      "title": "Reduserte eksponeringen i systemd‑sandkassen for hver enhet plattformen installerer — tjenesten for dødmannsknappen fra 9,6 UNSAFE til 1,5, en internetteksponert git‑forge fra 8,3 EXPOSED til 1,5 — og la til en parserkontroll ved konvergering etter å ha funnet et feilstavet direktiv som ble ignorert i det stille i tre enhetsmaler.",
      "summary": "Hver enhet kjører med de privilegiene den trenger og ingen andre, tallene er målt og ikke påstått, og den typen feil der en skrivefeil blir et sikkerhetstiltak…",
      "content_html": "<p><strong>Situasjon.</strong> Tretten enhetsfiler var installert av dette repoet og kjørte på produksjonsverten med de privilegiene tjenestestyreren gir som standard, og det er de fleste. En herdingsscore satte tjenesten for dødmannsknappen til 9,6 og kalte den usikker; den internetteksponerte git‑forgen fikk 8,3 og betegnelsen eksponert. Begge tallene var korrekte, og ingen av dem hadde ført til noe, fordi en score uten en terskel knyttet til seg er et tall folk lærer seg å hoppe over.</p>\n<p><strong>Oppgave.</strong> Hver enhet trengte en sandkasse som sto i forhold til det den faktisk gjør, og sandkassene trengte en sjekk, fordi feilmodusen til en feil sandkasse er at enheten starter og så svikter et sted parseren ikke kan se.</p>\n<p><strong>Handling.</strong> Privilegiebegrensninger gikk inn i alle enhetsmaler — et privilegietak låst til enhetens egne privilegier, et filter for systemkall, begrensede adressefamilier og minne som kan være skrivbart eller kjørbart, men aldri begge deler. De målte resultatene: dødmannsknappen gikk fra 9,6 til 1,5, forgen fra 8,3 til 1,5 og begge sikkerhetskopienhetene fra 9,6 til 2,3 og 2,5. Arbeidet med dem avdekket noe bedre enn scorene. Ett direktiv hadde vært feilstavet i tre enhetsmaler så lenge de rollene hadde eksistert — et navn som ser plausibelt ut, men ikke finnes, og som tjenestestyreren besvarer ved å logge at den ikke kjenner igjen nøkkelen, og så starte enheten likevel. Et søk med grep etter et direktiv beviser at det ble skrevet; bare parseren beviser at det trådte i kraft. Derfor kjører en verifiseringsrolle nå hver enhet gjennom tjenestestyrerens eget verifiseringsverktøy under kjøringen, og rollen inkluderes av hver rolle som installerer en enhet. Det dette fortsatt ikke fanger, står klart i de samme dokumentene: alle direktiver som kan ødelegge disse enhetene, ødelegger dem ved kjøretid, ikke ved parsing, og en score kan ikke si om et skript fortsatt når frem til sin siste linje.</p>\n<p><strong>Resultat.</strong> Hver enhet kjører med de privilegiene den trenger og ingen andre, tallene er målt og ikke påstått, og den typen feil der en skrivefeil blir et sikkerhetstiltak som i det stille mangler, får nå en kjøring til å feile. Begrensningene ved målingen står dokumentert ved siden av den, og det er den delen som hindrer neste leser i å stole for mye på en score i det grønne området.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Containere (Docker/Kubernetes)",
        "Infrastruktur",
        "Linux og servere",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "Containerisering og orkestrering",
        "Infrastruktur som kode",
        "Sikkerhet og tilgangsstyring",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-seven-read-only-host-reporting-roles-120/",
      "url": "https://engineer.company/nb/portfolio/built-seven-read-only-host-reporting-roles-120/",
      "title": "Bygde sju skrivebeskyttede rapporteringsroller som gjengir en vert i drift som Markdown — fakta, tilgang, git, metrikker, trafikk, sikkerhet og leverandørens inventar — etter en regel om at ingen tall skrives ut som kjøringen ikke har målt.",
      "summary": "Hele driftsmiljøet kan beskrives fra repoet ved behov, og rapportene har funnet virkelige ting: de to døde sikkerhetstiltakene, en ikke-deklarert lyttende port…",
      "content_html": "<p><strong>Situasjon.</strong> Verten hadde ikke noe dashbord og skulle heller ikke få noe, fordi en metrikkstack ikke får plass på 464 MB og ikke ville vært verdt kostnaden om den gjorde det. Det etterlot et ærlig hull: det fantes ingen måte å svare på spørsmål som hvem som har tilgang, hvor mye dette repoet har vokst, hvordan trafikken ser ut, eller om herdingen faktisk håndheves.</p>\n<p><strong>Oppgave.</strong> Disse spørsmålene trengte svar som kunne lages ved behov, som ikke kostet verten noe så lenge ingen spurte, og som aldri kunne endre det de beskrev.</p>\n<p><strong>Handling.</strong> Sju roller som bare leser, og som hver gjengir en levende vert som Markdown i repoet. Et facts‑øyeblikksbilde som dekker operativsystem, maskinvare, lagring, nettverk, tjenester, pakker, lyttende sockets og prosessorutnyttelse beregnet fra to målinger av kjernens egen teller. En tilgangsrapport som dekker kontoer, sudo‑omfang, nøkkelfingeravtrykk, forge‑brukere og databaseroller. En git‑rapport per repo. En metrikkrapport over dagens innsamlede målinger. En trafikkrapport bygget fra webserverens personvernmaskerte logg. En leverandøroversikt på tvers av fire leverandørflater — DNS, to sky‑API‑er og et API for dedikerte servere. Og en sikkerhetsrapport som svarer på om herdingen håndheves og ikke bare er konfigurert, og som skriver ut brannmurreglene i treffrekkefølge og det aktive regelsettet for blokkeringer. Den styrende regelen for alle sju er at det ikke skrives ut noe tall som kjøringen ikke har målt — en rapport som fyller et hull med et plausibelt tall, er verre enn en som lar det stå tomt, fordi det bare er det plausible tallet som blir trodd.</p>\n<p><strong>Resultat.</strong> Hele driftsmiljøet kan beskrives fra repoet ved behov, og rapportene har funnet virkelige ting: de to døde sikkerhetstiltakene, en ikke‑deklarert lyttende port og observasjonen at trafikkarkivet bare er så gammelt som siste gang noen husket å høste det. Det siste er dokumentert som en begrensning med en konkret kostnad — seksten dager av ett nettsteds historikk som ble rotert bort mellom høstingene og ikke finnes noe sted.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Infrastruktur",
        "Interessenter og rapportering",
        "Linux og servere",
        "Overvåking og observerbarhet",
        "Pålitelighet og overvåking",
        "Systemadministrasjon",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/deployed-the-companys-own-git-forge-121/",
      "url": "https://engineer.company/nb/portfolio/deployed-the-companys-own-git-forge-121/",
      "title": "Satte selskapets egen git‑forge på Soft Serve i produksjon, privat som standard, uten webpanel og med SSH‑porten bundet til loopback bak en hoppvert, og gjorde landingssiden foran den til en byggeartefakt fra hovednettstedet i stedet for en kopi som vedlikeholdes for hånd.",
      "summary": "Selskapet hoster sin egen kode på sin egen maskinvare, og siden foran den arver hver sjekk hovednettstedet består, i stedet for å drive bort fra det på måter…",
      "content_html": "<p><strong>Situasjon.</strong> Selskapets kildekode lå hos en tredjeparts vertstjeneste, som er et rimelig sted for den og en dårlig match for en bedrift som argumenterer overfor kundene sine med at den ikke gir dataene deres til mellomledd. Å drive en forge i stedet betyr å drive en forge: autentisering, tilgangskontroll, lagring, sikkerhetskopier og et offentlig ansikt utad.</p>\n<p><strong>Oppgave.</strong> En kanonisk git‑server måtte settes opp på den eksisterende verten, med minst mulig angrepsflate og uten noe administrasjonspanel på web, og med en landingsside foran som ikke forfaller.</p>\n<p><strong>Handling.</strong> Forgen er én enkelt binærfil som styres av operativsystemet, installert fra leverandørens pakkebrønn og bevisst valgt fordi den er SSH‑først uten noe administrativt webgrensesnitt — jo mindre grensesnitt, desto mindre å forsvare. Den er privat som standard: anonym tilgang avvises, tilgang uten nøkkel avvises, og hvert deklarert repo er merket som privat i stedet for å stole på at det er vanskelig å finne. SSH‑lytteren binder seg bare til loopback‑grensesnittet på port 23231 og nås utenfra via en jump host, slik at brannmurens deklarerte flate ikke vokser. En protokollmultiplekser som ville lagt HTTPS og git over SSH på én offentlig port ved å inspisere de første bytene i en tilkobling, er skrevet og står klar bak en hovedbryter, og den bryteren er av: git for flere brukere over den offentlige porten trengs ikke ennå, og en lytter ingen bruker, er angrepsflate. Landingssiden foran den var det mer interessante problemet. Den hadde vært en håndvedlikeholdt kopi av hovednettstedets design i et eget repo, og hver eneste forskjell som noen gang ble funnet mellom de to, viste seg å være et uhell og ikke en beslutning: en typeskala som gjorde ordmerket omtrent ni prosent for stort, en fontdeklarasjon som slo sammen to vekter til én på enhver maskin der familien var installert, ornamenter som var skjult under en viss bredde, slik at de manglet på hver eneste telefon, en vekt som ble brukt uten at noen font for den ble levert, og verken et main‑landemerke eller en overskrift på øverste nivå på noen side. Fem av fem, og ikke én av dem synlig i et skjermbilde. Kopien ble slettet; landingssiden bygges nå av hovednettstedets egne maler og leveres som en artefakt.</p>\n<p><strong>Resultat.</strong> Selskapet hoster sin egen kode på sin egen maskinvare, og siden foran den arver hver sjekk hovednettstedet består, i stedet for å drive bort fra det på måter bare en måling kan se. Avvik er fortsatt mulig og må nå skrives ned.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Systemadministrasjon",
        "Webutvikling",
        "DevOps og CI/CD‑automatisering",
        "Infrastruktur som kode",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/deployed-a-container-plane-on-podman-and-quadlet-122/",
      "url": "https://engineer.company/nb/portfolio/deployed-a-container-plane-on-podman-and-quadlet-122/",
      "title": "Satte et containerlag på Podman og Quadlet under systemd i produksjon i stedet for Docker, fordi Docker publiserer containerporter over vertsbrannmurens egne regler — og ga produksjonssettinger en uprivilegert bruker med én fast kommando i stedet for root.",
      "summary": "Containerne kjører under tjenestestyreren som allerede var betrodd, bak brannmuren som allerede var deklarert, og en rutinemessig utgivelse trenger ingen…",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen trengte et sted å kjøre applikasjonscontainere. Det åpenbare valget var bransjestandarden, og det åpenbare valget var feil for denne verten på en helt bestemt måte: den skriver om kjernens regler i pakkefilteret og publiserer containerporter over brannmurreglene som herdingsrollen installerer, slik at en container i det stille blir tilgjengelig fra internett uansett hva brannmuren har fått beskjed om.</p>\n<p><strong>Oppgave.</strong> Det måtte velges et containerlag som ikke går utenom brannmuren, ikke legger til en ekstra tjenestestyrer ved siden av den som allerede er betrodd, og ikke krever at en person er root for å levere en utgivelse.</p>\n<p><strong>Handling.</strong> Laget er en daemonløs containermotor som driver enheter generert av operativsystemets egen tjenestestyrer. Det finnes ingen ekstra prosessbehandler: containere er tjenester, de starter slik tjenester starter, og de beskrives i den samme deklarative formen som alt annet. Containerne bruker vertens nettverk uten noen publiserte porter i det hele tatt, og det fjerner spørsmålet om å gå utenom brannmuren i stedet for å avbøte det. Enhetene kjører på systemnivå og ikke rootless, og det er dokumentert som en avveining — det holder automatiseringen enkel og normal, på bekostning av den strengere isoleringen rootless ville gitt, og notatet sier hvilken vei man skal gå hvis utbrudd fra containeren noen gang blir viktigere enn enkel automatisering. Produksjonssetting ble skilt fra provisjonering: root setter opp stien for produksjonssetting én gang, og deretter setter en uprivilegert bruker i produksjon på nytt ved å kjøre ett fast skript gjennom en avgrenset regel som tillater den kommandoen og ingen argumenter. Bygg imaget, start tjenesten på nytt. Ingen root‑shell, ingen vilkårlige kommandoer.</p>\n<p><strong>Resultat.</strong> Containerne kjører under tjenestestyreren som allerede var betrodd, bak brannmuren som allerede var deklarert, og en rutinemessig utgivelse trenger ingen privilegert tilgang. Valget mot standarden er skrevet ned med begrunnelse, og det betyr mer enn selve valget — den neste som kommer, vil få beskjed om å bruke standarden av hver eneste artikkel vedkommende leser.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Containere (Docker/Kubernetes)",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Sikkerhet",
        "Containerisering og orkestrering",
        "DevOps og CI/CD‑automatisering",
        "Sikkerhet og tilgangsstyring",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/provisioned-a-second-server-from-code-123/",
      "url": "https://engineer.company/nb/portfolio/provisioned-a-second-server-from-code-123/",
      "title": "Automatiserte klargjøringen av en server nummer to hos en annen skyleverandør, der brannmuren opprettes før maskinen slik at den blir født bak en, og begge leverandørenes brannmurer er skrevet direkte mot deres REST‑API‑er for å slippe en collection fra tredjepart.",
      "summary": "En ny vert kan opprettes, eller tas over på nytt, fra repoet, bak en brannmur som allerede finnes, til en kostnad filen oppgir.",
      "content_html": "<p><strong>Situasjon.</strong> Det trengtes en maskin til for et applikasjonslag, hos en annen leverandør enn den første, og den første maskinens egen historikk var argumentet for hvordan det skulle gjøres: den var opprettet for hånd, og brannmuren var lagt til etterpå, noe som etterlater et vindu der en fersk vert med en standard passordpolicy kan nås fra internett.</p>\n<p><strong>Oppgave.</strong> Maskinen måtte opprettes fra repoet, og den måtte bli født bak brannmuren sin i stedet for å få en kort tid etterpå.</p>\n<p><strong>Handling.</strong> Rekkefølgen var bærende for designet. Brannmur‑playet kjører før server‑playet, slik at regelsettet finnes før det er noe for det å beskytte; hos den andre leverandøren kjører taggingen før skybrannmuren, fordi en brannmur som retter seg mot tagger, trenger at taggene finnes først. Begge leverandørenes brannmurer er skrevet direkte mot REST‑API‑ene deres gjennom en generisk HTTP‑oppgave i stedet for gjennom en collection fra leverandøren, og det fjerner en avhengighet og versjonsdriften dens, på bekostning av at formen på forespørslene må skrives for hånd. Rollen som oppretter maskinen, tar over en eksisterende maskin i stedet for å duplisere den hvis den allerede finnes, så det er trygt å kjøre på nytt. Den er også dokumentert, i sin egen fil, som den ene rollen i repoet som bruker penger — instanstypen, regionen, spesifikasjonen og den månedlige kostnaden i euro er skrevet ned, fordi et play som fakturerer noen, bør si det der de kommer til å lese det. Det playet er også det som ikke kan øves på den vanlige måten: den generiske HTTP‑oppgaven deklarerer ikke støtte for check mode, så en tørrkjøring hopper over hver oppgave i det. Øvingen gjøres i stedet mot brannmur‑playet, som er gratis og reversibelt.</p>\n<p><strong>Resultat.</strong> En ny vert kan opprettes, eller tas over på nytt, fra repoet, bak en brannmur som allerede finnes, til en kostnad filen oppgir. Det ene en tørrkjøring ikke kan dekke, er navngitt i den samme filen i stedet for å bli liggende som en overraskelse.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Python",
        "Sikkerhet",
        "Skytjenester",
        "Infrastruktur som kode",
        "Nettverk og VPN‑oppsett",
        "Sikkerhet og tilgangsstyring",
        "Skyinfrastruktur og migrering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/wrote-a-scope-rule-after-a-credential-leak-124/",
      "url": "https://engineer.company/nb/portfolio/wrote-a-scope-rule-after-a-credential-leak-124/",
      "title": "Skrev en avgrensningsregel inn i repoet etter at en omstrukturering hadde ført et annet selskaps inventar, brannmuråpninger og tekst inn i det — og holdt de karantenesatte restene under skanneren for hemmeligheter i stedet for å unnta dem.",
      "summary": "Omfangsgrensen er en skriftlig regel med en oppgitt test, restene er synlige i stedet for begravd, og den konkrete formen for påloggingsopplysning som slapp…",
      "content_html": "<p><strong>Situasjon.</strong> Arbeid gjort for et annet selskap fulgte med gjennom en omstrukturering av repoet og ble liggende. Det som fulgte med, var ikke abstrakt: en kladdelogg med gyldige påloggingsopplysninger i klartekst, som lå i treet forbi alle sperrer gjennom det meste av repoets historikk; en utdatert inventarfil som navnga det selskapets vert; og en oppgavefil for brannmuren som åpnet porter for seksten av det selskapets kundenettverk. Ingenting av det kjørte. Det er nettopp derfor det overlevde — noe som ikke kjører noe sted, blir aldri gjennomgått igjen.</p>\n<p><strong>Oppgave.</strong> Grensen måtte bli en regel med en test knyttet til seg, ikke en intensjon, og restene måtte håndteres på en måte som ikke bare skjulte dem.</p>\n<p><strong>Handling.</strong> Regelen er nå den innledende seksjonen i repoets instruksjonssett: dette repoet forvalter ett selskaps infrastruktur og bare den, og en annen parts vert, inventar, brannmuråpning, DNS‑sone eller påloggingsopplysning hører ikke hjemme i det — ikke engang deaktivert, kommentert ut eller parkert i en fil ingen playbook importerer. Den praktiske testen står skrevet ved siden av: ville dette selskapet fortsatt være ansvarlig for dette hvis forholdet tok slutt. Restene ble satt i karantene i en tydelig navngitt katalog i stedet for å bli slettet, slik at historikken forblir lesbar, og karantenen er bevisst delvis — de to strukturelle linterne hopper over den, mens hemmelighetsskanneren, vault‑vakten og emoji‑sjekken med vilje fortsatt leser den, fordi det er de tre som ville fange det som slapp inn. Selve lekkasjen ga en linterregel: et egendefinert mønster som flagger en påloggingsopplysning som sendes som et kommandolinjeflagg, som er formen den lekkede hadde, og som standardregelsettet ikke fanget.</p>\n<p><strong>Resultat.</strong> Omfangsgrensen er en skriftlig regel med en oppgitt test, restene er synlige i stedet for begravd, og den konkrete formen for påloggingsopplysning som slapp gjennom, får nå en commit til å feile. Lærdommen som er dokumentert ved siden av, er den generelle: den farlige artefakten er ikke den som kjører, men den som ikke gjør det, fordi det er den ingen leser igjen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Datastyring",
        "Dokumentasjon",
        "Infrastruktur",
        "Sikkerhet",
        "Datastyring og datakvalitet",
        "Sikkerhet og tilgangsstyring",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/split-every-operational-secret-per-host-125/",
      "url": "https://engineer.company/nb/portfolio/split-every-operational-secret-per-host-125/",
      "title": "Delte opp de fire driftshemmelighetene per vert etter å ha slått fast at to verter som deler én dødmannsknapp, varsler mindre enn to knapper, ikke mer, og at en felles passordfrase for sikkerhetskopier gjør to verter til ett repo.",
      "summary": "Hemmelighetene er per vert i selve utformingen, og de to feiltilstandene som ville ha fulgt av å dele dem, er skrevet ned der neste person redigerer filen.",
      "content_html": "<p><strong>Situasjon.</strong> Plattformen var bygget for én vert og skulle snart få to. Flere driftshemmeligheter var skrevet som enkeltverdier ut fra den antakelsen: ett sikkerhetskopirepo, én passfrase, én varslingsknapp. Å utvide dem til vert nummer to ved å dele dem, er den minste motstands vei, og det er feil på to separate måter, som begge svikter i det stille.</p>\n<p><strong>Oppgave.</strong> Hver hemmelighet måtte bli en oppslagstabell med verten som nøkkel, og begrunnelsen måtte skrives ned, fordi den delte varianten ser riktig ut og ikke koster noe før den dagen det betyr noe.</p>\n<p><strong>Handling.</strong> To argumenter avgjorde saken, og begge er dokumentert ved siden av konfigurasjonen. En dødmannsknapp som deles av to verter, varsler mindre enn to knapper, ikke mer: så lenge én av vertene fortsatt pinger, holder sjekken seg grønn mens den andre er død, så å legge en vert til på en delt knapp reduserer aktivt dekningen for den som allerede var der. Og en streng for et sikkerhetskopirepo er bare en plassering — passfrasen er hele krypteringen, så to verter som deler en passfrase, er ikke to sikkerhetskopirepoer med en felles hemmelighet, men ett sikkerhetskopirepo med to kataloger i. Hver verdi ble en oppslagstabell med vertsnavnet i inventaret som nøkkel, slått opp per vert av en delt oppgavefil som både kjøringen og preflighten kjører. Den eksisterende verten ble deretter ført opp i alle fire tabellene med sikkerhetskopirepoet sitt og begge knappene stående tomme, som er den sannferdige tilstanden og holder de to åpne initiativene ærlige om at det de mangler, er operatørens halvdel av arbeidet og ikke koden.</p>\n<p><strong>Resultat.</strong> Hemmelighetene er per vert i selve utformingen, og de to feiltilstandene som ville ha fulgt av å dele dem, er skrevet ned der neste person redigerer filen. De tomme oppføringene er den delen som er verdt å ta vare på: de sier at koblingen finnes og verdien ikke gjør det, og det er et annet og mer nyttig utsagn enn en manglende nøkkel.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Infrastruktur",
        "Linux og servere",
        "Løsningsarkitektur",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Infrastruktur som kode",
        "Pålitelighet og overvåking",
        "Sikkerhet og tilgangsstyring",
        "Sikkerhetskopiering og gjenoppretting"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-multilingual-static-site-in-hugo-126/",
      "url": "https://engineer.company/nb/portfolio/built-a-multilingual-static-site-in-hugo-126/",
      "title": "Bygde et flerspråklig statisk nettsted i Hugo med 108 malfiler, blant dem 53 partials, som publiserer hver side i fire representasjoner fra ett innholdstre på tre språk, og på nytt under sju fokuserte underdomener bygd fra det samme treet.",
      "summary": "Nettstedet leverer tre språk og fire representasjoner fra ett tre, har ingen backend å angripe og ingenting som gir en regning, og den ene biten JavaScript i…",
      "content_html": "<p><strong>Situasjon.</strong> Selskapet trengte et offentlig nettsted som virker på tre språk, beviser kompetanse i stedet for å påstå den, ikke koster noe å drive og ikke gir de besøkende videre til noen. De fleste av disse er vanlige krav. Sammen utelukker de nesten alle publiseringssystemer, fordi en backend ved kjøretid er noe som må sikres, patches, betales for og forklares på en personvernside.</p>\n<p><strong>Oppgave.</strong> Nettstedet måtte genereres i sin helhet ved byggetid og likevel oppføre seg som et moderne nettsted — søkbart, installerbart, syndikert, utskrivbart og lesbart med skjermleser på hvert språk det leveres på.</p>\n<p><strong>Handling.</strong> Det er et statisk nettsted: 108 malfiler, hvorav 53 er partials og 18 er shortcodes, tre språk, ingen backend ved kjøretid og ett førstepartsskript som har fått et avgrenset unntak for et porteføljefilter der hvert kontrollelement er skjult til skriptet kjører. Funksjonalitet legges til ved byggetid og ikke i nettleseren, og det er en produktbeslutning og skrevet ned som en. Hver side publiseres i fire representasjoner fra ett innholdstre — HTML, en Markdown‑tvilling, et Gemini‑dokument og en oppføring i en Gopher‑meny — og forsiden legger i tillegg til tre syndikeringsfeeder og et manifest for installerbar applikasjon. Taksonomiene er bevisst to og ikke én, og skillet er bærende: en kategori er en emnetagg på arbeidet, en tjeneste er noe selskapet selger, og å slå dem sammen ville ha gjort katalogen til en liste over ferdigheter i stedet for en liste over tilbud. Det samme treet bygges så sju ganger til, én gang per fokusert underdomene, ved å peke generatoren mot en annen innholdskatalog i stedet for å forke noe.</p>\n<p><strong>Resultat.</strong> Nettstedet leverer tre språk og fire representasjoner fra ett tre, har ingen backend å angripe og ingenting som gir en regning, og den ene biten JavaScript i det er bundet av en kontrakt som en linter håndhever. Kostnaden er at alt interaktivt må løses ved byggetid eller ikke i det hele tatt, noe som har utelukket flere ting som ville vært enkle med en server, og som er grunnen til at søkeindeksen ble kostnadsberegnet og lagt på is i stedet for levert.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Internasjonalisering",
        "Webutvikling",
        "Ytelsesoptimalisering",
        "Fullstack‑produktutvikling",
        "Internasjonalisering og lokalisering",
        "Klar for AI‑agenter",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-22-linter-commit-gate-127/",
      "url": "https://engineer.company/nb/portfolio/built-a-22-linter-commit-gate-127/",
      "title": "Bygde en commit‑port med 22 enlinjes lintere pluss fem som fortjener et avsnitt, uten advarselsnivå og uten tillatte undertrykkinger i koden, som dekker HTML, CSS, JavaScript, Python, YAML, Markdown, shell, lenker, staving, hemmeligheter og typografi.",
      "summary": "Mekaniske innvendinger kommer fra en maskin før en commit finnes, og kvalitetsporten er den eneste som gjennomgår endringer i dette prosjektet.",
      "content_html": "<p><strong>Situasjon.</strong> En bidragsveiledning er et sett med forslag. Alle er enige i den, og så er det fredag, endringen er liten, og veiledningen taper. I et enpersonsprosjekt er det verre og ikke bedre, fordi det ikke finnes noen som gjennomgår endringene i det hele tatt — det eneste som står mellom en dårlig endring og produksjon, er personen som skrev den, i det øyeblikket vedkommende er minst tilbøyelig til å krangle med seg selv.</p>\n<p><strong>Oppgave.</strong> Standardene måtte være kjørbare, slik at et brudd på én av dem får en commit til å feile i stedet for å vente på å bli lagt merke til.</p>\n<p><strong>Handling.</strong> Det som vokste frem, er en kvalitetsport med 22 lintere uten noe advarselsnivå: hver diagnostikk er en feil, og selve nettstedsgeneratoren kjører med advarsler oppgradert til feil, slik at selv en melding om utfasing stopper byggingen. De åpenbare er der — HTML‑validering, CSS, JavaScript, Python, YAML, Markdown, shell, staving, hemmeligheter, døde lenker. De interessante er de prosjektspesifikke sjekkene som intet hyllevareverktøy har noen mening om: at begge fargetemaene maler hver lagdelte flate med like mange lag, at intet fotografi vises bredere enn halvparten av kildebildets piksler, at treet som settes i produksjon, ikke inneholder noen private stier eller vertsnavn, at teksten følger tonereglene på alle tre språk, at en side har en Markdown‑tvilling og en gyldig representasjon i en alternativ protokoll. Inline‑undertrykking er forbudt uten unntak — ingen ignore‑kommentarer, ingen disable‑direktiver, ingen omgåelse av hooken og ingen omdøping av filer for å slippe unna et filmønster. Regelen som holder dette ærlig, er at en feil som fantes fra før, ikke er en unnskyldning: når en sjekk avdekker en feil ingen har innført, rettes den i samme omgang.</p>\n<p><strong>Resultat.</strong> Mekaniske innvendinger kommer fra en maskin før en commit finnes, og kvalitetsporten er den eneste som gjennomgår endringer i dette prosjektet. Kostnaden er oppgitt og ikke skjult: det går tregt å committe, og en dårlig skrevet sperre er rett og slett til å bli gal av når den må omgås, og det er grunnen til at sperrene selv senere ble lagt under et eget formateringsverktøy og en egen linter.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Python",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Produktstrategi og krav",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/cut-the-visual-quality-gate-to-ten-minutes-128/",
      "url": "https://engineer.company/nb/portfolio/cut-the-visual-quality-gate-to-ten-minutes-128/",
      "title": "Kuttet nettstedets nettleserdrevne kvalitetsport fra 1 636 sekunder til 615 ved å kjøre kontrollene med de lengste først gjennom en worker‑pool begrenset til fire baner, etter å ha målt at alfabetisk rekkefølge kostet 320 sekunder mot 224.",
      "summary": "Kvalitetsporten gikk fra 1 636 sekunder til 615, mens sjekkene ble bredere og ikke tynnere — den serielle kostnaden gikk opp og veggklokketiden gikk ned.",
      "content_html": "<p><strong>Situasjon.</strong> Elleve av nettstedets sjekker styrer en hodeløs nettleser: layout ved hver vindusform designet tegner opp, typeskala, utvidelse av oversatt tekst, kontrast, tilgjengelighetsregler, tvungne farger, maskotstørrelse, bevegelse, utskrift i seks papirkombinasjoner, konsollfeil og visuell regresjon. Kjørt én om gangen tok de 1 636 sekunder, litt over tjuesju minutter. En kvalitetsport som tar tjuesju minutter, er en kvalitetsport som blir hoppet over, og en sjekk som hoppes over, kan ikke skilles fra en som består.</p>\n<p><strong>Oppgave.</strong> Veggklokketiden måtte ned så langt at det å kjøre dem var standardvalget og ikke en beslutning, uten at noen av dem ble svekket.</p>\n<p><strong>Handling.</strong> Arbeidet begynte med måling. Hver sjekk ble tidtatt for seg på en maskin med tolv kjerner: layout på 405 sekunder, typografi på 301, oversettelse på 282, kontrast på 189, tilgjengelighet på 178, og så videre ned til sytten. To funn formet svaret. Å kjøre alle samtidig gikk tregere enn å kjøre fire om gangen — 265 sekunder mot 224 — fordi hver sjekk i seg selv er en nettleser som gjør parallelt arbeid, og å overbelaste maskinen koster mer enn samtidigheten vinner. Og å sortere etter lengste kjøretid først slo alfabetisk rekkefølge med nesten en tredjedel, 224 sekunder mot 320, som er det klassiske resultatet fra jobbplanlegging og viser seg her fordi sjekkene varierer med en faktor på tjue i kostnad. Derfor er kjøreren en avgrenset arbeiderpool, dimensjonert etter antall kjerner med minst to og høyst fire arbeidere, som får de lengste sjekkene først. Samtidig ble sjekkene utvidet i stedet for innsnevret: de deler nå én viewport‑tabell med tjueto vindusformer, utledet fra hver media query stilarket faktisk inneholder, noe som alene tok layoutsjekken fra 95 sekunder til 405.</p>\n<p><strong>Resultat.</strong> Kvalitetsporten gikk fra 1 636 sekunder til 615, mens sjekkene ble bredere og ikke tynnere — den serielle kostnaden gikk opp og veggklokketiden gikk ned. Målingen er den delen som er verdt å ta vare på: to valg som høres fornuftige ut, å kjøre alt samtidig og å kjøre ting i den rekkefølgen de ble skrevet, var hvert for seg målbart dårligere enn alternativet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Frontend‑utvikling",
        "Python",
        "Testing og kvalitetssikring",
        "Ytelsesoptimalisering",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/replaced-27-font-sizes-with-a-six-step-scale-129/",
      "url": "https://engineer.company/nb/portfolio/replaced-27-font-sizes-with-a-six-step-scale-129/",
      "title": "Erstattet 27 gjengitte skriftstørrelser, der de nærmeste naboene lå 0,6 % fra hverandre, med en Major Third‑skala i seks trinn, og skrev linteren som avviser den tjueåttende.",
      "summary": "Seks størrelser der det var tjuesju, med et oppgitt forholdstall, en oppgitt linjelengde og en sjekk som avviser den neste uplanlagte verdien.",
      "content_html": "<p><strong>Situasjon.</strong> En måling av teksten slik nettstedet gjengir den, fant tjuesju forskjellige skriftstørrelser i bruk. Flere lå mindre enn én prosent fra hverandre — fem verdier mellom 0,8 og 0,85 av brødtekststørrelsen var i bruk samtidig, en forskjell ingen leser kan oppfatte og enhver fremtidig redaktør kommer til å bygge videre på. To av overskriftene fikk ingen størrelse fra stilarket i det hele tatt og falt gjennom til nettleserens egne standardverdier, et forholdstall på 1,33 som ikke stemte med noe annet på siden.</p>\n<p><strong>Oppgave.</strong> Størrelsene måtte bli en skala med et oppgitt forholdstall, og noe måtte hindre den tjueåttende.</p>\n<p><strong>Handling.</strong> Skalaen er en Major Third med forholdstall 1,25, seks navngitte trinn fra småskrift til displayskrift, hvert av dem et token og ikke en verdi. Overskriftene er eksplisitt koblet til trinn i stedet for å arve det nettleseren måtte mene, og det var det som rettet de to som ikke hadde noen egen størrelse. Bare det nederste trinnet har en minstestørrelse, slik at småskrift forblir lesbar på en telefon uten at hele skalaen låses fast. Logotyper er unntatt ved navn og ikke ved et uhell. Linteren er det som får det til å holde: den gjengir nettstedet og feiler på den tjueåttende distinkte størrelsen. Den har allerede gjort seg fortjent til plassen sin to ganger — den fanget en overskrift som kom inn på 1,17 av brødtekststørrelsen, som ikke er et trinn i noe som helst, og navnga forholdstallet i feilmeldingen; og den fanget inline‑kode på 0,9, som er nøyaktig den typen verdi skalaen finnes for å forhindre. Sammen med skalaen kom en linjelengde på omtrent syttito tegn, som erstattet en arvet fast bredde som ga nittien tegn på en gjennomgangsside og hundre og ti på kontaktsiden.</p>\n<p><strong>Resultat.</strong> Seks størrelser der det var tjuesju, med et oppgitt forholdstall, en oppgitt linjelengde og en sjekk som avviser den neste uplanlagte verdien. Begrensningen er reell og tidvis upraktisk: et design som vil ha en størrelse mellom to trinn, må flytte seg til et trinn eller argumentere for å endre skalaen, og den diskusjonen er tatt og tapt mer enn én gang.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Merkevare, markedsføring og SEO",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/took-wcag-2-2-level-aa-across-the-whole-site-130/",
      "url": "https://engineer.company/nb/portfolio/took-wcag-2-2-level-aa-across-the-whole-site-130/",
      "title": "Førte 32 representative sider — én per mal per språk — til WCAG 2.2 nivå AA i begge fargetemaene, pluss to AAA‑kriterier, med en dokumentert samsvarsoversikt og en kontroll som forsvarer hver påstand.",
      "summary": "Samsvar hevdes på nivå AA på tre språk og i to temaer, med to AAA-kriterier utover det og ett oppgitt som unntak, og hver påstand har en sjekk bak seg.",
      "content_html": "<p><strong>Situasjon.</strong> Et konsulentselskap som selger ingeniørfaglig skjønn og leverer et utilgjengelig nettsted, har et troverdighetsproblem før det har et tilgjengelighetsproblem. Nettstedet har dessuten en uvanlig stor flate for det: tre språk, to fargetemaer, et komplett utskriftsstilark, en palett med mørkt som utgangspunkt, håndtegnede annotasjoner og en illustrert figur — og hver av dem er en måte å feile et kriterium på i én konfigurasjon og bestå det i en annen.</p>\n<p><strong>Oppgave.</strong> Nettstedet måtte oppfylle WCAG 2.2 på nivå AA på hver side, på hvert språk og i begge temaene, og samsvaret måtte forsvares av sjekker i stedet for å bli påstått i et dokument.</p>\n<p><strong>Handling.</strong> En samsvarsoversikt dokumenterer hvert kriterium som er omfattet, og hver rad oppgir hva som oppfyller det og hva som sjekker det. To kriterier på nivå AAA er tatt med utover målet — visuell presentasjon fullt ut, og fokusutseende, som tilbys og ikke hevdes. Automatiseringen kjører en regelmotor for tilgjengelighet med reglene for beste praksis slått på og ikke bare samsvarsreglene, en forskjell på tretti regler som fikk betydning med en gang: én av de ekstra reglene feilet i det øyeblikket den ble slått på, fordi merkevaresymbolet lå utenfor alle landemerker på hver underside. En annen, tregere sjekk kjører trettito representative sider i to fargeskjemaer og to bredder, og det den slår fast, er uvanlig: fokus bevises i piksler og ikke i dokumentet, ved å trykke på tab‑tasten på ordentlig og sammenligne skjermbilder, fordi identiske piksler betyr at brukeren ikke kan se hvor fokus er, uansett hva markupen sier. Den går også gjennom hele siden på jakt etter en tastaturfelle, bruker de spesifiserte overstyringene av tekstavstand, dobler rotens skriftstørrelse og måler linjelengde, blokkjustering, sentrering og avsnittsavstand.</p>\n<p><strong>Resultat.</strong> Samsvar hevdes på nivå AA på tre språk og i to temaer, med to AAA‑kriterier utover det og ett oppgitt som unntak, og hver påstand har en sjekk bak seg. Det vanskeligste av dem var kontrast over fotografiet, som en automatisert regelmotor rapporterer som umulig å vurdere, og som nettstedet hadde forsvart med et argument i stedet for et tall: de faktiske tallene var 49 av 64 par av side og tema under AA. To flater designet aldri hadde tatt eierskap til, viste seg å være årsaken, og sjekken som fant dem, er nå et minstekrav — hvert par må holde AA over veggen byggingen tegner, og ethvert par som ikke gjør det, får byggingen til å feile. Det nettstedet selv sier på kolofonsiden sin, er den ærlige delen: intet automatisert verktøy finner mer enn omtrent en tredjedel av WCAG‑bruddene, etikettene som vises ved hover, kan ikke lukkes med en tast fordi det krever et skript og dette nettstedet kjører ett, og det er ikke testet med ekte hjelpemiddelteknologi — de begrensningene er skrevet der en leser vil se dem, i stedet for å være utelatt fra påstanden.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Internasjonalisering",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Webutvikling",
        "Internasjonalisering og lokalisering",
        "Klar for AI‑agenter",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/fixed-11-accessibility-defects-the-browser-called-healthy-131/",
      "url": "https://engineer.company/nb/portfolio/fixed-11-accessibility-defects-the-browser-called-healthy-131/",
      "title": "Fant og rettet 11 feil i universell utforming som nettleseren meldte var i orden — åtte bunntekstlenker som fortsatt lå i tabulatorrekkefølgen bak pointer‑events, en scroll‑tidslinje som klippet kolofonen av tre sider, og en regelmotor som kjørte 70 av sine 105 regler.",
      "summary": "Elleve feil er rettet, og det som er mer nyttig, er fire regler som lever lenger enn dem: en vakt som tester én akse, forsvarer én akse; et regelsett som ikke…",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet besto de automatiske reglene for universell utforming. Det hadde også elleve feil som disse reglene ikke kunne se, fordi hver av dem var en egenskap ved hvordan siden ble gjengitt, ikke ved hva oppmerkingen sa — den typen en validator melder som i orden, og som en tastaturbruker støter på i løpet av sekunder.</p>\n<p><strong>Oppgave.</strong> Feilene måtte finnes, rettes og føres inn i et register med regelen hver av dem ga opphav til, slik at det er feilklassen som lukkes, ikke enkelttilfellet.</p>\n<p><strong>Handling.</strong> De falt i tre grupper. Størrelse: et nøkkelord i stilarket, brukt som om det var relativt, låste et helt område til seksten piksler mens brødteksten gikk opp til tjueto; et element for senket skrift ble brukt i betydningen «mindre»; tekst som ble krympet, gjorde linjene lengre helt til en tittel nådde nittiseks tegn; og en målt maksimalhøyde ble sprengt av nettopp den overstyringen av tekstavstand som nettstedet hevder å støtte. Fokus og klipping: åtte lenker i bunnteksten ble værende i tabulatorrekkefølgen bak en egenskap som fjerner pekerinteraksjon og ingenting annet; en overflow‑regel skapte en rullebeholder ingen ønsket seg; og en rullestyrt animasjon, som rett og slett er inaktiv på en side som er for kort til å rulle, klippet bunnteksten permanent bort på tre sider. Og to feil i selve vaktene, som er de som er verdt å nevne — hver vindusform layoutkontrollen åpnet, var ni hundre piksler høy, så en telefon i liggende format på 852 ganger 393 ga sju piksler mellom to elementer, og ingen kontroll hadde noen gang sett etter; og regelmotoren hadde kjørt sytti av sine hundre og fem regler, så regelen som ville ha fanget det manglende landemerket, var ikke med i settet.</p>\n<p><strong>Resultat.</strong> Elleve feil er rettet, og det som er mer nyttig, er fire regler som lever lenger enn dem: en vakt som tester én akse, forsvarer én akse; et regelsett som ikke inneholder regelen, kan ikke håndheve den; høyde er en dimensjon, så test den; og bevis synlighet i piksler, ikke i dokumentet. Da layoutkontrollen ble åpnet på nytt, nå på riktig måte, feilet den hundre og tjueåtte ganger på tvers av tre språk, blant annet i et vindu på telefonstørrelse der den siste linjen i hero‑seksjonen lå tjueen piksler under folden.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Webutvikling",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/generated-321-achievement-pages-from-a-sqlite-export-132/",
      "url": "https://engineer.company/nb/portfolio/generated-321-achievement-pages-from-a-sqlite-export-132/",
      "title": "Genererte 495 resultatsider på tre språk fra en skrivebeskyttet SQLite‑eksport, med sideadressen skrevet som data, slik at en rettet setning ikke lenger flyttet siden og brøt lenken.",
      "summary": "Fire hundre og nittifem sider genereres fra én kilde, å rette en formulering koster ingenting, og adressene dette nettstedet noen gang har publisert, svarer…",
      "content_html": "<p><strong>Situasjon.</strong> Selskapets porteføljeinnhold skrives i en egen database — den samme som produserer CV‑en — og nettstedet må publisere det som sider, på tre språk, uten at de to kopiene glir fra hverandre. Den naive tilnærmingen, å skrive sidene for hånd og holde dem i takt, svikter ved den første rettelsen.</p>\n<p><strong>Oppgave.</strong> Nettstedet måtte generere innholdet sitt fra databasen som inndata til bygget, med sideadresser som overlever at setningene på dem skrives om.</p>\n<p><strong>Handling.</strong> En eksportør leser databasen skrivebeskyttet og skriver én side per resultat per språk — 495 sider — pluss taksonomi- og tjenestedataene malene trenger. Den bruker bare standardbiblioteket, så nettstedets bygg er ikke avhengig av generatorens miljø, og fordi utdataene er sjekket inn, bygger nettstedet selvstendig. Den enkeltbeslutningen i eksportøren som får størst konsekvenser, gjelder adresser. Nettstedet utledet tidligere en sides URL fra de første ordene i den engelske formuleringen, så å rette en setning flyttet siden i det stille og brøt alle lenker til den — et nettsted som argumenterer for seg selv med at det retter ting, påførte seg selv en død lenke hver gang det gjorde det. Adressen skrives nå som data: én rad per adresse per resultat, den første er gjeldende, og hver senere er en utfaset adresse som nettstedet sender ut som en omdirigering. To andre feller fra det samme arbeidet er dokumentert. En sammenligning av perioder mot en tekstkolonne traff i det stille alle trettito radene på grunn av hvordan databasen tildeler typeaffinitet i en sammenligning. Og språklisten er nå den eneste aksen som skriveløkken, datafilene og spørringene alle utleder fra, så å legge til et fjerde språk er én oppføring i en oppslagstabell i stedet for et søk.</p>\n<p><strong>Resultat.</strong> Fire hundre og nittifem sider genereres fra én kilde, å rette en formulering koster ingenting, og adressene dette nettstedet noen gang har publisert, svarer fortsatt. Eksportøren eier også nøyaktig én presentasjonsbeslutning — hvordan tjenester grupperes i tematiske seksjoner — og det er bevisst: alt annet den skriver, tilhører databasen, og en gruppeoverskrift som mangler et språk, får eksporten til å feile i stedet for å vise engelsk over oversatt innhold.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Databaser",
        "Datapipelines (ETL/ELT)",
        "Frontend‑utvikling",
        "Internasjonalisering",
        "Python",
        "SQL",
        "Webutvikling",
        "Internasjonalisering og lokalisering",
        "Merkevare, markedsføring og SEO",
        "Utvikling av datapipelines (ETL/ELT)",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/closed-the-colour-system-at-28-colours-133/",
      "url": "https://engineer.company/nb/portfolio/closed-the-colour-system-at-28-colours-133/",
      "title": "Lukket fargesystemet ved 28 dokumenterte farger med en linter som avviser en verdi som er malt, men udokumentert, dokumentert, men ikke malt, feilmålt, skrevet om som en literal, eller innenfor en perseptuell avstand på 0,02 fra en som allerede finnes.",
      "summary": "Paletten er et lukket, målt sett som ikke kan vokse i det stille, og de to nesten like skrivemåtene som utløste arbeidet, er borte.",
      "content_html": "<p><strong>Situasjon.</strong> Farger på et nettsted med mørkt tema som standard, et lyst tema, et utskriftsstilark, en modus for tvungne farger og en illustrert merkevare forblir ikke et lite sett av seg selv. Fargene hadde allerede begynt å gli ut, slik de alltid gjør: to skrivemåter av samme farge så nær hverandre at ingen kunne skille dem, verdier skrevet inn på nytt som litteraler ved siden av tokenene som definerte dem, og dokumenterte farger som ingenting malte.</p>\n<p><strong>Oppgave.</strong> Paletten måtte bli et lukket sett med en uttalt terskel for hvor ulike to farger må være, og noe måtte håndheve at settet forble lukket.</p>\n<p><strong>Handling.</strong> Tjueåtte farger, hver dokumentert med kontrastforholdet den måler på flaten den står på, og elleve merkevareverdier deklarert én gang som tokens. Terskelen er numerisk, ikke redaksjonell: to farger som ligger nærmere hverandre enn en perseptuell avstand på 0,02 i et uniformt fargerom, er én farge med to skrivemåter. Linteren feiler på fem ulike måter — en verdi som males, men ikke er dokumentert, som er dokumentert, men ikke males, som er registrert med feil kontrastforhold, som ligger innenfor terskelen til en farge som allerede finnes, eller en merkevareverdi skrevet på nytt som litteral. Den fant to med en gang: en temafarge som fantes i to skrivemåter 0,018 fra hverandre, og en bakgrunnsfarge som sto i stedet for en vegg den lå 0,021 fra. Gjennomsiktighet bruker relativ fargesyntaks i stedet for blandingsfunksjonen, nettopp fordi vakten ikke kan se gjennom en blanding, og en palettvakt som kan omgås, er ingen vakt. Regelen som hører med, er kort: endre et token heller enn en regel, legg bare til en farge når ingen passer, og senk aldri et dokumentert kontrastforhold for å få et design til å fungere.</p>\n<p><strong>Resultat.</strong> Paletten er et lukket, målt sett som ikke kan vokse i det stille, og de to nesten like skrivemåtene som utløste arbeidet, er borte. Det er en begrensning som av og til sier nei — et design som vil ha en litt annen blåfarge, må ta den som finnes eller argumentere for en tjueniende farge, og den argumentasjonen må ta med kontrastforholdet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Merkevare, markedsføring og SEO",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/found-the-light-theme-missing-an-overlay-layer-134/",
      "url": "https://engineer.company/nb/portfolio/found-the-light-theme-missing-an-overlay-layer-134/",
      "title": "Fant ut at det lyse temaet hadde manglet et helsides overleggslag helt siden det ble skrevet, ved å kreve at begge temaene maler hver lagdelte flate med like mange lag.",
      "summary": "Et lag som mangler i ett tema, får nå en commit til å feile, og et lag som hadde manglet siden det ble skrevet, er malt.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet leveres med to fargetemaer. Det mørke er standard og blir sett på hele tiden; det lyse er en overstyring som bare vises ved en bestemt systeminnstilling, noe som betyr at det blir sett langt sjeldnere, og av ingen som kontrollerer det. Temaparitet er en feilklasse der en flate bygges én gang og formuleres på nytt én gang, og den nye formuleringen i det stille mister et lag.</p>\n<p><strong>Oppgave.</strong> Pariteten trengte en assert, fordi det eneste alternativet er at et menneske husker å bytte innstilling og se etter.</p>\n<p><strong>Handling.</strong> Kontrollen er liten og strukturell: for hver lagdelt flate teller den lagene hvert tema maler, og feiler når antallene er ulike. Den sammenligner ikke utseende — temaene skal se forskjellige ut — den sammenligner sammensetning, som er det som skal være identisk. Den fant feilen ved første kjøring. Dekket som fyller hele siden, har tre lag i det mørke temaet, et fasettmønster over et slør over veggen, og det lyse temaet formulerte det på nytt med to. Fasettoverlegget hadde manglet i det lyse temaet fra den dagen det ble skrevet, og ingenting hadde noen gang sagt fra, fordi en side som mangler ett av tre bakgrunnslag, ser ut som en designbeslutning og ikke som en feil. Rettelsen var én regel; notatet som ble skrevet etterpå, dokumenterte hva overlegget koster i kontrast, så tillegget er målt i stedet for antatt å være gratis.</p>\n<p><strong>Resultat.</strong> Et lag som mangler i ett tema, får nå en commit til å feile, og et lag som hadde manglet siden det ble skrevet, er malt. Dette er den typen feil hele kontrolltilnærmingen sikter mot — ingenting var ødelagt, ingenting ga feilmelding, siden ble gjengitt riktig, og den hadde vært feil i månedsvis.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/made-reduced-motion-honest-135/",
      "url": "https://engineer.company/nb/portfolio/made-reduced-motion-honest-135/",
      "title": "Gjorde innstillingen for redusert bevegelse ærlig etter å ha funnet elleve selektorer som fortsatt animerte under den, fordi en universell transition‑none‑regel taper på spesifisitet mot enhver regel med en klasse.",
      "summary": "Innstillingen respekteres i praksis og ikke bare i intensjonen, og elleve aktive animasjoner som en generell regel så ut til å dekke, er faktisk dekket.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet respekterte innstillingen for redusert bevegelse med én regel som slo av alle overganger. Den så fullstendig ut, linteren hadde ingenting å utsette på den, og med en emulert innstilling for redusert bevegelse animerte elleve selektorer fortsatt.</p>\n<p><strong>Oppgave.</strong> Innstillingen måtte respekteres i praksis, og årsaken til at den åpenbare regelen sviktet, måtte gjøres om til noe den neste personen ikke kan gjenta.</p>\n<p><strong>Handling.</strong> Årsaken er spesifisitet. En universell regel havner nederst i kaskaden og taper mot enhver regel som har en klasse, så en generell transition‑none blir slått av hver gjennomtenkte animasjon på siden — og det er hver animasjon det er verdt å slå av. Rettelsen var strukturell, ikke en lapp: alt som beveger seg, ligger nå i én sen del av stilarket, og en linter avviser en transform med overgang alle andre steder. Skillet regelen trekker, er bevisst og smalere enn det åpenbare: reduser bevegelse, ikke farge. En fargeovergang er ikke bevegelse, og å slå den av får grensesnitt til å føles ødelagte for folk som ikke ba om det, så en overgang som lister fargeegenskaper, består, og en overgang på bevegelse gjør det ikke. Bevegelse må også uttrykkes som en overgang og ikke som en keyframe‑animasjon, fordi den første er triviell å avbryte og den andre ikke er det. To beslektede feller kom ut av det samme arbeidet og er dokumentert med målinger: en pynt i menyen hadde dreid trettien grader av en dreining på seksti grader før den var halvveis synlig, og fordi formen er periodisk, ser en halv omdreining identisk ut for en tredjedel av kostnaden; og fem ulike egenskaper gjør hver for seg et element til inneholdende blokk for alt som er posisjonert relativt til visningsområdet, noe som i det stille hadde krympet et lukkelag til en brøkdel av skjermen.</p>\n<p><strong>Resultat.</strong> Innstillingen respekteres i praksis og ikke bare i intensjonen, og elleve aktive animasjoner som en generell regel så ut til å dekke, er faktisk dekket. Den generelle lærdommen er den som står øverst i det dokumentet: en regel som taper på spesifisitet, feiler i det stille, og hver rapport kaller det noe annet.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Webutvikling",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-637-line-print-stylesheet-136/",
      "url": "https://engineer.company/nb/portfolio/built-a-637-line-print-stylesheet-136/",
      "title": "Bygde et utskriftsstilark på 637 linjer mot fire dokumenterte atferder i gjengivelsesmotorer, blant dem innfelte overskrifter som ble skrevet ut hvitt på hvitt for enhver leser med nettleseren innstilt på mørkt.",
      "summary": "Nettstedet skrives ut på ukjent papir i begge temaene, og de fire motoroppførslene er skrevet ned med feilen hver av dem forårsaket.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedets casestudier er det noen skriver ut og tar med inn i et møte. Det gjør papir til en reell utdataform og ikke en høflighet, og papir er et annet medium enn en smal skjerm — leserens papirstørrelse er ukjent, leserens papirretning er ukjent, og nettleserens utskriftsoppførsel varierer mellom motorer på måter ingen forhåndsvisning på skjerm avslører.</p>\n<p><strong>Oppgave.</strong> Nettstedet måtte skrives riktig ut på ukjent papir, i begge fargetemaene, uten at et separat dokument ble vedlikeholdt ved siden av nettversjonen.</p>\n<p><strong>Handling.</strong> Det er ett stilark på 637 linjer med én enkelt sideregel og én utskriftsblokk. Sideregelen setter en marg og setter bevisst ingen papirstørrelse, så det leseren velger i dialogen, kommer gjennom, og alt annet tilpasser seg det. Rotskriftstørrelsen er låst i punkter, slik at alle relative mål har et fysisk anker, og én linjelengde på rundt syttito tegn brukes på de fire blokkene på øverste nivå — der arket vinner over designet, siden en side i liggende format ellers ville gitt linjer på rundt hundre og ti tegn. Fire motoroppførsler er dokumentert som feller, og hver av dem har forårsaket en reell feil. Visningsområdeenheter løses opp mot sideboksen i én motor og mot vinduet i en annen, så en side skrevet ut fra et bredt vindu får en tredjedel av hver linje kuttet bort i den andre. Elementer med fast posisjon males på hvert ark, så de dekorative er fjernet, og to av dem er gjenbrukt som brevhode og kolofon. Paletten bruker hvit tekst som standard fordi standardtemaet er mørkt, noe som skrev ut de fire seksjonsetikettene som løper inn i teksten, hvitt på hvitt — ordene var rett og slett borte. Og sidedelte medier kan ikke dele en flex- eller grid‑boks, noe som ga et tomt første ark på en resultatside. En kontroll i seks deler dekker dette og tester seks kombinasjoner av papir og retning, begge fargevalgsinnstillingene og det faktiske sidetallet opp mot det innholdshøyden tilsier.</p>\n<p><strong>Resultat.</strong> Nettstedet skrives ut på ukjent papir i begge temaene, og de fire motoroppførslene er skrevet ned med feilen hver av dem forårsaket. De kjente begrensningene er dokumentert i stedet for skjult: ingen sidetall eller levende kolumnetitler, én motor ignorerer kontroll av horunger og enker, og innfelte initialer er ikke universelle. En forvillet kommentaravslutning fikk en gang CSS‑linteren til å svelge en hel regel og passere feilfritt to ganger — bare utskriftskontrollen la merke til det.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Designsystemer og brukergrensesnitt",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "UX/UI‑design",
        "Webutvikling",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/mirrored-the-site-to-gemini-and-gopher-137/",
      "url": "https://engineer.company/nb/portfolio/mirrored-the-site-to-gemini-and-gopher-137/",
      "title": "Speilet hele nettstedet som 777 Gemini‑dokumenter og 777 Gopher‑dokumenter fra det samme publiserte treet, med null byte endret i HTML‑en.",
      "summary": "Nettstedet kan leses over fire protokoller fra ett bygg, med 777 dokumenter hver og null endring i HTML-en, og de alternative representasjonene koster omtrent…",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet er statisk, har ingen sporing og ingen backend under kjøring, og argumentet dets er at et dokument ikke trenger en megabyte JavaScript for å bli lest. Det argumentet er lett å fremføre og vanskelig å demonstrere. To små internettprotokoller demonstrerer det direkte, fordi ingen av dem kan bære et skript i det hele tatt.</p>\n<p><strong>Oppgave.</strong> Hele nettstedet måtte publiseres over Gemini og Gopher fra det samme innholdet, uten et andre innholdstre og uten å endre HTML‑en.</p>\n<p><strong>Handling.</strong> Begge er utdataformater fra det samme bygget og ikke en separat pipeline. Nettstedsgeneratoren fikk egendefinerte medietyper og utdataformater, og hver side, seksjon og taksonomiterm fikk to representasjoner til ved siden av HTML‑en og Markdown‑tvillingen sin. Resultatet er 777 Gemini‑dokumenter og 777 Gopher‑menyer produsert fra den samme kilden, produksjonssatt i det samme treet og servert av to små daemoner på den samme verten. To egenskaper gjorde det verdt å gjøre og ikke bare en kuriositet. Begge formatene er merket som ikke‑alternative representasjoner, så ingenting ved HTML‑en endret seg — ikke én byte, og det ble målt i stedet for antatt. Og Gopher‑formatet har ingen måte å legge en lenke inne i en setning på, siden en menylinje er tabulatorseparerte felt, så prosaen brytes hardt ved sekstiåtte kolonner under bygget; den begrensningen skjerpet skrivingen på en måte HTML‑en aldri krevde. Et Tor‑onion‑speil står ved siden av dem, og det er det eneste offentlige ansiktet plattformen kunne legge til uten å åpne en port i brannmuren, fordi daemonen åpner forbindelsen utover og ingenting kobler seg inn.</p>\n<p><strong>Resultat.</strong> Nettstedet kan leses over fire protokoller fra ett bygg, med 777 dokumenter hver og null endring i HTML‑en, og de alternative representasjonene koster omtrent tolv prosent av HTML‑ens egen vekt på disk. Bare én av de fire måles for trafikk, og det står i plattformens eget statistikkdokument i stedet for å bli ignorert i det stille.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Frontend‑utvikling",
        "Infrastruktur",
        "Internasjonalisering",
        "Webutvikling",
        "DevOps og CI/CD‑automatisering",
        "Internasjonalisering og lokalisering",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/replaced-963-structured-data-blocks-with-one-graph-138/",
      "url": "https://engineer.company/nb/portfolio/replaced-963-structured-data-blocks-with-one-graph-138/",
      "title": "Byttet ut 963 anonyme blokker med strukturerte data, som gjentok selskapet 1 671 ganger, mot én lenket graf av 16 typer utledet fra stabile opprinnelsesidentifikatorer.",
      "summary": "Én graf med stabil identitet erstatter 963 anonyme gjentakelser, og sidene bærer mindre, ikke mer.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet sendte ut strukturerte data slik de fleste nettsteder gjør: én blokk per side, der hver blokk beskrev organisasjonen på nytt fra bunnen av. På tvers av nettstedet ble det 963 blokker som gjentok det samme selskapet 1 671 ganger, anonymt — ingen stabil identifikator noe sted, så ingenting som leste dataene, kunne se at organisasjonen på én side var organisasjonen på en annen.</p>\n<p><strong>Oppgave.</strong> De strukturerte dataene måtte bli én graf med stabil identitet i stedet for en haug med blokker som tilfeldigvis inneholder de samme ordene.</p>\n<p><strong>Handling.</strong> Identifikatorene utstedes nå fra nettstedets origin — én for organisasjonen, én for nettstedet, én for personen — og hver blokk refererer til dem i stedet for å gjenta innholdet deres. Identifikatorene gjelder hele origin‑en, ikke hvert språk for seg, fordi selskapet er det samme selskapet på dansk. Seksten typer er i bruk og dekker tjenestekatalogen og tilbudene i den, omtalene og arbeidet de beskriver, samlingene og brødsmulestiene deres. To beslutninger holdt vekten nede. Listesider publiserer elementene sine bare med URL i stedet for å bygge dem inn, og det ble målt: å navngi dem på porteføljeindeksen ville ha betydd 107 fullstendige formuleringer og en økning på 52 % i den komprimerte siden. Og kontrollen som vokter dette, validerer ikke bare syntaks — den slår fast at hver blokk kan parses, at hver identifikator løses opp, og at hver URL grafen nevner, faktisk ble bygget, så en graf som peker på en side som ikke finnes, stoppes av kvalitetsporten.</p>\n<p><strong>Resultat.</strong> Én graf med stabil identitet erstatter 963 anonyme gjentakelser, og sidene bærer mindre, ikke mer. Målingen som startet arbeidet, er verdt å huske: nettstedet hadde sendt ut den samme organisasjonsbeskrivelsen 1 671 ganger uten at noe kunne knytte dem sammen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Automatisering og CI/CD",
        "Datastyring",
        "Frontend‑utvikling",
        "Merkevare og markedsføring",
        "Webutvikling",
        "Datastyring og datakvalitet",
        "Klar for AI‑agenter",
        "Merkevare, markedsføring og SEO",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/cut-the-stylesheet-bundle-from-87kb-to-31kb-139/",
      "url": "https://engineer.company/nb/portfolio/cut-the-stylesheet-bundle-from-87kb-to-31kb-139/",
      "title": "Kuttet stilarkpakken fra 87 KB til 31 KB ved å ta ut en base64‑kodet skrift, og fjernet 756 KB fordelt på 22 filer som ble publisert ved hver produksjonssetting og som ingenting refererte til.",
      "summary": "Pakken er 31 KB i stedet for 87, 756 KB med døde ressurser produksjonssettes ikke lenger, og hver beslutning har en måling knyttet til seg — også den som gikk…",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedets stilarkpakke var på 87 KB, noe som for et dokumentnettsted uten rammeverk er mesteparten av en sidevekt brukt før noe innhold kommer fram. Nettstedet publiserte også en katalog med ikoner ved hver produksjonssetting, generert én gang og ikke referert av noe siden.</p>\n<p><strong>Oppgave.</strong> Vekten måtte ned uten at designet endret seg, og den måtte ned av en grunn det gikk an å peke på, ikke gjennom generell opprydding.</p>\n<p><strong>Handling.</strong> Måling først. To tredjedeler av det største stilarket var én base64‑kodet font — omtrent 56 KB tekst som kodet omtrent 42 KB font — bygd inn for en gevinst ved første opptegning som en forhåndslastet ekstern fil får uansett, og betalt for på hver side, enten vekten trengtes eller ikke. Uten den gikk filen fra 84 KB til 28 og hele pakken fra 87 til 31. De tre fontvektene forhåndslastes i stedet, og den tredje kom med på listen først da feltdata viste at den lukket den lengste kritiske kjeden, ikke fordi tre hørtes fullstendig ut. Uavhengig av dette fant en gjennomgang av hva produksjonssettingen faktisk publiserte, tjueen genererte ikoner og en duplisert konfigurasjonsfil, 756 KB, levert hver gang og referert fra ingen steder; og nettstedets egen ikonfil gikk ned fra 145 KB til 15. Et moderne bildeformat ble vurdert for merkevaresymbolet, målt og avvist — det ble større, og valgmekanismen velger etter rekkefølge og ikke etter størrelse, så en moderne nettleser ville ha tatt den tyngre filen overalt. To kontroller holder nå grensen: ingen fotografier får vises bredere enn halvparten av kildepikslene sine, og hver illustrasjon må publiseres i en størrelse dens egne piksler dekker ved både standard og høy tetthet.</p>\n<p><strong>Resultat.</strong> Pakken er 31 KB i stedet for 87, 756 KB med døde ressurser produksjonssettes ikke lenger, og hver beslutning har en måling knyttet til seg — også den som gikk den andre veien, som er den nyttigste dokumentasjonen av de to.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Webutvikling",
        "Ytelsesoptimalisering",
        "Merkevare, markedsføring og SEO",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/fixed-a-sitemap-with-one-shared-timestamp-140/",
      "url": "https://engineer.company/nb/portfolio/fixed-a-sitemap-with-one-shared-timestamp-140/",
      "title": "Rettet et sitemap der 172 av 176 URL‑er delte ett endringstidspunkt, ved å hente datoen fra git‑historikken etter å ha slått fast at eksporten skriver om hver fil ved hver kjøring.",
      "summary": "En ny synkronisering etter en måned med innholdsendringer berører nå åtte filer av 186 i stedet for alle, og sitemapen sier noe sant.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedets sitemap fortalte søkemotorene at 172 av de 176 sidene var sist endret i det samme øyeblikket. Det er ikke en subtil unøyaktighet — en endringsdato er et løfte til en søkerobot om at innholdet på en side har endret seg, og et nettsted som gir det løftet 172 ganger samtidig, forteller enten sannheten om en fullstendig omskriving eller forteller ingen noe nyttig.</p>\n<p><strong>Oppgave.</strong> Datoene måtte beskrive innholdet og ikke filen, uten å finne opp en presisjon repoet ikke har.</p>\n<p><strong>Handling.</strong> Årsaken var at datoen kom fra filens endringstidspunkt, og innholdseksporten skriver om hver fil ved hver kjøring, så én enkelt synkronisering stemplet hele korpuset. Rettelsen flytter kilden til versjonshistorikken, med en reservekjede som først prøver et eksplisitt felt, så commit‑historikken, så filen. Fellen i den rettelsen er verdt å dokumentere: det bokstavelige feltnavnet må stå i listen, ellers leser generatoren det aldri, så en konfigurasjon som ser ut til å foretrekke en forfatterangitt dato, men utelater navnet, ignorerer i det stille hver forfatterangitte dato. To andre datobeslutninger kom ut av det samme arbeidet. Databasen lagrer nå når hver post ble skrevet og sist revidert, bevisst holdt atskilt fra når arbeidet fant sted — det er år mellom dem, og å blande dem sammen ville datere en side skrevet i år til for et tiår siden. Og radene i den filen er valgfrie, og ingenting utledes, fordi repoets egen historikk begynner etter at innholdet gjorde det: en manglende dato lar feltet stå tomt i stedet for å registrere en migrering, ut fra prinsippet om at et presist, feil tall er verre enn et fraværende, siden bare det feile blir trodd.</p>\n<p><strong>Resultat.</strong> En ny synkronisering etter en måned med innholdsendringer berører nå åtte filer av 186 i stedet for alle, og sitemapen sier noe sant. Den generelle regelen ble skrevet inn i metadatadokumentet ved siden av, fordi den samme fellen gjelder hvert felt som har en reservekjede.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Merkevare og markedsføring",
        "Python",
        "Testing og kvalitetssikring",
        "Webutvikling",
        "DevOps og CI/CD‑automatisering",
        "Klar for AI‑agenter",
        "Merkevare, markedsføring og SEO",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/ran-development-as-49-written-initiatives-141/",
      "url": "https://engineer.company/nb/portfolio/ran-development-as-49-written-initiatives-141/",
      "title": "Drev utviklingen av nettstedet som 51 skriftlige initiativer fordelt på 54 plandokumenter, hvert med sine åpne avkrysningsbokser, sine målinger og beslutningene det avslo — blant dem seks av åtte funn om agentberedskap som ble avvist med begrunnelsen registrert.",
      "summary": "Femtien beslutninger med tilhørende begrunnelse, omtrent tre fjerdedeler lukket, og de avslåtte er like lette å finne fram til som de aksepterte.",
      "content_html": "<p><strong>Situasjon.</strong> Et enpersonsselskap har ingen planleggingsmøter, ingen backlog‑grooming og ingen som kan være uenig i en beslutning. Det det har i stedet, er en sterk tendens til å gjøre det arbeidet som akkurat nå er interessant, og ingen skriftlige spor etterpå av hvorfor noe ble valgt — noe som blir et problem første gang en beslutning må tas opp igjen eller forsvares.</p>\n<p><strong>Oppgave.</strong> Utviklingen av nettstedet trengte en skriftlig dokumentasjon per initiativ som tåler å bli lest på nytt måneder senere, og som dekker initiativene som ble avslått, i tillegg til dem som ble bygget.</p>\n<p><strong>Handling.</strong> Femtien initiativer fordelt på femtifire nummererte og indekserte plandokumenter, hvert med problemet, målingene som ble gjort, beslutningen og de åpne avkrysningsboksene. De er ikke oppsummeringer skrevet i etterkant. De inneholder tallene som avgjorde diskusjonen — målingene av jobbplanlegging som formet kontrollkjøreren, bildeformatet som ble testet og avvist fordi det ble større, prisstigen som eieren bekreftet, gjennomgangen som fant at nettstedet beviste kompetanse på 371 sider uten å oppgi noen pris, noen form for oppdrag eller noe minimum noe sted. Avslåtte funn får samme behandling som aksepterte: en gjennomgang av agentberedskapen ga åtte funn, og seks ble avvist, hvert med avgjørelsen dokumentert, nettopp for at et senere automatisk forslag ikke i det stille skal kunne reversere en skriftlig beslutning. Statustavlen leser en plans avkrysningsbokser og ikke prosaen, fordi prosa sier «ferdig» og bokser sier hva som er åpent. Det skillet var ikke teoretisk — en dokumentasjonslinter fant senere seks planer som var merket som ferdige, men fortsatt hadde uavkryssede bokser.</p>\n<p><strong>Resultat.</strong> Femtien beslutninger med tilhørende begrunnelse, omtrent tre fjerdedeler lukket, og de avslåtte er like lette å finne fram til som de aksepterte. Det det koster, er at hvert initiativ har en skriftlig redegjørelse, noe som er en reell skatt på småarbeid og av og til har betydd at dokumentasjonen er lengre enn endringen den beskriver.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dokumentasjon",
        "Interessenter og rapportering",
        "Produkt og krav",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teknisk ledelse",
        "Klar for AI‑agenter",
        "Produktstrategi og krav",
        "Prosjektledelse (smidig)",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/put-the-documentation-under-its-own-linter-142/",
      "url": "https://engineer.company/nb/portfolio/put-the-documentation-under-its-own-linter-142/",
      "title": "Satte dokumentasjonen under sin egen linter — et linjebudsjett per fil som bare kan strammes inn, et krav om indeks og en kontroll av reelle stier — som fant seks av ni planer merket som ferdige med uavkryssede bokser, og fjorten commit‑sperrende oppgaver som ikke var nevnt i noe dokument.",
      "summary": "Tjueseks dokumenter på til sammen omtrent 2 666 effektive linjer, hvert under et budsjett som ikke kan vokse, med hver sti og hvert symbol de nevner…",
      "content_html": "<p><strong>Situasjon.</strong> Repoet styrer seg selv med skriftlige instrukser — tjueseks dokumenter som dekker kvalitetspolicyen, designsystemet, universell utforming, utskrift, metadata, oversettelse og git. Instrukser har et bestemt forfallsmønster: de blir lengre, de glir bort fra koden, de begynner å vise til filer som er flyttet, og ved en viss størrelse slutter de å bli lest. En regel som ikke blir lest, er ingen regel.</p>\n<p><strong>Oppgave.</strong> Dokumentasjonen trengte samme behandling som koden — en linter med grenser som feiler i stedet for å gi råd.</p>\n<p><strong>Handling.</strong> Åtte policyer, alle mekaniske. Et hardt tak på dokumentlengde og et mykere som advarer. Et linjebudsjett per fil som bare kan skrus nedover, så et dokument kan krympe, men ikke vokse igjen. Et krav om indeks over en viss terskel, og om en seksjonsindeks over en lavere. En plasseringsregel om hvilket dokument som eier hvilket emne. Gyldige lenker. Ærlige planer — statusen en plan oppgir, må stemme med dens egne avkrysningsbokser. En regel om at hver oppgave en commit må gjennom, er nevnt i et eller annet dokument. Og en kontroll av reelle stier: at hver katalogkvalifisert sti og hvert kodesymbol et dokument nevner, faktisk finnes. Den første kjøringen var argumentet for hele øvelsen: seks planer merket som ferdige med uavkryssede bokser, fjorten oppgaver som hver commit måtte gjennom og som ikke var nevnt i noe dokument i det hele tatt, tre ødelagte lenker, ett foreldreløst dokument og tre døde stier fordelt på fem dokumenter. Budsjettet avviste deretter sin egen forfatter — å skrive oppsummeringen av dette arbeidet inn i utviklingsdokumentet presset det over grensen, og slik oppsto et eget dokument, og det er nettopp slik policyen er ment å virke.</p>\n<p><strong>Resultat.</strong> Tjueseks dokumenter på til sammen omtrent 2 666 effektive linjer, hvert under et budsjett som ikke kan vokse, med hver sti og hvert symbol de nevner kontrollert mot treet. Det ubehagelige funnet er det som er verdt å gjenta: fjorten kontroller sto som port for hver eneste commit uten å stå i noe dokument, så reglene maskinen håndhevet og reglene menneskene leste, hadde allerede skilt lag.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Prosjektledelse",
        "Python",
        "Teknisk ledelse",
        "Testing og kvalitetssikring",
        "Prosjektledelse (smidig)",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/brought-the-quality-gate-code-under-a-linter-143/",
      "url": "https://engineer.company/nb/portfolio/brought-the-quality-gate-code-under-a-linter-143/",
      "title": "Satte 10 242 linjer JavaScript i kvalitetsporten under en formaterer og en linter etter å ha slått fast at det var den største kodemengden i repoet og den eneste ingenting leste, og rettet 13 funn uten å undertrykke noen.",
      "summary": "Den største og mest bærende koden i repoet er nå formatert, lintet og typekontrollert, med tretten funn rettet og null undertrykkinger.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedets kvalitetsport er trettito skript på til sammen 10 242 linjer JavaScript. Det var med god margin den største kodemengden i repoet, og det var den eneste kodemengden ingenting leste — ingen formaterer, ingen linter, ingen typekontroll. Programmene som håndhevet hver regel i prosjektet, var de eneste programmene som ikke var underlagt noen av dem.</p>\n<p><strong>Oppgave.</strong> Kontrollskriptene måtte holdes til den standarden de finnes for å håndheve, og formatereren måtte tilpasses dem, ikke omvendt.</p>\n<p><strong>Handling.</strong> Formatereren kom først, og innrykksbredden var den interessante beslutningen. Repoets standard er fire mellomrom; med fire mellomrom ville formatereren ha skrevet om 2 173 linjer i det største kontrollskriptet. En overstyring til to mellomrom for de filene reduserte det til 38 linjer med reelle avvik. Prinsippet som ble dokumentert sammen med den, er at formatereren tilpasses koden, ikke koden til formatereren — å omformatere to tusen linjer for å tilfredsstille en preferanse ødelegger muligheten til å lese historikken til filen. Så kom linteren, som ga tretten reelle funn, alle rettet og ingen undertrykt. Python‑kontrollskriptene fikk samme behandling gjennom en typekontroll konfigurert på middels strenghet med tretti enkeltregler fra det strenge nivået slått på i tillegg, valgt ved måling: med den innstillingen er treet stille, og av de tretti kandidatene var tjueni allerede stille og én slo ut — og den ble rettet i stedet for unntatt. Typekontrollen fant to reelle feil som linteren hadde sluppet gjennom uten anmerkninger, begge om formen på en verdi og ikke om syntaksen.</p>\n<p><strong>Resultat.</strong> Den største og mest bærende koden i repoet er nå formatert, lintet og typekontrollert, med tretten funn rettet og null undertrykkinger. Grunnen til at det betyr mer enn linjetallet antyder, står i planen: en feil i nettstedets stilark viser seg som en side som ser feil ut, og en feil i et kontrollskript viser seg som en kontroll som består når den ikke burde.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Python",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-database-driven-cv-and-portfolio-generator-144/",
      "url": "https://engineer.company/nb/portfolio/built-a-database-driven-cv-and-portfolio-generator-144/",
      "title": "Bygde en databasedrevet generator for CV, referanser, portefølje og søknadsbrev i Python — 41 moduler, 10 580 linjer — som gjengir seks utdataformater fra én SQLite‑kilde med 23 tabeller, satt sammen av en idempotent pipeline i 19 trinn.",
      "summary": "Seks formater, tre språk og fem temaer kommer ut av én database, og en rettet setning rettes én gang.",
      "content_html": "<p><strong>Situasjon.</strong> En CV, en referanseliste, en portefølje og et søknadsbrev er de samme faktaene ordnet på fire måter, og å holde dem som fire dokumenter betyr at hver rettelse gjøres fire ganger og til slutt ikke blir gjort. Med tre språk ganges det med tre. Feilmodusen er ikke at ett dokument er feil; den er at to dokumenter sier forskjellige ting, og at ingenting sier hvilket som er gjeldende.</p>\n<p><strong>Oppgave.</strong> Én kilde til fakta måtte produsere hvert dokument, på hvert språk, i hvert format, med oppsettet bestemt av kode og ikke av den som sist redigerte en fil.</p>\n<p><strong>Handling.</strong> Kilden er en SQLite‑database med tjuetre tabeller — resultater, titler, selskaper, regioner, kategorier, tjenester, profiler, referanser, sammendrag — satt sammen av en pipeline i nitten trinn som går fra en tom fil til en komplett database med én kommando. Trinnene er ordnet, og hvert av dem er skrevet slik at det er trygt å gjenta, så pipelinen kan kjøres mot en eksisterende database uten å duplisere en rad. Førtien Python‑moduler på til sammen 10 580 linjer gjengir seks utdataformater fra den: HTML, PDF i to varianter, ren tekst, Markdown og JSON, gjennom seks Jinja‑maler og åtte stilark. Avhengighetene under kjøring ble holdt på nøyaktig to, en malmotor og en PDF‑renderer, ut fra argumentet om at en dokumentgenerator som ikke kan installeres om fem år, ikke har bevart noe. Målretting er et førsteklasses konsept og ikke en manuell redigering: en profil velger hvilke resultater som vises og i hvilken rekkefølge, så et dokument rettet mot én type leser er en spørring og ikke en kopi.</p>\n<p><strong>Resultat.</strong> Seks formater, tre språk og fem temaer kommer ut av én database, og en rettet setning rettes én gang. Kostnaden er at systemet nå er den eneste måten å produsere et dokument på — det finnes ikke lenger en fil å åpne og redigere i en fart, og å legge til et språk betyr å legge det til overalt før noe som helst kan bygges.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "Databaser",
        "Fullstack‑utvikling",
        "Internasjonalisering",
        "Løsningsarkitektur",
        "Python",
        "SQL",
        "Backend- og API‑utvikling",
        "Databasedesign og datamodellering",
        "Fullstack‑produktutvikling",
        "Internasjonalisering og lokalisering",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/held-the-generator-to-964-tests-and-a-coverage-floor-145/",
      "url": "https://engineer.company/nb/portfolio/held-the-generator-to-964-tests-and-a-coverage-floor-145/",
      "title": "Holdt generatoren til 981 testtilfeller med et minstekrav på 92 % grendekning og advarsler behandlet som feil, og verifiserte idempotens ved å kjøre hele byggepipelinen to ganger fra en tom fil og kreve at den andre kjøringen ikke endret noe.",
      "summary": "Pipelinen kan kjøres på nytt mot en database i drift uten frykt, og det er det som i det hele tatt gjør trinnvist innholdsarbeid mulig.",
      "content_html": "<p><strong>Situasjon.</strong> En generator som setter sammen en database fra bunnen av, har en bestemt type feil: den virker første gang og ødelegger andre gang. Trinn som setter inn uten å sjekke, trinn som er avhengige av rekkefølgen i et tidligere trinns utdata, trinn som er trygge hver for seg, men ikke sammen. Ingenting av dette viser seg i en test som starter fra ingenting og kjøres én gang.</p>\n<p><strong>Oppgave.</strong> Det måtte bevises at bygget er repeterbart og ikke bare fungerer, og testsuiten måtte være stor nok og streng nok til at en regresjon ikke kunne slippe gjennom i det stille.</p>\n<p><strong>Handling.</strong> Idempotenstesten er den mest håndfaste og den mest nyttige: bygg hele databasen fra en tom fil, ta et øyeblikksbilde, kjør hele pipelinen på nitten trinn på nytt over resultatet, og krev at den andre kjøringen ikke endrer noe. Radantall, innhold og identifikatorer må alle stemme. Rundt den ligger 383 testfunksjoner — 981 tilfeller når de parameteriserte er utvidet — fordelt på førtifem filer og 7 944 linjer, som dekker pipelinen, rendererne, innholdslasterne, målrettingslogikken og kontrollskriptene. Grendekningen har et gulv på nittito prosent som håndheves i bygget i stedet for å rapporteres i et sammendrag, og suiten måler i dag omtrent 95 %, så gulvet har klaring uten å være pynt. Advarsler er konfigurert som feil, og det er innstillingen som betyr mest i praksis — en avviklingsmelding som skrives ut i to år, er en avviklingsmelding ingen leser, og kjøringen som gjør den om til en rød test, er kjøringen som får den rettet.</p>\n<p><strong>Resultat.</strong> Pipelinen kan kjøres på nytt mot en database i drift uten frykt, og det er det som i det hele tatt gjør trinnvist innholdsarbeid mulig. Gulvet er et gulv, ikke et mål, og det er verdt å si at nittito prosent grendekning fortsatt etterlater grener som ingenting noen gang har tatt — tallet begrenser risikoen, det fjerner den ikke, og to av feilene som ble funnet senere i prosjektet, lå i kode som dekningsrapporten viste som dekket.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Databaser",
        "DevOps",
        "Pålitelighet og sikkerhetskopier",
        "Python",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/established-pdf-ua-1-conformance-across-nine-documents-146/",
      "url": "https://engineer.company/nb/portfolio/established-pdf-ua-1-conformance-across-nine-documents-146/",
      "title": "Etablerte PDF/UA‑1‑samsvar for ni dokumenter med 106 av 106 regler, og fant at arkivvarianten feilet på én regel av 146 — et nesten‑treff som ser ut som bestått for alle som ikke kjører validatoren.",
      "summary": "Samsvar med kravene til universell utforming er bevist med full score, og arkivpåstanden er ærlig oppgitt som nesten oppfylt, med det konkrete avviket…",
      "content_html": "<p><strong>Situasjon.</strong> En PDF som ser riktig ut på skjermen, forteller deg ingenting om hvorvidt en skjermleser kan lese den, om overskriftene danner en struktur, om språket er deklarert, eller om den fortsatt vil kunne åpnes om tjue år. Samsvar med standardene for universelt utformet PDF og for arkiv‑PDF kan begge kontrolleres maskinelt, og et dokument som aldri har vært kjørt gjennom validatoren, er et dokument som kommer med en uverifisert påstand.</p>\n<p><strong>Oppgave.</strong> De genererte dokumentene måtte måles mot begge standardene, med resultatet dokumentert som et tall og ikke som en intensjon.</p>\n<p><strong>Handling.</strong> Ni dokumenter — CV‑en i variantene sine, referanselisten, porteføljen og søknadsbrevet, på tvers av språk — ble kjørt gjennom en uavhengig validator, hver for seg mot profilen for universell utforming og mot arkivprofilen. Profilen for universell utforming består med 106 av 106 regler, noe som krevde tagget struktur, deklarert dokumentspråk, alternativ tekst på hver grafikk som ikke er dekorativ, en reell tittel i metadataene og en eksplisitt leserekkefølge i stedet for den layouten tilfeldigvis gir. Arkivprofilen er det mer interessante resultatet: den feiler på nøyaktig én regel av 146. Det er den typen resultat det er lett å rapportere feil. Hundre og førtifem beståtte regler leses som samsvar i et sammendrag, og det er ikke samsvar; det er et dokument som vil bli avvist av et system som håndhever standarden. Regelen som feiler, er dokumentert med hva den er og hvorfor den ikke er lukket, i stedet for å bli avrundet bort.</p>\n<p><strong>Resultat.</strong> Samsvar med kravene til universell utforming er bevist med full score, og arkivpåstanden er ærlig oppgitt som nesten oppfylt, med det konkrete avviket navngitt. Begrensningen det er verdt å være direkte om, er at begge tallene kommer fra én validator; en annen implementasjon kan være uenig, og en regel som består, er bare et bevis på at akkurat denne kontrollen ikke hadde noe å si om den.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Python",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Webutvikling",
        "Teknisk dokumentasjon",
        "UI/UX‑design og designsystemer",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/enabled-every-python-linter-rule-as-an-error-147/",
      "url": "https://engineer.company/nb/portfolio/enabled-every-python-linter-rule-as-an-error-147/",
      "title": "Slo på alle reglene i Python‑linteren på feilnivå og arbeidet seg gjennom 1 815 funn ned til null, med en skriftlig begrunnelse for hvert av de få unntakene og to av dem sikret av en kontroll i stedet for en kommentar.",
      "summary": "Linteren kjører med full styrke og null funn, og hvert avvik er dokumentert på linjen der det tas.",
      "content_html": "<p><strong>Situasjon.</strong> De fleste prosjekter velger et behagelig utvalg av linterens regler, og utvalget bestemmes av hvilke regler som tilfeldigvis var stille den dagen det ble konfigurert. Det gjør konfigurasjonen til et register over kodens eksisterende vaner og ikke en standard koden holdes til, og hver regel som er utelatt, er en feilklasse ingen noen gang vil få høre om.</p>\n<p><strong>Oppgave.</strong> Standardinnstillingen måtte snus — hver regel verktøyet implementerer, slått på som feil — og etterslepet som fulgte, måtte arbeides ned til null i stedet for å forhandles ned ved å slå regler av igjen.</p>\n<p><strong>Handling.</strong> Å velge hele regelsettet ga 1 815 funn ved første kjøring. De ble gått gjennom etter kategori og ikke etter fil, fordi kategoriene forteller deg noe: ubrukte argumenter og innebygde navn som skygges, er støy, men sikkerhetskategorien, kategorien for muterbare standardverdier og kategorien for unntakshåndtering pekte hver på reell oppførsel. Ekte uforenligheter finnes — en formaterer og en linter kan være uenige om samme linje, og noen få regler motsier prosjektets egne bevisste valg — og hvert av det lille antallet unntak har en skriftlig begrunnelse der unntaket gjøres, som sier hva regelen ville ha og hvorfor denne koden gjør noe annet. To av dem går lenger og er støttet av en kontroll i stedet for en kommentar, så unntaket ikke i det stille kan utvides: regelen er slått av, og en test slår fast den konkrete egenskapen regelen ville ha håndhevet. Typekontroll kjører i streng modus ved siden av, noe som er en separat og mer krevende standard, og det er den som fanget feil linteren ikke kunne se, fordi de handler om hva en verdi er og ikke hvordan den er skrevet.</p>\n<p><strong>Resultat.</strong> Linteren kjører med full styrke og null funn, og hvert avvik er dokumentert på linjen der det tas. Kostnaden er reell og verdt å nevne: den strengeste innstillingen gir funn som virkelig ikke er verdt å handle på, og noen må gjøre den vurderingen 1 815 ganger i stedet for én gang.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Python",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/vendored-a-qr-encoder-in-522-lines-148/",
      "url": "https://engineer.company/nb/portfolio/vendored-a-qr-encoder-in-522-lines-148/",
      "title": "Bygde inn en QR‑enkoder — Reed‑Solomon over GF(256), fast moduloppsett, åtte maskemønstre — på 522 linjer i stedet for å ta inn en tredje kjøretidsavhengighet, og verifiserte den ved å lese den ferdige matrisen tilbake med en uavhengig skrevet dekoder.",
      "summary": "Antallet avhengigheter under kjøring ble værende på to, og koderen er den eneste vendorede komponenten i prosjektet.",
      "content_html": "<p><strong>Situasjon.</strong> Dokumentene trengte en QR‑kode som lenket til nettversjonen. Alle tilgjengelige biblioteker gjør dette, og å ta inn et av dem ville ha lagt til en tredje avhengighet under kjøring i et prosjekt som bevisst hadde holdt seg til to — en malmotor og en PDF‑renderer — ut fra argumentet om at en dokumentgenerator fortsatt skal kunne installeres om mange år.</p>\n<p><strong>Oppgave.</strong> Enten måtte avhengigheten godtas eller formatet implementeres, og implementasjonen måtte verifiseres som korrekt i stedet for bare å produsere noe firkantet og svart.</p>\n<p><strong>Handling.</strong> Koderen er på 522 linjer og implementerer de delene av spesifikasjonen bruken faktisk trenger: koding i byte‑modus, Reed‑Solomon‑feilretting over Galois‑kroppen med 256 elementer med generatorpolynomet bygd med påkrevd grad, den faste plasseringen av moduler med posisjonsmønstre, tidsmønstre og justeringsmønstre, format- og versjonsinformasjon, og alle åtte datamaskemønstre vurdert mot spesifikasjonens fire strafferegler, slik at masken med lavest poengsum velges i stedet for en fast maske. Verifiseringen er den delen som gjorde det forsvarlig. Å teste en koder mot dens egen logikk beviser ingenting, så den ferdige modulmatrisen leses tilbake av en dekoder skrevet uavhengig ut fra spesifikasjonens leserekkefølge, og testen slår fast at den dekodede strengen er lik inndataene. Det gjør «den produserer et plausibelt bilde» om til «den produserer en kode som dekodes til riktig URL», og det sjekkes ved hver kjøring i stedet for én gang på øyemål med en telefon.</p>\n<p><strong>Resultat.</strong> Antallet avhengigheter under kjøring ble værende på to, og koderen er den eneste vendorede komponenten i prosjektet. Det bør sies rett ut at dette ikke er en generell implementasjon — den støtter modusene og versjonene dokumentene bruker og ingenting annet, og den ærlige begrunnelsen er avhengighetsbudsjettet, ikke en påstand om at resultatet er bedre enn et modent bibliotek.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Dokumentasjon",
        "Python",
        "Testing og kvalitetssikring",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/wrote-tests-for-the-checkers-themselves-149/",
      "url": "https://engineer.company/nb/portfolio/wrote-tests-for-the-checkers-themselves-149/",
      "title": "Skrev tester for selve kontrollene etter å ha slått fast at en kontroll som bare får ren inndata, en dag vil melde rent fordi den ikke leste noe — plantet en stavefeil for å bekrefte at stavekontrollen finner den, og hentet et id‑område fra databasen i stedet for fra et tall i testen.",
      "summary": "Hvert kontrollskript i prosjektet har nå en test som beviser at det feiler på dårlige inndata, og assertene for intervaller leser fra sannhetskilden.",
      "content_html": "<p><strong>Situasjon.</strong> Prosjektet kjører et sett med egne kontrollskript over sitt eget innhold — en stavekontroll, en kontroll av identifikatorintervaller, en stemmekontroll, en kontroll av tallkonsistens. Hver av dem hadde vært grønn i månedsvis. Et kontrollskript som aldri har sett annet enn rene inndata og aldri har rapportert annet enn rent, kan ikke skilles fra et kontrollskript som ikke leser noe i det hele tatt, og det fantes ingen test i suiten som kunne skille de to.</p>\n<p><strong>Oppgave.</strong> Kontrollskriptene måtte tvinges til å bevise at de kan feile, og testdataene de sjekker mot, måtte slutte å være håndvedlikeholdte kopier av det de beskriver.</p>\n<p><strong>Handling.</strong> Mønsteret som brukes gjennomgående, er å plante feilen kontrollskriptet finnes for å finne, og kreve at det finner den. Stavekontrollen får et bevisst feilstavet ord, og testen feiler hvis kjøringen kommer tilbake ren. Stemmekontrollen får prosa i første person og må avvise den. Moteordkontrollen får et forbudt ord. Hver av disse er en liten test, og hver av dem lukket en reell blindsone, fordi to av kontrollskriptene viste seg å lese et smalere sett med filer enn dokumentasjonen deres hevdet, og hadde hoppet over innhold i det stille. Den andre endringen handler om hvor en test henter forventningene sine fra: kontrollen av identifikatorintervaller sammenlignet tidligere med et tall skrevet i testfilen, noe som betydde at en test måtte redigeres hver gang nytt innhold ble lagt til, og en redaktør som oppdaterte tallet uten å se etter, hadde slått av kontrollen. Nå utleder den intervallet fra databasen, så kontrollen beskriver dataene og ikke et utdatert minne om dem.</p>\n<p><strong>Resultat.</strong> Hvert kontrollskript i prosjektet har nå en test som beviser at det feiler på dårlige inndata, og assertene for intervaller leser fra sannhetskilden. Den ubehagelige delen er hva dette avdekket: en grønn kontroll hadde vært meningsløs minst to steder i en ukjent tidsperiode, og det finnes ingen måte å finne ut i etterkant hva som slapp gjennom.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Databaser",
        "Datastyring",
        "Python",
        "Testing og kvalitetssikring",
        "Datastyring og datakvalitet",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/found-the-commit-hooks-and-the-gate-disagreeing-150/",
      "url": "https://engineer.company/nb/portfolio/found-the-commit-hooks-and-the-gate-disagreeing-150/",
      "title": "Fant ut at commit‑hookene og kvalitetsporten kjørte ulike kontroller mens et dokument lovet at de var de samme, ved å sammenligne de to listene i en test — de fem som bare noen gang ble kjørt manuelt, var de som leste CV‑teksten.",
      "summary": "Hooken og porten kjører beviselig de samme kontrollene, og prosakontrollene kjører nå ved hver commit.",
      "content_html": "<p><strong>Situasjon.</strong> Prosjektet hadde en commit‑hook som kjører kontroller før en commit godtas, og en full kvalitetsport som kjøres ved behov. Et dokument hevdet at hooken kjører porten, så ingenting kunne nå historikken uten å bestå alt. Begge listene ble vedlikeholdt for hånd, i to forskjellige filer, og ingenting sammenlignet dem.</p>\n<p><strong>Oppgave.</strong> Påstanden måtte gjøres om til en assert, noe som betydde å telle opp begge settene programmatisk og feile når de sprikte.</p>\n<p><strong>Handling.</strong> Testen leser hook‑konfigurasjonen og portens oppgavedefinisjoner, løser hver av dem opp til settet med kontroller den faktisk kaller, og sammenligner. De stemte ikke overens. Fem kontroller fantes bare i porten og kjørte aldri ved commit, og de fem var ikke tilfeldige — det var de som leser selve CV‑prosaen: stemmekontrollen som holder gjennomgangene upersonlige, moteordkontrollen, kontrollen av tallkonsistens på tvers av språk, notasjonskontrollen og stavekontrollen av innholdet. Med andre ord kjørte hver kontroll som beskyttet kode, automatisk, og hver kontroll som beskyttet teksten, kjørte bare når noen husket det. Siden teksten er hele produktet, lå risikoen stikk motsatt av der man ville ha gjettet. Rettelsen var å ta de fem inn i hooken, noe som krevde at to av dem ble raske nok til å holde seg innenfor et tidsbudsjett før commit, og deretter å beholde sammenligningstesten, så de to listene ikke kan gli fra hverandre igjen. Dokumentet som hadde beskrevet en intensjon, beskriver nå noe som håndheves.</p>\n<p><strong>Resultat.</strong> Hooken og porten kjører beviselig de samme kontrollene, og prosakontrollene kjører nå ved hver commit. Avveiningen er kjøretiden til commit‑hooken, som har vokst og vil fortsette å vokse etter hvert som innholdet vokser, og det finnes et punkt der en treg hook blir omgått — så denne rettelsen har en holdbarhetstid som måles i hvor lenge kontrollene forblir raske.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Python",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/rehearsed-the-forge-side-ci-hook-151/",
      "url": "https://engineer.company/nb/portfolio/rehearsed-the-forge-side-ci-hook-151/",
      "title": "Prøvekjørte CI‑hooken på forge‑siden og fant to feil som ikke kunne oppdages ved å lese filen: en reserveløsning som la et uoppløselig argument på hookens inndata, og gits egen miljøvariabel som fulgte kvalitetsporten inn i utsjekkingen og gjorde 19 tester røde.",
      "summary": "Hooken fungerer i begge tilfeller, og begge feilene ble funnet før noen ekte push møtte dem. Begrensningen er at en prøvekjøring fortsatt er en simulering: den…",
      "content_html": "<p><strong>Situasjon.</strong> Den selvhostede git‑tjenesten tar imot push og kan kjøre en hook på serversiden som avviser arbeid som ikke består kvalitetsporten. En hook på serversiden er den ene delen av automatiseringen som ikke kan testes ved å kjøre den lokalt: den kjører i et nakent repo uten arbeidstre, i et miljø som git‑tjenesten setter opp, og leser de pushede referansene fra standard input. Å lese skriptet og slutte at det er riktig, er gjetting.</p>\n<p><strong>Oppgave.</strong> Hooken måtte prøvekjøres mot et ekte nakent repo og en ekte push før den kunne stoles på, i stedet for å bli produksjonssatt og oppdaget der.</p>\n<p><strong>Handling.</strong> Prøvekjøringen oppretter et nakent repo, installerer hooken, kloner repoet, committer, pusher og sjekker utfallet både når pushen godtas og når den avvises. Den fant to feil, og ingen av dem var synlige i filen. Den første var en reserveløsning i argumenthåndteringen: når hooken ikke kunne bestemme en referanse, satte den inn en plassholder som ikke kunne løses opp til et objekt, og fordi verdien kommer inn på hookens standard input og ikke som en parameter, dukket svikten opp som en feilmelding uten synlig sammenheng, mye lenger ned. Den andre er den som er verdt å huske. Når Git starter en hook, eksporterer den en miljøvariabel som angir repokatalogen, og den variabelen arves av alt hooken kjører. Kvalitetsporten sjekker ut den pushede revisjonen og kjører testsuiten i den, og testsuiten arvet pekeren til det nakne repoet — så nitten tester som berører git, slo opp i feil repo og ble røde, på kode som var riktig. Miljøet må tømmes ved grensen, og den grensen er usynlig med mindre det hele faktisk kjøres.</p>\n<p><strong>Resultat.</strong> Hooken fungerer i begge tilfeller, og begge feilene ble funnet før noen ekte push møtte dem. Begrensningen er at en prøvekjøring fortsatt er en simulering: den beviser at hooken tåler én form for push, og git‑tjenesten i produksjon får push som denne prøvekjøringen ikke lager, blant annet tvungne oppdateringer og sletting av grener.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Infrastruktur",
        "Linux og servere",
        "Python",
        "Testing og kvalitetssikring",
        "DevOps og CI/CD‑automatisering",
        "Infrastruktur som kode",
        "Systemadministrasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/moved-every-user-facing-string-into-a-content-tree-152/",
      "url": "https://engineer.company/nb/portfolio/moved-every-user-facing-string-into-a-content-tree-152/",
      "title": "Flyttet alle brukerrettede strenger ut av Python og inn i et innholdstre på 874 filer på tre språk, etter å ha funnet døde oversettelser ingen kunne se var døde, og en kontroll som i det stille vurderte en tredjedel av resultatene.",
      "summary": "Teksten kan nå redigeres uten å røre koden og sammenlignes på tvers av språk ved å telle. Prisen er at det å legge til et språk nå er entydig og ikke gradvis —…",
      "content_html": "<p><strong>Situasjon.</strong> Hver brukerrettet streng i generatoren — hvert resultat, hver prosjektgjennomgang, hver seksjonsoverskrift, hvert sammendrag, på tre språk — lå i Python‑kildekoden. Dermed blir redigering av en setning en kodeendring, gjennomgang av en oversettelse blir en diff mot kildekoden, og det blir umulig å se ved første øyekast om et språk er fullstendig. Det skjuler også den feiltypen som betyr noe: en oversettelse kan være til stede, feil og utilgjengelig på samme tid.</p>\n<p><strong>Oppgave.</strong> Strengene måtte flyttes ut av koden og inn i et innholdstre som kan telles, sammenlignes på tvers av språk og sjekkes uten å kjøre renderingen.</p>\n<p><strong>Handling.</strong> Det endte med 874 filer under en innholdskatalog, ordnet først etter type og så etter språk: resultater, prosjektgjennomganger, sammendrag, titler, selskaper, regioner, kategorier, tjenester, profiler, referanser og fragmenter av søknadsbrev. Tabulatorseparerte filer der enheten er en linje med en identifikator, enkeltfiler der enheten er et avsnitt. Lasteren leser dem ved bygging, og databasen settes sammen av dem, så treet er kilden og databasen er avledet. To funn kom direkte av at det gikk an å telle. Noen oversatte strenger hadde ikke lenger noe engelsk motstykke — døde oppføringer som ingenting viste frem, og som ingen kunne ha lagt merke til mens de lå innebygd i koden, fordi en ordboknøkkel ingen refererer til, ser nøyaktig ut som en det refereres til. Og en innholdssjekk som skulle vurdere hvert resultat, leste bare den delmengden den klarte å slå opp, vurderte trettisju av nittitre og meldte suksess. Da korpuset ble en katalog, ble begge disse synlige som et avvik i antall filer.</p>\n<p><strong>Resultat.</strong> Teksten kan nå redigeres uten å røre koden og sammenlignes på tvers av språk ved å telle. Prisen er at det å legge til et språk nå er entydig og ikke gradvis — treet gjør et ufullstendig språk åpenbart, og det er poenget, men det betyr også at delvise oversettelser ikke kan leveres i det stille mens de gjøres ferdige.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Datastyring",
        "Dokumentasjon",
        "Internasjonalisering",
        "Migrering og modernisering",
        "Python",
        "Backend- og API‑utvikling",
        "Databasemigrering og modernisering",
        "Datastyring og datakvalitet",
        "Internasjonalisering og lokalisering"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-cross-language-figure-and-notation-check-153/",
      "url": "https://engineer.company/nb/portfolio/built-a-cross-language-figure-and-notation-check-153/",
      "title": "Bygde en innholdskontroll på tvers av språk som feiler når en oversettelse mister et tall den engelske teksten oppgir, og når et språk bruker en notasjon som ikke hører hjemme i det — og fant to danske beskrivelser som manglet en måleverdi, og seksten ukrainske tekstbiter med engelske anførselstegn.",
      "summary": "Atten reelle feil ble lukket, og hele feilklassen med dem, siden hver fremtidige oversettelse sammenlignes med sin engelske kilde før den kan committes.",
      "content_html": "<p><strong>Situasjon.</strong> Den mest skadelige typen oversettelsesfeil i en CV er ikke en klønete formulering. Det er et tall som forsvinner. En engelsk setning som hevder en femtidobbel forbedring, oversatt til en setning som sier «betydelig», er en påstand som i det stille trekkes tilbake i ett marked og beholdes i et annet — og ingen stavekontroll, grammatikkontroll eller menneskelig lesing av målspråket alene vil noen gang merke det, fordi den oversatte setningen er fullgod prosa.</p>\n<p><strong>Oppgave.</strong> Det trengtes en sjekk som leser språkene opp mot hverandre i stedet for hvert for seg, på de to tingene som må overleve oversettelsen: tallene, og notasjonen hvert språk bruker for å skrive dem.</p>\n<p><strong>Handling.</strong> Tallsjekken trekker ut hvert tall, hver prosentsats, hver multiplikator og hver enhet fra den engelske strengen og krever at hver av dem finnes i hver oversettelse av strengen, med multiplikatorformene tilordnet per språk i stedet for sammenlignet bokstavelig — den engelske femti‑ganger‑formen svarer til en bestemt dansk formulering og en bestemt ukrainsk formulering, og sjekken kjenner tilordningen i stedet for å kreve sifrene alene. Notasjonssjekken er speilbildet av den: hvert språk har konvensjoner det må bruke og konvensjoner det ikke må bruke, blant annet desimalskilletegn, gruppering av tusener og anførselstegn. Ukrainsk bruker vinkelformede anførselstegn; engelske doble anførselstegn i en ukrainsk setning er like feil som et manglende tall, og langt lettere å få inn ved kopiering. Den første fullstendige kjøringen fant to danske beskrivelser der et nøkkeltall fra den engelske teksten var falt bort, og seksten ukrainske tekstpartier med anførselstegn i engelsk stil.</p>\n<p><strong>Resultat.</strong> Atten reelle feil ble lukket, og hele feilklassen med dem, siden hver fremtidige oversettelse sammenlignes med sin engelske kilde før den kan committes. Det sjekken ikke kan, er å vurdere mening: den beviser at tallet overlevde og at tegnsettingen er språkets egen, og en oversettelse som beholder hvert tall, men snur påstanden på hodet, består den uten anmerkning.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Datastyring",
        "Dokumentasjon",
        "Internasjonalisering",
        "Python",
        "Testing og kvalitetssikring",
        "Datastyring og datakvalitet",
        "Internasjonalisering og lokalisering",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-native-macos-messaging-client-in-swift-6-154/",
      "url": "https://engineer.company/nb/portfolio/built-a-native-macos-messaging-client-in-swift-6-154/",
      "title": "Bygde en native meldingsklient for macOS i Swift 6 og SwiftUI — 11 141 linjer fordelt på 53 filer — over C‑grensesnittet til en Rust‑kjerne lenket inn som et statisk arkiv fra en låst revisjon.",
      "summary": "En native klient som starter som en Mac-app, bruker plattformens egne materialer og kontroller og ikke har med seg noen innebygd nettleser.",
      "content_html": "<p><strong>Situasjon.</strong> Meldingsprotokollen hadde en godt testet kjerne skrevet i Rust og klienter på flere plattformer, men skrivebordsopplevelsen på macOS var et plattformuavhengig skall — det så ikke ut som en Mac‑app, oppførte seg ikke som en, og hadde med seg et kjøretidsmiljø som en native app ikke trenger. Kjernen eksponerer funksjonaliteten sin gjennom et C‑grensesnitt, og det betyr at ethvert språk som kan kalle C, kan bygge på den, og det kan Swift.</p>\n<p><strong>Oppgave.</strong> En native klient måtte bygges direkte på dette C‑grensesnittet, i plattformens gjeldende språk og gjeldende rammeverk for brukergrensesnitt, med kjernen lenket inn i stedet for levert ved siden av.</p>\n<p><strong>Handling.</strong> Appen er 11 141 linjer Swift fordelt på femtitre filer i app‑målet, bygd på SwiftUI med kjernen lenket inn som et statisk arkiv kompilert fra en fastlåst oppstrømsrevisjon. Å låse revisjonen i stedet for å følge en gren er beslutningen som gjør byggingen reproduserbar: C‑grensesnittet er kontrakten, og en kjerne i bevegelse endrer kontrakten uten at én linje Swift endres. Lagdelingen holder den usikre flaten liten — en tynn wrapper eier hver peker og hver streng som krysser grensen, modellene over den er vanlige Swift‑verdier, og grensesnittlaget ser aldri en rå peker. Alt C‑grensesnittet returnerer, har en eierskapsregel, og å ta feil av én gir enten en lekkasje eller en krasj uten noen kompilatoradvarsel imellom, så wrapperen er der hele prosjektets minnedisiplin bor. Byggingen drives av en oppgavekjører med femtiseks mål som dekker kompileringen av kjernen, Swift‑byggingen, testsuitene, linterne og pakkingen.</p>\n<p><strong>Resultat.</strong> En native klient som starter som en Mac‑app, bruker plattformens egne materialer og kontroller og ikke har med seg noen innebygd nettleser. Det som ikke er gjort, bør sies: dette er ikke en levert utgivelse. Det finnes ingen notarisert distribusjon, ingen oppdateringskanal og ett språk i grensesnittet, så det er en fungerende app og ikke et produkt noen andre kjører.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Fullstack‑utvikling",
        "Løsningsarkitektur",
        "UX/UI‑design",
        "Fullstack‑produktutvikling",
        "Plattform- og løsningsarkitektur",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/stopped-an-application-filling-memory-at-41mb-a-second-155/",
      "url": "https://engineer.company/nb/portfolio/stopped-an-application-filling-memory-at-41mb-a-second-155/",
      "title": "Stoppet en applikasjon som fylte minnet med 41 MB i sekundet — registrert 111 GB komprimerte sider på en maskin med 36 GB — ved å begrense hver hendelsesstrøm, abonnere etter hendelsestype og sette et frekvensbudsjett på loggingen, som tok 610 996 logglinjer ned til 1 411.",
      "summary": "Minnebruken holder seg flat under vedvarende last, og den samme økten som ga 610 996 logglinjer, gir 1 411.",
      "content_html": "<p><strong>Situasjon.</strong> Appen fylte minnet til operativsystemet drepte den. Den registrerte hendelsen nådde 111 GB komprimerte sider på en maskin med 36 GB, med en stigning på rundt 41 MB i sekundet, og loggfilen for én kort økt hadde 610 996 linjer. En maskin i den tilstanden er ikke treg, den er ubrukelig — prosessen drepes først etter at swappingen allerede har fått alt annet på skrivebordet til å slutte å svare.</p>\n<p><strong>Oppgave.</strong> Veksten måtte finnes, ikke gjettes, og hver ubegrenset kodevei måtte få en grense, fordi én begrenset kø ved siden av tre ubegrensede ikke er en løsning.</p>\n<p><strong>Handling.</strong> Det var tre årsaker som multipliserte hverandre, og det er multiplikasjonen som forklarer farten. Hendelsesstrømmen fra kjernen ble konsumert uten noen grense, så hendelsene kom raskere enn grensesnittet kunne behandle dem, og etterslepet ble beholdt i stedet for forkastet. Hver abonnent mottok hver hendelse og filtrerte etterpå, så kostnaden for én hendelse ble ganget med antall lyttere, og hver lytters filtrering allokerte minne. Og loggingen hadde ikke noe budsjett, så hver hendelse ga logglinjer — og det er leddet som forsterker alt det andre, fordi loggmengden var proporsjonal med mengden av det som gikk galt. Rettingen tok tak i alle tre: begrensede buffere med en eksplisitt regel for hva som skjer når de blir fulle, abonnement per hendelsestype så en lytter bare vekkes for hendelser den vil ha, og et frekvensbudsjett for loggingen som slår sammen gjentakelser i stedet for å skrive hver enkelt. En regresjonstest kjører en høy hendelsesrate og verifiserer at minnetaket holder, så grensen er en egenskap ved bygget og ikke en kommentar.</p>\n<p><strong>Resultat.</strong> Minnebruken holder seg flat under vedvarende last, og den samme økten som ga 610 996 logglinjer, gir 1 411. Den ærlige merknaden er at frekvensbudsjettet for loggingen kaster informasjon: når noe går raskt galt nå, er sporet av det bevisst ufullstendig, og det er en avveining gjort med åpne øyne mot alternativet, en maskin som stopper opp.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Frontend‑utvikling",
        "Løsningsarkitektur",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Testing og kvalitetssikring",
        "Ytelsesoptimalisering",
        "Pålitelighet og overvåking",
        "Plattform- og løsningsarkitektur"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/",
      "url": "https://engineer.company/nb/portfolio/adopted-swift-6-strict-concurrency-over-a-c-event-loop-156/",
      "title": "Tok i bruk fullstendig streng samtidighet i Swift 6 uten actors, og koblet en blokkerende C‑hendelsesløkke til main actor gjennom én produsent, én konsument og én rekkefølge — etter å ha slått fast at én task per hendelse mister rekkefølgen grensesnittet er avhengig av.",
      "summary": "Appen kompilerer med fullstendig streng samtidighetskontroll uten undertrykkinger, og hendelsesrekkefølgen er en strukturell egenskap og ikke et håp.",
      "content_html": "<p><strong>Situasjon.</strong> Den fullstendige, strenge samtidighetskontrollen i Swift 6 gjør datakappløp til kompileringsfeil i stedet for sporadiske krasj. Det er når den tas i bruk mot et C‑bibliotek at det blir vanskelig: kjernens hendelsesløkke er et blokkerende kall som må kjøre utenfor hovedtråden for alltid, og verdiene den leverer tilbake, er pekere uten noen samtidighetsgarantier i det hele tatt. Kompilatoren kan ikke resonnere om noe av dette og avviser alt til grensen er beskrevet eksplisitt.</p>\n<p><strong>Oppgave.</strong> Fullstendig kontroll måtte slås på uten nødutganger, og det betydde å designe overgangen fra en blokkerende C‑løkke til main actor i stedet for å annotere seg rundt den.</p>\n<p><strong>Handling.</strong> Den opplagte tilnærmingen er én actor per delsystem, og den ble forkastet på grunnlag av målinger, ikke smak. Å starte én oppgave per innkommende hendelse lar kjøretiden planlegge dem i vilkårlig rekkefølge, og kjernens hendelsesstrøm er ordnet — en hendelse om endret melding som kommer frem før hendelsen om opprettet melding som den viser til, gir et grensesnitt som viser en redigering av noe som ikke finnes ennå. Reentrancy i actorer gjør dette verre, ikke bedre, fordi en actor kan suspenderes midt i en metode og behandle et annet kall. Det som erstattet den, er bevisst enkelt: én produsenttråd som eier den blokkerende løkken, én konsument, én kø mellom dem og ett enkelt hopp over til main actor til slutt. Rekkefølgen bevares fordi det finnes nøyaktig én vei, og ingenting går forbi noe annet. De usikre typene som krysser den grensen, er pakket inn i typer der trådsikkerheten erklæres i wrapperen i stedet for å antas, og erklæringen er dokumentert med hvorfor den holder — pekeren eies av én tråd og kopieres før den overleveres.</p>\n<p><strong>Resultat.</strong> Appen kompilerer med fullstendig streng samtidighetskontroll uten undertrykkinger, og hendelsesrekkefølgen er en strukturell egenskap og ikke et håp. Prisen er at designet er mindre parallelt enn det kunne vært: alt går gjennom én konsument, og hvis den konsumenten noen gang blir en flaskehals, vil løsningen kreve at man på nytt utleder hvilke hendelser som trygt kan stokkes om, og det er nøyaktig den analysen denne utformingen unngikk.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Backend‑utvikling",
        "Frontend‑utvikling",
        "Løsningsarkitektur",
        "Pålitelighet og sikkerhetskopier",
        "Testing og kvalitetssikring",
        "Ytelsesoptimalisering",
        "Backend- og API‑utvikling",
        "Plattform- og løsningsarkitektur",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/wrote-a-parser-that-verifies-every-ffi-call-site-157/",
      "url": "https://engineer.company/nb/portfolio/wrote-a-parser-that-verifies-every-ffi-call-site-157/",
      "title": "Skrev en parser som leser den faktiske C‑headeren på 7 308 linjer og verifiserer hvert kallsted, hver enum‑konstant og at hver klasse som eier en peker, er final, etter at en håndskrevet plassholder‑header lot kall til tre fjernede funksjoner kompilere, lenke og krasje.",
      "summary": "Den feiltypen som ga den opprinnelige krasjen, kan ikke komme tilbake, fordi en utdatert referanse nå er en byggefeil og ikke en kjøretidsfeil.",
      "content_html": "<p><strong>Situasjon.</strong> Tidlig i prosjektet ble C‑grensesnittet representert av en håndskrevet headerfil som beskrev funksjonene appen forventet. Headerfilen kompilerte, appen ble lenket, og kall til tre funksjoner som ikke lenger fantes i kjernen, kom helt frem til å bli utført og krasjet. Både kompilatoren og lenkeren hadde latt seg tilfredsstille av en beskrivelse av biblioteket i stedet for biblioteket selv, og gapet viste seg først under kjøring.</p>\n<p><strong>Oppgave.</strong> Den ekte headerfilen — 7 308 linjer og 268 deklarasjoner — måtte bli autoriteten, og hver bruk av den i Swift‑koden måtte verifiseres mot den automatisk i stedet for ved gjennomgang.</p>\n<p><strong>Handling.</strong> Sjekken er en parser som leser den faktiske headerfilen fra oppstrøm og bygger mengden av funksjoner, enum‑konstanter og typer den deklarerer, og deretter leser Swift‑kildekoden og slår opp hvert kallsted og hver konstantreferanse mot den mengden. Et kall til en funksjon som headerfilen ikke deklarerer, får byggingen til å feile. En referanse til en enum‑konstant som har fått nytt navn, får byggingen til å feile. Per i dag refereres det til 132 av de 268 deklarasjonene, og det er nyttig i seg selv å vite hvilke 136 som er ubrukt, fordi det sier nøyaktig hvor mye av kjernen klienten ikke har nådd. Parseren håndhever også en regel kompilatoren ikke kan håndheve: hver Swift‑klasse som eier en peker inn i kjernen, må være final. En klasse som eier en peker og ikke er final, kan få subklasser, og en subklasse som overstyrer deinitialiseringen eller legger til sin egen levetid, endrer når pekeren frigjøres — en use‑after‑free uten noe unsafe‑nøkkelord i nærheten. Regelen sjekkes etter navn over hele treet.</p>\n<p><strong>Resultat.</strong> Den feiltypen som ga den opprinnelige krasjen, kan ikke komme tilbake, fordi en utdatert referanse nå er en byggefeil og ikke en kjøretidsfeil. Begrensningen er at parseren forstår headerfilens deklarasjoner og ikke semantikken: den beviser at det finnes en funksjon med et navn som stemmer, og en funksjon der betydningen eller eierskapsregelen er endret oppstrøms mens signaturen er beholdt, slipper gjennom uten kommentar.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "Dokumentasjon",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/took-the-test-suite-under-nine-seconds-158/",
      "url": "https://engineer.company/nb/portfolio/took-the-test-suite-under-nine-seconds-158/",
      "title": "Tok testsuiten fra seks tester over en grense på seksti sekunder til 135 beståtte på 8,9 sekunder ved å profilere hovedtråden og fjerne de to kallene den satt fast i under 3 989 av 4 017 prøver.",
      "summary": "Suiten gikk fra seks tester over seksti sekunder — 74 sekunder veggklokketid — til 135 tester som besto på 8,9, og dermed ligger den innenfor vinduet der den…",
      "content_html": "<p><strong>Situasjon.</strong> Testsuiten hadde en tidsgrense på seksti sekunder, og seks tester lå over den. En testsuite som tar over et minutt, slutter å bli kjørt før hver endring, og en suite som ikke kjøres før hver endring, er en rapport om fortiden. Instinktet i en slik situasjon er å heve grensen, og det er ved å heve grensen at en suite ender på ti minutter.</p>\n<p><strong>Oppgave.</strong> Tiden måtte finnes i stedet for å budsjetteres med, og det betydde å profilere suiten i stedet for å resonnere seg frem til hvilke tester som så dyre ut.</p>\n<p><strong>Handling.</strong> Profilen ble tatt på hovedtråden mens suiten kjørte, og resultatet motsa gjetningen. Hovedtråden befant seg inne i to kall i 3 989 av 4 017 målepunkter — så de trege testene var ikke de som gjorde mest arbeid, og nesten hver test gikk gjennom de samme to stedene. Det første var en fast ventetid som skulle la asynkront arbeid falle til ro før testen sjekket resultatet — i praksis en sleep, betalt av hver test som berørte hendelsesveien, enten arbeidet allerede var ferdig eller ikke. Den ble erstattet av venting på selve betingelsen med en tidsgrense, så en test som er klar på fem millisekunder, tar fem millisekunder, og bare en test som virkelig har hengt seg opp, betaler hele ventetiden. Det andre var oppsett per test som bygde en kostbar fixture på nytt hver gang, mens fixturen var skrivebeskyttet og kunne bygges én gang for hele suiten. Ingen av dem lå i en test noen ville ha pekt ut som treg; begge lå i den felles kodeveien, og det er derfor hele suiten var jevnt treg i stedet for at noen få tester skilte seg ut.</p>\n<p><strong>Resultat.</strong> Suiten gikk fra seks tester over seksti sekunder — 74 sekunder veggklokketid — til 135 tester som besto på 8,9, og dermed ligger den innenfor vinduet der den kjøres ved hver lagring. Forbeholdet er at den delte, skrivebeskyttede fixturen nå er et koblingspunkt — en test som endrer den, vil gi en feil i en annen test, og suitens fart avhenger av en disiplin som kompilatoren ikke håndhever.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Testing og kvalitetssikring",
        "Ytelsesoptimalisering",
        "DevOps og CI/CD‑automatisering",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/named-all-seventeen-interface-icons-for-screen-readers-159/",
      "url": "https://engineer.company/nb/portfolio/named-all-seventeen-interface-icons-for-screen-readers-159/",
      "title": "Ga hver kontroll med bare ikon i grensesnittet et navn for skjermlesere etter å ha funnet at send‑knappen ble lest opp som «arrow up circle, button», og skrev linteren som krever etiketten innen åtte linjer fra ikonet.",
      "summary": "Alle de sytten kontrollene oppgir funksjonen sin, og en attende kan ikke legges til uten en etikett.",
      "content_html": "<p><strong>Situasjon.</strong> Grensesnittet brukte systemsymboler for kontrollene sine, og et symbol uten tilgjengelighetsetikett leses opp av skjermleseren med sitt eget interne navn. Sendeknappen ble lest opp som «arrow up circle, button». Det samme gjorde alle de andre rene ikonkontrollene i appen, hver på sin måte — sytten av dem beskrev sin egen grafikk i stedet for funksjonen sin.</p>\n<p><strong>Oppgave.</strong> Hver ren ikonkontroll trengte en etikett som beskriver hva den gjør, og rettingen måtte komme sammen med en sjekk, fordi det neste ikonet som ble lagt til, ellers ville ført feilen inn igjen umiddelbart.</p>\n<p><strong>Handling.</strong> Hver av de sytten fikk en etikett som navngir handlingen i stedet for formen, og der kontrollens betydning avhenger av tilstand, følger etiketten tilstanden i stedet for å være fast. Sjekken er den delen som er verdt å beskrive, fordi det ikke finnes noen generell «er dette tilgjengelig»-regel for dette. Linteren ser etter konstruksjonen som lager en ren ikonkontroll, og krever deretter en tilgjengelighetsetikett innenfor åtte linjer fra den. Åtte linjer er en bevisst grov heuristikk, og den ble valgt ved måling: den er lang nok til å dekke hver legitim måte kodebasen skriver en slik kontroll med modifikatorene sine på, og kort nok til at en etikett festet til en annen visning lenger ned ikke tilfeldigvis oppfyller den. En presis regel her måtte ha forstått modifikatorkjedene i SwiftUI som et tre, og den billige nærhetsregelen fanger den faktiske feilen — som ikke er en kontroll med feil etikett, men en kontroll uten etikett.</p>\n<p><strong>Resultat.</strong> Alle de sytten kontrollene oppgir funksjonen sin, og en attende kan ikke legges til uten en etikett. Unøyaktigheten i regelen er reell og står beskrevet der regelen er definert: den kan oppfylles av en etikett på en visning ved siden av, så den beviser at det finnes en etikett i nærheten, ikke at etiketten er riktig, og riktigheten krever fortsatt at noen lytter på den.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/built-a-48-token-design-system-160/",
      "url": "https://engineer.company/nb/portfolio/built-a-48-token-design-system-160/",
      "title": "Bygde et designsystem med 48 tokens på plattformens eget glassmateriale og skrev linteren som avviser et magisk tall, en hardkodet skriftstørrelse eller en animasjon som ignorerer innstillingen for redusert bevegelse.",
      "summary": "Førtiåtte tokener beskriver hele grensesnittet, utseendet følger systemet, og ingen av de tre formene for forvitring kan committes.",
      "content_html": "<p><strong>Situasjon.</strong> Et grensesnitt som bygges ved å legge til visninger, samler opp verdier: en hjørneradius her, en skrift på fjorten punkter der, en animasjon på to tideler av et sekund et annet sted. Hver for seg er de rimelige, og samlet er de et design som ikke kan endres, fordi det ikke finnes noe slikt som «hjørneradiusen» å endre — det finnes førti av dem, litt forskjellige, spredt over femti filer.</p>\n<p><strong>Oppgave.</strong> Det visuelle vokabularet måtte reduseres til et navngitt sett, uttrykt med plattformens eget materiale i stedet for å finnes opp på nytt, og forsvares av en sjekk så det forblir redusert.</p>\n<p><strong>Handling.</strong> Systemet består av førtiåtte tokener i seks grupper: avstand, typografi, farge, radius, elevasjon og bevegelse. Farge og materiale bygger på plattformens semantiske farger og glassmaterialet dens i stedet for faste verdier, og det er det som får grensesnittet til å følge systemets utseende, aksentfargen og kontrastinnstillingene uten noen kode som holder øye med dem. Typografien er knyttet til plattformens tekststiler, så den skaleres med brukerens innstilling for tekststørrelse i stedet for å låse en punktverdi. Linteren avviser tre ting: en numerisk literal der et avstands- eller radiustoken hører hjemme, en hardkodet skriftstørrelse hvor som helst, og en animasjon som er deklarert uten å respektere innstillingen for redusert bevegelse. Den tredje er den som ellers ville forvitret raskest, fordi en animasjon legges til i et øyeblikk av finpuss, og sjekken av innstillingen er den delen som hoppes over — så regelen gjør det umulig å skrive selve animasjonen uten den, i stedet for å be noen om å huske det.</p>\n<p><strong>Resultat.</strong> Førtiåtte tokener beskriver hele grensesnittet, utseendet følger systemet, og ingen av de tre formene for forvitring kan committes. Begrensningen er at et tokensystem styrer konsistens og ikke kvalitet — alt stemmer nå overens, og å stemme overens er ikke det samme som å være godt designet, og det er en vurdering ingen linter i dette prosjektet gjør.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Designsystemer og brukergrensesnitt",
        "Frontend‑utvikling",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Merkevare, markedsføring og SEO",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/reached-the-unused-half-of-the-messaging-core-161/",
      "url": "https://engineer.company/nb/portfolio/reached-the-unused-half-of-the-messaging-core-161/",
      "title": "Tok i bruk den halvdelen av meldingskjernen applikasjonen aldri hadde brukt — overføring av sikkerhetskopier, forsvinnende meldinger, redigering og ny sending av meldinger, verifiserte invitasjoner, proxyer og krypteringspolicy — og kjørte hver test mot det ekte biblioteket uten mocks.",
      "summary": "Den halvparten av kjernen som tidligere ikke var nådd, kjøres nå av tester mot det ekte biblioteket, og wrapperen har den høyeste minstegrensen i prosjektet.",
      "content_html": "<p><strong>Situasjon.</strong> Klienten brukte omtrent halvparten av det meldingskjernen tilbyr. Den ubrukte halvparten var ikke obskur — overføring av sikkerhetskopier mellom enheter, forsvinnende meldinger, redigering og ny sending av meldinger, verifiserte invitasjonslenker, proxy‑konfigurasjon og krypteringspolicyen for en samtale. Hver av dem er en funksjon en bruker ville forvente, og hver av dem var et utestet område av C‑grensesnittet, og det er det farligste av de to, fordi et utestet område av et C‑grensesnitt er der eierskapsfeilene bor.</p>\n<p><strong>Oppgave.</strong> Funksjonaliteten som ikke var nådd, måtte kjøres og dekkes av tester, og testene måtte kjøre mot det ekte biblioteket i stedet for en erstatning.</p>\n<p><strong>Handling.</strong> Regelen som ble innført, var: ingen mocker for kjernen. En mock av et C‑grensesnitt koder inn utviklerens oppfatning av hva biblioteket gjør, og hver feil som er verdt å finne her, er et sted der den oppfatningen er feil — så en mockbasert test som består, er bevis om mocken. I stedet oppretter testene ekte kontoer i midlertidige kataloger, kjører det ekte biblioteket og sjekker hva det faktisk returnerer, og hver suite rydder opp sin egen tilstand. Det er det som gjorde testdekningen meningsfull: å teste overføring av sikkerhetskopier betydde å håndtere tilstandsmaskinen til en ekte overføring og feilveiene dens, og å teste verifiserte invitasjoner betydde å konstruere det ekte lenkeformatet og la biblioteket tolke det. Minstegrenser for testdekning ble satt per lag i stedet for som ett tall, på åttifire prosent for kjernewrapperen, syttifem for hjelpekode og sekstifem for modeller, ut fra resonnementet at laget som berører rå pekere, bør holdes høyest, og at en modell som stort sett består av lagrede egenskaper, ikke bør fylles opp med tester for å nå et gjennomsnitt.</p>\n<p><strong>Resultat.</strong> Den halvparten av kjernen som tidligere ikke var nådd, kjøres nå av tester mot det ekte biblioteket, og wrapperen har den høyeste minstegrensen i prosjektet. Prisen er fart og determinisme: tester mot det ekte biblioteket er tregere enn mocker og kan feile av årsaker i miljøet, og det er prisen for at de kan feile av ekte årsaker.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "API‑er og integrasjon",
        "Automatisering og CI/CD",
        "Backend‑utvikling",
        "Pålitelighet og sikkerhetskopier",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "Backend- og API‑utvikling",
        "DevOps og CI/CD‑automatisering",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/rewrote-the-error-messages-against-a-tone-standard-162/",
      "url": "https://engineer.company/nb/portfolio/rewrote-the-error-messages-against-a-tone-standard-162/",
      "title": "Skrev om applikasjonens feilmeldinger i tråd med en skriftlig tonestandard etter at en avvist innlogging ga brukeren skylden for en skrivefeil når leverandøren egentlig krevde et appspesifikt passord, og dekket det med en test som navngir leverandøren.",
      "summary": "Feilmeldingene følger en uttalt standard, og tilfellet som utløste den, er dekket av en test som feiler på den gamle teksten.",
      "content_html": "<p><strong>Situasjon.</strong> En innlogging hos en stor e‑postleverandør feilet, og appen fortalte brukeren at passordet var feil. Det var ikke feil. Den leverandøren krever et appspesifikt passord for tredjepartsklienter og avviser kontopassordet uansett hvor nøye det skrives inn. Meldingen sendte brukeren tilbake for å skrive inn på nytt noe som aldri kunne fungere, og den egentlige instruksen — gå og lag en annen type passord — sto ingen steder.</p>\n<p><strong>Oppgave.</strong> Feilmeldingene måtte skrives om etter en skriftlig standard i stedet for å lappes én om gangen, siden denne var det synlige eksempelet på en vane som gikk igjen i alle sammen.</p>\n<p><strong>Handling.</strong> Standarden har tre krav: si hva som skjedde, antyd aldri at brukeren har gjort noe galt når årsaken ligger et annet sted, og oppgi neste handling når det finnes en. Da standarden ble brukt på alle feilmeldingene, brøt de fleste minst ett av kravene — flere var feilstrengen fra det underliggende biblioteket sendt rett gjennom, og den beskriver en tilstand for en programmerer i stedet for en situasjon for et menneske. Tilfellet med leverandøren ble skrevet om til å nevne leverandøren, si at den krever et appspesifikt passord for andre klienter, og si hvor man lager et. Testen er det som hindrer at feilen kommer tilbake, og den er bevisst spesifikk: den fremkaller en mislykket innlogging mot den leverandøren og sjekker at meldingen inneholder leverandørens navn og frasen som beskriver hvilken type påloggingsinformasjon som kreves. En test som bare sjekket at en eller annen feil dukket opp, ville bestått med den opprinnelige, feilaktige meldingen, så sjekken gjelder innholdet, som er den eneste delen som noen gang var ødelagt.</p>\n<p><strong>Resultat.</strong> Feilmeldingene følger en uttalt standard, og tilfellet som utløste den, er dekket av en test som feiler på den gamle teksten. Det som står uløst, er skala: standarden håndheves ved gjennomgang og med én test på én melding, og de andre leverandørene med sine egne særlige krav har ingen tilsvarende test, så feiltypen er dokumentert og ikke lukket.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Dokumentasjon",
        "Frontend‑utvikling",
        "Produkt og krav",
        "Sikkerhet",
        "Testing og kvalitetssikring",
        "UX/UI‑design",
        "Produktstrategi og krav",
        "Teknisk dokumentasjon",
        "UI/UX‑design og designsystemer"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/audited-644-rust-crates-for-licence-compatibility-163/",
      "url": "https://engineer.company/nb/portfolio/audited-644-rust-crates-for-licence-compatibility-163/",
      "title": "Reviderte 644 Rust‑crates for lisenskompatibilitet ved hvert bygg, og beviste at kontrollen slår ut ved å skrive om lisensen til én crate og ved å flytte den låste kjernerevisjonen uten å regenerere.",
      "summary": "Lisensgrunnlaget for distribusjonen sjekkes ved hver bygging, og det er kjent at sjekken virker fordi den ble provosert til å feile, ikke fordi den aldri har…",
      "content_html": "<p><strong>Situasjon.</strong> Å lenke en Rust‑kjerne inn i en levert app betyr å levere alt den kjernen er avhengig av. Avhengighetsgrafen består av 644 crates. Hver av dem har en lisens, noen har mer enn én, og én enkelt copyleft‑crate som kommer inn tre nivåer ned gjennom en rutinemessig versjonsoppgradering, endrer hvilke vilkår appen som helhet kan distribueres under — i det stille, i en låsefil ingen leser linje for linje.</p>\n<p><strong>Oppgave.</strong> Lisenssituasjonen måtte verifiseres ved hver bygging i stedet for å gjennomgås én gang, med en eksplisitt tillatelsesliste, slik at en endring i grafen gir en byggefeil og ikke er en oppdagelse noen andre gjør senere.</p>\n<p><strong>Handling.</strong> Lisenskontrollen løser opp hele den transitive grafen og sjekker lisensuttrykket til hver crate mot en liste over vilkår prosjektet godtar, og evaluerer de boolske uttrykkene riktig — en crate som tilbyr et valg mellom to lisenser, er akseptabel hvis én av dem står på listen, og en crate som krever begge, er bare akseptabel hvis begge gjør det. Alt som ikke har treff, får byggingen til å feile i stedet for å gi en advarsel, og å legge til et vilkår på tillatelseslisten er en bevisst endring med en begrunnelse. En sjekk som aldri har feilet, kan ikke skilles fra en som ikke kan feile, så den ble provosert til å feile med vilje, to ganger. Én gang ved å skrive om lisensuttrykket til en crate til noe listen ikke godtar, som er formen på en crate som endrer vilkårene sine oppstrøms mellom versjoner — tilfellet ingen menneskelig gjennomgang fanger, fordi selve craten ikke er ny. Én gang ved å flytte den låste kjernerevisjonen uten å generere lisenskontrollen på nytt, som er formen på en kjerneoppgradering som i det stille tar med seg en ny avhengighet. Begge provokasjonene fikk byggingen til å feile slik det var meningen, og sjekken ble stående uendret i stedet for å bli utvidet.</p>\n<p><strong>Resultat.</strong> Lisensgrunnlaget for distribusjonen sjekkes ved hver bygging, og det er kjent at sjekken virker fordi den ble provosert til å feile, ikke fordi den aldri har sagt fra. Før den fantes, fremsatte app‑pakken, lisensfilen og prosjektets egen README alle en påstand om 644 crates som ingenting hadde verifisert. Begrensningen er presis og bør ikke overdrives: sjekken leser de deklarerte lisensmetadataene, og metadata kan være feil eller ufullstendige. Den beviser ingenting om en crate som deklarerer lisensen sin feil, og den er ikke juridisk rådgivning.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Datastyring",
        "DevOps",
        "Dokumentasjon",
        "Sikkerhet",
        "Datastyring og datakvalitet",
        "DevOps og CI/CD‑automatisering",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/held-five-repositories-to-one-history-standard-165/",
      "url": "https://engineer.company/nb/portfolio/held-five-repositories-to-one-history-standard-165/",
      "title": "Holdt fire repoer til én historikkstandard — konvensjonell, uten emojier og uten attribusjonslinjer, håndhevet av en hook for commit‑meldinger — sammen med 48 instruksjonsdokumenter som styrer hvordan arbeidet gjøres.",
      "summary": "Fire repoer deler ett historikkformat og én instruksjonsstruktur, og begge håndheves av hooks i stedet for av disiplin.",
      "content_html": "<p><strong>Situasjon.</strong> Fire repoer bygd over atten måneder av én person er situasjonen der prosess er lettest å hoppe over, fordi det ikke finnes noen å koordinere med, og kostnaden ved en uleselig historikk betales helt av ens fremtidige selv, som ikke har klaget ennå. Det er også situasjonen der en inkonsekvent historikk er mest sannsynlig, siden hvert repo kan gli inn i sine egne vaner uten at noe trekker dem sammen.</p>\n<p><strong>Oppgave.</strong> Én standard for historikk og én standard for instruksjoner måtte gjelde i hvert repo der arbeidet skrives, og det måtte håndheves maskinelt i stedet for å huskes.</p>\n<p><strong>Handling.</strong> Commitstandarden er et konvensjonelt prefiks som angir typen endring og et omfang, en emnelinje under en fast lengde, ingen emojier og ingen attribusjonslinjer av noe slag — det siste fordi en linje som krediterer et verktøy, ikke er et faktum om endringen, og historikken er til for fakta om endringer. En hook for commitmeldinger avviser alt som ikke følger standarden, i hvert repo som det arbeides i, så standarden er en egenskap ved repoet og ikke ved den som committer. Fordelingen i det største repoet viser hva arbeidet faktisk var: 156 commits med funksjoner, 133 rettinger, 128 dokumentasjon, 81 vedlikehold, 26 refaktoreringer, 20 stil, 5 ytelse og 3 test. Dokumentasjon ligger nær nok rettingene til å være verdt å legge merke til, og det er en følge av den andre halvdelen av dette — 48 instruksjonsdokumenter fordelt på de fire, hvert av dem dekker ett domene, hvert av dem er skrevet som regler i stedet for beskrivelse, og alle kan nås fra ett inngangsdokument per repo, så det finnes ett sted å begynne. En linter for dokumentasjon håndhever størrelsesbudsjetter per fil og at hver fil står i indeksen, så samlingen forblir navigerbar i stedet for å vokse til et arkiv.</p>\n<p><strong>Resultat.</strong> Fire repoer deler ett historikkformat og én instruksjonsstruktur, og begge håndheves av hooks i stedet for av disiplin. Det dette ikke gjør, er å gjøre historikken god: formatet sjekkes, men innholdet gjør det ikke, så en emnelinje som følger standarden, men som ikke beskriver noe, slipper gjennom like lett som en som forklarer endringen.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "DevOps",
        "Dokumentasjon",
        "Prosjektledelse",
        "Smidig og Scrum",
        "Teknisk ledelse",
        "DevOps og CI/CD‑automatisering",
        "Prosjektledelse (smidig)",
        "Teknisk dokumentasjon",
        "Teknisk ledelse og rådgivning"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/measured-thirteen-addresses-from-the-access-log-alone-166/",
      "url": "https://engineer.company/nb/portfolio/measured-thirteen-addresses-from-the-access-log-alone-166/",
      "title": "Målte tretten adresser ut fra webserverens egen tilgangslogg alene — ingen informasjonskapsel, ingen sporingsverktøy og ingen tredjepartstjeneste — maskerte klientnettverket idet hver linje skrives, og samlet resultatet i et permanent arkiv med 21 daglige tellinger og 20 månedlige fasetter.",
      "summary": "Tretten adresser måles på denne måten, og nettstedet leverer ingen informasjonskapsel, ingen sporer og ingen tredjepartsforespørsel, så det er ingenting å…",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet hadde ingen måte å vite om noen leste det. Hvert vanlig svar på det koster leseren noe — en informasjonskapsel, et skript, en piksel, en hostet tjeneste som får vite noe om en besøkende i forbifarten — og hvert av dem ble avvist før den første linjen av dette ble skrevet. Det som var igjen, var webserverens egen tilgangslogg, den ene registreringen som finnes uansett, fordi en forespørsel må besvares.</p>\n<p><strong>Oppgave.</strong> Spørsmålet var om en nyttig oversikt over lesingen kan bygges fra en logglinje der de identifiserende delene allerede er fjernet, og om fjerningen kan bevises i stedet for å loves.</p>\n<p><strong>Handling.</strong> Maskeringen skjer idet linjen skrives, ikke i en oppryddingsrunde etterpå, fordi et løfte om ikke å lagre noe brytes av å lagre det og rydde opp senere. Serveren utleder klientadressen under to navn, og begge maskeres — å maskere det ene ser riktig ut i konfigurasjonen mens hele adressen ligger på disk under det andre, noe bare en test mot den kjørende serveren avdekket. Sju headere for videresendt adresse fjernes helt, siden enkoderen skriver headerne nøyaktig slik klienten sendte dem, og en besøkende bak en bedriftsproxy ellers ville ført med seg sin egen fulle adresse inn i filen under et navn ingen maske noen gang så på. Cookie‑headeren, authorization‑headeren og den flyktige kildeporten går samme vei, og spørrestrenger kuttes før noe leser stien, så verken ordene noen skrev inn i en søkemotor eller ordene de skrev inn i nettstedets egen søkeboks, kan nå noen fil. Innsamlingen er pull‑basert: et skript som bare bruker standardbiblioteket, strømmer den aktive loggen og de roterte søsterfilene på verten og skriver ut tellinger i stedet for rader, begrenset av antall ulike stier og ikke av loggens størrelse, fordi maskinen har 464 MB. Oppbevaringen trengte to mekanismer i stedet for én, etter at et størrelsestak og et tidstak på samme hylle viste seg å bety at det minste vinner i det stille — et travelt nettsted beholdt ti dager i stedet for tretti, og et stille slettet ingenting i det hele tatt. Det som overlever loggen, er et arkiv med 21 tellinger per dag og 20 fasetter per måned, slått sammen i stedet for lagt til, ut fra regelen om at et vindu bare kan observere for lite, med totalen lagret ved siden av hver rangering, så en avkortet tabell oppgir hvor mye den utelot.</p>\n<p><strong>Resultat.</strong> Tretten adresser måles på denne måten, og nettstedet leverer ingen informasjonskapsel, ingen sporer og ingen tredjepartsforespørsel, så det er ingenting å samtykke til og ingenting for en tagg å kjøre i. Utformingen avviser mer enn den besvarer — unike besøkende, søkeord, en besøkendes vei gjennom nettstedet, tid på siden, klikk, geografi og enhet er alle umulige å besvare her og sier det — og begrensningene står ved siden av tallene i stedet for med liten skrift, fordi et grovt tall som leses som et presist, er verre enn ikke noe tall.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Datastyring",
        "Infrastruktur",
        "Overvåking og observerbarhet",
        "Python",
        "Sikkerhet",
        "Datastyring og datakvalitet",
        "Pålitelighet og overvåking",
        "Sikkerhet og tilgangsstyring"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/made-six-collectors-state-what-they-never-read-167/",
      "url": "https://engineer.company/nb/portfolio/made-six-collectors-state-what-they-never-read-167/",
      "title": "Fikk seks rapporteringsinnsamlere til å oppgi den delen av inndataene de aldri leste — filer som forble lukket, sonder som ikke var installert, journallinjer som ikke ble gjenkjent — og tilpasset terskelverdier til 116 målinger, etter å ha slått fast at en blind innsamling og en vert der alt er i orden, gir den samme siden.",
      "summary": "En side med tall fra denne plattformen viser nå sine egne blindsoner ved siden av funnene. Ingen del av selvmålingen er en port, og det er sagt rett ut og ikke…",
      "content_html": "<p><strong>Situasjon.</strong> Seks skrivebeskyttede innsamlere gjengav en aktiv vert som Markdown, og ikke én av dem registrerte sin egen kjøretid, filene den ikke kunne åpne, eller inndataene den hoppet over. Flere hadde feilveier som svelget feilen helt, og det gir en bestemt og farlig form for rapport: en side med tall som ser fullstendig og sunn ut, enten noe faktisk ble lest eller ikke. En rotert logg som kommer frem avkortet, fjerner hele tidsrommet sitt fra hvert tall på siden, og en uke med manglende trafikk ser nøyaktig ut som en rolig uke. Et speil der daemonen har endret loggformat, gir den samme nullen som et speil ingen besøkte. Hver probe i sikkerhetsrapporten ligger bak en test av om verktøyet finnes, så en vert som manglet verktøyene for brannmur, sockets og utestengning, viste ingen regler, ingen sockets og ingen jails — det visuelle kjennetegnet på en nedlåst vert, og i virkeligheten en blind vert.</p>\n<p><strong>Oppgave.</strong> To ting måtte bli umulige å forveksle med god helse. En rapport måtte si hva som regnes som dårlig i stedet for å overlate til et menneske å avgjøre det, og hver innsamler måtte oppgi hva dens egen innsamling kostet og hvor mye av inndataene den ikke kunne bruke.</p>\n<p><strong>Handling.</strong> Tersklene ble tilpasset de 116 målepunktene metrikkrapporten allerede hadde målt, aldri en gjetning, med de observerte spennene registrert ved siden av grensene. Vurderingen finner hver kolonne etter navn og ikke etter posisjon, og nekter å vurdere en rad der antall verdier ikke stemmer med antall etiketter, og det er det som beskytter den mot en endring i klokkeformatet som forskyver hver etikett med én. Rapporten skiller så mellom tre utfall i stedet for to: et brudd, ikke noe brudd, og ikke vurdert fordi ingenting målte det. På innsamlingssiden rapporterer hver innsamler nå kjøretiden og blindsonene sine — filer lest mot filer som ikke kunne leses, journallinjer lest mot journallinjer gjenkjent, repoer git nektet å lese, telt i stedet for vist som null, kilder spurt mot kilder som svarte, prober etterspurt mot prober som ikke er installert — og hver av disse radene skrives ut også når svaret er null, fordi en rad som forsvinner når den er tom, er den samme stillheten i en annen form. Hver mal fikk en eksplisitt gren for utdata skrevet av en innsamler som er eldre enn malen selv.</p>\n<p><strong>Resultat.</strong> En side med tall fra denne plattformen viser nå sine egne blindsoner ved siden av funnene. Ingen del av selvmålingen er en port, og det er sagt rett ut og ikke bare antydet: ingen grense er tilpasset en innsamlers kjøretid, så en innsamling som tar dobbelt så lang tid, er synlig og stopper ingen, og å tilpasse en slik grense ville kreve et referansegrunnlag fra den ekte verten som dette arbeidet bevisst ikke fant på. Grensene som finnes, er daglige gjennomsnitt, så en vert som når et tak i ett minutt, krysser ingenting her — dødmannsknappen på femten minutter fanger det, ingen av dem erstatter den andre, og begge rapportene sier det på siden.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Automatisering og CI/CD",
        "Dokumentasjon",
        "Overvåking og observerbarhet",
        "Pålitelighet og sikkerhetskopier",
        "Python",
        "Testing og kvalitetssikring",
        "Pålitelighet og overvåking",
        "Systemadministrasjon",
        "Teknisk dokumentasjon"
      ]
    },
    {
      "id": "https://engineer.company/nb/portfolio/published-the-site-as-a-tor-onion-mirror-on-a-readable-168/",
      "url": "https://engineer.company/nb/portfolio/published-the-site-as-a-tor-onion-mirror-on-a-readable-168/",
      "title": "Publiserte nettstedet som et Tor onion‑speil på en lesbar adresse som begynner med engineer, med nøkkelen generert utenfor verten slik at den aldri havnet i dette repoet eller på en bærbar PC, og lot det være den ene av fire inngangsdører som aldri telles.",
      "summary": "Nettstedet har fire inngangsdører til ett og samme bygg, og dette er den eneste som aldri telles.",
      "content_html": "<p><strong>Situasjon.</strong> Nettstedet svarte allerede på tre protokoller fra ett og samme bygg. En leser som trengte å nå det uten å avsløre at vedkommende hadde nådd det, tok fortsatt en tur via en utgangsnode til en vanlig adresse, og det er noe svakere enn en egen inngangsdør. Den opplagte innvendingen mot en fjerde dør er brannmuren, og den gjelder ikke: anonymitetsdaemonen når nettverket ved å opprette utgående kretser, så ingenting kobler seg inn utenfra, ingen port åpnes, og ingen av brannmurlagene har noe å synkronisere.</p>\n<p><strong>Oppgave.</strong> Speilet måtte servere de samme sidene som den vanlige adressen, publiseres overalt der de andre speilene publiseres, ha en adresse et menneske kan lese høyt i stedet for en tilfeldig streng, og aldri la den private nøkkelen som er adressen, havne i dette repoet eller på en bærbar maskin.</p>\n<p><strong>Handling.</strong> Rollen installerer daemonen fra distribusjonens egen pakke, skriver en konfigurasjon der den utgående proxyporten er slått av, og nekter å overskrive en identitet den ikke selv genererte — så en nøkkel som er plassert for hånd, blir ganske enkelt brukt. En lesbar adresse kan ikke velges, bare finnes: adressen er den offentlige nøkkelen i base32, så et prefiks kjøpes ved å generere nøkkelpar til ett av dem tilfeldigvis koder bokstavene man vil ha, og hvert tegn koster trettito ganger så mye som det forrige. Prefikset som ble kjøpt, leses som selskapets eget navn. Adressen er en deklarert verdi i inventaret, og hver converge‑kjøring sjekker at vertens faktiske identitetsfil stemmer med den, så en byttet nøkkel med en utdatert deklarasjon får kjøringen til å feile i stedet for å forbli stille. Nøkkelen fanges av begge hemmelighetsvaktene, etter at det ble målt at det opplagte mønsteret for en nøkkelfil treffer en deploy‑nøkkel og bommer på denne. Å pensjonere den tilfeldige adressen speilet var levert med, var en trinnvis overgang og ikke et bytte, så den publiserte adressen fortsatte å virke mens den lesbare ble publisert. Steget som så ut som det siste, var det ikke: nøkkelen kom riktig på plass og converge‑kjøringen meldte ingen endring, fordi ingenting ennå navnga den nye katalogen, så den nye adressen ble aldri publisert i det hele tatt — rollen feiler nå på en identitet på disk som ingen tjeneste navngir.</p>\n<p><strong>Resultat.</strong> Nettstedet har fire inngangsdører til ett og samme bygg, og dette er den eneste som aldri telles. Gemini‑forespørsler og Gopher‑tilkoblinger telles per dag uten adresse og uten sti; onion‑tjenesten har ingen teller og ingen innstilling for å legge til en, fordi det å telle en leser der er det ene den protokollen finnes for å forhindre, og blindheten er prisen for den holdningen og ikke et hull i rapporteringen. Speilet betalte seg også som et ekstra par øyne: de første leserne brukte en annen nettlesermotor, som ikke implementerer den rullestyrte animasjonen bunnteksten var avhengig av, så hver eneste av dem fikk kolofonen malt over hele forsiden. Det var en reell feil på det vanlige nettstedet, funnet av publikummet som ikke hadde noen annen måte å melde den på.</p>\n",
      "date_published": "2026-10-18T04:44:42+02:00",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb",
      "tags": [
        "Infrastruktur",
        "Linux og servere",
        "Sikkerhet",
        "Systemadministrasjon",
        "Webutvikling",
        "Infrastruktur som kode",
        "Sikkerhet og tilgangsstyring",
        "Utvikling av nettsteder og CMS"
      ]
    },
    {
      "id": "https://engineer.company/nb/notes/optimise-before-you-shard/",
      "url": "https://engineer.company/nb/notes/optimise-before-you-shard/",
      "title": "Optimaliser databasen før du sharder den",
      "summary": "De fleste databaser som ser ut til å trenge sharding, må leses først. Rekkefølgen vår: mål, stram inn, flytt lesingene, partisjoner og shard til slutt.",
      "content_html": "<p>Når en database blir treg under last, er det første forslaget som regel flere\nmaskiner. Noen ganger er det riktig. Oftere er den trege delen en håndfull\nsetninger ingen har lest ennå, og å spre dataene over flere servere sprer de\nsetningene over mer maskinvare uten å gjøre én eneste av dem billigere. Et\nproblem som ser uløselig ut, er ofte én spørringsplan unna et elegant svar, og\nhorisontal skalering velges iblant nettopp for at ingen skal måtte lese\ndatabasen du allerede har (les den før du kjøper mer av den).</p>\n<p>Dette er rekkefølgen vi arbeider i før vi deler opp noe: se, stram inn, ta\nlesingene bort fra primærnoden, partisjoner tabellene rundt det hete punktet, og\nshard først når én maskin er full av godt arbeid. Lynsalget, der hver kjøper\nskriver til én rad, er gjennomgått i\n<a href=\"/nb/notes/selling-limited-stock-without-overselling/\">Å selge den siste billetten nøyaktig én gang</a>.\nDette notatet er metoden rundt det, og den siste delen tar for seg det motsatte\nforslaget: selg fra hver server, og be om unnskyldning for enhetene som ble\nsolgt to ganger.</p>\n<h2 id=\"se-først\">Se først</h2>\n\n<p>PostgreSQL registrerer allerede det meste det er å vite. Før du endrer noe, still\nden fire spørsmål.</p>\n<p><strong>Hvilke setninger koster mest totalt?</strong> <code>pg_stat_statements</code>, en utvidelse som\nlastes via <code>shared_preload_libraries</code>, tar vare på hver normaliserte setning med\nantall kall og tidsbruk. Sorter etter total, ikke etter gjennomsnitt: en spørring\npå 2 ms som kjøres en million ganger i timen, koster mer enn en rapport på to\nsekunder som kjøres to ganger.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">calls</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"n\">total_exec_time</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">total_ms</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"n\">round</span><span class=\"p\">(</span><span class=\"n\">mean_exec_time</span><span class=\"p\">::</span><span class=\"nb\">numeric</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">         </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">mean_ms</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"k\">left</span><span class=\"p\">(</span><span class=\"n\">query</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">60</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">query</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">pg_stat_statements</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">ORDER</span><span class=\"w\"> </span><span class=\"k\">BY</span><span class=\"w\"> </span><span class=\"n\">total_exec_time</span><span class=\"w\"> </span><span class=\"k\">DESC</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">LIMIT</span><span class=\"w\"> </span><span class=\"mi\">10</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p><strong>Hva gjør den øverste egentlig?</strong> <code>EXPLAIN (ANALYZE, BUFFERS)</code> kjører setningen\nog viser planen den valgte, radene hvert steg faktisk returnerte mot radene det\nforventet, og hvor mange sider den leste. En skanning av en million rader for å\nreturnere ti, eller et estimat som bommer med en faktor hundre, er som regel hele\nhistorien. <code>ANALYZE</code> utfører det den forklarer, så pakk en skriving inn i <code>BEGIN</code>\nog <code>ROLLBACK</code>.</p>\n<p><strong>Hvem venter, og på hvem?</strong></p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">pid</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">wait_event_type</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">wait_event</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"n\">pg_blocking_pids</span><span class=\"p\">(</span><span class=\"n\">pid</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">blocked_by</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"k\">left</span><span class=\"p\">(</span><span class=\"n\">query</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"mi\">60</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"n\">query</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">pg_stat_activity</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">wait_event_type</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"s1\">&#39;Lock&#39;</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Rader her i et travelt minutt betyr at transaksjoner står i kø for hverandre,\nikke for maskinvare, og en større maskin gjør ikke en kø for én rad kortere.</p>\n<p><strong>Arbeider serveren, eller sjonglerer den?</strong> Tell de aktive øktene i\n<code>pg_stat_activity</code> mot kjernene. Når mange ganger flere er aktive enn det finnes\nkjerner, bruker serveren tiden sin på å bytte mellom dem, og en tilkoblingspool\nløser det før noen maskinvare gjør det. Les deretter <code>n_dead_tup</code> i\n<code>pg_stat_user_tables</code>: en tabell med flere døde rader enn levende blir skannet\ngjennom sitt eget søppel, og det er autovacuum som henger etter, ikke en database\nsom er for liten.</p>\n<p>Skriv ned det du finner. Det er utgangspunktet hvert senere steg måles mot, og\ndet ærlige svaret på «må vi sharde?» ligger ofte allerede der.</p>\n<h2 id=\"stram-inn-det-som-er-der\">Stram inn det som er der</h2>\n\n<p>Omtrent i rekkefølge etter gevinst for innsatsen.</p>\n<p><strong>Indekser som passer til spørsmålene.</strong> Planene ovenfor navngir skanningene. En\nindeks på kolonnene en het spørring filtrerer på, gjør en skanning om til et\noppslag, og en partiell indeks inneholder bare radene en spørring kan være ute\netter — enheter som fortsatt er til salgs, for eksempel — så den krymper etter\nhvert som et salg skrider frem, i stedet for å vokse:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">INDEX</span><span class=\"w\"> </span><span class=\"n\">units_unsold</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    </span><span class=\"k\">ON</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">item_id</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Hver indeks betales også ved hver skriving, så fjern dem der <code>idx_scan</code> i\n<code>pg_stat_user_indexes</code> blir stående på null.</p>\n<p><strong>Typer som sier hva verdien er.</strong> Skjemaer skrevet i en fart, av et menneske\neller en modell, griper etter <code>TEXT</code> der en <code>SMALLINT</code> eller en <code>BOOLEAN</code> ville\nholdt. En <code>BOOLEAN</code> er én byte, en <code>SMALLINT</code> to, og <code>'yes'</code> lagret som tekst er\nfire. PostgreSQL fyller også ut slik at hver kolonne med fast bredde havner på\nsin egen justeringsgrense, så en kolonnerekkefølge som er valgt tilfeldig, koster\nogså byte. Én lagerenhet, skrevet i en fart:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">sold</span><span class=\"w\">        </span><span class=\"nb\">text</span><span class=\"p\">,</span><span class=\"w\">  </span><span class=\"c1\">-- &#39;yes&#39; eller &#39;no&#39;\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">id</span><span class=\"w\">          </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">reserved</span><span class=\"w\">    </span><span class=\"nb\">text</span><span class=\"p\">,</span><span class=\"w\">  </span><span class=\"c1\">-- &#39;yes&#39; eller &#39;no&#39;\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">item_id</span><span class=\"w\">     </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">order_id</span><span class=\"w\">    </span><span class=\"nb\">bigint</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Og den samme enheten med typer som sier hva verdiene er, de bredeste først:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">id</span><span class=\"w\">          </span><span class=\"nb\">bigint</span><span class=\"w\"> </span><span class=\"k\">PRIMARY</span><span class=\"w\"> </span><span class=\"k\">KEY</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"n\">timestamptz</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">order_id</span><span class=\"w\">    </span><span class=\"nb\">bigint</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">item_id</span><span class=\"w\">     </span><span class=\"nb\">integer</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">sold</span><span class=\"w\">        </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">reserved</span><span class=\"w\">    </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>På PostgreSQL 18 måler den første raden 72 byte og den andre 54, en fjerdedel\nmindre, så mer av tabellen får plass i minnet. Typene avviser også verdier som\n<code>'maybe'</code>, som applikasjonen ellers måtte forsvare seg mot, og en <code>CHECK</code>-skranke\ngjør det samme for verdiområder: <code>quantity smallint CHECK (quantity &gt; 0)</code>.</p>\n<p><strong>Færre rundturer, og arbeidet der dataene er.</strong> Hver setning applikasjonen\nsender, koster en rundtur og en parsing. En løkke som leser hundre rader og\noppdaterer dem én etter én, er hundre og én rundturer der én mengdebasert\nsetning ville holdt, og en side som henter hver enhet av en vare for å telle de\nledige, sender tusenvis av rader for å regne ut ett tall. Regelen vi bruker: det\nsom må se dataene, går til databasen — filtrering, telling, en skriving som\navhenger av hva som er der — og forretningsregler blir i applikasjonen, fordi\ndatabasens CPU er den vanskeligste i systemet å få mer av.</p>\n<p><strong>Oppdateringer som blir på siden sin.</strong> En rad som endres mange ganger i\nminuttet, etterlater en ny versjon av seg selv hver gang. Når en oppdatering ikke\nendrer noen indeksert kolonne og siden har plass, beholder PostgreSQL den nye\nversjonen på samme side og rører ingen indeks i det hele tatt: en heap-only\ntuple, eller HOT-oppdatering. Hold derfor kolonnene en hyppig oppdatering\nendrer, utenfor indeksene, og la det være plass igjen på sidene:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">fillfactor</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"mi\">90</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Innstillingen gjelder sider som skrives fra da av, så sett den før radene lastes\ninn. På tabellen ovenfor var alle 2 000 reservasjoner som satte <code>reserved</code>,\n<code>reserved_at</code> og <code>order_id</code>, HOT, og av 2 000 oppdateringer som satte <code>sold</code>,\nsom den partielle indeksen leser, var ingen HOT. Salg skjer én gang per enhet;\nreservasjoner kan skje mange ganger. <code>n_tup_hot_upd</code> mot <code>n_tup_upd</code> i\n<code>pg_stat_user_tables</code> viser forholdet på et system i drift.</p>\n<h2 id=\"én-rad-sier-hvem-som-var-først\">Én rad sier hvem som var først</h2>\n\n<p>En enhet går til den første kjøperen hvis setning når den, og reservasjonen\nløper ut etter databasens klokke. To kolonner bærer den: <code>reserved</code>, en boolean,\nog <code>reserved_at</code>, tidspunktet den ble satt.</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"k\">true</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">(),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"n\">order_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">order</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">unit</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">        </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">           </span><span class=\"o\">&lt;</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">&#39;10 minutes&#39;</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Én rad tilbake: denne kjøperen har den. Ingen: noen andre var først, eller den er\nsolgt. To reservasjoner på samme enhet står i kø på radlåsen dens, og på\nREAD COMMITTED, standarden i PostgreSQL, sjekker den andre <code>WHERE</code>-leddet på nytt\nmot raden den første committet, så nøyaktig én av dem får en rad tilbake. En\nreservasjon som er eldre enn ti minutter, regnes som ledig, så ingenting trenger\nå rydde den bort for at svaret skal bli riktig.</p>\n<p>Det gjør boolean-verdien alene til et ufullstendig svar. <code>reserved</code> forblir sann\netter at en reservasjon har løpt ut, og bare de to kolonnene sammen sier om en\nenhet er reservert, så hver spørring som spør, må spørre om begge. En periodisk\nopprydding som nullstiller utløpte reservasjoner, holder tabellen ærlig for alle\nsom leser <code>reserved</code> alene, men korrektheten venter aldri på den. En skranke\nholder paret konsistent:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">ADD</span><span class=\"w\"> </span><span class=\"k\">CONSTRAINT</span><span class=\"w\"> </span><span class=\"n\">reserved_has_time</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">         </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Billettnotatet lagrer fristen, <code>held_until</code>, i stedet for starttidspunktet; det\ner det samme designet. Når kjøperen velger enheten — et sete, et nummerert\ntrykk — nevner setningen den, som her. Når en hvilken som helst enhet duger,\nvelger det notatets <code>SKIP LOCKED</code>-variant en som ingen andre holder.</p>\n<h2 id=\"lesinger-bort-fra-primærnoden\">Lesinger bort fra primærnoden</h2>\n\n<p>Mesteparten av trafikken i en nettbutikk er lesing: produktsiden, «3 igjen»,\nhandlekurven, ordrehistorikken. Ingenting av det trenger primærnoden, den ene\nmaskinen som må holdes fri for skrivinger. En strømmende replika tar de\nlesingene, og å legge til en er konfigurasjon, der en shard er et prosjekt.</p>\n<p>To forbehold følger med. En replika ligger litt etter, så en kjøper som nettopp\nhar reservert en enhet og så leser handlekurven sin fra den, ser kanskje ikke\nreservasjonen: les det en bruker nettopp har skrevet, fra primærnoden, eller send\nden brukeren til primærnoden i noen sekunder etter hver skriving. Og en lang\nrapport på en replika kan bli avbrutt når den holder igjen endringene replikaen\nspiller av. PostgreSQL-dokumentasjonen kaller dette spørringskonflikter og lister\nopp innstillingene som veier replikaforsinkelse opp mot dem.</p>\n<p>Den billigste lesingen er en som aldri når en database. Når hver enhet av en\nvare er reservert, kan ingen kjøpe den før en reservasjon løper ut eller frigis,\nog databasen kan si nøyaktig når det er:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"k\">count</span><span class=\"p\">(</span><span class=\"o\">*</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">FILTER</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">         </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">reserved</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">            </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">reserved_at</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">               </span><span class=\"o\">&lt;</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">&#39;10 minutes&#39;</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">         </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"k\">free</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"k\">min</span><span class=\"p\">(</span><span class=\"n\">reserved_at</span><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">&#39;10 minutes&#39;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">         </span><span class=\"k\">AS</span><span class=\"w\"> </span><span class=\"k\">until</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">units</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"n\">sold</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Når <code>free</code> er null, er <code>until</code> øyeblikket da den tidligste reservasjonen løper\nut: legg «fullt reservert» i hurtigbufferen med det som utløpstid, aldri lenger,\nog hver besøkende fram til da blir avvist av hurtigbufferen i stedet for av\ndatabasen. Når <code>free</code> er null og <code>until</code> er tom, er hver enhet solgt, og svaret\ner endelig. En reservasjon som frigis tidlig — et avvist kort, en forlatt\nhandlekurv — sletter oppføringen i hurtigbufferen, så enheten er straks til\nsalgs igjen.</p>\n<p>Hurtigbufferen sier bare nei. Et ja kommer alltid fra den betingede\n<code>UPDATE</code>-setningen ovenfor, så en utdatert hurtigbuffer kan koste et salg i noen\nsekunder og kan aldri selge en enhet to ganger. Det er billettnotatets\nhurtigbufrede «utsolgt», med en frist.</p>\n<h2 id=\"partisjoner-tabellene-rundt-den-hete-raden\">Partisjoner tabellene rundt den hete raden</h2>\n\n<p>Partisjonering deler én tabell i flere etter en nøkkel — en måned, en\nleietaker — bak ett navn. Det er verdt å si rett ut hva den ikke gjør: den gjør\nikke én rad som det kjempes om, raskere. Den raden bor fortsatt i én partisjon,\npå én maskin, bak én lås.</p>\n<p>Det den gjør, er å holde tabellene rundt det hete punktet små. Ordrer,\nreservasjonshistorikk og hendelser vokser i det uendelige, og de fleste\nspørringer vil ha den nyeste delen:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">id</span><span class=\"w\">          </span><span class=\"nb\">bigint</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">created_at</span><span class=\"w\">  </span><span class=\"n\">timestamptz</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">              </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">(),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">item_id</span><span class=\"w\">     </span><span class=\"nb\">integer</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">quantity</span><span class=\"w\">    </span><span class=\"nb\">smallint</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">              </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">quantity</span><span class=\"w\"> </span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">),</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">paid</span><span class=\"w\">        </span><span class=\"nb\">boolean</span><span class=\"w\"> </span><span class=\"k\">NOT</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\"> </span><span class=\"k\">DEFAULT</span><span class=\"w\"> </span><span class=\"k\">false</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">PRIMARY</span><span class=\"w\"> </span><span class=\"k\">KEY</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">id</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">created_at</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">)</span><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"k\">BY</span><span class=\"w\"> </span><span class=\"n\">RANGE</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">created_at</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">CREATE</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders_2026_10</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"k\">OF</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">VALUES</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"s1\">&#39;2026-10-01&#39;</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">              </span><span class=\"k\">TO</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"s1\">&#39;2026-11-01&#39;</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>En spørring som angir et datointervall, leser bare partisjonene innenfor det, og\ndenne månedens indekser holder seg små nok til å ligge i minnet. Å kvitte seg med\ngamle data er ikke lenger en <code>DELETE</code> av millioner av rader, med oppblåsingen og\nvacuumkjøringen som følger, men én setning:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">orders</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"n\">DETACH</span><span class=\"w\"> </span><span class=\"n\">PARTITION</span><span class=\"w\"> </span><span class=\"n\">orders_2025_10</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Primærnøkkelen må inneholde partisjonsnøkkelen, og derfor er den\n<code>(id, created_at)</code>. En spørring som ikke angir nøkkelen, leser hver partisjon, så\npartisjoner etter det spørringene allerede filtrerer på.</p>\n<h2 id=\"når-sharding-er-fornuftig\">Når sharding er fornuftig</h2>\n\n<p>Det finnes én grense sharding ikke kan flytte: hver vare har én eier. En enhet\nkan ikke reserveres på to servere samtidig, så sharding flytter hele varer,\naldri halvparten av én. Når én enkelt vare vokser fra maskinen sin, deler\nbillettnotatets bøtter beholdningen dens i mindre biter, og hver bit har igjen\nnøyaktig én eier.</p>\n<p>Innenfor det kaller vi sharding fornuftig når én av disse er sann, og ikke før:</p>\n<ul>\n<li><strong>Én maskin er full av godt arbeid.</strong> Setningene er allerede slanke — én\nbetinget skriving hver, ingenting eksternt inne i en transaksjon,\noppdateringer som blir på siden sin — og primærnodens CPU eller\nwrite-ahead-logg er fortsatt mettet, på den største maskinen det er verdt å\nkjøpe, på tvers av mange varer heller enn én. Én het vare er et bøtteproblem,\nikke et shardproblem.</li>\n<li><strong>Dataene har vokst fra vedlikeholdet sitt.</strong> En sikkerhetskopi kan ikke\ngjenopprettes innen tiden som er lovet virksomheten, en indeks kan ikke bygges\neller en tabell vacuumes i de stille timene, eller arbeidssettet får ikke\nlenger plass i minnet.</li>\n<li><strong>Isolasjon er kravet.</strong> En leietaker hvis kontrakt eller tilsynsmyndighet\nsier at dataene skal ligge for seg selv, eller hvis salg ikke får dele disk med\nnoen andres.</li>\n</ul>\n<p>Kostnaden havner på alt annet. Hver migrering, sikkerhetskopi, gjenoppretting\nog rapport kjøres én gang per shard; en join på tvers av shards blir\napplikasjonskode; og en spørring som ikke kjenner shardnøkkelen sin, spør alle\nsammen. Det er regningen delene ovenfor finnes for å unngå, og når den først er\nbetalt, er den vanskelig å få tilbake.</p>\n<h2 id=\"når-en-unnskyldning-er-billigere-enn-en-lås\">Når en unnskyldning er billigere enn en lås</h2>\n\n<p>Det finnes et seriøst argument for å gå den andre veien. La hver server selge fra\nbeholdningen den kan se, godta at en enhet nå og da går til to kjøpere, og la\nkundeservice refundere én av dem med en unnskyldning. Forretningslogikken blir\nkonsistent over tid, bygd for de 99,9 % av forespørslene som aldri kolliderer, og\nen asynkron løkke finner kollisjonene etterpå og gjør dem gode igjen. Til\ngjengjeld trenger butikken aldri å si «prøv igjen». Helland og Campbell legger\nfram den sterkeste formen av saken i <em>Building on Quicksand</em>: et system som\nkvitterer for arbeid før reservesystemet har fått det, gjør hvert løfte\nsannsynlighetsbasert, og bedrifter har alltid gjort opp løftene de ikke kunne\nholde, med unnskyldninger.</p>\n<p>Vi er enige i det meste av det. Vi er uenige om hvor det skal brukes.</p>\n<p><strong>Det meste av en butikk bør være konsistent over tid.</strong> Katalogen, søkeindeksen,\n«3 igjen»-merket, handlekurven, anbefalingene, e-postene, lagerstrømmen og\nrapportene kan alle ligge sekunder bak, og å la dem vente på primærnoden koster\nfart og kjøper ingenting. Dynamo ble bygd for nettopp dette: handlekurven til\nAmazon glemmer eller avviser aldri en «legg i handlekurven», selv mens maskiner\nsvikter, og prisen er at en sammenslåing kan bringe tilbake en vare kjøperen\nhadde fjernet. Etter reservasjonen er utsjekkingen i billettnotatet allerede\nasynkron: en saga av lokale steg, meldinger levert minst én gang, og konsumenter\nsom er idempotente, med ordren som nøkkel. En avstemmingsløkke hører hjemme i\nethvert design som snakker med en betalingsleverandør, fordi webhooks går tapt.</p>\n<p><strong>Reservasjonen er det ene stedet der det ikke holder.</strong> «Sjelden» forutsetter at\nkollisjoner er sjeldne, og i et lynsalg er kollisjonene salget. En server som\nselger ut fra sitt eget bilde, fortsetter å selge til den hører hva de andre har\nsolgt, så oversalget er omtrent forsøksraten ganger replikeringsforsinkelsen. I\nen enkel simulering deler to regioner 2 000 forsøk i sekundet, hver selger ut fra\nsin egen telling og hører om den andres salg 200 millisekunder senere. Over fem\nkjøringer av et salg på 10 000 enheter solgte hver kjøring mellom 191 og 205\nenheter som ikke fantes. Regnestykket forutsier 200: forsøksraten, ganger\nforsinkelsen, ganger andelen salg gjort i den andre regionen. Med én eier per\nvare solgte den samme modellen nøyaktig beholdningen. Det skjer hver gang noe\nblir utsolgt, enten noe svikter eller ikke, og det rammer på høydepunktet, når\nflest kjøpere ser på.</p>\n<p><strong>Oppetiden er mindre enn den høres ut.</strong> Gilbert og Lynch beviste avveiningen\nbak konsistens over tid: mens nettverket er partisjonert, kan et system svare\neller være konsistent, ikke begge deler. Abadis PACELC legger til den andre\nhalvdelen. Når ingenting er partisjonert, står avveiningen mellom latens og\nkonsistens, og én eier per vare, med slanke setninger, er allerede rask. Ingen av\ndesignene når 100 %: betalingsleverandøren, DNS og nettverkene mellom dem har\nsine egne driftsavbrudd. Det det strenge designet gir opp, er smalt. Når eieren\nav beholdningen til én vare ikke kan nås, slutter den varen å selge i de\nsekundene en failover tar, og delen i billettnotatet om å sette varen på pause\navgrenser pausen til varene på maskinen som feilet. Resten av butikken\nfortsetter å selge.</p>\n<p><strong>En unnskyldning er ikke en refusjon.</strong> Refusjonen er den billige delen.\nKjøperen fikk ja, og kan allerede ha bestilt toget, tatt fri fra jobben og\nfortalt det til vennene sine. Å høre «utsolgt» et sekund for tidlig er en\nskuffelse. Å høre at en bekreftet ordre er trukket tilbake, er et brutt løfte.\nKundeservice er bemannet for en vanlig dag, og oversalg kommer samlet, i årets\ntravleste minutt. Før du planlegger å trekke tilbake bekreftede ordrer, spør en\nadvokat hva en bekreftelse forplikter butikken til.</p>\n<p><strong>Behold den asynkrone delen, og flytt den til før ja-svaret.</strong> Det forslaget\ntreffer riktig, er at kjøperen ikke trenger å vente på en lås. Ta imot hvert\nklikk i en varig, replikert logg, én partisjon per vare, og fortell kjøperen at\nsvaret er på vei. Å ta imot et klikk avhenger da bare av loggen, og en logg som\ner replikert over tre maskiner, fortsetter å ta imot skrivinger når én av dem\nsvikter. Én eier per vare leser loggen i rekkefølge og avgjør, i\nminnehastighet, og svaret går tilbake som en melding i løpet av sekunder. Under\nen failover for den eieren venter forespørslene i loggen i stedet for å bli\navvist eller solgt to ganger. Ja-svaret er en reservasjon, og betalingen følger\netter den som i utsjekkingen i billettnotatet, så ingen betaler for en enhet de\nikke fikk. Avstemmingen skjer fortsatt, men før løftet i stedet for etter det.</p>\n<p><strong>Del beholdningen, ikke tellingen.</strong> Når virksomheten trenger at regioner\nselger uten å spørre hverandre, gi hver region en kvote av beholdningen og la den\nselge fra sin egen kvote uten å spørre noen. En region som går tom for kvote, ber\nom mer, og mot slutten av salget faller kvotene sammen til én eier igjen. Bailis\nog kolleger finner ut hvilke regler servere kan overholde uten å snakke med\nhverandre, og en beholdning som ikke får gå under null, er ikke én av dem. Kvoter\nfjerner ikke den samtalen; de flytter den bort fra den hete stien, til det\nsjeldne øyeblikket da en kvote fylles på. Det er designet som er konsistent over\ntid og likevel holder invarianten. Den eneste kostnaden er halen, der en kjøper\nblir avvist i én region mens en annen region fortsatt har beholdning.</p>\n<p><strong>Når oversalg er riktig strategi.</strong> Noen varer kan lages på nytt: et trykk på\nbestilling, en T-skjorte leverandøren fortsatt har, en lisensnøkkel. Å selge\nforbi tellingen er da en restordre med en ærlig dato, og konsistens over tid er\nriktig design. Flyselskaper overbooker med vilje. De kjenner andelen som ikke\nmøter opp, priser risikoen og kompenserer etter en regel skrevet på forhånd. Det\ner testen. Et oversalg er akseptabelt når virksomheten valgte størrelsen og\nbotemiddelet før salget. Det er en feil når det rammer den som tilfeldigvis\nklikket sist.</p>\n<p><strong>Velger du unnskyldninger, så bygg dem som betalinger.</strong> Løkken forslaget\nbeskriver, er en refusjonsrørledning, og den trenger det alt som flytter penger,\ntrenger:</p>\n<ul>\n<li><strong>En id på hvert salg,</strong> slik at et nytt forsøk er det samme salget, og en\nhovedbok over salg som bare kan utvides. Beholdningstallet utledes av\nhovedboken og redigeres aldri.</li>\n<li><strong>En regel for hvem som beholder enheten</strong> som du kunne forklart for kjøperen\nsom tapte. Ordne etter loggen, ikke etter klokkene på ulike servere, der noen\nfå millisekunders avvik ville avgjort hvem som får refusjonen.</li>\n<li><strong>Kompensasjoner som er idempotente og loggført:</strong> kanseller, annuller eller\nrefunder, gi kjøperen beskjed. De er angre-stegene i sagaen, kjørt av en\nmaskin.</li>\n<li><strong>En frist.</strong> Løkken må finne hvert oversalg før noe sendes, og før kjøperen\nfinner det.</li>\n<li><strong>Et budsjett, målt.</strong> Oversalg per salg, mot tallet virksomheten skrev under\npå, ved siden av invarianten på det samme dashbordet, med et varsel når\nbudsjettet er brukt opp.</li>\n</ul>\n<p>Svaret vårt er å være konsistent over tid overalt rundt reservasjonen, streng\nved reservasjonen og asynkron i hvordan svaret når kjøperen. Design for den\nvanlige veien, men i et lynsalg er kollisjonen den vanlige veien. Vil\nvirksomheten ha noen få oversalg i bytte mot noe, så gjør det til et tall\nvirksomheten valgte, med et botemiddel skrevet før salget, og mål det.</p>\n<h2 id=\"det-vi-ville-sagt-til-den-neste\">Det vi ville sagt til den neste</h2>\n\n<ul>\n<li><strong>Les før du kjøper.</strong> <code>pg_stat_statements</code> og én <code>EXPLAIN</code> svarer på flere\nkapasitetsspørsmål enn en ny server gjør.</li>\n<li><strong>La typene si hva dataene er.</strong> En boolean lagret som boolean er mindre, og\nden avviser tull.</li>\n<li><strong>Send databasen det som må se dataene, og ikke noe annet.</strong> Filtrering og\ntelling hører hjemme der. Forretningsregler gjør det ikke.</li>\n<li><strong>Hurtigbufferen sier nei; bare databasen sier ja.</strong> Da koster en utdatert\nhurtigbuffer sekunder, aldri en enhet.</li>\n<li><strong>Partisjonering er husarbeid.</strong> Den holder tabellene rundt en het rad små og\ngjør ingenting for raden.</li>\n<li><strong>Shard når én maskin er full av godt arbeid,</strong> ikke når den er full av\nventing.</li>\n<li><strong>Avstem før ja-svaret, ikke etter.</strong> Alt rundt reservasjonen kan være\nkonsistent over tid. Reservasjonen selv, gjort på den måten, overselger med\nforsøksraten ganger forsinkelsen, ved hvert salg.</li>\n</ul>\n<h2 id=\"kilder\">Kilder</h2>\n\n<ul>\n<li><a href=\"https://www.postgresql.org/docs/current/pgstatstatements.html\">PostgreSQL: pg_stat_statements</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/using-explain.html\">PostgreSQL: bruk av EXPLAIN</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/monitoring-stats.html\">PostgreSQL: det kumulative statistikksystemet</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-numeric.html\">PostgreSQL: numeriske typer</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-character.html\">PostgreSQL: tegntyper</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/datatype-boolean.html\">PostgreSQL: boolean-typen</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/indexes-partial.html\">PostgreSQL: partielle indekser</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/storage-hot.html\">PostgreSQL: heap-only tuples (HOT)</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/ddl-partitioning.html\">PostgreSQL: tabellpartisjonering</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/hot-standby.html\">PostgreSQL: hot standby og spørringskonflikter</a></li>\n<li><a href=\"https://arxiv.org/abs/0909.1788\">Pat Helland og Dave Campbell, Building on Quicksand</a></li>\n<li><a href=\"https://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf\">Seth Gilbert og Nancy Lynch, Brewer&rsquo;s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services</a></li>\n<li><a href=\"https://www.cs.umd.edu/~abadi/papers/abadi-pacelc.pdf\">Daniel Abadi, Consistency Tradeoffs in Modern Distributed Database System Design</a></li>\n<li><a href=\"https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf\">DeCandia et al., Dynamo: Amazon&rsquo;s Highly Available Key-value Store</a></li>\n<li><a href=\"https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf\">Hector Garcia-Molina og Kenneth Salem, Sagas</a></li>\n<li><a href=\"https://www.vldb.org/pvldb/vol8/p185-bailis.pdf\">Bailis et al., Coordination Avoidance in Database Systems</a></li>\n</ul>\n",
      "date_published": "2026-10-14T00:00:00Z",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb"
    },
    {
      "id": "https://engineer.company/nb/notes/selling-limited-stock-without-overselling/",
      "url": "https://engineer.company/nb/notes/selling-limited-stock-without-overselling/",
      "title": "Å selge den siste billetten nøyaktig én gang",
      "summary": "Et lynsalg overselger på én het rad, ikke på en stor database. Det som stopper det: en betinget skriving, reservasjoner som utløper, og en saga.",
      "content_html": "<p>Et lynsalg setter ti tusen enheter foran en million mennesker samtidig. Den\ninteressante feilen er ikke at butikken går ned. Det er at butikken holder seg\noppe og selger ti tusen og førti. Instinktet som følger — databasen er for\nliten, shard den — svarer på et spørsmål ingen stilte. Databasen var aldri for\nstor. Hver kjøper skrev til\nden samme raden (hele hendelsen bor i én rad).</p>\n<p>Her tar vi problemet fra hverandre før det når et prosjekt: hvor taket kommer\nfra, den ene setningen som fjerner det meste av det, og hva du bygger når én\ndatabase virkelig ikke er nok.</p>\n<h2 id=\"én-rad-alle-kjøpere\">Én rad, alle kjøpere</h2>\n\n<p>Tjue millioner rader fordelt over en klynge er et løst problem. Én rad som hver\nforespørsel må endre, er det ikke, og flere maskiner endrer ingenting ved det,\nfordi raden bor på én av dem. Distribuerte datalagre sier det rett ut: DynamoDB\ngir én enkelt partisjon rundt 1 000 skriveenheter i sekundet, og hver skriving\ntil ett element havner på partisjonen til det elementet, uansett hvor mange\nandre det finnes.</p>\n<p>Det ligger et teorem bak denne formen. Bailis og kolleger kaller en invariant\ninvariant-konfluent når replikaer kan ta imot skrivinger på egen hånd og likevel\nflettes sammen til en gyldig tilstand. <code>stock &gt;= 0</code> er skoleeksempelet på en som\nikke er det: to replikaer som hver ser én enhet igjen, vil hver selge den, og\ningen fletting kan ta det andre salget tilbake. Å aldri overselge krever\nkoordinering. Det eneste spørsmålet er hvor mye av den hvert salg betaler for.</p>\n<h2 id=\"tallet-som-avgjør-det\">Tallet som avgjør det</h2>\n\n<p>Taket for én vare er omtrent hvor mange salg én lås bærer, delt på hvor lenge\nlåsen holdes:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">maks. salg/s per vare ≈ salg per lås ÷ låsens holdetid\n</span></span></code></pre></div><table>\n\t<thead>\n\t\t\t<tr>\n\t\t\t\t\t<th>Hva som holder raden</th>\n\t\t\t\t\t<th>Tak per rad</th>\n\t\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t\t<tr>\n\t\t\t\t\t<td>Lås, så kall til betaling</td>\n\t\t\t\t\t<td>~2/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Lås, så applikasjonslogikk</td>\n\t\t\t\t\t<td>~100–200/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Én betinget <code>UPDATE</code></td>\n\t\t\t\t\t<td>~500–2 000/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Batcher på 100 per commit</td>\n\t\t\t\t\t<td>~50 000/s</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Én skriver i minnet</td>\n\t\t\t\t\t<td>~100 000+/s</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>Dette er størrelsesordener, ikke ytelsesmålinger; maskinvaren flytter dem. Det\nden ikke flytter, er formen. Det meste av en kort lås er commiten som venter på\nat loggen skal nå disken, og det er derfor batching vinner med to\nstørrelsesordener: hundre salg deler én diskskriving.</p>\n<p>Det gir fire spaker, og hvert design nedenfor trekker i én av dem:</p>\n<ol>\n<li><strong>Hold låsen kortere</strong> — én setning, ingenting over nettverket inne i den.</li>\n<li><strong>Gjør mer per lås</strong> — samle nedtellingene i batcher.</li>\n<li><strong>Ha flere låser</strong> — fordel beholdningen på flere nøkler.</li>\n<li><strong>Send færre forespørsler til låsen</strong> — avvis de fleste kjøperne før de når\nden.</li>\n</ol>\n<p>Den siste betyr mer enn den høres ut som. Med en million kjøpere og ti tusen\nenheter må 990 000 forespørsler mislykkes. Avvisninger er det meste av\ntrafikken, så «utsolgt» må være det billigste svaret systemet gir: et venterom\nforan salget, en port som slipper inn omtrent like mange kjøpere som det finnes\nenheter, et <code>sold_out</code>-flagg som CDN-et serverer uten å spørre noen, og en\nhastighetsbegrensning per konto.</p>\n<h2 id=\"få-den-ene-databasen-i-orden-først\">Få den ene databasen i orden først</h2>\n\n<p>Versjonen som går i sneglefart, eller som overselger, ser som regel slik ut:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">BEGIN</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\"> </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">UPDATE</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- applikasjonen sjekker tallet,\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- og kaller kanskje en betalingsleverandør\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\"> </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">new</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"k\">COMMIT</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Låsen er riktig. Det den holdes over, er feil: hvert millisekund applikasjonen\nbruker mellom <code>SELECT</code> og <code>UPDATE</code>, er et millisekund den neste kjøperen venter.\nUten <code>FOR UPDATE</code> er det verre. To kjøpere leser 1, begge skriver 0, og én enhet\ner solgt to ganger.</p>\n<p>Løsningen gjør sjekken og nedtellingen til én setning:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">-</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">qty</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">tenant_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">tenant</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">item_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">item</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">&gt;=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">qty</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">available</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- én rad tilbake: reservert. ingen: utsolgt.\n</span></span></span></code></pre></div><p>I PostgreSQL på READ COMMITTED venter en annen kjøper som når raden mens den\nførste holder den, og evaluerer så <code>WHERE</code>-leddet på nytt mot raden den første\nhar committet. Tok det salget den siste enheten, slår betingelsen feil, og den\nandre kjøperen får ingen rad tilbake — utsolgt — i stedet for et negativt tall.\nPå REPEATABLE READ eller SERIALIZABLE får den andre kjøperen i stedet en\nserialiseringsfeil, som er trygt, men gjør kampen om raden til nye forsøk; READ\nCOMMITTED er nivået denne setningen er skrevet for. InnoDB i MySQL kommer til\nsamme svar, fordi en <code>UPDATE</code> der leser den sist committede raden på alle nivåer.\nLegg også til begrensningen, slik at en feil et annet sted feiler høylytt i\nstedet for å selge beholdning som ikke finnes:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">ALTER</span><span class=\"w\"> </span><span class=\"k\">TABLE</span><span class=\"w\"> </span><span class=\"n\">inventory</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">ADD</span><span class=\"w\"> </span><span class=\"k\">CONSTRAINT</span><span class=\"w\"> </span><span class=\"n\">available_not_negative</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">  </span><span class=\"k\">CHECK</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">available</span><span class=\"w\"> </span><span class=\"o\">&gt;=</span><span class=\"w\"> </span><span class=\"mi\">0</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Så resten av arbeidet på den ene databasen, omtrent i rekkefølge etter gevinst:</p>\n<ul>\n<li><strong>Ingenting eksternt inne i transaksjonen.</strong> Ingen betalingskall, ingen\nHTTP-forespørsel, ingen publisert melding. Det hører hjemme før transaksjonen\neller etter den.</li>\n<li><strong>Rør den hete raden sist.</strong> Sett inn ordren og alt annet først, slik at låsen\npå den omstridte raden bare lever gjennom den siste setningen og commiten.</li>\n<li><strong>Ingen optimistisk låsing på en het teller.</strong> En versjonskolonne gjør kampen\nom raden til nye forsøk, og under et salg er det de nye forsøkene som er\nlasten.</li>\n<li><strong>Hent lesingene fra et annet sted.</strong> «12 igjen» på produktsiden kan komme fra\nen replika eller en hurtigbuffer som ligger et sekund etter. Bare salget\ntrenger primærnoden.</li>\n<li><strong>Gi lagerbeholdningen en egen database,</strong> slik at et salg aldri venter bak\nkatalogen på den samme disken.</li>\n<li><strong>Samle tilkoblingene i en pool.</strong> Hundre tusen kjøpere bør bli noen dusin\ntilkoblinger gjennom PgBouncer, ikke tusenvis av databaseprosesser som venter\npå én lås.</li>\n</ul>\n<p>Seter har en annen form, fordi hver enhet er unik, og de trenger en annen\nsetning. Hvert sete er en rad, og en kjøper hopper over radene en annen\ntransaksjon holder, i stedet for å vente bak dem:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-sql\" data-lang=\"sql\"><span class=\"line\"><span class=\"cl\"><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">seats</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">SET</span><span class=\"w\"> </span><span class=\"n\">held_by</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"k\">order</span><span class=\"p\">,</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">       </span><span class=\"n\">held_until</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">+</span><span class=\"w\"> </span><span class=\"nb\">interval</span><span class=\"w\"> </span><span class=\"s1\">&#39;10 min&#39;</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\"> </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">   </span><span class=\"k\">SELECT</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"w\"> </span><span class=\"k\">FROM</span><span class=\"w\"> </span><span class=\"n\">seats</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    </span><span class=\"k\">WHERE</span><span class=\"w\"> </span><span class=\"n\">event_id</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"p\">:</span><span class=\"n\">event</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"n\">sold_at</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">      </span><span class=\"k\">AND</span><span class=\"w\"> </span><span class=\"p\">(</span><span class=\"n\">held_until</span><span class=\"w\"> </span><span class=\"k\">IS</span><span class=\"w\"> </span><span class=\"k\">NULL</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">           </span><span class=\"k\">OR</span><span class=\"w\"> </span><span class=\"n\">now</span><span class=\"p\">()</span><span class=\"w\"> </span><span class=\"o\">&gt;</span><span class=\"w\"> </span><span class=\"n\">held_until</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    </span><span class=\"k\">LIMIT</span><span class=\"w\"> </span><span class=\"mi\">1</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"w\">    </span><span class=\"k\">FOR</span><span class=\"w\"> </span><span class=\"k\">UPDATE</span><span class=\"w\"> </span><span class=\"n\">SKIP</span><span class=\"w\"> </span><span class=\"n\">LOCKED</span><span class=\"p\">)</span><span class=\"w\">\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"n\">RETURNING</span><span class=\"w\"> </span><span class=\"n\">id</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>En utløpt reservasjon er rett og slett ledig igjen, så det trengs ingen\noppryddingsjobb for at svaret skal bli riktig. Og <code>now()</code> er databasens klokke,\nså applikasjonsservere med klokker som ikke stemmer overens, kan ikke være\nuenige om når en reservasjon gikk ut.</p>\n<p>For mange butikker er denne delen hele løsningen. Gjør den før noe av det som\nfølger, fordi sharding er en enveisdør: lett å gå gjennom, dyr å gå tilbake\ngjennom.</p>\n<h2 id=\"reserver-først-betal-etterpå\">Reserver først, betal etterpå</h2>\n\n<p>En kjøper som tar den siste enheten og så forlater kortskjemaet, har ikke kjøpt\nden. Et salg er derfor ikke én skriving, men en kort livssyklus:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">AVAILABLE → HELD, i minutter → SOLD\n</span></span><span class=\"line\"><span class=\"cl\">                             → RELEASED\n</span></span></code></pre></div><ul>\n<li><strong>Reservasjonen er knyttet til forespørselen, ikke til forsøket.</strong> Hver\nutsjekking har en idempotensnøkkel, og reservasjonstabellen har\n<code>UNIQUE (tenant_id, idempotency_key)</code>, slik at et klikk som prøves på nytt, får\ntilbake reservasjonen det allerede har, i stedet for en enhet til. Én\nPostgreSQL-detalj biter her: <code>INSERT ... ON CONFLICT DO NOTHING RETURNING id</code>\nreturnerer ingenting når raden fantes fra før, så «ingen rad tilbake» betyr\n«slå den opp», ikke «feilet».</li>\n<li><strong>Hver overgang er betinget.</strong> Å bekrefte et salg flytter en reservasjon fra\n<code>held</code> til <code>sold</code> bare der den fortsatt er <code>held</code>, og en frigivelse fører\nbeholdningen tilbake bare hvis dens egen oppdatering endret nøyaktig én rad.\nEn frigivelse som kjører to ganger — tidsavbruddet og avbryt-knappen, for\neksempel — gir enheten tilbake én gang.</li>\n<li><strong>Reservasjoner utløper etter minutter.</strong> Fem til ti er vanlig. Forleng\nreservasjonen én gang når betalingen starter, slik at et tregt kort ikke\nmister enheten det betaler for.</li>\n<li><strong>Sett et tak på enheter per kjøper ved reservasjonen,</strong> slik at én konto ikke\nkan holde hele beholdningen mens den bestemmer seg.</li>\n</ul>\n<p>Under alt dette ligger én linje, den mest nyttige i hele designet:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">available + held + sold = total\n</span></span></code></pre></div><p>Den holder etter hver transaksjon, én spørring sjekker den, og når den slutter å\nholde, har noe oversolgt eller mistet en enhet — før en kunde sier fra.</p>\n<h2 id=\"når-én-database-ikke-er-nok\">Når én database ikke er nok</h2>\n\n<p>Noen ganger er den virkelig ikke det: mange leietakere, mange salg samtidig,\nflere regioner. Strukturen som skalerer, holder fast ved én regel — hver vare\nhar nøyaktig én eier av beholdningen sin, en lagertjeneste, og ingenting annet\nbestemmer — og sharder den tjenesten etter <code>(tenant_id, item_id)</code>, slik at\nskrivingene for én vare aldri spenner over to shards. De største leietakerne får\negne celler, slik at salget deres ikke kan sulte ut noen andres. Bare\nreservasjonen må være synkron; alt etter den kan være en melding.</p>\n<p>For den hete varen inne i en shard finnes det fire verktøy, og hvert av dem\ntrekker i én av spakene over.</p>\n<p><strong>En port foran.</strong> En Redis-teller, som telles ned av et Lua-skript slik at\nsjekken og nedtellingen er ett steg, slipper inn omtrent like mange kjøpere som\ndet finnes enheter og avviser alle andre på mikrosekunder. Den er ikke\ndommeren. Redis replikerer asynkront, så etter en failover kan den nye\nprimærnoden ha glemt noen innslipp og latt noen for mange komme gjennom.\nDatabasens betingede <code>UPDATE</code> avgjør fortsatt, og en avstemmingsjobb nullstiller\nporten ut fra databasens tall.</p>\n<p><strong>Del opp beholdningen.</strong> Ti tusen enheter kan være hundre bøtter med hundre,\nhver sin egen rad eller sin egen shard, og hver kjøper sendes til én av dem:\nhundre låser i stedet for én. Ideen er gammel — O&rsquo;Neils escrow-transaksjoner i\n1986, så demarkasjonsprotokollen til Barbará og Garcia-Molina, så de begrensede\ntellerne til Balegas og kolleger — og den eneste reelle kostnaden er halen.\nBøtten til en kjøper går tom mens andre fortsatt har beholdning, så prøv enten\nen annen bøtte eller flytt restene, og slå dem sammen til én igjen mot slutten\nav et salg. Den samme ideen gir hver region en egen kvote, slik at de fleste\nsalg aldri krysser et hav for å spørre.</p>\n<p><strong>Én skriver per vare.</strong> Legg hver forespørsel for en vare på én\nKafka-partisjon, lest av én konsument som holder tellingen i minnet og committer\nbeslutningene sine i batcher. Én tråd og ingen låser: LMAX-formen. Forespørsler\nhar en id, og skriveren forkaster en den allerede har avgjort, fordi en kø\nleverer to ganger.</p>\n<p><strong>Enheter som rader.</strong> Når enhetene er få og unike — seter, nummererte utgaver —\nopprett én rad for hver på forhånd og reserver dem med <code>SKIP LOCKED</code> som over,\neller hold enhets-id-ene i et Redis-sett og hent én med <code>SPOP</code>. Da finnes det\ningen teller igjen å slåss om.</p>\n<h2 id=\"resten-av-utsjekkingen\">Resten av utsjekkingen</h2>\n\n<p>Betaling, ordre og kvittering er separate systemer, og ingen transaksjon spenner\nover dem. Tofase-commit er læreboksvaret og feil svar her: den blokkerer så\nlenge en deltaker er treg, den legger rundturer til den heteste stien, og\nbetalingsleverandører deltar ikke i den. En saga gjør jobben, en rekke lokale\nsteg, hvert med et steg som angrer det:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-text\" data-lang=\"text\"><span class=\"line\"><span class=\"cl\">1. reserver beholdning     angre: frigi reservasjonen\n</span></span><span class=\"line\"><span class=\"cl\">2. ventende ordre          angre: kanseller ordren\n</span></span><span class=\"line\"><span class=\"cl\">3. forleng reservasjonen   (utløper av seg selv)\n</span></span><span class=\"line\"><span class=\"cl\">4. autoriser betalingen    angre: annuller den\n</span></span><span class=\"line\"><span class=\"cl\">5. bekreft HELD → SOLD\n</span></span><span class=\"line\"><span class=\"cl\">6. trekk betalingen\n</span></span></code></pre></div><p>Å autorisere først og trekke sist betyr at en betaling som kommer etter at\nreservasjonen har utløpt, annulleres i stedet for å refunderes: pengene flyttet\nseg aldri. Hvert steg skriver meldingen sin til en outbox-tabell i den samme\ntransaksjonen som endringen den kunngjør, og change data capture publiserer\nden, så ingenting går tapt mellom lagret og sendt. Hver konsument er idempotent,\nmed ordren som nøkkel. Meldinger kommer minst én gang, og idempotens gjør det i\npraksis til én gang.</p>\n<p>En distribuert lås hører ikke hjemme noe sted i dette. Kleppmanns poeng er at\nden som holder en lås, kan bli stående på pause forbi leieperioden sin — en\nsøppelinnsamling, en virtuell maskin som henger — og fortsette å skrive, og\nkuren er et fencing-token som datalageret sjekker. Men et datalager som kan\nsjekke et token, kan sjekke <code>available &gt;= :qty</code>, og da trengte det aldri låsen.</p>\n<h2 id=\"sett-varen-på-pause-overselg-den-aldri\">Sett varen på pause, overselg den aldri</h2>\n\n<p>Konsistens og tilgjengelighet velges per spørsmål, ikke per system. «Kan jeg\nselge denne enheten?» må være konsistent: når eieren av en vares beholdning ikke\nkan nås, slutter den varen å selges. Alt annet — katalogen, «3 igjen»-merket,\nordrehistorikken — kan forbli tilgjengelig og være litt utdatert.</p>\n<p>Det stilleste oversalget kommer fra en failover. Med asynkron replikering\nbekrefter en primærnode et salg og dør før replikaen har fått det; replikaen\nforfremmes, salget er borte, og enheten selges på nytt. Løsningen er å gjøre et\nsalg varig på mer enn én maskin før det bekreftes: en synkron standby i\nPostgreSQL (<code>synchronous_commit</code> med <code>synchronous_standby_names</code>), eller et\ndatalager bygd på Raft. Det koster latens på hver commit, noe som forlenger\nnøyaktig den holdetiden for låsen som taket deles på, og batching og porten\nkjøper den tilbake. Avgrens pausen også: en shard som feiler, skal stoppe varene\npå den, ikke butikken.</p>\n<h2 id=\"å-bevise-det\">Å bevise det</h2>\n\n<p>Ingenting av dette bør tas på tro, heller ikke vårt.</p>\n<ul>\n<li><strong>Overvåk invarianten.</strong> Kjør <code>available + held + sold = total</code> etter en\ntidsplan, vis den i en graf og send varsel ved ethvert avvik. Null oversalg er\net tjenestenivåmål med en alarm, ikke et håp.</li>\n<li><strong>Fremprovoser kappløpet med vilje.</strong> Ti tusen samtidige kjøpere til hundre\nenheter, i testsuiten, med påstand om nøyaktig hundre solgt og ingen tall\nunder null.</li>\n<li><strong>Ødelegg ting under kappløpet.</strong> Drep primærnoden, kutt nettverket, sett en\nprosess på pause — i Jepsen-stil — og sjekk invarianten etterpå.</li>\n<li><strong>Lasttest avvisningene.</strong> Utsolgt-stien må holde seg rask når den er 99 % av\ntrafikken, og kanten må ta imot den.</li>\n<li><strong>Samle målingene på ett dashbord:</strong> låseventing, utløpte reservasjoner og\navstanden mellom portens telling og databasens.</li>\n</ul>\n<h2 id=\"i-hvilken-rekkefølge\">I hvilken rekkefølge</h2>\n\n<ol>\n<li>Mål først. <code>pg_stat_statements</code> og låseventingen sier om den hete raden i\ndet hele tatt er problemet.</li>\n<li>Gjør skrivingen til én betinget setning.</li>\n<li>Kvitt deg med last: et venterom, en port, et hurtigbufret «utsolgt».</li>\n<li>Gi lagerbeholdningen sin egen tjeneste og sin egen eier.</li>\n<li>Shard etter <code>(tenant_id, item_id)</code>, og bare hvis de fire første ikke var nok.</li>\n<li>Legg til en modus for hete varer, bøtter eller én enkelt skriver, for salget\ndu vet kommer.</li>\n<li>Regioner til slutt, med kvoter.</li>\n</ol>\n<h2 id=\"det-vi-ville-sagt-til-den-neste\">Det vi ville sagt til den neste</h2>\n\n<ul>\n<li><strong>Sharding svarer på et annet spørsmål.</strong> Den fordeler mange rader, og et\noversolgt salg er én rad. Finn ut hvilket av dem du har, før du bygger.</li>\n<li><strong>Gjør sjekken og nedtellingen til én setning.</strong> Det meste av avstanden mellom\ni orden og oversolgt er tiden mellom å lese et tall og å skrive det.</li>\n<li><strong>Det meste av trafikken er svaret nei.</strong> Gjør avvisning til det billigste\nsystemet gjør.</li>\n<li><strong>Sett på pause før du overselger.</strong> En vare som slutter å selges i tretti\nsekunder, er en unnskyldning. En vare som er solgt to ganger, er en refusjon,\nen supportkø og en kunde som husker det.</li>\n<li><strong>Skriv ned invarianten og overvåk den.</strong> Den er én linje, og den er det\neneste som sier at designet virker i produksjon og ikke bare i gjennomgangen.</li>\n</ul>\n<h2 id=\"kilder\">Kilder</h2>\n\n<ul>\n<li><a href=\"https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED\">PostgreSQL: isolasjonsnivået Read Committed</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE\">PostgreSQL: <code>FOR UPDATE</code> og <code>SKIP LOCKED</code></a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION\">PostgreSQL: synkron replikering</a></li>\n<li><a href=\"https://www.vldb.org/pvldb/vol8/p185-bailis.pdf\">Bailis et al., Coordination Avoidance in Database Systems</a></li>\n<li><a href=\"https://arxiv.org/abs/1503.09052\">Balegas et al., Extending Eventually Consistent Cloud Databases for Enforcing Numeric Invariants</a></li>\n<li><a href=\"https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html\">Martin Kleppmann: How to do distributed locking</a></li>\n<li><a href=\"https://martinfowler.com/articles/lmax.html\">Martin Fowler: The LMAX Architecture</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html\">DynamoDB: utforming av partisjonsnøkler</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html\">DynamoDB: partisjoner og datafordeling</a></li>\n<li><a href=\"https://microservices.io/patterns/data/saga.html\">microservices.io: Saga</a></li>\n<li><a href=\"https://microservices.io/patterns/data/transactional-outbox.html\">microservices.io: Transactional outbox</a></li>\n<li><a href=\"https://redis.io/docs/latest/operate/oss_and_stack/management/replication/\">Redis: replikering</a></li>\n<li><a href=\"https://jepsen.io/\">Jepsen</a></li>\n</ul>\n",
      "date_published": "2026-10-13T00:00:00Z",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb"
    },
    {
      "id": "https://engineer.company/nb/notes/safari-underline-font-subset/",
      "url": "https://engineer.company/nb/notes/safari-underline-font-subset/",
      "title": "Understrekingen som krysset hver bokstav",
      "summary": "Safari trakk understrekingene våre gjennom hver underlengde. Årsaken var et kyrillisk delsett av fonten, på sider som aldri laster det.",
      "content_html": "<p>Vårt eget nettsted understreker hver lenke og lar nettleseren skjære ut et\nmellomrom der halen på en bokstav krysser linjen — <code>g</code>, <code>p</code>, kommaet. I Safari\ngjorde den ikke det. Linjen gikk rett gjennom dem. Slik hadde det vært på de\nengelske og danske sidene så lenge nettstedet hadde vært\nflerspråklig (feilen ingen melder: den ser bare billig ut).</p>\n<p>Årsaken var tre fontdeklarasjoner for et alfabet de sidene aldri laster ned.</p>\n<h2 id=\"hva-nettleseren-skal-gjøre\">Hva nettleseren skal gjøre</h2>\n\n<p><code>text-decoration-skip-ink</code> er egenskapen som løfter en understreking unna en\nunderlengde. Den er slått på som standard, og i årevis har svaret på\n«understrekingen min ser feil ut» vært å gripe til <code>text-underline-offset</code> og\nskyve linjen lenger ned. Det er feil refleks: å flytte linjen endrer designet,\nog det var mellomrommet som faktisk manglet.</p>\n<p>Vi fant to reelle årsaker før den interessante, og begge er verdt å kjenne til.</p>\n<ul>\n<li><strong>En <code>1px</code>-understreking får et mellomrom i <code>1px</code>-størrelse.</strong> Bruddet\nmotoren skjærer ut, står i forhold til tykkelsen på selve linjen. Stilarket\nvårt låste tykkelsen til én piksel, noe som ga rundt 0,6px luft på hver side\nav en stamme — teknisk sett et mellomrom, visuelt en linje rett gjennom\nbokstaven. Å sette <code>text-decoration-thickness: auto</code> skalerte begge sammen og\ntok mellomrommet fra 3,5px til 12,4px uten å flytte understrekingen en eneste\npiksel.</li>\n<li><strong><code>from-font</code> er ikke det trygge standardvalget det høres ut som.</strong> Det leser\ntykkelsen skrifttypen selv oppgir, og Ubuntu oppgir 0,056em for Light,\n0,020em for Medium og 0,120em for Bold — en familie der Medium-linjen er under\nhalvparten så kraftig som Light-linjen. I store størrelser blir det en linje\npå seks piksler ved siden av en på to piksler på samme side.</li>\n</ul>\n<p>Det løste Chrome. Safari trakk fortsatt linjen gjennom alt.</p>\n<h2 id=\"den-delen-som-tok-lengst-tid\">Den delen som tok lengst tid</h2>\n\n<p>Vi målte det ordentlig: styrte en ekte Safari, farget understrekingen om, tok\nskjermbilder i åttedobbel skala og telte pikslene der linjen stopper og starter.</p>\n<table>\n\t<thead>\n\t\t\t<tr>\n\t\t\t\t\t<th></th>\n\t\t\t\t\t<th>tegnet tykkelse</th>\n\t\t\t\t\t<th>mellomrom rundt en underlengde</th>\n\t\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t\t<tr>\n\t\t\t\t\t<td>Chrome</td>\n\t\t\t\t\t<td>2,00px</td>\n\t\t\t\t\t<td>5,8 / 12,5 / 6,3px</td>\n\t\t\t</tr>\n\t\t\t<tr>\n\t\t\t\t\t<td>Safari</td>\n\t\t\t\t\t<td>1,38px</td>\n\t\t\t\t\t<td>1,8 / 1,0 / 1,0px</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>Safari skar altså ut et mellomrom. Det var bare seks ganger smalere enn\nChromes, og i lesestørrelse er et mellomrom på én piksel ikke noe mellomrom.\nDet vanlige grepet gjorde det dessuten verre: med 3px tykkelse gikk Safari fra\ntre mellomrom til ett, med 4px til ingen, fordi den ikke skalerer mellomrommet\nmed linjen. Den låser det til omtrent 0,07em i alle størrelser, der Chrome gir\n0,6em.</p>\n<p>Vi prøvde rundt tjue deklarasjoner mot det — <code>skip-ink: all</code>, begge de eldre\nskrivemåtene av egenskapen, hver tykkelse fra en halv piksel til fire,\n<code>text-underline-position</code>, <code>font-smoothing</code>, <code>text-rendering</code>, <code>paint-order</code>.\nIngenting gjorde det bredere. Den ærlige konklusjonen på det tidspunktet var at\nWebKit rett og slett satte et tak på mellomrommet, og at det ikke var mer å\ngjøre fra et stilark.</p>\n<p>Den konklusjonen var feil, og det som veltet den, var\n<a href=\"https://github.com/fontsource/fontsource/issues/1096\">en feilrapport mot et fontprosjekt</a>:\nSafari ignorerer skip-ink når en skrifttype lastes med et ikke-latinsk delsett.</p>\n<h2 id=\"den-egentlige-årsaken\">Den egentlige årsaken</h2>\n\n<p>Nettstedet leverer Ubuntu i seks deler — tre vekter latinsk, tre kyrillisk —\nslik at en ukrainsk side settes i samme skrifttype som en engelsk i stedet for å\nfalle tilbake på det systemet tilbyr. Hver del oppgir hvilket tegnområde den\ndekker, og nettleseren laster bare ned de delene en side faktisk trenger.</p>\n<p>Alle seks var deklarert under det ene familienavnet, som er den opplagte måten å\nskrive det på. Og nettopp det er utløseren, registrert som\n<a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit-feil 255159</a>: <strong>når ett\nsnitt i en familie oppgir et ikke-latinsk tegnområde, svekker Safari skip-ink\nfor hvert tegn i den familien</strong> — også latinsk tekst på en side som aldri henter\nden andre filen.</p>\n<p>En engelsk side tegnet understrekingene sine dårlig fordi en ukrainsk\nfontdeklarasjon fantes et sted i stilarket. Å slette de tre linjene under\nkjøring, uten å endre noe annet, tok mellomrommene fra 1,8/1,0/1,0px til\n4,5/11,0/5,0px.</p>\n<h2 id=\"løsningen\">Løsningen</h2>\n\n<p>Gi det andre skriftsystemet sitt eget familienavn, og nevn det i fontstakken\netter det første:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-css\" data-lang=\"css\"><span class=\"line\"><span class=\"cl\"><span class=\"p\">@</span><span class=\"k\">font-face</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"nt\">font-family</span><span class=\"o\">:</span> <span class=\"nt\">UbuntuCyrillic</span><span class=\"o\">;</span>   <span class=\"c\">/* var: Ubuntu */</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"nt\">src</span><span class=\"o\">:</span> <span class=\"nt\">url</span><span class=\"o\">(</span><span class=\"nt\">ubuntu_300_cyrillic</span><span class=\"p\">.</span><span class=\"nc\">woff2</span><span class=\"o\">)</span> <span class=\"nt\">format</span><span class=\"o\">(</span><span class=\"s1\">&#39;woff2&#39;</span><span class=\"o\">);</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"nt\">unicode-range</span><span class=\"o\">:</span> <span class=\"nt\">U</span><span class=\"o\">+</span><span class=\"nt\">0400-045F</span><span class=\"o\">,</span> <span class=\"nt\">U</span><span class=\"o\">+</span><span class=\"nt\">0490-0491</span><span class=\"o\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span><span class=\"line\"><span class=\"cl\">\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">:</span><span class=\"nd\">root</span> <span class=\"p\">{</span>\n</span></span><span class=\"line\"><span class=\"cl\">    <span class=\"nv\">--body-font</span><span class=\"p\">:</span> <span class=\"n\">Ubuntu</span><span class=\"p\">,</span> <span class=\"n\">UbuntuCyrillic</span><span class=\"p\">,</span> <span class=\"n\">system-ui</span><span class=\"p\">,</span> <span class=\"kc\">sans-serif</span><span class=\"p\">;</span>\n</span></span><span class=\"line\"><span class=\"cl\"><span class=\"p\">}</span>\n</span></span></code></pre></div><p>En latinsk bokstav finnes i den første familien og når aldri den andre. En\nkyrillisk faller forbi den første, som ikke har en slik glyf, og havner i den\nandre i stedet for i en systemskrift. Tegnområdene bestemmer fortsatt hva som\nlastes ned, så ingenting ved innlastingen endres: engelske og danske sider\nhenter tre latinske filer og ingen kyrilliske, ukrainske sider det motsatte.</p>\n<p>Tre omdøpte deklarasjoner og én oppføring i stakken. Mellomrommene i Safari\ngikk til 4,5/11,0/5,0px, Chrome ble ikke berørt, understrekingen flyttet seg\nikke, og ukrainsk settes fortsatt i Ubuntu med identiske bredder.</p>\n<h2 id=\"det-vi-ville-sagt-til-den-neste\">Det vi ville sagt til den neste</h2>\n\n<ul>\n<li><strong>En skip-ink-måling gjelder bare for motoren som laget den.</strong> Vi hadde en\ntabell med tall, en riktig diagnose og en reell løsning, og alt sammen var\nChromium. Skjermbildet som gjenåpnet saken, kom fra et menneske som så på\nsiden.</li>\n<li><strong>Grip til tykkelsen før forskyvningen.</strong> Å gjøre mellomrommet bredere\nbeholder designet; å flytte linjen ned erstatter det.</li>\n<li><strong>Å dele en skrifttype etter skriftsystem er riktig, men delingen hører\nhjemme i familienavnet også.</strong> Ett felles navn på tvers av skriftsystemer\nleses penere og koster deg skip-ink i Safari. Å slå dem sammen igjen ser ut\nsom opprydding og er en regresjon.</li>\n<li><strong>Innlastingen var aldri problemet.</strong> Hvert instinkt sier at en side som\ntegnes dårlig på grunn av en kyrillisk font, må laste ned noe den ikke skal.\nDet gjorde den ikke. Deklarasjonen alene var nok.</li>\n</ul>\n<h2 id=\"kilder\">Kilder</h2>\n\n<ul>\n<li><a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit-feil 255159 — <code>text-decoration-skip-ink</code> og delsett av fonter</a></li>\n<li><a href=\"https://github.com/fontsource/fontsource/issues/1096\">Fontsource-sak 1096, som pekte ut utløseren</a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/text-decoration-skip-ink\">MDN: <code>text-decoration-skip-ink</code></a></li>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/unicode-range\">MDN: <code>unicode-range</code></a></li>\n</ul>\n",
      "date_published": "2026-09-28T00:00:00Z",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb"
    },
    {
      "id": "https://engineer.company/nb/notes/solana-mobile-wallet-deeplinks/",
      "url": "https://engineer.company/nb/notes/solana-mobile-wallet-deeplinks/",
      "title": "Å åpne en dApp i Solana-lommebøker på mobil",
      "summary": "Hvorfor browse-dyplenker til Phantom, Solflare og Backpack svikter på mobil, og de nøyaktige formatene, utløserreglene og rettelsene som får dem til å virke.",
      "content_html": "<p>En React-dApp bygd på <code>@solana/wallet-adapter-react</code> kobler seg til lommebøker\npå datamaskinen uten problemer, men på en telefon faller den samme flyten fra\nhverandre: lommeboken må åpne dApp-en i sin egen innebygde nettleser, og\ndyplenkene som skulle få det til, svikter i det stille. Backpack havner på en\n«last ned appen»-side; Solflare åpner appen, men aldri nettstedet; hver variant\nser ut til å svikte. Vi tok problemet fra hverandre, og det viste seg å være\nfire separate problemer med ett felles symptom.</p>\n<h2 id=\"de-fire-problemene\">De fire problemene</h2>\n\n<ul>\n<li><strong>Backpack-lenken var feil bygd.</strong> Det eneste dokumenterte formatet er\n<code>https://backpack.app/ul/v1/browse/&lt;url&gt;?ref=&lt;ref&gt;</code> — en universell lenke med\nmål-URL-en i stien og en påkrevd <code>ref</code>. Et gjettet eget skjema som\n<code>backpack://ul/v1/browse?url=...</code> treffer ingen rute appen registrerer, så\nbrukeren havner på installasjonssiden til lommeboken.</li>\n<li><strong>Solflare trenger også sin universelle lenke:</strong>\n<code>https://solflare.com/ul/v1/browse/&lt;url&gt;?ref=&lt;ref&gt;</code>, ikke det nakne\n<code>solflare://</code>-skjemaet. Et nakent skjema kan starte appen uten å rute den\nvidere — og det er nøyaktig «appen åpnes, men fanen med nettstedet må åpnes\nfor hånd».</li>\n<li><strong>Begge parameterne må kodes.</strong> <code>url</code> er den fullstendige, absolutte adressen\ntil dApp-en, og <code>ref</code> er origin for siden som sender forespørselen, begge sendt\ngjennom <code>encodeURIComponent</code>. Et ukodet <code>?</code> eller <code>&amp;</code> i målet ødelegger\ntolkningen, og lommeboken åpner på startskjermen sin i stedet for i\nnettleserfanen.</li>\n<li><strong>Utløseren betyr like mye som lenken.</strong> Universelle lenker bytter bare app\nved en navigasjon operativsystemet stoler på — og de gjør med vilje ingenting\nnår de limes inn i adressefeltet, som også er måten en helt korrekt lenke\n«svikter» på under testing.</li>\n</ul>\n<h2 id=\"de-dokumenterte-formatene\">De dokumenterte formatene</h2>\n\n<ul>\n<li>Phantom: <code>https://phantom.app/ul/browse/&lt;url&gt;?ref=&lt;ref&gt;</code> — ingen <code>/v1</code> i\nakkurat denne.</li>\n<li>Solflare: <code>https://solflare.com/ul/v1/browse/&lt;url&gt;?ref=&lt;ref&gt;</code></li>\n<li>Backpack: <code>https://backpack.app/ul/v1/browse/&lt;url&gt;?ref=&lt;ref&gt;</code></li>\n</ul>\n<p>Ett mønster dekker alle tre:</p>\n<pre tabindex=\"0\"><code>const WALLET_BROWSE = {\n  phantom: (url, ref) =&gt;\n    `https://phantom.app/ul/browse/${url}?ref=${ref}`,\n  solflare: (url, ref) =&gt;\n    `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,\n  backpack: (url, ref) =&gt;\n    `https://backpack.app/ul/v1/browse/${url}?ref=${ref}`,\n};\n\nfunction walletBrowseLink(\n  walletName,\n  targetUrl = window.location.href,\n) {\n  const build = WALLET_BROWSE[walletName.toLowerCase()];\n  if (!build) return null;\n  return build(\n    encodeURIComponent(targetUrl),\n    encodeURIComponent(window.location.origin),\n  );\n}\n</code></pre><h2 id=\"utløs-lenken-slik-at-ios-og-android-godtar-den\">Utløs lenken slik at iOS og Android godtar den</h2>\n\n<ul>\n<li><strong>Gjengi et ekte anker, beregnet på forhånd.</strong> Et vanlig\n<code>&lt;a href={walletBrowseLink('phantom')}&gt;</code> er den mest pålitelige utløseren på\nbegge plattformene.</li>\n<li><strong>Må det skje programmatisk</strong>, så tilordne <code>window.location.href</code> synkront\ninne i håndtereren for trykket — ingen <code>await</code>, ingen <code>fetch</code>, ingen\n<code>setTimeout</code> først. Etter asynkront arbeid er gestkonteksten borte, og iOS\nfaller tilbake til nettstedet til lommeboken. Aldri <code>window.open</code>.</li>\n<li><strong>Test aldri ved å lime inn i adressefeltet.</strong> Universelle lenker utløses med\nvilje ikke der; test med en lenke du trykker på, eller en QR-kode som kameraet\nskanner.</li>\n<li><strong>Pass på webvisningene i meldingsapper.</strong> Åpnet i den innebygde nettleseren\ntil Telegram eller Instagram blir universelle lenker ofte slukt, og det vanlige\nnettstedet til lommeboken lastes i stedet. Gjenkjenning ut fra user agent er i\nbeste fall heuristisk, så gi også brukerne en synlig nødutgang: «åpne i Safari\neller Chrome, og koble til etterpå».</li>\n</ul>\n<h2 id=\"den-større-løsningen-på-android\">Den større løsningen på Android</h2>\n\n<p>Håndlagde dyplenker er iOS-historien. På Android lar Solana Mobiles Mobile\nWallet Adapter en dApp som kjører i mobilnettleseren, koble seg rett til den\ninstallerte lommebokappen, helt uten omveien om en innebygd nettleser. Nyere\nversjoner av <code>@solana/wallet-adapter-react</code> registrerer mobiladapteren\nautomatisk, så en oppgradering av wallet-adapter-pakkene kan løse Android på\negen hånd. Målarkitekturen: Mobile Wallet Adapter på Android, universelle\nbrowse-lenker på iOS, der Apple ikke tillater noe tilsvarende.</p>\n<h2 id=\"verifisering-på-en-enhet\">Verifisering på en enhet</h2>\n\n<ol>\n<li>Ekte enhet, lommebok installert, lenken åpnet fra systemnettleseren — ikke\nfra en meldingsapp.</li>\n<li>Trykk på en gjengitt lenke eller skann en QR-kode; lim aldri inn i\nadressefeltet.</li>\n<li>Bekreft at lommeboken åpnes og at dApp-en lastes i nettleserfanen i appen —\nden andre halvdelen er den som svikter.</li>\n<li>Gjenta uten lommeboken installert: den universelle lenken skal falle tilbake\ntil nettstedet til lommeboken. Dukker den siden opp mens appen er\ninstallert, er lenken eller utløseren fortsatt feil.</li>\n<li>Test deretter veien via meldingsappen, og legg til «åpne i\nnettleseren»-hintet hvis den svikter der.</li>\n</ol>\n<h2 id=\"kilder\">Kilder</h2>\n\n<ul>\n<li><a href=\"https://docs.phantom.com/phantom-deeplinks/deeplinks-ios-and-android\">Phantom: dyplenker på iOS og Android</a></li>\n<li><a href=\"https://docs.solflare.com/solflare/technical/deeplinks/other-methods/browse\">Solflare: Browse-dyplenken</a></li>\n<li><a href=\"https://docs.backpack.app/deeplinks/other-methods/browse\">Backpack: Browse-dyplenken</a></li>\n<li><a href=\"https://docs.solanamobile.com/mobile-wallet-adapter/mobile-apps\">Solana Mobile: Mobile Wallet Adapter</a></li>\n</ul>\n",
      "date_published": "2026-08-10T00:00:00Z",
      "date_modified": "2026-10-18T04:44:42+02:00",
      "language": "nb"
    }
  ]
}