Fjern ubl-tools og profilopslaget fra sitet
Build & Deploy / deploy (push) Successful in 54s

Claude-Session: https://claude.ai/code/session_01GGe7kUhooMFuzY6GaDb2w9
This commit is contained in:
Asger Geel Weirsøe
2026-11-09 16:05:08 +01:00
parent dcad720d64
commit 8d6183f689
13 changed files with 3 additions and 1454 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
[{"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"},
{%- for p in pages %}{% if p.components[0] == "projekter" %}{"type":"page","title":{{ ("Projekt: " ~ p.title) | json_encode | safe }},"url":"{{ p.path | trim_end(pat="/") }}","extra":{{ [p.extra.sprog, ...p.extra.aur, ...p.extra.tags] | join(sep=" ") | json_encode | safe }}},{% elif p.components[0] == "en" %}{"type":"page","title":{{ ("Project: " ~ p.title ~ " (English)") | json_encode | safe }},"url":"{{ p.path | trim_end(pat="/") }}","extra":{{ [p.extra.sprog, ...p.extra.aur, ...p.extra.tags] | join(sep=" ") | json_encode | safe }}},{% endif %}{% endfor -%}
{"type":"page","title":"Færdigbyggede pakker (tarballz)","url":"/tarballz/"},{"type":"page","title":"Blog","url":"/blog"},{"type":"page","title":"Tags","url":"/tags"},{"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"}
{"type":"page","title":"Færdigbyggede pakker (tarballz)","url":"/tarballz/"},{"type":"page","title":"Blog","url":"/blog"},{"type":"page","title":"Tags","url":"/tags"},{"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"}
{%- for p in pages %}{% if p.components[0] == "blog" %},{"type":"post","title":{{ p.title | json_encode | safe }},"url":"{{ p.path | trim_end(pat="/") }}","extra":{{ p.taxonomies.tags | default(value=[]) | join(sep=" ") | json_encode | safe }}}{% endif %}{% endfor %}]
-156
View File
@@ -1,156 +0,0 @@
{% extends "base.html" %}
{% block main %}
{% set d = load_data(path="data/profiles.json") %}
<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">{{ d.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>
{% for s in d.standarder %}<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-std="{{ s }}" aria-pressed="false">{{ s }}</button>{% endfor %}
</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>
{% for k in d.kategorier %}<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-kat="{{ k }}" aria-pressed="false">{{ k }}</button>{% endfor %}
</span>
</div>
<div class="mt-6 space-y-3">
{%- for p in d.profiles %}
<article
data-profile
data-standard="{{ p.standard }}"
data-kategori="{{ p.kategori }}"
data-search="{{ (p.standard ~ " " ~ p.kategori ~ " " ~ p.navn ~ " " ~ p.id ~ " " ~ p.customization ~ " " ~ (p.dokumenter | join(sep=" ")) ~ " " ~ p.anvendelse ~ " " ~ p.bemaerkninger) | lower }}"
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>
{% if p.kategori %}
<span
class="wt-tag wt-muted"
title="Gruppen profilen står under i Nemhandelsregistrets dropdown"
>
Register: {{ p.kategori }}
</span>
{% endif %}
</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">
{% for doc in p.dokumenter %}
<span class="wt-tag">{{ doc }}</span>
{% endfor %}
</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="{{ p.kilde.url | safe }}" target="_blank" rel="noopener" class="text-accent/80 transition-colors hover:text-accent">Kilde: {{ p.kilde.label }} ↗</a></p>
</article>
{%- endfor %}
</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>
<script>
(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>
{% endblock %}
-184
View File
@@ -1,184 +0,0 @@
{% extends "base.html" %}
{% block main %}
{% set d = load_data(path="data/profiles.json") %}
<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" >[{% for p in d.profiles %}{"standard":{{ p.standard | json_encode | safe }},"navn":{{ p.navn | json_encode | safe }},"id":{{ p.id | json_encode | safe }},"customization":{{ p.customization | json_encode | safe }},"dokumenter":{{ p.dokumenter | json_encode | safe }}}{% if not loop.last %},{% endif %}{% endfor %}]</script>
<script>
(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 { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[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>
{% endblock %}