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.
{ "version": "https://jsonfeed.org/version/1.1", "title": "Engineer — Portefølje og noter", "home_page_url": "https://engineer.company/da/", "feed_url": "https://engineer.company/da/feed.json", "description": "Engineer ApS — softwareudvikling & it-rådgivning i København, Danmark. En portefølje af dokumenterede ingeniørresultater inden for software, data, cloud og it.", "language": "da", "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/da/" } ], "items": [ { "id": "https://engineer.company/da/portfolio/a-site-that-meets-the-accessibility-standard/", "url": "https://engineer.company/da/portfolio/a-site-that-meets-the-accessibility-standard/", "title": "Et site, der lever op til tilgængelighedsstandarden", "summary": "Overholdelse påstås på niveau AA på tre sprog og i to temaer, med ét AAA-kriterium derudover og ét AA-kriterium nævnt som en undtagelse, og hver påstand har et…", "content_html": "<p><strong>Situation.</strong> Et konsulenthus, der sælger ingeniørmæssig dømmekraft og leverer et utilgængeligt website, har et troværdighedsproblem, før det har et tilgængelighedsproblem. Sitet har også usædvanligt mange steder, hvor det kan gå galt: tre sprog, to farvetemaer, et fuldt printstylesheet, en palet med mørkt som udgangspunkt, håndtegnede annotationer og en illustreret figur — og hver eneste af dem kan få et kriterium til at fejle i én opsætning og bestå i en anden.</p>\n<p><strong>Opgave.</strong> Sitet skulle overholde WCAG 2.2 på niveau AA på alle sider, på alle sprog og i begge temaer, og overholdelsen skulle underbygges af tjek og ikke bare påstås i et dokument.</p>\n<p><strong>Handling.</strong> En konformitetsrapport dokumenterer hvert kriterium, der er omfattet, og hver række angiver, hvad der opfylder det, og hvad der tjekker det. Ét kriterium på niveau AAA er opfyldt ud over målet: visuel præsentation, fuldt ud. Fokusudseende tilbydes, men påstås ikke, fordi kontrasten over fotografiet ikke kan bevises. Automatiseringen kører en regelmotor for tilgængelighed med dens best practice‑regler slået til og ikke kun konformitetsreglerne, hvilket er en forskel på tredive regler og fik betydning med det samme: en af de ekstra regler fejlede, i det øjeblik den blev slået til, fordi bomærket lå uden for alle landmærker på alle undersider. Et andet og langsommere tjek kører toogtredive repræsentative sider i to farveskemaer og to bredder, og det, det efterprøver, er usædvanligt: fokus bevises i pixels og ikke i dokumentet, ved at trykke på tabulatortasten for alvor og sammenligne skærmbilleder, for identiske pixels betyder, at brugeren ikke kan se, hvor fokus er, uanset hvad markuppen siger. Det gennemgår også hele siden på jagt efter en tastaturfælde, anvender de tekstafstande, kriteriet foreskriver, fordobler rodelementets skriftstørrelse og måler linjelængde, lige margener, centrering og afsnitsafstand.</p>\n<p><strong>Resultat.</strong> Overholdelse påstås på niveau AA på tre sprog og i to temaer, med ét AAA‑kriterium derudover og ét AA‑kriterium nævnt som en undtagelse, og hver påstand har et tjek bag sig. Det sværeste var kontrasten over fotografiet, som en automatisk regelmotor melder ikke kan evalueres, og som sitet havde forsvaret med et argument og ikke et tal: de reelle tal var 49 af 64 kombinationer af side og tema under AA. To flader, som designet aldrig havde taget ansvar for, viste sig at være årsagen, og tjekket, der fandt dem, er nu en nedre grænse — hvert par skal holde AA over den væg, buildet tegner, og et par, der ikke gør det, får buildet til at fejle. Det, sitet skriver på sin egen kreditside, er den ærlige del: intet automatisk værktøj finder mere end omkring en tredjedel af WCAG‑fejlene, de etiketter, der dukker op ved hover, kan ikke lukkes med en tast, fordi det kræver et script, og sitet kører kun ét, og der er ikke testet med rigtige hjælpemidler — de begrænsninger står skrevet, hvor en læser vil se dem, og er ikke udeladt af påstanden.</p>\n", "date_published": "2026-10-25T09:11:16+01:00", "date_modified": "2026-10-25T09:11:16+01:00", "language": "da", "tags": [ "Designsystemer & UI", "Dokumentation", "Frontend‑udvikling", "Internationalisering", "Test & QA", "UX/UI‑design", "Webudvikling", "Internationalisering & lokalisering", "Parathed til AI‑agenter", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/made-a-geographical-data-pipeline-50x-faster/", "url": "https://engineer.company/da/portfolio/made-a-geographical-data-pipeline-50x-faster/", "title": "Har gjort geodatapipelines 50 gange hurtigere", "summary": "Gjorde en geografisk datapipeline 50 gange hurtigere med SQL- og datamodeloptimering i PostgreSQL, MS SQL og Google BigQuery.", "content_html": "<p><strong>Situation.</strong> Organisationen oplevede betydelige forsinkelser i behandlingen af store mængder geografiske data, og det gik ud over effektiviteten i de datadrevne beslutningsprocesser og den analytiske rapportering.</p>\n<p><strong>Opgave.</strong> Målet var at optimere pipelinen for geografiske data, så ydeevnen blev bedre og behandlingstiden kortere, og dataanalyserne kunne udføres mere effektivt og i realtid.</p>\n<p><strong>Handling.</strong> Den eksisterende SQL‑programmering og de eksisterende datamodeller i PostgreSQL, MS SQL og Google Cloud BigQuery blev analyseret minutiøst. Analysen afdækkede flaskehalse, der skyldtes ineffektive indekseringsstrategier, uhensigtsmæssigt opbyggede forespørgsler og mangel på passende constraints. Problemerne blev afhjulpet med følgende tiltag:</p>\n<ul>\n<li>Datamodellerne blev redesignet, så kritiske datasæt blev normaliseret, og der blev indført partitioneringsstrategier for at gøre datahentningen hurtigere.</li>\n<li>Der blev anvendt avancerede indekseringsteknikker, herunder B‑tree- og GiST‑indeks i PostgreSQL, filtrerede indeks i MS SQL og clustering i BigQuery.</li>\n<li>Komplekse SQL‑forespørgsler blev optimeret ved at omskrive subqueries, skære ned på antallet af joins og tage materialiserede views i brug, hvor det var relevant.</li>\n<li>Der blev indført integritetsregler som foreign keys og check‑constraints, så dataene forblev konsistente, uden at det gik ud over ydeevnen.</li>\n</ul>\n<p><strong>Resultat.</strong> De omfattende optimeringer gjorde pipelinen for geografiske data 50 gange hurtigere og forkortede databehandlingstiden markant. Forbedringen gjorde realtidsanalyser mulige, styrkede rapporteringen og gav interessenterne rettidig, datadrevet indsigt, som i sidste ende bidrog til bedre underbyggede strategiske beslutninger.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Data engineering", "Data governance", "Data warehousing", "Databaser", "Datapipelines (ETL/ELT)", "GIS / geodata", "Optimering af ydeevne", "PostgreSQL", "SQL", "Databaseoptimering", "Design af data warehouse", "GIS & geospatiale løsninger", "Klargøring af data til AI", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/designed-and-ran-6-etl-elt-pipelines/", "url": "https://engineer.company/da/portfolio/designed-and-ran-6-etl-elt-pipelines/", "title": "Har designet og drevet 6 ETL/ELT‑pipelines", "summary": "Designede og driftede 6 ETL/ELT-pipelines (BigQuery, MSSQL, PostgreSQL) — et manuelt job på 3 timer blev til under 20 minutter med daglige synkroniseringer.", "content_html": "<p><strong>Situation.</strong> Organisationen havde brug for at samle og behandle store mængder strukturerede data fra et data warehouse på en fjernserver bag en IPsec VPN. Dataene var afgørende for interne analysedashboards og for eksterne Python‑API’er, som kunder og partnere brugte.</p>\n<p><strong>Opgave.</strong> Målet var at designe, implementere og vedligeholde et sæt robuste, automatiserede ETL/ELT‑pipelines, der sikkert kunne hente, transformere og indlæse data i Google BigQuery og samtidig sikre datanøjagtighed, ydeevne og skalerbarhed i alle systemerne.</p>\n<p><strong>Handling.</strong> Der blev designet, implementeret og administreret 6 ETL/ELT‑pipelines fra ende til anden, som omfattede en række teknologier, herunder Google BigQuery, MSSQL, PostgreSQL, shell‑scripting, PL/pgSQL og Transact‑SQL.</p>\n<ul>\n<li>Der blev etableret sikre forbindelser til en fjernserver beskyttet af en IPsec VPN, så datahentningen kunne automatiseres.</li>\n<li>Download af komprimerede dataarkiver med Parquet-, CSV- og .bak‑filer blev planlagt og afviklet.</li>\n<li>Der blev udviklet Bash- og Python‑scripts, som udpakkede filerne og klassificerede dem efter type og skema.</li>\n<li>For .bak‑filernes vedkommende blev sikkerhedskopierne gendannet i en lokal MSSQL Server‑instans med RESTORE DATABASE‑arbejdsgange, og skemaintegriteten blev valideret.</li>\n<li>Strukturerede data blev indlæst i staging‑skemaer i MSSQL og PostgreSQL med værktøjerne bcp, psql og SSIS, afhængigt af kildeformatet.</li>\n<li>Der blev skrevet modulære, genanvendelige PL/pgSQL- og T‑SQL‑procedurer, som rensede, normaliserede og berigede dataene ud fra forretningslogikken.</li>\n<li>Kuraterede datasæt og tabeller blev eksporteret til mellemformater, komprimeret med gzip og overført sikkert til en Google Cloud Storage‑bucket.</li>\n<li>Upload og skemamapping til Google BigQuery blev automatiseret med bq CLI og Python‑baserede scripts til dataindlæsning.</li>\n</ul>\n<p><strong>Resultat.</strong> De automatiserede pipelines reducerede det manuelle arbejde og behandlingstiden markant — fra over 3 timers manuelt arbejde til under 20 minutter fra ende til anden — og dataene blev synkroniseret dagligt i stedet for ugentligt. Python‑API’erne, der brugte dataene, fik 30 % bedre ydeevne, og det bedre overblik hjalp forretningsanalytikerne med hurtigere at levere indsigt til interessenterne. Løsningen er fortsat skalerbar og kan udvides med nye datakilder, efterhånden som forretningen vokser.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "Data engineering", "Data warehousing", "Databaser", "Datapipelines (ETL/ELT)", "Netværk & VPN", "PostgreSQL", "Python", "SQL", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Design af data warehouse", "Klargøring af data til AI", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/wrote-1000-rbac-rules-to-secure-linux-servers/", "url": "https://engineer.company/da/portfolio/wrote-1000-rbac-rules-to-secure-linux-servers/", "title": "Har skrevet 1.000 RBAC‑regler til Linux‑servere", "summary": "Skærpede sikkerheden med 1.000 RBAC-regler for udviklere, apps og Linux/DB-servere — 85 % lavere risiko for uautoriseret adgang, automatiseret med Ansible.", "content_html": "<p><strong>Situation.</strong> Organisationen havde brug for at stramme adgangskontrollen i flere systemer, herunder udviklermiljøer, applikationsinstanser, PostgreSQL, MS SQL og Linux‑servere. Selvom den underliggende RBAC‑model var enkel, lå udfordringen i at håndtere over 1.000 individuelle regler, der skulle dække forskellige brugerroller og systemkrav. Målet var at sikre strenge adgangsbegrænsninger uden at gøre løsningen mere kompleks.</p>\n<p><strong>Opgave.</strong> At implementere en skalerbar RBAC‑løsning ved at definere og håndhæve over 1.000 regler for adgangskontrol. Det indebar at knytte rettigheder til bestemte roller (f.eks. udviklere, applikationsinstanser og databaseadministratorer) og at sikre, at reglerne blev anvendt ensartet i alle systemer. Opgaven krævede også, at reglerne blev dokumenteret, og at udrulningen blev automatiseret for at undgå manuelle fejl.</p>\n<p><strong>Handling.</strong> Tilgangen gik ud på at skabe en enkel, modulær RBAC‑struktur, hvor rettighederne blev opdelt i klare, genanvendelige kategorier (f.eks. “skrivebeskyttet adgang til produktionsdatabaser”). Konfigurationen af hver regel blev automatiseret med Ansible, så den var ensartet i alle miljøer. Udviklere fik f.eks. kun adgang til de servere, der var tildelt dem, mens applikationsinstanserne havde begrænsede rettigheder for at forhindre lateral bevægelse. Processen prioriterede klarhed frem for kompleksitet, og hver regel var eksplicit knyttet til en bestemt rolle og et bestemt system.</p>\n<p><strong>Resultat.</strong> Med implementeringen kom over 1.000 regler på plads uden unødvendig kompleksitet, og risikoen for uautoriseret adgang faldt med 85 %. Automatiseringen gjorde udrulningen enklere og forkortede opsætningstiden med 60 % i forhold til manuel opsætning. Takket være den dokumenterede struktur kunne teamene hurtigt gennemgå eller ændre reglerne, så løsningen kunne skalere i takt med infrastrukturen. Med fokus på enkelhed og volumen gav løsningen en robust sikkerhed, uden at det gik ud over driftseffektiviteten.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Databaser", "DevOps", "Dokumentation", "Infrastruktur", "Linux & servere", "Sikkerhed", "Systemadministration", "Databaseadministration (DBA)", "Infrastructure as Code", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/led-access-management-across-m365-and-sso/", "url": "https://engineer.company/da/portfolio/led-access-management-across-m365-and-sso/", "title": "Har ledet adgangsstyring med M365 og SSO", "summary": "Ledede adgangsstyring med M365, 1Password, Red Hat SSO og OKTA — markant lavere risiko for uautoriseret adgang og sporbar adgang.", "content_html": "<p><strong>Situation.</strong> Adgangen til systemer og tjenester i virksomheden blev håndteret uensartet — rettigheder var tildelt ad hoc, over tid og af forskellige personer. Det er et dobbelt problem: det åbner for adgang, som ingen havde tilsigtet, og det gør revision næsten umulig, fordi ingen reelt kan sige, hvem der har adgang til hvad, eller hvorfor.</p>\n<p><strong>Opgave.</strong> Målet var at lukke sikkerhedshullet ved at centralisere og stramme adgangsstyringen i hele organisationen.</p>\n<p><strong>Handling.</strong> Omlægningen samlede identiteterne og adgangene i M365, 1Password, Red Hat SSO og OKTA SSO, så der kom ét sammenhængende overblik i stedet for spredte rettigheder i hvert enkelt system. Least privilege blev håndhævet — personer og systemer fik præcis det, de havde brug for, og intet derudover — og on- og offboarding blev standardiseret, så adgang blev givet og, lige så vigtigt, frataget hurtigt og ensartet i stedet for at blive hængende, efter at nogen var gået videre.</p>\n<p><strong>Resultat.</strong> Risikoen for uautoriseret adgang faldt markant, og adgangen blev ensartet og til at revidere — endelig kunne man svare på spørgsmålet “hvem har adgang til hvad, og hvorfor?” Det overraskende var, at det også gjorde hverdagen enklere for teamet: de rigtige døre gik let op, og de forkerte forblev lukkede, og sådan føles god adgangsstyring faktisk.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "Infrastruktur", "Sikkerhed", "Systemadministration", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/designed-a-json-passthrough-from-postgres-to-api/", "url": "https://engineer.company/da/portfolio/designed-a-json-passthrough-from-postgres-to-api/", "title": "Har designet en JSON‑passthrough‑arkitektur", "summary": "Designede en JSON-passthrough, hvor PostgreSQL-funktioner returnerer færdig JSON, som Go-API'et videresender uændret — frontenden afkoblet fra skemaet.", "content_html": "<p><strong>Situation.</strong> Normalt kommer data fra en database til en browser gennem et stafetløb af transformationer. Databasen giver API’et rækker, API’et unmarshaller dem til structs, omformer dem, serialiserer dem tilbage til JSON, og først derefter bliver de sendt ud. Hvert af de led er kode, man skriver, kode, man tester, og endnu et sted, hvor API’ets opfattelse af dataene og databasens kan glide fra hinanden.</p>\n<p><strong>Opgave.</strong> Idéen var at springe stafetløbet over. Hvis databasen kunne returnere det færdige svar, kunne API’et bare sende det videre, og frontenden kunne bygge direkte på databasens struktur i stedet for på en håndvedligeholdt kopi af den i Go.</p>\n<p><strong>Handling.</strong> Så det blev bygget som en ren passthrough. PostgreSQL‑funktionerne samler hele svaret som JSON — formgivningen er en SQL‑opgave og sker der, hvor dataene allerede ligger. Go‑handleren tager imod det som json.RawMessage og sender det videre uændret; den pakker det aldrig ud og koder det aldrig om. En lille QueryJSON‑helper gjorde mønstret til den letteste vej i stedet for noget, man skulle huske at gøre. Det, der faldt bort, var alt det mellemliggende maskineri, som et konventionelt lagdelt API samler på sig — DTO’erne, mapperne, svar‑structene.</p>\n<p><strong>Resultat.</strong> Handler‑koden blev markant kortere, og hvad der vejer tungere: frontenden holdt op med at være koblet til Go. Ændrer man, hvad en funktion returnerer, løber den nye struktur direkte igennem til klienten, uden at nogen retter en linje handler‑kode. Færre bevægelige dele, og en hel kategori af afvigelser mellem lagene findes simpelthen ikke her.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Databaser", "Optimering af ydeevne", "Platformarkitektur", "PostgreSQL", "SQL", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Platform- & løsningsarkitektur", "Værktøjer til AI‑agenter & MCP‑servere" ] }, { "id": "https://engineer.company/da/portfolio/migrated-the-api-to-huma-v2-at-100-parity/", "url": "https://engineer.company/da/portfolio/migrated-the-api-to-huma-v2-at-100-parity/", "title": "Har migreret API'et til Huma v2 med 100 % paritet", "summary": "Migrerede HTTP-API'et fra Fiber til Huma v2 — 649 paths, 760 operationer — 100 % paritet mellem registrerede ruter og den udgivne OpenAPI-beskrivelse.", "content_html": "<p><strong>Situation.</strong> API’et begyndte sit liv på Fiber med request‑validering skrevet i hånden, endpoint for endpoint. Det går fint, så længe der er en håndfuld endpoints. Det holder op med at gå, efterhånden som API’et vokser: den håndskrevne validering bliver en løbende vedligeholdelsesbyrde, og små inkonsistenser sniger sig ind, fordi hvert endpoints tjek er sit eget lille særtilfælde. Og der fandtes ingen samlet beskrivelse af API’ets struktur.</p>\n<p><strong>Opgave.</strong> Målet var validering, der kom fra typerne i stedet for fra håndskrevne tjek, og en egentlig kontrakt, der beskrev API’et — uden at sætte alt andet på pause for en big bang‑omskrivning.</p>\n<p><strong>Handling.</strong> HTTP‑laget blev flyttet over på Huma v2, der ligger oven på Fiber, så den eksisterende runtime blev bevaret. Hvert endpoint får input- og output‑structs, og Huma genererer request‑valideringen og responsmodelleringen ud fra de typer. En OpenAPI‑beskrivelse følger med gratis, hvilket betyder, at dokumentationen følger koden i stedet for at rådne i en wiki. Alt nyt blev skrevet mod Huma, og de eksisterende ruter blev migreret over, så præcis to endpoints er tilbage på rå Fiber — WebSocket‑endpointene, hvor man reelt har brug for selve socketten, og hvor Humas request/response‑model ikke passer. I dag er det vokset til 649 paths med 760 operationer og et tjek i CI, som sammenligner de ruter, serveren faktisk registrerer, med dem, OpenAPI‑beskrivelsen oplyser. Pariteten er 100 %, og den bliver der, fordi en rute, der ikke er beskrevet, får buildet til at fejle.</p>\n<p><strong>Resultat.</strong> Nye endpoints får validering og opdateret dokumentation, uden at nogen skal gøre en ekstra indsats for det, og en hel kategori af fejl i request‑håndteringen — “nå ja, vi glemte at tjekke det felt her”-slagsen — forsvandt. Med 760 operationer er beskrivelsen den eneste praktiske måde at læse API’et på, så garantien for, at den er komplet, betyder mere, end den gjorde ved halvtreds. Den typede kontrakt gjorde API’et både sikrere at ændre og lettere at overdrage til andre, for typerne viser, hvad et endpoint forventer.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Dokumentation", "Migrering & modernisering", "Platformarkitektur", "Test & QA", "Backend- & API‑udvikling", "Platform- & løsningsarkitektur", "Teknisk dokumentation", "Værktøjer til AI‑agenter & MCP‑servere" ] }, { "id": "https://engineer.company/da/portfolio/generated-the-api-contract-from-the-database-out/", "url": "https://engineer.company/da/portfolio/generated-the-api-contract-from-the-database-out/", "title": "Har genereret API‑kontrakten ud fra databasen", "summary": "Genererede API-kontrakten ud fra databasen — OpenAPI, en typet TypeScript-klient, mock-handlers og UI-grænser — med en kontrol i hvert led.", "content_html": "<p><strong>Situation.</strong> Frontend og backend bevæger sig i deres eget tempo, og deres antagelser om en request‑payload kan glide fra hinanden, uden at nogen bemærker det. Som regel opdager man det ved et 422 i browseren — når uoverensstemmelsen allerede er sendt i produktion, hvilket er det dyreste tidspunkt at finde ud af det på. At skrive de to sider i hånden ud fra det samme dokument løser ikke problemet; det flytter bare afvigelsen over på den, der glemte at læse dokumentet igen.</p>\n<p><strong>Opgave.</strong> De to sider skulle genereres fra én kilde i stedet for at blive aftalt mellem to, og hvert trin i genereringen skulle kontrolleres i stedet for at blive taget for givet.</p>\n<p><strong>Handling.</strong> Kæden starter ved databasen og løber udad. Skemaet og dets funktioner definerer datastrukturerne; Go‑typerne definerer API’et; Huma genererer OpenAPI‑beskrivelsen ud fra dem; en typet TypeScript‑klient på 44.076 linjer genereres ud fra den beskrivelse; 61 mock‑handlere genereres sammen med den, så frontendens egne tests kører mod den rigtige kontrakt i stedet for mod en håndskrevet fixture; og de grænser, brugerfladen håndhæver i en formular, kommer fra samme sted i stedet for at blive tastet ind i en validator igen. Hvert led har sin kontrol. Et kontrakttjek i CI sammenligner det, frontenden sender, med det, API’et forventer, og får buildet til at fejle ved afvigelser, og under det ligger en skemaprobe, der tjekker de faktiske strukturer og ikke en beskrivelse af dem. Kontrollen dækker bevidst de steder, hvor afvigelser gerne gemmer sig: valgfrie body‑felter, hvor “mangler” og “null” bliver forvekslet, og query‑parameter‑enums, hvor de to sider i al stilhed kan være uenige om de tilladte værdier.</p>\n<p><strong>Resultat.</strong> Et felt kan ikke ændre sig på kun den ene side — ændringen fejler ved det første led, der opdager den, i et build, minutter efter ændringen. Det fjernede en tilbagevendende og virkelig irriterende fejlkategori, den slags, der er usynlig i code review og først viser sig i runtime. Prisen er et genereringstrin midt i det hele: at regenerere er en sur pligt, og kæden er kun så troværdig som sit dårligst bevogtede led, hvilket er grunden til, at hvert led fik sin kontrol.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "API'er & integration", "Automatisering & CI/CD", "Backend‑udvikling", "DevOps", "Driftssikkerhed & backup", "Test & QA", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Værktøjer til AI‑agenter & MCP‑servere" ] }, { "id": "https://engineer.company/da/portfolio/modeled-348-tables-across-34-postgres-schemas/", "url": "https://engineer.company/da/portfolio/modeled-348-tables-across-34-postgres-schemas/", "title": "Har modelleret 348 tabeller i 34 Postgres‑skemaer", "summary": "Modellerede det maritime domæne i 348 normaliserede tabeller fordelt på 34 PostgreSQL-skemaer — SMALLINT-opslag, UUID v7-nøgler og én navnekonvention.", "content_html": "<p><strong>Situation.</strong> Kernen i platformen er et tæt maritimt domæne — professionelle, virksomheder, skibe, jobs, anmeldelser — og de refererer konstant til hinanden. En professionel sejler på skibe, arbejder for virksomheder og skriver anmeldelser; en virksomhed ejer skibe og slår jobs op. Næsten alle funktioner er databaseforespørgsler på tværs af det net, så hvor godt dataene er modelleret, afgør, hvor godt det meste af appen klarer sig, og hvor fornuftigt det er at udvide den.</p>\n<p><strong>Opgave.</strong> Det domæne skulle modelleres, så det forblev hurtigt og bevarede sin integritet, og så den næste entitetstype kunne tilføjes uden at skulle slås med skemaet.</p>\n<p><strong>Handling.</strong> Det er bygget op som normaliserede PostgreSQL‑skemaer, organiseret efter domæne — efterhånden 348 tabeller fordelt på 34 skemaer. De mange små, stabile enumerationer — statusser, typer, kategorier — blev til SMALLINT‑opslagstabeller, hvilket holder rækkerne kompakte og joins billige, i stedet for at der gemmes tekstkoder overalt. Entiteterne får UUID v7‑primærnøgler, så de er globalt unikke, men stadig tidsordnede i indekset. Én navnekonvention går igen overalt — tabelnavne i flertal inde i skemaer med navne i ental — og den anvendes uden undtagelse, hvilket som en ekstra gevinst undgår en masse kollisioner med reserverede ord. Og relationerne bæres af rigtige foreign keys og constraints, så integriteten er databasens opgave og ikke noget, applikationen skal huske.</p>\n<p><strong>Resultat.</strong> Resultatet er en datamodel, der er konsistent og hurtig og forudsigelig at arbejde i, fordi de samme regler gælder overalt — der er ingen særtilfælde at lære udenad. En ny funktion betyder som regel, at skemaet udvides i samme retning som det eksisterende og ikke imod det, og det er den eneste grund til, at 34 skemaer er en struktur og ikke et vildnis. Det er fundamentet, som resten af platformen står på.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Data engineering", "Data governance", "Databaser", "Optimering af ydeevne", "Platformarkitektur", "PostgreSQL", "SQL", "Data governance & datakvalitet", "Databasedesign & datamodellering", "Klargøring af data til AI", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/built-a-268-check-guard-engine-for-the-repo/", "url": "https://engineer.company/da/portfolio/built-a-268-check-guard-engine-for-the-repo/", "title": "Har bygget en kontrolmotor med 268 tjek", "summary": "Byggede en kontrolmotor med 268 registrerede commit-tjek, 277 lint-regler og 15 egne ESLint-regler — plus 146 tests af selve tjekkene.", "content_html": "<p><strong>Situation.</strong> Standarder, der er skrevet ned i en contributing‑guide, er forslag. Alle er enige i dem, og så er det fredag, ændringen er lille, og guiden taber. Kravet om nul advarsler kunne kun holde, hvis noget andet end god vilje holdt det oppe — og de lintere, der følger med hvert sprog, når langtfra frem til de projektspecifikke regler, der faktisk betyder noget, dem, der handler om, hvordan netop denne kodebase er tænkt til at fungere.</p>\n<p><strong>Opgave.</strong> De regler, projektet gik op i, skulle kunne eksekveres, så et brud på én af dem fik committen til at fejle i stedet for at vente på en reviewer med tid og hukommelse nok til at opdage det.</p>\n<p><strong>Handling.</strong> Ud af det voksede en kontrolmotor. Der er 274 check‑scripts, hvoraf 268 er registreret i commit‑hooks, foruden 277 JavaScript‑lintere og 96 shell- og 17 Python‑validatorer, der dækker de ting, som standardværktøjer ikke har nogen holdning til — at en migration kan rulles tilbage, at en oversættelsesnøgle findes i begge locales, at en registreret rute optræder i OpenAPI‑beskrivelsen, at ingen i det stille har slået en regel fra direkte i koden. Femten egne ESLint‑regler håndhæver husets mønstre i TypeScript. Én regel er værd at nævne for sig: enhver commit med præfikset fix: skal have en test med, der fejler uden den, så en fejl, der er rettet, forbliver rettet. Og fordi en ødelagt kontrol er værre end slet ingen kontrol — den lader alt passere, og ingen opdager det — har kontrollerne selv 146 test.</p>\n<p><strong>Resultat.</strong> Review‑tiden flyttede fra mekanik til design, fordi de mekaniske indvendinger allerede var fremsat af en maskine, før branchen blev pushet. Bagsiden er reel og værd at sige højt: det er langsomt at committe, og en dårligt skrevet kontrol er virkelig frustrerende at arbejde uden om. De 146 test findes, fordi netop det skete.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Teknisk ledelse", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk dokumentation", "Teknisk ledelse & rådgivning", "Udvikling af skræddersyede AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/moved-slow-work-onto-a-river-job-queue/", "url": "https://engineer.company/da/portfolio/moved-slow-work-onto-a-river-job-queue/", "title": "Har flyttet langsomme opgaver til en River‑jobkø", "summary": "Flyttede langsomt arbejde væk fra request-stien over på en River-jobkø: 15 worker-moduler, 8 planlagte opgaver og 20 pg_cron-vedligeholdelsesjobs.", "content_html": "<p><strong>Situation.</strong> Noget arbejde har ingen gang på jorden, mens en bruger venter. At sende mail, at genopbygge et søgeindeks, at generere et dokument, at genberegne placeringer — gør man noget af det inde i requesten, kigger brugeren på en spinner for noget, vedkommende aldrig bad om at se. Lægger man det i stedet i en goroutine, forsvinder det i det øjeblik, processen genstarter — og det vil den gøre, midt i en udrulning, uden spor af, at det nogensinde skulle være sket.</p>\n<p><strong>Opgave.</strong> Baggrundsarbejdet havde brug for et holdbart sted at ligge: en kø, der overlever en genstart, prøver igen ved fejl og kan undersøges, når noget ikke er sket.</p>\n<p><strong>Handling.</strong> Valget faldt på River, i høj grad fordi den har sin kø i PostgreSQL — databasen er i forvejen det autoritative register, så et job og de rækker, det rører, committer eller ruller tilbage sammen, og der er ingen ekstra infrastruktur at drive og forholde sig til. Bag den ligger 15 worker‑moduler og 8 planlagte opgaver. Nedenunder står 20 pg_cron‑jobs for den vedligeholdelse, som databasen er bedre egnet til at klare selv: at rydde gamle partitioner op, at rotere salts, at opdatere aggregater. Alt, der var langsomt nok til at blive bemærket, blev flyttet væk fra request‑stien og over på en af de to.</p>\n<p><strong>Resultat.</strong> Requests svarer hurtigt, og det langsomme arbejde bliver stadig gjort færdigt, med nye forsøg og en synlig historik, når det ikke lykkes. At holde køen i Postgres frem for i en dedikeret broker er en bevidst begrænsning: det skalerer ikke i det uendelige, og ved en vis mængde bliver det det forkerte svar. For en platform, hvis flaskehals alligevel er databasen, var én bevægelig del mindre mere værd end ekstra kapacitet, der ikke ville blive brugt.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Backend‑udvikling", "Databaser", "Driftssikkerhed & backup", "Optimering af ydeevne", "PostgreSQL", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Site reliability & overvågning", "Værktøjer til AI‑agenter & MCP‑servere" ] }, { "id": "https://engineer.company/da/portfolio/built-first-party-error-monitoring-and-tracing/", "url": "https://engineer.company/da/portfolio/built-first-party-error-monitoring-and-tracing/", "title": "Har bygget egen fejlovervågning og tracing", "summary": "Byggede egen fejlovervågning og OpenTelemetry-tracing — sanitering, detektion af spikes, symbolication og et heartbeat — bag 11 operatørvisninger.", "content_html": "<p><strong>Situation.</strong> Forskellen på en platform, der er oppe, og en platform, der virker, er, om nogen overhovedet ville opdage det. En fejl, en bruger rammer klokken elleve om aftenen, på en side, som ingen tester, er usynlig, medmindre noget går ud og samler den op. Det sædvanlige svar er at købe en hostet error tracker, og det er et godt svar — men det betyder også, at platformens egne fejl, stack traces og brugerkontekst ender hos en tredjepart.</p>\n<p><strong>Opgave.</strong> Fejl og traces skulle samles, grupperes og gøres til noget, man kunne handle på, uden at platformens indre forlod platformen.</p>\n<p><strong>Handling.</strong> Der blev bygget to dele. OpenTelemetry står for tracing over OTLP, så en langsom request kan følges på tværs af frontenden, API’et og databasen, i stedet for at man skal gætte. Ved siden af ligger en hjemmebygget fejlpipeline — en errmon‑service og en ingest‑service — der renser payloads før lagring, grupperer fejl i tilbagevendende problemer og ikke i en flad liste, opdager spikes og regressioner med en cooldown, så én dårlig udrulning ikke kalder nogen ud fyrre gange, oversætter minificerede frontend‑stack traces tilbage til læsbar kode og kører et syntetisk heartbeat for at bevise, at selve pipelinen er i live. Det hele lander i databasen som error events, error groups, en inbox, API‑latens og stack‑samples, og det kommer frem gennem 11 operatørvisninger, heriblandt én til service level objectives.</p>\n<p><strong>Resultat.</strong> Fejl bliver til en liste, nogen kan arbejde sig igennem, og en regression melder sig selv i stedet for at blive opdaget af en bruger. At bygge selv i stedet for at købe kostede reel tid og betyder, at det er én ting mere at vedligeholde — en købt tracker ville have kørt samme eftermiddag. Gevinsten var, at intet følsomt forlader platformen, og at alarmreglerne passer til netop denne platform og ikke til en generisk.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Backend‑udvikling", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Overvågning & observerbarhed", "Sikkerhed", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Site reliability & overvågning", "Udvikling af skræddersyede AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/1022-migrations-behind-a-schema-drift-gate/", "url": "https://engineer.company/da/portfolio/1022-migrations-behind-a-schema-drift-gate/", "title": "Har holdt 1.022 migrationer fri for skemadrift", "summary": "Holdt skemaet konsistent gennem 1.022 migrationer med en CI-gate, der bygger databasen begge veje og fejler, når de to ikke stemmer overens.", "content_html": "<p><strong>Situation.</strong> Et skema er beskrevet to gange i de fleste projekter: én gang af den indledende opsætning, der bygger det fra bunden, og én gang af de ophobede migrationer, der har fået det til at vokse. Begge skal give den samme database. Intet tjekker, at de gør det, så de glider fra hinanden — og afvigelsen er usynlig, indtil et nyopsat miljø opfører sig anderledes end produktion, som regel på det værst tænkelige tidspunkt.</p>\n<p><strong>Opgave.</strong> De to beskrivelser skulle automatisk og beviseligt være identiske, i stedet for at man en gang imellem bare troede, at de var det.</p>\n<p><strong>Handling.</strong> Databasen er versioneret som 1.022 migrationer, nummereret fra 036 til 1102, og rækkefølgedisciplinen omkring dem er kedelig og ikke til forhandling. Det, der får den til at holde, er et CI‑job, der bygger databasen to gange ved hver ændring: én gang fra et helt nyt startskema, én gang fra startskemaet plus hver eneste migration afspillet i rækkefølge — og så sammenligner de to. Ikke bare strukturen, som er den nemme halvdel, men også de seedede data, for en migration, der backfiller en opslagstabel forkert, er præcis lige så skadelig som en, der glemmer en kolonne, og kun den ene af dem dukker op i en skema‑diff. Enhver afvigelse får buildet til at fejle, og forskellen bliver skrevet ud.</p>\n<p><strong>Resultat.</strong> Et nyopsat miljø og et, der har kørt længe, er den samme database, og det bliver kontrolleret, ikke antaget. Prisen lander hos den, der skriver en migration: den skal virke, når den afspilles, og den skal virke fra bunden, hvilket kræver mere omtanke, end et hurtigt ALTER som regel får. Det er netop pointen — alternativet er at finde ud af det under en gendannelse, hvor svaret betyder noget, og der ikke er tid til at regne det ud.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Data governance", "Databaser", "DevOps", "Driftssikkerhed & backup", "Migrering & modernisering", "PostgreSQL", "Test & QA", "Databaseadministration (DBA)", "Databasemigrering & modernisering", "DevOps & CI/CD‑automatisering", "Klargøring af data til AI" ] }, { "id": "https://engineer.company/da/portfolio/built-fail-closed-rate-limiting-and-anti-abuse/", "url": "https://engineer.company/da/portfolio/built-fail-closed-rate-limiting-and-anti-abuse/", "title": "Har bygget fail‑closed rate limiting mod misbrug", "summary": "Byggede fail-closed misbrugskontroller: 22 Redis-baserede rate limiters, Cloudflare Turnstile, idempotens på requests og en origin-lås ved indgangen.", "content_html": "<p><strong>Situation.</strong> Et offentligt katalog over virksomheder og fagfolk er et mål fra den dag, det går i luften. Scrapere vil have dataene, spamkonti vil have rækkevidden, og et endpoint, der koster platformen rigtige penge at levere — søgning, eksport, alt, der rører et eksternt API — er værd at misbruge alene, fordi det er gratis at kalde. Intet af det er ondskab rettet mod netop denne platform; det er bare hverdagsvejr på det åbne internet.</p>\n<p><strong>Opgave.</strong> De dyre stier og dem, der inviterer til misbrug, havde brug for grænser, der holder under pres — også det pres, der opstår, når limiterens egen afhængighed er nede.</p>\n<p><strong>Handling.</strong> Der er 22 rate limitere, hver bygget til den sti, den beskytter, i stedet for ét globalt loft, fordi et loginforsøg, en søgning og en bulkeksport bliver til misbrug ved vidt forskellige hastigheder. Tilstanden ligger i Redis, så en grænse deles på tværs af instanser og ikke gælder pr. proces, hvor den er let at slippe uden om. Den vigtige beslutning er, hvad der sker, når Redis ikke er der: limiterne er fail‑closed. Trafik afvises frem for at blive vinket igennem, hvilket er det mindre bekvemme svar og det eneste forsvarlige. Omkring dem sidder Cloudflare Turnstile på de stier, der er værd at udfordre, 351 linjers idempotency‑middleware, så en skrivning, der bliver forsøgt igen, ikke bliver til to, en origin‑lås, der afviser requests, som ikke kommer ind ad hoveddøren, og en skræddersyet challenge på selve kataloget.</p>\n<p><strong>Resultat.</strong> Misbrug bliver dyrt for den, der misbruger, og billigt for platformen, og et udfald i limiterens eget lager ender i afvisning og ikke i en åben dør. At være fail‑closed betyder ganske vist, at et Redis‑problem bliver et problem, brugeren kan se — det er accepteret med vilje, fordi alternativet er, at et Redis‑problem bliver et problem på regningen.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Driftssikkerhed & backup", "Netværk & VPN", "Optimering af ydeevne", "Sikkerhed", "Backend- & API‑udvikling", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/built-the-operator-back-office-for-the-platform/", "url": "https://engineer.company/da/portfolio/built-the-operator-back-office-for-the-platform/", "title": "Har bygget platformens operatør‑back‑office", "summary": "Byggede operatørernes back-office — omkring 40 admin-ruter og 128 komponenter til ejerskabskrav, moderation, feature flags, cache og diagnostik.", "content_html": "<p><strong>Situation.</strong> Enhver platform får stille og roligt en applikation nummer to, og det er som regel den, ingen planlægger. Nogen skal godkende et ejerskabskrav, skjule en anmeldelse, slå en funktion til for en delmængde af brugerne, rydde en cache eller finde ud af, hvorfor én konto ser noget mærkeligt. Når den applikation ikke findes, er svaret en udvikler med en databasekonsol — hvilket er langsomt, ikke bliver logget og kun er én slåfejl fra en driftshændelse.</p>\n<p><strong>Opgave.</strong> At drive platformen i det daglige skulle kunne lade sig gøre uden en shell, så driften lå hos den, der havde vagten, og ikke hos den, der havde adgangsoplysningerne.</p>\n<p><strong>Handling.</strong> Back‑officen endte på omkring 40 admin‑ruter bygget af 128 komponenter, og den dækker arbejdet, som det faktisk kommer ind: at afgøre ejerskabskrav på virksomheder, moderere anmeldelser og opslag, slå feature flags til og fra, undersøge og rydde caches, læse diagnostik og lave den rapportering, direktøren beder om. Den kører på det samme designsystem og den samme genererede API‑klient som det offentlige produkt, hvilket var det afgørende valg — et internt værktøj bygget på sin egen stak bliver den del, ingen opdaterer, og derefter den del, ingen stoler på.</p>\n<p><strong>Resultat.</strong> De folk, der driver platformen, kan drive den, og hver handling går gennem den samme autorisation og efterlader det samme spor som alt andet. Det er en stor flade at holde testet og tilgængelig for et lille internt publikum, og den udgift er løbende. Det er stadig billigere end alternativet, som er en udvikler, der taster UPDATE mod produktion klokken ni en søndag aften.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Frontend‑udvikling", "Full stack‑udvikling", "Produkt & krav", "Systemadministration", "UX/UI‑design", "Full stack‑produktudvikling", "Produktstrategi & kravspecifikation", "Værktøjer til AI‑agenter & MCP‑servere" ] }, { "id": "https://engineer.company/da/portfolio/ran-a-continuous-security-scanning-programme/", "url": "https://engineer.company/da/portfolio/ran-a-continuous-security-scanning-programme/", "title": "Har kørt et løbende sikkerhedsscanningsprogram", "summary": "Etablerede et løbende sikkerhedsprogram — code scanning, DAST, afhængighedstjek, SBOM, secret scanning og pinnede actions — plus 21 sikkerhedsgennemgange.", "content_html": "<p><strong>Situation.</strong> Sikkerhedsarbejdet inde i applikationen — Content‑Security‑Policy’en, least‑privilege‑databaserollerne, den gentagne kontrol af rettigheder — beskytter platformen under kørsel. Intet af det siger noget om det, der bliver leveret: om en afhængighed fik en kendt sårbarhed i tirsdags, om en adgangsoplysning blev committet og rullet tilbage, eller om en action fra en tredjepart, der er låst til et tag, i det stille er blevet til et andet stykke kode.</p>\n<p><strong>Opgave.</strong> Sikkerheden i forsyningskæden og koden skulle være løbende og automatiseret, så status var et buildresultat og ikke en holdning.</p>\n<p><strong>Handling.</strong> Statisk analyse kører gennem CodeQL, dynamisk test mod en kørende instans gennem ZAP, og Go‑afhængigheder tjekkes med govulncheck. En SBOM genereres ved hvert build med Anchore og Syft, så man ved, hvad der blev leveret, i stedet for at skulle rekonstruere det bagefter. Gitleaks gennemsøger historikken efter adgangsoplysninger. Hver GitHub Action fra en tredjepart er låst til en commit‑hash og ikke til et tag, hvilket er den uglamourøse kontrol, der forhindrer, at et tag bliver flyttet bag ens ryg. Dependabot holder øje med 6 økosystemer. Ved siden af automatiseringen ligger 21 skriftlige sikkerhedsgennemgange, heriblandt en trusselsmodel og en vurdering op mod OWASP‑testguiden — for scannere finder de typer problemer, som nogen allerede har beskrevet, og det er i en trusselsmodel, at de problemer, der er særlige for netop denne platform, får et navn.</p>\n<p><strong>Resultat.</strong> En sårbarhed, der bliver offentliggjort upstream, dukker op som et fejlende build i stedet for som en nyhed. Mængden af fund er den reelle pris — en scanner, der rapporterer alt, lærer folk at ignorere den, og at holde signalet brugbart kræver løbende triage og ikke en engangsopsætning.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Driftssikkerhed & backup", "Sikkerhed", "DevOps & CI/CD‑automatisering", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/three-locks-on-the-door-none-of-them-locked/", "url": "https://engineer.company/da/portfolio/three-locks-on-the-door-none-of-them-locked/", "title": "Tre låse på døren, ingen af dem låst", "summary": "Tre beskyttelser, der aldrig én eneste gang havde grebet ind, gør det nu, og den fejlklasse, de tilhører — en kontrol, hvis fejltilstand er, at den bliver ved…", "content_html": "<p><strong>Situation.</strong> En hærdningsgennemgang af produktionsværten i august 2026 stillede et spørgsmål, der normalt får et selvsikkert svar: virker SSH‑beskyttelserne mod brute force. Alle tre var konfigureret, alle tre optrådte i hver eneste rapport, nogen kiggede i, og alle tre havde været uvirksomme siden den dag, værten blev bygget.</p>\n<p><strong>Opgave.</strong> Kontrollerne skulle efterprøves mod det, kernen rent faktisk gør ved en pakke, og ikke mod de konfigurationsfiler, der beskriver, hvad der burde ske med den.</p>\n<p><strong>Handling.</strong> At læse konfigurationen ville have bekræftet det forkerte svar tre gange, så gennemgangen læste det kørende system i stedet. Jailen, der skulle blokere indtrængningsforsøg, holdt øje med port 22, mens dæmonen var flyttet til 1986 under den indledende hærdning — hver blokering, den skrev, gjaldt en port, som intet lyttede på. Firewallens hastighedsbegrænsning var værre på en mere subtil måde: reglen fandtes, og den lå under en bredere regel, der matchede først. Firewallens brugerregler evalueres oppefra og ned, og det første match vinder, så en bred tilladelse over en hastighedsbegrænsning gør begrænsningen til død kode, der stadig står i hver statusoversigt. Den tredje var den mest tavse af dem: blokeringshandlingen kalder et eksternt pakkefilterprogram, som pakkesystemet kun anbefaler og ikke kræver, så på en vært uden programmet starter jailen, tæller og beslutter og fejler så i netop det øjeblik, hvor den forsøger at blokere. Alle tre rettelser var små. Det, der kom ud af det, var ikke rettelsen, men to regler, der nu gælder for repositoryet: en sikkerhedskontrol får en assertion og ikke en kommentar, og en firewall verificeres på reglernes placering og ikke på, om de findes.</p>\n<p><strong>Resultat.</strong> Tre beskyttelser, der aldrig én eneste gang havde grebet ind, gør det nu, og den fejlklasse, de tilhører — en kontrol, hvis fejltilstand er, at den bliver ved med at melde, at alt er i orden — er den klasse, som platformens tjek nu er bygget til at fange. Alle tre havde været uvirksomme lige fra bootstrap. Alle tre så sunde ud alle de steder, nogen kiggede, hvilket er hele grunden til, at de overlevede så længe.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Infrastruktur", "Linux & servere", "Netværk & VPN", "Overvågning & observerbarhed", "Sikkerhed", "Systemadministration", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/proved-the-intrusion-ban-works-every-run/", "url": "https://engineer.company/da/portfolio/proved-the-intrusion-ban-works-every-run/", "title": "Har bevist, at blokeringen virker ved hver kørsel", "summary": "En blokeringsvej, der holder op med at virke, får nu en konvergering til at fejle i stedet for at blive ved med at melde, at alt er i orden, hvilket var den…", "content_html": "<p><strong>Situation.</strong> Tjenesten, der blokerer indtrængningsforsøg, var allerede blevet grebet i at blokere en port, som SSH‑dæmonen ikke lyttede på. At rette porten lukkede det tilfælde. Det gjorde intet ved grunden til, at fejlen overlevede så længe, nemlig at en blokeringsvej ikke fejler synligt: tjenesten kører, jailen står som aktiv, og der er ikke noget sted, der viser, om en blokering nogensinde når kernen.</p>\n<p><strong>Opgave.</strong> Blokeringsvejen skulle afprøves mod det aktive regelsæt ved hver konvergering i stedet for at blive udledt af, at tjenesten var oppe.</p>\n<p><strong>Handling.</strong> Konvergeringen blokerer nu en adresse fra den blok, standarderne reserverer til dokumentation, læser den resulterende regel tilbage i kernens pakkefilter og ophæver blokeringen i en oprydningsblok, der kører, uanset om tjekket bestod eller fejlede. Det reserverede område er det bærende valg — testadressen tilhører ingen, så en efterladt blokering, der overlever en fejlet kørsel, ikke kan lukke et rigtigt netværk ude. Tilbagelæsningen fra kernen er den anden halvdel: tjenestens egen statusudskrift ville rapportere succes for en blokering, der ikke frembragte nogen regel, hvilket er præcis den fejl, der testes for. Derudover er blokeringsbackenden fastlåst i stedet for at blive overladt til tjenestens egen detektion, og logbackenden er sat til detektion, fordi styresystemet ikke leverer nogen traditionel autentificeringslog, og det forkerte valg fejler tavst ved at holde øje med en fil, der aldrig dukker op.</p>\n<p><strong>Resultat.</strong> En blokeringsvej, der holder op med at virke, får nu en konvergering til at fejle i stedet for at blive ved med at melde, at alt er i orden, hvilket var den eneste egenskab, der betød noget. Det koster nogle få sekunder ved hver kørsel, og hver gang bliver en firewallregel skrevet i produktion og trukket tilbage igen, hvilket er et reelt indgreb i et system i drift og blev accepteret med den begrundelse, at alternativet allerede havde vist sig at være værre.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Sikkerhed", "Test & QA", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter", "Site reliability & overvågning", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/added-a-one-second-preflight-for-secrets/", "url": "https://engineer.company/da/portfolio/added-a-one-second-preflight-for-secrets/", "title": "Har tilføjet et preflight‑tjek af hemmeligheder", "summary": "En hemmelighed, der ikke kan slås op, koster nu en afvisning på to sekunder på en bærbar i stedet for en delvist gennemført ændring på en server i drift.", "content_html": "<p><strong>Situation.</strong> Driftshemmelighederne var lige blevet delt op, så hver vært har sit eget sikkerhedskopilager, sin egen adgangssætning og sin egen alarmswitch. Repository‑koden var korrekt. Operatørens krypterede vault indeholdt stadig den tidligere værdi fra tiden med én vært, og intet kunne se uoverensstemmelsen, fordi vaulten bevidst kun ligger lokalt på operatørens maskine og er usynlig for repositoryets egen kvalitetsgate.</p>\n<p><strong>Opgave.</strong> Den type fejl skulle kunne opstå et sted, hvor den var billig, for den var lige opstået et sted, hvor den var dyr.</p>\n<p><strong>Handling.</strong> Den første kørsel efter ændringen koblede på den kørende vært, gennemførte otte opgaver og sagde nej ved den niende — korrekt, men inde fra en kørsel, der allerede havde skrevet swap‑indstillinger til produktion. En halvt gennemført kørsel er et dårligere svar end en afvisning, så der blev skrevet et preflight‑play: det rammer slet ingen vært, kører lokalt, indsamler ingen facts og udfører de samme to opgavefiler, som den rigtige kørsel bruger, mod operatørens vault. Det svarer på omkring et sekund, og provisioneringen kører det først. Den bærende beslutning er, at det er den samme kode og ikke en anden implementering af den samme regel, for et tjek, der gentager en regel på et andet sprog, ender med at være uenig med den, og det i al stilhed. Arbejdet med det afdækkede en fælde, som det selv var tæt på at skabe: facts, der sættes under et play, lever videre efter det play, så hvis preflight og den rigtige kørsel var blevet kædet sammen i ét kald, ville den rigtige rolle have fået en adgangssætning, som preflight allerede havde slået op, og tjekket ville være bestået på en vault, der stadig var forkert. Fælden blev reproduceret, før der blev sikret imod den, i begge opgavefiler.</p>\n<p><strong>Resultat.</strong> En hemmelighed, der ikke kan slås op, koster nu en afvisning på to sekunder på en bærbar i stedet for en delvist gennemført ændring på en server i drift. Preflight er bevidst ikke mærket til altid at køre og bevidst ikke serialiseret, og begge dele er noteret som beslutninger, ikke som standardvalg.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Sikkerhed", "Test & QA", "DevOps & CI/CD‑automatisering", "Infrastructure as Code", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/wrote-a-scope-rule-after-a-credential-leak/", "url": "https://engineer.company/da/portfolio/wrote-a-scope-rule-after-a-credential-leak/", "title": "Har skrevet en afgrænsningsregel efter et læk", "summary": "Omfangsgrænsen er en nedskrevet regel med en angivet test, resterne er synlige i stedet for begravet, og netop den form for adgangsoplysninger, der slap…", "content_html": "<p><strong>Situation.</strong> Arbejde, der var udført for en anden virksomhed, kom med gennem en omstrukturering af repositoryet og blev liggende. Det, der fulgte med, var ikke abstrakt: en kladdelog med gyldige adgangsoplysninger i klartekst, som lå i træet uden om alle kontroller gennem det meste af repositoryets historie; en forældet inventarliste, der nævnte den virksomheds vært; og en opgavefil til firewallen, der åbnede porte til seksten af dens kunders netværk. Intet af det kørte. Det er netop derfor, det overlevede — noget, der ikke kører nogen steder, bliver aldrig gennemgået igen.</p>\n<p><strong>Opgave.</strong> Grænsen skulle gøres til en regel med en tilhørende test i stedet for en hensigt, og resterne skulle håndteres på en måde, der ikke blot skjulte dem.</p>\n<p><strong>Handling.</strong> Reglen er nu det første afsnit i repositoryets instruktionssæt: dette repository styrer én virksomheds infrastruktur og kun den, og en anden parts vært, inventarliste, firewallåbning, DNS‑zone eller adgangsoplysninger hører ikke hjemme i det — heller ikke deaktiveret, udkommenteret eller parkeret i en fil, som ingen playbook importerer. Den praktiske test står lige ved siden af: om denne virksomhed stadig ville være ansvarlig for det, hvis samarbejdet ophørte. Resterne blev sat i karantæne i en mappe med et tydeligt navn i stedet for at blive slettet, så historikken stadig kan læses, og karantænen er bevidst delvis — de to strukturelle lintere springer den over, mens hemmelighedsscanneren, vault‑kontrollen og emoji‑tjekket bevidst stadig læser den, for det er de tre, der ville fange det, der slap ind. Selve lækket førte til en ny linterregel: et brugerdefineret mønster, der markerer adgangsoplysninger, som sendes som et kommandolinjeflag — netop den form, de lækkede oplysninger havde, og som standardregelsættet ikke fangede.</p>\n<p><strong>Resultat.</strong> Omfangsgrænsen er en nedskrevet regel med en angivet test, resterne er synlige i stedet for begravet, og netop den form for adgangsoplysninger, der slap igennem, får nu et commit til at fejle. Læren, der er noteret ved siden af, er den generelle: det farlige artefakt er ikke det, der kører, men det, der ikke gør, for det er det, ingen læser igen.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data governance", "Dokumentation", "Infrastruktur", "Sikkerhed", "Data governance & datakvalitet", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/split-every-operational-secret-per-host/", "url": "https://engineer.company/da/portfolio/split-every-operational-secret-per-host/", "title": "Har adskilt alle driftshemmeligheder pr. vært", "summary": "Hemmelighederne er pr. vært i kraft af selve opbygningen, og de to fejltilstande, der ville være fulgt af at dele dem, er skrevet ned der, hvor den næste…", "content_html": "<p><strong>Situation.</strong> Platformen var bygget til én vært og skulle snart have to. Flere driftshemmeligheder var skrevet som enkeltværdier ud fra den antagelse: ét sikkerhedskopilager, én adgangssætning, én alarmswitch. At udvide dem til en ekstra vært ved at dele dem er den letteste vej, og den er forkert på to forskellige måder, som begge fejler i al stilhed.</p>\n<p><strong>Opgave.</strong> Hver hemmelighed skulle gøres til en tabel med værten som nøgle, og begrundelsen skulle skrives ned, for den delte udgave ser korrekt ud og koster intet indtil den dag, det betyder noget.</p>\n<p><strong>Handling.</strong> To argumenter afgjorde sagen, og begge er noteret ved siden af konfigurationen. En dead man’s switch, som to værter deler, alarmerer mindre end to switches, ikke mere: så længe bare den ene vært stadig pinger, forbliver tjekket grønt, mens den anden er død, så at føje en vært til en delt switch reducerer ligefrem dækningen af den, der allerede var der. Og en streng, der angiver et sikkerhedskopilager, er kun en placering — adgangssætningen er hele krypteringen, så to værter, der deler en adgangssætning, er ikke to lagre med en fælles hemmelighed, men ét lager med to mapper. Hver værdi blev gjort til en tabel med værtsnavnet fra inventaret som nøgle og slås op pr. vært af en fælles opgavefil, som både den rigtige kørsel og preflight kører. Derefter blev den eksisterende vært ført op i alle fire tabeller med sit lager og begge switches tomme, og det er den sandfærdige tilstand og holder de to åbne initiativer ærlige om, at det, de mangler, er operatørens halvdel af arbejdet og ikke koden.</p>\n<p><strong>Resultat.</strong> Hemmelighederne er pr. vært i kraft af selve opbygningen, og de to fejltilstande, der ville være fulgt af at dele dem, er skrevet ned der, hvor den næste person redigerer filen. De tomme poster er den del, der er værd at beholde: de siger, at koblingen findes, men værdien ikke gør, og det er et andet og mere nyttigt udsagn end en manglende nøgle.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Løsningsarkitektur", "Sikkerhed", "Backup & disaster recovery", "Infrastructure as Code", "Sikkerhed & adgangsstyring", "Sikkerhed for AI‑agenter", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/built-a-22-linter-commit-gate/", "url": "https://engineer.company/da/portfolio/built-a-22-linter-commit-gate/", "title": "Har bygget en commit‑gate med 22 lintere", "summary": "Mekaniske indvendinger kommer fra en maskine, før et commit overhovedet findes, og gaten er den eneste reviewer, projektet har.", "content_html": "<p><strong>Situation.</strong> En bidragsvejledning er en samling forslag. Alle er enige i den, og så er det fredag, ændringen er lille, og vejledningen taber. På et enmandsprojekt er det værre, ikke bedre, for der er slet ingen reviewer — det eneste, der står mellem en dårlig ændring og produktion, er den person, der skrev den, i det øjeblik, hvor vedkommende er mindst tilbøjelig til at diskutere med sig selv.</p>\n<p><strong>Opgave.</strong> Standarderne skulle være eksekverbare, så et brud på én af dem får et commit til at fejle i stedet for at vente på, at nogen opdager det.</p>\n<p><strong>Handling.</strong> Resultatet blev en gate med 22 lintere uden noget advarselsniveau: hver diagnostik er en fejl, og selve sitegeneratoren kører med advarsler opgraderet til fejl, så selv en udfasning standser buildet. De oplagte er med — HTML‑validering, CSS, JavaScript, Python, YAML, Markdown, shell, stavning, hemmeligheder, døde links. De interessante er de projektspecifikke tjek, som intet standardværktøj har en holdning til: at begge farvetemaer maler hver lagdelt flade med det samme antal lag, at intet fotografi vises bredere end halvdelen af kildebilledets pixelbredde, at det udrullede træ ikke indeholder nogen privat sti eller noget privat værtsnavn, at teksten overholder tonereglerne på alle tre sprog, og at en side har en Markdown‑tvilling og en gyldig repræsentation i en alternativ protokol. Undertrykkelse inline er helt forbudt — ingen ignorér‑kommentar, intet deaktiveringsdirektiv, ingen omgåelse af hooket og ingen omdøbning af en fil for at undgå en matcher. Reglen, der holder det ærligt, er, at en fejl, der fandtes i forvejen, ikke er en undskyldning: afdækker et tjek en defekt, som ingen har indført, bliver den rettet i samme omgang.</p>\n<p><strong>Resultat.</strong> Mekaniske indvendinger kommer fra en maskine, før et commit overhovedet findes, og gaten er den eneste reviewer, projektet har. Prisen er oplyst i stedet for skjult: det er langsomt at committe, og en dårligt skrevet kontrol er virkelig irriterende at arbejde uden om, og derfor fik kontrollerne selv senere deres egen formatter og linter.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Frontend‑udvikling", "Python", "Test & QA", "DevOps & CI/CD‑automatisering", "Produktstrategi & kravspecifikation", "Teknisk dokumentation", "Udvikling af skræddersyede AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/generated-495-pages-from-a-sqlite-export/", "url": "https://engineer.company/da/portfolio/generated-495-pages-from-a-sqlite-export/", "title": "Har genereret 495 sider fra en SQLite‑eksport", "summary": "Fire hundrede og femoghalvfems sider genereres fra én kilde, det koster intet at rette et udsagn, og de adresser, dette site nogensinde har udgivet, svarer…", "content_html": "<p><strong>Situation.</strong> Virksomhedens porteføljeindhold skrives i en særskilt database — den samme, som CV’et laves ud fra — og websitet skal udgive det som sider, på tre sprog, uden at de to kopier glider fra hinanden. Den naive tilgang, at skrive siderne i hånden og holde dem i takt, går galt ved den første rettelse.</p>\n<p><strong>Opgave.</strong> Websitet skulle generere sit indhold fra databasen som et byggeinput, med sideadresser, der overlever, at sætningerne på dem bliver skrevet om.</p>\n<p><strong>Handling.</strong> En eksportør læser databasen skrivebeskyttet og skriver én side pr. achievement pr. sprog — 495 sider — plus de taksonomi- og ydelsesdata, skabelonerne har brug for. Den bruger kun standardbiblioteket, så sitets build ikke afhænger af generatorens miljø, og det committede output betyder, at sitet kan bygges selvstændigt. Den enkeltbeslutning i den, der har størst konsekvenser, handler om adresser. Sitet plejede at udlede en sides URL fra de første ord i dens engelske udsagn, så det at rette en sætning flyttede siden i stilhed og ødelagde alle links til den — et site, hvis argument for sig selv er, at det retter ting, og som betalte med et dødt link, hver gang det gjorde det. Adressen skrives nu som data: én række pr. adresse pr. achievement, hvor den første er den aktuelle, og alle senere er udfasede adresser, som sitet udsender som omdirigeringer. To andre fælder fra det samme arbejde er skrevet ned. En sammenligning af perioder med en tekstkolonne matchede i stilhed alle toogtredive rækker på grund af den måde, databasen tildeler typeaffinitet i en sammenligning. Og sproglisten er nu den ene akse, som skriveløkken, datafilerne og forespørgslerne alle udledes af, så et fjerde sprog kræver én post i en tabel og ikke en søgning.</p>\n<p><strong>Resultat.</strong> Fire hundrede og femoghalvfems sider genereres fra én kilde, det koster intet at rette et udsagn, og de adresser, dette site nogensinde har udgivet, svarer stadig. Eksportøren ejer også præcis én præsentationsbeslutning — hvordan ydelser grupperes i tematiske afsnit — og det er bevidst: alt andet, den skriver, tilhører databasen, og en gruppeoverskrift, der mangler et sprog, får eksporten til at fejle i stedet for at vise engelsk oven i oversat indhold.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Databaser", "Datapipelines (ETL/ELT)", "Frontend‑udvikling", "Internationalisering", "Python", "SQL", "Webudvikling", "Brand, marketing & SEO", "Internationalisering & lokalisering", "Klargøring af data til AI", "Udvikling af datapipelines (ETL/ELT)", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/replaced-963-metadata-blocks-with-one-graph/", "url": "https://engineer.company/da/portfolio/replaced-963-metadata-blocks-with-one-graph/", "title": "Har erstattet 963 metadatablokke med én graf", "summary": "Én graf med stabil identitet erstatter 963 anonyme gentagelser, og siderne bærer mindre, ikke mere.", "content_html": "<p><strong>Situation.</strong> Sitet udsendte strukturerede data, sådan som de fleste sites gør: én blok pr. side, og hver af dem beskrev organisationen helt forfra. På hele sitet blev det til 963 blokke, der gentog den samme virksomhed 1.671 gange, anonymt — uden nogen stabil identifikator nogen steder, så intet, der brugte dataene, kunne se, at organisationen på én side var organisationen på en anden.</p>\n<p><strong>Opgave.</strong> De strukturerede data skulle gøres til én graf med stabil identitet og ikke en bunke blokke, der tilfældigvis indeholder de samme ord.</p>\n<p><strong>Handling.</strong> Identifikatorer udstedes nu ud fra sitets origin — én til organisationen, én til sitet, én til personen — og hver blok henviser til dem i stedet for at gentage deres indhold. Identifikatorerne gælder for hele origin og ikke pr. sprog, for virksomheden er den samme virksomhed på dansk. Seksten typer er i spil og dækker ydelseskataloget og dets tilbud, anmeldelserne og det arbejde, de beskriver, samlingerne og deres brødkrummer. To beslutninger holdt vægten nede. Oversigtssider udgiver kun deres elementer som URL’er og indlejrer dem ikke, og det blev målt: at navngive dem på porteføljeoversigten ville have betydet 107 fulde udsagn og en stigning på 52 % i den komprimerede side. Og det tjek, der vogter det, validerer ikke kun syntaks — det efterprøver, at hver blok kan parses, at hver identifikator kan slås op, og at hver URL, grafen nævner, reelt blev bygget, så en graf, der peger på en side, som ikke findes, får gaten til at fejle.</p>\n<p><strong>Resultat.</strong> Én graf med stabil identitet erstatter 963 anonyme gentagelser, og siderne bærer mindre, ikke mere. Den måling, der satte arbejdet i gang, er værd at huske: sitet havde udsendt den samme beskrivelse af organisationen 1.671 gange, uden at noget var i stand til at koble dem sammen.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "API'er & integration", "Automatisering & CI/CD", "Brand & marketing", "Data governance", "Frontend‑udvikling", "Webudvikling", "Brand, marketing & SEO", "Data governance & datakvalitet", "Klargøring af data til AI", "Parathed til AI‑agenter", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/wrote-tests-for-the-checkers-themselves/", "url": "https://engineer.company/da/portfolio/wrote-tests-for-the-checkers-themselves/", "title": "Har skrevet tests af selve tjekkene", "summary": "Hvert tjek i projektet har nu en test, der beviser, at det fejler på dårlige inddata, og områdeforventningerne læses fra sandhedskilden.", "content_html": "<p><strong>Situation.</strong> Projektet kører en række specialbyggede tjek over sit eget indhold — en stavekontrol, et tjek af identifikatorernes talområde, et stemmetjek, et tjek af taloverensstemmelse. Hvert af dem havde kørt grønt i månedsvis. Et tjek, der kun nogensinde har set rene inddata og aldrig har meldt andet end rent, kan ikke skelnes fra et tjek, der slet intet læser, og der fandtes ingen test i suiten, som kunne kende forskel på de to.</p>\n<p><strong>Opgave.</strong> Tjekkene skulle kunne bevise, at de kan fejle, og de fixtures, de tjekker imod, skulle holde op med at være håndvedligeholdte kopier af det, de beskriver.</p>\n<p><strong>Handling.</strong> Mønsteret, der er anvendt hele vejen igennem, er at plante netop den defekt, tjekket findes for at fange, og kræve, at tjekket finder den. Stavekontrollen fodres med et bevidst fejlstavet ord, og testen fejler, hvis kørslen kommer rent tilbage. Stemmetjekket fodres med prosa i første person og skal afvise den. Buzzword‑tjekket fodres med et forbudt ord. Hvert af disse er en lille test, og hver af dem lukkede en reel blind vinkel, for to af tjekkene viste sig at læse et snævrere sæt filer end deres dokumentation påstod og i stilhed havde sprunget indhold over. Den anden ændring handler om, hvor en test henter sine forventninger: tjekket af identifikatorernes talområde sammenlignede før med et tal skrevet i testfilen, hvilket betød, at enhver tilføjelse af indhold krævede en redigering af en test, og en redaktør, der opdaterede tallet uden at se efter, havde slået tjekket fra. Det udleder nu området fra databasen, så tjekket beskriver dataene og ikke en forældet erindring om dem.</p>\n<p><strong>Resultat.</strong> Hvert tjek i projektet har nu en test, der beviser, at det fejler på dårlige inddata, og områdeforventningerne læses fra sandhedskilden. Det ubehagelige er, hvad dette blotlagde: et grønt tjek havde været meningsløst mindst to steder i et ukendt tidsrum, og der er ingen måde at finde ud af med tilbagevirkende kraft, hvad der slap igennem.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data governance", "Databaser", "Python", "Test & QA", "Data governance & datakvalitet", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning", "Udvikling af skræddersyede AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/made-six-collectors-state-what-they-skipped/", "url": "https://engineer.company/da/portfolio/made-six-collectors-state-what-they-skipped/", "title": "Har fået seks indsamlere til at melde det ulæste", "summary": "En side med denne platforms tal viser nu sine egne blinde vinkler ved siden af sine fund. Ingen af selvmålingerne er en gate, og det står der direkte i stedet…", "content_html": "<p><strong>Situation.</strong> Seks skrivebeskyttede indsamlere renderede en kørende host til Markdown, og ikke én af dem registrerede sin egen køretid, de filer, den ikke kunne åbne, eller de input, den sprang over. Flere havde fejlveje, der slugte fejlen helt, hvilket giver en bestemt og farlig form for rapport: en side med tal, der ser komplet og sund ud, uanset om noget som helst reelt blev læst. En rullet log, der ankommer afkortet, fjerner hele sit tidsrum fra hvert tal på siden, og en uge med manglende trafik læses præcis som en stille uge. Et spejl, hvis dæmon ændrede sit logformat, giver det samme nul som et spejl, ingen besøgte. Hver probe i sikkerhedsrapporten sidder bag en test for værktøjet, så en host, der manglede firewall-, socket- og ban‑værktøjerne, renderede ingen regler, ingen sockets og ingen jails — det visuelle kendetegn for en aflåst host, men i virkeligheden en blind host.</p>\n<p><strong>Opgave.</strong> To ting skulle gøres umulige at forveksle med en sund tilstand. En rapport skulle sige, hvad der tæller som dårligt, i stedet for at overlade det til et menneske at afgøre, og hver indsamler skulle sige, hvad dens egen indsamling kostede, og hvor meget af sit input den ikke kunne bruge.</p>\n<p><strong>Handling.</strong> Tærsklerne blev tilpasset de 116 stikprøver, metrikrapporten allerede havde målt, aldrig til et gæt, med de observerede intervaller skrevet ved siden af grænserne. Bedømmelsen finder hver kolonne ud fra navn og ikke ud fra position og nægter at bedømme en række, hvis antal værdier ikke matcher dens antal etiketter, hvilket er det, der beskytter den mod en ændring i tidsformatet, som forskyder hver etiket med én. Rapporten skelner derefter mellem tre udfald og ikke to: et brud, intet brud og ikke bedømt, fordi intet målte det. På indsamlingssiden rapporterer hver enkelt nu sin køretid og sin blindhed — filer læst over for filer, der ikke kunne læses, journallinjer læst over for journallinjer genkendt, repositories, som git nægtede at læse, talt op i stedet for vist som et nul, kilder spurgt over for kilder, der svarede, prober spurgt over for prober, der ikke var installeret — og hver af de rækker skrives, også når svaret er nul, for en række, der forsvinder, når den er tom, er den samme tavshed i en anden form. Hver skabelon fik en eksplicit gren for output, der er skrevet af en indsamler, som er ældre end skabelonen selv.</p>\n<p><strong>Resultat.</strong> En side med denne platforms tal viser nu sine egne blinde vinkler ved siden af sine fund. Ingen af selvmålingerne er en gate, og det står der direkte i stedet for at blive antydet: ingen grænse er tilpasset en indsamlers køretid, så en indsamling, der fordobles, er synlig og stopper ingen, og at tilpasse en grænse ville kræve en baseline fra den rigtige host, som dette arbejde bevidst ikke opfandt. De grænser, der findes, er daglige gennemsnit, så en host, der rører et loft i et minut, overskrider intet her — kontakten, der kører hvert kvarter, fanger det, ingen af dem erstatter den anden, og begge rapporter siger det på siden.</p>\n", "date_published": "2026-10-21T18:38:05+02:00", "date_modified": "2026-10-21T18:38:05+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Dokumentation", "Driftssikkerhed & backup", "Overvågning & observerbarhed", "Python", "Test & QA", "Site reliability & overvågning", "Systemadministration", "Teknisk dokumentation", "Udvikling af skræddersyede AI‑agenter" ] }, { "id": "https://engineer.company/da/portfolio/one-infrastructure-plan-for-14-departments/", "url": "https://engineer.company/da/portfolio/one-infrastructure-plan-for-14-departments/", "title": "Én infrastrukturplan til 14 afdelinger", "summary": "Designede en omfattende infrastrukturramme for 14 afdelinger i et forskningsinstitut — modulær, med samlede datapipelines og supportstrategier.", "content_html": "<p><strong>Situation.</strong> På et forskningsinstitut med 14 vidt forskellige forskningsgrupper arbejdede hver gruppe med sine egne datakilder, formater, skalaer og softwareværktøjer. Den tekniske ekspertise og de tilgængelige it‑ressourcer varierede meget fra gruppe til gruppe. Nogle få havde formået at bygge og udrulle skræddersyede it‑løsninger, men mange kæmpede med komplekse behov for datainfrastruktur, og det tog værdifuld tid og opmærksomhed fra selve forskningen.</p>\n<p><strong>Opgave.</strong> Opgaven var at udtænke en løsning, der ville give forskerne mulighed for at koncentrere sig om det videnskabelige arbejde frem for it‑udfordringer. Målet var at designe og implementere en skalerbar datainfrastruktur for hele instituttet, som kunne rumme de brede og forskelligartede behov hos størstedelen af forskningsgrupperne.</p>\n<p><strong>Handling.</strong> Der blev udviklet en robust og fremsynet infrastrukturplan, som balancerede fleksibilitet og standardisering. Planen skitserede de centrale komponenter, herunder en modulær arkitektur, integrationsveje for forskelligartede datakilder, brugervenlige grænseflader tilpasset forskellige tekniske kompetenceniveauer samt skalerbare løsninger til lagring og behandling. Den omfattede også strategier for onboarding, support og governance, der skulle sikre, at infrastrukturen blev taget i brug og kunne holde på længere sigt.</p>\n<p><strong>Resultat.</strong> Den færdige infrastrukturplan var både teknisk solid og strategisk på linje med instituttets forskningsmål. Den samlede organisationen om én vision for datahåndtering, viste en klar vej til at lette forskernes it‑byrde og lagde fundamentet for et fælles, effektivt og fremtidsparat forskningsdatamiljø. Planen blev godt modtaget for sin inkluderende tilgang, sin klarhed og sin tilpasningsevne og udstak en tydelig kurs for omstillingen af instituttets datainfrastruktur.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Data engineering", "Data governance", "Datapipelines (ETL/ELT)", "Dokumentation", "Infrastruktur", "Interessenter & rapportering", "Løsningsarkitektur", "Platformarkitektur", "Teknisk ledelse", "Data governance & datakvalitet", "Platform- & løsningsarkitektur", "Teknisk dokumentation", "Teknisk ledelse & rådgivning", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/managed-8-wireguard-and-ipsec-vpn-connections/", "url": "https://engineer.company/da/portfolio/managed-8-wireguard-and-ipsec-vpn-connections/", "title": "Har administreret og fejlsøgt 8 VPN‑forbindelser", "summary": "Administrerede og fejlsøgte 8 WireGuard- og IPsec-VPN-forbindelser på Google Cloud og Linux — gjorde en ende på tilbagevendende udfald.", "content_html": "<p><strong>Situation.</strong> Den sikre forbindelse mellem cloud’en og Linux‑systemerne on‑premises gik gennem flere VPN‑tunneler, og de var skrøbelige og besværlige at diagnosticere, når de faldt ud. En tunnel, der var nede, kunne afbryde kommunikationen mellem miljøerne, og fejlfindingen var langsom og usikker — man var aldrig helt sikker på, at man havde fundet den egentlige årsag.</p>\n<p><strong>Opgave.</strong> Opgaven var at administrere og fejlsøge disse forbindelser for at garantere sikker og uafbrudt kommunikation.</p>\n<p><strong>Handling.</strong> VPN‑miljøet kom under kontrol — otte WireGuard- og IPsec‑tunneler mellem Google Cloud og Linux‑systemerne blev administreret og fejlsøgt samlet i stedet for som otte separate mysterier. Konfigurationen blev standardiseret, så tunnelerne var ensartede og til at forstå i stedet for hver især at være et særtilfælde. Deres tilstand blev overvåget, og de tilbagevendende problemer med routing og nøgleudveksling blev løst ved roden i stedet for at blive dækket over med en genstart.</p>\n<p><strong>Resultat.</strong> Alle otte tunneler kørte sikkert og pålideligt, kommunikationen mellem miljøerne forblev beskyttet, og de tilbagevendende forbindelsesproblemer, der før afbrød arbejdet, hørte op. Det var rettelsen af de grundlæggende årsager i stedet for pleje af symptomerne, der forvandlede tunnelerne fra en tilbagevendende hovedpine til noget, der bare virkede.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Cloud", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Sikkerhed", "Systemadministration", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/contributed-to-user-interface-design-processes/", "url": "https://engineer.company/da/portfolio/contributed-to-user-interface-design-processes/", "title": "Har bidraget til design af brugergrænseflader", "summary": "Bidrog til UI-design med intuitive, gennemarbejdede og brugervenlige grænseflader ved at stille spørgsmål om brugervenlighed under designet.", "content_html": "<p><strong>Situation.</strong> Grænsefladerne i virksomhedens projekter var ujævne. Nogle var fine, andre var tydeligvis bygget af udviklere, der tænkte på datamodellen og ikke på den person, der skulle bruge dem, og designbeslutningerne blev ikke altid truffet med slutbrugeren i rummet. Især i et webkortprodukt er kortet den nemme del — det er betjeningen, filtreringen og forløbet omkring kortet, hvor folk farer vild.</p>\n<p><strong>Opgave.</strong> En del af rollen var at deltage i UI‑designprocessen og hjælpe med at gøre grænsefladerne til noget, folk oplevede som intuitivt og behageligt at bruge — ikke bare funktionelt.</p>\n<p><strong>Handling.</strong> Rollen var ikke designerens, men den var med i designprocessen og bidrog med et udviklerperspektiv: der blev stillet spørgsmål ved layoutet, ved forløbet gennem en opgave og ved, om en skærm virkelig var klar eller bare velkendt for dem, der havde bygget den. Der var tale om frontends i React, Angular og Vue oven på kort i Mapbox og Leaflet, og meget af brugervenligheden lå i detaljerne: hvordan man filtrerede et datasæt, hvordan man skiftede mellem etager på et indendørskort, og om systemet fortalte, hvad det var i gang med. Mest handlede det om at stille de naive brugerspørgsmål tidligt, mens de stadig var billige at rette, i stedet for efter release, når forvirringen vendte tilbage som supportsager.</p>\n<p><strong>Resultat.</strong> Grænsefladerne blev mere intuitive og mere gennemarbejdede, og slutbrugerne mærkede det direkte. Det løftede den samlede kvalitet af det, der blev leveret, og forskellen skyldtes først og fremmest, at brugervenlighedsspørgsmålene blev stillet under designet og ikke efter release.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Frontend‑udvikling", "GIS / geodata", "UX/UI‑design", "Webudvikling", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/overhauled-internal-processes-saving-8000-hours/", "url": "https://engineer.company/da/portfolio/overhauled-internal-processes-saving-8000-hours/", "title": "Har fornyet processer og sparet 8.000 timer", "summary": "Fornyede interne processer og sparede ~8.000 timer ved at forbedre softwarearkitektur, systemer og planlægningseffektivitet.", "content_html": "<p><strong>Situation.</strong> Måden, tingene blev gjort internt på, havde efterhånden samlet det sædvanlige rod — en softwarearkitektur, der var vokset ved påbygninger og ikke ved design, systemer, der virkede, men ikke effektivt, og en planlægning, der lod folk enten vente eller drukne i arbejde. Intet af det var akut, og netop derfor havde ingen rørt ved det, men det kostede stille og roligt en masse tid.</p>\n<p><strong>Opgave.</strong> Målet var en gennemgribende fornyelse af processerne — at finde spildet og fjerne det i stedet for at blive ved med at betale for det.</p>\n<p><strong>Handling.</strong> De interne processer blev omlagt på tre fronter: softwarearkitekturen, så den blev til noget, man kunne overskue og bygge videre på i stedet for at arbejde uden om; systemerne, der blev strømlinet, så rutinearbejdet holdt op med at tage mere tid, end det burde; og planlægningen, så kapaciteten passede til arbejdet. Ændringerne blev også gjort varige — forankret i teamets måde at arbejde på og ikke efterladt som et notat, alle nikkede til og glemte — for procesforbedringer, der ikke gøres permanente, glider bare tilbage til den gamle måde.</p>\n<p><strong>Resultat.</strong> Fornyelsen sparede omkring 8.000 timer ved at gøre arkitekturen, systemerne og planlægningen mærkbart mere effektive. Den kapacitet gik direkte tilbage til arbejde med højere værdi i stedet for at forsvinde i overhead, som ingen havde tænkt på at sætte spørgsmålstegn ved.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Infrastruktur", "Løsningsarkitektur", "Optimering af ydeevne", "Platformarkitektur", "Projektledelse", "Teknisk ledelse", "DevOps & CI/CD‑automatisering", "Platform- & løsningsarkitektur", "Projektledelse (Agile)", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/streamlined-ci-cd-processes-saving-4000-hours/", "url": "https://engineer.company/da/portfolio/streamlined-ci-cd-processes-saving-4000-hours/", "title": "Har strømlinet CI/CD og sparet 4.000 timer", "summary": "Strømlinede CI/CD og sparede ~4.000 timer — hurtigere, mere pålidelige releases, så teamet trygt kunne udgive.", "content_html": "<p><strong>Situation.</strong> At få software ud ad døren afhang af manuelle trin, der blev udført forskelligt — nogen skulle huske rækkefølgen og gjorde det en anelse anderledes hver gang — og det bremsede releases og slugte udviklertimer, der skulle være gået til at bygge ting.</p>\n<p><strong>Opgave.</strong> Målet var at strømline CI/CD‑processen og automatisere byggepipelines, så releases ikke længere var et manuelt ritual.</p>\n<p><strong>Handling.</strong> Der blev indført automatiserede bygge-, test- og udrulningspipelines, så vejen fra en ændring til, at den kørte i produktion, var standardiseret og ikke improviseret. De gentagne manuelle trin — dem, der var langsomme og, værre endnu, blev udført forskelligt alt efter, hvem der stod for dem — blev fjernet. Når byggepipelinen først gør det på samme måde hver gang, holder en hel klasse af “det virkede på min maskine”-problemer og halvt huskede udrulningstrin simpelthen op med at opstå.</p>\n<p><strong>Resultat.</strong> Automatiseringen gav omkring 4.000 timer tilbage og gjorde releases både hurtigere og mere pålidelige. Teamet kunne udgive uden at holde vejret — tilliden kom af, at processen var ensartet, og ikke af, at alle var forsigtige.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Driftssikkerhed & backup", "Teknisk ledelse", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/developed-a-data-analytics-reporting-system/", "url": "https://engineer.company/da/portfolio/developed-a-data-analytics-reporting-system/", "title": "Har udviklet et system til analyserapporter", "summary": "Byggede et Python-rapporteringssystem til dataanalyse, der øgede den kvartalsvise softwareomsætning 400 % med klare, rettidige rapporter.", "content_html": "<p><strong>Situation.</strong> Interessenterne fik ikke analyserne i en rettidig og læsbar form. Dataene fandtes, men det var langsomt og manuelt at omsætte dem til noget, man kunne træffe beslutninger ud fra, så overblikket over, hvordan det gik, haltede bagefter, og det samme gjorde de kommercielle beslutninger.</p>\n<p><strong>Opgave.</strong> Opgaven var at bygge et rapporteringssystem til dataanalyse — et system, der omsatte rådata til klar og regelmæssig indsigt, uden at nogen skulle stykke rapporten sammen i hånden hver gang.</p>\n<p><strong>Handling.</strong> Et rapporteringssystem i Python genererede PDF‑rapporter og automatiserede hele kæden: at hente dataene, køre analysen og præsentere resultatet i et overskueligt, ensartet format, som interessenterne rent faktisk kunne læse. Pointen var regelmæssighed og klarhed — den samme professionelle rapport kom forudsigeligt, så tallene blev noget, folk kiggede på som en selvfølge, og ikke noget, de skulle ud og grave efter.</p>\n<p><strong>Resultat.</strong> Det var rapporteringen, der øgede den kvartalsvise softwareomsætning med 400 %. Bedre og hurtigere analyser var ikke en back‑office‑luksus — når de folk, der træffer de kommercielle beslutninger, får klare og rettidige tal, bliver beslutningerne bedre, og her kunne det ses direkte på omsætningen.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Backend‑udvikling", "Data engineering", "Dataanalyse", "Interessenter & rapportering", "Produkt & krav", "Python", "Backend- & API‑udvikling", "Dataanalyse & BI‑dashboards", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/a-mapping-product-carried-end-to-end/", "url": "https://engineer.company/da/portfolio/a-mapping-product-carried-end-to-end/", "title": "Et kortprodukt, båret fra ende til anden", "summary": "Stod for arkitektur, opbygning og drift af infrastruktur, databehandling og kortapp i to år uden pause — produktets pålidelige rygrad.", "content_html": "<p><strong>Situation.</strong> En startup inden for grøn energi, der stadig var i en tidlig fase, var afhængig af én platform til at følge, overvåge og optimere vedvarende energianlæg, men havde hverken et dedikeret infrastrukturteam eller en etableret udviklingsorganisation til at bygge og drive den. Hele det tekniske fundament — cloud‑infrastruktur, databehandlingspipelines og den kundevendte GIS‑kortapplikation — skulle bygges op og holdes kørende uafbrudt på et marked, hvor enhver nedetid eller ethvert hul i data direkte undergravede kundernes tillid og omsætningen.</p>\n<p><strong>Opgave.</strong> Opgaven var egenhændigt at udforme, bygge og drive hele systemet fra ende til anden og dække platform- og dataudvikling, DevOps og site reliability. Ud over at skrive softwaren betød det at have det fulde ansvar for produktionen: at provisionere og hærde infrastrukturen, at designe det databehandlingslag, der forsynede kortet med data, og at sikre, at applikationen forblev tilgængelig døgnet rundt for en voksende kundebase — alt sammen inden for de rammer og det ubønhørlige tempo, der præger en startup i hastig udvikling.</p>\n<p><strong>Handling.</strong> I to år blev infrastrukturen, datapipelines og kortapplikationen designet, implementeret og holdt i drift uden afbrydelse — uden weekender, helligdage eller ferie, ofte ti til fjorten timer om dagen. En pragmatisk, modulær arkitektur blev valgt, så det hele kunne vedligeholdes af én person, med automatiseret provisionering, overvågning og alarmering, så problemer kunne opdages og løses hurtigt. Databehandlingen blev løbende finjusteret med henblik på pålidelighed og ydeevne, releases blev leveret trinvist, og hvert eneste lag — fra serverne til det kort, brugerne ser — blev personligt vedligeholdt og forbedret ud fra kundernes faktiske brug.</p>\n<p><strong>Resultat.</strong> Platformen var konstant tilgængelig og udviklede sig fra en skrøbelig tidlig prototype til produktets pålidelige rygrad, og den bar virksomheden gennem den kritiske vækstfase i kraft af én udviklers fulde ejerskab. Den tætte, praktiske forvaltning holdt infrastrukturen, data og kortapplikationen pålidelige nok til at understøtte mersalg, licensering af data og arbejdet med at vinde nye kunder, og den viste en sjælden grad af engagement, faglig bredde og ansvar for hele stakken fra ende til anden.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Data engineering", "Datapipelines (ETL/ELT)", "Driftssikkerhed & backup", "Full stack‑udvikling", "GIS / geodata", "Infrastruktur", "Platformarkitektur", "Full stack‑produktudvikling", "GIS & geospatiale løsninger", "Platform- & løsningsarkitektur", "Site reliability & overvågning", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/built-the-next-js-16-frontend-ssr-ssg-and-csr/", "url": "https://engineer.company/da/portfolio/built-the-next-js-16-frontend-ssr-ssg-and-csr/", "title": "Har bygget Next.js 16‑frontenden: SSR, SSG og CSR", "summary": "Byggede Next.js 16-frontenden med SSR/SSG/CSR og et server-shell prefetch-and-hydrate-mønster, der eliminerede N+1-fetches.", "content_html": "<p><strong>Situation.</strong> Det autentificerede dashboard skulle føles hurtigt og samtidig være reelt interaktivt, og de to mål trækker i hver sin retning, hvis man griber det naivt an. Henter man alt på klienten, bliver den første indlæsning langsom, og værre endnu får man N+1‑mønsteret, hvor hver komponent vågner og sender sin egen request af sted, så en enkelt side bliver til en kaskade af rundture.</p>\n<p><strong>Opgave.</strong> Hver del af appen skulle renderes på den måde, der faktisk passede til den, uden at opgive interaktiviteten på klientsiden, hvor den betød noget.</p>\n<p><strong>Handling.</strong> Next.js 16‑frontenden bruger den rette renderingsform til hver del i stedet for ét fælles valg. Marketingsider og offentlige sider genereres statisk — de ændrer sig ikke fra bruger til bruger, så der er ingen grund til at rendere dem ved hver request. De reelt interaktive dele renderes fortsat på klienten. Og der, hvor N+1‑kaskaden for alvor gør ondt — det autentificerede dashboard og de store oversigtslister — blev løsningen samlet i en factory i stedet for at blive håndkodet hver gang: createPrefetchedServerPage henter sidens data på serveren og giver dem videre til klienten, allerede udfyldt, så komponenterne starter med deres data i stedet for hver især at skulle spørge efter dem. En strategi for cache‑invalidering er dokumenteret for hver forespørgsel, så dataene forbliver friske, uden at appen henter ting igen, som den allerede har.</p>\n<p><strong>Resultat.</strong> De sider, der er flyttet over på denne factory, indlæses med én rundtur i stedet for en hel storm af dem, og fordi det er en factory og ikke et mønster, folk kopierer i hånden, arver den næste side, der flyttes over, opførslen gratis. Resten af den autentificerede app renderes stadig på klienten og venter på sin tur bag den — en migrering med en fungerende mekanisme og en oplagt næste side, ikke en afsluttet gennemgang.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Frontend‑udvikling", "Full stack‑udvikling", "Optimering af ydeevne", "Platformarkitektur", "Webudvikling", "Full stack‑produktudvikling", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/designed-an-ios-26-liquid-glass-design-system/", "url": "https://engineer.company/da/portfolio/designed-an-ios-26-liquid-glass-design-system/", "title": "Har skabt et iOS 26 'Liquid Glass'-designsystem", "summary": "Designede et iOS 26 'Liquid Glass'-designsystem og et kanonisk komponentbibliotek, håndhævet af lint-regler, så brugerfladen forbliver ensartet.", "content_html": "<p><strong>Situation.</strong> En brugerflade uden et fælles visuelt sprog og et fast sæt komponenter skrider, og den skrider hurtigt. Hver ny skærm genopfinder sine egne knapper og badges og kort, hver en lille smule anderledes, og de små inkonsistenser hober sig op, indtil produktet ser usammenhængende ud, og hver ændring betyder, at man skal ind og rette fem specialbyggede versioner af den samme ting.</p>\n<p><strong>Opgave.</strong> Designsystemet skulle være sammenhængende nok til at virke gennemtænkt og håndhævbart nok til ikke at smuldre, i det øjeblik teamet havde travlt.</p>\n<p><strong>Handling.</strong> Det visuelle sprog og komponentsystemet blev designet som én ting. Sproget er et iOS 26 “Liquid Glass”-look — gennemskinnelige paneler med backdrop‑blur, lagdelte skygger, en smule refraktion, spring‑animationer — bygget af Tailwind‑utilities. Oven på det ligger et kanonisk sæt komponenter, som alle skærme skal sammensættes af: Card, Label, Button, GlassIconButton, DirectoryGrid og resten. Det, der får det til at holde, er lintingen: regler, der afviser et hjemmelavet badge eller en hjemmelavet chip og henviser til den kanoniske komponent i stedet, så systemet bæres af værktøjer og ikke af, at den, der reviewer den dag, husker at tage sig af det. Og dokumentationen er koblet til konkrete noter, der går fra fejl til løsning, så vejledningen er “her er den forkerte måde og den rigtige”, ikke et abstrakt princip.</p>\n<p><strong>Resultat.</strong> Brugerfladen forbliver konsistent og tro mod brandet, og de engangskomponenter, der ellers ville brede sig, bliver fanget, før det sker. Visuel konsistens holdt op med at være et spørgsmål om alles disciplin og blev til noget, værktøjerne håndhæver, hvilket er den eneste version af det, der overlever en deadline.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Dokumentation", "Frontend‑udvikling", "UX/UI‑design", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/diagnosed-and-repaired-laptop-hardware/", "url": "https://engineer.company/da/portfolio/diagnosed-and-repaired-laptop-hardware/", "title": "Har diagnosticeret og repareret bærbare computere", "summary": "Diagnosticerede og reparerede laptop-hardware — skærme, hængsler, tastaturer, trackpads, bundkort, strøm — og løste softwareproblemer på Linux, Windows og Mac.", "content_html": "<p><strong>Situation.</strong> Laptops kom ind med alt, hvad der kan gå galt med en laptop — knuste skærme, ødelagte hængsler, døde tastaturer, svigtende trackpads, bundkortfejl, strømproblemer — plus softwaresiden på Linux, Windows og Mac. Kort sagt: uanset hvad der var galt med den, endte den på arbejdsbordet.</p>\n<p><strong>Opgave.</strong> Opgaven var at fejlfinde og reparere dem pålideligt — og pålideligt er nøgleordet, for en laptopreparation, der ikke holder, er bare et udskudt genbesøg.</p>\n<p><strong>Handling.</strong> Hardwaren blev repareret hele vejen rundt — skærme, hængsler, tastaturer, trackpads, bundkort, strøm — hvilket på bundkortniveau er ægte pillearbejde helt tæt på komponenterne, og softwareproblemerne blev løst på alle tre styresystemer. Den egentlige kunst ligger som regel i diagnosen: når en laptop ikke vil tænde, kan det være opladeren, kortet eller batteriet, og reparationen er aldrig bedre end gættet på, hvad der faktisk er i stykker. Derfor gik omhuen ud på at finde den reelle fejl, før noget som helst blev rørt.</p>\n<p><strong>Resultat.</strong> Maskinerne kom tilbage til ejerne velfungerende og pålidelige, med både hardware og software i orden. Kun en reparation, der holder, er værd at lave, og det var det at stille den rigtige diagnose først, der fik netop disse reparationer til at holde.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Linux & servere", "Systemadministration", "Hardwarereparation & datagendannelse", "It‑support & helpdesk" ] }, { "id": "https://engineer.company/da/portfolio/hardened-ssh-to-24-validated-directives/", "url": "https://engineer.company/da/portfolio/hardened-ssh-to-24-validated-directives/", "title": "Har hærdet SSH til 24 validerede direktiver", "summary": "SSH-sikkerheden er nu dæmonens svar og ikke repositoryets påstand, og forskellen er ikke teoretisk — den var allerede to indstillinger bred, da tjekket blev…", "content_html": "<p><strong>Situation.</strong> SSH er den eneste interaktive vej ind på virksomhedens vært, og dens konfiguration skrives af en automatiseringsrolle, der havde kørt rent i måneder. En adgangsrapport i september 2026 læste dæmonens egne effektive indstillinger og fandt to af dem i uoverensstemmelse med det, rollen havde skrevet ved hver eneste konvergering.</p>\n<p><strong>Opgave.</strong> Hærdningen skulle være noget, dæmonen bekræfter, og ikke noget, repositoryet påstår, for kløften mellem de to havde allerede stået åben i måneder, uden at nogen havde opdaget det.</p>\n<p><strong>Handling.</strong> Årsagen var rækkefølgen i konfigurationen. Styresystemet leverer sine egne standardværdier som aktive linjer, der ikke er udkommenteret, over det sted, hvor en drop‑in‑fil lander, og for de to omtalte indstillinger vinder den første forekomst. Rettelsen var et filnavnspræfiks, der sorterer foran leverandørens, hvilket er en ændring på ét tegn og præcis den slags, der forbliver i stykker, fordi ingen tænker på at kigge efter. Det, der blev bygget omkring den, betyder mere: tre valideringstrin ved hver konvergering. Kandidatfilen syntakstjekkes, før den installeres, så en ugyldig konfiguration aldrig når værten. Den samlede konfiguration tjekkes efter installationen. Derefter læses dæmonens eget output med de effektive indstillinger tilbage, og 24 direktiver efterprøves mod det, så en indstilling, der er skrevet, men bliver tilsidesat, får kørslen til at fejle. To direktiver blev bevidst udeladt, i begge tilfælde fordi dæmonen ikke længere understøtter dem, og fordi det kun ville se grundigt ud at skrive dem.</p>\n<p><strong>Resultat.</strong> SSH‑sikkerheden er nu dæmonens svar og ikke repositoryets påstand, og forskellen er ikke teoretisk — den var allerede to indstillinger bred, da tjekket blev skrevet. En konvergering, der ikke kan se, om en kontrol er trådt i kraft, har ikke verificeret den, og en søgning efter et direktiv beviser kun, at det blev skrevet.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Infrastruktur", "Linux & servere", "Sikkerhed", "Systemadministration", "Test & QA", "Infrastructure as Code", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/held-the-generator-to-981-tests-and-92-coverage/", "url": "https://engineer.company/da/portfolio/held-the-generator-to-981-tests-and-92-coverage/", "title": "Har givet generatoren 981 tests og 92 % dækning", "summary": "Pipelinen kan køres igen mod en database i drift uden frygt, hvilket er det, der overhovedet gør trinvist indholdsarbejde muligt.", "content_html": "<p><strong>Situation.</strong> En generator, der samler en database fra bunden, har en særlig slags fejl: den virker første gang og korrumperer anden gang. Trin, der indsætter uden at tjekke, trin, der afhænger af rækkefølgen i et tidligere trins output, trin, der er sikre hver for sig, men ikke sammen. Intet af det viser sig i en test, der starter fra ingenting og kører én gang.</p>\n<p><strong>Opgave.</strong> Det skulle bevises, at buildet kunne gentages og ikke blot virkede, og testsuiten skulle være stor nok og streng nok til, at en regression ikke kunne slippe ubemærket igennem den.</p>\n<p><strong>Handling.</strong> Idempotenstesten er den stumpe og den mest nyttige: byg hele databasen fra en tom fil, tag et øjebliksbillede, kør hele pipelinen i nitten trin igen over resultatet, og kræv, at andet gennemløb ikke ændrer noget. Rækketal, indhold og identifikatorer skal alle stemme. Omkring den ligger 383 testfunktioner — 981 tilfælde, når de parametriserede er foldet ud — fordelt på femogfyrre filer og 7.944 linjer, der dækker pipelinen, gengiverne, indholdsindlæserne, målretningslogikken og tjekkerne. Grendækningen har et gulv på tooghalvfems procent, der håndhæves i buildet og ikke blot rapporteres i et sammendrag, og suiten måler i øjeblikket omkring 95 %, så gulvet har luft uden at være dekorativt. Advarsler er konfigureret som fejl, hvilket er den indstilling, der betyder mest i praksis — en udfasningsmeddelelse, der udskrives i to år, er en udfasningsmeddelelse, ingen læser, og den kørsel, der forvandler den til en rød test, er den kørsel, der får den rettet.</p>\n<p><strong>Resultat.</strong> Pipelinen kan køres igen mod en database i drift uden frygt, hvilket er det, der overhovedet gør trinvist indholdsarbejde muligt. Gulvet er et gulv, ikke et mål, og det er værd at sige, at tooghalvfems procent grendækning stadig efterlader grene, som intet nogensinde har gennemløbet — tallet afgrænser risikoen, det fjerner den ikke, og to af de defekter, der senere blev fundet i projektet, lå i kode, som dækningsrapporten viste som dækket.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Databaser", "DevOps", "Driftssikkerhed & backup", "Python", "Test & QA", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/held-4-repositories-to-one-history-standard/", "url": "https://engineer.company/da/portfolio/held-4-repositories-to-one-history-standard/", "title": "Har holdt 4 repositories på én historikstandard", "summary": "Fire repositories deler ét historikformat og én anvisningsstruktur, begge håndhævet af hooks og ikke af disciplin.", "content_html": "<p><strong>Situation.</strong> Fire repositories, bygget op over atten måneder af én person, er den situation, hvor proces er lettest at springe over, for der er ingen at koordinere med, og prisen for en ulæselig historik betales helt af et fremtidigt jeg, der endnu ikke har beklaget sig. Det er også den situation, hvor en usammenhængende historik er mest sandsynlig, eftersom hvert repository kan glide over i sine egne vaner, uden at noget trækker dem sammen.</p>\n<p><strong>Opgave.</strong> Én standard for historik og én standard for anvisninger skulle gælde i alle de repositories, arbejdet skrives i, og de skulle håndhæves maskinelt i stedet for at skulle huskes.</p>\n<p><strong>Handling.</strong> Commit‑standarden er et konventionelt præfiks, der angiver ændringens art, og et scope, en emnelinje under en fast længde, ingen emojis og ingen kreditlinjer af nogen art — det sidste, fordi en linje, der krediterer et værktøj, ikke er en kendsgerning om ændringen, og historik er til kendsgerninger om ændringer. En hook på commit‑beskeden afviser alt, der ikke overholder standarden, i hvert repository, der rummer arbejde, så standarden er en egenskab ved repositoryet og ikke ved den, der committer. Fordelingen i det største repository viser, hvad arbejdet reelt bestod af: 156 funktions‑commits, 133 rettelser, 128 dokumentation, 81 vedligehold, 26 refaktoreringer, 20 stil, 5 ydeevne og 3 test. Dokumentation ligger så tæt på rettelser, at det er værd at bemærke, og det er en følge af den anden halvdel af arbejdet — 48 anvisningsdokumenter fordelt på de fire, hvert om ét område og hvert skrevet som regler og ikke som beskrivelse, og alle kan nås fra ét indgangsdokument pr. repository, så der er ét sted at begynde. En dokumentationslinter håndhæver størrelsesbudgetter pr. fil, og at hvert dokument står i indekset, så samlingen forbliver overskuelig og ikke vokser sig til et arkiv.</p>\n<p><strong>Resultat.</strong> Fire repositories deler ét historikformat og én anvisningsstruktur, begge håndhævet af hooks og ikke af disciplin. Det, dette ikke gør, er at gøre historikken god: formatet tjekkes, men det gør indholdet ikke, så en emnelinje, der overholder formatet, men intet beskriver, slipper præcis lige så let igennem som en, der forklarer ændringen.</p>\n", "date_published": "2026-10-21T10:44:46+02:00", "date_modified": "2026-10-21T10:44:46+02:00", "language": "da", "tags": [ "Agile & Scrum", "Automatisering & CI/CD", "DevOps", "Dokumentation", "Projektledelse", "Teknisk ledelse", "DevOps & CI/CD‑automatisering", "Projektledelse (Agile)", "Teknisk dokumentation", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/automated-delivery-of-20-gis-data-pipelines/", "url": "https://engineer.company/da/portfolio/automated-delivery-of-20-gis-data-pipelines/", "title": "Har automatiseret leveringen af 20 GIS‑pipelines", "summary": "Automatiserede 20 GIS-datapipelines og ETL-processer for appdata — strømlinet infrastrukturautomatisering og pålidelige, aktuelle data.", "content_html": "<p><strong>Situation.</strong> Platformen var afhængig af mange GIS‑datapipelines og ETL‑processer for applikationsdata, og de blev leveret og overvåget manuelt. Manuelt kørte pipelines skaber flaskehalse, de glider efterhånden ud af trit med hinanden, og værst af alt rummer de en konstant lille risiko for, at en af dem fejler i stilhed, uden at nogen opdager det, før dataene allerede er forkerte længere nede i kæden.</p>\n<p><strong>Opgave.</strong> Opgaven var at automatisere leveringen af de pipelines og ETL‑processer, så dataene flød pålideligt og forudsigeligt, uden at nogen skulle holde dem i hånden.</p>\n<p><strong>Handling.</strong> Tyve GIS‑datapipelines og ETL‑processerne for applikationsdata kom under automatiseret levering fra ende til anden. Planlægning, logning og fejlhåndtering blev standardiseret, så alle pipelines opførte sig ens, og — det afgørende — man kunne se det, når en af dem ikke gjorde. En stille fejl er kun stille, hvis intet holder øje. Alle pipelines blev også lagt ind i den eksisterende infrastrukturautomatisering og rapportering, så de indgik i ét sammenhængende system i stedet for at være en skuffe fuld af scripts, som nogen skulle huske at køre.</p>\n<p><strong>Resultat.</strong> Alle tyve pipelines og deres ETL kørte automatisk og forudsigeligt, og hele billedet af infrastrukturautomatisering og rapportering blev mere overskueligt. Forretningen fik pålidelige, aktuelle data, uden at nogen skulle føre dem igennem manuelt — og uden risikoen for stille fejl hængende over sig.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data engineering", "Datapipelines (ETL/ELT)", "DevOps", "Driftssikkerhed & backup", "GIS / geodata", "Overvågning & observerbarhed", "DevOps & CI/CD‑automatisering", "GIS & geospatiale løsninger", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/managed-5000-hours-of-map-development/", "url": "https://engineer.company/da/portfolio/managed-5000-hours-of-map-development/", "title": "Har designet og styret 5.000 timers kortudvikling", "summary": "Planlagde og styrede 5.000 timers kortudvikling med Scrum og Jira — synligt arbejde, afstemt med roadmappen.", "content_html": "<p><strong>Situation.</strong> Kortet blev ikke bygget i ét sprint. Det var tusindvis af timers arbejde fordelt på mange mennesker og mange måneder, og den slags indsats skrider, hvis ingen holder fast i scope og tidsplan. Overladt til sig selv bliver den stille og roligt forsinket.</p>\n<p><strong>Opgave.</strong> Ansvaret var at tilrettelægge og lede udviklingsindsatsen, så den leverede med vilje og ikke ved held — og at holde prioriteterne ærlige og fremdriften synlig for alle, der ville se den.</p>\n<p><strong>Handling.</strong> Arbejdet foregik agilt: ordentlig Scrum, hvor ceremonierne blev brugt og ikke bare opført, og hvor opgaverne blev fulgt i Jira, så alle kunne se, hvor tingene stod, uden at skulle spørge. Omkring 5.000 timers kortudvikling gik gennem den proces. Vægten lå mindre på ceremonier for deres egen skyld og mere på at holde prioriteterne rettet mod det vigtige og på at fange skred tidligt, mens de stadig var billige at rette.</p>\n<p><strong>Resultat.</strong> De 5.000 timer blev leveret kontrolleret og synligt i stedet for at forsvinde ind i en sort boks. Projektledelsen gjorde det, den skal — og det, der for det meste går ubemærket hen, når det virker: den holdt arbejdet på linje med målene og nogenlunde inden for tidsplanen, uden heltegerninger i sidste øjeblik.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Agile & Scrum", "Interessenter & rapportering", "Produkt & krav", "Projektledelse", "Teamledelse", "Produktstrategi & kravspecifikation", "Projektledelse (Agile)", "Teamopbygning & mentoring" ] }, { "id": "https://engineer.company/da/portfolio/led-company-growth-from-4-to-14-employees/", "url": "https://engineer.company/da/portfolio/led-company-growth-from-4-to-14-employees/", "title": "Har ledet væksten fra 4 til 14 medarbejdere", "summary": "Ledede virksomhedens vækst fra 4 til 14 medarbejdere med agile metoder — skalerede teamet uden at miste kvalitet eller sammenhold.", "content_html": "<p><strong>Situation.</strong> Virksomheden var klar til at vokse, og netop det øjeblik rummer en særlig fare: man tilføjer folk hurtigere end processer, og både kvaliteten og den fælles forståelse af, hvordan tingene gøres, begynder at flosse. Fire personer, der alle ved, hvad de andre laver, er noget helt andet end fjorten, der ikke ved det.</p>\n<p><strong>Opgave.</strong> Opgaven var at lede den vækst — at få nye folk ind og samtidig holde leveringen disciplineret og få teamet til at trække i samme retning.</p>\n<p><strong>Handling.</strong> Virksomheden gik fra fire til fjorten personer. Ansættelse og onboarding foregik bevidst og ikke i panik, men den større opgave var at indføre de arbejdsformer, der gjorde, at et team af den størrelse ikke snublede over sig selv — agile metoder, ordentlig projektledelse og de vaner, der holder alles arbejde synligt for alle andre. Den tiende og den fjortende nyansatte skulle træde ind i noget, der allerede havde en form, og ikke gætte sig til, hvordan tingene blev gjort.</p>\n<p><strong>Resultat.</strong> Teamet nåede op på fjorten, uden at kvaliteten faldt, og uden at det splintrede i folk, der ikke vidste, hvad de andre var i gang med. Det var den agile tilgang, der holdt en større gruppe produktiv — processen voksede i takt med antallet af medarbejdere i stedet for at halte bagefter.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Agile & Scrum", "Mentoring & coaching", "Projektledelse", "Teamledelse", "Teknisk ledelse", "Projektledelse (Agile)", "Teamopbygning & mentoring", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/managed-4000-computer-repairs/", "url": "https://engineer.company/da/portfolio/managed-4000-computer-repairs/", "title": "Har håndteret 4.000 computerreparationer", "summary": "Håndterede 4.000 computerreparationer med hurtig, effektiv løsning af hardware- og softwareproblemer — pålidelig service.", "content_html": "<p><strong>Situation.</strong> Der kom en konstant strøm af computerreparationer ind — fire tusind i alt over tid — og bag hver eneste stod en kunde, der ventede på at få sin maskine tilbage og komme videre med dagen. Hardwarefejl, softwarefejl, hele blandingen, og kunderne bedømte værkstedet på, hvor hurtigt og hvor ordentligt fejlene blev rettet.</p>\n<p><strong>Opgave.</strong> Opgaven var at styre reparationerne — at sørge for, at problemerne blev løst hurtigt og, lige så vigtigt, korrekt.</p>\n<p><strong>Handling.</strong> Fire tusind computerreparationer blev gennemført — hardware- og softwarefejl blev diagnosticeret, arbejdsgangen blev organiseret, så maskinerne kom igennem i stedet for at hobe sig op, og kvaliteten blev holdt, så en reparation blev udført ordentligt og ikke sendt ud igen for at fejle på ny ugen efter. En reparation, der kommer retur, er værre end en langsom; den koster kunden en tur mere og koster værkstedet tilliden. Derfor betød gennemløbet noget, men aldrig på bekostning af at få det rigtigt første gang.</p>\n<p><strong>Resultat.</strong> Alle fire tusind blev løst, og det gik hurtigt. Folk fik deres maskiner tilbage i fungerende stand og kom videre, og virksomhedens ry for pålidelig service holdt — og i et reparationsværksted er det hele forretningen.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Projektledelse", "Systemadministration", "Hardwarereparation & datagendannelse", "It‑support & helpdesk" ] }, { "id": "https://engineer.company/da/portfolio/designed-30-websites-from-photoshop-to-html/", "url": "https://engineer.company/da/portfolio/designed-30-websites-from-photoshop-to-html/", "title": "Har designet 30 websites fra Photoshop til HTML", "summary": "Designede 30 websites og omsatte Photoshop-designs til HTML — markante, gennemarbejdede og vedligeholdelsesvenlige sider.", "content_html": "<p><strong>Situation.</strong> Kunderne kom med et bestemt udseende for øje — ofte et færdigt visuelt design — og skulle have et website, der blev bygget trofast efter det. Det er i afstanden mellem en designfil og et fungerende site, at meget kvalitet vindes eller tabes: det er let at levere noget, der er nogenlunde rigtigt og alligevel forkert i detaljerne.</p>\n<p><strong>Opgave.</strong> Opgaven var at designe siderne og omsætte designene til nøjagtige og fleksible implementeringer.</p>\n<p><strong>Handling.</strong> Tredive websites blev designet, og Photoshop‑designene blev omsat til HTML i hånden. Trofasthed var standarden — afstandene, typografien og de detaljer, designeren havde i tankerne, ikke en tilnærmelse af dem — men det var fleksibilitet også, for et site, der matcher mockuppen pixel for pixel og så falder fra hinanden, så snart indholdet ændres, er ikke rigtig bygget godt. Målet var derfor markup, der forblev tro mod designet og samtidig var let at vedligeholde bagefter.</p>\n<p><strong>Resultat.</strong> De tredive sites svarede til deres design og forblev nemme at arbejde med — markante, gennemarbejdede websites, der holdt, også første gang nogen redigerede dem. Tro mod designet og stadig til at vedligeholde: det var den balance, der betød noget, og den ramte disse sites.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Frontend‑udvikling", "UX/UI‑design", "Webudvikling", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/delivered-full-progressive-web-app-support/", "url": "https://engineer.company/da/portfolio/delivered-full-progressive-web-app-support/", "title": "Har leveret en fuld Progressive Web App", "summary": "Leverede fuld Progressive Web App-understøttelse — installerbar og brugbar offline — med Workbox runtime-caching via next-pwa.", "content_html": "<p><strong>Situation.</strong> En stor del af platformens brugere er til søs. Søfarende og medarbejdere i marken, på telefonen, ofte offline eller på en dårlig forbindelse — ikke folk, der sidder ved et skrivebord på et stabilt kontor‑wifi. At bygge, som om alle havde et hurtigt netværk hele tiden, ville i det stille have udelukket en stor del af den reelle målgruppe.</p>\n<p><strong>Opgave.</strong> Appen skulle være installerbar som en native app og stadig brugbar, når netværket falder ud.</p>\n<p><strong>Handling.</strong> Den leveres som en fuldgyldig Progressive Web App. Den installeres på hjemmeskærmen med ordentlige ikoner i de forskellige størrelser, en splash‑skærm og temafarver, så den ligner og starter som en app og ikke som et bogmærke. Service workeren er sat op gennem next‑pwa‑pluginet, og Workbox’ runtime‑caching bruger CacheFirst til det, der ikke ændrer sig fra request til request — skrifttyper, billeder, lyd, video, CDN‑ressourcer — sammen med fornuftige cache‑control‑headere i HTTP. Fordi service workers kun for alvor fungerer over HTTPS, blev offline- og installationsadfærden valideret med test over HTTPS på rigtige enheder i stedet for blot at håbe på, at det virkede.</p>\n<p><strong>Resultat.</strong> Brugerne kan installere appen og blive ved med at bruge den offline, og gentagne indlæsninger hentes hurtigt fra cachen i stedet for over nettet. Platformen opfører sig som en native app på det udstyr, brugerne rent faktisk har med sig, hvilket for denne målgruppe ikke er en luksus — det er forskellen på, om appen er brugbar til søs eller ej.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Frontend‑udvikling", "Full stack‑udvikling", "Optimering af ydeevne", "Webudvikling", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/designed-the-products-ui-and-ux-end-to-end/", "url": "https://engineer.company/da/portfolio/designed-the-products-ui-and-ux-end-to-end/", "title": "Har designet hele produktets UI og UX", "summary": "Designede hele produktets UI/UX — directory-grids, kort/tabel-visninger, live-validatorer og breadcrumbs — til tætpakkede maritime data.", "content_html": "<p><strong>Situation.</strong> Platformen præsenterer brugerne for mange forskellige entiteter — professionelle, virksomheder, skibe, jobs, anmeldelser — og brugerfladen skulle være to ting, der trækker i hver sin retning: flot og reelt brugbar. Tæt nok til at vise rigtige maritime data, men ikke så tæt, at den bliver til en mur, man preller af på.</p>\n<p><strong>Opgave.</strong> Produktets UI og UX var ét samlet ansvar fra ende til anden — layoutene, interaktionsmønstrene og de mindre ting, som hvordan man bliver guidet gennem en formular uden at føle sig hakket på.</p>\n<p><strong>Handling.</strong> Et par beslutninger gjorde det meste af arbejdet. Oversigtsgitrene har en knap til at skifte mellem kort og tabel, så man kan bladre visuelt eller skimme en tæt tabel, og appen husker, hvad man valgte. Formularerne har live‑validering af kravene, der står over inputfeltet og viser hver regel i grønt, når den er opfyldt, og i ravgult, når den ikke er, så man bliver vejledt, mens man skriver, i stedet for at blive skældt ud, efter at man har sendt formularen. Navigationen bygger på ensartede breadcrumbs og entitetslayouts i to kolonner, så siderne føles som det samme produkt og ikke som en samling skærme uden indbyrdes sammenhæng. Og tilgangen til fejl er vejledning frem for fejlmeldinger — ingen røde mure, omdirigeringer i stedet for blindgyder, en app, der forsøger at holde brugeren i gang i stedet for at stoppe vedkommende.</p>\n<p><strong>Resultat.</strong> Resultatet er en poleret, ensartet oplevelse, der gør tætte maritime data overskuelige og guider folk gennem de komplicerede dele. Den fremstår gennemtænkt og troværdig, hvilket ikke er kosmetik på en platform, folk bruger til deres egentlige karriere — hvis den så sjusket ud, ville de stole mindre på dataene, og de ville have ret i det.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Frontend‑udvikling", "Produkt & krav", "UX/UI‑design", "Webudvikling", "Produktstrategi & kravspecifikation", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/built-the-entire-core-infrastructure-from-scratch/", "url": "https://engineer.company/da/portfolio/built-the-entire-core-infrastructure-from-scratch/", "title": "Har bygget hele kerneinfrastrukturen fra bunden", "summary": "Designede og byggede som en af de første ansatte hele kerneinfrastrukturen fra bunden for en SaaS-startup inden for grøn energi — grundlaget for hurtig vækst.", "content_html": "<p><strong>Situation.</strong> Der var tale om en SaaS‑startup inden for grøn energi med en lovende idé, men på det tidspunkt stort set uden teknisk fundament. Som en af de første ansatte kom man ind på det stadie, hvor der ikke er noget at vedligeholde, fordi der ikke findes noget — man lægger den grund, alle andre skal stå på.</p>\n<p><strong>Opgave.</strong> Opgaven var at bygge kerneinfrastrukturen og processerne omkring den helt fra bunden.</p>\n<p><strong>Handling.</strong> Hele kerneinfrastrukturen og dens understøttende processer blev designet og bygget — serverne, netværkene, dataflowet, sikkerheden, driftssiden. At gøre det i en startup betyder at træffe beslutninger, der er svære at omgøre senere, så målet var ikke bare at “få noget til at køre”, men at lægge et fundament, der kunne bære en hurtig vækst uden at skulle rives op, så snart virksomheden blev større. Tidlige infrastrukturvalg bliver enten det, der gør det muligt at skalere, eller det, man bruger et år på at rulle tilbage; sigtet var klart den første slags.</p>\n<p><strong>Resultat.</strong> Startuppen stod tilbage med et solidt teknisk fundament, og det var det, der gjorde det muligt for forretningen at vokse hurtigt bagefter. At være den, der bygger det grundlag op fra ingenting, er en særlig slags ansvar — gør man det rigtigt, bemærker ingen det; gør man det forkert, bemærker alle det — og dette grundlag holdt.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Cloud", "DevOps", "Infrastruktur", "Løsningsarkitektur", "Platformarkitektur", "Sikkerhed", "Systemadministration", "Cloud‑infrastruktur & migrering", "DevOps & CI/CD‑automatisering", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/automated-team-collaboration/", "url": "https://engineer.company/da/portfolio/automated-team-collaboration/", "title": "Har automatiseret teamsamarbejde", "summary": "Automatiserede samarbejde, adgangskode-, opgave- og tidsstyring og byggede et semi-automatisk system til projektfremvisning — højere produktivitet.", "content_html": "<p><strong>Situation.</strong> En stor del af teamets driftsarbejde — at koordinere, håndtere adgangskoder, holde styr på opgaver og tid — blev lavet i hånden, og manuel koordinering er en skjult omkostning: man lægger aldrig rigtig mærke til den, men den æder støt timer, der kunne bruges bedre.</p>\n<p><strong>Opgave.</strong> Målet var at automatisere det gentagne driftsarbejde.</p>\n<p><strong>Handling.</strong> De dele, der egnede sig til det, blev automatiseret — teamsamarbejde, adgangskodehåndtering, opgavestyring, tidsstyring — og oven på det et halvautomatisk system til at fremvise projekter. Tanken bag det hele var at tage rutinekoordineringen ud af folks hænder, så den kørte af sig selv, og lade dem bruge den frigjorte opmærksomhed på arbejde, der rent faktisk krævede et menneske. Fremvisningssystemet var den samme tankegang anvendt på noget mere synligt: at præsentationen af arbejdet for det meste skete automatisk og ikke var en manuel pligt hver gang.</p>\n<p><strong>Resultat.</strong> Driftseffektiviteten steg, og teamet blev mere produktivt, fordi den rutinekoordinering, der før krævede konstant menneskelig opmærksomhed, nu i vid udstrækning kørte af sig selv. Den tid, der før sivede ud i pseudoarbejde, gik nu til det egentlige arbejde.</p>\n", "date_published": "2026-10-21T08:22:55+02:00", "date_modified": "2026-10-21T08:22:55+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Projektledelse", "Sikkerhed", "Teknisk ledelse", "DevOps & CI/CD‑automatisering", "Projektledelse (Agile)", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/a-brand-200-times-better-known/", "url": "https://engineer.company/da/portfolio/a-brand-200-times-better-known/", "title": "Et brand 200 gange mere kendt", "summary": "Øgede brandets rækkevidde omkring 200 gange på offline- og onlineplatforme — fra ukendt nykommer til et kendt navn.", "content_html": "<p><strong>Situation.</strong> Engineer ApS var en helt ny konsulentvirksomhed med reel teknisk dybde, men næsten ingen havde hørt om den. Det er en helt særlig form for frustration: kompetencerne er der, og arbejdet ville blive godt, men det betyder ingenting, hvis de kunder og partnere, der ville efterspørge det, ikke ved, at man findes. En ung virksomhed skal ses, før den kan vinde noget som helst.</p>\n<p><strong>Opgave.</strong> Opgaven var at opbygge et brand, som folk rent faktisk ville genkende — både offline og online — og at omsætte virksomhedens tekniske troværdighed til synlig tilstedeværelse på markedet i stedet for at lade den forblive en velbevaret hemmelighed.</p>\n<p><strong>Handling.</strong> Brandet blev bygget op med omtanke og holdt konsekvent. Det begyndte med en klar identitet — en tone, et visuelt sprog og en portefølje, der satte konkrete tekniske resultater forrest i stedet for den vage “vi skaber værdi”-retorik, som alle andre bruger. Derefter blev det rullet ud i de kanaler, der tæller for den type virksomhed — websitet, LinkedIn, GitHub og fysiske arrangementer — og alle kontaktpunkter sagde det samme i stedet for at gå hver sin vej. Den røde tråd var at lade rigtige cases og rigtige resultater gå forrest, så troværdigheden var noget, man kunne se dokumentation for, og ikke blot en påstand.</p>\n<p><strong>Resultat.</strong> Brandets rækkevidde voksede omkring 200 gange på offline- og onlineplatformene — en ukendt nykommer blev et navn, folk rent faktisk genkendte. Og det var ikke rækkevidde for syns skyld: synligheden begyndte at give en jævn strøm af indgående henvendelser og muligheder, som simpelthen ikke havde været der før.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Brand & marketing", "Interessenter & rapportering", "Produkt & krav", "Brand, marketing & SEO", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/twice-the-engagement-from-listening-first/", "url": "https://engineer.company/da/portfolio/twice-the-engagement-from-listening-first/", "title": "Dobbelt så meget engagement, ved at lytte først", "summary": "Fordoblede brandengagement og -loyalitet (+100 %) via analyse af markedstendenser og forbrugeradfærd — et publikum, der kom tilbage.", "content_html": "<p><strong>Situation.</strong> I takt med at synligheden steg, nåede Engineer ApS ud til flere mennesker — men engagementet var tyndt. Folk lagde mærke til virksomheden og gik videre; den tidlige interesse blev ikke til varige relationer. Rækkevidde uden engagement er bare støj, og brandet skabte mere støj end forbindelser.</p>\n<p><strong>Opgave.</strong> Målet var at skabe et dybere engagement og en stærkere loyalitet og at gøre det ved rent faktisk at se på, hvad markedet og målgruppen reagerede på, i stedet for at stole på mavefornemmelsen.</p>\n<p><strong>Handling.</strong> Derfor blev brandet styret af data frem for intuition. Det betød at se på markedstendenserne og på, hvordan målgruppen reelt opførte sig i de forskellige kanaler — ikke på, hvad nogen gik ud fra, at den ville synes om, men på, hvad den påviseligt engagerede sig i. Det viste, hvilke emner og formater der fangede reel opmærksomhed, og indholdet og den opsøgende indsats blev rettet mod dem. Det vigtigste var at stramme feedbacksløjfen: se, hvad der ramte, udgiv mere af samme slags næste gang, og lad hver runde ramme lidt mere præcist end den forrige.</p>\n<p><strong>Resultat.</strong> Engagementet og loyaliteten blev fordoblet — en forbedring på 100 % — med en målgruppe, der vendte tilbage og engagerede sig i stedet for at kaste et enkelt blik og gå videre, og med mærkbart stærkere relationer til potentielle kunder og partnere. Brandet holdt op med at sende ud i tomrummet og begyndte at opbygge noget, der gav noget igen.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Brand & marketing", "Dataanalyse", "Interessenter & rapportering", "Produkt & krav", "Brand, marketing & SEO", "Dataanalyse & BI‑dashboards", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/a-month-of-meter-readings-in-180-milliseconds/", "url": "https://engineer.company/da/portfolio/a-month-of-meter-readings-in-180-milliseconds/", "title": "En måneds måleraflæsninger på 180 millisekunder", "summary": "Byggede en Python/SQL/Bash-pipeline til elforbrug, der samler 30-dages datasæt fra heterogene JSONL-kilder på 180 ms.", "content_html": "<p><strong>Situation.</strong> Virksomheden behandler store mængder heterogene eldata fra flere datakilder, som hver har sin egen rapporteringsfrekvens — fra timeintervaller til intervaller på 15 minutter og i nogle tilfælde uregelmæssige tidsstempler. Den variation gør det svært at skabe et sammenhængende og sammenligneligt datasæt. For at understøtte præcise energianalyser skal dataene normaliseres til ensartede forbrugsværdier pr. time, aggregeret pr. zone.</p>\n<p><strong>Opgave.</strong> Målet var at tage rå hændelsesdata fra et historisk datasæt (i JSONL‑format) og omdanne dem til elforbrugstal afstemt til hele timer, struktureret som én række pr. time og pr. zone. Konkret gik opgaven ud på at:</p>\n<ul>\n<li>afstemme tidsstemplede data til faste timeintervaller,</li>\n<li>aggregere den samlede elproduktion i hvert interval ved at summere værdierne for produktionsmikset,</li>\n<li>indregne den grænseoverskridende udveksling ved at lægge import til og trække eksport fra,</li>\n<li>skrive de endelige værdier ud i et struktureret, skalerbart format, der egner sig til videre analyse.</li>\n<li>Løsningen skulle desuden være effektiv nok til at skalere til store tidsvinduer (30+ dage) og til flere lande/zoner.</li>\n</ul>\n<p><strong>Handling.</strong> Opgaven blev grebet an med tre forskellige varianter af løsningen, implementeret i henholdsvis Python, JQ (til JSON‑behandling på kommandolinjen) og SQL og hver optimeret til sin egen sammenhæng:</p>\n<p>Python blev valgt for sin fleksibilitet, den lette databehandling og evnen til at håndtere transformationer i hukommelsen effektivt. Med rene Python‑funktioner blev der bygget en pipeline, som:</p>\n<ul>\n<li>parsede JSONL‑filerne til strukturerede dataframes</li>\n<li>samplede tidsseriedataene om til timeintervaller</li>\n<li>aggregerede produktionen og beregnede nettoelforbruget (produktion + import − eksport)</li>\n<li>eksporterede resultaterne som CSV eller indlæste dem i en let SQLite‑database, så de kunne efterses.</li>\n</ul>\n<p>JQ blev brugt til en hurtig løsning med minimale afhængigheder til kommandolinjemiljøer, bygget som en kæde af JQ‑filtre.</p>\n<p>I PostgreSQL blev JSONL‑dataene importeret, normaliserede tabeller oprettet og en række SQL‑forespørgsler skrevet.</p>\n<p>For at vurdere ydeevnen blev hver løsning benchmarket med historiske datasæt på både 1 dag og 30 dage.</p>\n<p><strong>Resultat.</strong> Python‑implementeringen viste sig at være den hurtigste og mest skalerbare løsning og klarede:</p>\n<ul>\n<li>1 dags data på blot 34 ms</li>\n<li>30 dages datasæt på 180 ms</li>\n</ul>\n<p>Det bekræftede, at den egnede sig til længere tidsrum med svartider under et sekund. Den gav også klar kode, der var nem at vedligeholde og let kunne udvides til behandling af flere zoner eller integreres i en ETL‑pipeline.</p>\n<p>Samlet set viste tilgangen med flere værktøjer fleksibilitet i valget af værktøjer, stærk ydeevneoptimering og robust håndtering af udfordringerne med tidsafstemning og aggregering i rigtige pipelines for eldata.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "Dataanalyse", "Optimering af ydeevne", "PostgreSQL", "Python", "SQL", "Databaseoptimering", "Dataanalyse & BI‑dashboards", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/improved-gis-map-performance-10x-on-postgresql/", "url": "https://engineer.company/da/portfolio/improved-gis-map-performance-10x-on-postgresql/", "title": "Har forbedret GIS‑kortydeevne 10x med PostgreSQL", "summary": "Gjorde en GIS-kortapplikation 10 gange hurtigere ved at migrere fra MSSQL til PostgreSQL — spatiale forespørgsler fra 2,5 s til under 250 ms.", "content_html": "<p><strong>Situation.</strong> Organisationens geografiske kortapplikation, som understøttede spatiale forespørgsler i realtid for mange brugere, led under alvorlige flaskehalse i ydeevnen. Forsinkelser ved rendering af kortlag og ved forespørgsler på lokationsbaserede data gik ud over både brugeroplevelsen og backend‑tjenesternes pålidelighed. Systemet kørte på en ældre Microsoft SQL Server‑database (MSSQL) uden indbygget geospatial indeksering.</p>\n<p><strong>Opgave.</strong> At forbedre kortapplikationens ydeevne, skalerbarhed og sikkerhed med det konkrete mål at reducere forespørgslernes latens og øge gennemløbet for spatiale operationer.</p>\n<p><strong>Handling.</strong> Der blev gennemført en strategisk databasemigrering fra MSSQL til PostgreSQL med PostGIS‑udvidelsen for at få indbygget geospatial understøttelse.</p>\n<ul>\n<li>Et nyt skema blev designet til spatiale data med GiST- og SP‑GiST‑indeks på geometri- og geografikolonner, så forespørgslerne blev hurtigere.</li>\n<li>Der blev defineret strenge foreign key- og check‑constraints, som sikrer den relationelle integritet og håndhæver valideringsregler for de spatiale koordinater.</li>\n<li>Over 50 millioner spatiale poster blev migreret via ETL‑pipelines med transformationstrin, der bragte dataene i overensstemmelse med de nye SRID‑standarder (EPSG:4326).</li>\n<li>PostgreSQL‑konfigurationsparametre (f.eks. <code>work_mem</code> og <code>effective_cache_size</code>) blev finjusteret for optimal I/O‑ydeevne ved samtidig adgang.</li>\n<li>Der blev implementeret rollebaseret adgangskontrol (RBAC) og row‑level security, så databeskyttelsespolitikkerne blev håndhævet for flere brugergrupper.</li>\n</ul>\n<p><strong>Resultat.</strong> Ydeevnen for spatiale forespørgsler blev 10 gange bedre, og den gennemsnitlige svartid faldt fra 2,5 sekunder til under 250 millisekunder. Backendens CPU‑belastning faldt med 65 %, og systemets tilgængelighed blev bedre i spidsbelastningsperioder. Sikkerheden blev også styrket med granulære adgangspolitikker og valideringsregler i databasen, som mindskede risikoen for korrupte spatiale data.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "GIS / geodata", "Migrering & modernisering", "Optimering af ydeevne", "PostgreSQL", "Sikkerhed", "SQL", "Databasemigrering & modernisering", "Databaseoptimering", "GIS & geospatiale løsninger" ] }, { "id": "https://engineer.company/da/portfolio/resolved-1000-issues-in-geographical-data/", "url": "https://engineer.company/da/portfolio/resolved-1000-issues-in-geographical-data/", "title": "Har løst 1.000 problemer i geografiske data", "summary": "Løste 1.000+ problemer i store mængder geografiske data og tidsseriedata med GDAL, PostGIS, ArcGIS, Mapbox, QGIS og SQL — 35 % hurtigere forespørgsler.", "content_html": "<p><strong>Situation.</strong> Under arbejdet på et stort geospatialt analyseprojekt stødte teamet på talrige uoverensstemmelser og anomalier i de geografiske datasæt og i tidsseriedataene. Problemerne gik ud over nøjagtigheden af de spatiale analyser og beslutningsværktøjer, som blev brugt i flere afdelinger.</p>\n<p><strong>Opgave.</strong> Ansvaret omfattede at identificere, løse og optimere over 1.000 datakvalitetsproblemer i de komplekse datasæt for at sikre integriteten og ydeevnen i de applikationer og visualiseringer, der byggede videre på dataene.</p>\n<p><strong>Handling.</strong> Spatiale fejl blev systematisk diagnosticeret og rettet med en kombination af værktøjer, herunder GDAL, QGIS og ArcGIS, og tilbagevendende datarensningsopgaver blev strømlinet med automatiserede arbejdsgange i Bash‑scripts. PostGIS håndterede avancerede spatiale forespørgsler og spatial indeksering, og der blev skrevet robuste procedurer i PL/pgSQL og Transact‑SQL, som håndterede og transformerede både geografiske og tidsmæssige data i PostgreSQL- og SQL Server‑databaserne. Derudover blev de rensede data integreret i interaktive visualiseringer med Mapbox, så slutbrugerne fik lettere adgang til dem.</p>\n<p><strong>Resultat.</strong> Over 1.000 kritiske problemer blev løst, og datanøjagtigheden og behandlingshastigheden blev markant bedre. Det bidrog direkte til, at køretiden for spatiale forespørgsler faldt med 35 %, og gjorde teamets spatiale analyser mere pålidelige. Arbejdet sikrede, at der konsekvent var data af høj kvalitet klar til brug i analyser og rapportering, til støtte for strategiske beslutninger i hele organisationen.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data engineering", "Data governance", "Databaser", "Datapipelines (ETL/ELT)", "GIS / geodata", "Optimering af ydeevne", "PostgreSQL", "SQL", "Data governance & datakvalitet", "GIS & geospatiale løsninger", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/built-the-companys-first-observability-dashboard/", "url": "https://engineer.company/da/portfolio/built-the-companys-first-observability-dashboard/", "title": "Har bygget det første observerbarhedsdashboard", "summary": "Byggede virksomhedens første dashboard for observerbarhed med indsigt i realtid — teams opdagede og løste hændelser 40 % hurtigere.", "content_html": "<p><strong>Situation.</strong> Virksomheden havde svært ved at overvåge systemernes ydeevne i realtid, og det førte ofte til, at der blev reageret for sent på driftshændelser, og at indblikket i infrastrukturens tilstand var begrænset. Der fandtes ingen central løsning, hvor teamene kunne få indsigt i driftsmetrikkerne.</p>\n<p><strong>Opgave.</strong> Opgaven var at udvikle en løsning, som gjorde det muligt for både tekniske og ikke‑tekniske interessenter at følge de vigtigste systemmetrikker i realtid, med fokus på tilgængelighed, overskuelighed og proaktiv opdagelse af problemer.</p>\n<p><strong>Handling.</strong> Virksomhedens første observability‑dashboard blev designet og implementeret. Det indsamlede alle væsentlige systemmetrikker — f.eks. CPU, RAM, HDD, temperatur og meget mere — fra Linux‑fjernservere via SSH. Selv Docker‑containere blev overvåget på den måde. Senere blev en anden version designet og implementeret med Grafana og Prometheus, som gav mere avancerede muligheder for visualisering og overvågning. I samarbejde med DevOps- og udviklingsteamene blev de kritiske metrikker fastlagt, f.eks. CPU‑udnyttelse, hukommelsesforbrug, oppetid for tjenesterne og API‑latens. Datapipelines blev konfigureret til at indsamle og behandle ydeevnemetrikker fra forskellige systemer, og dashboardet blev sat op på en stor tv‑skærm på kontoret for maksimal synlighed. Der blev også integreret alarmer ved overskridelse af tærskelværdier, så der kunne handles med det samme.</p>\n<p><strong>Resultat.</strong> Dashboardet gjorde systemerne markant mere gennemsigtige og forkortede reaktionstiden på driftsproblemer. Teamene kunne opdage og løse driftshændelser 40 % hurtigere. Det skabte også en kultur med fælles ejerskab for systemernes tilstand, fordi ydeevnedata blev tilgængelige for alle på kontoret, og bidrog i sidste ende til et mere stabilt og effektivt produktionsmiljø.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Containere (Docker/Kubernetes)", "Dataanalyse", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Overvågning & observerbarhed", "Dataanalyse & BI‑dashboards", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/delivered-8-power-bi-projects-with-manuals/", "url": "https://engineer.company/da/portfolio/delivered-8-power-bi-projects-with-manuals/", "title": "Har leveret 8 Power BI‑projekter med manualer", "summary": "Leverede 8 Power BI-analyseprojekter (Node.js API, Python FastAPI) med komplette manualer — 60 % mere effektiv rapportgenerering.", "content_html": "<p><strong>Situation.</strong> Organisationen havde brug for dynamisk, visuel indsigt i komplekse datasæt om elnettets ydeevne og nøgletal for den geografiske fordeling for at understøtte beslutninger i både tekniske og strategiske teams.</p>\n<p><strong>Opgave.</strong> At udvikle interaktive dashboards og rapporteringsløsninger, der effektivt kunne præsentere både realtidsdata og historiske geografiske og elektriske data, så interessenterne hurtigt kunne få øje på tendenser, anomalier og nøgletal.</p>\n<p><strong>Handling.</strong> Microsoft Power BI blev integreret med en skræddersyet backend‑stak med et NodeJS‑API og Python FastAPI for at strømline indlæsning, transformation og visualisering af data. Der blev designet og implementeret dashboards med kortvisualiseringer, nøgletal for energiforbrug, sporing af strømafbrydelser og indikatorer for nettets effektivitet. Der blev også udviklet genanvendelige skabeloner og detaljeret dokumentation, som understøttede skalerbarhed og brugervenlighed.</p>\n<p><strong>Resultat.</strong> Der blev leveret 8 dataanalyseprojekter, som gjorde rapportgenereringen 60 % mere effektiv og satte tværfaglige teams i stand til at træffe hurtigere, datadrevne beslutninger. Interessenterne meldte om en markant bedre forståelse af elnettets regionale ydeevne og en mere præcis ressourceplanlægning.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Data engineering", "Dataanalyse", "Dokumentation", "GIS / geodata", "Produkt & krav", "Python", "Backend- & API‑udvikling", "Dataanalyse & BI‑dashboards", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/released-500-electricity-and-gis-data-reports/", "url": "https://engineer.company/da/portfolio/released-500-electricity-and-gis-data-reports/", "title": "Har udgivet 500 el- og GIS‑datarapporter", "summary": "Udgav 500 rapporter om el- og GIS-dataanalyse — en tværgående reference for load balancing, energieffektivitet og anomalidetektion.", "content_html": "<p><strong>Situation.</strong> Teamet arbejdede med enorme datasæt fra fjernaflæste elmålere installeret i flere geografiske regioner. Målerne leverede detaljerede tidsseriedata om elforbrug, som energianalytikere, ingeniører og regionale planlæggere brugte til operationelle og strategiske beslutninger.</p>\n<p><strong>Opgave.</strong> Ansvaret omfattede at udarbejde analytiske rapporter af høj kvalitet, som var gennemsigtige og reproducerbare, og som kunne afdække mønstre i energiforbruget, opdage anomalier og identificere regionale forbrugstendenser — samtidig med at ikke‑tekniske interessenter let kunne fortolke og genbruge resultaterne.</p>\n<p><strong>Handling.</strong> Der blev udarbejdet og leveret over 500 dybdegående dataanalyserapporter, hvor alt dataudtræk, al transformation og al analyse blev udført i ren SQL direkte i cloudbaserede miljøer som PostgreSQL og BigQuery. Dataene omfattede geografiske koordinater, måler‑id’er, tidsstemplet energiforbrug og miljømetadata. SQL‑scripts med CTE’er, window functions, subqueries og geospatiale joins gjorde behandlingen skalerbar og effektiv.</p>\n<p>Hver rapport indeholdt kommenteret SQL‑kode, så kolleger og samarbejdspartnere fuldt ud kunne reproducere og efterprøve analysen, og det forkortede markant den tid, opfølgende analyser krævede. Der blev også tilføjet noter om fejlfinding, og almindelige datakvalitetsproblemer blev dokumenteret, f.eks. manglende GPS‑koordinater eller korrupte målerværdier, med anbefalede fremgangsmåder for håndteringen.</p>\n<p><strong>Resultat.</strong> Rapporterne blev et fast referencepunkt i alle afdelinger og hjalp med regional belastningsudligning, planlægning af energieffektivitet og opdagelse af anomalier. Fordi arbejdet sikrede fuld gennemsigtighed og reproducerbarhed, styrkede det interessenternes tillid til dataene og bidrog til mere præcise prognosemodeller og en forbedring af effektiviteten i driftsplanlægningen på 10–15 %.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Data governance", "Data warehousing", "Databaser", "Dataanalyse", "Dokumentation", "GIS / geodata", "Interessenter & rapportering", "PostgreSQL", "SQL", "Data governance & datakvalitet", "Dataanalyse & BI‑dashboards", "GIS & geospatiale løsninger" ] }, { "id": "https://engineer.company/da/portfolio/ran-100-postgresql-ms-sql-bigquery-warehouses/", "url": "https://engineer.company/da/portfolio/ran-100-postgresql-ms-sql-bigquery-warehouses/", "title": "Har drevet 100 data warehouse‑databaser", "summary": "Stod for arkitektur og drift af 100 data warehouse-databaser (PostgreSQL, MS SQL, BigQuery) til GIS- og tidsseriedata — 40–60 % hurtigere forespørgsler.", "content_html": "<p><strong>Situation.</strong> I en hurtigt voksende teknologivirksomhed var der et kritisk behov for at oprette, administrere og optimere en bred portefølje af over 100 databaser i PostgreSQL, Microsoft SQL Server og Google BigQuery. Databaserne håndterede primært komplekse datasæt, herunder data til geografiske informationssystemer (GIS) (f.eks. spatiale koordinater og lokationsbaserede analyser) og tidsseriedata (f.eks. sensoraflæsninger, logs og realtidsmetrikker). Den eksisterende infrastruktur havde problemer med skalerbarhed, forespørgslernes ydeevne og datakonsistens, særligt i takt med at datamængden voksede eksponentielt. Organisationen havde brug for en robust arkitektur, der kunne sikre pålidelige data og nedbringe driftsomkostningerne.</p>\n<p><strong>Opgave.</strong> Det primære mål var at designe, implementere og administrere et skalerbart data warehouse‑økosystem med høj ydeevne, skræddersyet til GIS- og tidsseriedata. Det omfattede:</p>\n<ul>\n<li>Håndtering af flaskehalse i ydeevnen for komplekse tidsbaserede og spatiale forespørgsler.</li>\n<li>Sikring af skalerbarhed, så de voksende datamængder kunne håndteres, uden at det gik ud over omkostningseffektiviteten.</li>\n<li>Samarbejde med tværfaglige teams (f.eks. data scientists og produktchefer) om at afstemme databasedesignet med forretningens behov.</li>\n</ul>\n<p><strong>Handling.</strong> Arbejdet fordelte sig på disse områder:</p>\n<ol>\n<li>Design af skalerbare arkitekturer:</li>\n</ol>\n<ul>\n<li>Der blev oprettet normaliserede og denormaliserede skemaer til PostgreSQL og SQL Server med spatial indeksering (f.eks. PostGIS til PostgreSQL) og tidsseriepartitionering for at optimere forespørgslernes ydeevne.</li>\n<li>BigQuerys tidspartitionerede og clustrede tabeller blev brugt til effektiv håndtering af store mængder tidsseriedata.</li>\n</ul>\n<ol start=\"2\">\n<li>Indførelse af optimeringsstrategier:</li>\n</ol>\n<ul>\n<li>Der blev indført teknikker til forespørgselsoptimering som indeksering, materialiserede views og caching for at reducere latensen i GIS- og tidsserieforespørgsler.</li>\n<li>Datakomprimering og kolonnebaseret lagring i BigQuery blev taget i brug for at minimere lageromkostningerne og øge scanningshastigheden.</li>\n</ul>\n<ol start=\"3\">\n<li>Samarbejde om integration på tværs af platforme:</li>\n</ol>\n<ul>\n<li>Best practices for datamodellering af GIS- og tidsseriedata blev dokumenteret som rettesnor for teamene i fremtidige projekter.</li>\n</ul>\n<p><strong>Resultat.</strong> Initiativerne førte til markante forbedringer:</p>\n<ul>\n<li>Bedre ydeevne: Svartiderne for forespørgsler på GIS- og tidsseriedata faldt med 40–60 %, hvilket gav hurtigere analyser og beslutninger.</li>\n<li>Driftssikkerhed: Automatiseret overvågning reducerede nedetiden med 50 %, og standardiserede processer øgede teamets produktivitet og reducerede antallet af fejl.</li>\n<li>Forretningsmæssig effekt: Den optimerede infrastruktur gjorde det muligt for virksomheden at lancere nye datadrevne produkter (f.eks. dashboards med realtidsanalyser) og opfylde de regulatoriske krav til data governance.</li>\n</ul>\n<p>Arbejdet styrkede organisationens evne til at håndtere komplekse dataudfordringer.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Data engineering", "Data governance", "Data warehousing", "Databaser", "Driftssikkerhed & backup", "GIS / geodata", "Optimering af ydeevne", "Platformarkitektur", "PostgreSQL", "SQL", "Databaseadministration (DBA)", "Databasedesign & datamodellering", "Databaseoptimering", "Design af data warehouse", "GIS & geospatiale løsninger" ] }, { "id": "https://engineer.company/da/portfolio/ran-10-postgresql-and-ms-sql-servers-on-ubuntu/", "url": "https://engineer.company/da/portfolio/ran-10-postgresql-and-ms-sql-servers-on-ubuntu/", "title": "Har drevet 10 PostgreSQL- og MS SQL‑servere", "summary": "Designede og vedligeholdt 10 PostgreSQL- og MS SQL-servere på Ubuntu Linux VPS med 99,9 % oppetid og 30 % hurtigere forespørgsler efter tuning.", "content_html": "<p><strong>Situation.</strong> Rollen som DevOps‑ingeniør i en mellemstor teknologivirksomhed gik ud på at administrere og optimere databaseinfrastrukturen, så den kunne understøtte en voksende brugerbase og forretningskritiske applikationer. Organisationen var stærkt afhængig af PostgreSQL og Microsoft SQL Server til datalagring og analyse, og det stillede krav om høj tilgængelighed, skalerbarhed og sikkerhed.</p>\n<p><strong>Opgave.</strong> Det primære ansvar var at designe, udrulle, konfigurere og vedligeholde 10 PostgreSQL- og MS SQL Server‑instanser i Ubuntu Linux VPS‑miljøer. Det omfattede at sikre optimal ydeevne, indføre robuste sikkerhedsprotokoller og etablere proaktiv overvågning for at forebygge nedbrud. Derudover omfattede opgaven at skalere infrastrukturen til fremtidig vækst, samtidig med at omkostningerne blev holdt nede, og branchestandarderne blev overholdt.</p>\n<p><strong>Handling.</strong> 1. Udrulning og konfiguration:</p>\n<ul>\n<li>PostgreSQL 14 og MS SQL Server 2019 blev installeret og konfigureret på VPS‑instanser med Ubuntu 20.04 LTS, så de var kompatible med virksomhedens applikationer.</li>\n<li>Der blev sat automatiske backups op med <code>pg_dump</code> til PostgreSQL og SQL Server Agent‑jobs til MS SQL, med opbevaringspolitikker og ekstern lagring.</li>\n<li>Serverkonfigurationerne blev optimeret (f.eks. hukommelsesallokering, caching af databaseforespørgsler og connection pooling) for at forbedre forespørgslernes ydeevne og reducere latensen.</li>\n</ul>\n<ol start=\"2\">\n<li>\n<p>Overvågning og vedligeholdelse:</p>\n<ul>\n<li>Der blev indført overvågningsværktøjer som Prometheus og Grafana til at følge CPU, hukommelse, disk‑I/O og forespørgslernes ydeevne i realtid.</li>\n<li>Databaserne og Ubuntu‑styresystemet blev løbende patchet og opdateret for at lukke sikkerhedshuller og sikre compliance.</li>\n<li>Der blev skrevet skræddersyede scripts til loganalyse.</li>\n</ul>\n</li>\n<li>\n<p>Sikkerhed og skalerbarhed:</p>\n<ul>\n<li>Der blev konfigureret firewalls (UFW) og håndhævet rollebaseret adgangskontrol (RBAC) for at beskytte følsomme data.</li>\n<li>Procedurer for disaster recovery blev dokumenteret, herunder point‑in‑time‑gendannelse og failover.</li>\n</ul>\n</li>\n</ol>\n<p><strong>Resultat.</strong> - 99,9 % oppetid på alle 10 databaseservere.</p>\n<ul>\n<li>30 % kortere svartider på forespørgsler takket være konfigurationstuning og indeksoptimering, som forbedrede applikationernes ydeevne.</li>\n<li>50 % mindre manuelt vedligeholdelsesarbejde takket være automatisering, hvilket frigjorde over 10 timer om måneden til strategiske projekter.</li>\n<li>Skalering af infrastrukturen til en stigning i brugertrafikken på 40 % uden forringelse af tjenesterne, hvilket bidrog til 20 % omsætningsvækst i det følgende kvartal.</li>\n<li>Anerkendelse fra CTO’en for indførelsen af best practices for sikkerhed, som forhindrede potentielle databrud.</li>\n</ul>\n<p>Erfaringen befæstede en dyb ekspertise i databaseadministration, DevOps‑automatisering og infrastrukturoptimering og gav pålidelige, sikre og skalerbare løsninger til komplekse virksomhedsmiljøer.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Databaser", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Optimering af ydeevne", "Overvågning & observerbarhed", "PostgreSQL", "Sikkerhed", "Systemadministration", "Databaseadministration (DBA)", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/accelerated-postgresql-performance-by-10x/", "url": "https://engineer.company/da/portfolio/accelerated-postgresql-performance-by-10x/", "title": "Har gjort PostgreSQL 10 gange hurtigere", "summary": "Gjorde PostgreSQL 10 gange hurtigere med indeksering, partitionering og forespørgselstuning af bruger-, tenant-, geo- og tidsseriedata — 40 % færre ressourcer.", "content_html": "<p><strong>Situation.</strong> PostgreSQL‑databasen betjente en mellemstor applikation, der håndterede brugerkonti, tenant‑oplysninger, geografiske data (f.eks. lokationer og regioner) og daglige målinger af elforbrug. Systemet kørte med lav trafik, men efterhånden som datamængderne voksede, opstod der problemer med ydeevnen. Forespørgsler på geospatiale data og tidsseriebaserede ellogs blev langsomme, og svartiderne svingede. Manglende optimeret indeksering, fragmenterede forespørgsler og upartitionerede tabeller forværrede problemet og skabte flaskehalse i kritiske funktioner som brugerautentifikation, tenant‑administration og datarapportering.</p>\n<p><strong>Opgave.</strong> Målet var at forbedre databasens ydeevne 10 gange uden at bygge den eksisterende arkitektur om. Fokus lå på at optimere udførelsen af forespørgsler, reducere latensen og sikre skalerbarhed til fremtidig datavækst. De vigtigste prioriteter var kortere svartider for geospatiale og tidsbaserede forespørgsler, mindre konkurrence om ressourcerne og bevaret dataintegritet, mens ændringerne blev gennemført.</p>\n<p><strong>Handling.</strong> 1. <strong>Indeksering:</strong> Kolonner, der ofte blev forespurgt (f.eks. bruger‑id’er, tenant‑id’er og geospatiale koordinater), blev analyseret, og der blev oprettet målrettede indeks. Til geospatiale data blev der tilføjet GiST‑indeks for at gøre spatiale forespørgsler hurtigere. Til filtre på flere kolonner, f.eks. tenant‑id kombineret med datointervaller for eldata, blev der indført sammensatte indeks.\n2. <strong>Partitionering:</strong> Tabellen med eldata blev range‑partitioneret efter tid og opdelt pr. dag/måned. Det mindskede den datamængde, forespørgslerne skulle gennemsøge, og gjorde scanningerne mere effektive. De geografiske data blev hash‑partitioneret for at fordele belastningen jævnt mellem noderne.\n3. <strong>Optimering af forespørgsler:</strong> Komplekse forespørgsler blev omskrevet for at undgå fulde tabelscanninger, med CTE’er (Common Table Expressions) og materialiserede views til ofte anvendte datasæt. Med værktøjet <code>EXPLAIN ANALYZE</code> blev ineffektive joins og subqueries identificeret og omstruktureret, så de fik bedre udførelsesplaner. Derudover blev caching af forespørgsler og connection pooling konfigureret for at reducere overhead.</p>\n<p><strong>Resultat.</strong> Efter optimeringen blev svartiderne for forespørgsler 10 gange bedre, og kritiske operationer (f.eks. brugerautentifikation og geospatiale opslag) blev udført på millisekunder. Databasens ressourceforbrug faldt med 40 %, så systemet kunne håndtere større datamængder uden at miste ydeevne. Brugerne meldte om en mere glidende oplevelse, og systemet blev mere skalerbart med henblik på fremtidig vækst. Ændringerne mindskede også behovet for hardwareopgraderinger, hvilket sparede omkostninger og sikrede pålideligheden på lang sigt. Projektet viste, hvordan strategisk indeksering, partitionering og finpudsning af forespørgsler kan forvandle selv systemer med lav belastning til effektive og fremtidssikre databaser.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Data engineering", "Databaser", "GIS / geodata", "Optimering af ydeevne", "PostgreSQL", "SQL", "Databaseadministration (DBA)", "Databaseoptimering" ] }, { "id": "https://engineer.company/da/portfolio/automated-gis-saas-deployment-with-github-actions/", "url": "https://engineer.company/da/portfolio/automated-gis-saas-deployment-with-github-actions/", "title": "Har automatiseret GIS‑SaaS‑udrulning med CI/CD", "summary": "Automatiserede udrulning af GIS-SaaS, databehandling og rapportering med GitHub Actions CI/CD, Python, Bash og SQL — kortere, pålidelige releases.", "content_html": "<p><strong>Situation.</strong> Udrulningen af GIS SaaS‑appen, behandlingen af dens data og udarbejdelsen af rapporterne var alt sammen manuelle trin — og manuelle trin er både langsomme og lumsk farlige. Hver release slugte udviklertid og rummede risikoen for en fejl, og det tilbagevendende arbejde med data og rapporter tærede på kapaciteten uge efter uge.</p>\n<p><strong>Opgave.</strong> Opgaven var at automatisere hele vejen fra kode til produktion og desuden den tilbagevendende databehandling og rapportering — med det mål, at releases skulle være hurtige, sikre og gentagelige i stedet for et omhyggeligt manuelt ritual hver gang.</p>\n<p><strong>Handling.</strong> Hele vejen blev automatiseret. Pipelines i GitHub Actions overtog cyklussen med test, build og udrulning, så en release ikke længere afhang af, at nogen huskede trinnene. Den tilbagevendende databehandling og rapporterne blev flyttet over i planlagte jobs i Python, Bash og SQL, så de kørte af sig selv i stedet for at være en pligt, der hvilede på nogen. Konfigurationen og hemmelighederne blev standardiseret, så alle miljøer opførte sig ens — og det er netop det, der gør det af med “det virker på min maskine”-overraskelserne, for så findes der ikke længere en “min maskine”, der adskiller sig fra produktion.</p>\n<p><strong>Resultat.</strong> Udrulning, databehandling og rapportering kørte nu automatisk og pålideligt, det manuelle slid forsvandt fra teamets bord, og releasecyklussen blev kortere. Teamet kunne bruge sin opmærksomhed på produktet i stedet for på driften omkring det.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "Data engineering", "Datapipelines (ETL/ELT)", "DevOps", "Driftssikkerhed & backup", "GIS / geodata", "Python", "SQL", "DevOps & CI/CD‑automatisering", "GIS & geospatiale løsninger", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/automated-100-critical-data-backups-using-barman/", "url": "https://engineer.company/da/portfolio/automated-100-critical-data-backups-using-barman/", "title": "Har automatiseret 100 kritiske databackups", "summary": "Automatiserede 100 kritiske databackups med Barman, Google Cloud, Bash og Python — gendannelse af data blev et testet faktum.", "content_html": "<p><strong>Situation.</strong> De kritiske GIS‑data, applikationsinstanserne og databaserne lå spredt på flere systemer, og deres backups var uensartede og delvis manuelle. For et produkt, der lever af sine data, er det ikke en risiko, man kan lade ligge — og den dag, man rent faktisk får brug for en backup, er netop den værste dag at opdage, at den var ufuldstændig.</p>\n<p><strong>Opgave.</strong> Opgaven var at garantere, at alle de kritiske data kunne gendannes, og det betød, at der skulle automatiseres omfattende, verificerede backups i hele driftsmiljøet — og “verificerede” er det ord, der betyder noget.</p>\n<p><strong>Handling.</strong> Backupordningen blev bygget fra ende til anden. Hundrede kritiske databackups blev automatiseret med Barman på PostgreSQL‑siden og Google Cloud til de eksterne kopier, og det hele blev orkestreret og valideret med Bash og Python — for en backup, man har taget, men aldrig tjekket, er ikke rigtig en backup, men et håb. Derfor var der opbevaringspolitikker, der holdt dem aktuelle, og integritetstjek, der bekræftede, at hver enkelt faktisk var i orden og ikke bare fandtes.</p>\n<p><strong>Resultat.</strong> Backups kørte automatisk og kunne verificeres for alle databaser, og dermed gik gendannelse af data fra at være en antagelse til at være afprøvet. En stor driftsrisiko blev fjernet fra forretningen og erstattet af en gendannelsesvej, man faktisk kunne stole på — forskellen var, at den her var tjekket og ikke bare konfigureret.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "Databaser", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "PostgreSQL", "Python", "Backup & disaster recovery", "Databaseadministration (DBA)", "DevOps & CI/CD‑automatisering" ] }, { "id": "https://engineer.company/da/portfolio/ran-20-docker-apps-on-google-cloud-and-aws/", "url": "https://engineer.company/da/portfolio/ran-20-docker-apps-on-google-cloud-and-aws/", "title": "Har drevet 20 Docker‑apps på Google Cloud og AWS", "summary": "Udrullede og vedligeholdt 20 containeriserede Docker-apps på Google Cloud og AWS — fejlsøgning med Podman og Kubernetes for stabile releases.", "content_html": "<p><strong>Situation.</strong> Der var et voksende antal containeriserede applikationer — herunder nogle R‑baserede analyseapps — som kørte i både Google Cloud og AWS. Fordelt på to clouds og et par container‑runtimes skulle de udrulles ensartet, og fejl skulle hurtigt kunne diagnosticeres, når noget gik galt — og det er sværere, end det lyder, når ikke to miljøer er helt ens.</p>\n<p><strong>Opgave.</strong> Opgaven var at udrulle og vedligeholde disse workloads pålideligt og at kunne diagnosticere problemer hurtigt, uanset runtime og cloud.</p>\n<p><strong>Handling.</strong> Containermiljøet blev administreret i begge clouds — tyve Docker‑applikationer blev udrullet og vedligeholdt med ensartet konfiguration og overvågning, så de ikke hver især blev deres eget snefnug. Når noget gik galt, foregik fejlfindingen med Podman og fejlsøgningen af orkestreringen med Kubernetes. De R‑baserede analyseapps fik særlig opmærksomhed i produktionsmiljøerne i Google Cloud og AWS og blev holdt stabile og reproducerbare, og det betyder noget for analyser — et resultat, man ikke kan reproducere, er ikke meget værd.</p>\n<p><strong>Resultat.</strong> Containermiljøet kørte pålideligt i begge clouds, problemer blev diagnosticeret hurtigere, og udrulningerne forblev stabile og reproducerbare. De applikationer, produktet var afhængigt af, forblev pålidelige, uanset hvilken cloud de tilfældigvis kørte i.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "Containere (Docker/Kubernetes)", "Dataanalyse", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Cloud‑infrastruktur & migrering", "Containerisering & orkestrering", "DevOps & CI/CD‑automatisering" ] }, { "id": "https://engineer.company/da/portfolio/managed-30-ubuntu-linux-vps-instances/", "url": "https://engineer.company/da/portfolio/managed-30-ubuntu-linux-vps-instances/", "title": "Har administreret 30 Ubuntu Linux VPS‑instanser", "summary": "Administrerede 30 Ubuntu Linux VPS-instanser med testede disaster recovery-strategier og solid netværksdrift — et robust fundament at vokse på.", "content_html": "<p><strong>Situation.</strong> Virksomheden kørte på en flåde af Ubuntu Linux VPS‑instanser, hvis opsætning var vokset organisk over tid — en høflig måde at sige, at den havde hobet sig op i stedet for at være blevet designet. Det efterlod huller: uensartede konfigurationer og ordninger for gendannelse og netværk, der mere var historiske tilfældigheder end en plan.</p>\n<p><strong>Opgave.</strong> Opgaven var at få flåden under ordentlig styring, styrke disaster recovery og gøre netværkskonfigurationen ensartet og fornuftig på alle instanser.</p>\n<p><strong>Handling.</strong> De tredive instanser kom under bevidst styring og blev behandlet som én sammenhængende flåde i stedet for tredive individuelle kæledyr. Der kom ægte disaster recovery på plads: backups og gendannelsesprocedurer, der rent faktisk blev testet, for en utestet gendannelse er bare en teori. Netværkskonfigurationen blev standardiseret med henblik på sikkerhed og ydeevne, så alle instanser fulgte den samme hærdede baseline i stedet for det, de tilfældigvis var endt med.</p>\n<p><strong>Resultat.</strong> Infrastrukturen blev robust og ensartet, med testede veje til gendannelse og et netværk, man kunne stole på. Risikoen for nedetid faldt, og forretningen fik et solidt fundament at vokse på i stedet for et kludetæppe, der hele tiden skulle passes og plejes.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Sikkerhed", "Systemadministration", "Backup & disaster recovery", "Netværk & VPN‑opsætning" ] }, { "id": "https://engineer.company/da/portfolio/cut-operational-risk-with-grafana-and-prometheus/", "url": "https://engineer.company/da/portfolio/cut-operational-risk-with-grafana-and-prometheus/", "title": "Har reduceret driftsrisici med Grafana", "summary": "Byggede et overvågningsdashboard med Grafana + Prometheus, der fanger problemer, før de eskalerer — fra brandslukning til forebyggelse.", "content_html": "<p><strong>Situation.</strong> Problemer blev som regel først opdaget, når de allerede havde ramt brugerne, fordi der ikke fandtes ét samlet overblik over systemernes tilstand. Uden det overblik var teamet konstant på bagkant — det reagerede på ting, der allerede var gået galt, i stedet for at se dem komme.</p>\n<p><strong>Opgave.</strong> Målet var at mindske driftsrisikoen ved at give teamet indblik i realtid i de systemer, det var afhængigt af.</p>\n<p><strong>Handling.</strong> Observability‑laget blev bygget ud: et overvågningsdashboard i Grafana og Prometheus, instrumentering af de centrale tjenester og — det, der virkelig betyder noget — metrikker med reel betydning i stedet for pyntetal, der ser travle ud, men intet fortæller. Derefter blev der sat alarmtærskler på dem, og alarmerne dukkede op dér, hvor teamet rent faktisk så dem og kunne nå at handle, mens der stadig var tid.</p>\n<p><strong>Resultat.</strong> Problemerne blev nu fanget og håndteret, før de eskalerede, og systemerne blev mere pålidelige. Teamet gik fra reaktiv brandslukning til en roligere og mere proaktiv drift, hvor problemerne blev fanget, mens de endnu var så små, at de var kedelige.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Overvågning & observerbarhed", "DevOps & CI/CD‑automatisering", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/led-a-gis-map-application-to-10x-revenue-growth/", "url": "https://engineer.company/da/portfolio/led-a-gis-map-application-to-10x-revenue-growth/", "title": "Har ført en GIS‑kortapp til 10x omsætningsvækst", "summary": "Ledede udviklingen af en GIS-kortapplikation, der blev en hjørnesten i platformen og øgede omsætningen 10 gange via mersalg og datalicensering.", "content_html": "<p><strong>Situation.</strong> Kerneudfordringen var at bygge en platform, der kunne spore, overvåge og optimere vedvarende energianlæg som solpaneler og vindmøller. Den oprindelige løsning manglede dog robuste geospatiale funktioner, og det gjorde det svært for kunderne at visualisere anlæggenes placering, analysere spatiale data eller omsætte dem til brugbar indsigt. Ledelsen så dette hul og prioriterede at udvikle en GIS‑kortapplikation (geografisk informationssystem) for at øge platformens værdi og imødekomme kundernes skiftende behov.</p>\n<p><strong>Opgave.</strong> Opgaven var at lede udviklingen af GIS‑applikationen, en afgørende komponent for at differentiere produktet på det konkurrenceprægede marked for grøn energi. Rollen rakte ud over softwareudvikling og omfattede funktionerne som teknisk leder, DevOps‑ingeniør, dataingeniør og SRE (Site Reliability Engineer), så løsningen fulgte virksomhedens vækstkurs. Målet var at skabe et skalerbart og intuitivt GIS‑værktøj, der var problemfrit integreret med SaaS‑platformen, så brugerne kunne visualisere anlæggenes placering, følge nøgletal for ydeevnen og bruge spatiale data i deres beslutninger. Det krævede en balance mellem teknisk innovation og de begrænsninger, der følger med en startup i vækst, samtidig med at produktet skulle kunne udvikle sig i takt med virksomheden.</p>\n<p><strong>Handling.</strong> Arbejdet begyndte med at definere applikationens kernefunktioner sammen med interessenterne med fokus på integration med den eksisterende SaaS‑platform og visualisering af data i realtid. Teamet var lille, så arkitekturen blev designet modulært med open source‑GIS‑biblioteker for at holde systemet let og skalerbart. Der blev også indført bygge- og udrulningspipelines, automatiseret provisionering af infrastruktur og overvågningsværktøjer for at sikre pålideligheden. Efterhånden som teamet voksede, blev nye udviklere mentoreret, det tværfaglige samarbejde fremmet og brugerfeedback prioriteret, så applikationen kunne forfines iterativt. Med tiden udviklede GIS‑værktøjet sig fra en simpel prototype til en gennemarbejdet platform med avancerede analyser og skræddersyede dashboards, der imødekom kundernes krav.</p>\n<p><strong>Resultat.</strong> GIS‑applikationen blev en hjørnesten i SaaS‑platformen og øgede omsætningen 10 gange gennem mersalg, datalicenser og nye kunder. Dens evne til at visualisere grønne energianlæg i realtid forbedrede kundernes driftseffektivitet, og den løbende finpudsning gjorde værktøjet til et primært dataaktiv. Projektets succes styrkede ikke kun virksomhedens omdømme i sektoren for vedvarende energi, men viste også værdien af en tværfaglig tilgang i et startup‑miljø i højt tempo. Ved at forene teknisk ekspertise med strategisk vision blev GIS‑applikationen en afgørende konkurrencefordel og en drivkraft for langsigtet vækst og innovation.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "DevOps", "Driftssikkerhed & backup", "Full stack‑udvikling", "GIS / geodata", "Løsningsarkitektur", "Mentoring & coaching", "Platformarkitektur", "Produkt & krav", "Teamledelse", "Teknisk ledelse", "Full stack‑produktudvikling", "GIS & geospatiale løsninger", "Platform- & løsningsarkitektur", "Produktstrategi & kravspecifikation", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/directed-full-stack-gis-map-development/", "url": "https://engineer.company/da/portfolio/directed-full-stack-gis-map-development/", "title": "Har styret full‑stack‑udviklingen af GIS‑kort", "summary": "Styrede full-stack GIS-kortudvikling (PostgreSQL, Mapbox, React, Node.js) — én integreret, udvidbar kerne i platformen.", "content_html": "<p><strong>Situation.</strong> På det tidspunkt var GIS‑kortet holdt op med at være en funktion og var blevet grunden til, at kunderne overhovedet loggede ind. Problemet var, at det var vokset frem stykke for stykke. De spatiale data lå i PostgreSQL, selve kortet blev tegnet med Mapbox, og applikationen omkring det var ReactJS i frontenden med NodeJS bagved. Hver del virkede for sig. De var bare ikke bygget til at passe sammen, og samlingerne begyndte at kunne ses.</p>\n<p><strong>Opgave.</strong> Rollen dækkede full‑stack‑udviklingen af kortet og den tekniske retning, der fulgte med: datamodellen, renderingen, API’et og React‑frontenden. Målet var at gøre fire ting, der tilfældigvis delte et repository, til ét produkt, der var værd at stå inde for.</p>\n<p><strong>Handling.</strong> Arkitekturen blev fastlagt først, og derefter foregik arbejdet tæt på koden i stedet for med styring på afstand. På datasiden blev den spatiale model i PostgreSQL holdt ryddelig, så forespørgslerne ikke sneglede sig af sted, efterhånden som datasættene voksede. Mapbox tegnede kortet; opgaven var at give det de rigtige data på de rigtige zoomniveauer i stedet for det hele på én gang. På applikationssiden blev arbejdet i ReactJS og NodeJS reviewet, teamet blev skubbet i retning af fælles konventioner, og ansvar blev flyttet tilbage til det lag, det hørte hjemme i, hver gang et lag begyndte at lække ind i det næste. En god del af det var uglamourøst arbejde: at fange de små uoverensstemmelser, før de størknede til arkitektur.</p>\n<p><strong>Resultat.</strong> Resultatet var én samlet kortapplikation, hvor data, rendering og brugerflade endelig trak i samme retning. Den blev en central del af platformen og noget, teamet kunne blive ved med at bygge videre på, uden at den gav sig, hver gang et nyt lag kom til.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Databaser", "Frontend‑udvikling", "Full stack‑udvikling", "GIS / geodata", "Optimering af ydeevne", "Platformarkitektur", "PostgreSQL", "Teknisk ledelse", "Backend- & API‑udvikling", "Full stack‑produktudvikling", "GIS & geospatiale løsninger", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/saved-4000-hours-growing-a-team-from-2-to-18/", "url": "https://engineer.company/da/portfolio/saved-4000-hours-growing-a-team-from-2-to-18/", "title": "Har sparet 4.000 timer: team vokset fra 2 til 18", "summary": "Sparede ~4.000 timer ved at udvide et team fra 2 til 18 og være mentor for det, redesigne workflows og opbygge varig udviklingskapacitet.", "content_html": "<p><strong>Situation.</strong> Et team på to kunne ikke længere følge med. Produktet trak mere arbejde ind, end to personer kunne levere, og arbejdsformen — viden i folks hoveder og ingen egentlige konventioner — ville ikke holde, når den blev skaleret op. Flere hænder i et så løst organiseret team gør som regel bare kaosset større.</p>\n<p><strong>Opgave.</strong> Opgaven var at udvide teamet og samtidig bygge den struktur, der kunne få en større gruppe til at arbejde hurtigere i stedet for langsommere. Den ene halvdel var at mentorere de nye. Den anden var at rette arbejdsgangene, så ingen gik i stå og ventede på andre.</p>\n<p><strong>Handling.</strong> Teamet voksede med tiden fra 2 til 18 personer, og ansættelse og mentorering blev betragtet som én og samme opgave: alle, der kom til, skulle kunne arbejde selvstændigt. Fælles standarder betød, at kode og processer så ens ud, uanset hvem der havde skrevet dem, og der blev lagt en reel indsats i overleveringerne mellem specialerne, for det er dér, teams stille og roligt mister dage. Når noget blev ved med at spænde ben for folk, blev processen rettet i stedet for symptomet.</p>\n<p><strong>Resultat.</strong> Det større og bedre mentorerede team, der arbejdede efter gennemtænkte arbejdsgange, sparede i omegnen af 4.000 timer. Men tallet er egentlig ikke pointen. Det, der blev bygget, var varig udviklingskapacitet — en gruppe, der kunne bære arbejdet, uanset om en bestemt person var til stede eller ej.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Agile & Scrum", "Mentoring & coaching", "Projektledelse", "Teamledelse", "Teknisk ledelse", "Projektledelse (Agile)", "Teamopbygning & mentoring", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/optimized-forecasting-and-investment-strategies/", "url": "https://engineer.company/da/portfolio/optimized-forecasting-and-investment-strategies/", "title": "Har optimeret prognose- og investeringsstrategier", "summary": "Optimerede prognoser og investeringsstrategi for 11 netselskaber med datadrevet GIS — troværdig, målrettet planlægning.", "content_html": "<p><strong>Situation.</strong> Netselskaber står og falder med deres beslutninger om, hvor nettet skal forstærkes, og hvor pengene skal sættes ind, og elleve af dem traf de beslutninger uden ret meget geospatial analyse at bygge på. De havde driftsdataene. Det, de manglede, var en måde at se dem på kortet, hvor mønstrene rent faktisk viser sig.</p>\n<p><strong>Opgave.</strong> Opgaven var at skærpe deres prognoser og investeringsstrategier med GIS — at gøre tabeller med målinger til noget, der viste dem, hvor kapaciteten var ved at blive knap, hvor risikoen voksede, og hvor den næste krone gjorde mest gavn.</p>\n<p><strong>Handling.</strong> Driftsdataene blev koblet med geospatial modellering, så de to supplerede hinanden. I stedet for abstrakte prognoser kunne man se på nettet rumligt og stille konkrete spørgsmål: hvilke strækninger nærmede sig deres grænser, og hvilke områder skulle have investeringer først? For elleve netselskaber betød det, at analysen blev tilpasset den måde, hvert enkelt rent faktisk drev sit net på, i stedet for at alle fik den samme skabelon i håb om, at den passede.</p>\n<p><strong>Resultat.</strong> Netselskaberne fik prognoser, de kunne stole på, og investeringsbeslutninger, der var målrettede i stedet for håbefulde. Da planlægningen blev forankret i det, kortet viste, blev det hele mere effektivt — penge og opmærksomhed gik derhen, hvor dataene pegede, og ikke derhen, hvor vanen pegede.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Data engineering", "Dataanalyse", "GIS / geodata", "Interessenter & rapportering", "Produkt & krav", "Dataanalyse & BI‑dashboards", "GIS & geospatiale løsninger", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/presented-200-ui-ux-improvements/", "url": "https://engineer.company/da/portfolio/presented-200-ui-ux-improvements/", "title": "Har præsenteret 200 UI/UX‑forbedringer", "summary": "Leverede 200 UI/UX-forbedringer til en GIS-kortapplikation og bidrog til 10 gange højere omsætning — omhyggelig UX som kommerciel drivkraft.", "content_html": "<p><strong>Situation.</strong> Kortgrænsefladen var blevet kraftfuld og undervejs også kompliceret. Nogle steder kunne man mærke, at kunderne ikke fik den værdi, der lå lige foran dem — god funktionalitet gemt bag klodsede interaktioner. Afstanden mellem det, produktet kunne, og det, folk fandt let at gøre, var en stille omkostning for virksomheden.</p>\n<p><strong>Opgave.</strong> Opgaven var at finde de huller og få rettelserne igennem: det arbejde med brugervenligheden, der ville gøre det lettere at få værdi ud af produktet og — ikke tilfældigt — gøre det mere værd kommercielt.</p>\n<p><strong>Handling.</strong> I stedet for at gætte på, hvad der var galt, blev feedbacken og brugsdataene gennemgået, og ud af det kom en backlog med 200 konkrete UI/UX‑forbedringer. De blev prioriteret efter effekt i stedet for at blive behandlet som ligeværdige, og de vigtigste blev argumenteret igennem og ført gennem teamet, så de blev sat i produktion som rigtige funktioner og ikke endte som en ønskeliste, der lå og blev forældet i et dokument.</p>\n<p><strong>Resultat.</strong> Grænsefladen blev mærkbart nemmere at bruge, og produktets værdi fulgte med: arbejdet bidrog til en tidobling af omsætningen. Det er et eksempel, der er værd at vende tilbage til, fordi det viser pointen så tydeligt — omhyggelig UX er ikke kosmetik, den kan ses på fakturaen.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Dataanalyse", "Designsystemer & UI", "Frontend‑udvikling", "Interessenter & rapportering", "Produkt & krav", "UX/UI‑design", "Produktstrategi & kravspecifikation", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/wrote-50000-words-of-software-documentation/", "url": "https://engineer.company/da/portfolio/wrote-50000-words-of-software-documentation/", "title": "Har skrevet 50.000 ord softwaredokumentation", "summary": "Skrev 50.000 ord softwaredokumentation i GitHub, Craft og Confluence — varig, gennemsigtig og reviderbar viden.", "content_html": "<p><strong>Situation.</strong> Det meste af det, der var vigtigt at vide om produktet og om teamets måde at arbejde på, lå i folks hoveder. Det går fint, lige indtil der kommer en ny, eller nogen rejser — og så går onboardingen i sneglefart, og en bid af organisationens hukommelse er én opsigelse fra at forsvinde for altid.</p>\n<p><strong>Opgave.</strong> Målet var at få den viden ud af hovederne og ind i dokumentation, som folk rent faktisk ville tage vare på og bruge: klar nok til at blive læst og struktureret nok til at blive vedligeholdt i stedet for at blive opgivet efter en måned.</p>\n<p><strong>Handling.</strong> En stor del af den blev skrevet manuelt, i alt omkring 50.000 ord, i Markdown i GitHub, Craft og Confluence, alt efter hvor hver del hørte hjemme. Den dækkede arkitekturen, processerne og det praktiske vejledningsmateriale — svarene på de spørgsmål, folk blev ved med at stille. Den blev holdt under versionsstyring og, lige så vigtigt, gjort let at finde, for dokumentation, som ingen kan finde, kan lige så godt ikke eksistere.</p>\n<p><strong>Resultat.</strong> Videnen blev robust i stedet for skrøbelig. Nye medarbejdere kom hurtigere i gang, processen blev noget, man kunne pege på i stedet for at rekonstruere efter hukommelsen, og det hele blev muligt at revidere: man kunne se, hvordan og hvorfor arbejdet blev udført, i stedet for at tage det på tro.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Dokumentation", "Mentoring & coaching", "Teamledelse", "Teknisk ledelse", "Teknisk dokumentation", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/gathered-and-analyzed-business-requirements/", "url": "https://engineer.company/da/portfolio/gathered-and-analyzed-business-requirements/", "title": "Har indsamlet og analyseret forretningskrav", "summary": "Omsatte forretningskrav til klare funktioner og user stories efter data governance-standarder — mindre tvetydighed og omarbejde.", "content_html": "<p><strong>Situation.</strong> Krav kom som regel i form af samtaler — nogen ville have noget, sådan cirka, og så var det op til udviklerne at gætte sig til detaljerne. Gætteri betyder omarbejde, og omarbejde er nok den dyreste måde at bygge noget på.</p>\n<p><strong>Opgave.</strong> Rollen lå mellem forretningen og udviklingen og oversatte den ene til den anden: løse behov skulle gøres til opgaver, som en udvikler kunne tage fat på uden at gætte, og undervejs skulle de holdes i tråd med standarderne for data governance.</p>\n<p><strong>Handling.</strong> Kravene blev afklaret direkte med interessenterne, og de akavede spørgsmål blev stillet tidligt i stedet for at dukke op sent. Derefter blev de skrevet ned som funktioner og user stories, der klart sagde, hvad “færdig” betød. Hver enkelt blev holdt op mod reglerne for data governance, for en funktion, der er nyttig, men håndterer data forkert, er ikke rigtig færdig. Hele formålet var, at man kunne læse en story og bygge det rigtige første gang.</p>\n<p><strong>Resultat.</strong> Udviklingen tog udgangspunkt i klare, aftalte funktioner i stedet for halvt forståede ønsker. Tvetydigheden faldt, og omarbejdet faldt med den, og leveringen forblev rettet mod de egentlige forretningsmål — inden for rammerne af data governance i stedet for at blive tilpasset dem bagefter.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Agile & Scrum", "Data governance", "Dokumentation", "Interessenter & rapportering", "Produkt & krav", "Projektledelse", "Data governance & datakvalitet", "Produktstrategi & kravspecifikation", "Projektledelse (Agile)" ] }, { "id": "https://engineer.company/da/portfolio/managed-30-successful-gis-projects-to-delivery/", "url": "https://engineer.company/da/portfolio/managed-30-successful-gis-projects-to-delivery/", "title": "Har styret 30 succesfulde GIS‑projekter", "summary": "Leverede 30 succesfulde GIS-projekter — en track record med stabil, nyskabende levering, der skabte varig kundetillid.", "content_html": "<p><strong>Situation.</strong> Virksomheden tjente sine penge på at levere GIS‑projekter, og mange af dem kørte samtidig. Væksten afhang af, at de blev leveret pålideligt — ikke ét flagskibsprojekt udført glimrende, men en stabil strøm af projekter, der landede til tiden og holdt, og det sker kun, når koordineringen i teamet rent faktisk fungerer.</p>\n<p><strong>Opgave.</strong> Ansvaret var at få porteføljen leveret — at holde scope, folk og tidsplaner afstemt med hinanden i alle projekterne og give teamet den retning, der skulle til for at komme i mål.</p>\n<p><strong>Handling.</strong> I de år gik leveringen af tredive GIS‑projekter gennem denne rolle. I praksis betød det at holde scope i ro, når det ville vokse, at sætte de rigtige folk på de rigtige opgaver og at være tæt nok på hvert projekt til at fange problemer, mens de stadig var små. Hvis noget var ved at skride, var det bedre at vide det tidligt og rokere om end at opdage det ved deadline. En stor del af jobbet handlede simpelthen om at holde alle bolde i luften og holde kundens forventninger realistiske om, hvad der kom hvornår.</p>\n<p><strong>Resultat.</strong> Alle tredive kom i mål. Den stabilitet betød mere, end noget enkelt projekt kunne have gjort — en kunde, der har set leverancerne komme tredive gange i træk, bekymrer sig ikke om nummer enogtredive, og det omdømme er en væsentlig del af grunden til, at virksomheden blev ved med at vokse.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Agile & Scrum", "GIS / geodata", "Interessenter & rapportering", "Projektledelse", "Teamledelse", "GIS & geospatiale løsninger", "Projektledelse (Agile)", "Teamopbygning & mentoring" ] }, { "id": "https://engineer.company/da/portfolio/improved-team-communication-and-collaboration/", "url": "https://engineer.company/da/portfolio/improved-team-communication-and-collaboration/", "title": "Har forbedret teamets kommunikation og samarbejde", "summary": "Sparede ~8.000 timer ved at indføre Slack, Mattermost, 1Password og Jira — jagt på information og dobbeltarbejde blev til reel levering.", "content_html": "<p><strong>Situation.</strong> Efterhånden som teamet voksede, havde kommunikationen og værktøjerne ikke fulgt med, og det kunne mærkes. Ting blev sagt ét sted og gik forbi dem, der havde brug for dem, arbejde blev lavet to gange, fordi ingen kunne se, hvad andre allerede havde gjort, og det tog længere tid at koordinere noget som helst end at udføre selve arbejdet. Den slags friktion er usynlig i hverdagen, men den løber op i meget tabt tid.</p>\n<p><strong>Opgave.</strong> Målet var at forbedre den måde, teamet kommunikerede og samarbejdede på, og at vinde den tid tilbage, som stille og roligt forsvandt i al den friktion.</p>\n<p><strong>Handling.</strong> Værktøjerne blev indført og standardiseret, og — det, der virkelig betyder noget — der blev fastlagt en praksis for, hvordan de skulle bruges, så de ikke bare blev endnu et sted at tjekke. Slack og Mattermost til kommunikation, 1Password, så delte hemmeligheder ikke blev sendt rundt på måder, ingen kunne holde styr på, og Jira, så opgaverne blev fulgt ét sted i stedet for at leve i folks hoveder og indbakker. Værktøjerne var den nemme del; det egentlige arbejde var at få alle til rent faktisk at bruge dem på samme måde.</p>\n<p><strong>Resultat.</strong> Kommunikation og samarbejde blev mærkbart bedre, og den strømlinede opsætning sparede i størrelsesordenen 8.000 arbejdstimer — tid, der før gik med at jagte information og lave arbejde om, og som nu gik til egentlig levering.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Agile & Scrum", "Automatisering & CI/CD", "Dokumentation", "Projektledelse", "Sikkerhed", "Teamledelse", "Projektledelse (Agile)", "Sikkerhed & adgangsstyring", "Teamopbygning & mentoring" ] }, { "id": "https://engineer.company/da/portfolio/led-30-gis-projects-from-build-to-support/", "url": "https://engineer.company/da/portfolio/led-30-gis-projects-from-build-to-support/", "title": "Har ledet over 30 GIS‑projekter", "summary": "Ledede udvikling, udrulning og support af 30+ GIS-projekter med PostgreSQL, Python, GDAL, ArcGIS, PostGIS og Mapbox gennem hele livscyklussen.", "content_html": "<p><strong>Situation.</strong> Virksomhedens samlede produktion var skræddersyet GIS — specialudviklet kortlægning og spatiale systemer bygget til en kundes konkrete problem og holdt kørende, når de først var i drift. At bygge systemet er kun den ene halvdel; et geospatialt projekt, der bliver sat i drift og så vælter i produktion, er ikke rigtig leveret.</p>\n<p><strong>Opgave.</strong> Projekterne gik fra ende til anden gennem denne rolle — udviklingen, udrulningen og supporten, når de først var i drift — med den tekniske retning for en ret bred stak.</p>\n<p><strong>Handling.</strong> Leverancerne omfattede mere end tredive GIS‑projekter, og hænderne var nede i hele stakken hele vejen igennem. PostgreSQL med PostGIS i bunden til de spatiale data, GDAL/OGR til at flytte dem mellem formater — shapefiles, GeoJSON, GeoTIFF, KML, vector tiles — og QGIS og JOSM til selve dataarbejdet. I frontenden blev der bygget webkort på Mapbox GL og Leaflet, nogle gange op mod ArcGIS- eller HERE‑API’erne, med applikationslaget i JavaScript, Python, PHP og SQL. Arbejdet spændte fra digital kortlægning i 2D og 3D over LiDAR‑behandling, georektifikation og vektorisering til indendørs kortlægning og navigation. Ansvaret rakte også ud over lanceringen — udrulningen og den løbende support i produktion var en del af det, så problemerne blev ikke sendt videre; man måtte leve med beslutningerne.</p>\n<p><strong>Resultat.</strong> Mere end tredive projekter blev bygget, udrullet og supporteret inden for hele det spænd. Det var ansvaret for hele livscyklussen og ikke kun for udviklingen, der holdt kvaliteten ærlig — man designer anderledes, når man ved, at det er en selv, der bliver ringet op, hvis noget går i stykker.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data engineering", "Databaser", "Driftssikkerhed & backup", "Full stack‑udvikling", "GIS / geodata", "PostgreSQL", "Python", "SQL", "Teamledelse", "Teknisk ledelse", "Webudvikling", "Backend- & API‑udvikling", "Full stack‑produktudvikling", "GIS & geospatiale løsninger", "Teknisk ledelse & rådgivning", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/guided-company-projects-through-development/", "url": "https://engineer.company/da/portfolio/guided-company-projects-through-development/", "title": "Har vejledt virksomhedsprojekter i udviklingen", "summary": "Vejledte gennemførelsen af mange virksomhedsprojekter med ekspertsupport i de sværeste udviklingsfaser — leverancerne blev mere stabile.", "content_html": "<p><strong>Situation.</strong> Mange projekter kørte samtidig, alle midt i udviklingsfasen, og de havde alle brug for stabil teknisk vejledning for at blive på sporet. Overlades projekter til sig selv, driver de — en forkert tilgang, der vælges tidligt, er blevet dyr, når nogen opdager den.</p>\n<p><strong>Opgave.</strong> Opgaven var at styre den gennemførelse — at være den tekniske støtte, teamene kunne læne sig op ad gennem udviklingsfaserne.</p>\n<p><strong>Handling.</strong> Arbejdet foregik hands‑on i mange af virksomhedens projekter på én gang. Det betød at hjælpe folk videre, når de sad fast, at se kritisk på en tilgang, før der blev bygget for meget oven på den, og i det hele taget at være tæt nok på til at fange et projekt på vej i den forkerte retning, mens det stadig kunne klares med en kurskorrektion og ikke krævede en genopbygning. Tanken var at være tilgængelig og ikke en flaskehals — at holde tingene i gang og ikke lade alt vente på én person.</p>\n<p><strong>Resultat.</strong> Projekterne kom mere gnidningsfrit gennem de vanskelige udviklingsfaser, fordi der var en erfaren person at læne sig op ad på de rigtige tidspunkter, og det gjorde leveringen mere stabil i hele virksomhedens portefølje.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Mentoring & coaching", "Projektledelse", "Teamledelse", "Teknisk ledelse", "Projektledelse (Agile)", "Teamopbygning & mentoring", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/administered-network-infrastructure/", "url": "https://engineer.company/da/portfolio/administered-network-infrastructure/", "title": "Har administreret netværksinfrastruktur", "summary": "Administrerede netværksinfrastruktur for 1.000+ servere — pålidelig udrulning, solid sikkerhed og hurtig fejlsøgning.", "content_html": "<p><strong>Situation.</strong> Virksomhedens drift hvilede på en stor serverpark — over tusind servere — og en serverpark af den størrelse forbliver ikke pålidelig af sig selv. Udrulning, sikkerhed og den konstante strøm af ting, der går galt, kræver alle nogen, der styrer dem med reel disciplin, ellers bliver det hele ustabilt, og ingen ved helt hvorfor.</p>\n<p><strong>Opgave.</strong> Opgaven var at administrere netværksinfrastrukturen — at holde den sikker, pålidelig og ensartet i en skala, hvor netop uensartethed er det, der vælter det hele.</p>\n<p><strong>Handling.</strong> Netværksinfrastrukturen omfattede mere end tusind servere. Udrulningen af systemer blev standardiseret, så hver server kom op på samme forudsigelige måde i stedet for at være en smule specialbygget hver gang; sikkerheden blev hærdet i stedet for at stole på, at ingen ville komme og lede; og fejlfindingen blev klaret, når noget alligevel gik i stykker. I den skala er det standardiseringen, der redder en — tusind snefnug er umulige at administrere, tusind ens servere er bare arbejde.</p>\n<p><strong>Resultat.</strong> Serverparken kørte med pålidelig udrulning, solid sikkerhed og problemer, der blev løst hurtigt i stedet for at ulme. Den slags infrastrukturarbejde er usynligt, når det går godt, og det er netop pointen — det var den stabile rygrad, som alt andet i virksomheden kørte på.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Sikkerhed", "Systemadministration", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/established-a-technical-support-department/", "url": "https://engineer.company/da/portfolio/established-a-technical-support-department/", "title": "Har etableret en teknisk supportafdeling", "summary": "Etablerede en teknisk supportafdeling, der betjente 10.000+ kunder — support gik fra reaktive rettelser til en skalerbar styrke for virksomheden.", "content_html": "<p><strong>Situation.</strong> Kundebasen voksede og havde brug for pålidelig teknisk support, men der var ingen dedikeret funktion, der kunne give den i nogen reel skala. Supporten foregik ad hoc, og det fungerer for en håndfuld kunder, men falder stille og roligt fra hinanden, efterhånden som antallet stiger.</p>\n<p><strong>Opgave.</strong> Opgaven var at opbygge en egentlig teknisk supportfunktion — en, der kunne betjene en stor og stadig voksende kundebase.</p>\n<p><strong>Handling.</strong> En afdeling for teknisk support blev bygget op fra grunden. Det betød at beslutte, hvordan den skulle fungere, før der blev ansat folk — processen for, hvordan en henvendelse kom ind og blev løst, værktøjerne til at håndtere mængden og standarden for, hvad god support var — og derefter at opbygge kapaciteten til at levere it‑support og fejlfinding til mange kunder på én gang. At starte på bar bund var ærligt talt en fordel; afdelingen kunne designes til den skala, virksomheden var på vej mod, i stedet for at lappe på noget, der var vokset frem tilfældigt.</p>\n<p><strong>Resultat.</strong> Afdelingen kom til at betjene over ti tusind kunder med pålidelig support og fejlfinding. Support gik fra at være noget, der blev klaret reaktivt, til at blive en reel styrke for virksomheden — noget, der voksede med kundebasen i stedet for at bukke under for den.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Interessenter & rapportering", "Mentoring & coaching", "Produkt & krav", "Projektledelse", "Systemadministration", "Teamledelse", "It‑support & helpdesk", "Projektledelse (Agile)", "Teamopbygning & mentoring" ] }, { "id": "https://engineer.company/da/portfolio/automated-ssl-tls-certs-for-100-docker-apps/", "url": "https://engineer.company/da/portfolio/automated-ssl-tls-certs-for-100-docker-apps/", "title": "Har automatiseret SSL/TLS til 100 Docker‑apps", "summary": "Automatiserede SSL/TLS-certifikater for 100 Docker-apps på Ubuntu Linux — slut med manuel fornyelse og nedbrud fra udløbne certifikater.", "content_html": "<p><strong>Situation.</strong> Hundrede Docker‑applikationer skulle alle have SSL/TLS‑certifikater, og certifikater er den slags, der fungerer fint, lige indtil de ikke gør. At udstede og forny hundrede af dem manuelt er langsomt og kedeligt, og det er præcis den type manuelt arbejde, hvor én glemt fornyelse lægger en app ned med en udløbsfejl på det værst tænkelige tidspunkt.</p>\n<p><strong>Opgave.</strong> Målet var at automatisere oprettelse og fornyelse af certifikater — så hver app havde gyldig, betroet kryptering, uden at nogen skulle huske at gøre noget.</p>\n<p><strong>Handling.</strong> Et ACME‑baseret workflow håndterede hele livscyklussen for de hundrede Docker‑applikationer — oprettede certifikaterne og fornyede dem, før de udløb — og udrullede dem automatisk på Ubuntu Linux‑værterne, der kørte en blanding af Apache og Nginx. Hele formålet var at fjerne mennesket fra processen, for det er mennesket, der glemmer.</p>\n<p><strong>Resultat.</strong> Alle hundrede applikationer bevarede gyldige certifikater og sikre forbindelser helt af sig selv. Det manuelle certifikatarbejde forsvandt, og med det en hel kategori af nedbrud, hvor noget går i stykker, ikke fordi det fejler, men fordi et certifikat stille og roligt er udløbet, uden at nogen har opdaget det.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Containere (Docker/Kubernetes)", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Sikkerhed", "Containerisering & orkestrering", "DevOps & CI/CD‑automatisering", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/saved-4000-hours-with-github-and-gitlab-ci-cd/", "url": "https://engineer.company/da/portfolio/saved-4000-hours-with-github-and-gitlab-ci-cd/", "title": "Har sparet 4.000 timer med CI/CD i GitHub", "summary": "Sparede ~4.000 timer ved at indføre CI/CD med GitHub, GitLab, Bash og Python — hurtigere og mere ensartet dataanalyse og udvikling.", "content_html": "<p><strong>Situation.</strong> Både dataanalysen og softwareudviklingen blev holdt tilbage af det samme: manuelle processer. Arbejdet bevægede sig langsomt og uensartet fra udvikling til levering, og analysesiden havde sin egen bunke af gentagne trin, som nogen udførte manuelt hver gang.</p>\n<p><strong>Opgave.</strong> Målet var at strømline begge dele ved at indføre moderne automatisering og CI/CD‑praksis i arbejdsgange, der ikke havde haft dem.</p>\n<p><strong>Handling.</strong> CI/CD‑praksis blev indført på GitHub og GitLab, med Bash og Python til selve automatiseringen nedenunder. De gentagne trin i både dataanalysen og udviklingen blev automatiseret, og vejen fra udvikling til levering blev standardiseret, så den var den samme hver gang og ikke blev opfundet på ny i hvert projekt. En stor del af arbejdet var at få analysesiden ind i det samme disciplinerede forløb som udviklingsarbejdet — den var hidtil blevet behandlet som en separat og mere manuel verden.</p>\n<p><strong>Resultat.</strong> De strømlinede processer sparede omkring 4.000 timer og satte fart på både dataanalysen og softwareudviklingen. Lige så vigtigt blev det, der blev leveret, mere ensartet — med færre overraskelser fra arbejde, der var blevet udført en anelse forskelligt hver gang.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data engineering", "Datapipelines (ETL/ELT)", "Dataanalyse", "DevOps", "Python", "Dataanalyse & BI‑dashboards", "DevOps & CI/CD‑automatisering", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/automated-data-processing-in-python-and-pl-pgsql/", "url": "https://engineer.company/da/portfolio/automated-data-processing-in-python-and-pl-pgsql/", "title": "Har automatiseret databehandling i Python og SQL", "summary": "Automatiserede databehandling med Shell, PL/pgSQL, Python og Transact-SQL — højere produktivitet og slut med små, tilbagevendende fejl.", "content_html": "<p><strong>Situation.</strong> En stadig strøm af tilbagevendende databehandling blev udført manuelt. Manuelt dataarbejde har to problemer på én gang: det sluger tid, og det er uensartet — udfører man den samme opgave i hånden tilstrækkeligt mange gange, bliver den gjort en anelse forskelligt, og nogle af de forskelle er fejl.</p>\n<p><strong>Opgave.</strong> Målet var at automatisere disse opgaver, både for at vinde tiden tilbage og for at gøre dem pålidelige.</p>\n<p><strong>Handling.</strong> Databehandlingen blev automatiseret i de databaser og systemer, den berørte, med de værktøjer, der passede til opgaven — shell‑scripting til at binde det hele sammen, PL/pgSQL og Transact‑SQL nede i databaserne og Python, hvor der var brug for mere, end SQL kunne give. Manuelle trin blev erstattet af jobs, der kørte på samme måde hver gang, og det er hele pointen: et script keder sig ikke, springer ikke et trin over og gør det ikke anderledes en fredag eftermiddag.</p>\n<p><strong>Resultat.</strong> Både produktiviteten og effektiviteten steg, det manuelle arbejde blev taget af folks bord, og databehandlingen blev ensartet og pålidelig i stedet for en kilde til små, tilbagevendende fejl.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data engineering", "Databaser", "Datapipelines (ETL/ELT)", "PostgreSQL", "Python", "SQL", "Databaseadministration (DBA)", "DevOps & CI/CD‑automatisering", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/saved-150-hours-a-month-across-30-projects/", "url": "https://engineer.company/da/portfolio/saved-150-hours-a-month-across-30-projects/", "title": "Har sparet 150 timer om måneden på 30 projekter", "summary": "Sparede ~150 timer om måneden i 30 projekter ved at optimere workflows og ressourcestyring — en tilbagevendende gevinst i hele porteføljen.", "content_html": "<p><strong>Situation.</strong> I en portefølje på omkring tredive projekter gik der hver eneste måned tid tabt til arbejdsgange, der aldrig var blevet optimeret, og til ujævn ressourcestyring — nogle medarbejdere var underudnyttede, andre overbelastede, og arbejdet blev planlagt forskelligt fra det ene projekt til det næste.</p>\n<p><strong>Opgave.</strong> Målet var at gøre porteføljen mere effektiv og vinde den tid tilbage, der forsvandt måned efter måned.</p>\n<p><strong>Handling.</strong> De tredive projekter blev gennemgået, og arbejdsgangene og ressourcestyringen blev forbedret samlet — flaskehalsene blev fjernet, arbejdsbyrden blev fordelt, så det ikke altid var de samme få personer, alt ventede på, og planlægningen og styringen af arbejdet blev standardiseret, så hvert projekt ikke var sit eget særtilfælde. Når tredive projekter hver taber lidt tid, løber det op; løsningen handlede mest om at gøre de gode vaner ensartede og ikke om at opfinde noget smart.</p>\n<p><strong>Resultat.</strong> Ændringerne sparede omkring 150 timer om måneden i hele porteføljen. Besparelsen er tilbagevendende og ikke en engangsgevinst — tredive projekter, der kører slankere, måned efter måned.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Agile & Scrum", "Automatisering & CI/CD", "Optimering af ydeevne", "Projektledelse", "Teamledelse", "Projektledelse (Agile)", "Teamopbygning & mentoring" ] }, { "id": "https://engineer.company/da/portfolio/managed-a-team-delivering-it-support/", "url": "https://engineer.company/da/portfolio/managed-a-team-delivering-it-support/", "title": "Har ledet et team, der leverede it‑support", "summary": "Ledede et team, der leverede it-support, datagendannelse og hardwarereparation til 1.000+ kunder — et ry for pålidelig service.", "content_html": "<p><strong>Situation.</strong> Virksomheden var stedet, hvor en stor kundebase henvendte sig, når deres it gik i stykker — support, datagendannelse, hardwarereparation, hele spektret. At imødekomme den slags stabile, uglamourøse efterspørgsel handler ikke om heltegerninger; det handler om at have et team, der fungerer godt dag efter dag, for arbejdet holder aldrig rigtig op med at strømme ind.</p>\n<p><strong>Opgave.</strong> Opgaven var at lede det team, der leverede alt dette, og at holde kvaliteten ensartet, uanset om det var en stille uge, eller alt kom på én gang.</p>\n<p><strong>Handling.</strong> Teamet stod for it‑support, datagendannelse og hardwarereparation for over tusind kunder. Meget af det handlede om organiseringen nedenunder — at sikre, at opgaver blev taget op og ikke tabt, at fastlægge en standard for, hvad “repareret” egentlig betød, så kunderne ikke fik halve reparationer tilbage, og at holde teamet effektivt, når køen var lang. Især datagendannelse er arbejde, man ikke kan tage let på; som regel er det nogens billeder eller hele deres forretning, der ligger på det drev, og de har allerede en dårlig dag, når de henvender sig.</p>\n<p><strong>Resultat.</strong> Teamet betjente over tusind kunder og opbyggede et solidt ry for pålidelig service. I den slags forretning er omdømmet alt — folk kommer igen og fortæller det videre, netop fordi problemet blev løst, sidste gang noget gik i stykker.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Interessenter & rapportering", "Mentoring & coaching", "Systemadministration", "Teamledelse", "Hardwarereparation & datagendannelse", "It‑support & helpdesk", "Teamopbygning & mentoring" ] }, { "id": "https://engineer.company/da/portfolio/configured-and-deployed-1000-wi-fi-routers/", "url": "https://engineer.company/da/portfolio/configured-and-deployed-1000-wi-fi-routers/", "title": "Har konfigureret og idriftsat 1.000 Wi‑Fi‑routere", "summary": "Konfigurerede og idriftsatte 1.000 Wi-Fi-routere med standardopsætning — pålidelig trådløs adgang og ydeevne for kunderne.", "content_html": "<p><strong>Situation.</strong> Kunderne havde brug for trådløst net, der bare virkede, og det krævede, at et stort antal Wi‑Fi‑routere blev konfigureret og udrullet — på samme omhyggelige måde hver gang, for en sjusket opsat router er enten usikker eller langsom, og som regel finder man først senere ud af hvilken af delene.</p>\n<p><strong>Opgave.</strong> Opgaven var at konfigurere og udrulle routerne for at give kunderne bedre netværksadgang og bedre ydeevne.</p>\n<p><strong>Handling.</strong> Tusind Wi‑Fi‑routere blev konfigureret og udrullet. Ved det antal ligger kunsten i at standardisere opsætningen — en ensartet, sikker og fornuftig konfiguration — i stedet for at finjustere hver enkelt fra bunden på dagen, for tusind håndlavede routere er tusind forskellige ting at supportere bagefter. Derfor blev de sat op med henblik på sikkerhed og ydeevne på samme måde hver gang og udrullet pålideligt hos kunderne.</p>\n<p><strong>Resultat.</strong> De tusind routere gav kunderne pålideligt trådløst net — bedre adgang og bedre ydeevne — og gjorde det ensartet, fordi opsætningen var standardiseret og ikke improviseret. Målet er en router, ingen behøver at tænke på igen, og de fleste af dem nåede dertil.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Infrastruktur", "Netværk & VPN", "Sikkerhed", "Systemadministration", "It‑support & helpdesk", "Netværk & VPN‑opsætning" ] }, { "id": "https://engineer.company/da/portfolio/administered-100-bare-bone-servers/", "url": "https://engineer.company/da/portfolio/administered-100-bare-bone-servers/", "title": "Har administreret 100 barebone‑servere", "summary": "Administrerede 100 barebone-servere, fysiske netværk og IP-telefoni — det robuste fundament, virksomheden voksede på.", "content_html": "<p><strong>Situation.</strong> Under alt, hvad virksomheden lavede, lå det fysiske lag — servere, selve netværkene og IP‑telefonien — og det hele skulle bare køre. Det lag er usynligt, når det virker, og meget synligt i det øjeblik, det ikke gør, og virksomhedens vækst afhang af, at det forblev pålideligt.</p>\n<p><strong>Opgave.</strong> Opgaven var at administrere infrastrukturen og holde den stabil, efterhånden som virksomheden voksede.</p>\n<p><strong>Handling.</strong> Hundrede barebone‑servere blev passet sammen med de fysiske netværk og IP‑telefonisystemerne — opsætning, vedligeholdelse og fejlfinding, når noget gik galt. Barebone‑servere betyder, at man arbejder direkte med hardwaren, så der er en praktisk, fysisk side af arbejdet: kablingen, kasserne og telefonsystemet, som alle lægger mærke til i samme sekund, en samtale bliver afbrudt. Opgaven var at holde det hele kedeligt, i den gode forstand.</p>\n<p><strong>Resultat.</strong> Serverne, netværkene og telefonien kørte pålideligt, og det var dette stabile fysiske fundament, der gjorde det muligt for virksomheden at blive ved med at vokse, uden at grunden skred under den.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Systemadministration", "Netværk & VPN‑opsætning" ] }, { "id": "https://engineer.company/da/portfolio/developed-100-web-applications-using-html-html5/", "url": "https://engineer.company/da/portfolio/developed-100-web-applications-using-html-html5/", "title": "Har udviklet 100 webapplikationer med HTML/HTML5", "summary": "Udviklede 100 webapplikationer med HTML5, CSS/SCSS, Django, WordPress og Joomla — hver løsning tilpasset kundens reelle behov.", "content_html": "<p><strong>Situation.</strong> Kunderne ville have webapplikationer, og de ville have vidt forskellige ting — forskellige størrelser, forskellige budgetter og alt fra “få mig bare online” til “byg mig noget skræddersyet”. At imødekomme det krævede alsidighed i stedet for at presse alle kunder ind i den samme teknologi.</p>\n<p><strong>Opgave.</strong> Opgaven var at bygge webapplikationer, der gav hver kunde en alsidig og effektiv tilstedeværelse på nettet.</p>\n<p><strong>Handling.</strong> Hundrede webapplikationer blev bygget, og pointen var at vælge værktøjet efter opgaven og ikke at have en favorit. Hvor en kunde havde brug for noget skræddersyet, blev det HTML/HTML5, CSS/SCSS og Django; hvor kunden havde brug for en mere standardiseret løsning, som de også selv kunne administrere, var WordPress eller Joomla det hurtigere og mere fornuftige svar. En del af kunsten var at vide, hvornår det var hvad — at tale en kunde fra en skræddersyet løsning, de ikke havde brug for, eller til en, de havde brug for.</p>\n<p><strong>Resultat.</strong> De hundrede applikationer gav kunderne en varieret og velfungerende tilstedeværelse på nettet, hver især bygget på den teknologi, der passede til den. Det, der gjorde dem effektive og ikke bare leverede, var, at tilgangen blev tilpasset kunden og ikke omvendt.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Frontend‑udvikling", "Full stack‑udvikling", "Python", "Webudvikling", "Full stack‑produktudvikling", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/administered-40-websites-on-apache-and-nginx/", "url": "https://engineer.company/da/portfolio/administered-40-websites-on-apache-and-nginx/", "title": "Har administreret 40 websites på Apache og Nginx", "summary": "Administrerede 40 websites på Ubuntu Linux med Apache og Nginx — hosting med høj tilgængelighed og ydeevne, som kunderne aldrig behøvede at tænke på.", "content_html": "<p><strong>Situation.</strong> Der var en portefølje af websites i drift, der skulle forblive oppe og forblive hurtige — og hosting er endnu en af de opgaver, der er usynlige, indtil et site går ned, og så er det pludselig det eneste, nogen interesserer sig for.</p>\n<p><strong>Opgave.</strong> Opgaven var at administrere disse sites og holde dem hurtige og med høj tilgængelighed.</p>\n<p><strong>Handling.</strong> Fyrre websites kørte på Ubuntu Linux‑hostingservere med en blanding af Apache og Nginx — konfiguration, ydeevneoptimering og løbende vedligeholdelse, så de forblev pålidelige under reel trafik. Det afgørende er netop den reelle trafik: et site, der klarer sig fint, når ingen bruger det, og vælter, når nogen gør, er ikke blevet administreret, det er bare blevet overladt til sig selv. Arbejdet bestod derfor i at holde dem sunde under den virkelige belastning.</p>\n<p><strong>Resultat.</strong> Alle fyrre kørte med høj tilgængelighed og god ydeevne og gav kunderne hosting, de ikke behøvede at tænke på. Stabil og pålidelig drift under reel brug er hele pointen med hosting, og det var præcis det, disse sites leverede.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Optimering af ydeevne", "Systemadministration", "Webudvikling", "Site reliability & overvågning", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/engineered-600-pl-pgsql-etl-elt-pipelines/", "url": "https://engineer.company/da/portfolio/engineered-600-pl-pgsql-etl-elt-pipelines/", "title": "Har udviklet 600 PL/pgSQL‑ETL/ELT‑pipelines", "summary": "Udviklede 600 PL/pgSQL ETL/ELT-pipelines i PostgreSQL dev og prod — 35 % kortere udførelsestid og mindst 50 % større gennemløb.", "content_html": "<p><strong>Situation.</strong> Organisationen håndterede store mængder transaktions- og analysedata i flere PostgreSQL‑miljøer til udvikling og produktion. De eksisterende datapipelines var fragmenterede og uensartede og skabte flaskehalse i ydeevnen, som forsinkede forretningskritisk rapportering og beslutningstagning.</p>\n<p><strong>Opgave.</strong> Opgaven var at designe og implementere robuste, skalerbare og effektive ETL/ELT‑pipelines i PL/pgSQL, der kunne strømline komplekse arbejdsgange for dataindtag, transformation og indlæsning. Et centralt mål var at forbedre forespørgslernes ydeevne og sikre dataintegriteten i alle miljøer.</p>\n<p><strong>Handling.</strong> Over 600 ETL/ELT‑pipelines baseret på PL/pgSQL blev udviklet til at automatisere udtræk, transformation og indlæsning af data fra forskellige kilder. De vigtigste tekniske tiltag var:</p>\n<ul>\n<li>Partitionerede tabeller og materialiserede views blev brugt til at optimere læseydeevnen for store datasæt.</li>\n<li>Primærnøgler og foreign keys blev anvendt til at bevare den referentielle integritet under transformationerne.</li>\n<li>Unikke og sammensatte indeks blev designet til at gøre JOIN‑operationer og komplekse filtreringskriterier hurtigere.</li>\n<li>Undtagelseshåndtering og transaktionsstyring blev indbygget for at sikre fejltolerance og rollback ved fejl.</li>\n<li>Inkrementelle indlæsninger blev indført med change data capture (CDC) og tidsstempelbaserede deltaer, så behandlingstiden faldt med over 60 %.</li>\n<li>Automatiserede procedurer til logning og revision blev udviklet til at følge pipelinekørslerne, overvåge afvigelser og lette fejlfindingen.</li>\n</ul>\n<p><strong>Resultat.</strong> Den nye ETL/ELT‑ramme gjorde databehandlingens arbejdsgange markant mere ensartede, pålidelige og hurtige:</p>\n<ul>\n<li>Den gennemsnitlige kørselstid for pipelines faldt med 35 %.</li>\n<li>Gennemløbet i datapipelines steg med over 50 %, så data var tilgængelige til rapportering næsten i realtid.</li>\n<li>Problemerne med datakvaliteten blev færre, da over 90 % af transformationsfejlene blev elimineret gennem bedre håndhævelse af constraints og bedre valideringslogik.</li>\n<li>Systemet blev lettere at vedligeholde og skalere, så fremtidige ændringer i datamodellen kan håndteres med et minimum af omarbejde.</li>\n</ul>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data engineering", "Data governance", "Databaser", "Datapipelines (ETL/ELT)", "Optimering af ydeevne", "PostgreSQL", "SQL", "Data governance & datakvalitet", "Databaseadministration (DBA)", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/a-tenth-of-the-cloud-bill-and-nothing-lost/", "url": "https://engineer.company/da/portfolio/a-tenth-of-the-cloud-bill-and-nothing-lost/", "title": "En tiendedel af skyregningen, uden at miste noget", "summary": "Reducerede infrastrukturbudgettet 10 gange uden produktivitetstab for en international kunde ved at nytænke stacken og forlade AWS.", "content_html": "<p><strong>Situation.</strong> En international kunde slæbte rundt på stærkt oppustede infrastrukturomkostninger. AWS‑opsætningen var blevet overdimensioneret og havde samlet tjenester op, som ikke længere blev brugt, så cloud‑regningen var vokset helt ud af proportion med, hvad forretningen reelt havde brug for. Det er en velkendt historie — ingen sætter sig for at bruge for mange penge, det hober sig bare op, når ingen holder øje med måleren.</p>\n<p><strong>Opgave.</strong> Opgaven var at skære markant ned på omkostningerne uden at miste produktivitet, og det krævede, at infrastrukturen blev gentænkt ordentligt i stedet for at blive trimmet i kanterne — trimning i kanterne flytter sjældent en regning, der er strukturelt for stor.</p>\n<p><strong>Handling.</strong> Arbejdet gik derfor hele vejen igennem. Først blev det kortlagt, hvad der faktisk var i brug — det er dér, de duplikerede og unødvendige tjenester afslører sig — og de blev skåret væk. Dernæst blev resten dimensioneret efter den reelle efterspørgsel i stedet for de pessimistiske gæt, den oprindelige opsætning var bygget på. Og det store greb var at flytte workloads helt væk fra AWS og over på en mere omkostningseffektiv hostingløsning — omhyggeligt og i etaper, så den kørende forretning aldrig mærkede migreringen foregå under sig.</p>\n<p><strong>Resultat.</strong> Budgetomkostningerne faldt til omkring en tiendedel uden tab af produktivitet — den samme kapacitet til en brøkdel af, hvad kunden hidtil havde betalt. Det frigjorde et mærkbart beløb, der måned efter måned stille og roligt var sivet ud i en overdimensioneret cloud‑regning, og for forretningen var det penge direkte tilbage på bundlinjen.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Cloud", "DevOps", "Infrastruktur", "Løsningsarkitektur", "Migrering & modernisering", "Platformarkitektur", "Cloud‑infrastruktur & migrering", "DevOps & CI/CD‑automatisering", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/a-maritime-platform-built-in-clean-layers/", "url": "https://engineer.company/da/portfolio/a-maritime-platform-built-in-clean-layers/", "title": "En maritim platform bygget i rene lag", "summary": "Designede arkitekturen for en lagdelt maritim platform — Next.js PWA, et Go-API (Huma/Fiber) og et PostgreSQL-funktionslag med al forretningslogik.", "content_html": "<p><strong>Situation.</strong> Produktet skulle være en maritim platform — professionelt netværk, virksomhedsanmeldelser, sponsoreret uddannelse — og det var et ungt produkt, hvilket er en høflig måde at sige på, at kravene ville flytte sig en hel del. Det, der skulle undgås, var en arkitektur, hvor en ændret forretningsregel betød, at man skulle ind og rette i frontenden, API’et og databasen på én gang. I en lille kodebase, der vokser hurtigt, er det netop den slags kobling, der gør en ændring på to linjer til en hel eftermiddags arbejde.</p>\n<p><strong>Opgave.</strong> Arkitektrollen indebar én beslutning, der skulle træffes på forhånd: hvor hver slags logik hørte hjemme, med grænser, der var tydelige nok til at holde under pres i stedet for at flyde ud, første gang nogen havde travlt.</p>\n<p><strong>Handling.</strong> Det endte med tre lag med én opgave hver. Next.js‑frontenden står for præsentation og interaktivitet og intet andet. Go‑API’et — Huma oven på Fiber — er bevidst tyndt: det router, validerer requesten, står for sikkerhedsfiltreringen og orkestrerer, men det rummer slet ingen forretningslogik. Al forretningslogikken ligger i PostgreSQL‑funktioner, som samler hele resultatet og returnerer det, så API’et blot sender det videre. Når en regel ændrer sig, ændrer den sig altså i ét lag, i SQL, og de to andre behøver ikke at vide det. Grænserne blev skrevet ned og derefter håndhævet i review, for en konvention, som ingen holder øje med, holder op med at være en konvention.</p>\n<p><strong>Resultat.</strong> Systemet forblev til at overskue. Forretningslogikken ligger ét sted, som man faktisk kan gennemgå, API’et er en kedelig adapter i ordets gode forstand, og frontenden er ligeglad, når skemaet ændrer sig under den. Det var den adskillelse, der gjorde det muligt at blive ved med at skrue nye funktioner på produktet, uden at arkitekturen i stilhed rådnede.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Databaser", "Frontend‑udvikling", "Full stack‑udvikling", "Løsningsarkitektur", "Platformarkitektur", "PostgreSQL", "Teknisk ledelse", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Full stack‑produktudvikling", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/built-organization-context-switching-for-users/", "url": "https://engineer.company/da/portfolio/built-organization-context-switching-for-users/", "title": "Har bygget skift af organisationskontekst", "summary": "Byggede skift af organisationskontekst med localStorage og en spejlet server-cookie — brugere optræder som administrerede organisationer med least privilege.", "content_html": "<p><strong>Situation.</strong> På platformen kan en maritim fagperson administrere organisationer — virksomheder, akademier — som ikke har deres egne logins. Personen er kontoen; organisationen er noget, personen handler på vegne af. En bruger skal derfor kunne bevæge sig gennem hele appen som en hvilken som helst af de organisationer, vedkommende administrerer, og frit skifte mellem dem, uden at den bekvemmelighed bliver til et hul i autorisationen.</p>\n<p><strong>Opgave.</strong> Kontekstskiftet skulle være hurtigt og diskret for brugeren og samtidig sikre, at den aktive kontekst aldrig i sig selv kunne give nogen en adgang, som vedkommende ikke havde ret til.</p>\n<p><strong>Handling.</strong> En OrganizationContext håndterer det, med lagring på begge sider. På klienten er localStorage den autoritative kilde til, hvilken organisation brugeren aktuelt handler som, så skiftet sker øjeblikkeligt — uden en tur forbi serveren. En cookie på serversiden spejler valget, så serverrenderede sider når frem til den samme kontekst under SSR; en getServerViewMode på serveren læser den. Det vigtige er, at ingen adgangsbeslutning bygger på noget af det. Autorisationen kontrolleres igen på serveren ved hver request. Frontend‑konteksten er der for oplevelsens skyld — så brugeren ser det rigtige — og serveren er den eneste instans, der afgør, hvad brugeren må.</p>\n<p><strong>Resultat.</strong> En bruger kan uden besvær handle som enhver af de organisationer, vedkommende administrerer, og brugerfladen forbliver synkroniseret på både klient og server. Men fordi rettighederne verificeres på serveren hver gang, svækker intet af den bekvemmelighed sikkerheden. Den, der piller ved indholdet af localStorage, ændrer, hvad vedkommendes egen brugerflade viser, og intet mere — serveren siger stadig nej.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Frontend‑udvikling", "Full stack‑udvikling", "Platformarkitektur", "Sikkerhed", "Backend- & API‑udvikling", "Platform- & løsningsarkitektur", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-email-with-failover-and-delivery-logging/", "url": "https://engineer.company/da/portfolio/built-email-with-failover-and-delivery-logging/", "title": "Har bygget e‑mail med failover og leveringslog", "summary": "Byggede e-mail som en platformsfunktion: tre udbydere med failover, delivery-webhooks, logning af afsendelse og levering, skabeloner og kampagneudsendelser.", "content_html": "<p><strong>Situation.</strong> E‑mail vejer tungt på platformen — verificering, notifikationer, digests, kampagner, alt det, en bruger rent faktisk venter på. Og en mailudbyder er præcis den slags afhængighed, der fejler lydløst: konfigurationen ser fin ud, appen starter, og man opdager først, at noget er galt, når et rigtigt menneske aldrig får den besked, vedkommende blev lovet. Det er den værste måde at finde ud af det på. Med kun én udbyder bliver det værre, fordi fejlen er total, og det er en anden, der skal rette den.</p>\n<p><strong>Opgave.</strong> E‑mail skulle behandles som en funktion, platformen selv ejer, og ikke som et klientbibliotek, den kalder — en funktion, der kan overleve et nedbrud hos en udbyder, kan gøre rede for, hvad der skete med en given besked, og slår højlydt alarm ved opstart i de miljøer, hvor stilhed er farlig.</p>\n<p><strong>Handling.</strong> Tre udbydere ligger bag én grænseflade — SendGrid som den primære, med SMTP2GO og Azure Communication Services i reserve — og failover mellem dem sker automatisk i stedet for at være en konfigurationsændring, der skal laves under pres. Levering tages ikke for givet: indgående webhooks melder tilbage, hvad hver udbyder gjorde med en besked, og begge sider registreres i en afsendelseslog og en tabel over leveringshændelser, så “fik den her person sin verificeringsmail” er en databaseforespørgsel og ikke et gæt. Skabeloner holder beskedteksterne ude af koden, og et separat broadcast‑skema — 5 tabeller og 24 funktioner — sender kampagner ud til brugersegmenter, hvilket er en anden opgave end transaktionsmail og blev bygget som en selvstændig løsning. Foran det hele sender et opstartstjek en rigtig besked gennem stakken, styret af et flag: i udviklingsmiljøet logger det en advarsel og fortsætter, for ingen vil have, at deres laptop nægter at starte på grund af en udløbet sandkassenøgle, og i staging og produktion er en fejl fatal, og processen stopper i stedet for at udrulle et build, der ikke kan sende mail. Selve afsendelsen går gennem en SSRF‑beskyttet klient med en timeout på 30 sekunder, og den asynkrone levering har retries og backoff, så et kortvarigt udfald ikke koster en besked.</p>\n<p><strong>Resultat.</strong> En hel kategori af lydløse fejl gik fra “en bruger opdager det dage senere” til “udrulningen stopper”, og en udbyder med en dårlig eftermiddag betyder nu en forringet levering snarere end et nedbrud. Prisen er tre integrationer at holde kørende i stedet for én og leveringslogs, der vokser og løbende skal ryddes op i — begge dele accepteret, fordi e‑mail er den kanal, platformen ikke kan komme uden om.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Cloud", "Driftssikkerhed & backup", "Sikkerhed", "Backend- & API‑udvikling", "Sikkerhed & adgangsstyring", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/designed-a-function-first-postgresql-data-layer/", "url": "https://engineer.company/da/portfolio/designed-a-function-first-postgresql-data-layer/", "title": "Har designet et funktionsbaseret Postgres‑datalag", "summary": "Designede et function-first datalag i PostgreSQL — 1.275 stored functions fordelt på 34 skemaer — med adgangsgrænsen håndhævet af databasen selv.", "content_html": "<p><strong>Situation.</strong> Forretningslogik har det med at lække. Lidt ender i API’et, lidt i noget SQL, som en handler kører inline, og inden længe er den samme regel skrevet på to‑tre lidt forskellige måder, og der er intet sted, man kan pege på og sige “det her er, hvad systemet gør med sine data.” Det er sådan, fejl og sikkerhedshuller sniger sig ind.</p>\n<p><strong>Opgave.</strong> Målet var at samle det hele ét sted: al læsning og skrivning skulle gå gennem databasen, API’et skulle være en tynd adapter, der ikke kender forretningsreglerne, og det hele skulle kunne låses stramt af.</p>\n<p><strong>Handling.</strong> Datalaget er funktionsbaseret. Skemaet er delt op efter domæne — identity, organization, review, message, notification og 29 andre, 34 i alt — og hver operation, appen kan udføre, er én af 1.275 PostgreSQL‑funktioner, som den kalder; der er slet ingen direkte tabeladgang fra Go. Og databasen håndhæver det. Den rolle, API’et logger ind som, mariner, har EXECUTE på app‑funktionerne og USAGE på skemaerne og intet andet — ingen SELECT, ingen INSERT, ingen mulighed for at røre en tabel direkte — hvilket løber op i cirka 4.000 eksplicitte grants i stedet for én generel. Funktionerne kører som SECURITY DEFINER og ejes af en separat function_owner‑rolle uden login og med en fastlåst search_path, og superuser‑kontoen er forbeholdt migrationer og cron, langt væk fra den kørende app.</p>\n<p><strong>Resultat.</strong> Logikken ligger ét sted, som man rent faktisk kan gennemgå, API’et forbliver tyndt og kedeligt i ordets gode forstand, og adgangsgrænsen håndhæves af Postgres selv og ikke af, at alle husker reglerne. Hvis API’et på en eller anden måde blev kompromitteret, kunne det stadig ikke gøre noget, funktionerne ikke tillader. Med 1.275 funktioner har disciplinen en reel pris — et nyt felt er en migration og en funktionsændring, ikke en linje i en databaseforespørgsel — og den friktion er prisen for, at grænsen holder.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data governance", "Databaser", "Platformarkitektur", "PostgreSQL", "Sikkerhed", "SQL", "Backend- & API‑udvikling", "Data governance & datakvalitet", "Databasedesign & datamodellering", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/quicker-answers-from-a-better-choice-of-key/", "url": "https://engineer.company/da/portfolio/quicker-answers-from-a-better-choice-of-key/", "title": "Hurtigere svar med et bedre valg af nøgle", "summary": "Indførte tidsordnede UUID v7-nøgler (PostgreSQL 18) — mindre B-tree-fragmentering og hurtigere inserts og range-forespørgsler i alle tabeller.", "content_html": "<p><strong>Situation.</strong> Hver entitet har brug for et unikt id, og det oplagte standardvalg er en tilfældig UUID. Problemet er, at tilfældige id’er havner spredt ud over hele B‑tree‑indekset. Inserts spredes, indekset bliver fragmenteret, og efterhånden som tabellerne vokser, går det ud over både skrivninger og range‑scans. På en platform, der skal blive ved med at vokse, er det et langsomt, snigende tab, man helst ikke bygger ind fra starten.</p>\n<p><strong>Opgave.</strong> At bevare UUID’ens globale unikhed, men slippe af med den fragmentering, som tilfældigheden fører med sig.</p>\n<p><strong>Handling.</strong> Standarden blev UUID v7, som er tidsordnet — de forreste bits er et tidsstempel, så nye rækker lægger sig i rækkefølge i indekset i stedet for at blive strøet ud over det. PostgreSQL 18 har den indbygget som uuidv7(), og den er pakket ind i en lille uuid_generate_v7()-funktion, så det samme kald også fungerer rent på Azures Flexible Server; derefter blev den gjort til standard for entiteternes primærnøgler i hele skemaet. Intet eksotisk ved det; det er den slags beslutning, der er billig, hvis man træffer den tidligt, og besværlig at indføre bagefter.</p>\n<p><strong>Resultat.</strong> Id’erne forblev globalt unikke, indekset holdt op med at blive fragmenteret, sådan som tilfældige UUID’er forårsager, og tidsordnede inserts og range‑forespørgsler blev hurtigere — ensartet i alle tabeller, uden at nogen skulle tænke over det igen. Som en ekstra gevinst får hver entitet gratis en nøgle, der kan sorteres efter tid.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data engineering", "Databaser", "Optimering af ydeevne", "PostgreSQL", "Databasedesign & datamodellering", "Databaseoptimering" ] }, { "id": "https://engineer.company/da/portfolio/implemented-a-catalogue-driven-deep-merge/", "url": "https://engineer.company/da/portfolio/implemented-a-catalogue-driven-deep-merge/", "title": "Har implementeret en katalogdrevet deep‑merge", "summary": "Implementerede en katalogdrevet deep-merge for lagrede JSON-præferencer — forhindrer nedbrud ved manglende nøgler, når skemaet udvikler sig.", "content_html": "<p><strong>Situation.</strong> Platformen gemmer JSON‑præferencer — notifikationsindstillinger og den slags — som en brugers gemte værdier lagt oven på et sæt standardværdier. Den oprindelige merge gjorde det på ét niveau. Problemet viser sig senere: tilføjer man en ny nøgle til standardværdierne, har rækker, der blev gemt, før nøglen fandtes, den simpelthen ikke. Så læser noget klientkode feltet, får undefined og går ned — for netop de brugere, der har været med længst.</p>\n<p><strong>Opgave.</strong> Det skulle være sikkert at videreudvikle præferenceskemaet, så en ny indstilling aldrig kunne ødelægge noget for dem, der havde tilmeldt sig, før den fandtes.</p>\n<p><strong>Handling.</strong> Mergen på ét niveau blev erstattet af en deep merge, der bruger standardværdierne som katalog. Standardværdierne behandles som den autoritative liste over alle de nøgler, der skal findes; brugerens gemte værdier merges rekursivt ovenpå, så alt, hvad der står i kataloget, med garanti er til stede i resultatet, uanset om det fandtes i den gemte blob. Tilføjer man en nøgle til standardværdierne, dukker den automatisk op i de effektive præferencer for alle eksisterende rækker, indlejrede nøgler inklusive. Ændringen blev rullet ud via en migrering, så eksisterende data fik gavn af den med det samme i stedet for at skulle vente på at blive skrevet om.</p>\n<p><strong>Resultat.</strong> Præferencer kan vokse uden frygt. En ny indstilling risikerer ikke længere at få klienten til at gå ned på et undefined‑felt, og frontenden holdt op med at have brug for defensive tjek spredt ud over alle de steder, hvor den læser en præference. Det gav også resten af platformen en pålidelig måde at udvide enhver gemt JSON‑blob på — selve mønsteret, ikke kun den ene rettelse.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data governance", "Databaser", "Migrering & modernisering", "PostgreSQL", "Backend- & API‑udvikling", "Data governance & datakvalitet", "Databasedesign & datamodellering" ] }, { "id": "https://engineer.company/da/portfolio/provisioned-azure-infrastructure-as-code/", "url": "https://engineer.company/da/portfolio/provisioned-azure-infrastructure-as-code/", "title": "Har provisioneret Azure‑infrastruktur som kode", "summary": "Provisionerede Azure-infrastruktur som kode med Bicep — Container Apps, PostgreSQL, Front Door/WAF — reproducerbar i dev, staging og prod.", "content_html": "<p><strong>Situation.</strong> Platformen lever på Azure, og Azure sat op i hånden — klik rundt i portalen, en indstilling justeret her og der — er en fælde. Opsætningen glider ud af kurs, ingen husker, hvorfor noget er, som det er, og at genopbygge det efter en dårlig dag er langsomt og nervepirrende. Med mere end ét miljø at holde i takt bliver det kun værre.</p>\n<p><strong>Opgave.</strong> Læg det hele i kode, så et miljø er noget, man kan læse, reviewe og genskabe, i stedet for en bunke manuelt opsat tilstand.</p>\n<p><strong>Handling.</strong> Hele infrastrukturen er defineret i Bicep. Hvert miljø — testing, staging, product — bygges ud fra de samme templates: api og www, der kører som Azure Container Apps i et managed environment, en PostgreSQL Flexible Server, Redis til caching, Front Door med en WAF‑politik forrest og netværket nedenunder (VNet, NSG, private DNS), med Log Analytics koblet på til diagnostik. Images hentes fra projektets Azure Container Registry. Fordi det hele er parametriseret, kræver et nyt miljø eller en ændring af et eksisterende blot en pull request og ikke en supportsag til en selv.</p>\n<p><strong>Resultat.</strong> Miljøerne blev reproducerbare og til at reviewe. Afvigelser holdt op med at være et mysterium, fordi sandheden ligger i koden, og at sætte infrastruktur op eller genskabe den handler om at anvende templates og ikke om at huske, hvad der blev klikket på sidst. Det er forskellen mellem infrastruktur, man ejer, og infrastruktur, der ejer én.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Netværk & VPN", "Platformarkitektur", "Sikkerhed", "Cloud‑infrastruktur & migrering", "Infrastructure as Code", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-github-actions-ci-cd-pipelines/", "url": "https://engineer.company/da/portfolio/built-github-actions-ci-cd-pipelines/", "title": "Har bygget GitHub Actions CI/CD‑pipelines", "summary": "Byggede GitHub Actions CI/CD med et distroless produktions-frontend-image og promovering gennem flere miljøer — releases blev ren rutine.", "content_html": "<p><strong>Situation.</strong> Udgivelser bør ikke afhænge af, at nogen husker trinnene, og det, der ender med at køre i produktion, bør ikke være en oppustet container til alle formål med en shell og en package manager, den aldrig får brug for — det er bare angrebsflade, der ligger der uden grund.</p>\n<p><strong>Opgave.</strong> Gør vejen fra commit til kørende kode i Azure automatisk, og hold produktionsimages så små og stramt låst, som hver workload tillader.</p>\n<p><strong>Handling.</strong> Byggepipelinen er GitHub Actions. Separate workflows står for kodens kvalitetskontrol, testene og udrulningerne til de enkelte miljøer, med CodeQL, dependency review og et SBOM‑trin ved siden af, så intet når et miljø uden først at have bestået tjekkene. Alle images er multi‑stage‑builds, og base‑imaget for hver del blev valgt ud fra dens egne behov i stedet for som ét fælles valg: frontenden leveres på et distroless image (gcr.io/distroless/cc‑debian13 — ingen shell, ingen package manager), Go‑API’et på en slank Alpine og database‑imaget på postgres‑slim. Promovering flytter et build gennem miljøerne ad en fastlagt rute og ikke i hånden.</p>\n<p><strong>Resultat.</strong> Releases holdt op med at være et omhyggeligt manuelt ritual og blev en rutinemæssig, kedelig hændelse, hvilket er præcis, hvad man ønsker sig af releases. Frontenden i produktion kører på stort set det mindst mulige, tjekkene fanger problemerne, før de når frem, og “udrulning” er noget, byggepipelinen klarer, og ikke noget, nogen sveder sig igennem.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "Containere (Docker/Kubernetes)", "DevOps", "Driftssikkerhed & backup", "Sikkerhed", "Test & QA", "Containerisering & orkestrering", "DevOps & CI/CD‑automatisering" ] }, { "id": "https://engineer.company/da/portfolio/authored-578-go-task-automation-targets/", "url": "https://engineer.company/da/portfolio/authored-578-go-task-automation-targets/", "title": "Har skrevet 578 go‑task‑automatiseringsmål", "summary": "Skrev 578 go-task-mål i native-, Docker- og HTTPS-tilstand — linting, test, database og udrulning samlet i én værktøjskæde.", "content_html": "<p><strong>Situation.</strong> Kodebasen er et flersproget monorepo — Go, TypeScript, SQL, Python, shell — og hvert sprog har sin egen måde at bygge, teste, linte og køre på. Lader man det være, betyder det, at alle går rundt med en mental huskeseddel over værktøjsspecifikke kommandoer, og at nye folk bruger deres første dag på bare at finde ud af, hvordan man overhovedet får tingene til at køre.</p>\n<p><strong>Opgave.</strong> Giv hele projektet én hoveddør: én ensartet måde at køre hvad som helst på, uanset hvilket sprog det tilfældigvis er skrevet i.</p>\n<p><strong>Handling.</strong> Det er bygget med go‑task — et Taskfile‑lag, der er vokset til 578 navngivne mål, 50 i rodfilen og 528 i filer med egne navnerum under den. Der er udviklingstilstandene (native, Docker, en HTTPS‑variant til at teste PWA og mobil), kodekvaliteten (lint, format, test, fix for alle sprogene), databasehåndtering og de miljøspecifikke build- og deploy‑opgaver. Der er endda en low‑memory‑tilstand til maskiner, der ikke har RAM nok til at bygge frontenden på den sædvanlige måde. Pointen var aldrig at have mange tasks, men at man aldrig behøver at kende den underliggende kommando.</p>\n<p><strong>Resultat.</strong> Enhver kan køre task –list og se hele værktøjskæden lagt frem og køre en hvilken som helst del af den på samme måde, uanset hvad der er under motorhjelmen. Onboarding blev kortere, og de små, dumme fejl — forkert flag, forkert mappe, halvt husket kommando — forsvandt for det meste. Tallet er også en advarsel: 578 mål er langt mere, end nogen kan holde i hovedet, så det er navngivningen og navnerummene, der holder det brugbart — antallet er ikke noget at være stolt af.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Teknisk ledelse", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/owned-end-to-end-platform-deployments-to-azure/", "url": "https://engineer.company/da/portfolio/owned-end-to-end-platform-deployments-to-azure/", "title": "Har haft ansvaret for alle udrulninger til Azure", "summary": "Havde ansvaret for end-to-end Azure-udrulninger — releases gennem dev, staging og produktion ad en defineret, gentagelig vej.", "content_html": "<p><strong>Situation.</strong> Platformen skulle nå brugerne i flere Azure‑miljøer, og udrulningen er sømmen, hvor infrastrukturen, byggepipelinen og applikationen mødes. Det er også her, en lille fejl holder op med at være en bug og bliver til et nedbrud, så det er den del, man mindst af alt vil lave i hånden og halvt på hukommelsen.</p>\n<p><strong>Opgave.</strong> Hele udrulningen, fra ende til anden, blev samlet under ét ansvar, så en ændring hver gang kom ud i hvert miljø på samme forudsigelige måde.</p>\n<p><strong>Handling.</strong> Releases følger en fast rute — udvikling, derefter staging, derefter produktion — i stedet for at nogen pusher direkte til et live miljø. Udrulningspipelinen bygger images og udruller dem, og Bicep‑templatene holder målinfrastrukturen ens fra det ene miljø til det næste, så et build ikke hver gang promoveres ind i et miljø, der er en smule anderledes. Konfiguration, der er forskellig fra miljø til miljø, holdes adskilt fra hemmeligheder, hvilket betyder, at det samme byggede artefakt kan promoveres gennem miljøerne og blot henter de rette indstillinger der, hvor det lander, i stedet for at blive bygget om til hvert enkelt miljø.</p>\n<p><strong>Resultat.</strong> Ændringer når ud til hvert miljø på en forudsigelig måde, ad en fastlagt vej og uden manuelle ad hoc‑udrulninger indimellem. Udgivelser blev et kontrolleret, gentageligt trin i stedet for et øjeblik, hvor alle holdt vejret, og det er en stor del af forklaringen på, at den live platform forblev stabil, mens den stadig ændrede sig hurtigt nedenunder.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Cloud", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Sikkerhed", "Cloud‑infrastruktur & migrering", "DevOps & CI/CD‑automatisering", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/implemented-backups-and-disaster-recovery-as-code/", "url": "https://engineer.company/da/portfolio/implemented-backups-and-disaster-recovery-as-code/", "title": "Har implementeret backup og disaster recovery", "summary": "Implementerede databasebackups og en disaster recovery-strategi med infrastruktur som kode — hurtig, reproducerbar genopretning.", "content_html": "<p><strong>Situation.</strong> Et produkt, der lever på sine data, har ikke råd til at miste nogen, og “der er backups et sted” er et håb, ikke en genopretningsplan. Den eneste backup, der er noget værd, er en, man ved kan gendannes, i et miljø, man ved kan genopbygges. Produktet havde hverken det ene eller det andet skrevet ned.</p>\n<p><strong>Opgave.</strong> Få defineret, hvad der reelt kan genoprettes, i stedet for at antage det — dataene, miljøet omkring dem og en ærlig redegørelse for, hvor langt det rækker i dag.</p>\n<p><strong>Handling.</strong> Opbevaringen af backups er konfigureret i Bicep sammen med den database, de beskytter, så gendannelse til et vilkårligt tidspunkt er en egenskab ved templaten og ikke en indstilling, nogen engang klikkede på i en portal. Miljøet omkring databasen er også defineret som infrastructure‑as‑code, og det er den oversete halvdel, som folk glemmer: at gendanne en database i et miljø, man ville skulle genopbygge i hånden efter hukommelsen, er ikke rigtig genopretning. Derefter blev beredskabet gennemgået og skrevet ned — gendannelsesproceduren, den øvelse, der skal måle den, og de huller, der stadig står åbne: geo‑redundans er slået fra, og målet for genopretningstid er foreslået og ikke målt, fordi der til dato ikke er gennemført nogen øvelse.</p>\n<p><strong>Resultat.</strong> Genopretning holdt op med at være en vag forsikring og blev et dokumenteret beredskab, hvor hullerne er navngivet. Det lyder mindre imponerende end “disaster recovery: klaret”, og det er væsentligt mere værd — den næste, der rører ved det, ved, hvad der er dækket, hvad der ikke er, og præcis hvilken øvelse der lukker hullet. Et navngivet hul kan man lukke; et unavngivet bliver opdaget under en hændelse.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Cloud", "Databaser", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "PostgreSQL", "Backup & disaster recovery", "Databaseadministration (DBA)", "Infrastructure as Code" ] }, { "id": "https://engineer.company/da/portfolio/hardened-the-application-with-nonce-based-csp/", "url": "https://engineer.company/da/portfolio/hardened-the-application-with-nonce-based-csp/", "title": "Har hærdet applikationen med nonce‑baseret CSP", "summary": "Hærdede appen med nonce-baseret CSP, HSTS, SameSite-cookies, least-privilege-databaseroller og server-side-genkontrol af rettigheder.", "content_html": "<p><strong>Situation.</strong> Platformen rummer professionelle og organisatoriske data, den slags, folk forventer bliver håndteret ordentligt, så en enkelt forsvarslinje ville aldrig være nok. Arbejdsantagelsen må være, at klienten er fjendtlig — at alt, hvad browseren håndhæver, kan slås fra af den, der sidder ved browseren — og sikkerheden skal holde alligevel.</p>\n<p><strong>Opgave.</strong> Platformen skulle hærdes i hvert eneste lag — frontend, API, database — så sikkerheden blev håndhævet af serveren, uafhængigt af hvad brugerfladen nu tilfældigvis tillod.</p>\n<p><strong>Handling.</strong> I frontenden sætter Next.js’ middleware — <code>proxy.ts</code> — en Content‑Security‑Policy med en ny nonce for hver request og strict‑dynamic, plus HSTS og SameSite‑cookies, så browseren er stramt begrænset i, hvad den vil køre og sende. I API’et er der rate limiting, CORS, grænser for request‑størrelse, inputvalidering, før noget når databasen, og logning af de sikkerhedsrelevante hændelser. I databasen logger API’et ind som en least‑privilege‑rolle, der kun kan EXECUTE app‑funktionerne, funktionerne kører som SECURITY DEFINER, og alt er parametriseret. Og rettighederne — tier, rolle, organisation, afgrænsning til skibe — kontrolleres igen på serveren ved hver request, mens kontrollerne i frontenden kun betragtes som UX. De afgør, hvad man ser; serveren afgør, hvad man må.</p>\n<p><strong>Resultat.</strong> Sikkerheden afhænger ikke af, at brugerfladen opfører sig pænt. Beskyttelserne ligger i lag, så den, der kommer forbi ét, ikke dermed er forbi resten, og det hele er bygget på den antagelse, at man ikke kan stole på klienten — hvilket er den rigtige antagelse for data, folk overlader i fortrolighed.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Databaser", "Frontend‑udvikling", "Platformarkitektur", "Sikkerhed", "Backend- & API‑udvikling", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-an-english-danish-i18n-system/", "url": "https://engineer.company/da/portfolio/built-an-english-danish-i18n-system/", "title": "Har bygget et engelsk/dansk i18n‑system", "summary": "Etablerede et engelsk/dansk i18n-system med 12.027 beskeder pr. locale i 458 filer — linter-håndhævet ordforråd og et budget for filstørrelse.", "content_html": "<p><strong>Situation.</strong> Platformen kører på engelsk og dansk, og oversættelsessystemer har det med at udarte til et rod. Filerne vokser uden grænser, nøgler lækker — de findes i ét sprog og mangler i det andet — og de sprogspecifikke konventioner bliver anvendt ujævnt, så det ene sprog ender med at lyde, som om det var oversat af et udvalg, hvis medlemmer ikke talte sammen. For et professionelt produkt er det ikke en lille skønhedsfejl; det virker sjusket.</p>\n<p><strong>Opgave.</strong> Internationaliseringen skulle kunne skalere — begge sprog skulle forblive konsistente og korrekte, og filerne skulle stadig kunne vedligeholdes af et menneske et år senere.</p>\n<p><strong>Handling.</strong> Det er bygget på next‑intl med regler, som værktøjerne faktisk håndhæver. Den danske side har et fastlåst ordforråd og en fastlåst stil — ægte æøå, det uformelle “du”, de rette accenter i bydeform og semantiske mappinger for sammensatte ord, så de oversættes efter betydning og ikke ord for ord — og en linter holder den fast på det. Der er et hårdt budget på 400 linjer pr. navnerumsfil og en tilgang, hvor et stort område deles op i indlejrede filer, der flettes rent sammen igen, i stedet for at én fil vokser i det uendelige; det budget er grunden til, at 12.027 beskeder pr. sprog ligger i 458 filer og ikke i en håndfuld enorme. Et linter‑tjek sammenligner nøglerne på tværs af sprogene, så intet lækker eller mangler. Og gemte overrides flettes ind ud fra kataloget i stedet for via skrøbelige fallbacks på øverste niveau — den samme deep merge‑idé, som bruges til præferencerne.</p>\n<p><strong>Resultat.</strong> Begge sprog forbliver korrekte og konsistente med 24.054 beskeder tilsammen, og filerne forbliver til at vedligeholde, efterhånden som antallet af strenge stiger. Sprogkvaliteten blev til noget, værktøjerne garanterer ved hver commit, i stedet for noget, der i det stille forringes, hver gang nogen tilføjer en streng i en fart. Budgettet er den bærende del: uden en grænse pr. fil ville 458 filer have været tolv, og ingen ville åbne dem.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Dokumentation", "Frontend‑udvikling", "Full stack‑udvikling", "Internationalisering", "Internationalisering & lokalisering" ] }, { "id": "https://engineer.company/da/portfolio/set-a-zero-warnings-bar-across-six-languages/", "url": "https://engineer.company/da/portfolio/set-a-zero-warnings-bar-across-six-languages/", "title": "Har indført nul advarsler som krav i seks sprog", "summary": "Indførte krav om nul advarsler i Go, TypeScript, SQL, Python, Shell og Markdown — håndhævet af pre-commit-hooks.", "content_html": "<p><strong>Situation.</strong> Advarsler, der hober sig op, har en snigende, ætsende virkning. Hver melding, man ignorerer, sænker barren en smule, og når først buildet spytter fyrre af dem ud, læser ingen af dem, og et reelt problem står i listen for øjnene af alle, fordi “advarsler” er blevet til baggrundsstøj. I en flersproget kodebase er der så meget desto flere støjkilder, der kan få det til at ske.</p>\n<p><strong>Opgave.</strong> Repoet skulle have én kompromisløs kvalitetsstandard for alle sprog, så problemer blev rettet i stedet for at hobe sig op.</p>\n<p><strong>Handling.</strong> Der blev indført en politik om nul advarsler, og det er værktøjerne, der håndhæver den, for en politik, der bygger på alles årvågenhed, bukker under i den første travle uge. Hver eneste linter‑melding er en fejl — der er intet “warn”-niveau at gemme sig i — og det er det samme på tværs af hele stakken: Go med golangci‑lint, TypeScript med ESLint, SQL med SQLFluff, Python med Ruff, shell med ShellCheck, Markdown med markdownlint. Inline‑undertrykkelse er forbudt, så man kan ikke dække over en melding; man er nødt til rent faktisk at rette problemet. Pre‑commit- og pre‑push‑hooks kører linterne og testene, så en commit, der ville indføre et problem, slet ikke bliver lavet. Der er endda grænser for funktions- og fillængde, så moduler ikke vokser sig så store, at de ikke længere er til at læse.</p>\n<p><strong>Resultat.</strong> Problemer bliver rettet ved kilden i stedet for at blive skubbet over i en backlog, som ingen får ryddet, og kodebasen forbliver ren som udgangspunkt og ikke takket være heltemodige oprydninger engang imellem. Standarden er den samme, uanset hvilket sprog man arbejder i, og det er værktøjerne, der opretholder den — ikke nogens viljestyrke — og derfor holder den faktisk.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Teknisk ledelse", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/set-the-platforms-founding-technical-decisions/", "url": "https://engineer.company/da/portfolio/set-the-platforms-founding-technical-decisions/", "title": "Har fastlagt platformens grundbeslutninger", "summary": "Fastlagde platformens grundlæggende beslutninger i november 2025 — lagdelingen, database-first-dataadgang og kravet om nul advarsler — og de holder stadig.", "content_html": "<p><strong>Situation.</strong> Repositoryet blev oprettet den 25. november 2025 og var helt tomt. Det, der bliver besluttet i de første par uger af et sådant projekt, vejer uforholdsmæssigt tungt: lagdelingen, hvor forretningslogikken må ligge, hvad kvalitetsstandarden er. De valg er billige at træffe på dag tre og næsten umulige at lave om i måned seks, hvor alt, hvad der er skrevet siden, bygger på dem.</p>\n<p><strong>Opgave.</strong> De grundlæggende beslutninger skulle træffes bevidst og tidligt og i en form, der kunne holde til at blive overdraget til andre og til en langt større kodebase, end der fandtes dengang.</p>\n<p><strong>Handling.</strong> Tre beslutninger gjorde det meste af arbejdet. Stakken blev lagdelt, så hver del har én opgave — en Next.js‑frontend, et Go‑API, der validerer og videresender, et PostgreSQL‑funktionslag, der ejer forretningsreglerne — i stedet for at logikken havnede der, hvor det nu passede bedst den eftermiddag. Dataadgang blev lagt bag stored functions fra starten, og det er det valg, alt andet i databasearbejdet følger af; at indføre det bagefter ville have betydet at skrive samtlige handlers om. Og kravet om nul advarsler blev indført, før der var ret meget kode at holde op imod det, for en standard, der indføres ved commit 5.000, er et oprydningsprojekt, mens den samme standard ved commit 50 blot er den måde, repositoryet fungerer på. Ingen af de tre var den nemme løsning dengang, og alle tre kostede fart i den første måned.</p>\n<p><strong>Resultat.</strong> Ni måneder og flere tusinde commits senere holder alle tre stadig: lagene er ikke flydt sammen, ingen handler taler direkte med en tabel, og buildet giver stadig ingen advarsler. Det er den prøve, en grundlæggende beslutning skal bestå — ikke om den lød rigtig, men om den holdt, da den mødte den mængde arbejde, der fulgte, for det er dér, bekvemme valg som regel i det stille bliver opgivet.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Databaser", "DevOps", "Frontend‑udvikling", "Full stack‑udvikling", "Løsningsarkitektur", "Platformarkitektur", "Sikkerhed", "Teamledelse", "Teknisk ledelse", "Backend- & API‑udvikling", "Full stack‑produktudvikling", "Platform- & løsningsarkitektur", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/delivered-analysis-and-regular-progress-reports/", "url": "https://engineer.company/da/portfolio/delivered-analysis-and-regular-progress-reports/", "title": "Har leveret analyser og løbende statusrapporter", "summary": "Leverede analyser og løbende statusrapporter til direktøren — omsatte tekniske metrikker og leveringsstatus til trygge beslutninger.", "content_html": "<p><strong>Situation.</strong> Efterhånden som platformen tog form, skulle dens fremdrift og tilstand være synlige for ledelsen i en form, den rent faktisk kunne handle på. Rå tekniske signaler — build‑status, leveringstempo, hændelser — siger ikke meget i sig selv til en, der træffer beslutninger om produkt og investeringer; de ligger på det forkerte niveau. Nogen måtte oversætte.</p>\n<p><strong>Opgave.</strong> Den oversættelse var opgaven: at tage den tekniske virkelighed — hvor leverancerne stod, hvad systemets målinger viste — og rapportere den klart og regelmæssigt til CEO’en, så beslutningerne hvilede på fakta og ikke på gætværk.</p>\n<p><strong>Handling.</strong> Der blev indført en fast rapporteringsrytme i stedet for at rapportere, når nogen spurgte, hvilket altid er en anelse for sent. Fremdrift, scope, risici og systemtilstand blev fulgt og omsat til klare, beslutningsorienterede opdateringer — hvad der var på rette spor, hvad der var i fare, og hvad en given prioritering reelt ville koste i fravalg. Det, der skulle undgås, var at aflevere rå tal og overlade fortolkningen til nogen uden kontekst; hver rapport kom med konkrete anbefalinger, og hvor det hjalp, blev fortællingen underbygget af de bagvedliggende analyser, så ledelsen selv kunne gå i dybden og ikke behøvede at tage det for gode varer.</p>\n<p><strong>Resultat.</strong> Ledelsen fik et klart, ærligt og aktuelt billede af udviklingsarbejdet og kunne styre produktprioriteter og investeringer på et sikrere grundlag i stedet for at flyve i blinde. Rapporteringen holdt op med at være et statusritual, som ingen læser, og blev til noget, beslutninger rent faktisk blev truffet ud fra — og det holdt det tekniske arbejde og forretningens retning på samme kurs.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Dataanalyse", "Interessenter & rapportering", "Produkt & krav", "Projektledelse", "Teknisk ledelse", "Dataanalyse & BI‑dashboards", "Projektledelse (Agile)", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/ran-24-7-support-for-1000-iptv-ott-servers/", "url": "https://engineer.company/da/portfolio/ran-24-7-support-for-1000-iptv-ott-servers/", "title": "Har ydet 24/7‑support til ca. 1.000 IPTV‑servere", "summary": "Leverede 24/7-infrastruktursupport til en IPTV/OTT-streamingplatform — ~1.000 servere plus kundesystemer i Kina, USA og Tyskland.", "content_html": "<p><strong>Situation.</strong> Der var tale om en IPTV/OTT‑streamingplatform med kunder spredt over Kina, USA og Tyskland, og det betød, at der ikke fandtes noget stille tidspunkt til vedligeholdelse — et eller andet sted sad der altid nogen og så med. Nedetid på sådan en platform er ikke et abstrakt måltal; det er nogens fjernsyn, der bare går i stå, og de er ligeglade med hvorfor.</p>\n<p><strong>Opgave.</strong> Opgaven var at holde platformens infrastruktur tilgængelig døgnet rundt — og vel at mærke døgnet rundt, ikke “kontortid plus en tilkaldevagt, som ingen svarer på”.</p>\n<p><strong>Handling.</strong> 24/7‑supporten dækkede omkring tusind servere og derudover et pænt antal systemer, som kunderne selv ejede — administration, overvågning, sikring og konfiguration på tværs af hele streamingmiljøet. Fordi kunderne sad i tre vidt forskellige tidszoner, fandtes “efter arbejdstid” ikke rigtig; et problem klokken 3 om natten lokal tid var bedste sendetid for andre, så det blev behandlet som bedste sendetid. En stor del af arbejdet bestod i at opdage, at noget var ved at skride, før det blev til et nedbrud, for på en streamingplatform, der sender live, kan man ikke bare rette tingene i det stille bagefter.</p>\n<p><strong>Resultat.</strong> Platformen var uafbrudt tilgængelig for et globalt publikum, og problemerne blev fanget og håndteret, uanset hvornår på døgnet de dukkede op, før de nåede en seers skærm. På en 24/7‑tjeneste er det hele opgaven — succes er, at der ikke sker noget, og det var præcis, hvad seerne ønskede.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Cloud", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Overvågning & observerbarhed", "Systemadministration", "It‑support & helpdesk", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/kept-iptv-streaming-signals-running-non-stop/", "url": "https://engineer.company/da/portfolio/kept-iptv-streaming-signals-running-non-stop/", "title": "Har sikret uafbrudt levering af IPTV‑signaler", "summary": "Sikrede uafbrudt IPTV-streaming mellem leverandører og kunder — overvågede og vedligeholdt streamingnetværk og IP-telefoni døgnet rundt.", "content_html": "<p><strong>Situation.</strong> IPTV står og falder med, at signalet kommer igennem. Streamene løber fra leverandørerne gennem platformen og ud til kunderne, og et brud et hvilket som helst sted i den kæde betyder en sort skærm for nogen. IP‑telefonien kørte side om side med den og havde samme krav: den skulle bare virke.</p>\n<p><strong>Opgave.</strong> Opgaven var at garantere, at signalleveringen og telefonien kørte uden afbrydelser.</p>\n<p><strong>Handling.</strong> Streamingnetværket og IP‑telefonien blev overvåget, fejlsøgt og vedligeholdt døgnet rundt. Pointen med at holde konstant øje er, at streamingproblemer viser sig som en forringelse, før de bliver til et egentligt udfald — en stream, der begynder at hakke, et link, der bliver ustabilt — og er man opmærksom, kan man fange det, mens det hakker, og ikke først, når skærmen bliver sort. En stor del af arbejdet handlede derfor om at være på forkant med signalet i stedet for at reagere på klager over det.</p>\n<p><strong>Resultat.</strong> Streamene og opkaldene forblev pålidelige på hele platformen, og problemer blev opdaget og rettet, før nogen nåede at bemærke, at tjenesten faldt ud. At holde et signal kørende mellem leverandører og slutkunder uden synlige huller er stille, konstant arbejde, og stille er netop, hvad det skal være.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Overvågning & observerbarhed", "Systemadministration", "Netværk & VPN‑opsætning", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/built-and-maintained-django-apps-for-iptv/", "url": "https://engineer.company/da/portfolio/built-and-maintained-django-apps-for-iptv/", "title": "Har bygget og vedligeholdt Django‑apps til IPTV", "summary": "Udviklede og vedligeholdt Django-webapplikationer til en IPTV-platform — nye funktioner samt bedre ydeevne og stabilitet.", "content_html": "<p><strong>Situation.</strong> Omkring IPTV‑platformen lå der webapplikationer bygget på Django — de dele, folk rent faktisk klikkede på — og de skulle hele tiden udvikles videre: nye funktioner og den ydeevne og stabilitet, som en platform, der kører døgnet rundt, kræver.</p>\n<p><strong>Opgave.</strong> Opgaven var at holde applikationerne funktionsrige, hurtige og stabile.</p>\n<p><strong>Handling.</strong> Django‑webapplikationerne blev udviklet og vedligeholdt — nye funktioner blev bygget, fejl blev rettet, og der blev arbejdet målrettet på ydeevne og stabilitet, i samarbejde med resten af teamet og ikke isoleret i et hjørne. I et system, der betjener kunder uafbrudt, er stabiliteten ikke en luksus ved siden af funktionerne; den er den ramme, funktionerne skal holde sig inden for. En prangende funktion, der får systemet til at vakle, er ikke meget værd, når systemet ikke har råd til at vakle.</p>\n<p><strong>Resultat.</strong> Applikationerne blev ved med at få flere funktioner og forblev samtidig pålidelige for både de interne teams og kunderne. At bygge videre på noget uden at gøre det ustabilt var den balance, der betød noget her, og den holdt arbejdet fast ved.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Frontend‑udvikling", "Full stack‑udvikling", "Python", "Webudvikling", "Backend- & API‑udvikling", "Full stack‑produktudvikling", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/built-infrastructure-that-still-runs-years-later/", "url": "https://engineer.company/da/portfolio/built-infrastructure-that-still-runs-years-later/", "title": "Har bygget infrastruktur, der holder i årevis", "summary": "Planlagde og byggede infrastruktur til interne og eksterne systemer — robust nok til stadig at køre år senere med minimale ændringer.", "content_html": "<p><strong>Situation.</strong> Efterhånden som organisationen voksede, skulle dens interne og eksterne systemer hele tiden have nye funktioner skruet på. Den nemme løsning er det, der går hurtigst i dag; problemet med den nemme løsning er, at man står og retter i det igen om et halvt år.</p>\n<p><strong>Opgave.</strong> Opgaven var at planlægge og bygge infrastrukturfunktionalitet, der rent faktisk ville holde — som ikke bare virkede nu, men blev ved med at virke.</p>\n<p><strong>Handling.</strong> Ny infrastrukturfunktionalitet blev planlagt og implementeret på tværs af de interne og eksterne systemer, designet til at holde — den slags, man bygger ordentligt én gang, så den kører i årevis med et minimum af indgreb i stedet for at kræve konstant opmærksomhed. Det er et bevidst valg hver gang: at tænke lidt mere over tingene fra starten, så man ikke binder sig til at passe den i al evighed.</p>\n<p><strong>Resultat.</strong> Systemerne forblev funktionelle og effektive længe efter, at de blev bygget, og kørte i årevis næsten uden ændringer. Den lange levetid er det egentlige mål for infrastrukturarbejde — enhver kan lave noget, der virker i dag; at lave noget, der stadig stille og roligt virker mange år senere, er både sværere og mere nyttigt.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Infrastruktur", "Løsningsarkitektur", "Platformarkitektur", "Systemadministration", "Platform- & løsningsarkitektur", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/maintained-a-legacy-hugo-site-and-improved-its-ui/", "url": "https://engineer.company/da/portfolio/maintained-a-legacy-hugo-site-and-improved-its-ui/", "title": "Har vedligeholdt et ældre Hugo‑website og dets UI", "summary": "Vedligeholdt og forbedrede et ældre Hugo-baseret website og bidrog med UI/UX-forbedringer til det centrale asset management-produkt.", "content_html": "<p><strong>Situation.</strong> Der var et ældre website bygget på Hugo — en generator til statiske sites — der kørte ved siden af virksomhedens hovedprodukt, et asset management‑system. Websitet var det gamle og etablerede; det var i produktet, den reelle værdi lå.</p>\n<p><strong>Opgave.</strong> Opgaven var at holde websitet i god stand og samtidig forbedre oplevelsen af produktet.</p>\n<p><strong>Handling.</strong> Det statiske Hugo‑site blev vedligeholdt og forbedret — holdt opdateret og fungerende — mens forbedringer af UI/UX og feedback samtidig blev ført ind i det primære asset management‑produkt. At dele opmærksomheden mellem et ældre site og flagskibsproduktet handler mest om ikke at lade det gamle forfalde, mens man har fokus på det nye; begge repræsenterer virksomheden over for nogen, så begge skulle holdes i ordentlig stand.</p>\n<p><strong>Resultat.</strong> Websitet forblev opdateret i stedet for langsomt at forælde, og hovedproduktets brugervenlighed blev bedre gennem løbende, velinformerede finjusteringer — den slags, der kommer af rent faktisk at bruge og tænke over produktet frem for at redesigne det i ét stort greb.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Frontend‑udvikling", "Full stack‑udvikling", "UX/UI‑design", "Webudvikling", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/designed-a-time-management-and-reporting-system/", "url": "https://engineer.company/da/portfolio/designed-a-time-management-and-reporting-system/", "title": "Har designet et tids- og rapporteringssystem", "summary": "Designede et tids- og rapporteringssystem, der forblev i produktion i årevis uden væsentlige ændringer.", "content_html": "<p><strong>Situation.</strong> Teamet havde ikke en pålidelig måde at styre tid og rapportere fremdrift på. Det betyder som regel, at det foregår i spredte regneark og i folks hukommelse, og at rapporteringen bliver et hastværksarbejde ved slutningen af hver periode i stedet for noget, der helt naturligt følger af den måde, folk arbejder på.</p>\n<p><strong>Opgave.</strong> Opgaven var at bygge et system til begge dele, der rent faktisk ville holde.</p>\n<p><strong>Handling.</strong> Et system til tidsstyring og rapportering blev designet op omkring den måde, teamet reelt arbejdede på, i stedet for at påtvinge teamet en standardproces, som det ville finde veje uden om. Det er hele kunsten med interne værktøjer — passer værktøjet til den reelle arbejdsgang, bruger folk det; kæmper det imod arbejdsgangen, opgiver de det i det stille, og så er man tilbage ved regnearkene. Derfor blev det bygget, så det passede til virkeligheden og ikke til et ideal.</p>\n<p><strong>Resultat.</strong> Det forblev i produktion i årevis uden væsentlige ændringer. Det er den ros, man ønsker sig for et internt værktøj — ikke at det var imponerende, men at det bare blev ved med at virke, og at ingen nogensinde fik brug for at udskifte det. Noget, der klarer sig gennem flere års daglig brug uden at blive rørt, var tydeligvis bygget til at passe.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Produkt & krav", "Projektledelse", "Teknisk ledelse", "Dataanalyse & BI‑dashboards", "Produktstrategi & kravspecifikation", "Projektledelse (Agile)" ] }, { "id": "https://engineer.company/da/portfolio/integrated-a-company-wide-password-manager/", "url": "https://engineer.company/da/portfolio/integrated-a-company-wide-password-manager/", "title": "Har integreret en fælles adgangskodeløsning", "summary": "Integrerede et virksomhedsdækkende system til adgangskodestyring — stærkere sikkerhed og strømlinet adgangskontrol i hele organisationen.", "content_html": "<p><strong>Situation.</strong> Adgangsoplysninger blev håndteret uensartet — hver især lagrede og delte dem på sin egen improviserede måde — og netop den uensartethed er i sig selv sikkerhedsrisikoen. Det er sjældent et dramatisk brud; det er en adgangskode i en chatbesked, et delt login, som ingen nogensinde skifter adgangskode på, en langsom ophobning af små eksponeringer.</p>\n<p><strong>Opgave.</strong> Opgaven var at samle adgangsoplysningerne ét sted og gøre dem sikre.</p>\n<p><strong>Handling.</strong> Der blev indført et fælles system til håndtering af adgangskoder for hele virksomheden, så der var én ensartet, sikker måde at lagre og dele oplysningerne på i stedet for hver enkelts personlige vaner. Pointen med at gøre det for hele virksomheden er netop, at det ikke er op til den enkelte — en password manager, som kun halvdelen af teamet bruger, hjælper knap nok, for risikoen ligger i den halvdel, der lader være. Målet var derfor at gøre den sikre måde til standarden overalt.</p>\n<p><strong>Resultat.</strong> Sikkerheden blev bedre, og adgangskontrollen blev enklere og mere konsistent i hele organisationen. Når alle oplysningerne først ligger ét centralt administreret sted, bliver en hel række små, kedelige eksponeringer simpelthen umulige — og det er det meste af, hvad sikkerhed i praksis handler om.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Infrastruktur", "Sikkerhed", "Systemadministration", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/combined-web-development-with-seo-and-content/", "url": "https://engineer.company/da/portfolio/combined-web-development-with-seo-and-content/", "title": "Har kombineret webudvikling med SEO og indhold", "summary": "Skabte succes for kunder på nettet ved at kombinere skræddersyet webudvikling og -design med SEO, indholdsstrategi og tekstforfatning.", "content_html": "<p><strong>Situation.</strong> Kunderne ville egentlig ikke have et website; de ville have det, et website skal gøre for dem — blive fundet, trække kunder til, rent faktisk fungere som en kanal. Et smukt site, ingen kan finde, er en fiasko, der ligner en succes.</p>\n<p><strong>Opgave.</strong> Opgaven var at samle udviklingen og vækstdelen i ét tilbud i stedet for at aflevere et site og ønske kunden held og lykke.</p>\n<p><strong>Handling.</strong> De to halvdele blev sat sammen — skræddersyet webudvikling og -design på den ene side, SEO, indholdsstrategi og tekstforfatning på den anden — så kunden fik noget, der både var velbygget og rent faktisk til at finde. De to ting bliver som regel behandlet som separate opgaver, og det er sådan, man ender med et flot site, der ikke ligger højt i nogen søgeresultater, eller et veloptimeret site, der er ubehageligt at bruge. Når begge dele lå samme sted, blev sitet designet til at blive fundet fra starten og ikke optimeret bagefter, når alt andet var på plads.</p>\n<p><strong>Resultat.</strong> Kunderne endte med stærkere synlighed og mere engagement — deres sites fungerede som ægte vækstkanaler og ikke som onlinebrochurer. At bygge sitet og gøre det til at finde i én arbejdsgang er det, der forvandlede et website fra en udgift til noget, der rent faktisk tjente sig selv ind.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Brand & marketing", "Frontend‑udvikling", "Produkt & krav", "UX/UI‑design", "Webudvikling", "Brand, marketing & SEO", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/recovered-data-from-sd-cards-hdds-ssds-and-raid/", "url": "https://engineer.company/da/portfolio/recovered-data-from-sd-cards-hdds-ssds-and-raid/", "title": "Har gendannet data fra harddiske, SSD'er og RAID", "summary": "Gendannede kritiske data fra SD-kort, harddiske, SSD'er, RAID-arrays, eksterne drev og Mac — filer reddet fra enheder, der var afskrevet som tabt.", "content_html": "<p><strong>Situation.</strong> Folk dukkede op med lagermedier, der var gået i stykker eller beskadiget, og med data på dem, som de akut skulle have tilbage — og når nogen først bærer et dødt drev ind i en butik, er de som regel ikke længere bare bekymrede, men i fuld panik. Billeder, forretningsfiler, den eneste kopi af noget, der betød noget.</p>\n<p><strong>Opgave.</strong> Opgaven var at gendanne deres data, uanset hvilke medier de kom ind med.</p>\n<p><strong>Handling.</strong> Datagendannelsen omfattede stort set alle almindelige lagertyper — SD‑kort, roterende harddiske, SSD’er, RAID‑arrays, eksterne drev, Mac‑systemer. Hver af dem går i stykker og giver sine data fra sig på hver sin måde: en SSD er et andet problem end et pladedrev, et RAID‑array er et andet problem igen, og et Mac‑filsystem har sine egne særheder. Arbejdet handlede derfor lige så meget om at kende den rette tilgang til det konkrete medie som om nogen enkelt teknik.</p>\n<p><strong>Resultat.</strong> Kunderne fik kritiske data tilbage fra enheder, de allerede havde afskrevet som tabte, og det gjaldt alle almindelige lagertyper. Der er en helt særlig lettelse at se i folks ansigt, når man rækker drevet tilbage med filerne i behold — det var hele pointen med arbejdet, og det skete på tværs af alle slags medier.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Systemadministration", "Hardwarereparation & datagendannelse", "It‑support & helpdesk" ] }, { "id": "https://engineer.company/da/portfolio/built-a-test-suite-across-go-sql-and-browser/", "url": "https://engineer.company/da/portfolio/built-a-test-suite-across-go-sql-and-browser/", "title": "Har bygget en testsuite til Go, SQL og browseren", "summary": "Byggede en lagdelt testsuite — 981 Go-tests, 543 frontend-specs og 494 SQL-adfærdstests — med gates for mutation, property-based test og tilgængelighed.", "content_html": "<p><strong>Situation.</strong> En platform, der holder sin forretningslogik i databasen, har et testproblem, de fleste projekter ikke har. Logikken ligger ikke i det sprog, testframeworket er godt til — den ligger i SQL, bag funktionsgrænser, og SQL er præcis den slags kode, der ender utestet, fordi den er besværlig at teste. Lægger man et Go‑API og en Next.js‑frontend oven på, bliver “de vigtige dele er dækket” stille og roligt til “de dele, der var nemme at dække, er dækket”.</p>\n<p><strong>Opgave.</strong> Hvert lag skulle have sin adfærd kontrolleret dér, hvor adfærden faktisk hører hjemme, i stedet for at alt blev kontrolleret udefra gennem en browser.</p>\n<p><strong>Handling.</strong> Fire lag fik fire slags test. Go‑API’et har 981 testfunktioner fordelt på 310 filer. Frontenden har 543 Vitest- og Playwright‑specs, heraf 48 end‑to‑end‑filer og 30 browser‑specs. Databasen har 494 adfærdstestfiler — 116.876 linjer SQL, der kontrollerer resultaterne via 6.654 rejste exceptions — så en stored function bliver testet i databasen og ikke gennem tre applikationslag ovenover. Oven over dem ligger de test, der tester testene: mutationstest med Stryker ødelægger med vilje en linje og fejler, hvis ingen test opdager det, og fast‑check genererer input, som ingen havde tænkt på at skrive ned. En axe‑core‑kontrol kræver nul overtrædelser af WCAG 2.0 og 2.1 på niveau A og AA, hvilket gør tilgængelighed til noget, der får buildet til at fejle, snarere end til et revisionsfund måneder senere. Tærsklerne for testdækning bliver kun hævet. Hele suiten kører som 10 jobs i et workflow på 612 linjer.</p>\n<p><strong>Resultat.</strong> At ændre noget strukturelt holdt op med at være skræmmende, og det er det eneste, der forhindrer en kodebase af den størrelse i at forkalke. Den ærlige pris er tid — suiten er langsom, den lægger en ekstra byrde på hver eneste ændring, og i den størrelse skal den selv vedligeholdes. Til gengæld får man evnen til at blive ved med at bevæge sig hurtigt, og det er mere værd end de minutter, den tager.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Backend‑udvikling", "DevOps", "Driftssikkerhed & backup", "Frontend‑udvikling", "PostgreSQL", "Test & QA", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/built-stripe-and-in-app-purchase-entitlements/", "url": "https://engineer.company/da/portfolio/built-stripe-and-in-app-purchase-entitlements/", "title": "Har bygget adgangsret via Stripe og in‑app‑køb", "summary": "Byggede betalings- og rettighedslaget — Stripe med Apple og Google in-app purchase — adgang til directory, søgning og eksport styret af en adgangsmodel.", "content_html": "<p><strong>Situation.</strong> Penge er den del af en platform, ingen har lov til at tage let på. Et abonnement skal kunne overleve et udløbet kort, en refusion, et planskifte, en webhook, der ankommer to gange, og en webhook, der ankommer i forkert rækkefølge. I det øjeblik betaling kan ske i tre butikker — et kort på nettet, Apple i den ene app store, Google i den anden — findes der tre forskellige udlægninger af, hvad nogen har købt, og produktet har stadig brug for ét svar på ét spørgsmål: hvad må denne person lige nu?</p>\n<p><strong>Opgave.</strong> At tage imod penge og at give adgang skulle være to systemer i stedet for ét, så en ny butik ikke betød, at hver eneste adgangskontrol i produktet skulle skrives om.</p>\n<p><strong>Handling.</strong> Stripe håndterer kort og abonnementer gennem 18 Go‑filer, og Apple- og Google‑in‑app‑køb kommer ind gennem deres egen kvitteringsverifikation. Alle tre løber sammen i et payments‑skema på 9 tabeller og 34 funktioner — og stopper så dér. Det, produktet faktisk spørger om, er et separat access‑skema på 11 tabeller og 34 funktioner, som svarer på “må denne konto det her?” uden at vide eller bryde sig om, hvilken butik der har betalt for det. Det svar styrer adgangen til virksomhedskataloget, søgningen og dataeksporten, og det genkontrolleres på serveren ved hver request, for en skjult knap er en høflighed og ikke en kontrol.</p>\n<p><strong>Resultat.</strong> En ny butik rører nu ved payments‑siden og lader adgangskontrollerne være, og et supportspørgsmål om nogens adgang har én tabel at kigge i frem for tre. Prisen er to skemaer, hvor et mindre produkt ville nøjes med ét, plus et rettighedsopslag på requests, der ellers ville have været gratis.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Backend‑udvikling", "Databaser", "PostgreSQL", "Produkt & krav", "Sikkerhed", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Produktstrategi & kravspecifikation", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-first-party-analytics-inside-postgresql/", "url": "https://engineer.company/da/portfolio/built-first-party-analytics-inside-postgresql/", "title": "Har bygget egen analytics i PostgreSQL", "summary": "Byggede egen produktanalyse i PostgreSQL — 47 funktioner over partitionerede tabeller, der selv rydder op — pseudonymiseret og betinget af samtykke.", "content_html": "<p><strong>Situation.</strong> Produktbeslutninger kræver tal, og den almindelige måde at få dem på er at sætte et tag fra en tredjepart på hver side og lade andres servere holde øje med brugerne. Det er hurtigt, det er gratis ved små mængder, og det betyder, at besøgendes adfærd på en professionel netværksplatform — hvem der kiggede på hvilken virksomhed, hvem der søgte efter hvad — bliver et aktiv, en annoncevirksomhed sidder på. For en platform, hvis brugere er maritime fagfolk, der kan identificeres, er det en dårlig handel.</p>\n<p><strong>Opgave.</strong> Produktteamet havde brug for funnels, retention og hændelsesdata, indsamlet på en måde, som platformen kunne stå inde for, og som en bruger kunne sige nej til.</p>\n<p><strong>Handling.</strong> Analytics kom til at ligge i PostgreSQL, i et skema på 7 tabeller og 47 funktioner. Hændelsestabellerne er partitionerede — 22 partitionsdefinitioner — og rydder automatisk op i sig selv efter en fast plan, for prisen ved first‑party analytics ligger ikke i at indsamle hændelserne, men i at gemme dem for evigt. Identitet er pseudonymiseret bag et salt, der roterer, så en besøgende ikke kan følges hen over rotationsgrænsen, ikke engang inde fra databasen; det er et bevidst loft over, hvad dataene kan svare på. Klienten tager højde for samtykke og sender intet, før samtykket er givet, i stedet for at sende og filtrere bagefter. Konverteringsfunnels beregnes i databasen, ved siden af dataene, i stedet for at blive eksporteret et andet sted hen for at blive joinet tilbage.</p>\n<p><strong>Resultat.</strong> Spørgsmål om, hvordan produktet bruges, besvares fra platformens egne tabeller, uden at noget forlader den, og uden et tag i siden. Begrænsningerne er den ærlige del: at rotere saltet koster muligheden for kohorteanalyser over lang tid, og intet af det her kommer med de dashboards, man får gratis med et hostet værktøj. Begge dele blev accepteret med åbne øjne.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Data engineering", "Data governance", "Databaser", "Dataanalyse", "Optimering af ydeevne", "PostgreSQL", "SQL", "Data governance & datakvalitet", "Databasedesign & datamodellering", "Dataanalyse & BI‑dashboards", "Udvikling af datapipelines (ETL/ELT)" ] }, { "id": "https://engineer.company/da/portfolio/built-company-ownership-claims-and-adjudication/", "url": "https://engineer.company/da/portfolio/built-company-ownership-claims-and-adjudication/", "title": "Har bygget ejerskabskrav og deres afgørelse", "summary": "Byggede hele forløbet for ejerskabskrav på virksomheder: en bruger gør krav, en administrator afgør sagen, og godkendelsen omskriver autorisationsgrafen.", "content_html": "<p><strong>Situation.</strong> Et katalog, der er seedet fra offentlige kilder, har et strukturelt problem: virksomhederne i det har ikke selv sat sig der. Før eller siden dukker nogen fra en af dem op og vil rette sin egen post — og der er ingen relation mellem den person og den post, kun en påstand om, at der er en. Godkender man kravet for let, redigerer en konkurrent virksomhedens side. Godkender man det for langsomt, bliver kataloget ved med at være forkert.</p>\n<p><strong>Opgave.</strong> Der skulle være en vej fra “det her er min virksomhed” til reel råderet over posten, med en menneskelig beslutning i midten og et spor bagefter.</p>\n<p><strong>Handling.</strong> Ejerskabskrav blev bygget fra ende til anden, fordelt på 108 commits og begge applikationer. En bruger indsender et krav med dokumentation; det lander på en arbejdsliste i back‑officen; en administrator gennemgår det og godkender eller afviser det med en begrundelse, der går tilbage til den, der rejste kravet. Det interessante er, hvad en godkendelse gør — det er ikke et flag på en række. Godkendelsen omskriver autorisationsgrafen, så kontoen får en rigtig relation til organisationen, og det er den samme relation, som hvert eneste rettighedstjek i platformen allerede slår op i. Kontrollen ligger i afgørelsen, ikke i kodestien, og ingen funktion har været nødt til at lære om ejerskabskrav for at respektere den.</p>\n<p><strong>Resultat.</strong> En virksomhed kan overtage og rette sin egen post, uden at nogen redigerer databasen i hånden, og hver tildeling af råderet har en navngiven godkender og en begrundelse knyttet til sig. Den menneskelige gennemgang er bevidst flaskehalsen; et automatisk tjek på et domænenavn ville være hurtigere og ville tage fejl i præcis de tilfælde, der betyder mest.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Frontend‑udvikling", "Full stack‑udvikling", "Produkt & krav", "Sikkerhed", "Full stack‑produktudvikling", "Produktstrategi & kravspecifikation", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-a-loyalty-wallet-leagues-and-badges/", "url": "https://engineer.company/da/portfolio/built-a-loyalty-wallet-leagues-and-badges/", "title": "Har bygget loyalitetspoint, ligaer og badges", "summary": "Byggede loyalitets- og omdømmesystemet — 67 funktioner over et ledger på 31 tabeller, ligaer, badges og en shop — med rækkelåsning på saldoen.", "content_html": "<p><strong>Situation.</strong> Et professionelt netværk har et koldstartsproblem: platformen er først værd at bruge, når andre allerede bruger den, og indtil da er der ikke meget grund til at komme tilbage. Den sædvanlige løftestang er et belønningssystem — point for at bidrage, en status, der afspejler omdømme — som er nemt at beskrive og lumsk at bygge, for i det øjeblik point kan bruges, er de penge, og hver eneste fejl, man kan begå med penge, kan begås her.</p>\n<p><strong>Opgave.</strong> Bidrag skulle kunne måles og belønnes, med en saldo, der ikke kunne bruges to gange.</p>\n<p><strong>Handling.</strong> Loyalty‑skemaet løber op i 31 tabeller og 67 funktioner, med status i yderligere 5 tabeller og 32 funktioner, og discovery- og personalization‑skemaer ved siden af til at afgøre, hvad et givent medlem ser. Oven på ledgeret sidder ligaer, badges og en indløsningsshop, hvor en saldo bliver til noget virkeligt. Det, der krævede omhu, var den ældste fejl i bogen: tjekker man saldoen og bruger den bagefter, kommer to requests, der ankommer samtidig, begge igennem tjekket. Hver mutation tager en row lock på walleten, før den læser den, så den anden request venter på, at den første bliver færdig, i stedet for at kappes med den. At walleten ligger i den samme database som alt andet, er det, der overhovedet gør det muligt — saldoen og det, den købte, committer sammen eller slet ikke.</p>\n<p><strong>Resultat.</strong> Bidrag bliver målt og belønnet, og en saldo er et regnestykke og ikke en tilnærmelse. Row locking er det langsommere svar og blev valgt alligevel: kamp om den samme wallet bliver til ventetid, og alternativet er et medlem, der bruger de samme point to gange, og nogen, der afstemmer det i hånden bagefter.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Databaser", "Optimering af ydeevne", "PostgreSQL", "Produkt & krav", "SQL", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/built-the-social-layer-posts-feed-and-groups/", "url": "https://engineer.company/da/portfolio/built-the-social-layer-posts-feed-and-groups/", "title": "Har bygget det sociale lag: opslag, feed, grupper", "summary": "Byggede platformens sociale lag — opslag, feed, grupper, mentions, følgergraf og et digest over mærkedage — på samme function-first datalag.", "content_html": "<p><strong>Situation.</strong> Et katalog over virksomheder er et opslagsværk. Folk slår op i det og går igen. Det, der gør en professionel platform værd at vende tilbage til, er, at der er andre mennesker på den — hvilket betyder opslag, grupper og en grund til at komme tilbage, som ikke er en mail, der beder en om det.</p>\n<p><strong>Opgave.</strong> Der skulle lægges et socialt lag på, uden at det blev til et system nummer to med sine egne regler, sine egne rettigheder og sin egen måde at gemme ting på.</p>\n<p><strong>Handling.</strong> Opslag, et feed, grupper, mentions, en follower‑graf og et digest over mærkedage kom ind fordelt på 188 commits, alt sammen på det samme function‑first datalag som resten af platformen. Den begrænsning gjorde det meste af arbejdet: follower‑grafen er tabeller og funktioner som alt andet, en mention slås op gennem det samme identity‑skema, som kataloget bruger, og et opslag arver den moderationskø, der allerede var bygget til anmeldelser. Intet her havde brug for sit eget lager eller sin egen rettighedsmodel. Feedet er det eneste sted, der gjorde modstand, for at samle en personaliseret timeline effektivt er et helt andet problem end at hente en række, og det er der, personalization- og discovery‑arbejdet gør sig fortjent til sin plads.</p>\n<p><strong>Resultat.</strong> Platformen har en grund til at blive åbnet på en dag, hvor ingen har brug for at slå en virksomhed op, og den kom uden en parallel stak at vedligeholde. Om et socialt lag er den rigtige investering for et maritimt katalog, er et produktspørgsmål og ikke et teknisk spørgsmål — det er her, fordi produktet bad om det, og det er bygget på samme måde som alt omkring det.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Databaser", "Frontend‑udvikling", "Full stack‑udvikling", "Produkt & krav", "Webudvikling", "Backend- & API‑udvikling", "Full stack‑produktudvikling", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/built-the-hiring-and-seafarer-career-workspaces/", "url": "https://engineer.company/da/portfolio/built-the-hiring-and-seafarer-career-workspaces/", "title": "Har bygget job- og karriereværktøj til søfarende", "summary": "Byggede jobmarkedspladsen og søfarendes karriereområde — 151 stored functions fordelt på 48 tabeller — stillinger matchet mod dokumenterede kvalifikationer.", "content_html": "<p><strong>Situation.</strong> Det kommercielle argument for et maritimt professionelt netværk er rekruttering: virksomheder har brug for besætning og officerer, og søfarende har brug for hyre. Begge halvdele fandtes allerede på platformen i den forkerte form — virksomhederne lå i kataloget, fagfolkene havde profiler, og der var intet, der forbandt en ledig stilling med den person, der var kvalificeret til at udfylde den. Kvalifikationerne er den svære del, for i denne branche består de af certifikater, grader og dokumenteret sejltid og ikke af en jobtitel.</p>\n<p><strong>Opgave.</strong> Ledige stillinger, ansøgninger og verificerbare kvalifikationsbeviser for søfarende skulle modelleres ordentligt og ikke bare som fritekst på en profil.</p>\n<p><strong>Handling.</strong> To domæner blev bygget. Rekrutteringssiden løber op i 32 tabeller og 118 funktioner, der dækker ledige stillinger, ansøgninger, shortlisting og arbejdsgiverens overblik over rekrutteringsforløbet. Karriere‑workspacet har 16 tabeller og 33 funktioner, der rummer certifikater, gradprogression og sejltid, med dokumentupload og en gennemgangskø, så et kvalifikationsbevis bliver kontrolleret og ikke bare påstået. At modellere progressionen som en graf i stedet for en liste, er det, der gør matchningen brugbar — en grad kan nås fra en anden grad med bestemte certifikater og nok registreret tid til søs, og det er den struktur, der gør det muligt at matche en ledig stilling mod en karriere snarere end mod et nøgleord. Karriere‑workspacet ligger bag et feature flag og er ikke frigivet fuldt ud; rekrutteringssiden er live.</p>\n<p><strong>Resultat.</strong> En ledig stilling kan matches mod dokumenterede kvalifikationer i stedet for en selvangivet jobtitel, og det er hele forskellen mellem en jobportal og et rekrutteringsværktøj i denne branche. Verifikationen er bevidst flaskehalsen — en gennemgangskø skalerer ikke på samme måde som et automatisk tjek, og et automatisk tjek ville certificere folk, der ikke burde certificeres.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data governance", "Databaser", "Full stack‑udvikling", "PostgreSQL", "Produkt & krav", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Full stack‑produktudvikling", "Produktstrategi & kravspecifikation" ] }, { "id": "https://engineer.company/da/portfolio/servers-you-can-rebuild-from-scratch/", "url": "https://engineer.company/da/portfolio/servers-you-can-rebuild-from-scratch/", "title": "Servere, I kan bygge op fra bunden igen", "summary": "Miljøet kan reproduceres fra repositoryet, og de dele af det, der kun var sande, fordi nogen huskede dem, er nu assertions, der får en kørsel til at fejle.", "content_html": "<p><strong>Situation.</strong> Engineer ApS driver sit eget miljø — en webtilstedeværelse, en git‑forge, en database, sikkerhedskopier, mailtransport og DNS — og der var ingen at overdrage driften til. Et enmandsfirma har de samme fejltilstande som et stort, men ingen redundans, hvilket gør det sædvanlige svar — en person, der husker, hvordan værten blev sat op — til den mulighed, der er allersværest at få fat i.</p>\n<p><strong>Opgave.</strong> Hele miljøet skulle beskrives i et repository og ikke i nogens hoved, og i en form, der konvergerer en rigtig vært i stedet for blot at dokumentere den.</p>\n<p><strong>Handling.</strong> Det, der voksede ud af det, er 19 playbooks og 34 roller fordelt på 12.065 linjer YAML. Formen betyder mere end størrelsen. Sammensætningen er data og ikke flag: en vært ligger i en tier‑gruppe, hvis variabler erklærer, hvilke roller den kører, så provisionering uden argumenter konvergerer hver vært til dens erklærede tilstand. Idempotens er en kontrakt, ikke en ambition — en konvergeret vært rapporterer nul ændringer, og et play, der ikke kan sige det, er ikke færdigt. Fire kommandonavnerum holder løfterne adskilt: en kontrol, der ikke rører nogen vært, rapporter, der læser en vært og aldrig ændrer den, provisionering, der ændrer en vært, så den matcher repositoryet, og verifikation, der bevidst ændrer en vært og returnerer en dom.</p>\n<p><strong>Resultat.</strong> Miljøet kan reproduceres fra repositoryet, og de dele af det, der kun var sande, fordi nogen huskede dem, er nu assertions, der får en kørsel til at fejle. Prisen er reel: hver ændring tager længere tid end at redigere en fil direkte på serveren, og en konvergering, der kun bliver halvt gennemført, er værre end en, der nægter, og derfor blev der senere sat et preflight‑play foran den. Den afvejning blev truffet bevidst, og den har holdt.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Platformarkitektur", "Systemadministration", "Cloud‑infrastruktur & migrering", "DevOps & CI/CD‑automatisering", "Infrastructure as Code" ] }, { "id": "https://engineer.company/da/portfolio/a-whole-company-on-one-small-server/", "url": "https://engineer.company/da/portfolio/a-whole-company-on-one-small-server/", "title": "En hel virksomhed på en lille server", "summary": "Hele virksomheden kører på en maskine, der koster mindre om måneden end en frokost, og designet er blevet bedre af disciplinen og ikke bare billigere.", "content_html": "<p><strong>Situation.</strong> Virksomhedens produktionsvært er en cloud‑instans med én kerne, 512 MB hukommelse og 10 GB disk, hvoraf omkring 464 MB er anvendelige. Alt, hvad forretningen kører offentligt, ligger på den: webserveren, der terminerer TLS for syv domæner, git‑forgen, en Tor‑onion‑tjeneste, en Gemini‑server, en Gopher‑server, krypterede sikkerhedskopier og blokering af indtrængningsforsøg. Den sædvanlige reaktion på den liste er at købe en større maskine.</p>\n<p><strong>Opgave.</strong> Begrænsningen skulle behandles som et arkitektonisk input og ikke som et problem, man løser med penge, for det ærlige spørgsmål var ikke, om en større maskine ville virke, men om designet havde brug for en.</p>\n<p><strong>Handling.</strong> Hukommelsen blev det argument, der afgjorde beslutningerne. Der er ingen overvågningsagent, ingen metrikpipeline og intet dashboard — rapportering er et pull: syv kommandoer, der læser værten og gengiver Markdown, ændrer intet og kun kører, når man beder om det. Supervisoren er systemd og ikke en ekstra procesmanager lagt oven på den, og containerlaget er Quadlet‑enheder under den samme supervisor i stedet for en dæmon med sin egen. Webpanel‑platforme blev udelukket på designstadiet af samme grund. Da spørgsmålet kom op, om værten kunne bære en onion‑tjeneste, kom svaret fra en dags målinger og ikke fra en holdning: den tilgængelige hukommelse faldt aldrig under omkring 310 MB af 464, swap lå på 2,6 procent, og processoren var 99,7 procent inaktiv.</p>\n<p><strong>Resultat.</strong> Hele virksomheden kører på en maskine, der koster mindre om måneden end en frokost, og designet er blevet bedre af disciplinen og ikke bare billigere. Prisen er, at der ikke er råderum til noget skødesløst — mailtjenesten ligger bevidst slet ikke på denne maskine — den er skrevet som et installationsstillads, der venter på sin egen vært, fordi en mailserver kræver råderum, som denne maskine allerede har brugt, og det er skrevet ned i stedet for at blive opdaget senere.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Løsningsarkitektur", "Optimering af ydeevne", "Platformarkitektur", "Systemadministration", "Cloud‑infrastruktur & migrering", "Infrastructure as Code", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/verified-firewall-rules-by-position/", "url": "https://engineer.company/da/portfolio/verified-firewall-rules-by-position/", "title": "Har verificeret firewallregler efter position", "summary": "Firewallens sikkerhed tjekkes nu på den måde, en pakke oplever den. Det mest nyttige udfald var ikke tjekket, men det, det afslørede om den rapportering, der…", "content_html": "<p><strong>Situation.</strong> En firewallstatusoversigt viser et sæt regler. Et pakkefilter evaluerer en ordnet liste og standser ved det første match. Det er forskellige ting, og forskellen er usynlig i hvert eneste værktøj, der udskriver et sammendrag — og sådan kunne værten køre i et år med en hastighedsbegrænsning, som ingen pakke nogensinde nåede, fordi den lå under en bredere regel, der matchede først.</p>\n<p><strong>Opgave.</strong> Verifikationen af firewallen skulle læse placering og ikke medlemskab, for det var allerede påvist, at forekomst ikke beviser noget.</p>\n<p><strong>Handling.</strong> Tjekkene læser nu den nummererede regeloversigt og den aktive filterkæde og efterprøver rækkefølgen. Der findes præcis én regel pr. port og altid med en protokol, for en regel uden protokol udvider i det stille overfladen. SSH‑porten har en hastighedsbegrænsning og aldrig en tilladelse ved siden af, eftersom en tilladelse over en begrænsning er præcis den overskygning, der blev fundet. Den erklærede offentlige overflade er en kort liste på fem porte, efterprøvet som en helhed og ikke tjekket enkeltvis, så en port, der dukker op uden at være erklæret, får tjekket til at fejle i stedet for at afhænge af, at nogen opdager den. Sikkerhedsrapporten gengiver reglerne i matchrækkefølge med en advarsel øverst i afsnittet, der forklarer, hvordan den skal læses, for ellers vil den næste, der åbner filen, læse den som et sæt.</p>\n<p><strong>Resultat.</strong> Firewallens sikkerhed tjekkes nu på den måde, en pakke oplever den. Det mest nyttige udfald var ikke tjekket, men det, det afslørede om den rapportering, der gik forud: hvert eneste involveret værktøj havde udskrevet hastighedsbegrænsningen i et år, korrekt, og ingen af dem var blevet stillet det ene spørgsmål, der betød noget.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Infrastruktur", "Linux & servere", "Netværk & VPN", "Sikkerhed", "Test & QA", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/backups-that-have-actually-been-restored/", "url": "https://engineer.company/da/portfolio/backups-that-have-actually-been-restored/", "title": "Sikkerhedskopier, der rent faktisk er gendannet", "summary": "Virksomheden kan miste værten og få sine data tilbage, og den sætning hviler på en gendannelse, der kørte i sidste måned, ikke på en sikkerhedskopi, der kørte…", "content_html": "<p><strong>Situation.</strong> Alt, hvad virksomheden har — git‑repositorierne, databasen, sitet — lå på én cloud‑instans, og udbyderens snapshot af den var hele gendannelsesberedskabet. Et snapshot hos udbyderen er godt at have og dårligt at stole på: det ligger under samme konto som den maskine, det beskytter, ingen her har krypteret det, og ingen havde nogensinde gendannet fra et.</p>\n<p><strong>Opgave.</strong> Sikkerhedskopierne skulle krypteres, ligge uden for værten, udtyndes efter en opbevaringspolitik og — den del, der som regel springes over — faktisk bruges til gendannelse efter en fast plan, uden at nogen skulle huske at gøre det.</p>\n<p><strong>Handling.</strong> Sikkerhedskopieringen kører på en systemd‑timer: et databasedump, der hvor der er en database, derefter et krypteret, deduplikeret snapshot over SFTP til lager hos en anden udbyder, så en udtynding efter opbevaringspolitikken og til sidst et integritetstjek. En dead man’s switch pinges kun ved succes, og netop den skelnen gør den til en alarm og ikke en log — en kørsel, der fejler, siger ingenting, og det er tavsheden, der udløser alarmen. Derudover kører der hver måned en gendannelsesøvelse: den henter en kendt fil ud af sikkerhedskopilageret og sammenligner den, så det, der kontrolleres, er en gendannelse og ikke en sikkerhedskopi. Adgangssætningen skrives til en fil, som systemd‑enhederne læser, i stedet for at blive sendt gennem miljøvariabler, fordi supervisoren fortolker escape‑tegn i miljøvariablernes værdier, og en adgangssætning med en omvendt skråstreg ville i al stilhed være blevet en anden end den, der oprettede lageret. Bagefter blev gendannelsesberedskabet gennemgået og dokumenteret, og de huller, der var tilbage, blev nævnt eksplicit i dokumentet i stedet for at vente på at blive opdaget under en hændelse.</p>\n<p><strong>Resultat.</strong> Virksomheden kan miste værten og få sine data tilbage, og den sætning hviler på en gendannelse, der kørte i sidste måned, ikke på en sikkerhedskopi, der kørte i nat. Gennemgangens mest værdifulde resultat var listen over det, der stadig ikke er dækket, og det er netop den del, som en grøn sikkerhedskopirapport i kraft af sin opbygning ikke kan fortælle nogen.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Dokumentation", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Sikkerhed", "Backup & disaster recovery", "Infrastructure as Code", "Site reliability & overvågning", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/built-dead-mans-switch-host-monitoring/", "url": "https://engineer.company/da/portfolio/built-dead-mans-switch-host-monitoring/", "title": "Har bygget overvågning med dead man's switch", "summary": "Værten har nu en alarm, hvis fejltilstand er at slå ud, og den navngivning, der ville have ødelagt den, er rettet.", "content_html": "<p><strong>Situation.</strong> En vært med 464 MB, der kører en forge, en database og en webserver, kan realistisk dø på to måder: den løber tør for hukommelse, eller den løber tør for diskplads. Ingen af delene varsler sig selv. Begge kan forudses helt præcist nogle timer i forvejen, hvis noget holder øje, og det gjorde intet.</p>\n<p><strong>Opgave.</strong> Værten havde brug for en alarm, der virker, når værten ikke gør — og det udelukker alt, der skal sende en besked i selve fejløjeblikket.</p>\n<p><strong>Handling.</strong> Svaret er en dead man’s switch på en timer. Hvert kvarter, med jitter, måler et lille script den tilgængelige hukommelse og den ledige diskplads og pinger kun en ekstern tjeneste, hvis begge ligger over deres tærskelværdier. Tavsheden er alarmen. En maskine, der er løbet tør for hukommelse, har mistet netværket eller ikke længere kan starte, giver præcis det samme signal som en usund maskine, og det er den korrekte opførsel og grunden til, at denne løsning blev valgt frem for en agent, der rapporterer en status. Der måles tilgængelig hukommelse, ikke fri hukommelse, og netop den skelnen blev den mest lærerige del af arbejdet: scriptet havde hele tiden læst den rigtige kolonne, mens seks variabelnavne omkring det sagde “free”. På en sund Linux‑maskine er den frie hukommelse tæt på nul, fordi kernen bruger ledig hukommelse til cache, så en læser, der holdt de navne op mod en tærskel på ti procent, ville se tolv megabyte fri på en maskine med 464 MB, konkludere, at tjekket var i stykker, og rette det med en ændring på ét tegn, der gør en fungerende alarm til en, der står permanent i alarm og bliver slået fra inden for en uge.</p>\n<p><strong>Resultat.</strong> Værten har nu en alarm, hvis fejltilstand er at slå ud, og den navngivning, der ville have ødelagt den, er rettet. Switchens reelle begrænsning står i dens egen dokumentation: den beviser, at maskinen er sund, ikke at sitet svarer, og det er to forskellige spørgsmål.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Optimering af ydeevne", "Overvågning & observerbarhed", "Infrastructure as Code", "Site reliability & overvågning", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/made-ansible-check-mode-tell-the-truth/", "url": "https://engineer.company/da/portfolio/made-ansible-check-mode-tell-the-truth/", "title": "Har fået check‑tilstanden til at vise sandheden", "summary": "En prøvekørsel måler nu enten noget eller siger, at den ikke gjorde det, og ingen af delene er den tredje mulighed, den tidligere havde.", "content_html": "<p><strong>Situation.</strong> En prøvekørsel mod produktion er tænkt som den sikre måde at finde ud af, hvad en ændring vil gøre. I september 2026 fejlede en prøvekørsel på en assertion, der ganske enkelt havde et forkert billede af værten, og rådede operatøren til at sætte et flag, der ville have slækket på en sikkerhedssandkasse. Prøvekørslen havde ikke læst værten. Den havde læst en værdi, som værten aldrig havde afgivet, og draget en konklusion ud fra den.</p>\n<p><strong>Opgave.</strong> Hver probe, hvis resultat indgår i en beslutning, skulle gøres ærlig i check‑tilstand, og de prober, der ikke kunne gøres ærlige, skulle sige det højt i stedet for at tie stille.</p>\n<p><strong>Handling.</strong> Årsagen er en egenskab ved værktøjet, som er dokumenteret og let at glemme: kommandomodulet kører ikke i check‑tilstand, og den værdi, det registrerer, er ikke et tomt resultat — det er en succes med tomt output. Enhver betingelse, der læser den værdi, træffer derfor sin beslutning ud fra noget, der aldrig blev målt, og den beslutter i den retning, dens egen logik tilfældigvis peger. En gennemgang af alle registrerede prober fandt seks, der hver løj på sin egen måde: én rapporterede, at supervisoren accepterer alle unit‑direktiver, uden at have spurgt supervisoren, en anden rapporterede, at der intet var at gøre, på en vært med firewallen slået fra. Hver af dem fik én af to former. En probe, der læser allerede eksisterende tilstand, som kørslen ikke har rørt, markeres, så den også kører i check‑tilstand. En probe, der ikke kan køre, springes over, og en besked angiver specifikt, hvad der ikke blev verificeret — for tavshed i en kørselslog ser præcis ud som en bestået kørsel.</p>\n<p><strong>Resultat.</strong> En prøvekørsel måler nu enten noget eller siger, at den ikke gjorde det, og ingen af delene er den tredje mulighed, den tidligere havde. Den generelle regel kom ind i udviklingsvejledningen i samme ændring: check‑tilstand må ikke lyve, og en probe, der ikke kan se, har pligt til at melde sin blindhed i stedet for at udlede en dom af den.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Driftssikkerhed & backup", "Infrastruktur", "Linux & servere", "Test & QA", "DevOps & CI/CD‑automatisering", "Infrastructure as Code", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/reconciled-a-20-record-dns-zone-as-code/", "url": "https://engineer.company/da/portfolio/reconciled-a-20-record-dns-zone-as-code/", "title": "Har afstemt en DNS‑zone med 20 poster deklarativt", "summary": "Zonen er versioneret, kan sammenlignes og kan eksporteres, og den ene beslutning, der gik imod det oplagte standardvalg, er skrevet ned med sin begrundelse, så…", "content_html": "<p><strong>Situation.</strong> Virksomhedens DNS havde omkring tyve poster fordelt på et website, et git‑underdomæne, to personlige omdirigeringer, mailrouting gennem en hostet postkasseudbyder, et afsenderdomæne til transaktionsmails og autentificeringsposterne for begge. Det hele lå i en udbyders webkonsol, så den aktuelle tilstand var det, den seneste person, der klikkede, havde efterladt.</p>\n<p><strong>Opgave.</strong> Zonen skulle gøres til en erklæring i repositoryet, med en måde at sammenligne den erklæring med det, udbyderen reelt svarer med.</p>\n<p><strong>Handling.</strong> Zonen er skrevet som data — aktive poster, applikationsposter, transportpolitikposter og en særskilt liste over poster, der afventer fjernelse, hvilket er den ærlige måde at registrere en sletning, der endnu ikke er sket. Ét play afstemmer den mod udbyderens API. To yderligere indgange ligger bag tags, der eksplicit skal vælges til: en gennemgang, der rapporterer forskellen uden at ændre noget, og en eksport, der skriver zonen ud i det standardiserede zonefilformat, så den kan læses af noget, der ikke er dette repository. Den mest interessante beslutning var en omvending. En løsning, der sendte de to websites gennem udbyderens indholdsnetværk, blev bygget, virkede og blev derefter slået fra igen — for den koster netop den sætning, som virksomhedens privatlivsside handler om. Med en proxy foran håndterer en anden virksomhed hver besøgendes adresse og hver URL, den besøgende beder om, før vi gør, og under deres politik i stedet for vores. For en forretning, der skiller sig ud ved at love, at ingen kigger med, er det en dårligere byttehandel end de angreb, den beskytter imod.</p>\n<p><strong>Resultat.</strong> Zonen er versioneret, kan sammenlignes og kan eksporteres, og den ene beslutning, der gik imod det oplagte standardvalg, er skrevet ned med sin begrundelse, så ingen tager sagen op igen ved et uheld. Gennemgangen er den del, der bruges mest, for man vil oftere kende forskellen end lukke den.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Automatisering & CI/CD", "Infrastruktur", "Netværk & VPN", "Python", "Sikkerhed", "Infrastructure as Code", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/cut-systemd-sandbox-exposure-on-every-unit/", "url": "https://engineer.company/da/portfolio/cut-systemd-sandbox-exposure-on-every-unit/", "title": "Har strammet systemd‑sandboxen på hver unit", "summary": "Hver enhed kører med de privilegier, den har brug for, og ikke dem, den ikke har brug for; tallene er målt, ikke påstået, og den fejltype, hvor en tastefejl…", "content_html": "<p><strong>Situation.</strong> Tretten unit‑filer blev installeret af dette repository og kører på produktionsværten med de privilegier, supervisoren giver som standard, og det er de fleste. En hærdningsscore gav dead man’s switch‑tjenesten 9,6 og kaldte den usikker; den internetvendte git‑forge fik 8,3 og betegnelsen eksponeret. Begge tal var korrekte, og ingen af dem havde ført til noget, for en score uden en tilknyttet tærskel er et tal, folk lærer at springe over.</p>\n<p><strong>Opgave.</strong> Hver enhed skulle have en sandkasse, der står i forhold til det, den reelt gør, og sandkasserne skulle have et tjek, for en forkert sandkasse fejler på den måde, at enheden starter og derefter går i stykker et sted, parseren ikke kan se.</p>\n<p><strong>Handling.</strong> Privilegiebegrænsninger kom ind i alle unit‑skabeloner — et privilegieloft låst fast på enhedens egne privilegier, et systemkaldsfilter, begrænsede adressefamilier og hukommelse, der er skrivbar eller eksekverbar, men aldrig begge dele. De målte resultater: dead man’s switch gik fra 9,6 til 1,5, forgen fra 8,3 til 1,5 og de to sikkerhedskopienheder fra 9,6 til 2,3 og 2,5. Arbejdet med dem afdækkede noget, der var mere værd end scorerne. Ét direktiv havde været stavet forkert i tre unit‑skabeloner, lige så længe de roller havde eksisteret — et navn, der ser plausibelt ud, men ikke findes, og som supervisoren reagerer på ved at logge, at den ikke genkender nøglen, og starte enheden alligevel. En søgning efter et direktiv beviser, at det blev skrevet; kun parseren beviser, at det fik virkning. Derfor kører en verifikationsrolle nu hver enhed gennem supervisorens egen verifikator under kørslen, og den inkluderes af alle roller, der installerer en enhed. Det, den stadig ikke kan se, står klart i de samme dokumenter: alle direktiver, der kan ødelægge disse enheder, ødelægger dem under kørsel, ikke ved parsning, og en score kan ikke afgøre, om et script stadig når sin sidste linje.</p>\n<p><strong>Resultat.</strong> Hver enhed kører med de privilegier, den har brug for, og ikke dem, den ikke har brug for; tallene er målt, ikke påstået, og den fejltype, hvor en tastefejl bliver til en kontrol, der i al stilhed mangler, får nu en kørsel til at fejle. Målingens grænser er noteret ved siden af den, og det er den del, der forhindrer den næste læser i at stole for meget på et grønt scoreinterval.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Containere (Docker/Kubernetes)", "Infrastruktur", "Linux & servere", "Sikkerhed", "Test & QA", "Containerisering & orkestrering", "Infrastructure as Code", "Sikkerhed & adgangsstyring", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/built-7-read-only-host-reporting-roles/", "url": "https://engineer.company/da/portfolio/built-7-read-only-host-reporting-roles/", "title": "Har bygget 7 skrivebeskyttede rapporteringsroller", "summary": "Hele infrastrukturen kan beskrives ud fra repositoryet efter behov, og rapporterne har fundet reelle ting: de to døde sikkerhedskontroller, en ikke-erklæret…", "content_html": "<p><strong>Situation.</strong> Værten havde intet dashboard og skulle heller ikke have et, for en metrikstak kan ikke være på 464 MB og ville ikke have været sin pris værd, selv hvis den kunne. Det efterlod et ærligt hul: der var ingen måde at besvare spørgsmål om, hvem der har adgang, hvor meget dette repository er vokset, hvordan trafikken ser ud, eller om hærdningen reelt håndhæves.</p>\n<p><strong>Opgave.</strong> Spørgsmålene skulle have svar, der kunne fremstilles efter behov, ikke kostede værten noget, så længe ingen spurgte, og aldrig kunne ændre det, de beskrev.</p>\n<p><strong>Handling.</strong> Syv skrivebeskyttede roller, der hver gengiver en kørende vært som Markdown i repositoryet. Et øjebliksbillede af værtens facts, der dækker styresystem, hardware, lagring, netværk, tjenester, pakker, lyttende sockets og processorudnyttelse beregnet ud fra to målinger af kernens egen tæller. En adgangsrapport, der dækker konti, sudo‑omfang, nøglefingeraftryk, forge‑brugere og databaseroller. En git‑rapport pr. repository. En metrikrapport over dagens indsamlede målinger. En trafikrapport bygget på webserverens privatlivsmaskerede log. En udbyderoversigt over fire udbydergrænseflader — DNS, to cloud‑API’er og et API til dedikerede servere. Og en sikkerhedsrapport, der besvarer, om hærdningen er håndhævet og ikke blot konfigureret, og som udskriver firewallreglerne i den rækkefølge, de matches i, og det aktive regelsæt for blokeringer. Den styrende regel for alle syv er, at der ikke udskrives noget tal, som kørslen ikke har målt — en rapport, der fylder et hul med et plausibelt tal, er værre end en, der lader det stå tomt, for det er kun det plausible tal, man tror på.</p>\n<p><strong>Resultat.</strong> Hele infrastrukturen kan beskrives ud fra repositoryet efter behov, og rapporterne har fundet reelle ting: de to døde sikkerhedskontroller, en ikke‑erklæret lyttende port og den iagttagelse, at trafikarkivet kun er så gammelt som den seneste gang, nogen huskede at indsamle det. Den sidste er beskrevet som en begrænsning, og prisen er noteret konkret — seksten dages historik for ét site, som blev roteret væk mellem to indsamlinger og ikke findes nogen steder.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Dokumentation", "Infrastruktur", "Interessenter & rapportering", "Linux & servere", "Overvågning & observerbarhed", "Site reliability & overvågning", "Systemadministration", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/your-code-on-your-own-hardware/", "url": "https://engineer.company/da/portfolio/your-code-on-your-own-hardware/", "title": "Jeres kode på jeres eget udstyr", "summary": "Virksomheden hoster sin egen kode på sin egen hardware, og siden foran den arver alle de tjek, hovedsitet består, i stedet for at glide væk fra det på måder,…", "content_html": "<p><strong>Situation.</strong> Virksomhedens kildekode lå hos en tredjeparts hostingtjeneste, og det er et rimeligt sted for den, men det passer dårligt til en forretning, hvis argument over for kunderne er, at den ikke overlader deres data til mellemled. At køre en forge selv betyder at køre en forge: autentificering, adgangskontrol, lagring, sikkerhedskopier og en offentlig forside til den.</p>\n<p><strong>Opgave.</strong> Der skulle sættes en kanonisk git‑server op på den eksisterende vært med den mindst mulige angrebsflade og uden administrationspanel på nettet, og foran den en landingsside, der ikke forfalder.</p>\n<p><strong>Handling.</strong> Forgen er en enkelt binærfil, der overvåges af styresystemet og er installeret fra leverandørens pakkerepository, og den er bevidst valgt, fordi den er SSH‑først og ikke har nogen administrativ webgrænseflade — jo mindre grænseflade, jo mindre at forsvare. Den er privat som standard: anonym adgang afvises, adgang uden nøgle afvises, og hvert erklæret repository er markeret som privat i stedet for at stole på, at ingen finder det. Dens SSH‑lytter binder kun til loopback‑grænsefladen på port 23231 og nås udefra gennem en jump host, så firewallens erklærede angrebsflade ikke vokser. En protokolmultiplexer, der kan lægge HTTPS og git over SSH på én offentlig port ved at inspicere de første bytes af en forbindelse, er skrevet og klar bag en hovedafbryder, og den afbryder er slået fra: der er endnu ikke brug for git med flere brugere over den offentlige port, og en lytter, som ingen bruger, er angrebsflade. Landingssiden foran den var det mere interessante problem. Den havde været en håndvedligeholdt kopi af hovedsitets design i sit eget repository, og hver eneste forskel, der nogensinde var fundet mellem de to, viste sig at være et uheld og ikke en beslutning: en typografisk skala, der gengav ordmærket omkring ni procent for stort, en fonterklæring, der slog to vægte sammen til én på enhver maskine, hvor familien var installeret, ornamenter, der var skjult under en bestemt bredde og derfor manglede på alle telefoner, en vægt, der blev brugt, uden at der fulgte en font med til den, og hverken et main‑landmærke eller en overskrift på øverste niveau på nogen side. Fem ud af fem, og ikke én af dem synlig på et skærmbillede. Kopien blev slettet; landingssiden bygges nu af hovedsitets egne skabeloner og leveres som et artefakt.</p>\n<p><strong>Resultat.</strong> Virksomheden hoster sin egen kode på sin egen hardware, og siden foran den arver alle de tjek, hovedsitet består, i stedet for at glide væk fra det på måder, som kun en måling kan se. Det er stadig muligt at afvige, men det skal nu skrives ned.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Infrastruktur", "Linux & servere", "Systemadministration", "Webudvikling", "DevOps & CI/CD‑automatisering", "Infrastructure as Code", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/ran-containers-on-podman-and-quadlet/", "url": "https://engineer.company/da/portfolio/ran-containers-on-podman-and-quadlet/", "title": "Har kørt containere på Podman og Quadlet", "summary": "Containere kører under den supervisor, der allerede var betroet, bag den firewall, der allerede var erklæret, og en rutinemæssig release kræver ingen…", "content_html": "<p><strong>Situation.</strong> Platformen havde brug for et sted at køre applikationscontainere. Det oplagte valg var branchestandarden, og det oplagte valg var forkert for denne vært på en helt bestemt måde: den omskriver kernens pakkefilterregler og placerer containerporte over de firewallregler, hærdningsrollen installerer, så en container i al stilhed bliver tilgængelig fra internettet, uanset hvad firewallen har fået at vide.</p>\n<p><strong>Opgave.</strong> Der skulle vælges et containerlag, som ikke går uden om firewallen, ikke tilføjer endnu en supervisor ved siden af den, der allerede var betroet, og ikke kræver, at en person er root for at sende en release ud.</p>\n<p><strong>Handling.</strong> Laget er en dæmonløs containermotor, der kører enheder, som genereres af styresystemets egen supervisor. Der er ingen ekstra proceshåndtering: containere er tjenester, de starter, som tjenester starter, og de beskrives i den samme deklarative form som alt andet. Containerne bruger værtens netværk uden nogen publicerede porte overhovedet, og det fjerner spørgsmålet om at omgå firewallen i stedet for blot at afbøde det. Enhederne kører på systemniveau og ikke rootless, og det er noteret som en afvejning — det holder automatiseringen enkel og normal, på bekostning af den strengere isolation, rootless ville give, og noten siger, hvilken vej man skal gå, hvis udbrud fra containere nogensinde kommer til at betyde mere end enkel automatisering. Udrulning blev adskilt fra provisionering: root sætter udrulningsstien op én gang, og derefter udruller en bruger uden privilegier igen ved at køre ét fast script gennem en afgrænset regel, der tillader netop den kommando og ingen argumenter. Byg imaget, genstart tjenesten. Ingen root‑shell, ingen vilkårlige kommandoer.</p>\n<p><strong>Resultat.</strong> Containere kører under den supervisor, der allerede var betroet, bag den firewall, der allerede var erklæret, og en rutinemæssig release kræver ingen privilegeret adgang. Valget imod standarden er skrevet ned med sin begrundelse, og det betyder mere end selve valget — den næste person vil få at vide, at man skal bruge standarden, af hver eneste artikel, vedkommende læser.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Containere (Docker/Kubernetes)", "DevOps", "Infrastruktur", "Linux & servere", "Sikkerhed", "Cloud‑infrastruktur & migrering", "Containerisering & orkestrering", "DevOps & CI/CD‑automatisering", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/provisioned-a-second-server-from-code/", "url": "https://engineer.company/da/portfolio/provisioned-a-second-server-from-code/", "title": "Har provisioneret en ekstra server ud fra kode", "summary": "En ny vært kan oprettes eller overtages igen fra repositoryet, bag en firewall, der allerede findes, og til en pris, som filen oplyser.", "content_html": "<p><strong>Situation.</strong> Der var brug for endnu en maskine til et applikationslag, hos en anden udbyder end den første, og den første maskines egen historie var argumentet for, hvordan det skulle gøres: den var oprettet manuelt, og dens firewall var tilføjet bagefter, og det efterlader et tidsvindue, hvor en helt ny vært med en standardpolitik for adgangskoder kan nås fra internettet.</p>\n<p><strong>Opgave.</strong> Maskinen skulle oprettes fra repositoryet, og den skulle fødes bag sin firewall i stedet for at få én kort efter.</p>\n<p><strong>Handling.</strong> Designet hvilede på rækkefølgen. Firewall‑playet kører før server‑playet, så regelsættet findes, før der er noget, det skal beskytte; hos den anden udbyder kører tagging før cloud‑firewallen, fordi en firewall, der retter sig mod tags, kræver, at taggene findes først. Begge udbyderes firewalls skrives direkte mod deres REST‑API’er gennem en generisk HTTP‑opgave i stedet for gennem en collection fra leverandøren, og det fjerner en afhængighed og de versionsafvigelser, den fører med sig, på bekostning af, at requests skal skrives i hånden. Den rolle, der opretter maskinen, overtager en eksisterende maskine i stedet for at oprette en dublet, hvis den allerede findes, så det er sikkert at køre den igen. Den er også dokumenteret, i sin egen fil, som den eneste rolle i repositoryet, der bruger penge — instanstype, region, specifikation og den månedlige pris i euro står alle skrevet, for et play, der sender en regning til nogen, bør sige det der, hvor vedkommende vil læse det. Det play er også det, der ikke kan afprøves på den sædvanlige måde: den generiske HTTP‑opgave erklærer ingen understøttelse af check‑tilstand, så en prøvekørsel springer alle opgaver i det over. Afprøvningen sker i stedet mod firewall‑playet, som er gratis og kan rulles tilbage.</p>\n<p><strong>Resultat.</strong> En ny vært kan oprettes eller overtages igen fra repositoryet, bag en firewall, der allerede findes, og til en pris, som filen oplyser. Det eneste, en prøvekørsel ikke kan dække, er nævnt i den samme fil i stedet for at blive efterladt som en overraskelse.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Automatisering & CI/CD", "Cloud", "Infrastruktur", "Python", "Sikkerhed", "Cloud‑infrastruktur & migrering", "Infrastructure as Code", "Netværk & VPN‑opsætning", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-a-multilingual-hugo-site-in-3-languages/", "url": "https://engineer.company/da/portfolio/built-a-multilingual-hugo-site-in-3-languages/", "title": "Har bygget et Hugo‑site på 3 sprog", "summary": "Sitet udgiver tre sprog og fire repræsentationer fra ét træ, har ingen backend at angribe og intet at fakturere, og det ene stykke JavaScript i det er bundet…", "content_html": "<p><strong>Situation.</strong> Virksomheden havde brug for et offentligt site, der virker på tre sprog, viser, hvad virksomheden kan, i stedet for at påstå det, intet koster at drive og ikke overlader sine besøgende til nogen. De fleste af dem er almindelige krav. Tilsammen udelukker de næsten alle indholdsstyringssystemer, for en backend, der kører i drift, er noget, der skal sikres, opdateres, betales for og forklares på en privatlivsside.</p>\n<p><strong>Opgave.</strong> Sitet skulle genereres udelukkende på byggetidspunktet og alligevel opføre sig som et moderne site — søgbart, installerbart, syndikeret, til at printe og læsbart for en skærmlæser på alle de sprog, det udgives på.</p>\n<p><strong>Handling.</strong> Det er et statisk site: 108 skabelonfiler, hvoraf 53 er partials og 18 er shortcodes, tre sprog, ingen backend i drift og ét førstepartsscript, der er tilladt som en afgrænset undtagelse til et porteføljefilter, hvis kontroller alle er skjult, indtil det kører. Funktionalitet tilføjes på byggetidspunktet og ikke i browseren, og det er en produktbeslutning, der også er skrevet ned som sådan. Hver side udgives i fire repræsentationer fra ét indholdstræ — HTML, en Markdown‑tvilling, et Gemini‑dokument og en Gopher‑menupost — og forsiden lægger desuden tre syndikeringsfeeds og et manifest til en installerbar applikation oveni. Taksonomierne er bevidst to og ikke én, og skelnen bærer meget: en kategori er et emnemærke på arbejdet, en ydelse er noget, virksomheden sælger, og at slå dem sammen ville have gjort kataloget til en liste over færdigheder i stedet for en liste over tilbud. Det samme træ bygges derefter syv gange til, én gang pr. fokuseret subdomæne, ved at pege generatoren på en anden indholdsmappe i stedet for at forgrene noget som helst.</p>\n<p><strong>Resultat.</strong> Sitet udgiver tre sprog og fire repræsentationer fra ét træ, har ingen backend at angribe og intet at fakturere, og det ene stykke JavaScript i det er bundet af en kontrakt, som en linter håndhæver. Prisen er, at alt interaktivt skal løses på byggetidspunktet eller slet ikke, og det har udelukket flere ting, der ville have været lette med en server, og er grunden til, at søgeindekset blev prissat og lagt på hylden i stedet for at blive sendt ud.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Frontend‑udvikling", "Full stack‑udvikling", "Internationalisering", "Optimering af ydeevne", "Webudvikling", "Full stack‑produktudvikling", "Internationalisering & lokalisering", "Parathed til AI‑agenter", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/a-safety-net-that-no-longer-costs-half-an-hour/", "url": "https://engineer.company/da/portfolio/a-safety-net-that-no-longer-costs-half-an-hour/", "title": "Et sikkerhedsnet, der ikke længere koster en halv time", "summary": "Gaten gik fra 1.636 sekunder til 615, mens tjekkene blev bredere og ikke tyndere — den serielle omkostning steg, og den samlede tid faldt.", "content_html": "<p><strong>Situation.</strong> Elleve af sitets tjek styrer en headless browser: layout ved alle de vinduesformer, designet tegner, typeskala, udvidelse af oversat tekst, kontrast, tilgængelighedsregler, tvungne farver, maskotstørrelse, bevægelse, print i seks papirkombinationer, konsolfejl og visuel regression. Kørt ét ad gangen tog de 1.636 sekunder, lidt over syvogtyve minutter. En gate, der tager syvogtyve minutter, er en gate, der bliver sprunget over, og et tjek, der springes over, kan ikke skelnes fra et, der består.</p>\n<p><strong>Opgave.</strong> Den samlede køretid skulle så langt ned, at det at køre dem blev det normale og ikke en beslutning, uden at nogen af dem blev svækket.</p>\n<p><strong>Handling.</strong> Arbejdet begyndte med måling. Hvert tjek blev tidsmålt enkeltvis på en maskine med tolv kerner: layout på 405 sekunder, typografi på 301, oversættelse på 282, kontrast på 189, tilgængelighed på 178 og så videre ned til sytten. To fund formede svaret. At køre dem alle på én gang var langsommere end at køre fire ad gangen — 265 sekunder mod 224 — for hvert tjek er selv en browser, der arbejder parallelt, og det koster mere at overbelaste maskinen, end samtidigheden vinder. Og at sortere efter længste behandlingstid først slog alfabetisk rækkefølge med næsten en tredjedel, 224 sekunder mod 320, og det er det klassiske resultat fra planlægningsteorien, som viser sig her, fordi tjekkenes omkostning varierer med en faktor tyve. Derfor er afvikleren en begrænset pulje af workers, dimensioneret ud fra antallet af kerner med mindst to og højst fire, og den fodres med det længste først. Samtidig blev tjekkene udvidet i stedet for indsnævret: de deler nu én viewport‑tabel med toogtyve vinduesformer, udledt af alle de media queries, stylesheetet reelt indeholder, og det alene tog layouttjekket fra 95 sekunder til 405.</p>\n<p><strong>Resultat.</strong> Gaten gik fra 1.636 sekunder til 615, mens tjekkene blev bredere og ikke tyndere — den serielle omkostning steg, og den samlede tid faldt. Målingen er den del, der er værd at beholde: to valg, der lød fornuftige, at køre alt på én gang og at køre tingene i den rækkefølge, de blev skrevet i, var hver især målbart dårligere end alternativet.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Frontend‑udvikling", "Optimering af ydeevne", "Python", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/replaced-27-font-sizes-with-a-6-step-scale/", "url": "https://engineer.company/da/portfolio/replaced-27-font-sizes-with-a-6-step-scale/", "title": "Har erstattet 27 skriftstørrelser med 6 trin", "summary": "Seks størrelser, hvor der var syvogtyve, med et angivet forhold, en angivet linjelængde og et tjek, der afviser den næste uplanlagte værdi.", "content_html": "<p><strong>Situation.</strong> En måling af sitets renderede tekst fandt syvogtyve forskellige skriftstørrelser i brug. Flere lå mindre end én procent fra hinanden — fem værdier mellem 0,8 og 0,85 af brødteksten var i brug samtidig, og det er en forskel, som ingen læser kan opfatte, men som enhver kommende redaktør vil føje nye værdier til. To af overskrifterne fik slet ingen størrelse fra stylesheetet og faldt tilbage på browserens egne standardværdier, et forhold på 1,33, der ikke matchede noget andet på siden.</p>\n<p><strong>Opgave.</strong> Størrelserne skulle gøres til en skala med et angivet forhold, og noget skulle forhindre nummer otteogtyve.</p>\n<p><strong>Handling.</strong> Skalaen er en stor terts med forholdet 1,25 og seks navngivne trin fra det med småt til display, hvert et token og ikke en værdi. Overskrifterne er eksplicit knyttet til trin i stedet for at arve det, browseren nu mener, og det var det, der rettede de to, som ikke havde nogen størrelse af egen. Kun det nederste trin har en nedre grænse, så det med småt forbliver læseligt på en telefon, uden at hele skalaen er låst fast. Logotyper er undtaget ved navn og ikke ved et tilfælde. Linteren er den del, der får det til at holde: den renderer sitet og fejler ved størrelse nummer otteogtyve. Den har allerede vist sit værd to gange — den fangede en overskrift, der dukkede op med 1,17 gange brødteksten, hvilket ikke er et trin i nogen skala, og angav forholdet i fejlbeskeden; og den fangede inline‑kode på 0,9, som er præcis den slags værdi, skalaen findes for at forhindre. Sammen med skalaen kom en linjelængde på omkring tooghalvfjerds tegn, der erstattede en nedarvet fast bredde, som gav enoghalvfems tegn på en anmeldelsesside og hundrede og ti på kontaktsiden.</p>\n<p><strong>Resultat.</strong> Seks størrelser, hvor der var syvogtyve, med et angivet forhold, en angivet linjelængde og et tjek, der afviser den næste uplanlagte værdi. Begrænsningen er reel og indimellem ubekvem: et design, der vil have en størrelse mellem to trin, må flytte sig til et trin eller argumentere for at ændre skalaen, og den diskussion er blevet taget og tabt mere end én gang.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Dokumentation", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "Brand, marketing & SEO", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/the-accessibility-faults-your-checker-misses/", "url": "https://engineer.company/da/portfolio/the-accessibility-faults-your-checker-misses/", "title": "De tilgængelighedsfejl, jeres tjekværktøj overser", "summary": "Elleve defekter rettet og, mere nyttigt, fire regler, der overlever dem: en kontrol, der tester én akse, forsvarer én akse; et regelsæt, der ikke indeholder…", "content_html": "<p><strong>Situation.</strong> Sitet bestod sine automatiske tilgængelighedsregler. Det havde også elleve defekter, som de regler ikke kunne se, fordi hver af dem var en egenskab ved, hvordan siden blev gengivet, og ikke ved, hvad markuppen sagde — den slags, som en validator melder i orden, og som en tastaturbruger rammer inden for få sekunder.</p>\n<p><strong>Opgave.</strong> Defekterne skulle findes, rettes og skrives op i en fortegnelse med den regel, hver af dem gav anledning til, så det er typen af defekt, der lukkes, og ikke det enkelte tilfælde.</p>\n<p><strong>Handling.</strong> De faldt i tre grupper. Størrelse: et nøgleord i stylesheetet, brugt som var det relativt, låste et helt område fast på seksten pixels, mens brødteksten voksede op til toogtyve; et subscript‑element blev brugt til at betyde “mindre”; mindre tekst gjorde en linje længere, indtil en titel nåede seksoghalvfems tegn; og et målt højdeloft blev sprængt af netop den tekstafstand, sitet hævder at understøtte. Fokus og beskæring: otte footer‑links blev liggende i tab‑rækkefølgen bag en egenskab, der fjerner pointer‑interaktion og intet andet; en overflow‑regel skabte en scroll‑container, ingen ville have; og en scroll‑drevet animation, som simpelthen er inaktiv på en side, der er for kort til at scrolle, klippede permanent footeren af på tre sider. Og to defekter i selve kontrollerne, som er dem, der er værd at nævne — hver vinduesform, layouttjekket åbnede, var ni hundrede pixels høj, så en telefon i liggende format på 852 gange 393 gav syv pixels mellem to elementer, og intet tjek havde nogensinde kigget; og regelmotoren havde kørt halvfjerds af sine hundrede og fem regler, så den regel, der ville have fanget det manglende landmærke, var ikke med i sættet.</p>\n<p><strong>Resultat.</strong> Elleve defekter rettet og, mere nyttigt, fire regler, der overlever dem: en kontrol, der tester én akse, forsvarer én akse; et regelsæt, der ikke indeholder reglen, kan ikke håndhæve den; højde er en dimension, så test den; og bevis synlighed i pixels og ikke i dokumentet. Da layouttjekket blev genåbnet ordentligt, fejlede det hundrede og otteogtyve gange på tre sprog, herunder i et vindue på størrelse med en telefon, hvor hero‑sektionens afsluttende linje lå enogtyve pixels under folden.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "Webudvikling", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/closed-the-colour-system-at-28-colours/", "url": "https://engineer.company/da/portfolio/closed-the-colour-system-at-28-colours/", "title": "Har låst farvesystemet fast på 28 farver", "summary": "Paletten er et lukket, målt sæt, der ikke kan vokse i det stille, og de to næsten ens stavemåder, der gav anledning til den, er væk.", "content_html": "<p><strong>Situation.</strong> Farverne på et site med mørkt som standard, et lyst tema, et printstylesheet, en tilstand med tvungne farver og et illustreret brand forbliver ikke et lille sæt af sig selv. De var allerede begyndt at glide, sådan som de altid gør: to stavemåder af den samme farve, der lå så tæt, at ingen kunne skelne dem, værdier, der var tastet ind igen som faste værdier ved siden af de tokens, der definerede dem, og dokumenterede farver, som intet malede.</p>\n<p><strong>Opgave.</strong> Paletten skulle gøres til et lukket sæt med en angivet tærskel for, hvornår to farver er forskellige, og noget skulle håndhæve, at sættet forblev lukket.</p>\n<p><strong>Handling.</strong> Otteogtyve farver, hver dokumenteret med det kontrastforhold, den måler på den flade, den optræder på, og elleve brandfarver erklæret én gang som tokens. Tærsklen er talmæssig og ikke redaktionel: to farver, der ligger tættere end en perceptuel afstand på 0,02 i et ensartet farverum, er én farve med to stavemåder. Linteren fejler på fem forskellige måder — en værdi, der er malet, men udokumenteret, dokumenteret, men ikke malet, noteret med det forkerte forhold, inden for tærsklen af en, der allerede findes, eller en brandfarve, der er skrevet igen som en fast værdi. Den fandt to med det samme: en temafarve, der fandtes i to stavemåder 0,018 fra hinanden, og en baggrundsfarve, der stod i stedet for en væg, den lå 0,021 fra. Gennemsigtighed bruger relativ farvesyntaks og ikke blandingsfunktionen, netop fordi kontrollen ikke kan se gennem en blanding, og en paletkontrol, der kan omgås, er ikke en kontrol. Reglen, der følger med, er kort: ret et token og ikke en regel, tilføj kun en farve, når ingen passer, og sænk aldrig et dokumenteret forhold for at få et design til at virke.</p>\n<p><strong>Resultat.</strong> Paletten er et lukket, målt sæt, der ikke kan vokse i det stille, og de to næsten ens stavemåder, der gav anledning til den, er væk. Det er en begrænsning, der indimellem siger nej — et design, der vil have en lidt anden blå, må tage den, der findes, eller argumentere for en niogtyvende farve, og det argument skal indeholde forholdet.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Dokumentation", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "Brand, marketing & SEO", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/found-the-light-theme-missing-a-whole-layer/", "url": "https://engineer.company/da/portfolio/found-the-light-theme-missing-a-whole-layer/", "title": "Har fundet et manglende lag i det lyse tema", "summary": "Et lag, der mangler i ét tema, får nu et commit til at fejle, og et, der havde manglet, siden det blev skrevet, bliver nu malet.", "content_html": "<p><strong>Situation.</strong> Sitet leverer to farvetemaer. Det mørke er standarden og bliver set på hele tiden; det lyse er en overstyring, der kun træder i kraft under en systemindstilling, hvilket betyder, at det ses langt sjældnere og aldrig af nogen, der tjekker det. Temaparitet er en defektklasse, hvor en flade bygges én gang og gentages én gang, og gentagelsen i stilhed taber et lag.</p>\n<p><strong>Opgave.</strong> Pariteten havde brug for en assertion, for det eneste alternativ er en person, der husker at skifte indstilling og kigge.</p>\n<p><strong>Handling.</strong> Tjekket er lille og strukturelt: for hver lagdelt flade tælles de lag, hvert tema maler, og det fejler, når tallene er forskellige. Det sammenligner ikke udseende — temaerne skal se forskellige ud — men sammensætning, og det er den, der bør være identisk. Det fandt defekten ved sin første kørsel. Sitets helsidesbaggrund er tre lag i det mørke tema, et facetmønster over et slør over væggen, og det lyse tema gentog den med to. Facetlaget havde manglet i det lyse tema, fra den dag det blev skrevet, og intet havde nogensinde sagt det, for en side, der mangler ét af tre baggrundslag, ligner en designbeslutning og ikke en fejl. Rettelsen var én regel; opskrivningen bagefter noterede, hvad laget koster i kontrast, så tilføjelsen er målt og ikke antaget at være gratis.</p>\n<p><strong>Resultat.</strong> Et lag, der mangler i ét tema, får nu et commit til at fejle, og et, der havde manglet, siden det blev skrevet, bliver nu malet. Det er netop den slags defekt, hele tjektilgangen sigter mod — intet var i stykker, intet gav fejl, siden blev gengivet korrekt, og den havde været forkert i månedsvis.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/made-the-reduced-motion-setting-honest/", "url": "https://engineer.company/da/portfolio/made-the-reduced-motion-setting-honest/", "title": "Har sikret, at reduceret bevægelse virker", "summary": "Indstillingen respekteres i praksis og ikke kun i hensigt, og elleve kørende animationer, som en generel regel så ud til at dække, er nu reelt dækket.", "content_html": "<p><strong>Situation.</strong> Sitet respekterede indstillingen for reduceret bevægelse med en enkelt regel, der slog alle overgange fra. Den så komplet ud, den bestod linteren, og med en emuleret indstilling for reduceret bevægelse animerede elleve selektorer stadig.</p>\n<p><strong>Opgave.</strong> Indstillingen skulle respekteres i praksis, og grunden til, at den oplagte regel fejlede, skulle gøres til noget, den næste person ikke kan gentage.</p>\n<p><strong>Handling.</strong> Årsagen er specificitet. En universel regel ligger i bunden af kaskaden og taber til enhver regel med en klasse, så en generel transition‑none bliver overtrumfet af hver eneste gennemtænkte animation på siden — og det er netop de animationer, der er værd at slå fra. Rettelsen var strukturel og ikke en lap: alt, der bevæger sig, ligger nu i én sen del af stylesheetet, og en linter afviser en overgang på transform alle andre steder. Den skelnen, reglen trækker, er bevidst og snævrere end den oplagte: reducér bevægelse, ikke farve. En farveovergang er ikke bevægelse, og at slå den fra får brugerflader til at føles i stykker for folk, der ikke har bedt om det, så en overgang, der opregner farveegenskaber, består, og en overgang på bevægelse gør ikke. Bevægelse skal også udtrykkes som en overgang og ikke som en keyframe‑animation, for den første kan trivielt afbrydes, og den anden kan ikke. To beslægtede fælder kom ud af det samme arbejde og er noteret med målinger: et menuornament var enogtredive grader inde i en drejning på tres grader, før det var halvt synligt, og fordi formen er periodisk, ser en halv drejning identisk ud til en tredjedel af prisen; og fem forskellige egenskaber gør hver især et element til den indeholdende blok for alt, der er placeret i forhold til viewporten, hvilket i stilhed havde skrumpet det lag, der lukker ved et klik, til en brøkdel af skærmen.</p>\n<p><strong>Resultat.</strong> Indstillingen respekteres i praksis og ikke kun i hensigt, og elleve kørende animationer, som en generel regel så ud til at dække, er nu reelt dækket. Den generelle lære er den, der står øverst i det dokument: en regel, der taber på specificitet, fejler i stilhed, og hver rapport kalder det noget andet.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "Webudvikling", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/built-a-637-line-print-stylesheet/", "url": "https://engineer.company/da/portfolio/built-a-637-line-print-stylesheet/", "title": "Har bygget et 637 linjer langt printstylesheet", "summary": "Sitet printer på ukendt papir i begge temaer, og de fire former for motoradfærd er skrevet ned med den defekt, hver af dem gav.", "content_html": "<p><strong>Situation.</strong> Sitets casestudier er det, nogen printer og tager med til et møde. Det gør papir til et reelt output og ikke bare en høflighed, og papir er et andet medie end en smal skærm — læserens papirstørrelse er ukendt, læserens orientering er ukendt, og browserens printadfærd varierer mellem motorer på måder, som ingen forhåndsvisning på skærmen afslører.</p>\n<p><strong>Opgave.</strong> Sitet skulle printe korrekt på ukendt papir, i begge farvetemaer, uden at et særskilt dokument skulle vedligeholdes ved siden af webudgaven.</p>\n<p><strong>Handling.</strong> Det er ét stylesheet på 637 linjer med én sideregel og én printblok. Sidereglen sætter en margen og sætter bevidst ingen papirstørrelse, så uanset hvad læseren vælger i dialogen, slår det igennem, og alt andet tilpasser sig det. Rodelementets skriftstørrelse er låst fast i punkter, så hver relativ måling har et fysisk anker, og én linjelængde på omkring tooghalvfjerds tegn gælder for de fire blokke på øverste niveau — og her vinder arket over designet, for en liggende side ville ellers lade en linje løbe op på omkring hundrede og ti tegn. Fire former for motoradfærd er dokumenteret som fælder, og hver af dem har forårsaget en reel defekt. Viewport‑enheder regnes ud fra sideboksen i én motor og fra vinduet i en anden, så en side, der printes fra et bredt vindue, får skåret en tredjedel af hver linje af i den anden. Fastplacerede elementer males på hvert ark, så de dekorative er fjernet, og to af dem er genbrugt som brevhoved og kolofon. Paletten bruger som standard hvid tekst, fordi standardtemaet er mørkt, og det printede de fire indledende afsnitsetiketter hvidt på hvidt — ordene var simpelthen væk. Og paginerede medier kan ikke dele en flex- eller grid‑boks, hvilket gav et blankt første ark på en achievement‑side. Et tjek i seks dele dækker det og tester seks kombinationer af papir og orientering, begge indstillinger for farveskema og det reelle sideantal mod det, indholdshøjden tilsiger.</p>\n<p><strong>Resultat.</strong> Sitet printer på ukendt papir i begge temaer, og de fire former for motoradfærd er skrevet ned med den defekt, hver af dem gav. De kendte grænser er noteret og ikke skjult: ingen sidetal eller løbende sidehoveder, én motor ignorerer kontrollen af enlige linjer øverst og nederst på siden, og initialer virker ikke overalt. En forvildet kommentarafslutning fik engang CSS‑linteren til at sluge en hel regel og bestå uden fejl to gange — kun printtjekket opdagede det.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Designsystemer & UI", "Dokumentation", "Frontend‑udvikling", "UX/UI‑design", "Webudvikling", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/mirrored-the-site-to-gemini-and-gopher/", "url": "https://engineer.company/da/portfolio/mirrored-the-site-to-gemini-and-gopher/", "title": "Har spejlet sitet til Gemini og Gopher", "summary": "Sitet kan læses over fire protokoller fra ét build, med 777 dokumenter hver og ingen ændring i HTML'en, og de alternative repræsentationer fylder omkring tolv…", "content_html": "<p><strong>Situation.</strong> Sitet er statisk, har ingen sporing og ingen backend i drift, og dets argument er, at et dokument ikke har brug for en megabyte JavaScript for at kunne læses. Det argument er let at fremføre og svært at påvise. To små internetprotokoller påviser det direkte, for ingen af dem kan overhovedet bære et script.</p>\n<p><strong>Opgave.</strong> Hele sitet skulle udgives over Gemini og Gopher fra det samme indhold, uden et andet indholdstræ og uden at ændre HTML’en.</p>\n<p><strong>Handling.</strong> Begge er outputformater af det samme build og ikke en særskilt byggepipeline. Sitegeneratoren fik egne medietyper og outputformater, og hver side, hvert afsnit og hvert taksonomibegreb fik to repræsentationer mere ved siden af sin HTML og sin Markdown‑tvilling. Resultatet er 777 Gemini‑dokumenter og 777 Gopher‑menuer, der frembringes fra den samme kilde, udrulles til det samme træ og leveres af to små dæmoner på den samme vært. To egenskaber gjorde det værd at gøre og ikke bare til en kuriositet. Begge formater er markeret som ikke‑alternative repræsentationer, så intet ved HTML’en ændrede sig — ikke én byte, og det blev målt og ikke antaget. Og Gopher‑formatet har ingen måde at placere et link inde i en sætning på, eftersom en menulinje består af tabulatoradskilte felter, så teksten ombrydes hårdt ved otteogtres kolonner under buildet; den begrænsning skærpede skrivningen på en måde, HTML’en aldrig krævede. Et Tor‑onion‑spejl ligger ved siden af dem, og det er den eneste offentlige indgang, platformen kunne tilføje uden at åbne en port i firewallen, for dæmonen ringer ud, og intet ringer ind.</p>\n<p><strong>Resultat.</strong> Sitet kan læses over fire protokoller fra ét build, med 777 dokumenter hver og ingen ændring i HTML’en, og de alternative repræsentationer fylder omkring tolv procent af HTML’ens egen vægt på disken. Kun én af de fire måles for trafik, og det står i platformens eget statistikdokument i stedet for at blive ignoreret i stilhed.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Frontend‑udvikling", "Infrastruktur", "Internationalisering", "Webudvikling", "DevOps & CI/CD‑automatisering", "Internationalisering & lokalisering", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/cut-the-stylesheet-bundle-from-87-kb-to-31-kb/", "url": "https://engineer.company/da/portfolio/cut-the-stylesheet-bundle-from-87-kb-to-31-kb/", "title": "Har skåret stylesheet‑bundtet fra 87 KB til 31 KB", "summary": "Bundtet er på 31 KB i stedet for 87, 756 KB døde filer bliver ikke længere udrullet, og hver beslutning har en måling knyttet til sig — også den, der gik den…", "content_html": "<p><strong>Situation.</strong> Sitets stylesheet‑bundt var på 87 KB, og for et dokumentsite uden framework er det det meste af en sidevægt, der er brugt, før noget indhold når frem. Sitet udgav også en mappe med ikoner ved hver udrulning, genereret én gang og ikke refereret af noget siden.</p>\n<p><strong>Opgave.</strong> Vægten skulle ned, uden at designet ændrede sig, og den skulle ned af en grund, man kunne pege på, og ikke ved almindelig oprydning.</p>\n<p><strong>Handling.</strong> Måling først. To tredjedele af det største stylesheet var én base64‑kodet font — omkring 56 KB tekst, der koder omkring 42 KB font — indlejret for at opnå en gevinst ved første optegning, som en forudindlæst ekstern fil alligevel får, og betalt på hver side, uanset om vægten var nødvendig eller ej. Da den blev taget ud, faldt den fil fra 84 KB til 28 og hele bundtet fra 87 til 31. De tre fontvægte forudindlæses i stedet, og den tredje kom først på listen, da feltdata viste, at den lukkede den længste kritiske kæde, og ikke fordi tre lød komplet. Særskilt fandt en gennemgang af, hvad udrulningen reelt udgav, enogtyve genererede ikoner og en dubleret konfigurationsfil, 756 KB, der blev sendt ud hver gang og ikke refereret fra noget sted; og selve sitets ikonfil kom ned fra 145 KB til 15. Et moderne billedformat blev vurderet til bomærket, målt og fravalgt — filen blev større, og udvælgelsesmekanismen vælger efter rækkefølge og ikke efter størrelse, så en moderne browser ville have taget den tungere fil overalt. To tjek sikrer nu, at det holder: intet fotografi må vises bredere end halvdelen af sine kildepixels, og hver illustration skal udgives i en størrelse, dens egne pixels understøtter ved både normal og høj tæthed.</p>\n<p><strong>Resultat.</strong> Bundtet er på 31 KB i stedet for 87, 756 KB døde filer bliver ikke længere udrullet, og hver beslutning har en måling knyttet til sig — også den, der gik den anden vej, og det er den mest nyttige optegnelse af de to.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Frontend‑udvikling", "Optimering af ydeevne", "Webudvikling", "Brand, marketing & SEO", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/fixed-a-sitemap-with-one-shared-timestamp/", "url": "https://engineer.company/da/portfolio/fixed-a-sitemap-with-one-shared-timestamp/", "title": "Har rettet et sitemap med ét fælles tidsstempel", "summary": "En ny synkronisering efter en måneds redigeringer af indholdet rører nu otte filer ud af 186 i stedet for dem alle, og sitemappet siger noget sandt.", "content_html": "<p><strong>Situation.</strong> Sitets sitemap fortalte søgemaskinerne, at 172 af dets 176 sider sidst var ændret i samme øjeblik. Det er ikke en subtil unøjagtighed — en ændringsdato er et løfte til en crawler om, at en sides indhold er ændret, og et site, der giver det løfte 172 gange på én gang, fortæller enten sandheden om en fuld omskrivning eller fortæller ingen noget brugbart.</p>\n<p><strong>Opgave.</strong> Datoerne skulle beskrive indholdet og ikke filen, uden at opfinde en præcision, som repositoryet ikke har.</p>\n<p><strong>Handling.</strong> Årsagen var, at datoen kom fra filens ændringstidspunkt, og indholdseksporten skriver hver fil om ved hver kørsel, så en enkelt synkronisering stemplede hele korpusset. Rettelsen flytter kilden til versionshistorikken med en fallback‑kæde, der først prøver et eksplicit felt, så commit‑historikken og til sidst filen. Fælden i den rettelse er værd at notere: selve feltnavnet skal stå på listen, ellers læser generatoren det aldrig, så en konfiguration, der ser ud til at foretrække en angivet dato, men udelader navnet, ignorerer i stilhed alle angivne datoer. To andre beslutninger om datoer kom ud af det samme arbejde. Databasen registrerer nu, hvornår hver post blev skrevet og sidst revideret, bevidst holdt adskilt fra, hvornår arbejdet fandt sted — de ligger år fra hinanden, og at blande dem sammen ville datere en side, der er skrevet i år, til et årti siden. Og rækkerne i den fil er valgfrie, og intet udledes, for repositoryets egen historik begynder senere end indholdets: en manglende dato lader feltet stå tomt i stedet for at registrere en migrering, ud fra det princip, at et præcist forkert tal er værre end et manglende, eftersom det kun er det forkerte, der bliver troet.</p>\n<p><strong>Resultat.</strong> En ny synkronisering efter en måneds redigeringer af indholdet rører nu otte filer ud af 186 i stedet for dem alle, og sitemappet siger noget sandt. Den generelle regel kom ind i metadatadokumentet ved siden af, for den samme fælde gælder for hvert felt, der har en fallback‑kæde.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Brand & marketing", "Python", "Test & QA", "Webudvikling", "Brand, marketing & SEO", "DevOps & CI/CD‑automatisering", "Parathed til AI‑agenter", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/ran-development-as-51-written-initiatives/", "url": "https://engineer.company/da/portfolio/ran-development-as-51-written-initiatives/", "title": "Har kørt udvikling som 51 nedskrevne initiativer", "summary": "Enoghalvtreds beslutninger med begrundelsen vedhæftet, omtrent tre fjerdedele lukket, og de fravalgte er lige så lette at finde frem som de accepterede.", "content_html": "<p><strong>Situation.</strong> Et enmandsfirma har intet planlægningsmøde, ingen gennemgang af backloggen og ingen, der kan være uenig i en beslutning. I stedet har det en stærk tilbøjelighed til at lave det arbejde, der lige nu er interessant, og ingen optegnelse bagefter over, hvorfor noget blev valgt — og det bliver et problem, første gang en beslutning skal genovervejes eller forsvares.</p>\n<p><strong>Opgave.</strong> Udviklingen af sitet havde brug for en skriftlig optegnelse pr. initiativ, der overlever at blive genlæst måneder senere, og som dækker både dem, der blev fravalgt, og dem, der blev bygget.</p>\n<p><strong>Handling.</strong> Enoghalvtreds initiativer fordelt på fireoghalvtreds nummererede og indekserede plandokumenter, hver med problemet, de målinger, der blev foretaget, beslutningen og dens åbne afkrydsningsfelter. De er ikke sammendrag skrevet bagefter. De rummer de tal, der afgjorde diskussionen — planlægningsmålingerne, der formede tjekafvikleren, billedformatet, der blev testet og fravalgt, fordi filen blev større, prisstigen, som ejeren bekræftede, og gennemgangen, der viste, at sitet beviste, hvad virksomheden kan, på 371 sider uden at oplyse nogen pris, nogen samarbejdsform eller noget minimum nogen steder. Fravalgte fund får samme behandling som accepterede: en gennemgang af agentparathed gav otte fund, og seks blev afvist, hver med afgørelsen noteret, netop så et senere automatiseret forslag ikke i stilhed kan omgøre en skriftlig beslutning. Statustavlen læser en plans afkrydsningsfelter og ikke dens tekst, for tekst siger “færdig”, og felter siger, hvad der er åbent. Den skelnen var ikke teoretisk — en dokumentationslinter fandt senere seks planer markeret som færdige, selvom de havde felter, der ikke var krydset af.</p>\n<p><strong>Resultat.</strong> Enoghalvtreds beslutninger med begrundelsen vedhæftet, omtrent tre fjerdedele lukket, og de fravalgte er lige så lette at finde frem som de accepterede. Prisen er, at hvert initiativ har en opskrivning, og det er en reel skat på småt arbejde og har indimellem betydet, at optegnelsen er længere end den ændring, den beskriver.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Agile & Scrum", "Dokumentation", "Interessenter & rapportering", "Produkt & krav", "Projektledelse", "Teknisk ledelse", "Parathed til AI‑agenter", "Produktstrategi & kravspecifikation", "Projektledelse (Agile)", "Teknisk dokumentation", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/put-the-documentation-under-its-own-linter/", "url": "https://engineer.company/da/portfolio/put-the-documentation-under-its-own-linter/", "title": "Har sat dokumentationen under sin egen linter", "summary": "Seksogtyve dokumenter med i alt omkring 2.666 effektive linjer, hvert under et budget, der ikke kan vokse, og hver sti og hvert symbol, de nævner, er tjekket…", "content_html": "<p><strong>Situation.</strong> Repositoryet styrer sig selv med skrevne instruktioner — seksogtyve dokumenter, der dækker kvalitetspolitikken, designsystemet, tilgængelighed, print, metadata, oversættelse og git. Instruktioner forfalder efter et bestemt mønster: de bliver længere, de glider væk fra koden, de begynder at henvise til filer, der er flyttet, og ved en vis størrelse holder de op med at blive læst. En regel, der ikke bliver læst, er ikke en regel.</p>\n<p><strong>Opgave.</strong> Dokumentationen havde brug for samme behandling som koden — en linter med grænser, der får noget til at fejle og ikke bare giver råd.</p>\n<p><strong>Handling.</strong> Otte politikker, alle mekaniske. Et hårdt loft over dokumentlængden og et blødere, der advarer. Et linjebudget pr. fil, der kun nogensinde strammes, så et dokument kan skrumpe, men ikke vokse igen. Et krav om indeks over en tærskel og et afsnitsindeks over en mindre. En placeringsregel for, hvilket dokument der ejer hvilket emne. Gyldige links. Ærlige planer — den status, en plan påstår, skal svare til dens egne afkrydsningsfelter. En regel om, at hver opgave, der spærrer for et commit, er nævnt i et eller andet dokument. Og et tjek af reelle stier: at hver sti med mappe og hvert kodesymbol, et dokument nævner, også findes. Den første kørsel var argumentet for hele øvelsen: seks planer markeret som færdige, selvom de havde felter, der ikke var krydset af, fjorten opgaver, der spærrede for hvert commit og ikke var nævnt i noget dokument overhovedet, tre ødelagte links, ét forældreløst dokument og tre døde stier fordelt på fem dokumenter. Budgettet afviste derefter sin egen forfatter — da sammendraget af dette arbejde blev skrevet ind i udviklingsdokumentet, kom det over sin grænse, og det er sådan, et særskilt dokument opstod, hvilket er præcis sådan, politikken skal virke.</p>\n<p><strong>Resultat.</strong> Seksogtyve dokumenter med i alt omkring 2.666 effektive linjer, hvert under et budget, der ikke kan vokse, og hver sti og hvert symbol, de nævner, er tjekket mod træet. Det ubehagelige fund er det, der er værd at gentage: fjorten tjek spærrede for hvert eneste commit uden at optræde i noget dokument, så de regler, maskinen håndhævede, og de regler, menneskene læste, var allerede gledet fra hinanden.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Dokumentation", "Projektledelse", "Python", "Teknisk ledelse", "Test & QA", "Projektledelse (Agile)", "Teknisk dokumentation", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/linted-the-10242-line-quality-gate-itself/", "url": "https://engineer.company/da/portfolio/linted-the-10242-line-quality-gate-itself/", "title": "Har lintet selve kvalitetsgaten på 10.242 linjer", "summary": "Den største og mest bærende kode i repositoryet er nu formateret, lintet og typetjekket, med tretten fund rettet og nul undertrykkelser.", "content_html": "<p><strong>Situation.</strong> Sitets kvalitetsgate er toogtredive scripts på i alt 10.242 linjer JavaScript. Den var med god margin den største kodemængde i repositoryet, og den var den eneste kodemængde, intet læste — ingen formatter, ingen linter, intet typetjek. De programmer, der håndhævede hver regel i projektet, var de eneste programmer, der ikke selv var underlagt nogen af dem.</p>\n<p><strong>Opgave.</strong> Tjekkerne skulle holdes op mod den standard, de findes for at håndhæve, og formatteren skulle tilpasses dem og ikke omvendt.</p>\n<p><strong>Handling.</strong> Formatteren kom først, og indrykningsbredden var den interessante beslutning. Repositoryets standard er fire mellemrum; med fire mellemrum ville formatteren have omskrevet 2.173 linjer af den største tjekker. En overstyring til to mellemrum for de filer bragte det ned på 38 linjers reel afvigelse. Princippet, der blev noteret sammen med den, er, at formatteren tilpasses koden og ikke koden formatteren — at omformatere to tusind linjer for at tilfredsstille en præference ødelægger muligheden for at læse filens historik. Derefter linteren, som gav tretten reelle fund, alle rettet og ingen undertrykt. Python‑tjekkerne fik samme behandling gennem et typetjek, der er sat til et mellemniveau af strenghed med tredive enkeltregler fra det strengeste niveau slået til oveni, valgt ved måling: med den indstilling er træet tavst, og af de tredive kandidater var niogtyve allerede tavse, og én slog ud — og den blev rettet i stedet for undtaget. Typetjekket fandt to reelle defekter, som linteren havde ladet passere, begge om en værdis form og ikke dens syntaks.</p>\n<p><strong>Resultat.</strong> Den største og mest bærende kode i repositoryet er nu formateret, lintet og typetjekket, med tretten fund rettet og nul undertrykkelser. Grunden til, at det betyder mere, end linjeantallet antyder, står i planen: en defekt i sitets stylesheet viser sig som en side, der ser forkert ud, og en defekt i en tjekker viser sig som et tjek, der består, når det ikke burde.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Frontend‑udvikling", "Python", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/built-a-database-driven-cv-generator/", "url": "https://engineer.company/da/portfolio/built-a-database-driven-cv-generator/", "title": "Har bygget en databasedrevet CV‑generator", "summary": "Seks formater, tre sprog og fem temaer kommer ud af én database, og en rettet sætning rettes én gang.", "content_html": "<p><strong>Situation.</strong> Et CV, et referenceark, en portefølje og en ansøgning er de samme fakta arrangeret på fire måder, og at holde dem som fire dokumenter betyder, at hver rettelse foretages fire gange og til sidst ikke bliver det. Tre sprog ganger det med tre. Fejltilstanden er ikke, at et dokument er forkert; den er, at to dokumenter er uenige, og at intet siger, hvilket af dem der er det gældende.</p>\n<p><strong>Opgave.</strong> Én kilde til fakta skulle frembringe hvert dokument, på hvert sprog, i hvert format, med arrangementet afgjort af kode og ikke af den, der sidst redigerede en fil.</p>\n<p><strong>Handling.</strong> Kilden er en SQLite‑database med treogtyve tabeller — achievements, titler, virksomheder, regioner, kategorier, ydelser, profiler, referencer, resuméer — samlet af en pipeline i nitten trin, der kører fra en tom fil til en komplet database med én kommando. Trinnene er ordnede, og hvert er skrevet til at være sikkert at gentage, så pipelinen kan køres mod en eksisterende database uden at duplikere en række. Enogfyrre Python‑moduler på i alt 10.580 linjer gengiver seks outputformater fra den: HTML, PDF i to varianter, ren tekst, Markdown og JSON, gennem seks Jinja‑skabeloner og otte stylesheets. Kørselsafhængighederne blev holdt nede på præcis to, en skabelonmotor og en PDF‑gengiver, ud fra det argument, at en dokumentgenerator, der ikke kan installeres om fem år, ikke har bevaret noget. Målretning er et førsteklasses begreb og ikke en manuel redigering: en profil vælger, hvilke achievements der optræder, og i hvilken rækkefølge, så et dokument rettet mod én slags læser er en databaseforespørgsel og ikke en kopi.</p>\n<p><strong>Resultat.</strong> Seks formater, tre sprog og fem temaer kommer ud af én database, og en rettet sætning rettes én gang. Prisen er, at systemet nu er den eneste måde at frembringe et dokument på — der er ikke længere en fil at åbne og redigere i en fart, og at tilføje et sprog betyder at tilføje det overalt, før noget som helst kan bygges.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Backend‑udvikling", "Databaser", "Full stack‑udvikling", "Internationalisering", "Løsningsarkitektur", "Python", "SQL", "Backend- & API‑udvikling", "Databasedesign & datamodellering", "Full stack‑produktudvikling", "Internationalisering & lokalisering", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/reached-pdf-ua-1-conformance-at-106-of-106/", "url": "https://engineer.company/da/portfolio/reached-pdf-ua-1-conformance-at-106-of-106/", "title": "Har opnået PDF/UA‑1‑konformitet: 106 af 106", "summary": "Tilgængelighedsoverholdelse er bevist med fuldt hus, og arkivpåstanden er oplyst ærligt som næsten opfyldt, med det konkrete hul nævnt.", "content_html": "<p><strong>Situation.</strong> En PDF, der ser rigtig ud på skærmen, fortæller intet om, hvorvidt en skærmlæser kan læse den, om dens overskrifter danner en struktur, om dens sprog er erklæret, eller om den stadig kan åbnes om tyve år. Tilgængelig PDF og arkiv‑PDF er begge standarder, hvis overholdelse kan tjekkes maskinelt, og et dokument, der aldrig er kørt gennem validatoren, er et dokument, der fremsætter en uverificeret påstand.</p>\n<p><strong>Opgave.</strong> De genererede dokumenter skulle måles mod begge standarder, med resultatet noteret som et tal og ikke som en hensigt.</p>\n<p><strong>Handling.</strong> Ni dokumenter — CV’et i sine varianter, referencearket, porteføljen og ansøgningen, på flere sprog — blev kørt gennem en uafhængig validator for tilgængelighedsprofilen og arkivprofilen hver for sig. Tilgængelighedsprofilen består med 106 af 106 regler, hvilket krævede tagget struktur, erklæret dokumentsprog, alternativ tekst på hver ikke‑dekorativ grafik, en rigtig titel i metadataene og en eksplicit læserækkefølge i stedet for den, layoutet tilfældigvis giver. Arkivprofilen er det mere interessante resultat: den fejler præcis én regel af 146. Det er den slags resultat, der er let at fejlrapportere. Hundrede og femogfyrre beståede regler ligner overholdelse i et sammendrag, og det er ikke overholdelse; det er et dokument, der vil blive afvist af et system, der håndhæver standarden. Den fejlende regel er noteret med, hvad den er, og hvorfor den ikke er lukket, i stedet for at blive rundet væk.</p>\n<p><strong>Resultat.</strong> Tilgængelighedsoverholdelse er bevist med fuldt hus, og arkivpåstanden er oplyst ærligt som næsten opfyldt, med det konkrete hul nævnt. Den begrænsning, der er værd at sige ligeud, er, at begge tal kommer fra én validator; en anden implementering kan være uenig, og en regel, der består, er kun bevis for, at denne tjekker intet havde at sige om den.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Dokumentation", "Python", "Test & QA", "UX/UI‑design", "Webudvikling", "Teknisk dokumentation", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/enabled-every-linter-rule-as-an-error/", "url": "https://engineer.company/da/portfolio/enabled-every-linter-rule-as-an-error/", "title": "Har slået alle linterregler til som fejl", "summary": "Linteren kører ved fuld styrke med nul fund, og hver afvigelse er dokumenteret på den linje, hvor den tages.", "content_html": "<p><strong>Situation.</strong> De fleste projekter vælger en behagelig delmængde af deres linters regler, og delmængden vælges efter, hvilke regler der var tavse den dag, den blev konfigureret. Det gør konfigurationen til en optegnelse over kodens eksisterende vaner og ikke en standard, koden holdes op mod, og hver regel, der er slået fra, er en defektklasse, ingen nogensinde får besked om.</p>\n<p><strong>Opgave.</strong> Standarden skulle vendes om — hver regel, værktøjet implementerer, slået til som en fejl — og det resulterende efterslæb arbejdes ned til nul og ikke forhandles ned ved at slå regler fra igen.</p>\n<p><strong>Handling.</strong> Det komplette regelsæt gav 1.815 fund ved første kørsel. De blev arbejdet igennem efter kategori og ikke efter fil, for kategorierne fortæller noget: ubrugte argumenter og skyggede indbyggede navne er støj, men sikkerhedskategorien, kategorien for foranderlige standardværdier og kategorien for undtagelseshåndtering pegede hver især på reel adfærd. Der findes ægte uforeneligheder — en formatter og en linter kan være uenige om den samme linje, og nogle få regler modsiger projektets egne bevidste valg — og hver af det lille antal undtagelser har en skriftlig begrundelse dér, hvor undtagelsen gøres, som siger, hvad reglen ville, og hvorfor denne kode gør noget andet. To af dem går videre og er bakket op af et tjek og ikke en kommentar, så undtagelsen ikke ubemærket kan brede sig: reglen er slået fra, og en test efterprøver netop den egenskab, reglen ville have håndhævet. Typetjek kører i streng tilstand ved siden af, hvilket er en særskilt og hårdere standard, og det er den, der fangede defekter, linteren ikke kunne se, fordi de handler om, hvad en værdi er, og ikke om, hvordan den er skrevet.</p>\n<p><strong>Resultat.</strong> Linteren kører ved fuld styrke med nul fund, og hver afvigelse er dokumenteret på den linje, hvor den tages. Prisen er reel og værd at nævne: den strengeste indstilling frembringer fund, der reelt ikke er værd at handle på, og nogen skal træffe den vurdering 1.815 gange i stedet for én gang.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Python", "Test & QA", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/vendored-a-qr-encoder-in-522-lines/", "url": "https://engineer.company/da/portfolio/vendored-a-qr-encoder-in-522-lines/", "title": "Har indlejret en QR‑encoder på 522 linjer", "summary": "Antallet af kørselsafhængigheder forblev to, og indkoderen er den eneste medleverede komponent i projektet.", "content_html": "<p><strong>Situation.</strong> Dokumenterne havde brug for en QR‑kode, der henviser til onlineversionen. Alle tilgængelige biblioteker kan det, og at tage et af dem ville have tilføjet en tredje kørselsafhængighed til et projekt, der bevidst havde holdt sig til to — en skabelonmotor og en PDF‑gengiver — ud fra det argument, at en dokumentgenerator stadig skal kunne installeres om flere år.</p>\n<p><strong>Opgave.</strong> Enten skulle afhængigheden accepteres eller formatet implementeres, og implementeringen skulle efterprøves som korrekt og ikke blot frembringe noget firkantet og sort.</p>\n<p><strong>Handling.</strong> Indkoderen er på 522 linjer og implementerer de dele af specifikationen, som formålet reelt kræver: indkodning i byte‑tilstand, Reed‑Solomon‑fejlkorrektion over Galois‑legemet med 256 elementer med generatorpolynomiet bygget i den krævede grad, det faste modullayout med dets søgemønstre, timingmønstre og justeringsmønstre, format- og versionsinformation samt alle otte datamaskemønstre vurderet mod specifikationens fire strafregler, så masken med den laveste score vælges i stedet for en fast. Efterprøvningen er den del, der gjorde det forsvarligt. At teste en indkoder mod dens egen logik beviser intet, så den færdige modulmatrix læses tilbage af en afkoder, der er skrevet uafhængigt ud fra specifikationens læserækkefølge, og testen fastslår, at den afkodede streng er lig med inddata. Det gør “den frembringer et plausibelt billede” til “den frembringer en kode, der afkoder til den rigtige URL”, og det tjekkes ved hver kørsel og ikke én gang med øjet og en telefon.</p>\n<p><strong>Resultat.</strong> Antallet af kørselsafhængigheder forblev to, og indkoderen er den eneste medleverede komponent i projektet. Det skal siges ligeud, at dette ikke er en generel implementering — den understøtter de tilstande og versioner, dokumenterne bruger, og intet andet, og den ærlige begrundelse er afhængighedsbudgettet og ikke nogen påstand om, at resultatet er bedre end et modent bibliotek.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Dokumentation", "Optimering af ydeevne", "Python", "Test & QA", "Backend- & API‑udvikling", "Platform- & løsningsarkitektur" ] }, { "id": "https://engineer.company/da/portfolio/found-the-hooks-and-the-gate-disagreeing/", "url": "https://engineer.company/da/portfolio/found-the-hooks-and-the-gate-disagreeing/", "title": "Har fundet uenighed mellem hooks og kvalitetsgate", "summary": "Hooken og porten kører beviseligt de samme tjek, og prosatjekkene kører nu ved hver commit. Afvejningen er commit-hookens køretid, som voksede og vil blive ved…", "content_html": "<p><strong>Situation.</strong> Projektet havde en commit‑hook, der kører tjek, før en commit accepteres, og en fuld kvalitetsport, der køres efter behov. Et dokument angav, at hooken kører porten, så intet kunne nå historikken uden at bestå alt. Begge lister blev vedligeholdt i hånden, i to forskellige filer, og intet sammenlignede dem.</p>\n<p><strong>Opgave.</strong> Påstanden skulle gøres til en efterprøvning, hvilket betød at opregne begge mængder programmatisk og fejle, når de afviger.</p>\n<p><strong>Handling.</strong> Testen læser hook‑konfigurationen og portens opgavedefinitioner, udleder for hver af dem den mængde tjek, den reelt kalder, og sammenligner. De stemte ikke. Fem tjek fandtes kun i porten og kørte aldrig ved commit, og de fem var ikke tilfældige — det var dem, der læser selve CV’ets prosa: stemmetjekket, der holder anmeldelser upersonlige, buzzword‑tjekket, tjekket af taloverensstemmelse mellem sprogene, notationstjekket og stavekontrollen over indhold. Med andre ord kørte hvert tjek, der beskytter kode, automatisk, og hvert tjek, der beskytter teksten, kørte kun, når nogen huskede det. Da teksten er hele produktet, lå risikoen stik modsat af, hvor nogen ville have gættet. Rettelsen var at bringe de fem ind i hooken, hvilket krævede at gøre to af dem hurtige nok til at holde sig inden for tidsbudgettet for en pre‑commit‑hook, og derefter at holde sammenligningstesten på plads, så de to lister ikke kan glide fra hinanden igen. Dokumentet, der havde beskrevet en hensigt, beskriver nu noget håndhævet.</p>\n<p><strong>Resultat.</strong> Hooken og porten kører beviseligt de samme tjek, og prosatjekkene kører nu ved hver commit. Afvejningen er commit‑hookens køretid, som voksede og vil blive ved med at vokse, efterhånden som indholdet vokser, og der findes et punkt, hvor en langsom hook bliver omgået — så denne rettelse har en holdbarhed målt i, hvor længe tjekkene forbliver hurtige.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Dokumentation", "Python", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk dokumentation", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/rehearsed-the-forge-ci-hook-found-2-defects/", "url": "https://engineer.company/da/portfolio/rehearsed-the-forge-ci-hook-found-2-defects/", "title": "Har prøvekørt forgens CI‑hook og fundet 2 fejl", "summary": "Hooken virker på begge veje, og begge defekter blev fundet, før noget rigtigt push mødte dem. Begrænsningen er, at en prøve stadig er en simulering: den…", "content_html": "<p><strong>Situation.</strong> Den selvhostede git‑forge tager imod pushes og kan køre en hook på serversiden for at afvise arbejde, der ikke består porten. En hook på serversiden er det ene stykke automatik, der ikke kan testes ved at køre den lokalt: den udføres i et bart repository uden arbejdstræ, i et miljø, som forgen sætter op, og læser de pushede referencer fra sin standardinddata. At læse scriptet og konkludere, at det er korrekt, er et gæt.</p>\n<p><strong>Opgave.</strong> Hooken skulle prøves af mod et rigtigt bart repository og et rigtigt push, før man stolede på den, og ikke udrulles først og afsløre sine fejl bagefter.</p>\n<p><strong>Handling.</strong> Prøven opretter et bart repository, installerer hooken, kloner det, committer, pusher og efterprøver både accept- og afvisningsvejen. Den fandt to defekter, og ingen af dem var synlige i filen. Den første var en fallback i argumenthåndteringen: når hooken ikke kunne bestemme en reference, indsatte den en pladsholder, der ikke var et objekt, der kunne slås op, og fordi værdien ankommer på hookens standardinddata og ikke som en parameter, viste fejlen sig som en urelateret fejl meget længere nede. Den anden er den, der er værd at huske. Git eksporterer en miljøvariabel med repositoryets mappe, når den kalder en hook, og den variabel nedarves af alt, hvad hooken kører. Porten tjekker den pushede revision ud og kører testsuiten inde i den, og testsuiten nedarvede henvisningen til det bare repository — så nitten test, der rører git, blev slået op mod det forkerte repository og gik røde på kode, der var korrekt. Miljøet skal ryddes ved grænsen, og den grænse er usynlig, medmindre tingen reelt køres.</p>\n<p><strong>Resultat.</strong> Hooken virker på begge veje, og begge defekter blev fundet, før noget rigtigt push mødte dem. Begrænsningen er, at en prøve stadig er en simulering: den beviser, at hooken overlever én type push, og forgen ser i drift pushes, som denne prøve ikke konstruerer, herunder tvungne opdateringer og sletning af grene.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Infrastruktur", "Linux & servere", "Python", "Test & QA", "DevOps & CI/CD‑automatisering", "Infrastructure as Code", "Systemadministration" ] }, { "id": "https://engineer.company/da/portfolio/moved-every-string-into-a-content-tree/", "url": "https://engineer.company/da/portfolio/moved-every-string-into-a-content-tree/", "title": "Har flyttet alle tekststrenge til et indholdstræ", "summary": "Prosaen kan nu redigeres uden at røre kode og sammenlignes mellem sprogene ved at tælle. Prisen er, at tilføjelsen af et sprog nu er utvetydig og ikke gradvis…", "content_html": "<p><strong>Situation.</strong> Hver brugervendt streng i generatoren — hver achievement, hver anmeldelse, hver afsnitsoverskrift, hvert resumé, på tre sprog — lå inde i Python‑kildekode. Det gør redigeringen af en sætning til en kodeændring, gør gennemgangen af en oversættelse til en diff mod kildekode og gør det umuligt at se med et blik, om et sprog er komplet. Det skjuler også den fejltilstand, der betyder noget: en oversættelse kan være til stede, forkert og utilgængelig på samme tid.</p>\n<p><strong>Opgave.</strong> Strengene skulle flyttes ud af koden og ind i et indholdstræ, der kan tælles, sammenlignes mellem sprogene og tjekkes uden at køre gengiveren.</p>\n<p><strong>Handling.</strong> Resultatet er 874 filer under en indholdsmappe, ordnet efter art og derefter efter sprog: achievements, anmeldelser, resuméer, titler, virksomheder, regioner, kategorier, ydelser, profiler, referencer og ansøgningsbrudstykker. Tabulatorseparerede filer, hvor enheden er en linje med en identifikator, enkeltfiler, hvor enheden er et afsnit. Indlæseren læser dem på byggetidspunktet, og databasen samles ud fra dem, så træet er kilden, og databasen er afledt. To fund kom direkte ud af at kunne tælle. Nogle oversatte strenge havde ikke længere nogen engelsk modpart — døde poster, som intet gengav, og som ingen kunne have bemærket, mens de var indlejret i kode, for en urefereret ordbogsnøgle ser præcis ud som en refereret. Og et indholdstjek, der skulle bedømme hver achievement, læste kun den delmængde, det kunne slå op, bedømte syvogtredive ud af treoghalvfems og meldte succes. At gøre korpusset til en mappe gjorde begge dele synlige som en uoverensstemmelse i filtal.</p>\n<p><strong>Resultat.</strong> Prosaen kan nu redigeres uden at røre kode og sammenlignes mellem sprogene ved at tælle. Prisen er, at tilføjelsen af et sprog nu er utvetydig og ikke gradvis — træet gør et ufuldstændigt sprog åbenlyst, hvilket er pointen, men det betyder også, at delvise oversættelser ikke i stilhed kan udgives, mens de gøres færdige.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Data governance", "Dokumentation", "Internationalisering", "Migrering & modernisering", "Python", "Backend- & API‑udvikling", "Data governance & datakvalitet", "Databasemigrering & modernisering", "Internationalisering & lokalisering" ] }, { "id": "https://engineer.company/da/portfolio/built-a-cross-language-figure-check/", "url": "https://engineer.company/da/portfolio/built-a-cross-language-figure-check/", "title": "Har bygget et tværsprogligt tjek af tal", "summary": "Atten reelle defekter er lukket, og hele fejlklassen med dem, da hver fremtidig oversættelse sammenlignes med sin engelske kilde, før den kan committes.", "content_html": "<p><strong>Situation.</strong> Den mest skadelige slags oversættelsesfejl i et CV er ikke en kejtet vending. Det er et tal, der forsvinder. En engelsk sætning, der hævder en forbedring med en faktor halvtreds, oversat til en sætning, der siger “betydeligt”, er en påstand, der i stilhed trækkes tilbage på ét marked og fastholdes på et andet — og ingen stavekontrol, grammatikkontrol eller menneskelig læsning af målsproget alene vil nogensinde bemærke det, for den oversatte sætning er fuldkommen god prosa.</p>\n<p><strong>Opgave.</strong> Der var brug for et tjek, som læser sprogene op mod hinanden og ikke hvert for sig, på de to ting, der skal overleve oversættelse: tallene og den notation, hvert sprog bruger til at skrive dem.</p>\n<p><strong>Handling.</strong> Taltjekket udtrækker hvert tal, hver procentdel, hver multiplikator og hver enhed fra den engelske streng og kræver, at hver af dem optræder i hver oversættelse af den streng, med multiplikatorformerne tilknyttet pr. sprog i stedet for at blive matchet ordret — den engelske form for halvtreds gange svarer til en bestemt dansk vending og en bestemt ukrainsk vending, og tjekket kender sammenhængen i stedet for kun at kræve cifrene. Notationstjekket er spejlbilledet af det: hvert sprog har konventioner, det skal bruge, og konventioner, det ikke må bruge, herunder decimalskilletegn, tusindgruppering og anførselstegn. Ukrainsk bruger vinkelanførselstegn; engelske dobbelte anførselstegn i en ukrainsk sætning er lige så forkerte som et manglende tal og langt lettere at indføre ved kopiering. Den første fulde kørsel fandt to danske beskrivelser, hvor et måltal, der stod i den engelske tekst, var faldet ud, og seksten ukrainske tekststykker med anførselstegn i engelsk stil.</p>\n<p><strong>Resultat.</strong> Atten reelle defekter er lukket, og hele fejlklassen med dem, da hver fremtidig oversættelse sammenlignes med sin engelske kilde, før den kan committes. Det, tjekket ikke kan, er at bedømme betydningen: det beviser, at tallet overlevede, og at tegnsætningen følger sprogets egne regler, og en oversættelse, der bevarer hvert tal, mens den vender påstanden om, består det uden videre.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data governance", "Dokumentation", "Internationalisering", "Python", "Test & QA", "Data governance & datakvalitet", "Internationalisering & lokalisering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/built-a-native-macos-client-in-swift-6/", "url": "https://engineer.company/da/portfolio/built-a-native-macos-client-in-swift-6/", "title": "Har bygget en native macOS‑beskedklient i Swift 6", "summary": "En native klient, der starter som et Mac-program, bruger platformens egne materialer og kontroller og ikke har nogen indlejret browser.", "content_html": "<p><strong>Situation.</strong> Beskedprotokollen havde en velafprøvet kerne skrevet i Rust og klienter på flere platforme, men skrivebordsoplevelsen på macOS var en platformsuafhængig skal — den så ikke ud som et Mac‑program, opførte sig ikke som et og slæbte rundt på et runtime‑miljø, som et native program ikke har brug for. Kernen stiller sine funktioner til rådighed gennem en C‑grænseflade, hvilket betyder, at ethvert sprog, der kan kalde C, kan bygge på den, og det kan Swift.</p>\n<p><strong>Opgave.</strong> En native klient skulle bygges direkte på den C‑grænseflade, i platformens nuværende sprog og nuværende UI‑framework, med kernen linket ind og ikke leveret ved siden af.</p>\n<p><strong>Handling.</strong> Programmet er 11.141 linjer Swift fordelt på treoghalvtreds filer i app‑målet, bygget på SwiftUI med kernen linket ind som et statisk arkiv, kompileret fra en fastlåst upstream‑revision. At fastlåse revisionen i stedet for at følge en gren er den beslutning, der gør buildet reproducerbart: C‑grænsefladen er kontrakten, og en kerne i bevægelse ændrer kontrakten uden at ændre en linje Swift. Lagdelingen holder den usikre overflade lille — en tynd indpakning ejer hver pointer og hver streng, der krydser grænsen, modellerne over den er almindelige Swift‑værdier, og grænsefladelaget ser aldrig en rå pointer. Alt, hvad C‑grænsefladen returnerer, har en ejerskabsregel, og en enkelt fejl i dem giver enten en lækage eller et nedbrud uden nogen advarsel fra oversætteren undervejs, så indpakningen er der, hvor hele projektets hukommelsesdisciplin bor. Buildet drives af en task runner med seksoghalvtreds mål, der dækker kompileringen af kernen, Swift‑buildet, testsuiterne, linterne og pakningen.</p>\n<p><strong>Resultat.</strong> En native klient, der starter som et Mac‑program, bruger platformens egne materialer og kontroller og ikke har nogen indlejret browser. Det, der ikke er gjort, bør siges: dette er ikke en udgivet version. Der er ingen notariseret distribution, ingen opdateringskanal og ét sprog i grænsefladen, så det er et fungerende program og ikke et produkt, som andre bruger.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Designsystemer & UI", "Frontend‑udvikling", "Full stack‑udvikling", "Løsningsarkitektur", "UX/UI‑design", "Full stack‑produktudvikling", "Platform- & løsningsarkitektur", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/an-app-that-ate-41-mb-a-second-stopped/", "url": "https://engineer.company/da/portfolio/an-app-that-ate-41-mb-a-second-stopped/", "title": "En app, der åd 41 MB i sekundet, standset", "summary": "Hukommelsen holder sig flad under vedvarende belastning, og den samme session, der frembragte 610.996 loglinjer, frembringer 1.411.", "content_html": "<p><strong>Situation.</strong> Programmet fyldte hukommelsen, indtil styresystemet slog det ihjel. Den registrerede hændelse nåede op på 111 GB komprimerede sider på en maskine med 36 GB, stigende med omkring 41 MB i sekundet, og logfilen for en enkelt kort session rummede 610.996 linjer. En maskine i den tilstand er ikke langsom, den er ubrugelig — nedlukningen kommer, efter at swap allerede har fået alt andet på skrivebordet til at holde op med at svare.</p>\n<p><strong>Opgave.</strong> Væksten skulle findes og ikke gættes på, og hver ubegrænset vej skulle gives en grænse, for én afgrænset kø ved siden af tre uafgrænsede er ikke en rettelse.</p>\n<p><strong>Handling.</strong> Der var tre årsager, der gangede sig op med hinanden, og det er grunden til, at det gik så hurtigt. Hændelsesstrømmen fra kernen blev forbrugt uden nogen grænse, så hændelser ankom hurtigere, end grænsefladen kunne anvende dem, og efterslæbet blev gemt i stedet for kasseret. Hver abonnent modtog hver hændelse og filtrerede bagefter, så omkostningen ved én hændelse blev ganget med antallet af lyttere, og hver lytters filtrering allokerede. Og logningen havde intet budget, så hver hændelse gav loglinjer — og det er det led, der forstærker resten, for logningens omfang voksede i takt med omfanget af det, der gik galt. Rettelsen tog fat på alle tre: afgrænsede buffere med en udtrykkelig politik for, hvad der sker, når de er fulde, abonnement efter hændelsestype, så en lytter kun vækkes af de hændelser, den vil have, og et hastighedsbudget for logningen, der slår gentagelser sammen i stedet for at skrive hver enkelt. En regressionstest driver en høj hændelseshastighed og efterprøver, at hukommelsesloftet holder, så grænsen er en egenskab ved buildet og ikke en kommentar.</p>\n<p><strong>Resultat.</strong> Hukommelsen holder sig flad under vedvarende belastning, og den samme session, der frembragte 610.996 loglinjer, frembringer 1.411. Den ærlige bemærkning er, at hastighedsbudgettet for logningen kasserer information: når noget går galt hurtigt nu, er optegnelsen over det bevidst ufuldstændig, og det er en afvejning, der er truffet med åbne øjne, over for alternativet: en maskine, der går i stå.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Driftssikkerhed & backup", "Frontend‑udvikling", "Løsningsarkitektur", "Optimering af ydeevne", "Overvågning & observerbarhed", "Test & QA", "Platform- & løsningsarkitektur", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/adopted-swift-6-strict-concurrency-over-c/", "url": "https://engineer.company/da/portfolio/adopted-swift-6-strict-concurrency-over-c/", "title": "Har indført Swift 6 strict concurrency over C", "summary": "Programmet kompilerer under fuldstændigt strengt samtidighedstjek uden undertrykkelser, og hændelsesrækkefølgen er en strukturel egenskab og ikke et håb.", "content_html": "<p><strong>Situation.</strong> Swift 6’s fuldstændige strenge samtidighedstjek gør datakapløb til kompileringsfejl i stedet for lejlighedsvise nedbrud. Det er, når det skal indføres op mod et C‑bibliotek, at det bliver svært: kernens hændelsesløkke er et blokerende kald, der skal køre uden for hovedtråden for evigt, og de værdier, den leverer tilbage, er pointere uden nogen som helst samtidighedsgarantier. Oversætteren kan ikke ræsonnere om noget af det og vil afvise alt, indtil grænsen er beskrevet udtrykkeligt.</p>\n<p><strong>Opgave.</strong> Fuldstændigt tjek skulle slås til uden nogen nødudgange, hvilket betød at udforme overgangen fra en blokerende C‑løkke til hovedaktøren i stedet for at annotere sig uden om den.</p>\n<p><strong>Handling.</strong> Den nærliggende fremgangsmåde er en aktør pr. delsystem, og den blev forkastet på grundlag af måling og ikke smag. At starte en opgave pr. indkommende hændelse lader runtime‑miljøet planlægge dem i vilkårlig rækkefølge, og kernens hændelsesstrøm er ordnet — en besked‑ændret‑hændelse, der overhaler den besked‑oprettet‑hændelse, den henviser til, frembringer en grænseflade, der viser en redigering af noget, der endnu ikke findes. At aktører er reentrante, gør dette værre, ikke bedre, for en aktør kan suspendere midt i en metode og behandle et andet kald. Det, der erstattede den, er bevidst enkelt: én producenttråd, der ejer den blokerende løkke, én forbruger, én kø imellem dem og et enkelt hop over på hovedaktøren til sidst. Rækkefølgen bevares, fordi der er præcis én vej, og intet overhaler noget. De usikre typer, der krydser den grænse, er pakket ind i typer, hvis trådsikkerhed hævdes ved indpakningen i stedet for at blive taget for givet, og påstanden er dokumenteret med, hvorfor den holder — pointeren ejes af én tråd og kopieres, før den overdrages.</p>\n<p><strong>Resultat.</strong> Programmet kompilerer under fuldstændigt strengt samtidighedstjek uden undertrykkelser, og hændelsesrækkefølgen er en strukturel egenskab og ikke et håb. Prisen er, at designet er mindre parallelt, end den kunne være: alt ledes gennem én forbruger, og hvis den forbruger nogensinde bliver en flaskehals, vil rettelsen kræve, at man på ny udleder, hvilke hændelser der trygt kan omordnes, hvilket er præcis den analyse, dette undgik.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Backend‑udvikling", "Driftssikkerhed & backup", "Frontend‑udvikling", "Løsningsarkitektur", "Optimering af ydeevne", "Test & QA", "Backend- & API‑udvikling", "Platform- & løsningsarkitektur", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/wrote-a-parser-verifying-every-ffi-call/", "url": "https://engineer.company/da/portfolio/wrote-a-parser-verifying-every-ffi-call/", "title": "Har skrevet en parser, der tjekker hvert FFI‑kald", "summary": "Den defektklasse, der frembragte det oprindelige nedbrud, kan ikke gentage sig, for en forældet henvisning er nu en buildfejl og ikke en kørselsfejl.", "content_html": "<p><strong>Situation.</strong> Tidligt i projektet blev C‑grænsefladen repræsenteret af en håndskrevet header, der beskrev de funktioner, programmet forventede. Den header kunne kompileres, programmet blev linket, og kald til tre funktioner, der ikke længere fandtes i kernen, nåede frem til at blive kaldt og brød ned. Både oversætteren og linkeren havde ladet sig nøje med en beskrivelse af biblioteket og ikke selve biblioteket, og kløften viste sig først under kørsel.</p>\n<p><strong>Opgave.</strong> Den rigtige header — 7.308 linjer og 268 erklæringer — skulle være den gældende autoritet, og hver anvendelse af den i Swift‑koden skulle efterprøves mod den automatisk og ikke ved gennemgang.</p>\n<p><strong>Handling.</strong> Tjekket er en parser, der læser den faktiske upstream‑header og opbygger mængden af funktioner, enum‑konstanter og typer, den erklærer, og derefter læser Swift‑kildekoden og slår hvert kaldested og hver konstanthenvisning op i den mængde. Et kald til en funktion, headeren ikke erklærer, får buildet til at fejle. En henvisning til en enum‑konstant, der er blevet omdøbt, får buildet til at fejle. Lige nu henvises der til 132 af de 268 erklæringer, og at vide, hvilke 136 der ikke bruges, er i sig selv nyttigt, for det siger præcis, hvor meget af kernen klienten ikke har nået. Parseren håndhæver også en regel, oversætteren ikke kan: hver Swift‑klasse, der ejer en pointer ind i kernen, skal være final. En klasse, der ejer en pointer og ikke er final, kan nedarves, og en underklasse, der overstyrer deinitialiseringen eller tilføjer sin egen levetid, ændrer, hvornår pointeren frigives — en use‑after‑free uden noget unsafe‑nøgleord i nærheden. Reglen tjekkes ud fra navn i hele træet.</p>\n<p><strong>Resultat.</strong> Den defektklasse, der frembragte det oprindelige nedbrud, kan ikke gentage sig, for en forældet henvisning er nu en buildfejl og ikke en kørselsfejl. Begrænsningen er, at parseren forstår headerens erklæringer og ikke dens betydning: den beviser, at en funktion findes med et matchende navn, og en funktion, hvis betydning eller ejerskabsregel ændrede sig upstream, mens signaturen blev bevaret, slipper igennem uden bemærkning.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Automatisering & CI/CD", "Backend‑udvikling", "Dokumentation", "Sikkerhed", "Test & QA", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Teknisk dokumentation" ] }, { "id": "https://engineer.company/da/portfolio/took-the-test-suite-to-8-9-seconds/", "url": "https://engineer.company/da/portfolio/took-the-test-suite-to-8-9-seconds/", "title": "Har fået testsuiten ned på 8,9 sekunder", "summary": "Suiten gik fra seks test over tres sekunder — 74 sekunders vægurstid — til 135 test bestået på 8,9, hvilket bringer den inden for det vindue, hvor den kører,…", "content_html": "<p><strong>Situation.</strong> Testsuiten havde en tidsgrænse på tres sekunder, og seks test lå over den. En testsuite, der tager over et minut, holder op med at blive kørt før hver ændring, og en suite, der ikke køres før hver ændring, er en rapport om fortiden. Instinktet i den situation er at hæve grænsen, og det er sådan, en suite ender på ti minutter.</p>\n<p><strong>Opgave.</strong> Tiden skulle findes og ikke bare budgetteres ind, hvilket betød at profilere suiten i stedet for at ræsonnere om, hvilke test der så dyre ud.</p>\n<p><strong>Handling.</strong> Profilen blev taget på hovedtråden, mens suiten kørte, og resultatet modsagde gættet. Hovedtråden sad inde i to kald i 3.989 stikprøver ud af 4.017 — så de langsomme test var ikke dem, der udførte mest arbejde, og næsten hver eneste test kom gennem de samme to steder. Det første var en fast ventetid, der skulle lade asynkront arbejde falde til ro før efterprøvningen — reelt en sleep, betalt af hver test, der rørte hændelsesvejen, uanset om arbejdet allerede var færdigt. Den blev erstattet af en ventetid på den faktiske betingelse med en timeout, så en test, der er klar på fem millisekunder, tager fem millisekunder, og kun en test, der reelt sidder fast, betaler den fulde ventetid. Det andet var opsætning pr. test, der genopbyggede en dyr fixture hver gang, selvom fixturen var skrivebeskyttet og kunne bygges én gang for suiten. Ingen af dem lå i en test, nogen ville have udpeget som langsom; begge lå i den fælles vej, hvilket er grunden til, at hele suiten var ensartet langsom og ikke bare havde nogle få afvigere.</p>\n<p><strong>Resultat.</strong> Suiten gik fra seks test over tres sekunder — 74 sekunders vægurstid — til 135 test bestået på 8,9, hvilket bringer den inden for det vindue, hvor den kører, hver gang en fil gemmes. Forbeholdet er, at den fælles skrivebeskyttede fixture nu er et koblingspunkt — en test, der ændrer den, vil frembringe en fejl i en anden test, og suitens hastighed afhænger af en disciplin, oversætteren ikke håndhæver.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "DevOps", "Optimering af ydeevne", "Test & QA", "DevOps & CI/CD‑automatisering", "Teknisk ledelse & rådgivning" ] }, { "id": "https://engineer.company/da/portfolio/named-all-17-icons-for-screen-readers/", "url": "https://engineer.company/da/portfolio/named-all-17-icons-for-screen-readers/", "title": "Har navngivet alle 17 ikoner til skærmlæsere", "summary": "Alle sytten kontroller annoncerer deres funktion, og en attende kan ikke tilføjes uden en. Reglens unøjagtighed er reel og står dér, hvor reglen er defineret:…", "content_html": "<p><strong>Situation.</strong> Grænsefladen brugte systemsymboler til sine kontroller, og et symbol uden en tilgængelighedsetiket læses op af skærmlæseren med sit eget interne navn. Send‑knappen blev annonceret som “pil op cirkel, knap”. Det samme gjaldt alle de andre kontroller med kun et ikon i programmet, hver på sin måde — sytten af dem beskrev deres eget billede i stedet for deres funktion.</p>\n<p><strong>Opgave.</strong> Hver kontrol med kun et ikon havde brug for en etiket, der beskriver, hvad den gør, og rettelsen skulle følges af et tjek, for det næste ikon, der blev tilføjet, ville ellers genindføre defekten med det samme.</p>\n<p><strong>Handling.</strong> Hver af de sytten fik en etiket, der angiver handlingen og ikke formen, og hvor kontrollens betydning afhænger af tilstand, følger etiketten tilstanden i stedet for at være fast. Tjekket er den del, der er værd at beskrive, for en generel regel af typen “er dette tilgængeligt” findes ikke til det formål. Linteren leder efter den konstruktion, der skaber en kontrol med kun et ikon, og kræver derefter en tilgængelighedsetiket højst otte linjer fra den. Otte linjer er en bevidst grov tommelfingerregel, og den blev valgt ved måling: den er lang nok til at spænde over hver legitim måde, kodebasen skriver en af disse kontroller med dens modifikatorer, og kort nok til, at en etiket, der er knyttet til en anden visning længere nede, ikke ved et uheld opfylder den. En præcis regel her ville skulle forstå SwiftUI’s modifikatorkæder som et træ, og den billige nærhedsregel fanger den faktiske fejl — som ikke er en kontrol med en forkert etiket, men en kontrol helt uden etiket.</p>\n<p><strong>Resultat.</strong> Alle sytten kontroller annoncerer deres funktion, og en attende kan ikke tilføjes uden en. Reglens unøjagtighed er reel og står dér, hvor reglen er defineret: den kan opfyldes af en etiket på en nabovisning, så den beviser, at en etiket findes i nærheden, men ikke, at etiketten er korrekt, og korrekthed kræver stadig, at nogen lytter til den.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Dokumentation", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/built-a-48-token-macos-design-system/", "url": "https://engineer.company/da/portfolio/built-a-48-token-macos-design-system/", "title": "Har bygget et macOS‑designsystem med 48 tokens", "summary": "Otteogfyrre tokens beskriver hele grænsefladen, udseendet følger systemet, og ingen af de tre former for smuldren kan committes.", "content_html": "<p><strong>Situation.</strong> En grænseflade bygget ved at tilføje visninger ophober værdier: en hjørneradius her, en skriftstørrelse på fjorten punkter der, en animation på to tiendedele sekund et andet sted. Hver for sig er de rimelige, og tilsammen udgør de et design, der ikke kan ændres, for der findes ikke noget, der hedder “hjørneradiussen”, som man kan ændre — der er fyrre af dem, en smule forskellige, spredt over halvtreds filer.</p>\n<p><strong>Opgave.</strong> Det visuelle ordforråd skulle skæres ned til et navngivet sæt, der bygger på platformens eget materiale i stedet for at genopfinde det, og forsvares af et tjek, så det forbliver nedskåret.</p>\n<p><strong>Handling.</strong> Systemet er otteogfyrre tokens i seks grupper: mellemrum, typografi, farve, radius, ophøjning og bevægelse. Farve og materiale er bygget på platformens semantiske farver og dens glasmateriale i stedet for faste værdier, hvilket er det, der får grænsefladen til at følge systemets udseende, accentfarven og kontrastindstillingerne uden nogen kode, der holder øje med dem. Typografien er knyttet til platformens tekststile, så den skalerer med brugerens valgte tekststørrelse i stedet for at fastlåse en punktværdi. Linteren afviser tre ting: en fast talværdi, hvor et mellemrums- eller radiustoken hører hjemme, en hårdkodet skriftstørrelse hvor som helst og en animation, der er erklæret uden at respektere præferencen for reduceret bevægelse. Den tredje er den, der ellers ville smuldre hurtigst, for en animation tilføjes under finpudsningen, og præferencetjekket er den del, der bliver sprunget over — så reglen gør selve animationen umulig at skrive uden tjekket i stedet for at bede nogen om at huske det.</p>\n<p><strong>Resultat.</strong> Otteogfyrre tokens beskriver hele grænsefladen, udseendet følger systemet, og ingen af de tre former for smuldren kan committes. Begrænsningen er, at et tokensystem styrer ensartethed og ikke kvalitet — alt passer nu sammen, og at passe sammen er ikke det samme som at være veludformet, hvilket er en vurdering, ingen linter i dette projekt foretager.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Designsystemer & UI", "Frontend‑udvikling", "Test & QA", "UX/UI‑design", "Brand, marketing & SEO", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/reached-the-unused-half-of-the-core/", "url": "https://engineer.company/da/portfolio/reached-the-unused-half-of-the-core/", "title": "Har taget den ubrugte halvdel af kernen i brug", "summary": "Den halvdel af kernen, der tidligere ikke blev brugt, afprøves nu af test mod det rigtige bibliotek, og indpakningen har den højeste minimumsgrænse i…", "content_html": "<p><strong>Situation.</strong> Klienten brugte omtrent halvdelen af det, beskedkernen tilbyder. Den ubrugte halvdel var ikke obskur — sikkerhedskopioverførsel mellem enheder, forsvindende beskeder, redigering og gensendelse af beskeder, verificerede indbydelseslinks, proxykonfiguration og krypteringspolitikken for en samtale. Hver af dem er en funktion, en bruger ville forvente, og hver af dem var et utestet område af C‑grænsefladen, hvilket er den farligere kendsgerning, for et utestet område af en C‑grænseflade er der, hvor ejerskabsfejlene bor.</p>\n<p><strong>Opgave.</strong> Den uudnyttede funktionalitet skulle tages i brug og dækkes af test, og testene skulle køre mod det rigtige bibliotek og ikke mod en erstatning.</p>\n<p><strong>Handling.</strong> Reglen, der blev vedtaget, var ingen mocks for kernen. En mock af en C‑grænseflade indkoder udviklerens antagelse om, hvad biblioteket gør, og hver defekt, der er værd at finde her, er et sted, hvor den antagelse er forkert — så når en test baseret på mocks består, beviser det noget om mocken. I stedet opretter testene rigtige konti i midlertidige mapper, kalder det rigtige bibliotek og tjekker, hvad det reelt returnerer, og hver suite rydder op efter sin egen tilstand. Det var det, der gjorde dækningen meningsfuld: at teste sikkerhedskopioverførsel betød at håndtere tilstandsmaskinen i en rigtig overførsel og dens fejlveje, og at teste verificerede indbydelser betød at konstruere det rigtige linkformat og få biblioteket til at fortolke det. Minimumsgrænser for dækning blev sat pr. lag i stedet for som ét tal, med fireogfirs procent for kerneindpakningen, femoghalvfjerds for hjælpefunktioner og femogtres for modeller, ud fra den betragtning, at det lag, der rører rå pointere, bør holdes højest, og at en model, der mest består af lagrede egenskaber, ikke bør polstres med test for at nå et gennemsnit.</p>\n<p><strong>Resultat.</strong> Den halvdel af kernen, der tidligere ikke blev brugt, afprøves nu af test mod det rigtige bibliotek, og indpakningen har den højeste minimumsgrænse i projektet. Omkostningen er hastighed og forudsigelighed: test mod det rigtige bibliotek er langsommere end mocks, og de kan fejle af årsager i miljøet, hvilket er prisen for, at de kan fejle af reelle årsager.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "API'er & integration", "Automatisering & CI/CD", "Backend‑udvikling", "Driftssikkerhed & backup", "Sikkerhed", "Test & QA", "Backend- & API‑udvikling", "DevOps & CI/CD‑automatisering", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/rewrote-error-messages-to-stop-blaming-users/", "url": "https://engineer.company/da/portfolio/rewrote-error-messages-to-stop-blaming-users/", "title": "Har renset fejlbeskederne for bebrejdelser", "summary": "Fejlfladen følger en nedskrevet standard, og det tilfælde, der foranledigede den, er dækket af en test, der fejler på den gamle tekst.", "content_html": "<p><strong>Situation.</strong> Et login hos en stor mailudbyder mislykkedes, og programmet fortalte brugeren, at adgangskoden var forkert. Den var ikke forkert. Den udbyder kræver en programspecifik adgangskode til tredjepartsklienter og afviser kontoens adgangskode, uanset hvor omhyggeligt den tastes. Beskeden sendte brugeren ud i at genindtaste noget, der aldrig kunne virke, og den reelle anvisning — gå ind og opret en anden slags adgangskode — stod ingen steder.</p>\n<p><strong>Opgave.</strong> Fejlbeskederne skulle omskrives efter en skriftlig standard i stedet for at blive lappet én ad gangen, for denne var det synlige eksempel på en vane, der gik igen i dem alle.</p>\n<p><strong>Handling.</strong> Standarden har tre krav: sig, hvad der skete, antyd aldrig, at brugeren gjorde noget forkert, når årsagen ligger et andet sted, og angiv næste skridt, når der findes et. Målt mod standarden overtrådte de fleste beskeder i fejlfladen mindst ét krav — flere var det underliggende biblioteks fejlstreng sendt direkte videre, og den beskriver en tilstand for en programmør og ikke en situation for et menneske. Beskeden for den udbyder blev omskrevet, så den nævner udbyderen, oplyser, at den kræver en programspecifik adgangskode til andre klienter, og siger, hvor man opretter en. Testen er det, der forhindrer tilbagefald, og den er bevidst konkret: den fremkalder et mislykket login hos den udbyder og tjekker, at beskeden indeholder udbyderens navn og den formulering, der beskriver den krævede type adgangsoplysninger. En test, der kun tjekkede, at der viste sig en fejl, ville bestå med den oprindelige forkerte besked, så assertionen gælder indholdet, som er den eneste del, der nogensinde var i stykker.</p>\n<p><strong>Resultat.</strong> Fejlfladen følger en nedskrevet standard, og det tilfælde, der foranledigede den, er dækket af en test, der fejler på den gamle tekst. Det, der forbliver uafklaret, er omfanget: standarden håndhæves ved gennemgang og ved én test på én besked, og de øvrige udbydere med deres egne særlige krav har ingen tilsvarende test, så fejlklassen er dokumenteret, men ikke lukket.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Dokumentation", "Frontend‑udvikling", "Produkt & krav", "Sikkerhed", "Test & QA", "UX/UI‑design", "Produktstrategi & kravspecifikation", "Teknisk dokumentation", "UI/UX‑design & designsystemer" ] }, { "id": "https://engineer.company/da/portfolio/know-the-licence-of-everything-you-ship/", "url": "https://engineer.company/da/portfolio/know-the-licence-of-everything-you-ship/", "title": "Kend licensen på alt, hvad I sender ud", "summary": "Distributionsstatus tjekkes ved hvert build, og man ved, at tjekket virker, fordi det blev provokeret til at fejle, og ikke fordi det aldrig har sagt noget.", "content_html": "<p><strong>Situation.</strong> At linke en Rust‑kerne ind i et program, der skal leveres, betyder at levere alt, hvad den kerne afhænger af. Afhængighedsgrafen er 644 crates. Hver af dem har en licens, nogle har mere end én, og en enkelt copyleft‑crate, der dukker op tre niveauer nede via en rutinemæssig versionsopdatering, ændrer, hvilke vilkår programmet som helhed må distribueres under — i stilhed, i en låsefil, som ingen læser linje for linje.</p>\n<p><strong>Opgave.</strong> Licensstatus skulle verificeres ved hvert build i stedet for at blive gennemgået én gang, med en eksplicit tilladelsesliste, så en ændring i grafen giver en buildfejl og ikke bliver en opdagelse, som en anden gør senere.</p>\n<p><strong>Handling.</strong> Gennemgangen udleder hele den transitive graf og tjekker hver crates licensudtryk mod en liste over vilkår, projektet accepterer, og evaluerer de booleske udtryk korrekt — en crate, der tilbyder et valg mellem to licenser, er acceptabel, hvis en af dem står på listen, og en crate, der kræver begge, er kun acceptabel, hvis begge står på listen. Alt uden match får buildet til at fejle i stedet for at give en advarsel, og at føje et vilkår til tilladelseslisten er en bevidst redigering med en begrundelse. Et tjek, der aldrig har fejlet, kan ikke skelnes fra et, der ikke kan fejle, så det blev bevidst provokeret til at fejle, to gange. Én gang ved at omskrive en crates licensudtryk til noget, listen ikke accepterer, hvilket svarer til en crate, der ændrer sine vilkår upstream mellem versioner — det tilfælde, som ingen menneskelig gennemgang fanger, fordi selve craten ikke er ny. Én gang ved at flytte den fastlåste kernerevision uden at generere gennemgangen igen, hvilket svarer til en kerneopdatering, der i stilhed trækker en ny afhængighed med sig. Begge provokationer fik buildet til at fejle som tilsigtet, og tjekket blev beholdt, som det var, i stedet for at blive gjort bredere.</p>\n<p><strong>Resultat.</strong> Distributionsstatus tjekkes ved hvert build, og man ved, at tjekket virker, fordi det blev provokeret til at fejle, og ikke fordi det aldrig har sagt noget. Før det fandtes, fremsatte pakken, licensfilen og projektets eget README alle en påstand om 644 crates, som intet havde verificeret. Begrænsningen er præcis og bør ikke overdrives: tjekket læser de erklærede licensmetadata, og metadata kan være forkerte eller ufuldstændige. Det beviser intet om en crate, der angiver sin licens forkert, og det er ikke juridisk rådgivning.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data governance", "DevOps", "Dokumentation", "Sikkerhed", "Data governance & datakvalitet", "DevOps & CI/CD‑automatisering", "Sikkerhed & adgangsstyring" ] }, { "id": "https://engineer.company/da/portfolio/built-the-visual-identity-from-2-drawings/", "url": "https://engineer.company/da/portfolio/built-the-visual-identity-from-2-drawings/", "title": "Har skabt den visuelle identitet fra 2 tegninger", "summary": "Én tegning danner nu grundlag for hvert udgivet ikon, faneikonet medregnet, en ny generering er én kommando, og en forældet fil får buildet til at fejle.", "content_html": "<p><strong>Situation.</strong> Et lille firmas visuelle identitet købes som regel, genereres maskinelt eller sættes sammen af arkivmateriale, og resultatet er en identitet, der ikke tilhører nogen. Det andet problem er værre: håndlavet grafik, der kun findes som eksporterede filer, hvor kildetegningen er gået tabt, eksporterne redigeres direkte, og inden for et år afviger de versioner, der er i brug, fra hinanden, og der er ingen original tilbage, der kan afgøre det.</p>\n<p><strong>Opgave.</strong> Identiteten skulle tegnes i stedet for at blive anskaffet, og kæden fra tegning til udgivet fil skulle kunne gentages, så hver fil på sitet kan udledes og ikke bare opbevares.</p>\n<p><strong>Handling.</strong> Det genererede billedmateriale begyndte med to håndlavede tegninger: firmamærket, en rastertegning, og et tandhjul i vektorgrafik, tegnet til browserfanen ud fra den tanke, at en kraftig silhuet holder ved 16 pixels. Gengivet side om side ved 16, 32 og 48 pixels på lyse og mørke fanebjælker forblev mærket genkendeligt, mens tandhjulet blev utydeligt, så faneikonet gik over til mærket, og hvert ikon, sitet udgiver, genereres nu ud fra den ene tegning. Ud fra den genererer scripts hver afledt form: favicon‑sættet i de krævede størrelser, touch‑ikonerne og opstartsbillederne i hver enhedsstørrelse. Genereringen er en opgave, enhver kan køre, så spørgsmålet “hvor kommer denne fil fra” har en kommando som svar. Forældelsestjekket er det stykke, der får det til at holde: det sammenligner commit‑datoen for hver afledt fil med kildens og fejler, når en afledt fil blev committet før sin kilde, hvilket fanger netop den fejl, dette design findes for at forhindre — nogen redigerer kilden, glemmer at generere igen, og det udgivne site bliver ved med at vise grafik, der ikke længere svarer til originalen. Reglen om, at afledte filer aldrig redigeres i hånden, står skrevet dér, hvor kilderne ligger.</p>\n<p><strong>Resultat.</strong> Én tegning danner nu grundlag for hvert udgivet ikon, faneikonet medregnet, en ny generering er én kommando, og en forældet fil får buildet til at fejle. Afvejningen er en hård afhængighed af værktøjskæden: de afledte filer er committet, så sitet kan bygges hvor som helst, men en ændring af identiteten kræver, at genereringsværktøjerne stadig virker, og et kildeformat, der holder op med at kunne læses, tager hele identiteten med sig.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Brand & marketing", "Designsystemer & UI", "Test & QA", "UX/UI‑design", "Webudvikling", "Brand, marketing & SEO", "UI/UX‑design & designsystemer", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/portfolio/measured-13-addresses-with-no-tracking/", "url": "https://engineer.company/da/portfolio/measured-13-addresses-with-no-tracking/", "title": "Har målt 13 adresser uden tracking", "summary": "Tretten adresser måles på den måde, og sitet sender ingen cookie, ingen tracker og ingen tredjepartsforespørgsel, så der er intet at give samtykke til og intet…", "content_html": "<p><strong>Situation.</strong> Sitet kunne ikke vide, om nogen læste det. Ethvert almindeligt svar på det koster læseren noget — en cookie, et script, en pixel, en hostet tjeneste, der lærer noget om en besøgende i forbifarten — og hvert af dem blev afvist, før den første linje af arbejdet blev skrevet. Tilbage var webserverens egen adgangslog, den ene optegnelse, der findes under alle omstændigheder, fordi en forespørgsel skal besvares.</p>\n<p><strong>Opgave.</strong> Spørgsmålet var, om en brugbar optegnelse over læserskaren kan bygges ud fra en loglinje, der allerede har fået de identificerende dele fjernet, og om fjernelsen kan bevises og ikke bare loves.</p>\n<p><strong>Handling.</strong> Maskeringen sker i det øjeblik, linjen skrives, og ikke i en oprydning bagefter, for et løfte om ikke at gemme noget brydes ved at gemme det og rydde op senere. Serveren udleder klientadressen under to navne, og begge maskeres — at maskere det ene ser korrekt ud i konfigurationen, mens den fulde adresse ligger på disk under det andet, hvilket kun en live‑probe fandt. Syv headere med videresendte adresser fjernes helt, eftersom encoderen skriver headerne præcis, som klienten sendte dem, og en besøgende bag en virksomhedsproxy ellers ville tage sin egen fulde adresse med ind i filen under et navn, som ingen maske nogensinde kiggede på. Cookie‑headeren, autorisations‑headeren og den flygtige kildeport går samme vej, og query‑strenge klippes, før noget som helst læser stien, så hverken de ord, nogen skrev i en søgemaskine, eller de ord, de skrev i sitets eget søgefelt, kan nå nogen fil. Indsamlingen er et pull: et script, der kun bruger standardbiblioteket, streamer den aktive log og dens rullede søskende på hosten og udskriver optællinger i stedet for rækker, afgrænset af antallet af forskellige stier og ikke af logens størrelse, fordi maskinen har 464 MB. Opbevaringen krævede to mekanismer og ikke én, efter at et størrelsesloft og et tidsloft på samme hylde viste sig at betyde, at det mindste i stilhed vinder — et travlt site beholdt ti dage i stedet for tredive, og et stille slettede slet ikke noget. Det, der overlever loggen, er et arkiv med 21 optællinger om dagen og 20 facetter om måneden, der flettes i stedet for at blive tilføjet, ud fra reglen om, at et vindue kun kan observere for lidt, og med totalen gemt ved siden af hver rangering, så en afkortet tabel selv oplyser, hvor meget den har udeladt.</p>\n<p><strong>Resultat.</strong> Tretten adresser måles på den måde, og sitet sender ingen cookie, ingen tracker og ingen tredjepartsforespørgsel, så der er intet at give samtykke til og intet for et tag at køre i. Designet afviser mere, end det svarer på — unikke besøgende, søgeord, en besøgendes vej gennem sitet, tid på siden, klik, geografi og enhed kan ikke besvares her, og det står der også — og grænserne står ved siden af tallene i stedet for med småt, fordi et groft tal, der læses som et præcist, er værre end intet tal.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Automatisering & CI/CD", "Data governance", "Infrastruktur", "Overvågning & observerbarhed", "Python", "Sikkerhed", "Data governance & datakvalitet", "Sikkerhed & adgangsstyring", "Site reliability & overvågning" ] }, { "id": "https://engineer.company/da/portfolio/published-a-readable-tor-onion-mirror/", "url": "https://engineer.company/da/portfolio/published-a-readable-tor-onion-mirror/", "title": "Har udgivet et læsbart Tor‑onion‑spejl", "summary": "Sitet har fire hoveddøre til ét build, og denne er den eneste, der aldrig tælles. Gemini-forespørgsler og Gopher-forbindelser tælles pr.", "content_html": "<p><strong>Situation.</strong> Sitet svarede allerede på tre protokoller ud fra ét build. En læser, der havde brug for at nå det uden at afsløre, at vedkommende havde nået det, lavede stadig en rundtur via en exit‑node til en almindelig adresse, hvilket er en svagere løsning end en særskilt hoveddør. Den oplagte indvending mod en fjerde dør er firewallen, og den gælder ikke: anonymitetsdæmonen når nettet ved at oprette udgående kredsløb, så der kommer ingen indgående forbindelser, ingen port åbnes, og ingen af de to firewall‑lag har noget at synkronisere.</p>\n<p><strong>Opgave.</strong> Spejlet skulle levere de samme sider som den almindelige adresse, udgives, hvor de andre spejle udgives, have en adresse, som et menneske kan læse højt, i stedet for en tilfældig streng og aldrig lade den private nøgle, der udgør adressen, røre dette repository eller en laptop.</p>\n<p><strong>Handling.</strong> Rollen installerer dæmonen fra distributionens egen pakke, skriver en konfiguration med den udgående proxyport slået fra og nægter at overskrive en identitet, den ikke selv har genereret — så en nøgle, der er lagt på plads i hånden, bliver simpelthen brugt. En læsbar adresse kan ikke vælges, kun findes: adressen er den offentlige nøgle i base32, så et præfiks købes ved at generere nøglepar, indtil et af dem tilfældigvis koder de ønskede bogstaver, og hvert tegn koster toogtredive gange så meget som det forrige. Det præfiks, der blev købt, læses som virksomhedens eget navn. Adressen er en erklæret værdi i inventaret, og hver konvergering tjekker, at hostens faktiske identitetsfil matcher den, så en udskiftet nøgle med en forældet erklæring får kørslen til at fejle i stedet for at passere i stilhed. Nøglen fanges af begge hemmelighedskontroller, efter at en måling viste, at det oplagte mønster for en nøglefil matcher en deploy‑nøgle, men misser denne. At udfase den tilfældige adresse, som spejlet var udgivet med, var en trinvis omlægning og ikke et enkelt skift, så den udgivne adresse blev ved med at virke, mens den læsbare blev udgivet. Det trin, der lignede det sidste, var det ikke: nøglen landede korrekt, og konvergeringen meldte ingen ændring, fordi intet endnu henviste til den nye mappe, så den nye adresse blev slet ikke udgivet — rollen fejler nu på en identitet på disk, som ingen tjeneste henviser til.</p>\n<p><strong>Resultat.</strong> Sitet har fire hoveddøre til ét build, og denne er den eneste, der aldrig tælles. Gemini‑forespørgsler og Gopher‑forbindelser tælles pr. dag uden adresse og uden sti; onion‑adressen har ingen tæller og intet flag til at tilføje en, for at tælle en læser dér er netop det ene, den protokol findes for at forhindre, og blindheden er prisen for den holdning og ikke et hul i rapporteringen. Spejlet betalte sig også som en ekstra vurdering: dets første læsere sad på en anden browsermotor, som ikke implementerer den scroll‑drevne animation, footeren byggede på, så hver af dem fik kolofonen malet hen over forsiden. Det var en reel fejl på det almindelige site, fundet af det publikum, der ikke havde nogen anden måde at melde den på.</p>\n", "date_published": "2026-10-20T23:59:50+02:00", "date_modified": "2026-10-20T23:59:50+02:00", "language": "da", "tags": [ "Infrastruktur", "Linux & servere", "Sikkerhed", "Systemadministration", "Webudvikling", "Infrastructure as Code", "Sikkerhed & adgangsstyring", "Webstedsudvikling & CMS" ] }, { "id": "https://engineer.company/da/notes/optimise-before-you-shard/", "url": "https://engineer.company/da/notes/optimise-before-you-shard/", "title": "Optimér databasen, før du sharder den", "summary": "De fleste databaser, der ser ud til at skulle shardes, skal læses først. Rækkefølgen: mål, stram op, flyt læsninger, partitionér og shard til sidst.", "content_html": "<p>Når en database bliver langsom under belastning, er det første forslag som\nregel flere maskiner. Nogle gange er det rigtigt. Oftere er den langsomme del\nen håndfuld sætninger, som ingen har læst endnu, og at sprede data over flere\nservere spreder de sætninger over mere hardware uden at gøre én eneste af dem\nbilligere. Et problem, der ser uløseligt ud, ligger ofte kun én forespørgselsplan fra en\npæn løsning, og horisontal skalering vælges nogle gange netop, så ingen behøver at\nlæse\nden database, du allerede har (læs den, før du køber mere af den).</p>\n<p>Det her er rækkefølgen, vi arbejder i, før vi deler noget: kig, stram op, tag\nlæsningerne væk fra den primære, partitionér tabellerne omkring det varme punkt,\nog shard først, når én maskine er fuld af godt arbejde. Flash-salget, hvor alle\nkøbere skriver til én række, er gennemgået i\n<a href=\"/da/notes/selling-limited-stock-without-overselling/\">At sælge den sidste billet præcis én gang</a>.\nDenne note beskriver metoden omkring det, og dens sidste afsnit tager det modsatte\nforslag op: sælg fra hver server, og undskyld for de enheder, der blev solgt to\ngange.</p>\n<h2 id=\"kig-først\">Kig først</h2>\n\n<p>PostgreSQL registrerer allerede det meste af det, der er at vide. Stil den fire\nspørgsmål, før du ændrer noget.</p>\n<p><strong>Hvilke sætninger koster mest i alt?</strong> <code>pg_stat_statements</code>, en udvidelse, der\nindlæses gennem <code>shared_preload_libraries</code>, gemmer hver normaliseret sætning med\nantal kald og tid. Sortér efter total, ikke efter gennemsnit: en\nSQL-forespørgsel på 2 ms, der kører en million gange i timen, koster mere end en\nrapport på to sekunder, der kører to gange.</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>Hvad gør den øverste egentlig?</strong> <code>EXPLAIN (ANALYZE, BUFFERS)</code> kører sætningen\nog viser den plan, den valgte, de rækker, hvert trin faktisk returnerede, mod\ndem, den forventede, og hvor mange sider den læste. En scanning af en million\nrækker for at returnere ti, eller et estimat, der rammer ved siden af med en\nfaktor hundrede, er som regel hele historien. <code>ANALYZE</code> udfører det, den\nforklarer, så pak en skrivning ind i <code>BEGIN</code> og <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\">'Lock'</span><span class=\"p\">;</span><span class=\"w\">\n</span></span></span></code></pre></div><p>Rækker her i et travlt minut betyder, at transaktioner står i kø efter\nhinanden, ikke efter hardware, og en større maskine gør ikke en kø én række\nkortere.</p>\n<p><strong>Arbejder serveren, eller jonglerer den?</strong> Sammenlign antallet af aktive sessioner i\n<code>pg_stat_activity</code> med antallet af kerner. Når mange gange flere er aktive, end der er\nkerner, bruger serveren sin tid på at skifte mellem dem, og en forbindelsespulje\nløser det, før nogen hardware gør. Læs derefter <code>n_dead_tup</code> i\n<code>pg_stat_user_tables</code>: en tabel med flere døde rækker end levende bliver scannet\ngennem sit eget affald, og det er autovacuum, der halter bagefter, ikke en\ndatabase, der er for lille.</p>\n<p>Skriv ned, hvad du finder. Det er udgangspunktet, som hvert senere skridt måles\nmod, og det ærlige svar på “skal vi sharde?” står ofte allerede i det.</p>\n<h2 id=\"stram-op-i-det-der-er\">Stram op i det, der er</h2>\n\n<p>Nogenlunde i rækkefølge efter udbytte i forhold til indsats.</p>\n<p><strong>Indekser, der passer til spørgsmålene.</strong> Planerne ovenfor udpeger\nscanningerne. Et indeks på de kolonner, en varm forespørgsel filtrerer på, gør\nen scanning til et opslag, og et partielt indeks rummer kun de rækker, en\nforespørgsel kan have brug for — for eksempel enheder, der stadig er til salg — så\ndet skrumper, mens et salg skrider frem, i stedet for at 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>Hvert indeks betales også ved hver skrivning, så fjern de indekser, hvis <code>idx_scan</code> i\n<code>pg_stat_user_indexes</code> bliver ved med at stå på nul.</p>\n<p><strong>Typer, der siger, hvad værdien er.</strong> Skemaer skrevet hurtigt, af et menneske\neller en model, griber efter <code>TEXT</code>, hvor en <code>SMALLINT</code> eller en <code>BOOLEAN</code> ville\nvære nok. En <code>BOOLEAN</code> er én byte, en <code>SMALLINT</code> to, og <code>'yes'</code> gemt som tekst\ner fire. PostgreSQL indsætter desuden fyld, så hver kolonne med fast bredde\nbegynder på sin egen justeringsgrænse, og en kolonnerækkefølge valgt tilfældigt\nkoster derfor også bytes. Én lagerenhed, skrevet hurtigt:</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\">-- 'yes' eller 'no'\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\">-- 'yes' eller 'no'\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 enhed med typer, der siger, hvad værdierne 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 række 72 bytes og den anden 54, en fjerdedel\nmindre, så mere af tabellen kan ligge i hukommelsen. Typerne afviser også\nværdier som <code>'maybe'</code>, som applikationen ellers skulle forsvare sig imod, og en\n<code>CHECK</code>-begrænsning gør det samme for værdiområder:\n<code>quantity smallint CHECK (quantity > 0)</code>.</p>\n<p><strong>Færre rundture, og arbejdet der, hvor data er.</strong> Hver sætning, applikationen\nsender, koster en rundtur og en parsning. En løkke, der læser hundrede rækker og\nopdaterer dem én ad gangen, er hundrede og én rundture, hvor én mængdebaseret\nsætning ville gøre det, og en side, der henter hver enhed af en vare for at\ntælle de ledige, sender tusindvis af rækker for at beregne ét tal. Reglen, vi\nbruger: det, der skal se data, går til databasen — filtrering, optælling, en\nskrivning, der afhænger af, hvad der er der — og forretningsregler bliver i\napplikationen, fordi databasens CPU er den, det er sværest at få mere af i hele\nsystemet.</p>\n<p><strong>Opdateringer, der bliver på deres side.</strong> En række, der ændres mange gange i\nminuttet, efterlader en ny version af sig selv hver gang. Når en opdatering ikke\nændrer nogen indekseret kolonne, og siden har plads, beholder PostgreSQL den nye\nversion på samme side og rører slet ikke noget indeks: en heap-only tuple, eller\nHOT-opdatering. Hold derfor de kolonner, en hyppig opdatering ændrer, ude af\nindeksene, og efterlad plads på siderne:</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>Indstillingen gælder for sider, der skrives fra da af, så sæt den, før rækkerne\nindlæses. På tabellen ovenfor var 2.000 reservationer, der satte <code>reserved</code>,\n<code>reserved_at</code> og <code>order_id</code>, alle HOT, og af 2.000 opdateringer, der satte\n<code>sold</code>, som det partielle indeks læser, var ingen HOT. Et salg sker én gang pr.\nenhed; en reservation kan ske mange gange. <code>n_tup_hot_upd</code> mod <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-række-siger-hvem-der-kom-først\">Én række siger, hvem der kom først</h2>\n\n<p>En enhed går til den første køber, hvis sætning når den, og reservationen\nudløber på databasens ur. Den hviler på to kolonner: <code>reserved</code>, en boolean, og\n<code>reserved_at</code>, det øjeblik, den blev sat.</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\"><</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\">'10 minutes'</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 række tilbage: denne køber har den. Ingen: en anden kom først, eller den er\nsolgt. To reservationer på samme enhed står i kø ved dens rækkelås, og under READ\nCOMMITTED, PostgreSQL’s standard, tjekker den anden <code>WHERE</code>-betingelsen igen mod\nden række, den første committede, så præcis én af dem får en række tilbage. En\nreservation, der er ældre end ti minutter, tæller som ledig, så intet behøver at\nrydde den op, for at svaret er rigtigt.</p>\n<p>Det gør boolean’en alene til et ufuldstændigt svar. <code>reserved</code> forbliver sand,\nefter en reservation er udløbet, og kun de to kolonner sammen siger, om en enhed\ner optaget, så hver forespørgsel, der spørger, skal spørge til begge. En periodisk\noprydning, der nulstiller udløbne reservationer, holder tabellen ærlig for den,\nder læser <code>reserved</code> alene, men korrektheden venter aldrig på den. En\nbegrænsning holder parret 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>Billetnoten gemmer fristen, <code>held_until</code>, i stedet for starten; det er det\nsamme design. Når køberen vælger enheden — et sæde, et nummereret tryk —\nnavngiver sætningen den, som her. Når en hvilken som helst enhed er god nok,\nvælger varianten med <code>SKIP LOCKED</code> i den note en enhed, som ingen andre har\nreserveret.</p>\n<h2 id=\"læsninger-væk-fra-den-primære\">Læsninger væk fra den primære</h2>\n\n<p>Det meste af en butiks trafik er læsning: produktsiden, “3 tilbage”, kurven,\nordrehistorikken. Intet af det har brug for den primære, den ene maskine, der\nskal holdes fri til skrivninger. En streaming-replika tager de læsninger, og at\ntilføje én er et spørgsmål om konfiguration, mens en shard er et projekt.</p>\n<p>To forbehold følger med. En replika halter en smule bagefter, så en køber, der\nlige har reserveret en enhed og så læser sin kurv fra den, ser måske ikke\nreservationen: læs det, en bruger lige har skrevet, fra den primære, eller send\nden bruger til den primære i nogle sekunder efter hver skrivning. Og en lang\nrapport på en replika kan blive annulleret, når den holder de ændringer\ntilbage, replikaen afspiller. PostgreSQL-dokumentationen kalder det\nforespørgselskonflikter og nævner de indstillinger, der afvejer replikaens\nforsinkelse mod dem.</p>\n<p>Den billigste læsning er den, der aldrig når en database. Når hver enhed af en\nvare er reserveret, kan ingen købe den, før en reservation udløber eller\nfrigives, og databasen kan sige præcis, hvornå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\"><</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\">'10 minutes'</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\">'10 minutes'</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 nul, er <code>until</code> det øjeblik, den tidligste reservation udløber:\ncache “fuldt reserveret” med det som udløbstid, aldrig længere, og hver\nbesøgende indtil da bliver afvist af cachen i stedet for af databasen. Når\n<code>free</code> er nul, og <code>until</code> er tom, er hver enhed solgt, og svaret er endeligt.\nEn reservation, der frigives tidligt — et afvist kort, en forladt kurv — sletter\ncache-posten, så enheden er til salg igen med det samme.</p>\n<p>Cachen siger kun nej. Et ja kommer altid fra den betingede <code>UPDATE</code> ovenfor, så\nen forældet cache kan koste et salg i nogle sekunder og kan aldrig sælge en\nenhed to gange. Det er billetnotens cachede “udsolgt”, med en frist.</p>\n<h2 id=\"partitionér-tabellerne-omkring-den-varme-række\">Partitionér tabellerne omkring den varme række</h2>\n\n<p>Partitionering deler én tabel i flere efter en nøgle — en måned, en lejer — bag\nét navn. Det er værd at sige ligeud, hvad den ikke gør: den gør ikke én\nomstridt række hurtigere. Den række ligger stadig i én partition, på én maskine,\nbag én lås.</p>\n<p>Det, den gør, er at holde tabellerne omkring det varme punkt små. Ordrer,\nreservationshistorik og hændelser vokser uden ophør, og de fleste forespørgsler\nvil have den nyeste del:</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\">></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\">'2026-10-01'</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\">'2026-11-01'</span><span class=\"p\">);</span><span class=\"w\">\n</span></span></span></code></pre></div><p>En forespørgsel, der nævner et datointerval, læser kun partitionerne inden for\ndet, og denne måneds indekser forbliver små nok til at ligge i hukommelsen.\nOprydning holder op med at være en <code>DELETE</code> af millioner af rækker, med den\noppustning og vacuum, der følger, og bliver én sætning:</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øglen skal indeholde partitionsnøglen, og derfor er den\n<code>(id, created_at)</code>. En forespørgsel, der ikke nævner nøglen, læser hver\npartition, så partitionér efter det, forespørgslerne allerede filtrerer på.</p>\n<h2 id=\"når-sharding-er-rimeligt\">Når sharding er rimeligt</h2>\n\n<p>Der er én grænse, sharding ikke kan flytte: hver vare har én ejer. En enhed kan\nikke reserveres på to servere på samme tid, så sharding flytter hele varer,\naldrig halvdelen af én. Når en enkelt vare vokser fra sin maskine, deler\nbilletnotens spande dens lager i mindre stykker, og hvert stykke har igen\npræcis én ejer.</p>\n<p>Inden for det kalder vi sharding rimeligt, når én af disse ting er sand, og\nikke før:</p>\n<ul>\n<li><strong>Én maskine er fuld af godt arbejde.</strong> Sætningerne er allerede slanke — én\nbetinget skrivning hver, intet eksternt inde i en transaktion, opdateringer,\nder bliver på deres side — og den primæres CPU eller write-ahead-log er stadig\nmættet, på den største maskine, det er værd at købe, på tværs af mange varer\nog ikke kun én. Én varm vare er et spandeproblem, ikke et shardproblem.</li>\n<li><strong>Data er vokset fra sin vedligeholdelse.</strong> En backup kan ikke gendannes\ninden for den tid, forretningen er lovet, et indeks kan ikke bygges eller en\ntabel vacuumes i de stille timer, eller arbejdssættet passer ikke længere i\nhukommelsen.</li>\n<li><strong>Isolation er kravet.</strong> En lejer, hvis kontrakt eller tilsynsmyndighed siger,\nat deres data skal ligge for sig selv, eller hvis salg ikke må dele disk med nogen\nandens.</li>\n</ul>\n<p>Prisen lander på alt andet. Hver migrering, backup, gendannelse og rapport kører\nén gang pr. shard; en join på tværs af shards bliver applikationskode; og en\nforespørgsel, der ikke kender sin shardnøgle, spørger dem alle. Det er\nregningen, afsnittene ovenfor er der for at undgå, og når den først er betalt,\ner den svær at få tilbage.</p>\n<h2 id=\"når-en-undskyldning-er-billigere-end-en-lås\">Når en undskyldning er billigere end en lås</h2>\n\n<p>Der findes et seriøst argument for at gå den anden vej. Lad hver server sælge af\ndet lager, den kan se, acceptér, at en enhed nu og da går til to købere, og lad\nkundeservice refundere den ene af dem med en undskyldning. Forretningslogikken\nbliver konsistent med tiden (eventual consistency), bygget til de 99,9 % af\nforespørgslerne, der aldrig støder sammen, og en asynkron løkke finder\nsammenstødene bagefter og retter op på dem. Til gengæld behøver butikken aldrig\nat sige “prøv igen”. Helland og Campbell giver argumentet dets stærkeste form i\n<em>Building on Quicksand</em>: et system, der kvitterer for arbejde, før dets backup\nhar det, gør hvert løfte til en sandsynlighed, og virksomheder har altid gjort\nde løfter, de ikke kunne holde, op med en undskyldning.</p>\n<p>Vi er enige i det meste af det. Vi er uenige om, hvor det skal bruges.</p>\n<p><strong>Det meste af en butik bør være konsistent med tiden.</strong> Kataloget,\nsøgeindekset, mærket “3 tilbage”, kurven, anbefalingerne, mailene, lagerfeedet\nog rapporterne kan alle køre sekunder bagud, og at lade dem vente på den primære\nkoster fart og giver intet igen. Dynamo blev bygget til netop det: Amazons\nindkøbskurv glemmer eller afviser aldrig et “læg i kurven”, heller ikke mens\nmaskiner fejler, og prisen er, at en fletning kan bringe en vare tilbage, som\nkøberen havde fjernet. Efter reservationen er billetnotens checkout allerede\nasynkron: en saga af lokale trin, beskeder, der leveres mindst én gang, og\nforbrugere, der er idempotente, med ordren som nøgle. En afstemningsløkke hører\nhjemme i ethvert design, der taler med en betalingsudbyder, for webhooks går\ntabt.</p>\n<p><strong>Reservationen er det ene sted, hvor det ikke holder.</strong> “Sjældent” forudsætter,\nat sammenstød er sjældne, og i et flash-salg er sammenstødene salget. En server,\nder sælger ud fra sit eget billede, bliver ved med at sælge, indtil den hører,\nhvad de andre har solgt, så oversalget er omtrent forsøgsraten gange\nreplikeringsforsinkelsen. I en simpel simulering deler to regioner 2.000 forsøg\ni sekundet, hver sælger ud fra sin egen optælling og hører om den andens salg\n200 millisekunder senere. Over fem kørsler af et salg på 10.000 enheder solgte\nhver kørsel mellem 191 og 205 enheder, der ikke fandtes. Regnestykket forudsiger\n200: forsøgsraten gange forsinkelsen gange den andel af salget, der sker i den\nanden region. Med én ejer pr. vare solgte den samme model præcis det, der var på\nlager. Det sker, hver gang lageret bliver solgt ud, uanset om noget fejlede, og\ndet sker på højdepunktet, når flest købere ser med.</p>\n<p><strong>Oppetiden er mindre, end den lyder.</strong> Gilbert og Lynch beviste den\nbyttehandel, som konsistens med tiden indgår: mens netværket er delt, kan et\nsystem svare, eller det kan være konsistent, ikke begge dele. Abadis PACELC\ntilføjer den anden halvdel. Når intet er delt, står valget mellem latens og\nkonsistens, og én ejer pr. vare med slanke sætninger er allerede hurtig. Ingen\naf de to designs når 100 %: betalingsudbyderen, DNS og netværkene imellem har\nderes egne nedbrud. Det, det strenge design giver afkald på, er begrænset. Når\nejeren af én vares lager ikke kan nås, holder den vare op med at sælge i de\nsekunder, en failover tager, og billetnotens afsnit om at sætte varen på pause\nbegrænser pausen til varerne på den maskine, der fejlede. Resten af butikken\nsælger videre.</p>\n<p><strong>En undskyldning er ikke en refusion.</strong> Refusionen er den billige del. Køberen\nfik et ja og har måske allerede bestilt toget, taget fri og fortalt det til\nvennerne. At høre “udsolgt” et sekund for tidligt er en skuffelse. At høre, at\nen bekræftet ordre er trukket tilbage, er et brudt løfte. Kundeservice er\nbemandet til en almindelig dag, og oversalg kommer samlet, i årets travleste\nminut. Før du planlægger at trække bekræftede ordrer tilbage, så spørg en\njurist, hvad en bekræftelse forpligter butikken til.</p>\n<p><strong>Behold den asynkrone del, og flyt den før ja’et.</strong> Det, forslaget har ret i,\ner, at køberen ikke skal vente på en lås. Tag imod hvert klik i en holdbar,\nreplikeret log, én partition pr. vare, og fortæl køberen, at svaret er på vej.\nAt tage imod et klik afhænger så kun af loggen, og en log, der er replikeret\nover tre maskiner, bliver ved med at tage imod skrivninger, når én af dem\nfejler. Én ejer pr. vare læser loggen i rækkefølge og beslutter i hukommelsens\ntempo, og svaret går tilbage som en besked inden for sekunder. Når den ejer får\nen failover, venter forespørgslerne i loggen i stedet for at blive afvist eller\nsolgt to gange. Ja’et er en reservation, og betalingen følger efter, som i\nbilletnotens checkout, så ingen betaler for en enhed, de ikke fik. Afstemningen\nsker stadig, men før løftet i stedet for efter det.</p>\n<p><strong>Del lageret, ikke optællingen.</strong> Når forretningen har brug for, at regioner\nsælger uden at spørge hinanden, så giv hver region en kvote af lageret, og lad\nden sælge af sin egen kvote uden at spørge nogen. En region, hvis kvote slipper\nop, beder om mere, og mod slutningen af salget falder kvoterne tilbage til én\nejer. Bailis og kolleger kortlægger, hvilke regler servere kan overholde uden\nat tale sammen, og et lager, der ikke må gå under nul, er ikke en af dem. Kvoter\nfjerner ikke den samtale; de flytter den væk fra det enkelte salg, til det\nsjældne øjeblik, hvor en kvote fyldes op. Det er det design, der er konsistent\nmed tiden og alligevel holder invarianten. Dets eneste pris er halen, hvor en\nkøber bliver afvist i én region, mens en anden region stadig har lager.</p>\n<p><strong>Når oversalg er den rigtige politik.</strong> Nogle varer kan laves igen: et print on\ndemand, en T-shirt, som leverandøren stadig har, en licensnøgle. At sælge ud over\nlagertallet er så en restordre med en ærlig dato, og konsistens med tiden er det\nrigtige design. Flyselskaber overbooker med vilje. De kender deres\nudeblivelsesrater, prissætter risikoen og kompenserer efter en regel, der er\nskrevet på forhånd. Det er testen. Et oversalg er acceptabelt, når forretningen\nvalgte dets størrelse og dets afhjælpning før salget. Det er en fejl, når det\nrammer den, der tilfældigvis klikkede sidst.</p>\n<p><strong>Hvis du vælger undskyldninger, så byg dem som betalinger.</strong> Løkken, forslaget\nbeskriver, er en refusionspipeline, og den har brug for det samme som alt andet,\nder flytter penge:</p>\n<ul>\n<li><strong>Et id på hvert salg,</strong> så et nyt forsøg er det samme salg, og en journal\nover salg, som der kun tilføjes til. Lagertallet udledes af journalen og\nredigeres aldrig.</li>\n<li><strong>En regel for, hvem der beholder enheden,</strong> som du kunne forklare den køber,\nder tabte. Ordn efter loggen, ikke efter forskellige serveres ure, hvor få\nmillisekunders forskydning ville afgøre, hvem der får refusionen.</li>\n<li><strong>Kompensationer, der er idempotente og logget:</strong> annullér, frigiv eller\nrefundér, og fortæl køberen det. De er sagaens fortryd-trin, kørt af en\nmaskine.</li>\n<li><strong>En frist.</strong> Løkken skal finde hvert oversalg, før noget bliver sendt, og før\nkøberen finder det.</li>\n<li><strong>Et budget, målt.</strong> Oversalg pr. salg, mod det tal, forretningen skrev under\npå, ved siden af invarianten på samme dashboard, med en alarm, når budgettet\ner brugt.</li>\n</ul>\n<p>Vores svar er at være konsistent med tiden overalt omkring reservationen, streng\nved reservationen og asynkron i den måde, svaret når køberen på. Design efter det\nalmindelige forløb, men i et flash-salg er sammenstødet det almindelige forløb. Hvis\nforretningen vil have nogle få oversalg til gengæld for noget, så gør det til et\ntal, forretningen valgte, med en afhjælpning skrevet før salget, og mål det.</p>\n<h2 id=\"hvad-vi-ville-sige-til-den-næste\">Hvad vi ville sige til den næste</h2>\n\n<ul>\n<li><strong>Læs, før du køber.</strong> <code>pg_stat_statements</code> og én <code>EXPLAIN</code> besvarer flere\nkapacitetsspørgsmål end en ny server.</li>\n<li><strong>Lad typerne sige, hvad data er.</strong> En boolean gemt som boolean er mindre, og\nden afviser vrøvl.</li>\n<li><strong>Send databasen det, der skal se data, og intet andet.</strong> Filtrering og\noptælling hører hjemme dér. Forretningsregler gør ikke.</li>\n<li><strong>Cachen siger nej; kun databasen siger ja.</strong> Så koster en forældet cache\nsekunder, aldrig en enhed.</li>\n<li><strong>Partitionering er oprydning.</strong> Den holder tabellerne omkring en varm række\nsmå og gør intet for rækken.</li>\n<li><strong>Shard, når én maskine er fuld af godt arbejde,</strong> ikke når den er fuld af\nventen.</li>\n<li><strong>Afstem før ja’et, ikke efter.</strong> Alt omkring reservationen kan være konsistent\nmed tiden. Selve reservationen, lavet på den måde, oversælger med forsøgsraten\ngange 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: brug af EXPLAIN</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/monitoring-stats.html\">PostgreSQL: det kumulative statistiksystem</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: tabelpartitionering</a></li>\n<li><a href=\"https://www.postgresql.org/docs/current/hot-standby.html\">PostgreSQL: hot standby og forespørgselskonflikter</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’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’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": "da" }, { "id": "https://engineer.company/da/notes/selling-limited-stock-without-overselling/", "url": "https://engineer.company/da/notes/selling-limited-stock-without-overselling/", "title": "At sælge den sidste billet præcis én gang", "summary": "Et flash-salg oversælger på én varm række, ikke på en for stor database. Det, der stopper det: en betinget skrivning, reservationer, der udløber, og en saga.", "content_html": "<p>Et flash-salg stiller ti tusind enheder foran en million mennesker på én gang.\nDen interessante fejl er ikke, at butikken går ned. Det er, at butikken bliver\noppe og sælger ti tusind og fyrre. Instinktet bagefter — databasen er for lille,\nså shard den — svarer på et spørgsmål, ingen har stillet. Databasen var aldrig\nfor stor. Alle købere skrev til\nden samme række (hele hændelsen ligger i én række).</p>\n<p>Vi skiller problemet ad her, før det rammer et projekt: hvor loftet kommer fra,\nden ene sætning, der fjerner det meste af det, og hvad man bygger, når én\ndatabase reelt ikke er nok.</p>\n<h2 id=\"én-række-alle-købere\">Én række, alle købere</h2>\n\n<p>Tyve millioner rækker fordelt over en klynge er et løst problem. Én række, som\nhver eneste forespørgsel skal ændre, er ikke, og flere maskiner rører den ikke,\nfor rækken ligger på én af dem. Distribuerede lagre siger det ligeud: DynamoDB\ngiver én partition omkring 1.000 skriveenheder i sekundet, og hver skrivning til\nén vare lander på varens partition, uanset hvor mange andre der findes.</p>\n<p>Der ligger et teorem bag formen. Bailis og kolleger kalder en invariant\ninvariant-konfluent, når replikaer kan tage imod skrivninger hver for sig og\nstadig flettes til en gyldig tilstand. <code>stock >= 0</code> er skoleeksemplet på én,\nder ikke er det: to replikaer, der hver ser én enhed tilbage, sælger den hver\nisær, og ingen fletning kan tage det andet salg tilbage. At aldrig oversælge\nkræver koordinering. Spørgsmålet er kun, hvor meget af den hvert salg betaler\nfor.</p>\n<h2 id=\"tallet-der-afgør-det\">Tallet, der afgør det</h2>\n\n<p>Loftet for én vare er groft sagt, hvor mange salg én lås bærer, divideret med\nhvor længe lå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 pr. vare ≈ salg pr. 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>Hvad holder rækken</th>\n\t\t\t\t\t<th>Loft pr. række</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, kald så 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, kør så app-logik</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>Batches af 100 pr. 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 hukommelsen</td>\n\t\t\t\t\t<td>~100.000+/s</td>\n\t\t\t</tr>\n\t</tbody>\n</table>\n<p>Det er størrelsesordener, ikke benchmarks; hardwaren flytter dem. Det, den ikke\nflytter, er formen. Det meste af en kort lås er committen, der venter på, at\nloggen når disken, og derfor vinder batching med to størrelsesordener: hundrede\nsalg deler én flush.</p>\n<p>Så er der fire håndtag tilbage, og hvert design herunder trækker i ét af dem:</p>\n<ol>\n<li><strong>Hold låsen kortere</strong> — én sætning, intet over netværket inde i den.</li>\n<li><strong>Gør mere pr. lås</strong> — saml nedskrivningerne i batches.</li>\n<li><strong>Hav flere låse</strong> — del lageret over flere nøgler.</li>\n<li><strong>Send færre forespørgsler til låsen</strong> — afvis de fleste købere, før de\nnår frem.</li>\n</ol>\n<p>Det sidste betyder mere, end det lyder til. Med en million købere og ti tusind\nenheder skal 990.000 forespørgsler fejle. Afvisninger er det meste af\ntrafikken, så “udsolgt” skal være det billigste svar, systemet giver: et\nventeværelse foran salget, en port, der lukker omtrent lige så mange købere ind,\nsom der er enheder, et <code>sold_out</code>-flag, som CDN’et serverer uden at spørge\nnogen, og en hastighedsgrænse pr. konto.</p>\n<h2 id=\"ret-den-ene-database-først\">Ret den ene database først</h2>\n\n<p>Den version, der kryber af sted eller oversælger, ser som regel sådan ud:</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\">-- applikationen tjekker tallet\n</span></span></span><span class=\"line\"><span class=\"cl\"><span class=\"c1\">-- og kalder måske en betalingsudbyder\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 rigtig. Det, den holdes hen over, er forkert: hvert millisekund,\napplikationen bruger mellem <code>SELECT</code> og <code>UPDATE</code>, er et millisekund, den næste\nkøber venter. Uden <code>FOR UPDATE</code> er det værre. To købere læser 1, begge skriver\n0, og én enhed er solgt to gange.</p>\n<p>Rettelsen gør tjekket og nedskrivningen til én sætning:</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\">>=</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 række tilbage: reserveret. ingen: udsolgt.\n</span></span></span></code></pre></div><p>I PostgreSQL på READ COMMITTED venter en anden køber, der når rækken, mens den\nførste holder den, og evaluerer så <code>WHERE</code>-betingelsen igen mod den række, den\nførste committede. Tog det salg den sidste enhed, fejler betingelsen, og den\nanden køber får ingen række tilbage — udsolgt — i stedet for et negativt tal. På\nREPEATABLE READ eller SERIALIZABLE får den anden køber i stedet en\nserialiseringsfejl, hvilket er sikkert, men gør konkurrence til genforsøg; READ\nCOMMITTED er det niveau, sætningen er skrevet til. MySQL’s InnoDB når samme\nsvar, fordi en <code>UPDATE</code> dér læser den senest committede række på ethvert\nniveau. Tilføj også begrænsningen, så en fejl et andet sted fejler højlydt i\nstedet for at sælge lager, der ikke findes:</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\">>=</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 af arbejdet i den ene database, nogenlunde efter udbytte:</p>\n<ul>\n<li><strong>Intet eksternt inde i transaktionen.</strong> Intet betalingskald, ingen\nHTTP-forespørgsel, ingen besked sendt. Det hører til før transaktionen eller\nefter den.</li>\n<li><strong>Rør den varme række sidst.</strong> Indsæt ordren og alt andet først, så låsen på\nden omstridte række kun varer under den sidste sætning og committen.</li>\n<li><strong>Ingen optimistisk låsning på en varm tæller.</strong> En versionskolonne gør\nkonkurrence til genforsøg, og under et salg er genforsøgene belastningen.</li>\n<li><strong>Server læsningerne fra et andet sted.</strong> Produktsidens “12 tilbage” kan komme\nfra en replika eller en cache, der er et sekund bagud. Kun salget behøver den\nprimære.</li>\n<li><strong>Giv lageret sin egen database,</strong> så et salg aldrig skal konkurrere med\nkataloget om den samme disk.</li>\n<li><strong>Pool forbindelserne.</strong> Hundrede tusind købere bør være et par dusin\nforbindelser gennem PgBouncer, ikke tusindvis af backends, der venter på én\nlås.</li>\n</ul>\n<p>Pladser har en anden form, fordi hver enhed er unik, og de vil have en anden\nsætning. Hver plads er en række, og en køber springer de rækker over, som en\nanden transaktion holder, i stedet for at vente på 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\">'10 min'</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\">></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 udløbet reservation er simpelthen ledig igen, så der skal ingen oprydder til,\nfor at svaret er rigtigt. Og <code>now()</code> er databasens ur, så applikationsservere,\nhvis ure er uenige, ikke kan være uenige om, hvornår en reservation udløb.</p>\n<p>For mange butikker er dette afsnit hele rettelsen. Gør det før alt herunder, for\nsharding er en dør, der kun går én vej: let at gå igennem, dyr at gå tilbage\nigennem.</p>\n<h2 id=\"reservér-først-betal-bagefter\">Reservér først, betal bagefter</h2>\n\n<p>En køber, der tager den sidste enhed og så forlader kortformularen, har ikke\nkøbt den. Så et salg er ikke én skrivning, men et kort livsforløb:</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>Reservationen har forespørgslens nøgle, ikke forsøgets.</strong> Hvert checkout\nbærer en idempotensnøgle, og reservationstabellen bærer\n<code>UNIQUE (tenant_id, idempotency_key)</code>, så et gentaget klik får den\nreservation, det allerede har, og ikke en enhed mere. En detalje i PostgreSQL\nbider her: <code>INSERT ... ON CONFLICT DO NOTHING RETURNING id</code> returnerer intet,\nnår rækken fandtes i forvejen, så “ingen række tilbage” betyder “slå den op”,\nikke “fejlede”.</li>\n<li><strong>Hver overgang er betinget.</strong> At bekræfte et salg flytter en reservation fra\n<code>held</code> til <code>sold</code> kun dér, hvor den stadig er <code>held</code>, og en frigivelse giver\nkun lageret tilbage, hvis dens egen opdatering ændrede præcis én række. En\nfrigivelse, der kører to gange — timeouten og annullér-knappen, for eksempel\n— giver enheden tilbage én gang.</li>\n<li><strong>Reservationer udløber efter minutter.</strong> Fem til ti er det almindelige.\nForlæng reservationen én gang, når betalingen starter, så et langsomt kort\nikke mister den enhed, det er ved at betale for.</li>\n<li><strong>Begræns antallet af enheder pr. køber ved reservationen,</strong> så én konto ikke\nkan holde hele lageret, mens den tænker sig om.</li>\n</ul>\n<p>Under det hele ligger én linje, den mest nyttige i 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 gælder efter hver transaktion, én forespørgsel tjekker den, og når den\nholder op med at gælde, har noget oversolgt eller tabt en enhed — før en kunde\nsiger det.</p>\n<h2 id=\"når-én-database-ikke-er-nok\">Når én database ikke er nok</h2>\n\n<p>Nogle gange er den reelt ikke det: mange lejere, mange salg på én gang, flere\nregioner. Den struktur, der skalerer, holder fast i én regel — hver vare har\npræcis én ejer af sit lager, en lagertjeneste, og intet andet bestemmer — og\nsharder den tjeneste efter <code>(tenant_id, item_id)</code>, så én vares skrivninger\naldrig spænder over to shards. De største lejere får deres egne celler, så deres\nsalg ikke kan sulte andres ud. Kun reservationen behøver at være synkron;\nalt efter den kan være en besked.</p>\n<p>Til den varme vare inde i en shard er der fire værktøjer, og hvert trækker i ét\naf håndtagene ovenfor.</p>\n<p><strong>En port foran.</strong> En tæller i Redis, nedskrevet af et Lua-script, så tjek og\nnedskrivning er ét trin, lukker omtrent lige så mange købere ind, som der er\nenheder, og afviser alle andre på mikrosekunder. Den er ikke dommeren. Redis\nreplikerer asynkront, så efter en failover kan den nye primære have glemt nogle\nadgange og lukket lidt for mange ind. Databasens betingede <code>UPDATE</code> bestemmer\nstadig, og en afstemningsproces nulstiller porten ud fra databasens tal.</p>\n<p><strong>Del lageret.</strong> Ti tusind enheder kan være hundrede spande med hundrede, hver\nsin række eller sin shard, med hver køber sendt til én: hundrede låse i stedet\nfor én. Idéen er gammel — O’Neils escrow-transaktioner fra 1986, så Barbará og\nGarcia-Molinas demarkationsprotokol, så Balegas og kollegers begrænsede tællere\n— og dens ene reelle pris er halen. En købers spand løber tør, mens andre stadig\nhar lager, så prøv enten en anden spand eller flyt resterne, og saml dem til én\nigen, når salget nærmer sig sin afslutning. Samme idé giver hver region sin egen\nkvote, så de fleste salg aldrig krydser et hav for at spørge.</p>\n<p><strong>Én skriver pr. vare.</strong> Læg hver forespørgsel på en vare på én Kafka-partition,\nlæst af én forbruger, der holder tallet i hukommelsen og committer sine\nafgørelser i batches. Én tråd og ingen låse: LMAX-formen. Forespørgsler bærer et\nid, og skriveren smider én væk, den allerede har afgjort, for en kø leverer to\ngange.</p>\n<p><strong>Enheder som rækker.</strong> Når enhederne er få og unikke — pladser, nummererede\nudgaver — så opret én række til hver på forhånd og gør krav på dem med\n<code>SKIP LOCKED</code> som ovenfor, eller hold enhedernes id’er i et Redis-sæt og tag én\nmed <code>SPOP</code>. Så er der ingen tæller tilbage at slås om.</p>\n<h2 id=\"resten-af-checkout\">Resten af checkout</h2>\n\n<p>Betaling, ordre og kvittering er separate systemer, og ingen transaktion spænder\nover dem. Tofaset commit er lærebogens svar og det forkerte her: det blokerer,\nmens en deltager er langsom, det lægger rundture til den varmeste sti, og\nbetalingsudbydere deltager ikke i det. En saga klarer opgaven, en række lokale\ntrin, hver med et trin, der fortryder 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. reservér lager fortryd: frigiv reservationen\n</span></span><span class=\"line\"><span class=\"cl\">2. afventende ordre fortryd: annullér ordren\n</span></span><span class=\"line\"><span class=\"cl\">3. forlæng reservationen (udløber af sig selv)\n</span></span><span class=\"line\"><span class=\"cl\">4. autorisér betaling fortryd: annullér den\n</span></span><span class=\"line\"><span class=\"cl\">5. bekræft HELD → SOLD\n</span></span><span class=\"line\"><span class=\"cl\">6. hæv betalingen\n</span></span></code></pre></div><p>At autorisere først og hæve sidst betyder, at en betaling, der kommer efter\nsin reservations udløb, annulleres i stedet for at refunderes: pengene flyttede\nsig aldrig. Hvert trin skriver sin besked i en outbox-tabel i samme transaktion\nsom den ændring, den melder, og change data capture udgiver den, så intet går\ntabt mellem gemt og sendt. Hver forbruger er idempotent, med ordren som nøgle.\nBeskeder kommer mindst én gang, og idempotens gør det til i praksis én gang.</p>\n<p>En distribueret lås hører ingen steder hjemme her. Kleppmanns pointe er, at en\nlåseholder kan gå i stå forbi sin lejeperiode — en garbage collection, en\nvirtuel maskine, der hænger — og blive ved med at skrive, og kuren er et\nfencing-token, som lageret tjekker. Men et lager, der kan tjekke et token, kan\ntjekke <code>available >= :qty</code>, og så har det aldrig haft brug for låsen.</p>\n<h2 id=\"sæt-varen-på-pause-oversælg-den-aldrig\">Sæt varen på pause, oversælg den aldrig</h2>\n\n<p>Konsistens og tilgængelighed vælges pr. spørgsmål, ikke pr. system. “Må jeg\nsælge denne enhed?” skal være konsistent: når ejeren af en vares lager ikke kan\nnås, holder den vare op med at sælge. Alt andet — kataloget, mærket med\n“3 tilbage”, ordrehistorikken — kan forblive tilgængeligt og være en smule\nforældet.</p>\n<p>Det stilleste oversalg kommer fra en failover. Med asynkron replikering\nbekræfter en primær et salg og dør, før dens replika har det; replikaen\nforfremmes, salget er væk, og enheden sælges igen. Rettelsen er at gøre et salg\nholdbart på mere end én maskine, før det bekræftes: en synkron standby i\nPostgreSQL (<code>synchronous_commit</code> med <code>synchronous_standby_names</code>) eller et lager\nbygget på Raft. Det koster latens på hver commit, hvilket forlænger præcis den\nholdetid, loftet divideres med, og batching og porten køber den tilbage. Afgræns\nogså pausen: en shard, der fejler, skal stoppe varerne på den, ikke butikken.</p>\n<h2 id=\"bevis-det\">Bevis det</h2>\n\n<p>Intet af det her skal tages på tro, heller ikke vores.</p>\n<ul>\n<li><strong>Hold øje med invarianten.</strong> Kør <code>available + held + sold = total</code> efter en\ntidsplan, tegn den, og alarmér ved enhver afvigelse. Nul oversalg er et\nserviceniveaumål med en alarm, ikke et håb.</li>\n<li><strong>Fremprovokér kapløbet.</strong> Ti tusind samtidige købere til hundrede enheder, i\ntestsuiten, der kræver præcis hundrede solgte og intet tal under nul.</li>\n<li><strong>Ødelæg ting under kapløbet.</strong> Dræb den primære, kap netværket, sæt en proces\npå pause — i Jepsens ånd — og tjek invarianten bagefter.</li>\n<li><strong>Belastningstest afvisningerne.</strong> Udsolgt-stien skal forblive hurtig, når den\ner 99 procent af trafikken, og kanten skal kunne tage den.</li>\n<li><strong>Saml målingerne på ét dashboard:</strong> låseventetider, udløbne reservationer og\nafstanden mellem portens tal og databasens.</li>\n</ul>\n<h2 id=\"i-hvilken-rækkefølge\">I hvilken rækkefølge</h2>\n\n<ol>\n<li>Mål først. <code>pg_stat_statements</code> og låseventetiderne siger, om den varme\nrække overhovedet er problemet.</li>\n<li>Gør skrivningen til én betinget sætning.</li>\n<li>Skær belastningen ned: et venteværelse, en port, et cachet “udsolgt”.</li>\n<li>Giv lageret sin egen tjeneste og sin egen ejer.</li>\n<li>Shard efter <code>(tenant_id, item_id)</code>, og kun hvis de første fire ikke var nok.</li>\n<li>Tilføj en tilstand til varme varer, spande eller én skriver, til det salg, du\nved kommer.</li>\n<li>Regioner til sidst, med kvoter.</li>\n</ol>\n<h2 id=\"hvad-vi-ville-sige-til-den-næste\">Hvad vi ville sige til den næste</h2>\n\n<ul>\n<li><strong>Sharding svarer på et andet spørgsmål.</strong> Det spreder mange rækker, og et\noversolgt salg er én række. Find ud af, hvilken af delene du har, før du\nbygger.</li>\n<li><strong>Gør tjek og nedskrivning til én sætning.</strong> Det meste af afstanden mellem\nfint og oversolgt er tiden mellem at læse et tal og at skrive det.</li>\n<li><strong>Det meste af trafikken er svaret nej.</strong> Gør afvisning til det billigste,\nsystemet gør.</li>\n<li><strong>Sæt på pause, før du oversælger.</strong> En vare, der holder op med at sælge i\ntredive sekunder, er en undskyldning. En vare, der er solgt to gange, er en\nrefusion, en supportkø og en kunde, der husker det.</li>\n<li><strong>Skriv invarianten ned, og hold øje med den.</strong> Den er én linje, og den er det\neneste, der siger, at designet virker i drift og ikke kun i review.</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: Read Committed-isolation</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: design af partitionsnøgler</a></li>\n<li><a href=\"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html\">DynamoDB: partitioner 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": "da" }, { "id": "https://engineer.company/da/notes/safari-underline-font-subset/", "url": "https://engineer.company/da/notes/safari-underline-font-subset/", "title": "Understregningen, der gik gennem hvert bogstav", "summary": "Safari trak vores understregninger gennem hver underlængde. Årsagen var et kyrillisk skriftudsnit på sider, der aldrig henter det.", "content_html": "<p>Vores eget site understreger alle links og lader browseren klippe et hul, hvor\net bogstavs hale krydser linjen — <code>g</code>‘et, <code>p</code>‘et, kommaet. Det gjorde Safari\nikke. Linjen gik lige igennem dem. Sådan havde det været på de engelske og de\ndanske sider, lige så længe sitet havde været\nflersproget (den fejl, ingen melder, fordi den bare ser billig ud).</p>\n<p>Årsagen var tre skrifterklæringer for et alfabet, de sider aldrig henter.</p>\n<h2 id=\"hvad-browseren-skal-gøre\">Hvad browseren skal gøre</h2>\n\n<p><code>text-decoration-skip-ink</code> er den egenskab, der løfter en understregning uden om\nen underlængde. Den er slået til som standard, og i årevis har svaret på “min\nunderstregning ser forkert ud” været at gribe fat i <code>text-underline-offset</code> og\nskubbe linjen længere ned. Det er den forkerte refleks: at flytte linjen ændrer\ndesignet, og det, der faktisk manglede, var hullet.</p>\n<p>Vi fandt to reelle årsager, før vi nåede til den interessante, og begge er\nværd at kende.</p>\n<ul>\n<li><strong>En understregning på <code>1px</code> får et hul i størrelsen <code>1px</code>.</strong> Det hul,\nmotoren klipper, står i forhold til tykkelsen på den linje, der klipper det.\nVores stylesheet havde låst tykkelsen til én pixel, hvilket efterlod omkring\n0,6px luft på hver side af en stamme — teknisk set et hul, visuelt en linje\nlige gennem bogstavet. <code>text-decoration-thickness: auto</code> skalerede begge dele\nsammen og tog hullet fra 3,5px til 12,4px uden at flytte understregningen en\neneste pixel.</li>\n<li><strong><code>from-font</code> er ikke den sikre standard, det lyder som.</strong> Den læser den\ntykkelse, skrifttypen selv angiver, og Ubuntu angiver 0,056em for Light,\n0,020em for Medium og 0,120em for Bold — en familie, hvis Medium-linje vejer\nunder halvdelen af dens Light-linje. I store størrelser er det en seks pixel\nbred linje ved siden af en to pixel bred på samme side.</li>\n</ul>\n<p>Det rettede Chrome. Safari trak stadig linjen igennem det hele.</p>\n<h2 id=\"den-del-der-tog-længst-tid\">Den del, der tog længst tid</h2>\n\n<p>Vi målte det ordentligt: kør et rigtigt Safari, farv understregningen om, tag et\nskærmbillede i otte gange skala, og tæl de pixels, hvor linjen stopper og\nstarter.</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>hul omkring en underlængde</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 klippede altså et hul. Det var bare seks gange smallere end Chromes, og i\nlæsestørrelse er et hul på én pixel ikke et hul. Og det sædvanlige\ngreb gjorde kun ondt værre: ved en tykkelse på 3px faldt Safari fra tre huller til\nét, ved 4px til ingen, for den skalerer ikke hullet med linjen. Den låser det\ntil omkring 0,07em i alle størrelser, hvor Chrome giver 0,6em.</p>\n<p>Vi afprøvede omkring tyve erklæringer imod den — <code>skip-ink: all</code>, begge de gamle\nstavemåder af egenskaben, hver tykkelse fra en halv pixel til fire,\n<code>text-underline-position</code>, <code>font-smoothing</code>, <code>text-rendering</code>, <code>paint-order</code>.\nIntet gjorde hullet bredere. Den ærlige konklusion på det tidspunkt var, at\nWebKit ganske enkelt satte et loft over hullet, og at der ikke var mere at gøre\nfra et stylesheet.</p>\n<p>Den konklusion var forkert, og det, der væltede den, var\n<a href=\"https://github.com/fontsource/fontsource/issues/1096\">en fejlrapport mod et skriftprojekt</a>:\nSafari ignorerer skip-ink, når en skrifttype indlæses med et ikke-latinsk\nudsnit.</p>\n<h2 id=\"den-egentlige-årsag\">Den egentlige årsag</h2>\n\n<p>Sitet serverer Ubuntu i seks dele — tre vægte latinsk, tre kyrillisk — så en\nukrainsk side sættes i samme skrifttype som en engelsk i stedet for at falde\ntilbage på det, systemet nu tilbyder. Hver del angiver det tegnområde, den\ndækker, og browseren henter kun de dele, en side rent faktisk har brug for.</p>\n<p>Alle seks var erklæret under det ene familienavn, hvilket er den oplagte måde at\nskrive det på. Og netop det er udløseren, registreret som\n<a href=\"https://bugs.webkit.org/show_bug.cgi?id=255159\">WebKit-fejl 255159</a>: <strong>når ét\nsnit i en familie angiver et ikke-latinsk tegnområde, forringer Safari\nskip-ink for hvert eneste tegn i den familie</strong> — også latinsk tekst på en side,\nder aldrig henter den anden fil.</p>\n<p>En engelsk side tegnede sine understregninger dårligt, fordi der fandtes en\nukrainsk skrifterklæring et sted i stylesheetet. Da vi slettede de tre linjer\nunder kørsel og ikke ændrede andet, gik hullerne fra 1,8/1,0/1,0px til\n4,5/11,0/5,0px.</p>\n<h2 id=\"rettelsen\">Rettelsen</h2>\n\n<p>Giv det andet skriftsystem sit eget familienavn, og nævn det i skriftstakken\nefter 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\">'woff2'</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>Et latinsk bogstav findes i den første familie og når aldrig den anden. Et\nkyrillisk falder forbi den første, som ikke har et sådant tegn, og lander i den\nanden i stedet for i en systemskrift. Tegnområderne bestemmer stadig, hvad der\nhentes, så intet ved indlæsningen ændrer sig: engelske og danske sider henter\ntre latinske filer og ingen kyrilliske, ukrainske sider omvendt.</p>\n<p>Tre omdøbte erklæringer og én post i stakken. Safaris huller gik til\n4,5/11,0/5,0px, Chrome forblev urørt, understregningen flyttede sig ikke, og\nukrainsk sættes stadig i Ubuntu i nøjagtig samme bredder.</p>\n<h2 id=\"hvad-vi-ville-sige-til-den-næste\">Hvad vi ville sige til den næste</h2>\n\n<ul>\n<li><strong>En skip-ink-måling gælder kun for den motor, der frembragte den.</strong> Vi havde\nen tabel med tal, en korrekt diagnose og en reel rettelse — og det hele\nvar Chromium. Det skærmbillede, der genåbnede sagen, kom fra et menneske, der\nkiggede på siden.</li>\n<li><strong>Grib fat i tykkelsen før forskydningen.</strong> At gøre hullet bredere bevarer\ndesignet; at flytte linjen ned erstatter det.</li>\n<li><strong>At dele en skrifttype op efter skriftsystem er rigtigt, men opdelingen hører\nogså hjemme i familienavnet.</strong> Ét navn på tværs af skriftsystemer ser pænere\nud i koden og koster dig skip-ink i Safari. At slå dem sammen igen ligner\noprydning og er en tilbagegang.</li>\n<li><strong>Indlæsningen var aldrig problemet.</strong> Al intuition siger, at en side, der\ntegner dårligt på grund af en kyrillisk skrift, må hente noget, den ikke\nburde. Det gjorde den ikke. Erklæringen 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-fejl 255159 — <code>text-decoration-skip-ink</code> og skriftudsnit</a></li>\n<li><a href=\"https://github.com/fontsource/fontsource/issues/1096\">Fontsource-sag 1096, som navngav udlø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": "da" }, { "id": "https://engineer.company/da/notes/solana-mobile-wallet-deeplinks/", "url": "https://engineer.company/da/notes/solana-mobile-wallet-deeplinks/", "title": "Sådan åbner man en dApp i Solana-mobilwallets", "summary": "Hvorfor browse-deeplinks til Phantom, Solflare og Backpack fejler på mobilen — og de formater, regler og rettelser, der får dem til at virke.", "content_html": "<p>En React-dApp bygget på <code>@solana/wallet-adapter-react</code> forbinder\ndesktop-wallets uden problemer, men på en telefon falder det samme flow fra\nhinanden: wallet’en skal åbne dApp’en i sin egen indbyggede browser, og de\ndeeplinks, der skulle klare det, virker bare ikke. Backpack lander på en “hent\nappen”-side; Solflare åbner appen, men aldrig sitet; alle varianter ser ud til\nat fejle. Vi skilte problemet ad, og det viste sig at være fire adskilte\nproblemer med ét fælles symptom.</p>\n<h2 id=\"de-fire-problemer\">De fire problemer</h2>\n\n<ul>\n<li><strong>Backpack-linket var forkert bygget.</strong> Det eneste dokumenterede format er\n<code>https://backpack.app/ul/v1/browse/<url>?ref=<ref></code> — et universelt link med\nmål-URL’en i stien og et påkrævet <code>ref</code>. Et gæt med eget skema som\n<code>backpack://ul/v1/browse?url=...</code> matcher ingen rute i appen, så brugeren\nender på wallet’ens installationsside.</li>\n<li><strong>Solflare skal også bruge sit universelle link:</strong>\n<code>https://solflare.com/ul/v1/browse/<url>?ref=<ref></code> — ikke det rå\n<code>solflare://</code>-skema. Et råt skema kan starte appen uden at dirigere den —\nhvilket er præcis “appen åbner, men site-fanen må åbnes manuelt”.</li>\n<li><strong>Begge parametre skal være kodet.</strong> <code>url</code> er dApp’ens fulde absolutte\nadresse, og <code>ref</code> er den kaldende origin, hver især gennem\n<code>encodeURIComponent</code>. Et ukodet <code>?</code> eller <code>&</code> i målet ødelægger\nfortolkningen, og wallet’en åbner på sin forside i stedet for browserfanen.</li>\n<li><strong>Udløsningen betyder lige så meget som linket.</strong> Universelle links skifter\nkun app ved en navigation, styresystemet stoler på — og de gør med vilje\ningenting, når de indsættes i adresselinjen, hvilket også er sådan, et helt\nkorrekt link “fejler” under test.</li>\n</ul>\n<h2 id=\"de-dokumenterede-formater\">De dokumenterede formater</h2>\n\n<ul>\n<li>Phantom: <code>https://phantom.app/ul/browse/<url>?ref=<ref></code> — uden <code>/v1</code> i\nnetop dette.</li>\n<li>Solflare: <code>https://solflare.com/ul/v1/browse/<url>?ref=<ref></code></li>\n<li>Backpack: <code>https://backpack.app/ul/v1/browse/<url>?ref=<ref></code></li>\n</ul>\n<p>Ét mønster dækker alle tre:</p>\n<pre tabindex=\"0\"><code>const WALLET_BROWSE = {\n phantom: (url, ref) =>\n `https://phantom.app/ul/browse/${url}?ref=${ref}`,\n solflare: (url, ref) =>\n `https://solflare.com/ul/v1/browse/${url}?ref=${ref}`,\n backpack: (url, ref) =>\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=\"udløs-linket-så-ios-og-android-accepterer-det\">Udløs linket, så iOS og Android accepterer det</h2>\n\n<ul>\n<li><strong>Render et rigtigt anker, beregnet på forhånd.</strong> Et almindeligt\n<code><a href={walletBrowseLink('phantom')}></code> er den mest pålidelige udløser på\nbegge platforme.</li>\n<li><strong>Skal det ske programmatisk</strong>, så tildel <code>window.location.href</code> synkront\ninde i tryk-handleren — ingen <code>await</code>, ingen <code>fetch</code>, ingen <code>setTimeout</code>\nførst. Efter asynkront arbejde er gestus-konteksten væk, og iOS falder\ntilbage til wallet’ens websted. Aldrig <code>window.open</code>.</li>\n<li><strong>Test aldrig ved at indsætte i adresselinjen.</strong> Universelle links udløses\nmed vilje ikke dér; test med et link, der trykkes på, eller en QR-kode, som\nkameraet scanner.</li>\n<li><strong>Pas på messenger-webviews.</strong> Åbnet i Telegrams eller Instagrams indbyggede\nbrowser bliver universelle links ofte slugt, og wallet’ens almindelige\nwebsted indlæses i stedet. User-agent-detektion er i bedste fald et gæt, så\ngiv også brugerne en synlig nødudgang: “åbn i Safari eller Chrome, og\nforbind derefter”.</li>\n</ul>\n<h2 id=\"den-større-løsning-på-android\">Den større løsning på Android</h2>\n\n<p>Håndbyggede deeplinks er iOS-historien. På Android lader Solana Mobiles Mobile\nWallet Adapter en dApp i mobilbrowseren forbinde direkte til den installerede\nwallet-app, helt uden omvejen om den indbyggede browser. Nyere versioner af\n<code>@solana/wallet-adapter-react</code> registrerer mobiladapteren automatisk, så en\nopgradering af wallet-adapter-pakkerne kan i sig selv løse problemet på Android.\nMålarkitekturen: Mobile Wallet Adapter på Android, universelle browse-links på\niOS, hvor Apple ikke tillader en tilsvarende løsning.</p>\n<h2 id=\"efterprøv-på-en-enhed\">Efterprøv på en enhed</h2>\n\n<ol>\n<li>Rigtig enhed, wallet installeret, link åbnet fra systembrowseren — ikke fra\nen messenger.</li>\n<li>Tryk på et renderet link, eller scan en QR-kode; indsæt aldrig i\nadresselinjen.</li>\n<li>Bekræft, at wallet’en åbner, og at dApp’en indlæses i dens indbyggede\nbrowserfane — anden halvdel er den, der fejler.</li>\n<li>Gentag uden wallet’en installeret: det universelle link skal falde tilbage\ntil wallet’ens websted. Ser du dén side, mens appen er installeret, er\nlinket eller udløsningen stadig forkert.</li>\n<li>Test derefter messenger-vejen, og tilføj “åbn i browser”-hjælpen, hvis den\nfejler dér.</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: deeplinks på iOS og Android</a></li>\n<li><a href=\"https://docs.solflare.com/solflare/technical/deeplinks/other-methods/browse\">Solflare: Browse-deeplinket</a></li>\n<li><a href=\"https://docs.backpack.app/deeplinks/other-methods/browse\">Backpack: Browse-deeplinket</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": "da" } ] }