Skip to main content
Petanque Life
← Back to all features
18

CMS & Websites

171 features · 19 subsystems

Every tenant (FIPJP, continental, national federation), region, and club gets their own website with a unique URL and full CMS capabilities. The CMS is a core differentiator — replacing the fragmented, outdated websites that federations and clubs manage today.

Multi-Site Platform

F18.01
Planned

> **Statusrättelse 2026-08 (faktum 19).** Raderna nedan stod som `[DONE]`/`[IMPL]` med

How it works
  • F18.01.01 Shipped

    Multiple sites per tenant/club/region (1:N) — one marked primary per scope; each with a unique URL

    ✅ 449770bd.m-1 — 1:N per (tenant, org_node)-skop; primärinvarianten i ett partiellt unikt index (D-11)
  • F18.01.02 Shipped

    Platform subdomain (e.g., norrbotten-bk.petanque-life.com)

    ✅ 449770bd.m-1 (sträng + serverbyggt url) + 449770bd.m-4 — DNS-provisioneringen genom SubdomainProvisioner-porten: subdomain_dns_*-kolumnerna, svepet cms_domain_reconcile och :repair-dns (se F18.14.06)
  • F18.01.03 Shipped

    Custom domain support (e.g., www.norrlandbk.se) with auto-SSL

    ✅ 449770bd.m-4 delvis: hela domänkontraktet (registrering, CNAME/TXT-bevis, statusmaskin, resolve, cert-grind) · ⊘ själva TLS-utfärdandet är edgens och därmed go-live (se F18.14.03)
  • F18.01.04 Shipped

    Site creation wizard (pick template, set branding, go live)

    ✅ 449770bd.m-1 — wizarden skapar sajt + mallens standardsidor i EN transaktion · ⊘ branding-steget är m-3
  • F18.01.05 Shipped

    Site activation/deactivation

    ✅ 449770bd.m-1 — setup → active ⇄ inactive genom domänens enda söm (D-12)
  • F18.01.06 Shipped

    Tenant-scoped site management (federation admin manages federation site + club sites)

    ✅ 449770bd.m-1 — skopfiltrerad lista, MÄTT sammanställning och bulk-status
  • F18.01.07 Shipped

    Site directory (find federation/club websites)

    ✅ 449770bd.m-1 — anonym /v1/public/cms/directory (+ /stats) med cache/ETag
  • F18.01.08 Shipped

    Path-prefix routing — child OrgNode-CmsSites kan mountas under förälderns domän (www.svenskboule.se/<distrikt>/); 1–3 nivåer; reserverar systemslugs; canonical URL pekar på path-formen

    ✅ 449770bd.m-4 — se F18.14.08 (PUT/DELETE …/mount, härledd composed_path, 1—3 nivåer, reservlista som data) · ⊘ canonical-URL:en är m-11 (SEO)

Page Builder

F18.02
Planned

> **Statusrättelse 2026-08 (faktum 13).** Raderna nedan stod som `[DONE]` med hänvisning

How it works
  • F18.02.01 Shipped

    Drag-and-drop page builder with content blocks

    ✅ 449770bd.m-2 — block som RESURSER med servergenererat block_id (add/replace/delete/:reorder, If-Match-låsta mot sidans version); drag-and-drop-YTAN i admin är t-2
  • F18.02.02 Shipped

    Block types: text, image, gallery, video, map, embed, form, columns

    ✅ 449770bd.m-2 delvis: de TIO redaktionella typerna (text, image, gallery, hero, accordion, contact_info, video, map, embed, columns) + reusable_block_ref, typad validering med fältpekare och allowlist-sanerad embed · ⊘ form är m-9 (formulärmodellen)
  • F18.02.03 Planned

    Dynamic data blocks: ranking table, results, calendar, club directory, venue map

    ⊘ m-6 — typerna avvisas i m-2 med 422 block_type_not_available och ägarmilstolpen i klartext, aldrig en tyst accept
  • F18.02.04 Shipped

    Page templates library (homepage, about, contact, competition, news)

    ✅ 449770bd.m-2 — de NIO sidmallarna på sin EGEN väg (GET /v1/cms/page-templates) + :apply-template (replace/merge) med ärligt skipped_blocks för obyggda typer
  • F18.02.05 Shipped

    Custom page creation with SEO-friendly URLs

    ✅ 449770bd.m-2 — POST /v1/cms/sites/{id}/pages med Slugify (å→a/ö→o) + dedup · ⊘ canonical/meta/no_index är m-11 (SEO)
  • F18.02.06 Shipped

    Page versioning and draft/publish workflow

    ✅ 449770bd.m-2 — statusmaskinen med kantgatade mandat (cms:approve/cms:publish:immediate), versioner med monotont version_no, tak 50, auto-snapshot vid publicering och före revert (revert ⇒ utkast)
  • F18.02.07 Shipped

    Page scheduling (publish at future date)

    ✅ 449770bd.m-2 — :schedule/:unschedule + F1-7-jobbet cms_page_schedule_tick (enda vägen scheduled → published; förfallen unpublish_at ⇒ arkiverad)
  • F18.02.08 Shipped

    Reusable block components (header, footer, sidebar)

    ✅ 449770bd.m-2 — en rad per sajt, refererad via reusable_block_ref och löst vid LÄSNING (aldrig kopierad), MÄTT used_on_pages, mjuk radering som vägrar med 409 reusable_block_in_use

Theme & Branding Engine

F18.03
Planned

> **Statusrättelse 2026-08.** Raderna nedan stod som `[DONE]` med hänvisning till den

How it works
  • F18.03.01 Shipped

    Per-site theming (colors, fonts, logo)

    ✅ 449770bd.m-3 delvis: PUT /v1/cms/sites/{id}/theme — sajtens EGNA färger, den kurerade typografin (GET /v1/cms/font-catalog) och hörnradien, med kontrastdomen (4,5:1 fel inom överlägget, 3:1 varning mot basen) · ⊘ logo_url ägs av F1-9; logo-position/-höjd, favicon och bytes/varianter är ⊘ D-MEDIA-INFRA + m-5 (renderarens chrome-detalj)
  • F18.03.02 Shipped

    Pre-built theme templates (modern, classic, sport, minimal)

    ✅ 449770bd.m-3 — EXAKT fyra kodägda mallar på GET /v1/cms/theme-templates (+ /{id}) och POST /v1/cms/sites/{id}:apply-theme; classic = plattformens baslinje Fraunces/Inter. Mallen går genom SAMMA validering som en redaktörs egen sparning och rör aldrig custom_css, arvet, menyn eller basens token
  • F18.03.03 Shipped

    Header/footer layout customization

    ✅ 449770bd.m-3 — PUT …/header-layout (sex presets + sticky/sök/språkväljare) och PUT …/footer-layout (sex presets, länkgrupper med sajtlokala page-mål, kontaktinfo, sociala länkar ur allowlist) · ⊘ nyhetsbrevsflaggan (show_newsletter_signup avvisas — anmälningsformuläret är m-9), höjd/färgöverskrivningar (renderarens tokens, m-5)
  • F18.03.04 Shipped

    Custom CSS injection (for advanced users)

    ✅ 449770bd.m-3 — PUT/GET …/custom-css med 50 kB-tak, hela den förbjudna listan prövad EFTER normalisering (avvisar, strippar aldrig tyst), url(-allowlist, !important som varning och dry_run (validera utan sparning). Audit-diffen bär storlek + sha256, aldrig texten · ⊘ selektor-skopningen vid rendering är m-5
  • F18.03.05 Shipped

    Responsive design (mobile-first)

    ✅ 449770bd.m-3 — PUT/GET …/responsive (breddläge, bredd-preset, max_content_width_px 320—1920, mobil header, mobil fontskalning). Brytpunkterna lagras aldrig — de är husets design-tokens (F1-10), inte en per-sajt-inställning
  • F18.03.06 Shipped

    Dark mode support

    ✅ 449770bd.m-3 — PUT/GET …/dark-mode med off|light|dark|auto och de FJORTON pinnade färgfälten (mörklägesvarianter CMS:et äger; F1-9 har ingen dark-token)
  • F18.03.07 Shipped

    Federation brand inheritance (club sites can inherit national branding)

    ✅ 449770bd.m-3 — PUT/GET …/brand-inheritance per aspekt, ETT steg djupt; logo/namn/strategi/custom-CSS alltid barnets; self och direkt A↔B ⇒ 422 circular_inheritance; okänd/annan tenants/raderad förälder ⇒ SAMMA 422 (ingen existensläcka); raderad förälder vid läsning ⇒ 200 + inheritance_broken
  • F18.03.08 Shipped

    Default-tema: bottle-green + ivory, Fraunces display + Inter body

    ✅ 449770bd.m-3 delvis: classic-mallen ÄR baslinjen (Fraunces/Inter på husets design-tokens) och basens färger kommer ur F1-9:s tenant_theme · ⊘ själva tokenpaketet ägs av F1-10 (packages/shared/src/design/tokens/)
  • F18.03.09 Planned

    Per-portal designprofil (tenant-overridable theme, BrandingConfig)

    ⊘ F1-9/D-ORGS — tenantens kanoniska token bor i tenant_theme/org_node_theme med sina egna endpoints; D-CMS lägger bara sitt sajt-överlägg ovanpå och skriver aldrig basen

Blog & News

F18.04
Planned

> **Statusrättelse 2026-08.** Raderna nedan stod som `[DONE]` mot den **raderade**

How it works
  • F18.04.01 Shipped

    Article creation with rich text editor — PL-F1804a

    ✅ 449770bd.m-7 — POST/GET /v1/cms/sites/{id}/blog + GET/PATCH/DELETE /v1/cms/blog/{id}, auto-slug ur m-1:s translitterering. Bokförd avvikelse: kroppen är m-2:s ordnade blockarray, aldrig fri rich-text-HTML — huset tar aldrig emot markup det sedan måste sanera; HTML GENERERAS i stället på servern för RSS full och syndikeringens content_html
  • F18.04.02 Shipped

    Categories and tags — PL-F1804a

    ✅ 449770bd.m-7 — de ÅTTA kategorierna som DATA i koden (internal/cms/blog_categories.go) med i18n-etiketter per lanseringsspråk, läsbara på GET /v1/cms/blog-categories; fria taggar i text[]; okänd kategori ⇒ 422 även i filterläge
  • F18.04.03 Shipped

    Featured image and excerpt — PL-F1804a

    ✅ 449770bd.m-7 — featured_image_ref är en REFERENS mot husets publika mediagate (aldrig en fri URL) med i18n alt-text, lede som excerpt; :feature/:unfeature + :pin/:unpin, och featured HÄRLEDS vid läsning ur featured_until (ingen jobbskrivare rör raden)
  • F18.04.04 Shipped

    Article scheduling (publish at future date) — PL-F1804a

    ✅ 449770bd.m-7 — statusmaskinen draft → in_review → scheduled → published → archived med kantmandat (inga nya capabilities), :schedule/:unschedule som planens enda vägar och F1-7-svepet cms_blog_schedule_tick som kanten scheduled → published:s ENDA skrivare (rutten vägrar den med 409)
  • F18.04.05 Shipped

    Multi-author support — PL-F1804b

    ✅ 449770bd.m-7 — authors jsonb med de FEM rollerna och …/authors-familjen (dubblett ⇒ 409, primär kan inte tas bort ⇒ 422); bylinen är ett VISNINGSNAMN — aldrig e-post, aldrig ett identitets-id i ett publikt svar
  • F18.04.06 Shipped

    News feed aggregation (pull news from parent federation) — PL-F1804b

    ✅ 449770bd.m-7 — interna prenumerationer där servern resolver OrgNoden till dess primärsajt (själv/annan tenant ⇒ 422, dubblett ⇒ 409, include_in_public styr publik synlighet) OCH externa källor genom porten NewsFeedFetcher med SSRF-vakt + svepet cms_news_external_fetch; det publika flödet är EN paginerad union med källmärkning egen|intern|extern och obligatorisk attribution. Ingen kopiering: aggregated_from_post_id är reserverad och oskriven
  • F18.04.07 Shipped

    RSS feed generation — PL-F1804b

    ✅ 449770bd.m-7 — RSS 2.0 byggt med encoding/xml, dc:creator per författare, category + enclosure genom mediagaten, serverbyggda länkar/feed_urls, content_mode lede|full, stark ETag + 304, ?language=-filter och rss_enabled=false ⇒ 404. ✅ web-exponeringen är 449770bd.m-7.t-2: rss.xml, /{lang}/rss.xml och serverns egen /blog/rss på sajtens EGEN värd STRÖMMAR API:ts feed (web bygger aldrig egen XML — grindat), och upptäcktslänken <link rel="alternate" type="application/rss+xml"> publiceras BARA när feeden faktiskt svarar 200 (huset har ingen publik läsning av rss_enabled, och en länk mot en avstängd feed hade gett läsaren ett trasigt flöde)
  • F18.04.08 Shipped

    Comment system (moderated) — PL-F1804b

    ✅ 449770bd.m-7 — publik POST ⇒ 202 {status:"pending"} utan id, trådning mot GODKÄND förälder, moderering (approve|reject) bakom cms:edit med stämplad moderated_by/at, auto-flagg vid tröskeln 3, honeypot-fältet website (byte-identiskt svar, noll rader) och den domänpersisterade missbruksvakten 30/timme/källa/sajt ⇒ 429 + Retry-After (oberoende av den globala rate-limit-flaggan)
  • F18.04.09 Shipped

    Publik nyhetsyta på sajtens egen värd — flöde, artikeldetalj och kommentars-UI

    ✅ 449770bd.m-7.t-2 — news_feed-blocket SSR-först utan JS (poster i FÖRSTA HTML-svaret, källbadges egen|intern|extern, ETT featured-kort i hero-läge, attributionsnot, tre riktiga tillstånd), kategori- och källfilter som LÄNKAR med serverns egna query-parametrar (ingen klientfiltrering), artikeldetaljen på serverns EGEN väg /blog/{slug} med m-5/m-6:s blockrenderare återanvänd och byte-identisk neutral 404 för utkast/granskning/arkiv, samt kommentarsformuläret (namn + text + frivillig e-post) genom sajtens EGET ursprung. Ingen optimistisk rendering: inskicket ger pending-beskedet, kommentaren visas först när den godkänts. Vy-spec: specs/web/views/nyheter.md
  • F18.04.10 Shipped

    Redaktionens adminflikar (Artiklar · Skriv/redigera · Prenumerationer · Flöde & RSS) + modereringskön

    ✅ 449770bd.m-7.t-2 — de fyra flikarna i SAMMA sida /nyheter som D-COMM:s synd/outlets, med mandatet per FLIK (cms:view respektive syndication:manage, båda riktningarna mekaniskt bevisade). Artikellistan med FYRA SERVERfilter, editorn som återanvänder sidbyggarens blockredigerare (ingen andra blockeditor), prenumerationsflikens tre paneler (auto_republish, include_in_public, :republish), flödesflikens källtabell + RSS-panel med serverbyggda feed_urls under ETag-lås, och modereringskön (pending/flagged först, godkänn/avvisa med motivering, trådkontext, artikelreferens). ⊘ fliken Social media (some) är F09.05/F09.08 och renderas INTE ALLS. Vy-spec: specs/admin/views/nyheter.md
  • F18.04.11 Shipped

    Feed-upptäckt och RSS på tenantens egen värd

    ✅ 449770bd.m-7.t-2 — middlewarens rewrite av rss.xml / /{lang}/rss.xml / /blog/rss till strömnings-sömmen /sajt-rss; sajten är VÄRDENS, inte frågans (sajt-id ur Host, språk ur VÄGEN), serverns status och cachehuvuden bärs vidare oförändrade och rss_enabled = false ⇒ 404. Bevisat med curl -H "Host: …" i tools/gate/dcms-m7-e2e.mjs (m-5:s bevismekanism)

Navigation & Structure

F18.05
Planned

> **Statusrättelse 2026-08.** Raderna nedan stod som `[DONE]` mot den **raderade**

How it works
  • F18.05.01 Shipped

    Menu builder (custom navigation)

    ✅ 449770bd.m-3 — PUT …/navigation + POST/DELETE …/navigation/items med SERVERGENERERADE, stabila item_id, max tre nivåer (422 menu_too_deep), sajtlokala page-mål (422 unknown_menu_target, samma svar för påhittat och främmande id), https-krav på externa mål, broken-dekoration vid läsning i admin och fail-closed-filter i den publika projektionen
  • F18.05.02 Shipped

    Auto-generated pages (rankings, calendar, results — pulled from API)

    ✅ 449770bd.m-6 — POST /v1/cms/sites/{id}:auto-pages skapar utkast med auto_page_type + sitt enda data-block; svaret {created, existing, skipped} och DDL:ens unique (site_id, auto_page_type) gör anropet konvergent. Sidan har ingen egen livscykel — den publiceras genom det vanliga workflowet. En rankings-sida utan publik D-RANK-preset skapas inte utan bokförs skipped: no_public_preset · ⊘ /pages/{id}/refresh byggs inte (blocket läser live, det finns inget kopierat att uppdatera)
  • F18.05.03 Shipped

    Breadcrumb navigation

    ✅ 449770bd.m-2 — parent_page_id-kedjan med cykeldetektering, publik + autentiserad rutt (m-3 lägger bara flaggan show_breadcrumbs i menydokumentet)
  • F18.05.04 Planned

    Search within site

    ⊘ m-5 (publik rendering + sajt-sök)
  • F18.05.05 Shipped

    Sitemap generation (XML for search engines)

    ✅ 449770bd.m-11 — GET /v1/public/cms/sites/{id}/sitemap.xml svarar alltid ett <sitemapindex> (D-12) mot …/sitemap-{n}.xml (5 000 poster/sida). Sitemapen är VÄRDENS (D-13): rot-sajten listar även varje monterad barnsajts sidor i värdens URL-form; barnens egna /{mount}/sitemap.xml finns inte (404). Posten bär <lastmod>/<changefreq>/<priority> + xhtml:link hreflang ur m-10:s alternates-mängd. En opublicerad sida, en no_index-sida och en sajt utan publik värd står ALDRIG där; sitemap_enabled=false ⇒ 404 byte-identiskt med en okänd sajt
  • F18.05.06 Shipped

    Footer customization (links, contact info, social media)

    ✅ 449770bd.m-3 — PUT …/footer-layout (sex presets, länkgrupper, kontaktinfo, sociala länkar ur allowlist) + den publika GET /v1/public/cms/sites/{id}/footer med arvet applicerat · ⊘ nyhetsbrevs-anmälan och övriga formulär i sidfoten är m-9

Media Library

F18.06
Planned

> **Rättad mot rebuilden (`449770bd.m-8`, D-CMS m-8).** Statusarna nedan beskrev den

How it works
  • F18.06.01 Shipped

    Image upload and management per site

    PL-F1806a | ✅ byggd — cms_media + grant→PUT→commit genom media.BlobStore; mappar, taggar, filter, sök
  • F18.06.02 Shipped

    Automatic image optimization (resize, compress, WebP)

    PL-F1806a | ✅ byggd, ny trappa — husets ENDA varianttrappa {avif,webp}-{150,320,800,1600} via svepet cms_media_variant_generate (ALDRIG uppskalning). Den gamla JPEG-tiern (thumbnail/small/medium/large) finns inte
  • F18.06.03 Shipped

    Document upload (PDF, Word)

    PL-F1806a | ✅ byggd — MIME-allowlist + magic-bytes-prövning (PDF/DOC/DOCX/ODT). page_count/word_count mäts INTE (huset läser inga dokument)
  • F18.06.04 Shipped

    Video embedding (YouTube, Vimeo, direct upload)

    PL-F1806a | ✅ byggd — server-detekterad provider, youtube-nocookie-embed HÄRLEDD, direktvideo mot kvoten. Ingen tumnagel för direktvideo (⊘ D-MEDIA-INFRA/sidecar: huset har ingen video-transcoding)
  • F18.06.05 Shipped

    Photo gallery creation

    PL-F1806b | ⊘ ej byggd, ingen ny entitet — sajtens galleri är m-2:s gallery-BLOCK (renderat i m-5 som grid, ingen karusell); fotogallerier med samtycke ägs av D-COMM (photo_gallery)
  • F18.06.06 Shipped

    CDN delivery for all media

    PL-F1806b | ⊘ go-live-infra — den publika serveringen bär stark ETag + Cache-Control: public, max-age + nosniff; CDN-rewrite/immutable-host är driftbeslut, inte applikationskod
  • F18.06.07 Shipped

    Storage quota per site

    PL-F1806b | ✅ byggd — MÄTT storage_used_bytes (radlåst inkrement i commit-transaktionen), coalesce(cms_site.storage_quota_bytes, PL_CMS_MEDIA_QUOTA_BYTES), 413 cms_media_quota_exceeded. Kvot-VÄLJAREN och varningsnotisen är Ö-4 (ägarlösa: en redaktör ska inte höja sitt eget tak)
  • F18.06.08 Shipped

    Public + lösenordsskyddad media via blob storage

    PL-T234 (Azure) | ✅ byggd, ANNAN infrastruktur — Azure Storage/SAS är AVVECKLAT; bytesvägen är porten media.BlobStore (local default / s3 → Clever Cellar). Åtkomstnivåerna public|members|groups HÄRLEDS per anrop (ingen lagrad dom)

Forms & Interaction

F18.07
Planned

> **Rättad mot rebuilden (`449770bd.m-9`, D-CMS m-9).** Statusarna nedan beskrev den

How it works
  • F18.07.01 Shipped

    Form builder (contact, feedback, registration interest)

    PL-F1807 | ✅ byggd — cms_form med de sex typerna och de tolv fälttyperna, validering med FÄLTPEKARE (taket 50 fält), serverfödda id/slug/status/version och statusmaskinen draft→active→closed→archived. Katalogen är KODÄGD (GET /v1/cms/form-catalog) · ⊘ Ö-1 fälttypen *Filuppladdning* (anonym uppladdning kräver kvot-/MIME-/malware-postur — ägarbeslut)
  • F18.07.02 Shipped

    Form submissions management (view, export, notify)

    PL-F1807 | ✅ byggd — serverfiltrerad inkorg per formulär OCH per sajt, detalj med etiketter/signaler (submitter_ip bara för cms:edit), triage-maskinen, HÅRD radering (frigör RSVP-plats) och strömmad CSV med neutraliserad formelinjektion, utan honeypot/ip · ⊘ Ö-4 utgående svar/tråd från konsolen (D-SYS supportworkbench äger ärendetrådar; panelen ger mailto:)
  • F18.07.03 Shipped

    Email notification on form submission

    PL-F1807 | ✅ byggd som JOBB — cms_form_notification_dispatch (*/5 * * * *) genom husets ENDA e-postutgång och den DELADE spärrlistan; utfallet MÄTS per mottagare i cms_form_notification_delivery, vars unique (submission_id, recipient) ÄR single-fire-vakten. Spam mejlas aldrig, och aldrig retroaktivt. Ämnesmallens variabelmängd är sluten (okänd variabel ⇒ 422 redan i konfigurationen)
  • F18.07.04 Shipped

    Anti-spam protection (captcha)

    PL-F1807 | ✅ byggd, annan tröskel och andra utfall — honeypot 0.9 · time-check 0.5 (SERVERNS klocka via signerat form_token, aldrig en klientstämpel) · captcha 0.3 bakom porten CaptchaVerifier vald av MILJÖN. Poängen klampas och jämförs med formulärets auto_spam_threshold. Ett leverantörsfel ⇒ unavailable/vikt 0 och en accepterad rad; en signal utan konfiguration ⇒ skipped med orsak — aldrig ett ok ingen prövade
  • F18.07.05 Shipped

    Event RSVP / sign-up forms

    PL-F1807 | ✅ byggd — kapaciteten MÄTT inuti transaktionen efter select … for update, väntelista i anmälningsordning med uppflyttning i SAMMA transaktion som platsen frigörs, 409 rsvp_full/409 rsvp_closed (klockan i FRÅGAN). Publik summering med EXAKT {places_left, waitlist_open, closed} — taket, kön och deltagarna är aldrig publika

SEO & Analytics

F18.08
Planned

> **Rebuild-status (`449770bd.m-11`).** Hela familjen är byggd mot Go-API:t. `[DONE]`-taggen

How it works
  • F18.08.01 Shipped

    Per-page meta titles and descriptions — PL-F1808

    ✅ PUT/GET /v1/cms/pages/{id}/seo + /v1/cms/blog/{id}/seo. Titeln och beskrivningen härleds när de inte är satta: titeln ur title_format/title_separator (hemsidan får bara sajtnamnet), beskrivningen mätt ur första text-blocket med m-10:s språkregel. Ingen text ⇒ ingen description-tagg
  • F18.08.02 Shipped

    Open Graph images for social sharing — PL-F1808

    ✅ GET …/pages/{slug}/open-graph. og:image löses sidans bild → första hero/image-blocket → sajtens default → logotypen → ingen; kandidaten måste vara en publik, icke-arkiverad mediepost (m-8:s gate) — en delningsbild bakom en åtkomstgrind är en trasig delning. og:locale härleds ur språkregistrets CountryCode (D-11)
  • F18.08.03 Shipped

    Structured data (JSON-LD: SportOrganization, Event, etc.) — PL-F1808

    ✅ GET …/pages/{slug}/structured-data. Redaktören väljer typ, huset äger fakta (D-6): logo ur upplöst varumärke, sameAs ur sidfoten, memberOf ur org-trädet. sport: "Pétanque" är en konstant (D-7). Event-noder bara när källan faktiskt gav rader · ⊘ Ö-9 BreadcrumbList
  • F18.08.04 Shipped

    Analytics integration (privacy-respecting, e.g., Plausible/Umami) — PL-F1808

    ✅ GET/PUT …/analytics-config + GET …/analytics-script. Skriptet byggs av servern ur en SLUTEN katalog (D-8): ingen fri script_url, ingen inline-JS, självhostning väljs av miljön. Bara cookie-fria leverantörer ⇒ ingen banner · ⊘ Ö-2 ingest av leverantörens statistik-API
  • F18.08.05 Shipped

    Page performance monitoring — PL-F1808

    ✅ POST /v1/public/cms/sites/{id}/performance (RUM-beacon, 202) + GET /v1/cms/sites/{id}/performance (p75 + klassning mot deklarerade trösklar). Under 20 mätpunkter ⇒ insufficient_data, aldrig ett p75 som ser exakt ut · ⊘ Ö-8 Lighthouse-korten (C-5/D-OPS)
  • F18.08.06 Shipped

    Search engine indexing control (robots.txt, canonical URLs) — PL-F1808

    ✅ GET …/robots.txt + …/canonical. Canonical pekar på sajtens kanoniska värd även från den andra (D-4); utan publik värd finns ingen canonical (D-3). Robots är ett värdprotokoll (D-13) · ⊘ Ö-3 Search Console/IndexNow

Multi-Language Sites

F18.09
Planned

> **Rättad mot rebuilden (`449770bd.m-10`, D-CMS m-10).** Statusarna nedan beskrev den

How it works
  • F18.09.01 Shipped

    Multi-language page content (parallel versions per language)

    PL-F1809 | ✅ byggd, annan modell — innehållet är {lang→text} i sidans title och i varje block­fält blockkatalogen deklarerar som i18nField (aldrig parallella sidrader). cms_page.translations fick sin FORM i 00216: en post per språk med status, translator_ref, reviewed_by/reviewed_at, source_version, source_fingerprint, notes. Båda identitetsreferenserna står i båda identitetsmanifesten (erasure: actor · merge: actor-repoint)
  • F18.09.02 Shipped

    Language switcher in navigation

    PL-F1809 | ✅ byggd, FLYTTAD nyckel — legacys boolean header_layout.show_language_switcher är ersatt av language_config.switcher_placement med FYRA lägen (header|footer|header_footer|hidden). Headern avvisar den gamla nyckeln med ett pekande 422 (D-3), migrationens datasteg bevarade varje sajts placering. Växlarens data kommer ur den publika projektionen GET /v1/public/cms/sites/{id}/languages — renderarens ENDA språkkälla
  • F18.09.03 Shipped

    Default language per site

    PL-F1809 | ✅ byggd med EN skrivare — PUT /v1/cms/sites/{id}/languages är enda vägen till default_language, supported_languages och language_config; sajt-PATCH:ens språkfält ⇒ 422 som pekar hit (D-2). Varje tagg kommer ur plattformens EGET register (GET /v1/cms/language-catalog, cms:view, stark ETag) — D-CMS listar inga språk själv. Att ta bort ett språk raderar ingenting: innehållet ligger kvar och svaret bokför content_languages_dropped[]
  • F18.09.04 Shipped

    Translation workflow (flag pages needing translation)

    PL-F1809 | ✅ byggd, utan flaggan — fyra lagrade ord (not_started|in_progress|needs_review|approved) med sluten kantlista och mandat per kant (cms:edit · cms:approve · cms:publish:immediate), If-Match på SIDANS ETag och EN audit-rad per kant. Föråldrat är HÄRLETT ur källavtrycket, aldrig lagrat — legacys flag-outdated och DELETE …/translations/{lang} är konsoliderade bort (Ö-2). Täckningen är MÄTT av EN funktion som matar matrisen, …/translations/needed och sidlistans languages_with_content
  • F18.09.05 Shipped

    hreflang tags for SEO

    PL-F1809 | ✅ byggd som EN härledning — GET /v1/public/cms/sites/{id}/pages/{slug}/hreflang ger en post per språk sidan faktiskt är TILLGÄNGLIG i (policyn respekterad) plus x-default. URL-formen är default-språket på ren väg och övriga under /{lang}, absolut över sajtens publika värd inklusive monteringen. Saknar sajten publik värd är mängden tom och base_url null — en påhittad adress vore fabricerad data i ett SEO-huvud. m-11:s seo-head/canonical/sitemap KONSUMERAR mängden

Role-Based Editing

F18.10
Planned

> **Statusrättelse 2026-08 (faktum 19).** Beskrivningen nedan gäller den **raderade**

How it works
  • F18.10.01 Shipped

    Federation editor role (manages federation site)

    ✅ 449770bd.m-1 — rollmallen federation_content_editor (tenant-skop; når hela förbundet via husets ScopeTree)
  • F18.10.02 Shipped

    Club editor role (manages club site only)

    ✅ 449770bd.m-1 — club_content_editor på klubbens org_node; annan klubbs sajt ⇒ 404
  • F18.10.03 Shipped

    Region editor role (manages region landing page)

    ✅ 449770bd.m-1 — region_content_editor på distriktets org_node (distriktet + dess subträd)
  • F18.10.04 Planned

    Content approval workflow (draft > review > publish) for larger federations

    ⊘ m-2 — sidans statusmaskin och cms:approve byggs där
  • F18.10.05 Shipped

    Audit trail on all content changes

    ✅ 449770bd.m-1 för sajtkärnan (cms.site.*, cms.page.created i audit_log; nekanden i auth_audit_event) · ⊘ sid-/blockmutationer är m-2
  • F18.10.06 Planned

    Content scheduling permissions (who can publish immediately vs. schedule)

    ⊘ m-2 — cms:publish:immediate / cms:publish:schedule registreras först när schemaläggningen finns

Dynamic Data Integration

F18.11
Planned

> **Statusrättelse 2026-08.** Raderna nedan stod som `[DONE]` med hänvisning till den

How it works
  • F18.11.01 Shipped

    Ranking table widget (auto-updated)

    ✅ 449770bd.m-6 — preset_slug (krävs) + dimension_key/limit/show_club/show_rank_change/link_to_full_list, data ur D-RANK:s egna sömmar (PublishedPresetBySlug + PublicBoardRows). Presetens publika status upplöses vid läsning (den ägs av D-RANK och är föränderlig) — okänd/icke-publik ⇒ neutral 404
  • F18.11.02 Shipped

    Competition calendar widget

    ✅ 449770bd.m-6 — ListPublicCompetitions (D-COMP) serverfiltrerad på sajtens tenant + feedens egna level/discipline; fönstret from_months_ahead, view_mode list|cards
  • F18.11.03 Shipped

    Recent results widget

    ✅ 449770bd.m-6 — ListCompetitionResultsForCompetitionPublic (D-COMP äger publiceringsbegreppet); show_scores ger den publicerade placeringslistan
  • F18.11.04 Shipped

    Club directory widget

    ✅ 449770bd.m-6 — ListOrgNodes (F1-9) med parent_org_node_id som söm-filter, level_type, sort_by name|level, show_contact
  • F18.11.05 Shipped

    Venue map widget

    ✅ 449770bd.m-6 — ListPublicVenues via den nya publika projektionen GET /v1/public/tenants/{code}/venues (exakt {id, name, venue_type, latitude, longitude, court_count, locality}, D-VENUE:s yta; se specs/api/endpoints/venues.md)
  • F18.11.06 Shipped

    Live score widget (during competitions)

    ✅ 449770bd.m-6 — ListMatchesForCompetitionPublic (D-COMP), pågående matcher, show_court; cache-TTL 10 s. ⊘ mene-detaljen finns inte i den publika matchprojektionen och byggs inte (ett fält utan källa vore fabricerad data)
  • F18.11.07 Shipped

    Player profile widget

    ✅ 449770bd.m-6 med bokförd avvikelse: redaktörens uttryckliga players[] (user_ref) genom D-RANK:s publika profilsömmar — inget club_id-filter, eftersom ingen publik klubbroster-feed finns och exponeringsbeslutet inte ägs av D-CMS
  • F18.11.08 Shipped

    Federation statistics widget (member count, club count, etc.)

    ✅ 449770bd.m-6 delvis, bokfört: show_member_count/show_growth ur D-DASH:s egen beräkning (samma som /v1/public/tenants/{code}/stats: members_active_total, growth_12m_pct, as_of) · ⊘ club_count/competition_count/license_count/trends byggs inte — ingen publik källa bär dem (klubbantalet finns ärligt i club_directory)
  • F18.11.09 Shipped

    Upcoming events widget

    ✅ 449770bd.m-6 — ListPublicEventsPage (D-EVENTS) med tenant-predikat i frågan; from_months_ahead, show_location, show_description
  • F18.11.10 Shipped

    Sponsor banner widget

    ✅ 449770bd.m-6 med bokförd avvikelse: redaktörens kuraterade sponsors[] (namn, logo_media_ref, http/https-url, tier) — huset har ingen sponsordomän, så en "sponsorfeed" hade varit påhittad data
  • F18.11.11 Shipped

    Sparad widget-konfiguration + radera-skydd

    ✅ 449770bd.m-6 — cms_widget_config (migration 00206, CHECK:ade TTL-/refresh-gränser) och GET/POST/GET/PATCH/DELETE /v1/cms/sites/{id}/widgets[/{config_id}] bakom cms:view/cms:edit med If-Match; DELETE av en refererad konfiguration ⇒ 409 widget_config_in_use med de refererande sidornas id/slug (inaktivering är den ärliga nedtagningen). Audit cms.widget_config.created|updated|deleted
  • F18.11.12 Shipped

    Cache-kontraktet

    ✅ 449770bd.m-6 — Cache-Control: public, max-age=<TTL> + svag ETag/304; auto_refresh_seconds är klientens intervall. ⊘ ingen widget-datacache i databasen, inget jobb och ingen /refresh-rutt (non-goal): en andra kopia av sportdatan hade blivit en andra sanning
  • F18.11.13 Shipped

    Serverns blockkatalog

    ✅ 449770bd.m-6 — GET /v1/cms/block-catalog ({type, built, dynamic, owner_milestone?}): admin-palettens enda källa, aldrig en klientlista som kan glida ur fas med BlockTypeBuilt

SEO & Search Optimization

F18.13

> **Rebuild-status (`449770bd.m-11`).** PL-F1813a/b:s regler är ärvda — **formen** är ny.

How it works

Theme & Customization

F18.12

**PL-F1812b: F18.12 CMS theme och customization del 2/2** — Implemented:

How it works
  • F18.12.01 Shipped

    Theme gallery with live preview — PL-F1812a

    ✅ PL-F1812a
  • F18.12.02 Shipped

    Per-club color scheme presets — PL-F1812a

    ✅ PL-F1812a
  • F18.12.03 Shipped

    Custom CSS injection enhancements (variables, scoping, validation) — PL-F1812a

    ✅ PL-F1812a
  • F18.12.04 Shipped

    Logo upload with auto-sizing (8 variants) — PL-F1812a

    ✅ PL-F1812a
  • F18.12.05 Shipped

    Header/footer layout selection — PL-F1812b

    ✅ PL-F1812b
  • F18.12.06 Shipped

    Page width and content max-width configuration — PL-F1812b

    ✅ PL-F1812b
  • F18.12.07 Shipped

    Font pair selection (heading + body) — PL-F1812b

    ✅ PL-F1812b
  • F18.13.01 Shipped

    Enhanced per-page meta title/description with configurable title templates, length limits, auto-description, SEO preview and audit endpoints

    ✅ PL-F1813a
  • F18.13.02 Shipped

    Enhanced Open Graph tags with og:locale and og:locale:alternate for multi-language sites plus article-specific OG properties

    ✅ PL-F1813a
  • F18.13.03 Shipped

    Dedicated Twitter Card support with site-level defaults, per-page creator handle, separate twitter:image:alt, and card type fallback

    ✅ PL-F1813a
  • F18.13.04 Shipped

    Schema.org SportsClub structured data with sport="Pétanque", logo, sameAs social profiles, and memberOf federation hierarchy

    ✅ PL-F1813a
  • F18.13.05 Shipped

    Auto-generated sitemap.xml for CMS sites with lastmod, changefreq, priority, and xhtml:link hreflang alternates for multi-language sites

    ✅ PL-F1813b
  • F18.13.06 Shipped

    Enhanced robots.txt per CMS instance with crawl-delay, custom disallow/allow patterns, user-agent rules, and sitemap reference

    ✅ PL-F1813b
  • F18.13.07 Shipped

    Enhanced canonical URLs with language-aware resolution (default uses clean path, non-default includes prefix) and canonical+alternates endpoint

    ✅ PL-F1813b
  • F18.13.08 Shipped

    hreflang link tags for all available language versions plus x-default pointing to default language with partial translation awareness

    ✅ PL-F1813b

Custom Domains

F18.14
Planned

> **Statusrättelse 2026-08 (faktum 12/19).** Raderna nedan stod som `[DONE]` med

How it works
  • F18.14.01 Shipped

    Custom hostname-registrering med CNAME-instruktioner

    ✅ 449770bd.m-4 — cms_domain (migration 00203) + POST /v1/cms/sites/{id}/domains med Idempotency-Key; instruktionerna är serverns (CNAME mot PL_CMS_EDGE_CNAME_TARGET + TXT _petanque-verify.<host> = pl-verify=<token>), aldrig en literal i klienten. Global unikhet bland ej raderade via partiellt unikt index; 409 hostname_taken, 400 invalid_hostname, 400 hostname_in_platform_zone
  • F18.14.02 Shipped

    CNAME/TXT-valideringsflöde

    ✅ 449770bd.m-4 — POST …/domains/{id}:verify och svepet delar EN dom (cms.AssessDomainProof, P-11): verified|partial|none|wrong_target, där wrong_target bär både observerat och förväntat mål. Statusmaskinen pending → pending_validation → active|failed (failed inte terminalt) går genom cms.TransitionDomainStatus — den enda sömmen. Ett resolver-fel stämplar last_checked_at men lämnar status orörd och skriver noll audit
  • F18.14.03 Planned

    Automatisk TLS-provisionering

    ⊘ go-live (C-3/C-4) — edgen (Caddy on-demand TLS) och dess certcache är infrastruktur, inte applikationskod; initiativets uttalade out-of-scope. Plattformens del är F18.14.04. ssl_status/edge-hostname-id byggs inte (D-1): ett fält utan skrivare är en lögn om att vi vet något
  • F18.14.04 Shipped

    Cert-utfärdande gate mot Let's Encrypt-abuse

    ✅ 449770bd.m-4 — GET /v1/public/cms/domains/cert-allowed?domain=, deny-by-default och indexbackad: 200 endast för aktiv sajts plattforms-subdomän i zonen eller active custom-domän på aktiv sajt, allt annat 404. Cache-Control: no-store och ingen processcache — en :verify-flip syns utan omstart
  • F18.14.05 Planned

    Admin-UI för domänhantering (add/verify/remove, statusbadges, kopierbar CNAME-target)

    ⊘ 449770bd.m-4 t-2 (domänguiden i admin) — t-1 levererar serverkontraktet
  • F18.14.06 Shipped

    Plattform-subdomän-konvention <slug>.<zon> med auto-DNS

    ✅ 449770bd.m-4 — sex additiva cms_site-kolumner (subdomain_dns_status|_error|_provisioned_at|_attempts, mount_parent_site_id, mount_path_slug). Requesten väntar aldrig på en DNS-leverantör: wizard/PATCH skriver bara pending, zonen rörs av svepet cms_domain_reconcile (*/5 * * * *, försökstak 8) eller av POST …:repair-dns (synkron, idempotent, nollställer taket, 422 utan subdomän). Adaptern väljs av miljön (PL_CMS_DNS_BACKEND); utan adapter hoppar svepet ärligt över med en logg
  • F18.14.07 Shipped

    Hostname-resolve för edge-routing

    ✅ 449770bd.m-4 — GET /v1/public/cms/domains/resolve/{hostname} med egen publik projektion, match ∈ {platform_subdomain, custom_domain}, monteringar djupast först, max-age=60 + svag ETag/304, och fail-closed 404 utan formskillnad (okänt namn, pending/raderad domän, setup/inaktiv sajt ger samma kropp — annars läcker domänkatalogen)
  • F18.14.08 Shipped

    Montering: barnsajt under förälderns värd (/<distrikt>/<klubb>/) — stänger F18.01.08

    ✅ 449770bd.m-4 — PUT/DELETE /v1/cms/sites/{id}/mount; composed_path och active härleds vid läsning, lagras aldrig (en lagrad väg blir osann när en förälder byter slug). Max 3 nivåer, reservlistan som data (_next, account, api, book, proxy, rss, sitemap, sites), 409 mount_slug_taken, 422 mount_cycle, 422 mount_too_deep. cms.ResolveMountMatch delas av resolven och m-5:s renderare

Premium UX (dual-mode adaptive, SSR-prestanda, editorial) [DONE] `PL-T158`

F18.15
Planned

**PL-T158: F18.15 web/ premium-UX** — Implemented:

How it works
  • F18.15.01 Shipped

    Dual-mode adaptive shell (mobile drawer + bottom-nav / desktop full nav)

    ✅ PL-T158
  • F18.15.02 Shipped

    Editorial display-skala (Fraunces opsz, ss01)

    ✅ PL-T158
  • F18.15.03 Shipped

    PetanqueImage AVIF/WebP/JPEG + focal-point + lazy/priority

    ✅ PL-T158
  • F18.15.04 Shipped

    TenantBrandStyle SSR CSS-var-injektion + auto-accent

    ✅ PL-T158
  • F18.15.05 Shipped

    SSR-prestanda + canonical + hreflang + Lighthouse-budget

    ✅ PL-T158
  • F18.15.06 Shipped

    JSON-LD Organization + WebSite per tenant page

    ✅ PL-T158
  • F18.15.07 Shipped

    Sitemap + robots med tenant-noindex-respekt

    ✅ PL-T158
  • F18.15.08 Shipped

    Skip-to-content + axe-baseline + focus-visible

    ✅ PL-T158
  • F18.15.09 Shipped

    Editorial footer + språkväxlare med session-minne

    ✅ PL-T158, ♻️ Rebuild fa798ce7.m-4.t-2 (D-LOCALE FR-2/FR-4: webs skal talar nu förbundets/personens språk — <html lang> ur tenantens default_locale (literalen "sv" borta), header-/footer-/metadata-texten ur microcopy (siteChrome.*) i den locale chromen faktiskt renderar i, och en kontobunden språkväxlare i headern (web-language-picker) som PATCH:ar /v1/me och byter UI:t utan omladdning — valet följer kontot till appen, inte en cookie. Anonym besökare får ingen kontoväxel; kedjan (enhetsspråk → tenant-default → en) styr. Hydreringen är mismatch-fri: server-prop före mount, store efter (useChromeCopy). Se specs/web/views/site-chrome.md §Chromens SPRÅK)
  • F18.15.10 Shipped

    Shared ThemeProvider + HtmlThemeBridge

    ✅ PL-T158

Ministry-report PDF-template-engine [DONE] `PL-T223`

F18.16
Planned

Multi-language PDF-template-resolution för `MinistryImpactReport`. Template

  • F18.16.01 Shipped

    MinistryReportTemplate-config per (tenant_id, funder_type, lang) med pdf_template_ref + required_sections + value_assumptions

    ✅ PL-T223
  • F18.16.02 Shipped

    Template-resolution med fallback-kedja (exact/en/tenant-default/no_template) och audit-warning vid fallback

    ✅ PL-T223
  • F18.16.03 Shipped

    PDF 1.4-wrapper utan extern dependency, deterministisk för testbarhet

    ✅ PL-T223
  • F18.16.04 Shipped

    Multi-lang PDF-export med X-Template-Resolution-header för spårbarhet

    ✅ PL-T223

Club inquiry CMS form-shortcode [DONE] `PL-T222`

F18.17
Planned

Klubbsidor på `web/`-CMS:et exponerar ett intresseanmälnings-formulär som

  • F18.17.01 Shipped

    ClubInquiryFormBlock-komponent på web/, lokaliserade etiketter (en/sv/fr/es), default inquiry_type per shortcode-prop

    ✅ PL-T222
  • F18.17.02 Shipped

    Honeypot-fält (website, off-screen) + Cloudflare-Turnstile-integration via /public/config/turnstile-key

    ✅ PL-T222
  • F18.17.03 Shipped

    Block-registrering i BlockRenderer så CMS-sidor renderar type: "club_inquiry_form"

    ✅ PL-T222
  • F18.17.04 Shipped

    Public POST-endpoint /clubs/{club_id}/inquiries med rate-limit 10/h per IP och 5-min dedup på (email, club_id, subject)

    ✅ PL-T222

Open-Source Continuity Layer [PARTIAL] `PL-T230` / D-CONTINUITY

F18.18
In progress

> Spec-källa anger "F18.10 Open-Source Continuity Layer" — F18.10 är dock

  • F18.18.01 Shipped

    Omgrundad (e5099cda.m-1): tools/audit/audit-template-portability.mjs skannar ombyggnadens lager 1 (web/components/sajt/, packages/shared/src/design/tokens/, packages/shared/src/microcopy/bundles/) ur den DELADE rotkällan tools/lib/oss-mirror-sources.mjs och fäller @petanque-life/shared/api-client, …/auth, next/headers, next/server, rå fetch(, process.env.PL_* och tvärberoenden in i web/lib/web/app. En rot som resolvar till noll filer ⇒ exit 2 (den gamla grön-som-ljuger-defekten)

    ✅ D-CONTINUITY
  • F18.18.02 Shipped

    *(ej byggd i greenfield)* Mirror-header-injicering tools/generators/inject-mirror-headers.mjs (idempotent, bevarar "use client"/"use server"-pragmas, två header-typer för portabel resp. allowlistad fil)

    ✅ PL-T230
  • F18.18.03 Shipped

    Omgrundad (e5099cda.m-1): --init/--sync --target <dir> ur SAMMA rotkälla som auditen, med auditen som pre-flight före varje skrivning (audit_failed ⇒ exit ≠ 0). Manifest v2 (RÄKNADE tal, kind + last_changed per kopierad fil, reason per överhoppad, audit-block) skrivs identiskt till <target>/.oss-mirror-manifest.json och den gitignorerade tools/.state/oss-mirror-state.json; å/ä/ö byte-bevaras

    ✅ D-CONTINUITY
  • F18.18.04 Shipped

    Omgrundad (e5099cda.m-1): GET /v1/sys/oss-templates/status läser spegelns DB-persisterade tillstånd (oss_mirror_state) + audit-block + GitHub-kort + last_sync_runs[]; capability sys.oss_templates.read (sys_engineer + sys_security), Cache-Control: private, max-age=300, ingen audit-rad på läsning. GitHub-degradering är ett 200 med sentinel (github_token_not_configured / github_unreachable:<orsak> / github_status_<n>), aldrig 5xx

    ✅ D-CONTINUITY
  • F18.18.05 Shipped

    Omgrundad (e5099cda.m-1): POST /v1/sys/oss-templates/sync ⇒ 202 {job_id, enqueued_at, triggered_by}; capability sys.oss_templates.sync (endast sys_engineer) + husets fresh-auth; EXAKT en WORM-rad sys.oss_templates.sync.enqueue per faktiskt köad körning. Dubbelklick ⇒ 409 sync_already_in_progress ur en DB-invariant (partiellt unikt index), noll audit — en AVSLUTAD körning blockerar aldrig

    ✅ D-CONTINUITY
  • F18.18.06 Shipped

    *(byggs i e5099cda.m-1.t-2)* Sys-vyn sys /oss-templates. Legacy-skissen var views/sys/oss-templates-status (sys/app/(dashboard)/oss-templates-status.tsx) — hero med last-synced-at/commit/by, GitHub-stats-panel, mirrored-files-tabell, skipped-allowlist-lista, "Sync now"-knapp (fresh-auth via lib/api.ts); typed klient sys/src/lib/oss-templates.ts; sidebar-länk under Styrning-gruppen

    ✅ PL-T230
  • F18.18.07 Shipped

    Strategi-doc docs/business/oss-strategy.md (varför MIT, varför separat repo, copyright, risk-bedömning, Continuity Promise-formulering) + arkitektur-doc docs/engineering/architecture/03b-continuity-layer.md (3-lagers-modell, two-mechanism-jämförelse mot PL-T227/T228); pricing-rad i docs/business/business-model.md

    ✅ PL-T230
  • F18.18.08 Shipped

    Omgrundad (e5099cda.m-1): allowlisten kräver file + import + icke-tomt reason (missing_allowlist_reason), och en post vars fil inte längre finns fälls som stale_allowlist_entry — utan det kunde listan beskriva ett system som inte finns kvar och ändå se städad ut. Legacy-posterna (web/src/…) är borttagna

    ✅ D-CONTINUITY
  • F18.18.09 Shipped

    Delad rotkälla tools/lib/oss-mirror-sources.mjs — EN deklaration av lager-1-mängden (rot → målgrupp → kind), importerad av BÅDE auditen och synkverktyget. Två deklarationer kan glida isär i tysthet; en kan det inte

    ✅ D-CONTINUITY
  • F18.18.10 Shipped

    Grindsteget L3f i tools/gate/verify.sh — portabilitetsauditen som kontinuerlig kontroll (default PÅ, röd vid fynd OCH vid konfigurationsfel; ventilen SKIP_PORTABILITY_GUARD=1 endast för partiella lokala körningar). L3d/L3e orörda

    ✅ D-CONTINUITY
  • F18.18.11 Shipped

    DB-state (Greenfield-avvikelsen, bokförd): goose-migrationen 00221_dcontinuity_m1_oss_mirror.sql med de plattforms-globala (tenant-lösa) oss_mirror_state + oss_mirror_sync_run. Legacy-modellen läste manifestet från API-processens filsystem — omöjligt i den driftsatta Go-containern, som varken har monorepo-trädet eller node

    ✅ D-CONTINUITY
  • F18.18.12 Shipped

    Registreringssömmen PUT /v1/sys/oss-templates/manifest (fresh-auth + sys.oss_templates.sync): manifest v2-schema med 422 + fältpekare, registered_by ur token (no-idor — kroppens synced_by blir det aldrig), EN WORM-rad sys.oss_templates.manifest.register och noll per no-op (byte-identisk om-registrering ⇒ 200, oförändrad state). Store-sömmen är EN och delas med jobbet

    ✅ D-CONTINUITY
  • F18.18.13 Shipped

    F1-7-jobbet oss_mirror_sync — porten MirrorSyncPort väljs ur PL_OSS_MIRROR_BACKEND (aldrig gissad); local-adaptern kör synkverktyget mot PL_OSS_MIRROR_REPO_PATH och registrerar det MÄTTA manifestet genom store-sömmen. Utan backend bokförs körningen skipped_no_backend med en logg och ingen state-rad rörs. Utfallsorden succeeded/failed:<orsak>/skipped_no_backend är domänkonstanter. Riktig driftadapter = go-live-arbete

    ✅ D-CONTINUITY
  • F18.18.14 Shipped

    Capabilities + rollfrysning: sys.oss_templates.read (sys_engineer + sys_security) och sys.oss_templates.sync (endast sys_engineer) i katalogen; den frysta sys-rollmängden uppdaterad med edit-logg och en matris-vakt i BÅDA riktningar. Inga breddade rollmallar

    ✅ D-CONTINUITY
  • F18.18.15 Shipped

    Ingen seed (medvetet): spegelstatus seedas aldrig — ett seedat manifest vore ett påstående om en synk som aldrig körts. Tomläget (manifest: null + manifest_missing) är designat, och den körbara lokala beviskedjan (--init → --sync → PUT …/manifest → run-job oss_mirror_sync) står i runbooken

    ✅ D-CONTINUITY
  • F18.18.16 Shipped

    Datamodellen (e5099cda.m-2): goose-migrationen 00223_dcontinuity_m2_continuity_export.sql med continuity_export_config (EN rad per tenant: aktivering, intervall, age-mottagarnyckel + SERVER-räknat encryption_key_fingerprint, destination + OPAK env:<NAMN>-credentialpekare, larmtröskel, retention) och den APPEND-ONLY continuity_export_run (mätta bytes_written/files_written/row_count_total, manifest jsonb, artifact bytea, destination_result). Ingen lagrad overdue-/SLA-kolumn — förseningen HÄRLEDS av EN delad funktion som både sys-listan och tenant-statusen kallar. Single-flight är en DB-invariant (partiellt unikt index), inte ett if

    ✅ D-CONTINUITY
  • F18.18.17 Shipped

    Continuity-registryn med mekaniskt bevisad fullständighet: klassnings-manifestet api/db/migrations/continuity-coverage-manifest.json klassar VARJE tabell med tenant_id-kolumn vid HEAD som exported (dataset = tabellnamnet) eller excluded (ägare + skäl ≥ 20 tecken), och api/internal/continuity/registry.go byggs UR manifestet — varje exported-post blir en VERKLIG, tenant-skopad, paginerad läsning genom HELA tabellen (inget radtak). En oklassad tabell, en stale post eller en exported-post utan levande dataset är RÖTT; vakten är en permanent ratchet i testsviten

    ✅ D-CONTINUITY
  • F18.18.18 Shipped

    Deny-mängden (api/internal/continuity/deny.go): EN kodkälla för de hemlighetsbärande kolumnKLASSERNA (password/secret/token/credential/private_key/signing_key/encryption_key/key_ref/api_key/access_key/passkey/salt/hash/cipher) med en motiverad allowlist (integritetskedjornas hashar, encryption_key_fingerprint, »credential« i domänbetydelsen meriter) vars skäl skrivs ut i artefaktens README. F1-3:s credential-/sessions-/tokentabeller är excluded per definition — ett uttag ska kunna återskapa verksamheten, inte inloggningarna

    ✅ D-CONTINUITY
  • F18.18.19 Shipped

    Den age-krypterade artefakten (e5099cda.m-2, FR-3): EN zip med manifest.json + README.md i klartext och datasets/<namn>.ndjson.age per dataset, okomprimerade (zip Store — 1:1-lagring är vad som gör byte-flip-verifiering meningsfull). files[].sha256/size_bytes mäts över de LAGRADE bytesen (verifierbart UTAN privatnyckel), plaintext_sha256/row_count före krypteringen, i strömmen. Krypteringen är fail-closed: en enabled konfig utan giltig age1…-mottagare ⇒ failed + encryption_key_missing, INGEN artefakt, ingen push. coverage[] är aldrig tom och bär ägare + skäl per lucka

    ✅ D-CONTINUITY
  • F18.18.20 Shipped

    F1-7-jobben continuity_export_run (*/15 * * * *, singleton: schemalägger förfallna uttag med villkoret i FRÅGAN, claim:ar köade snapshot|final som en SKRIVNING (for update skip locked), levererar och gallrar artefakt-bytes bortom retention_policy_days — även för en disabled konfig, medan raden och manifestet står kvar) och continuity_verify_run (40 4 * * *: kvartalsvis om-hashning; avvikelse ⇒ failed med mismatches[] {path, expected_sha256, actual_sha256}, och det röda står kvar för alltid). ContinuityDestinationPort väljs ur PL_CONTINUITY_DESTINATION_BACKEND (aldrig gissad ur URI:n) med en s3-adapter bevisad mot minio; utan backend bokförs skipped_no_backend och uttaget byggs ändå. Utfallsorden är domänkonstanter

    ✅ D-CONTINUITY
  • F18.18.21 Shipped

    Sys-API:t /v1/sys/continuity/* (8 rutter): capabilities sys.continuity.read (sys_engineer + sys_security) · sys.continuity.configure (endast sys_security) · sys.continuity.run (endast sys_engineer), fresh-auth på varje mutation, ETag/If-Match-konvergens (byte-identisk om-PUT ⇒ noll audit, ingen versionsbump), EN-regeln vid dubbelbeställning (samma run_id, already_live), kind: delta ⇒ 422 med sitt varför, och WORM-taxonomin sys.continuity.config.upsert/…config.disable/…run.order/…artifact.download

    ✅ D-CONTINUITY
  • F18.18.22 Shipped

    Tenant-API:t /v1/tenant/continuity/* (6 rutter) med continuity:read/continuity:manage i den BEFINTLIGA federation_admin-mallen: status med härledd försening, egna körningar (annan tenants run_id ⇒ 404, aldrig 403), hemhämtning av det egna uttaget, nyckelrotation (omräknat fingerprint; ogiltig nyckel ⇒ 422 invalid_public_key utan sidoeffekt) och en destinationsprövning som MÄTER en riktig 1 KiB-rundresa och städar i try/finally. Inga tenant-mutationer av aktivering/destination/schema (permanent gräns) och ingen seed — den körbara kedjan står i kontinuitet.md

    ✅ D-CONTINUITY
  • F18.18.23 Shipped

    Datamodellen (e5099cda.m-3): goose-migrationen 00225_dcontinuity_m3_escrow.sql med fyra plattforms-globala, TENANT-LÖSA tabeller — escrow_deposit (APPEND-ONLY liggare; idempotensen är DB-invarianten unik (release_version, escrow_provider), ingen update-/delete-väg finns i store-lagret), escrow_deposit_run (körningslogg med claim-i-skrivningen), escrow_verification_drill (unik quarter_label = en drillplikt per kvartal) och escrow_gap_state (first-run-ankare + episodbokföring). Ingen kolumn lagrar covered/has_gap/overdue — täckningen härleds vid läsning

    ✅ D-CONTINUITY
  • F18.18.24 Shipped

    Den omgrundade verktygskedjan (tools/escrow/): källmängden är DATA (stage-roots.json, exkluderar det spårade dev-nyckelparet api/.local-keys/), build-bundle.sh stagear ur det COMMITTADE trädet (determinism OCH hemlighetsgräns i ett drag; en död stage-rot är exit 2), check-bundle-guard.sh är den mekaniska hemlighets-/datagränsvakten, age-bin.sh resolvar PL_ESCROW_AGE_BIN → age → go run ./cmd/agecli (grindmiljön saknar age-CLI:n, mätt) och manifestet är byte-identiskt mellan två byggen av samma commit (väggklockan bor i BUILD-INFO.txt)

    ✅ D-CONTINUITY
  • F18.18.25 Shipped

    Den omgrundade drillen verify-restore.sh: dekrypterar, verifierar HELA sha-kedjan (BUILD-INFO ↔ manifest ↔ varje fil, FÖRE varje stack-boot), reser en ENGÅNGS-Postgres ur bundlens egen docker-compose.yml (unikt namn + ledig port, riven i en trap), kör bundlens EGNA migrate up + serve och probar /livez + /status; rapporten på stdout ÄR kroppen som avgör drillen. Cosmos-/Bicep-vägen är borta ur skriptet — attic-hänvisningen för gamla bundles bor i runbooken

    ✅ D-CONTINUITY
  • F18.18.26 Shipped

    F1-7-jobben escrow_deposit_build (claim som skrivning; EscrowDepositPort ur PL_ESCROW_DEPOSIT_BACKEND, aldrig gissad; fail-closed på saknad age-mottagare FÖRE första byten ⇒ ingen oförseglad artefakt kan existera; utan backend skipped_no_backend UTAN liggarrad), escrow_deposit_gap_alert (90 dygn per provider; D-6-degraderat larm — slog ERROR + EN WORM-rad per provider och episod, single-fire som skrivvillkorad bokföring) och escrow_verification_quarterly (öppnar kvartalets drillrad; drillen SJÄLV är medvetet manuell)

    ✅ D-CONTINUITY
  • F18.18.27 Shipped

    M2M-record-sömmen POST /v1/sys/escrow/internal/record-deposit: HMAC-SHA256 över den RÅA kroppen är dess enda auth — en sys-JWT accepteras ALDRIG, inte ens sys_security med färsk verifiering (den som skriver ett kvitto ska vara den som byggde artefakten). Hemlighetsregeln är fail-closed (frånvarande ⇒ dev-fixtur ENDAST i local/test; explicit tom ⇒ 401 escrow_internal_secret_unset i varje miljö), servern MÄTER included_tenants_count själv och ett inskickat värde ignoreras. EN store-söm delas av rutten och jobbet

    ✅ D-CONTINUITY
  • F18.18.28 Shipped

    Operatörsytan /v1/sys/escrow/{status,deposits,manual-deposit,drills/{id}:resolve}: sys.escrow.read (sys_security + sys_finance) och sys.escrow.manage (endast sys_security, fresh-auth) — sys_engineer/sys_support nekas ÄVEN läsning (legacy-fördelningen; ägarändring är en push-back, och sysroles-frysningen går röd vid en tyst breddning). Statusen räknar fyra rullande 90-dygnsfönster ur liggaren, liggarlistan paginerar i husets kanoniska keyset-form och ingen mutationsrutt mot liggaren existerar — varken i koden eller i openapi. Ingen seed (FR-8.1): den körbara kedjan står i deponering.md

    ✅ D-CONTINUITY
  • F18.18.29 Shipped

    Datamodellen (e5099cda.m-4): goose-migrationen 00228_dcontinuity_m4_winddown.sql med wind_down_event (singleton medan den lever — ett PARTIELLT unikt index tillåter högst EN active; ingen DELETE-yta, varken i store-lagret eller i openapi), wind_down_reserve (EN levande rad per förbund via partiellt index; historiska rader står KVAR; belopp som heltal i minsta enhet + ISO-4217-valuta), wind_down_drill (unik label) och wind_down_alert_state (m-3:s escrow_gap_state-mönster: first_evaluated_at-ankare + episodbokföring). Ingen kolumn lagrar ett composite-, nivå-, täcknings- eller frysord — allt härleds vid läsning

    ✅ D-CONTINUITY
  • F18.18.30 Shipped

    Wind-down-händelsen med two-person (FR-2): events:preview ⇒ MÄTTA konsekvenser (aktiverade uttagskonfigurationer, hur många som saknar nyckel, levande federationsavtal, reservlägen, klausulläge) → events:confirm av en ANNAN sys_security-identitet. Självbekräftelse ⇒ 409 wind_down_second_approver_must_differ FÖRE varje mutation; en andra kedja ⇒ 409 wind_down_already_active ur DATABASENS index, aldrig ur ett if. Fasresan 0→7 är monoton (If-Match), :complete kräver fas 7 (422 phases_incomplete) och :abort kräver samma two-person-form

    ✅ PL-T230
  • F18.18.31 Shipped

    Frysseamen (FR-2.2): winddown.Active är EN billig läsning per anrop — ingen flagga, ingen cache, ingen invalidering att glömma — och konsumeras av EXAKT POST /v1/public/onboarding-requests + POST /v1/sys/tenants ⇒ 409 wind_down_active med clause_url. Läsfel FRYSER (fail-closed); efter :abort/:complete lyfter frysningen av sig själv. Ingen annan skrivning fryses — befintliga förbund arbetar hela avvecklingen igenom, det ÄR löftet (legacys platform.wind_down_active-flagga och read-only-flaggorna är ⊘, bokförda)

    ✅ PL-T230
  • F18.18.32 Shipped

    F1-7-jobbet wind_down_tick (*/15 * * * *): plikt 1 köar ett final-uttag per aktiverad exportkonfiguration som saknar en final-körning efter triggered_at — villkoret i FRÅGAN, köandet genom m-2:s EGEN store-söm (dubbelkö omöjlig via continuity_export_run_live_uq), och en konfiguration utan registrerad nyckel får INGEN körning (ärlig skip: en okrypterad nödexport får aldrig existera). Plikt 2: drillförfall > 90 dygn ⇒ slog ERROR + EXAKT EN WORM-rad per episod, med first-run-ankare så en färsk databas inte larmar. Ingen SEV-incident (D-OPS äger dem)

    ✅ PL-T230
  • F18.18.33 Shipped

    Reservens livscykel (FR-4, PL-T229): pending → funded ⇄ short genom en övergångstabell (422 invalid_status_transition), mark_funded sätter verifieringsfristen +1 år, :release-preview/:release-confirm kräver aktiv händelse (422 no_active_wind_down_event) + two-person och stämplar FÖR VILKEN händelse reserven betalades ut, :return bara i fredstid. Målet är AVTALETS, aldrig serverberäknat ur omsättningen. Att BOKFÖRA (sys.wind_down.reserve.manage, endast sys_finance) och att BETALA UT (sys.wind_down.release, endast sys_security) är skilda mandat hos olika roller. Legacys Stripe-webhook är ⊘: en escrow-deposition tecknas inte med ett kortköp

    ✅ PL-T229
  • F18.18.34 Shipped

    Klausulåterbruket + den publika läsningen (FR-5, PL-T229): ingen parallell liggare — klausulen är doc_type='wind_down_clause' i det BEFINTLIGA sys_legal_document, publiceras genom legal-registrets egna rutter och accepteras genom den befintliga clickwrapen. Ny publik GET /v1/public/legal/wind-down-clause?lang=sv|en|fr|es serverar registrets EGEN content_sha256 + de kod-deklarerade commitment_months: 12 / mechanism_options: [deposit, insurance], Cache-Control: public, max-age=3600, 400 invalid_language / 404 clause_not_published — ingen fil-fallback (en källtext i repot är ett utkast, inte ett löfte) och inget content_html

    ✅ PL-T229
  • F18.18.35 Shipped

    Readiness-compositen + tenant-insynen (FR-6/FR-9.1): GET /v1/sys/wind-down/readiness (+ /{tenant_id}) över UNIONEN levande federationsavtal ∪ exportkonfiguration ∪ levande reserv, med FEM signaler (reserv · uttag · escrow · klausul · drill) HÄRLEDDA av rena funktioner i det nya paketet api/internal/winddown med injicerad klocka, overall = värsta och kod-deklarerade trösklar. EN sammansatt läsning, aldrig N+1. Compositen ÄR D-OPS-sömmen: ingen push, ingen webhook, inget eget schema (en läsande konsument kan inte hamna ur synk). sys.wind_down.read bärs av sys_security + sys_support + sys_finance; sys_engineer nekas ÄVEN läsning. Förbundets egen GET /v1/tenant/continuity/wind-down (continuity:read) maskar referensen (*+3) och visar klausulstatus + händelseläge utan operatörer eller skäl. Ingen seed — den körbara kedjan står i beredskap.md

    ✅ PL-T230

CMS access control: gated pages per group + grupp-modell [DONE] `PL-T205`

F18.19
Planned

CMS-sidor och enskilda block kan begränsas till tre access-nivåer

  • F18.19.01 Shipped

    CmsAccessGroup-datamodell tenant-scopad med slug/name/kind/auto_category/auto_entity_id/member_user_ids/member_count/last_synced_at/is_active + uniqueness på (tenant_id, slug)

    ✅ PL-T205
  • F18.19.02 Shipped

    CmsPage utökad med access_level ∈ {public, members, groups}, allowed_group_ids[], preview_for_anonymous: dict[lang, str], block_access per ContentBlock

    ✅ PL-T205
  • F18.19.03 Shipped

    Auto-group-katalog (9 kategorier) seedas via POST /admin/cms-access/seed; per-grupp-sync via POST /admin/cms-access/{id}/sync; bulk-sync via POST /admin/cms-access/sync

    ✅ PL-T205
  • F18.19.04 Shipped

    Custom-grupp-CRUD /tenant/groups med audit-trail (cms.access_group_created/updated/deleted/members_added/member_removed); 409 vid radering av grupp som refereras av page

    ✅ PL-T205
  • F18.19.05 Shipped

    services.cms_access.evaluate_page_access returnerar AccessDecision ∈ {allow, require_login, denied_not_member, denied_not_in_group}; evaluate_block_access är ren (utan I/O) för per-block-gating i SSR

    ✅ PL-T205
  • F18.19.06 Shipped

    Public CMS-endpoints (/public/sites/{id}/pages, /public/sites/{id}/pages/{slug}) filtrerar listor och redacterar block enligt viewer; gated sidor läcker inte i sitemap eller söklistor

    ✅ PL-T205
  • F18.19.07 Shipped

    GET /me/groups med tenant_id/user_id/group_ids[]/is_active_member/hash (sha256 över sorterade group_ids, 32 chars) — SSR cache-key-segment

    ✅ PL-T205
  • F18.19.08 Shipped

    web/-SSR resolveViewerAccess() läser petanque_member_token-cookie + forwardar bearer; getPageBySlug byter till cache: "no-store" när auth-header skickas; AccessGatePlaceholder renderar 3 deny-decisions med i18n-preview

    ✅ PL-T205
  • F18.19.09 Shipped

    Admin-UI: /(dashboard)/cms/access-groups (lista, filter på kind/slug, seed/sync-knappar) + /(dashboard)/cms/access-groups/[id] (detalj — auto read-only, custom edit/add/remove/delete) + /(dashboard)/cms/access-groups/new (skapa custom)

    ✅ PL-T205
  • F18.19.10 Shipped

    42 backend-tester (tests/test_pl_t205_cms_access.py) över modellinvarianter, custom-CRUD, auto-seeding, access-evaluation, user-groups-payload och HTTP-routes

    ✅ PL-T205