feat: flyt sitet fra Astro til Zola
Samme indhold, URL'er og udseende, bygget af en enkelt zola-binær og Tailwind standalone-CLI uden Node. Kanonisk domæne er asger.weirsoe.dk. Blogindlæg ligger som page bundles med frontmatter Zola forstår, så de gamle stempler er ugyldige, indtil scripts/omstempl.py er kørt med PUBLISH_KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7e13068dd2
commit
e58a11c2b4
+10
-18
@@ -15,31 +15,23 @@ jobs:
|
||||
# Fuld historik til sitemap-lastmod og IndexNow
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# Runneren udelader optional deps, så sharp-binæren installeres eksplicit
|
||||
- name: Install dependencies
|
||||
env:
|
||||
npm_config_omit: ""
|
||||
npm_config_optional: "true"
|
||||
run: |
|
||||
npm install --include=optional
|
||||
npm install --include=optional --os=linux --libc=glibc --cpu=x64 sharp
|
||||
node -e "console.log('sharp', require('sharp').versions.sharp)"
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Verify blog approvals
|
||||
env:
|
||||
PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
|
||||
run: node scripts/verify-approvals.mjs
|
||||
run: python3 scripts/verify-approvals.py
|
||||
|
||||
- name: Fetch zola and tailwind
|
||||
run: sh scripts/hent-vaerktoej.sh
|
||||
|
||||
- name: Build site
|
||||
run: npm run build
|
||||
run: sh scripts/build.sh
|
||||
|
||||
# tar over SSH med atomisk swap (runner-imaget har ikke rsync)
|
||||
- name: Deploy dist/ over SSH
|
||||
- name: Deploy public/ over SSH
|
||||
env:
|
||||
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
|
||||
DEPLOY_USER: ${{ vars.DEPLOY_USER }}
|
||||
@@ -55,7 +47,7 @@ jobs:
|
||||
SSH="ssh -i $HOME/.ssh/deploy_key -o StrictHostKeyChecking=accept-new"
|
||||
ssh-keyscan -t ed25519 "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
$SSH "$DEPLOY_USER@$DEPLOY_HOST" "rm -rf $DEPLOY_PATH.new && mkdir -p $DEPLOY_PATH.new"
|
||||
tar -C dist -czf - . | $SSH "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
tar -C public -czf - . | $SSH "$DEPLOY_USER@$DEPLOY_HOST" \
|
||||
"tar -C $DEPLOY_PATH.new -xzf - \
|
||||
&& rm -rf $DEPLOY_PATH.old \
|
||||
&& { mv $DEPLOY_PATH $DEPLOY_PATH.old 2>/dev/null || true; } \
|
||||
@@ -66,4 +58,4 @@ jobs:
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
|
||||
run: node scripts/indexnow.mjs
|
||||
run: python3 scripts/indexnow.py
|
||||
|
||||
+6
-4
@@ -1,5 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
.DS_Store
|
||||
bun.lock*
|
||||
public/
|
||||
static/css/
|
||||
data/gitea.json
|
||||
static/processed_images/
|
||||
.tools/
|
||||
__pycache__/
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# asger.weirsoe.dk
|
||||
|
||||
Personlig side (CV + blog) bygget med Astro + Bun + Tailwind. Statisk output, selvhostet bag nginx, deployet via Gitea Actions.
|
||||
Personlig side (CV + blog) bygget med Zola + Tailwind. Statisk output, selvhostet bag nginx, deployet via Gitea Actions. Ingen Node i byggekæden.
|
||||
|
||||
## Udvikling
|
||||
```
|
||||
bun install
|
||||
bun run dev # http://localhost:4321
|
||||
bun run build # -> dist/
|
||||
sh scripts/hent-vaerktoej.sh # zola og tailwindcss til .tools/
|
||||
sh scripts/build.sh # -> public/
|
||||
```
|
||||
|
||||
## Nyt blogindlæg
|
||||
Læg en `.md`-fil i `src/content/blog/` med frontmatter (title, date, tags, description, draft). Push til main → bygges og deployes automatisk.
|
||||
`build.sh` henter repos og heatmap fra Gitea (`scripts/prebuild.py`), bygger CSS, kører `zola build` og efterbehandler (`scripts/postbuild.py`: sitemap, service worker). `PW_PREVIEW=1` bygger preview'et under `/udkast`.
|
||||
|
||||
## Blogindlæg
|
||||
Indlæg ligger som `content/blog/<slug>/index.md` med billederne ved siden af, men de lægges ikke her i hånden. Et indlæg skrives i det private repo agw/pw-drafts og udgives med "Godkend og udgiv", som stempler det og committer det hertil. CI (`scripts/verify-approvals.py`) nægter at bygge et indlæg uden gyldigt stempel.
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-cyrillic-ext-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-cyrillic-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-greek-ext-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+1F00-1FFF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-greek-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-vietnamese-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-latin-ext-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter Variable';
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
font-weight: 100 900;
|
||||
src: url(../fonts/inter-latin-wght-normal.woff2) format('woff2-variations');
|
||||
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/* Bygges med Tailwind standalone-CLI til static/css/site.css (se scripts/build.sh). */
|
||||
@import "tailwindcss" source(none);
|
||||
@source "../templates";
|
||||
@source "../content";
|
||||
@import "./inter.css";
|
||||
|
||||
/* Fælles tema (Weircon-wind) */
|
||||
@import "../vendor/weirsoe-theme/adapters/tailwind.css";
|
||||
@import "../vendor/weirsoe-theme/weirsoe-theme.css" layer(components);
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter Variable", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
html[data-theme="terminal"] {
|
||||
--font-sans: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
:root { --wt-font-sans: var(--font-sans); }
|
||||
@layer base { a { color: inherit; } }
|
||||
|
||||
/* Samme fade mellem sider som før */
|
||||
@view-transition { navigation: auto; }
|
||||
|
||||
/* Kommandopalette */
|
||||
.palette-overlay {
|
||||
position: fixed; inset: 0; z-index: 100;
|
||||
background: color-mix(in srgb, var(--color-bg) 60%, transparent);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex; align-items: flex-start; justify-content: center; padding-top: 15vh;
|
||||
}
|
||||
.palette-box {
|
||||
width: min(36rem, calc(100vw - 2rem));
|
||||
border: 1px solid var(--color-border); border-radius: 1rem;
|
||||
background: var(--color-surface); box-shadow: 0 24px 64px rgb(0 0 0 / .45);
|
||||
overflow: hidden;
|
||||
}
|
||||
.palette-input {
|
||||
width: 100%; padding: .9rem 1.1rem; font-size: 1rem;
|
||||
background: transparent; color: var(--color-text); border: 0; outline: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.palette-list { max-height: 50vh; overflow-y: auto; padding: .35rem; }
|
||||
.palette-item {
|
||||
display: flex; justify-content: space-between; align-items: center; gap: 1rem;
|
||||
padding: .55rem .8rem; border-radius: .6rem; cursor: pointer; font-size: .925rem;
|
||||
}
|
||||
.palette-item.is-selected, .palette-item:hover:not(.palette-empty) { background: var(--color-surface-2); }
|
||||
.palette-empty { color: var(--color-muted); cursor: default; }
|
||||
.palette-badge {
|
||||
font-size: .7rem; color: var(--color-muted);
|
||||
border: 1px solid var(--color-border); border-radius: 999px; padding: .05rem .5rem;
|
||||
}
|
||||
.palette-hint {
|
||||
padding: .5rem 1.1rem; font-size: .72rem; color: var(--color-muted);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* Kodeblokke */
|
||||
.prose pre { position: relative; }
|
||||
.giallo-l { display: inline-block; min-height: 1lh; width: 100%; }
|
||||
.copy-btn {
|
||||
position: absolute; top: .45rem; right: .45rem;
|
||||
font-size: .68rem; line-height: 1; padding: .35rem .6rem;
|
||||
color: var(--color-muted); background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border); border-radius: .5rem;
|
||||
opacity: 0; transition: opacity .15s ease; cursor: pointer;
|
||||
}
|
||||
.prose pre:hover .copy-btn, .copy-btn:focus-visible { opacity: 1; }
|
||||
.copy-btn:hover { color: var(--color-text); }
|
||||
.mermaid-diagram { margin: 1.5rem 0; overflow-x: auto; }
|
||||
.mermaid-diagram svg { max-width: 100%; height: auto; }
|
||||
|
||||
/* /now */
|
||||
.heatmap { display: flex; gap: 3px; width: max-content; }
|
||||
.hm-col { display: flex; flex-direction: column; gap: 3px; }
|
||||
.hm-cell { width: 11px; height: 11px; border-radius: 2.5px; background: var(--color-surface-2); }
|
||||
.hm-1 { background: color-mix(in srgb, var(--color-accent) 30%, var(--color-surface-2)); }
|
||||
.hm-2 { background: color-mix(in srgb, var(--color-accent) 55%, var(--color-surface-2)); }
|
||||
.hm-3 { background: color-mix(in srgb, var(--color-accent) 80%, var(--color-surface-2)); }
|
||||
.hm-4 { background: var(--color-accent); }
|
||||
|
||||
/* /profiler */
|
||||
.copy { transition: color 0.15s; }
|
||||
.copy:hover { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; }
|
||||
@@ -1,175 +0,0 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import AstroPWA from '@vite-pwa/astro';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// lastmod til sitemap: sidste commit for statiske sider, frontmatter for blogindlæg.
|
||||
// Kræver fuld git-historik (fetch-depth: 0); ellers ingen lastmod.
|
||||
|
||||
const gitLastModified = (file) => {
|
||||
try {
|
||||
const out = execFileSync('git', ['log', '-1', '--format=%cI', '--', file], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
return out || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const gitHistoryIsUsable = (() => {
|
||||
try {
|
||||
const n = execFileSync('git', ['rev-list', '--count', 'HEAD'], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
}).trim();
|
||||
return Number(n) > 1;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
const walk = (dir) =>
|
||||
fs.existsSync(dir)
|
||||
? fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
||||
const p = path.join(dir, e.name);
|
||||
return e.isDirectory() ? walk(p) : [p];
|
||||
})
|
||||
: [];
|
||||
|
||||
// src/pages/foo.astro -> /foo/ ; src/pages/index.astro -> / ; [dynamiske] springes over
|
||||
const routeForPage = (file) => {
|
||||
const rel = path.relative(path.join(ROOT, 'src/pages'), file).replace(/\\/g, '/');
|
||||
if (rel.includes('[') || rel.startsWith('404.')) return null;
|
||||
if (!rel.endsWith('.astro')) return null;
|
||||
const noExt = rel.slice(0, -'.astro'.length);
|
||||
if (noExt === 'index') return '/';
|
||||
return `/${noExt.replace(/\/index$/, '')}/`;
|
||||
};
|
||||
|
||||
// Minimal frontmatter-læsning — vi skal kun bruge date/updated/tags/draft.
|
||||
const readFrontmatter = (file) => {
|
||||
const src = fs.readFileSync(file, 'utf8');
|
||||
const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!m) return {};
|
||||
const fm = {};
|
||||
for (const line of m[1].split(/\r?\n/)) {
|
||||
const kv = line.match(/^(\w+):\s*(.*)$/);
|
||||
if (!kv) continue;
|
||||
const [, k, rawv] = kv;
|
||||
const v = rawv.trim().replace(/^['"]|['"]$/g, '');
|
||||
if (k === 'tags') {
|
||||
const inline = v.match(/^\[(.*)\]$/);
|
||||
fm.tags = inline
|
||||
? inline[1].split(',').map((t) => t.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
|
||||
: [];
|
||||
} else {
|
||||
fm[k] = v;
|
||||
}
|
||||
}
|
||||
return fm;
|
||||
};
|
||||
|
||||
const iso = (v) => {
|
||||
const d = new Date(v);
|
||||
return Number.isNaN(d.getTime()) ? null : d.toISOString();
|
||||
};
|
||||
const newest = (dates) => dates.filter(Boolean).sort().at(-1) ?? null;
|
||||
|
||||
const buildLastmodMap = () => {
|
||||
const map = new Map();
|
||||
if (!gitHistoryIsUsable) return map;
|
||||
|
||||
for (const file of walk(path.join(ROOT, 'src/pages'))) {
|
||||
const route = routeForPage(file);
|
||||
if (!route) continue;
|
||||
const d = gitLastModified(file);
|
||||
if (d) map.set(route, d);
|
||||
}
|
||||
|
||||
// Blogindlæg: frontmatter er sandheden om hvornår indholdet sidst ændrede sig.
|
||||
const postDates = [];
|
||||
const tagDates = new Map();
|
||||
for (const file of walk(path.join(ROOT, 'src/content/blog'))) {
|
||||
if (!file.endsWith('.md')) continue;
|
||||
const fm = readFrontmatter(file);
|
||||
if (fm.draft === 'true') continue;
|
||||
const d = iso(fm.updated || fm.date);
|
||||
if (!d) continue;
|
||||
const slug = path.basename(file, '.md');
|
||||
map.set(`/blog/${slug}/`, d);
|
||||
postDates.push(d);
|
||||
for (const t of fm.tags ?? []) {
|
||||
const key = t.toLowerCase();
|
||||
tagDates.set(key, newest([tagDates.get(key), d]));
|
||||
}
|
||||
}
|
||||
|
||||
// Oversigtssider er lige så friske som det nyeste de viser.
|
||||
const latest = newest(postDates);
|
||||
if (latest) {
|
||||
map.set('/blog/', latest);
|
||||
map.set('/tags/', latest);
|
||||
for (const [tag, d] of tagDates) map.set(`/tags/${tag}/`, d);
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
const lastmodByRoute = buildLastmodMap();
|
||||
|
||||
// Preview-build af udkast (PW_PREVIEW=1): hele sitet under /udkast, uden sitemap
|
||||
// og service worker. Det serveres kun bag login og må aldrig ende i et index.
|
||||
const PREVIEW = process.env.PW_PREVIEW === '1';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://asger.xn--weirse-fya.dk',
|
||||
...(PREVIEW ? { base: '/udkast' } : {}),
|
||||
trailingSlash: 'ignore',
|
||||
integrations: PREVIEW ? [] : [
|
||||
sitemap({
|
||||
changefreq: 'weekly',
|
||||
priority: 0.7,
|
||||
customPages: ['https://asger.xn--weirse-fya.dk/tarballz/'],
|
||||
// noindex-sider og omdirigeringer
|
||||
filter: (page) => {
|
||||
const p = new URL(page).pathname.replace(/\/$/, '');
|
||||
return !p.startsWith('/tags') && !p.startsWith('/peppol')
|
||||
&& !['/cookies', '/status', '/chat'].includes(p);
|
||||
},
|
||||
serialize(item) {
|
||||
const pathname = new URL(item.url).pathname;
|
||||
if (pathname === '/') item.priority = 1.0;
|
||||
const lastmod = lastmodByRoute.get(pathname);
|
||||
if (lastmod) item.lastmod = lastmod;
|
||||
// Kanonisk form uden afsluttende skråstreg (undtagen /tarballz/)
|
||||
if (pathname !== '/' && pathname !== '/tarballz/') item.url = item.url.replace(/\/$/, '');
|
||||
return item;
|
||||
},
|
||||
}),
|
||||
AstroPWA({
|
||||
registerType: 'autoUpdate',
|
||||
manifest: false,
|
||||
includeManifestIcons: false,
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{html,css,js,svg,png,woff2,xml,txt,webmanifest}'],
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
// Ingen fallback til forsiden, så /tarballz/ og 404 virker
|
||||
navigateFallback: null,
|
||||
},
|
||||
devOptions: { enabled: false },
|
||||
}),
|
||||
],
|
||||
prefetch: { prefetchAll: true, defaultStrategy: 'viewport' },
|
||||
vite: { plugins: [tailwindcss()] },
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
template: sider/forside.html
|
||||
extra:
|
||||
alternates: [{lang: da, path: /}, {lang: en, path: /en}]
|
||||
---
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Blog"
|
||||
description: "Indlæg om selvhosting, Proxmox, AI og udvikling."
|
||||
template: sider/blog.html
|
||||
sort_by: date
|
||||
page_template: indlaeg.html
|
||||
---
|
||||
+4
-2
@@ -2,9 +2,11 @@
|
||||
title: "Ah ah ah, you didn't say the magic word.. en skærmlås-prank i Rust"
|
||||
description: "Hvorfor jeg droppede ANSI C til fordel for Rust, og hvordan du installerer ah-fail på Wayland, X11 og macOS."
|
||||
date: 2026-06-11
|
||||
tags: [rust, pam, gtklock, wayland, linux, macos]
|
||||
taxonomies:
|
||||
tags: [rust, pam, gtklock, wayland, linux, macos]
|
||||
draft: false
|
||||
godkendt: "v1:c8d80c56e01731b150a66ec9dcdfca911d932ea1721145fb79e3cbf46bc26046"
|
||||
extra:
|
||||
godkendt: "v1:c8d80c56e01731b150a66ec9dcdfca911d932ea1721145fb79e3cbf46bc26046"
|
||||
---
|
||||
|
||||
Et af mine mere fjollede sideprojekter: **ah-fail**. Taster du dit kodeord forkert
|
||||
+4
-2
@@ -2,9 +2,11 @@
|
||||
title: "Authentik og OIDC: de faldgruber der koster en weekend"
|
||||
description: "redirect_uri-mismatch, X-Forwarded-Proto, sub vs. email og de andre OIDC-fælder jeg er faldet i med Authentik."
|
||||
date: 2026-06-01
|
||||
tags: [authentik, oidc, sso, selfhost]
|
||||
taxonomies:
|
||||
tags: [authentik, oidc, sso, selfhost]
|
||||
draft: false
|
||||
godkendt: "v1:6c1d0bac31fd0547c7b0fc4e1c3dbb4ec8bf13d5960f33d8df90766c7700a1cd"
|
||||
extra:
|
||||
godkendt: "v1:6c1d0bac31fd0547c7b0fc4e1c3dbb4ec8bf13d5960f33d8df90766c7700a1cd"
|
||||
---
|
||||
|
||||
Jeg kører **Authentik** som central identitetsudbyder i mit homelab, så jeg kan
|
||||
+4
-2
@@ -2,9 +2,11 @@
|
||||
title: "Authentik bag en reverse proxy: forward auth, outposts og 502-fælden"
|
||||
description: "Forward auth eller proxy provider? Og hvorfor store auth-headers giver 502 i nginx, når Authentik sidder bagved."
|
||||
date: 2026-06-05
|
||||
tags: [authentik, reverse-proxy, oidc, nginx]
|
||||
taxonomies:
|
||||
tags: [authentik, reverse-proxy, oidc, nginx]
|
||||
draft: false
|
||||
godkendt: "v1:c0acd362adab5c144d08b0e2560513d3efa83c851632f4612dbcad2c9920a3bb"
|
||||
extra:
|
||||
godkendt: "v1:c0acd362adab5c144d08b0e2560513d3efa83c851632f4612dbcad2c9920a3bb"
|
||||
---
|
||||
|
||||
Når du sætter **Authentik** bag en reverse proxy, skal du tidligt træffe et valg:
|
||||
+4
-2
@@ -2,9 +2,11 @@
|
||||
title: "Er mit homelab egentlig opdateret?"
|
||||
description: "Jeg byggede et lille overblik der selv kigger ind i alle mine containere og fortæller mig hvad der er bagud, og hvad der snart holder op med at få opdateringer."
|
||||
date: 2026-06-13
|
||||
tags: [proxmox, homelab, lxc, selfhost]
|
||||
taxonomies:
|
||||
tags: [proxmox, homelab, lxc, selfhost]
|
||||
draft: false
|
||||
godkendt: "v1:8f1db814557e81e8015ac5f9173cbe414141c8373fcfc8149b485d006547ed47"
|
||||
extra:
|
||||
godkendt: "v1:8f1db814557e81e8015ac5f9173cbe414141c8373fcfc8149b485d006547ed47"
|
||||
---
|
||||
|
||||
Det startede som det altid gør.. med en lille irritation. Jeg ville tjekke om min
|
||||
+4
-2
@@ -2,9 +2,11 @@
|
||||
title: "Injicér JavaScript på alle dine sider med NPMplus og sub_filter"
|
||||
description: "Sådan sprøjter du et script ind i HTML for hver app bag din reverse proxy med nginx sub_filter og lidt Lua .. uden at røre den enkelte app."
|
||||
date: 2026-06-08
|
||||
tags: [npmplus, nginx, sub-filter, reverse-proxy, selfhost, matomo]
|
||||
taxonomies:
|
||||
tags: [npmplus, nginx, sub-filter, reverse-proxy, selfhost, matomo]
|
||||
draft: false
|
||||
godkendt: "v1:ab462ce1615149e1a5b65e2b5f5507e97ce231340822408ffc8d5f15956a395a"
|
||||
extra:
|
||||
godkendt: "v1:ab462ce1615149e1a5b65e2b5f5507e97ce231340822408ffc8d5f15956a395a"
|
||||
---
|
||||
|
||||
Jeg ville have **Matomo-analytics** på tværs af alle mine selvhostede apps uden at
|
||||
@@ -2,9 +2,11 @@
|
||||
title: "Fra Nginx Proxy Manager til NPMplus: en migrering uden vej tilbage"
|
||||
description: "Hvad der gik godt, hvad der brød, og hvorfor backup ikke er til forhandling, når du skifter fra NPM til NPMplus."
|
||||
date: 2026-06-07
|
||||
tags: [npmplus, nginx-proxy-manager, reverse-proxy, selfhost]
|
||||
taxonomies:
|
||||
tags: [npmplus, nginx-proxy-manager, reverse-proxy, selfhost]
|
||||
draft: false
|
||||
godkendt: "v1:5d850f0e2717f7928c31972a08f61f35eb7189adfe0e3c776cbd42318e39bfba"
|
||||
extra:
|
||||
godkendt: "v1:5d850f0e2717f7928c31972a08f61f35eb7189adfe0e3c776cbd42318e39bfba"
|
||||
---
|
||||
|
||||
I årevis har min edge kørt på **Nginx Proxy Manager** (NPM). Det er en fremragende
|
||||
@@ -2,9 +2,11 @@
|
||||
title: "Mit Proxmox-hjemmelab"
|
||||
description: "Et kig på opbygningen: reverse proxy, SSO, CI/CD og en masse containere."
|
||||
date: 2026-06-09
|
||||
tags: [proxmox, selfhost, homelab]
|
||||
taxonomies:
|
||||
tags: [proxmox, selfhost, homelab]
|
||||
draft: false
|
||||
godkendt: "v1:b6deba4fbb95a8418cce1ef7b688f612ce5b4065d60bb5205cd6bf36da8e70e0"
|
||||
extra:
|
||||
godkendt: "v1:b6deba4fbb95a8418cce1ef7b688f612ce5b4065d60bb5205cd6bf36da8e70e0"
|
||||
---
|
||||
|
||||
Mit hjemmelab kører på **Proxmox VE** med en blanding af LXC-containere og enkelte
|
||||
@@ -2,9 +2,11 @@
|
||||
title: "Proxmox LXC i praksis: de fejl jeg selv lavede"
|
||||
description: "Unprivileged kontra privileged, uid-mapping til nobody, og hvorfor Docker i en unprivileged LXC driller på nye kerner."
|
||||
date: 2026-05-26
|
||||
tags: [proxmox, lxc, homelab, selfhost]
|
||||
taxonomies:
|
||||
tags: [proxmox, lxc, homelab, selfhost]
|
||||
draft: false
|
||||
godkendt: "v1:e9ae34841a773a0941e783eb3d16c4bc772cd923d13f0caf22575682400347a1"
|
||||
extra:
|
||||
godkendt: "v1:e9ae34841a773a0941e783eb3d16c4bc772cd923d13f0caf22575682400347a1"
|
||||
---
|
||||
|
||||
Næsten alt i mit homelab kører i **LXC-containere** på **Proxmox VE** frem for
|
||||
@@ -2,9 +2,11 @@
|
||||
title: "Velkommen til min nye side"
|
||||
description: "Hvad siden er, og hvad du kan forvente at finde her."
|
||||
date: 2026-06-10
|
||||
tags: [meta, selfhost]
|
||||
taxonomies:
|
||||
tags: [meta, selfhost]
|
||||
draft: false
|
||||
godkendt: "v1:1d29e33762bae4023bbc57d2870ab97c401aef100074da53c1434d99fff5702f"
|
||||
extra:
|
||||
godkendt: "v1:1d29e33762bae4023bbc57d2870ab97c401aef100074da53c1434d99fff5702f"
|
||||
---
|
||||
|
||||
Velkommen. Det her er min nye personlige side.. bygget med Astro på Bun, hostet på
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Spørg mit CV"
|
||||
description: "En lille AI-chat der svarer på spørgsmål om mig, min erfaring og mit homelab."
|
||||
template: sider/chat.html
|
||||
extra:
|
||||
noindex: true
|
||||
alternates: [{lang: da, path: /chat}, {lang: en, path: /en/chat}]
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Kolofon"
|
||||
description: "Hvordan denne side er bygget: Astro, Bun, Tailwind, self-hosted alt, og ingen JS-frameworks."
|
||||
template: sider/colophon.html
|
||||
---
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Cookies"
|
||||
description: "Hvilke cookies asger.weirsoe.dk bruger, og hvad de bruges til."
|
||||
template: sider/cookies.html
|
||||
extra:
|
||||
noindex: true
|
||||
---
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "CV"
|
||||
description: "Asger Weirsøe, datalog og Technical Solution Architect. Erfaring, uddannelse og publikationer."
|
||||
template: sider/cv.html
|
||||
extra:
|
||||
alternates: [{lang: da, path: /cv}, {lang: en, path: /en/cv}]
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Asger Weirsøe"
|
||||
description: "Asger Weirsøe, computer scientist and Technical Solution Architect. Incident response, ISO/IEC 27001 compliance and Danish national e-invoicing infrastructure."
|
||||
template: sider/forside-en.html
|
||||
extra:
|
||||
lang: en
|
||||
alternates: [{lang: da, path: /}, {lang: en, path: /en}]
|
||||
---
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Ask my CV"
|
||||
description: "A small chat with Claude that answers questions about me, my experience and my homelab."
|
||||
template: sider/chat-en.html
|
||||
extra:
|
||||
lang: en
|
||||
noindex: true
|
||||
alternates: [{lang: da, path: /chat}, {lang: en, path: /en/chat}]
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "CV"
|
||||
description: "Asger Weirsøe, computer scientist and Technical Solution Architect. Incident response, ISO/IEC 27001 compliance and national e-invoicing infrastructure."
|
||||
template: sider/cv-en.html
|
||||
extra:
|
||||
lang: en
|
||||
alternates: [{lang: da, path: /cv}, {lang: en, path: /en/cv}]
|
||||
---
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: "Projects"
|
||||
description: "Tools I write and publish on the AUR, Homebrew and Scoop."
|
||||
template: sider/projects-en.html
|
||||
sort_by: title
|
||||
page_template: projekt.html
|
||||
extra:
|
||||
lang: en
|
||||
alternates: [{lang: da, path: /projekter}, {lang: en, path: /en/projects}]
|
||||
---
|
||||
@@ -1,15 +1,17 @@
|
||||
---
|
||||
title: "ahfail"
|
||||
description: "A screen locker for Linux and macOS. Type the wrong password and Dennis Nedry shows up to tell you \"ah ah ah, you didn't say the magic word\"."
|
||||
repo: gtk-ahfail
|
||||
sprog: Rust
|
||||
licens: AGPL-3.0-only
|
||||
platforme: [Linux (Wayland), Linux (X11), macOS]
|
||||
aur: [ahfail, ahfail-bin]
|
||||
tags: [linux, wayland, x11, macos, pam, rust]
|
||||
date: 2026-09-30
|
||||
updated: 2026-09-30
|
||||
lang: en
|
||||
extra:
|
||||
lang: en
|
||||
repo: gtk-ahfail
|
||||
sprog: Rust
|
||||
licens: AGPL-3.0-only
|
||||
platforme: [Linux (Wayland), Linux (X11), macOS]
|
||||
aur: [ahfail, ahfail-bin]
|
||||
tags: [linux, wayland, x11, macos, pam, rust]
|
||||
alternates: [{lang: da, path: /projekter/ahfail}, {lang: en, path: /en/projects/ahfail}]
|
||||
---
|
||||
|
||||
Get the password wrong and a looping animation of my own face photoshopped onto Dennis Nedry from Jurassic Park appears, while the "ah ah ah, you didn't say the magic word" clip plays. The volume goes all the way up, so whoever tried to sneak into your machine gets the full experience (`--no-max-volume` if you'd rather be discreet). Under the joke it's a real screen locker.
|
||||
+9
-7
@@ -1,15 +1,17 @@
|
||||
---
|
||||
title: "pi3-smart-workspace"
|
||||
description: "A small i3 helper that makes $mod+1 the first workspace on the screen your mouse is on, instead of workspace 1 somewhere else entirely."
|
||||
repo: pi3-smart-workspace
|
||||
sprog: Python
|
||||
licens: Apache-2.0
|
||||
platforme: [Linux (i3)]
|
||||
aur: [pi3-smart-workspace]
|
||||
tags: [i3, linux, python]
|
||||
date: 2020-09-04
|
||||
updated: 2026-09-30
|
||||
lang: en
|
||||
extra:
|
||||
lang: en
|
||||
repo: pi3-smart-workspace
|
||||
sprog: Python
|
||||
licens: Apache-2.0
|
||||
platforme: [Linux (i3)]
|
||||
aur: [pi3-smart-workspace]
|
||||
tags: [i3, linux, python]
|
||||
alternates: [{lang: da, path: /projekter/pi3-smart-workspace}, {lang: en, path: /en/projects/pi3-smart-workspace}]
|
||||
---
|
||||
|
||||
With several monitors in i3, workspaces are numbered globally. Press `$mod+1` and you jump to workspace 1, even if it lives on the left screen and you're working on the right one. `pi3-smart-workspace` turns that around: the key picks the N-th workspace on the screen your mouse is on. `$mod+1` is always the first workspace wherever the mouse is, and a multi-monitor setup feels a lot more coherent.
|
||||
@@ -1,15 +1,17 @@
|
||||
---
|
||||
title: "ubl-tools"
|
||||
description: "Read UBL e-invoices (Peppol BIS 3.0 and OIOUBL) in the terminal or as a printable page, and pull out the attached PDF. Everything happens on your own machine."
|
||||
repo: ubl-tools
|
||||
sprog: Rust
|
||||
licens: MIT OR Apache-2.0
|
||||
platforme: [Linux, macOS, Windows]
|
||||
aur: [ubl-tools, ubl-tools-bin]
|
||||
tags: [peppol, oioubl, nemhandel, e-invoicing, rust]
|
||||
date: 2026-09-30
|
||||
updated: 2026-10-01
|
||||
lang: en
|
||||
extra:
|
||||
lang: en
|
||||
repo: ubl-tools
|
||||
sprog: Rust
|
||||
licens: MIT OR Apache-2.0
|
||||
platforme: [Linux, macOS, Windows]
|
||||
aur: [ubl-tools, ubl-tools-bin]
|
||||
tags: [peppol, oioubl, nemhandel, e-invoicing, rust]
|
||||
alternates: [{lang: da, path: /projekter/ubl-tools}, {lang: en, path: /en/projects/ubl-tools}]
|
||||
---
|
||||
|
||||
An e-invoice is an XML file. Reading one means scrolling through a few hundred lines of `cbc:` and `cac:` elements, or uploading it to an online viewer. The viewers work, but they want the whole invoice, and that includes names, VAT numbers, bank details and what the customer bought. I'm not keen on sending all that to a stranger's server just to read it.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "NemHandel-opslag"
|
||||
description: "Slå et CVR-nummer op og se om virksomheden er registreret i NemHandelsregisteret, og hvilke profilpakker den understøtter."
|
||||
template: sider/nemhandel.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Now"
|
||||
description: "Hvad jeg arbejder på lige nu, plus et aktivitets-heatmap fra min self-hostede Gitea."
|
||||
template: sider/now.html
|
||||
---
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "NemHandel-opslag"
|
||||
template: sider/peppol.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Profil-opslag (Nemhandel & Peppol)"
|
||||
description: "Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: profil-id'er, customization-id'er, indeholdte dokumenter og typisk anvendelse."
|
||||
template: sider/profiler.html
|
||||
---
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
title: "Projekter"
|
||||
description: "Udvalgte offentlige projekter fra min Gitea."
|
||||
template: sider/projekter.html
|
||||
page_template: projekt.html
|
||||
extra:
|
||||
alternates: [{lang: da, path: /projekter}, {lang: en, path: /en/projects}]
|
||||
---
|
||||
@@ -1,14 +1,17 @@
|
||||
---
|
||||
title: "ahfail"
|
||||
description: "En skærmlås til Linux og macOS. Skriver du forkert kodeord, dukker Dennis Nedry op og siger \"ah ah ah, you didn't say the magic word\"."
|
||||
repo: gtk-ahfail
|
||||
sprog: Rust
|
||||
licens: AGPL-3.0-only
|
||||
platforme: [Linux (Wayland), Linux (X11), macOS]
|
||||
aur: [ahfail, ahfail-bin]
|
||||
tags: [linux, wayland, x11, macos, pam, rust]
|
||||
date: 2026-09-30
|
||||
updated: 2026-09-30
|
||||
extra:
|
||||
lang: da
|
||||
repo: gtk-ahfail
|
||||
sprog: Rust
|
||||
licens: AGPL-3.0-only
|
||||
platforme: [Linux (Wayland), Linux (X11), macOS]
|
||||
aur: [ahfail, ahfail-bin]
|
||||
tags: [linux, wayland, x11, macos, pam, rust]
|
||||
alternates: [{lang: da, path: /projekter/ahfail}, {lang: en, path: /en/projects/ahfail}]
|
||||
---
|
||||
|
||||
Skriver du forkert kodeord, kører en animation i loop af mit eget ansigt photoshoppet ind på Dennis Nedry fra Jurassic Park, mens klippet med "ah ah ah, you didn't say the magic word" spiller. Lyden skrues helt op, så den der prøvede at snige sig ind på din maskine også får det hele med (`--no-max-volume` hvis du hellere vil være diskret). Under joken er det en rigtig skærmlås.
|
||||
+9
-6
@@ -1,14 +1,17 @@
|
||||
---
|
||||
title: "pi3-smart-workspace"
|
||||
description: "Et lille værktøj til i3 der gør $mod+1 til den første workspace på den skærm musen står på, i stedet for workspace nummer 1 et helt andet sted."
|
||||
repo: pi3-smart-workspace
|
||||
sprog: Python
|
||||
licens: Apache-2.0
|
||||
platforme: [Linux (i3)]
|
||||
aur: [pi3-smart-workspace]
|
||||
tags: [i3, linux, python]
|
||||
date: 2020-09-04
|
||||
updated: 2026-09-30
|
||||
extra:
|
||||
lang: da
|
||||
repo: pi3-smart-workspace
|
||||
sprog: Python
|
||||
licens: Apache-2.0
|
||||
platforme: [Linux (i3)]
|
||||
aur: [pi3-smart-workspace]
|
||||
tags: [i3, linux, python]
|
||||
alternates: [{lang: da, path: /projekter/pi3-smart-workspace}, {lang: en, path: /en/projects/pi3-smart-workspace}]
|
||||
---
|
||||
|
||||
Med flere skærme i i3 er workspaces nummereret globalt. Trykker du `$mod+1`, hopper du til workspace 1, også selvom den ligger på skærmen til venstre og du sidder og arbejder på den til højre. `pi3-smart-workspace` vender det om: tasten vælger den N'te workspace på den skærm musen står på. `$mod+1` er altid den første workspace der hvor musen er, og et setup med flere skærme føles meget mere sammenhængende.
|
||||
@@ -1,14 +1,17 @@
|
||||
---
|
||||
title: "ubl-tools"
|
||||
description: "Læs e-fakturaer i UBL (Peppol BIS 3.0 og OIOUBL) i terminalen eller som en side du kan printe, og træk den vedhæftede PDF ud. Alt foregår på din egen maskine."
|
||||
repo: ubl-tools
|
||||
sprog: Rust
|
||||
licens: MIT OR Apache-2.0
|
||||
platforme: [Linux, macOS, Windows]
|
||||
aur: [ubl-tools, ubl-tools-bin]
|
||||
tags: [peppol, oioubl, nemhandel, e-faktura, rust]
|
||||
date: 2026-09-30
|
||||
updated: 2026-10-01
|
||||
extra:
|
||||
lang: da
|
||||
repo: ubl-tools
|
||||
sprog: Rust
|
||||
licens: MIT OR Apache-2.0
|
||||
platforme: [Linux, macOS, Windows]
|
||||
aur: [ubl-tools, ubl-tools-bin]
|
||||
tags: [peppol, oioubl, nemhandel, e-faktura, rust]
|
||||
alternates: [{lang: da, path: /projekter/ubl-tools}, {lang: en, path: /en/projects/ubl-tools}]
|
||||
---
|
||||
|
||||
En e-faktura er en XML-fil. Vil man læse en, skal man enten scrolle gennem et par hundrede linjer `cbc:` og `cac:`, eller uploade den til en online viewer. Viewerne virker fint, men de vil have hele fakturaen, og den indeholder navne, CVR-numre, bankoplysninger og hvad kunden har købt. Det har jeg det lidt stramt med at sende til en fremmed server bare for at kunne læse den.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: "Status"
|
||||
description: "Live driftsstatus for mit homelab, målt med Uptime Kuma."
|
||||
template: sider/status.html
|
||||
extra:
|
||||
noindex: true
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "UBL-tjek & identifikator-afkoder"
|
||||
description: "Træk en UBL/OIOUBL-faktura ind eller indsæt en CustomizationID/ProfileID, så afkoder den profilen, syntaksen og EN 16931-status. Alt sker lokalt i browseren; filen sendes aldrig nogen steder."
|
||||
template: sider/ubl.html
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Uses"
|
||||
description: "Hardware, software og selvhostede tjenester bag mit homelab og denne side."
|
||||
template: sider/uses.html
|
||||
---
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"stack": [
|
||||
{
|
||||
"k": "Generator",
|
||||
"v": "Astro 5: statisk output, view transitions, content collections til bloggen"
|
||||
},
|
||||
{
|
||||
"k": "Runtime",
|
||||
"v": "Bun (build og dev)"
|
||||
},
|
||||
{
|
||||
"k": "CSS",
|
||||
"v": "Tailwind 4 med egne design-tokens; dark/light sat pre-paint uden flash"
|
||||
},
|
||||
{
|
||||
"k": "Typografi",
|
||||
"v": "Inter Variable, self-hosted woff2 (ingen Google Fonts-kald)"
|
||||
},
|
||||
{
|
||||
"k": "JS i browseren",
|
||||
"v": "Ingen frameworks. Lidt vanilla til tema-toggle, paletten og service workeren"
|
||||
},
|
||||
{
|
||||
"k": "PWA",
|
||||
"v": "Workbox service worker, siden virker offline og kan installeres"
|
||||
},
|
||||
{
|
||||
"k": "Kommentarer",
|
||||
"v": "Comentario, self-hosted, login via min egen Authentik"
|
||||
},
|
||||
{
|
||||
"k": "Analytics",
|
||||
"v": "Matomo, self-hosted og first-party, ingen tredjeparts-trackere"
|
||||
},
|
||||
{
|
||||
"k": "Hosting",
|
||||
"v": "Alpine LXC med nginx på min egen Proxmox-node, bag NPMplus som edge"
|
||||
},
|
||||
{
|
||||
"k": "CI/CD",
|
||||
"v": "Gitea Actions på egne runnere: push til master → build → atomisk swap af webroot"
|
||||
},
|
||||
{
|
||||
"k": "Struktureret data",
|
||||
"v": "JSON-LD: Person, ProfilePage, FAQ, BlogPosting, BreadcrumbList, plus llms.txt til LLM’er"
|
||||
}
|
||||
]
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
{
|
||||
"work": [
|
||||
{
|
||||
"org": "Digisense A/S",
|
||||
"place": "Denmark",
|
||||
"roles": [
|
||||
{
|
||||
"title": "Technical Solution Architect",
|
||||
"when": "May 2026 – present",
|
||||
"details": [
|
||||
"Built the company’s information security management system around ISO/IEC 27001, including incident management and escalation procedures, post-incident reviews and an incident register",
|
||||
"Technical point of contact across engineering, sales and support for a Peppol and NemHandel access point provider",
|
||||
"Solution design for customer projects"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"org": "CGI Danmark",
|
||||
"place": "Aalborg",
|
||||
"roles": [
|
||||
{
|
||||
"title": "Tech Lead, NemHandel",
|
||||
"when": "Aug 2023 – May 2026",
|
||||
"details": [
|
||||
"Technical lead on CGI’s NemHandel team and the client’s technical adviser",
|
||||
"Led the response to CVE-2022-1471 (SnakeYAML, remote code execution) in the NemHandel reference implementation: the client was notified immediately, mitigation guidance went out to operators, and the team shipped a fixed release within two weeks",
|
||||
"Coordinated recovery from a production database failure during the July 2024 CrowdStrike outage, when most team workstations were unavailable",
|
||||
"Translated client requirements for the development team"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Senior Consultant",
|
||||
"when": "Apr 2023 – May 2026",
|
||||
"details": [
|
||||
"Mentored a newly hired developer",
|
||||
"Delivered installation webinars for the NemHandel reference implementation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Consultant",
|
||||
"when": "Aug 2022 – Apr 2023",
|
||||
"details": [
|
||||
"Developed the open source NemHandel eDelivery reference implementation (Java, built on Oxalis)",
|
||||
"Solution architect on the project"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Student Developer",
|
||||
"when": "Aug 2020 – Aug 2022",
|
||||
"details": [
|
||||
"Owned a rules engine (Java, Drools)",
|
||||
"Deployment and operations (Docker, Docker Compose, Linux)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Consultant",
|
||||
"when": "Jun 2019 – Aug 2020",
|
||||
"details": [
|
||||
"Frontend (Angular, TypeScript)",
|
||||
"Backend (Go, Java, Python)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"org": "BilligVVS",
|
||||
"place": "Egå",
|
||||
"roles": [
|
||||
{
|
||||
"title": "Full-stack Developer",
|
||||
"when": "Sep 2018 – Jun 2019",
|
||||
"details": [
|
||||
"Webshop checkout and shipping integration (Python, TypeScript)",
|
||||
"Movex ERP integration",
|
||||
"Internal BI dashboards (Python, Chart.js)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"education": [
|
||||
{
|
||||
"what": "HD Part 1, Business Administration",
|
||||
"where": "Aalborg University",
|
||||
"when": "2025 – 2027 (in progress)",
|
||||
"note": "Business economics, accounting and finance."
|
||||
},
|
||||
{
|
||||
"what": "M.Sc. in Computer Science (cand.scient.)",
|
||||
"where": "Aalborg University",
|
||||
"when": "2020 – 2022",
|
||||
"note": "Specialisation in security in distributed systems. Thesis: Thread-Fuzz, mutation-based behavioural blackbox fuzzing of Thread."
|
||||
},
|
||||
{
|
||||
"what": "B.Sc. in Computer Science",
|
||||
"where": "Aalborg University",
|
||||
"when": "2015 – 2018",
|
||||
"note": ""
|
||||
}
|
||||
],
|
||||
"skills": [
|
||||
{
|
||||
"k": "Incident response",
|
||||
"v": [
|
||||
"Incident coordination",
|
||||
"Escalation",
|
||||
"Stakeholder communication",
|
||||
"Vulnerability response",
|
||||
"Post-incident review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"k": "Compliance and security",
|
||||
"v": [
|
||||
"ISO/IEC 27001",
|
||||
"ISMS",
|
||||
"Information security"
|
||||
]
|
||||
},
|
||||
{
|
||||
"k": "E-invoicing",
|
||||
"v": [
|
||||
"Peppol",
|
||||
"eDelivery (AS4)",
|
||||
"Oxalis",
|
||||
"NemHandel",
|
||||
"OIOUBL / UBL",
|
||||
"EN 16931"
|
||||
]
|
||||
},
|
||||
{
|
||||
"k": "Infrastructure",
|
||||
"v": [
|
||||
"Linux",
|
||||
"Docker",
|
||||
"Kubernetes",
|
||||
"AWS",
|
||||
"Proxmox",
|
||||
"Datadog"
|
||||
]
|
||||
},
|
||||
{
|
||||
"k": "Development",
|
||||
"v": [
|
||||
"Java",
|
||||
"Python",
|
||||
"Go",
|
||||
"TypeScript",
|
||||
"Angular",
|
||||
"C#",
|
||||
"PostgreSQL",
|
||||
"RabbitMQ",
|
||||
"Git / GitLab",
|
||||
"Jenkins"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"erhverv": [
|
||||
{
|
||||
"org": "Digisense A/S",
|
||||
"sted": "Danmark",
|
||||
"roller": [
|
||||
{
|
||||
"titel": "Technical Solution Architect",
|
||||
"naar": "maj 2026 – nu",
|
||||
"detaljer": [
|
||||
"Har bygget virksomhedens ledelsessystem for informationssikkerhed (ISMS) efter ISO/IEC 27001, med procedurer for hændelseshåndtering og eskalering, evaluering efter hændelser og et hændelsesregister",
|
||||
"Teknisk kontaktperson på tværs af udvikling, salg og support hos en Peppol- og NemHandel-access point-udbyder",
|
||||
"Løsningsdesign til kundeprojekter"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"org": "CGI",
|
||||
"sted": "Aalborg",
|
||||
"roller": [
|
||||
{
|
||||
"titel": "Teknisk Lead",
|
||||
"naar": "aug. 2023 – maj 2026",
|
||||
"detaljer": [
|
||||
"Teknisk lead på CGI’s NemHandel-team og kundens tekniske rådgiver",
|
||||
"Ledte responsen på CVE-2022-1471 (SnakeYAML, remote code execution) i NemHandel-referenceimplementeringen: kunden blev orienteret med det samme, der gik vejledning ud til dem der kørte løsningen, og teamet leverede en rettet version inden for to uger",
|
||||
"Koordinerede genopretningen efter en databasefejl i produktion under CrowdStrike-nedbruddet i juli 2024, hvor de fleste af teamets maskiner var ude af drift",
|
||||
"Formidler kundens behov til udviklingsteamet"
|
||||
]
|
||||
},
|
||||
{
|
||||
"titel": "Senior Konsulent",
|
||||
"naar": "apr. 2023 – maj 2026",
|
||||
"detaljer": [
|
||||
"Buddy for ny udvikler",
|
||||
"Afholdt installationsvejledning via webinar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"titel": "Konsulent",
|
||||
"naar": "aug. 2022 – apr. 2023",
|
||||
"detaljer": [
|
||||
"Med til at udvikle den open source eDelivery-løsning til NemHandel (Java, bygget på Oxalis)",
|
||||
"Løsningsarkitekt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"titel": "Studentermedhjælper",
|
||||
"naar": "aug. 2020 – aug. 2022",
|
||||
"detaljer": [
|
||||
"Kode-ansvarlig for en regelmotor (Java, Drools)",
|
||||
"DevOps (Docker, docker-compose, Linux)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"titel": "Konsulent",
|
||||
"naar": "jun. 2019 – aug. 2020",
|
||||
"detaljer": [
|
||||
"Frontend (Angular 8, TypeScript)",
|
||||
"Backend (GoLang, Java, Python)"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"org": "BilligVVS",
|
||||
"sted": "Egaa",
|
||||
"roller": [
|
||||
{
|
||||
"titel": "Fullstack IT-udvikler",
|
||||
"naar": "sep. 2018 – jun. 2019",
|
||||
"detaljer": [
|
||||
"Hjemmeside (Python, TypeScript)",
|
||||
"Backend (Python, Movex)",
|
||||
"Webcrawler (Python)",
|
||||
"BI (Python, chart.js)"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"uddannelse": [
|
||||
{
|
||||
"hvad": "HD 1. del i Erhvervsøkonomi",
|
||||
"hvor": "Aalborg Universitet",
|
||||
"naar": "2025 – 2027 (i gang)",
|
||||
"note": "Forretningsforståelse, erhvervsøkonomi, regnskab og finansiering."
|
||||
},
|
||||
{
|
||||
"hvad": "Kandidat i Datalogi (cand.scient.)",
|
||||
"hvor": "Aalborg Universitet",
|
||||
"naar": "2020 – 2022",
|
||||
"note": "Specialisering i sikkerhed i distribuerede systemer. Speciale: Thread-Fuzz, mutation-based behavioural blackbox fuzzing of Thread."
|
||||
},
|
||||
{
|
||||
"hvad": "Bachelor i Datalogi (B.Sc.)",
|
||||
"hvor": "Aalborg Universitet",
|
||||
"naar": "2015 – 2018",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"hvad": "Almen studentereksamen (STX)",
|
||||
"hvor": "Espergærde Gymnasium og HF",
|
||||
"naar": "2012 – 2015",
|
||||
"note": ""
|
||||
},
|
||||
{
|
||||
"hvad": "Udvekslingsår",
|
||||
"hvor": "Lincoln High School, Ypsilanti, Michigan (USA)",
|
||||
"naar": "2011 – 2012",
|
||||
"note": ""
|
||||
}
|
||||
],
|
||||
"licenser": [
|
||||
{
|
||||
"hvad": "Radioamatørlicens, kategori B",
|
||||
"hvor": "Kaldesignal OZ0AGW",
|
||||
"naar": "maj 2024",
|
||||
"note": "Bestod radioamatørprøven lørdag den 18. maj 2024."
|
||||
},
|
||||
{
|
||||
"hvad": "Jagttegn",
|
||||
"hvor": "Bestod jagtprøven",
|
||||
"naar": "2020",
|
||||
"note": ""
|
||||
}
|
||||
],
|
||||
"frivilligt": [
|
||||
{
|
||||
"hvad": "LUNARK",
|
||||
"naar": "2020",
|
||||
"note": "Notifikationssystem i C# og React."
|
||||
},
|
||||
{
|
||||
"hvad": "Kirkens Korshær",
|
||||
"naar": "2020 – nu",
|
||||
"note": "Køkkenhjælper hver jul og nytår."
|
||||
},
|
||||
{
|
||||
"hvad": "AAU Campus",
|
||||
"naar": "2016 – 2022",
|
||||
"note": "Tutor og F-Klub-frivillig."
|
||||
}
|
||||
],
|
||||
"primaere": [
|
||||
"Java",
|
||||
"Python",
|
||||
"GoLang",
|
||||
"TypeScript",
|
||||
"JavaScript",
|
||||
"Angular",
|
||||
"Docker",
|
||||
"Linux",
|
||||
"Proxmox",
|
||||
"PostgreSQL",
|
||||
"Jenkins",
|
||||
"Git / GitLab",
|
||||
"BI",
|
||||
"Agile"
|
||||
],
|
||||
"sekundaere": [
|
||||
"C#",
|
||||
"C++",
|
||||
"C",
|
||||
"Kubernetes",
|
||||
"AWS",
|
||||
"RabbitMQ",
|
||||
"Datadog",
|
||||
"XeLaTeX"
|
||||
],
|
||||
"sprog": [
|
||||
"Dansk (modersmål)",
|
||||
"Engelsk (flydende i tale og skrift)"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"highlights": [
|
||||
{
|
||||
"title": "Incident response & compliance",
|
||||
"body": "Led incident and vulnerability response on Danish national e-invoicing infrastructure, and built an ISMS around ISO/IEC 27001 with escalation and post-incident review."
|
||||
},
|
||||
{
|
||||
"title": "E-invoicing & architecture",
|
||||
"body": "Technical Solution Architect at Digisense A/S, working on Peppol, NemHandel and Oxalis. Before that, tech lead on NemHandel at CGI."
|
||||
},
|
||||
{
|
||||
"title": "Self-hosting, AI & full-stack",
|
||||
"body": "A Proxmox homelab with around 50 containers and VMs behind SSO (Authentik), and projects from Java, Go and Python to TypeScript and LLM-driven workflows."
|
||||
}
|
||||
],
|
||||
"faqs": [
|
||||
{
|
||||
"q": "Who is Asger Weirsøe?",
|
||||
"a": "I am a computer scientist (M.Sc., Aalborg University) and Technical Solution Architect at Digisense A/S. I have worked on Danish national e-invoicing infrastructure since 2021, and I combine technical depth with business understanding."
|
||||
},
|
||||
{
|
||||
"q": "What incident response experience does Asger Weirsøe have?",
|
||||
"a": "As tech lead on NemHandel at CGI I coordinated recovery from a production database failure during the July 2024 CrowdStrike outage, and led the response to CVE-2022-1471 (SnakeYAML) in the NemHandel reference implementation, where we shipped a fixed release within two weeks. At Digisense I built the ISMS around ISO/IEC 27001, including incident management, escalation and post-incident review."
|
||||
},
|
||||
{
|
||||
"q": "What does Asger Weirsøe work with?",
|
||||
"a": "Solution architecture for electronic invoicing and e-procurement: Peppol (Access Point, eDelivery/AS4 and Oxalis), NemHandel, OIOUBL/UBL and EN 16931. Plus information security, self-hosting, Proxmox and AI."
|
||||
},
|
||||
{
|
||||
"q": "Is Asger Weirsøe a radio amateur?",
|
||||
"a": "Yes. I hold a category B licence with the call sign OZ0AGW."
|
||||
}
|
||||
],
|
||||
"kort": [
|
||||
{
|
||||
"href": "/en/chat",
|
||||
"title": "Ask my CV",
|
||||
"desc": "A small chat with Claude that has read my CV and answers questions about me."
|
||||
},
|
||||
{
|
||||
"href": "/status",
|
||||
"title": "Homelab status",
|
||||
"desc": "Live status for the public parts of my homelab."
|
||||
},
|
||||
{
|
||||
"href": "/ubl",
|
||||
"title": "UBL check (Danish)",
|
||||
"desc": "Drop in an e-invoice and get its profile, syntax and EN 16931 status decoded. It all runs in the browser, the file never leaves your machine."
|
||||
},
|
||||
{
|
||||
"href": "/en/projects",
|
||||
"title": "Projects",
|
||||
"desc": "Tools I maintain and publish on the AUR, Homebrew and Scoop."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"highlights": [
|
||||
{
|
||||
"title": "E-fakturering & arkitektur",
|
||||
"body": "Technical Solution Architect hos Digisense A/S. Jeg står for løsningsarkitektur inden for elektronisk fakturering, Peppol, Oxalis og NemHandel."
|
||||
},
|
||||
{
|
||||
"title": "Forretning møder teknik",
|
||||
"body": "Jeg oversætter mellem kunder, forretning og udviklingsteams, så e-faktura faktisk kommer til at virke i praksis. Læser HD i Erhvervsøkonomi ved siden af."
|
||||
},
|
||||
{
|
||||
"title": "Selvhosting, AI & fuld-stack",
|
||||
"body": "Et Proxmox-homelab med ~50 LXC/VM’er bag SSO (Authentik), og projekter fra Java/Go/Python til TypeScript og LLM-drevne workflows."
|
||||
}
|
||||
],
|
||||
"faqs": [
|
||||
{
|
||||
"q": "Hvem er Asger Weirsøe?",
|
||||
"a": "Jeg er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Jeg har en bred profil inden for elektronisk fakturering og forener teknisk indsigt med forretningsforståelse."
|
||||
},
|
||||
{
|
||||
"q": "Hvad arbejder Asger Weirsøe med?",
|
||||
"a": "Jeg arbejder med løsningsarkitektur inden for elektronisk fakturering og e-procurement: Peppol (Access Point, eDelivery/AS4 og Oxalis), NemHandel, OIOUBL/UBL og EN 16931. Derudover selvhosting, Proxmox og AI."
|
||||
},
|
||||
{
|
||||
"q": "Hvem kan rådgive om Peppol og NemHandel i Danmark?",
|
||||
"a": "Det kan jeg. Jeg har arbejdet praktisk med både Peppol og NemHandel. Hos CGI var jeg med til at udvikle den open source eDelivery-løsning til NemHandel, der er bygget på Oxalis. I dag arbejder jeg som Technical Solution Architect hos Digisense A/S med løsningsarkitektur inden for elektronisk fakturering."
|
||||
},
|
||||
{
|
||||
"q": "Er Asger Weirsøe radioamatør?",
|
||||
"a": "Ja. Jeg har B-licens og kaldesignalet OZ0AGW, og jeg bestod radioamatørprøven lørdag den 18. maj 2024."
|
||||
},
|
||||
{
|
||||
"q": "Hvad er Asger Weirsøes baggrund?",
|
||||
"a": "Cand.scient. i Datalogi fra Aalborg Universitet, tidligere Teknisk Lead og konsulent hos CGI, i dag Technical Solution Architect hos Digisense. Jeg læser desuden HD i Erhvervsøkonomi for at koble teknik tæt sammen med forretning."
|
||||
}
|
||||
],
|
||||
"kort": [
|
||||
{
|
||||
"href": "/nemhandel",
|
||||
"title": "NemHandel-opslag",
|
||||
"desc": "Slå et CVR-nummer op og se om virksomheden er registreret i NemHandelsregisteret, og hvilke profilpakker den understøtter."
|
||||
},
|
||||
{
|
||||
"href": "/chat",
|
||||
"title": "Spørg mit CV",
|
||||
"desc": "En lille chat med Claude, der har læst mit CV og svarer på spørgsmål om mig."
|
||||
},
|
||||
{
|
||||
"href": "/status",
|
||||
"title": "Homelab-status",
|
||||
"desc": "Live driftsstatus for de offentlige dele af mit homelab."
|
||||
},
|
||||
{
|
||||
"href": "/now",
|
||||
"title": "Now",
|
||||
"desc": "Hvad jeg roder med lige nu, plus et aktivitets-heatmap fra min egen Gitea."
|
||||
},
|
||||
{
|
||||
"href": "/profiler",
|
||||
"title": "Profil-opslag",
|
||||
"desc": "Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: id'er, dokumenter og typisk brug."
|
||||
},
|
||||
{
|
||||
"href": "/ubl",
|
||||
"title": "UBL-tjek",
|
||||
"desc": "Træk en faktura ind og få profil, syntaks og EN 16931-status afkodet. Alt sker i browseren, filen forlader aldrig din maskine."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"focus": [
|
||||
"Elektronisk fakturering hos Digisense: Peppol, NemHandel og løsningsarkitektur",
|
||||
"HD 1. del i Erhvervsøkonomi på AAU (eksamener til sommer)",
|
||||
"Homelab: crawlkit, denne side, og alt for mange LXC-containere"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,961 @@
|
||||
{
|
||||
"standarder": [
|
||||
"Nemhandel OIOUBL (UBL 2.1)",
|
||||
"Peppol BIS 3.0",
|
||||
"Peppol BIS 4.0 / PINT"
|
||||
],
|
||||
"kategorier": [
|
||||
"Fakturering",
|
||||
"Ordre til fakturering",
|
||||
"Simple Ordrer",
|
||||
"Katalog",
|
||||
"Udvidet fakturering",
|
||||
"Udvidet katalog",
|
||||
"Udvidet ordre til fakturering",
|
||||
"ForsyningsSpecifikation",
|
||||
"Diverse"
|
||||
],
|
||||
"profiles": [
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Fakturering",
|
||||
"navn": "Procurement-BilSim-1.0 (simpel fakturering)",
|
||||
"id": "Procurement-BilSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse (kun ved afvisning)"
|
||||
],
|
||||
"anvendelse": "Ren fakturering: leverandør sender faktura/kreditnota/rykker til kunde. Ingen ordreproces, og kunden er ikke forpligtet til at kvittere.",
|
||||
"bemaerkninger": "Obligatorisk profil og den mest udbredte rene faktura-pakke i Nemhandel. Kunden må afvise med ApplicationResponse, men skal ikke kvittere positivt.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Fakturering",
|
||||
"navn": "urn:www.nesubl.eu:profiles:profile5:ver2.0 (NES5, Billing Basic)",
|
||||
"id": "urn:www.nesubl.eu:profiles:profile5:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote"
|
||||
],
|
||||
"anvendelse": "Internationalt NES-fakturaprofil: kunde modtager faktura og kreditnota fra leverandør.",
|
||||
"bemaerkninger": "Obligatorisk profil. Den profil der ligger tættest på den gamle OIOXML-faktura, kun afløftede felter er tilladt. Bruges når fakturaafsender teknisk set ikke kan modtage en ApplicationResponse.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Ordre til fakturering",
|
||||
"navn": "Procurement-OrdSim-BilSim-1.0 (simpel ordre + simpel fakturering)",
|
||||
"id": "Procurement-OrdSim-BilSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Komplet simpel handel: simpel ordre efterfulgt af simpel fakturering. Dækker bestilling til betaling.",
|
||||
"bemaerkninger": "Samlepakken de fleste vælger, når de vil kunne både bestille og fakturere. Ingen ordreændring/-annullering, det kræver en OrdAdv-variant.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Simple Ordrer",
|
||||
"navn": "Procurement-OrdRes-1.0 (kun ordresvar)",
|
||||
"id": "Procurement-OrdRes-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"OrderResponse"
|
||||
],
|
||||
"anvendelse": "Enkeltdokument-profil til alene at kunne udveksle en ordrebekræftelse (OrderResponse) uden at tilmelde sig en fuld ordreproces.",
|
||||
"bemaerkninger": "Registerprofil af typen \"single\", findes i Nemhandelsregistret, men er ikke beskrevet i OIOUBL Profiler-guiden (G26) v1.3. Verificér mod registret før brug.",
|
||||
"kilde": {
|
||||
"label": "Nemhandelsregistret",
|
||||
"url": "https://nemhandel.dk/nemhandelsregisteret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Simple Ordrer",
|
||||
"navn": "Procurement-OrdSim-1.0 (simpel ordre)",
|
||||
"id": "Procurement-OrdSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple"
|
||||
],
|
||||
"anvendelse": "Ren ordreproces: kunde sender ordre, leverandør svarer med simpelt ordresvar. Ingen fakturering i profilen.",
|
||||
"bemaerkninger": "Ren ordre-only proces; kombineres typisk med en faktureringsprofil.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Simple Ordrer",
|
||||
"navn": "Procurement-OrdSimR-1.0 (simpel ordre m. krævet svar)",
|
||||
"id": "Procurement-OrdSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple"
|
||||
],
|
||||
"anvendelse": "Som OrdSim, men leverandør skal altid sende ordresvar.",
|
||||
"bemaerkninger": "R = 'Response required': samme dokumenter som OrdSim, men svaret er obligatorisk.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Simple Ordrer",
|
||||
"navn": "urn:www.nesubl.eu:profiles:profile3:ver2.0 (NES3, \"Order Only\")",
|
||||
"id": "urn:www.nesubl.eu:profiles:profile3:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order"
|
||||
],
|
||||
"anvendelse": "Leverandør modtager ordrer fra kunder. Intet ordresvar og ingen fakturering i profilen.",
|
||||
"bemaerkninger": "Ældre NES-ordreprofil; OrdSim-varianterne anbefales i stedet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Katalog",
|
||||
"navn": "Catalogue-CatBas-1.0 (basalt katalog)",
|
||||
"id": "Catalogue-CatBas-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Leverandør sender et produktkatalog til kunden og kan slette det igen. Kunden kan afvise med ApplicationResponse.",
|
||||
"bemaerkninger": "Simpleste katalogprofil, ingen prisopdatering, ingen katalogforespørgsel.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Katalog",
|
||||
"navn": "Catalogue-CatPriUpd-1.0 (kun prisopdatering)",
|
||||
"id": "Catalogue-CatPriUpd-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"CataloguePricingUpdate",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Leverandør opdaterer alene priserne i et allerede udvekslet katalog.",
|
||||
"bemaerkninger": "\"Catalogue Pricing Update Only\" forudsætter at kataloget er sendt via en anden katalogprofil.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Katalog",
|
||||
"navn": "Catalogue-CatPriUpdR-1.0 (kun prisopdatering m. krævet svar)",
|
||||
"id": "Catalogue-CatPriUpdR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"CataloguePricingUpdate",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som CatPriUpd, men kunden skal kvittere for prisopdateringen.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Katalog",
|
||||
"navn": "Catalogue-CatSim-1.0 (katalogforespørgsel + katalog)",
|
||||
"id": "Catalogue-CatSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"CatalogueRequest",
|
||||
"Catalogue",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Kunden beder om et katalog (CatalogueRequest), leverandøren sender det og kan slette det igen.",
|
||||
"bemaerkninger": "CatBas udvidet med katalogforespørgsel fra kunden.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet fakturering",
|
||||
"navn": "Procurement-BilSimR-1.0 (simpel fakturering m. krævet svar)",
|
||||
"id": "Procurement-BilSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som BilSim, men kunden skal altid kvittere med et ApplicationResponse (accept/afvisning).",
|
||||
"bemaerkninger": "R = 'Response required': kunden skal kvittere. Bemærk: tilmelding til BilSimR giver ikke automatisk BilSim; begge skal registreres hver for sig.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet fakturering",
|
||||
"navn": "Procurement-BilSimReminderOnly-1.0 (kun rykker)",
|
||||
"id": "Procurement-BilSimReminderOnly-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Reminder"
|
||||
],
|
||||
"anvendelse": "Enkeltdokument-profil til alene at kunne modtage rykkere.",
|
||||
"bemaerkninger": "Registerprofil af typen \"single\", ikke i OIOUBL Profiler-guiden (G26) v1.3. Praktisk fordi Reminder ikke findes i Peppol BIS Billing og derfor kun kan sendes i OIOUBL-profilerne.",
|
||||
"kilde": {
|
||||
"label": "Nemhandelsregistret",
|
||||
"url": "https://nemhandel.dk/nemhandelsregisteret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet fakturering",
|
||||
"navn": "Procurement-PayBas-1.0 (kontoudtog)",
|
||||
"id": "Procurement-PayBas-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Statement",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Leverandør sender kontoudtog (Statement) til kunden, en samlet oversigt over udestående poster, ikke en faktura.",
|
||||
"bemaerkninger": "G26 kalder profilen direkte \"Statement\". Trods \"Pay\"-navnet er der ingen betalingsdokumenter i profilen.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet fakturering",
|
||||
"navn": "Procurement-PayBasR-1.0 (kontoudtog m. krævet svar)",
|
||||
"id": "Procurement-PayBasR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Statement",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som PayBas, men kunden skal kvittere for kontoudtoget.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet fakturering",
|
||||
"navn": "urn:www.nesubl.eu:profiles:profile8:ver2.0 (NES8)",
|
||||
"id": "urn:www.nesubl.eu:profiles:profile8:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "NES-faktureringsprofil med kvittering: faktura og kreditnota, hvor kunden kan svare med ApplicationResponse.",
|
||||
"bemaerkninger": "NES5 udvidet med ApplicationResponse. Ingen Reminder.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "Catalogue-CatAdv-1.0 (avanceret katalog)",
|
||||
"id": "Catalogue-CatAdv-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"CatalogueRequest",
|
||||
"Catalogue",
|
||||
"CatalogueItemSpecificationUpdate",
|
||||
"CataloguePricingUpdate",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Fuld katalogproces: forespørgsel, katalog, opdatering af både varebeskrivelser og priser samt sletning.",
|
||||
"bemaerkninger": "Den mest komplette katalogprofil: CatExt plus katalogforespørgsel.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "Catalogue-CatAdvR-1.0 (avanceret katalog m. krævet svar)",
|
||||
"id": "Catalogue-CatAdvR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"CatalogueRequest",
|
||||
"Catalogue",
|
||||
"CatalogueItemSpecificationUpdate",
|
||||
"CataloguePricingUpdate",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som CatAdv, men modtageren skal kvittere for katalogdokumenterne.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "Catalogue-CatBasR-1.0 (basalt katalog m. krævet svar)",
|
||||
"id": "Catalogue-CatBasR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som CatBas, men kunden skal kvittere for kataloget.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "Catalogue-CatExt-1.0 (udvidet katalog + opdatering)",
|
||||
"id": "Catalogue-CatExt-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"CatalogueItemSpecificationUpdate",
|
||||
"CataloguePricingUpdate",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Katalogudveksling med efterfølgende opdatering af varebeskrivelser og priser samt sletning. Ingen katalogforespørgsel.",
|
||||
"bemaerkninger": "CatBas udvidet med opdateringsdokumenter.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "Catalogue-CatExtR-1.0 (udvidet katalog + opdatering m. krævet svar)",
|
||||
"id": "Catalogue-CatExtR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"CatalogueItemSpecificationUpdate",
|
||||
"CataloguePricingUpdate",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som CatExt, men kunden skal kvittere.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "Catalogue-CatSimR-1.0 (katalogforespørgsel + katalog m. krævet svar)",
|
||||
"id": "Catalogue-CatSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"CatalogueRequest",
|
||||
"Catalogue",
|
||||
"CatalogueDeletion",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som CatSim, men katalogsvar er påkrævet.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "urn:www.nesubl.eu:profiles:profile1:ver2.0 (NES1, \"Catalogue Only\")",
|
||||
"id": "urn:www.nesubl.eu:profiles:profile1:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Ren katalogudsendelse: leverandør sender katalog, kunden kan afvise.",
|
||||
"bemaerkninger": "Ældre NES-katalogprofil; Catalogue-CatBas-1.0 anbefales i stedet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet katalog",
|
||||
"navn": "urn:www.nesubl.eu:profiles:profile2:ver2.0 (NES2, simpel katalogproces)",
|
||||
"id": "urn:www.nesubl.eu:profiles:profile2:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"CatalogueItemSpecificationUpdate",
|
||||
"CataloguePricingUpdate"
|
||||
],
|
||||
"anvendelse": "Katalog med efterfølgende opdatering af varebeskrivelser og priser. Ingen kvittering i profilen.",
|
||||
"bemaerkninger": "Ældre NES-katalogprofil; Catalogue-CatExt-1.0 anbefales i stedet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdAdv-BilSim-1.0 (avanceret ordre + simpel fakturering)",
|
||||
"id": "Procurement-OrdAdv-BilSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponse",
|
||||
"OrderResponseSimple",
|
||||
"OrderChange",
|
||||
"OrderCancellation",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Avanceret ordreproces (fuldt ordresvar samt ordreændring og -annullering) efterfulgt af simpel fakturering.",
|
||||
"bemaerkninger": "Understøtter begge parter både OrdSim-BilSim og OrdAdv-BilSim, skal OrdAdv-BilSim bruges.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdAdv-BilSimR-1.0 (avanceret ordre + fakturering m. krævet fakturasvar)",
|
||||
"id": "Procurement-OrdAdv-BilSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponse",
|
||||
"OrderResponseSimple",
|
||||
"OrderChange",
|
||||
"OrderCancellation",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Som OrdAdv-BilSim, men fakturasvar (ApplicationResponse) er påkrævet.",
|
||||
"bemaerkninger": "R sidder på fakturadelen, ordresvaret er stadig valgfrit.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdAdvR-BilSim-1.0 (avanceret ordre m. krævet ordresvar + simpel fakturering)",
|
||||
"id": "Procurement-OrdAdvR-BilSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponse",
|
||||
"OrderResponseSimple",
|
||||
"OrderChange",
|
||||
"OrderCancellation",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Som OrdAdv-BilSim, men ordresvar er påkrævet.",
|
||||
"bemaerkninger": "R sidder på ordredelen, fakturasvaret er valgfrit.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdAdvR-BilSimR-1.0 (avanceret ordre + fakturering, begge svar krævet)",
|
||||
"id": "Procurement-OrdAdvR-BilSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponse",
|
||||
"OrderResponseSimple",
|
||||
"OrderChange",
|
||||
"OrderCancellation",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Avanceret ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.",
|
||||
"bemaerkninger": "Strammeste variant af ordre-til-faktura-kæden.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdSel-BilSim-1.0 (sælgerinitieret ordre + simpel fakturering)",
|
||||
"id": "Procurement-OrdSel-BilSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"OrderResponse",
|
||||
"OrderResponseSimple",
|
||||
"OrderChange",
|
||||
"OrderCancellation",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Sælgerinitieret handel: leverandøren starter med en ordrebekræftelse (fx efter aftale i marken eller telefonisk), kunden kan ændre eller annullere, og der faktureres bagefter.",
|
||||
"bemaerkninger": "Bemærk: ingen Order i profilen, \"Sel\" = seller initiated, så processen starter med OrderResponse.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdSel-BilSimR-1.0 (sælgerinitieret ordre + fakturering m. krævet svar)",
|
||||
"id": "Procurement-OrdSel-BilSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"OrderResponse",
|
||||
"OrderResponseSimple",
|
||||
"OrderChange",
|
||||
"OrderCancellation",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Som OrdSel-BilSim, men fakturasvar er påkrævet.",
|
||||
"bemaerkninger": "Ingen Order i profilen, processen starter hos sælger.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdSim-BilSimR-1.0 (simpel ordre + fakturering m. krævet fakturasvar)",
|
||||
"id": "Procurement-OrdSim-BilSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Simpel ordre + fakturering hvor fakturasvar (ApplicationResponse) er påkrævet.",
|
||||
"bemaerkninger": "R sidder på fakturadelen.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdSimR-BilSim-1.0 (simpel ordre m. krævet ordresvar + fakturering)",
|
||||
"id": "Procurement-OrdSimR-BilSim-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Som OrdSim-BilSim, men ordresvar er påkrævet.",
|
||||
"bemaerkninger": "R sidder på ordredelen.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "Procurement-OrdSimR-BilSimR-1.0 (simpel ordre + fakturering, begge svar krævet)",
|
||||
"id": "Procurement-OrdSimR-BilSimR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"Reminder",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "Simpel ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.",
|
||||
"bemaerkninger": "Begge R’er sat, modparten skal kvittere i begge led.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Udvidet ordre til fakturering",
|
||||
"navn": "urn:www.nesubl.eu:profiles:profile7:ver2.0 (NES7, ordre til faktura)",
|
||||
"id": "urn:www.nesubl.eu:profiles:profile7:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"OrderResponseSimple",
|
||||
"Invoice",
|
||||
"CreditNote",
|
||||
"ApplicationResponse *"
|
||||
],
|
||||
"anvendelse": "NES-profil for simpel ordre efterfulgt af simpel fakturering.",
|
||||
"bemaerkninger": "Ældre NES-pendant til OrdSim-BilSim. Ingen Reminder i profilen.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "ForsyningsSpecifikation",
|
||||
"navn": "Reference-Utility-1.0 (forsyningsspecifikation, UTS)",
|
||||
"id": "Reference-Utility-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"UtilityStatement",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Forsyningsbranchen (el/vand/varme/gas): leverandør sender detaljeret forbrugs-/forsyningsspecifikation der ledsager fakturaen; kunden kan svare med ApplicationResponse.",
|
||||
"bemaerkninger": "Bruges som tillæg til en faktureringsprofil, UTS erstatter ikke fakturaen.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "ForsyningsSpecifikation",
|
||||
"navn": "Reference-UtilityR-1.0 (forsyningsspecifikation m. krævet svar)",
|
||||
"id": "Reference-UtilityR-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"UtilityStatement",
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Som Reference-Utility, men kunden skal kvittere for forsyningsspecifikationen.",
|
||||
"bemaerkninger": "R = svar påkrævet.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"kategori": "Diverse",
|
||||
"navn": "Procurement-TecRes-1.0 (kvittering på modtaget dokument)",
|
||||
"id": "Procurement-TecRes-1.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"ApplicationResponse"
|
||||
],
|
||||
"anvendelse": "Ren kvitteringsprofil: bruges til at svare på et modtaget dokument, typisk når afsendelsen ikke kan behandles pga. tekniske fejl eller en ikke-understøttet profil.",
|
||||
"bemaerkninger": "Bruges som ProfileID på selve ApplicationResponse’en, ikke som en forretningsproces man handler i.",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Nemhandel OIOUBL (UBL 2.1)",
|
||||
"navn": "urn:www.cenbii.eu:profile:bii30:ver2.0 (forsendelsesadvis / despatch advice)",
|
||||
"id": "urn:www.cenbii.eu:profile:bii30:ver2.0",
|
||||
"customization": "OIOUBL-2.x",
|
||||
"dokumenter": [
|
||||
"DespatchAdvice"
|
||||
],
|
||||
"anvendelse": "Leverandør varsler en forsendelse, så kunden kan forberede modtagelse og afstemme leveringen mod ordren.",
|
||||
"bemaerkninger": "CEN BII-profil optaget i OIOUBL (G26 afsnit 3.3.15). Ikke placeret i en registerkategori her, bekræft gruppen i Nemhandelsregistrets dropdown før brug.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "OIOUBL Profiler (G26)",
|
||||
"url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Billing 3.0",
|
||||
"id": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0",
|
||||
"customization": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote"
|
||||
],
|
||||
"anvendelse": "Den centrale e-faktura/kreditnota-pakke i Peppol. CIUS af EN 16931. Bruges til EU-grænsehandel og DK/Peppol-fakturering.",
|
||||
"bemaerkninger": "Langt den mest brugte BIS3-profil. Invoice Response er en separat profil. Bemærk: Reminder findes ikke i Peppol, brug OIOUBL-profilerne til rykkere.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS Billing 3.0",
|
||||
"url": "https://docs.peppol.eu/poacc/billing/3.0/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Self-Billing 3.0",
|
||||
"id": "urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0",
|
||||
"customization": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0#conformant#urn:fdc:peppol.eu:2017:poacc:selfbilling:international:3.0",
|
||||
"dokumenter": [
|
||||
"Invoice (self-billed)",
|
||||
"CreditNote"
|
||||
],
|
||||
"anvendelse": "Selvfakturering: køber udsteder fakturaen på vegne af sælger.",
|
||||
"bemaerkninger": "Genbruger Billing-syntaks med self-billing customization.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · Self-Billing 3.0",
|
||||
"url": "https://docs.peppol.eu/poacc/self-billing/3.0/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Invoice Response 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:invoice_response:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:invoice_response:3",
|
||||
"dokumenter": [
|
||||
"ApplicationResponse (Invoice Response)"
|
||||
],
|
||||
"anvendelse": "Køber sender struktureret status/svar på en modtaget faktura (accepteret, afvist, under behandling, betalt).",
|
||||
"bemaerkninger": "Bruges sammen med Billing til at lukke faktura-livscyklussen. Peppol-pendant til OIOUBL-ernes \"R\"-profiler.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Message Level Response 3.0 (MLR)",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:mlr:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:mlr:3",
|
||||
"dokumenter": [
|
||||
"ApplicationResponse (Message Level Response)"
|
||||
],
|
||||
"anvendelse": "Teknisk kvittering på beskedniveau: bekræfter modtagelse/validering af et hvilket som helst dokument.",
|
||||
"bemaerkninger": "Transport-/syntaks-niveau svar, ikke forretningsmæssigt indhold. Peppol-pendant til Procurement-TecRes-1.0.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Ordering 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:ordering:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:order:3 / urn:fdc:peppol.eu:poacc:trns:order_response:3",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"Order Response"
|
||||
],
|
||||
"anvendelse": "Fuld ordreproces: køber sender ordre, sælger svarer med ordrebekræftelse/-svar.",
|
||||
"bemaerkninger": "Order = Trdm01, Order Response = Trdm76. Peppol-pendant til Procurement-OrdSim-1.0.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Order Only 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:order_only:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:order:3",
|
||||
"dokumenter": [
|
||||
"Order"
|
||||
],
|
||||
"anvendelse": "Kun afsendelse af ordre uden forventet struktureret ordresvar.",
|
||||
"bemaerkninger": "Enkel ordre-only proces. Peppol-pendant til NES3 (\"Order Only\").",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Order Agreement 3.0 (Advanced Ordering)",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:order_agreement:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:order_agreement:3",
|
||||
"dokumenter": [
|
||||
"Order",
|
||||
"Order Response (Advanced)"
|
||||
],
|
||||
"anvendelse": "Avanceret ordre der etablerer en bindende ordreaftale mellem parterne.",
|
||||
"bemaerkninger": "Bruges hvor ordresvar danner juridisk aftale.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Catalogue 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:catalogue:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:catalogue:3",
|
||||
"dokumenter": [
|
||||
"Catalogue",
|
||||
"ApplicationResponse (Catalogue Response)"
|
||||
],
|
||||
"anvendelse": "Sælger sender produktkatalog (priser/varer) til køber, der kan svare med accept/afvisning.",
|
||||
"bemaerkninger": "Grundlag for efterfølgende ordrer. Peppol-pendant til Catalogue-CatBasR-1.0.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Catalogue without Response 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:catalogue_wo_response:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:catalogue:3",
|
||||
"dokumenter": [
|
||||
"Catalogue"
|
||||
],
|
||||
"anvendelse": "Katalogudsendelse uden forventet katalogsvar.",
|
||||
"bemaerkninger": "Variant uden Catalogue Response.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Punch Out 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:punch_out:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:punch_out:3",
|
||||
"dokumenter": [
|
||||
"Catalogue (Punch Out)"
|
||||
],
|
||||
"anvendelse": "Punch-out katalog: køber 'hopper ud' til sælgers webshop og henter en kurv tilbage.",
|
||||
"bemaerkninger": "Bruges i e-handels-/webshop-integrationer.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 3.0",
|
||||
"navn": "Peppol BIS Despatch Advice 3.0",
|
||||
"id": "urn:fdc:peppol.eu:poacc:bis:despatch_advice:3",
|
||||
"customization": "urn:fdc:peppol.eu:poacc:trns:despatch_advice:3",
|
||||
"dokumenter": [
|
||||
"Despatch Advice (følgeseddel)"
|
||||
],
|
||||
"anvendelse": "Sælger sender forsendelses-/leveringsmeddelelse (hvad er afsendt, hvornår, hvordan) inden faktura.",
|
||||
"bemaerkninger": "Logistik-dokument mellem ordre og faktura. Peppol-pendant til urn:www.cenbii.eu:profile:bii30:ver2.0 i OIOUBL.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · BIS v3",
|
||||
"url": "https://docs.peppol.eu/poacc/upgrade-3/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 4.0 / PINT",
|
||||
"navn": "EU PINT Billing (v1.0.1; forløber for BIS Billing 4.0)",
|
||||
"id": "urn:peppol:pint:billing-1@eu-1 (PINT EU Billing)",
|
||||
"customization": "urn:peppol:pint:billing-1@eu-1",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote"
|
||||
],
|
||||
"anvendelse": "Peppols internationale fakturamodel (PINT) tilpasset EU. Implementerer BIS Billing 3.0 som PINT-kompatibel uden ændringer; baner vej for grænseoverskridende fakturering mod lande med egne PINT-specs (AU/JP/MY/NZ/SG m.fl.).",
|
||||
"bemaerkninger": "v1.0.0 udgivet okt. 2025, nu v1.0.1. CIUS af EN 16931. NB: identifikator-format afviger fra BIS3, verificér mod docs.peppol.eu/poac/eu/pint-eu inden brug.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "docs.peppol.eu · EU PINT Billing",
|
||||
"url": "https://docs.peppol.eu/poac/eu/pint-eu/"
|
||||
}
|
||||
},
|
||||
{
|
||||
"standard": "Peppol BIS 4.0 / PINT",
|
||||
"navn": "Peppol BIS Billing 4.0 (kommende)",
|
||||
"id": "(endnu ikke endeligt fastlagt)",
|
||||
"customization": "(endnu ikke endeligt fastlagt)",
|
||||
"dokumenter": [
|
||||
"Invoice",
|
||||
"CreditNote (forventet)"
|
||||
],
|
||||
"anvendelse": "Næste generation der lægger BIS og PINT sammen til ét globalt faktura-format. Forventet 2026, endnu ikke udgivet (pr. juni 2026).",
|
||||
"bemaerkninger": "STATUS: under udarbejdelse pr. juni 2026, ID'er og indhold ikke endeligt publiceret. Brug BIS Billing 3.0 / EU PINT i mellemtiden.",
|
||||
"kategori": "",
|
||||
"kilde": {
|
||||
"label": "Peppol roadmap",
|
||||
"url": "https://docs.peppol.eu/poac/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"baseGraph": {
|
||||
"@context": "https://schema.org",
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "WebSite",
|
||||
"@id": "https://asger.weirsoe.dk/#website",
|
||||
"url": "https://asger.weirsoe.dk/",
|
||||
"name": "Asger Weirsøe",
|
||||
"description": "Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense. Peppol, NemHandel, e-faktura, selvhosting, Proxmox og AI.",
|
||||
"inLanguage": "da-DK",
|
||||
"publisher": {
|
||||
"@id": "https://asger.weirsoe.dk/#person"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Person",
|
||||
"@id": "https://asger.weirsoe.dk/#person",
|
||||
"name": "Asger Geel Weirsøe",
|
||||
"alternateName": "Asger Weirsøe",
|
||||
"url": "https://asger.weirsoe.dk/",
|
||||
"image": "https://asger.weirsoe.dk/asger.png",
|
||||
"jobTitle": [
|
||||
"Datalog",
|
||||
"Technical Solution Architect"
|
||||
],
|
||||
"hasOccupation": {
|
||||
"@type": "Occupation",
|
||||
"name": "Datalog",
|
||||
"occupationalCategory": "2512 Softwareudvikler"
|
||||
},
|
||||
"hasCredential": [
|
||||
{
|
||||
"@type": "EducationalOccupationalCredential",
|
||||
"credentialCategory": "degree",
|
||||
"educationalLevel": "Master's degree",
|
||||
"name": "Cand.scient. i Datalogi",
|
||||
"recognizedBy": {
|
||||
"@type": "CollegeOrUniversity",
|
||||
"name": "Aalborg Universitet"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "EducationalOccupationalCredential",
|
||||
"credentialCategory": "license",
|
||||
"name": "Radioamatørlicens, kategori B (kaldesignal OZ0AGW)",
|
||||
"dateCreated": "2024-05-18"
|
||||
},
|
||||
{
|
||||
"@type": "EducationalOccupationalCredential",
|
||||
"credentialCategory": "license",
|
||||
"name": "Jagttegn",
|
||||
"dateCreated": "2020"
|
||||
}
|
||||
],
|
||||
"callSign": "OZ0AGW",
|
||||
"worksFor": {
|
||||
"@type": "Organization",
|
||||
"name": "Digisense A/S"
|
||||
},
|
||||
"alumniOf": {
|
||||
"@type": "CollegeOrUniversity",
|
||||
"name": "Aalborg Universitet"
|
||||
},
|
||||
"homeLocation": {
|
||||
"@type": "Place",
|
||||
"name": "Nørresundby, Danmark"
|
||||
},
|
||||
"nationality": {
|
||||
"@type": "Country",
|
||||
"name": "Danmark"
|
||||
},
|
||||
"knowsLanguage": [
|
||||
"da",
|
||||
"en"
|
||||
],
|
||||
"knowsAbout": [
|
||||
"Peppol",
|
||||
"Peppol Access Point",
|
||||
"Peppol BIS",
|
||||
"Peppol eDelivery",
|
||||
"Oxalis",
|
||||
"NemHandel",
|
||||
"Elektronisk fakturering",
|
||||
"E-faktura",
|
||||
"Electronic invoicing",
|
||||
"OIOUBL",
|
||||
"UBL",
|
||||
"e-Delivery (AS4)",
|
||||
"EN 16931",
|
||||
"E-procurement",
|
||||
"EDI",
|
||||
"Softwarearkitektur",
|
||||
"Løsningsarkitektur",
|
||||
"Forretningsforståelse",
|
||||
"Digitalisering af fakturaflows",
|
||||
"Rådgivning",
|
||||
"Kravspecifikation",
|
||||
"Incident response",
|
||||
"Incident management",
|
||||
"Hændelseshåndtering",
|
||||
"ISO/IEC 27001",
|
||||
"Informationssikkerhed",
|
||||
"ISMS",
|
||||
"Sårbarhedshåndtering",
|
||||
"Proxmox",
|
||||
"Selvhosting",
|
||||
"Kunstig intelligens",
|
||||
"Linux",
|
||||
"DevOps",
|
||||
"OIDC",
|
||||
"SSO",
|
||||
"Fuld-stack udvikling",
|
||||
"Docker",
|
||||
"Amatørradio",
|
||||
"Radioamatør",
|
||||
"Radiokommunikation"
|
||||
],
|
||||
"sameAs": [
|
||||
"https://www.linkedin.com/in/ag-weirs%C3%B8e/",
|
||||
"https://mastodon.social/@asgerweirsoee",
|
||||
"https://github.com/asger-weirsoee",
|
||||
"https://gitea.weircon.dk/agw",
|
||||
"https://arxiv.org/abs/2310.00999"
|
||||
],
|
||||
"description": "Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Han bygger bro mellem forretning og teknik og har en bred profil inden for elektronisk fakturering og e-procurement: Peppol (Access Point, eDelivery/AS4, Oxalis), NemHandel og OIOUBL, kombineret med forretningsforståelse, selvhosting, Proxmox og AI. Han er desuden licenseret radioamatør med B-licens og kaldesignalet OZ0AGW."
|
||||
}
|
||||
]
|
||||
},
|
||||
"profilePage": {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "ProfilePage",
|
||||
"@id": "https://asger.weirsoe.dk/#profilepage",
|
||||
"url": "https://asger.weirsoe.dk/",
|
||||
"name": "Asger Weirsøe",
|
||||
"mainEntity": {
|
||||
"@id": "https://asger.weirsoe.dk/#person"
|
||||
},
|
||||
"about": {
|
||||
"@id": "https://asger.weirsoe.dk/#person"
|
||||
},
|
||||
"inLanguage": "da-DK"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"title": "Homelab",
|
||||
"items": [
|
||||
{
|
||||
"name": "Proxmox VE",
|
||||
"note": "Hypervisor på egen hardware. ~50 LXC-containere og et par KVM-VM’er, én tjeneste pr. container."
|
||||
},
|
||||
{
|
||||
"name": "NPMplus",
|
||||
"note": "Reverse proxy og TLS-terminering på kanten. Alt trafik ind går her igennem."
|
||||
},
|
||||
{
|
||||
"name": "Authentik",
|
||||
"note": "Self-hosted SSO (OIDC/LDAP). Én identitet til alle tjenester, bygget fra source."
|
||||
},
|
||||
{
|
||||
"name": "Gitea + Gitea Actions",
|
||||
"note": "Egen git-hosting og CI/CD. Denne side deployes automatisk ved push."
|
||||
},
|
||||
{
|
||||
"name": "MariaDB / Redis / PostgreSQL",
|
||||
"note": "Databaser som dedikerede containere."
|
||||
},
|
||||
{
|
||||
"name": "Matomo",
|
||||
"note": "Self-hosted analytics, first-party og uden tredjeparter."
|
||||
},
|
||||
{
|
||||
"name": "WireGuard",
|
||||
"note": "Tunneler og netns-baserede egress-profiler til alt der skal ud af huset på en pæn måde."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Udvikling",
|
||||
"items": [
|
||||
{
|
||||
"name": "Linux + code-server",
|
||||
"note": "Udvikling foregår i en dedikeret LXC med VS Code i browseren."
|
||||
},
|
||||
{
|
||||
"name": "Java · Go · Python · TypeScript",
|
||||
"note": "Java til Peppol/eDelivery-verdenen, Python/TS til alt det andet."
|
||||
},
|
||||
{
|
||||
"name": "Claude Code",
|
||||
"note": "LLM-drevne workflows, fra blogindlæg til Proxmox-administration."
|
||||
},
|
||||
{
|
||||
"name": "Docker",
|
||||
"note": "Hvor det giver mening. Ellers foretrækker jeg native installationer i LXC."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Denne side",
|
||||
"items": [
|
||||
{
|
||||
"name": "Astro 5 + Bun + Tailwind 4",
|
||||
"note": "Statisk output, ingen JS-frameworks i browseren. Se kolofonen for detaljer."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"name": "asger-weirsoe-dk",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev --host",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^4.0.11",
|
||||
"@astrojs/sitemap": "^3.3.0",
|
||||
"@fontsource-variable/inter": "^5.2.5",
|
||||
"@tailwindcss/vite": "^4.1.5",
|
||||
"@vite-pwa/astro": "^0.5.0",
|
||||
"astro": "^5.7.0",
|
||||
"mermaid": "^11.15.0",
|
||||
"sharp": "^0.34.5",
|
||||
"tailwindcss": "^4.1.5"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
Contact: mailto:asger@weirsoe.dk
|
||||
Expires: 2027-06-30T22:00:00.000Z
|
||||
Preferred-Languages: da, en
|
||||
Canonical: https://asger.xn--weirse-fya.dk/.well-known/security.txt
|
||||
Policy: https://asger.xn--weirse-fya.dk/colophon
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
# Bygger sitet til public/. PW_PREVIEW=1 giver preview-buildet under /udkast.
|
||||
set -eu
|
||||
TOOLS="${TOOLS:-.tools}"
|
||||
python3 scripts/prebuild.py
|
||||
"$TOOLS/tailwindcss" --input assets/site.css --output static/css/site.css --minify
|
||||
"$TOOLS/zola" build
|
||||
python3 scripts/postbuild.py
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
"""Godkendelsesstemplet på blogindlæg: HMAC-SHA256 over sti, indhold og billeder.
|
||||
|
||||
canonical() skal matche godkendelsestjenesten (service/app.py i agw/pw-drafts) byte for byte.
|
||||
"""
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
|
||||
BLOG = "content/blog"
|
||||
|
||||
|
||||
def canonical(rel, text, assets=()):
|
||||
"""assets: [(sti, sha256-hex)] for billederne i bundlen, sorteret efter sti."""
|
||||
lines = text.replace("\r\n", "\n").split("\n")
|
||||
if lines and lines[0] == "---" and "---" in lines[1:]:
|
||||
end = lines.index("---", 1)
|
||||
lines = [l for i, l in enumerate(lines) if not (0 < i < end and re.match(r"\s*godkendt:", l))]
|
||||
return f"{rel}\n" + "\n".join(lines) + "".join(f"\n@asset {p} {h}" for p, h in assets)
|
||||
|
||||
|
||||
def sign(key, rel, text, assets=()):
|
||||
return "v1:" + hmac.new(key, canonical(rel, text, assets).encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
with open(path, "rb") as f:
|
||||
return hashlib.sha256(f.read()).hexdigest()
|
||||
|
||||
|
||||
def assets_for(slug):
|
||||
"""Alt i bundlen ud over index.md er billeder, og de ligger fladt."""
|
||||
d = f"{BLOG}/{slug}"
|
||||
out = []
|
||||
for name in os.listdir(d):
|
||||
if name == "index.md":
|
||||
continue
|
||||
p = f"{d}/{name}"
|
||||
if not os.path.isfile(p):
|
||||
raise ValueError(f"{p}: undermapper understøttes ikke")
|
||||
if name.endswith(".md"):
|
||||
raise ValueError(f"{p}: kun index.md må være markdown i en bundle")
|
||||
out.append((p, sha256_file(p)))
|
||||
return sorted(out)
|
||||
|
||||
|
||||
def read_text(path):
|
||||
with open(path, "rb") as f:
|
||||
return f.read().decode()
|
||||
|
||||
|
||||
def stamp_of(text):
|
||||
"""(stempel eller None, om indlægget er markeret draft: true)"""
|
||||
lines = text.replace("\r\n", "\n").split("\n")
|
||||
end = lines.index("---", 1) if lines[0] == "---" and "---" in lines[1:] else -1
|
||||
if end < 0:
|
||||
return None, False
|
||||
fm = lines[1:end]
|
||||
g = next((l for l in fm if re.match(r"\s*godkendt:", l)), None)
|
||||
draft = any(re.fullmatch(r"draft:\s*true\s*", l) for l in fm)
|
||||
return (re.sub(r"^\s*godkendt:\s*", "", g).strip("'\"") if g else None), draft
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Henter zola og Tailwind standalone-CLI til .tools/, fastlåst på version og sha256.
|
||||
# Bruges af CI her og i agw/pw-drafts.
|
||||
set -eu
|
||||
ZOLA=0.23.6
|
||||
ZOLA_SHA256=7d77a4220a4f699cba2fc0e0c7859f780e887e269159ad4fab90e11ea16c906f
|
||||
TAILWIND=4.3.3
|
||||
TAILWIND_SHA256=dc61b3ac6b8c9ca874c0cc4c57b2409791a64c5540404ca5f5367360babc313a
|
||||
|
||||
mkdir -p .tools
|
||||
curl -fsSL -o .tools/zola.tar.gz \
|
||||
"https://github.com/getzola/zola/releases/download/v$ZOLA/zola-v$ZOLA-x86_64-unknown-linux-musl.tar.gz"
|
||||
curl -fsSL -o .tools/tailwindcss \
|
||||
"https://github.com/tailwindlabs/tailwindcss/releases/download/v$TAILWIND/tailwindcss-linux-x64"
|
||||
sha256sum -c - <<EOF
|
||||
$ZOLA_SHA256 .tools/zola.tar.gz
|
||||
$TAILWIND_SHA256 .tools/tailwindcss
|
||||
EOF
|
||||
tar -xzf .tools/zola.tar.gz -C .tools
|
||||
rm .tools/zola.tar.gz
|
||||
chmod +x .tools/zola .tools/tailwindcss
|
||||
@@ -1,90 +0,0 @@
|
||||
// Pinger IndexNow med de sider der er ændret i dette push.
|
||||
// Kør efter `astro build` (bruger dist/sitemap-0.xml).
|
||||
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const HOST = 'asger.xn--weirse-fya.dk';
|
||||
const KEY = 'b68b31655cb9c1a662d53905dd0b1b06';
|
||||
const ORIGIN = `https://${HOST}`;
|
||||
|
||||
const git = (args) => {
|
||||
try {
|
||||
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const sitemapUrls = () => {
|
||||
const xml = fs.readFileSync('dist/sitemap-0.xml', 'utf8');
|
||||
return new Set([...xml.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]));
|
||||
};
|
||||
|
||||
// src/pages/foo.astro -> /foo/ ; src/content/blog/bar.md -> /blog/bar/
|
||||
const routeForFile = (f) => {
|
||||
let m = f.match(/^src\/pages\/(.+)\.astro$/);
|
||||
if (m) {
|
||||
if (m[1].includes('[')) return null; // dynamiske ruter kan vi ikke slå op
|
||||
if (m[1] === 'index') return '/';
|
||||
return `/${m[1].replace(/\/index$/, '')}/`;
|
||||
}
|
||||
m = f.match(/^src\/content\/blog\/(.+)\.md$/);
|
||||
if (m) return `/blog/${m[1]}/`;
|
||||
return null;
|
||||
};
|
||||
|
||||
const changedFiles = () => {
|
||||
const { GITHUB_EVENT_BEFORE, GITHUB_SHA } = process.env;
|
||||
const zero = /^0*$/;
|
||||
if (GITHUB_EVENT_BEFORE && !zero.test(GITHUB_EVENT_BEFORE)) {
|
||||
const d = git(['diff', '--name-only', GITHUB_EVENT_BEFORE, GITHUB_SHA || 'HEAD']);
|
||||
if (d !== null) return d ? d.split('\n') : [];
|
||||
}
|
||||
const d = git(['diff', '--name-only', 'HEAD~1', 'HEAD']);
|
||||
return d ? d.split('\n') : null;
|
||||
};
|
||||
|
||||
const files = changedFiles();
|
||||
if (files === null) {
|
||||
console.log('IndexNow: kan ikke bestemme hvad der ændrede sig (shallow clone?) — springer over.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const valid = sitemapUrls();
|
||||
const urls = [...new Set(files.map(routeForFile).filter(Boolean).map((r) => ORIGIN + r))].filter((u) =>
|
||||
valid.has(u),
|
||||
);
|
||||
|
||||
if (!urls.length) {
|
||||
console.log('IndexNow: ingen sider med ændret indhold i dette push — springer over.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const body = {
|
||||
host: HOST,
|
||||
key: KEY,
|
||||
keyLocation: `${ORIGIN}/${KEY}.txt`,
|
||||
urlList: urls,
|
||||
};
|
||||
|
||||
console.log(`IndexNow: indsender ${urls.length} URL(er):`);
|
||||
for (const u of urls) console.log(' ', u);
|
||||
|
||||
// `node scripts/indexnow.mjs --dry-run` viser hvad der ville blive sendt.
|
||||
if (process.argv.includes('--dry-run')) {
|
||||
console.log('IndexNow: --dry-run, sender ikke.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const res = await fetch('https://api.indexnow.org/indexnow', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
console.log(`IndexNow: HTTP ${res.status} ${res.statusText}`);
|
||||
if (![200, 202].includes(res.status)) {
|
||||
console.log(await res.text().catch(() => ''));
|
||||
console.log('IndexNow: afvist — men deployet er allerede ude, så vi fejler ikke buildet.');
|
||||
}
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pinger IndexNow med de sider der er ændret i dette push. Kør efter buildet
|
||||
(bruger public/sitemap-0.xml). --dry-run viser hvad der ville blive sendt."""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
HOST = "asger.weirsoe.dk"
|
||||
KEY = "b68b31655cb9c1a662d53905dd0b1b06"
|
||||
ORIGIN = f"https://{HOST}"
|
||||
|
||||
|
||||
def git(*args):
|
||||
r = subprocess.run(["git", *args], capture_output=True, text=True)
|
||||
return r.stdout.strip() if r.returncode == 0 else None
|
||||
|
||||
|
||||
def sitemap_urls():
|
||||
with open("public/sitemap-0.xml") as f:
|
||||
return set(re.findall(r"<loc>([^<]+)</loc>", f.read()))
|
||||
|
||||
|
||||
def route_for_file(f):
|
||||
"""Kanonisk form uden afsluttende skråstreg, som i sitemap'et."""
|
||||
m = re.fullmatch(r"content/(.+?)(/index|/_index|_index)?\.md", f)
|
||||
if m:
|
||||
return "/" + m[1].rstrip("/") if m[1] != "_index" else "/"
|
||||
m = re.fullmatch(r"templates/sider/(.+)\.html", f) or re.fullmatch(r"data/(.+)\.json", f)
|
||||
if m:
|
||||
name = {"forside": "", "forside-en": "en", "profiles": "profiler"}.get(m[1], m[1])
|
||||
return "/en/" + name[:-3] if name.endswith("-en") else "/" + name
|
||||
return None
|
||||
|
||||
|
||||
def changed_files():
|
||||
before, sha = os.environ.get("GITHUB_EVENT_BEFORE"), os.environ.get("GITHUB_SHA")
|
||||
if before and not re.fullmatch(r"0*", before):
|
||||
d = git("diff", "--name-only", before, sha or "HEAD")
|
||||
if d is not None:
|
||||
return d.split("\n") if d else []
|
||||
d = git("diff", "--name-only", "HEAD~1", "HEAD")
|
||||
return d.split("\n") if d else None
|
||||
|
||||
|
||||
def main():
|
||||
files = changed_files()
|
||||
if files is None:
|
||||
print("IndexNow: kan ikke bestemme hvad der ændrede sig (shallow clone?), springer over.")
|
||||
return
|
||||
valid = sitemap_urls()
|
||||
urls = sorted({ORIGIN + r for r in map(route_for_file, files) if r} & valid)
|
||||
if not urls:
|
||||
print("IndexNow: ingen sider med ændret indhold i dette push, springer over.")
|
||||
return
|
||||
print(f"IndexNow: indsender {len(urls)} URL(er):")
|
||||
for u in urls:
|
||||
print(" ", u)
|
||||
if "--dry-run" in sys.argv:
|
||||
print("IndexNow: --dry-run, sender ikke.")
|
||||
return
|
||||
body = {"host": HOST, "key": KEY, "keyLocation": f"{ORIGIN}/{KEY}.txt", "urlList": urls}
|
||||
req = urllib.request.Request("https://api.indexnow.org/indexnow", data=json.dumps(body).encode(),
|
||||
headers={"Content-Type": "application/json; charset=utf-8"}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
print(f"IndexNow: HTTP {r.status} {r.reason}")
|
||||
except urllib.error.URLError as e:
|
||||
# Deployet er allerede ude, så vi fejler ikke buildet.
|
||||
print(f"IndexNow: afvist: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stempler de udgivne indlæg om efter flytningen fra src/content/blog/<slug>.md til
|
||||
content/blog/<slug>/index.md. Stien og frontmatter indgår i stemplet, så de gamle er ugyldige.
|
||||
|
||||
Kør på zola-branchen, med master stadig på Astro-versionen:
|
||||
|
||||
PUBLISH_KEY=<hex> python3 scripts/omstempl.py [--fra master]
|
||||
PUBLISH_KEY=<hex> python3 scripts/verify-approvals.py
|
||||
git commit -am "blog: stempl indlæg om efter flytning til Zola"
|
||||
|
||||
Scriptet underskriver kun det der allerede var godkendt. For hvert indlæg tjekker det at
|
||||
det gamle stempel er gyldigt, og at den nye fil er præcis den gamle i nyt format. Fejler
|
||||
ét indlæg, skrives der intet.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from godkendelse import BLOG, assets_for, read_text, sign, stamp_of # noqa: E402
|
||||
|
||||
OLD = "src/content/blog"
|
||||
|
||||
|
||||
def git(*args, binary=False):
|
||||
return subprocess.run(["git", *args], check=True, capture_output=True, text=not binary).stdout
|
||||
|
||||
|
||||
def old_sign(key, rel, text, assets):
|
||||
"""Det gamle format: stemplet stod som topniveau-linje og er det eneste der skæres fra."""
|
||||
lines = text.replace("\r\n", "\n").split("\n")
|
||||
end = lines.index("---", 1)
|
||||
lines = [l for i, l in enumerate(lines) if not (0 < i < end and l.startswith("godkendt:"))]
|
||||
tail = "".join(f"\n@asset {p} {h}" for p, h in assets)
|
||||
body = f"{rel}\n" + "\n".join(lines) + tail
|
||||
return "v1:" + hmac.new(key, body.encode(), hashlib.sha256).hexdigest()
|
||||
|
||||
|
||||
def migrated(text, stamp):
|
||||
"""Den gamle fil i det nye format: tags under taxonomies, stemplet under extra."""
|
||||
lines = text.split("\n")
|
||||
end = lines.index("---", 1)
|
||||
head = []
|
||||
for l in lines[1:end]:
|
||||
if l.startswith("godkendt:"):
|
||||
continue
|
||||
head += ["taxonomies:", " " + l] if l.startswith("tags:") else [l]
|
||||
return "\n".join(["---", *head, "extra:", f' godkendt: "{stamp}"', *lines[end:]])
|
||||
|
||||
|
||||
def old_assets(ref, slug):
|
||||
out = git("ls-tree", "--name-only", ref, f"{OLD}/{slug}/").split()
|
||||
return sorted((p, hashlib.sha256(git("show", f"{ref}:{p}", binary=True)).hexdigest()) for p in out)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--fra", default="master", help="ref med de gamle, godkendte indlæg")
|
||||
args = ap.parse_args()
|
||||
if not os.environ.get("PUBLISH_KEY"):
|
||||
sys.exit("PUBLISH_KEY mangler.")
|
||||
key = bytes.fromhex(os.environ["PUBLISH_KEY"])
|
||||
|
||||
slugs = sorted(n for n in os.listdir(BLOG) if os.path.isdir(f"{BLOG}/{n}"))
|
||||
new, errors = {}, []
|
||||
for slug in slugs:
|
||||
rel = f"{BLOG}/{slug}/index.md"
|
||||
old_rel = f"{OLD}/{slug}.md"
|
||||
try:
|
||||
old = git("show", f"{args.fra}:{old_rel}")
|
||||
except subprocess.CalledProcessError:
|
||||
errors.append(f"{rel}: findes ikke i {args.fra} som {old_rel}; kun godkendelsesflowet må stemple nyt")
|
||||
continue
|
||||
text = read_text(rel)
|
||||
stamp_gl, draft = stamp_of(old)
|
||||
o_assets = old_assets(args.fra, slug)
|
||||
n_assets = assets_for(slug)
|
||||
if draft or not stamp_gl or not hmac.compare_digest(stamp_gl, old_sign(key, old_rel, old, o_assets)):
|
||||
errors.append(f"{old_rel}: det gamle stempel er ikke gyldigt med denne nøgle")
|
||||
continue
|
||||
if text != migrated(old, stamp_of(text)[0]):
|
||||
errors.append(f"{rel}: indholdet er ændret i forhold til {args.fra}")
|
||||
continue
|
||||
if [h for _, h in o_assets] != [h for _, h in n_assets]:
|
||||
errors.append(f"{rel}: billederne er ændret i forhold til {args.fra}")
|
||||
continue
|
||||
new[rel] = migrated(old, sign(key, rel, text, n_assets))
|
||||
if errors:
|
||||
print("\n".join(f"✗ {e}" for e in errors), file=sys.stderr)
|
||||
sys.exit(f"\n{len(errors)} fejl. Intet er skrevet.")
|
||||
for rel, text in new.items():
|
||||
with open(rel, "w", encoding="utf-8", newline="") as f:
|
||||
f.write(text)
|
||||
print(f"✓ {rel}")
|
||||
print(f"\n{len(new)} indlæg stemplet om. Kør scripts/verify-approvals.py og commit.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Efterbehandling af Zolas output i public/:
|
||||
|
||||
- overskrift-id'er som på den gamle side (bevarer æøå, så gamle #ankre stadig virker)
|
||||
- sitemap-index.xml + sitemap-0.xml med lastmod fra git og frontmatter
|
||||
- sw.js med precache-liste
|
||||
- PW_PREVIEW=1: hele sitet flyttes ind under /udkast, uden sitemap og service worker
|
||||
"""
|
||||
import hashlib
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
OUT = pathlib.Path("public")
|
||||
SITE = "https://asger.weirsoe.dk"
|
||||
PREVIEW = os.environ.get("PW_PREVIEW") == "1"
|
||||
BASE = "/udkast"
|
||||
|
||||
|
||||
def pages():
|
||||
return sorted(OUT.rglob("*.html"))
|
||||
|
||||
|
||||
def slug(text):
|
||||
"""Samme regler som github-slugger, som den gamle build brugte."""
|
||||
s = html.unescape(re.sub(r"<[^>]+>", "", text)).strip().lower()
|
||||
s = re.sub(r"[^\w\- ]", "", s)
|
||||
return s.replace(" ", "-")
|
||||
|
||||
|
||||
def heading_ids():
|
||||
for f in pages():
|
||||
src = f.read_text()
|
||||
if 'class="prose' not in src:
|
||||
continue
|
||||
seen = {}
|
||||
|
||||
def fix(m):
|
||||
s = slug(m[3])
|
||||
n = seen.get(s, 0)
|
||||
seen[s] = n + 1
|
||||
return f'<h{m[1]} id="{s}-{n}"{m[2]}>' if n else f'<h{m[1]} id="{s}"{m[2]}>'
|
||||
|
||||
f.write_text(re.sub(r'<h([1-6]) id="[^"]*"([^>]*)>(.*?)(?=</h\1>)',
|
||||
lambda m: fix(m) + m[3], src, flags=re.S))
|
||||
|
||||
|
||||
def git_date(*files):
|
||||
if int(subprocess.run(["git", "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout or 0) < 2:
|
||||
return None
|
||||
out = subprocess.run(["git", "log", "-1", "--format=%cI", "--", *files], capture_output=True, text=True).stdout
|
||||
return out.strip() or None
|
||||
|
||||
|
||||
def frontmatter(path):
|
||||
text = path.read_text()
|
||||
head = text.split("---\n", 2)[1]
|
||||
return {k: v.strip().strip("'\"") for k, v in re.findall(r"^\s*(\w+):(.*)$", head, re.M)}
|
||||
|
||||
|
||||
def iso(date):
|
||||
return f"{date}T00:00:00.000Z"
|
||||
|
||||
|
||||
def lastmods():
|
||||
"""Rute -> lastmod: sidste commit for statiske sider, frontmatter for blogindlæg."""
|
||||
routes = {}
|
||||
for md in pathlib.Path("content").rglob("*.md"):
|
||||
fm = frontmatter(md)
|
||||
if "blog" in md.parts or md.parts[1] == "projekter" or "projects" in md.parts:
|
||||
continue
|
||||
route = "/" + str(md.relative_to("content").with_suffix("")).removesuffix("_index")
|
||||
route = route.rstrip("/") or "/"
|
||||
tpl = fm.get("template")
|
||||
d = git_date(str(md), f"templates/{tpl}") if tpl else git_date(str(md))
|
||||
if d:
|
||||
routes[route] = d
|
||||
posts = []
|
||||
for md in pathlib.Path("content/blog").glob("*/index.md"):
|
||||
fm = frontmatter(md)
|
||||
d = iso(fm.get("updated") or fm["date"])
|
||||
routes[f"/blog/{md.parent.name}"] = d
|
||||
posts.append(d)
|
||||
if posts:
|
||||
routes["/blog"] = max(posts)
|
||||
return routes
|
||||
|
||||
|
||||
def sitemap():
|
||||
skip = ("/tags", "/peppol")
|
||||
skip_exact = ("/cookies", "/status", "/chat")
|
||||
routes = []
|
||||
for f in pages():
|
||||
rel = f.relative_to(OUT)
|
||||
if rel.name != "index.html":
|
||||
continue
|
||||
r = "/" + str(rel.parent).replace(".", "", 1) if str(rel.parent) == "." else f"/{rel.parent}"
|
||||
if r.startswith(skip) or r in skip_exact:
|
||||
continue
|
||||
routes.append(r)
|
||||
routes.append("/tarballz/")
|
||||
mods = lastmods()
|
||||
urls = []
|
||||
for r in sorted(routes):
|
||||
mod = mods.get(r)
|
||||
urls.append(f"<url><loc>{SITE}{r}</loc>" + (f"<lastmod>{mod}</lastmod>" if mod else "")
|
||||
+ f"<changefreq>weekly</changefreq><priority>{'1.0' if r == '/' else '0.7'}</priority></url>")
|
||||
ns = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"'
|
||||
(OUT / "sitemap-0.xml").write_text(f'<?xml version="1.0" encoding="UTF-8"?><urlset {ns}>{"".join(urls)}</urlset>')
|
||||
newest = max(mods.values(), default=None)
|
||||
(OUT / "sitemap-index.xml").write_text(
|
||||
f'<?xml version="1.0" encoding="UTF-8"?><sitemapindex {ns}><sitemap><loc>{SITE}/sitemap-0.xml</loc>'
|
||||
+ (f"<lastmod>{newest}</lastmod>" if newest else "") + "</sitemap></sitemapindex>")
|
||||
|
||||
|
||||
def service_worker():
|
||||
"""Precache-listen afhænger af buildet, resten af sw.js ligger i scripts/sw.js."""
|
||||
exts = {".html", ".css", ".js", ".svg", ".png", ".woff2", ".xml", ".txt", ".webmanifest"}
|
||||
files = sorted(p for p in OUT.rglob("*") if p.is_file() and p.suffix in exts
|
||||
and p.name not in ("sw.js", "mermaid.min.js"))
|
||||
h = hashlib.sha256()
|
||||
for p in files:
|
||||
h.update(p.read_bytes())
|
||||
urls = ["/" + str(p.relative_to(OUT)) for p in files]
|
||||
src = pathlib.Path("scripts/sw.js").read_text()
|
||||
src = src.replace("__VERSION__", h.hexdigest()[:12]).replace("__URLS__", json.dumps(urls, ensure_ascii=False))
|
||||
(OUT / "sw.js").write_text(src)
|
||||
|
||||
|
||||
def rebase():
|
||||
"""Rod-relative links og billeder i HTML peger ind under /udkast."""
|
||||
attr = re.compile(r'\b(href|src|action)="/(?!/|udkast)')
|
||||
srcset = re.compile(r'\bsrcset="([^"]*)"')
|
||||
|
||||
def fix_set(m):
|
||||
return 'srcset="' + re.sub(r"(^|, )/(?!/)", rf"\1{BASE}/", m[1]) + '"'
|
||||
|
||||
for f in pages():
|
||||
src = f.read_text()
|
||||
f.write_text(srcset.sub(fix_set, attr.sub(rf'\1="{BASE}/', src)))
|
||||
|
||||
|
||||
def main():
|
||||
(OUT / "sitemap.xml").unlink(missing_ok=True)
|
||||
heading_ids()
|
||||
if PREVIEW:
|
||||
rebase()
|
||||
else:
|
||||
sitemap()
|
||||
service_worker()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Henter det sitet viser fra Gitea ved build og skriver data/gitea.json:
|
||||
repos til /projekter og aktivitets-heatmap til /now. Er Gitea nede, bygges sitet
|
||||
alligevel, med en fejlbesked på /projekter og en tom graf på /now."""
|
||||
import datetime as dt
|
||||
import json
|
||||
import urllib.request
|
||||
|
||||
BASE, USER = "https://gitea.weircon.dk", "agw"
|
||||
HIDE = set() # repo-navne der skjules helt
|
||||
PINNED = ["ubl-tools", "gtk-ahfail", "pi3-smart-workspace"] # vises først, i den rækkefølge
|
||||
WEEKS = 26
|
||||
MAANEDER = ["januar", "februar", "marts", "april", "maj", "juni", "juli", "august",
|
||||
"september", "oktober", "november", "december"]
|
||||
|
||||
|
||||
def get(path):
|
||||
req = urllib.request.Request(f"{BASE}/api/v1{path}", headers={"Accept": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.load(r)
|
||||
|
||||
|
||||
def repos():
|
||||
data = get(f"/users/{USER}/repos?limit=50&sort=updated")
|
||||
rs = [r for r in data if not (r["fork"] or r["archived"] or r["private"] or r["name"].lower() in HIDE)]
|
||||
pinned = [p.lower() for p in PINNED]
|
||||
rs.sort(key=lambda r: r["updated_at"], reverse=True)
|
||||
rs.sort(key=lambda r: pinned.index(r["name"].lower()) if r["name"].lower() in pinned else len(pinned))
|
||||
keep = ("name", "description", "html_url", "language", "stars_count")
|
||||
return [{k: r[k] for k in keep} for r in rs]
|
||||
|
||||
|
||||
def level(n):
|
||||
return 0 if n == 0 else 1 if n < 3 else 2 if n < 6 else 3 if n < 12 else 4
|
||||
|
||||
|
||||
def heatmap():
|
||||
per_day = {}
|
||||
try:
|
||||
for b in get(f"/users/{USER}/heatmap"):
|
||||
d = dt.datetime.fromtimestamp(b["timestamp"], dt.timezone.utc).date().isoformat()
|
||||
per_day[d] = per_day.get(d, 0) + b["contributions"]
|
||||
except Exception as e:
|
||||
print(f"heatmap: {e}")
|
||||
# 26 uger bagud, uger som kolonner (man-søn)
|
||||
today = dt.date.today()
|
||||
cur = today - dt.timedelta(days=WEEKS * 7 - 1 + today.weekday())
|
||||
weeks = []
|
||||
while cur <= today:
|
||||
week = []
|
||||
for _ in range(7):
|
||||
if cur > today:
|
||||
break
|
||||
n = per_day.get(cur.isoformat(), 0)
|
||||
week.append({"date": cur.isoformat(), "n": n, "level": level(n)})
|
||||
cur += dt.timedelta(days=1)
|
||||
weeks.append(week)
|
||||
return weeks, sum(per_day.values()), f"{today.day}. {MAANEDER[today.month - 1]} {today.year}"
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
rs, error = repos(), False
|
||||
except Exception as e:
|
||||
print(f"repos: {e}")
|
||||
rs, error = [], True
|
||||
weeks, total, updated = heatmap()
|
||||
with open("data/gitea.json", "w") as f:
|
||||
json.dump({"repos": rs, "error": error, "weeks": weeks, "weeks_shown": WEEKS,
|
||||
"total": total, "updated": updated}, f, ensure_ascii=False)
|
||||
print(f"gitea: {len(rs)} repos, {total} bidrag")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
// Håndskrevet service worker: precacher sitet, så det virker offline.
|
||||
// postbuild.py sætter version og URL-liste ind. Alt der ikke er precachet, går til netværket.
|
||||
const CACHE = 'pw-__VERSION__';
|
||||
const URLS = __URLS__;
|
||||
const known = new Set(URLS);
|
||||
|
||||
self.addEventListener('install', (e) => {
|
||||
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(URLS)).then(() => self.skipWaiting()));
|
||||
});
|
||||
|
||||
// Rydder forrige version, også de gamle workbox-caches.
|
||||
self.addEventListener('activate', (e) => {
|
||||
e.waitUntil(caches.keys()
|
||||
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
||||
.then(() => self.clients.claim()));
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (e) => {
|
||||
const url = new URL(e.request.url);
|
||||
if (e.request.method !== 'GET' || url.origin !== location.origin) return;
|
||||
const p = url.pathname;
|
||||
const key = [p, p.replace(/\/?$/, '/index.html')].find((k) => known.has(k));
|
||||
if (key) e.respondWith(caches.match(key).then((r) => r || fetch(e.request)));
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
// Stopper buildet hvis et blogindlæg mangler et gyldigt `godkendt:`-stempel
|
||||
// (HMAC-SHA256 over sti, indhold og billeder).
|
||||
//
|
||||
// PUBLISH_KEY=<hex> node scripts/verify-approvals.mjs
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const DIR = 'src/content/blog';
|
||||
const key = process.env.PUBLISH_KEY;
|
||||
if (!key) {
|
||||
console.error('PUBLISH_KEY mangler; kan ikke verificere godkendelser.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Skal matche godkendelsestjenesten byte for byte.
|
||||
const canonical = (relPath, text, assets = []) => {
|
||||
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
||||
if (lines[0] === '---') {
|
||||
const end = lines.indexOf('---', 1);
|
||||
if (end > 0) {
|
||||
for (let i = end - 1; i > 0; i--) if (/^godkendt:/.test(lines[i])) lines.splice(i, 1);
|
||||
}
|
||||
}
|
||||
const tail = assets.map(([p, h]) => `\n@asset ${p} ${h}`).join('');
|
||||
return `${relPath}\n${lines.join('\n')}${tail}`;
|
||||
};
|
||||
|
||||
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
||||
|
||||
// Billeder til <slug>.md ligger fladt i mappen <slug>/ ved siden af.
|
||||
const assetsFor = (rel) => {
|
||||
const dir = rel.replace(/\.mdx?$/, '');
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir, { withFileTypes: true })
|
||||
.map((e) => {
|
||||
if (!e.isFile()) throw new Error(`${dir}/${e.name}: undermapper understøttes ikke`);
|
||||
const p = `${dir}/${e.name}`;
|
||||
return [p, sha256(fs.readFileSync(p))];
|
||||
})
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
};
|
||||
|
||||
const stampOf = (text) => {
|
||||
const lines = text.replace(/\r\n/g, '\n').split('\n');
|
||||
const end = lines[0] === '---' ? lines.indexOf('---', 1) : -1;
|
||||
if (end < 0) return { stamp: null, draft: false };
|
||||
const fm = lines.slice(1, end);
|
||||
const g = fm.find((l) => /^godkendt:/.test(l));
|
||||
const draft = fm.some((l) => /^draft:\s*true\s*$/.test(l));
|
||||
return { stamp: g ? g.replace(/^godkendt:\s*/, '').replace(/^['"]|['"]$/g, '') : null, draft };
|
||||
};
|
||||
|
||||
const entries = fs.readdirSync(DIR, { withFileTypes: true });
|
||||
const files = entries.filter((e) => e.isFile() && /\.mdx?$/.test(e.name)).map((e) => `${DIR}/${e.name}`);
|
||||
|
||||
let bad = 0;
|
||||
// En billedmappe uden et indlæg ved siden af er aldrig blevet godkendt.
|
||||
for (const e of entries.filter((e) => e.isDirectory())) {
|
||||
if (!files.some((f) => f.replace(/\.mdx?$/, '') === `${DIR}/${e.name}`)) {
|
||||
console.error(`✗ ${DIR}/${e.name}/: filer uden et godkendt indlæg`); bad++;
|
||||
}
|
||||
}
|
||||
for (const rel of files) {
|
||||
const text = fs.readFileSync(rel, 'utf8');
|
||||
const { stamp, draft } = stampOf(text);
|
||||
if (draft) { console.error(`✗ ${rel}: udkast (draft: true) må ikke ligge i det offentlige repo`); bad++; continue; }
|
||||
if (!stamp) { console.error(`✗ ${rel}: mangler godkendelse`); bad++; continue; }
|
||||
let assets;
|
||||
try { assets = assetsFor(rel); } catch (err) { console.error(`✗ ${err.message}`); bad++; continue; }
|
||||
const want = 'v1:' + crypto.createHmac('sha256', Buffer.from(key, 'hex')).update(canonical(rel, text, assets)).digest('hex');
|
||||
const ok = stamp.length === want.length && crypto.timingSafeEqual(Buffer.from(stamp), Buffer.from(want));
|
||||
if (!ok) { console.error(`✗ ${rel}: godkendelsen passer ikke til indholdet (ændret efter godkendelse?)`); bad++; continue; }
|
||||
console.log(`✓ ${rel}${assets.length ? ` (+${assets.length} billeder)` : ''}`);
|
||||
}
|
||||
if (bad) {
|
||||
console.error(`\n${bad} indlæg er ikke godkendt. Intet bliver udgivet.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`\nAlle ${files.length} indlæg er godkendt.`);
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stopper buildet hvis et blogindlæg mangler et gyldigt `godkendt:`-stempel.
|
||||
|
||||
PUBLISH_KEY=<hex> python3 scripts/verify-approvals.py
|
||||
"""
|
||||
import hmac
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from godkendelse import BLOG, assets_for, read_text, sign, stamp_of # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
key = os.environ.get("PUBLISH_KEY")
|
||||
if not key:
|
||||
print("PUBLISH_KEY mangler; kan ikke verificere godkendelser.", file=sys.stderr)
|
||||
return 1
|
||||
key = bytes.fromhex(key)
|
||||
bad = ok = 0
|
||||
|
||||
def fail(msg):
|
||||
nonlocal bad
|
||||
print(f"✗ {msg}", file=sys.stderr)
|
||||
bad += 1
|
||||
|
||||
for name in sorted(os.listdir(BLOG)):
|
||||
path = f"{BLOG}/{name}"
|
||||
if name == "_index.md":
|
||||
continue
|
||||
# Zola bygger enhver .md under content/blog, så intet må ligge uden for en bundle.
|
||||
if not os.path.isdir(path):
|
||||
fail(f"{path}: et indlæg skal ligge som {BLOG}/<slug>/index.md")
|
||||
continue
|
||||
rel = f"{path}/index.md"
|
||||
if not os.path.isfile(rel):
|
||||
fail(f"{path}/: filer uden et godkendt indlæg")
|
||||
continue
|
||||
text = read_text(rel)
|
||||
stamp, draft = stamp_of(text)
|
||||
if draft:
|
||||
fail(f"{rel}: udkast (draft: true) må ikke ligge i det offentlige repo")
|
||||
continue
|
||||
if not stamp:
|
||||
fail(f"{rel}: mangler godkendelse")
|
||||
continue
|
||||
try:
|
||||
assets = assets_for(name)
|
||||
except ValueError as e:
|
||||
fail(str(e))
|
||||
continue
|
||||
if not hmac.compare_digest(stamp, sign(key, rel, text, assets)):
|
||||
fail(f"{rel}: godkendelsen passer ikke til indholdet (ændret efter godkendelse?)")
|
||||
continue
|
||||
ok += 1
|
||||
print(f"✓ {rel}" + (f" (+{len(assets)} billeder)" if assets else ""))
|
||||
if bad:
|
||||
print(f"\n{bad} indlæg er ikke godkendt. Intet bliver udgivet.", file=sys.stderr)
|
||||
return 1
|
||||
print(f"\nAlle {ok} indlæg er godkendt.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 847 KiB |
@@ -1,117 +0,0 @@
|
||||
---
|
||||
import { SITE } from '../consts';
|
||||
import { baseGraph } from '../seo';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
type?: 'website' | 'article';
|
||||
publishedTime?: string;
|
||||
modifiedTime?: string;
|
||||
tags?: string[];
|
||||
jsonLd?: unknown[];
|
||||
// noindex, follow
|
||||
noindex?: boolean;
|
||||
// Sidens sprog (standard: dansk) og dens oversættelser, som hreflang-links.
|
||||
lang?: 'da' | 'en';
|
||||
alternates?: { lang: 'da' | 'en'; path: string }[];
|
||||
}
|
||||
const {
|
||||
title, description, image, type = 'website',
|
||||
publishedTime, modifiedTime, tags = [], jsonLd = [], noindex = false,
|
||||
lang = 'da', alternates = [],
|
||||
} = Astro.props;
|
||||
const hreflang = alternates.map((a) => ({ lang: a.lang, href: new URL(a.path, SITE.url).href }));
|
||||
const xDefault = hreflang.find((a) => a.lang === 'da');
|
||||
|
||||
const pageTitle = title ? `${title} · ${SITE.name}` : `${SITE.title} · Datalog & Technical Solution Architect`;
|
||||
const desc = description ?? SITE.description;
|
||||
// Kanonisk form er uden afsluttende skråstreg, som i sitemap og interne links.
|
||||
const canonicalPath = Astro.url.pathname.replace(/\/$/, '') || '/';
|
||||
const canonical = new URL(canonicalPath, SITE.url).href;
|
||||
const ogImage = new URL(image ?? '/og.png', SITE.url).href;
|
||||
const graphs = [baseGraph, ...jsonLd];
|
||||
---
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={desc} />
|
||||
<meta name="author" content="Asger Weirsøe" />
|
||||
<meta name="google-site-verification" content="l5LfnJZFSkVCauhwN9pND_gvxdnZJbx3FM6fpEK8nzE" />
|
||||
<meta name="robots" content={noindex ? "noindex, follow" : "index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content={type} />
|
||||
<meta property="og:site_name" content={SITE.name} />
|
||||
<meta property="og:title" content={pageTitle} />
|
||||
<meta property="og:description" content={desc} />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:locale" content={lang === 'en' ? 'en_GB' : 'da_DK'} />
|
||||
{hreflang.map((a) => <link rel="alternate" hreflang={a.lang} href={a.href} />)}
|
||||
{xDefault && <link rel="alternate" hreflang="x-default" href={xDefault.href} />}
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content={pageTitle} />
|
||||
{type === 'article' && publishedTime && <meta property="article:published_time" content={publishedTime} />}
|
||||
{type === 'article' && modifiedTime && <meta property="article:modified_time" content={modifiedTime} />}
|
||||
{type === 'article' && <meta property="article:author" content="Asger Weirsøe" />}
|
||||
{type === 'article' && tags.map((t) => <meta property="article:tag" content={t} />)}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content={pageTitle} />
|
||||
<meta name="twitter:description" content={desc} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
|
||||
<meta name="theme-color" content="#0b0f17" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#f6f8fb" media="(prefers-color-scheme: light)" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link rel="alternate" type="application/rss+xml" title={SITE.name} href="/rss.xml" />
|
||||
<link rel="me" href="https://mastodon.social/@asgerweirsoee" />
|
||||
|
||||
{graphs.map((g) => <script type="application/ld+json" set:html={JSON.stringify(g)} />)}
|
||||
|
||||
<script is:inline>
|
||||
var _paq = (window._paq = window._paq || []);
|
||||
_paq.push(['setDomains', ['*.weirsoe.dk', '*.xn--weirse-fya.dk']]);
|
||||
_paq.push(['setVisitorCookieTimeout', 31536000]);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function () {
|
||||
try {
|
||||
var w = screen.width, h = screen.height, dpr = Math.round(window.devicePixelRatio || 1);
|
||||
var k = Math.min(w, h) + 'x' + Math.max(w, h) + '@' + dpr;
|
||||
var ua = navigator.userAgent || '';
|
||||
var isApple = /iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
|
||||
var iphone = {
|
||||
'320x568@2': 'iPhone SE(1)/5s',
|
||||
'375x667@2': 'iPhone SE(2/3)/6/7/8',
|
||||
'414x736@3': 'iPhone 6+/7+/8+ Plus',
|
||||
'375x812@3': 'iPhone X/XS/11Pro/12mini/13mini',
|
||||
'390x844@3': 'iPhone 12/13/14/16e (6,1 tomme notch)',
|
||||
'393x852@3': 'iPhone 14Pro/15/15Pro/16',
|
||||
'402x874@3': 'iPhone 16Pro',
|
||||
'414x896@2': 'iPhone XR/11',
|
||||
'414x896@3': 'iPhone XSMax/11ProMax',
|
||||
'428x926@3': 'iPhone 12ProMax/13ProMax/14Plus',
|
||||
'430x932@3': 'iPhone 14ProMax/15Plus/15ProMax/16Plus',
|
||||
'440x956@3': 'iPhone 16ProMax'
|
||||
};
|
||||
var model = iphone[k] ? iphone[k] : (isApple ? ('Apple ukendt ' + k) : ('Andet ' + k + ' ' + (navigator.platform || '?')));
|
||||
_paq.push(['setCustomDimension', 2, model]);
|
||||
} catch (e) {}
|
||||
})();
|
||||
_paq.push(['trackPageView']);
|
||||
(function () {
|
||||
var u = '/_a/';
|
||||
_paq.push(['setTrackerUrl', u + 'a']);
|
||||
_paq.push(['setSiteId', '2']);
|
||||
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
|
||||
g.async = true; g.src = u + 'a.js'; s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
</script>
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
interface Props { href: string; title: string; date: Date; description?: string; tags?: string[]; }
|
||||
const { href, title, date, description, tags = [] } = Astro.props;
|
||||
const d = date.toLocaleDateString('da-DK', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
---
|
||||
<article class="group wt-card wt-card--hover">
|
||||
<a href={href} class="block">
|
||||
<div class="text-xs text-muted">{d}</div>
|
||||
<h3 class="mt-1 text-lg font-semibold tracking-tight group-hover:text-accent">{title}</h3>
|
||||
{description && <p class="mt-2 text-sm text-muted">{description}</p>}
|
||||
</a>
|
||||
{tags.length > 0 && (
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
{tags.map((t) => (
|
||||
<a href={`/tags/${t}`} class="wt-chip">#{t}</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
@@ -1,78 +0,0 @@
|
||||
---
|
||||
// Én projektside, på dansk eller engelsk. Ruterne /projekter/[slug] og
|
||||
// /en/projects/[slug] giver bare indgangen og evt. oversættelsen med.
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import { render } from 'astro:content';
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { SITE } from '../consts';
|
||||
import { softwareSourceCode, breadcrumb } from '../seo';
|
||||
|
||||
interface Props {
|
||||
side: CollectionEntry<'projekter'>;
|
||||
oversat?: CollectionEntry<'projekter'>; // samme projekt på det andet sprog
|
||||
}
|
||||
const { side, oversat } = Astro.props;
|
||||
const { Content } = await render(side);
|
||||
const d = side.data;
|
||||
const en = d.lang === 'en';
|
||||
const slug = side.id.replace(/^en\//, '');
|
||||
const sti = (lang: 'da' | 'en') => (lang === 'en' ? `/en/projects/${slug}` : `/projekter/${slug}`);
|
||||
const oversigt = en ? '/en/projects' : '/projekter';
|
||||
|
||||
const T = en
|
||||
? { hjem: 'Home', projekter: 'Projects', sprog: 'Language', licens: 'Licence', koerer: 'Runs on',
|
||||
kilde: 'Source on Gitea', bin: 'Prebuilt packages' }
|
||||
: { hjem: 'Forside', projekter: 'Projekter', sprog: 'Sprog', licens: 'Licens', koerer: 'Kører på',
|
||||
kilde: 'Kildekode på Gitea', bin: 'Færdigbyggede pakker' };
|
||||
|
||||
const url = new URL(sti(d.lang), SITE.url).href;
|
||||
const repo = `${SITE.giteaBase}/${SITE.giteaUser}/${d.repo}`;
|
||||
const aur = d.aur.map((n) => ({ navn: n, url: `https://aur.archlinux.org/packages/${n}` }));
|
||||
const published = d.date.toISOString();
|
||||
const modified = (d.updated ?? d.date).toISOString();
|
||||
const ld = softwareSourceCode({
|
||||
url, name: d.title, description: d.description, codeRepository: repo,
|
||||
language: d.sprog, license: d.licens, platforms: d.platforme,
|
||||
sameAs: aur.map((a) => a.url), dateCreated: published, dateModified: modified, keywords: d.tags,
|
||||
lang: d.lang,
|
||||
});
|
||||
const crumbs = breadcrumb([
|
||||
{ name: T.hjem, url: `${SITE.url}${en ? '/en' : '/'}` },
|
||||
{ name: T.projekter, url: `${SITE.url}${oversigt}` },
|
||||
{ name: d.title, url },
|
||||
]);
|
||||
const alternates = oversat
|
||||
? [{ lang: d.lang, path: sti(d.lang) }, { lang: oversat.data.lang, path: sti(oversat.data.lang) }]
|
||||
: [];
|
||||
---
|
||||
<Base title={d.title} description={d.description} type="article" lang={d.lang} {alternates}
|
||||
publishedTime={published} modifiedTime={modified} tags={d.tags} jsonLd={[ld, crumbs]}>
|
||||
<div class="text-sm text-muted">
|
||||
<nav aria-label={en ? 'Breadcrumb' : 'Brødkrumme'}>
|
||||
<a href={en ? '/en' : '/'} class="hover:text-accent">{T.hjem}</a>
|
||||
<span class="px-1.5 text-muted/60">/</span>
|
||||
<a href={oversigt} class="hover:text-accent">{T.projekter}</a>
|
||||
</nav>
|
||||
</div>
|
||||
<article class="mt-4">
|
||||
<header class="border-b border-border pb-6">
|
||||
<h1 class="text-4xl font-bold tracking-tight">{d.title}</h1>
|
||||
<p class="mt-3 max-w-2xl text-lg text-muted">{d.description}</p>
|
||||
<dl class="mt-5 flex flex-wrap gap-x-6 gap-y-2 text-sm">
|
||||
<div><dt class="inline text-muted">{T.sprog} </dt><dd class="inline">{d.sprog}</dd></div>
|
||||
<div><dt class="inline text-muted">{T.licens} </dt><dd class="inline">{d.licens}</dd></div>
|
||||
{d.platforme.length > 0 && <div><dt class="inline text-muted">{T.koerer} </dt><dd class="inline">{d.platforme.join(', ')}</dd></div>}
|
||||
</dl>
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
<a href={repo} class="wt-chip">{T.kilde}</a>
|
||||
{aur.map((a) => (
|
||||
<a href={a.url} class="wt-chip">AUR: {a.navn}</a>
|
||||
))}
|
||||
{d.aur.some((n) => n.endsWith('-bin')) && (
|
||||
<a href="/tarballz/" class="wt-chip">{T.bin}</a>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<div class="prose mt-8"><Content /></div>
|
||||
</article>
|
||||
</Base>
|
||||
@@ -1,43 +0,0 @@
|
||||
export const SITE = {
|
||||
name: 'Asger Weirsøe',
|
||||
title: 'Asger Weirsøe',
|
||||
description: 'Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense. Peppol, NemHandel, e-faktura, selvhosting, Proxmox og AI.',
|
||||
url: 'https://asger.xn--weirse-fya.dk',
|
||||
lang: 'da',
|
||||
email: 'asger@weirsoe.dk',
|
||||
location: 'Nørresundby, Danmark',
|
||||
giteaUser: 'agw',
|
||||
giteaBase: 'https://gitea.weircon.dk',
|
||||
};
|
||||
|
||||
export const NAV = [
|
||||
{ href: '/', label: 'Forside' },
|
||||
{ href: '/cv', label: 'CV' },
|
||||
{ href: '/projekter', label: 'Projekter' },
|
||||
{ href: '/blog', label: 'Blog' },
|
||||
];
|
||||
|
||||
export const NAV_EN = [
|
||||
{ href: '/en', label: 'Home' },
|
||||
{ href: '/en/cv', label: 'CV' },
|
||||
{ href: '/en/projects', label: 'Projects' },
|
||||
{ href: '/blog', label: 'Blog (Danish)' },
|
||||
];
|
||||
|
||||
export const SOCIALS = [
|
||||
{ href: 'https://www.linkedin.com/in/ag-weirs%C3%B8e/', label: 'LinkedIn' },
|
||||
{ href: 'https://mastodon.social/@asgerweirsoee', label: 'Mastodon' },
|
||||
{ href: 'https://github.com/asger-weirsoee', label: 'GitHub' },
|
||||
{ href: 'https://gitea.weircon.dk/agw', label: 'Gitea' },
|
||||
{ href: 'mailto:asger@weirsoe.dk', label: 'Email' },
|
||||
];
|
||||
|
||||
// Styr hvad der vises på /projekter.
|
||||
// hide: repo-navne der skjules helt. pinned: vises først (i den rækkefølge).
|
||||
export const PROJECTS = {
|
||||
hide: [] as string[],
|
||||
pinned: ['ubl-tools', 'gtk-ahfail', 'pi3-smart-workspace'] as string[],
|
||||
};
|
||||
|
||||
// Sat når udkast bygges til preview (se astro.config.mjs).
|
||||
export const PREVIEW = typeof process !== 'undefined' && process.env?.PW_PREVIEW === '1';
|
||||
@@ -1,41 +0,0 @@
|
||||
import { defineCollection, z } from 'astro:content';
|
||||
import { glob } from 'astro/loaders';
|
||||
|
||||
const blog = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
|
||||
schema: () =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string().default(''),
|
||||
date: z.coerce.date(),
|
||||
updated: z.coerce.date().optional(),
|
||||
tags: z.array(z.string()).default([]),
|
||||
draft: z.boolean().default(false),
|
||||
// Godkendelsesstempel; tjekkes af scripts/verify-approvals.mjs før build.
|
||||
godkendt: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Projektsider: én markdown-fil pr. værktøj jeg udgiver. Dansk i roden
|
||||
// (/projekter/<slug>), engelsk i en/ (/en/projects/<slug>) med samme filnavn.
|
||||
// Frontmatter bærer de faste links, så siden og søgeindekset kan vise dem ens.
|
||||
const projekter = defineCollection({
|
||||
loader: glob({ pattern: '**/*.md', base: './src/content/projekter' }),
|
||||
schema: () =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
repo: z.string(), // repo-navn på Gitea under SITE.giteaUser
|
||||
sprog: z.string(), // fx Rust, Python
|
||||
licens: z.string(),
|
||||
platforme: z.array(z.string()).default([]),
|
||||
aur: z.array(z.string()).default([]), // AUR-pakkenavne
|
||||
tags: z.array(z.string()).default([]),
|
||||
date: z.coerce.date(), // første udgivelse
|
||||
updated: z.coerce.date().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
lang: z.enum(['da', 'en']).default('da'),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = { blog, projekter };
|
||||
@@ -1,546 +0,0 @@
|
||||
// E-dokumentprofiler (Nemhandel OIOUBL + Peppol BIS/PINT) til /profiler-opslaget.
|
||||
// Data vedligeholdes her; siden bygger søge-/filter-UI'et af det.
|
||||
//
|
||||
// De danske profiler er ordnet som i Nemhandelsregistrets profil-dropdown, og `kategori`
|
||||
// er registrets egen gruppeoverskrift ("Profiler for: …"). Dokumentlisterne følger
|
||||
// OIOUBL Profiler-guiden (G26) v1.3, tabel 1-3.
|
||||
// * = ApplicationResponse bruges her kun til afvisning; det forretningsmæssige svar på en
|
||||
// ordre er altid OrderResponse/OrderResponseSimple.
|
||||
export interface EProfile {
|
||||
standard: string;
|
||||
/** Nemhandelsregistrets gruppeoverskrift ("Profiler for: …"). Kun danske profiler. */
|
||||
kategori?: string;
|
||||
navn: string;
|
||||
id: string;
|
||||
customization: string;
|
||||
dokumenter: string[];
|
||||
anvendelse: string;
|
||||
bemaerkninger: string;
|
||||
}
|
||||
|
||||
const NH = 'Nemhandel OIOUBL (UBL 2.1)';
|
||||
|
||||
export const profiles: EProfile[] = [
|
||||
// ── Profiler for: Fakturering ──────────────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Fakturering',
|
||||
navn: 'Procurement-BilSim-1.0 (simpel fakturering)',
|
||||
id: 'Procurement-BilSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse (kun ved afvisning)'],
|
||||
anvendelse: 'Ren fakturering: leverandør sender faktura/kreditnota/rykker til kunde. Ingen ordreproces, og kunden er ikke forpligtet til at kvittere.',
|
||||
bemaerkninger: 'Obligatorisk profil og den mest udbredte rene faktura-pakke i Nemhandel. Kunden må afvise med ApplicationResponse, men skal ikke kvittere positivt.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Fakturering',
|
||||
navn: 'urn:www.nesubl.eu:profiles:profile5:ver2.0 (NES5, Billing Basic)',
|
||||
id: 'urn:www.nesubl.eu:profiles:profile5:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Invoice', 'CreditNote'],
|
||||
anvendelse: 'Internationalt NES-fakturaprofil: kunde modtager faktura og kreditnota fra leverandør.',
|
||||
bemaerkninger: 'Obligatorisk profil. Den profil der ligger tættest på den gamle OIOXML-faktura, kun afløftede felter er tilladt. Bruges når fakturaafsender teknisk set ikke kan modtage en ApplicationResponse.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Ordre til fakturering ────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Ordre til fakturering',
|
||||
navn: 'Procurement-OrdSim-BilSim-1.0 (simpel ordre + simpel fakturering)',
|
||||
id: 'Procurement-OrdSim-BilSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Komplet simpel handel: simpel ordre efterfulgt af simpel fakturering. Dækker bestilling til betaling.',
|
||||
bemaerkninger: 'Samlepakken de fleste vælger, når de vil kunne både bestille og fakturere. Ingen ordreændring/-annullering, det kræver en OrdAdv-variant.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Simple Ordrer ────────────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Simple Ordrer',
|
||||
navn: 'Procurement-OrdRes-1.0 (kun ordresvar)',
|
||||
id: 'Procurement-OrdRes-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['OrderResponse'],
|
||||
anvendelse: 'Enkeltdokument-profil til alene at kunne udveksle en ordrebekræftelse (OrderResponse) uden at tilmelde sig en fuld ordreproces.',
|
||||
bemaerkninger: 'Registerprofil af typen "single", findes i Nemhandelsregistret, men er ikke beskrevet i OIOUBL Profiler-guiden (G26) v1.3. Verificér mod registret før brug.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Simple Ordrer',
|
||||
navn: 'Procurement-OrdSim-1.0 (simpel ordre)',
|
||||
id: 'Procurement-OrdSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple'],
|
||||
anvendelse: 'Ren ordreproces: kunde sender ordre, leverandør svarer med simpelt ordresvar. Ingen fakturering i profilen.',
|
||||
bemaerkninger: 'Ren ordre-only proces; kombineres typisk med en faktureringsprofil.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Simple Ordrer',
|
||||
navn: 'Procurement-OrdSimR-1.0 (simpel ordre m. krævet svar)',
|
||||
id: 'Procurement-OrdSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple'],
|
||||
anvendelse: 'Som OrdSim, men leverandør skal altid sende ordresvar.',
|
||||
bemaerkninger: "R = 'Response required': samme dokumenter som OrdSim, men svaret er obligatorisk.",
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Simple Ordrer',
|
||||
navn: 'urn:www.nesubl.eu:profiles:profile3:ver2.0 (NES3, "Order Only")',
|
||||
id: 'urn:www.nesubl.eu:profiles:profile3:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order'],
|
||||
anvendelse: 'Leverandør modtager ordrer fra kunder. Intet ordresvar og ingen fakturering i profilen.',
|
||||
bemaerkninger: 'Ældre NES-ordreprofil; OrdSim-varianterne anbefales i stedet.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Katalog ──────────────────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Katalog',
|
||||
navn: 'Catalogue-CatBas-1.0 (basalt katalog)',
|
||||
id: 'Catalogue-CatBas-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Catalogue', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Leverandør sender et produktkatalog til kunden og kan slette det igen. Kunden kan afvise med ApplicationResponse.',
|
||||
bemaerkninger: 'Simpleste katalogprofil, ingen prisopdatering, ingen katalogforespørgsel.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Katalog',
|
||||
navn: 'Catalogue-CatPriUpd-1.0 (kun prisopdatering)',
|
||||
id: 'Catalogue-CatPriUpd-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['CataloguePricingUpdate', 'ApplicationResponse'],
|
||||
anvendelse: 'Leverandør opdaterer alene priserne i et allerede udvekslet katalog.',
|
||||
bemaerkninger: '"Catalogue Pricing Update Only" forudsætter at kataloget er sendt via en anden katalogprofil.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Katalog',
|
||||
navn: 'Catalogue-CatPriUpdR-1.0 (kun prisopdatering m. krævet svar)',
|
||||
id: 'Catalogue-CatPriUpdR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['CataloguePricingUpdate', 'ApplicationResponse'],
|
||||
anvendelse: 'Som CatPriUpd, men kunden skal kvittere for prisopdateringen.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Katalog',
|
||||
navn: 'Catalogue-CatSim-1.0 (katalogforespørgsel + katalog)',
|
||||
id: 'Catalogue-CatSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Kunden beder om et katalog (CatalogueRequest), leverandøren sender det og kan slette det igen.',
|
||||
bemaerkninger: 'CatBas udvidet med katalogforespørgsel fra kunden.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Udvidet fakturering ──────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet fakturering',
|
||||
navn: 'Procurement-BilSimR-1.0 (simpel fakturering m. krævet svar)',
|
||||
id: 'Procurement-BilSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse'],
|
||||
anvendelse: 'Som BilSim, men kunden skal altid kvittere med et ApplicationResponse (accept/afvisning).',
|
||||
bemaerkninger: "R = 'Response required': kunden skal kvittere. Bemærk: tilmelding til BilSimR giver ikke automatisk BilSim; begge skal registreres hver for sig.",
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet fakturering',
|
||||
navn: 'Procurement-BilSimReminderOnly-1.0 (kun rykker)',
|
||||
id: 'Procurement-BilSimReminderOnly-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Reminder'],
|
||||
anvendelse: 'Enkeltdokument-profil til alene at kunne modtage rykkere.',
|
||||
bemaerkninger: 'Registerprofil af typen "single", ikke i OIOUBL Profiler-guiden (G26) v1.3. Praktisk fordi Reminder ikke findes i Peppol BIS Billing og derfor kun kan sendes i OIOUBL-profilerne.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet fakturering',
|
||||
navn: 'Procurement-PayBas-1.0 (kontoudtog)',
|
||||
id: 'Procurement-PayBas-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Statement', 'ApplicationResponse'],
|
||||
anvendelse: 'Leverandør sender kontoudtog (Statement) til kunden, en samlet oversigt over udestående poster, ikke en faktura.',
|
||||
bemaerkninger: 'G26 kalder profilen direkte "Statement". Trods "Pay"-navnet er der ingen betalingsdokumenter i profilen.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet fakturering',
|
||||
navn: 'Procurement-PayBasR-1.0 (kontoudtog m. krævet svar)',
|
||||
id: 'Procurement-PayBasR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Statement', 'ApplicationResponse'],
|
||||
anvendelse: 'Som PayBas, men kunden skal kvittere for kontoudtoget.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet fakturering',
|
||||
navn: 'urn:www.nesubl.eu:profiles:profile8:ver2.0 (NES8)',
|
||||
id: 'urn:www.nesubl.eu:profiles:profile8:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Invoice', 'CreditNote', 'ApplicationResponse'],
|
||||
anvendelse: 'NES-faktureringsprofil med kvittering: faktura og kreditnota, hvor kunden kan svare med ApplicationResponse.',
|
||||
bemaerkninger: 'NES5 udvidet med ApplicationResponse. Ingen Reminder.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Udvidet katalog ──────────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'Catalogue-CatAdv-1.0 (avanceret katalog)',
|
||||
id: 'Catalogue-CatAdv-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Fuld katalogproces: forespørgsel, katalog, opdatering af både varebeskrivelser og priser samt sletning.',
|
||||
bemaerkninger: 'Den mest komplette katalogprofil: CatExt plus katalogforespørgsel.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'Catalogue-CatAdvR-1.0 (avanceret katalog m. krævet svar)',
|
||||
id: 'Catalogue-CatAdvR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Som CatAdv, men modtageren skal kvittere for katalogdokumenterne.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'Catalogue-CatBasR-1.0 (basalt katalog m. krævet svar)',
|
||||
id: 'Catalogue-CatBasR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Catalogue', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Som CatBas, men kunden skal kvittere for kataloget.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'Catalogue-CatExt-1.0 (udvidet katalog + opdatering)',
|
||||
id: 'Catalogue-CatExt-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Katalogudveksling med efterfølgende opdatering af varebeskrivelser og priser samt sletning. Ingen katalogforespørgsel.',
|
||||
bemaerkninger: 'CatBas udvidet med opdateringsdokumenter.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'Catalogue-CatExtR-1.0 (udvidet katalog + opdatering m. krævet svar)',
|
||||
id: 'Catalogue-CatExtR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Som CatExt, men kunden skal kvittere.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'Catalogue-CatSimR-1.0 (katalogforespørgsel + katalog m. krævet svar)',
|
||||
id: 'Catalogue-CatSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueDeletion', 'ApplicationResponse'],
|
||||
anvendelse: 'Som CatSim, men katalogsvar er påkrævet.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'urn:www.nesubl.eu:profiles:profile1:ver2.0 (NES1, "Catalogue Only")',
|
||||
id: 'urn:www.nesubl.eu:profiles:profile1:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Catalogue', 'ApplicationResponse'],
|
||||
anvendelse: 'Ren katalogudsendelse: leverandør sender katalog, kunden kan afvise.',
|
||||
bemaerkninger: 'Ældre NES-katalogprofil; Catalogue-CatBas-1.0 anbefales i stedet.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet katalog',
|
||||
navn: 'urn:www.nesubl.eu:profiles:profile2:ver2.0 (NES2, simpel katalogproces)',
|
||||
id: 'urn:www.nesubl.eu:profiles:profile2:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate'],
|
||||
anvendelse: 'Katalog med efterfølgende opdatering af varebeskrivelser og priser. Ingen kvittering i profilen.',
|
||||
bemaerkninger: 'Ældre NES-katalogprofil; Catalogue-CatExt-1.0 anbefales i stedet.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Udvidet ordre til fakturering ────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdAdv-BilSim-1.0 (avanceret ordre + simpel fakturering)',
|
||||
id: 'Procurement-OrdAdv-BilSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Avanceret ordreproces (fuldt ordresvar samt ordreændring og -annullering) efterfulgt af simpel fakturering.',
|
||||
bemaerkninger: 'Understøtter begge parter både OrdSim-BilSim og OrdAdv-BilSim, skal OrdAdv-BilSim bruges.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdAdv-BilSimR-1.0 (avanceret ordre + fakturering m. krævet fakturasvar)',
|
||||
id: 'Procurement-OrdAdv-BilSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Som OrdAdv-BilSim, men fakturasvar (ApplicationResponse) er påkrævet.',
|
||||
bemaerkninger: 'R sidder på fakturadelen, ordresvaret er stadig valgfrit.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdAdvR-BilSim-1.0 (avanceret ordre m. krævet ordresvar + simpel fakturering)',
|
||||
id: 'Procurement-OrdAdvR-BilSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Som OrdAdv-BilSim, men ordresvar er påkrævet.',
|
||||
bemaerkninger: 'R sidder på ordredelen, fakturasvaret er valgfrit.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdAdvR-BilSimR-1.0 (avanceret ordre + fakturering, begge svar krævet)',
|
||||
id: 'Procurement-OrdAdvR-BilSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Avanceret ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.',
|
||||
bemaerkninger: 'Strammeste variant af ordre-til-faktura-kæden.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdSel-BilSim-1.0 (sælgerinitieret ordre + simpel fakturering)',
|
||||
id: 'Procurement-OrdSel-BilSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Sælgerinitieret handel: leverandøren starter med en ordrebekræftelse (fx efter aftale i marken eller telefonisk), kunden kan ændre eller annullere, og der faktureres bagefter.',
|
||||
bemaerkninger: 'Bemærk: ingen Order i profilen, "Sel" = seller initiated, så processen starter med OrderResponse.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdSel-BilSimR-1.0 (sælgerinitieret ordre + fakturering m. krævet svar)',
|
||||
id: 'Procurement-OrdSel-BilSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Som OrdSel-BilSim, men fakturasvar er påkrævet.',
|
||||
bemaerkninger: 'Ingen Order i profilen, processen starter hos sælger.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdSim-BilSimR-1.0 (simpel ordre + fakturering m. krævet fakturasvar)',
|
||||
id: 'Procurement-OrdSim-BilSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Simpel ordre + fakturering hvor fakturasvar (ApplicationResponse) er påkrævet.',
|
||||
bemaerkninger: 'R sidder på fakturadelen.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdSimR-BilSim-1.0 (simpel ordre m. krævet ordresvar + fakturering)',
|
||||
id: 'Procurement-OrdSimR-BilSim-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Som OrdSim-BilSim, men ordresvar er påkrævet.',
|
||||
bemaerkninger: 'R sidder på ordredelen.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'Procurement-OrdSimR-BilSimR-1.0 (simpel ordre + fakturering, begge svar krævet)',
|
||||
id: 'Procurement-OrdSimR-BilSimR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'],
|
||||
anvendelse: 'Simpel ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.',
|
||||
bemaerkninger: 'Begge R’er sat, modparten skal kvittere i begge led.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Udvidet ordre til fakturering',
|
||||
navn: 'urn:www.nesubl.eu:profiles:profile7:ver2.0 (NES7, ordre til faktura)',
|
||||
id: 'urn:www.nesubl.eu:profiles:profile7:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'ApplicationResponse *'],
|
||||
anvendelse: 'NES-profil for simpel ordre efterfulgt af simpel fakturering.',
|
||||
bemaerkninger: 'Ældre NES-pendant til OrdSim-BilSim. Ingen Reminder i profilen.',
|
||||
},
|
||||
|
||||
// ── Profiler for: ForsyningsSpecifikation ──────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'ForsyningsSpecifikation',
|
||||
navn: 'Reference-Utility-1.0 (forsyningsspecifikation, UTS)',
|
||||
id: 'Reference-Utility-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['UtilityStatement', 'ApplicationResponse'],
|
||||
anvendelse: 'Forsyningsbranchen (el/vand/varme/gas): leverandør sender detaljeret forbrugs-/forsyningsspecifikation der ledsager fakturaen; kunden kan svare med ApplicationResponse.',
|
||||
bemaerkninger: 'Bruges som tillæg til en faktureringsprofil, UTS erstatter ikke fakturaen.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'ForsyningsSpecifikation',
|
||||
navn: 'Reference-UtilityR-1.0 (forsyningsspecifikation m. krævet svar)',
|
||||
id: 'Reference-UtilityR-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['UtilityStatement', 'ApplicationResponse'],
|
||||
anvendelse: 'Som Reference-Utility, men kunden skal kvittere for forsyningsspecifikationen.',
|
||||
bemaerkninger: 'R = svar påkrævet.',
|
||||
},
|
||||
|
||||
// ── Profiler for: Diverse ──────────────────────────────────────────────────
|
||||
{
|
||||
standard: NH,
|
||||
kategori: 'Diverse',
|
||||
navn: 'Procurement-TecRes-1.0 (kvittering på modtaget dokument)',
|
||||
id: 'Procurement-TecRes-1.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['ApplicationResponse'],
|
||||
anvendelse: 'Ren kvitteringsprofil: bruges til at svare på et modtaget dokument, typisk når afsendelsen ikke kan behandles pga. tekniske fejl eller en ikke-understøttet profil.',
|
||||
bemaerkninger: 'Bruges som ProfileID på selve ApplicationResponse’en, ikke som en forretningsproces man handler i.',
|
||||
},
|
||||
{
|
||||
standard: NH,
|
||||
navn: 'urn:www.cenbii.eu:profile:bii30:ver2.0 (forsendelsesadvis / despatch advice)',
|
||||
id: 'urn:www.cenbii.eu:profile:bii30:ver2.0',
|
||||
customization: 'OIOUBL-2.x',
|
||||
dokumenter: ['DespatchAdvice'],
|
||||
anvendelse: 'Leverandør varsler en forsendelse, så kunden kan forberede modtagelse og afstemme leveringen mod ordren.',
|
||||
bemaerkninger: 'CEN BII-profil optaget i OIOUBL (G26 afsnit 3.3.15). Ikke placeret i en registerkategori her, bekræft gruppen i Nemhandelsregistrets dropdown før brug.',
|
||||
},
|
||||
|
||||
// ── Peppol BIS 3.0 ─────────────────────────────────────────────────────────
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Billing 3.0',
|
||||
id: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0',
|
||||
customization: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0',
|
||||
dokumenter: ['Invoice', 'CreditNote'],
|
||||
anvendelse: 'Den centrale e-faktura/kreditnota-pakke i Peppol. CIUS af EN 16931. Bruges til EU-grænsehandel og DK/Peppol-fakturering.',
|
||||
bemaerkninger: 'Langt den mest brugte BIS3-profil. Invoice Response er en separat profil. Bemærk: Reminder findes ikke i Peppol, brug OIOUBL-profilerne til rykkere.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Self-Billing 3.0',
|
||||
id: 'urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0',
|
||||
customization: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0#conformant#urn:fdc:peppol.eu:2017:poacc:selfbilling:international:3.0',
|
||||
dokumenter: ['Invoice (self-billed)', 'CreditNote'],
|
||||
anvendelse: 'Selvfakturering: køber udsteder fakturaen på vegne af sælger.',
|
||||
bemaerkninger: 'Genbruger Billing-syntaks med self-billing customization.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Invoice Response 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:invoice_response:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:invoice_response:3',
|
||||
dokumenter: ['ApplicationResponse (Invoice Response)'],
|
||||
anvendelse: 'Køber sender struktureret status/svar på en modtaget faktura (accepteret, afvist, under behandling, betalt).',
|
||||
bemaerkninger: 'Bruges sammen med Billing til at lukke faktura-livscyklussen. Peppol-pendant til OIOUBL-ernes "R"-profiler.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Message Level Response 3.0 (MLR)',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:mlr:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:mlr:3',
|
||||
dokumenter: ['ApplicationResponse (Message Level Response)'],
|
||||
anvendelse: 'Teknisk kvittering på beskedniveau: bekræfter modtagelse/validering af et hvilket som helst dokument.',
|
||||
bemaerkninger: 'Transport-/syntaks-niveau svar, ikke forretningsmæssigt indhold. Peppol-pendant til Procurement-TecRes-1.0.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Ordering 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:ordering:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:order:3 / urn:fdc:peppol.eu:poacc:trns:order_response:3',
|
||||
dokumenter: ['Order', 'Order Response'],
|
||||
anvendelse: 'Fuld ordreproces: køber sender ordre, sælger svarer med ordrebekræftelse/-svar.',
|
||||
bemaerkninger: 'Order = Trdm01, Order Response = Trdm76. Peppol-pendant til Procurement-OrdSim-1.0.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Order Only 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:order_only:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:order:3',
|
||||
dokumenter: ['Order'],
|
||||
anvendelse: 'Kun afsendelse af ordre uden forventet struktureret ordresvar.',
|
||||
bemaerkninger: 'Enkel ordre-only proces. Peppol-pendant til NES3 ("Order Only").',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Order Agreement 3.0 (Advanced Ordering)',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:order_agreement:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:order_agreement:3',
|
||||
dokumenter: ['Order', 'Order Response (Advanced)'],
|
||||
anvendelse: 'Avanceret ordre der etablerer en bindende ordreaftale mellem parterne.',
|
||||
bemaerkninger: 'Bruges hvor ordresvar danner juridisk aftale.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Catalogue 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:catalogue:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:catalogue:3',
|
||||
dokumenter: ['Catalogue', 'ApplicationResponse (Catalogue Response)'],
|
||||
anvendelse: 'Sælger sender produktkatalog (priser/varer) til køber, der kan svare med accept/afvisning.',
|
||||
bemaerkninger: 'Grundlag for efterfølgende ordrer. Peppol-pendant til Catalogue-CatBasR-1.0.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Catalogue without Response 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:catalogue_wo_response:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:catalogue:3',
|
||||
dokumenter: ['Catalogue'],
|
||||
anvendelse: 'Katalogudsendelse uden forventet katalogsvar.',
|
||||
bemaerkninger: 'Variant uden Catalogue Response.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Punch Out 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:punch_out:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:punch_out:3',
|
||||
dokumenter: ['Catalogue (Punch Out)'],
|
||||
anvendelse: "Punch-out katalog: køber 'hopper ud' til sælgers webshop og henter en kurv tilbage.",
|
||||
bemaerkninger: 'Bruges i e-handels-/webshop-integrationer.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 3.0',
|
||||
navn: 'Peppol BIS Despatch Advice 3.0',
|
||||
id: 'urn:fdc:peppol.eu:poacc:bis:despatch_advice:3',
|
||||
customization: 'urn:fdc:peppol.eu:poacc:trns:despatch_advice:3',
|
||||
dokumenter: ['Despatch Advice (følgeseddel)'],
|
||||
anvendelse: 'Sælger sender forsendelses-/leveringsmeddelelse (hvad er afsendt, hvornår, hvordan) inden faktura.',
|
||||
bemaerkninger: 'Logistik-dokument mellem ordre og faktura. Peppol-pendant til urn:www.cenbii.eu:profile:bii30:ver2.0 i OIOUBL.',
|
||||
},
|
||||
|
||||
// ── Peppol BIS 4.0 / PINT ──────────────────────────────────────────────────
|
||||
{
|
||||
standard: 'Peppol BIS 4.0 / PINT',
|
||||
navn: 'EU PINT Billing (v1.0.1; forløber for BIS Billing 4.0)',
|
||||
id: 'urn:peppol:pint:billing-1@eu-1 (PINT EU Billing)',
|
||||
customization: 'urn:peppol:pint:billing-1@eu-1',
|
||||
dokumenter: ['Invoice', 'CreditNote'],
|
||||
anvendelse: 'Peppols internationale fakturamodel (PINT) tilpasset EU. Implementerer BIS Billing 3.0 som PINT-kompatibel uden ændringer; baner vej for grænseoverskridende fakturering mod lande med egne PINT-specs (AU/JP/MY/NZ/SG m.fl.).',
|
||||
bemaerkninger: 'v1.0.0 udgivet okt. 2025, nu v1.0.1. CIUS af EN 16931. NB: identifikator-format afviger fra BIS3, verificér mod docs.peppol.eu/poac/eu/pint-eu inden brug.',
|
||||
},
|
||||
{
|
||||
standard: 'Peppol BIS 4.0 / PINT',
|
||||
navn: 'Peppol BIS Billing 4.0 (kommende)',
|
||||
id: '(endnu ikke endeligt fastlagt)',
|
||||
customization: '(endnu ikke endeligt fastlagt)',
|
||||
dokumenter: ['Invoice', 'CreditNote (forventet)'],
|
||||
anvendelse: 'Næste generation der lægger BIS og PINT sammen til ét globalt faktura-format. Forventet 2026, endnu ikke udgivet (pr. juni 2026).',
|
||||
bemaerkninger: "STATUS: under udarbejdelse pr. juni 2026, ID'er og indhold ikke endeligt publiceret. Brug BIS Billing 3.0 / EU PINT i mellemtiden.",
|
||||
},
|
||||
];
|
||||
|
||||
export const standarder = [...new Set(profiles.map((p) => p.standard))];
|
||||
|
||||
/** Nemhandelsregistrets kategorier i registrets egen rækkefølge. */
|
||||
export const kategorier = [...new Set(profiles.map((p) => p.kategori).filter((k): k is string => Boolean(k)))];
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
import Head from '../components/Head.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Palette from '../components/Palette.astro';
|
||||
import CookieBanner from '../components/CookieBanner.astro';
|
||||
import { SITE, PREVIEW } from '../consts';
|
||||
import '../styles/global.css';
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
type?: 'website' | 'article';
|
||||
publishedTime?: string;
|
||||
modifiedTime?: string;
|
||||
tags?: string[];
|
||||
jsonLd?: unknown[];
|
||||
noindex?: boolean;
|
||||
lang?: 'da' | 'en';
|
||||
alternates?: { lang: 'da' | 'en'; path: string }[];
|
||||
}
|
||||
const { title, description, image, type, publishedTime, modifiedTime, tags, jsonLd, noindex, lang, alternates } = Astro.props;
|
||||
---
|
||||
<!doctype html>
|
||||
<html lang={lang ?? SITE.lang}>
|
||||
<head>
|
||||
{/* Skal stå i <head>, ellers dropper Astro <html> og <head>. */}
|
||||
<Fragment set:html={`<!--
|
||||
_ ____ ____ _____ ____
|
||||
/ \\ / ___| / ___| ____| _ \\
|
||||
/ _ \\ \\___ \\| | _| _| | |_) |
|
||||
/ ___ \\ ___) | |_| | |___| _ <
|
||||
/_/ \\_\\____/ \\____|_____|_| \\_\\
|
||||
|
||||
Statisk Astro, self-hosted på Proxmox.
|
||||
Prøv Cmd/Ctrl+K.. eller den gamle kode: op op ned ned venstre højre venstre højre B A.
|
||||
-->`} />
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const apply = () => {
|
||||
const t = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||||
document.documentElement.dataset.theme = t;
|
||||
};
|
||||
apply();
|
||||
document.addEventListener('astro:after-swap', apply);
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-theme-toggle]');
|
||||
if (!btn) return;
|
||||
const next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.dataset.theme = next;
|
||||
localStorage.setItem('theme', next);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<Head {title} {description} {image} {type} {publishedTime} {modifiedTime} {tags} {jsonLd} noindex={noindex || PREVIEW} {lang} {alternates} />
|
||||
<ClientRouter />
|
||||
{!PREVIEW && <script is:inline src="/registerSW.js"></script>}
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col antialiased">
|
||||
{PREVIEW && (
|
||||
<div class="sticky top-0 z-50 bg-amber-400 px-4 py-2 text-center text-sm font-semibold text-black">
|
||||
UDKAST, ikke udgivet. <a href="/udkast/" class="underline">Tilbage til godkendelse</a>
|
||||
</div>
|
||||
)}
|
||||
<Header {lang} {alternates} />
|
||||
<main class="mx-auto w-full max-w-3xl flex-1 px-5 py-12">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer {lang} />
|
||||
<Palette />
|
||||
<CookieBanner {lang} />
|
||||
<script is:inline>
|
||||
// Konami-kode → terminal-tema
|
||||
(() => {
|
||||
if (window.__konami) return; window.__konami = true;
|
||||
const seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
|
||||
let pos = 0;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
pos = e.key === seq[pos] ? pos + 1 : (e.key === seq[0] ? 1 : 0);
|
||||
if (pos !== seq.length) return;
|
||||
pos = 0;
|
||||
document.documentElement.dataset.theme = 'terminal';
|
||||
localStorage.setItem('theme', 'terminal');
|
||||
document.body.animate(
|
||||
[{ transform: 'rotate(0)' }, { transform: 'rotate(360deg)' }],
|
||||
{ duration: 900, easing: 'ease-in-out' });
|
||||
console.log('%c> adgang givet. velkommen til terminalen.', 'color:#4bff7e;font-family:monospace;font-size:14px');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="404">
|
||||
<div class="py-20 text-center">
|
||||
<h1 class="text-6xl font-bold tracking-tight">404</h1>
|
||||
<p class="mt-4 text-muted">Siden findes ikke.</p>
|
||||
<a href="/" class="mt-8 wt-btn wt-btn--primary wt-btn--lg">Til forsiden</a>
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,142 +0,0 @@
|
||||
---
|
||||
import { getCollection, render } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { SITE, PREVIEW } from '../../consts';
|
||||
import { blogPosting, breadcrumb } from '../../seo';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
|
||||
}
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await render(post);
|
||||
const d = post.data.date.toLocaleDateString('da-DK', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
|
||||
const url = new URL(`/blog/${post.id}`, SITE.url).href;
|
||||
const published = post.data.date.toISOString();
|
||||
const modified = (post.data.updated ?? post.data.date).toISOString();
|
||||
const wordCount = (post.body?.trim().split(/\s+/).filter(Boolean).length) || undefined;
|
||||
const ld = blogPosting({
|
||||
url, headline: post.data.title, description: post.data.description,
|
||||
datePublished: published, dateModified: modified,
|
||||
tags: post.data.tags, image: new URL('/og.png', SITE.url).href,
|
||||
wordCount, section: post.data.tags[0] || 'Blog',
|
||||
});
|
||||
const crumbs = breadcrumb([
|
||||
{ name: 'Forside', url: `${SITE.url}/` },
|
||||
{ name: 'Blog', url: `${SITE.url}/blog` },
|
||||
{ name: post.data.title, url },
|
||||
]);
|
||||
---
|
||||
<Base
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
type="article"
|
||||
publishedTime={published}
|
||||
modifiedTime={modified}
|
||||
tags={post.data.tags}
|
||||
jsonLd={[ld, crumbs]}
|
||||
>
|
||||
<nav aria-label="Brødkrumme" class="text-sm text-muted">
|
||||
<a href="/" class="hover:text-accent">Forside</a>
|
||||
<span class="px-1.5 text-muted/60">/</span>
|
||||
<a href="/blog" class="hover:text-accent">Blog</a>
|
||||
</nav>
|
||||
<article class="mt-4">
|
||||
<header class="border-b border-border pb-6">
|
||||
<div class="text-sm text-muted">{d}</div>
|
||||
<h1 class="mt-2 text-4xl font-bold tracking-tight">{post.data.title}</h1>
|
||||
{post.data.tags.length > 0 && (
|
||||
<div class="mt-4 flex flex-wrap gap-1.5">
|
||||
{post.data.tags.map((t) => (
|
||||
<a href={`/tags/${t}`} class="wt-chip">#{t}</a>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div class="prose mt-8"><Content /></div>
|
||||
</article>
|
||||
|
||||
{!PREVIEW && (
|
||||
<Fragment>
|
||||
<section class="mt-16 border-t border-border pt-8">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Kommentarer</h2>
|
||||
<comentario-comments class="mt-6 block" live-update="true"></comentario-comments>
|
||||
</section>
|
||||
<script is:inline>
|
||||
(function () {
|
||||
if (!window.__comentarioLoaded) {
|
||||
window.__comentarioLoaded = true;
|
||||
var s = document.createElement('script');
|
||||
s.src = 'https://comentario.weircon.dk/comentario.js';
|
||||
s.defer = true;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<style is:global>
|
||||
.prose pre { position: relative; }
|
||||
.copy-btn {
|
||||
position: absolute; top: .45rem; right: .45rem;
|
||||
font-size: .68rem; line-height: 1; padding: .35rem .6rem;
|
||||
color: var(--color-muted); background: var(--color-surface-2);
|
||||
border: 1px solid var(--color-border); border-radius: .5rem;
|
||||
opacity: 0; transition: opacity .15s ease; cursor: pointer;
|
||||
}
|
||||
.prose pre:hover .copy-btn, .copy-btn:focus-visible { opacity: 1; }
|
||||
.copy-btn:hover { color: var(--color-text); }
|
||||
</style>
|
||||
<script>
|
||||
// Mermaid-diagrammer: ```mermaid-blokke rendres client-side. Dynamic import =
|
||||
// bundlen (self-hosted) hentes KUN på indlæg der faktisk har diagrammer.
|
||||
async function renderMermaid() {
|
||||
const blocks = document.querySelectorAll('pre[data-language="mermaid"] code, code.language-mermaid');
|
||||
if (!blocks.length) return;
|
||||
const { default: mermaid } = await import('mermaid');
|
||||
mermaid.initialize({ startOnLoad: false,
|
||||
theme: document.documentElement.dataset.theme === 'light' ? 'default' : 'dark' });
|
||||
let i = 0;
|
||||
for (const code of blocks) {
|
||||
try {
|
||||
const { svg } = await mermaid.render(`mmd-${i++}`, code.innerText);
|
||||
const el = document.createElement('div');
|
||||
el.className = 'mermaid-diagram';
|
||||
el.innerHTML = svg;
|
||||
code.closest('pre')?.replaceWith(el);
|
||||
} catch { /* lad kodeblokken stå hvis diagrammet ikke parser */ }
|
||||
}
|
||||
}
|
||||
renderMermaid();
|
||||
document.addEventListener('astro:page-load', renderMermaid);
|
||||
</script>
|
||||
<style is:global>
|
||||
.mermaid-diagram { margin: 1.5rem 0; overflow-x: auto; }
|
||||
.mermaid-diagram svg { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
if (window.__copycode) return; window.__copycode = true;
|
||||
var enhance = function () {
|
||||
document.querySelectorAll('.prose pre').forEach(function (pre) {
|
||||
if (pre.querySelector('.copy-btn')) return;
|
||||
var b = document.createElement('button');
|
||||
b.className = 'copy-btn'; b.type = 'button'; b.textContent = 'kopiér';
|
||||
b.addEventListener('click', function () {
|
||||
var code = pre.querySelector('code');
|
||||
navigator.clipboard.writeText(code ? code.innerText : pre.innerText).then(function () {
|
||||
b.textContent = 'kopieret ✓';
|
||||
setTimeout(function () { b.textContent = 'kopiér'; }, 1500);
|
||||
});
|
||||
});
|
||||
pre.appendChild(b);
|
||||
});
|
||||
};
|
||||
document.addEventListener('astro:page-load', enhance);
|
||||
enhance();
|
||||
})();
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import PostCard from '../../components/PostCard.astro';
|
||||
import { SITE } from '../../consts';
|
||||
import { breadcrumb, collectionPage } from '../../seo';
|
||||
|
||||
const posts = (await getCollection('blog', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
|
||||
const blogUrl = `${SITE.url}/blog`;
|
||||
const jsonLd = [
|
||||
collectionPage({ url: blogUrl, name: 'Blog · Asger Weirsøe',
|
||||
description: 'Indlæg om selvhosting, Proxmox, AI og udvikling.' }),
|
||||
breadcrumb([{ name: 'Forside', url: `${SITE.url}/` }, { name: 'Blog', url: blogUrl }]),
|
||||
];
|
||||
---
|
||||
<Base title="Blog" description="Indlæg om selvhosting, Proxmox, AI og udvikling." jsonLd={jsonLd}>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<h1 class="text-4xl font-bold tracking-tight">Blog</h1>
|
||||
<a href="/tags" class="text-sm text-accent hover:underline">Tags</a>
|
||||
</div>
|
||||
<div class="mt-8 grid gap-4">
|
||||
{posts.map((p) => (
|
||||
<PostCard href={`/blog/${p.id}`} title={p.data.title} date={p.data.date} description={p.data.description} tags={p.data.tags} />
|
||||
))}
|
||||
{posts.length === 0 && <p class="text-muted">Ingen indlæg endnu.</p>}
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import Chat from '../components/Chat.astro';
|
||||
---
|
||||
<Base title="Spørg mit CV" description="En lille AI-chat der svarer på spørgsmål om mig, min erfaring og mit homelab." noindex
|
||||
alternates={[{ lang: 'da', path: '/chat' }, { lang: 'en', path: '/en/chat' }]}>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Spørg mit CV</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
En lille chat med <strong class="text-text">Claude</strong> fra Anthropic, der har læst mit CV og denne
|
||||
side og svarer på spørgsmål om mig.. fx <em>“hvad har Asger lavet med Peppol?”</em> eller
|
||||
<em>“hvilken stack kører hans homelab?”</em>. Den svarer kun på spørgsmål om mig.
|
||||
</p>
|
||||
|
||||
<Chat />
|
||||
</Base>
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
|
||||
const stack = [
|
||||
{ k: 'Generator', v: 'Astro 5: statisk output, view transitions, content collections til bloggen' },
|
||||
{ k: 'Runtime', v: 'Bun (build og dev)' },
|
||||
{ k: 'CSS', v: 'Tailwind 4 med egne design-tokens; dark/light sat pre-paint uden flash' },
|
||||
{ k: 'Typografi', v: 'Inter Variable, self-hosted woff2 (ingen Google Fonts-kald)' },
|
||||
{ k: 'JS i browseren', v: 'Ingen frameworks. Lidt vanilla til tema-toggle, paletten og service workeren' },
|
||||
{ k: 'PWA', v: 'Workbox service worker, siden virker offline og kan installeres' },
|
||||
{ k: 'Kommentarer', v: 'Comentario, self-hosted, login via min egen Authentik' },
|
||||
{ k: 'Analytics', v: 'Matomo, self-hosted og first-party, ingen tredjeparts-trackere' },
|
||||
{ k: 'Hosting', v: 'Alpine LXC med nginx på min egen Proxmox-node, bag NPMplus som edge' },
|
||||
{ k: 'CI/CD', v: 'Gitea Actions på egne runnere: push til master → build → atomisk swap af webroot' },
|
||||
{ k: 'Struktureret data', v: 'JSON-LD: Person, ProfilePage, FAQ, BlogPosting, BreadcrumbList, plus llms.txt til LLM’er' },
|
||||
];
|
||||
---
|
||||
<Base title="Kolofon" description="Hvordan denne side er bygget: Astro, Bun, Tailwind, self-hosted alt, og ingen JS-frameworks.">
|
||||
<h1 class="text-4xl font-bold tracking-tight">Kolofon</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Hele kæden fra commit til din skærm kører på udstyr jeg selv drifter. Ingen CDN’er,
|
||||
ingen tredjeparts-fonts, ingen eksterne trackere.. bare en statisk side der gør hvad den får besked på.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 wt-card wt-card--flush">
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
{stack.map((row) => (
|
||||
<tr class="border-b border-border/60 last:border-0">
|
||||
<td class="w-40 bg-surface/60 px-4 py-3 font-semibold align-top">{row.k}</td>
|
||||
<td class="px-4 py-3 text-muted">{row.v}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Sikkerhed</h2>
|
||||
<p class="mt-3 max-w-2xl text-sm text-muted">
|
||||
Fundet noget der ser forkert ud? Skriv til <a class="text-accent hover:underline" href="mailto:asger@weirsoe.dk">asger@weirsoe.dk</a>.
|
||||
Der ligger også en <a class="text-accent hover:underline" href="/.well-known/security.txt">security.txt</a> (RFC 9116).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p class="mt-10 text-sm text-muted">
|
||||
Se også <a class="text-accent hover:underline" href="/uses">uses</a>.
|
||||
</p>
|
||||
</Base>
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="Cookies" description="Hvilke cookies asger.weirsøe.dk bruger, og hvad de bruges til." noindex>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Cookies</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Denne side bruger cookies til at forbedre din oplevelse og lave anonym statistik over,
|
||||
hvordan siden bruges. <strong class="text-text">Ved at bruge siden accepterer du, at der
|
||||
sættes cookies.</strong> Du kan altid slette dem igen i din browser.
|
||||
</p>
|
||||
|
||||
<h2 class="mt-10 text-2xl font-bold tracking-tight">Hvad bruger vi dem til?</h2>
|
||||
<div class="mt-5 grid gap-4">
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold tracking-tight">Statistik & analyse</h3>
|
||||
<p class="mt-2 text-sm text-muted">
|
||||
Jeg bruger <strong class="text-text">Matomo</strong> til at se hvor mange der besøger siden,
|
||||
og hvordan den bruges, så jeg kan gøre den bedre. Matomo kører på mine egne servere, dine
|
||||
data bliver <strong class="text-text">ikke delt med tredjeparter</strong> og ikke solgt.
|
||||
</p>
|
||||
<ul class="mt-3 space-y-1 text-sm text-muted">
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">_pk_id</code>: et tilfældigt id der tæller dig som samme besøgende over tid (op til 1 år).</li>
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">_pk_ses</code>: markerer dit aktuelle besøg (udløber efter ca. 30 minutter).</li>
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">WEC_VID</code>: et tilsvarende besøgs-id på mine egne tjenester.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold tracking-tight">Funktionelt</h3>
|
||||
<p class="mt-2 text-sm text-muted">
|
||||
Husker dine valg på siden. Det er ikke sporing, og det forlader aldrig din browser.
|
||||
</p>
|
||||
<ul class="mt-3 space-y-1 text-sm text-muted">
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">theme</code>: dit valgte tema (lyst/mørkt/terminal).</li>
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">wec_cookie_ack</code>: husker at du har set denne cookie-besked, så den ikke dukker op igen.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 text-sm text-muted">
|
||||
Spørgsmål? Skriv til <a href="mailto:asger@weirsoe.dk" class="text-accent underline">asger@weirsoe.dk</a>.
|
||||
</p>
|
||||
</Base>
|
||||
@@ -1,251 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { SITE } from '../consts';
|
||||
|
||||
const erhverv = [
|
||||
{
|
||||
org: 'Digisense A/S', sted: 'Danmark', roller: [
|
||||
{ titel: 'Technical Solution Architect', naar: 'maj 2026 – nu', detaljer: ['Har bygget virksomhedens ledelsessystem for informationssikkerhed (ISMS) efter ISO/IEC 27001, med procedurer for hændelseshåndtering og eskalering, evaluering efter hændelser og et hændelsesregister', 'Teknisk kontaktperson på tværs af udvikling, salg og support hos en Peppol- og NemHandel-access point-udbyder', 'Løsningsdesign til kundeprojekter'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
org: 'CGI', sted: 'Aalborg', roller: [
|
||||
{ titel: 'Teknisk Lead', naar: 'aug. 2023 – maj 2026', detaljer: ['Teknisk lead på CGI’s NemHandel-team og kundens tekniske rådgiver', 'Ledte responsen på CVE-2022-1471 (SnakeYAML, remote code execution) i NemHandel-referenceimplementeringen: kunden blev orienteret med det samme, der gik vejledning ud til dem der kørte løsningen, og teamet leverede en rettet version inden for to uger', 'Koordinerede genopretningen efter en databasefejl i produktion under CrowdStrike-nedbruddet i juli 2024, hvor de fleste af teamets maskiner var ude af drift', 'Formidler kundens behov til udviklingsteamet'] },
|
||||
{ titel: 'Senior Konsulent', naar: 'apr. 2023 – maj 2026', detaljer: ['Buddy for ny udvikler', 'Afholdt installationsvejledning via webinar'] },
|
||||
{ titel: 'Konsulent', naar: 'aug. 2022 – apr. 2023', detaljer: ['Med til at udvikle den open source eDelivery-løsning til NemHandel (Java, bygget på Oxalis)', 'Løsningsarkitekt'] },
|
||||
{ titel: 'Studentermedhjælper', naar: 'aug. 2020 – aug. 2022', detaljer: ['Kode-ansvarlig for en regelmotor (Java, Drools)', 'DevOps (Docker, docker-compose, Linux)'] },
|
||||
{ titel: 'Konsulent', naar: 'jun. 2019 – aug. 2020', detaljer: ['Frontend (Angular 8, TypeScript)', 'Backend (GoLang, Java, Python)'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
org: 'BilligVVS', sted: 'Egaa', roller: [
|
||||
{ titel: 'Fullstack IT-udvikler', naar: 'sep. 2018 – jun. 2019', detaljer: ['Hjemmeside (Python, TypeScript)', 'Backend (Python, Movex)', 'Webcrawler (Python)', 'BI (Python, chart.js)'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const uddannelse = [
|
||||
{ hvad: 'HD 1. del i Erhvervsøkonomi', hvor: 'Aalborg Universitet', naar: '2025 – 2027 (i gang)', note: 'Forretningsforståelse, erhvervsøkonomi, regnskab og finansiering.' },
|
||||
{ hvad: 'Kandidat i Datalogi (cand.scient.)', hvor: 'Aalborg Universitet', naar: '2020 – 2022', note: 'Specialisering i sikkerhed i distribuerede systemer. Speciale: Thread-Fuzz, mutation-based behavioural blackbox fuzzing of Thread.' },
|
||||
{ hvad: 'Bachelor i Datalogi (B.Sc.)', hvor: 'Aalborg Universitet', naar: '2015 – 2018', note: '' },
|
||||
{ hvad: 'Almen studentereksamen (STX)', hvor: 'Espergærde Gymnasium og HF', naar: '2012 – 2015', note: '' },
|
||||
{ hvad: 'Udvekslingsår', hvor: 'Lincoln High School, Ypsilanti, Michigan (USA)', naar: '2011 – 2012', note: '' },
|
||||
];
|
||||
|
||||
const licenser = [
|
||||
{
|
||||
hvad: 'Radioamatørlicens, kategori B',
|
||||
hvor: 'Kaldesignal OZ0AGW',
|
||||
naar: 'maj 2024',
|
||||
note: 'Bestod radioamatørprøven lørdag den 18. maj 2024.',
|
||||
},
|
||||
{
|
||||
hvad: 'Jagttegn',
|
||||
hvor: 'Bestod jagtprøven',
|
||||
naar: '2020',
|
||||
note: '',
|
||||
},
|
||||
];
|
||||
|
||||
const frivilligt = [
|
||||
{ hvad: 'LUNARK', naar: '2020', note: 'Notifikationssystem i C# og React.' },
|
||||
{ hvad: 'Kirkens Korshær', naar: '2020 – nu', note: 'Køkkenhjælper hver jul og nytår.' },
|
||||
{ hvad: 'AAU Campus', naar: '2016 – 2022', note: 'Tutor og F-Klub-frivillig.' },
|
||||
];
|
||||
|
||||
const primaere = ['Java', 'Python', 'GoLang', 'TypeScript', 'JavaScript', 'Angular', 'Docker', 'Linux', 'Proxmox', 'PostgreSQL', 'Jenkins', 'Git / GitLab', 'BI', 'Agile'];
|
||||
const sekundaere = ['C#', 'C++', 'C', 'Kubernetes', 'AWS', 'RabbitMQ', 'Datadog', 'XeLaTeX'];
|
||||
const sprog = ['Dansk (modersmål)', 'Engelsk (flydende i tale og skrift)'];
|
||||
---
|
||||
<Base title="CV" description="Asger Weirsøe, datalog og Technical Solution Architect. Erfaring, uddannelse og publikationer."
|
||||
alternates={[{ lang: 'da', path: '/cv' }, { lang: 'en', path: '/en/cv' }]}>
|
||||
<h1 class="text-4xl font-bold tracking-tight">CV</h1>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
|
||||
<span>{SITE.location}</span>
|
||||
<a class="text-accent hover:underline" href={`mailto:${SITE.email}`}>{SITE.email}</a>
|
||||
<a class="text-accent hover:underline" href="https://www.linkedin.com/in/ag-weirs%C3%B8e/">LinkedIn</a>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 max-w-2xl text-muted">
|
||||
Datalog (cand.scient.) fra Aalborg Universitet med en bred profil. Jeg forener teknisk
|
||||
indsigt med forretningsforståelse og er god til at få komplekse løsninger til at lande i
|
||||
praksis, særligt inden for elektronisk fakturering. Jeg har stået for hændelseshåndtering
|
||||
på national e-faktura-infrastruktur og bygget et ISMS efter ISO/IEC 27001. Karrieren gik fra fuld-stack udvikling
|
||||
hos BilligVVS til konsulent- og Teknisk Lead-rollen hos CGI, og i dag er jeg Technical
|
||||
Solution Architect hos Digisense A/S, hvor jeg bygger bro mellem kunder, forretning og
|
||||
udviklingsteams. Ved siden af læser jeg HD i Erhvervsøkonomi for at koble teknik og
|
||||
forretning endnu tættere.
|
||||
</p>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Fagområder</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Jeg arbejder med <strong class="text-text">elektronisk fakturering</strong> (e-faktura / electronic
|
||||
invoicing) og e-procurement. Hos CGI var jeg med til at udvikle den open source eDelivery-løsning
|
||||
til NemHandel, der er bygget på den open source-baserede access point <strong class="text-text">Oxalis</strong> (Java). I dag er jeg
|
||||
Technical Solution Architect hos Digisense A/S og arbejder med løsningsarkitektur på området:
|
||||
<strong class="text-text">Peppol</strong> (BIS, Access Point og eDelivery/AS4),
|
||||
<strong class="text-text">NemHandel</strong>, <strong class="text-text">OIOUBL/UBL</strong> og den
|
||||
europæiske e-faktura-standard EN 16931.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{['Peppol', 'Peppol Access Point', 'Oxalis', 'NemHandel', 'e-faktura', 'Electronic invoicing', 'OIOUBL / UBL', 'e-Delivery (AS4)', 'EN 16931', 'e-procurement'].map((k) => (
|
||||
<span class="wt-tag">{k}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Hændelseshåndtering og compliance</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Når noget går ned i produktion, er det tekniske kun halvdelen. Den anden halvdel er at få de
|
||||
rigtige folk i gang, holde kunden orienteret og bagefter lære noget af det. Det har jeg gjort i
|
||||
praksis som teknisk lead på NemHandel hos CGI. Under <strong class="text-text">CrowdStrike-nedbruddet
|
||||
i juli 2024</strong> ramte en databasefejl produktionen, samtidig med at de fleste af teamets maskiner
|
||||
var nede.. så vi koordinerede over telefonen med hostingleverandøren og den ene udvikler, hvis maskine
|
||||
stadig virkede, og databasen var tilbage samme eftermiddag. Da den kritiske SnakeYAML-sårbarhed
|
||||
<strong class="text-text">CVE-2022-1471</strong> ramte NemHandel-referenceimplementeringen, orienterede
|
||||
vi kunden med det samme, der gik vejledning ud til dem der kørte løsningen, og vi havde en rettet
|
||||
version ude inden for to uger.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-muted">
|
||||
Hos Digisense har jeg bygget virksomhedens ledelsessystem for informationssikkerhed efter
|
||||
<strong class="text-text">ISO/IEC 27001</strong>, med procedurer for hændelseshåndtering og eskalering,
|
||||
evaluering efter hændelser og et hændelsesregister. Det er arbejdet der skal ligge klar før det brænder,
|
||||
så ingen skal opfinde responsen undervejs. Interessen er ikke ny: min kandidat er specialiseret i
|
||||
sikkerhed i distribuerede systemer.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{['Incident response', 'Hændelseshåndtering', 'Eskalering', 'Post-incident review', 'Sårbarhedshåndtering', 'Interessentkommunikation', 'ISO/IEC 27001', 'ISMS', 'Informationssikkerhed'].map((k) => (
|
||||
<span class="wt-tag">{k}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Erhvervserfaring</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{erhverv.map((e) => (
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold">{e.org} <span class="font-normal text-muted">· {e.sted}</span></h3>
|
||||
<div class="mt-3 space-y-3">
|
||||
{e.roller.map((r) => (
|
||||
<div>
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<span class="font-medium text-text">{r.titel}</span>
|
||||
<span class="text-xs text-muted">{r.naar}</span>
|
||||
</div>
|
||||
<ul class="mt-1 list-disc pl-5 text-sm text-muted">
|
||||
{r.detaljer.map((d) => <li>{d}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Uddannelse</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{uddannelse.map((u) => (
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{u.hvad}</h3><span class="text-xs text-muted">{u.naar}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted">{u.hvor}</p>
|
||||
{u.note && <p class="mt-1 text-sm text-muted/80">{u.note}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Licenser</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{licenser.map((l) => (
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{l.hvad}</h3><span class="text-xs text-muted">{l.naar}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted">{l.hvor}</p>
|
||||
{l.note && <p class="mt-1 text-sm text-muted">{l.note}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Publikationer</h2>
|
||||
<div class="mt-4 wt-card">
|
||||
<a href="https://arxiv.org/abs/2310.00999" target="_blank" rel="noopener" class="font-medium hover:text-accent">
|
||||
CGAAL: Distributed On-The-Fly ATL Model Checker with Heuristics
|
||||
</a>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Carlsen, Frydenskov, Jensen, Rasmussen, Sørensen, <span class="text-text">Weirsøe</span>, Jensen & Larsen.
|
||||
<em>EPTCS</em>, vol. 390 (2023), s. 99–114. <span class="text-muted/80">arXiv:2310.00999</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Frivilligt arbejde</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{frivilligt.map((f) => (
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{f.hvad}</h3><span class="text-xs text-muted">{f.naar}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-muted">{f.note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Primære kompetencer</h2>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{primaere.map((k) => <span class="wt-tag">{k}</span>)}
|
||||
</div>
|
||||
<h2 class="mt-8 text-sm font-semibold uppercase tracking-widest text-accent">Sekundære</h2>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{sekundaere.map((k) => <span class="wt-tag wt-muted">{k}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Sprog</h2>
|
||||
<ul class="mt-4 space-y-2 text-sm">
|
||||
{sprog.map((s) => <li class="wt-card wt-card--compact">{s}</li>)}
|
||||
</ul>
|
||||
<h2 class="mt-8 text-sm font-semibold uppercase tracking-widest text-accent">Interesser</h2>
|
||||
<p class="mt-4 text-sm text-muted">
|
||||
Selvhosting og drift: jeg har gennem mange år bygget og vedligeholdt et homelab med
|
||||
<a class="text-accent hover:underline" href="https://www.proxmox.com/" target="_blank" rel="noopener"> Proxmox</a>,
|
||||
bag SSO via <a class="text-accent hover:underline" href="https://goauthentik.io/" target="_blank" rel="noopener">Authentik</a>.
|
||||
Det har ført til bidrag i open source-projekter som Qutebrowser og Stregsystemet (IT-studerendes betalingssystem på AAU).
|
||||
</p>
|
||||
<p class="mt-3 text-sm text-muted">
|
||||
Jeg er desuden licenseret radioamatør med <strong class="text-text">B-licens</strong> og kaldesignalet
|
||||
<strong class="text-text">OZ0AGW</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Natur og jagt</h2>
|
||||
<div class="mt-4 wt-card">
|
||||
<p class="text-muted">
|
||||
Jeg har jagttegn, men jagten er mest anledningen. Det jeg tager ud efter, er at stå op
|
||||
længe før resten af landet og træde ud i den stilhed, der kun findes tidligt om morgenen,
|
||||
og så se solen komme op og disen lette hen over markerne.
|
||||
</p>
|
||||
<p class="mt-3 text-muted">
|
||||
Planter, fugle, årstidernes skiften: jeg kan lide at kunne sætte navn på det jeg ser, og
|
||||
at lægge mærke til hvad der har ændret sig siden sidst. Ender turen uden at der bliver
|
||||
nedlagt noget, har den været lige så meget værd.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import Chat from '../../components/Chat.astro';
|
||||
---
|
||||
<Base title="Ask my CV" description="A small chat with Claude that answers questions about me, my experience and my homelab." noindex
|
||||
lang="en" alternates={[{ lang: 'da', path: '/chat' }, { lang: 'en', path: '/en/chat' }]}>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Ask my CV</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
A small chat with <strong class="text-text">Claude</strong> from Anthropic. It has read my CV and this
|
||||
site and answers questions about me.. try <em>“what incident response experience does Asger have?”</em>
|
||||
or <em>“what runs in his homelab?”</em>. It only answers questions about me.
|
||||
</p>
|
||||
|
||||
<Chat lang="en" />
|
||||
</Base>
|
||||
@@ -1,181 +0,0 @@
|
||||
---
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { SITE } from '../../consts';
|
||||
|
||||
const work = [
|
||||
{
|
||||
org: 'Digisense A/S', place: 'Denmark', roles: [
|
||||
{ title: 'Technical Solution Architect', when: 'May 2026 – present', details: ['Built the company’s information security management system around ISO/IEC 27001, including incident management and escalation procedures, post-incident reviews and an incident register', 'Technical point of contact across engineering, sales and support for a Peppol and NemHandel access point provider', 'Solution design for customer projects'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
org: 'CGI Danmark', place: 'Aalborg', roles: [
|
||||
{ title: 'Tech Lead, NemHandel', when: 'Aug 2023 – May 2026', details: ['Technical lead on CGI’s NemHandel team and the client’s technical adviser', 'Led the response to CVE-2022-1471 (SnakeYAML, remote code execution) in the NemHandel reference implementation: the client was notified immediately, mitigation guidance went out to operators, and the team shipped a fixed release within two weeks', 'Coordinated recovery from a production database failure during the July 2024 CrowdStrike outage, when most team workstations were unavailable', 'Translated client requirements for the development team'] },
|
||||
{ title: 'Senior Consultant', when: 'Apr 2023 – May 2026', details: ['Mentored a newly hired developer', 'Delivered installation webinars for the NemHandel reference implementation'] },
|
||||
{ title: 'Consultant', when: 'Aug 2022 – Apr 2023', details: ['Developed the open source NemHandel eDelivery reference implementation (Java, built on Oxalis)', 'Solution architect on the project'] },
|
||||
{ title: 'Student Developer', when: 'Aug 2020 – Aug 2022', details: ['Owned a rules engine (Java, Drools)', 'Deployment and operations (Docker, Docker Compose, Linux)'] },
|
||||
{ title: 'Consultant', when: 'Jun 2019 – Aug 2020', details: ['Frontend (Angular, TypeScript)', 'Backend (Go, Java, Python)'] },
|
||||
],
|
||||
},
|
||||
{
|
||||
org: 'BilligVVS', place: 'Egå', roles: [
|
||||
{ title: 'Full-stack Developer', when: 'Sep 2018 – Jun 2019', details: ['Webshop checkout and shipping integration (Python, TypeScript)', 'Movex ERP integration', 'Internal BI dashboards (Python, Chart.js)'] },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const education = [
|
||||
{ what: 'HD Part 1, Business Administration', where: 'Aalborg University', when: '2025 – 2027 (in progress)', note: 'Business economics, accounting and finance.' },
|
||||
{ what: 'M.Sc. in Computer Science (cand.scient.)', where: 'Aalborg University', when: '2020 – 2022', note: 'Specialisation in security in distributed systems. Thesis: Thread-Fuzz, mutation-based behavioural blackbox fuzzing of Thread.' },
|
||||
{ what: 'B.Sc. in Computer Science', where: 'Aalborg University', when: '2015 – 2018', note: '' },
|
||||
];
|
||||
|
||||
const skills = [
|
||||
{ k: 'Incident response', v: ['Incident coordination', 'Escalation', 'Stakeholder communication', 'Vulnerability response', 'Post-incident review'] },
|
||||
{ k: 'Compliance and security', v: ['ISO/IEC 27001', 'ISMS', 'Information security'] },
|
||||
{ k: 'E-invoicing', v: ['Peppol', 'eDelivery (AS4)', 'Oxalis', 'NemHandel', 'OIOUBL / UBL', 'EN 16931'] },
|
||||
{ k: 'Infrastructure', v: ['Linux', 'Docker', 'Kubernetes', 'AWS', 'Proxmox', 'Datadog'] },
|
||||
{ k: 'Development', v: ['Java', 'Python', 'Go', 'TypeScript', 'Angular', 'C#', 'PostgreSQL', 'RabbitMQ', 'Git / GitLab', 'Jenkins'] },
|
||||
];
|
||||
---
|
||||
<Base title="CV" description="Asger Weirsøe, computer scientist and Technical Solution Architect. Incident response, ISO/IEC 27001 compliance and national e-invoicing infrastructure."
|
||||
lang="en" alternates={[{ lang: 'da', path: '/cv' }, { lang: 'en', path: '/en/cv' }]}>
|
||||
<h1 class="text-4xl font-bold tracking-tight">CV</h1>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
|
||||
<span>Aalborg, Denmark</span>
|
||||
<a class="text-accent hover:underline" href={`mailto:${SITE.email}`}>{SITE.email}</a>
|
||||
<a class="text-accent hover:underline" href="https://www.linkedin.com/in/ag-weirs%C3%B8e/">LinkedIn</a>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 max-w-2xl text-muted">
|
||||
Technical Solution Architect who has worked on Danish national e-invoicing infrastructure
|
||||
since 2021. I have led responses to production outages and security vulnerabilities,
|
||||
coordinating engineers, hosting providers and the client, and I built an
|
||||
information security management system around ISO/IEC 27001 with procedures for incident
|
||||
response, escalation and post-incident review. M.Sc. in Computer Science, specialising in
|
||||
security in distributed systems, and currently studying business administration alongside work.
|
||||
</p>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Areas of expertise</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
I work with <strong class="text-text">electronic invoicing</strong> and e-procurement. At CGI I helped
|
||||
develop the open source eDelivery solution for NemHandel, built on the open source access point
|
||||
<strong class="text-text">Oxalis</strong> (Java). Today I am a Technical Solution Architect at Digisense A/S,
|
||||
working on solution architecture in the same field: <strong class="text-text">Peppol</strong> (BIS,
|
||||
Access Point and eDelivery/AS4), <strong class="text-text">NemHandel</strong>,
|
||||
<strong class="text-text">OIOUBL/UBL</strong> and the European e-invoicing standard EN 16931.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{['Peppol', 'Peppol Access Point', 'Oxalis', 'NemHandel', 'E-invoicing', 'OIOUBL / UBL', 'eDelivery (AS4)', 'EN 16931', 'E-procurement'].map((k) => (
|
||||
<span class="wt-tag">{k}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Incident response and compliance</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
When something breaks in production, the technical fix is only half of it. The other half is
|
||||
getting the right people moving, keeping the client informed and learning from it afterwards.
|
||||
During the <strong class="text-text">July 2024 CrowdStrike outage</strong> a database failure hit our
|
||||
hosted production platform just as most of the team’s workstations went down. As tech lead on
|
||||
CGI’s NemHandel team I coordinated by phone with the hosting provider and the one developer whose
|
||||
machine still worked, and the database was restored that afternoon. When the critical SnakeYAML
|
||||
vulnerability <strong class="text-text">CVE-2022-1471</strong> affected the NemHandel reference
|
||||
implementation, we notified the client immediately, mitigation guidance went out to operators,
|
||||
and we shipped a fixed release within two weeks.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-muted">
|
||||
At Digisense I built the information security management system around
|
||||
<strong class="text-text">ISO/IEC 27001</strong>: incident management and escalation procedures,
|
||||
post-incident reviews and an incident register. That is the work that has to be in place before
|
||||
anything is on fire, so nobody has to invent the response as it happens.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Experience</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{work.map((e) => (
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold">{e.org} <span class="font-normal text-muted">· {e.place}</span></h3>
|
||||
<div class="mt-3 space-y-3">
|
||||
{e.roles.map((r) => (
|
||||
<div>
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<span class="font-medium text-text">{r.title}</span>
|
||||
<span class="text-xs text-muted">{r.when}</span>
|
||||
</div>
|
||||
<ul class="mt-1 list-disc pl-5 text-sm text-muted">
|
||||
{r.details.map((d) => <li>{d}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Education</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{education.map((u) => (
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{u.what}</h3><span class="text-xs text-muted">{u.when}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted">{u.where}</p>
|
||||
{u.note && <p class="mt-1 text-sm text-muted/80">{u.note}</p>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Publication</h2>
|
||||
<div class="mt-4 wt-card">
|
||||
<a href="https://arxiv.org/abs/2310.00999" target="_blank" rel="noopener" class="font-medium hover:text-accent">
|
||||
CGAAL: Distributed On-The-Fly ATL Model Checker with Heuristics
|
||||
</a>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Carlsen, Frydenskov, Jensen, Rasmussen, Sørensen, <span class="text-text">Weirsøe</span>, Jensen & Larsen.
|
||||
<em>EPTCS</em>, vol. 390 (2023), pp. 99–114. <span class="text-muted/80">arXiv:2310.00999</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Skills</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{skills.map((s) => (
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-text">{s.k}</h3>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{s.v.map((k) => <span class="wt-tag">{k}</span>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Languages</h2>
|
||||
<ul class="mt-4 space-y-2 text-sm">
|
||||
<li class="wt-card wt-card--compact">Danish (native)</li>
|
||||
<li class="wt-card wt-card--compact">English (fluent, spoken and written)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Projects and interests</h2>
|
||||
<p class="mt-4 text-sm text-muted">
|
||||
I run a self-hosted Proxmox environment with around 50 containers and virtual machines,
|
||||
single sign-on through Authentik, central logging and monitoring. My family uses it every day.
|
||||
I have contributed to qutebrowser and Stregsystemet,
|
||||
and I am a licensed radio amateur (call sign <strong class="text-text">OZ0AGW</strong>).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -1,100 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import portrait from '../../assets/asger.png';
|
||||
|
||||
const highlights = [
|
||||
{ title: 'Incident response & compliance', body: 'Led incident and vulnerability response on Danish national e-invoicing infrastructure, and built an ISMS around ISO/IEC 27001 with escalation and post-incident review.' },
|
||||
{ title: 'E-invoicing & architecture', body: 'Technical Solution Architect at Digisense A/S, working on Peppol, NemHandel and Oxalis. Before that, tech lead on NemHandel at CGI.' },
|
||||
{ title: 'Self-hosting, AI & full-stack', body: 'A Proxmox homelab with around 50 containers and VMs behind SSO (Authentik), and projects from Java, Go and Python to TypeScript and LLM-driven workflows.' },
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
q: 'Who is Asger Weirsøe?',
|
||||
a: 'I am a computer scientist (M.Sc., Aalborg University) and Technical Solution Architect at Digisense A/S. I have worked on Danish national e-invoicing infrastructure since 2021, and I combine technical depth with business understanding.',
|
||||
},
|
||||
{
|
||||
q: 'What incident response experience does Asger Weirsøe have?',
|
||||
a: 'As tech lead on NemHandel at CGI I coordinated recovery from a production database failure during the July 2024 CrowdStrike outage, and led the response to CVE-2022-1471 (SnakeYAML) in the NemHandel reference implementation, where we shipped a fixed release within two weeks. At Digisense I built the ISMS around ISO/IEC 27001, including incident management, escalation and post-incident review.',
|
||||
},
|
||||
{
|
||||
q: 'What does Asger Weirsøe work with?',
|
||||
a: 'Solution architecture for electronic invoicing and e-procurement: Peppol (Access Point, eDelivery/AS4 and Oxalis), NemHandel, OIOUBL/UBL and EN 16931. Plus information security, self-hosting, Proxmox and AI.',
|
||||
},
|
||||
{
|
||||
q: 'Is Asger Weirsøe a radio amateur?',
|
||||
a: 'Yes. I hold a category B licence with the call sign OZ0AGW.',
|
||||
},
|
||||
];
|
||||
---
|
||||
<Base title="Asger Weirsøe" description="Asger Weirsøe, computer scientist and Technical Solution Architect. Incident response, ISO/IEC 27001 compliance and Danish national e-invoicing infrastructure."
|
||||
lang="en" alternates={[{ lang: 'da', path: '/' }, { lang: 'en', path: '/en' }]}>
|
||||
<section class="grid items-center gap-8 py-6 sm:grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<p class="font-medium text-accent">Hi, I am</p>
|
||||
<h1 class="mt-1 text-5xl font-bold tracking-tight sm:text-6xl">Asger Weirsøe</h1>
|
||||
<p class="mt-3 text-xl font-semibold text-accent sm:text-2xl">Computer scientist & Technical Solution Architect</p>
|
||||
<p class="mt-5 max-w-xl text-lg text-muted">
|
||||
M.Sc. in Computer Science from Aalborg University and Technical Solution Architect at Digisense A/S.
|
||||
I have spent my career on Danish national <strong class="text-text">e-invoicing infrastructure</strong>,
|
||||
where I have led the response when production broke and built the processes that make the next
|
||||
response better. In my spare time I run my own Proxmox homelab, tinker with AI projects and get on
|
||||
the air as a radio amateur (OZ0AGW).
|
||||
</p>
|
||||
<div class="mt-7 flex flex-wrap gap-3">
|
||||
<a href="/en/cv" class="wt-btn wt-btn--primary wt-btn--lg">See my CV</a>
|
||||
<a href="/en/chat" class="wt-btn wt-btn--lg">Ask my CV</a>
|
||||
<a href="/en/projects" class="wt-btn wt-btn--lg">Projects</a>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src={portrait}
|
||||
alt="Asger Weirsøe"
|
||||
width={260}
|
||||
densities={[1, 2]}
|
||||
class="mx-auto h-44 w-44 rounded-2xl border border-border object-cover shadow-xl sm:h-56 sm:w-56"
|
||||
loading="eager"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="mt-14 grid gap-4 sm:grid-cols-3">
|
||||
{highlights.map((h) => (
|
||||
<div class="wt-card">
|
||||
<h2 class="font-semibold tracking-tight">{h.title}</h2>
|
||||
<p class="mt-2 text-sm text-muted">{h.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">More than a CV</h2>
|
||||
<div class="mt-5 grid gap-4 sm:grid-cols-2">
|
||||
{[
|
||||
{ href: '/en/chat', title: 'Ask my CV', desc: 'A small chat with Claude that has read my CV and answers questions about me.' },
|
||||
{ href: '/status', title: 'Homelab status', desc: 'Live status for the public parts of my homelab.' },
|
||||
{ href: '/ubl', title: 'UBL check (Danish)', desc: 'Drop in an e-invoice and get its profile, syntax and EN 16931 status decoded. It all runs in the browser, the file never leaves your machine.' },
|
||||
{ href: '/en/projects', title: 'Projects', desc: 'Tools I maintain and publish on the AUR, Homebrew and Scoop.' },
|
||||
].map((c) => (
|
||||
<a href={c.href} class="group wt-card wt-card--hover">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{c.title} →</h3>
|
||||
<p class="mt-2 text-sm text-muted">{c.desc}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-muted/80">Press <kbd class="wt-kbd">Ctrl/⌘ K</kbd> to jump anywhere, or run <code class="rounded bg-surface-2 px-1.5 py-0.5">curl asger.weirsøe.dk</code> in a terminal.</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Frequently asked questions</h2>
|
||||
<div class="mt-5 grid gap-3">
|
||||
{faqs.map((f) => (
|
||||
<details class="group wt-card">
|
||||
<summary class="cursor-pointer list-none font-semibold tracking-tight marker:hidden">{f.q}</summary>
|
||||
<p class="mt-3 text-sm text-muted">{f.a}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-muted">The blog and the e-invoicing tools are in Danish.</p>
|
||||
</section>
|
||||
</Base>
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import ProjektSide from '../../../components/ProjektSide.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const alle = await getCollection('projekter', ({ data }) => !data.draft);
|
||||
const da = new Map(alle.filter((p) => p.data.lang === 'da').map((p) => [p.id, p]));
|
||||
return alle
|
||||
.filter((p) => p.data.lang === 'en')
|
||||
.map((side) => {
|
||||
const slug = side.id.replace(/^en\//, '');
|
||||
return { params: { slug }, props: { side, oversat: da.get(slug) } };
|
||||
});
|
||||
}
|
||||
const { side, oversat } = Astro.props;
|
||||
---
|
||||
<ProjektSide {side} {oversat} />
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../../layouts/Base.astro';
|
||||
import { SITE } from '../../../consts';
|
||||
import { collectionPage } from '../../../seo';
|
||||
|
||||
const sider = (await getCollection('projekter', ({ data }) => !data.draft && data.lang === 'en'))
|
||||
.sort((a, b) => a.data.title.localeCompare(b.data.title));
|
||||
const ld = collectionPage({
|
||||
url: `${SITE.url}/en/projects`, name: 'Projects',
|
||||
description: 'Tools I write and publish on the AUR, Homebrew and Scoop.',
|
||||
});
|
||||
---
|
||||
<Base title="Projects" description="Tools I write and publish on the AUR, Homebrew and Scoop." lang="en"
|
||||
alternates={[{ lang: 'da', path: '/projekter' }, { lang: 'en', path: '/en/projects' }]} jsonLd={[ld]}>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Projects</h1>
|
||||
<p class="mt-3 text-muted">Tools I maintain and publish as packages. Everything else lives on my <a class="text-accent hover:underline" href={`${SITE.giteaBase}/${SITE.giteaUser}`}>Gitea</a>.</p>
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
{sider.map((p) => (
|
||||
<a href={`/en/projects/${p.id.replace(/^en\//, '')}`}
|
||||
class="group wt-card wt-card--hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{p.data.title}</h3>
|
||||
{p.data.aur.length > 0 && <span class="text-xs text-muted">AUR</span>}
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 text-sm text-muted">{p.data.description}</p>
|
||||
<div class="mt-3 text-xs text-accent">{p.data.sprog} · Read more →</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Image } from 'astro:assets';
|
||||
import Base from '../layouts/Base.astro';
|
||||
import PostCard from '../components/PostCard.astro';
|
||||
import portrait from '../assets/asger.png';
|
||||
import { profilePage, faqPage } from '../seo';
|
||||
|
||||
const posts = (await getCollection('blog', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.slice(0, 3);
|
||||
|
||||
const highlights = [
|
||||
{ title: 'E-fakturering & arkitektur', body: 'Technical Solution Architect hos Digisense A/S. Jeg står for løsningsarkitektur inden for elektronisk fakturering, Peppol, Oxalis og NemHandel.' },
|
||||
{ title: 'Forretning møder teknik', body: 'Jeg oversætter mellem kunder, forretning og udviklingsteams, så e-faktura faktisk kommer til at virke i praksis. Læser HD i Erhvervsøkonomi ved siden af.' },
|
||||
{ title: 'Selvhosting, AI & fuld-stack', body: 'Et Proxmox-homelab med ~50 LXC/VM’er bag SSO (Authentik), og projekter fra Java/Go/Python til TypeScript og LLM-drevne workflows.' },
|
||||
];
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
q: 'Hvem er Asger Weirsøe?',
|
||||
a: 'Jeg er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Jeg har en bred profil inden for elektronisk fakturering og forener teknisk indsigt med forretningsforståelse.',
|
||||
},
|
||||
{
|
||||
q: 'Hvad arbejder Asger Weirsøe med?',
|
||||
a: 'Jeg arbejder med løsningsarkitektur inden for elektronisk fakturering og e-procurement: Peppol (Access Point, eDelivery/AS4 og Oxalis), NemHandel, OIOUBL/UBL og EN 16931. Derudover selvhosting, Proxmox og AI.',
|
||||
},
|
||||
{
|
||||
q: 'Hvem kan rådgive om Peppol og NemHandel i Danmark?',
|
||||
a: 'Det kan jeg. Jeg har arbejdet praktisk med både Peppol og NemHandel. Hos CGI var jeg med til at udvikle den open source eDelivery-løsning til NemHandel, der er bygget på Oxalis. I dag arbejder jeg som Technical Solution Architect hos Digisense A/S med løsningsarkitektur inden for elektronisk fakturering.',
|
||||
},
|
||||
{
|
||||
q: 'Er Asger Weirsøe radioamatør?',
|
||||
a: 'Ja. Jeg har B-licens og kaldesignalet OZ0AGW, og jeg bestod radioamatørprøven lørdag den 18. maj 2024.',
|
||||
},
|
||||
{
|
||||
q: 'Hvad er Asger Weirsøes baggrund?',
|
||||
a: 'Cand.scient. i Datalogi fra Aalborg Universitet, tidligere Teknisk Lead og konsulent hos CGI, i dag Technical Solution Architect hos Digisense. Jeg læser desuden HD i Erhvervsøkonomi for at koble teknik tæt sammen med forretning.',
|
||||
},
|
||||
];
|
||||
|
||||
const jsonLd = [profilePage, faqPage(faqs)];
|
||||
---
|
||||
<Base jsonLd={jsonLd} alternates={[{ lang: 'da', path: '/' }, { lang: 'en', path: '/en' }]}>
|
||||
<section class="grid items-center gap-8 py-6 sm:grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<p class="font-medium text-accent">Hej, jeg er</p>
|
||||
<h1 class="mt-1 text-5xl font-bold tracking-tight sm:text-6xl">Asger Weirsøe</h1>
|
||||
<p class="mt-3 text-xl font-semibold text-accent sm:text-2xl">Datalog & Technical Solution Architect</p>
|
||||
<p class="mt-5 max-w-xl text-lg text-muted">
|
||||
Datalog (cand.scient.) fra Aalborg Universitet og Technical Solution Architect hos Digisense A/S.
|
||||
Til daglig bygger jeg bro mellem forretning og teknik og får <strong class="text-text">elektronisk fakturering</strong>
|
||||
som Peppol og NemHandel til at virke i praksis. I min fritid drifter jeg mit eget
|
||||
Proxmox-homelab, roder med AI-projekter og går på luften som radioamatør (OZ0AGW).
|
||||
</p>
|
||||
<div class="mt-7 flex flex-wrap gap-3">
|
||||
<a href="/cv" class="wt-btn wt-btn--primary wt-btn--lg">Se mit CV</a>
|
||||
<a href="/projekter" class="wt-btn wt-btn--lg">Projekter</a>
|
||||
<a href="/blog" class="wt-btn wt-btn--lg">Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
<Image
|
||||
src={portrait}
|
||||
alt="Asger Weirsøe"
|
||||
width={260}
|
||||
densities={[1, 2]}
|
||||
class="mx-auto h-44 w-44 rounded-2xl border border-border object-cover shadow-xl sm:h-56 sm:w-56"
|
||||
loading="eager"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="mt-14 grid gap-4 sm:grid-cols-3">
|
||||
{highlights.map((h) => (
|
||||
<div class="wt-card">
|
||||
<h2 class="font-semibold tracking-tight">{h.title}</h2>
|
||||
<p class="mt-2 text-sm text-muted">{h.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Mere end et CV</h2>
|
||||
<div class="mt-5 grid gap-4 sm:grid-cols-2">
|
||||
{[
|
||||
{ href: '/nemhandel', title: 'NemHandel-opslag', desc: 'Slå et CVR-nummer op og se om virksomheden er registreret i NemHandelsregisteret, og hvilke profilpakker den understøtter.' },
|
||||
{ href: '/chat', title: 'Spørg mit CV', desc: 'En lille chat med Claude, der har læst mit CV og svarer på spørgsmål om mig.' },
|
||||
{ href: '/status', title: 'Homelab-status', desc: 'Live driftsstatus for de offentlige dele af mit homelab.' },
|
||||
{ href: '/now', title: 'Now', desc: 'Hvad jeg roder med lige nu, plus et aktivitets-heatmap fra min egen Gitea.' },
|
||||
{ href: '/profiler', title: 'Profil-opslag', desc: 'Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: id\'er, dokumenter og typisk brug.' },
|
||||
{ href: '/ubl', title: 'UBL-tjek', desc: 'Træk en faktura ind og få profil, syntaks og EN 16931-status afkodet. Alt sker i browseren, filen forlader aldrig din maskine.' },
|
||||
].map((c) => (
|
||||
<a href={c.href} class="group wt-card wt-card--hover">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{c.title} →</h3>
|
||||
<p class="mt-2 text-sm text-muted">{c.desc}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-muted/80">Tryk <kbd class="wt-kbd">Ctrl/⌘ K</kbd> og hop direkte til alt, eller <code class="rounded bg-surface-2 px-1.5 py-0.5">curl asger.weirsøe.dk</code> i en terminal.</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Ofte stillede spørgsmål</h2>
|
||||
<div class="mt-5 grid gap-3">
|
||||
{faqs.map((f) => (
|
||||
<details class="group wt-card">
|
||||
<summary class="cursor-pointer list-none font-semibold tracking-tight marker:hidden">
|
||||
{f.q}
|
||||
</summary>
|
||||
<p class="mt-3 text-sm text-muted">{f.a}</p>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{posts.length > 0 && (
|
||||
<section class="mt-16">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Seneste indlæg</h2>
|
||||
<a href="/blog" class="text-sm text-accent hover:underline">Alle indlæg →</a>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-4">
|
||||
{posts.map((p) => (
|
||||
<PostCard href={`/blog/${p.id}`} title={p.data.title} date={p.data.date} description={p.data.description} tags={p.data.tags} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</Base>
|
||||
@@ -1,115 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
---
|
||||
<Base title="NemHandel-opslag" description="Slå et CVR-nummer op og se om virksomheden er registreret i NemHandelsregisteret, og hvilke profilpakker den understøtter.">
|
||||
<h1 class="text-4xl font-bold tracking-tight">NemHandel-opslag</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Tast et CVR-nummer og se om virksomheden er registreret i NemHandelsregisteret, og hvilke
|
||||
<strong class="text-text">profilpakker</strong> den understøtter. Det er et øjebliksbillede,
|
||||
ikke en autoritativ kilde.
|
||||
</p>
|
||||
|
||||
<form id="pep-form" class="mt-8 flex gap-3">
|
||||
<input id="pep-cvr" type="text" inputmode="numeric" pattern="[0-9]{8}" maxlength="8" required
|
||||
placeholder="CVR (8 cifre)" autocomplete="off"
|
||||
class="w-44 wt-input wt-input--lg" />
|
||||
<button type="submit" class="wt-btn wt-btn--primary wt-btn--lg">Slå op</button>
|
||||
</form>
|
||||
|
||||
<div id="pep-result" class="mt-8" hidden>
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h2 id="pep-name" class="text-xl font-semibold tracking-tight"></h2>
|
||||
<span id="pep-badge" class="wt-tag"></span>
|
||||
</div>
|
||||
<p id="pep-cvr-line" class="mt-1 text-sm text-muted"></p>
|
||||
<div id="pep-caps" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="pep-error" class="mt-8 text-sm text-muted" hidden></p>
|
||||
<div id="peppol-net" class="mt-4" hidden></div>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var form = document.getElementById('pep-form');
|
||||
if (!form || form.__wired) return; form.__wired = true;
|
||||
var esc = function (s) { return String(s).replace(/[&<>"]/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
|
||||
var PROF = {
|
||||
'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0': 'Peppol BIS Billing 3.0',
|
||||
'urn:fdc:peppol.eu:poacc:trns:order:3': 'Ordering (Order)',
|
||||
'urn:fdc:peppol.eu:poacc:trns:order_response:3': 'Order Response',
|
||||
'urn:fdc:peppol.eu:poacc:trns:despatch_advice:3': 'Despatch Advice',
|
||||
'urn:fdc:peppol.eu:poacc:trns:catalogue:3': 'Catalogue',
|
||||
'urn:fdc:peppol.eu:poacc:trns:invoice_response:3': 'Invoice Response',
|
||||
'urn:fdc:peppol.eu:poacc:trns:mlr:3': 'Message Level Response',
|
||||
};
|
||||
async function loadPeppol(cvr) {
|
||||
var box = document.getElementById('peppol-net'); box.hidden = false;
|
||||
box.innerHTML = '<div class="wt-card"><p class="text-sm text-muted">Slår op i Peppol-netværket…</p></div>';
|
||||
try {
|
||||
var r = await fetch('/api/peppol/smp/' + encodeURIComponent(cvr));
|
||||
var d = await r.json();
|
||||
if (!r.ok) { box.innerHTML = '<div class="wt-card"><p class="text-sm text-muted">Peppol-opslag ikke tilgængeligt lige nu.</p></div>'; return; }
|
||||
if (!d.on_peppol) {
|
||||
box.innerHTML = '<div class="wt-card"><div class="flex items-baseline justify-between gap-2"><h2 class="text-xl font-semibold tracking-tight">Peppol-netværk</h2><span class="wt-tag wt-muted">Ikke fundet</span></div><p class="mt-2 text-sm text-muted">Virksomheden blev ikke fundet i Peppol Directory via CVR-opslag (kan stadig være på Peppol under et andet identifikator-scheme).</p></div>';
|
||||
return;
|
||||
}
|
||||
var docs = (d.doctypes || []).map(function (dt) {
|
||||
var label = PROF[dt.customization] || dt.customization;
|
||||
return '<span class="wt-tag">' + esc(dt.document) + ' · ' + esc(label) + '</span>';
|
||||
}).join('');
|
||||
box.innerHTML = '<div class="wt-card">' +
|
||||
'<div class="flex flex-wrap items-baseline justify-between gap-2"><h2 class="text-xl font-semibold tracking-tight">Peppol-netværk</h2>' +
|
||||
'<span class="wt-tag wt-tag--accent">På Peppol</span></div>' +
|
||||
'<p class="mt-1 text-sm text-muted">' + esc(d.name || '') + (d.country ? ' · ' + esc(d.country) : '') +
|
||||
' · <span class="font-mono text-xs">' + esc(d.participant_id || '') + '</span>' + (d.reg_date ? ' · reg. ' + esc(d.reg_date) : '') + '</p>' +
|
||||
'<p class="mt-4 text-xs font-semibold uppercase tracking-widest text-accent">Kan modtage</p>' +
|
||||
'<div class="mt-2 flex flex-wrap gap-2">' + (docs || '<span class="text-sm text-muted">ingen dokumenttyper</span>') + '</div>' +
|
||||
'<p class="mt-3 text-xs text-muted/80">Live fra Peppol Directory via egen proxy, intet gemt.</p></div>';
|
||||
} catch (_) {
|
||||
box.innerHTML = '<div class="wt-card"><p class="text-sm text-muted">Kunne ikke nå Peppol-opslaget.</p></div>';
|
||||
}
|
||||
}
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
var res = document.getElementById('pep-result'), err = document.getElementById('pep-error');
|
||||
res.hidden = true; err.hidden = true;
|
||||
var cvr = document.getElementById('pep-cvr').value.trim();
|
||||
var btn = form.querySelector('button'); btn.disabled = true; btn.textContent = 'Slår op…';
|
||||
loadPeppol(cvr);
|
||||
try {
|
||||
var r = await fetch('/api/nemhandel/' + encodeURIComponent(cvr));
|
||||
var data = await r.json();
|
||||
if (!r.ok) { err.textContent = data.error || 'Noget gik galt.. prøv igen.'; err.hidden = false; return; }
|
||||
document.getElementById('pep-name').textContent = data.name || 'Ukendt navn';
|
||||
document.getElementById('pep-cvr-line').textContent = 'CVR ' + data.cvr;
|
||||
var badge = document.getElementById('pep-badge');
|
||||
badge.textContent = data.registered ? 'Registreret i NemHandel' : 'Ikke registreret';
|
||||
badge.className = 'wt-tag ' + (data.registered ? 'wt-tag--accent' : 'wt-muted');
|
||||
var caps = document.getElementById('pep-caps');
|
||||
if (data.registered && (data.profiles.length || data.networks.length)) {
|
||||
caps.innerHTML =
|
||||
(data.networks.length ? '<p class="text-xs font-semibold uppercase tracking-widest text-accent">Netværk</p>' +
|
||||
'<div class="mt-2 flex flex-wrap gap-2">' + data.networks.map(function (n) {
|
||||
return '<span class="wt-tag">' + esc(n) + '</span>'; }).join('') + '</div>' : '') +
|
||||
(data.profiles.length ? '<p class="mt-4 text-xs font-semibold uppercase tracking-widest text-accent">Profilpakker</p>' +
|
||||
'<div class="mt-2 flex flex-wrap gap-2">' + data.profiles.map(function (p) {
|
||||
return '<span class="wt-tag">' + esc(p) + '</span>'; }).join('') + '</div>' : '');
|
||||
} else {
|
||||
caps.innerHTML = '<p class="text-sm text-muted">' + (data.registered
|
||||
? 'Registreret, men ingen profildata i seneste opdatering.'
|
||||
: 'Virksomheden findes i CVR, men er ikke registreret som e-faktura-modtager.') + '</p>';
|
||||
}
|
||||
res.hidden = false;
|
||||
} catch (_) {
|
||||
err.textContent = 'Kunne ikke nå opslagstjenesten.. prøv igen om lidt.'; err.hidden = false;
|
||||
} finally { btn.disabled = false; btn.textContent = 'Slå op'; }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<p class="mt-10 text-xs text-muted/80">
|
||||
API: <code class="rounded bg-surface-2 px-1.5 py-0.5">curl https://asger.weirsøe.dk/api/nemhandel/<cvr></code> (rate-limited).
|
||||
</p>
|
||||
</Base>
|
||||
@@ -1,80 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { SITE } from '../consts';
|
||||
|
||||
const focus = [
|
||||
'Elektronisk fakturering hos Digisense: Peppol, NemHandel og løsningsarkitektur',
|
||||
'HD 1. del i Erhvervsøkonomi på AAU (eksamener til sommer)',
|
||||
'Homelab: crawlkit, denne side, og alt for mange LXC-containere',
|
||||
];
|
||||
|
||||
// Aktivitets-heatmap fra Gitea ved build; ingen graf hvis Gitea er nede.
|
||||
type Beat = { timestamp: number; contributions: number };
|
||||
let perDay = new Map<string, number>();
|
||||
try {
|
||||
const r = await fetch(`${SITE.giteaBase}/api/v1/users/${SITE.giteaUser}/heatmap`);
|
||||
if (r.ok) for (const b of (await r.json()) as Beat[]) {
|
||||
const d = new Date(b.timestamp * 1000).toISOString().slice(0, 10);
|
||||
perDay.set(d, (perDay.get(d) || 0) + b.contributions);
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// 26 uger bagud, uger som kolonner (man-søn), niveau 0-4
|
||||
const WEEKS = 26;
|
||||
const today = new Date(); today.setHours(12, 0, 0, 0);
|
||||
const start = new Date(today); start.setDate(start.getDate() - (WEEKS * 7 - 1) - ((today.getDay() + 6) % 7));
|
||||
const weeks: { date: string; n: number }[][] = [];
|
||||
const cur = new Date(start);
|
||||
while (cur <= today) {
|
||||
const week: { date: string; n: number }[] = [];
|
||||
for (let i = 0; i < 7 && cur <= today; i++) {
|
||||
const key = cur.toISOString().slice(0, 10);
|
||||
week.push({ date: key, n: perDay.get(key) || 0 });
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
weeks.push(week);
|
||||
}
|
||||
const level = (n: number) => (n === 0 ? 0 : n < 3 ? 1 : n < 6 ? 2 : n < 12 ? 3 : 4);
|
||||
const total = [...perDay.values()].reduce((a, b) => a + b, 0);
|
||||
const updated = today.toLocaleDateString('da-DK', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
---
|
||||
<Base title="Now" description="Hvad jeg arbejder på lige nu, plus et aktivitets-heatmap fra min self-hostede Gitea.">
|
||||
<h1 class="text-4xl font-bold tracking-tight">Now</h1>
|
||||
<p class="mt-2 text-sm text-muted">Senest opdateret {updated} · à la <a class="text-accent hover:underline" href="https://nownownow.com/about" target="_blank" rel="noopener">nownownow.com</a></p>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Lige nu</h2>
|
||||
<ul class="mt-4 space-y-3">
|
||||
{focus.map((f) => (
|
||||
<li class="wt-card text-sm">{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{weeks.length > 0 && (
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Git-aktivitet</h2>
|
||||
<p class="mt-2 text-sm text-muted">{total} bidrag i mit eget Gitea det seneste år, de seneste {WEEKS} uger:</p>
|
||||
<div class="mt-4 overflow-x-auto wt-card">
|
||||
<div class="heatmap" role="img" aria-label="Aktivitets-heatmap over git-bidrag">
|
||||
{weeks.map((w) => (
|
||||
<div class="hm-col">
|
||||
{w.map((d) => <span class={`hm-cell hm-${level(d.n)}`} title={`${d.date}: ${d.n} bidrag`} />)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted/80">Offentlige projekter ligger på <a class="text-accent hover:underline" href={`${SITE.giteaBase}/${SITE.giteaUser}`} target="_blank" rel="noopener">gitea.weircon.dk</a>.</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.heatmap { display: flex; gap: 3px; width: max-content; }
|
||||
.hm-col { display: flex; flex-direction: column; gap: 3px; }
|
||||
.hm-cell { width: 11px; height: 11px; border-radius: 2.5px; background: var(--color-surface-2); }
|
||||
.hm-1 { background: color-mix(in srgb, var(--color-accent) 30%, var(--color-surface-2)); }
|
||||
.hm-2 { background: color-mix(in srgb, var(--color-accent) 55%, var(--color-surface-2)); }
|
||||
.hm-3 { background: color-mix(in srgb, var(--color-accent) 80%, var(--color-surface-2)); }
|
||||
.hm-4 { background: var(--color-accent); }
|
||||
</style>
|
||||
</Base>
|
||||
@@ -1,191 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { profiles, standarder, kategorier } from '../data/profiles';
|
||||
|
||||
const searchString = (p: (typeof profiles)[number]) =>
|
||||
[p.standard, p.kategori ?? '', p.navn, p.id, p.customization, p.dokumenter.join(' '), p.anvendelse, p.bemaerkninger]
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
|
||||
// Registerprofilerne af "single"-typen står ikke i G26, dem henviser vi til registret for.
|
||||
const kunIRegistret = new Set(['Procurement-OrdRes-1.0', 'Procurement-BilSimReminderOnly-1.0']);
|
||||
|
||||
const kilde = (p: (typeof profiles)[number]) => {
|
||||
if (kunIRegistret.has(p.id))
|
||||
return { label: 'Nemhandelsregistret', url: 'https://nemhandel.dk/nemhandelsregisteret' };
|
||||
if (p.standard.startsWith('Nemhandel'))
|
||||
return { label: 'OIOUBL Profiler (G26)', url: 'https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf' };
|
||||
if (p.standard.includes('PINT'))
|
||||
return p.navn.includes('(kommende)')
|
||||
? { label: 'Peppol roadmap', url: 'https://docs.peppol.eu/poac/' }
|
||||
: { label: 'docs.peppol.eu \u00b7 EU PINT Billing', url: 'https://docs.peppol.eu/poac/eu/pint-eu/' };
|
||||
if (p.navn.includes('Self-Billing'))
|
||||
return { label: 'docs.peppol.eu \u00b7 Self-Billing 3.0', url: 'https://docs.peppol.eu/poacc/self-billing/3.0/' };
|
||||
if (p.navn.includes('Billing 3.0'))
|
||||
return { label: 'docs.peppol.eu \u00b7 BIS Billing 3.0', url: 'https://docs.peppol.eu/poacc/billing/3.0/' };
|
||||
return { label: 'docs.peppol.eu \u00b7 BIS v3', url: 'https://docs.peppol.eu/poacc/upgrade-3/' };
|
||||
};
|
||||
---
|
||||
<Base
|
||||
title="Profil-opslag (Nemhandel & Peppol)"
|
||||
description="Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: profil-id'er, customization-id'er, indeholdte dokumenter og typisk anvendelse."
|
||||
>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Profil-opslag</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Browse og søg i e-dokumentprofilerne for <strong class="text-text">Nemhandel OIOUBL</strong> og
|
||||
<strong class="text-text">Peppol BIS/PINT</strong>: profil-/proces-id, customization-id, hvilke
|
||||
dokumenter profilen indeholder og typisk anvendelse. Klik på et id for at kopiere det.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-sm text-muted">
|
||||
De danske profiler står i samme rækkefølge og under samme gruppenavne som i
|
||||
<strong class="text-text">Nemhandelsregistrets</strong> profil-dropdown (“Profiler for: …”), så du kan
|
||||
slå direkte op mellem registrering og dokumentindhold.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div class="relative flex-1">
|
||||
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted">⌕</span>
|
||||
<input
|
||||
id="p-search"
|
||||
type="search"
|
||||
placeholder="Søg profil, id, dokument…"
|
||||
autocomplete="off"
|
||||
aria-label="Søg profiler"
|
||||
class="w-full wt-input wt-input--lg pl-9"
|
||||
/>
|
||||
</div>
|
||||
<span id="p-count" class="text-sm text-muted">{profiles.length} profiler</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-std="" aria-pressed="true">Alle</button>
|
||||
{standarder.map((s) => <button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-std={s} aria-pressed="false">{s}</button>)}
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<span class="mr-2 text-xs text-muted">Nemhandelsregistrets kategori:</span>
|
||||
<span class="mt-1 inline-flex flex-wrap gap-2 align-middle">
|
||||
<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-kat="" aria-pressed="true">Alle</button>
|
||||
{kategorier.map((k) => <button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-kat={k} aria-pressed="false">{k}</button>)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-3">
|
||||
{
|
||||
profiles.map((p) => { const k = kilde(p); return (
|
||||
<article
|
||||
data-profile
|
||||
data-standard={p.standard}
|
||||
data-kategori={p.kategori ?? ''}
|
||||
data-search={searchString(p)}
|
||||
class="wt-card wt-card--hover"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="wt-tag wt-tag--accent">
|
||||
{p.standard}
|
||||
</span>
|
||||
{p.kategori && (
|
||||
<span
|
||||
class="wt-tag wt-muted"
|
||||
title="Gruppen profilen står under i Nemhandelsregistrets dropdown"
|
||||
>
|
||||
Register: {p.kategori}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 class="mt-2 text-lg font-semibold tracking-tight">{p.navn}</h2>
|
||||
|
||||
<dl class="mt-3 grid gap-1 text-xs sm:grid-cols-[10rem_1fr]">
|
||||
<dt class="text-muted">Profil/proces-id</dt>
|
||||
<dd>
|
||||
<button type="button" data-copy={p.id} title="Klik for at kopiere" class="copy break-all text-left font-mono">
|
||||
{p.id}
|
||||
</button>
|
||||
</dd>
|
||||
<dt class="text-muted">Customization-id</dt>
|
||||
<dd>
|
||||
<button type="button" data-copy={p.customization} title="Klik for at kopiere" class="copy break-all text-left font-mono">
|
||||
{p.customization}
|
||||
</button>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
{p.dokumenter.map((d) => (
|
||||
<span class="wt-tag">{d}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-sm text-muted">{p.anvendelse}</p>
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
<span class="font-semibold text-muted">Bemærk:</span> {p.bemaerkninger}
|
||||
</p>
|
||||
<p class="mt-3 text-xs"><a href={k.url} target="_blank" rel="noopener" class="text-accent/80 transition-colors hover:text-accent">Kilde: {k.label} ↗</a></p>
|
||||
</article>
|
||||
); })
|
||||
}
|
||||
</div>
|
||||
|
||||
<p id="p-none" class="mt-6 text-sm text-muted" hidden>Ingen profiler matcher.</p>
|
||||
|
||||
<p class="mt-10 text-xs text-muted">
|
||||
Kilder: OIOUBL Profiler-guiden (G26) v1.3 (dokumentlister, tabel 1-3), Nemhandelsregistret
|
||||
(gruppering og profilnavne) og Peppol BIS/PINT (docs.peppol.eu). * = ApplicationResponse bruges kun
|
||||
til afvisning; det forretningsmæssige svar på en ordre er altid OrderResponse/OrderResponseSimple.
|
||||
Øjebliksbillede.. verificér altid id'er mod den officielle dokumentation før brug.
|
||||
</p>
|
||||
|
||||
<style is:inline>
|
||||
.copy { transition: color 0.15s; }
|
||||
.copy:hover { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; }
|
||||
</style>
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var cards = [].slice.call(document.querySelectorAll('[data-profile]'));
|
||||
var search = document.getElementById('p-search');
|
||||
var stdChips = [].slice.call(document.querySelectorAll('button[data-std]'));
|
||||
var katChips = [].slice.call(document.querySelectorAll('button[data-kat]'));
|
||||
var count = document.getElementById('p-count');
|
||||
var none = document.getElementById('p-none');
|
||||
var q = '', std = '', kat = '';
|
||||
function apply() {
|
||||
var shown = 0;
|
||||
cards.forEach(function (c) {
|
||||
var okS = !std || c.getAttribute('data-standard') === std;
|
||||
var okK = !kat || c.getAttribute('data-kategori') === kat;
|
||||
var okQ = !q || c.getAttribute('data-search').indexOf(q) > -1;
|
||||
var hit = okS && okK && okQ;
|
||||
c.hidden = !hit;
|
||||
if (hit) shown++;
|
||||
});
|
||||
count.textContent = shown + (shown === 1 ? ' profil' : ' profiler');
|
||||
none.hidden = shown > 0;
|
||||
}
|
||||
search.addEventListener('input', function () { q = search.value.trim().toLowerCase(); apply(); });
|
||||
function wire(group, attr, set) {
|
||||
group.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
set(b.getAttribute(attr) || '');
|
||||
group.forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); });
|
||||
apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
wire(stdChips, 'data-std', function (v) { std = v; });
|
||||
wire(katChips, 'data-kat', function (v) { kat = v; });
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest && e.target.closest('[data-copy]');
|
||||
if (!btn || !navigator.clipboard) return;
|
||||
navigator.clipboard.writeText(btn.getAttribute('data-copy')).then(function () {
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = 'kopieret ✓';
|
||||
setTimeout(function () { btn.textContent = orig; }, 1200);
|
||||
}).catch(function () {});
|
||||
});
|
||||
var qp = new URLSearchParams(location.search).get('q');
|
||||
if (qp) { search.value = qp; q = qp.trim().toLowerCase(); }
|
||||
apply();
|
||||
})();
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import ProjektSide from '../../components/ProjektSide.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const alle = await getCollection('projekter', ({ data }) => !data.draft);
|
||||
const en = new Map(alle.filter((p) => p.data.lang === 'en').map((p) => [p.id.replace(/^en\//, ''), p]));
|
||||
return alle
|
||||
.filter((p) => p.data.lang === 'da')
|
||||
.map((side) => ({ params: { slug: side.id }, props: { side, oversat: en.get(side.id) } }));
|
||||
}
|
||||
const { side, oversat } = Astro.props;
|
||||
---
|
||||
<ProjektSide {side} {oversat} />
|
||||
@@ -1,69 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import { SITE, PROJECTS } from '../../consts';
|
||||
|
||||
// Repos med en projektside her på sitet linker dertil; resten går direkte til Gitea.
|
||||
const sider = new Map(
|
||||
(await getCollection('projekter', ({ data }) => !data.draft && data.lang === 'da')).map((p) => [p.data.repo.toLowerCase(), p]),
|
||||
);
|
||||
|
||||
type Repo = { name: string; description: string | null; html_url: string; language: string | null; stars_count: number; updated_at: string; fork: boolean; archived: boolean; private: boolean; };
|
||||
let repos: Repo[] = [];
|
||||
let error = false;
|
||||
try {
|
||||
const res = await fetch(`${SITE.giteaBase}/api/v1/users/${SITE.giteaUser}/repos?limit=50&sort=updated`, {
|
||||
headers: { 'Accept': 'application/json' },
|
||||
});
|
||||
if (res.ok) {
|
||||
const hide = new Set(PROJECTS.hide.map((s) => s.toLowerCase()));
|
||||
const data = (await res.json()) as Repo[];
|
||||
repos = data
|
||||
.filter((r) => !r.fork && !r.archived && !r.private && !hide.has(r.name.toLowerCase()));
|
||||
// pinned først (i opsat rækkefølge), resten efter senest opdateret
|
||||
const order = (n: string) => {
|
||||
const i = PROJECTS.pinned.findIndex((p) => p.toLowerCase() === n.toLowerCase());
|
||||
return i === -1 ? Infinity : i;
|
||||
};
|
||||
repos.sort((a, b) => {
|
||||
const oa = order(a.name), ob = order(b.name);
|
||||
if (oa !== ob) return oa - ob;
|
||||
return +new Date(b.updated_at) - +new Date(a.updated_at);
|
||||
});
|
||||
} else { error = true; }
|
||||
} catch { error = true; }
|
||||
---
|
||||
<Base title="Projekter" description="Udvalgte offentlige projekter fra min Gitea."
|
||||
alternates={[{ lang: 'da', path: '/projekter' }, { lang: 'en', path: '/en/projects' }]}>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Projekter</h1>
|
||||
<p class="mt-3 text-muted">Hentet live fra min <a class="text-accent hover:underline" href={`${SITE.giteaBase}/${SITE.giteaUser}`}>Gitea</a>.</p>
|
||||
|
||||
{error && <p class="mt-8 wt-card text-muted">Kunne ikke hente repositories lige nu.</p>}
|
||||
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
{repos.map((r) => {
|
||||
const side = sider.get(r.name.toLowerCase());
|
||||
return side ? (
|
||||
<a href={`/projekter/${side.id}`}
|
||||
class="group wt-card wt-card--hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{side.data.title}</h3>
|
||||
{side.data.aur.length > 0 && <span class="text-xs text-muted">AUR</span>}
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 text-sm text-muted">{side.data.description}</p>
|
||||
<div class="mt-3 text-xs text-accent">{side.data.sprog} · Læs mere →</div>
|
||||
</a>
|
||||
) : (
|
||||
<a href={r.html_url} target="_blank" rel="noopener"
|
||||
class="group wt-card wt-card--hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{r.name}</h3>
|
||||
{r.stars_count > 0 && <span class="text-xs text-muted">★ {r.stars_count}</span>}
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 text-sm text-muted">{r.description || 'Ingen beskrivelse.'}</p>
|
||||
{r.language && <div class="mt-3 text-xs text-accent">{r.language}</div>}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,23 +0,0 @@
|
||||
import rss from '@astrojs/rss';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { SITE } from '../consts';
|
||||
|
||||
export async function GET(context) {
|
||||
const posts = (await getCollection('blog', ({ data }) => !data.draft))
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
return rss({
|
||||
title: SITE.title,
|
||||
description: SITE.description,
|
||||
site: context.site,
|
||||
// Samme kanoniske URL-form som canonical og sitemap: uden afsluttende
|
||||
// skråstreg. @astrojs/rss tilføjer den ellers selv (default true).
|
||||
trailingSlash: false,
|
||||
items: posts.map((p) => ({
|
||||
title: p.data.title,
|
||||
description: p.data.description,
|
||||
pubDate: p.data.date,
|
||||
link: `/blog/${p.id}`,
|
||||
})),
|
||||
customData: `<language>da-dk</language>`,
|
||||
});
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
// Build-time søgeindeks til command paletten (⌘K): faste sider + alle blogindlæg.
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function GET() {
|
||||
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
const projekter = await getCollection('projekter', ({ data }) => !data.draft);
|
||||
const items = [
|
||||
{ type: 'page', title: 'Forside', url: '/' },
|
||||
{ type: 'page', title: 'CV', url: '/cv' },
|
||||
{ type: 'page', title: 'CV (English)', url: '/en/cv' },
|
||||
{ type: 'page', title: 'Home (English)', url: '/en' },
|
||||
{ type: 'page', title: 'Ask my CV (English)', url: '/en/chat' },
|
||||
{ type: 'page', title: 'Projekter', url: '/projekter' },
|
||||
...projekter.map((p) => p.data.lang === 'en'
|
||||
? { type: 'page', title: `Project: ${p.data.title} (English)`, url: `/en/projects/${p.id.replace(/^en\//, '')}`,
|
||||
extra: [p.data.sprog, ...p.data.aur, ...p.data.tags].join(' ') }
|
||||
: { type: 'page', title: `Projekt: ${p.data.title}`, url: `/projekter/${p.id}`,
|
||||
extra: [p.data.sprog, ...p.data.aur, ...p.data.tags].join(' ') }),
|
||||
{ type: 'page', title: 'Færdigbyggede pakker (tarballz)', url: '/tarballz/' },
|
||||
{ type: 'page', title: 'Blog', url: '/blog' },
|
||||
{ type: 'page', title: 'Tags', url: '/tags' },
|
||||
{ type: 'page', title: 'NemHandel-opslag', url: '/nemhandel' },
|
||||
{ type: 'page', title: 'Profil-opslag (Nemhandel & Peppol)', url: '/profiler' },
|
||||
{ type: 'page', title: 'UBL-tjek & identifikator-afkoder', url: '/ubl' },
|
||||
{ type: 'page', title: 'Spørg mit CV (AI-chat)', url: '/chat' },
|
||||
{ type: 'page', title: 'Status (homelab)', url: '/status' },
|
||||
{ type: 'page', title: 'Now', url: '/now' },
|
||||
{ type: 'page', title: 'Uses', url: '/uses' },
|
||||
{ type: 'page', title: 'Kolofon', url: '/colophon' },
|
||||
{ type: 'page', title: 'Cookies', url: '/cookies' },
|
||||
...posts
|
||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||
.map((p) => ({
|
||||
type: 'post',
|
||||
title: p.data.title,
|
||||
url: `/blog/${p.id}`,
|
||||
extra: (p.data.tags || []).join(' '),
|
||||
})),
|
||||
];
|
||||
return new Response(JSON.stringify(items), {
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
});
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
const STATUS_URL = 'https://status.weircon.dk/status/weircon';
|
||||
---
|
||||
<Base title="Status" description="Live driftsstatus for mit homelab, målt med Uptime Kuma." noindex>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Status</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Live status for de offentlige dele af mit homelab, målt af en self-hosted
|
||||
<a class="text-accent hover:underline" href="https://uptime.kuma.pet" target="_blank" rel="noopener">Uptime Kuma</a>.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 wt-card wt-card--flush">
|
||||
<iframe src={STATUS_URL} title="Uptime Kuma statusside" loading="lazy"
|
||||
class="h-[70vh] w-full" style="border:0"></iframe>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-muted">
|
||||
Vises siden ikke? Åbn den direkte: <a class="text-accent hover:underline" href={STATUS_URL} target="_blank" rel="noopener">{STATUS_URL}</a>
|
||||
</p>
|
||||
</Base>
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
import PostCard from '../../components/PostCard.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
const tags = [...new Set(posts.flatMap((p) => p.data.tags))];
|
||||
return tags.map((tag) => ({
|
||||
params: { tag },
|
||||
props: { posts: posts.filter((p) => p.data.tags.includes(tag)).sort((a, b) => b.data.date.getTime() - a.data.date.getTime()), tag },
|
||||
}));
|
||||
}
|
||||
const { posts, tag } = Astro.props;
|
||||
---
|
||||
<Base title={`#${tag}`} description={`Indlæg om ${tag}.`} noindex>
|
||||
<a href="/tags" class="text-sm text-muted hover:text-accent">← Alle tags</a>
|
||||
<h1 class="mt-3 text-4xl font-bold tracking-tight">#{tag}</h1>
|
||||
<div class="mt-8 grid gap-4">
|
||||
{posts.map((p) => (
|
||||
<PostCard href={`/blog/${p.id}`} title={p.data.title} date={p.data.date} description={p.data.description} tags={p.data.tags} />
|
||||
))}
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import Base from '../../layouts/Base.astro';
|
||||
|
||||
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
const counts = new Map<string, number>();
|
||||
for (const p of posts) for (const t of p.data.tags) counts.set(t, (counts.get(t) ?? 0) + 1);
|
||||
const tags = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
||||
---
|
||||
<Base title="Tags" description="Alle emner på bloggen." noindex>
|
||||
<h1 class="text-4xl font-bold tracking-tight">Tags</h1>
|
||||
<div class="mt-8 flex flex-wrap gap-2">
|
||||
{tags.map(([t, n]) => (
|
||||
<a href={`/tags/${t}`} class="wt-chip">
|
||||
#{t} <span class="text-muted">({n})</span>
|
||||
</a>
|
||||
))}
|
||||
{tags.length === 0 && <p class="text-muted">Ingen tags endnu.</p>}
|
||||
</div>
|
||||
</Base>
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
import { profiles } from '../data/profiles';
|
||||
|
||||
// Minimal data til klient-matchning (id + customization + navn + standard + dokumenter).
|
||||
const idx = profiles.map((p) => ({
|
||||
standard: p.standard,
|
||||
navn: p.navn,
|
||||
id: p.id,
|
||||
customization: p.customization,
|
||||
dokumenter: p.dokumenter,
|
||||
}));
|
||||
---
|
||||
<Base
|
||||
title="UBL-tjek & identifikator-afkoder"
|
||||
description="Træk en UBL/OIOUBL-faktura ind eller indsæt en CustomizationID/ProfileID, så afkoder den profilen, syntaksen og EN 16931-status. Alt sker lokalt i browseren; filen sendes aldrig nogen steder."
|
||||
>
|
||||
<h1 class="text-4xl font-bold tracking-tight">UBL-tjek</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Træk en <strong class="text-text">UBL/OIOUBL-faktura</strong> (eller andet UBL-dokument) ind, eller indsæt en
|
||||
<strong class="text-text">CustomizationID/ProfileID</strong>, så afkoder den profilen, syntaksen og
|
||||
EN 16931-status og kobler til <a href="/profiler" class="text-accent">profil-opslaget</a>.
|
||||
Alt sker <strong class="text-text">lokalt i din browser</strong>; intet sendes nogen steder.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-muted">
|
||||
Vil du læse hele fakturaen eller have den vedhæftede PDF ud, så har jeg skrevet
|
||||
<a href="/projekter/ubl-tools" class="text-accent hover:underline">ubl-tools</a> til terminalen,
|
||||
også på Windows og macOS.
|
||||
</p>
|
||||
|
||||
<div id="drop" class="mt-8 wt-dropzone">
|
||||
<p class="text-muted">Træk en <code class="rounded bg-surface-2 px-1.5 py-0.5">.xml</code>-fil hertil, eller
|
||||
<label class="cursor-pointer text-accent hover:underline">vælg en fil<input id="file" type="file" accept=".xml,text/xml,application/xml" class="hidden" /></label>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer text-sm text-muted">…eller indsæt XML / en identifikator manuelt</summary>
|
||||
<textarea id="paste" rows="6" placeholder="Indsæt UBL-XML, eller bare en CustomizationID/ProfileID…"
|
||||
class="mt-2 w-full wt-input wt-input--lg font-mono text-xs"></textarea>
|
||||
<p class="mt-2"><button id="analyze" type="button" class="wt-btn wt-btn--primary">Afkod</button></p>
|
||||
</details>
|
||||
|
||||
<div id="result" class="mt-6" hidden></div>
|
||||
<p id="err" class="mt-6 text-sm" hidden></p>
|
||||
|
||||
<p class="mt-10 text-xs text-muted/80">
|
||||
Heuristik mod kendte profiler (samme datagrundlag som <a href="/profiler" class="text-accent">/profiler</a>) +
|
||||
EN 16931-/Peppol-/OIOUBL-mønstre. Privat: ingen upload, ingen sporing af indholdet.
|
||||
</p>
|
||||
|
||||
<script type="application/json" id="profiles-data" set:html={JSON.stringify(idx)}></script>
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var PROFILES = JSON.parse(document.getElementById('profiles-data').textContent || '[]');
|
||||
var drop = document.getElementById('drop'), fileInput = document.getElementById('file'),
|
||||
paste = document.getElementById('paste'), result = document.getElementById('result'),
|
||||
err = document.getElementById('err');
|
||||
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
|
||||
|
||||
// Electronic Address Scheme (ISO 6523): de gængse, DK/Norden i fokus.
|
||||
var EAS = {
|
||||
'0088': 'GLN (GS1 Global Location Number)',
|
||||
'0184': 'DK:CVR (dansk CVR-nummer)',
|
||||
'0198': 'DK:SE (dansk SE-nummer)',
|
||||
'0096': 'DK:P (dansk P-nummer)',
|
||||
'0192': 'NO:ORG (norsk organisasjonsnummer)',
|
||||
'0007': 'SE:ORG (svensk organisationsnummer)',
|
||||
'0208': 'BE:CBE (belgisk virksomhedsnr.)',
|
||||
'0151': 'AU:ABN', '0060': 'DUNS', '0235': 'JP', '0211': 'IT:PA',
|
||||
'9915': 'Peppol test-deltager', '9930': 'DE:LWID (Leitweg-ID)',
|
||||
};
|
||||
|
||||
function findEl(doc, name) {
|
||||
var all = doc.getElementsByTagName('*');
|
||||
for (var i = 0; i < all.length; i++) if (all[i].localName === name) return all[i];
|
||||
return null;
|
||||
}
|
||||
function txt(doc, name) { var e = findEl(doc, name); return e ? (e.textContent || '').trim() : ''; }
|
||||
|
||||
// afkod en CustomizationID/ProfileID-streng til menneskesprog
|
||||
function decode(custom, profile) {
|
||||
var notes = [], standard = null, match = null;
|
||||
var c = (custom || '').trim(), p = (profile || '').trim();
|
||||
|
||||
// 1) eksakt match mod kendte profiler
|
||||
match = PROFILES.find(function (x) { return c && x.customization === c; })
|
||||
|| PROFILES.find(function (x) { return p && x.id === p; })
|
||||
|| PROFILES.find(function (x) { return c && x.id === c; });
|
||||
|
||||
// 2) mønster-heuristik (også når der ikke er fuldt match)
|
||||
var blob = (c + ' ' + p).toLowerCase();
|
||||
if (blob.indexOf('en16931') > -1) notes.push('EN 16931-kompatibel (CIUS af den europæiske kernefaktura)');
|
||||
if (blob.indexOf('peppol.eu') > -1 || blob.indexOf('fdc:peppol') > -1) { standard = 'Peppol BIS'; notes.push('Peppol-netværket'); }
|
||||
if (blob.indexOf('pint') > -1) { standard = 'Peppol PINT'; notes.push('PINT (Peppol International)'); }
|
||||
if (blob.indexOf('billing:3.0') > -1) notes.push('Billing 3.0-proces');
|
||||
if (blob.indexOf('selfbilling') > -1) notes.push('Selvfakturering (køber udsteder)');
|
||||
if (blob.indexOf('oioubl') > -1) { standard = standard || 'OIOUBL (NemHandel)'; notes.push('OIOUBL, dansk UBL-tilpasning'); }
|
||||
if (/procurement-/i.test(p)) { standard = standard || 'OIOUBL (NemHandel)'; if (/r-|r$|simr|advr/i.test(p)) notes.push("'R' = svar påkrævet"); }
|
||||
|
||||
return { match: match, standard: match ? match.standard : standard, notes: notes };
|
||||
}
|
||||
|
||||
function badge(text, kind) {
|
||||
return '<span class="wt-tag' + (kind === 'ok' ? ' wt-tag--accent' : '') + '">' + esc(text) + '</span>';
|
||||
}
|
||||
function row(dt, dd) {
|
||||
return '<div class="grid gap-1 sm:grid-cols-[10rem_1fr]"><dt class="text-muted">' + esc(dt) +
|
||||
'</dt><dd class="break-all font-mono">' + (dd || '<span class="text-muted">—</span>') + '</dd></div>';
|
||||
}
|
||||
|
||||
function renderXml(doc) {
|
||||
var root = doc.documentElement;
|
||||
var docType = root ? root.localName : '?';
|
||||
var ublv = txt(doc, 'UBLVersionID');
|
||||
var custom = txt(doc, 'CustomizationID');
|
||||
var profile = txt(doc, 'ProfileID');
|
||||
var id = txt(doc, 'ID');
|
||||
var dec = decode(custom, profile);
|
||||
|
||||
// EAS på parter
|
||||
var endpoints = [];
|
||||
var eps = doc.getElementsByTagName('*');
|
||||
for (var i = 0; i < eps.length; i++) {
|
||||
if (eps[i].localName === 'EndpointID') {
|
||||
var scheme = eps[i].getAttribute('schemeID') || '';
|
||||
endpoints.push({ scheme: scheme, val: (eps[i].textContent || '').trim() });
|
||||
}
|
||||
}
|
||||
var epHtml = endpoints.slice(0, 4).map(function (e) {
|
||||
return '<div>' + badge((EAS[e.scheme] || ('EAS ' + e.scheme || 'ukendt')), '') +
|
||||
' <span class="font-mono text-xs">' + esc(e.val) + '</span></div>';
|
||||
}).join('');
|
||||
|
||||
var head = dec.match
|
||||
? '<div class="flex flex-wrap items-center gap-2">' + badge(dec.match.standard, 'ok') +
|
||||
'</div><h2 class="mt-2 text-xl font-semibold tracking-tight">' + esc(dec.match.navn) + '</h2>'
|
||||
: '<h2 class="text-xl font-semibold tracking-tight">' + esc(docType) +
|
||||
(dec.standard ? ' <span class="text-muted text-base font-normal">· ' + esc(dec.standard) + '</span>' : '') + '</h2>';
|
||||
|
||||
result.innerHTML =
|
||||
'<div class="wt-card">' + head +
|
||||
'<dl class="mt-3 space-y-1 text-xs">' +
|
||||
row('Dokumenttype', '<span class="font-semibold">' + esc(docType) + '</span> (UBL ' + (esc(ublv) || '2.x') + ')') +
|
||||
row('ID', esc(id)) +
|
||||
row('ProfileID', esc(profile)) +
|
||||
row('CustomizationID', esc(custom)) +
|
||||
'</dl>' +
|
||||
(dec.notes.length ? '<div class="mt-3 flex flex-wrap gap-1.5">' + dec.notes.map(function (n) { return badge(n, ''); }).join('') + '</div>' : '') +
|
||||
(epHtml ? '<p class="mt-3 text-xs text-muted">Parter (EAS):</p><div class="mt-1 space-y-1">' + epHtml + '</div>' : '') +
|
||||
(dec.match ? '<p class="mt-3 text-xs"><a class="text-accent hover:underline" href="/profiler?q=' + encodeURIComponent(dec.match.id) + '">Se profilen i /profiler ↗</a></p>'
|
||||
: '<p class="mt-3 text-xs text-muted">Ingen eksakt match i profil-databasen, afkodet ud fra mønstre.</p>') +
|
||||
'</div>';
|
||||
result.hidden = false; err.hidden = true;
|
||||
}
|
||||
|
||||
function renderId(s) {
|
||||
var dec = decode(s, s);
|
||||
result.innerHTML = '<div class="wt-card">' +
|
||||
(dec.match ? '<div class="flex flex-wrap items-center gap-2">' + badge(dec.match.standard, 'ok') +
|
||||
'</div><h2 class="mt-2 text-xl font-semibold tracking-tight">' + esc(dec.match.navn) + '</h2>'
|
||||
: '<h2 class="text-xl font-semibold tracking-tight">Identifikator</h2>') +
|
||||
'<dl class="mt-3 space-y-1 text-xs">' + row('Streng', esc(s)) + '</dl>' +
|
||||
(dec.notes.length ? '<div class="mt-3 flex flex-wrap gap-1.5">' + dec.notes.map(function (n) { return badge(n, ''); }).join('') + '</div>'
|
||||
: '<p class="mt-3 text-xs text-muted">Genkendte ingen kendte mønstre.</p>') +
|
||||
(dec.match ? '<p class="mt-3 text-xs"><a class="text-accent hover:underline" href="/profiler?q=' + encodeURIComponent(dec.match.id) + '">Se profilen i /profiler ↗</a></p>' : '') +
|
||||
'</div>';
|
||||
result.hidden = false; err.hidden = true;
|
||||
}
|
||||
|
||||
function analyze(text) {
|
||||
text = (text || '').trim();
|
||||
if (!text) return;
|
||||
if (text.indexOf('<') === 0 || text.indexOf('<?xml') === 0) {
|
||||
var doc = new DOMParser().parseFromString(text, 'application/xml');
|
||||
if (doc.getElementsByTagName('parsererror').length) {
|
||||
err.textContent = 'Kunne ikke parse XML.. er filen velformet?'; err.hidden = false; result.hidden = true; return;
|
||||
}
|
||||
renderXml(doc);
|
||||
} else {
|
||||
renderId(text);
|
||||
}
|
||||
}
|
||||
|
||||
// fil-input + drag/drop
|
||||
function readFile(f) { var r = new FileReader(); r.onload = function () { analyze(String(r.result)); }; r.readAsText(f); }
|
||||
fileInput.addEventListener('change', function () { if (fileInput.files[0]) readFile(fileInput.files[0]); });
|
||||
['dragenter', 'dragover'].forEach(function (ev) { drop.addEventListener(ev, function (e) {
|
||||
e.preventDefault(); drop.classList.add('is-over'); }); });
|
||||
['dragleave', 'drop'].forEach(function (ev) { drop.addEventListener(ev, function (e) {
|
||||
e.preventDefault(); drop.classList.remove('is-over'); }); });
|
||||
drop.addEventListener('drop', function (e) { var f = e.dataTransfer && e.dataTransfer.files[0]; if (f) readFile(f); });
|
||||
document.getElementById('analyze').addEventListener('click', function () { analyze(paste.value); });
|
||||
paste.addEventListener('keydown', function (e) { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') analyze(paste.value); });
|
||||
})();
|
||||
</script>
|
||||
</Base>
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
import Base from '../layouts/Base.astro';
|
||||
|
||||
const sections = [
|
||||
{
|
||||
title: 'Homelab',
|
||||
items: [
|
||||
{ name: 'Proxmox VE', note: 'Hypervisor på egen hardware. ~50 LXC-containere og et par KVM-VM’er, én tjeneste pr. container.' },
|
||||
{ name: 'NPMplus', note: 'Reverse proxy og TLS-terminering på kanten. Alt trafik ind går her igennem.' },
|
||||
{ name: 'Authentik', note: 'Self-hosted SSO (OIDC/LDAP). Én identitet til alle tjenester, bygget fra source.' },
|
||||
{ name: 'Gitea + Gitea Actions', note: 'Egen git-hosting og CI/CD. Denne side deployes automatisk ved push.' },
|
||||
{ name: 'MariaDB / Redis / PostgreSQL', note: 'Databaser som dedikerede containere.' },
|
||||
{ name: 'Matomo', note: 'Self-hosted analytics, first-party og uden tredjeparter.' },
|
||||
{ name: 'WireGuard', note: 'Tunneler og netns-baserede egress-profiler til alt der skal ud af huset på en pæn måde.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Udvikling',
|
||||
items: [
|
||||
{ name: 'Linux + code-server', note: 'Udvikling foregår i en dedikeret LXC med VS Code i browseren.' },
|
||||
{ name: 'Java · Go · Python · TypeScript', note: 'Java til Peppol/eDelivery-verdenen, Python/TS til alt det andet.' },
|
||||
{ name: 'Claude Code', note: 'LLM-drevne workflows, fra blogindlæg til Proxmox-administration.' },
|
||||
{ name: 'Docker', note: 'Hvor det giver mening. Ellers foretrækker jeg native installationer i LXC.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Denne side',
|
||||
items: [
|
||||
{ name: 'Astro 5 + Bun + Tailwind 4', note: 'Statisk output, ingen JS-frameworks i browseren. Se kolofonen for detaljer.' },
|
||||
],
|
||||
},
|
||||
];
|
||||
---
|
||||
<Base title="Uses" description="Hardware, software og selvhostede tjenester bag mit homelab og denne side.">
|
||||
<h1 class="text-4xl font-bold tracking-tight">Uses</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Inspireret af <a class="text-accent hover:underline" href="https://uses.tech" target="_blank" rel="noopener">uses.tech</a>:
|
||||
det jeg faktisk bruger. Kort version: alt der kan selvhostes, bliver selvhostet.
|
||||
</p>
|
||||
|
||||
{sections.map((s) => (
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">{s.title}</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{s.items.map((it) => (
|
||||
<div class="wt-card">
|
||||
<span class="font-semibold">{it.name}</span>
|
||||
<p class="mt-1 text-sm text-muted">{it.note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</Base>
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
import { SITE } from './consts';
|
||||
|
||||
export const personId = `${SITE.url}/#person`;
|
||||
export const websiteId = `${SITE.url}/#website`;
|
||||
|
||||
export const person = {
|
||||
'@type': 'Person',
|
||||
'@id': personId,
|
||||
name: 'Asger Geel Weirsøe',
|
||||
alternateName: 'Asger Weirsøe',
|
||||
url: `${SITE.url}/`,
|
||||
image: `${SITE.url}/asger.png`,
|
||||
jobTitle: ['Datalog', 'Technical Solution Architect'],
|
||||
hasOccupation: {
|
||||
'@type': 'Occupation',
|
||||
name: 'Datalog',
|
||||
occupationalCategory: '2512 Softwareudvikler',
|
||||
},
|
||||
hasCredential: [
|
||||
{
|
||||
'@type': 'EducationalOccupationalCredential',
|
||||
credentialCategory: 'degree',
|
||||
educationalLevel: "Master's degree",
|
||||
name: 'Cand.scient. i Datalogi',
|
||||
recognizedBy: { '@type': 'CollegeOrUniversity', name: 'Aalborg Universitet' },
|
||||
},
|
||||
{
|
||||
'@type': 'EducationalOccupationalCredential',
|
||||
credentialCategory: 'license',
|
||||
name: 'Radioamatørlicens, kategori B (kaldesignal OZ0AGW)',
|
||||
dateCreated: '2024-05-18',
|
||||
},
|
||||
{
|
||||
'@type': 'EducationalOccupationalCredential',
|
||||
credentialCategory: 'license',
|
||||
name: 'Jagttegn',
|
||||
dateCreated: '2020',
|
||||
},
|
||||
],
|
||||
callSign: 'OZ0AGW',
|
||||
worksFor: { '@type': 'Organization', name: 'Digisense A/S' },
|
||||
alumniOf: { '@type': 'CollegeOrUniversity', name: 'Aalborg Universitet' },
|
||||
homeLocation: { '@type': 'Place', name: 'Nørresundby, Danmark' },
|
||||
nationality: { '@type': 'Country', name: 'Danmark' },
|
||||
knowsLanguage: ['da', 'en'],
|
||||
knowsAbout: [
|
||||
'Peppol', 'Peppol Access Point', 'Peppol BIS', 'Peppol eDelivery', 'Oxalis',
|
||||
'NemHandel', 'Elektronisk fakturering', 'E-faktura', 'Electronic invoicing',
|
||||
'OIOUBL', 'UBL', 'e-Delivery (AS4)', 'EN 16931', 'E-procurement', 'EDI',
|
||||
'Softwarearkitektur', 'Løsningsarkitektur', 'Forretningsforståelse',
|
||||
'Digitalisering af fakturaflows', 'Rådgivning', 'Kravspecifikation',
|
||||
'Incident response', 'Incident management', 'Hændelseshåndtering', 'ISO/IEC 27001',
|
||||
'Informationssikkerhed', 'ISMS', 'Sårbarhedshåndtering',
|
||||
'Proxmox', 'Selvhosting', 'Kunstig intelligens', 'Linux', 'DevOps',
|
||||
'OIDC', 'SSO', 'Fuld-stack udvikling', 'Docker',
|
||||
'Amatørradio', 'Radioamatør', 'Radiokommunikation',
|
||||
],
|
||||
sameAs: [
|
||||
'https://www.linkedin.com/in/ag-weirs%C3%B8e/',
|
||||
'https://mastodon.social/@asgerweirsoee',
|
||||
'https://github.com/asger-weirsoee',
|
||||
'https://gitea.weircon.dk/agw',
|
||||
'https://arxiv.org/abs/2310.00999',
|
||||
],
|
||||
description:
|
||||
'Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Han bygger bro mellem forretning og teknik og har en bred profil inden for elektronisk fakturering og e-procurement: Peppol (Access Point, eDelivery/AS4, Oxalis), NemHandel og OIOUBL, kombineret med forretningsforståelse, selvhosting, Proxmox og AI. Han er desuden licenseret radioamatør med B-licens og kaldesignalet OZ0AGW.',
|
||||
};
|
||||
|
||||
export const website = {
|
||||
'@type': 'WebSite',
|
||||
'@id': websiteId,
|
||||
url: `${SITE.url}/`,
|
||||
name: SITE.name,
|
||||
description: SITE.description,
|
||||
inLanguage: 'da-DK',
|
||||
publisher: { '@id': personId },
|
||||
};
|
||||
|
||||
export const baseGraph = { '@context': 'https://schema.org', '@graph': [website, person] };
|
||||
|
||||
export const profilePage = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'ProfilePage',
|
||||
'@id': `${SITE.url}/#profilepage`,
|
||||
url: `${SITE.url}/`,
|
||||
name: 'Asger Weirsøe',
|
||||
mainEntity: { '@id': personId },
|
||||
about: { '@id': personId },
|
||||
inLanguage: 'da-DK',
|
||||
};
|
||||
|
||||
export function faqPage(items: { q: string; a: string }[]) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'FAQPage',
|
||||
inLanguage: 'da-DK',
|
||||
mainEntity: items.map((it) => ({
|
||||
'@type': 'Question',
|
||||
name: it.q,
|
||||
acceptedAnswer: { '@type': 'Answer', text: it.a },
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function blogPosting(opts: {
|
||||
url: string; headline: string; description: string;
|
||||
datePublished: string; dateModified: string; tags: string[]; image: string;
|
||||
wordCount?: number; section?: string;
|
||||
}) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BlogPosting',
|
||||
headline: opts.headline,
|
||||
description: opts.description,
|
||||
url: opts.url,
|
||||
mainEntityOfPage: opts.url,
|
||||
datePublished: opts.datePublished,
|
||||
dateModified: opts.dateModified,
|
||||
inLanguage: 'da-DK',
|
||||
image: opts.image,
|
||||
keywords: opts.tags.join(', '),
|
||||
articleSection: opts.section || 'Blog',
|
||||
...(opts.wordCount ? { wordCount: opts.wordCount } : {}),
|
||||
author: { '@id': personId },
|
||||
publisher: { '@id': personId },
|
||||
isPartOf: { '@id': websiteId },
|
||||
};
|
||||
}
|
||||
|
||||
export function breadcrumb(items: { name: string; url: string }[]) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: items.map((it, i) => ({
|
||||
'@type': 'ListItem', position: i + 1, name: it.name, item: it.url,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function collectionPage(opts: { url: string; name: string; description: string }) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
'@id': opts.url,
|
||||
url: opts.url,
|
||||
name: opts.name,
|
||||
description: opts.description,
|
||||
inLanguage: 'da-DK',
|
||||
isPartOf: { '@id': websiteId },
|
||||
about: { '@id': personId },
|
||||
};
|
||||
}
|
||||
|
||||
export function softwareSourceCode(opts: {
|
||||
url: string; name: string; description: string; codeRepository: string;
|
||||
language: string; license: string; platforms: string[]; sameAs: string[];
|
||||
dateCreated: string; dateModified: string; keywords: string[]; lang?: 'da' | 'en';
|
||||
}) {
|
||||
return {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'SoftwareSourceCode',
|
||||
'@id': `${opts.url}#software`,
|
||||
url: opts.url,
|
||||
name: opts.name,
|
||||
description: opts.description,
|
||||
codeRepository: opts.codeRepository,
|
||||
programmingLanguage: opts.language,
|
||||
license: opts.license,
|
||||
runtimePlatform: opts.platforms,
|
||||
sameAs: opts.sameAs,
|
||||
dateCreated: opts.dateCreated,
|
||||
dateModified: opts.dateModified,
|
||||
keywords: opts.keywords,
|
||||
inLanguage: opts.lang === 'en' ? 'en' : 'da-DK',
|
||||
author: { '@id': personId },
|
||||
maintainer: { '@id': personId },
|
||||
};
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@import "@fontsource-variable/inter";
|
||||
|
||||
/* Fælles tema (Weircon-wind) */
|
||||
@import "../../vendor/weirsoe-theme/adapters/tailwind.css";
|
||||
@import "../../vendor/weirsoe-theme/weirsoe-theme.css" layer(components);
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter Variable", ui-sans-serif, system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
html[data-theme="terminal"] {
|
||||
--font-sans: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
:root { --wt-font-sans: var(--font-sans); }
|
||||
@layer base { a { color: inherit; } }
|
||||
@@ -0,0 +1,5 @@
|
||||
Contact: mailto:asger@weirsoe.dk
|
||||
Expires: 2027-06-30T22:00:00.000Z
|
||||
Preferred-Languages: da, en
|
||||
Canonical: https://asger.weirsoe.dk/.well-known/security.txt
|
||||
Policy: https://asger.weirsoe.dk/colophon
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 847 KiB After Width: | Height: | Size: 847 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user