feat(ms-ai-architect): release v1.9.0 with playground v3 + screenshot suite

Version bump: v1.8.0 -> v1.9.0 (minor — plugin API surface unchanged).

Version sync:
- .claude-plugin/plugin.json (canonical), README.md badge,
  CHANGELOG.md (full v1.9.0 entry with playground v3 architecture,
  validation suite, A11Y artifacts, SemVer rationale),
  marketplace root README.md listing.

Screenshot suite (new):
- scripts/screenshots/capture-playground.py — Playwright Python automation
  that opens playground from file://, populates __store with Statens vegvesen
  ANPR demo data, navigates each surface, paste-imports fixtures, scrolls to
  the relevant report-slot, and saves viewport screenshots.
- 6 PNG screenshots in playground/screenshots/ covering: onboarding (18/18
  filled), home (3 projects), catalog (24 commands across 5 expansion groups),
  classify pyramid (high-risk Annex III), ROS 5x5 matrix + 7-dim radar,
  cost P10/P50/P90 distribution.

Doc updates (3 levels per repo policy):
- Plugin README: new "Screenshots" subsection embeds all 6 with description
  columns, plus reproduce command.
- Plugin CLAUDE.md: new "Screenshot-suite (v1.9.0)" subsection documenting
  the automation, demo-state seeding, and re-run trigger conditions.
- Marketplace root README: ms-ai-architect listing now mentions the
  screenshot suite + reproduce command.

Reproduce screenshots: python3 scripts/screenshots/capture-playground.py.

Notes:
- Light-mode tokens are not in the vendored design-system yet. The toggle
  swaps data-theme + label correctly (Step 13 mechanics intact), but the
  CSS palette only ships dark. Captured dark-mode only; light-mode capture
  re-enables when shared/playground-design-system gains [data-theme="light"]
  overrides.
- Local CSS fix in playground HTML: added `[hidden] { display: none !important; }`
  in the inline app-shell <style> block. The vendored .error-summary rule
  sets display: flex which overrode HTML's [hidden] default, leaking the
  onboarding error banner on cold start. Plugin-local for now; a proper
  fix belongs in shared/playground-design-system/components-tier3.css.

Verified post-bump:
- bash tests/validate-plugin.sh -> 215/215 PASS
- bash tests/run-e2e.sh --playground -> 240/240 PASS
This commit is contained in:
Kjell Tore Guttormsen 2026-05-03 20:40:07 +02:00
commit 9664bf1b1c
13 changed files with 435 additions and 4 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ms-ai-architect",
"version": "1.8.0",
"version": "1.9.0",
"description": "Microsoft AI Solution Architect - structured architecture guidance for the full Microsoft AI stack",
"author": {
"name": "Kjell Tore Guttormsen"

View file

@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.9.0] - 2026-05-03
### Added — Playground v3 (full architecture rewrite)
The playground at `playground/ms-ai-architect-playground.html` was rebuilt from scratch as a multi-surface decision-builder + report viewer (3867 lines, single-file). It replaces the v2 5-step pipeline with a project-aware app that persists state across sessions and visualizes parsed reports inline.
- **4 surfaces:**
- Onboarding — 5 grouped expansion sections (organization, technology, security, architecture, business) with 18 fields covering organization profile, tech stack, compliance, architecture preferences, and business references. Drives prefill across all 24 commands.
- Home — 3 entry tracks (Onboard / New project / Catalog) plus project list with completion-meter per project.
- Catalog — 24 commands grouped in 5 expansion categories (regulatory, security, economy, documentation, tool) with full-text search, filtered counts, and direct form-launch.
- Project — per-project tabs (one per command category), command form prefilled from shared onboarding state, paste-back markdown import that parses and visualizes reports inline.
- **Persistent state:** IndexedDB primary store with localStorage fallback. Schema-versioned (`STATE_KEY = 'ms-ai-architect-state-v1'`) with eager `MIGRATIONS` pipeline that runs at cold-load and import time. Throttled writer prevents excessive disk traffic.
- **17 inline report renderers** routed via canonical archetype-routing table:
- Regulatory (6): classify pyramid, requirements list, transparency text, FRIA, conformity checklist, DPIA matrix-risk-6×5
- Security (3): security 5×5 matrix, ROS 5×5 matrix with 7-dimension threat library, review findings
- Economy (2): cost distribution P10/P50/P90, license capability matrix
- Documentation (6): migrate phased plan, ADR, summary markdown, POC verdict, utredning, compare
- **14 markdown parsers** with the same archetype keys, exposed as `window.__PARSERS`. All parsers return `{ok, data}` or `{ok, errors[]}` — never throw.
- **Light/dark theme toggle** in the topbar, persisted in `localStorage('ms-ai-architect-theme')`. FOUC-safe: a small bootstrap script in `<head>` reads the saved value before stylesheets parse.
- **Export/import** as JSON Decision Record envelope (Blob + FileReader). Schema-version-aware on import; downgrades trigger MIGRATIONS automatically.
- **Vendored design-system** at `playground/vendor/playground-design-system/`, kept in sync via `scripts/sync-design-system.mjs ms-ai-architect` from the marketplace root. SHA-256 MANIFEST detects local drift.
- **24 ACTIONS** registered through a single delegated click handler on `document` — keeps event-handler footprint minimal.
### Added — Playground validation
- `tests/test-playground-v3.sh` — bash 3.2-compatible static structure tests (170 PASS lines): vendored CSS link order, file://-safety, surfaces, 24 commands, 14 parsers, 17 renderers, design-system class usage, exposed globals, 23 ACTIONS handlers.
- `tests/test-playground-parsers.sh` — 70 PASS lines: validates 17 fixtures × parser routing keys + handlePasteImport wiring.
- `tests/run-e2e.sh --playground` — new flag dispatching both suites; included in `--all`.
- `playground/test-fixtures/` — 17 synthetic markdown fixtures (one per report-producing command, ANPR traffic-analysis example) covering all canonical parser archetypes.
- `playground/A11Y-RAPPORT.md` — accessibility report skeleton with WCAG 2.2 AA test grid for both themes.
- `playground/MANUAL-CHECKLIST.md` — 10-section manual QA checklist (round-trip, schema migration, project CRUD, command form prefill, paste-import per report type, parse errors, export/import cycle, theme toggle, file://-standalone, axe-core a11y per surface) with concrete DevTools-console assertions.
### Changed
- `playground/ms-ai-architect-playground.html` — replaced (v2 1990 lines → v3 3867 lines). Same canonical filename, so external links continue to resolve.
- Plugin README — new "## Playground (v3)" section documenting the 4-surface architecture and validation matrix.
- Plugin CLAUDE.md — replaced v2 5-step pipeline section with v3 architecture overview. `docs/playground-v2-spec.md` retained as historical reference but no longer the contract; v3 spec lives at `.claude/projects/2026-05-03-playground-v3-architecture/`.
- Marketplace root README — playground listing for ms-ai-architect updated to describe v3 architecture, persistence model, 17 renderers, theme toggle, and 240-test validation.
### Notes
- **Plugin API surface unchanged.** All 24 commands, 12 agents, 5 skills (387 reference docs), 2 hooks, and MCP server configuration remain identical to v1.8.0. Playground v3 is an additive, optional UI surface — users invoking `/architect:*` directly see no behavioral difference.
- v1.9.0 is a minor bump per SemVer because the plugin contract (commands/agents/skills/hooks/MCP) is backward-compatible. The playground's local IndexedDB state schema would only affect users who had the v2 playground running; existing state is auto-migrated by the eager MIGRATIONS pipeline.
---
## [1.8.0] - 2026-04-09
### Added

View file

@ -193,6 +193,12 @@ Interaktiv decision-builder + rapport-viewer for Microsoft AI-beslutninger. Erst
| Manuell A11Y QA | Se `playground/MANUAL-CHECKLIST.md` | 10 seksjoner inkl. axe-core-kjøring per surface |
| A11Y-rapport | `playground/A11Y-RAPPORT.md` | Skjelett — fylles ut etter kjøring |
### Screenshot-suite (v1.9.0)
`scripts/screenshots/capture-playground.py` automatiserer screenshots av alle 4 surfaces + 3 rapport-arketyper via Playwright. Output går til `playground/screenshots/` og embeddes i plugin README. Krever Playwright Python-pakke + chromium. Kjøres med `python3 scripts/screenshots/capture-playground.py` — ca. 15-20 sekunder. Demo-state seedes inline (Statens vegvesen ANPR-eksempel matcher fixture-filene), så ingen interaktivt brukerflow er nødvendig.
Kjør på nytt etter design-endringer i playground eller `shared/playground-design-system/` (etter sync).
### Vendored design-system
Playground laster CSS fra `playground/vendor/playground-design-system/` — en vendored

View file

@ -6,7 +6,7 @@
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
![Version](https://img.shields.io/badge/version-1.8.0-blue)
![Version](https://img.shields.io/badge/version-1.9.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Docs](https://img.shields.io/badge/reference_docs-387-green)
![Agents](https://img.shields.io/badge/agents-12-orange)
@ -356,7 +356,7 @@ Interactive **decision-builder + report viewer** for Microsoft AI architecture d
- **4 surfaces:** Onboarding (18 shared fields) → Home (project list + 3 entry tracks) → Catalog (24 commands grouped by 5 expansion categories with search) → Project (per-project tabs, command form prefill, paste-back report import + visualization)
- **Persistent state:** IndexedDB primary store with localStorage fallback. Schema-versioned (`STATE_KEY = 'ms-ai-architect-state-v1'`) with eager `MIGRATIONS` pipeline.
- **17 report renderers:** Each report-producing command has a parser (markdown → structured) and renderer (structured → HTML visualization: pyramid, matrix, radar, findings, distribution, capability-matrix, etc.) wired through a canonical archetype-routing table.
- **Theme:** Dark default + light mode toggle, persisted in `localStorage('ms-ai-architect-theme')`.
- **Theme:** Dark default + light mode toggle, persisted in `localStorage('ms-ai-architect-theme')`. Light-mode tokens are pending in the shared design-system; the toggle works but currently only swaps the label.
- **Export/import:** JSON Decision Record envelope (Blob + FileReader), schema-version-aware on import.
```bash
@ -364,6 +364,19 @@ Interactive **decision-builder + report viewer** for Microsoft AI architecture d
open plugins/ms-ai-architect/playground/ms-ai-architect-playground.html
```
### Screenshots
| Surface | Preview | What you see |
|---------|---------|--------------|
| Onboarding (18 shared fields, 5 grouped expansions) | ![Onboarding](playground/screenshots/01-onboarding.png) | Sidebar with form-progress per group · all 5 groups collapsed when complete · "Lagre og fortsett" enabled when 18/18 filled |
| Home (project list + 3 entry tracks) | ![Home](playground/screenshots/02-home.png) | Greeting based on shared state · 3 entry-track cards · project cards with completion meter (0/17 reports) · sticky topbar with theme toggle |
| Catalog (24 commands across 5 expansion categories, full-text search) | ![Catalog](playground/screenshots/03-catalog.png) | Search input · 24 of 24 hits · expansion groups (Regulatorisk/Sikkerhet/Økonomi/Dokumentasjon/Verktøy) · expanded card with `RAPPORT` pill, description, argument hints, and "Åpne skjema" |
| Project — EU AI Act classify report (pyramid renderer) | ![Classify pyramid](playground/screenshots/04-project-classify-pyramide.png) | 4-tier risk pyramid with active tier (`aria-current="true"`) · role + justification · obligations bullet list per Art. 9-49 |
| Project — ROS analysis (5×5 matrix + 7-dimension radar) | ![ROS matrix](playground/screenshots/05-project-ros-matrix.png) | 5×5 risk matrix · color-banded cells with threat-bubbles per consequence × likelihood · radar overlay covering 7 ROS-AI dimensions (Etterlevelse/Konfidensialitet/Integritet/Sporbarhet/Pålitelighet/Robusthet) |
| Project — Cost report (P10/P50/P90 distribution) | ![Cost distribution](playground/screenshots/06-project-cost-distribution.png) | P10/P50/P90 bars · per-component NOK breakdown table · 3-year capex/opex/total/akkumulert ledger |
Reproduce: `python3 scripts/screenshots/capture-playground.py` (Playwright + chromium required).
### Validation
| Test | Command | Coverage |

View file

@ -36,6 +36,11 @@
Kompakt med vilje — ingen komponent-CSS skal duplikeres her. -->
<style>
main#app { min-height: 100vh; padding: 0; }
/* Hidden-attribute respekt. Vendored .error-summary, .modal-backdrop osv.
setter eksplisitt display, som overstyrer HTMLs default [hidden] {display:none}.
Globalt override slik at hidden-attributt faktisk skjuler elementet. */
[hidden] { display: none !important; }
.app-shell { max-width: 1200px; margin: 0 auto; padding: var(--space-6) var(--space-5); }
.app-shell--wide { max-width: 1400px; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -0,0 +1,360 @@
#!/usr/bin/env python3
"""
capture-playground.py Tar screenshots av Playground v3 i alle 4 surfaces × 2 themes.
Bruker Playwright Python (sync API). Åpner playground/ms-ai-architect-playground.html
direkte fra disk via file:// URL og populerer state programmatisk via window.__store
før hvert screenshot. Dette gir reproduserbare screenshots uten å klikke gjennom hele
brukerflyten manuelt.
Output: playground/screenshots/*.png
Kjøring:
python3 scripts/screenshots/capture-playground.py
Krav: Playwright + chromium installert. macOS:
pip3 install playwright
playwright install chromium
"""
import json
import sys
from pathlib import Path
from playwright.sync_api import sync_playwright, Page
PLUGIN_ROOT = Path(__file__).resolve().parent.parent.parent
HTML_PATH = PLUGIN_ROOT / "playground" / "ms-ai-architect-playground.html"
FIXTURES_DIR = PLUGIN_ROOT / "playground" / "test-fixtures"
SCREENSHOTS_DIR = PLUGIN_ROOT / "playground" / "screenshots"
VIEWPORT = {"width": 1440, "height": 900}
TALL_VIEWPORT = {"width": 1440, "height": 1200} # for surfaces med mer innhold
# Demo-data — Statens vegvesen ANPR-eksempelet (matcher fixtures).
DEMO_SHARED = {
"organization": {
"name": "Statens vegvesen",
"description": "Trafikketat ansvarlig for vegnett, kjøretøy og sjåføropplæring",
"sector": "Statlig",
"size": "2000-10000",
"regulatory_requirements": [
"Personopplysningsloven/GDPR",
"Sikkerhetsloven",
"Forvaltningsloven",
"Offentleglova",
],
},
"technology": {
"cloud_platform": ["Azure", "M365"],
"license_type": "E5",
"ai_services_in_use": ["Azure OpenAI", "Copilot for M365", "Azure AI Search"],
},
"security": {
"data_classification": ["Intern", "Fortrolig"],
"data_residency": "Norge",
"dpia_practice": "Systematisk",
"certifications": "ISO 27001, ISO/IEC 42001 (under etablering)",
},
"architecture": {
"preferred_platform": "Azure AI Foundry",
"integration_needs": ["M365", "SharePoint", "Fagsystemer", "REST API-er"],
"annual_ai_budget": "2M-10M",
},
"business": {
"governance_model": "Sentralisert",
"doc_format_preferences": ["Markdown", "SharePoint Wiki"],
"reference_architecture": "TOGAF + intern AI-styringsmodell",
},
}
DEMO_PROJECTS = [
{
"id": "proj-anpr-2026",
"name": "ANPR Trafikkanalyse",
"description": "Automatisk skiltgjenkjenning for trafikkflyt-analyse på E18.",
"createdAt": "2026-04-15T09:00:00Z",
"local": {
"system_name": "ANPR Trafikkanalyse",
"system_description": "Sanntids skiltgjenkjenning av kjøretøy på utvalgte strekninger på E18 for trafikkflyt-analyse.",
"interaction_type": "automatisering",
"users": "Vegtrafikkavdelingen + analytikere",
"risk_level_assumption": "Høy",
"risk_classification": "Høy",
"org_role": "Provider",
"data_sources": "Kameraer langs E18, kjøretøyregisteret (begrenset), værdata fra met.no",
},
"reports": {},
},
{
"id": "proj-saksbehandler-2026",
"name": "Saksbehandlerassistent",
"description": "Copilot-basert assistent for førerkortssøknader og kjøretøy-registrering.",
"createdAt": "2026-04-22T10:30:00Z",
"local": {
"system_name": "Saksbehandlerassistent v1",
"system_description": "M365 Copilot Studio-agent som hjelper saksbehandlere med førerkort-søknader, henter relevant lovverk og foreslår sakssvar.",
"interaction_type": "beslutningsstøtte",
"users": "Saksbehandlere ved 22 trafikkstasjoner",
"risk_level_assumption": "Begrenset",
"risk_classification": "Begrenset",
"org_role": "Deployer",
},
"reports": {},
},
{
"id": "proj-chatbot-2026",
"name": "Brukerstøtte chatbot",
"description": "Publikumsrettet chatbot på vegvesen.no for førerkort- og kjøretøyspørsmål.",
"createdAt": "2026-05-01T13:15:00Z",
"local": {},
"reports": {},
},
]
def read_fixture(name: str) -> str:
"""Les en av de 17 ANPR-fixture-filene."""
return (FIXTURES_DIR / f"{name}.md").read_text(encoding="utf-8")
def seed_state(page: Page, *, projects=True):
"""Populerer __store.state med demo-data og trigger render."""
payload = {
"shared": DEMO_SHARED,
"projects": DEMO_PROJECTS if projects else [],
}
page.evaluate(
"""({shared, projects}) => {
for (const k of Object.keys(shared)) {
const target = window.__store.state.shared[k];
for (const f of Object.keys(shared[k])) target[f] = shared[k][f];
}
if (projects.length > 0) {
window.__store.state.projects.length = 0;
for (const p of projects) window.__store.state.projects.push(p);
}
window.__scheduleRender();
}""",
payload,
)
page.wait_for_timeout(200)
def import_reports(page: Page, project_id: str, command_ids: list):
"""Kaller __handlePasteImport på et aktivt prosjekt etter at project-surface
er rendret. kalles ETTER navigate('project') siden handlePasteImport
rendrer direkte til DOM-slottet og scheduleRender ville slette det."""
page.evaluate(
"""({pid}) => { window.__store.state.activeProjectId = pid; }""",
{"pid": project_id},
)
for cmd in command_ids:
markdown = read_fixture(cmd)
page.evaluate(
"""({cmd, md}) => { window.__handlePasteImport(cmd, md); }""",
{"cmd": cmd, "md": markdown},
)
page.wait_for_timeout(150)
def navigate(page: Page, surface: str, project_id: str = None, project_tab: str = None):
"""Bytter aktiv surface (og evt. aktivt prosjekt + tab) og trigger render."""
page.evaluate(
"""({surface, pid}) => {
if (pid) window.__store.state.activeProjectId = pid;
window.__navigate(surface);
}""",
{"surface": surface, "pid": project_id},
)
page.wait_for_timeout(250)
# Project-tab er module-local (currentProjectTab) — må klikkes via faktisk knapp
if surface == "project" and project_tab:
page.evaluate(
"""(tab) => {
const btn = document.querySelector('[data-action=\"project-tab\"][data-tab=\"' + tab + '\"]');
if (btn) btn.click();
}""",
project_tab,
)
page.wait_for_timeout(200)
def set_theme(page: Page, theme: str):
"""Setter tema (light/dark) før screenshot."""
page.evaluate(
"""(theme) => {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('ms-ai-architect-theme', theme); } catch(e){}
// Re-render topbar slik at theme-toggle-label oppdateres
const labels = document.querySelectorAll('[data-theme-label]');
for (const l of labels) l.textContent = theme === 'dark' ? 'Mørk' : 'Lys';
}""",
theme,
)
page.wait_for_timeout(100)
def shoot(page: Page, name: str, *, full_page: bool = False, scroll_to: str = None):
"""Lagrer en screenshot. Default: viewport-only.
full_page=True: hele scroll-høyden
scroll_to: CSS-selektor som scrolles inn i view før shot
"""
out = SCREENSHOTS_DIR / f"{name}.png"
if scroll_to:
page.evaluate(
"""(sel) => {
const el = document.querySelector(sel);
if (el) el.scrollIntoView({block: 'start', behavior: 'instant'});
window.scrollBy(0, -20);
}""",
scroll_to,
)
page.wait_for_timeout(120)
page.screenshot(path=str(out), full_page=full_page)
print(f" [{name}.png] {out.relative_to(PLUGIN_ROOT)}")
def open_clean(browser, viewport=None):
"""Åpner playground med ren state og venter på bootstrap."""
context = browser.new_context(viewport=viewport or VIEWPORT)
page = context.new_page()
# Hopp til about:blank først for å rense localStorage/IDB fra forrige
# kontekst-kjøring (file:// kan dele storage på samme browser-instans).
page.goto("about:blank")
page.evaluate(
"""async () => {
try { localStorage.clear(); } catch(e){}
try { sessionStorage.clear(); } catch(e){}
if (typeof indexedDB !== 'undefined' && indexedDB.databases) {
try {
const dbs = await indexedDB.databases();
await Promise.all(dbs.map(d => new Promise(r => {
const req = indexedDB.deleteDatabase(d.name); req.onsuccess = req.onerror = req.onblocked = r;
})));
} catch(e){}
}
}"""
)
page.goto(f"file://{HTML_PATH}")
# Vent til __store er eksponert (bootstrap fullført)
page.wait_for_function("() => window.__store && window.__CATALOG", timeout=10_000)
# Skjul evt. error-banner som kan dukke opp fra bootstrap-kanter
page.evaluate(
"""() => {
const errs = document.querySelectorAll('[data-onboarding-errors]');
for (const e of errs) e.setAttribute('hidden', '');
}"""
)
return context, page
def main():
if not HTML_PATH.exists():
print(f"FEIL: {HTML_PATH} mangler", file=sys.stderr)
sys.exit(1)
SCREENSHOTS_DIR.mkdir(parents=True, exist_ok=True)
print(f"Lagrer screenshots til: {SCREENSHOTS_DIR.relative_to(PLUGIN_ROOT)}")
print()
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=True)
# NB: light-mode tokens er ikke implementert i det vendrede
# design-systemet ennå (kun mørk-tema-vars). Theme-toggle bytter
# data-theme + label korrekt, men CSS-fargene endres ikke før
# tokens.css får [data-theme="light"]-overrides. Vi capture'r kun
# mørk-modus for nå. Når light-tokens kommer, endre listen til
# ("dark", "light") for å regenerere parsuffix-screenshots.
for theme in ("dark",):
suffix = "" if theme == "dark" else "-light"
print(f"--- Theme: {theme} ---")
# 1. Onboarding (utfylt) — TALL viewport for å vise sidebar + alle 5 grupper
ctx, page = open_clean(browser, viewport=TALL_VIEWPORT)
seed_state(page, projects=False)
navigate(page, "onboarding")
set_theme(page, theme)
page.evaluate(
"""() => {
const errs = document.querySelectorAll('[data-onboarding-errors]');
for (const e of errs) e.setAttribute('hidden', '');
}"""
)
page.wait_for_timeout(150)
shoot(page, f"01-onboarding{suffix}")
ctx.close()
# 2. Home (med 3 prosjekter)
ctx, page = open_clean(browser)
seed_state(page)
navigate(page, "home")
set_theme(page, theme)
page.wait_for_timeout(150)
shoot(page, f"02-home{suffix}")
ctx.close()
# 3. Catalog (alle grupper synlig) — utvid de første 2 gruppene
ctx, page = open_clean(browser)
seed_state(page)
navigate(page, "catalog")
set_theme(page, theme)
page.evaluate(
"""() => {
const exps = document.querySelectorAll('[data-action="catalog-toggle-group"]');
// Klikk de 2 første for å utvide
for (let i = 0; i < Math.min(2, exps.length); i++) exps[i].click();
}"""
)
page.wait_for_timeout(200)
shoot(page, f"03-catalog{suffix}")
ctx.close()
# 4. Project — classify pyramide (scroll til classify report-slot)
ctx, page = open_clean(browser)
seed_state(page)
navigate(page, "project", project_id="proj-anpr-2026", project_tab="regulatory")
set_theme(page, theme)
import_reports(page, "proj-anpr-2026", ["classify"])
shoot(
page,
f"04-project-classify-pyramide{suffix}",
scroll_to='[data-report-slot="classify"]',
)
ctx.close()
# 5. Project — ROS matrix
ctx, page = open_clean(browser)
seed_state(page)
navigate(page, "project", project_id="proj-anpr-2026", project_tab="security")
set_theme(page, theme)
import_reports(page, "proj-anpr-2026", ["ros"])
shoot(
page,
f"05-project-ros-matrix{suffix}",
scroll_to='[data-report-slot="ros"]',
)
ctx.close()
# 6. Project — Cost distribution
ctx, page = open_clean(browser)
seed_state(page)
navigate(page, "project", project_id="proj-anpr-2026", project_tab="economy")
set_theme(page, theme)
import_reports(page, "proj-anpr-2026", ["cost"])
shoot(
page,
f"06-project-cost-distribution{suffix}",
scroll_to='[data-report-slot="cost"]',
)
ctx.close()
browser.close()
print()
print(f"Ferdig. Screenshots i {SCREENSHOTS_DIR.relative_to(PLUGIN_ROOT)}/")
if __name__ == "__main__":
main()