From 2ed1b7954ab2167c02fc2775403fd46d59e9e33c Mon Sep 17 00:00:00 2001 From: user Date: Tue, 21 Jul 2026 18:20:10 +0200 Subject: [PATCH] feat(L08): add Docker fundamentals card --- .gitea/workflows/render-pdf.yml | 63 + .gitignore | 30 + Makefile | 11 + README.md | 24 + answers/docker-evidence.md | 8 + doc/assets/docker-model.png | Bin 0 -> 94925 bytes doc/assets/docker-model.svg | 26 + doc/generated/main.tex | 356 +++ doc/main.tex | 7 + doc/pdf/.gitkeep | 1 + json/card_source.json | 125 ++ lesson-flow.yaml | 37 + package-lock.json | 811 +++++++ package.json | 16 + scripts/card_dev.mjs | 75 + scripts/card_server.mjs | 918 ++++++++ scripts/export_lesson_report.mjs | 107 + scripts/lib/card_control.mjs | 376 ++++ scripts/lib/card_progress.mjs | 193 ++ scripts/lib/card_state_db.mjs | 161 ++ scripts/lib/checkpoint_controller.mjs | 423 ++++ scripts/lib/nvim_ui_bridge.mjs | 447 ++++ scripts/render_card_layouts.sh | 20 + scripts/render_pdf.sh | 85 + src/fixtures/container_lab.py | 68 + src/tasks/task01_build_image_spec.sh | 9 + src/tasks/task02_container_lifecycle.sh | 9 + src/tasks/task03_volume_and_bind.sh | 12 + stem-card.yaml | 9 + tests/card_contract.test.mjs | 25 + tests/card_state_db.test.mjs | 67 + tests/test_tasks.sh | 5 + tools/card-action.sh | 12 + web/app.css | 1 + web/app.js | 61 + web/card-data.json | 2738 +++++++++++++++++++++++ web/figures/docker-model.svg | 26 + web/index.html | 15 + web/style.css | 42 + 39 files changed, 7419 insertions(+) create mode 100644 .gitea/workflows/render-pdf.yml create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 answers/docker-evidence.md create mode 100644 doc/assets/docker-model.png create mode 100644 doc/assets/docker-model.svg create mode 100644 doc/generated/main.tex create mode 100644 doc/main.tex create mode 100644 doc/pdf/.gitkeep create mode 100644 json/card_source.json create mode 100644 lesson-flow.yaml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/card_dev.mjs create mode 100644 scripts/card_server.mjs create mode 100644 scripts/export_lesson_report.mjs create mode 100644 scripts/lib/card_control.mjs create mode 100644 scripts/lib/card_progress.mjs create mode 100644 scripts/lib/card_state_db.mjs create mode 100644 scripts/lib/checkpoint_controller.mjs create mode 100644 scripts/lib/nvim_ui_bridge.mjs create mode 100755 scripts/render_card_layouts.sh create mode 100755 scripts/render_pdf.sh create mode 100755 src/fixtures/container_lab.py create mode 100755 src/tasks/task01_build_image_spec.sh create mode 100755 src/tasks/task02_container_lifecycle.sh create mode 100755 src/tasks/task03_volume_and_bind.sh create mode 100644 stem-card.yaml create mode 100644 tests/card_contract.test.mjs create mode 100644 tests/card_state_db.test.mjs create mode 100755 tests/test_tasks.sh create mode 100755 tools/card-action.sh create mode 100644 web/app.css create mode 100644 web/app.js create mode 100644 web/card-data.json create mode 100644 web/figures/docker-model.svg create mode 100644 web/index.html create mode 100644 web/style.css diff --git a/.gitea/workflows/render-pdf.yml b/.gitea/workflows/render-pdf.yml new file mode 100644 index 0000000..2362972 --- /dev/null +++ b/.gitea/workflows/render-pdf.yml @@ -0,0 +1,63 @@ +name: Render PDF + +on: + push: + branches: + - deploy + workflow_dispatch: + +jobs: + render-pdf: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install TeX + run: | + if ! command -v latexmk >/dev/null 2>&1; then + if ! command -v apt-get >/dev/null 2>&1; then + echo "latexmk is missing and apt-get is unavailable" >&2 + exit 1 + fi + if command -v sudo >/dev/null 2>&1; then + SUDO=sudo + else + SUDO= + fi + $SUDO apt-get update + $SUDO apt-get install -y --no-install-recommends \ + latexmk \ + texlive-fonts-recommended \ + texlive-lang-polish \ + texlive-latex-base \ + texlive-latex-extra \ + texlive-latex-recommended + fi + + - name: Render PDF with commit metadata + run: ./scripts/render_pdf.sh + + - name: Commit rendered PDF + run: | + set -eu + repo_name="${GITHUB_REPOSITORY#*/}" + pdf_file="$(find doc/pdf -maxdepth 1 -type f -name '*.pdf' -print -quit)" + if [ -z "$pdf_file" ]; then + echo "rendered PDF is missing" >&2 + exit 1 + fi + remote_sha="$(git ls-remote origin refs/heads/deploy | cut -f1)" + if [ "$remote_sha" != "$GITHUB_SHA" ]; then + echo "deploy advanced from $GITHUB_SHA to $remote_sha; skip publishing stale PDF" + exit 0 + fi + git config user.name "gitea-actions" + git config user.email "gitea-actions@noreply.local" + printf '%s\n' "$GITHUB_SHA" > doc/pdf/source-commit.txt + git add -u doc/pdf + git add -f "$pdf_file" + git add doc/pdf/source-commit.txt + git diff --cached --quiet && exit 0 + git commit -m "Render ${repo_name} PDF for ${GITHUB_SHA::12} [skip actions]" + git push origin HEAD:deploy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c13e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +*.aux +*.log +*.out +*.fls +*.fdb_latexmk +*.synctex.gz +*.upa +*.upb +*.pdf +build-meta.tex +.rv/ +.stem/ +.nvim/ +.vim/ +.build/ +.gdb_history +host-build/ +build/*/prog.bin +build/*/prog.elf +build/*/prog.lst +build/*/prog.map +cmake-build-*/ +node_modules/ +__pycache__/ +*.pyc +vendor/Hazard3/ +vendor/lab-runtime/ +web/*.map +doc/session-reports/ +json/lesson_progress.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..db5e4b4 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +TASK ?= task01_build_image_spec +.PHONY: build test run trace clean +build: + @tools/card-action.sh build +test: + @STEM_TASK="$(TASK)" tools/card-action.sh test +run: test +trace: + @STEM_TASK="$(TASK)" tools/card-action.sh debug +clean: + @find .stem/artifacts -type f -name 'trace.log' -delete 2>/dev/null || true diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d1f6c8 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# L08 · Docker Fundamentals + +Karta uczy modelu image/container oraz cyklu `build`, `run`, `inspect`, `stop`, +`rm`, volume i bind mount. Profil `native-amd64` celowo nie otrzymuje socketu +Docker/Podman, dlatego automatyczne testy używają jawnego `container_lab.py` — +deterministycznego modelu stanów offline. Komunikat PASS nie oznacza wykonania +prawdziwego kontenera. + +| Task | Dowód PASS | +| --- | --- | +| `task01_build_image_spec` | poprawny Dockerfile, 4 instrukcje, stabilny digest | +| `task02_container_lifecycle` | created/running → exited → removed | +| `task03_volume_and_bind` | dane volume i bind przeżywają usunięcie kontenera | + +```bash +stemctl card use console-bash L08 +stemctl test native-amd64 console-bash L08 1 +stemctl test native-amd64 console-bash L08 2 +stemctl debug native-amd64 console-bash L08 3 +``` + +Na maszynie operatorskiej z rootless Podmanem/Dockerem uczeń powtarza te same +etapy prawdziwym CLI. Socket silnika nie może być montowany do profilu ucznia. +Wyniki wpisz do [answers/docker-evidence.md](answers/docker-evidence.md). diff --git a/answers/docker-evidence.md b/answers/docker-evidence.md new file mode 100644 index 0000000..723e946 --- /dev/null +++ b/answers/docker-evidence.md @@ -0,0 +1,8 @@ +# L08 · Docker Fundamentals — evidence + +- Task01: instrukcje Dockerfile, tag, digest i tryb silnika: +- Task02: stany przed/po stop oraz po rm: +- Task03: dane volume/bind po rm kontenera i po rm volume: +- Ścieżka `trace.log`: + +Wyjaśnij różnicę image/container i volume/bind. Zaznacz, że PASS pochodzi z modelu offline, nie prawdziwego silnika. diff --git a/doc/assets/docker-model.png b/doc/assets/docker-model.png new file mode 100644 index 0000000000000000000000000000000000000000..fc449f3492369f963743c0a4628aa336b2168446 GIT binary patch literal 94925 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≈G#%0Q#>1OY$XKS&0hy0R}CPc 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 literal 0 HcmV?d00001 diff --git a/doc/assets/docker-model.svg b/doc/assets/docker-model.svg new file mode 100644 index 0000000..a4ec188 --- /dev/null +++ b/doc/assets/docker-model.svg @@ -0,0 +1,26 @@ + + Model obrazu, kontenera i danych + Dockerfile buduje niezmienny obraz. Obraz uruchamia kontener, który przechodzi przez stany running, exited i removed. Volume i bind mount żyją niezależnie od kontenera. + + + + + + + Dockerfilekontekst build + build + IMAGEtag + digest + run + CONTAINERrunning + stop + exited + rm + + VOLUMEzarządza engine + BIND MOUNTścieżka hosta + + Usunięcie konteneranie usuwa danych. + + diff --git a/doc/generated/main.tex b/doc/generated/main.tex new file mode 100644 index 0000000..ff2f5bc --- /dev/null +++ b/doc/generated/main.tex @@ -0,0 +1,356 @@ +% Generated from json/card_source.json by tools/render_card.py. +% Do not edit manually; update the JSON and regenerate. + +\documentclass[12pt,a4paper]{article} + +\usepackage[T1]{fontenc} +\usepackage{lmodern} +\usepackage[utf8]{inputenc} +\usepackage[polish]{babel} +\usepackage{csquotes} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{xcolor} +\usepackage[a4paper,left=2.15cm,right=2.15cm,top=0.5cm,bottom=1cm,headheight=28mm,headsep=3mm,includehead,includefoot]{geometry} +\usepackage{eso-pic} +\usepackage{marginnote} +\usepackage{array} +\usepackage{tabularx} +\usepackage{graphicx} +\usepackage{pdflscape} +\usepackage{float} +\usepackage{silence} +\WarningFilter{soulutf8}{This package is obsolete} +\usepackage{pdfcomment} +\usepackage{enumitem} +\usepackage{needspace} +\usepackage{fancyhdr} +\usepackage{lastpage} +\usepackage{microtype} +\usepackage[most]{tcolorbox} +\usepackage{qrcode} +\IfFileExists{references.bib}{% + \usepackage[backend=biber,style=numeric,sorting=none]{biblatex}% + \addbibresource{references.bib}% +}{} + +\hypersetup{hidelinks} + +\IfFileExists{build-meta.tex}{% + \input{build-meta.tex}% +}{% + \newcommand{\BuildCommit}{lokalna}% +} + +\newcommand{\DocumentAuthor}{M. Pabiszczak} +\newcommand{\DocumentYear}{2026} +\newcommand{\CardSeries}{console-bash} +\newcommand{\CardSeriesTitle}{Bash · Console Tools} +\newcommand{\CardNumber}{08} +\newcommand{\CardCount}{08} +\newcommand{\CardSlug}{docker} +\newcommand{\CardVersion}{v00.01} +\newcommand{\CardStatus}{Gotowa do wykonania} +\newcommand{\DocumentUUID}{b6d3b00d-6912-502d-95d7-3ee47c60f1db} + +\newcommand{\EmptyCheck}{\(\square\)} +\newcommand{\EscAnswerLines}[1][3]{\par\noindent\dotfill\par\noindent\dotfill\par\noindent\dotfill} +\newcommand{\EscWriteRow}[1][2.8em]{\rule{0pt}{#1}} +\setlength{\parindent}{0pt} +\setlength{\parskip}{0.45em} +\setlength{\headheight}{28mm} +\setlength{\headsep}{3mm} +\setlength{\footskip}{8mm} +\setlength{\marginparwidth}{1.5cm} +\setlength{\marginparsep}{0.25cm} +\renewcommand{\arraystretch}{1.22} +% Margin separator lines disabled for layout trial. + +\newcommand{\ESCPageResourceHeader}{% + \begingroup% + \setlength{\parskip}{0pt}% + \setlength{\fboxsep}{0pt}% + \setlength{\fboxrule}{0.35pt}% + \setlength{\arrayrulewidth}{0.35pt}% + \noindent\fbox{% + \begin{minipage}[c][27.5mm][c]{\dimexpr\textwidth-2\fboxrule\relax}% + \begin{minipage}[c][27mm][c]{\dimexpr\linewidth-24mm-0.35pt\relax}% + \setlength{\tabcolsep}{0pt}% + \renewcommand{\arraystretch}{0}% + \begin{tabularx}{\linewidth}{@{}p{\dimexpr0.50000000\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.15126050\linewidth-\arrayrulewidth\relax}|X@{}}% + \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries TITLE}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{7.7}{7.9}\selectfont\ttfamily\bfseries L08 · Docker Fundamentals — image, container and storage}\hspace{0.45mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries VER.}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{6.6}{6.8}\selectfont\ttfamily\bfseries 1}\hspace{0.45mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries DATETIME}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{6.4}{6.6}\selectfont\ttfamily\bfseries 21.07.2026}\hspace{0.45mm}}\par} \\% + \end{tabularx}% + \par\nointerlineskip% + \hrule height0.35pt% + \nointerlineskip% + \begin{tabularx}{\linewidth}{@{}p{\dimexpr0.50000000\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.25210084\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.14285714\linewidth-\arrayrulewidth\relax}|X@{}}% + \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries PROJECT}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{7.7}{7.9}\selectfont\ttfamily\bfseries Warsztat terminalowy}\hspace{0.45mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries SERIES}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{6.4}{6.6}\selectfont\ttfamily\bfseries Bash · Console Tools}\hspace{0.45mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries CARD}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{6.4}{6.6}\selectfont\ttfamily\bfseries 08/08}\hspace{0.45mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{3.2}{3.4}\selectfont\ttfamily\bfseries SHEET}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{6.4}{6.6}\selectfont\ttfamily\bfseries \thepage/\pageref{LastPage}}\hspace{0.45mm}}\par} \\% + \end{tabularx}% + \par\nointerlineskip% + \hrule height0.35pt% + \nointerlineskip% + \begin{tabularx}{\linewidth}{@{}p{\dimexpr0.05882353\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.05042017\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.14705882\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.09243697\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.07563025\linewidth-\arrayrulewidth\relax}|p{\dimexpr0.07563025\linewidth-\arrayrulewidth\relax}|X@{}}% + \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.15mm}{\fontsize{2.85}{3}\selectfont\ttfamily\bfseries SUBJ.}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{4.45}{4.65}\selectfont\ttfamily\bfseries Inf.}\hspace{0.15mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.15mm}{\fontsize{2.85}{3}\selectfont\ttfamily\bfseries PROG.}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{4.45}{4.65}\selectfont\ttfamily\bfseries —}\hspace{0.15mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.15mm}{\fontsize{2.85}{3}\selectfont\ttfamily\bfseries CORE}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{4.45}{4.65}\selectfont\ttfamily\bfseries —}\hspace{0.15mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.15mm}{\fontsize{2.85}{3}\selectfont\ttfamily\bfseries SCOPE}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{4.45}{4.65}\selectfont\ttfamily\bfseries —}\hspace{0.15mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.15mm}{\fontsize{2.85}{3}\selectfont\ttfamily\bfseries LEVEL}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{4.45}{4.65}\selectfont\ttfamily\bfseries —}\hspace{0.15mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hspace{0.15mm}{\fontsize{2.85}{3}\selectfont\ttfamily\bfseries POS.}\par\nointerlineskip\vspace{0.12mm}\noindent\makebox[\linewidth][r]{{\fontsize{4.45}{4.65}\selectfont\ttfamily\bfseries —}\hspace{0.15mm}}\par} & \parbox[c][6.11722mm][c]{\linewidth}{\hbox to\linewidth{\hspace{0.35mm}{\fontsize{3.4}{3.75}\selectfont\ttfamily\bfseries GITEA UUID}\hfill{\fontsize{3.4}{3.75}\selectfont\ttfamily b6d3b00d-6912-502d-95d7-3ee47c60f1db}\hspace{0.35mm}}\par\nointerlineskip\hbox to\linewidth{\hspace{0.35mm}{\fontsize{3.4}{3.75}\selectfont\ttfamily\bfseries CARD UUID} {\fontsize{3.4}{3.75}\selectfont\ttfamily b6d3b00d-6912-502d-95d7-3ee47c60f1db}\hfill{\fontsize{3.4}{3.75}\selectfont\ttfamily\bfseries AUTHOR M. Pabiszczak}\hspace{0.35mm}}} \\% + \end{tabularx}% + \par\nointerlineskip% + \hrule height0.35pt% + \nointerlineskip% + \begin{tabularx}{\linewidth}{@{}X@{}}% + \parbox[c][4.07815mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{4.5}{4.85}\selectfont\ttfamily\bfseries GITEA} {\fontsize{4.5}{4.85}\selectfont\ttfamily\href{https://gitea.local/edu-inf/lab-console-docker}{\nolinkurl{https://gitea.local/edu-inf/lab-console-docker}}}} \\% + \end{tabularx}% + \par\nointerlineskip% + \hrule height0.35pt% + \nointerlineskip% + \begin{tabularx}{\linewidth}{@{}X@{}}% + \parbox[c][4.07815mm][c]{\linewidth}{\hspace{0.45mm}{\fontsize{4.5}{4.85}\selectfont\ttfamily\bfseries HTML} {\fontsize{4.5}{4.85}\selectfont\ttfamily\href{http://localhost:8080}{\nolinkurl{http://localhost:8080}}}} \\% + \end{tabularx}% + \end{minipage}% + \vrule width0.35pt% + \begin{minipage}[c][27mm][c]{24mm}\centering% + {\tiny QR wyłączony}% + \end{minipage}% + \end{minipage}% + }% + \endgroup% +} + +\pagestyle{fancy} +\fancyhf{} +\fancyhead[C]{\ESCPageResourceHeader} +\fancyfoot[L]{{\fontsize{6.4}{7.0}\selectfont\ttfamily UUID \DocumentUUID}} +\fancyfoot[C]{{\fontsize{6.4}{7.0}\selectfont\ttfamily \DocumentAuthor}} +\fancyfoot[R]{{\fontsize{6.4}{7.0}\selectfont\ttfamily STRONA \thepage/\pageref{LastPage}}} +\renewcommand{\headrulewidth}{0pt} +\renewcommand{\footrulewidth}{0.35pt} + +\newcommand{\ESCMarginTag}[4]{% + \hspace*{#1}\textcolor{#2}{#3~#4}% +} +\newcommand{\ESCSectionBlockStart}{% + \Needspace{8\baselineskip}% + \par\vspace{0.35em}% + \noindent\textcolor{black!28}{\rule{\textwidth}{0.35pt}}% + \par\vspace{0.15em}% +} +\newcommand{\ESCSectionBlockEnd}{% + \par\vspace{0.45em}% +} +\newcommand{\ESCTinyStepSeparator}{% + \par\vspace{0.10em}% + \noindent{\color{black!18}\leaders\hbox{\rule{0.60em}{0.22pt}\hspace{0.38em}}\hfill\kern0pt}% + \par\vspace{0.10em}% +} +\newtcolorbox{ESCTaskFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black,boxsep=0pt,left=1.4mm,right=1.4mm,top=0.8mm,bottom=0.8mm,colbacktitle=black!7,coltitle=black,title={\ttfamily\bfseries TASK\quad #1}} +\newtcolorbox{ESCBlockFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!55,boxsep=0pt,left=1.0mm,right=1.0mm,top=0.6mm,bottom=0.6mm,colbacktitle=black!4,coltitle=black,title={\ttfamily\bfseries BLOCK\quad #1}} +\newtcolorbox{ESCStepFrame}[1]{enhanced,breakable,arc=0pt,boxrule=0.35pt,colback=white,colframe=black!28,boxsep=0pt,left=0.8mm,right=0.8mm,top=0.45mm,bottom=0.45mm,colbacktitle=black!2,coltitle=black,title={\ttfamily STEP\quad #1}} +\newtcolorbox{ESCConclusionFrame}{enhanced,breakable,arc=0pt,boxrule=0.45pt,colback=blue!2,colframe=blue!45!black,boxsep=0pt,left=1.2mm,right=1.2mm,top=0.7mm,bottom=0.7mm,colbacktitle=blue!7,coltitle=black,title={\ttfamily\bfseries WNIOSEK}} + +\newcommand{\ESCLearningTreeWidget}{% + \reversemarginpar + \marginnote[% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \begin{minipage}[t]{\marginparwidth}% + \raggedright + {\bfseries\textcolor{black!65}{TECH}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \hspace*{0.28em}\textcolor{orange!85!black}{EK}\textcolor{black!48}{}\par% + \hspace*{0.54em}\textcolor{blue!70!black}{KW}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% + ]{} + \normalmarginpar + + \marginnote{% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \hspace*{0.06cm}% + \begin{minipage}[t]{\dimexpr\marginparwidth-0.06cm\relax}% + \raggedright + {\bfseries\textcolor{black!65}{OG}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \hspace*{0.28em}\textcolor{green!50!black}{EN~~WO}\textcolor{black!48}{}\par% + \hspace*{0.54em}\textcolor{blue!70!black}{KW~~WS}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% + } + +} + +\begin{document} +\sloppy + +\vspace{1.0em} +\noindent{\Large\bfseries Cel karty}\par +\vspace{0.35em} +Uczeń rozróżnia image od container, przeprowadza cykl życia oraz wyjaśnia trwałość volume i bind mount. Test automatyczny działa jako jawny model offline i nie oznacza uruchomienia prawdziwego silnika. + +\vspace{0.8em} +\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}} +\vspace{0.7em} +\noindent{\Large\bfseries Zakres tasków}\par +\vspace{0.35em} + +\vspace{0.55em} +\small\begin{tabularx}{\textwidth}{@{}>{\ttfamily\raggedright\arraybackslash}p{1.10cm}>{\ttfamily\raggedright\arraybackslash}p{1.45cm}X>{\raggedright\arraybackslash}p{2.50cm}>{\raggedright\arraybackslash}p{1.75cm}>{\ttfamily\raggedright\arraybackslash}p{1.50cm}@{}} +\textbf{Krok} & \textbf{Numer tasku} & \textbf{Najważniejsza idea} & \textbf{Priorytet} & \textbf{Status} & \textbf{Version} \\ \hline +\texttt{8} & \texttt{Task01} & Dockerfile, build, tag i digest & obowiązkowe & opracowane & \texttt{v00.01} \\ \hline +\texttt{8} & \texttt{Task02} & running, exited, removed & obowiązkowe & opracowane & \texttt{v00.01} \\ \hline +\texttt{8} & \texttt{Task03} & volume i bind mount & obowiązkowe & opracowane & \texttt{v00.01} \\ \hline +\end{tabularx} +\normalsize + + +\vspace{0.8em} +\noindent\textcolor{black!25}{\rule{\textwidth}{0.35pt}} + +\clearpage + +\ESCSectionBlockStart +\section{Task01 · Zbuduj specyfikację image} +\reversemarginpar +\marginnote[% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \begin{minipage}[t]{\marginparwidth}% + \raggedright + {\bfseries\textcolor{black!65}{TECH}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% +]{}[-3.1em] +\normalmarginpar + +\marginnote{% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \hspace*{0.06cm}% + \begin{minipage}[t]{\dimexpr\marginparwidth-0.06cm\relax}% + \raggedright + {\bfseries\textcolor{black!65}{OG}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% +}[-3.1em] + + +\begingroup +\scriptsize\ttfamily\color{black!60}\sloppy +\noindent drzewka: \pdftooltip[width=\textwidth]{D1}{CB08.WE01.OG.IP.01 | WE 01: Image, container i storage. Bezpieczny model podstaw\textCR konteneryzacji. | EN IP CB08.01: Uczeń analizuje niezmienną specyfikację image. | KW IP\textCR CB08.01: Wyjaśnia różnicę między tagiem i digestem.} \pdftooltip[width=\textwidth]{D2}{CB08.WE01.TECH.I4.01 | WE 01: Image, container i storage. Bezpieczny model podstaw\textCR konteneryzacji. | EK INF04 CB08.01: Uczeń modeluje cykl życia i storage. | KW INF04 CB08.01:\textCR Potwierdza stany i niezależność danych.}\par +\vspace{0.10em}% +\noindent K1: Potwierdź składnię, 4 instrukcje, digest i offline-model.\quad D1\par +\par\vspace{0.18em}% +\endgroup + +Przygotuj minimalny Dockerfile, sprawdź cztery instrukcje i wylicz stabilny digest kontekstu. Tag jest nazwą, digest identyfikatorem treści. +\begin{figure}[H] + \centering + \includegraphics[width=0.72\linewidth]{\detokenize{../assets/docker-model.png}} + \caption{Image jest specyfikacją; kontener ma własny cykl życia, a dane mogą żyć niezależnie.} + \label{fig:docker-model} +\end{figure} +\ESCSectionBlockEnd + +\ESCSectionBlockStart +\section{Task02 · Cykl życia kontenera} +\reversemarginpar +\marginnote[% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \begin{minipage}[t]{\marginparwidth}% + \raggedright + {\bfseries\textcolor{black!65}{TECH}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% +]{}[-3.1em] +\normalmarginpar + +\marginnote{% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \hspace*{0.06cm}% + \begin{minipage}[t]{\dimexpr\marginparwidth-0.06cm\relax}% + \raggedright + {\bfseries\textcolor{black!65}{OG}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% +}[-3.1em] + + +\begingroup +\scriptsize\ttfamily\color{black!60}\sloppy +\noindent drzewka: \pdftooltip[width=\textwidth]{D1}{CB08.WE01.OG.IP.01 | WE 01: Image, container i storage. Bezpieczny model podstaw\textCR konteneryzacji. | EN IP CB08.01: Uczeń analizuje niezmienną specyfikację image. | KW IP\textCR CB08.01: Wyjaśnia różnicę między tagiem i digestem.} \pdftooltip[width=\textwidth]{D2}{CB08.WE01.TECH.I4.01 | WE 01: Image, container i storage. Bezpieczny model podstaw\textCR konteneryzacji. | EK INF04 CB08.01: Uczeń modeluje cykl życia i storage. | KW INF04 CB08.01:\textCR Potwierdza stany i niezależność danych.}\par +\vspace{0.10em}% +\noindent K1: Zmierz running, exited i removed.\quad D2\par +\par\vspace{0.18em}% +\endgroup + +Uruchom instancję image, odczytaj stan running, zatrzymaj ją do exited, a następnie usuń. Stop nie jest tym samym co rm. +\ESCSectionBlockEnd + +\ESCSectionBlockStart +\section{Task03 · Volume i bind mount} +\reversemarginpar +\marginnote[% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \begin{minipage}[t]{\marginparwidth}% + \raggedright + {\bfseries\textcolor{black!65}{TECH}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% +]{}[-3.1em] +\normalmarginpar + +\marginnote{% + \begin{minipage}{\marginparwidth}% + \raggedright + {\fontsize{3.55}{3.95}\selectfont\ttfamily + \hspace*{0.06cm}% + \begin{minipage}[t]{\dimexpr\marginparwidth-0.06cm\relax}% + \raggedright + {\bfseries\textcolor{black!65}{OG}\par}% + \vspace{0.08em}% + \hspace*{0.00em}\textcolor{red}{WE~01}\textcolor{black!48}{}\par% + \end{minipage}% + }% + \end{minipage}% +}[-3.1em] + + +\begingroup +\scriptsize\ttfamily\color{black!60}\sloppy +\noindent drzewka: \pdftooltip[width=\textwidth]{D1}{CB08.WE01.OG.IP.01 | WE 01: Image, container i storage. Bezpieczny model podstaw\textCR konteneryzacji. | EN IP CB08.01: Uczeń analizuje niezmienną specyfikację image. | KW IP\textCR CB08.01: Wyjaśnia różnicę między tagiem i digestem.} \pdftooltip[width=\textwidth]{D2}{CB08.WE01.TECH.I4.01 | WE 01: Image, container i storage. Bezpieczny model podstaw\textCR konteneryzacji. | EK INF04 CB08.01: Uczeń modeluje cykl życia i storage. | KW INF04 CB08.01:\textCR Potwierdza stany i niezależność danych.}\par +\vspace{0.10em}% +\noindent K1: Potwierdź trwałość i jawne usunięcie volume; zapisz trace.log.\quad D2\par +\par\vspace{0.18em}% +\endgroup + +Zapisz dane w nazwanym volume i ścieżce bind. Po usunięciu kontenera oba zasoby pozostają; volume usuwa się osobno, a bind pozostaje własnością hosta. +\ESCSectionBlockEnd + +\end{document} diff --git a/doc/main.tex b/doc/main.tex new file mode 100644 index 0000000..401169f --- /dev/null +++ b/doc/main.tex @@ -0,0 +1,7 @@ +\newcommand{\PublisherDomain}{mpabi} +\newcommand{\CardArea}{inf} +\newcommand{\CardSeries}{console-bash} +\newcommand{\CardNumber}{08} +\newcommand{\CardSlug}{docker} +\newcommand{\CardVersion}{v00.01} +\newcommand{\DocumentUUID}{b6d3b00d-6912-502d-95d7-3ee47c60f1db} diff --git a/doc/pdf/.gitkeep b/doc/pdf/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/doc/pdf/.gitkeep @@ -0,0 +1 @@ + diff --git a/json/card_source.json b/json/card_source.json new file mode 100644 index 0000000..13b510b --- /dev/null +++ b/json/card_source.json @@ -0,0 +1,125 @@ +{ + "$schema": "../../../../tools/card-layouts/schemas/card-source.schema.json", + "schema": "esc-card-source.v1", + "card": { + "id": "mpabi-inf-console-bash-08-docker", + "series": "console-bash", + "series_title": "Bash · Console Tools", + "number": "08", + "count": "08", + "slug": "docker", + "title": "L08 · Docker Fundamentals — image, container and storage", + "topic": "Obraz, cykl życia kontenera, volume i bind mount", + "project": "Warsztat terminalowy", + "subject": "Informatyka", + "level": "Rok 1 · Console", + "revision_date": "2026-07-21T00:00:00+02:00", + "status": "Gotowa do wykonania", + "version": "v00.01", + "uuid": "b6d3b00d-6912-502d-95d7-3ee47c60f1db", + "author": "M. Pabiszczak", + "year": "2026" + }, + "generated": {"tex": "doc/generated/main.tex", "html": "web/index.html", "html_css": "web/style.css", "react_app": true, "html_tree_inspector": false}, + "template": "templates/karta-klasyczna.json", + "title_block": { + "category": "KARTA PRACY · INFORMATYKA · CONSOLE", + "standard": "ISO 7200", + "prepared_by": "M. Pabiszczak", + "prepared_on": "21.07.2026", + "checked_by": "testy automatyczne", + "approved_by": "—", + "url": "http://localhost:8080", + "repository_url": "https://gitea.local/edu-inf/lab-console-docker", + "revision": "1", + "issued_on": "21.07.2026", + "series": "CONSOLE-BASH-08", + "document_type": "karta pracy", + "tool": "card-layouts", + "sheet": "1 / 1", + "show_qr": false, + "show_repository_qr": false, + "height_cm": 3, + "repeat_on_every_page": true, + "replace_front_matter": true + }, + "front_page_scope": { + "title": "Cel karty", + "content_tex": "Uczeń rozróżnia image od container, przeprowadza cykl życia oraz wyjaśnia trwałość volume i bind mount. Test automatyczny działa jako jawny model offline i nie oznacza uruchomienia prawdziwego silnika.", + "scope_title": "Zakres tasków", + "scope_table": { + "headers": ["Krok", "Numer tasku", "Najważniejsza idea", "Priorytet", "Status", "Version"], + "rows": [ + {"chapter": "8", "task": "Task01", "idea_tex": "Dockerfile, build, tag i digest", "priority": "obowiązkowe", "status": "ready", "version": "v00.01"}, + {"chapter": "8", "task": "Task02", "idea_tex": "running, exited, removed", "priority": "obowiązkowe", "status": "ready", "version": "v00.01"}, + {"chapter": "8", "task": "Task03", "idea_tex": "volume i bind mount", "priority": "obowiązkowe", "status": "ready", "version": "v00.01"} + ] + } + }, + "learning_effects": { + "CB08.EN01": {"text": "Uczeń analizuje relację Dockerfile, image, tagu i digestu.", "label": "Image specification", "bloom_level": "Analiza", "assessment_criteria": ["CB08.KW01"]}, + "CB08.EK01": {"text": "Uczeń przeprowadza cykl życia kontenera i rozróżnia mechanizmy trwałych danych.", "label": "Lifecycle and storage", "bloom_level": "Zastosowanie", "assessment_criteria": ["CB08.KW01"]} + }, + "assessment_criteria": { + "CB08.KW01": {"text": "Poprawna specyfikacja image, przejścia running–exited–removed oraz trwałość volume i bind są potwierdzone; tryb offline i brak socketu są jawne.", "learning_effects": ["CB08.EN01", "CB08.EK01"]} + }, + "educational_requirements": { + "CB08.WE01": { + "text": "Bezpieczny model podstaw konteneryzacji.", + "label": "Image, container i storage", + "learning_effects": ["CB08.EN01", "CB08.EK01"], + "learning_tree": { + "schema": "we-learning-tree.v1", + "policy": "Profil ucznia nie otrzymuje socketu engine. Automatyczny PASS pochodzi z audytowalnego modelu offline i nie jest przedstawiany jako rzeczywiste wykonanie Docker/Podman.", + "ogolne": [{"tree_id": "CB08.WE01.OG.IP.01", "effect_ref": "CB08.EN01", "display": "EN IP CB08.01", "text": "Uczeń analizuje niezmienną specyfikację image.", "kw": [{"criterion_ref": "CB08.KW01", "display": "KW IP CB08.01", "text": "Wyjaśnia różnicę między tagiem i digestem."}]}], + "zawodowe": [{"tree_id": "CB08.WE01.TECH.I4.01", "effect_ref": "CB08.EK01", "display": "EK INF04 CB08.01", "text": "Uczeń modeluje cykl życia i storage.", "kw": [{"criterion_ref": "CB08.KW01", "display": "KW INF04 CB08.01", "text": "Potwierdza stany i niezależność danych."}]}] + } + } + }, + "sections": [ + { + "title": "Task01 · Zbuduj specyfikację image", + "content_kind": "prose", + "content_tex": "Przygotuj minimalny Dockerfile, sprawdź cztery instrukcje i wylicz stabilny digest kontekstu. Tag jest nazwą, digest identyfikatorem treści.", + "educational_requirement_refs": ["CB08.WE01"], "learning_effect_refs": ["CB08.EN01", "CB08.EK01"], "assessment_criterion_refs": ["CB08.KW01"], "area_tree_refs": ["CB08.WE01.OG.IP.01", "CB08.WE01.TECH.I4.01"], + "steps": [{"id": "T01-IMAGE", "title": "Potwierdź składnię, 4 instrukcje, digest i offline-model.", "tree_refs": ["CB08.WE01.OG.IP.01"]}], + "assets": [{"path": "assets/docker-model.png", "html_path": "assets/docker-model.svg", "caption": "Image jest specyfikacją; kontener ma własny cykl życia, a dane mogą żyć niezależnie.", "label": "fig:docker-model", "alt": "Diagram Dockerfile, image, container, volume i bind mount.", "kind": "diagram", "width": 0.72}] + }, + { + "title": "Task02 · Cykl życia kontenera", + "content_kind": "prose", + "content_tex": "Uruchom instancję image, odczytaj stan running, zatrzymaj ją do exited, a następnie usuń. Stop nie jest tym samym co rm.", + "educational_requirement_refs": ["CB08.WE01"], "learning_effect_refs": ["CB08.EN01", "CB08.EK01"], "assessment_criterion_refs": ["CB08.KW01"], "area_tree_refs": ["CB08.WE01.OG.IP.01", "CB08.WE01.TECH.I4.01"], + "steps": [{"id": "T02-LIFECYCLE", "title": "Zmierz running, exited i removed.", "tree_refs": ["CB08.WE01.TECH.I4.01"]}] + }, + { + "title": "Task03 · Volume i bind mount", + "content_kind": "prose", + "content_tex": "Zapisz dane w nazwanym volume i ścieżce bind. Po usunięciu kontenera oba zasoby pozostają; volume usuwa się osobno, a bind pozostaje własnością hosta.", + "educational_requirement_refs": ["CB08.WE01"], "learning_effect_refs": ["CB08.EN01", "CB08.EK01"], "assessment_criterion_refs": ["CB08.KW01"], "area_tree_refs": ["CB08.WE01.OG.IP.01", "CB08.WE01.TECH.I4.01"], + "steps": [{"id": "T03-STORAGE", "title": "Potwierdź trwałość i jawne usunięcie volume; zapisz trace.log.", "tree_refs": ["CB08.WE01.TECH.I4.01"]}] + } + ], + "tasks": { + "task01": {"prompt_tex": "Zbuduj specyfikację image z Dockerfile.", "criterion": "syntax=valid instructions=4 digest=sha256:* engine=offline-model socket_exposed=0", "assessment_criterion_ref": "CB08.KW01"}, + "task02": {"prompt_tex": "Przeprowadź cykl życia kontenera.", "criterion": "running=1 inspected=running stopped=exited removed=1 engine=offline-model", "assessment_criterion_ref": "CB08.KW01"}, + "task03": {"prompt_tex": "Porównaj trwałość volume i bind mount.", "criterion": "volume_persists_after_container_rm=1 bind_persists=1 volume_removed_explicitly=1", "assessment_criterion_ref": "CB08.KW01"} + }, + "tasks_order": ["task01", "task02", "task03"], + "mission": "Zrozumieć image, container i storage bez udostępniania uczniowi uprzywilejowanego socketu.", + "objectives": ["Wyjaśnisz tag i digest.", "Przeprowadzisz cykl życia kontenera.", "Rozróżnisz volume i bind mount."], + "agenda": [{"time": "0--20 min", "work": "Dockerfile i image."}, {"time": "20--40 min", "work": "Cykl życia."}, {"time": "40--65 min", "work": "Storage, granica bezpieczeństwa i trace."}], + "tech_stack": {"software": [{"name": "container_lab.py", "use": "jawny model stanów offline"}, {"name": "POSIX shell", "use": "izolowane scenariusze"}, {"name": "stemctl", "use": "profil native-amd64"}], "hardware": [{"name": "Brak socketu engine", "use": "celowa granica profilu ucznia"}]}, + "container_test": { + "title": "Test native-amd64", + "commands": ["stemctl test native-amd64 console-bash L08 1", "stemctl test native-amd64 console-bash L08 2", "stemctl debug native-amd64 console-bash L08 3"], + "pass_condition": "Trzy komunikaty PASS z engine=offline-model, socket_exposed=0 oraz trace.log; nie oznacza uruchomienia prawdziwego silnika." + }, + "safety_rules": [ + {"title": "Bez socketu silnika", "body": "Nie montuj /var/run/docker.sock ani socketu Podman do profilu ucznia."}, + {"title": "Uczciwy dowód", "body": "PASS modelu offline nie jest dowodem rzeczywistego wykonania Docker/Podman."}, + {"title": "Izolacja", "body": "Stan modelu i dane powstają wyłącznie w katalogu mktemp."} + ], + "hardware_procedure": [{"step": "1", "action": "Zbuduj specyfikację.", "condition": "4 instrukcje i stabilny digest."}, {"step": "2", "action": "Przeprowadź lifecycle.", "condition": "running, exited, removed."}, {"step": "3", "action": "Sprawdź storage.", "condition": "Dane przetrwały rm kontenera."}], + "references": [] +} diff --git a/lesson-flow.yaml b/lesson-flow.yaml new file mode 100644 index 0000000..e53da56 --- /dev/null +++ b/lesson-flow.yaml @@ -0,0 +1,37 @@ +schema: 1 +card: console-bash/L08 +title: Docker Fundamentals +steps: + - id: image + task: task01_build_image_spec + action: test + profile: native-amd64 + target: native + command: stemctl test native-amd64 console-bash L08 1 + board: + show: [build_context, Dockerfile, instructions, image_tag, digest, engine_mode] + highlight: [immutable_spec, content_digest, offline_model] + question: Dlaczego tag i digest nie są tym samym? + evidence: [syntax_valid, instruction_count, digest, engine_mode] + - id: lifecycle + task: task02_container_lifecycle + action: test + profile: native-amd64 + target: native + command: stemctl test native-amd64 console-bash L08 2 + board: + show: [image, container_name, running, inspect, exited, removed] + highlight: [image_vs_container, state_transition, explicit_remove] + question: Dlaczego stop nie usuwa kontenera? + evidence: [running_status, exited_status, removed, engine_mode] + - id: storage + task: task03_volume_and_bind + action: debug + profile: native-amd64 + target: native + command: stemctl debug native-amd64 console-bash L08 3 + board: + show: [container, named_volume, bind_path, remove_container, persistent_data, trace_log] + highlight: [lifecycle_independence, engine_managed, host_managed] + question: Kto zarządza ścieżką volume, a kto ścieżką bind mount? + evidence: [volume_persists, bind_persists, explicit_volume_remove, trace_log] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3d47428 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,811 @@ +{ + "name": "mpabi-inf-console-bash-08-docker", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mpabi-inf-console-bash-08-docker", + "dependencies": { + "@fastify/websocket": "^11.3.0", + "@msgpack/msgpack": "^3.1.3", + "fastify": "^5.6.0" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/websocket": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/@fastify/websocket/-/websocket-11.3.0.tgz", + "integrity": "sha512-g89ag4BCcD9YP5wBZXixzoLnuf5j89p/sXFcfpCiv2pdEkYYukBEoK3heVzqsp0EAtszVDc2BBZG0KZqeAShIA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.3", + "fastify-plugin": "^6.0.0", + "ws": "^8.16.0" + } + }, + "node_modules/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.0.tgz", + "integrity": "sha512-ZodJ2cRiLVWGi9IgPb3mbgSqM4CD3LexCHkuv0FfBXHJI1ADfucTD06m6clO2Cy5RZYsw/SiCVl/dyrFI/SYWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz", + "integrity": "sha512-A9L0ziuWGQHgEEVgF3davQ9vbD93IuX+lo2IsxapQmu5b/Y/ynn9m9K5JHt9dvyJXOFc5iN0Zk5GHEOqnzhWjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", + "integrity": "sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..eff5113 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "mpabi-inf-console-bash-08-docker", + "private": true, + "type": "module", + "scripts": { + "dev": "node scripts/card_dev.mjs", + "serve": "node scripts/card_server.mjs", + "report": "node scripts/export_lesson_report.mjs", + "test": "node --test tests/*.test.mjs" + }, + "dependencies": { + "@fastify/websocket": "^11.3.0", + "@msgpack/msgpack": "^3.1.3", + "fastify": "^5.6.0" + } +} diff --git a/scripts/card_dev.mjs b/scripts/card_dev.mjs new file mode 100644 index 0000000..324642f --- /dev/null +++ b/scripts/card_dev.mjs @@ -0,0 +1,75 @@ +import { watch } from 'node:fs'; +import { access } from 'node:fs/promises'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const repositoryRoot = path.resolve(process.cwd()); +const layoutsRoot = path.resolve(process.env.CARD_LAYOUTS_ROOT ?? path.join(repositoryRoot, '../../../tools/card-layouts')); +const renderScript = path.join(repositoryRoot, 'scripts/render_card_layouts.sh'); +const watched = [ + [path.join(repositoryRoot, 'json'), name => name === 'card_source.json'], + [path.join(repositoryRoot, 'doc/assets'), () => true], + [path.join(layoutsRoot, 'react/src'), name => /\.(tsx?|css)$/.test(name)], + [path.join(layoutsRoot, 'react'), name => /^(build\.mjs|tsconfig\.json)$/.test(name)], + [path.join(layoutsRoot, 'tools'), name => name === 'render_card.py'], + [path.join(layoutsRoot, 'schemas'), name => name === 'card-source.schema.json'] +]; + +let build = null; +let queued = false; +let debounce = null; + +function render() { + if (build) { + queued = true; + return; + } + process.stdout.write('[card-dev] generowanie karty…\n'); + build = spawn(renderScript, { + cwd: repositoryRoot, + env: { ...process.env, CARD_LAYOUTS_ROOT: layoutsRoot }, + stdio: 'inherit' + }); + build.once('exit', code => { + build = null; + process.stdout.write(code === 0 + ? '[card-dev] karta gotowa; przeglądarka odświeży się automatycznie.\n' + : `[card-dev] generator zakończył się kodem ${code}.\n`); + if (queued) { + queued = false; + render(); + } + }); +} + +function scheduleRender() { + clearTimeout(debounce); + debounce = setTimeout(render, 180); +} + +await access(renderScript); +for (const [directory, accepts] of watched) { + try { + await access(directory); + watch(directory, (_event, filename) => { + if (filename && accepts(String(filename))) scheduleRender(); + }); + process.stdout.write(`[card-dev] obserwuję ${directory}\n`); + } catch { + process.stderr.write(`[card-dev] pomijam brakujący katalog ${directory}\n`); + } +} + +const server = spawn(process.execPath, ['--watch', 'scripts/card_server.mjs'], { + cwd: repositoryRoot, + stdio: 'inherit' +}); +const stop = signal => { + if (build) build.kill(signal); + server.kill(signal); + process.exit(0); +}; +process.on('SIGINT', () => stop('SIGINT')); +process.on('SIGTERM', () => stop('SIGTERM')); + +render(); diff --git a/scripts/card_server.mjs b/scripts/card_server.mjs new file mode 100644 index 0000000..808d0c5 --- /dev/null +++ b/scripts/card_server.mjs @@ -0,0 +1,918 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { execFile } from 'node:child_process'; +import { createServer } from 'node:http'; +import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { promisify } from 'node:util'; +import Fastify from 'fastify'; +import websocket from '@fastify/websocket'; +import { + STATUSES, + catalogFromCard, + normaliseProgress, + progressSummary, + readJson, + statusLabel, + teamsMarkdown, + updateItem, + writeJsonAtomically +} from './lib/card_progress.mjs'; +import { + attachNvimUi, + callSelectedNvim, + resizeSelectedNvimUi, + selectedNvimTarget +} from './lib/nvim_ui_bridge.mjs'; +import { activateCheckpoint, checkpointStatus } from './lib/checkpoint_controller.mjs'; +import { CardStateDatabase } from './lib/card_state_db.mjs'; +import { + activateNavigation, + KEYBOARD_SHORTCUTS, + moveNavigation, + navigationCatalog, + normalizeViewerState, + patchViewerState, + selectNavigation +} from './lib/card_control.mjs'; + +const repositoryRoot = path.resolve(process.cwd()); +const cardFile = path.join(repositoryRoot, 'json/card_source.json'); +const progressFile = path.resolve(process.env.CARD_PROGRESS_FILE ?? path.join(repositoryRoot, 'json/lesson_progress.json')); +const stateDatabaseFile = path.resolve( + process.env.CARD_STATE_DATABASE ?? path.join(repositoryRoot, '.stem/card-state.sqlite3') +); +const apiSocket = path.resolve( + process.env.CARD_API_SOCKET ?? path.join(repositoryRoot, '.stem/card-api.sock') +); +const evidenceRoot = path.resolve( + process.env.CARD_EVIDENCE_ROOT ?? path.join(repositoryRoot, '.stem/evidence') +); +const webRoot = path.join(repositoryRoot, 'web'); +const host = process.env.CARD_HOST ?? '127.0.0.1'; +const port = Number(process.env.CARD_PORT ?? 8080); +const app = Fastify({ logger: true }); +const execFileAsync = promisify(execFile); +const stateDatabase = new CardStateDatabase(stateDatabaseFile); +await app.register(websocket, { + options: { + maxPayload: 64 * 1024 + } +}); +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.html': 'text/html; charset=utf-8', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.puml': 'text/plain; charset=utf-8', + '.svg': 'image/svg+xml; charset=utf-8', + '.webp': 'image/webp' +}; + +let writeQueue = Promise.resolve(); +let currentNavigation = stateDatabase.read('navigation'); +let navigationRevision = Number.isSafeInteger(Number(currentNavigation?.revision)) + ? Number(currentNavigation.revision) + : 0; +const persistedViewer = stateDatabase.read('viewer'); +let currentViewer = normalizeViewerState(persistedViewer); +if (persistedViewer && JSON.stringify(persistedViewer) !== JSON.stringify(currentViewer)) { + stateDatabase.write('viewer', currentViewer); +} +let viewerRevision = Number.isSafeInteger(Number(currentViewer?.revision)) + ? Number(currentViewer.revision) + : 0; + +function actor(value, fallback = 'system') { + return typeof value === 'string' && value.trim() + ? value.trim().slice(0, 160) + : fallback; +} + +async function zoomSelectedHostPane() { + const selected = await selectedNvimTarget(); + const instance = String(selected.metadata?.instance ?? ''); + const result = await execFileAsync('tmux', [ + 'list-panes', '-a', '-F', + '#{pane_id}\t#{@stem_instance}\t#{@stem_role}\t#{window_zoomed_flag}' + ]); + const panes = result.stdout.trim().split('\n').filter(Boolean).map(line => { + const [id, stemInstance, role, zoomed] = line.split('\t'); + return { id, stemInstance, role, zoomed }; + }); + const exact = panes.find(pane => pane.stemInstance === instance && pane.role === 'debugger'); + const debuggerPanes = panes.filter(pane => pane.role === 'debugger'); + const pane = exact ?? (debuggerPanes.length === 1 ? debuggerPanes[0] : null); + if (!pane) return { found: false, instance }; + if (pane.zoomed !== '1') { + await execFileAsync('tmux', ['resize-pane', '-Z', '-t', pane.id]); + } + const dimensions = await execFileAsync('tmux', [ + 'display-message', '-p', '-t', pane.id, + '#{pane_width}x#{pane_height}\t#{window_zoomed_flag}' + ]); + const [size, zoomed] = dimensions.stdout.trim().split('\t'); + return { found: true, pane: pane.id, instance, size, zoomed: zoomed === '1' }; +} + +function expectedNvimGrid(hostPane) { + const match = hostPane?.found && typeof hostPane.size === 'string' + ? /^(\d+)x(\d+)$/.exec(hostPane.size) + : null; + if (!match) return null; + return { + width: Number(match[1]), + // The nested tmux status line occupies one row below the Neovim pane. + height: Math.max(1, Number(match[2]) - 1) + }; +} + +async function settleSelectedNvimTerminalUi(hostPane) { + const target = expectedNvimGrid(hostPane); + let innerTmux = null; + try { + innerTmux = await callSelectedNvim('nvim_exec_lua', [` + if not vim.env.TMUX or vim.env.TMUX == '' then + return { attempted = false } + end + local result = vim.system( + { 'tmux', 'resize-window', '-A' }, + { text = true } + ):wait() + return { + attempted = true, + code = result.code, + stderr = vim.trim(result.stderr or '') + } + `, []]); + } catch (error) { + innerTmux = { attempted: true, error: error instanceof Error ? error.message : String(error) }; + } + + const deadline = Date.now() + 1800; + let terminal = null; + do { + const uis = await callSelectedNvim('nvim_list_uis', []); + terminal = Array.isArray(uis) + ? uis.find(ui => ui?.stdin_tty && ui?.stdout_tty) ?? null + : null; + if ( + terminal + && (!target || (terminal.width >= target.width && terminal.height >= target.height)) + ) break; + await new Promise(resolve => setTimeout(resolve, 80)); + } while (Date.now() < deadline); + + return { + target, + terminal: terminal + ? { width: terminal.width, height: terminal.height } + : null, + inner_tmux: innerTmux, + settled: Boolean( + terminal + && (!target || (terminal.width >= target.width && terminal.height >= target.height)) + ) + }; +} + +function audit(eventType, eventActor, payload = {}, options = {}) { + return stateDatabase.appendEvent({ + eventType, + actor: actor(eventActor), + navigation: options.navigation === undefined ? currentNavigation : options.navigation, + evidenceRef: options.evidenceRef ?? null, + payload + }); +} + +function html(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); +} + +function auditReportHtml(events) { + const rows = events.map(event => { + const position = [event.task_id, event.block_id, event.phase_id, event.step_id] + .filter(Boolean) + .join(' / '); + const evidence = event.evidence_ref + ? `

${html(event.payload?.caption ?? event.event_type)}
${html(event.payload?.caption ?? '')}
` + : ''; + return `
${html(event.event_type)}${html(event.actor)}

${html(position)}

${evidence}
`; + }).join('\n'); + return `Ślad lekcji

Chronologiczny ślad lekcji

Wpisy i timestampy pochodzą z backendu; zrzuty są dowodami przypiętymi do zdarzeń.

${rows}
`; +} + +async function restoreNavigation() { + if (!currentNavigation) return; + try { + const catalog = await currentNavigationCatalog(); + currentNavigation = selectNavigation( + catalog, + currentNavigation, + navigationRevision, + currentNavigation.selected_at + ); + } catch { + currentNavigation = null; + navigationRevision = 0; + } +} + +async function currentNavigationCatalog() { + return navigationCatalog(await readJson(cardFile)); +} + +async function currentCardIdentity() { + const source = await readFile(cardFile); + const card = JSON.parse(source.toString('utf8')).card ?? {}; + return { + schema: 'stem-card-identity.v1', + id: String(card.id ?? ''), + uuid: String(card.uuid ?? ''), + version: String(card.version ?? ''), + source_sha256: createHash('sha256').update(source).digest('hex') + }; +} + +async function selectedNvimState() { + return callSelectedNvim('nvim_exec_lua', [` + local win = vim.api.nvim_get_current_win() + local buf = vim.api.nvim_win_get_buf(win) + local cursor = vim.api.nvim_win_get_cursor(win) + local mode = vim.api.nvim_get_mode() + return { + window = win, + buffer = buf, + path = vim.api.nvim_buf_get_name(buf), + cursor = { row = cursor[1], column = cursor[2] }, + mode = mode.mode, + blocking = mode.blocking, + changedtick = vim.api.nvim_buf_get_changedtick(buf), + columns = vim.o.columns, + lines = vim.o.lines + } + `, []]); +} + +async function webRevision() { + const files = ['index.html', 'app.js', 'app.css', 'style.css', 'card-data.json']; + for (const directory of ['figures', 'debug-snapshots']) { + try { + for (const name of await readdir(path.join(webRoot, directory))) { + files.push(path.join(directory, name)); + } + } catch { + // A card without optional generated assets is valid. + } + } + const revisions = await Promise.all(files.sort().map(async name => { + const info = await stat(path.join(webRoot, name)); + return `${name}:${info.mtimeMs}:${info.size}`; + })); + return revisions.join('|'); +} + +async function cardAndProgress() { + const card = await readJson(cardFile); + let saved; + try { + saved = await readJson(progressFile); + } catch (error) { + if (error.code !== 'ENOENT') throw error; + saved = {}; + } + return { + card, + progress: normaliseProgress(card, saved, { + repository: process.env.CARD_SOURCE_REPOSITORY, + revision: process.env.CARD_SOURCE_REVISION + }) + }; +} + +async function updateProgress(itemId, change) { + const operation = writeQueue.then(async () => { + const { card, progress } = await cardAndProgress(); + updateItem(progress, itemId, change); + await writeJsonAtomically(progressFile, progress); + return { card, progress }; + }); + writeQueue = operation.catch(() => undefined); + return operation; +} + +function trackerAssets() { + const css = ``; + const script = ``; + return { css, script }; +} + +function injectTracker(html, revision) { + const usesReactRenderer = html.includes(''); + const { css, script } = usesReactRenderer ? { css: '', script: '' } : trackerAssets(); + const reload = ``; + return html + .replace('', `${css}`) + .replace('', `${script}${reload}`); +} + +app.get('/api/progress', async () => { + const { card, progress } = await cardAndProgress(); + return { + card: progress.card, + session: progress.session, + catalog: catalogFromCard(card), + progress: { ...progress, summary: progressSummary(progress) }, + statuses: Object.fromEntries(STATUSES.map(status => [status, statusLabel[status]])) + }; +}); + +app.put('/api/progress/:itemId', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + if (!STATUSES.includes(body.status)) { + return reply.code(400).send({ error: `status musi być jednym z: ${STATUSES.join(', ')}` }); + } + try { + const progressActor = actor(body.actor, 'teacher'); + const { progress } = await updateProgress(request.params.itemId, { + status: body.status, + note: body.note, + actor: progressActor + }); + audit('progress.status', progressActor, { + item_id: request.params.itemId, + status: body.status, + note: typeof body.note === 'string' ? body.note.slice(0, 2000) : null + }); + return { progress: { ...progress, summary: progressSummary(progress) } }; + } catch (error) { + return reply.code(error.message.startsWith('Nieznany element') ? 404 : 500).send({ error: error.message }); + } +}); + +app.get('/api/report/teams.md', async (_request, reply) => { + const { card, progress } = await cardAndProgress(); + return reply + .type('text/markdown; charset=utf-8') + .header('content-disposition', `inline; filename="${progress.session.id}-teams.md"`) + .send(teamsMarkdown(card, progress)); +}); + +app.get('/api/report/lesson.html', async (_request, reply) => reply + .type('text/html; charset=utf-8') + .send(auditReportHtml(stateDatabase.listEvents({ limit: 5000 })))); + +app.get('/api/events', async request => { + const query = request.query && typeof request.query === 'object' ? request.query : {}; + const afterId = Math.max(0, Number.isSafeInteger(Number(query.after_id)) ? Number(query.after_id) : 0); + const limit = Math.max(1, Math.min(1000, Number.isSafeInteger(Number(query.limit)) ? Number(query.limit) : 200)); + return { + schema: 'stem-card-event-log.v1', + events: stateDatabase.listEvents({ afterId, limit }) + }; +}); + +app.post('/api/events', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const eventType = typeof body.event_type === 'string' ? body.event_type.trim() : ''; + if (!/^[a-z][a-z0-9_.-]{1,79}$/.test(eventType)) { + return reply.code(400).send({ error: 'event_type ma niepoprawny format.' }); + } + const payload = body.payload && typeof body.payload === 'object' ? body.payload : {}; + if (JSON.stringify(payload).length > 16 * 1024) { + return reply.code(413).send({ error: 'payload zdarzenia jest zbyt duży.' }); + } + return { + schema: 'stem-card-event.v1', + event: audit(eventType, actor(body.actor, 'api'), payload) + }; +}); + +app.post('/api/evidence', { bodyLimit: 12 * 1024 * 1024 }, async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const match = typeof body.data_url === 'string' + ? /^data:image\/(png|jpeg|webp);base64,([A-Za-z0-9+/=]+)$/.exec(body.data_url) + : null; + if (!match) return reply.code(400).send({ error: 'Wymagany jest data_url obrazu PNG, JPEG albo WebP.' }); + const bytes = Buffer.from(match[2], 'base64'); + if (!bytes.length || bytes.length > 8 * 1024 * 1024) { + return reply.code(413).send({ error: 'Zrzut musi mieć od 1 B do 8 MiB.' }); + } + const extension = match[1] === 'jpeg' ? 'jpg' : match[1]; + const name = `${new Date().toISOString().replaceAll(':', '-')}-${randomUUID()}.${extension}`; + await mkdir(evidenceRoot, { recursive: true }); + await writeFile(path.join(evidenceRoot, name), bytes, { mode: 0o600 }); + const evidenceRef = `/api/evidence/${name}`; + const event = stateDatabase.appendEvent({ + eventType: 'checkpoint.screenshot', + actor: actor(body.actor, 'browser'), + navigation: currentNavigation, + evidenceRef, + payload: { + caption: typeof body.caption === 'string' ? body.caption.slice(0, 500) : '', + mime: `image/${match[1]}`, + size_bytes: bytes.length, + snapshot_ref: typeof body.snapshot_ref === 'string' ? body.snapshot_ref : currentNavigation?.snapshot_ref + } + }); + return reply.code(201).send({ schema: 'stem-card-evidence.v1', event }); +}); + +app.get('/api/evidence/:name', async (request, reply) => { + const name = String(request.params.name ?? ''); + if (!/^[A-Za-z0-9_.+-]+\.(png|jpg|webp)$/.test(name)) return reply.code(404).send('Not found'); + try { + const file = path.join(evidenceRoot, name); + const info = await stat(file); + if (!info.isFile()) return reply.code(404).send('Not found'); + return reply + .type(contentTypes[path.extname(file).toLowerCase()] ?? 'application/octet-stream') + .send(await readFile(file)); + } catch { + return reply.code(404).send('Not found'); + } +}); + +app.get('/api/card-revision', async () => ({ revision: await webRevision() })); + +app.get('/api/identity', async () => currentCardIdentity()); + +app.get('/api/checkpoint/status', async () => checkpointStatus()); + +app.get('/api/keyboard', async () => ({ + schema: 'stem-card-keyboard.v1', + hierarchy: ['card', 'task', 'block', 'phase', 'step', 'snapshot'], + shortcuts: KEYBOARD_SHORTCUTS +})); + +app.get('/api/navigation/catalog', async () => currentNavigationCatalog()); + +app.get('/api/navigation/current', async () => ({ + current: currentNavigation, + checkpoint: checkpointStatus() +})); + +app.put('/api/navigation/current', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + try { + const catalog = await currentNavigationCatalog(); + navigationRevision += 1; + currentNavigation = selectNavigation(catalog, body, navigationRevision); + stateDatabase.write('navigation', currentNavigation); + audit('navigation.select', currentNavigation.actor, { + focus_level: currentNavigation.focus_level, + sync_requested: currentNavigation.sync_requested + }); + return { current: currentNavigation }; + } catch (error) { + return reply.code(400).send({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.post('/api/navigation/move', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + try { + const catalog = await currentNavigationCatalog(); + navigationRevision += 1; + currentNavigation = moveNavigation( + catalog, + currentNavigation, + body, + navigationRevision + ); + stateDatabase.write('navigation', currentNavigation); + audit('navigation.move', currentNavigation.actor, { + level: body.level ?? currentNavigation.focus_level, + delta: Number(body.delta), + sync_requested: false + }); + return { current: currentNavigation }; + } catch (error) { + return reply.code(400).send({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.get('/api/viewer/state', async () => ({ viewer: currentViewer })); + +app.put('/api/viewer/state', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + try { + const requestedViewer = body.viewer && typeof body.viewer === 'object' ? body.viewer : body; + const requestedNvim = requestedViewer.nvim && typeof requestedViewer.nvim === 'object' + ? requestedViewer.nvim + : null; + viewerRevision += 1; + currentViewer = patchViewerState(currentViewer, body, viewerRevision); + stateDatabase.write('viewer', currentViewer); + if (requestedNvim && ['visible', 'sync', 'control', 'expanded', 'fit'].some(key => key in requestedNvim)) { + audit('viewer.nvim', actor(body.actor, 'browser'), { + visible: currentViewer.nvim.visible, + sync: currentViewer.nvim.sync, + control: currentViewer.nvim.control, + expanded: currentViewer.nvim.expanded, + fit: currentViewer.nvim.fit + }); + } + if (requestedViewer.allocator && typeof requestedViewer.allocator === 'object' + && 'visible' in requestedViewer.allocator) { + audit('viewer.allocator', actor(body.actor, 'browser'), { + visible: currentViewer.allocator.visible + }); + } + return { viewer: currentViewer }; + } catch (error) { + return reply.code(400).send({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.get('/api/control/state', async () => ({ + schema: 'stem-card-control-state.v1', + revision: `${navigationRevision}:${viewerRevision}`, + navigation: currentNavigation, + viewer: currentViewer, + checkpoint: checkpointStatus() +})); + +app.get('/api/nvim/state', async (_request, reply) => { + try { + return { + schema: 'stem-card-nvim-state.v1', + state: await selectedNvimState() + }; + } catch (error) { + return reply.code(503).send({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.put('/api/nvim/cursor', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const row = Number(body.row); + const column = Number(body.column); + const requestedWindow = body.window == null ? null : Number(body.window); + if (!Number.isSafeInteger(row) || row < 1 || !Number.isSafeInteger(column) || column < 0) { + return reply.code(400).send({ error: 'Kursor wymaga row >= 1 i column >= 0.' }); + } + if (requestedWindow != null && (!Number.isSafeInteger(requestedWindow) || requestedWindow < 1)) { + return reply.code(400).send({ error: 'window musi być poprawnym identyfikatorem Neovima.' }); + } + try { + const window = requestedWindow ?? await callSelectedNvim('nvim_get_current_win'); + await callSelectedNvim('nvim_win_set_cursor', [window, [row, column]]); + return { + schema: 'stem-card-nvim-state.v1', + state: await selectedNvimState() + }; + } catch (error) { + return reply.code(503).send({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.post('/api/nvim/input', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const keys = typeof body.keys === 'string' ? body.keys : ''; + if (!keys || keys.length > 256) { + return reply.code(400).send({ error: 'keys musi zawierać od 1 do 256 znaków.' }); + } + try { + const accepted = await callSelectedNvim('nvim_input', [keys]); + return { accepted, state: await selectedNvimState() }; + } catch (error) { + return reply.code(503).send({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.get('/api/state', async () => { + let nvim; + try { + nvim = { available: true, state: await selectedNvimState() }; + } catch (error) { + nvim = { available: false, error: error instanceof Error ? error.message : String(error) }; + } + return { + schema: 'stem-card-state.v1', + navigation: currentNavigation, + viewer: currentViewer, + checkpoint: checkpointStatus(), + nvim, + endpoints: { + keyboard: '/api/keyboard', + navigation_catalog: '/api/navigation/catalog', + navigation_current: '/api/navigation/current', + navigation_move: '/api/navigation/move', + navigation_sync: '/api/navigation/sync', + events: '/api/events', + evidence: '/api/evidence', + lesson_report: '/api/report/lesson.html', + identity: '/api/identity', + viewer: '/api/viewer/state', + nvim_reset: '/api/nvim/reset', + nvim_redraw: '/api/nvim/redraw', + nvim_cursor: '/api/nvim/cursor', + nvim_input: '/api/nvim/input' + } + }; +}); + +app.post('/api/checkpoint/activate', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const eventActor = actor(body.actor, 'browser'); + audit('checkpoint.request', eventActor, { + snapshot_ref: body.snapshot_ref, + generation: Number(body.generation) + }); + try { + const result = await activateCheckpoint({ + repositoryRoot, + cardFile, + snapshotRef: body.snapshot_ref, + generation: Number(body.generation), + expectedBinding: body.expected_binding, + expectedIdentity: body.expected_identity + }); + audit('checkpoint.ready', eventActor, { + snapshot_ref: body.snapshot_ref, + generation: Number(body.generation), + status: result.status, + message: result.message + }); + return result; + } catch (error) { + audit('checkpoint.failed', eventActor, { + snapshot_ref: body.snapshot_ref, + generation: Number(body.generation), + message: error instanceof Error ? error.message : String(error) + }); + return reply.code(409).send({ + status: 'failed', + snapshot_ref: body.snapshot_ref, + message: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.post('/api/nvim/reset', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const eventActor = actor(body.actor, 'browser'); + audit('nvim.reset.request', eventActor, { source: 'web.F1' }); + try { + await callSelectedNvim('nvim_command', ['CpuReset']); + return { + schema: 'stem-card-nvim-reset.v1', + status: 'accepted', + actor: eventActor, + state: await selectedNvimState() + }; + } catch (error) { + audit('nvim.reset.failed', eventActor, { + source: 'web.F1', + message: error instanceof Error ? error.message : String(error) + }); + return reply.code(503).send({ + status: 'failed', + message: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.post('/api/nvim/redraw', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const eventActor = actor(body.actor, 'nvim'); + let hostPane; + try { + hostPane = await zoomSelectedHostPane(); + } catch (error) { + hostPane = { found: false, error: error instanceof Error ? error.message : String(error) }; + } + await new Promise(resolve => setTimeout(resolve, 120)); + try { + const terminalUi = await settleSelectedNvimTerminalUi(hostPane); + const externalUi = await resizeSelectedNvimUi(); + await callSelectedNvim('nvim_command', ['silent! StudentLayoutFit | redraw!']); + const state = await selectedNvimState(); + audit('nvim.redraw', eventActor, { + source: 'api', + host_pane: hostPane, + terminal_ui: terminalUi, + external_ui: externalUi, + columns: state.columns, + lines: state.lines + }); + return { + schema: 'stem-card-nvim-redraw.v1', + status: 'ready', + host_pane: hostPane, + terminal_ui: terminalUi, + external_ui: externalUi, + state + }; + } catch (error) { + return reply.code(503).send({ + status: 'failed', + host_pane: hostPane, + message: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.post('/api/navigation/sync', async (request, reply) => { + const body = request.body && typeof request.body === 'object' ? request.body : {}; + const eventActor = actor(body.actor, 'nvim'); + const resolvedFrom = currentNavigation?.focus_level ?? null; + let activatedNavigation; + try { + const catalog = await currentNavigationCatalog(); + activatedNavigation = activateNavigation( + catalog, + currentNavigation, + eventActor, + navigationRevision + 1 + ); + } catch (error) { + return reply.code(409).send({ + status: 'failed', + message: error instanceof Error ? error.message : String(error) + }); + } + navigationRevision += 1; + currentNavigation = activatedNavigation; + stateDatabase.write('navigation', currentNavigation); + audit('navigation.activate', eventActor, { + source: 'navigation.sync', + resolved_from: resolvedFrom, + focus_level: currentNavigation.focus_level, + snapshot_ref: currentNavigation.snapshot_ref + }); + const snapshotRef = currentNavigation.snapshot_ref; + audit('checkpoint.request', eventActor, { + source: 'navigation.sync', + snapshot_ref: snapshotRef + }); + try { + const checkpoint = await activateCheckpoint({ + repositoryRoot, + cardFile, + snapshotRef, + generation: Date.now() + }); + audit('checkpoint.ready', eventActor, { + source: 'navigation.sync', + snapshot_ref: snapshotRef, + status: checkpoint.status, + message: checkpoint.message + }); + return { + schema: 'stem-card-navigation-sync.v1', + actor: eventActor, + current: currentNavigation, + checkpoint + }; + } catch (error) { + audit('checkpoint.failed', eventActor, { + source: 'navigation.sync', + snapshot_ref: snapshotRef, + message: error instanceof Error ? error.message : String(error) + }); + return reply.code(409).send({ + status: 'failed', + snapshot_ref: snapshotRef, + message: error instanceof Error ? error.message : String(error) + }); + } +}); + +app.get('/api/nvim-ui', { websocket: true }, (socket, request) => { + const origin = request.headers.origin; + const expectedOrigin = `http://${request.headers.host}`; + if (origin && origin !== expectedOrigin) { + socket.close(1008, 'Origin not allowed'); + return; + } + attachNvimUi(socket); +}); + +app.get('/*', async (request, reply) => { + const requested = request.params['*'] || 'index.html'; + const relative = requested === '' ? 'index.html' : requested; + const target = path.resolve(webRoot, relative); + if (target !== webRoot && !target.startsWith(`${webRoot}${path.sep}`)) return reply.code(403).send('Forbidden'); + try { + const info = await stat(target); + if (!info.isFile()) return reply.code(404).send('Not found'); + if (path.basename(target) === 'index.html') { + return reply + .type('text/html; charset=utf-8') + .send(injectTracker(await readFile(target, 'utf8'), await webRevision())); + } + return reply + .type(contentTypes[path.extname(target).toLowerCase()] ?? 'application/octet-stream') + .send(await readFile(target)); + } catch { + return reply.code(404).send('Not found'); + } +}); + +let socketServer = null; +app.addHook('onClose', async () => { + if (socketServer) { + await new Promise(resolve => socketServer.close(resolve)); + } + await rm(apiSocket, { force: true }); + stateDatabase.close(); +}); + +await restoreNavigation(); +await app.listen({ host, port }); + +await rm(apiSocket, { force: true }); +socketServer = createServer(app.routing); +await new Promise((resolve, reject) => { + socketServer.once('error', reject); + socketServer.listen(apiSocket, () => { + socketServer.off('error', reject); + resolve(); + }); +}); diff --git a/scripts/export_lesson_report.mjs b/scripts/export_lesson_report.mjs new file mode 100644 index 0000000..bba5cf6 --- /dev/null +++ b/scripts/export_lesson_report.mjs @@ -0,0 +1,107 @@ +import { mkdir, writeFile } from 'node:fs/promises'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { + catalogFromCard, + normaliseProgress, + readJson, + statusLabel, + teamsMarkdown +} from './lib/card_progress.mjs'; + +const root = path.resolve(process.cwd()); +const card = await readJson(path.join(root, 'json/card_source.json')); +const progressFile = path.resolve(process.env.CARD_PROGRESS_FILE ?? path.join(root, 'json/lesson_progress.json')); +let savedProgress; +try { + savedProgress = await readJson(progressFile); +} catch (error) { + if (error.code !== 'ENOENT') throw error; + savedProgress = {}; +} +const progress = normaliseProgress(card, savedProgress, { + repository: process.env.CARD_SOURCE_REPOSITORY, + revision: process.env.CARD_SOURCE_REVISION +}); +const reportDir = path.join(root, 'doc/session-reports'); +const basename = `${card.card.id}-${progress.session.id}`; +const markdownFile = path.join(reportDir, `${basename}.md`); +const texFile = path.join(reportDir, `${basename}.tex`); +const pdfFile = path.join(reportDir, `${basename}.pdf`); + +function tex(value) { + return String(value ?? '') + .replaceAll('\\', '\\textbackslash{}') + .replaceAll('&', '\\&').replaceAll('%', '\\%').replaceAll('$', '\\$') + .replaceAll('#', '\\#').replaceAll('_', '\\_').replaceAll('{', '\\{') + .replaceAll('}', '\\}').replaceAll('~', '\\textasciitilde{}') + .replaceAll('^', '\\textasciicircum{}'); +} + +function reportTex() { + const catalog = catalogFromCard(card); + const rows = catalog.map(item => { + const current = progress.items[item.id]; + return `${tex(item.id)} & ${tex(statusLabel[current.status])} & ${tex(current.updated_at || '—')} & ${tex(item.title)} \\\\ \\hline`; + }).join('\n'); + const events = progress.events.length + ? progress.events.map(event => `${tex(event.at)} & ${tex(event.item_id)} & ${tex(`${statusLabel[event.from]} → ${statusLabel[event.to]}`)} & ${tex(event.note || '—')} \\\\ \\hline`).join('\n') + : `— & — & Brak zmian & — \\\\ \\hline`; + return `\\documentclass[11pt,a4paper]{article} +\\usepackage[utf8]{inputenc} +\\usepackage[T1]{fontenc} +\\usepackage[polish]{babel} +\\usepackage[margin=18mm]{geometry} +\\usepackage{longtable} +\\usepackage{array} +\\usepackage{booktabs} +\\begin{document} +\\section*{${tex(card.card.title)} — zapis zajęć} +\\begin{tabular}{ll} +Karta & ${tex(card.card.id)} (${tex(card.card.version)}) \\\\ +Materiały & ${tex(progress.card.repository || 'nieuzupełniono')} \\\\ +Rewizja & ${tex(progress.card.revision || 'nieuzupełniono')} \\\\ +Sesja & ${tex(progress.session.id)} \\\\ +Prowadzący & ${tex(progress.session.teacher || 'nieuzupełniono')} \\\\ +Rozpoczęcie & ${tex(progress.session.started_at || 'brak')} \\\\ +\\end{tabular} + +\\subsection*{Zakres i status} +\\begin{longtable}{p{0.13\\textwidth}p{0.18\\textwidth}p{0.22\\textwidth}p{0.38\\textwidth}} +\\toprule Element & Status & Ostatnia zmiana & Zakres \\\\ \\midrule +${rows} +\\bottomrule +\\end{longtable} + +\\subsection*{Historia zatwierdzeń} +\\begin{longtable}{p{0.22\\textwidth}p{0.13\\textwidth}p{0.25\\textwidth}p{0.30\\textwidth}} +\\toprule Czas & Element & Zmiana & Notatka \\\\ \\midrule +${events} +\\bottomrule +\\end{longtable} +\\end{document} +`; +} + +function run(command, args, options) { + return new Promise((resolve, reject) => { + const child = spawn(command, args, options); + child.on('error', reject); + child.on('exit', code => code === 0 ? resolve() : reject(new Error(`${command} zakończył się kodem ${code}`))); + }); +} + +await mkdir(reportDir, { recursive: true }); +await writeFile(markdownFile, teamsMarkdown(card, progress), 'utf8'); +await writeFile(texFile, reportTex(), 'utf8'); +console.log(`Markdown: ${path.relative(root, markdownFile)}`); +console.log(`TeX: ${path.relative(root, texFile)}`); + +if (!process.argv.includes('--no-pdf')) { + try { + await run('latexmk', ['-pdf', '-interaction=nonstopmode', '-outdir=' + reportDir, texFile], { cwd: root, stdio: 'inherit' }); + console.log(`PDF: ${path.relative(root, pdfFile)}`); + } catch (error) { + console.warn(`Nie utworzono PDF (${error.message}). Pliki Markdown i TeX są gotowe.`); + } +} diff --git a/scripts/lib/card_control.mjs b/scripts/lib/card_control.mjs new file mode 100644 index 0000000..8f4a34f --- /dev/null +++ b/scripts/lib/card_control.mjs @@ -0,0 +1,376 @@ +const NAVIGATION_LEVELS = ['card', 'task', 'block', 'phase', 'step', 'snapshot']; + +export const KEYBOARD_SHORTCUTS = [ + { keys: 'Alt+ArrowUp/Alt+ArrowDown', action: 'navigation.move', level: 'task', description: 'poprzedni/następny TASK' }, + { keys: 'Ctrl+ArrowUp/Ctrl+ArrowDown', action: 'navigation.move', level: 'block', description: 'poprzedni/następny BLOCK' }, + { keys: 'Shift+ArrowUp/Shift+ArrowDown', action: 'navigation.move', level: 'phase', description: 'poprzednia/następna PHASE' }, + { keys: 'Ctrl+Shift+ArrowUp/Ctrl+Shift+ArrowDown', action: 'navigation.move', level: 'step', description: 'poprzedni/następny STEP' }, + { keys: 'Alt+Shift+ArrowUp/Alt+Shift+ArrowDown', action: 'navigation.move', level: 'snapshot', description: 'poprzedni/następny unikalny SNAPSHOT' }, + { keys: 'ArrowUp/ArrowDown', action: 'navigation.move', level: 'current', description: 'poprzedni/następny element aktywnego poziomu' }, + { keys: 'ArrowRight', action: 'navigation.level', direction: 'child', description: 'zejdź poziom niżej' }, + { keys: 'ArrowLeft', action: 'navigation.level', direction: 'parent', description: 'wróć poziom wyżej' }, + { keys: 'Enter', action: 'navigation.activate', description: 'wybierz element i przy SYNC ON odtwórz jego stan' }, + { keys: 'F1', action: 'nvim.reset', description: 'zresetuj aktualny cel i odśwież debugger' }, + { keys: 'F2', action: 'navigation.activate', description: 'zsynchronizuj cel z kursorem UML; BLOCK wybiera pierwszy krok' }, + { keys: 'Ctrl+Backquote', action: 'nvim.redraw', description: 'maksymalizuj pane, dopasuj siatkę i odśwież Neovima' }, + { keys: 'Ctrl+Enter', action: 'progress.toggle', description: 'zatwierdź lub cofnij zatwierdzenie bieżącego kroku' }, + { keys: 'Escape', action: 'navigation.level', direction: 'parent', description: 'anuluj albo wróć poziom wyżej' }, + { keys: 'Alt+Digit1', action: 'viewer.nvim.visible.toggle', description: 'pokaż/ukryj panel Neovima' }, + { keys: 'Alt+Digit2', action: 'viewer.nvim.sync.toggle', description: 'przełącz SYNC OFF/SYNC ON' }, + { keys: 'Alt+Digit3', action: 'viewer.nvim.control.toggle', description: 'uzbrój/rozbrój sterowanie Neovimem' }, + { keys: 'Alt+Digit4', action: 'viewer.nvim.expanded.toggle', description: 'przełącz normalną/wysoką wysokość panelu' }, + { keys: 'Alt+Digit5', action: 'viewer.nvim.fit.toggle', description: 'dopasuj cały ekran Neovima' }, + { keys: 'Alt+Digit6', action: 'viewer.allocator.visible.toggle', description: 'przypnij/ukryj model alokatora' }, + { keys: 'F12', action: 'viewer.keyboard.toggle', description: 'pokaż/ukryj skorowidz klawiatury' } +]; + +function text(value, fallback = '') { + return typeof value === 'string' && value.trim() ? value.trim() : fallback; +} + +function node(value, fallbackId, fallbackLabel) { + return { + id: text(value?.id, fallbackId), + label: text(value?.label, fallbackLabel) + }; +} + +export function navigationCatalog(card) { + const entries = []; + const taskIds = []; + const blockIds = new Map(); + const globalSnapshots = []; + + for (const [sectionIndex, section] of (card.sections ?? []).entries()) { + for (const [assetIndex, asset] of (section.assets ?? []).entries()) { + const interactive = asset.interactive; + if (!interactive?.phases?.length) continue; + const task = node( + interactive.task, + `section-${sectionIndex + 1}`, + text(section.title, `Sekcja ${sectionIndex + 1}`) + ); + const block = node( + interactive.block, + text(asset.label, `asset-${assetIndex + 1}`).replace(/^fig:/, ''), + text(interactive.title, text(asset.caption, `Blok ${assetIndex + 1}`)) + ); + if (!taskIds.includes(task.id)) taskIds.push(task.id); + const taskBlocks = blockIds.get(task.id) ?? []; + if (!taskBlocks.includes(block.id)) taskBlocks.push(block.id); + blockIds.set(task.id, taskBlocks); + const blockPhases = interactive.phases; + const blockSnapshots = []; + + for (const [phaseIndex, phaseValue] of blockPhases.entries()) { + const phase = node(phaseValue, `phase-${phaseIndex + 1}`, `Faza ${phaseIndex + 1}`); + const phaseSnapshots = []; + for (const [stepIndex, stepValue] of (phaseValue.steps ?? []).entries()) { + const step = node(stepValue, `step-${stepIndex + 1}`, `Krok ${stepIndex + 1}`); + const snapshotRef = text(stepValue.snapshot_ref) || null; + if (snapshotRef && !phaseSnapshots.includes(snapshotRef)) phaseSnapshots.push(snapshotRef); + if (snapshotRef && !blockSnapshots.includes(snapshotRef)) blockSnapshots.push(snapshotRef); + if (snapshotRef && !globalSnapshots.includes(snapshotRef)) globalSnapshots.push(snapshotRef); + entries.push({ + task: { ...task, index: taskIds.indexOf(task.id) }, + block: { ...block, index: taskBlocks.indexOf(block.id) }, + phase: { ...phase, index: phaseIndex }, + step: { + ...step, + number: Number.isSafeInteger(Number(stepValue.number)) ? Number(stepValue.number) : stepIndex + 1, + index: stepIndex, + global_index: entries.length + }, + snapshot: snapshotRef ? { + ref: snapshotRef, + index: phaseSnapshots.indexOf(snapshotRef), + block_index: blockSnapshots.indexOf(snapshotRef), + global_index: globalSnapshots.indexOf(snapshotRef) + } : null, + asset: { + label: text(asset.label), + section_index: sectionIndex, + asset_index: assetIndex + } + }); + } + } + } + } + + return { + schema: 'stem-card-navigation-catalog.v1', + levels: NAVIGATION_LEVELS, + entries + }; +} + +function integer(value) { + const parsed = Number(value); + return Number.isSafeInteger(parsed) ? parsed : null; +} + +function entryMatches(entry, request) { + const ids = { + task: text(request.task?.id, text(request.task_id)), + block: text(request.block?.id, text(request.block_id)), + phase: text(request.phase?.id, text(request.phase_id)), + step: text(request.step?.id, text(request.step_id)) + }; + for (const [level, id] of Object.entries(ids)) { + if (id && entry[level].id !== id) return false; + } + const snapshotRef = text(request.snapshot_ref, text(request.snapshot?.ref)); + return !snapshotRef || entry.snapshot?.ref === snapshotRef; +} + +function findNavigationEntry(catalog, request) { + const globalStep = integer(request.position?.global_step_index); + if (globalStep != null) return catalog.entries[globalStep] ?? null; + const matches = catalog.entries.filter(entry => entryMatches(entry, request)); + if (!matches.length) return null; + const phaseIndex = integer(request.position?.phase_index); + const stepIndex = integer(request.position?.step_index); + const snapshotIndex = integer(request.position?.snapshot_index); + return matches.find(entry => + (phaseIndex == null || entry.phase.index === phaseIndex) + && (stepIndex == null || entry.step.index === stepIndex) + && (snapshotIndex == null || entry.snapshot?.index === snapshotIndex) + ) ?? matches[0]; +} + +export function selectNavigation(catalog, request, revision = 1, now = new Date().toISOString()) { + if (!request || typeof request !== 'object') throw new Error('Stan nawigacji musi być obiektem.'); + const entry = findNavigationEntry(catalog, request); + if (!entry) throw new Error('Nie znaleziono wskazanej pozycji w katalogu karty.'); + const focusLevel = text(request.focus_level, 'step'); + if (!NAVIGATION_LEVELS.includes(focusLevel)) { + throw new Error(`focus_level musi być jednym z: ${NAVIGATION_LEVELS.join(', ')}.`); + } + return { + schema: 'stem-card-navigation.v2', + revision, + selected_at: now, + actor: text(request.actor, 'browser'), + focus_level: focusLevel, + cursor_visible: focusLevel !== 'card', + task: entry.task, + block: entry.block, + phase: entry.phase, + step: entry.step, + snapshot: entry.snapshot, + snapshot_ref: entry.snapshot?.ref ?? null, + position: { + task_index: entry.task.index, + block_index: entry.block.index, + phase_index: entry.phase.index, + step_index: entry.step.index, + global_step_index: entry.step.global_index, + snapshot_index: entry.snapshot?.index ?? null, + global_snapshot_index: entry.snapshot?.global_index ?? null + }, + sync_requested: request.sync_requested === true + }; +} + +function uniqueAtLevel(entries, level, current) { + const seen = new Set(); + return entries.filter(entry => { + let key; + if (level === 'task') key = entry.task.id; + else if (level === 'block') key = `${entry.task.id}/${entry.block.id}`; + else if (level === 'phase') key = `${entry.task.id}/${entry.block.id}/${entry.phase.id}`; + else if (level === 'snapshot') key = entry.snapshot?.ref; + else key = `${entry.task.id}/${entry.block.id}/${entry.phase.id}/${entry.step.id}`; + if (!key || seen.has(key)) return false; + seen.add(key); + if (level === 'phase') return entry.task.id === current.task.id && entry.block.id === current.block.id; + if (level === 'step' || level === 'snapshot') { + return entry.task.id === current.task.id && entry.block.id === current.block.id; + } + return true; + }); +} + +export function moveNavigation(catalog, current, command, revision = 1, now = new Date().toISOString()) { + if (!current) throw new Error('Najpierw ustaw bieżącą pozycję nawigacji.'); + const requestedLevel = text(command?.level, current.focus_level); + const level = requestedLevel === 'current' ? current.focus_level : requestedLevel; + if (!NAVIGATION_LEVELS.includes(level)) throw new Error('Niepoprawny poziom nawigacji.'); + if (level === 'card') throw new Error('Poziom CARD nie ma elementów UML do przewijania.'); + const delta = integer(command?.delta); + if (delta == null || delta === 0 || Math.abs(delta) > 100) throw new Error('delta musi być niezerową liczbą całkowitą.'); + const candidates = uniqueAtLevel(catalog.entries, level, current); + const currentIndex = candidates.findIndex(entry => { + if (level === 'task') return entry.task.id === current.task.id; + if (level === 'block') return entry.block.id === current.block.id && entry.task.id === current.task.id; + if (level === 'phase') return entry.phase.id === current.phase.id; + if (level === 'snapshot') return entry.snapshot?.ref === current.snapshot_ref; + return entry.step.id === current.step.id && entry.phase.id === current.phase.id; + }); + const targetIndex = Math.max(0, Math.min(candidates.length - 1, Math.max(0, currentIndex) + delta)); + const target = candidates[targetIndex]; + if (!target) throw new Error('Brak elementu na wskazanym poziomie.'); + return selectNavigation(catalog, { + task_id: target.task.id, + block_id: target.block.id, + phase_id: target.phase.id, + step_id: target.step.id, + snapshot_ref: target.snapshot?.ref, + focus_level: level, + // Moving the teaching cursor never mutates the debugger. Synchronisation + // is a separate, explicit activation (Enter in WWW or F2 in Neovim). + sync_requested: false, + actor: text(command.actor, 'api') + }, revision, now); +} + +export function activateNavigation(catalog, current, actor = 'nvim', revision = 1, now = new Date().toISOString()) { + if (!current) throw new Error('Najpierw wybierz BLOCK zawierający diagram UML.'); + if (current.focus_level === 'card' || current.focus_level === 'task') { + throw new Error('F2 wymaga zaznaczonego BLOCK, PHASE, STEP albo SNAPSHOT.'); + } + + const target = catalog.entries.find(entry => { + if (entry.task.id !== current.task.id || entry.block.id !== current.block.id) return false; + if (current.focus_level === 'block') return true; + if (entry.phase.id !== current.phase.id) return false; + if (current.focus_level === 'phase') return true; + if (current.focus_level === 'snapshot') return entry.snapshot?.ref === current.snapshot_ref; + return entry.step.id === current.step.id; + }); + if (!target) throw new Error('Nie znaleziono kroku UML, który można zsynchronizować.'); + if (!target.snapshot?.ref) throw new Error('Wybrany krok UML nie ma snapshotu do synchronizacji.'); + + return selectNavigation(catalog, { + task_id: target.task.id, + block_id: target.block.id, + phase_id: target.phase.id, + step_id: target.step.id, + snapshot_ref: target.snapshot.ref, + focus_level: 'step', + sync_requested: true, + actor: text(actor, 'nvim') + }, revision, now); +} + +export function initialViewerState() { + return { + schema: 'stem-card-viewer-state.v1', + revision: 0, + updated_at: null, + actor: null, + scale: 2.18, + scroll: { x: 0, y: 0, page_index: 0, sheet_scroll_top: 0 }, + viewport: { width: 0, height: 0 }, + allocator: { visible: true }, + nvim: { + visible: true, + sync: false, + control: false, + expanded: false, + fit: false, + connection: 'unknown', + screen_cursor: null, + grid: null + } + }; +} + +function finite(value, minimum, maximum, name) { + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < minimum || parsed > maximum) { + throw new Error(`${name} jest poza zakresem ${minimum}..${maximum}.`); + } + return parsed; +} + +function boolean(value, fallback) { + return typeof value === 'boolean' ? value : fallback; +} + +export function patchViewerState(current, request, revision = current.revision + 1, now = new Date().toISOString()) { + if (!request || typeof request !== 'object') throw new Error('Stan viewer musi być obiektem.'); + const patch = request.viewer && typeof request.viewer === 'object' ? request.viewer : request; + const scroll = patch.scroll && typeof patch.scroll === 'object' ? patch.scroll : {}; + const viewport = patch.viewport && typeof patch.viewport === 'object' ? patch.viewport : {}; + const allocator = patch.allocator && typeof patch.allocator === 'object' ? patch.allocator : {}; + const nvim = patch.nvim && typeof patch.nvim === 'object' ? patch.nvim : {}; + const cursor = nvim.screen_cursor && typeof nvim.screen_cursor === 'object' + ? { + row: Math.trunc(finite(nvim.screen_cursor.row, 0, 100000, 'nvim.screen_cursor.row')), + column: Math.trunc(finite(nvim.screen_cursor.column, 0, 100000, 'nvim.screen_cursor.column')) + } + : current.nvim.screen_cursor; + const grid = nvim.grid && typeof nvim.grid === 'object' + ? { + columns: Math.trunc(finite(nvim.grid.columns, 1, 10000, 'nvim.grid.columns')), + rows: Math.trunc(finite(nvim.grid.rows, 1, 10000, 'nvim.grid.rows')) + } + : current.nvim.grid; + const sync = boolean(nvim.sync, current.nvim.sync); + const control = sync && boolean(nvim.control, current.nvim.control); + const visible = boolean(nvim.visible, current.nvim.visible); + const fit = visible && boolean(nvim.fit, current.nvim.fit); + const expanded = visible && !fit && boolean(nvim.expanded, current.nvim.expanded); + const pageIndex = scroll.page_index == null + ? current.scroll.page_index + : Math.trunc(finite(scroll.page_index, 0, 100000, 'scroll.page_index')); + const pageChanged = scroll.page_index != null && pageIndex !== current.scroll.page_index; + return { + schema: 'stem-card-viewer-state.v1', + revision, + updated_at: now, + actor: text(request.actor, 'browser'), + scale: patch.scale == null ? current.scale : finite(patch.scale, 0.25, 3, 'scale'), + scroll: { + x: scroll.x == null ? current.scroll.x : finite(scroll.x, 0, 10000000, 'scroll.x'), + y: scroll.y == null ? current.scroll.y : finite(scroll.y, 0, 10000000, 'scroll.y'), + page_index: pageIndex, + sheet_scroll_top: scroll.sheet_scroll_top == null + ? (pageChanged ? 0 : current.scroll.sheet_scroll_top ?? 0) + : finite(scroll.sheet_scroll_top, 0, 10000000, 'scroll.sheet_scroll_top') + }, + viewport: { + width: viewport.width == null ? current.viewport.width : finite(viewport.width, 0, 100000, 'viewport.width'), + height: viewport.height == null ? current.viewport.height : finite(viewport.height, 0, 100000, 'viewport.height') + }, + allocator: { + visible: boolean(allocator.visible, current.allocator?.visible ?? true) + }, + nvim: { + visible, + sync, + control, + expanded, + fit, + connection: text(nvim.connection, current.nvim.connection), + screen_cursor: cursor, + grid + } + }; +} + +export function normalizeViewerState(value) { + const initial = initialViewerState(); + if (!value || typeof value !== 'object') return initial; + const revisionValue = Number(value.revision); + const revision = Number.isSafeInteger(revisionValue) && revisionValue >= 0 + ? revisionValue + : 0; + const updatedAt = typeof value.updated_at === 'string' ? value.updated_at : null; + const actor = typeof value.actor === 'string' && value.actor.trim() + ? value.actor.trim() + : null; + try { + const normalized = patchViewerState( + initial, + { actor: actor ?? 'browser', viewer: value }, + revision, + updatedAt + ); + return { ...normalized, actor }; + } catch { + return { ...initial, revision, updated_at: updatedAt, actor }; + } +} diff --git a/scripts/lib/card_progress.mjs b/scripts/lib/card_progress.mjs new file mode 100644 index 0000000..74bd1b4 --- /dev/null +++ b/scripts/lib/card_progress.mjs @@ -0,0 +1,193 @@ +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; +import path from 'node:path'; + +export const STATUSES = ['pending', 'approved']; + +export const statusLabel = { + pending: 'niezatwierdzony', + approved: 'zatwierdzony' +}; + +function normaliseStatus(status) { + if (status === 'approved' || status === 'completed' || status === 'discussed') return 'approved'; + return 'pending'; +} + +export async function readJson(file) { + return JSON.parse(await readFile(file, 'utf8')); +} + +export async function writeJsonAtomically(file, value) { + await mkdir(path.dirname(file), { recursive: true }); + const temporary = `${file}.${process.pid}.${Date.now()}.tmp`; + await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); + await rename(temporary, file); +} + +export function catalogFromCard(card) { + return card.sections.flatMap((section, sectionIndex) => { + const sectionSteps = (section.steps ?? []).map((step, stepIndex) => ({ + id: step.id, + title: step.title, + kind: 'section-step', + section_index: sectionIndex + 1, + section_title: section.title, + step_index: stepIndex + 1 + })); + const interactiveItems = (section.assets ?? []).flatMap(asset => { + const phases = asset.interactive?.phases ?? []; + if (phases.length) { + return phases.flatMap(phase => + (phase.steps ?? []) + .filter(step => typeof step.progress_id === 'string' && step.progress_id) + .map(step => ({ + id: step.progress_id, + title: `${String(asset.label || 'interaktywny UML').replace(/^fig:/, '')}: ${phase.label} / ${String(step.number).padStart(2, '0')} ${step.label}`, + kind: 'interactive-step', + section_index: sectionIndex + 1, + section_title: section.title, + asset_label: asset.label, + phase_id: phase.id, + step_id: step.id + })) + ); + } + return (asset.interactive?.stages ?? []) + .filter(stage => typeof stage.progress_id === 'string' && stage.progress_id) + .map(stage => ({ + id: stage.progress_id, + title: `${String(asset.label || 'interaktywny UML').replace(/^fig:/, '')}: ${stage.label}`, + kind: 'interactive-stage', + section_index: sectionIndex + 1, + section_title: section.title, + asset_label: asset.label, + stage_id: stage.id + })); + }).map((item, itemIndex) => ({ + ...item, + step_index: sectionSteps.length + itemIndex + 1 + })); + return [...sectionSteps, ...interactiveItems]; + }); +} + +export function normaliseProgress(card, progress, material = {}) { + const catalog = catalogFromCard(card); + const knownIds = new Set(catalog.map(item => item.id)); + const items = {}; + + for (const item of catalog) { + const previous = progress.items?.[item.id] ?? {}; + items[item.id] = { + status: normaliseStatus(previous.status), + updated_at: previous.updated_at ?? null, + note: typeof previous.note === 'string' ? previous.note : '' + }; + } + + return { + schema: 'stem-card-progress.v2', + card: { + id: card.card.id, + source: progress.card?.source || 'json/card_source.json', + version: card.card.version, + repository: progress.card?.repository || material.repository || '', + revision: progress.card?.revision || material.revision || '' + }, + session: { + id: progress.session?.id || `${new Date().toISOString().slice(0, 10)}-${card.card.slug}`, + title: progress.session?.title || card.card.title, + teacher: progress.session?.teacher || '', + started_at: progress.session?.started_at ?? null + }, + items, + events: Array.isArray(progress.events) + ? progress.events + .filter(event => knownIds.has(event.item_id)) + .map(event => ({ + ...event, + from: normaliseStatus(event.from), + to: normaliseStatus(event.to) + })) + : [] + }; +} + +export function progressSummary(progress) { + const counts = Object.fromEntries(STATUSES.map(status => [status, 0])); + for (const item of Object.values(progress.items)) counts[item.status] += 1; + return { total: Object.keys(progress.items).length, counts }; +} + +export function updateItem(progress, itemId, { status, note, actor = 'teacher', at = new Date().toISOString() }) { + if (!STATUSES.includes(status)) throw new Error(`Nieznany status: ${status}`); + const item = progress.items[itemId]; + if (!item) throw new Error(`Nieznany element karty: ${itemId}`); + + const nextNote = typeof note === 'string' ? note.trim() : item.note; + const changed = item.status !== status || item.note !== nextNote; + if (!changed) return false; + + const from = item.status; + item.status = status; + item.updated_at = at; + item.note = nextNote; + if (!progress.session.started_at) progress.session.started_at = at; + progress.events.push({ at, item_id: itemId, from, to: status, actor, note: nextNote }); + return true; +} + +function escapeMarkdown(value) { + return String(value ?? '').replaceAll('|', '\\|').replaceAll('\n', '
'); +} + +export function teamsMarkdown(card, progress) { + const catalog = catalogFromCard(card); + const byStatus = status => catalog.filter(item => progress.items[item.id]?.status === status); + const summary = progressSummary(progress); + const heading = `# ${card.card.title} — zapis zajęć`; + const metadata = [ + `- Karta: \`${card.card.id}\` (${card.card.version})`, + `- Materiał Edu: ${progress.card.repository || 'nieuzupełniono'}`, + `- Rewizja materiału: \`${progress.card.revision || 'nieuzupełniono'}\``, + `- Sesja: \`${progress.session.id}\``, + `- Prowadzący: ${progress.session.teacher || 'nieuzupełniono'}`, + `- Rozpoczęcie: ${progress.session.started_at || 'brak'}`, + `- Stan: ${summary.counts.approved}/${summary.total} zatwierdzone` + ]; + const list = (title, status) => { + const rows = byStatus(status); + const body = rows.length + ? rows.map(item => { + const state = progress.items[item.id]; + const timestamp = state.updated_at ? ` — ${state.updated_at}` : ''; + const note = state.note ? ` — ${state.note}` : ''; + return `- [${status === 'approved' ? 'x' : ' '}] \`${item.id}\` ${item.title}${timestamp}${note}`; + }).join('\n') + : '- Brak.'; + return `## ${title}\n\n${body}`; + }; + const timeline = progress.events.length + ? progress.events.map(event => { + const item = catalog.find(candidate => candidate.id === event.item_id); + return `| ${event.at} | \`${event.item_id}\` | ${statusLabel[event.from]} → ${statusLabel[event.to]} | ${escapeMarkdown(event.note)} |`; + }).join('\n') + : '| — | — | Brak zmian | — |'; + + return [ + heading, + '', + ...metadata, + '', + list('Zatwierdzone', 'approved'), + '', + list('Pozostało do zatwierdzenia', 'pending'), + '', + '## Historia zatwierdzeń', + '', + '| Czas | Element | Zmiana | Notatka |', + '| --- | --- | --- | --- |', + timeline, + '' + ].join('\n'); +} diff --git a/scripts/lib/card_state_db.mjs b/scripts/lib/card_state_db.mjs new file mode 100644 index 0000000..b0a89a1 --- /dev/null +++ b/scripts/lib/card_state_db.mjs @@ -0,0 +1,161 @@ +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; + +const STATE_SCHEMA = 'stem-card-backend-state.v1'; + +export class CardStateDatabase { + constructor(file) { + this.file = path.resolve(file); + mkdirSync(path.dirname(this.file), { recursive: true }); + this.database = new DatabaseSync(this.file); + this.database.exec(` + PRAGMA journal_mode = WAL; + PRAGMA synchronous = NORMAL; + CREATE TABLE IF NOT EXISTS card_state ( + key TEXT PRIMARY KEY, + schema_name TEXT NOT NULL, + revision INTEGER NOT NULL, + updated_at TEXT NOT NULL, + value_json TEXT NOT NULL + ) STRICT; + CREATE TABLE IF NOT EXISTS card_event ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + occurred_at TEXT NOT NULL, + event_type TEXT NOT NULL, + actor TEXT NOT NULL, + task_id TEXT, + block_id TEXT, + phase_id TEXT, + step_id TEXT, + snapshot_ref TEXT, + evidence_ref TEXT, + payload_json TEXT NOT NULL + ) STRICT; + CREATE INDEX IF NOT EXISTS card_event_occurred_at + ON card_event(occurred_at, id); + `); + this.readStatement = this.database.prepare(` + SELECT value_json + FROM card_state + WHERE key = ? + `); + this.writeStatement = this.database.prepare(` + INSERT INTO card_state (key, schema_name, revision, updated_at, value_json) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + schema_name = excluded.schema_name, + revision = excluded.revision, + updated_at = excluded.updated_at, + value_json = excluded.value_json + `); + this.appendEventStatement = this.database.prepare(` + INSERT INTO card_event ( + occurred_at, event_type, actor, + task_id, block_id, phase_id, step_id, snapshot_ref, + evidence_ref, payload_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + RETURNING id + `); + this.listEventsStatement = this.database.prepare(` + SELECT + id, occurred_at, event_type, actor, + task_id, block_id, phase_id, step_id, snapshot_ref, + evidence_ref, payload_json + FROM card_event + WHERE id > ? + ORDER BY id ASC + LIMIT ? + `); + } + + read(key) { + const row = this.readStatement.get(key); + if (!row) return null; + try { + return JSON.parse(row.value_json); + } catch { + return null; + } + } + + write(key, value) { + const revision = Number.isSafeInteger(Number(value?.revision)) + ? Number(value.revision) + : 0; + const updatedAt = String(value?.updated_at ?? value?.selected_at ?? new Date().toISOString()); + this.writeStatement.run( + key, + STATE_SCHEMA, + revision, + updatedAt, + JSON.stringify(value) + ); + return value; + } + + appendEvent({ + eventType, + actor = 'system', + navigation = null, + evidenceRef = null, + payload = {}, + occurredAt = new Date().toISOString() + }) { + const row = this.appendEventStatement.get( + String(occurredAt), + String(eventType), + String(actor), + navigation?.task?.id ?? null, + navigation?.block?.id ?? null, + navigation?.phase?.id ?? null, + navigation?.step?.id ?? null, + navigation?.snapshot_ref ?? null, + evidenceRef == null ? null : String(evidenceRef), + JSON.stringify(payload ?? {}) + ); + return { + id: Number(row.id), + occurred_at: String(occurredAt), + event_type: String(eventType), + actor: String(actor), + task_id: navigation?.task?.id ?? null, + block_id: navigation?.block?.id ?? null, + phase_id: navigation?.phase?.id ?? null, + step_id: navigation?.step?.id ?? null, + snapshot_ref: navigation?.snapshot_ref ?? null, + evidence_ref: evidenceRef == null ? null : String(evidenceRef), + payload: payload ?? {} + }; + } + + listEvents({ afterId = 0, limit = 200 } = {}) { + return this.listEventsStatement.all(Number(afterId), Number(limit)).map(row => { + let payload = {}; + try { + payload = JSON.parse(row.payload_json); + } catch { + // Preserve the audit row even if an old payload cannot be decoded. + } + return { + id: Number(row.id), + occurred_at: row.occurred_at, + event_type: row.event_type, + actor: row.actor, + task_id: row.task_id, + block_id: row.block_id, + phase_id: row.phase_id, + step_id: row.step_id, + snapshot_ref: row.snapshot_ref, + evidence_ref: row.evidence_ref, + payload + }; + }); + } + + close() { + this.database.close(); + } +} + +export { STATE_SCHEMA }; diff --git a/scripts/lib/checkpoint_controller.mjs b/scripts/lib/checkpoint_controller.mjs new file mode 100644 index 0000000..84f2b39 --- /dev/null +++ b/scripts/lib/checkpoint_controller.mjs @@ -0,0 +1,423 @@ +import { createHash, randomUUID } from 'node:crypto'; +import { readFile, realpath } from 'node:fs/promises'; +import path from 'node:path'; +import { callSelectedNvim } from './nvim_ui_bridge.mjs'; + +const POLL_MS = 100; +const state = { + wantedGeneration: 0, + active: null, + activeControl: null, + last: null, + queue: Promise.resolve() +}; + +function delay(milliseconds) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + +function errorText(error) { + return error instanceof Error ? error.message : String(error); +} + +async function readJson(file) { + return JSON.parse(await readFile(file, 'utf8')); +} + +function runtimeRoot() { + return process.env.XDG_RUNTIME_DIR ?? '/run/user/1000'; +} + +function selectionRoot() { + return process.env.MCP_CONTAINER_SELECTION_ROOT + ?? path.join(runtimeRoot(), 'stem/mcp-selected'); +} + +async function selectedBinding() { + const selectedDirectory = await realpath(path.join(selectionRoot(), 'current')); + const [metadata, socketPath] = await Promise.all([ + readJson(path.join(selectedDirectory, 'selection.json')), + realpath(path.join(selectedDirectory, 'n.sock')) + ]); + return { + metadata, + socketPath, + epoch: `${metadata.container_id ?? 'unknown'}:${socketPath}` + }; +} + +async function assertBindingCurrent(expected) { + const current = await selectedBinding(); + if (current.epoch !== expected.epoch) { + throw new Error('Cel MCP zmienił się podczas replay; wynik starego kontenera został odrzucony.'); + } +} + +export function assertExpectedBinding(binding, expected) { + // Browser activation follows the currently selected teaching session and + // remains backward compatible. CLI/orchestrator callers pin all fields. + if (expected == null) return; + if (typeof expected !== 'object') throw new Error('expected_binding musi być obiektem.'); + for (const field of ['container_id', 'instance', 'profile', 'target']) { + const wanted = typeof expected[field] === 'string' ? expected[field] : ''; + const actual = typeof binding.metadata[field] === 'string' ? binding.metadata[field] : ''; + if (!wanted || wanted !== actual) { + throw new Error(`Cel MCP zmienił się przed replay (${field}: ${actual || '?'} != ${wanted || '?'}).`); + } + } +} + +export function assertExpectedIdentity(actual, expected) { + // Browser-only calls may omit the pin. Orchestrators send all fields so a + // card hot reload cannot silently replay a recipe from another revision. + if (expected == null) return; + if (typeof expected !== 'object') throw new Error('expected_identity musi być obiektem.'); + for (const field of ['id', 'uuid', 'version', 'source_sha256']) { + const wanted = typeof expected[field] === 'string' ? expected[field] : ''; + const observed = typeof actual[field] === 'string' ? actual[field] : ''; + if (!wanted || wanted !== observed) { + throw new Error(`Karta zmieniła się przed replay (${field}: ${observed || '?'} != ${wanted || '?'}).`); + } + } +} + +async function readCardSnapshot(cardFile) { + const source = await readFile(cardFile); + const document = JSON.parse(source.toString('utf8')); + const card = document.card ?? {}; + return { + document, + identity: { + id: String(card.id ?? ''), + uuid: String(card.uuid ?? ''), + version: String(card.version ?? ''), + source_sha256: createHash('sha256').update(source).digest('hex') + } + }; +} + +async function sha256(file) { + const hash = createHash('sha256'); + hash.update(await readFile(file)); + return hash.digest('hex'); +} + +async function gitBlobSha1(file) { + const content = await readFile(file); + const hash = createHash('sha1'); + hash.update(`blob ${content.length}\0`); + hash.update(content); + return hash.digest('hex'); +} + +function hostPath(repositoryRoot, containerPath) { + if (!path.isAbsolute(containerPath)) { + const resolved = path.resolve(repositoryRoot, containerPath); + if (resolved === repositoryRoot || resolved.startsWith(`${repositoryRoot}${path.sep}`)) { + return resolved; + } + } + if (containerPath === '/workspace') return repositoryRoot; + if (containerPath.startsWith('/workspace/')) { + return path.join(repositoryRoot, containerPath.slice('/workspace/'.length)); + } + throw new Error(`Ścieżka spoza /workspace nie jest dozwolona: ${containerPath}`); +} + +async function nvimLua(source, args = [], binding = null) { + return callSelectedNvim( + 'nvim_exec_lua', + [source, args], + binding ? { socketPath: binding.socketPath } : {} + ); +} + +async function callPinnedNvim(binding, method, parameters = []) { + return callSelectedNvim(method, parameters, { socketPath: binding.socketPath }); +} + +async function sendGdb(command, binding) { + const available = await callPinnedNvim(binding, 'nvim_call_function', [ + 'exists', + ['*TermDebugSendCommand'] + ]); + if (available !== 1) { + throw new Error('Termdebug nie udostępnia TermDebugSendCommand'); + } + return callPinnedNvim(binding, 'nvim_call_function', [ + 'TermDebugSendCommand', + [command] + ]); +} + +async function interruptGdb(binding) { + return nvimLua(` + for _, buffer in ipairs(vim.api.nvim_list_bufs()) do + local name = vim.api.nvim_buf_get_name(buffer) + if vim.bo[buffer].buftype == 'terminal' and name:find('gdb%-multiarch') then + local channel = vim.bo[buffer].channel + if channel and channel > 0 then + vim.api.nvim_chan_send(channel, string.char(3)) + return true + end + end + end + return false + `, [], binding); +} + +async function gdbTerminalRunning(binding) { + return nvimLua(` + for _, buffer in ipairs(vim.api.nvim_list_bufs()) do + local name = vim.api.nvim_buf_get_name(buffer) + if vim.bo[buffer].buftype == 'terminal' and name:find('gdb%-multiarch') then + local channel = vim.bo[buffer].channel + if channel and channel > 0 and vim.fn.jobwait({ channel }, 0)[1] == -1 then + return true + end + end + end + return false + `, [], binding); +} + +async function cleanupDeadGdbBuffers(binding) { + return nvimLua(` + local dead = {} + for _, buffer in ipairs(vim.api.nvim_list_bufs()) do + local name = vim.api.nvim_buf_get_name(buffer) + if vim.bo[buffer].buftype == 'terminal' and name:find('gdb%-multiarch') then + local channel = vim.bo[buffer].channel + if not channel or channel <= 0 or vim.fn.jobwait({ channel }, 0)[1] ~= -1 then + table.insert(dead, buffer) + end + end + end + for _, buffer in ipairs(dead) do + for _, window in ipairs(vim.fn.win_findbuf(buffer)) do + if vim.api.nvim_win_is_valid(window) and #vim.api.nvim_list_wins() > 1 then + pcall(vim.api.nvim_win_close, window, true) + end + end + if vim.api.nvim_buf_is_valid(buffer) then + pcall(vim.api.nvim_buf_delete, buffer, { force = true }) + end + end + return #dead + `, [], binding); +} + +async function ensureTermdebug(binding) { + const probe = gdbTerminalRunning(binding); + let running; + try { + running = await Promise.race([ + probe, + delay(500).then(() => { throw new Error('timeout'); }) + ]); + } catch { + // Termdebug cleanup sometimes leaves a hit-enter prompt which blocks an + // RPC expression while Neovim still accepts input events. + await callPinnedNvim(binding, 'nvim_input', ['']).catch(() => undefined); + running = await probe; + } + if (running) { + await cleanupDeadGdbBuffers(binding); + await callPinnedNvim(binding, 'nvim_command', ['silent! StudentLayoutFit']).catch(() => undefined); + return; + } + // A failed Termdebug job can leave Neovim at a hit-enter prompt. Feeding + // Enter only in the dead-GDB branch clears it before the recovery command. + await callPinnedNvim(binding, 'nvim_input', ['']).catch(() => undefined); + await cleanupDeadGdbBuffers(binding); + await callPinnedNvim(binding, 'nvim_command', ['silent! StudentTermdebug']); + const deadline = Date.now() + 8000; + while (Date.now() < deadline) { + if (await gdbTerminalRunning(binding)) { + // TermdebugStartPost queues target/directory/dashboard setup. + await delay(1200); + await cleanupDeadGdbBuffers(binding); + await callPinnedNvim(binding, 'nvim_command', ['silent! StudentLayoutFit']).catch(() => undefined); + return; + } + await delay(100); + } + throw new Error('Nie udało się automatycznie odtworzyć procesu GDB w Termdebug.'); +} + +async function assertEditorReady(repositoryRoot, artifact, binding) { + const modified = await nvimLua(` + local result = {} + for _, buffer in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buffer) and vim.bo[buffer].modified then + table.insert(result, vim.api.nvim_buf_get_name(buffer)) + end + end + return result + `, [], binding); + if (Array.isArray(modified) && modified.some(name => name.endsWith(artifact.source))) { + throw new Error('Bufor Task04 ma niezapisane zmiany; zapis lub przebudowa są wymagane przed replay.'); + } + const elf = await nvimLua('return vim.env.ELF_FILE or ""', [], binding); + if (typeof elf !== 'string' || !elf) throw new Error('Neovim nie ma ELF_FILE bieżącej sesji.'); + const elfOnHost = hostPath(repositoryRoot, elf); + if (artifact.source_git_blob) { + const sourceOnHost = hostPath(repositoryRoot, artifact.source); + const actualSourceBlob = await gitBlobSha1(sourceOnHost); + if (actualSourceBlob !== artifact.source_git_blob) { + throw new Error('Źródło Task04 różni się od wersji użytej do pomiaru karty; przebuduj kartę i snapshoty.'); + } + } + if (artifact.hazard3_image_sha256 && binding.metadata.profile === 'hazard3-sim') { + const imageOnHost = hostPath(repositoryRoot, artifact.hazard3_image); + const actual = await sha256(imageOnHost); + if (actual !== artifact.hazard3_image_sha256) { + throw new Error('Obraz wykonywalny Hazard3 różni się od obrazu użytego do pomiaru karty; zregeneruj snapshoty.'); + } + } + return { elf, elfOnHost }; +} + +async function waitForResult(resultFile, operationId, timeoutMs, binding) { + const deadline = Date.now() + timeoutMs; + let latest = null; + while (Date.now() < deadline) { + try { + const current = await readJson(resultFile); + if (current.operation_id === operationId) { + latest = current; + state.active = current; + if (current.status === 'ready' || current.status === 'failed') return current; + } + } catch { + // GDB writes atomically; absence before the first phase is expected. + } + await delay(POLL_MS); + } + await interruptGdb(binding).catch(() => undefined); + await delay(200); + throw new Error(`Timeout replay; ostatni stan: ${latest?.status ?? 'brak odpowiedzi GDB'}`); +} + +async function runActivation({ + repositoryRoot, + cardFile, + snapshotRef, + generation, + expectedBinding, + expectedIdentity +}) { + if (generation !== state.wantedGeneration) { + return { status: 'cancelled', generation, snapshot_ref: snapshotRef, message: 'Zastąpione nowszym krokiem.' }; + } + const [cardSnapshot, binding] = await Promise.all([readCardSnapshot(cardFile), selectedBinding()]); + const card = cardSnapshot.document; + assertExpectedIdentity(cardSnapshot.identity, expectedIdentity); + assertExpectedBinding(binding, expectedBinding); + const selection = binding.metadata; + const registry = card.debug_checkpoints; + const recipe = registry?.items?.[snapshotRef]; + if (!recipe) throw new Error(`Brak recepty checkpointu: ${snapshotRef}`); + if (!registry.targets?.[selection.profile]) { + throw new Error(`Karta nie ma adaptera replay dla profilu ${selection.profile ?? '?'}.`); + } + await assertBindingCurrent(binding); + await ensureTermdebug(binding); + await assertEditorReady(repositoryRoot, registry.artifact, binding); + const operationId = randomUUID(); + const payload = { + operation_id: operationId, + generation, + snapshot_ref: snapshotRef, + profile: selection.profile, + semantics: registry.semantics, + stop: recipe.stop, + verify: recipe.verify ?? { expressions: [] } + }; + state.active = { + status: 'replaying', + phase: 'dispatch', + operation_id: operationId, + generation, + snapshot_ref: snapshotRef, + profile: selection.profile + }; + state.activeControl = { generation, binding }; + await sendGdb('source /workspace/tools/gdb/stem_checkpoint.py', binding); + const token = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64url'); + await sendGdb(`stem-checkpoint-activate ${token}`, binding); + const resultFile = path.join( + repositoryRoot, + '.stem/instances', + selection.instance, + 'gdb-sync.json.checkpoint.json' + ); + const result = await waitForResult( + resultFile, + operationId, + selection.profile === 'rp2350' ? 45000 : 20000, + binding + ); + if (result.status !== 'ready') { + throw new Error(result.message ?? `Replay ${snapshotRef} nie osiągnął stanu ready.`); + } + await assertBindingCurrent(binding); + state.last = result; + state.active = null; + state.activeControl = null; + await callPinnedNvim(binding, 'nvim_command', ['silent! StudentViewReset']).catch(() => undefined); + await callPinnedNvim(binding, 'nvim_command', ['redraw!']).catch(() => undefined); + return result; +} + +export function checkpointStatus() { + return { active: state.active, last: state.last, wanted_generation: state.wantedGeneration }; +} + +export function activateCheckpoint({ + repositoryRoot, + cardFile, + snapshotRef, + generation, + expectedBinding, + expectedIdentity +}) { + if (typeof snapshotRef !== 'string' || !/^task04\.[a-z0-9.-]+$/.test(snapshotRef)) { + return Promise.reject(new Error('Niepoprawny snapshot_ref.')); + } + if (generation != null && (!Number.isSafeInteger(generation) || generation <= 0)) { + return Promise.reject(new Error('generation musi być dodatnią, bezpieczną liczbą całkowitą.')); + } + const nextGeneration = Number.isSafeInteger(generation) && generation > 0 + ? generation + : state.wantedGeneration + 1; + state.wantedGeneration = Math.max(state.wantedGeneration + 1, nextGeneration); + const activeControl = state.activeControl; + if ( + activeControl + && activeControl.generation < state.wantedGeneration + && state.active?.phase === 'run-to-stop' + ) { + void interruptGdb(activeControl.binding).catch(() => undefined); + } + const request = { + repositoryRoot, + cardFile, + snapshotRef, + generation: state.wantedGeneration, + expectedBinding, + expectedIdentity + }; + const operation = state.queue.then(() => runActivation(request)); + state.queue = operation.catch(error => { + state.last = { + status: 'failed', + snapshot_ref: snapshotRef, + generation: request.generation, + message: errorText(error) + }; + state.active = null; + if (state.activeControl?.generation === request.generation) state.activeControl = null; + }); + return operation; +} diff --git a/scripts/lib/nvim_ui_bridge.mjs b/scripts/lib/nvim_ui_bridge.mjs new file mode 100644 index 0000000..2cecea2 --- /dev/null +++ b/scripts/lib/nvim_ui_bridge.mjs @@ -0,0 +1,447 @@ +import net from 'node:net'; +import path from 'node:path'; +import { readFile, realpath } from 'node:fs/promises'; +import { decodeMultiStream, encode } from '@msgpack/msgpack'; + +const OPEN = 1; +const DEFAULT_COLUMNS = 190; +const DEFAULT_ROWS = 50; +const RETRY_DELAY_MS = 1200; +const TARGET_POLL_MS = 750; +const MAX_BUFFERED_BYTES = 4 * 1024 * 1024; + +function errorText(error) { + return error instanceof Error ? error.message : String(error); +} + +function delay(milliseconds) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + +async function selectedTarget(selectionRoot) { + const current = path.join(selectionRoot, 'current'); + const socketLink = path.join(current, 'n.sock'); + const [socketPath, metadataText] = await Promise.all([ + realpath(socketLink), + readFile(path.join(current, 'selection.json'), 'utf8') + ]); + const metadata = JSON.parse(metadataText); + return { + socketPath, + metadata, + epoch: `${metadata.container_id ?? 'unknown'}:${socketPath}` + }; +} + +class NvimRpcClient { + constructor(socketPath, onNotification) { + this.socketPath = socketPath; + this.onNotification = onNotification; + this.socket = new net.Socket(); + this.nextMessageId = 1; + this.pending = new Map(); + this.closed = false; + this.readLoop = null; + } + + async connect() { + await new Promise((resolve, reject) => { + const connected = () => { + this.socket.off('error', failed); + resolve(); + }; + const failed = error => { + this.socket.off('connect', connected); + reject(error); + }; + this.socket.once('connect', connected); + this.socket.once('error', failed); + this.socket.connect(this.socketPath); + }); + this.readLoop = this.consume().catch(error => { + this.rejectPending(error); + if (!this.closed) this.socket.destroy(error); + throw error; + }); + } + + async consume() { + for await (const message of decodeMultiStream(this.socket)) { + if (!Array.isArray(message)) continue; + if (message[0] === 1) { + const pending = this.pending.get(message[1]); + if (!pending) continue; + this.pending.delete(message[1]); + if (message[2]) pending.reject(new Error(JSON.stringify(message[2]))); + else pending.resolve(message[3]); + } else if (message[0] === 2) { + this.onNotification(message[1], message[2]); + } + } + throw new Error('Neovim zamknął połączenie RPC.'); + } + + call(method, parameters = []) { + if (this.closed) return Promise.reject(new Error('Połączenie RPC jest zamknięte.')); + const id = this.nextMessageId++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve, reject }); + this.socket.write(encode([0, id, method, parameters]), error => { + if (!error) return; + this.pending.delete(id); + reject(error); + }); + }); + } + + rejectPending(error) { + for (const pending of this.pending.values()) pending.reject(error); + this.pending.clear(); + } + + close() { + if (this.closed) return; + this.closed = true; + this.rejectPending(new Error('Połączenie RPC zostało zamknięte.')); + this.socket.destroy(); + } +} + +function safeSend(websocket, message) { + if (websocket.readyState !== OPEN) return; + if (websocket.bufferedAmount > MAX_BUFFERED_BYTES) { + websocket.close(1013, 'Neovim UI client is too slow'); + return; + } + websocket.send(JSON.stringify(message)); +} + +function existingUiSize(uis) { + const valid = Array.isArray(uis) + ? uis.filter(item => Number.isInteger(item?.width) && Number.isInteger(item?.height)) + : []; + const terminal = valid.find(item => item?.stdin_tty && item?.stdout_tty); + if (terminal) { + return { width: terminal.width, height: terminal.height }; + } + return { + // Without a terminal UI there is nothing to mirror, so retain a useful + // external-UI fallback size until the selected terminal is attached again. + width: Math.max(DEFAULT_COLUMNS, ...valid.map(item => item.width)), + height: Math.max(DEFAULT_ROWS, ...valid.map(item => item.height)) + }; +} + +class NvimUiHub { + constructor(selectionRoot, onEmpty) { + this.selectionRoot = selectionRoot; + this.onEmpty = onEmpty; + this.clients = new Set(); + this.rpc = null; + this.stopped = false; + this.started = false; + this.status = { type: 'status', state: 'connecting' }; + this.pendingResize = null; + this.resizeTimer = null; + this.appliedUiSize = ''; + this.uiSize = { width: DEFAULT_COLUMNS, height: DEFAULT_ROWS }; + this.uiAttached = false; + this.refreshingUi = null; + } + + broadcast(message) { + this.status = message.type === 'status' ? message : this.status; + for (const client of this.clients) safeSend(client, message); + } + + add(websocket) { + this.clients.add(websocket); + safeSend(websocket, this.status); + const remove = () => this.remove(websocket); + websocket.once('close', remove); + websocket.once('error', remove); + websocket.on('message', payload => this.handleMessage(payload)); + if (!this.started) { + this.started = true; + void this.run().catch(error => { + this.broadcast({ type: 'status', state: 'offline', message: errorText(error) }); + }); + } else if (this.rpc && this.uiAttached) { + // A newly opened browser has no copy of redraw events emitted before it + // joined the shared hub. Reattaching only this external UI makes Neovim + // send one complete line-grid frame. The real TTY UI stays attached. + void this.refreshUi().catch(() => undefined); + } + } + + refreshUi() { + if (this.refreshingUi) return this.refreshingUi; + const rpc = this.rpc; + const size = this.uiSize; + if (!rpc || !this.uiAttached) return Promise.resolve(); + this.refreshingUi = (async () => { + await rpc.call('nvim_ui_detach'); + if (rpc !== this.rpc || this.stopped) return; + this.uiAttached = false; + await rpc.call('nvim_ui_attach', [size.width, size.height, { + rgb: false, + ext_linegrid: true, + ext_multigrid: false, + override: false + }]); + if (rpc === this.rpc) this.uiAttached = true; + })().finally(() => { + this.refreshingUi = null; + }); + return this.refreshingUi; + } + + remove(websocket) { + this.clients.delete(websocket); + if (this.clients.size > 0) return; + this.stopped = true; + if (this.resizeTimer) clearTimeout(this.resizeTimer); + this.rpc?.close(); + this.onEmpty(); + } + + scheduleResize(columns, rows) { + this.pendingResize = { columns, rows }; + if (this.resizeTimer) clearTimeout(this.resizeTimer); + this.resizeTimer = setTimeout(() => { + this.resizeTimer = null; + void this.applyResize().catch(() => undefined); + }, 90); + } + + async resizeToTerminal() { + if (this.resizeTimer) { + clearTimeout(this.resizeTimer); + this.resizeTimer = null; + } + this.pendingResize = { + columns: this.uiSize.width, + rows: this.uiSize.height + }; + await this.applyResize(); + return this.uiSize; + } + + async applyResize() { + const requested = this.pendingResize; + const rpc = this.rpc; + this.pendingResize = null; + if (!requested || !rpc || this.stopped) return; + const uis = await rpc.call('nvim_list_uis'); + if (rpc !== this.rpc) return; + const terminalUi = Array.isArray(uis) + ? uis.find(ui => ui?.stdin_tty && ui?.stdout_tty) + : null; + const columns = Number.isInteger(terminalUi?.width) + ? terminalUi.width + : requested.columns; + const rows = Number.isInteger(terminalUi?.height) + ? terminalUi.height + : requested.rows; + const size = `${columns}x${rows}`; + if (size === this.appliedUiSize) return; + await rpc.call('nvim_ui_try_resize', [columns, rows]); + if (rpc !== this.rpc) return; + await rpc.call('nvim_command', ['silent! StudentLayoutFit']); + this.uiSize = { width: columns, height: rows }; + this.appliedUiSize = size; + } + + handleMessage(payload) { + if (!this.rpc || this.stopped) return; + try { + const message = JSON.parse(payload.toString('utf8')); + if (message?.type === 'refresh') { + void this.resizeToTerminal() + .then(() => this.refreshUi()) + .catch(() => undefined); + return; + } + if (message?.type === 'input' && typeof message.keys === 'string' && message.keys.length <= 256) { + void this.rpc.call('nvim_input', [message.keys]).catch(() => undefined); + return; + } + if (message?.type !== 'mouse') return; + const button = ['left', 'middle', 'right', 'wheel'].includes(message.button) + ? message.button + : null; + const action = ['press', 'drag', 'release', 'up', 'down'].includes(message.action) + ? message.action + : null; + const row = Number(message.row); + const column = Number(message.column); + if (!button || !action || !Number.isInteger(row) || !Number.isInteger(column)) return; + void this.rpc.call('nvim_input_mouse', [ + button, + action, + typeof message.modifier === 'string' ? message.modifier.slice(0, 16) : '', + 0, + Math.max(0, row), + Math.max(0, column) + ]).catch(() => undefined); + } catch { + // Only the typed input/mouse/refresh capabilities above are exposed. + } + } + + async run() { + while (!this.stopped && this.clients.size > 0) { + let selected; + try { + selected = await selectedTarget(this.selectionRoot); + } catch (error) { + this.broadcast({ + type: 'status', + state: 'unselected', + message: `Brak aktywnego celu MCP: ${errorText(error)}` + }); + await delay(RETRY_DELAY_MS); + continue; + } + + this.broadcast({ + type: 'status', + state: 'connecting', + epoch: selected.epoch, + target: selected.metadata + }); + + let targetTimer = null; + try { + this.rpc = new NvimRpcClient(selected.socketPath, (method, parameters) => { + if (method !== 'redraw') return; + this.broadcast({ + type: 'redraw', + epoch: selected.epoch, + events: parameters + }); + }); + await this.rpc.connect(); + this.appliedUiSize = ''; + this.uiAttached = false; + void this.rpc.readLoop.catch(() => undefined); + const uis = await this.rpc.call('nvim_list_uis'); + const size = existingUiSize(uis); + this.uiSize = size; + await this.rpc.call('nvim_set_client_info', [ + 'stem-card-browser', + { major: 0, minor: 1, patch: 0 }, + 'ui', + {}, + { website: 'local://stem-card', license: 'private' } + ]); + await this.rpc.call('nvim_ui_attach', [size.width, size.height, { + rgb: false, + ext_linegrid: true, + ext_multigrid: false, + override: false + }]); + this.uiAttached = true; + this.broadcast({ + type: 'status', + state: 'connected', + epoch: selected.epoch, + target: selected.metadata, + grid: size, + control: 'typed-input' + }); + + const targetChanged = new Promise((_, reject) => { + targetTimer = setInterval(async () => { + try { + const current = await selectedTarget(this.selectionRoot); + if (current.epoch !== selected.epoch) { + reject(new Error('Zmieniono cel MCP.')); + } + } catch (error) { + reject(error); + } + }, TARGET_POLL_MS); + }); + await Promise.race([this.rpc.readLoop, targetChanged]); + } catch (error) { + if (!this.stopped) { + this.broadcast({ + type: 'status', + state: 'offline', + epoch: selected.epoch, + target: selected.metadata, + message: errorText(error) + }); + } + } finally { + if (targetTimer) clearInterval(targetTimer); + this.uiAttached = false; + this.rpc?.close(); + this.rpc = null; + } + if (!this.stopped) await delay(RETRY_DELAY_MS); + } + } +} + +const hubs = new Map(); + +function selectionRootFor(options = {}) { + return options.selectionRoot + ?? process.env.MCP_CONTAINER_SELECTION_ROOT + ?? path.join(process.env.XDG_RUNTIME_DIR ?? '/run/user/1000', 'stem/mcp-selected'); +} + +export async function callSelectedNvim(method, parameters = [], options = {}) { + const selectionRoot = selectionRootFor(options); + const hub = hubs.get(selectionRoot); + const pinnedSocketPath = options.socketPath + ? await realpath(options.socketPath) + : null; + if ( + hub?.rpc + && !hub.stopped + && (!pinnedSocketPath || hub.rpc.socketPath === pinnedSocketPath) + ) { + return hub.rpc.call(method, parameters); + } + const socketPath = pinnedSocketPath ?? (await selectedTarget(selectionRoot)).socketPath; + const rpc = new NvimRpcClient(socketPath, () => undefined); + await rpc.connect(); + void rpc.readLoop.catch(() => undefined); + try { + return await rpc.call(method, parameters); + } finally { + rpc.close(); + } +} + +export async function resizeSelectedNvimUi(options = {}) { + const selectionRoot = selectionRootFor(options); + const hub = hubs.get(selectionRoot); + if (!hub?.rpc || hub.stopped || !hub.uiAttached) { + return { attached: false, grid: null }; + } + const grid = await hub.resizeToTerminal(); + await hub.refreshUi(); + return { attached: true, grid }; +} + +export async function selectedNvimTarget(options = {}) { + return selectedTarget(selectionRootFor(options)); +} + +/** + * Attach a browser to the one shared external UI for the selected container. + * The browser never receives the Unix socket and cannot issue arbitrary RPC. + */ +export function attachNvimUi(websocket, options = {}) { + const selectionRoot = selectionRootFor(options); + let hub = hubs.get(selectionRoot); + if (!hub || hub.stopped) { + hub = new NvimUiHub(selectionRoot, () => hubs.delete(selectionRoot)); + hubs.set(selectionRoot, hub); + } + hub.add(websocket); +} diff --git a/scripts/render_card_layouts.sh b/scripts/render_card_layouts.sh new file mode 100755 index 0000000..cabe262 --- /dev/null +++ b/scripts/render_card_layouts.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh +set -eu + +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) +repo_root=$(CDPATH= cd "$script_dir/.." && pwd) + +if [ -n "${CARD_LAYOUTS_ROOT:-}" ]; then + layouts_root=$CARD_LAYOUTS_ROOT +else + layouts_root=$repo_root/../../../tools/card-layouts +fi + +renderer=$layouts_root/tools/render_card.py +if [ ! -f "$renderer" ]; then + echo "Nie znaleziono generatora card-layouts: $renderer" >&2 + echo "Ustaw CARD_LAYOUTS_ROOT na checkout edu-tools/card-layouts." >&2 + exit 1 +fi + +exec python3 "$renderer" "$repo_root" "$@" diff --git a/scripts/render_pdf.sh b/scripts/render_pdf.sh new file mode 100755 index 0000000..349d42c --- /dev/null +++ b/scripts/render_pdf.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env sh +set -eu + +DOC_DIR="doc" +TEX_FILE="${CARD_TEX_FILE:-generated/main.tex}" +METADATA_TEX_FILE="${CARD_PDF_METADATA_TEX_FILE:-main.tex}" + +script_dir=$(CDPATH= cd "$(dirname "$0")" && pwd) +repo_root=$(CDPATH= cd "$script_dir/.." && pwd) +repo_name="${REPO_NAME:-$(basename "$repo_root")}" +tex_path="$repo_root/$DOC_DIR/$TEX_FILE" +metadata_tex_path="$repo_root/$DOC_DIR/$METADATA_TEX_FILE" +tex_dir=$(dirname "$tex_path") +tex_name=$(basename "$tex_path") + +read_tex_command() { + sed -n "s/^\\\\newcommand{\\\\$1}{\\([^}]*\\)}$/\\1/p" "$metadata_tex_path" | head -n 1 +} + +publisher_domain=$(read_tex_command PublisherDomain) +card_area=$(read_tex_command CardArea) +card_series=$(read_tex_command CardSeries) +card_number=$(read_tex_command CardNumber) +card_slug=$(read_tex_command CardSlug) +card_version=$(read_tex_command CardVersion) +document_uuid=$(read_tex_command DocumentUUID) + +if [ -z "$publisher_domain" ] || [ -z "$card_area" ] || [ -z "$card_series" ] || \ + [ -z "$card_number" ] || [ -z "$card_slug" ] || [ -z "$card_version" ] || \ + [ -z "$document_uuid" ]; then + echo "missing PDF filename metadata in $metadata_tex_path" >&2 + exit 1 +fi + +if [ ! -f "$tex_path" ]; then + echo "missing generated card TeX: $tex_path" >&2 + exit 1 +fi + +commit="${GITEA_SHA:-${GITHUB_SHA:-}}" +if [ -z "$commit" ]; then + commit=$(git -C "$repo_root" rev-parse HEAD) +fi +short_commit=$(printf '%s' "$commit" | cut -c1-12) + +meta_file="$tex_dir/build-meta.tex" +tmp_meta="$meta_file.tmp.$$" +cleanup() { + rm -f \ + "$meta_file" \ + "$tmp_meta" \ + "$tex_dir/${tex_name%.tex}.upa" \ + "$tex_dir/${tex_name%.tex}.upb" +} +trap cleanup EXIT HUP INT TERM + +printf '\\newcommand{\\BuildCommit}{%s}\n' "$short_commit" > "$tmp_meta" +mv "$tmp_meta" "$meta_file" + +pdf_out_dir="${PDF_OUT_DIR:-$repo_root/doc/pdf}" +mkdir -p "$pdf_out_dir" +out_dir=$(CDPATH= cd "$pdf_out_dir" && pwd) +pdf_basename="$publisher_domain-$card_area-$card_series-$card_number-$card_slug-$card_version-$document_uuid.pdf" +find "$out_dir" -maxdepth 1 -type f -name '*.pdf' -delete + +( + cd "$tex_dir" + latexmk -pdf -interaction=nonstopmode -halt-on-error -outdir="$out_dir" "$tex_name" +) + +source_pdf="$out_dir/${tex_name%.tex}.pdf" +target_pdf="$out_dir/$pdf_basename" +if [ "$source_pdf" != "$target_pdf" ]; then + mv "$source_pdf" "$target_pdf" +fi + +find "$out_dir" -maxdepth 1 -type f \( \ + -name '*.aux' -o \ + -name '*.log' -o \ + -name '*.out' -o \ + -name '*.fls' -o \ + -name '*.fdb_latexmk' -o \ + -name '*.upa' -o \ + -name '*.upb' \ +\) -delete diff --git a/src/fixtures/container_lab.py b/src/fixtures/container_lab.py new file mode 100755 index 0000000..e62e25e --- /dev/null +++ b/src/fixtures/container_lab.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Small offline state model for teaching container lifecycle; not a container engine.""" +import argparse +import hashlib +import json +import shutil +from pathlib import Path + + +def load(path: Path): + return json.loads(path.read_text()) if path.exists() else {"images": {}, "containers": {}, "volumes": {}} + + +def save(path: Path, state): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(state, indent=2, sort_keys=True) + "\n") + + +def build(state_path: Path, image: str, context: Path): + dockerfile = context / "Dockerfile" + lines = [line.strip() for line in dockerfile.read_text().splitlines() if line.strip() and not line.lstrip().startswith("#")] + instructions = [line.split(None, 1)[0].upper() for line in lines] + allowed = {"FROM", "LABEL", "COPY", "CMD", "ENTRYPOINT", "ENV", "WORKDIR", "USER"} + if not instructions or instructions[0] != "FROM" or "CMD" not in instructions or any(item not in allowed for item in instructions): + raise SystemExit("invalid Dockerfile for offline lesson model") + digest = hashlib.sha256() + for file_path in sorted(path for path in context.rglob("*") if path.is_file()): + digest.update(str(file_path.relative_to(context)).encode()); digest.update(b"\0"); digest.update(file_path.read_bytes()) + value = "sha256:" + digest.hexdigest() + state = load(state_path); state["images"][image] = {"digest": value, "instructions": instructions}; save(state_path, state) + print(f"{value} instructions={len(instructions)}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("state", type=Path) + sub = parser.add_subparsers(dest="command", required=True) + p = sub.add_parser("build"); p.add_argument("image"); p.add_argument("context", type=Path) + p = sub.add_parser("image-inspect"); p.add_argument("image") + p = sub.add_parser("run"); p.add_argument("name"); p.add_argument("image"); p.add_argument("--volume"); p.add_argument("--bind", type=Path) + p = sub.add_parser("inspect"); p.add_argument("name") + p = sub.add_parser("stop"); p.add_argument("name") + p = sub.add_parser("rm"); p.add_argument("name") + p = sub.add_parser("volume-create"); p.add_argument("name") + p = sub.add_parser("volume-path"); p.add_argument("name") + p = sub.add_parser("volume-rm"); p.add_argument("name") + args = parser.parse_args(); state = load(args.state) + if args.command == "build": build(args.state, args.image, args.context); return + if args.command == "image-inspect": print(json.dumps(state["images"][args.image], sort_keys=True)); return + if args.command == "volume-create": + path = args.state.parent / "volumes" / args.name; path.mkdir(parents=True, exist_ok=True); state["volumes"][args.name] = str(path); save(args.state, state); print(path); return + if args.command == "volume-path": print(state["volumes"][args.name]); return + if args.command == "volume-rm": + path = Path(state["volumes"].pop(args.name)); shutil.rmtree(path); save(args.state, state); print("removed"); return + if args.command == "run": + if args.image not in state["images"] or args.name in state["containers"]: raise SystemExit("invalid image or duplicate container") + if args.volume and args.volume not in state["volumes"]: raise SystemExit("unknown volume") + state["containers"][args.name] = {"image": args.image, "status": "running", "volume": args.volume, "bind": str(args.bind) if args.bind else None}; save(args.state, state); print("running"); return + container = state["containers"].get(args.name) + if not container: raise SystemExit("unknown container") + if args.command == "inspect": print(container["status"]); return + if args.command == "stop": container["status"] = "exited"; save(args.state, state); print("exited"); return + if args.command == "rm": + if container["status"] == "running": raise SystemExit("stop container before rm") + del state["containers"][args.name]; save(args.state, state); print("removed"); return + + +if __name__ == "__main__": main() diff --git a/src/tasks/task01_build_image_spec.sh b/src/tasks/task01_build_image_spec.sh new file mode 100755 index 0000000..ecb5996 --- /dev/null +++ b/src/tasks/task01_build_image_spec.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +root="${CARD_ROOT:?CARD_ROOT required}"; work="$(mktemp -d)"; trap 'rm -rf -- "$work"' EXIT +mkdir "$work/context"; printf 'hello from image\n' >"$work/context/hello.txt" +printf 'FROM scratch\nLABEL lesson="console"\nCOPY hello.txt /hello.txt\nCMD ["cat", "/hello.txt"]\n' >"$work/context/Dockerfile" +result="$(python3 "$root/src/fixtures/container_lab.py" "$work/state.json" build lesson:v1 "$work/context")" +digest="${result%% *}"; [[ "$digest" =~ ^sha256:[0-9a-f]{64}$ && "$result" == *'instructions=4' ]] +python3 "$root/src/fixtures/container_lab.py" "$work/state.json" image-inspect lesson:v1 | grep -q '"CMD"' +printf 'PASS task01 syntax=valid instructions=4 digest=%s engine=offline-model socket_exposed=0\n' "$digest" diff --git a/src/tasks/task02_container_lifecycle.sh b/src/tasks/task02_container_lifecycle.sh new file mode 100755 index 0000000..8d6dae1 --- /dev/null +++ b/src/tasks/task02_container_lifecycle.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +root="${CARD_ROOT:?CARD_ROOT required}"; work="$(mktemp -d)"; trap 'rm -rf -- "$work"' EXIT +mkdir "$work/context"; printf 'payload\n' >"$work/context/file"; printf 'FROM scratch\nCOPY file /file\nCMD ["cat", "/file"]\n' >"$work/context/Dockerfile" +lab=(python3 "$root/src/fixtures/container_lab.py" "$work/state.json") +"${lab[@]}" build lesson:v1 "$work/context" >/dev/null; [[ "$("${lab[@]}" run lesson-1 lesson:v1)" == running ]]; [[ "$("${lab[@]}" inspect lesson-1)" == running ]] +[[ "$("${lab[@]}" stop lesson-1)" == exited ]]; [[ "$("${lab[@]}" inspect lesson-1)" == exited ]]; [[ "$("${lab[@]}" rm lesson-1)" == removed ]] +if "${lab[@]}" inspect lesson-1 >/dev/null 2>&1; then exit 1; fi +printf 'PASS task02 image=lesson:v1 running=1 inspected=running stopped=exited removed=1 engine=offline-model\n' diff --git a/src/tasks/task03_volume_and_bind.sh b/src/tasks/task03_volume_and_bind.sh new file mode 100755 index 0000000..7b02c04 --- /dev/null +++ b/src/tasks/task03_volume_and_bind.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail +root="${CARD_ROOT:?CARD_ROOT required}"; work="$(mktemp -d)"; trap 'rm -rf -- "$work"' EXIT +mkdir "$work/context" "$work/bind"; printf 'payload\n' >"$work/context/file"; printf 'FROM scratch\nCOPY file /file\nCMD ["cat", "/file"]\n' >"$work/context/Dockerfile" +lab=(python3 "$root/src/fixtures/container_lab.py" "$work/state.json") +"${lab[@]}" build lesson:v1 "$work/context" >/dev/null; volume_path="$("${lab[@]}" volume-create lesson-data)" +"${lab[@]}" run lesson-1 lesson:v1 --volume lesson-data --bind "$work/bind" >/dev/null +printf 'counter=1\n' >"$volume_path/state.txt"; printf 'report=ready\n' >"$work/bind/report.txt" +"${lab[@]}" stop lesson-1 >/dev/null; "${lab[@]}" rm lesson-1 >/dev/null +[[ "$(cat "$volume_path/state.txt")" == 'counter=1' && "$(cat "$work/bind/report.txt")" == 'report=ready' ]] +"${lab[@]}" volume-rm lesson-data >/dev/null; [[ ! -e "$volume_path" && -f "$work/bind/report.txt" ]] +printf 'PASS task03 volume_persists_after_container_rm=1 bind_persists=1 volume_removed_explicitly=1 engine=offline-model\n' diff --git a/stem-card.yaml b/stem-card.yaml new file mode 100644 index 0000000..61d66a6 --- /dev/null +++ b/stem-card.yaml @@ -0,0 +1,9 @@ +schema: 1 +targets: + native: {profile: native-amd64, actions: [build, test, run, debug]} +actions: + build: [bash, tools/card-action.sh, build] + test: [bash, tools/card-action.sh, test] + run: [bash, tools/card-action.sh, run] + debug: [bash, tools/card-action.sh, debug] +artifacts: {directory: .stem/artifacts} diff --git a/tests/card_contract.test.mjs b/tests/card_contract.test.mjs new file mode 100644 index 0000000..80d9030 --- /dev/null +++ b/tests/card_contract.test.mjs @@ -0,0 +1,25 @@ +import assert from 'node:assert/strict'; +import { access, readFile } from 'node:fs/promises'; +import test from 'node:test'; + +const source = JSON.parse(await readFile(new URL('../json/card_source.json', import.meta.url), 'utf8')); + +test('L08 identity and task order are stable', () => { + assert.equal(source.card.number, '08'); + assert.equal(source.card.uuid, 'b6d3b00d-6912-502d-95d7-3ee47c60f1db'); + assert.deepEqual(source.tasks_order, ['task01', 'task02', 'task03']); +}); + +test('engine boundary and lifecycle are explicit', () => { + const text = JSON.stringify(source); + for (const token of ['offline-model', 'socket_exposed=0', 'build', 'running', 'exited', 'volume', 'bind', 'trace.log']) { + assert.match(text, new RegExp(token, 'i')); + } + assert.match(text, /nie oznacza uruchomienia prawdziwego silnika/i); +}); + +test('all task sources exist', async () => { + for (const name of ['task01_build_image_spec', 'task02_container_lifecycle', 'task03_volume_and_bind']) { + await access(new URL(`../src/tasks/${name}.sh`, import.meta.url)); + } +}); diff --git a/tests/card_state_db.test.mjs b/tests/card_state_db.test.mjs new file mode 100644 index 0000000..be502eb --- /dev/null +++ b/tests/card_state_db.test.mjs @@ -0,0 +1,67 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, rm } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { CardStateDatabase } from '../scripts/lib/card_state_db.mjs'; + +test('card state survives closing and reopening SQLite', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'stem-card-state-')); + const file = path.join(directory, 'state.sqlite3'); + const state = { + schema: 'stem-card-navigation.v2', + revision: 7, + selected_at: '2026-07-17T12:00:00.000Z', + snapshot_ref: 'task04.alloc5.cursor' + }; + + try { + const first = new CardStateDatabase(file); + first.write('navigation', state); + first.close(); + + const second = new CardStateDatabase(file); + assert.deepEqual(second.read('navigation'), state); + assert.equal(second.read('missing'), null); + second.close(); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); + +test('audit log is append-only, ordered and keeps navigation timestamps', async () => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'stem-card-audit-')); + const file = path.join(directory, 'state.sqlite3'); + try { + const database = new CardStateDatabase(file); + const navigation = { + task: { id: 'task04' }, + block: { id: 'allocator-flow' }, + phase: { id: 'alloc5' }, + step: { id: 'alloc5-commit' }, + snapshot_ref: 'task04.alloc5.commit' + }; + const first = database.appendEvent({ + eventType: 'navigation.activate', + actor: 'nvim', + navigation, + occurredAt: '2026-07-17T12:00:00.000Z', + payload: { source: 'F2' } + }); + const second = database.appendEvent({ + eventType: 'checkpoint.ready', + actor: 'nvim', + navigation, + occurredAt: '2026-07-17T12:00:01.000Z' + }); + assert.equal(second.id, first.id + 1); + assert.deepEqual( + database.listEvents({ afterId: first.id, limit: 10 }).map(event => event.event_type), + ['checkpoint.ready'] + ); + assert.equal(database.listEvents()[0].snapshot_ref, navigation.snapshot_ref); + database.close(); + } finally { + await rm(directory, { recursive: true, force: true }); + } +}); diff --git a/tests/test_tasks.sh b/tests/test_tasks.sh new file mode 100755 index 0000000..2033530 --- /dev/null +++ b/tests/test_tasks.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +root="${CARD_ROOT:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)}"; task="${1:?task required}" +case "$task" in task01_build_image_spec|task02_container_lifecycle|task03_volume_and_bind) ;; *) exit 2 ;; esac +exec env CARD_ROOT="$root" "$root/src/tasks/$task.sh" diff --git a/tools/card-action.sh b/tools/card-action.sh new file mode 100755 index 0000000..fe38eeb --- /dev/null +++ b/tools/card-action.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail +action="${1:?action required}"; root="${STEM_REPO:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)}"; profile="${STEM_PROFILE:-native-amd64}"; target="${STEM_TARGET:-native}"; selector="${STEM_TASK:-task1}" +tasks=(task01_build_image_spec task02_container_lifecycle task03_volume_and_bind) +resolve_task(){ local value="${1,,}" name; for name in "${tasks[@]}"; do [[ "$value" == "$name" ]] && { printf '%s\n' "$name"; return; }; done; [[ "$value" =~ ^(task|t)?[-_]?0*([1-3])($|[-_].*) ]] && { printf '%s\n' "${tasks[${BASH_REMATCH[2]} - 1]}"; return; }; exit 2; } +task="$(resolve_task "$selector")"; artifact_dir="$root/.stem/artifacts/$profile/$target/$task"; mkdir -p "$artifact_dir" +case "$profile:$target:$action" in + native-amd64:native:build) bash -n "$root"/src/tasks/*.sh "$root/tests/test_tasks.sh"; python3 -m py_compile "$root/src/fixtures/container_lab.py"; printf 'BUILD tasks=3 engine=offline-model socket_exposed=0\n' ;; + native-amd64:native:test|native-amd64:native:run) exec env CARD_ROOT="$root" "$root/tests/test_tasks.sh" "$task" ;; + native-amd64:native:debug) trace_file="$artifact_dir/trace.log"; set +e; env CARD_ROOT="$root" bash -x "$root/tests/test_tasks.sh" "$task" >"$trace_file" 2>&1; status=$?; set -e; cat "$trace_file"; [[ $status -eq 0 && -s "$trace_file" ]]; printf 'TRACE %s log=%s\n' "$task" "$trace_file" ;; + *) exit 2 ;; +esac diff --git a/web/app.css b/web/app.css new file mode 100644 index 0000000..388a0ab --- /dev/null +++ b/web/app.css @@ -0,0 +1 @@ +.paper{display:flex;flex-direction:column;gap:3.3cm}.paper>.sheet,.paper>.sheet-spread>.sheet{width:210mm;height:297mm;min-height:297mm;max-height:297mm;margin:0 auto;overflow:hidden}.paper>.sheet.sheet--landscape,.paper>.sheet-spread>.sheet.sheet--landscape{width:297mm;min-width:297mm;max-width:297mm;height:210mm;min-height:210mm;max-height:210mm}.paper>.sheet>.sheet-content,.paper>.sheet-spread>.sheet>.sheet-content{box-sizing:border-box;height:297mm;min-height:297mm;max-height:297mm;overflow-x:hidden;overflow-y:hidden}.paper>.sheet.sheet--landscape>.sheet-content,.paper>.sheet-spread>.sheet.sheet--landscape>.sheet-content{height:210mm;min-height:210mm;max-height:210mm}.page-resource-footer{position:absolute;z-index:4;right:21.5mm;bottom:3mm;left:21.5mm;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:8px;min-height:5mm;border-top:.35mm solid #252525;background:#fff;padding-top:1.2mm;color:#68747a;font:650 7px/1.15 ui-monospace,monospace}.page-resource-footer__uuid{overflow:hidden;color:#4d5d65;text-align:left;text-overflow:ellipsis;white-space:nowrap;font:inherit}.page-resource-footer__author{color:#4d5d65;text-align:center;white-space:nowrap}.page-resource-footer__page{color:#283940;text-align:right;white-space:nowrap}.sheet-spread{display:contents}.paper.is-diagram-spread .sheet-spread{display:grid;grid-template-columns:repeat(var(--spread-columns, 1),max-content);width:max-content;max-width:none;margin:0 auto;align-items:flex-start;gap:2mm;zoom:var(--spread-fit-scale, 1)}.paper.is-diagram-spread .sheet-spread>.sheet{flex:0 0 auto}.viewer-chrome{position:sticky;top:0;z-index:80;width:100%}.viewer-action-toast{position:absolute;z-index:12;right:9px;bottom:9px;max-width:min(720px,calc(100vw - 18px));box-sizing:border-box;overflow:hidden;border:1px solid #42b8e7;border-radius:3px;background:#102c38ee;color:#dff7ff;padding:5px 9px;box-shadow:0 4px 14px #0007;text-overflow:ellipsis;white-space:nowrap;font:750 10px/1.25 ui-monospace,monospace}.viewer-chrome .topbar{position:sticky;top:0;z-index:90}.viewpoint-bar{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:1px;min-height:31px;border-top:1px solid #9aa8ae;border-bottom:1px solid #718087;background:#718087;box-shadow:0 2px 5px #0002}.viewpoint-bar button{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:5px;min-width:0;border:0;background:#f7f9fa;color:#1f3038;padding:5px 7px;text-align:left;cursor:pointer}.viewpoint-bar button:hover,.viewpoint-bar button:focus-visible{z-index:1;background:#e9f3f7;outline:2px solid #287495;outline-offset:-2px}.viewpoint-bar button[aria-current=location]{background:#dcecf3;box-shadow:inset 0 -3px #167ca4}.viewpoint-bar button>span{color:#1b6785;font:850 10px/1 ui-monospace,monospace}.viewpoint-bar button>strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:750 10px/1.1 system-ui,sans-serif}.viewpoint-bar button.is-unavailable{background:#eef0f1;color:#5d686d;cursor:not-allowed}.viewpoint-bar button.is-unavailable>span{color:#68747a}.viewpoint-bar button.is-unavailable>small{border:1px solid #9da7ab;border-radius:2px;padding:1px 3px;font:750 7px/1 ui-monospace,monospace}.card-library-toggle{display:inline-flex;align-items:center;flex:none;height:28px;gap:6px;border:1px solid #6e7d84;border-radius:3px;background:#f6f8f9;color:#26373f;padding:2px 8px;font:750 11px/1 ui-monospace,monospace;cursor:pointer}.card-library-toggle:hover,.card-library-toggle:focus-visible{border-color:#1d7799;background:#edf7fa;outline:none}.card-library-toggle.is-active{border-color:#126d8f;background:#174f67;color:#fff}.card-library-toggle-icon{font:700 16px/1 system-ui,sans-serif}.card-library-toggle kbd{color:#267697;font:inherit}.card-library-toggle.is-active kbd{color:#aee9ff}.runtime-source-selector{display:inline-flex;align-items:center;flex:none;height:28px;box-sizing:border-box;gap:5px;border:1px solid #6e7d84;border-radius:3px;background:#edf2f4;color:#44545b;padding-left:7px;font:750 9px/1 ui-monospace,monospace;letter-spacing:.05em}.runtime-source-selector select{height:26px;min-width:17ch;max-width:19ch;border:0;border-left:1px solid #a8b4b9;border-radius:0 2px 2px 0;background:#fff;color:#174f67;padding:2px 22px 2px 7px;font:800 10px/1 ui-monospace,monospace;cursor:pointer}.runtime-source-selector select:hover,.runtime-source-selector select:focus-visible{background:#edf7fa;outline:2px solid #287495;outline-offset:-2px}.runtime-source-selector option:disabled{color:#7d898e}:root{--side-panel-top: 36px;--side-panel-width: min(960px, calc(100vw - 28px) );--side-panel-trigger-length: 56px}.side-panel-dock{position:fixed;z-index:240;top:var(--side-panel-top);bottom:0;width:0;pointer-events:none}.side-panel-dock--left{left:0}.side-panel-dock--right{right:0}.side-panel-rail{position:fixed;z-index:243;top:calc(var(--side-panel-top) + 8px);display:flex;width:13px;flex-direction:column;gap:10px;pointer-events:auto}.side-panel-dock--left .side-panel-rail{left:0}.side-panel-dock--right .side-panel-rail{right:0}.side-panel-trigger{--side-panel-accent: #167ca4;position:relative;display:grid;width:13px;height:var(--side-panel-trigger-length);place-items:center;border:0;border-radius:0;background:transparent;padding:0;cursor:pointer}.side-panel-trigger:before{content:"";display:block;width:3px;height:calc(var(--side-panel-trigger-length) - 8px);border-radius:999px;background:var(--side-panel-accent);box-shadow:0 0 0 1px #ffffff73,0 3px 10px -5px var(--side-panel-accent);opacity:.78;transition:width .14s ease,opacity .14s ease,box-shadow .14s ease}.side-panel-trigger:hover:before,.side-panel-trigger:focus-visible:before,.side-panel-trigger.is-open:before{width:4px;opacity:1}.side-panel-trigger.is-pinned:before{width:5px;box-shadow:0 0 0 1px #ffffff9e,0 3px 12px -4px var(--side-panel-accent)}.side-panel-trigger:focus-visible{outline:2px solid var(--side-panel-accent);outline-offset:-2px}.side-panel-trigger>span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}.side-panel{position:fixed;z-index:241;top:var(--side-panel-top);bottom:0;display:flex;width:var(--side-panel-width);max-width:calc(100vw - 20px);box-sizing:border-box;flex-direction:column;overflow:hidden;border-color:#819098;background:#f7f9fa;color:#17252c;pointer-events:none;transition:transform .18s cubic-bezier(.22,.78,.24,1);will-change:transform}.side-panel-dock--left .side-panel{left:0;border-right:1px solid #819098;box-shadow:15px 0 34px -24px #101b218c;transform:translate(calc(-100% - 14px))}.side-panel-dock--right .side-panel{right:0;border-left:1px solid #819098;box-shadow:-15px 0 34px -24px #101b218c;transform:translate(calc(100% + 14px))}.side-panel.is-open{transform:translate(0);pointer-events:auto}.side-panel:after{content:"";position:absolute;top:10px;width:4px;height:48px;border-radius:999px;background:var(--side-panel-accent);pointer-events:none}.side-panel-dock--left .side-panel:after{right:-2px}.side-panel-dock--right .side-panel:after{left:-2px}.side-panel-header{display:flex;min-height:38px;box-sizing:border-box;align-items:center;justify-content:space-between;gap:10px;border-bottom:1px solid #aebbc1;background:#e7edef;padding:4px 7px 4px 10px}.side-panel-header>div:first-child{display:flex;min-width:0;align-items:baseline;gap:7px}.side-panel-header small{color:#52666f;font:600 16px/1 ui-monospace,monospace;letter-spacing:.08em}.side-panel-header strong{overflow:hidden;color:#11191d;text-overflow:ellipsis;white-space:nowrap;font:600 16px/1.2 system-ui,sans-serif}.side-panel-actions{display:flex;flex:none;gap:4px}.side-panel-actions button{display:grid;min-width:38px;height:24px;place-items:center;border:1px solid #8b999f;border-radius:3px;background:#fff;color:#26373f;padding:0 6px;font:650 16px/1 ui-monospace,monospace;cursor:pointer}.side-panel-actions button:last-child{min-width:24px;padding:0;font-size:18px}.side-panel-actions button:hover,.side-panel-actions button:focus-visible,.side-panel-actions button.is-pinned{border-color:var(--side-panel-accent);background:#edf7fa;outline:none}.side-panel-body,.card-library-panel{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden}.card-library-header{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:18px 18px 15px;border-bottom:1px solid #bdc9ce;background:#e8eef1}.card-library-header small{color:#52666f;font:600 16px/1.25 ui-monospace,monospace;letter-spacing:.09em;text-transform:uppercase}.card-library-header h2{margin:4px 0 2px;font:750 22px/1.15 system-ui,sans-serif}.card-library-header p{margin:0;color:#607078;font:500 16px/1.35 system-ui,sans-serif}.card-library-header>button{display:grid;width:34px;height:34px;place-items:center;flex:none;border:1px solid #77878e;border-radius:3px;background:#fff;color:#25353c;padding:0;font:25px/1 system-ui,sans-serif;cursor:pointer}.card-library-header>button:hover,.card-library-header>button:focus-visible{border-color:#126d8f;background:#edf7fa;outline:2px solid #85c5dc;outline-offset:1px}.card-library-tree{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:6px 0 18px;scrollbar-color:#8a9aa1 #edf1f3}.card-library-tree details{border:0}.card-library-tree summary{display:grid;grid-template-columns:14px minmax(0,1fr) auto;align-items:center;gap:6px;min-height:30px;box-sizing:border-box;padding:5px 10px 5px 4px;list-style:none;cursor:pointer;user-select:none}.card-library-tree summary::-webkit-details-marker{display:none}.card-library-tree summary:hover,.card-library-tree summary:focus-visible{background:#eaf1f4;outline:none}.card-library-branch{color:#25363d;font:16px/1 ui-monospace,monospace;transform:rotate(-90deg);transition:transform .12s ease}.card-library-tree details[open]>summary .card-library-branch{transform:rotate(0)}.card-library-tree summary strong,.card-library-tree summary small{display:block}.card-library-tree summary strong{color:#070a0b;font:550 16px/1.3 system-ui,sans-serif}.card-library-tree summary small{margin-top:2px;color:#303b40;font:500 16px/1.25 ui-monospace,monospace;letter-spacing:.04em}.card-library-tree summary output{min-width:22px;color:#26363d;padding:0 2px;text-align:center;font:550 16px/1.2 ui-monospace,monospace}.card-library-subject>summary{min-height:42px;border-bottom:1px solid #9eacb2;background:#dfe8ec;padding:7px 14px}.card-library-subject>summary:hover,.card-library-subject>summary:focus-visible{background:#d5e1e5}.card-library-subject>summary strong{color:#000;font:600 16px/1.3 system-ui,sans-serif}.card-library-subject>summary small{color:#28353a;font:500 16px/1.25 ui-monospace,monospace}.card-library-level-list{margin-left:18px;border-left:1px solid #8e9da4}.card-library-level>summary{min-height:38px;border-bottom:1px solid #cbd4d8;padding-left:8px}.card-library-level>summary strong{color:#000;font:550 16px/1.3 system-ui,sans-serif}.card-library-series-list{margin-left:18px;border-left:1px solid #a5b1b6;padding:2px 0 6px}.card-library-series>summary{min-height:34px;padding-left:8px}.card-library-series>summary strong{color:#000;font:550 16px/1.3 system-ui,sans-serif}.card-library-series>summary small{color:#26343a;font:500 16px/1.25 ui-monospace,monospace}.card-library-cards,.card-library-tasks{list-style:none;margin-top:0;margin-bottom:2px;padding:0}.card-library-cards{margin-left:18px;border-left:1px solid #bac3c7}.card-library-card-entry{margin:0}.card-library-card{display:grid;grid-template-columns:19px minmax(0,1fr) auto;align-items:start;gap:5px;min-height:38px;box-sizing:border-box;border:0;border-left:3px solid transparent;color:#000;padding:5px 8px 5px 3px;text-decoration:none}.card-library-node{color:#536168;font:16px/1.35 ui-monospace,monospace}.card-library-card-copy,.card-library-card-copy strong,.card-library-card-copy>span{display:block;min-width:0}.card-library-card-copy strong{color:#000;font:550 16px/1.3 ui-monospace,monospace}.card-library-card-copy>span{margin-top:2px;color:#0b0d0e;font:500 16px/1.35 system-ui,sans-serif;white-space:normal}.card-library-card-meta{display:flex;align-items:flex-end;flex-direction:column;gap:2px}.card-library-card-meta small{color:#26343a;font:500 16px/1.25 ui-monospace,monospace}.card-library-card.is-available{cursor:pointer}.card-library-card.is-available:hover,.card-library-card.is-available:focus-visible{background:#eaf5f8;outline:none}.card-library-card.is-current{border-left-color:#16769b;background:#dff1f7}.card-library-card.is-current:hover,.card-library-card.is-current:focus-visible{background:#cfeaf3;outline:1px solid #5ba9c4;outline-offset:-1px}.card-library-card.is-unavailable{color:#000}.card-library-card.is-unavailable .card-library-card-copy strong,.card-library-card.is-unavailable .card-library-card-meta small{color:#1d292e}.card-library-card.is-unavailable .card-library-card-copy>span{color:#0b0d0e}.card-library-tasks{margin-left:36px;border-left:1px solid #c3cacc}.card-library-tasks li>a,.card-library-tasks li>span{display:grid;grid-template-columns:19px auto minmax(0,1fr);gap:5px;align-items:start;color:#000;padding:4px 8px 5px 3px;text-decoration:none}.card-library-tasks li>a:hover,.card-library-tasks li>a:focus-visible{background:#eaf5f8;outline:none}.card-library-tasks strong{color:#000;font:550 16px/1.35 ui-monospace,monospace}.card-library-tasks li>a>span:last-child,.card-library-tasks li>span>span:last-child{color:#111;font:500 16px/1.35 system-ui,sans-serif}.card-library-footer{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:10px 16px;border-top:1px solid #bdc9ce;background:#edf1f3;color:#5a6a72;font:500 16px/1.3 system-ui,sans-serif}.card-library-footer span{display:inline-flex;align-items:center;gap:4px}.card-library-footer i{width:7px;height:7px;border-radius:50%;background:#929da2}.card-library-footer i.is-online{background:#26815b}.card-library-footer kbd{margin-left:auto;border:1px solid #a2afb5;border-radius:2px;background:#fff;padding:2px 5px;font:600 16px/1 ui-monospace,monospace}.card-library-treegrid{min-height:0;flex:1;overflow:auto;margin:8px;border:1px solid rgb(31 35 40 / 18%);border-radius:6px;background:#fff;scrollbar-color:#93a2a8 #edf1f2;scrollbar-width:thin}.card-library-treegrid-row{display:grid;min-width:760px;min-height:38px;box-sizing:border-box;grid-template-columns:minmax(500px,1fr) 110px 120px;align-items:stretch;border-bottom:1px solid rgb(31 35 40 / 9%);color:#273941;font:500 16px/1.25 ui-monospace,monospace}.card-library-treegrid-row>[role=gridcell],.card-library-treegrid-row>[role=columnheader]{display:flex;min-width:0;align-items:center;overflow:hidden;padding:7px 9px;text-overflow:ellipsis;white-space:nowrap}.card-library-treegrid-row>[role=gridcell]+[role=gridcell],.card-library-treegrid-row>[role=columnheader]+[role=columnheader]{border-left:1px solid rgb(31 35 40 / 9%)}.card-library-treegrid-head{position:sticky;z-index:3;top:0;min-height:34px;border-bottom-color:#1f232833;background:#eef1f2;color:#3e525b;font-weight:700;letter-spacing:.045em}.card-library-treegrid-row:not(.card-library-treegrid-head):hover{background:#1f23280a}.card-library-treegrid-row.is-subject{background:#dfe8e5}.card-library-treegrid-row.is-level{background:#f5f8f8}.card-library-treegrid-row.is-series{background:#f9fbfb}.card-library-treegrid-row.is-card{min-height:42px;background:#fff}.card-library-treegrid-row.is-task{background:#fbfcfc}.card-library-treegrid-row.is-current{box-shadow:inset 3px 0 #1680a5;background:#e1f1f6}.card-library-tree-title{gap:6px;padding-left:calc(8px + (var(--card-library-depth, 0) * 21px))!important;font-family:system-ui,sans-serif}.card-library-tree-expander{display:inline-grid;width:24px;height:24px;flex:none;place-items:center;border:0;background:transparent;color:#405961;padding:0;font:18px/1 ui-monospace,monospace;cursor:pointer}.card-library-tree-expander:hover,.card-library-tree-expander:focus-visible{background:#165d6b1a;color:#165d6b;outline:1px solid rgb(22 93 107 / 28%)}.card-library-tree-expander.is-empty{cursor:default}.card-library-tree-link,.card-library-tree-label{display:flex;min-width:0;align-items:baseline;gap:7px;overflow:hidden;color:#10181c;text-decoration:none;white-space:nowrap}.card-library-tree-link:hover,.card-library-tree-link:focus-visible{color:#075d7c;outline:none;text-decoration:underline;text-underline-offset:2px}.card-library-tree-link strong,.card-library-tree-label strong{flex:none;color:inherit;font:700 16px/1.25 ui-monospace,monospace}.card-library-tree-link>span,.card-library-tree-label>span{min-width:0;overflow:hidden;color:inherit;text-overflow:ellipsis;white-space:nowrap;font:500 16px/1.25 system-ui,sans-serif}.card-library-treegrid-row.is-subject .card-library-tree-label strong,.card-library-treegrid-row.is-level .card-library-tree-label strong,.card-library-treegrid-row.is-series .card-library-tree-label strong{font-family:system-ui,sans-serif;font-weight:650}.card-library-tree-state{display:inline-flex;min-width:76px;min-height:24px;box-sizing:border-box;align-items:center;justify-content:center;border:1px solid transparent;border-radius:5px;color:#33474f;padding:2px 7px;font:650 14px/1 ui-monospace,monospace;white-space:nowrap}.card-library-tree-state[data-state=current]{border-color:#3184a0;background:#e9f5f8;color:#075d7c}.card-library-tree-state[data-state=online]{border-color:#4b9a7a;background:#edf8f2;color:#176144}.card-library-tree-state[data-state=plan]{border-color:#c59a49;background:#fff7e5;color:#775313}.card-library-treegrid-row.is-unavailable .card-library-tree-label,.card-library-treegrid-row.is-unavailable .card-library-tree-label strong{color:#10181c}.side-toc,.side-progress{min-height:0;flex:1;overflow-y:auto;background:#fff;color:#11191d}.side-toc>header,.side-progress>header{padding:16px 18px 13px;border-bottom:1px solid #bdc9ce;background:#f0f4f5}.side-toc>header small,.side-progress>header small{color:#53666f;font:750 9px/1 ui-monospace,monospace;letter-spacing:.08em}.side-toc>header h2,.side-progress>header h2{margin:4px 0 0;color:#10181c;font:750 21px/1.15 system-ui,sans-serif}.side-toc ol{margin:0;padding:7px 0 20px;list-style:none}.side-toc li{border-bottom:1px solid #e2e7e9}.side-toc a{display:grid;grid-template-columns:34px minmax(0,1fr);gap:9px;align-items:baseline;padding:10px 16px;color:#11191d;text-decoration:none}.side-toc a:hover,.side-toc a:focus-visible{background:#eaf5f8;outline:none}.side-toc a span{color:#267493;font:800 10px/1 ui-monospace,monospace}.side-toc a strong{font:650 13px/1.3 system-ui,sans-serif}.side-progress-summary{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:16px;border:1px solid #b6c2c7;background:#f4f7f8;padding:14px;font:650 13px/1.3 system-ui,sans-serif}.side-progress-summary strong{color:#1d617b;font:850 20px/1 ui-monospace,monospace}.side-progress-links{display:grid;gap:8px;padding:0 16px 18px}.side-progress-links a{border:1px solid #87979e;border-radius:3px;background:#fff;color:#174f67;padding:10px 12px;text-decoration:none;font:700 12px/1.2 system-ui,sans-serif}.side-progress-links a:hover,.side-progress-links a:focus-visible{border-color:#167ca4;background:#edf7fa;outline:none}.side-progress-error{margin:0 16px 14px;border-left:3px solid #a52d2d;background:#fff1f1;color:#7d1f1f;padding:8px 10px;font:12px/1.35 system-ui,sans-serif}.classroom-panel{display:flex;min-height:0;flex:1;flex-direction:column;overflow:hidden;background:#f7f9fa;color:#11191d}.classroom-treegrid{min-height:0;flex:1;overflow:auto;margin:8px;border:1px solid rgb(31 35 40 / 18%);border-radius:6px;background:#fff;scrollbar-color:#93a2a8 #edf1f2;scrollbar-width:thin}.classroom-treegrid-row{display:grid;min-width:920px;box-sizing:border-box;grid-template-columns:minmax(410px,1fr) 100px 150px 150px 110px;align-items:center;border-bottom:1px solid rgb(31 35 40 / 8%);color:#273941;font:500 16px/1.25 ui-monospace,monospace}.classroom-treegrid-row>[role=gridcell],.classroom-treegrid-row>[role=columnheader]{min-width:0;overflow:hidden;padding:8px;text-overflow:ellipsis;white-space:nowrap}.classroom-treegrid-row>[role=gridcell]+[role=gridcell],.classroom-treegrid-row>[role=columnheader]+[role=columnheader]{border-left:1px solid rgb(31 35 40 / 8%)}.classroom-treegrid-head{position:sticky;z-index:2;top:0;border-bottom-color:#1f23282e;background:#eef1f2;color:#3e525b;font-weight:650;letter-spacing:.04em}.classroom-node-row:hover{background:#1f23280a}.classroom-node-row.is-class{background:#dfe8e5}.classroom-node-row.is-student{background:#f8faf9}.classroom-node-row.is-task{background:#edf3f5}.classroom-node-row.is-block,.classroom-node-row.is-exercises{background:#f5f7f8}.classroom-node-row.is-phase{background:#fafbfb}.classroom-node-title{display:flex;align-items:center;gap:6px;padding-left:calc(8px + (var(--classroom-depth, 0) * 20px))!important;font-family:system-ui,sans-serif}.classroom-node-title strong{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:16px;font-weight:500}.classroom-node-row.is-class .classroom-node-title strong,.classroom-node-row.is-student .classroom-node-title strong,.classroom-node-row.is-task .classroom-node-title strong{font-weight:650}.classroom-expander{display:inline-grid;width:24px;height:24px;flex:none;place-items:center;border:0;background:transparent;color:#405961;padding:0;font:18px/1 ui-monospace,monospace;cursor:pointer}.classroom-expander:hover,.classroom-expander:focus-visible{background:#165d6b1a;color:#165d6b;outline:1px solid rgb(22 93 107 / 28%)}.classroom-expander.is-empty{cursor:default}.classroom-status,.classroom-sync,.classroom-visit,.classroom-commit{display:inline-flex;min-width:92px;box-sizing:border-box;align-items:center;justify-content:center;border:1px solid;border-radius:5px;padding:4px 6px;font:550 16px/1 ui-monospace,monospace;letter-spacing:.015em}.classroom-status[data-status=online]{border-color:#6b9a87;background:#e7f4ee;color:#155e43}.classroom-status[data-status=away]{border-color:#baa06a;background:#fff5d9;color:#765510}.classroom-status[data-status=offline]{border-color:#a5afb3;background:#f0f2f3;color:#596970}.classroom-status[data-status=error]{border-color:#bd7b7b;background:#fff0f0;color:#8c2929}.classroom-sync[data-sync=follow]{border-color:#6c98aa;background:#e8f3f7;color:#155c77}.classroom-sync[data-sync=paused]{border-color:#baa06a;background:#fff6df;color:#765510}.classroom-sync[data-sync=local]{border-color:#9a86ae;background:#f4eef9;color:#65427c}.classroom-visit[data-visit=present],.classroom-commit[data-commit=committed]{border-color:#6b9a87;background:#e7f4ee;color:#155e43}.classroom-visit[data-visit=missed],.classroom-commit[data-commit=missing]{border-color:#bd7b7b;background:#fff0f0;color:#8c2929}.classroom-visit[data-visit=pending],.classroom-commit[data-commit=working]{border-color:#baa06a;background:#fff6df;color:#765510}.classroom-legend{display:flex;flex:none;flex-wrap:wrap;gap:7px 12px;border-top:1px solid #aebbc1;background:#edf1f2;padding:9px 11px;color:#485c65;font:500 16px/1.35 system-ui,sans-serif}.classroom-legend span{display:inline-flex;align-items:center;gap:4px}.classroom-legend i{width:12px;height:12px;border:1px solid #73858c;border-radius:50%;background:#aeb7bb}.classroom-legend i[data-status=online]{border-color:#3e7c63;background:#45a27b}.classroom-legend i[data-status=away]{border-color:#a0782d;background:#dcae51}.classroom-legend i[data-status=offline]{border-color:#859399;background:#b6c0c4}.classroom-legend b{color:#65427c;font:600 16px/1 ui-monospace,monospace}.viewer-chrome.is-nvim-fit{display:flex;width:100%;height:100vh;height:100dvh;max-height:100dvh;flex-direction:column;overflow:hidden;background:#11171a}.viewer-chrome.is-nvim-fit .topbar,.viewer-chrome.is-nvim-fit .viewpoint-bar,.viewer-chrome.is-nvim-fit .allocator-panel{flex:none}.viewer-chrome.is-nvim-fit .nvim-panel{display:flex;min-height:0;flex:1;flex-direction:column}.viewer-chrome.is-nvim-fit .nvim-screen-scroll{min-height:0;height:auto!important;flex:1}.viewer-chrome.is-nvim-fit .nvim-resize-handle{display:none}.viewer-chrome.is-nvim-fit .nvim-screen-stage{width:100%;height:100%;margin:0;zoom:1}.viewer-chrome.is-nvim-fit .nvim-screen-content{right:1mm;overflow:hidden;scrollbar-width:none}.viewer-chrome.is-nvim-fit .nvim-screen-grid{display:flex;width:100%;min-height:0;height:100%;align-items:center;justify-content:center}.viewer-chrome.is-nvim-fit .nvim-screen{width:auto!important;height:auto!important;max-width:100%;max-height:100%}.viewer-chrome.is-nvim-fit .nvim-screen-content::-webkit-scrollbar{display:none}.topbar-nvim-section{display:flex;align-items:center;min-width:0;gap:6px}.nvim-toolbar-boundary{flex:none;color:#6f7d83;font:700 14px/1 ui-monospace,monospace}.nvim-toolbar-context{flex:none;width:5ch;color:#263238;font:800 12px/1 ui-monospace,monospace;letter-spacing:.08em;text-align:center}.nvim-toolbar-actions{display:flex;align-items:center;gap:3px}.nvim-toolbar-button{display:inline-flex;align-items:center}.nvim-toolbar-key{flex:0 0 2ch;color:#267697;font:inherit;text-align:center}.nvim-toolbar-label{flex:0 0 5ch;max-width:5ch;overflow:hidden;text-align:left;white-space:nowrap}.topbar-nvim-meta{display:flex;align-items:baseline;min-width:0;max-width:min(44vw,720px);gap:6px}.topbar-nvim-meta strong,.topbar-nvim-meta small{overflow:auto;text-overflow:ellipsis;white-space:nowrap}.topbar-nvim-meta strong{color:#263238;font:700 12px/1.25 system-ui,sans-serif}.topbar-nvim-meta small{color:#66747a;font:500 10px/1.25 system-ui,sans-serif}.viewer-nvim-toggle{border:1px solid #68747a;border-radius:3px;background:#fff;color:#263238;padding:2px 8px;font:650 16px/1.3 system-ui,sans-serif;cursor:pointer}.viewer-nvim-toggle[aria-pressed=true]{border-color:#267697;background:#eaf6fb;color:#174d64}.allocator-panel{width:100%;box-sizing:border-box;border-top:1px solid #40515a;border-bottom:1px solid #40515a;background:#f7f9f5;color:#182127;box-shadow:0 4px 12px #0002;font:11px/1.25 system-ui,sans-serif}.allocator-panel>header{display:grid;grid-template-columns:125px minmax(260px,auto) 1fr;gap:8px;align-items:baseline;min-height:22px;padding:3px 8px;border-bottom:1px solid #b7c1c4;background:#e8eef0}.allocator-panel>header>span{color:#166987;font:800 9px/1.2 ui-monospace,monospace;letter-spacing:.06em}.allocator-panel>header>strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:750 11px/1.2 ui-monospace,monospace}.allocator-panel>header>small{overflow:hidden;color:#536168;text-overflow:ellipsis;white-space:nowrap}.allocator-panel-body{display:grid;grid-template-columns:230px minmax(420px,1fr) minmax(310px,.8fr);gap:9px;align-items:stretch;padding:5px 8px 7px}.allocator-metrics{display:grid;grid-template-columns:1fr 1fr;gap:3px 8px;margin:0}.allocator-metrics>div{min-width:0;border-left:2px solid #8ea2ab;padding-left:5px}.allocator-metrics dt{color:#627077;font:750 7px/1.2 ui-monospace,monospace;letter-spacing:.05em}.allocator-metrics dd{margin:1px 0 0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:700 10px/1.2 ui-monospace,monospace}.allocator-arena-view{min-width:0}.allocator-arena-view.is-inactive{opacity:.42}.allocator-arena-labels{display:flex;justify-content:space-between;gap:8px;margin-bottom:3px;color:#536168;font:700 8px/1.2 ui-monospace,monospace}.allocator-arena-labels strong{color:#075f80}.allocator-pinned-arena{position:relative;display:grid;grid-template-columns:repeat(var(--allocator-size),minmax(2px,1fr));height:32px;border:1px solid #28343a;background:#fff}.allocator-pinned-arena i{min-width:0;border-right:1px solid #e1e5e6;background:#fbfcfc}.allocator-pinned-arena i:nth-child(8n){border-right-color:#87969d}.allocator-pinned-arena i.is-allocated{border-right-color:#b2d8e6;background:#43a8ce}.allocator-cursor{position:absolute;z-index:2;top:-4px;bottom:-4px;width:2px;background:#d02d25;transform:translate(-1px);box-shadow:0 0 0 1px #fff9}.allocator-cursor:before{content:"allocp";position:absolute;top:1px;left:4px;color:#8d1d17;font:800 7px/1 ui-monospace,monospace}.allocator-facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 7px;min-width:0}.allocator-facts>div{display:flex;min-width:0;justify-content:space-between;gap:5px;border-bottom:1px dotted #b4bec2}.allocator-facts span{overflow:hidden;color:#46565e;text-overflow:ellipsis;white-space:nowrap}.allocator-facts code{overflow:hidden;color:#142e39;text-overflow:ellipsis;white-space:nowrap;font:700 9px/1.3 ui-monospace,monospace}.allocator-facts p{grid-column:1 / -1;margin:0;color:#647278}.nvim-panel{width:100%;box-sizing:border-box;border-bottom:1px solid #52616a;background:var(--desk, #eee);color:#e7eef1;box-shadow:0 5px 16px #0004;font:12px/1.3 system-ui,sans-serif}.nvim-status{flex:none;border:1px solid #66757c;border-radius:999px;padding:1px 6px;color:#c9d3d8;font:750 9px/1.35 ui-monospace,monospace;letter-spacing:.04em}.nvim-status--connected,.nvim-status--ready{border-color:#31a86d;background:#173b2a;color:#8aefb7}.nvim-status--connecting,.nvim-status--replaying,.nvim-status--verifying{border-color:#c38f2a;background:#3e321b;color:#ffd47d}.nvim-status--failed,.nvim-status--offline,.nvim-status--unselected{border-color:#a34b4b;background:#3b2020;color:#faa}.nvim-control-indicator{flex:none;border:1px solid #60747e;border-radius:3px;background:#222e34;color:#b9c6cc;padding:3px 8px;font:700 10px/1.2 ui-monospace,monospace;letter-spacing:.04em;white-space:nowrap;cursor:pointer}.nvim-control-indicator.is-active{border-color:#3cb3e2;background:#17465a;color:#d9f5ff}.nvim-control-indicator.is-engaged{box-shadow:inset 0 0 0 1px #8ee4ff}.nvim-work-indicator{flex:none;border:1px solid #7a878d;border-radius:3px;background:#f4f6f7;color:#56646a;padding:3px 8px;font:700 10px/1.2 ui-monospace,monospace;letter-spacing:.03em;white-space:nowrap;cursor:pointer}.nvim-work-indicator.is-active{border-color:#cf3d31;background:#fff0ee;color:#9f241b}.nvim-work-indicator.is-engaged{box-shadow:inset 0 0 0 1px #cf3d31}.nvim-work-indicator:disabled{border-color:#a8b0b4;background:#e6e9ea;color:#7c878c;cursor:not-allowed;opacity:.8}.nvim-height-toggle{flex:none;border:1px solid #75848b;border-radius:3px;background:#f4f6f7;color:#425159;padding:3px 8px;font:700 10px/1.2 ui-monospace,monospace;letter-spacing:.03em;white-space:nowrap;cursor:pointer}.nvim-height-toggle.is-active{border-color:#267697;background:#eaf6fb;color:#174d64}.topbar-nvim-section .nvim-toolbar-button{flex:0 0 9.5ch;width:9.5ch;min-width:9.5ch;max-width:9.5ch;height:24px;box-sizing:border-box;justify-content:flex-start;gap:.55ch;padding:2px .55ch;font:750 12px/1 ui-monospace,monospace;letter-spacing:0}.nvim-shortcut-strip{flex:none;color:#536168;font:650 10px/1.2 ui-monospace,monospace;white-space:nowrap}.nvim-shortcut-strip kbd{color:#263238;font:inherit}.nvim-screen-scroll{position:relative;width:100%;box-sizing:border-box;height:var(--nvim-panel-height, 75vh);max-height:none;min-height:240px;overflow:auto;overscroll-behavior:contain;background:var(--desk, #eee);outline:none;scrollbar-color:#58666d #171d20}.nvim-screen-stage{position:relative;width:210mm;margin:0 auto;background:#fff;zoom:var(--viewer-canvas-scale)}.nvim-screen-surface{position:absolute;inset:0 4mm;box-sizing:border-box;padding:1mm;background:#fff}.nvim-screen-frame{position:relative;height:100%;box-sizing:border-box;border:1px solid #267697;background:#101315;box-shadow:0 0 0 3px #000}.nvim-screen-scroll[data-sync=on] .nvim-screen-frame{border-color:#35b9ef}.nvim-screen-scroll[data-control=true] .nvim-screen-frame{box-shadow:0 0 0 3px #cf3d31}.nvim-screen-scroll[data-control=true]{cursor:text}.nvim-screen-content{position:absolute;inset:1mm -5mm 1mm 1mm;overflow-x:hidden;overflow-y:scroll;overscroll-behavior:contain;background:transparent;scrollbar-width:thin;scrollbar-color:#829097 #fff}.nvim-screen-grid{width:calc(100% - 6mm);min-height:100%;background:#101315}.nvim-screen-content::-webkit-scrollbar{width:.8mm}.nvim-screen-content::-webkit-scrollbar-track{background:#fff}.nvim-screen-content::-webkit-scrollbar-thumb{border:.12mm solid #fff;border-radius:99px;background:#829097}.nvim-screen-content::-webkit-scrollbar-thumb:hover{background:#aeb9be}.nvim-screen{display:block;max-width:none;margin:0;outline:none;image-rendering:auto}.nvim-screen:focus{box-shadow:none}.nvim-screen-empty{position:absolute;inset:0;display:grid;place-content:center;gap:5px;padding:16px;color:#9cabb2;text-align:center}.nvim-screen-empty strong{color:#e0e8eb;font:750 11px/1.3 ui-monospace,monospace}.nvim-resize-handle{position:relative;width:100%;height:12px;border-top:1px solid #46565e;background:#182126;cursor:ns-resize;touch-action:none}.nvim-resize-handle:after{content:"";position:absolute;top:4px;left:50%;width:min(160px,22vw);height:3px;border-radius:999px;background:#6c7e87;transform:translate(-50%)}.nvim-resize-handle:hover,.nvim-resize-handle:focus-visible{background:#23323a;outline:none}.nvim-resize-handle:hover:after,.nvim-resize-handle:focus-visible:after{background:#35b9ef}.shortcut-help-backdrop{position:fixed;z-index:200;inset:0;display:grid;place-items:center;padding:20px;background:#10171dcc}.shortcut-help{width:min(680px,calc(100vw - 40px));max-height:calc(100vh - 40px);overflow:auto;border:1px solid #71818a;border-radius:6px;background:#f8fafb;color:#172127;box-shadow:0 18px 60px #0008;font:14px/1.4 system-ui,sans-serif}.shortcut-help>header{display:flex;align-items:start;justify-content:space-between;gap:16px;padding:16px 18px;border-bottom:1px solid #c8d1d5;background:#e8eef1}.shortcut-help header small{color:#52646d;font:750 10px/1.2 ui-monospace,monospace;letter-spacing:.08em}.shortcut-help h2{margin:4px 0 0;font-size:17px}.shortcut-help header button{width:32px;height:32px;border:1px solid #71818a;border-radius:3px;background:#fff;font:22px/1 system-ui,sans-serif;cursor:pointer}.shortcut-help dl{margin:0;padding:10px 18px}.shortcut-help dl>div{display:grid;grid-template-columns:minmax(180px,.8fr) 1.5fr;gap:14px;align-items:center;padding:8px 0;border-bottom:1px solid #dde3e6}.shortcut-help dt,.shortcut-help dd{margin:0}.shortcut-help kbd{display:inline-block;border:1px solid #839198;border-bottom-width:2px;border-radius:3px;background:#fff;padding:3px 7px;font:700 12px/1.2 ui-monospace,monospace}.shortcut-help>p{margin:0;padding:4px 18px 16px;color:#526068}.memory-react{border:1px solid #555;background:#fff;font:14px/1.35 system-ui,sans-serif;padding:10px;color:#1b1b1b}.memory-react>header{display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid #bbb;padding-bottom:7px}.memory-react>header small{display:block;margin-bottom:2px;color:#555;font:700 10px/1.2 ui-monospace,monospace;letter-spacing:.08em}.memory-react h3{font-size:15px;margin:0}.memory-actions{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}.memory-actions button{border:1px solid #777;background:#fff;padding:4px 8px;cursor:pointer}.memory-actions button[aria-pressed=true]{background:#dff3fb;border-color:#237da1;font-weight:700}.memory-lanes{display:grid;grid-template-columns:repeat(auto-fit,minmax(125px,1fr));gap:6px;margin:8px 0}.memory-lane{min-width:0;display:flex;flex-direction:column;align-items:stretch;gap:3px;border:1px solid #999;background:#fafafa;padding:7px;text-align:left}.memory-lane.compile{background:#fff8dc}.memory-lane.bss{background:#edf7fb}.memory-lane.stack{background:#f3f8ef}.memory-lane.registers{background:#f4f4f4}.memory-lane.text{background:#f7f0fa}.memory-lane>b{font-size:11px;letter-spacing:.04em}.memory-lane button{border:1px solid transparent;background:transparent;padding:3px;text-align:left;cursor:pointer}.memory-lane button:hover,.memory-lane button.active{border-color:#238bb7;background:#fff}.memory-lane code{font-size:10px;white-space:normal}.arena-wrap{margin:12px 0 6px}.arena-label{display:flex;justify-content:space-between;margin-bottom:4px}.arena{--allocp: 0;--arena-size: 64;position:relative;display:grid;grid-template-columns:repeat(var(--arena-size),1fr);height:40px;border:1px solid #222;margin-bottom:28px}.arena i{border-right:1px solid #ddd}.arena .allocp{position:absolute;left:calc(var(--allocp) / var(--arena-size) * 100%);top:43px;transform:translate(-50%);color:#087da8;font:700 11px/1.2 ui-monospace,monospace;white-space:nowrap}.memory-stage{border-top:1px solid #ddd;padding-top:7px;margin:0}.memory-inspector{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;margin-top:7px;padding:7px;border-left:3px solid #238bb7;background:#f5fbfd}.memory-inspector strong{grid-row:1/3}.memory-inspector code{font-size:11px}.memory-inspector span{font-size:12px}.asset-static-fallback{display:none}.uml-react{border:1px solid #555;background:#fff;font:13px/1.35 system-ui,sans-serif;color:#1b1b1b}.uml-react>header{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 8px;border-bottom:1px solid #bbb;background:#fafafa}.uml-react>header small{display:block;color:#555;font:700 10px/1.2 ui-monospace,monospace;letter-spacing:.06em}.uml-react h3{margin:2px 0 0;font-size:15px}.uml-actions{display:flex;gap:3px;flex-wrap:wrap;justify-content:flex-end}.uml-actions button{border:1px solid #aaa;border-radius:2px;background:#fcfcfc;color:#444;padding:2px 6px;font:600 10px/1.25 ui-monospace,monospace;cursor:pointer}.uml-actions button:hover{border-color:#64879a;background:#f5fafc}.uml-actions button[data-completed=true]{border-color:#62a47f;background:#edf8f1;color:#14613a}.uml-actions button[aria-pressed=true]{border-color:#287495;background:#edf7fb;color:#174d64;box-shadow:inset 0 0 0 1px #b9dce9}.uml-canvas{display:block;overflow-x:hidden;overflow-y:visible;box-sizing:border-box;padding:8px 10px;text-align:center}.uml-svg-host{width:max-content;min-width:100%}.uml-canvas img,.uml-svg-host svg{display:block;width:auto!important;max-width:none!important;height:auto!important;max-height:none;margin-inline:auto;object-fit:contain}.uml-svg-host{cursor:pointer}.uml-svg-host.is-tiled{position:relative;width:var(--uml-tile-page-width);min-width:var(--uml-tile-page-width);height:var(--uml-tile-page-height);overflow:hidden;cursor:default}.uml-svg-host.is-tiled>svg{position:absolute;top:var(--uml-tile-top);left:var(--uml-tile-left);margin:0}.uml-static-crop{position:absolute;top:var(--uml-tile-top);left:var(--uml-tile-left);overflow:hidden}.uml-static-crop>img{display:block;max-width:none!important;max-height:none!important;margin:0!important;object-fit:fill!important;transform-origin:0 0}.uml-tile-part{display:inline-block;margin-left:.65em;color:#607985;font:650 9px/1.2 ui-monospace,monospace;letter-spacing:.03em;white-space:nowrap}.uml-phase-actions>span{display:inline-flex;align-items:center;border:1px solid #9eb2bd;border-radius:2px;background:#f1f5f7;color:#3d5967;padding:2px 6px;font:700 10px/1.25 ui-monospace,monospace}.uml-stage-region{cursor:pointer;transition:fill .14s ease,stroke .14s ease,stroke-width .14s ease}.uml-stage-region-completed.uml-stage-region-bg{fill:#e7f6ed!important;fill-opacity:1!important;stroke:#258154!important;stroke-width:3px!important}.uml-stage-region-completed.uml-stage-region-outline{stroke:#258154!important;stroke-width:3px!important}.uml-stage-region-active.uml-stage-region-bg{fill:#e7f6fc!important;fill-opacity:1!important;stroke:#187ea7!important;stroke-width:3px!important}.uml-stage-region-active.uml-stage-region-outline{stroke:#187ea7!important;stroke-width:3px!important}.uml-stage-region-active.uml-stage-region-completed.uml-stage-region-bg{fill:#e7f6ed!important}.uml-stage-region:focus{outline:none;stroke:#005f87!important;stroke-width:4px!important}.uml-inline-warning{display:block;padding-top:5px;color:#8b2d20;font-size:11px}.uml-stage{display:grid;grid-template-columns:auto 1fr;gap:3px 10px;margin:0 8px 8px;padding:8px;border-left:3px solid #315f78;background:#eef7fb}.uml-stage strong{grid-row:1/3}.uml-stage code{grid-column:2;font-size:11px}.uml-approval{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 8px 8px;padding-top:6px;border-top:1px solid #d2d2d2;color:#555;font-size:11px}.uml-approval button{flex:0 0 auto;border:1px solid #477a5d;border-radius:2px;background:#edf8f1;color:#174e30;padding:3px 7px;font:600 10px/1.25 system-ui,sans-serif;cursor:pointer}.uml-approval button:disabled{border-color:#bbb;background:#f3f3f3;color:#888;cursor:not-allowed}.uml-react>.uml-snapshot-header{display:block;padding:5px 8px 4px}.uml-header-first-line{display:flex;align-items:center;justify-content:space-between;gap:10px}.uml-header-first-line h3{flex:1 1 auto;min-width:0;margin:0;color:#20282c;text-align:left;font-size:12px;line-height:1.25}.uml-header-first-line h3>span{color:#245f7a;font-family:ui-monospace,monospace;letter-spacing:.04em}.uml-header-description{max-width:620px;margin:2px 0 0;color:#555;text-align:left;font-size:8px}.uml-after-description{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:baseline;gap:4mm;margin-top:3mm;border:1px solid #b8c2c7;border-left:1.2mm solid #2b7898;background:#f7fafb;padding:2.2mm 3mm;color:#26363e;text-align:left}.uml-after-description strong{color:#245f7a;font:800 8px/1.25 ui-monospace,monospace;letter-spacing:.06em;text-transform:uppercase}.uml-after-description p{margin:0;font:400 10px/1.35 system-ui,sans-serif}.uml-header-navigation{display:flex;flex:0 0 auto;align-items:center;justify-content:flex-end;gap:6px}.uml-page-continuation{display:inline-flex;flex:0 0 auto;align-items:center;gap:2px;min-height:22px;border:1px solid #86a5b3;border-radius:3px;background:#edf7fa;padding:1px 5px;color:#126b8c;font:850 15px/1 ui-monospace,monospace;animation:uml-continuation-pulse 1.15s ease-in-out infinite alternate}.uml-page-continuation i{display:inline-grid;min-width:14px;place-items:center;font-style:normal}@keyframes uml-continuation-pulse{0%{border-color:#9fb5be;background:#f6fafb;color:#4c7180;box-shadow:none}to{border-color:#167ca4;background:#dff2f8;color:#075c7c;box-shadow:0 0 0 2px #8dcde433}}@media(prefers-reduced-motion:reduce){.uml-page-continuation{animation:none}}.uml-phase-actions,.uml-step-actions{display:flex;align-items:center;flex-wrap:wrap;gap:3px}.uml-phase-actions{flex:0 0 auto;flex-wrap:nowrap;justify-content:flex-end;white-space:nowrap}.uml-phase-actions button,.uml-step-actions button,.uml-edit-toggle,.uml-snapshot-cursor button{border:1px solid #aaa;border-radius:2px;background:#fff;color:#444;cursor:pointer;font:600 9px/1.2 ui-monospace,monospace}.uml-edit-toggle{min-width:42px;padding:3px 7px;border:1px solid #8a9ca5;border-radius:2px;background:#f7f9fa;color:#334d59;cursor:pointer;font:750 9px/1.2 ui-monospace,monospace;letter-spacing:.04em}.uml-edit-toggle.is-active{border-color:#b56b16;background:#fff3df;color:#713c00;box-shadow:inset 0 0 0 1px #f0c78f}.uml-phase-actions button{padding:3px 7px}.uml-phase-actions button[aria-pressed=true]{border-color:#287495;background:#edf7fb;color:#174d64}.uml-step-actions{min-height:0;padding:0;border:0;background:transparent}.uml-step-actions>span{margin-right:4px;color:#555;font:700 9px/1.2 ui-monospace,monospace}.uml-step-actions button{min-width:25px;padding:2px 4px}.uml-step-actions button[data-completed=true]{border-color:#62a47f;background:#edf8f1;color:#14613a}.uml-step-actions button[aria-pressed=true]{border-color:#287495;background:#287495;color:#fff}.uml-step-actions button[data-mode=CODE]:after,.uml-step-actions button[data-mode=RUN]:after{margin-left:3px;font-size:7px;letter-spacing:.02em}.uml-step-actions button[data-mode=CODE]:after{content:"C"}.uml-step-actions button[data-mode=RUN]:after{content:"R"}.uml-snapshot-layout{background:#fff;border-bottom:1px solid #ddd}.uml-step-canvas{min-width:0}.uml-step-canvas .uml-svg-host svg{max-height:none}.uml-step-hit{cursor:pointer;fill:transparent}.uml-step-hit-active{fill:#3ea6ce!important;fill-opacity:.08!important}.uml-step-element{transition:fill .12s ease,stroke .12s ease,stroke-width .12s ease}line.uml-step-element-completed:not(.uml-step-element-active){stroke:#258154!important;stroke-width:1.8px!important}polygon.uml-step-element-completed:not(.uml-step-element-active){fill:#258154!important;stroke:#258154!important}text.uml-step-element-completed:not(.uml-step-element-active){fill:#1d7049!important}line.uml-step-element-active{stroke:#087ca8!important;stroke-width:2.6px!important}polygon.uml-step-element-active{fill:#087ca8!important;stroke:#087ca8!important}text.uml-step-element-active{fill:#075d7d!important;font-weight:700!important}.uml-step-hit:focus{outline:none;stroke:#bd554d;stroke-width:.8px;stroke-dasharray:3 3}.uml-class-svg-active>g>*:not(.uml-class-focus-region):not(.uml-class-step-hit):not(.uml-layout-hit):not(.uml-layout-resize){opacity:.62;transition:opacity .12s ease}.uml-class-svg-active .uml-step-element:not(.uml-step-element-active){opacity:.98!important}.uml-class-svg-active text.uml-step-element-tone-a:not(.uml-step-element-active){fill:#174f69!important}.uml-class-svg-active text.uml-step-element-tone-b:not(.uml-step-element-active){fill:#586581!important}.uml-class-svg-active rect.uml-step-element-tone-a:not(.uml-step-element-active){fill:#eaf5f8!important;stroke:#3f7890!important}.uml-class-svg-active rect.uml-step-element-tone-b:not(.uml-step-element-active){fill:#f2eef8!important;stroke:#7b86a1!important}.uml-class-svg-active path.uml-step-element-tone-a:not(.uml-step-element-active),.uml-class-svg-active line.uml-step-element-tone-a:not(.uml-step-element-active),.uml-class-svg-active polygon.uml-step-element-tone-a:not(.uml-step-element-active){stroke:#3f7890!important}.uml-class-svg-active path.uml-step-element-tone-b:not(.uml-step-element-active),.uml-class-svg-active line.uml-step-element-tone-b:not(.uml-step-element-active),.uml-class-svg-active polygon.uml-step-element-tone-b:not(.uml-step-element-active){stroke:#7b86a1!important}.uml-class-svg-active .uml-step-element-active{opacity:1!important}.uml-class-svg-active text.uml-step-element-active{fill:#b42318!important;font-weight:700!important}.uml-class-svg-active path.uml-step-element-active,.uml-class-svg-active line.uml-step-element-active,.uml-class-svg-active polygon.uml-step-element-active,.uml-class-svg-active rect.uml-step-element-active{stroke:#bd554d!important;stroke-width:1.35px!important}.uml-class-svg-active polygon.uml-step-element-active{fill:#bd554d!important}.uml-class-focus-region{display:none}.uml-step-canvas.is-layout-editing{position:relative;touch-action:none;background-color:#fbfcfd;background-image:linear-gradient(#7d98a20d 1px,transparent 1px),linear-gradient(90deg,#7d98a20d 1px,transparent 1px);background-size:12px 12px}.uml-layout-editing .uml-step-hit,.uml-layout-editing .uml-class-focus-region{pointer-events:none}.uml-layout-hit{fill:#4e9ec4;fill-opacity:.035;stroke:#247a9e;stroke-width:1.1px;stroke-dasharray:5 3;vector-effect:non-scaling-stroke;cursor:grab;pointer-events:all}.uml-layout-hit:hover{fill-opacity:.09;stroke-width:1.6px}.uml-layout-hit.is-selected{fill:#f7a928;fill-opacity:.08;stroke:#b66b00;stroke-width:1.8px;stroke-dasharray:none}.uml-layout-hit:active{cursor:grabbing}.uml-layout-resize{fill:#fff;stroke:#b66b00;stroke-width:1.5px;vector-effect:non-scaling-stroke;cursor:nwse-resize;pointer-events:all}.uml-layout-editor{display:grid;grid-template-columns:minmax(140px,.8fr) minmax(240px,1.8fr) minmax(260px,1.4fr);gap:7px 10px;align-items:end;padding:8px 10px 9px;border-top:2px solid #b97822;background:#f7f5f0;color:#26343a;text-align:left;font:10px/1.3 system-ui,sans-serif}.uml-layout-editor-title{grid-column:1/-1;display:flex;align-items:baseline;gap:9px}.uml-layout-editor-title strong{color:#713c00;font:800 11px/1.2 ui-monospace,monospace;text-transform:uppercase}.uml-layout-editor-title span{color:#65737a}.uml-layout-editor-title b{margin-left:auto;color:#9c2f1b;font-size:9px}.uml-layout-editor label{display:grid;gap:2px;color:#52636b;font:700 8px/1.2 ui-monospace,monospace;text-transform:uppercase}.uml-layout-editor input,.uml-layout-editor select,.uml-layout-editor textarea{box-sizing:border-box;width:100%;min-width:0;border:1px solid #9ca9ae;border-radius:2px;background:#fff;color:#172126;padding:4px 5px;font:10px/1.25 ui-monospace,monospace;text-transform:none}.uml-layout-editor textarea{resize:vertical;line-height:1.35}.uml-layout-number-grid,.uml-layout-style-grid{display:grid;gap:5px}.uml-layout-number-grid{grid-template-columns:repeat(4,minmax(48px,1fr))}.uml-layout-style-grid{grid-template-columns:repeat(5,minmax(54px,1fr))}.uml-layout-editor-text{align-self:stretch}.uml-layout-editor input[type=color]{min-height:27px;padding:2px}.uml-layout-editor-actions{grid-column:1/-1;display:flex;gap:5px}.uml-layout-editor-actions button{border:1px solid #87979f;border-radius:2px;background:#fff;color:#283940;padding:4px 8px;cursor:pointer;font:700 9px/1.2 ui-monospace,monospace}.uml-layout-editor-actions button.is-primary{border-color:#287495;background:#287495;color:#fff}.uml-layout-editor-actions button.is-danger{margin-left:auto;border-color:#a26156;color:#812b1c}.uml-layout-editor-actions button:disabled{opacity:.45;cursor:not-allowed}.uml-layout-editor output{grid-column:1/-1;min-height:1.2em;color:#52636b;font:9px/1.25 ui-monospace,monospace}@media(max-width:900px){.uml-layout-editor{grid-template-columns:1fr}.uml-layout-editor-title,.uml-layout-editor-actions,.uml-layout-editor output{grid-column:1}}@media print{.uml-edit-toggle,.uml-layout-editor,.uml-layout-hit,.uml-layout-resize{display:none!important}}.uml-snapshot-panel{display:grid;gap:8px;min-width:0;padding:8px;background:#fafafa;font-size:10px}.uml-snapshot-panel>header{display:flex;justify-content:space-between;gap:8px;align-items:center}.uml-snapshot-panel>header>div:first-child>span,.uml-snapshot-panel>header>div:first-child>strong{display:block}.uml-snapshot-panel>header>div:first-child>span{color:#5a5a5a;font:700 9px/1.2 ui-monospace,monospace}.uml-snapshot-panel>header>div:first-child>strong{font-size:12px}.uml-snapshot-panel>header>div:first-child>strong code{display:inline-block;min-width:24px;color:#075d7d}.uml-snapshot-cursor{display:flex;align-items:center;gap:4px}.uml-snapshot-cursor button{width:23px;height:23px;padding:0}.uml-snapshot-cursor button:disabled{opacity:.35;cursor:default}.uml-snapshot-cursor output{min-width:34px;text-align:center;font:700 9px/1 ui-monospace,monospace}.uml-snapshot-description{margin:0;color:#333}.uml-snapshot-panel section{min-width:0;border:1px solid #d0d0d0;background:#fff;padding:7px}.uml-snapshot-panel h4{margin:0 0 4px;color:#315f78;font:700 9px/1.2 ui-monospace,monospace;text-transform:uppercase}.uml-state-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;min-width:0}.uml-snapshot-registers dl{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:3px;margin:0}.uml-snapshot-registers dl>div{display:grid;grid-template-columns:auto 1fr;gap:5px;min-width:0;padding:2px 4px;border-left:2px solid #bbb;background:#f6f6f6}.uml-snapshot-registers dl>div.primary{grid-column:1/-1;border-color:#287495;background:#edf7fb}.uml-snapshot-registers dt{color:#555;font-weight:700}.uml-snapshot-registers dd,.uml-snapshot-variables dd,.uml-snapshot-memory dd{min-width:0;margin:0;overflow-wrap:anywhere;text-align:right;font:600 9px/1.25 ui-monospace,monospace}.uml-snapshot-variables dl{display:grid;gap:3px;margin:0}.uml-snapshot-variables dl>div{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:5px;padding:3px 5px;border-left:2px solid #5f899b;background:#f5f8f9}.uml-snapshot-variables dt{font-weight:700}.uml-snapshot-variables dt small{display:block;color:#777;font:8px/1.2 ui-monospace,monospace}.uml-snapshot-variables [data-state=uninitialised],.uml-stack-lane article[data-state=uninitialised]{color:#777;background:#f5f5f5}.uml-snapshot-variables [data-state=uninitialised] dt:after,.uml-stack-lane article[data-state=uninitialised]>strong:after{content:" ?";color:#9a6a00}.uml-arena-snapshot>div{position:relative;height:10px;border:1px solid #666;background:repeating-linear-gradient(90deg,#fff 0 7px,#eee 7px 8px)}.uml-arena-snapshot>div span{display:block;height:100%;background:#b8ddeb}.uml-arena-snapshot>div i{position:absolute;top:-3px;bottom:-3px;width:2px;background:#075d7d;transform:translate(-1px)}.uml-arena-snapshot>p{display:grid;grid-template-columns:1fr auto 1fr;gap:4px;margin:3px 0;font-size:8px}.uml-arena-snapshot>p code:last-child{text-align:right}.uml-arena-bytes{display:block;margin-bottom:4px;overflow-wrap:anywhere;color:#666;font-size:8px}.uml-snapshot-memory dl{display:grid;gap:2px;margin:0}.uml-snapshot-memory dl>div{display:grid;grid-template-columns:1fr auto;gap:4px;padding-top:2px;border-top:1px solid #eee}.uml-snapshot-memory dt{font-weight:700}.uml-snapshot-memory dt small{display:block;color:#777;font:8px/1.2 ui-monospace,monospace}.uml-snapshot-stack>header{display:flex;align-items:baseline;justify-content:space-between;gap:8px}.uml-snapshot-stack>header>p{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:4px 9px;margin:0;color:#555;font-size:8px}.uml-stack-axis{display:flex;justify-content:space-between;gap:8px;margin:3px 89px 2px;color:#777;font:8px/1.2 ui-monospace,monospace}.uml-stack-lane{display:flex;align-items:stretch;min-width:0;overflow-x:auto;border:1px solid #66757c;background:#eef1f2}.uml-stack-boundary{display:grid;place-content:center;flex:0 1 82px;min-width:64px;padding:5px;background:#273941;color:#fff;text-align:center;font:700 8px/1.25 ui-monospace,monospace}.uml-stack-boundary small{color:#a9d9eb;font-size:8px;text-transform:uppercase}.uml-stack-gap{display:grid;place-content:center;flex:.7 1 70px;min-width:60px;min-height:52px;border-right:1px dashed #9da8ad;color:#78858b;text-align:center;font:8px/1.2 ui-monospace,monospace}.uml-stack-gap strong,.uml-stack-gap small{display:block}.uml-stack-gap strong{color:#52636b}.uml-stack-gap small{color:#78858b;font-size:8px}.uml-stack-lane article{display:grid;align-content:center;flex:1 1 92px;min-width:76px;padding:5px 7px;border-right:1px solid #9da8ad;background:#fff;text-align:center}.uml-stack-lane article[data-state=initialised]{border-top:3px solid #2c8060}.uml-stack-lane article[data-state=saved]{border-top:3px solid #6d7196;background:#f7f6fb}.uml-stack-lane article strong,.uml-stack-lane article code,.uml-stack-lane article small{display:block;overflow-wrap:anywhere}.uml-stack-lane article strong{font:700 9px/1.2 ui-monospace,monospace}.uml-stack-lane article code{margin:2px 0;color:#1f4555;font-size:8px}.uml-stack-lane article small{color:#777;font:8px/1.2 ui-monospace,monospace}.uml-snapshot-observation p{margin:0 0 3px}.uml-snapshot-observation small{display:block;overflow-wrap:anywhere;color:#666;font:8px/1.3 ui-monospace,monospace}.task-conclusion{margin:8px 0 0;padding:7px 9px;border:1px solid #315f78;background:#f3f9fc}.task-conclusion>strong{display:block;margin-bottom:3px;color:#315f78;font:700 10px/1.2 ui-monospace,monospace}.task-conclusion p{margin:0}.section-sheet--asset .section-heading{align-items:baseline;margin:0 0 1.5mm}.section-sheet--asset .section-heading .section-index{margin-right:4mm;color:#68757b;font-size:10px;font-weight:600}.section-sheet--asset .section-heading .section-title{color:#20282c;font:750 13px/1.2 system-ui,sans-serif}.section-sheet--asset .card-section>p{max-width:110ch;margin:0 0 2mm;color:#58656b;font:9px/1.3 system-ui,sans-serif}.section-sheet--asset .card-asset{margin-top:2mm}.lesson-progress-control{display:inline-flex;align-items:center;margin-right:7px;padding:2px 7px;border:1px solid #8c8c8c;border-radius:12px;background:#fff;cursor:pointer}.lesson-progress-control:disabled{cursor:wait;opacity:.6}.lesson-progress-control[data-status=approved]{background:#e7f6ed;border-color:#18794e}.lesson-progress-topbar{display:flex;flex:none;align-items:center;gap:5px;min-width:0;padding-left:7px;border-left:1px solid #a9b3b8;color:#33444c;font:650 10px/1.2 system-ui,sans-serif;white-space:nowrap}.lesson-progress-topbar strong{color:#52636b;font:800 9px/1 ui-monospace,monospace;letter-spacing:.05em}.lesson-progress-topbar output{min-width:3.8ch;border:1px solid #91a1a8;border-radius:999px;background:#fff;padding:2px 6px;color:#1e566e;text-align:center;font:800 9px/1 ui-monospace,monospace}.lesson-progress-topbar a{color:#245f7a;text-decoration:none}.lesson-progress-topbar a:hover,.lesson-progress-topbar a:focus-visible{text-decoration:underline}.lesson-progress-error{color:#9b1c1c;font-weight:900}.step-row{display:flex;align-items:center;margin:4px 0}.section-anchor{position:relative;top:-45px}.app-load-error{max-width:760px;margin:10vh auto;padding:24px;border:1px solid #9b1c1c;background:#fff;font-family:system-ui,sans-serif}.app-load-error pre{white-space:pre-wrap}@media(max-width:800px){.viewpoint-bar{grid-template-columns:repeat(4,minmax(0,1fr))}.viewpoint-bar button{min-height:28px}.card-library-toggle>span:last-child{display:none}.card-library-toggle{gap:4px;padding-inline:6px}.topbar-nvim-meta{display:none}.topbar-nvim-section .nvim-control-indicator{max-width:42vw;overflow:hidden;text-overflow:ellipsis}.nvim-shortcut-strip,.nvim-work-indicator{display:none}.allocator-panel>header{grid-template-columns:auto 1fr}.allocator-panel>header>small{display:none}.allocator-panel-body{grid-template-columns:1fr}.allocator-metrics,.allocator-facts{display:none}.memory-lanes{grid-template-columns:1fr 1fr}.memory-react>header,.uml-react>header{align-items:flex-start;flex-direction:column}.uml-header-first-line,.uml-header-navigation{width:100%;align-items:flex-start;flex-direction:column}.uml-header-navigation{gap:4px}.memory-actions{justify-content:flex-start}.uml-snapshot-layout{display:block}.uml-step-canvas{border:0}.uml-phase-actions{width:100%;justify-content:flex-end}.uml-state-grid{grid-template-columns:1fr}.uml-snapshot-stack>header{align-items:flex-start;flex-direction:column}.uml-snapshot-stack>header>p{justify-content:flex-start}.uml-stack-axis{margin-right:0;margin-left:0}}@media print{@page card-portrait{size:A4 portrait;margin:0}@page card-landscape{size:A4 landscape;margin:0}.sheet--portrait{page:card-portrait}.sheet--landscape{page:card-landscape}.paper{display:block;gap:0}.paper>.sheet,.paper>.sheet>.sheet-content,.paper>.sheet-spread>.sheet,.paper>.sheet-spread>.sheet>.sheet-content{height:auto;min-height:0;max-height:none;overflow:visible}.sheet-spread{display:contents!important}.viewer-chrome,.side-panel-dock,.memory-react{display:none!important}.asset-static-fallback{display:block!important;width:100%}.uml-actions,.uml-phase-actions,.uml-step-actions,.uml-snapshot-panel,.uml-stage,.uml-approval,.lesson-progress-topbar,.lesson-progress-control,.uml-page-continuation{display:none!important}.uml-after-description{break-inside:avoid}} diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..77bf0cb --- /dev/null +++ b/web/app.js @@ -0,0 +1,61 @@ +var Op=Object.create;var Ss=Object.defineProperty;var $p=Object.getOwnPropertyDescriptor;var Fp=Object.getOwnPropertyNames;var Dp=Object.getPrototypeOf,jp=Object.prototype.hasOwnProperty;var jt=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Up=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of Fp(t))!jp.call(e,o)&&o!==n&&Ss(e,o,{get:()=>t[o],enumerable:!(r=$p(t,o))||r.enumerable});return e};var wo=(e,t,n)=>(n=e!=null?Op(Dp(e)):{},Up(t||!e||!e.__esModule?Ss(n,"default",{value:e,enumerable:!0}):n,e));var Is=jt(Y=>{"use strict";var xr=Symbol.for("react.element"),Vp=Symbol.for("react.portal"),Bp=Symbol.for("react.fragment"),Hp=Symbol.for("react.strict_mode"),Kp=Symbol.for("react.profiler"),Wp=Symbol.for("react.provider"),Gp=Symbol.for("react.context"),Yp=Symbol.for("react.forward_ref"),Qp=Symbol.for("react.suspense"),qp=Symbol.for("react.memo"),Xp=Symbol.for("react.lazy"),Ns=Symbol.iterator;function Zp(e){return e===null||typeof e!="object"?null:(e=Ns&&e[Ns]||e["@@iterator"],typeof e=="function"?e:null)}var Cs={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ls=Object.assign,zs={};function Un(e,t,n){this.props=e,this.context=t,this.refs=zs,this.updater=n||Cs}Un.prototype.isReactComponent={};Un.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};Un.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function As(){}As.prototype=Un.prototype;function Xi(e,t,n){this.props=e,this.context=t,this.refs=zs,this.updater=n||Cs}var Zi=Xi.prototype=new As;Zi.constructor=Xi;Ls(Zi,Un.prototype);Zi.isPureReactComponent=!0;var Es=Array.isArray,Ps=Object.prototype.hasOwnProperty,Ji={current:null},Ts={key:!0,ref:!0,__self:!0,__source:!0};function Rs(e,t,n){var r,o={},i=null,l=null;if(t!=null)for(r in t.ref!==void 0&&(l=t.ref),t.key!==void 0&&(i=""+t.key),t)Ps.call(t,r)&&!Ts.hasOwnProperty(r)&&(o[r]=t[r]);var s=arguments.length-2;if(s===1)o.children=n;else if(1{"use strict";Os.exports=Is()});var Ws=jt(ie=>{"use strict";function ol(e,t){var n=e.length;e.push(t);e:for(;0>>1,o=e[r];if(0<_o(o,t))e[r]=t,e[n]=o,n=r;else break e}}function Nt(e){return e.length===0?null:e[0]}function Lo(e){if(e.length===0)return null;var t=e[0],n=e.pop();if(n!==t){e[0]=n;e:for(var r=0,o=e.length,i=o>>>1;r_o(s,n))u_o(d,s)?(e[r]=d,e[u]=n,r=u):(e[r]=s,e[l]=n,r=l);else if(u_o(d,n))e[r]=d,e[u]=n,r=u;else break e}}return t}function _o(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}typeof performance=="object"&&typeof performance.now=="function"?($s=performance,ie.unstable_now=function(){return $s.now()}):(tl=Date,Fs=tl.now(),ie.unstable_now=function(){return tl.now()-Fs});var $s,tl,Fs,Tt=[],en=[],rm=1,pt=null,De=3,zo=!1,Sn=!1,kr=!1,Us=typeof setTimeout=="function"?setTimeout:null,Vs=typeof clearTimeout=="function"?clearTimeout:null,Ds=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function il(e){for(var t=Nt(en);t!==null;){if(t.callback===null)Lo(en);else if(t.startTime<=e)Lo(en),t.sortIndex=t.expirationTime,ol(Tt,t);else break;t=Nt(en)}}function ll(e){if(kr=!1,il(e),!Sn)if(Nt(Tt)!==null)Sn=!0,sl(al);else{var t=Nt(en);t!==null&&ul(ll,t.startTime-e)}}function al(e,t){Sn=!1,kr&&(kr=!1,Vs(Sr),Sr=-1),zo=!0;var n=De;try{for(il(t),pt=Nt(Tt);pt!==null&&(!(pt.expirationTime>t)||e&&!Ks());){var r=pt.callback;if(typeof r=="function"){pt.callback=null,De=pt.priorityLevel;var o=r(pt.expirationTime<=t);t=ie.unstable_now(),typeof o=="function"?pt.callback=o:pt===Nt(Tt)&&Lo(Tt),il(t)}else Lo(Tt);pt=Nt(Tt)}if(pt!==null)var i=!0;else{var l=Nt(en);l!==null&&ul(ll,l.startTime-t),i=!1}return i}finally{pt=null,De=n,zo=!1}}var Ao=!1,Co=null,Sr=-1,Bs=5,Hs=-1;function Ks(){return!(ie.unstable_now()-Hse||125r?(e.sortIndex=n,ol(en,e),Nt(Tt)===null&&e===Nt(en)&&(kr?(Vs(Sr),Sr=-1):kr=!0,ul(ll,n-r))):(e.sortIndex=o,ol(Tt,e),Sn||zo||(Sn=!0,sl(al))),e};ie.unstable_shouldYield=Ks;ie.unstable_wrapCallback=function(e){var t=De;return function(){var n=De;De=t;try{return e.apply(this,arguments)}finally{De=n}}}});var Ys=jt((Sh,Gs)=>{"use strict";Gs.exports=Ws()});var Zd=jt(lt=>{"use strict";var om=Eo(),ot=Ys();function L(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Tl=Object.prototype.hasOwnProperty,im=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Qs={},qs={};function lm(e){return Tl.call(qs,e)?!0:Tl.call(Qs,e)?!1:im.test(e)?qs[e]=!0:(Qs[e]=!0,!1)}function am(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function sm(e,t,n,r){if(t===null||typeof t>"u"||am(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Qe(e,t,n,r,o,i,l){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=o,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=l}var Fe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Fe[e]=new Qe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Fe[t]=new Qe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Fe[e]=new Qe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Fe[e]=new Qe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Fe[e]=new Qe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Fe[e]=new Qe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Fe[e]=new Qe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Fe[e]=new Qe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Fe[e]=new Qe(e,5,!1,e.toLowerCase(),null,!1,!1)});var Na=/[\-:]([a-z])/g;function Ea(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Na,Ea);Fe[t]=new Qe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Na,Ea);Fe[t]=new Qe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Na,Ea);Fe[t]=new Qe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Fe[e]=new Qe(e,1,!1,e.toLowerCase(),null,!1,!1)});Fe.xlinkHref=new Qe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Fe[e]=new Qe(e,1,!1,e.toLowerCase(),null,!0,!0)});function _a(e,t,n,r){var o=Fe.hasOwnProperty(t)?Fe[t]:null;(o!==null?o.type!==0:r||!(2s||o[l]!==i[s]){var u=` +`+o[l].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=l&&0<=s);break}}}finally{dl=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Tr(e):""}function um(e){switch(e.tag){case 5:return Tr(e.type);case 16:return Tr("Lazy");case 13:return Tr("Suspense");case 19:return Tr("SuspenseList");case 0:case 2:case 15:return e=pl(e.type,!1),e;case 11:return e=pl(e.type.render,!1),e;case 1:return e=pl(e.type,!0),e;default:return""}}function Ol(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Kn:return"Fragment";case Hn:return"Portal";case Rl:return"Profiler";case Ca:return"StrictMode";case Ml:return"Suspense";case Il:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case rc:return(e.displayName||"Context")+".Consumer";case nc:return(e._context.displayName||"Context")+".Provider";case La:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case za:return t=e.displayName||null,t!==null?t:Ol(e.type)||"Memo";case nn:t=e._payload,e=e._init;try{return Ol(e(t))}catch{}}return null}function cm(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return Ol(t);case 8:return t===Ca?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function yn(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ic(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function dm(e){var t=ic(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var o=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(l){r=""+l,i.call(this,l)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(l){r=""+l},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function To(e){e._valueTracker||(e._valueTracker=dm(e))}function lc(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ic(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function li(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function $l(e,t){var n=t.checked;return xe({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Zs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=yn(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ac(e,t){t=t.checked,t!=null&&_a(e,"checked",t,!1)}function Fl(e,t){ac(e,t);var n=yn(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Dl(e,t.type,n):t.hasOwnProperty("defaultValue")&&Dl(e,t.type,yn(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function Js(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Dl(e,t,n){(t!=="number"||li(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Rr=Array.isArray;function nr(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o"+t.valueOf().toString()+"",t=Ro.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Wr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var Or={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},pm=["Webkit","ms","Moz","O"];Object.keys(Or).forEach(function(e){pm.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Or[t]=Or[e]})});function dc(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Or.hasOwnProperty(e)&&Or[e]?(""+t).trim():t+"px"}function pc(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,o=dc(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,o):e[n]=o}}var mm=xe({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Vl(e,t){if(t){if(mm[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(L(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(L(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(L(61))}if(t.style!=null&&typeof t.style!="object")throw Error(L(62))}}function Bl(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Hl=null;function Aa(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var Kl=null,rr=null,or=null;function nu(e){if(e=co(e)){if(typeof Kl!="function")throw Error(L(280));var t=e.stateNode;t&&(t=Mi(t),Kl(e.stateNode,e.type,t))}}function mc(e){rr?or?or.push(e):or=[e]:rr=e}function fc(){if(rr){var e=rr,t=or;if(or=rr=null,nu(e),t)for(e=0;e>>=0,e===0?32:31-(Nm(e)/Em|0)|0}var Mo=64,Io=4194304;function Mr(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function ci(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,o=e.suspendedLanes,i=e.pingedLanes,l=n&268435455;if(l!==0){var s=l&~o;s!==0?r=Mr(s):(i&=l,i!==0&&(r=Mr(i)))}else l=n&~o,l!==0?r=Mr(l):i!==0&&(r=Mr(i));if(r===0)return 0;if(t!==0&&t!==r&&(t&o)===0&&(o=r&-r,i=t&-t,o>=i||o===16&&(i&4194240)!==0))return t;if((r&4)!==0&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function so(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-zt(t),e[t]=n}function zm(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Fr),du=" ",pu=!1;function Mc(e,t){switch(e){case"keyup":return rf.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Ic(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Wn=!1;function lf(e,t){switch(e){case"compositionend":return Ic(t);case"keypress":return t.which!==32?null:(pu=!0,du);case"textInput":return e=t.data,e===du&&pu?null:e;default:return null}}function af(e,t){if(Wn)return e==="compositionend"||!Fa&&Mc(e,t)?(e=Tc(),Xo=Ia=an=null,Wn=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=gu(n)}}function Dc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Dc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function jc(){for(var e=window,t=li();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=li(e.document)}return t}function Da(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function hf(e){var t=jc(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Dc(n.ownerDocument.documentElement,n)){if(r!==null&&Da(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var o=n.textContent.length,i=Math.min(r.start,o);r=r.end===void 0?i:Math.min(r.end,o),!e.extend&&i>r&&(o=r,r=i,i=o),o=hu(n,i);var l=hu(n,r);o&&l&&(e.rangeCount!==1||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==l.node||e.focusOffset!==l.offset)&&(t=t.createRange(),t.setStart(o.node,o.offset),e.removeAllRanges(),i>r?(e.addRange(t),e.extend(l.node,l.offset)):(t.setEnd(l.node,l.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Gn=null,Xl=null,jr=null,Zl=!1;function yu(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Zl||Gn==null||Gn!==li(r)||(r=Gn,"selectionStart"in r&&Da(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),jr&&Zr(jr,r)||(jr=r,r=mi(Xl,"onSelect"),0qn||(e.current=oa[qn],oa[qn]=null,qn--)}function le(e,t){qn++,oa[qn]=e.current,e.current=t}var vn={},Be=xn(vn),Ze=xn(!1),Pn=vn;function ur(e,t){var n=e.type.contextTypes;if(!n)return vn;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var o={},i;for(i in n)o[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=o),o}function Je(e){return e=e.childContextTypes,e!=null}function gi(){pe(Ze),pe(Be)}function _u(e,t,n){if(Be.current!==vn)throw Error(L(168));le(Be,t),le(Ze,n)}function Qc(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var o in r)if(!(o in t))throw Error(L(108,cm(e)||"Unknown",o));return xe({},n,r)}function hi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||vn,Pn=Be.current,le(Be,e),le(Ze,Ze.current),!0}function Cu(e,t,n){var r=e.stateNode;if(!r)throw Error(L(169));n?(e=Qc(e,t,Pn),r.__reactInternalMemoizedMergedChildContext=e,pe(Ze),pe(Be),le(Be,e)):pe(Ze),le(Ze,n)}var Vt=null,Ii=!1,Sl=!1;function qc(e){Vt===null?Vt=[e]:Vt.push(e)}function _f(e){Ii=!0,qc(e)}function wn(){if(!Sl&&Vt!==null){Sl=!0;var e=0,t=ne;try{var n=Vt;for(ne=1;e>=l,o-=l,Bt=1<<32-zt(t)+o|n<z?(U=P,P=null):U=P.sibling;var F=g(p,P,h[z],w);if(F===null){P===null&&(P=U);break}e&&P&&F.alternate===null&&t(p,P),c=i(F,c,z),A===null?C=F:A.sibling=F,A=F,P=U}if(z===h.length)return n(p,P),fe&&Nn(p,z),C;if(P===null){for(;zz?(U=P,P=null):U=P.sibling;var ae=g(p,P,F.value,w);if(ae===null){P===null&&(P=U);break}e&&P&&ae.alternate===null&&t(p,P),c=i(ae,c,z),A===null?C=ae:A.sibling=ae,A=ae,P=U}if(F.done)return n(p,P),fe&&Nn(p,z),C;if(P===null){for(;!F.done;z++,F=h.next())F=y(p,F.value,w),F!==null&&(c=i(F,c,z),A===null?C=F:A.sibling=F,A=F);return fe&&Nn(p,z),C}for(P=r(p,P);!F.done;z++,F=h.next())F=f(P,p,z,F.value,w),F!==null&&(e&&F.alternate!==null&&P.delete(F.key===null?z:F.key),c=i(F,c,z),A===null?C=F:A.sibling=F,A=F);return e&&P.forEach(function(me){return t(p,me)}),fe&&Nn(p,z),C}function _(p,c,h,w){if(typeof h=="object"&&h!==null&&h.type===Kn&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Po:e:{for(var C=h.key,A=c;A!==null;){if(A.key===C){if(C=h.type,C===Kn){if(A.tag===7){n(p,A.sibling),c=o(A,h.props.children),c.return=p,p=c;break e}}else if(A.elementType===C||typeof C=="object"&&C!==null&&C.$$typeof===nn&&Au(C)===A.type){n(p,A.sibling),c=o(A,h.props),c.ref=Lr(p,A,h),c.return=p,p=c;break e}n(p,A);break}else t(p,A);A=A.sibling}h.type===Kn?(c=An(h.props.children,p.mode,w,h.key),c.return=p,p=c):(w=ii(h.type,h.key,h.props,null,p.mode,w),w.ref=Lr(p,c,h),w.return=p,p=w)}return l(p);case Hn:e:{for(A=h.key;c!==null;){if(c.key===A)if(c.tag===4&&c.stateNode.containerInfo===h.containerInfo&&c.stateNode.implementation===h.implementation){n(p,c.sibling),c=o(c,h.children||[]),c.return=p,p=c;break e}else{n(p,c);break}else t(p,c);c=c.sibling}c=Pl(h,p.mode,w),c.return=p,p=c}return l(p);case nn:return A=h._init,_(p,c,A(h._payload),w)}if(Rr(h))return S(p,c,h,w);if(Nr(h))return b(p,c,h,w);Wo(p,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,c!==null&&c.tag===6?(n(p,c.sibling),c=o(c,h),c.return=p,p=c):(n(p,c),c=Al(h,p.mode,w),c.return=p,p=c),l(p)):n(p,c)}return _}var dr=ed(!0),td=ed(!1),bi=xn(null),xi=null,Jn=null,Ba=null;function Ha(){Ba=Jn=xi=null}function Ka(e){var t=bi.current;pe(bi),e._currentValue=t}function aa(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function lr(e,t){xi=e,Ba=Jn=null,e=e.dependencies,e!==null&&e.firstContext!==null&&((e.lanes&t)!==0&&(Xe=!0),e.firstContext=null)}function yt(e){var t=e._currentValue;if(Ba!==e)if(e={context:e,memoizedValue:t,next:null},Jn===null){if(xi===null)throw Error(L(308));Jn=e,xi.dependencies={lanes:0,firstContext:e}}else Jn=Jn.next=e;return t}var Cn=null;function Wa(e){Cn===null?Cn=[e]:Cn.push(e)}function nd(e,t,n,r){var o=t.interleaved;return o===null?(n.next=n,Wa(t)):(n.next=o.next,o.next=n),t.interleaved=n,Yt(e,r)}function Yt(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var rn=!1;function Ga(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function rd(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Kt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function mn(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,(X&2)!==0){var o=r.pending;return o===null?t.next=t:(t.next=o.next,o.next=t),r.pending=t,Yt(e,n)}return o=r.interleaved,o===null?(t.next=t,Wa(r)):(t.next=o.next,o.next=t),r.interleaved=t,Yt(e,n)}function Jo(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ta(e,n)}}function Pu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var o=null,i=null;if(n=n.firstBaseUpdate,n!==null){do{var l={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};i===null?o=i=l:i=i.next=l,n=n.next}while(n!==null);i===null?o=i=t:i=i.next=t}else o=i=t;n={baseState:r.baseState,firstBaseUpdate:o,lastBaseUpdate:i,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function wi(e,t,n,r){var o=e.updateQueue;rn=!1;var i=o.firstBaseUpdate,l=o.lastBaseUpdate,s=o.shared.pending;if(s!==null){o.shared.pending=null;var u=s,d=u.next;u.next=null,l===null?i=d:l.next=d,l=u;var m=e.alternate;m!==null&&(m=m.updateQueue,s=m.lastBaseUpdate,s!==l&&(s===null?m.firstBaseUpdate=d:s.next=d,m.lastBaseUpdate=u))}if(i!==null){var y=o.baseState;l=0,m=d=u=null,s=i;do{var g=s.lane,f=s.eventTime;if((r&g)===g){m!==null&&(m=m.next={eventTime:f,lane:0,tag:s.tag,payload:s.payload,callback:s.callback,next:null});e:{var S=e,b=s;switch(g=t,f=n,b.tag){case 1:if(S=b.payload,typeof S=="function"){y=S.call(f,y,g);break e}y=S;break e;case 3:S.flags=S.flags&-65537|128;case 0:if(S=b.payload,g=typeof S=="function"?S.call(f,y,g):S,g==null)break e;y=xe({},y,g);break e;case 2:rn=!0}}s.callback!==null&&s.lane!==0&&(e.flags|=64,g=o.effects,g===null?o.effects=[s]:g.push(s))}else f={eventTime:f,lane:g,tag:s.tag,payload:s.payload,callback:s.callback,next:null},m===null?(d=m=f,u=y):m=m.next=f,l|=g;if(s=s.next,s===null){if(s=o.shared.pending,s===null)break;g=s,s=g.next,g.next=null,o.lastBaseUpdate=g,o.shared.pending=null}}while(!0);if(m===null&&(u=y),o.baseState=u,o.firstBaseUpdate=d,o.lastBaseUpdate=m,t=o.shared.interleaved,t!==null){o=t;do l|=o.lane,o=o.next;while(o!==t)}else i===null&&(o.shared.lanes=0);Mn|=l,e.lanes=l,e.memoizedState=y}}function Tu(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=El.transition;El.transition={};try{e(!1),t()}finally{ne=n,El.transition=r}}function xd(){return vt().memoizedState}function Af(e,t,n){var r=gn(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},wd(e))kd(t,n);else if(n=nd(e,t,n,r),n!==null){var o=Ye();At(n,e,r,o),Sd(n,t,r)}}function Pf(e,t,n){var r=gn(e),o={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(wd(e))kd(t,o);else{var i=e.alternate;if(e.lanes===0&&(i===null||i.lanes===0)&&(i=t.lastRenderedReducer,i!==null))try{var l=t.lastRenderedState,s=i(l,n);if(o.hasEagerState=!0,o.eagerState=s,Pt(s,l)){var u=t.interleaved;u===null?(o.next=o,Wa(t)):(o.next=u.next,u.next=o),t.interleaved=o;return}}catch{}finally{}n=nd(e,t,o,r),n!==null&&(o=Ye(),At(n,e,r,o),Sd(n,t,r))}}function wd(e){var t=e.alternate;return e===be||t!==null&&t===be}function kd(e,t){Ur=Si=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Sd(e,t,n){if((n&4194240)!==0){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,Ta(e,n)}}var Ni={readContext:yt,useCallback:je,useContext:je,useEffect:je,useImperativeHandle:je,useInsertionEffect:je,useLayoutEffect:je,useMemo:je,useReducer:je,useRef:je,useState:je,useDebugValue:je,useDeferredValue:je,useTransition:je,useMutableSource:je,useSyncExternalStore:je,useId:je,unstable_isNewReconciler:!1},Tf={readContext:yt,useCallback:function(e,t){return Mt().memoizedState=[e,t===void 0?null:t],e},useContext:yt,useEffect:Mu,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,ti(4194308,4,gd.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ti(4194308,4,e,t)},useInsertionEffect:function(e,t){return ti(4,2,e,t)},useMemo:function(e,t){var n=Mt();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Mt();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Af.bind(null,be,e),[r.memoizedState,e]},useRef:function(e){var t=Mt();return e={current:e},t.memoizedState=e},useState:Ru,useDebugValue:ts,useDeferredValue:function(e){return Mt().memoizedState=e},useTransition:function(){var e=Ru(!1),t=e[0];return e=zf.bind(null,e[1]),Mt().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=be,o=Mt();if(fe){if(n===void 0)throw Error(L(407));n=n()}else{if(n=t(),Te===null)throw Error(L(349));(Rn&30)!==0||ad(r,t,n)}o.memoizedState=n;var i={value:n,getSnapshot:t};return o.queue=i,Mu(ud.bind(null,r,i,e),[e]),r.flags|=2048,lo(9,sd.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Mt(),t=Te.identifierPrefix;if(fe){var n=Ht,r=Bt;n=(r&~(1<<32-zt(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=oo++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=l.createElement(n,{is:r.is}):(e=l.createElement(n),n==="select"&&(l=e,r.multiple?l.multiple=!0:r.size&&(l.size=r.size))):e=l.createElementNS(e,n),e[It]=t,e[to]=r,Rd(e,t,!1,!1),t.stateNode=e;e:{switch(l=Bl(n,r),n){case"dialog":de("cancel",e),de("close",e),o=r;break;case"iframe":case"object":case"embed":de("load",e),o=r;break;case"video":case"audio":for(o=0;ofr&&(t.flags|=128,r=!0,zr(i,!1),t.lanes=4194304)}else{if(!r)if(e=ki(l),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),zr(i,!0),i.tail===null&&i.tailMode==="hidden"&&!l.alternate&&!fe)return Ue(t),null}else 2*ke()-i.renderingStartTime>fr&&n!==1073741824&&(t.flags|=128,r=!0,zr(i,!1),t.lanes=4194304);i.isBackwards?(l.sibling=t.child,t.child=l):(n=i.last,n!==null?n.sibling=l:t.child=l,i.last=l)}return i.tail!==null?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=ke(),t.sibling=null,n=ve.current,le(ve,r?n&1|2:n&1),t):(Ue(t),null);case 22:case 23:return as(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&(t.mode&1)!==0?(tt&1073741824)!==0&&(Ue(t),t.subtreeFlags&6&&(t.flags|=8192)):Ue(t),null;case 24:return null;case 25:return null}throw Error(L(156,t.tag))}function jf(e,t){switch(Ua(t),t.tag){case 1:return Je(t.type)&&gi(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return pr(),pe(Ze),pe(Be),qa(),e=t.flags,(e&65536)!==0&&(e&128)===0?(t.flags=e&-65537|128,t):null;case 5:return Qa(t),null;case 13:if(pe(ve),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(L(340));cr()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return pe(ve),null;case 4:return pr(),null;case 10:return Ka(t.type._context),null;case 22:case 23:return as(),null;case 24:return null;default:return null}}var Yo=!1,Ve=!1,Uf=typeof WeakSet=="function"?WeakSet:Set,O=null;function er(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){we(e,t,r)}else n.current=null}function ha(e,t,n){try{n()}catch(r){we(e,t,r)}}var Ku=!1;function Vf(e,t){if(Jl=di,e=jc(),Da(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var o=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch{n=null;break e}var l=0,s=-1,u=-1,d=0,m=0,y=e,g=null;t:for(;;){for(var f;y!==n||o!==0&&y.nodeType!==3||(s=l+o),y!==i||r!==0&&y.nodeType!==3||(u=l+r),y.nodeType===3&&(l+=y.nodeValue.length),(f=y.firstChild)!==null;)g=y,y=f;for(;;){if(y===e)break t;if(g===n&&++d===o&&(s=l),g===i&&++m===r&&(u=l),(f=y.nextSibling)!==null)break;y=g,g=y.parentNode}y=f}n=s===-1||u===-1?null:{start:s,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(ea={focusedElem:e,selectionRange:n},di=!1,O=t;O!==null;)if(t=O,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,O=e;else for(;O!==null;){t=O;try{var S=t.alternate;if((t.flags&1024)!==0)switch(t.tag){case 0:case 11:case 15:break;case 1:if(S!==null){var b=S.memoizedProps,_=S.memoizedState,p=t.stateNode,c=p.getSnapshotBeforeUpdate(t.elementType===t.type?b:_t(t.type,b),_);p.__reactInternalSnapshotBeforeUpdate=c}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(L(163))}}catch(w){we(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,O=e;break}O=t.return}return S=Ku,Ku=!1,S}function Vr(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var o=r=r.next;do{if((o.tag&e)===e){var i=o.destroy;o.destroy=void 0,i!==void 0&&ha(t,n,i)}o=o.next}while(o!==r)}}function Fi(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function ya(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Od(e){var t=e.alternate;t!==null&&(e.alternate=null,Od(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[It],delete t[to],delete t[ra],delete t[Nf],delete t[Ef])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function $d(e){return e.tag===5||e.tag===3||e.tag===4}function Wu(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||$d(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function va(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=fi));else if(r!==4&&(e=e.child,e!==null))for(va(e,t,n),e=e.sibling;e!==null;)va(e,t,n),e=e.sibling}function ba(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(ba(e,t,n),e=e.sibling;e!==null;)ba(e,t,n),e=e.sibling}var Oe=null,Ct=!1;function tn(e,t,n){for(n=n.child;n!==null;)Fd(e,t,n),n=n.sibling}function Fd(e,t,n){if(Ot&&typeof Ot.onCommitFiberUnmount=="function")try{Ot.onCommitFiberUnmount(Ai,n)}catch{}switch(n.tag){case 5:Ve||er(n,t);case 6:var r=Oe,o=Ct;Oe=null,tn(e,t,n),Oe=r,Ct=o,Oe!==null&&(Ct?(e=Oe,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):Oe.removeChild(n.stateNode));break;case 18:Oe!==null&&(Ct?(e=Oe,n=n.stateNode,e.nodeType===8?kl(e.parentNode,n):e.nodeType===1&&kl(e,n),qr(e)):kl(Oe,n.stateNode));break;case 4:r=Oe,o=Ct,Oe=n.stateNode.containerInfo,Ct=!0,tn(e,t,n),Oe=r,Ct=o;break;case 0:case 11:case 14:case 15:if(!Ve&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){o=r=r.next;do{var i=o,l=i.destroy;i=i.tag,l!==void 0&&((i&2)!==0||(i&4)!==0)&&ha(n,t,l),o=o.next}while(o!==r)}tn(e,t,n);break;case 1:if(!Ve&&(er(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(s){we(n,t,s)}tn(e,t,n);break;case 21:tn(e,t,n);break;case 22:n.mode&1?(Ve=(r=Ve)||n.memoizedState!==null,tn(e,t,n),Ve=r):tn(e,t,n);break;default:tn(e,t,n)}}function Gu(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Uf),t.forEach(function(r){var o=Xf.bind(null,e,r);n.has(r)||(n.add(r),r.then(o,o))})}}function Et(e,t){var n=t.deletions;if(n!==null)for(var r=0;ro&&(o=l),r&=~i}if(r=o,r=ke()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*Hf(r/1960))-r,10e?16:e,sn===null)var r=!1;else{if(e=sn,sn=null,Ci=0,(X&6)!==0)throw Error(L(331));var o=X;for(X|=4,O=e.current;O!==null;){var i=O,l=i.child;if((O.flags&16)!==0){var s=i.deletions;if(s!==null){for(var u=0;uke()-is?zn(e,0):os|=n),et(e,t)}function Wd(e,t){t===0&&((e.mode&1)===0?t=1:(t=Io,Io<<=1,(Io&130023424)===0&&(Io=4194304)));var n=Ye();e=Yt(e,t),e!==null&&(so(e,t,n),et(e,n))}function qf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Wd(e,n)}function Xf(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,o=e.memoizedState;o!==null&&(n=o.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(L(314))}r!==null&&r.delete(t),Wd(e,n)}var Gd;Gd=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||Ze.current)Xe=!0;else{if((e.lanes&n)===0&&(t.flags&128)===0)return Xe=!1,Ff(e,t,n);Xe=(e.flags&131072)!==0}else Xe=!1,fe&&(t.flags&1048576)!==0&&Xc(t,vi,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;ni(e,t),e=t.pendingProps;var o=ur(t,Be.current);lr(t,n),o=Za(null,t,r,e,o,n);var i=Ja();return t.flags|=1,typeof o=="object"&&o!==null&&typeof o.render=="function"&&o.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Je(r)?(i=!0,hi(t)):i=!1,t.memoizedState=o.state!==null&&o.state!==void 0?o.state:null,Ga(t),o.updater=$i,t.stateNode=o,o._reactInternals=t,ua(t,r,e,n),t=pa(null,t,r,!0,i,n)):(t.tag=0,fe&&i&&ja(t),Ge(null,t,o,n),t=t.child),t;case 16:r=t.elementType;e:{switch(ni(e,t),e=t.pendingProps,o=r._init,r=o(r._payload),t.type=r,o=t.tag=Jf(r),e=_t(r,e),o){case 0:t=da(null,t,r,e,n);break e;case 1:t=Vu(null,t,r,e,n);break e;case 11:t=ju(null,t,r,e,n);break e;case 14:t=Uu(null,t,r,_t(r.type,e),n);break e}throw Error(L(306,r,""))}return t;case 0:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:_t(r,o),da(e,t,r,o,n);case 1:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:_t(r,o),Vu(e,t,r,o,n);case 3:e:{if(Ad(t),e===null)throw Error(L(387));r=t.pendingProps,i=t.memoizedState,o=i.element,rd(e,t),wi(t,r,null,n);var l=t.memoizedState;if(r=l.element,i.isDehydrated)if(i={element:r,isDehydrated:!1,cache:l.cache,pendingSuspenseBoundaries:l.pendingSuspenseBoundaries,transitions:l.transitions},t.updateQueue.baseState=i,t.memoizedState=i,t.flags&256){o=mr(Error(L(423)),t),t=Bu(e,t,r,n,o);break e}else if(r!==o){o=mr(Error(L(424)),t),t=Bu(e,t,r,n,o);break e}else for(nt=pn(t.stateNode.containerInfo.firstChild),rt=t,fe=!0,Lt=null,n=td(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(cr(),r===o){t=Qt(e,t,n);break e}Ge(e,t,r,n)}t=t.child}return t;case 5:return od(t),e===null&&la(t),r=t.type,o=t.pendingProps,i=e!==null?e.memoizedProps:null,l=o.children,ta(r,o)?l=null:i!==null&&ta(r,i)&&(t.flags|=32),zd(e,t),Ge(e,t,l,n),t.child;case 6:return e===null&&la(t),null;case 13:return Pd(e,t,n);case 4:return Ya(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=dr(t,null,r,n):Ge(e,t,r,n),t.child;case 11:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:_t(r,o),ju(e,t,r,o,n);case 7:return Ge(e,t,t.pendingProps,n),t.child;case 8:return Ge(e,t,t.pendingProps.children,n),t.child;case 12:return Ge(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,o=t.pendingProps,i=t.memoizedProps,l=o.value,le(bi,r._currentValue),r._currentValue=l,i!==null)if(Pt(i.value,l)){if(i.children===o.children&&!Ze.current){t=Qt(e,t,n);break e}}else for(i=t.child,i!==null&&(i.return=t);i!==null;){var s=i.dependencies;if(s!==null){l=i.child;for(var u=s.firstContext;u!==null;){if(u.context===r){if(i.tag===1){u=Kt(-1,n&-n),u.tag=2;var d=i.updateQueue;if(d!==null){d=d.shared;var m=d.pending;m===null?u.next=u:(u.next=m.next,m.next=u),d.pending=u}}i.lanes|=n,u=i.alternate,u!==null&&(u.lanes|=n),aa(i.return,n,t),s.lanes|=n;break}u=u.next}}else if(i.tag===10)l=i.type===t.type?null:i.child;else if(i.tag===18){if(l=i.return,l===null)throw Error(L(341));l.lanes|=n,s=l.alternate,s!==null&&(s.lanes|=n),aa(l,n,t),l=i.sibling}else l=i.child;if(l!==null)l.return=i;else for(l=i;l!==null;){if(l===t){l=null;break}if(i=l.sibling,i!==null){i.return=l.return,l=i;break}l=l.return}i=l}Ge(e,t,o.children,n),t=t.child}return t;case 9:return o=t.type,r=t.pendingProps.children,lr(t,n),o=yt(o),r=r(o),t.flags|=1,Ge(e,t,r,n),t.child;case 14:return r=t.type,o=_t(r,t.pendingProps),o=_t(r.type,o),Uu(e,t,r,o,n);case 15:return Cd(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:_t(r,o),ni(e,t),t.tag=1,Je(r)?(e=!0,hi(t)):e=!1,lr(t,n),Nd(t,r,o),ua(t,r,o,n),pa(null,t,r,!0,e,n);case 19:return Td(e,t,n);case 22:return Ld(e,t,n)}throw Error(L(156,t.tag))};function Yd(e,t){return wc(e,t)}function Zf(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function gt(e,t,n,r){return new Zf(e,t,n,r)}function us(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jf(e){if(typeof e=="function")return us(e)?1:0;if(e!=null){if(e=e.$$typeof,e===La)return 11;if(e===za)return 14}return 2}function hn(e,t){var n=e.alternate;return n===null?(n=gt(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function ii(e,t,n,r,o,i){var l=2;if(r=e,typeof e=="function")us(e)&&(l=1);else if(typeof e=="string")l=5;else e:switch(e){case Kn:return An(n.children,o,i,t);case Ca:l=8,o|=8;break;case Rl:return e=gt(12,n,t,o|2),e.elementType=Rl,e.lanes=i,e;case Ml:return e=gt(13,n,t,o),e.elementType=Ml,e.lanes=i,e;case Il:return e=gt(19,n,t,o),e.elementType=Il,e.lanes=i,e;case oc:return ji(n,o,i,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case nc:l=10;break e;case rc:l=9;break e;case La:l=11;break e;case za:l=14;break e;case nn:l=16,r=null;break e}throw Error(L(130,e==null?e:typeof e,""))}return t=gt(l,n,t,o),t.elementType=e,t.type=r,t.lanes=i,t}function An(e,t,n,r){return e=gt(7,e,r,t),e.lanes=n,e}function ji(e,t,n,r){return e=gt(22,e,r,t),e.elementType=oc,e.lanes=n,e.stateNode={isHidden:!1},e}function Al(e,t,n){return e=gt(6,e,null,t),e.lanes=n,e}function Pl(e,t,n){return t=gt(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function eg(e,t,n,r,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=fl(0),this.expirationTimes=fl(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=fl(0),this.identifierPrefix=r,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function cs(e,t,n,r,o,i,l,s,u){return e=new eg(e,t,n,s,u),t===1?(t=1,i===!0&&(t|=8)):t=0,i=gt(3,null,null,t),e.current=i,i.stateNode=e,i.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ga(i),e}function tg(e,t,n){var r=3{"use strict";function Jd(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(Jd)}catch(e){console.error(e)}}Jd(),ep.exports=Zd()});var rp=jt(fs=>{"use strict";var np=tp();fs.createRoot=np.createRoot,fs.hydrateRoot=np.hydrateRoot;var _h});var ip=jt(Ki=>{"use strict";var lg=Eo(),ag=Symbol.for("react.element"),sg=Symbol.for("react.fragment"),ug=Object.prototype.hasOwnProperty,cg=lg.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,dg={key:!0,ref:!0,__self:!0,__source:!0};function op(e,t,n){var r,o={},i=null,l=null;n!==void 0&&(i=""+n),t.key!==void 0&&(i=""+t.key),t.ref!==void 0&&(l=t.ref);for(r in t)ug.call(t,r)&&!dg.hasOwnProperty(r)&&(o[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)o[r]===void 0&&(o[r]=t[r]);return{$$typeof:ag,type:e,key:i,ref:l,props:o,_owner:cg.current}}Ki.Fragment=sg;Ki.jsx=op;Ki.jsxs=op});var gs=jt((Ph,lp)=>{"use strict";lp.exports=ip()});var k=wo(Eo(),1),Sp=wo(rp(),1);var a=wo(gs(),1),Wi=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,pg="http://localhost:8080",Np="https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl",ap=["localhost","k3s_localhost","k3s_vps"],mg={localhost:"LOCALHOST",k3s_localhost:"K3S@LOCALHOST",k3s_vps:"K3S@VPS"};function sp(e){try{let t=new URL(String(e??""));return["http:","https:"].includes(t.protocol)?(t.search="",t.hash="",t.href.replace(/\/$/,"")):""}catch{return""}}function fg(){return window.location.pathname.split("/").filter(Boolean).find(e=>Wi.test(e))?.toLowerCase()??""}function gg(){let e=new URL(Np).host;return window.location.host===e?"k3s_vps":["localhost","127.0.0.1","::1"].includes(window.location.hostname)&&window.location.port==="8080"?"localhost":"k3s_localhost"}function hg(){let e=window.__STEM_CARD_RUNTIME__,t=ap.includes(e?.active_source)?e?.active_source:gg(),n=new Map(Array.isArray(e?.sources)?e.sources.map(o=>[o?.id,o]):[]),r=ap.map(o=>{let i=n.get(o),l=o==="localhost"?t===o?window.location.origin:pg:o==="k3s_vps"?Np:t===o?window.location.origin:"",s=sp(i?.base_url)||sp(l),u=t===o;return{id:o,label:String(i?.label||mg[o]),base_url:s,available:u||!!s&&i?.available!==!1,reason:String(i?.reason||(s?"":"\u0179r\xF3d\u0142o nie ma skonfigurowanego adresu"))}});return{schema:"stem.card-runtime/v1",default_source:"localhost",active_source:t,current_resource_uuid:String(e?.current_resource_uuid||fg()).toLowerCase(),sources:r}}function Ep(e,t,n=""){return!e.available||!e.base_url||!Wi.test(t)?"":`${e.base_url}/${t.toLowerCase()}/${n}`}function yg(e){let t=String(e.resource_uuid??"").toLowerCase();if(Wi.test(t))return t;for(let n of[e.local_href,e.href])try{let r=new URL(String(n??""),window.location.href).pathname.split("/").filter(Boolean).find(o=>Wi.test(o));if(r)return r.toLowerCase()}catch{}return""}function vg(e,t){return Ep(t,yg(e))||e.local_href||e.href}var _p={hidden:"UKRYTY",connecting:"\u0141\u0104CZENIE",connected:"ONLINE \xB7 LIVE",replaying:"ONLINE \xB7 REPLAY",verifying:"ONLINE \xB7 WERYFIKACJA",ready:"ONLINE \xB7 GOTOWY",failed:"ONLINE \xB7 B\u0141\u0104D STANU",offline:"OFFLINE \xB7 ZAPIS",unselected:"OFFLINE \xB7 BRAK CELU"},up=.25,Dn=1,cp=4;function bg(e){let t=window.getComputedStyle(e).overflowY;return t==="auto"||t==="scroll"||t==="hidden"||t==="clip"}function Cp(e){let t=e.getBoundingClientRect(),n=e.offsetHeight||e.clientHeight,r=n>0?t.height/n:1,o=t.top+e.clientTop*r;return{top:o,bottom:o+e.clientHeight*r}}function Lp(e){let t=document.querySelector(".viewer-chrome");if(t?.classList.contains("is-nvim-fit"))return null;let n=0,r=window.innerHeight;if(t){let o=t.getBoundingClientRect(),i=e.getBoundingClientRect();o.lefti.left&&o.bottom>0&&o.topDn?{top:n,bottom:r}:null}function xg(e){let t=Lp(e);if(!t)return null;let{top:n,bottom:r}=t;for(let o=e.parentElement;o&&o!==document.body&&o!==document.documentElement;o=o.parentElement){if(!bg(o))continue;let i=Cp(o);n=Math.max(n,i.top),r=Math.min(r,i.bottom)}return r-n>Dn?{top:n,bottom:r}:null}function Qi(){return document.scrollingElement??document.documentElement}function wg(e){let t=[];for(let r=e.parentElement;r&&r!==document.body&&r!==document.documentElement;r=r.parentElement){let o=window.getComputedStyle(r).overflowY;(o==="auto"||o==="scroll")&&r.scrollHeight-r.clientHeight>1&&t.push(r)}let n=Qi();return t.includes(n)||t.push(n),t}function kg(e){if(e===Qi())return 1;let t=e.getBoundingClientRect(),n=e.offsetHeight||e.clientHeight,r=n>0?t.height/n:1;return Number.isFinite(r)&&r>0?r:1}function Gi(e,t){let n=e.style.scrollBehavior;e.style.scrollBehavior="auto",t.left!=null&&(e.scrollLeft=t.left),t.top!=null&&(e.scrollTop=t.top),e.style.scrollBehavior=n}function Sg(e,t){let n=e.closest(".sheet-content");if(!n)return 0;let r=Cp(n),o=0;if(r.bottom<=t.top?o=r.bottom-t.bottom:r.top>=t.bottom&&(o=r.top-t.top),Math.abs(o)<=Dn)return 0;let i=Qi(),l=i.scrollTop,s=Math.max(0,i.scrollHeight-i.clientHeight);return Gi(i,{top:Math.max(0,Math.min(s,l+o))}),i.scrollTop-l}function dp(e,t){let n=t;for(let r of wg(e)){if(Math.abs(n)<=Dn)break;let o=kg(r),i=Math.max(0,r.scrollHeight-r.clientHeight),l=r.scrollTop,s=Math.max(0,Math.min(i,l+n/o));Gi(r,{top:s}),n-=(r.scrollTop-l)*o}return t-n}function pp(e,t){let n=e.getBoundingClientRect(),r=(n.top+n.bottom)/2,o=t.bottom-t.top,i=t.top+o*up,l=t.bottom-o*up;return rl?r-l:0}var Ft=({html:e,className:t})=>(0,a.jsx)("div",{className:t,dangerouslySetInnerHTML:{__html:e}});function zp(e,t){e.setAttribute("preserveAspectRatio","xMinYMin meet"),t&&(e.setAttribute("viewBox",`${t.x} ${t.y} ${t.width} ${t.height}`),e.setAttribute("width",`${t.width}px`),e.setAttribute("height",`${t.height}px`),e.dataset.tileIndex=String(t.index),e.dataset.tileRow=String(t.row),e.dataset.tileColumn=String(t.column))}function Ng(e){return e.normalize("NFKD").replace(/[\u0300-\u036f]/g,"").toLowerCase().replace(/[^a-z0-9_.:-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,120)||"uml-element"}function ys(e){let t=e;if(typeof t.getBBox!="function")return null;try{let n=t.getBBox();return[n.x,n.y,n.width,n.height].every(Number.isFinite)?{x:n.x,y:n.y,width:n.width,height:n.height}:null}catch{return null}}function Eg(e){let t=ys(e);if(!t)return null;let n=Number(e.dataset.umlLayoutOriginX),r=Number(e.dataset.umlLayoutOriginY),o=Number(e.dataset.umlLayoutDx),i=Number(e.dataset.umlLayoutDy),l=Number(e.dataset.umlLayoutSx),s=Number(e.dataset.umlLayoutSy);return[n,r,o,i,l,s].every(Number.isFinite)?{x:n+(t.x-n)*l+o,y:r+(t.y-r)*s+i,width:t.width*l,height:t.height*s}:t}function Ap(e){e.dataset.umlLayoutRemembered!=="true"&&(e.dataset.umlLayoutRemembered="true",e.dataset.umlLayoutOriginalTransform=e.getAttribute("transform")??"",e.dataset.umlLayoutOriginalVisibility=e.getAttribute("visibility")??"",e.dataset.umlLayoutOriginalStyle=e.getAttribute("style")??"",e instanceof SVGTextElement&&(e.dataset.umlLayoutOriginalText=e.textContent??"",e.dataset.umlLayoutOriginalX=e.getAttribute("x")??"",e.dataset.umlLayoutOriginalTextAnchor=e.getAttribute("text-anchor")??"",e.dataset.umlLayoutOriginalFontFamily=e.getAttribute("font-family")??"",e.dataset.umlLayoutOriginalFontSize=e.getAttribute("font-size")??"",e.dataset.umlLayoutOriginalFontWeight=e.getAttribute("font-weight")??""),e instanceof SVGRectElement&&(e.dataset.umlLayoutOriginalFill=e.getAttribute("fill")??"",e.dataset.umlLayoutOriginalStroke=e.getAttribute("stroke")??"",e.dataset.umlLayoutOriginalStrokeWidth=e.getAttribute("stroke-width")??""))}function _g(e){e.querySelectorAll(".uml-layout-hit,.uml-layout-resize,.uml-layout-generated").forEach(t=>t.remove()),e.querySelectorAll("[data-uml-layout-remembered='true']").forEach(t=>{let n=(r,o)=>{o?t.setAttribute(r,o):t.removeAttribute(r)};n("transform",t.dataset.umlLayoutOriginalTransform),n("visibility",t.dataset.umlLayoutOriginalVisibility),n("style",t.dataset.umlLayoutOriginalStyle),t instanceof SVGTextElement&&(t.textContent=t.dataset.umlLayoutOriginalText??"",n("x",t.dataset.umlLayoutOriginalX),n("text-anchor",t.dataset.umlLayoutOriginalTextAnchor),n("font-family",t.dataset.umlLayoutOriginalFontFamily),n("font-size",t.dataset.umlLayoutOriginalFontSize),n("font-weight",t.dataset.umlLayoutOriginalFontWeight)),t instanceof SVGRectElement&&(n("fill",t.dataset.umlLayoutOriginalFill),n("stroke",t.dataset.umlLayoutOriginalStroke),n("stroke-width",t.dataset.umlLayoutOriginalStrokeWidth)),delete t.dataset.umlLayoutOriginX,delete t.dataset.umlLayoutOriginY,delete t.dataset.umlLayoutDx,delete t.dataset.umlLayoutDy,delete t.dataset.umlLayoutSx,delete t.dataset.umlLayoutSy,delete t.dataset.umlLayoutId})}function Cg(e,t){let n=e.querySelector(":scope > g")??e,r=Array.from(n.children).filter(s=>s instanceof SVGElement),o=r.filter(s=>s instanceof SVGRectElement).map(s=>({rect:s,box:ys(s)})).filter(s=>!!(s.box&&s.box.width>=30&&s.box.height>=14)).sort((s,u)=>s.box.width*s.box.height-u.box.width*u.box.height),i=new Map;for(let s of r){let u=ys(s);if(!u)continue;let d=u.x+u.width/2,m=u.y+u.height/2,y=o.find(({box:g})=>d>=g.x-.5&&d<=g.x+g.width+.5&&m>=g.y-.5&&m<=g.y+g.height+.5);y&&i.set(s,y.rect)}let l=new Set;return o.map(({rect:s,box:u},d)=>{let m=r.filter(c=>i.get(c)===s);m.includes(s)||m.unshift(s);let y=m.filter(c=>c instanceof SVGTextElement).sort((c,h)=>Number(c.getAttribute("y"))-Number(h.getAttribute("y"))),g=(y[0]?.textContent??s.id??`element-${d+1}`).trim(),f=/^(\d{1,3})\b/.exec(g)?.[1]?.padStart(2,"0"),S=s.id||(f?t.get(f):"")||g,b=Ng(S),_=b,p=2;for(;l.has(_);)_=`${b}-${p++}`;return l.add(_),m.forEach(Ap),{id:_,box:u,rect:s,members:m,texts:y}})}function mo(e){let t=e.texts[0],n=e.rect.getAttribute("style")??"",r=i=>new RegExp(`${i}\\s*:\\s*([^;]+)`,"i").exec(n)?.[1]?.trim(),o=Number(t?.getAttribute("font-size")??12);return{...e.box,z:0,scale:1,rotation:0,text:{title:(e.texts[0]?.textContent??"").trim(),description:e.texts.slice(1).map(i=>(i.textContent??"").trim()).join(` +`),lines:e.texts.map(i=>(i.textContent??"").trim()),font_family:t?.getAttribute("font-family")??"Monospace",font_size:Number.isFinite(o)?o:12,font_weight:Number(t?.getAttribute("font-weight")??400)||400,align:"left"},style:{fill:e.rect.getAttribute("fill")??r("fill")??"#edf6fa",stroke:e.rect.getAttribute("stroke")??r("stroke")??"#2c7794",stroke_width:Number(e.rect.getAttribute("stroke-width")??r("stroke-width")??1.5)}}}function Lg(e,t){let n=Math.max(12,Number(t.width)||e.box.width),r=Math.max(12,Number(t.height)||e.box.height),o=Number.isFinite(Number(t.x))?Number(t.x):e.box.x,i=Number.isFinite(Number(t.y))?Number(t.y):e.box.y,l=n/Math.max(1,e.box.width),s=r/Math.max(1,e.box.height),u=o-e.box.x,d=i-e.box.y,m=`translate(${u} ${d}) translate(${e.box.x} ${e.box.y}) scale(${l} ${s}) translate(${-e.box.x} ${-e.box.y})`;e.members.forEach(f=>{Ap(f);let S=f.dataset.umlLayoutOriginalTransform??"";f.setAttribute("transform",`${m}${S?` ${S}`:""}`),f.dataset.umlLayoutOriginX=String(e.box.x),f.dataset.umlLayoutOriginY=String(e.box.y),f.dataset.umlLayoutDx=String(u),f.dataset.umlLayoutDy=String(d),f.dataset.umlLayoutSx=String(l),f.dataset.umlLayoutSy=String(s),f.dataset.umlLayoutId=e.id});let y=t.text??{},g=y.lines??[y.title??"",...y.description?y.description.split(` +`):[]];e.texts.forEach((f,S)=>{S(e.tile?.page_height??0)?"landscape":"portrait",scale:1},elements:{},relations:{}}}function vs(e){if(!e)return{};let t=e.count===1,n=t?Math.max(0,(e.page_width-e.width)/2):e.column===1&&e.columns>1?e.page_width-e.width:e.column===e.columns?0:Math.max(0,(e.page_width-e.width)/2),r=t?0:e.row===1&&e.rows>1?e.page_height-e.height:e.row===e.rows?0:Math.max(0,(e.page_height-e.height)/2);return{"--uml-tile-page-width":`${e.page_width}px`,"--uml-tile-page-height":`${t?e.height:e.page_height}px`,"--uml-tile-left":`${n}px`,"--uml-tile-top":`${r}px`}}function zg({html:e}){return(0,a.jsx)(Ft,{html:e})}function Ag({html:e=""}){return e?(0,a.jsx)(Ft,{html:e}):null}function Pg(e){let t=e.spread_count??1;if(t<=1)return null;let n=Math.max(1,Math.min(t,e.spread_index??1)),r=Math.max(1,Math.min(t,e.spread_columns??1)),o=n-1,i=Math.floor(o/r),l=o%r;return{left:l>0,right:l+10,down:n+r<=t,index:n,count:t}}function Pp({continuation:e}){if(!e)return null;let t=[e.left&&["left","\u2190","kontynuacja po lewej"],e.up&&["up","\u2191","kontynuacja powy\u017Cej"],e.down&&["down","\u2193","kontynuacja poni\u017Cej"],e.right&&["right","\u2192","kontynuacja po prawej"]].filter(Boolean);return(0,a.jsx)("span",{className:"uml-page-continuation","aria-label":`Logiczny diagram: strona ${e.index} z ${e.count}`,title:`Ten sam diagram \xB7 strona ${e.index}/${e.count}`,children:t.map(([n,r,o])=>(0,a.jsx)("i",{"data-direction":n,"aria-label":o,children:r},n))})}function mp({side:e,html:t}){return(0,a.jsx)("aside",{className:`pdf-margin ${e}`,dangerouslySetInnerHTML:{__html:t}})}function bs({header:e,footer:t="",left:n="",right:r="",children:o,className:i=""}){return(0,a.jsx)("article",{className:`sheet ${i}`,children:(0,a.jsxs)("div",{className:"sheet-content",children:[(0,a.jsx)(zg,{html:e}),(0,a.jsx)(mp,{side:"left",html:n}),(0,a.jsx)("section",{className:"pdf-main card-section",children:o}),(0,a.jsx)(mp,{side:"right",html:r}),(0,a.jsx)(Ag,{html:t})]})})}function Tg({viewpoints:e}){let t=e.filter(i=>i.status==="enabled"&&i.target_id),[n,r]=(0,k.useState)(t[0]?.id??"");(0,k.useEffect)(()=>{if(!t.length)return;let i=0,l=()=>{window.cancelAnimationFrame(i),i=window.requestAnimationFrame(()=>{let s=document.querySelector(".viewer-chrome")?.getBoundingClientRect().bottom??0,u=t.map(m=>({viewpoint:m,element:document.getElementById(m.target_id)})).filter(m=>!!m.element);if(!u.length)return;let d=u.reduce((m,y)=>{let g=Math.abs(y.element.getBoundingClientRect().top-s-18);return g{window.cancelAnimationFrame(i),window.removeEventListener("scroll",l),window.removeEventListener("resize",l)}},[e]);let o=i=>{if(i.status!=="enabled"||!i.target_id)return;let l=document.getElementById(i.target_id);if(!l)return;let u=document.querySelector(".viewer-chrome")?.getBoundingClientRect().height??0;r(i.id),window.scrollTo({top:Math.max(0,window.scrollY+l.getBoundingClientRect().top-u-8),behavior:"smooth"})};return(0,a.jsx)("nav",{className:"viewpoint-bar","aria-label":"Perspektywy architektury A1\u2013A8",children:e.map(i=>{let l=i.status==="unavailable";return(0,a.jsxs)("button",{type:"button",className:l?"is-unavailable":"","aria-current":!l&&n===i.id?"location":void 0,disabled:l,title:l?i.reason:i.subtitle,onClick:()=>o(i),children:[(0,a.jsx)("span",{children:i.id}),(0,a.jsx)("strong",{children:i.label}),l&&(0,a.jsx)("small",{children:"N/D"})]},i.id)})})}function Rg({runtime:e,onSelect:t}){let n=e.sources.find(r=>r.id===e.active_source);return(0,a.jsxs)("label",{className:"runtime-source-selector","data-stem-source-selector":"true",title:n?.base_url||n?.reason||"Wyb\xF3r \u017Ar\xF3d\u0142a aplikacji",children:[(0,a.jsx)("span",{children:"\u0179R\xD3D\u0141O"}),(0,a.jsx)("select",{"aria-label":"\u0179r\xF3d\u0142o aplikacji",value:e.active_source,onChange:r=>t(r.currentTarget.value),children:e.sources.map(r=>(0,a.jsxs)("option",{value:r.id,disabled:!r.available,children:[r.label,r.available?"":" \xB7 BRAK"]},r.id))})]})}function Mg({toc:e,runtime:t,onSourceSelect:n,libraryAvailable:r,libraryOpen:o,setLibraryOpen:i,scale:l,setScale:s,nvimVisible:u,setNvimVisible:d,nvimSyncMode:m,setNvimSyncMode:y,nvimControlMode:g,setNvimControlMode:f,nvimExpanded:S,setNvimExpanded:b,nvimFit:_,setNvimFit:p,allocatorVisible:c,setAllocatorVisible:h,hasAllocatorModel:w,diagramSpread:C,setDiagramSpread:A,hasDiagramSpread:P,nvimSummary:z,progress:U,progressError:F}){return(0,a.jsx)("nav",{className:"topbar",children:(0,a.jsxs)("div",{className:"topbar-inner",children:[r&&(0,a.jsxs)("button",{type:"button",className:`card-library-toggle${o?" is-active":""}`,"aria-controls":"side-panel-left-library","aria-expanded":o,"aria-label":o?"Alt+0: zamknij serie i karty":"Alt+0: poka\u017C serie i karty",title:o?"Alt+0 \xB7 Zamknij serie i karty":"Alt+0 \xB7 Serie i karty",onClick:()=>i(!o),children:[(0,a.jsx)("span",{"aria-hidden":"true",className:"card-library-toggle-icon",children:"\u2630"}),(0,a.jsx)("kbd",{children:"A0"}),(0,a.jsx)("span",{children:"KARTY"})]}),(0,a.jsx)(Rg,{runtime:t,onSelect:n}),(0,a.jsxs)("details",{children:[(0,a.jsx)("summary",{children:"Spis tre\u015Bci"}),(0,a.jsx)("div",{className:"toc-links",children:e.map(ae=>(0,a.jsx)("a",{href:`#${ae.id}`,children:ae.label},ae.id))})]}),(0,a.jsxs)("div",{className:"topbar-nvim-section","aria-label":"Panel Neovima",children:[(0,a.jsx)("span",{className:"nvim-toolbar-boundary","aria-hidden":"true",children:"|"}),(0,a.jsx)("span",{className:"nvim-toolbar-context",children:"NVIM"}),(0,a.jsxs)("div",{className:"nvim-toolbar-actions","aria-label":"Narz\u0119dzia okna Neovima",children:[(0,a.jsxs)("button",{className:"viewer-nvim-toggle nvim-toolbar-button","aria-label":u?"Alt+1: ukryj Neovim":"Alt+1: poka\u017C Neovim",title:u?"Alt+1 \xB7 Ukryj Neovim":"Alt+1 \xB7 Poka\u017C Neovim","aria-pressed":u,onClick:()=>d(!u),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A1"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"PANEL"})]}),(0,a.jsxs)("button",{type:"button",className:`nvim-control-indicator nvim-toolbar-button${z.syncMode?" is-active":""}${z.syncReady?" is-engaged":""}`,"aria-label":`Alt+2: synchronizacja ${m?"w\u0142\u0105czona":"wy\u0142\u0105czona"}`,title:`Alt+2 \xB7 SYNC ${m?"ON":"OFF"}`,"aria-pressed":m,onClick:()=>y(!m),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A2"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"SYNC"})]}),(0,a.jsxs)("button",{type:"button",className:`nvim-work-indicator nvim-toolbar-button${g?" is-active":""}${z.controlEnabled?" is-engaged":""}`,"aria-label":`Alt+3: sterowanie ${m?g?"w\u0142\u0105czone":"wy\u0142\u0105czone":"niedost\u0119pne"}`,title:m?`Alt+3 \xB7 Sterowanie ${g?"ON":"OFF"}`:"Alt+3 \xB7 Sterowanie wymaga SYNC ON","aria-pressed":g,disabled:!m,onClick:()=>f(!g),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A3"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"CTRL"})]}),(0,a.jsxs)("button",{type:"button",className:`nvim-height-toggle nvim-toolbar-button${S?" is-active":""}`,"aria-label":`Alt+4: ${S?"przywr\xF3\u0107 normaln\u0105 wysoko\u015B\u0107":"ustaw wysoki panel"}`,title:S?"Alt+4 \xB7 Normalna wysoko\u015B\u0107":"Alt+4 \xB7 Wysoki panel 90%","aria-pressed":S,onClick:()=>b(!S),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A4"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"HIGH"})]}),(0,a.jsxs)("button",{type:"button",className:`nvim-height-toggle nvim-toolbar-button${_?" is-active":""}`,"aria-label":`Alt+5: automatyczne dopasowanie ${_?"w\u0142\u0105czone":"wy\u0142\u0105czone"}`,title:_?"Alt+5 \xB7 Wy\u0142\u0105cz auto-fit":"Alt+5 \xB7 Poka\u017C ca\u0142y Neovim","aria-pressed":_,onClick:()=>p(!_),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A5"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"FIT"})]}),w&&(0,a.jsxs)("button",{type:"button",className:`nvim-height-toggle nvim-toolbar-button${c?" is-active":""}`,"aria-label":`Alt+6: model alokatora ${c?"przypi\u0119ty":"ukryty"}`,title:c?"Alt+6 \xB7 Ukryj model alokatora":"Alt+6 \xB7 Przypnij model alokatora","aria-pressed":c,onClick:()=>h(!c),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A6"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"MODEL"})]}),P&&(0,a.jsxs)("button",{type:"button",className:`nvim-height-toggle nvim-toolbar-button${C?" is-active":""}`,"aria-label":`Alt+7: diagram UML ${C?"jako osobne kartki A4":"jako po\u0142\u0105czony spread"}`,title:C?"Alt+7 \xB7 Osobne kartki A4":"Alt+7 \xB7 Po\u0142\u0105cz strony UML","aria-pressed":C,onClick:()=>A(!C),children:[(0,a.jsx)("kbd",{className:"nvim-toolbar-key",children:"A7"}),(0,a.jsx)("span",{className:"nvim-toolbar-label",children:"UML"})]})]}),(0,a.jsx)("span",{className:"nvim-toolbar-boundary","aria-hidden":"true",children:"|"}),u&&(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("span",{className:`nvim-status nvim-status--${z.state}`,children:_p[z.state]}),(0,a.jsxs)("span",{className:"topbar-nvim-meta",title:z.message,children:[(0,a.jsx)("strong",{children:z.instance??"Neovim MCP"}),z.detail&&(0,a.jsx)("small",{children:z.detail})]}),(0,a.jsxs)("span",{className:"nvim-shortcut-strip","aria-label":"Skr\xF3ty panelu Neovima",children:[(0,a.jsx)("kbd",{children:"F1"})," reset \xB7 ",(0,a.jsx)("kbd",{children:"F2"})," sync \xB7 ",(0,a.jsx)("kbd",{children:"Alt+W"})," okna \xB7 ",(0,a.jsx)("kbd",{children:"F12"})," skr\xF3ty"]})]})]}),(0,a.jsx)(ah,{progress:U,error:F}),(0,a.jsxs)("div",{className:"viewer-zoom-controls",children:[(0,a.jsxs)("output",{className:"viewer-zoom-status",children:["P\u0142\xF3tno ",Math.round(l*100),"% \xB7 Tre\u015B\u0107 100%"]}),(0,a.jsx)("button",{className:"viewer-zoom-reset",onClick:()=>s(2.18),children:"Reset zoom"})]})]})})}function fp(e,t){for(let n of e.sections)for(let r of n.assets){let o=r.interactive;if(!(o?.kind!=="uml-sequence"||!o.phases?.length)&&!(t?.task.id&&o.task?.id!==t.task.id)&&!(t?.block.id&&o.block?.id!==t.block.id))for(let i of o.phases){if(t?.phase.id&&i.id!==t.phase.id)continue;let l=t?.step.id?i.steps.find(s=>s.id===t.step.id):i.steps[0];if(l)return{task:o.task??null,block:o.block??null,phase:{id:i.id,label:i.label},step:l,focusLevel:t?.focus_level??"step",activate:!1}}}return null}function Ig({visible:e,model:t}){let[n,r]=(0,k.useState)(null);if((0,k.useEffect)(()=>{let g=f=>{r(f.detail??null)};return window.addEventListener("stem:nvim-step",g),r(fp(t)),fetch("/api/navigation/current",{cache:"no-store"}).then(f=>f.ok?f.json():Promise.reject()).then(f=>{let S=fp(t,f.current??null);S&&r(S)}).catch(()=>{}),()=>window.removeEventListener("stem:nvim-step",g)},[t]),!e)return null;let o=n?.focusLevel??"card",i=n?.step.snapshot?.memory.arena,l=!!(n&&o!=="card"&&o!=="task"),s=Math.max(1,Number(i?.size_bytes??64)),u=i?.offset==null?null:Math.max(0,Math.min(s,Number(i.offset))),d=i?.bytes.trim().split(/\s+/).filter(Boolean)??[],m=Math.min(s,64),y=n?[...(n.step.snapshot?.memory.items??[]).map(g=>({name:g.name,value:g.value})),...(n.step.snapshot?.frame.fields??[]).map(g=>({name:g.name,value:g.value}))].filter((g,f,S)=>S.findIndex(b=>b.name===g.name)===f).slice(0,8):[];return(0,a.jsxs)("section",{className:"allocator-panel","aria-label":"Przypi\u0119ty model alokatora",children:[(0,a.jsxs)("header",{children:[(0,a.jsx)("span",{children:"ALLOCATOR \xB7 PINNED"}),(0,a.jsx)("strong",{children:n?`${n.phase.label} \xB7 ${String(n.step.number).padStart(2,"0")} ${n.step.label}`:"Oczekiwanie na diagram UML"}),(0,a.jsx)("small",{children:l?n.step.snapshot?.description??n.step.description:"Wybierz co najmniej BLOCK. F2 wybierze jego pierwszy krok i wykona synchronizacj\u0119."})]}),(0,a.jsxs)("div",{className:"allocator-panel-body",children:[(0,a.jsxs)("dl",{className:"allocator-metrics",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("dt",{children:"FOCUS"}),(0,a.jsx)("dd",{children:o.toUpperCase()})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)("dt",{children:"BASE"}),(0,a.jsx)("dd",{children:i?.base??"\u2014"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)("dt",{children:"ALLOCP"}),(0,a.jsx)("dd",{children:i?.cursor??"\u2014"})]}),(0,a.jsxs)("div",{children:[(0,a.jsx)("dt",{children:"USED / FREE"}),(0,a.jsx)("dd",{children:u==null?"\u2014":`${u} / ${s-u} B`})]})]}),(0,a.jsxs)("div",{className:`allocator-arena-view${l?"":" is-inactive"}`,children:[(0,a.jsxs)("div",{className:"allocator-arena-labels",children:[(0,a.jsx)("span",{children:"allocbuf[0]"}),(0,a.jsx)("strong",{children:u==null?"allocp jeszcze nieustawiony":`offset ${u}`}),(0,a.jsxs)("span",{children:["allocbuf[",s-1,"]"]})]}),(0,a.jsxs)("div",{className:"allocator-pinned-arena",style:{"--allocator-size":m},children:[Array.from({length:m},(g,f)=>(0,a.jsx)("i",{className:u!=null&&f(0,a.jsxs)("div",{children:[(0,a.jsx)("span",{children:g.name}),(0,a.jsx)("code",{children:g.value})]},g.name)):(0,a.jsx)("p",{children:"Brak snapshotu."})})]})]})}var Yi=()=>({text:" ",highlight:0});function gp(e=1,t=1){return{width:e,height:t,cells:Array.from({length:t},()=>Array.from({length:e},Yi)),highlights:new Map,foreground:15263976,background:1053461,cursor:{row:0,column:0}}}function Og(e,t,n){if(t<=0||n<=0)return;let r=e.cells;e.width=t,e.height=n,e.cells=Array.from({length:n},(o,i)=>Array.from({length:t},(l,s)=>r[i]?.[s]??Yi()))}function Tp(e){return`stem-card:nvim-grid:${e}`}function $g(e,t){if(t.width<=1||t.height<=1)return;let n={schema:"stem-nvim-grid.v1",snapshot_ref:e,saved_at:new Date().toISOString(),width:t.width,height:t.height,cells:t.cells.map(r=>r.map(o=>[o.text,o.highlight])),highlights:Array.from(t.highlights.entries()),foreground:t.foreground,background:t.background,cursor:{...t.cursor}};try{localStorage.setItem(Tp(e),JSON.stringify(n))}catch{}}function Rp(e,t){try{return e?.schema!=="stem-nvim-grid.v1"||e.snapshot_ref!==t||!Number.isInteger(e.width)||!Number.isInteger(e.height)||e.width<2||e.height<2||e.width>400||e.height>200||e.cells.length!==e.height?null:{width:e.width,height:e.height,cells:e.cells.map(n=>n.map(([r,o])=>({text:typeof r=="string"?r:" ",highlight:Number.isInteger(o)?o:0}))),highlights:new Map(e.highlights),foreground:e.foreground,background:e.background,cursor:e.cursor}}catch{return null}}function Fg(e){try{return Rp(JSON.parse(localStorage.getItem(Tp(e))??"null"),e)}catch{return null}}async function hp(e){let t=e.snapshot_ref;if(!t)return null;let n=Fg(t);if(n)return n;let r=e.view?.recorded_grid_ref;if(!r||/^(?:[a-z]+:|\/\/)/i.test(r))return null;try{let o=await fetch(r,{cache:"no-store"});return o.ok?Rp(await o.json(),t):null}catch{return null}}var Dg=[1842207,12590120,3064446,16106001,1997028,9978299,702940,12631996,6184036,15545147,5759881,16311388,5349887,12607947,5231357,16184820];function ho(e){let t=Number(e);if(!Number.isInteger(t)||t<0||t>255)return;if(t<16)return Dg[t];if(t<232){let r=t-16,o=[0,95,135,175,215,255],i=o[Math.floor(r/36)],l=o[Math.floor(r%36/6)],s=o[r%6];return i<<16|l<<8|s}let n=8+(t-232)*10;return n<<16|n<<8|n}function jg(e){if(!e||typeof e!="object")return null;let t=e;return Object.keys(t).length?{...t,foreground:ho(t.foreground),background:ho(t.background),special:ho(t.special)}:null}function Ug(e){if(!e||typeof e!="object")return null;let t=e;return Object.keys(t).length?{...t}:null}function Vg(e,t){for(let n of t){if(!Array.isArray(n)||typeof n[0]!="string")continue;let r=n[0],o=n.slice(1);for(let i of o)if(Array.isArray(i)){if(r==="grid_resize"){let[l,s,u]=i.map(Number);l===1&&Og(e,s,u)}else if(r==="default_colors_set"){let l=Number(i[0]),s=Number(i[1]),u=l>=0?l:ho(i[3]),d=s>=0?s:ho(i[4]);typeof u=="number"&&Number.isFinite(u)&&u>=0&&(e.foreground=u),typeof d=="number"&&Number.isFinite(d)&&d>=0&&(e.background=d)}else if(r==="hl_attr_define"){let l=Number(i[0]),s=jg(i[2])??Ug(i[1]);Number.isInteger(l)&&s&&typeof s=="object"&&e.highlights.set(l,s)}else if(r==="grid_clear"&&Number(i[0])===1)e.cells=Array.from({length:e.height},()=>Array.from({length:e.width},Yi));else if(r==="grid_cursor_goto"&&Number(i[0])===1)e.cursor={row:Number(i[1]),column:Number(i[2])};else if(r==="grid_line"&&Number(i[0])===1){let l=Number(i[1]),s=Number(i[2]),u=i[3];if(!e.cells[l]||!Array.isArray(u))continue;let d=0;for(let m of u){if(!Array.isArray(m))continue;let y=typeof m[0]=="string"?m[0]:" ";Number.isInteger(m[1])&&(d=Number(m[1]));let g=Number.isInteger(m[2])?Number(m[2]):1;for(let f=0;f=0&&(e.cells[l][s]={text:y,highlight:d}),s+=1}}else if(r==="grid_scroll"&&Number(i[0])===1){let l=Number(i[1]),s=Number(i[2]),u=Number(i[3]),d=Number(i[4]),m=Number(i[5]),y=Number(i[6]),g=e.cells.map(f=>f.map(S=>({...S})));for(let f=l;f=l&&b=u&&_i.text||" ").join(""),r=[],o=0;for(;o=n&&t.cursor.rowz.column-P.column||P.row-z.row):["asm","reg","memory"].includes(C??"")&&w.sort((P,z)=>P.column-z.column||P.row-z.row);let A=w[Math.max(0,Number(d.occurrence??1)-1)];m=A?.row??-1,y=A?.column??-1,f=d.text.length}if(m<0||y<0||m>=t.height||y>=t.width)continue;let S=Math.max(0,Number(d.padding_cells??1)),b=Math.max(0,y-S)*i,_=Math.max(0,m-S)*l,p=Math.min(t.width-y+S,f+S*2)*i,c=Math.min(t.height-m+S,g+S*2)*l,h=s[u.tone??"danger"];if(o.strokeStyle=h,o.lineWidth=2,u.shape==="underline"?(o.beginPath(),o.moveTo(b,_+c-2),o.lineTo(b+p,_+c-2),o.stroke()):o.strokeRect(b+1,_+1,Math.max(1,p-2),Math.max(1,c-2)),u.label){o.font="700 10px system-ui, sans-serif";let w=o.measureText(u.label).width+8;o.fillStyle=h,o.fillRect(b+1,Math.max(0,_-15),w,14),o.fillStyle="#fff",o.fillText(u.label,b+5,Math.max(10,_-4))}}}function Wg(e){if(e.nativeEvent.isComposing||e.metaKey)return null;let n={Enter:"CR",Escape:"Esc",Backspace:"BS",Delete:"Del",Tab:"Tab",ArrowLeft:"Left",ArrowRight:"Right",ArrowUp:"Up",ArrowDown:"Down",Home:"Home",End:"End",PageUp:"PageUp",PageDown:"PageDown",Insert:"Insert"}[e.key]??(/^F\d{1,2}$/.test(e.key)?e.key:null),r=[e.ctrlKey?"C":"",e.altKey?"M":"",e.shiftKey&&n?"S":""].filter(Boolean).join("-");return n?`<${r?`${r}-`:""}${n}>`:e.key.length!==1?null:e.ctrlKey||e.altKey?`<${r}-${e.key==="<"?"lt":e.key}>`:e.key==="<"?"":e.key}function Gg({visible:e,syncMode:t,controlMode:n,expanded:r,fit:o,scale:i,onSummaryChange:l}){let s=(0,k.useRef)(null),u=(0,k.useRef)(null),d=(0,k.useRef)(null),m=(0,k.useRef)(0),y=(0,k.useRef)(0),g=(0,k.useRef)(0),f=(0,k.useRef)(gp()),S=(0,k.useRef)(null),b=(0,k.useRef)(null),_=(0,k.useRef)(null),p=(0,k.useRef)(0),c=(0,k.useRef)(null),h=(0,k.useRef)(null),[w,C]=(0,k.useState)(null),[A,P]=(0,k.useState)(!1),[z,U]=(0,k.useState)(0),[F,ae]=(0,k.useState)(!1),[me,q]=(0,k.useState)(()=>{let T=Number(localStorage.getItem("stem-card-nvim-height-vh"));return Number.isFinite(T)&&T>=25&&T<=90?T:75}),[V,Re]=(0,k.useState)({state:"hidden"}),[Se,J]=(0,k.useState)({state:"idle"}),at=Se.state==="replaying"||Se.state==="verifying",bt=t&&V.state==="connected"&&!at,Me=n&&F&&V.state==="connected"&&!at,ge=()=>{s.current&&Kg(s.current,f.current,_.current===b.current?.step.snapshot_ref?b.current?.step.view?.annotations??[]:[])},yr=()=>{m.current||(m.current=window.requestAnimationFrame(()=>{m.current=0,ge()}))};if((0,k.useEffect)(()=>{let T=D=>{let G=D.detail;b.current=G,C(G),_.current=null;let te=G.step.snapshot_ref;if(V.state!=="connected"&&te&&hp(G.step).then(Q=>{!Q||b.current?.step.snapshot_ref!==te||(f.current=Q,P(!0),_.current=te,J({state:"idle",snapshot_ref:te,message:"Pokazano ostatni zapisany, zweryfikowany ekran."}),ge())}),ge(),!G.activate)return;let st=G.step.mode??"RUN",xt=async()=>{if(!G.step.code_ref)return null;let Q=await fetch("/api/nvim/open",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({code_ref:G.step.code_ref,actor:document.documentElement.dataset.cardClientId??"browser"})}),Ae=await Q.json().catch(()=>({}));if(!Q.ok)throw new Error(Ae.message??Ae.error??`HTTP ${Q.status}`);return Ae};if(st==="CODE"||!te){xt().then(()=>{J({state:"ready",phase:"code",message:G.step.code_ref?`Otwarto ${G.step.code_ref}; program nie zosta\u0142 zrestartowany.`:"Krok CODE nie ma przypisanego code_ref."})}).catch(Q=>{J({state:"failed",phase:"code",message:Q instanceof Error?Q.message:String(Q)})});return}if(!e||V.state!=="connected"){J({state:"failed",snapshot_ref:te,message:"Cel jest offline; pozostaje zapisany wynik tego kroku."});return}let ze=xt(),oe=Math.max(p.current+1,Date.now());p.current=oe,c.current?.abort();let wt=new AbortController;c.current=wt,J({state:"replaying",phase:"dispatch",generation:oe,snapshot_ref:te,message:"Resetuj\u0119 cel i odtwarzam wykonanie do wybranego kroku."});let kn=0,Dt=async()=>{try{let Q=await fetch("/api/checkpoint/status",{cache:"no-store",signal:wt.signal});if(!Q.ok)return;let Ae=await Q.json(),ut=Ae.active?.generation===oe?Ae.active:null;if(!ut||p.current!==oe)return;J({state:ut.phase==="verify"?"verifying":"replaying",phase:ut.phase,generation:oe,snapshot_ref:te,message:ut.message})}catch{}};kn=window.setInterval(Dt,120),Dt(),ze.then(()=>fetch("/api/checkpoint/activate",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({snapshot_ref:te,generation:oe}),signal:wt.signal})).then(async Q=>{let Ae=await Q.json();if(p.current!==oe)return;if(!Q.ok||Ae.status!=="ready")throw new Error(Ae.message??`Replay zako\u0144czy\u0142 si\u0119 kodem HTTP ${Q.status}.`);let ut=d.current;if(ut?.readyState!==WebSocket.OPEN)throw new Error("Replay zako\u0144czony, ale most Neovima jest offline.");let Ne=y.current;ut.send(JSON.stringify({type:"refresh"}));let Ee=Date.now()+1800;for(;y.current<=Ne&&Date.now()window.setTimeout(kt,20));if(y.current<=Ne)throw new Error("Neovim nie potwierdzi\u0142 pe\u0142nego redraw po replay.");if(p.current!==oe)return;_.current=te,J({state:"ready",phase:Ae.phase,generation:oe,snapshot_ref:te,message:Ae.message??"Stan maszyny zosta\u0142 odtworzony i zweryfikowany."}),$g(te,f.current),ge(),await new Promise(kt=>window.requestAnimationFrame(()=>kt()));let vr=s.current;if(vr&&b.current?.step.snapshot_ref===te)try{fetch("/api/evidence",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({actor:document.documentElement.dataset.cardClientId??"browser",snapshot_ref:te,event_id:G.step.event_id??null,strategy_ref:G.step.strategy_ref??null,code_ref:G.step.code_ref??null,caption:`${String(G.step.number).padStart(2,"0")} ${G.step.label}`,data_url:vr.toDataURL("image/png")})}).catch(()=>{})}catch{}}).catch(Q=>{wt.signal.aborted||p.current!==oe||(hp(G.step).then(Ae=>{!Ae||b.current?.step.snapshot_ref!==te||(f.current=Ae,P(!0),_.current=te,ge())}),J({state:"failed",generation:oe,snapshot_ref:te,message:Q instanceof Error?Q.message:String(Q)}))}).finally(()=>{window.clearInterval(kn),c.current===wt&&(c.current=null)})};return window.addEventListener("stem:nvim-step",T),()=>{window.removeEventListener("stem:nvim-step",T),c.current?.abort()}},[V.state,e]),(0,k.useEffect)(()=>{localStorage.setItem("stem-card-nvim-height-vh",me.toFixed(2))},[me]),(0,k.useEffect)(()=>{let T=window.requestAnimationFrame(ge);return()=>window.cancelAnimationFrame(T)},[r,o,i]),(0,k.useEffect)(()=>{let T=()=>{let D=d.current;D?.readyState===WebSocket.OPEN&&D.send(JSON.stringify({type:"refresh"})),ge()};return window.addEventListener("stem:nvim-refresh",T),()=>window.removeEventListener("stem:nvim-refresh",T)},[]),(0,k.useEffect)(()=>{let T=V.state;V.state==="connected"&&(T=Se.state==="idle"?"connected":Se.state),l({state:T,message:Se.message??V.message,instance:V.target?.instance??V.target?.container_name,detail:[V.target?.profile??w?.step.view?.connection_ref,w?`${String(w.step.number).padStart(2,"0")} ${w.step.label}`:void 0,Se.phase].filter(Boolean).join(" \xB7 "),syncMode:t,syncReady:bt,controlMode:n,controlEnabled:Me,screenCursor:{...f.current.cursor},grid:{columns:f.current.width,rows:f.current.height}})},[w,Se,Me,n,z,l,V,t,bt]),(0,k.useEffect)(()=>()=>window.clearTimeout(g.current),[]),(0,k.useEffect)(()=>{if(!Me)return;let T=D=>{let G=d.current;if(G?.readyState!==WebSocket.OPEN)return;if(D.altKey&&!D.ctrlKey&&!D.metaKey&&D.key.toLowerCase()==="w"){D.preventDefault(),D.stopImmediatePropagation(),D.repeat||G.send(JSON.stringify({type:"input",keys:""}));return}let te={ArrowLeft:"h",ArrowDown:"j",ArrowUp:"k",ArrowRight:"l"};D.altKey&&!D.ctrlKey&&!D.metaKey&&te[D.key]&&(D.preventDefault(),D.stopImmediatePropagation(),D.repeat||G.send(JSON.stringify({type:"input",keys:`${te[D.key]}`})))};return window.addEventListener("keydown",T,!0),()=>window.removeEventListener("keydown",T,!0)},[Me]),(0,k.useEffect)(()=>{if(!e||!t){d.current?.close(),d.current=null,ae(!1),Re(e?{state:"offline",message:"SYNC OFF: pokazuj\u0119 zapisany ekran bez po\u0142\u0105czenia z kontenerem."}:{state:"hidden"});return}let T=!1,D=0,G=()=>{if(T)return;let te=window.location.protocol==="https:"?"wss:":"ws:",st=new WebSocket(`${te}//${window.location.host}/api/nvim-ui`);d.current=st,Re({state:"connecting"}),st.addEventListener("message",xt=>{try{let ze=JSON.parse(String(xt.data));if(ze.type==="status"){typeof ze.epoch=="string"&&S.current&&S.current!==ze.epoch&&(f.current=gp(),P(!1),_.current=null),typeof ze.epoch=="string"&&(S.current=ze.epoch),Re(ze);return}ze.type==="redraw"&&Array.isArray(ze.events)&&(Vg(f.current,ze.events),f.current.width>1&&f.current.height>1&&P(!0),ze.events.some(oe=>Array.isArray(oe)&&oe[0]==="flush")&&(y.current+=1,yr(),g.current||(g.current=window.setTimeout(()=>{g.current=0,U(oe=>oe+1)},120))))}catch{}}),st.addEventListener("close",()=>{d.current===st&&(d.current=null),T||(D=window.setTimeout(G,1200))}),st.addEventListener("error",()=>{Re(xt=>({...xt,state:"offline",message:"Most Neovima jest chwilowo niedost\u0119pny."}))})};return G(),()=>{T=!0,window.clearTimeout(D),window.cancelAnimationFrame(m.current),m.current=0,d.current?.close(),d.current=null}},[t,e]),!e)return null;let se=T=>{let D=d.current;D?.readyState===WebSocket.OPEN&&D.send(JSON.stringify(T))},Zt=T=>{let D=T.currentTarget.getBoundingClientRect();return{row:Math.max(0,Math.min(f.current.height-1,Math.floor((T.clientY-D.top)/D.height*f.current.height))),column:Math.max(0,Math.min(f.current.width-1,Math.floor((T.clientX-D.left)/D.width*f.current.width)))}},ue=T=>[T.shiftKey?"S":"",T.ctrlKey?"C":"",T.altKey?"A":""].filter(Boolean).join("-");return(0,a.jsxs)("section",{className:"nvim-panel","aria-label":"Neovim po\u0142\u0105czony z kontenerem",onWheel:T=>{T.stopPropagation();let D=T.target;(!(D instanceof Element)||!D.closest(".nvim-screen-scroll"))&&T.preventDefault()},children:[(0,a.jsxs)("div",{ref:u,className:"nvim-screen-scroll",style:o?void 0:{height:`${r?90:me}vh`},"data-sync":t?"on":"off","data-control":Me?"true":"false",tabIndex:0,onPointerEnter:T=>{ae(!0),T.currentTarget.focus({preventScroll:!0})},onPointerLeave:T=>{ae(!1),T.currentTarget.blur()},onMouseDown:T=>T.currentTarget.focus({preventScroll:!0}),onKeyDown:T=>{if(!Me)return;let D=Wg(T);D&&(T.preventDefault(),T.stopPropagation(),se({type:"input",keys:D}))},children:[(0,a.jsx)("div",{className:"nvim-screen-stage",style:o?void 0:{height:`${(r?90:me)/Math.max(i,.25)}vh`},children:(0,a.jsx)("div",{className:"nvim-screen-surface",children:(0,a.jsx)("div",{className:"nvim-screen-frame",children:(0,a.jsx)("div",{className:"nvim-screen-content",children:(0,a.jsx)("div",{className:"nvim-screen-grid",children:(0,a.jsx)("canvas",{ref:s,className:"nvim-screen",tabIndex:-1,"aria-label":Me?"Interaktywny ekran Neovima":"Ekran Neovima",onMouseDown:T=>{if(!Me||V.state!=="connected")return;T.preventDefault();let D=Zt(T),G=T.button===1?"middle":T.button===2?"right":"left";se({type:"mouse",button:G,action:"press",modifier:ue(T),...D})},onMouseUp:T=>{if(!Me||V.state!=="connected")return;T.preventDefault();let D=Zt(T),G=T.button===1?"middle":T.button===2?"right":"left";se({type:"mouse",button:G,action:"release",modifier:ue(T),...D})},onWheel:T=>{if(!Me||V.state!=="connected")return;T.preventDefault();let D=Zt(T);se({type:"mouse",button:"wheel",action:T.deltaY<0?"up":"down",modifier:ue(T),...D})},onContextMenu:T=>Me&&T.preventDefault()})})})})})}),!A&&(0,a.jsxs)("div",{className:"nvim-screen-empty",children:[(0,a.jsx)("strong",{children:_p[V.state]}),(0,a.jsx)("span",{children:V.message??"Oczekiwanie na ekran Neovima lub zapisany stan kroku."})]})]}),(0,a.jsx)("div",{className:"nvim-resize-handle",role:"separator","aria-label":"Zmie\u0144 wysoko\u015B\u0107 panelu Neovima","aria-orientation":"horizontal","aria-valuemin":25,"aria-valuemax":90,"aria-valuenow":Math.round(me),tabIndex:0,title:"Przeci\u0105gnij, aby zmieni\u0107 wysoko\u015B\u0107. Dwuklik: 75%.",onPointerDown:T=>{T.preventDefault(),h.current={y:T.clientY,heightVh:me},T.currentTarget.setPointerCapture(T.pointerId)},onPointerMove:T=>{let D=h.current;if(!D)return;let G=(T.clientY-D.y)/window.innerHeight*100;q(Math.max(25,Math.min(90,D.heightVh+G)))},onPointerUp:T=>{h.current=null,T.currentTarget.hasPointerCapture(T.pointerId)&&T.currentTarget.releasePointerCapture(T.pointerId)},onPointerCancel:()=>{h.current=null},onDoubleClick:()=>q(75),onKeyDown:T=>{if(T.key==="ArrowUp"||T.key==="ArrowDown"){T.preventDefault();let D=T.key==="ArrowUp"?-2:2;q(G=>Math.max(25,Math.min(90,G+D)))}else T.key==="Home"&&(T.preventDefault(),q(75))}})]})}function Yg({column:e,row:t}){return e==="chapter"||e==="task"||e==="version"?(0,a.jsx)("td",{children:(0,a.jsx)("code",{children:t[e]})}):e==="idea"?(0,a.jsx)("td",{dangerouslySetInnerHTML:{__html:t.idea_html}}):e==="status"?(0,a.jsx)("td",{children:(0,a.jsx)("span",{className:"scope-status","data-status":t.status,title:t.status,children:t.status_label})}):(0,a.jsx)("td",{children:t[e]})}function Qg({front:e}){let t=e.scope_columns??["chapter","task","idea","priority"];return(0,a.jsxs)(bs,{header:e.header_html,footer:e.footer_html,left:e.left_margin_html,right:e.right_margin_html,className:"front-sheet",children:[(0,a.jsxs)("section",{className:"front-scope front-goal",children:[(0,a.jsx)("h2",{children:e.goal_title}),(0,a.jsx)(Ft,{html:e.goal_html})]}),(0,a.jsxs)("section",{className:"front-scope front-card-scope",children:[(0,a.jsx)("h2",{children:e.scope_title}),e.scope_html&&(0,a.jsx)(Ft,{html:e.scope_html}),(0,a.jsx)("div",{className:"scope-table-wrap",children:(0,a.jsxs)("table",{className:"scope-table scope-table--task",children:[(0,a.jsx)("thead",{children:(0,a.jsx)("tr",{children:e.scope_headers.map(n=>(0,a.jsx)("th",{children:n},n))})}),(0,a.jsx)("tbody",{children:e.scope_rows.map(n=>(0,a.jsx)("tr",{className:n.key?"scope-row--key":"",children:t.map(r=>(0,a.jsx)(Yg,{column:r,row:n},r))},`${n.chapter}-${n.task}`))})]})})]})]})}function qg({taskRef:e,step:t}){return(0,a.jsxs)("li",{id:`${e}-${t.id}`,children:[(0,a.jsx)("strong",{children:t.title}),(0,a.jsx)(Ft,{html:t.content_html})]})}function Xg({taskRef:e,block:t}){let n=t.kind==="exercise"?"\u0106wiczenie":"Block";return(0,a.jsxs)("section",{className:`task-block task-block--${t.kind}`,id:`${e}-${t.id}`,"data-stem-nav":"block",children:[(0,a.jsxs)("header",{children:[(0,a.jsx)("span",{children:n}),(0,a.jsx)("h4",{children:t.title})]}),(0,a.jsx)(Ft,{html:t.content_html}),t.steps.length>0&&(0,a.jsx)("ol",{className:"block-steps",children:t.steps.map(r=>(0,a.jsx)(qg,{taskRef:e,step:r},r.id))})]})}function Zg({task:e}){return e.flow.length?(0,a.jsxs)("article",{className:"task-flow",id:e.ref,"data-stem-nav":"task",children:[(0,a.jsxs)("header",{className:"task-flow-header",children:[(0,a.jsx)("span",{children:e.ref.toUpperCase()}),(0,a.jsx)("h3",{children:e.title}),e.uuid&&(0,a.jsx)("code",{children:e.uuid})]}),e.flow.map(t=>(0,a.jsx)(Xg,{taskRef:e.ref,block:t},t.id)),(0,a.jsxs)("footer",{children:[(0,a.jsx)("strong",{children:"Task acceptance:"})," ",e.criterion]})]}):(0,a.jsxs)("article",{className:"task-legacy",id:e.ref,"data-stem-nav":"task",children:[(0,a.jsx)("strong",{children:e.ref.toUpperCase()}),(0,a.jsx)(Ft,{html:e.prompt_html??""}),(0,a.jsxs)("footer",{children:[(0,a.jsx)("strong",{children:"Task acceptance:"})," ",e.criterion]})]})}var yp={compile:"KOMPILACJA",bss:".BSS",stack:"STOS",registers:"REJESTRY",text:".TEXT"},vp=["#9bcfe2","#c4dfc0","#f4c889","#d2b6dc"];function Jg({config:e,title:t}){let n=e.arena_size??64,r=e.allocations??[5,7],o=e.symbols??[],i=`${e.storage_key??"allocator-memory-flow"}-stage`,[l,s]=(0,k.useState)(()=>Math.min(r.length,Number(localStorage.getItem(i)??0))),[u,d]=(0,k.useState)(o[0]?.id??"");(0,k.useEffect)(()=>localStorage.setItem(i,String(l)),[l,i]);let m=r.slice(0,l).reduce((_,p)=>_+p,0),y=o.find(_=>_.id===u)??o[0],g=Array.from({length:n},(_,p)=>p),f=_=>{let p=0;for(let c=0;co.some(p=>p.area===_));return(0,a.jsxs)("div",{className:"memory-react","aria-label":`Interaktywny diagram: ${t}`,children:[(0,a.jsxs)("header",{children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("small",{children:"INTERAKTYWNY MODEL PAMI\u0118CI"}),(0,a.jsx)("h3",{children:t})]}),(0,a.jsxs)("div",{className:"memory-actions",children:[(0,a.jsx)("button",{"aria-pressed":l===0,onClick:()=>s(0),children:"definicje / reset"}),r.map((_,p)=>(0,a.jsxs)("button",{"aria-pressed":l===p+1,onClick:()=>s(p+1),children:["alloc(",_,")"]},`${_}-${p}`))]})]}),(0,a.jsx)("div",{className:"memory-lanes",children:b.map(_=>(0,a.jsxs)("section",{className:`memory-lane ${_}`,children:[(0,a.jsx)("b",{children:yp[_]}),o.filter(p=>p.area===_).map(p=>(0,a.jsx)("button",{className:u===p.id?"active":"",onClick:()=>d(p.id),children:(0,a.jsx)("code",{children:p.declaration})},p.id))]},_))}),(0,a.jsxs)("div",{className:"arena-wrap",children:[(0,a.jsxs)("div",{className:"arena-label",children:[(0,a.jsxs)("code",{children:["allocbuf[",n,"]"]}),(0,a.jsxs)("span",{children:["u\u017Cyte ",m," B / wolne ",n-m," B"]})]}),(0,a.jsxs)("div",{className:"arena",style:{"--allocp":m,"--arena-size":n},children:[g.map(_=>{let p=f(_);return(0,a.jsx)("i",{style:p>=0?{background:vp[p%vp.length]}:void 0},_)}),(0,a.jsxs)("span",{className:"allocp",children:["allocp \u2191 ",m]})]})]}),(0,a.jsxs)("p",{className:"memory-stage",children:[(0,a.jsxs)("strong",{children:[l+1,"/",r.length+1]})," ",(0,a.jsx)("code",{children:S})]}),y&&(0,a.jsxs)("aside",{className:"memory-inspector",children:[(0,a.jsx)("strong",{children:y.id}),(0,a.jsx)("code",{children:y.declaration}),(0,a.jsx)("span",{children:y.description})]})]})}function eh({config:e,asset:t,progress:n,onSetStatus:r}){let o=e.stages??[],i=`${e.storage_key??t.label}-uml-stage`,l=(0,k.useRef)(null),[s,u]=(0,k.useState)(""),[d,m]=(0,k.useState)(!1),[y,g]=(0,k.useState)(!1),[f,S]=(0,k.useState)(()=>Math.max(0,Math.min(o.length-1,Number(localStorage.getItem(i)??0))));(0,k.useEffect)(()=>localStorage.setItem(i,String(f)),[f,i]),(0,k.useEffect)(()=>{let c=!0;return m(!1),fetch(t.href,{cache:"no-store"}).then(h=>{if(!h.ok)throw new Error(`HTTP ${h.status}`);return h.text()}).then(h=>{let w=new DOMParser().parseFromString(h,"image/svg+xml"),C=w.documentElement;if(C.nodeName.toLowerCase()!=="svg"||w.querySelector("parsererror"))throw new Error("Niepoprawny SVG");w.querySelectorAll("script, foreignObject").forEach(A=>A.remove()),zp(C,t.tile),c&&u(C.outerHTML)}).catch(()=>{c&&m(!0)}),()=>{c=!1}},[t.href,t.tile]),(0,k.useEffect)(()=>{let c=l.current?.querySelector("svg");if(!c)return;c.setAttribute("role","img"),c.setAttribute("aria-label",t.alt),c.querySelectorAll("[data-uml-stage]").forEach(C=>{C.classList.remove("uml-stage-region","uml-stage-region-bg","uml-stage-region-outline","uml-stage-region-active","uml-stage-region-completed"),C.removeAttribute("data-uml-stage"),C.removeAttribute("data-uml-stage-label"),C.removeAttribute("role"),C.removeAttribute("tabindex"),C.removeAttribute("aria-label")});let h=Array.from(c.querySelectorAll("text")),w=Array.from(c.querySelectorAll("rect"));o.forEach((C,A)=>{if(!C.svg_label)return;let P=h.find(q=>(q.textContent??"").trim().startsWith(C.svg_label)),z=Number(P?.getAttribute("y"));if(!P||!Number.isFinite(z))return;let U=w.map(q=>{let V=Number(q.getAttribute("y")),Re=Number(q.getAttribute("height")),Se=Number(q.getAttribute("width")),J=q.getAttribute("style")??"",at=Number(J.match(/stroke-width:\s*([\d.]+)/)?.[1]??q.getAttribute("stroke-width")??0);return{rect:q,y:V,height:Re,width:Se,strokeWidth:at,area:Se*Re}}).filter(({y:q,height:V,width:Re,strokeWidth:Se})=>Number.isFinite(q)&&Number.isFinite(V)&&Number.isFinite(Re)&&Se>=2&&q<=z&&q+V>=z).sort((q,V)=>q.area-V.area),F=U[0];if(!F)return;let ae=U.filter(({y:q,height:V,width:Re})=>Math.abs(q-F.y)<.1&&Math.abs(V-F.height)<.1&&Math.abs(Re-F.width)<.1),me=!!(C.progress_id&&n?.items?.[C.progress_id]?.status==="approved");ae.forEach(({rect:q},V)=>{let Re=q.getAttribute("fill")!=="none";q.dataset.umlStage=String(A),q.dataset.umlStageLabel=C.label,q.classList.add("uml-stage-region",Re?"uml-stage-region-bg":"uml-stage-region-outline"),q.classList.toggle("uml-stage-region-active",A===f),q.classList.toggle("uml-stage-region-completed",me),V===0&&(q.setAttribute("role","button"),q.setAttribute("tabindex","0"),q.setAttribute("aria-label",`Poka\u017C etap ${C.label}`))})})},[t.alt,n,f,o,s]);let b=(c,h)=>{let w=l.current?.querySelector("svg");if(!w)return;let C=Array.from(w.querySelectorAll(".uml-stage-region-bg[data-uml-stage]")).filter(P=>{let z=P.getBoundingClientRect();return c>=z.left&&c<=z.right&&h>=z.top&&h<=z.bottom}).sort((P,z)=>{let U=P.getBoundingClientRect(),F=z.getBoundingClientRect();return U.width*U.height-F.width*F.height})[0],A=Number(C?.dataset.umlStage);Number.isInteger(A)&&S(A)},_=c=>{if(c.key!=="Enter"&&c.key!==" ")return;let h=c.target,w=Number(h.dataset?.umlStage);Number.isInteger(w)&&(c.preventDefault(),S(w))},p=o[f];return(0,a.jsxs)("div",{className:"uml-react",children:[(0,a.jsxs)("header",{children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("small",{children:"KANONICZNY DIAGRAM UML \xB7 PLANTUML"}),(0,a.jsx)("h3",{children:"Reset i dwa przydzia\u0142y liniowe"})]}),(0,a.jsx)("div",{className:"uml-actions",children:o.map((c,h)=>(0,a.jsxs)("button",{"aria-pressed":f===h,"data-completed":c.progress_id&&n?.items?.[c.progress_id]?.status==="approved"?"true":"false",onClick:()=>S(h),children:[c.progress_id&&n?.items?.[c.progress_id]?.status==="approved"&&(0,a.jsx)("span",{"aria-hidden":"true",children:"\u2713 "}),c.label]},c.id))})]}),(0,a.jsxs)("div",{ref:l,className:"uml-canvas",onClick:c=>b(c.clientX,c.clientY),onKeyDown:_,children:[s?(0,a.jsx)("div",{className:`uml-svg-host${t.tile?" is-tiled":""}`,style:vs(t.tile),dangerouslySetInnerHTML:{__html:s}}):(0,a.jsx)("img",{src:t.href,alt:t.alt}),d&&(0,a.jsx)("span",{className:"uml-inline-warning",children:"Widok statyczny \u2014 nie uda\u0142o si\u0119 wczyta\u0107 interaktywnego SVG."})]}),p&&(0,a.jsxs)("aside",{className:"uml-stage",children:[(0,a.jsx)("strong",{children:p.label}),(0,a.jsx)("span",{children:p.description}),p.code_ref&&(0,a.jsx)("code",{children:p.code_ref})]}),p?.progress_id&&(0,a.jsxs)("footer",{className:"uml-approval",children:[(0,a.jsx)("span",{children:n?.items?.[p.progress_id]?.status==="approved"?"Ten stan jest zatwierdzony i zapisany w JSON-ie zaj\u0119\u0107.":"Kursor wybiera stan; zatwierdzenie zapisuje go w JSON-ie zaj\u0119\u0107."}),(0,a.jsx)("button",{disabled:!n||y,onClick:async()=>{if(!p.progress_id||!n)return;let c=n.items?.[p.progress_id]?.status==="approved";g(!0);try{await r(p.progress_id,c?"pending":"approved")}finally{g(!1)}},children:y?"Zapisywanie\u2026":n?.items?.[p.progress_id]?.status==="approved"?"Cofnij zatwierdzenie":"Zatwierd\u017A stan"})]})]})}function th({config:e,asset:t,progress:n,onSetStatus:r,continuation:o}){let i=e.phases??[],l=i.flatMap(v=>v.steps.map(x=>({phase:v,step:x}))),s=`${e.storage_key??t.label}-uml-step`,u=`${e.storage_key??t.label}-navigation-level`,d=(0,k.useRef)(null),m=(0,k.useRef)(null),y=(0,k.useRef)(new Map),g=(0,k.useRef)(null),f=(0,k.useRef)(0),S=(0,k.useRef)(!1),b=(0,k.useRef)(null),_=(0,k.useRef)(!0),[p,c]=(0,k.useState)(""),[h,w]=(0,k.useState)(!1),[C,A]=(0,k.useState)(!1),[P,z]=(0,k.useState)(null),[U,F]=(0,k.useState)(null),[ae,me]=(0,k.useState)(!1),[q,V]=(0,k.useState)(""),[Re,Se]=(0,k.useState)(!1),[J,at]=(0,k.useState)(null),[bt,Me]=(0,k.useState)([]),[ge,yr]=(0,k.useState)(0),[se,Zt]=(0,k.useState)(()=>Math.max(0,Math.min(l.length-1,Number(localStorage.getItem(s)??0)))),[ue,T]=(0,k.useState)(()=>{let v=localStorage.getItem(u);return v==="card"||v==="task"||v==="block"||v==="phase"||v==="step"||v==="snapshot"?v:"step"});(0,k.useEffect)(()=>localStorage.setItem(s,String(se)),[se,s]),(0,k.useEffect)(()=>localStorage.setItem(u,ue),[ue,u]),(0,k.useEffect)(()=>{let v=!0;return w(!1),fetch(t.href,{cache:"no-store"}).then(x=>{if(!x.ok)throw new Error(`HTTP ${x.status}`);return x.text()}).then(x=>{let R=new DOMParser().parseFromString(x,"image/svg+xml"),M=R.documentElement;if(M.nodeName.toLowerCase()!=="svg"||R.querySelector("parsererror"))throw new Error("Niepoprawny SVG");R.querySelectorAll("script, foreignObject").forEach($=>$.remove()),zp(M,t.tile),v&&c(M.outerHTML)}).catch(()=>{v&&w(!0)}),()=>{v=!1}},[t.href,t.tile]),(0,k.useEffect)(()=>{let v=!0,x=hs(t,e);if(V("Wczytywanie uk\u0142adu\u2026"),Se(!1),!t.source_href)return z(x),F(x),V("Brak \u017Ar\xF3d\u0142a PlantUML \u2014 zapis wy\u0142\u0105czony."),()=>{v=!1};let R=t.source_href.replace(/\.puml(?:\?.*)?$/,".layout.json");return fetch(`/api/uml-layout?source=${encodeURIComponent(t.source_href)}`,{cache:"no-store"}).then(async M=>{if(!M.ok)throw new Error(`HTTP ${M.status}`);return M.json()}).catch(async()=>{let M=await fetch(R,{cache:"no-store"});return M.ok?{layout:await M.json(),source_digest:"",stale:!1}:{layout:null,source_digest:"",stale:!1}}).then(M=>{if(!v)return;let $=M.layout??hs(t,e,M.source_digest??"");z($),F($),me(!1),Se(!!M.stale),V(M.layout?"Uk\u0142ad wczytany z JSON.":"Uk\u0142ad PlantUML \u2014 brak r\u0119cznych zmian.")}).catch(M=>{v&&(z(x),F(x),V(`Nie wczytano uk\u0142adu: ${M instanceof Error?M.message:String(M)}`))}),()=>{v=!1}},[t.source_href,t.label,e.block?.id,e.task?.id]),(0,k.useEffect)(()=>{let v=m.current?.querySelector("svg");if(!v||!U)return;_g(v);let x=new Map(l.map(({step:j})=>[String(j.number).padStart(2,"0"),j.id])),R=Cg(v,x),M=new Map(R.map(j=>[j.id,j]));y.current=M;let $=R.map(j=>j.id);Me(j=>j.join("\0")===$.join("\0")?j:$),R.forEach(j=>{let E=U.elements[j.id];E&&Lg(j,E)});let H=v.viewBox.baseVal;(U.canvas.width<=0||U.canvas.height<=0)&&F(j=>j&&{...j,canvas:{...j.canvas,width:H?.width||Number(v.getAttribute("width")?.replace("px",""))||0,height:H?.height||Number(v.getAttribute("height")?.replace("px",""))||0}})},[l,p,U]),(0,k.useEffect)(()=>{let v=m.current?.querySelector("svg");if(!v)return;let x=e.kind==="uml-class"&&v.contains(document.activeElement);v.setAttribute("role","img"),v.setAttribute("aria-label",t.alt),v.classList.toggle("uml-class-svg",e.kind==="uml-class"),v.classList.toggle("uml-class-svg-active",e.kind==="uml-class"&&ue!=="card"),v.querySelectorAll(".uml-step-hit, .uml-class-focus-region").forEach(N=>N.remove()),v.querySelectorAll("[data-uml-step-element]").forEach(N=>{N.classList.remove("uml-step-element","uml-step-element-active","uml-step-element-completed","uml-step-element-tone-a","uml-step-element-tone-b"),N.removeAttribute("data-uml-step-element"),N.removeAttribute("data-uml-step-claim-area")});let R=Array.from(v.querySelectorAll("text")),M=Array.from(v.querySelectorAll("line")),$=Array.from(v.querySelectorAll("polygon"));if(e.kind==="uml-class"){let N=W=>W?Eg(W):null,I=W=>{if(!W.length)return null;let B=Math.min(...W.map(ee=>ee.x)),he=Math.min(...W.map(ee=>ee.y)),ce=Math.max(...W.map(ee=>ee.x+ee.width)),Z=Math.max(...W.map(ee=>ee.y+ee.height));return{x:B,y:he,width:ce-B,height:Z-he}},K=Array.from(v.querySelectorAll("g > *")),re=Array.from(v.querySelectorAll("[id]"));l.forEach(({step:W},B)=>{let he=W.svg_label??String(W.number).padStart(2,"0"),ce=R.find(ye=>(ye.textContent??"").trim().startsWith(he));if(!ce)return;let Z=N(ce);if(!Z)return;let ee=Number(ce.getAttribute("y")),He=R.filter(ye=>Number.isFinite(ee)&&Math.abs(Number(ye.getAttribute("y"))-ee)<.8),Ie=W.svg_target?re.find(ye=>ye.id===W.svg_target||ye.id.startsWith(`${W.svg_target}-`))??null:null,Ke=N(Ie)??I(He.map(ye=>N(ye)).filter(ye=>ye!==null))??Z,St=Ie?.tagName.toLowerCase()==="rect",br=Ie?[Ie,ce]:He;St&&K.forEach(ye=>{let Jt=N(ye);if(!Jt||ye===Ie)return;let ws=Jt.x+Jt.width/2,ks=Jt.y+Jt.height/2;ws>=Ke.x&&ws<=Ke.x+Ke.width&&ks>=Ke.y&&ks<=Ke.y+Ke.height&&br.push(ye)});let Ip=!!(W.progress_id&&n?.items?.[W.progress_id]?.status==="approved"),xs=Math.max(1,Ke.width*Ke.height);br.forEach(ye=>{let Jt=Number(ye.dataset.umlStepClaimArea);Number.isFinite(Jt)&&Jt{let K=N.svg_label??String(N.number).padStart(2,"0"),re=R.find(ee=>(ee.textContent??"").trim()===K),W=Number(re?.getAttribute("y"));if(!re||!Number.isFinite(W))return;let B=W+5,he=!!(N.progress_id&&n?.items?.[N.progress_id]?.status==="approved"),ce=R.filter(ee=>Math.abs(Number(ee.getAttribute("y"))-W)<.8);ce.push(...M.filter(ee=>{let He=Number(ee.getAttribute("y1")),Ie=Number(ee.getAttribute("y2"));return Number.isFinite(He)&&Number.isFinite(Ie)&&Math.abs(He-Ie)<.8&&Math.abs((He+Ie)/2-B)<=14})),ce.push(...$.filter(ee=>{let He=(ee.getAttribute("points")??"").trim().split(/\s+/).map(St=>Number(St.split(",")[1])).filter(Number.isFinite);if(!He.length)return!1;let Ie=Math.min(...He),Ke=Math.max(...He);return Ke-Ie<=16&&Math.abs((Ie+Ke)/2-B)<=14})),ce.forEach(ee=>{ee.dataset.umlStepElement=String(I),ee.classList.add("uml-step-element"),ee.classList.toggle("uml-step-element-active",I===se),ee.classList.toggle("uml-step-element-completed",he)});let Z=document.createElementNS("http://www.w3.org/2000/svg","rect");Z.setAttribute("x",String(j)),Z.setAttribute("y",String(W-12)),Z.setAttribute("width",String(E)),Z.setAttribute("height","31"),Z.setAttribute("fill","transparent"),Z.setAttribute("pointer-events","all"),Z.setAttribute("role","button"),Z.setAttribute("tabindex","0"),Z.setAttribute("aria-label",`Poka\u017C krok ${String(N.number).padStart(2,"0")}: ${N.label}`),Z.dataset.umlStep=String(I),Z.classList.add("uml-step-hit"),Z.classList.toggle("uml-step-hit-active",ue!=="card"&&I===se),Z.classList.toggle("uml-step-hit-completed",he),v.appendChild(Z)})},[t.alt,e.kind,l,ue,n,se,p,U]),(0,k.useEffect)(()=>{let v=m.current?.querySelector("svg");if(v&&(v.querySelectorAll(".uml-layout-hit,.uml-layout-resize").forEach(x=>x.remove()),v.classList.toggle("uml-layout-editing",C),!(!C||!U)))for(let x of y.current.values()){let R=U.elements[x.id]??mo(x),M=document.createElementNS("http://www.w3.org/2000/svg","rect");M.setAttribute("x",String(R.x)),M.setAttribute("y",String(R.y)),M.setAttribute("width",String(Math.max(12,R.width))),M.setAttribute("height",String(Math.max(12,R.height))),M.setAttribute("tabindex","0"),M.setAttribute("role","button"),M.setAttribute("aria-label",`Edytuj element UML ${x.id}`),M.dataset.umlLayoutId=x.id,M.dataset.umlLayoutAction="move",M.classList.add("uml-layout-hit"),x.id===J&&M.classList.add("is-selected");let $=document.createElementNS("http://www.w3.org/2000/svg","title");if($.textContent=`${x.id} \xB7 przeci\u0105gnij, aby zmieni\u0107 po\u0142o\u017Cenie`,M.appendChild($),v.appendChild(M),x.id===J){let H=Math.max(8,Math.min(14,Math.min(R.width,R.height)*.1)),j=document.createElementNS("http://www.w3.org/2000/svg","rect");j.setAttribute("x",String(R.x+R.width-H/2)),j.setAttribute("y",String(R.y+R.height-H/2)),j.setAttribute("width",String(H)),j.setAttribute("height",String(H)),j.dataset.umlLayoutId=x.id,j.dataset.umlLayoutAction="resize",j.classList.add("uml-layout-resize"),v.appendChild(j)}}},[p,C,U,J,bt]),(0,k.useEffect)(()=>{C&&(!J||!y.current.has(J))&&at(bt[0]??null)},[C,bt,J]),(0,k.useEffect)(()=>{if(ge===0||f.current===ge)return;let v=!1,x=0,R=$=>{if(v)return;let H=m.current?.querySelector(`.uml-step-hit[data-uml-step="${se}"]`);if(!H)return;if(document.querySelector(".viewer-chrome")?.classList.contains("is-nvim-fit")){f.current=ge;return}let E=Lp(H);if(!E){f.current=ge;return}let N=xg(H);if(!N){let re=Sg(H,E),W=re===0?pp(H,E):0,B=re===0?dp(H,W):re;if($Dn){x=window.requestAnimationFrame(()=>R($+1));return}f.current=ge;return}let I=pp(H,N);if(Math.abs(I)<=Dn){f.current=ge;return}let K=dp(H,I);if($Dn){x=window.requestAnimationFrame(()=>R($+1));return}f.current=ge},M=window.requestAnimationFrame(()=>R(0));return()=>{v=!0,window.cancelAnimationFrame(M),window.cancelAnimationFrame(x)}},[ge,se,p]);let D=(v,x,R=ue,M=!0)=>{let $={task:e.task??null,block:e.block??null,phase:{id:v.phase.id,label:v.phase.label},step:v.step,focusLevel:R,activate:x&&document.documentElement.dataset.nvimSyncMode==="on"},H=()=>{window.dispatchEvent(new CustomEvent("stem:nvim-step",{detail:$}))};if(!M){H();return}let j=fetch("/api/navigation/current",{method:"PUT",headers:{"content-type":"application/json"},body:JSON.stringify({task:$.task,block:$.block,phase:$.phase,step:{id:$.step.id,number:$.step.number,label:$.step.label,mode:$.step.mode??($.step.snapshot_ref?"RUN":"CODE"),code_ref:$.step.code_ref??null,event_id:$.step.event_id??null,strategy_ref:$.step.strategy_ref??null},snapshot_ref:$.step.snapshot_ref??null,focus_level:$.focusLevel,sync_requested:!!$.activate,actor:document.documentElement.dataset.cardClientId??"browser"})});$.activate?j.then(H,H):(H(),j.catch(()=>{}))},G=(v,x=!1,R=ue,M=!0)=>{let $=Math.max(0,Math.min(l.length-1,v)),H=l[$];if(H){if(T(R),$===se){D(H,x,R,M);return}yr(j=>j+1),S.current=x,b.current=R,_.current=M,Zt($)}},te=(v,x)=>{let R=m.current?.querySelector("svg"),M=R?.getScreenCTM();if(!R||!M)return null;let $=R.createSVGPoint();return $.x=v,$.y=x,$.matrixTransform(M.inverse())},st=v=>{if(!C||!U)return;let x=v.target?.closest(".uml-layout-hit[data-uml-layout-id],.uml-layout-resize[data-uml-layout-id]"),R=x?.dataset.umlLayoutId,M=R?y.current.get(R):null,$=te(v.clientX,v.clientY);if(!R||!M||!$)return;v.preventDefault(),v.stopPropagation();let H=U.elements[R]??mo(M);g.current={id:R,action:x?.dataset.umlLayoutAction==="resize"?"resize":"move",pointerId:v.pointerId,startX:$.x,startY:$.y,initial:H},at(R),F(j=>j&&{...j,elements:{...j.elements,[R]:H}}),v.currentTarget.setPointerCapture(v.pointerId)},xt=v=>{let x=g.current;if(!x||x.pointerId!==v.pointerId)return;let R=te(v.clientX,v.clientY);if(!R)return;v.preventDefault();let M=R.x-x.startX,$=R.y-x.startY,H=x.action==="resize"?{...x.initial,width:Math.max(24,x.initial.width+M),height:Math.max(18,x.initial.height+$)}:{...x.initial,x:x.initial.x+M,y:x.initial.y+$};F(j=>j&&{...j,elements:{...j.elements,[x.id]:H}}),me(!0)},ze=v=>{let x=g.current;!x||x.pointerId!==v.pointerId||(g.current=null,v.currentTarget.hasPointerCapture(v.pointerId)&&v.currentTarget.releasePointerCapture(v.pointerId))},oe=(v,x)=>{let R=y.current.get(v);R&&(F(M=>{if(!M)return M;let $=M.elements[v]??mo(R);return{...M,elements:{...M.elements,[v]:x($)}}}),me(!0))},wt=async()=>{if(!U||!t.source_href)return;let v=Object.fromEntries(Array.from(y.current.values()).map(R=>[R.id,U.elements[R.id]??mo(R)])),x={...U,elements:v};V("Zapisywanie uk\u0142adu\u2026");try{let R=await fetch("/api/uml-layout",{method:"PUT",headers:{"content-type":"application/json"},body:JSON.stringify({source:t.source_href,layout:x,actor:document.documentElement.dataset.cardClientId??"browser"})});if(!R.ok)throw new Error((await R.json().catch(()=>null))?.message??`HTTP ${R.status}`);let M=await R.json();z(M.layout),F(M.layout),me(!1),Se(!1),V(`Zapisano ${Object.keys(M.layout.elements).length} element\xF3w w JSON.`)}catch(R){V(`B\u0142\u0105d zapisu: ${R instanceof Error?R.message:String(R)}`)}},kn=()=>{P&&(F(P),me(!1),V("Przywr\xF3cono ostatnio zapisany uk\u0142ad."))},Dt=async()=>{if(!(!t.source_href||!window.confirm("Usun\u0105\u0107 r\u0119czny uk\u0142ad i wr\xF3ci\u0107 do geometrii PlantUML?"))){V("Resetowanie uk\u0142adu\u2026");try{let v=await fetch(`/api/uml-layout?source=${encodeURIComponent(t.source_href)}&actor=${encodeURIComponent(document.documentElement.dataset.cardClientId??"browser")}`,{method:"DELETE"});if(!v.ok)throw new Error(`HTTP ${v.status}`);let x=hs(t,e,P?.source_digest??"");z(x),F(x),me(!1),at(null),V("Przywr\xF3cono automatyczny uk\u0142ad PlantUML.")}catch(v){V(`B\u0142\u0105d resetu: ${v instanceof Error?v.message:String(v)}`)}}},Q=(v,x)=>{if(C)return;let R=Array.from(m.current?.querySelectorAll(".uml-step-hit[data-uml-step]")??[]).filter($=>{let H=$.getBoundingClientRect();return v>=H.left&&v<=H.right&&x>=H.top&&x<=H.bottom}).sort(($,H)=>{let j=$.getBoundingClientRect(),E=H.getBoundingClientRect();return Math.abs(x-(j.top+j.bottom)/2)-Math.abs(x-(E.top+E.bottom)/2)}),M=Number(R[0]?.dataset.umlStep);Number.isInteger(M)&&G(M,!1,"step")},Ae=v=>{if(C||v.key!=="Enter"&&v.key!==" ")return;let x=v.target,R=Number(x.dataset?.umlStep);Number.isInteger(R)&&(v.preventDefault(),G(R,v.key==="Enter","step"))},ut=v=>{let x=l.findIndex(R=>R.phase.id===v.id);x>=0&&G(x,!1,"phase")},Ne=l[se];if((0,k.useEffect)(()=>{Ne&&(D(Ne,S.current,b.current??ue,_.current),S.current=!1,b.current=null,_.current=!0)},[Ne?.phase.id,Ne?.step.id,e.block]),(0,k.useEffect)(()=>{let v=x=>{let R=x.detail;if(!R||e.task?.id&&R.task.id!==e.task.id||e.block?.id&&R.block.id!==e.block.id)return;let M=l.findIndex($=>$.phase.id===R.phase.id&&$.step.id===R.step.id);if(M<0){T("card");return}G(M,!1,R.focus_level,!1)};return window.addEventListener("stem:navigation-command",v),()=>window.removeEventListener("stem:navigation-command",v)},[e.block?.id,e.task?.id,l,ue,se]),(0,k.useEffect)(()=>{let v=x=>{if(x.metaKey||C||x.target?.closest("input, textarea, select, [contenteditable='true']"))return;let M=d.current;if(!M)return;let $=Array.from(document.querySelectorAll("[data-stem-nav='uml-block']")),H=window.innerHeight/2;if($.sort((K,re)=>{let W=K.getBoundingClientRect(),B=re.getBoundingClientRect();return Math.abs((W.top+W.bottom)/2-H)-Math.abs((B.top+B.bottom)/2-H)})[0]!==M)return;if(x.key==="Enter"&&x.ctrlKey&&!x.altKey&&!x.shiftKey){if(!Ne?.step.progress_id||!n)return;x.preventDefault(),x.stopPropagation();let K=n.items?.[Ne.step.progress_id]?.status==="approved";r(Ne.step.progress_id,K?"pending":"approved");return}if(x.key==="Enter"&&!x.altKey&&!x.shiftKey){if(!Ne)return;x.preventDefault(),x.stopPropagation(),D(Ne,!0,ue);return}let E=["card","task","block","phase","step","snapshot"];if((x.key==="ArrowLeft"||x.key==="ArrowRight"||x.key==="Escape")&&!x.altKey&&!x.ctrlKey&&!x.shiftKey){let K=x.key==="ArrowRight"?1:-1,re=E[Math.max(0,Math.min(E.length-1,E.indexOf(ue)+K))];x.preventDefault(),x.stopPropagation(),T(re),Ne&&D(Ne,!1,re);return}if(x.key!=="ArrowUp"&&x.key!=="ArrowDown"||ue==="card"&&!x.altKey&&!x.ctrlKey&&!x.shiftKey)return;let N=x.key==="ArrowUp"?-1:1,I=ue;if(x.altKey&&x.shiftKey&&!x.ctrlKey)I="snapshot";else if(x.ctrlKey&&x.shiftKey&&!x.altKey)I="step";else if(x.altKey&&!x.ctrlKey&&!x.shiftKey)I="task";else if(x.ctrlKey&&!x.altKey&&!x.shiftKey)I="block";else if(x.shiftKey&&!x.altKey&&!x.ctrlKey)I="phase";else if(x.altKey||x.ctrlKey||x.shiftKey)return;x.preventDefault(),x.stopPropagation(),fetch("/api/navigation/move",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({level:I,delta:N,sync_requested:!1,actor:document.documentElement.dataset.cardClientId??"browser"})}).then(K=>K.ok?K.json():Promise.reject(new Error(`HTTP ${K.status}`))).then(K=>{K.current&&window.dispatchEvent(new CustomEvent("stem:navigation-command",{detail:K.current}))}).catch(()=>{})};return window.addEventListener("keydown",v),()=>window.removeEventListener("keydown",v)},[Ne,l,ue,r,n,C]),!Ne)return null;let{phase:Ee,step:vr}=Ne,kt=l.filter(v=>v.phase.id===Ee.id),yo=e.block?.label.match(/^A[1-8]\b/)?.[0]??"UML",vo=(e.title??e.block?.label??t.caption).replace(/^A[1-8]\s*[·—:-]\s*/i,""),jn=J?y.current.get(J)??null:null,ct=jn?U?.elements[jn.id]??mo(jn):null;return(0,a.jsxs)("div",{ref:d,className:"uml-react uml-snapshot-react","data-stem-nav":"uml-block","data-task-id":e.task?.id,"data-block-id":e.block?.id,"data-navigation-level":ue,children:[(0,a.jsx)("header",{className:"uml-snapshot-header",children:(0,a.jsxs)("div",{className:"uml-header-first-line",children:[(0,a.jsxs)("h3",{children:[(0,a.jsx)("span",{children:yo})," \u2014 ",vo,t.tile&&(0,a.jsxs)("small",{className:"uml-tile-part",children:["cz\u0119\u015B\u0107 ",t.tile.index,"/",t.tile.count," \xB7 r",t.tile.row," c",t.tile.column]})]}),(0,a.jsxs)("div",{className:"uml-header-navigation",children:[(0,a.jsx)(Pp,{continuation:o}),t.source_href&&(0,a.jsx)("button",{className:`uml-edit-toggle${C?" is-active":""}`,"aria-pressed":C,title:"R\u0119czna edycja po\u0142o\u017Cenia i parametr\xF3w blok\xF3w UML",onClick:()=>A(v=>!v),children:C?"ZAMKNIJ EDIT":"EDIT"}),(0,a.jsx)("div",{className:"uml-phase-actions","aria-label":"Fazy diagramu",children:i.map(v=>(0,a.jsx)("button",{"aria-pressed":Ee.id===v.id,onClick:()=>ut(v),children:v.label},v.id))}),(0,a.jsx)("div",{className:"uml-step-actions","aria-label":`Checkpointy fazy ${Ee.label}`,children:kt.map(v=>{let x=l.indexOf(v),R=!!(v.step.progress_id&&n?.items?.[v.step.progress_id]?.status==="approved");return(0,a.jsx)("button",{"aria-label":`Krok ${v.step.number}: ${v.step.label}`,"aria-pressed":ue!=="card"&&x===se,"data-completed":R?"true":"false","data-mode":v.step.mode??"RUN",title:`${v.step.mode??"RUN"} \xB7 ${v.step.label}`,onClick:()=>G(x),children:String(v.step.number).padStart(2,"0")},v.step.id)})})]})]})}),(0,a.jsxs)("div",{className:"uml-snapshot-layout",children:[(0,a.jsxs)("div",{ref:m,className:`uml-canvas uml-step-canvas${C?" is-layout-editing":""}`,onClick:v=>Q(v.clientX,v.clientY),onKeyDown:Ae,onPointerDown:st,onPointerMove:xt,onPointerUp:ze,onPointerCancel:ze,children:[p?(0,a.jsx)("div",{className:`uml-svg-host${t.tile?" is-tiled":""}`,style:vs(t.tile),dangerouslySetInnerHTML:{__html:p}}):(0,a.jsx)("img",{src:t.href,alt:t.alt}),h&&(0,a.jsx)("span",{className:"uml-inline-warning",children:"Widok statyczny \u2014 wybierz krok przyciskiem nad diagramem."})]}),C&&U&&(0,a.jsxs)("aside",{className:"uml-layout-editor","aria-label":"Edytor uk\u0142adu UML",children:[(0,a.jsxs)("div",{className:"uml-layout-editor-title",children:[(0,a.jsx)("strong",{children:"UML layout"}),(0,a.jsx)("span",{children:ae?"niezapisane zmiany":"zapisany"}),Re&&(0,a.jsx)("b",{children:"\u0179r\xF3d\u0142o PlantUML zmieni\u0142o si\u0119"})]}),(0,a.jsxs)("label",{children:["Element",(0,a.jsx)("select",{value:J??"",onChange:v=>at(v.target.value||null),children:bt.map(v=>(0,a.jsx)("option",{value:v,children:v},v))})]}),J&&ct&&(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("div",{className:"uml-layout-number-grid",children:["x","y","width","height"].map(v=>(0,a.jsxs)("label",{children:[v,(0,a.jsx)("input",{type:"number",step:"1",min:v==="width"||v==="height"?12:void 0,value:Math.round(ct[v]*100)/100,onChange:x=>oe(J,R=>({...R,[v]:Number(x.target.value)}))})]},v))}),(0,a.jsxs)("label",{className:"uml-layout-editor-text",children:["Tekst bloku \u2014 jeden wiersz SVG na lini\u0119",(0,a.jsx)("textarea",{rows:Math.max(3,Math.min(10,ct.text?.lines?.length??3)),value:(ct.text?.lines??[]).join(` +`),onChange:v=>oe(J,x=>({...x,text:{...x.text,title:v.target.value.split(` +`)[0]??"",description:v.target.value.split(` +`).slice(1).join(` +`),lines:v.target.value.split(` +`)}}))})]}),(0,a.jsxs)("div",{className:"uml-layout-style-grid",children:[(0,a.jsxs)("label",{children:["Font",(0,a.jsx)("input",{type:"number",min:"4",max:"96",step:"0.5",value:ct.text?.font_size??12,onChange:v=>oe(J,x=>({...x,text:{...x.text,font_size:Number(v.target.value)}}))})]}),(0,a.jsxs)("label",{children:["Wyr\xF3wnanie",(0,a.jsxs)("select",{value:ct.text?.align??"left",onChange:v=>oe(J,x=>({...x,text:{...x.text,align:v.target.value}})),children:[(0,a.jsx)("option",{value:"left",children:"left"}),(0,a.jsx)("option",{value:"center",children:"center"}),(0,a.jsx)("option",{value:"right",children:"right"})]})]}),(0,a.jsxs)("label",{children:["Wype\u0142nienie",(0,a.jsx)("input",{type:"color",value:ct.style?.fill??"#edf6fa",onChange:v=>oe(J,x=>({...x,style:{...x.style,fill:v.target.value}}))})]}),(0,a.jsxs)("label",{children:["Obramowanie",(0,a.jsx)("input",{type:"color",value:ct.style?.stroke??"#2c7794",onChange:v=>oe(J,x=>({...x,style:{...x.style,stroke:v.target.value}}))})]}),(0,a.jsxs)("label",{children:["Linia",(0,a.jsx)("input",{type:"number",min:"0.5",max:"12",step:"0.25",value:ct.style?.stroke_width??1.5,onChange:v=>oe(J,x=>({...x,style:{...x.style,stroke_width:Number(v.target.value)}}))})]})]})]}),(0,a.jsxs)("div",{className:"uml-layout-editor-actions",children:[(0,a.jsx)("button",{className:"is-primary",disabled:!ae,onClick:()=>void wt(),children:"Zapisz uk\u0142ad"}),(0,a.jsx)("button",{disabled:!ae,onClick:kn,children:"Cofnij zmiany"}),(0,a.jsx)("button",{className:"is-danger",onClick:()=>void Dt(),children:"Reset PlantUML"})]}),(0,a.jsx)("output",{children:q})]})]})]})}function nh(e){return e.config.phases?.length?(0,a.jsx)(th,{...e}):(0,a.jsx)(eh,{...e})}function rh({asset:e,continuation:t}){let n=e.tile;if(!n)return(0,a.jsx)("img",{src:e.href,alt:e.alt,loading:"lazy"});let r=e.diagram_block_label?.match(/^A[1-8]\b/)?.[0]??e.diagram_title?.match(/^A[1-8]\b/)?.[0]??e.caption.match(/^A[1-8]\b/)?.[0]??"UML",o=(e.diagram_title??e.caption).replace(/^A[1-8]\s*[·—:-]\s*/i,"");return(0,a.jsxs)("div",{className:"uml-react uml-snapshot-react uml-static-tile",children:[(0,a.jsx)("header",{className:"uml-snapshot-header",children:(0,a.jsxs)("div",{className:"uml-header-first-line",children:[(0,a.jsxs)("h3",{children:[(0,a.jsx)("span",{children:r})," \u2014 ",o,(0,a.jsxs)("small",{className:"uml-tile-part",children:["cz\u0119\u015B\u0107 ",n.index,"/",n.count," \xB7 r",n.row," c",n.column]})]}),(0,a.jsxs)("div",{className:"uml-header-navigation",children:[(0,a.jsx)(Pp,{continuation:t}),(0,a.jsx)("div",{className:"uml-phase-actions","aria-label":"Fazy logicznego diagramu",children:(e.diagram_phase_labels??[]).map(i=>(0,a.jsx)("span",{children:i},i))})]})]})}),(0,a.jsx)("div",{className:"uml-snapshot-layout",children:(0,a.jsx)("div",{className:"uml-canvas uml-step-canvas",children:(0,a.jsx)("div",{className:"uml-svg-host is-tiled is-static",style:vs(n),children:(0,a.jsx)("div",{className:"uml-static-crop",style:{width:`${n.width}px`,height:`${n.height}px`},children:(0,a.jsx)("img",{src:e.href,alt:e.alt,style:{width:`${n.full_width}px`,height:`${n.full_height}px`,transform:`translate(${-(n.x-n.full_x)}px, ${-(n.y-n.full_y)}px)`}})})})})})]})}function oh({asset:e,number:t,progress:n,onSetStatus:r,continuation:o}){let i=e.interactive?.kind,l=!!i;return(0,a.jsxs)("figure",{className:`card-asset asset-${e.kind} ${l?"asset-interactive":""}`,id:e.label.replace(":","-"),children:[i==="uml-sequence"||i==="uml-class"?(0,a.jsx)(nh,{config:e.interactive,asset:e,progress:n,onSetStatus:r,continuation:o}):i==="allocator-memory-flow"?(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(Jg,{config:e.interactive,title:e.caption}),(0,a.jsx)("img",{className:"asset-static-fallback",src:e.href,alt:e.alt})]}):e.tile?(0,a.jsx)(rh,{asset:e,continuation:o}):(0,a.jsx)("a",{href:e.href,target:"_blank",rel:"noopener",children:(0,a.jsx)("img",{src:e.href,alt:e.alt,loading:"lazy",style:{width:`${e.width*100}%`}})}),(0,a.jsxs)("figcaption",{children:["Rys. ",e.figure_number??t,". ",e.caption]}),e.diagram_description&&(0,a.jsxs)("aside",{className:"uml-after-description",children:[(0,a.jsx)("strong",{children:"Interpretacja"}),(0,a.jsx)("p",{children:e.diagram_description})]})]})}function ih({steps:e,progress:t,onCycle:n}){if(!e.length)return null;let r={pending:"\u25CB niezatwierdzony",approved:"\u25CF zatwierdzony"};return(0,a.jsx)("div",{className:"step-map",children:e.map(o=>{let i=t?.items?.[o.id]?.status??"pending";return(0,a.jsxs)("div",{className:`step-row ${i==="approved"?"lesson-progress-completed":""}`,children:[(0,a.jsx)("button",{className:"lesson-progress-control","data-status":i,onClick:()=>n(o.id,i),disabled:!t,children:r[i]}),(0,a.jsx)("span",{className:"step-title",children:o.title})]},o.id)})})}function lh({section:e,progress:t,onCycle:n,onSetStatus:r,figureOffset:o}){let i=Pg(e);return(0,a.jsxs)(bs,{header:e.header_html,footer:e.footer_html,left:e.left_margin_html,right:e.right_margin_html,className:`section-sheet${e.assets.length?" section-sheet--asset":""} sheet--${e.page_orientation||"portrait"}`,children:[(0,a.jsx)("div",{id:e.id,className:"section-anchor"}),e.show_heading&&(0,a.jsxs)("h2",{className:"section-heading",children:[(0,a.jsx)("span",{className:"section-index",children:e.index}),(0,a.jsx)("span",{className:"section-title",children:e.title})]}),(0,a.jsx)(ih,{steps:e.steps,progress:t,onCycle:n}),e.content_html&&(0,a.jsx)(Ft,{html:e.content_html}),(0,a.jsx)("div",{className:"tasks",children:e.tasks.map(l=>(0,a.jsx)(Zg,{task:l},l.ref))}),e.assets.map((l,s)=>(0,a.jsx)(oh,{asset:l,number:o+s+1,progress:t,onSetStatus:r,continuation:i},l.label)),e.tasks.map(l=>l.conclusion_html&&(0,a.jsxs)("aside",{className:"task-conclusion",children:[(0,a.jsx)("strong",{children:"WNIOSEK"}),(0,a.jsx)(Ft,{html:l.conclusion_html})]},`${l.ref}-conclusion`))]})}function ah({progress:e,error:t}){if(!e?.summary&&!t)return null;let n=e?.summary.counts;return(0,a.jsxs)("section",{className:"lesson-progress-topbar","aria-label":"Przebieg zaj\u0119\u0107",children:[(0,a.jsx)("strong",{children:"PRZEBIEG"}),n&&(0,a.jsxs)("output",{title:"Zatwierdzone kroki",children:[n.approved,"/",e.summary.total]}),t&&(0,a.jsx)("span",{className:"lesson-progress-error",title:t,children:"!"}),(0,a.jsx)("a",{href:"/api/report/teams.md",target:"_blank",rel:"noopener",title:"Otw\xF3rz raport do Teams",children:"Teams"}),(0,a.jsx)("a",{href:"/api/report/lesson.html",target:"_blank",rel:"noopener",title:"Otw\xF3rz \u015Blad lekcji ze zrzutami",children:"\u015Alad"})]})}var Fn=[{id:"u01",name:"Alicja Nowak",laptop:3,connection:"online",sync:"follow",position:"A5\xB708"},{id:"u02",name:"Bartosz Kowalski",laptop:7,connection:"online",sync:"follow",position:"A5\xB708"},{id:"u03",name:"Celina Wi\u015Bniewska",laptop:11,connection:"away",sync:"paused",position:"A5\xB705"},{id:"u04",name:"Dawid W\xF3jcik",laptop:14,connection:"offline",sync:"follow",position:"\u2014"},{id:"u05",name:"Emilia Kami\u0144ska",laptop:18,connection:"online",sync:"local",position:"A3\xB702"},{id:"u06",name:"Filip Lewandowski",laptop:22,connection:"error",sync:"follow",position:"A5\xB706"},{id:"u07",name:"Gabriela Zieli\u0144ska",laptop:24,connection:"online",sync:"follow",position:"A5\xB708"}];function sh(e){let t=new Map,n=(r,o)=>{let i=r.trim()||"task",l=t.get(i);return l||(l={id:i,label:o.trim()||i.toUpperCase(),blocks:[],exercises:[]},t.set(i,l)),l};for(let r of e.sections){for(let o of r.assets){let i=o.interactive;if(!i?.task||!i.block)continue;let l=n(i.task.id,i.task.label),s=`diagram:${i.block.id}`,u=l.blocks.find(d=>d.id===s);u||(u={id:s,label:i.block.label,kind:"diagram",phases:[],steps:[]},l.blocks.push(u));for(let d of i.phases??[]){let m=u.phases.find(y=>y.id===d.id);m||(m={...d,steps:[]},u.phases.push(m));for(let y of d.steps)m.steps.some(g=>g.id===y.id)||m.steps.push(y)}}for(let o of r.tasks){let i=n(o.ref,o.title);i.label=o.title||i.label;for(let l of o.flow){if(l.kind==="exercise"){i.exercises.some(u=>u.id===l.id)||i.exercises.push(l);continue}let s=`flow:${l.id}`;i.blocks.some(u=>u.id===s)||i.blocks.push({id:s,label:l.title,kind:"flow",phases:[],steps:l.steps.map((u,d)=>({id:u.id,number:d+1,label:u.title,description:u.title,mode:"RUN"}))})}}}return[...t.values()]}function bp({side:e,panels:t,activeId:n,pinnedId:r,onActiveChange:o,onPinnedChange:i}){let l=(0,k.useRef)(0),s=t.find(f=>f.id===n)??null,u=(0,k.useCallback)(()=>{window.clearTimeout(l.current)},[]),d=(0,k.useCallback)(()=>{u(),!r&&(l.current=window.setTimeout(()=>o(null),150))},[u,o,r]),m=(0,k.useCallback)(f=>{u(),r||o(f)},[u,o,r]),y=(0,k.useCallback)(f=>{if(u(),r===f){i(null),o(null);return}i(f),o(f)},[u,o,i,r]),g=(0,k.useCallback)(()=>{u(),i(null),o(null)},[u,o,i]);return(0,k.useEffect)(()=>()=>window.clearTimeout(l.current),[]),t.length?(0,a.jsxs)("div",{className:`side-panel-dock side-panel-dock--${e}`,"data-side":e,children:[(0,a.jsx)("nav",{className:"side-panel-rail","aria-label":`Panele ${e==="left"?"lewe":"prawe"}`,children:t.map(f=>{let S=n===f.id,b=r===f.id;return(0,a.jsx)("button",{type:"button",className:`side-panel-trigger${S?" is-open":""}${b?" is-pinned":""}`,style:{"--side-panel-accent":f.accent},"aria-controls":`side-panel-${e}-${f.id}`,"aria-expanded":S,"aria-pressed":b,"aria-label":`${f.title}: ${b?"odpnij":"przypnij"} panel`,title:`${f.title} \xB7 ${b?"Fixed":"Auto"}`,onMouseEnter:()=>m(f.id),onMouseLeave:d,onFocus:()=>m(f.id),onClick:()=>y(f.id),children:(0,a.jsx)("span",{children:f.railLabel})},f.id)})}),(0,a.jsx)("aside",{id:s?`side-panel-${e}-${s.id}`:void 0,className:`side-panel${s?" is-open":""}`,"aria-hidden":!s,"aria-label":s?.title,style:s?{"--side-panel-accent":s.accent}:void 0,onMouseEnter:u,onMouseLeave:d,onFocusCapture:u,children:s&&(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)("header",{className:"side-panel-header",children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("small",{children:e==="left"?"LEWY PANEL":"PRAWY PANEL"}),(0,a.jsx)("strong",{children:s.title})]}),(0,a.jsxs)("div",{className:"side-panel-actions",children:[(0,a.jsx)("button",{type:"button",className:r===s.id?"is-pinned":"",onClick:()=>y(s.id),title:r===s.id?"Tryb Fixed \u2014 kliknij, aby przej\u015B\u0107 do Auto":"Tryb Auto \u2014 kliknij, aby przypi\u0105\u0107",children:r===s.id?"Fixed":"Auto"}),(0,a.jsx)("button",{type:"button",onClick:g,"aria-label":`Zamknij ${s.title}`,children:"\xD7"})]})]}),(0,a.jsx)("div",{className:"side-panel-body",children:s.content})]})})]}):null}function go({id:e,depth:t,kind:n,title:r,label:o,refValue:i,stateValue:l,stateKind:s,hasChildren:u=!1,expanded:d=!1,onToggle:m,href:y,current:g=!1,unavailable:f=!1,onNavigate:S}){let b=y?(0,a.jsx)("a",{className:"card-library-tree-link","data-stem-source-aware":"true",href:y,onClick:S,"aria-current":g?"page":void 0,title:g?"Poka\u017C bie\u017C\u0105c\u0105 kart\u0119":`Otw\xF3rz: ${r}`,children:o}):(0,a.jsx)("span",{className:"card-library-tree-label",title:r,children:o});return(0,a.jsxs)("div",{className:`card-library-treegrid-row is-${n}${g?" is-current":""}${f?" is-unavailable":""}`,role:"row","aria-level":t+1,"aria-expanded":u?d:void 0,"aria-disabled":f||void 0,"data-node-id":e,style:{"--card-library-depth":t},children:[(0,a.jsxs)("span",{className:"card-library-tree-title",role:"gridcell",children:[u?(0,a.jsx)("button",{className:"card-library-tree-expander",type:"button",onClick:m,"aria-label":`${d?"Zwi\u0144":"Rozwi\u0144"}: ${r}`,children:d?"\u25BE":"\u25B8"}):(0,a.jsx)("span",{className:"card-library-tree-expander is-empty","aria-hidden":"true"}),b]}),(0,a.jsx)("span",{role:"gridcell",children:i}),(0,a.jsx)("span",{role:"gridcell",children:(0,a.jsx)("b",{className:"card-library-tree-state","data-state":s,children:l})})]})}function uh({library:e,source:t,onNavigate:n}){if(!e)return null;let r=e.subjects.reduce((m,y)=>m+y.levels.reduce((g,f)=>g+f.series.length,0),0),o=e.subjects.reduce((m,y)=>m+y.levels.reduce((g,f)=>g+f.series.reduce((S,b)=>S+b.cards.filter(_=>_.available).length,0),0),0),i=e.subjects.reduce((m,y)=>m+y.levels.reduce((g,f)=>g+f.series.reduce((S,b)=>S+b.cards.length,0),0),0),[l,s]=(0,k.useState)(()=>{let m=new Set;return e.subjects.forEach(y=>{let g=`subject:${y.id}`;y.current&&m.add(g),y.levels.forEach(f=>{let S=`${g}:level:${f.id}`;f.current&&m.add(S),f.series.forEach(b=>{let _=`${S}:series:${b.id}`;b.current&&m.add(_),b.cards.forEach(p=>{p.current&&(p.tasks?.length??0)>0&&m.add(`${_}:card:${p.id}`)})})})}),m}),u=m=>l.has(m),d=m=>{s(y=>{let g=new Set(y);return g.has(m)?g.delete(m):g.add(m),g})};return(0,a.jsxs)("div",{className:"card-library-panel",children:[(0,a.jsxs)("div",{className:"card-library-treegrid",role:"treegrid","aria-label":"Serie, karty i taski","aria-colcount":3,children:[(0,a.jsxs)("div",{className:"card-library-treegrid-row card-library-treegrid-head",role:"row",children:[(0,a.jsx)("span",{role:"columnheader",children:"DRZEWO"}),(0,a.jsx)("span",{role:"columnheader",children:"REF"}),(0,a.jsx)("span",{role:"columnheader",children:"STATUS"})]}),e.subjects.map(m=>{let y=`subject:${m.id}`;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(go,{id:y,depth:0,kind:"subject",title:m.title,label:(0,a.jsx)("strong",{children:m.title}),refValue:"PRZEDMIOT",stateValue:`${m.levels.length} POZ.`,hasChildren:m.levels.length>0,expanded:u(y),onToggle:()=>d(y)}),u(y)&&m.levels.map(g=>{let f=`${y}:level:${g.id}`;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(go,{id:f,depth:1,kind:"level",title:g.title,label:(0,a.jsx)("strong",{children:g.title}),refValue:"ROK",stateValue:`${g.series.length} SERII`,hasChildren:g.series.length>0,expanded:u(f),onToggle:()=>d(f)}),u(f)&&g.series.map(S=>{let b=`${f}:series:${S.id}`;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(go,{id:b,depth:2,kind:"series",title:S.title,label:(0,a.jsx)("strong",{children:S.title}),refValue:S.role==="reference"?"REF.":"SERIA",stateValue:`${S.cards.length} KART`,hasChildren:S.cards.length>0,expanded:u(b),onToggle:()=>d(b)}),u(b)&&S.cards.map(_=>{let p=`${b}:card:${_.id}`,c=_.tasks??[],h=_.current||_.available,w=vg(_,t),C=_.current?"current":_.available?"online":"plan";return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(go,{id:p,depth:3,kind:"card",title:`${_.id} \xB7 ${_.title}`,label:(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("strong",{children:_.id}),(0,a.jsx)("span",{children:_.title})]}),refValue:_.version||"\u2014",stateValue:_.current?"BIE\u017B\u0104CA":_.available?"ONLINE":"PLAN",stateKind:C,hasChildren:c.length>0,expanded:u(p),onToggle:()=>d(p),href:h?_.current?"#":w:void 0,current:_.current,unavailable:!h,onNavigate:n}),u(p)&&c.map(A=>{let P=_.current?`#${A.id}`:`${w}#${A.id}`;return(0,a.jsx)(go,{id:`${p}:task:${A.id}`,depth:4,kind:"task",title:`${A.id.toUpperCase()} \xB7 ${A.title}`,label:(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)("strong",{children:A.id.toUpperCase()}),(0,a.jsx)("span",{children:A.title})]}),refValue:"TASK",stateValue:h?"GOTOWY":"PLAN",stateKind:h?"online":"plan",href:h?P:void 0,unavailable:!h,onNavigate:n},`${p}:task:${A.id}`)})]},p)})]},b)})]},f)})]},y)})]}),(0,a.jsxs)("footer",{className:"card-library-footer",children:[(0,a.jsx)("span",{children:e.workspace||"STEM"}),(0,a.jsxs)("span",{children:[r," serii"]}),(0,a.jsxs)("span",{children:[(0,a.jsx)("i",{className:"is-online"})," ",o,"/",i," online"]}),(0,a.jsx)("kbd",{children:"Esc"})]})]})}function ch({toc:e,onNavigate:t}){return(0,a.jsxs)("nav",{className:"side-toc","aria-label":"Spis tre\u015Bci karty",children:[(0,a.jsxs)("header",{children:[(0,a.jsx)("small",{children:"KARTA PRACY"}),(0,a.jsx)("h2",{children:"Spis tre\u015Bci"})]}),(0,a.jsx)("ol",{children:e.map((n,r)=>(0,a.jsx)("li",{children:(0,a.jsxs)("a",{href:`#${n.id}`,onClick:t,children:[(0,a.jsx)("span",{children:String(r+1).padStart(2,"0")}),(0,a.jsx)("strong",{children:n.label})]})},n.id))})]})}function dh({progress:e,error:t}){let n=e?.summary?.counts;return(0,a.jsxs)("section",{className:"side-progress","aria-label":"Przebieg zaj\u0119\u0107 i raport",children:[(0,a.jsxs)("header",{children:[(0,a.jsx)("small",{children:"DOWODY I RAPORT"}),(0,a.jsx)("h2",{children:"Przebieg zaj\u0119\u0107"})]}),(0,a.jsxs)("div",{className:"side-progress-summary",children:[(0,a.jsx)("span",{children:"Zatwierdzone kroki"}),(0,a.jsx)("strong",{children:n?`${n.approved}/${e.summary.total}`:"\u2014"})]}),t&&(0,a.jsx)("p",{className:"side-progress-error",children:t}),(0,a.jsxs)("div",{className:"side-progress-links",children:[(0,a.jsx)("a",{href:"/api/report/teams.md",target:"_blank",rel:"noopener",children:"Raport do Teams"}),(0,a.jsx)("a",{href:"/api/report/lesson.html",target:"_blank",rel:"noopener",children:"\u015Alad lekcji ze zrzutami"})]})]})}var ph={online:"ONLINE",away:"ZAJ\u0118TY",offline:"OFFLINE",error:"B\u0141\u0104D"},mh={follow:"SYNC",paused:"PAUZA",local:"LOKAL."},xp={present:"BY\u0141",missed:"NIE BY\u0141",pending:"OCZEK."},fh={committed:"COMMIT",working:"PRACA",missing:"BRAK"};function Mp(e){let t=2166136261;for(let n=0;n>>0}function wp(e,t,n){let r=Mp(`${e.id}:step:${t}`),o=r%8,i=o===0?"missed":o===1?"pending":"present",l=8+n*4+r%3,s=10+Math.floor(l/60),u=l%60,d=(e.laptop*7+r)%60;return{state:i,timestamp:i==="present"?`${String(s).padStart(2,"0")}:${String(u).padStart(2,"0")}:${String(d).padStart(2,"0")}`:"\u2014"}}function kp(e,t){let n=Mp(`${e.id}:exercise:${t}`),r=n%6,o=r===0?"missing":r===1?"working":"committed";return{state:o,commit:o==="committed"?n.toString(16).padStart(8,"0").slice(0,7):"\u2014"}}function Xt({id:e,depth:t,title:n,kind:r,refValue:o,session:i,evidence:l,state:s,hasChildren:u=!1,expanded:d=!1,onToggle:m}){return(0,a.jsxs)("div",{className:`classroom-treegrid-row classroom-node-row is-${r}`,role:"row","aria-level":t+1,"aria-expanded":u?d:void 0,"data-node-id":e,style:{"--classroom-depth":t},children:[(0,a.jsxs)("span",{className:"classroom-node-title",role:"gridcell",title:n,children:[u?(0,a.jsx)("button",{className:"classroom-expander",type:"button",onClick:m,"aria-label":`${d?"Zwi\u0144":"Rozwi\u0144"}: ${n}`,children:d?"\u25BE":"\u25B8"}):(0,a.jsx)("span",{className:"classroom-expander is-empty","aria-hidden":"true"}),(0,a.jsx)("strong",{children:n})]}),(0,a.jsx)("span",{role:"gridcell",children:o}),(0,a.jsx)("span",{role:"gridcell",children:i}),(0,a.jsx)("span",{role:"gridcell",children:l}),(0,a.jsx)("span",{role:"gridcell",children:s})]})}function gh({model:e}){let t=sh(e),n=Fn[0]?.id,r=t[0]?.id,[o,i]=(0,k.useState)(()=>new Set(["class",...n?[`student:${n}`]:[],...n&&r?[`student:${n}:task:${r}`]:[]])),l=m=>o.has(m),s=m=>{i(y=>{let g=new Set(y);return g.has(m)?g.delete(m):g.add(m),g})},u=Fn.filter(m=>m.connection==="online"||m.connection==="away").length,d=Fn.filter(m=>m.sync==="follow").length;return(0,a.jsxs)("section",{className:"classroom-panel","aria-label":"Klasa i stanowiska uczni\xF3w",children:[(0,a.jsxs)("div",{className:"classroom-treegrid",role:"treegrid","aria-label":"Uczniowie, struktura karty, obecno\u015B\u0107 i commity","aria-colcount":5,children:[(0,a.jsxs)("div",{className:"classroom-treegrid-row classroom-treegrid-head",role:"row",children:[(0,a.jsx)("span",{role:"columnheader",children:"DRZEWO"}),(0,a.jsx)("span",{role:"columnheader",children:"REF"}),(0,a.jsx)("span",{role:"columnheader",children:"SESJA"}),(0,a.jsx)("span",{role:"columnheader",children:"DOW\xD3D"}),(0,a.jsx)("span",{role:"columnheader",children:"STAN"})]}),(0,a.jsx)(Xt,{id:"class",depth:0,kind:"class",title:"2TP \xB7 grupa A",refValue:`${Fn.length} ucz.`,session:`${u}/${Fn.length}`,evidence:`${d}/${Fn.length} SYNC`,state:`${t.length} TASK`,hasChildren:!0,expanded:l("class"),onToggle:()=>s("class")}),l("class")&&Fn.map(m=>{let y=`student:${m.id}`;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(Xt,{id:y,depth:1,kind:"student",title:m.name,refValue:`L${String(m.laptop).padStart(2,"0")}`,session:(0,a.jsx)("b",{className:"classroom-status","data-status":m.connection,children:ph[m.connection]}),evidence:(0,a.jsx)("b",{className:"classroom-sync","data-sync":m.sync,children:mh[m.sync]}),state:m.position,hasChildren:t.length>0,expanded:l(y),onToggle:()=>s(y)}),l(y)&&t.map(g=>{let f=`${y}:task:${g.id}`,S=g.exercises.filter(b=>kp(m,b.id).state==="committed").length;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(Xt,{id:f,depth:2,kind:"task",title:g.label,refValue:g.id.toUpperCase(),session:`${g.blocks.length} BLOCK`,evidence:`${S}/${g.exercises.length} COMMIT`,state:"TASK",hasChildren:g.blocks.length+g.exercises.length>0,expanded:l(f),onToggle:()=>s(f)}),l(f)&&g.blocks.map(b=>{let _=`${f}:block:${b.id}`,p=b.phases.length||b.steps.length;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(Xt,{id:_,depth:3,kind:"block",title:b.label,refValue:b.kind==="diagram"?"BLOCK":"TASK BLOCK",session:`${p} ${b.phases.length?"PHASE":"STEP"}`,evidence:"\u2014",state:b.kind==="diagram"?"UML":"FLOW",hasChildren:p>0,expanded:l(_),onToggle:()=>s(_)}),l(_)&&b.phases.map(c=>{let h=`${_}:phase:${c.id}`;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(Xt,{id:h,depth:4,kind:"phase",title:`PHASE \xB7 ${c.label}`,refValue:`${c.steps.length} STEP`,session:"\u2014",evidence:"\u2014",state:"PHASE",hasChildren:c.steps.length>0,expanded:l(h),onToggle:()=>s(h)}),l(h)&&c.steps.map(w=>{let C=wp(m,`${b.id}:${c.id}:${w.id}`,w.number);return(0,a.jsx)(Xt,{id:`${h}:step:${w.id}`,depth:5,kind:"step",title:`STEP ${String(w.number).padStart(2,"0")} \xB7 ${w.label}`,refValue:w.mode??"CODE",session:(0,a.jsx)("b",{className:"classroom-visit","data-visit":C.state,children:xp[C.state]}),evidence:C.timestamp,state:C.state==="present"?"OK":C.state==="missed"?"BRAK":"\u2014"},`${h}:step:${w.id}`)})]},h)}),l(_)&&b.steps.map(c=>{let h=wp(m,`${b.id}:${c.id}`,c.number);return(0,a.jsx)(Xt,{id:`${_}:step:${c.id}`,depth:4,kind:"step",title:`STEP ${String(c.number).padStart(2,"0")} \xB7 ${c.label}`,refValue:c.mode??"RUN",session:(0,a.jsx)("b",{className:"classroom-visit","data-visit":h.state,children:xp[h.state]}),evidence:h.timestamp,state:h.state==="present"?"OK":h.state==="missed"?"BRAK":"\u2014"},`${_}:step:${c.id}`)})]},_)}),l(f)&&g.exercises.length>0&&(()=>{let b=`${f}:exercises`;return(0,a.jsxs)(k.default.Fragment,{children:[(0,a.jsx)(Xt,{id:b,depth:3,kind:"exercises",title:"EXERCISES",refValue:`${g.exercises.length} EX`,session:`${S}/${g.exercises.length}`,evidence:"GIT",state:S===g.exercises.length?"DONE":"TODO",hasChildren:!0,expanded:l(b),onToggle:()=>s(b)}),l(b)&&g.exercises.map(_=>{let p=kp(m,_.id);return(0,a.jsx)(Xt,{id:`${b}:${_.id}`,depth:4,kind:"exercise",title:`EXERCISE \xB7 ${_.title}`,refValue:_.id,session:(0,a.jsx)("b",{className:"classroom-commit","data-commit":p.state,children:fh[p.state]}),evidence:p.commit,state:p.state==="committed"?"DONE":p.state==="working"?"W TOKU":"TODO"},`${b}:${_.id}`)})]},b)})()]},f)})]},m.id)})]}),(0,a.jsxs)("footer",{className:"classroom-legend",children:[(0,a.jsxs)("span",{children:[(0,a.jsx)("b",{children:"BY\u0141"})," obecny przy omawianym STEP"]}),(0,a.jsxs)("span",{children:[(0,a.jsx)("b",{children:"COMMIT"})," rozwi\u0105zanie EXERCISE zapisane w Git"]})]})]})}function hh({open:e,onClose:t}){return e?(0,a.jsx)("div",{className:"shortcut-help-backdrop",role:"presentation",onMouseDown:t,children:(0,a.jsxs)("section",{className:"shortcut-help",role:"dialog","aria-modal":"true","aria-labelledby":"shortcut-help-title",onMouseDown:r=>r.stopPropagation(),children:[(0,a.jsxs)("header",{children:[(0,a.jsxs)("div",{children:[(0,a.jsx)("small",{children:"F12 \xB7 NAWIGACJA DYDAKTYCZNA"}),(0,a.jsx)("h2",{id:"shortcut-help-title",children:"CARD \u2192 TASK \u2192 BLOCK \u2192 PHASE \u2192 STEP \u2192 SNAPSHOT"})]}),(0,a.jsx)("button",{onClick:t,"aria-label":"Zamknij skorowidz",children:"\xD7"})]}),(0,a.jsx)("dl",{children:[["Alt + \u2191 / \u2193","poprzedni / nast\u0119pny TASK"],["Ctrl + \u2191 / \u2193","poprzedni / nast\u0119pny BLOCK"],["Shift + \u2191 / \u2193","poprzednia / nast\u0119pna PHASE"],["Ctrl + Shift + \u2191 / \u2193","poprzedni / nast\u0119pny STEP"],["Alt + Shift + \u2191 / \u2193","poprzedni / nast\u0119pny unikalny SNAPSHOT"],["\u2191 / \u2193","poprzedni / nast\u0119pny element aktywnego poziomu"],["\u2192","zejd\u017A poziom ni\u017Cej"],["\u2190 / Esc","wr\xF3\u0107 poziom wy\u017Cej"],["Enter","wybierz element; przy SYNC ON odtw\xF3rz stan"],["F1","reset aktualnego celu Neovima/kontenera"],["F2","synchronizuj cel z kursorem drzewa UML"],["Ctrl + `","maksymalizuj pane, dopasuj siatk\u0119 i od\u015Bwie\u017C Neovima"],["Ctrl + Enter","zatwierd\u017A lub cofnij zatwierdzenie kroku"],["Alt + 0","wysu\u0144 lub schowaj panel serii i kart"],["Alt + 1","poka\u017C lub ukryj panel Neovima"],["Alt + 2","prze\u0142\u0105cz SYNC OFF / SYNC ON"],["Alt + 3","uzbr\xF3j lub rozbr\xF3j sterowanie Neovimem"],["Alt + 4","wysoki panel 90% / zapisana wysoko\u015B\u0107 normalna"],["Alt + 5","dopasuj ca\u0142y ekran Neovima; wy\u0142\u0105cz r\u0119czny suwak"],["Alt + 6","przypnij lub ukryj model alokatora"],["Alt + 7","po\u0142\u0105cz segmenty UML w szeroki spread / wr\xF3\u0107 do A4"],["Alt + 3, potem najed\u017A","przeka\u017C klawiatur\u0119 i mysz do aktywnej sesji"],["Alt + W","prefiks polece\u0144 okien Neovima ()"],["Alt + strza\u0142ka","przejd\u017A do s\u0105siedniego okna Neovima"],["F12","poka\u017C lub ukryj ten skorowidz"]].map(([r,o])=>(0,a.jsxs)("div",{children:[(0,a.jsx)("dt",{children:(0,a.jsx)("kbd",{children:r})}),(0,a.jsx)("dd",{children:o})]},r))}),(0,a.jsx)("p",{children:"Przy SYNC ON wyb\xF3r stepu odtwarza i weryfikuje przypi\u0119ty stan maszyny. SYNC OFF nie resetuje debuggera. Po najechaniu panel przekazuje do Neovima wy\u0142\u0105cznie kontrolowane zdarzenia klawiatury i myszy, wi\u0119c mo\u017Cna normalnie kontynuowa\u0107 debugowanie."})]})}):null}function yh({model:e}){let t=!!e.has_allocator_model,[n]=(0,k.useState)(hg),r=n.sources.find(E=>E.id===n.active_source)??n.sources[0],o=(0,k.useRef)(null),i=(0,k.useRef)(null),l=(0,k.useRef)(`browser-${globalThis.crypto?.randomUUID?.()??Math.random().toString(36).slice(2)}`),s=(0,k.useRef)(0),u=(0,k.useRef)(0),d=(0,k.useRef)(!1),m=(0,k.useRef)(!1),y=(0,k.useRef)(0),g=(0,k.useRef)(0),f=(0,k.useRef)(0),[S,b]=(0,k.useState)(2.18),[_,p]=(0,k.useState)(()=>localStorage.getItem("stem-card-nvim-visible")!=="false"),[c,h]=(0,k.useState)(()=>localStorage.getItem("stem-card-nvim-sync-mode")==="true"),[w,C]=(0,k.useState)(()=>localStorage.getItem("stem-card-nvim-sync-mode")==="true"&&localStorage.getItem("stem-card-nvim-control-mode")==="true"),[A,P]=(0,k.useState)(!1),[z,U]=(0,k.useState)(!1),[F,ae]=(0,k.useState)(()=>t&&localStorage.getItem("stem-card-allocator-visible")!=="false"),[me,q]=(0,k.useState)(()=>{let E=localStorage.getItem("stem-card-diagram-spread");return E===null?!!e.has_spread:E==="true"}),[V,Re]=(0,k.useState)({state:"hidden",syncMode:!1,syncReady:!1,controlMode:!1,controlEnabled:!1}),[Se,J]=(0,k.useState)(null),[at,bt]=(0,k.useState)(""),[Me,ge]=(0,k.useState)(null),[yr,se]=(0,k.useState)(null),[Zt,ue]=(0,k.useState)(null),[T,D]=(0,k.useState)(null),[G,te]=(0,k.useState)(!1),[st,xt]=(0,k.useState)(""),[ze,oe]=(0,k.useState)(()=>window.innerWidth),wt=Me==="library",kn=!!(Me||Zt),Dt=(0,k.useCallback)(E=>{if(E){ge("library"),se("library");return}ge(N=>N==="library"?null:N),se(N=>N==="library"?null:N)},[]),Q=(0,k.useCallback)(E=>{window.clearTimeout(f.current),xt(E),f.current=window.setTimeout(()=>xt(""),4200)},[]),Ae=(0,k.useCallback)(E=>{let N=n.sources.find(K=>K.id===E);if(!N?.available||!N.base_url){Q(`${N?.label??E} \xB7 \u017Ar\xF3d\u0142o niedost\u0119pne`);return}if(N.id===n.active_source)return;let I=Ep(N,n.current_resource_uuid,window.location.hash);if(!I){Q(`${N.label} \xB7 karta nie ma UUID zasobu`);return}window.location.assign(I)},[n,Q]),ut=(0,k.useCallback)(async E=>{Q(`${E} \xB7 wykonywanie\u2026`);try{let I=await fetch(E==="F1"?"/api/nvim/reset":"/api/navigation/sync",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({actor:l.current})}),K=await I.json().catch(()=>({}));if(!I.ok)throw new Error(K.message??K.error??`HTTP ${I.status}`);E==="F2"&&K.current?(u.current=Math.max(u.current,Number(K.current.revision??0)),window.dispatchEvent(new CustomEvent("stem:navigation-command",{detail:K.current})),Q(`F2 \xB7 ${String(K.current.step.number).padStart(2,"0")} ${K.current.step.label} \xB7 READY`)):Q("F1 \xB7 reset przyj\u0119ty"),window.setTimeout(()=>window.dispatchEvent(new Event("stem:nvim-refresh")),120)}catch(N){Q(`${E} \xB7 ${N instanceof Error?N.message:String(N)}`)}},[Q]),Ne=(0,k.useCallback)(async()=>{Q("Ctrl+` \xB7 przeliczanie widoku\u2026");try{let E=await fetch("/api/nvim/redraw",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({actor:l.current})}),N=await E.json().catch(()=>({}));if(!E.ok)throw new Error(N.message??N.error??`HTTP ${E.status}`);let I=N.external_ui?.grid,K=I?` \xB7 ${I.width}\xD7${I.height}`:"";Q(`Ctrl+\` \xB7 widok od\u015Bwie\u017Cony${K}`),window.dispatchEvent(new Event("stem:nvim-refresh"))}catch(E){Q(`Ctrl+\` \xB7 ${E instanceof Error?E.message:String(E)}`)}},[Q]),Ee=(0,k.useCallback)(async E=>{try{let N=await fetch("/api/viewer/state",{method:"PUT",headers:{"content-type":"application/json"},body:JSON.stringify({actor:l.current,viewer:E})});if(!N.ok)return;let I=await N.json();s.current=Math.max(s.current,Number(I.viewer?.revision??0))}catch{}},[]),vr=E=>{let N=Math.max(.25,Math.min(3,E));b(N),Ee({scale:N})};(0,k.useEffect)(()=>{document.documentElement.style.setProperty("--viewer-canvas-scale",S.toFixed(3))},[S]),(0,k.useEffect)(()=>{if(!me)return;let E=i.current;if(!E)return;let N=0,I=()=>{window.cancelAnimationFrame(N),N=window.requestAnimationFrame(()=>{E.scrollLeft=Math.max(0,(E.scrollWidth-E.clientWidth)/2)})},K=window.requestAnimationFrame(I);return window.addEventListener("resize",I),()=>{window.cancelAnimationFrame(K),window.cancelAnimationFrame(N),window.removeEventListener("resize",I)}},[me,S,ze]),(0,k.useEffect)(()=>{let E=()=>oe(window.innerWidth);return window.addEventListener("resize",E),()=>window.removeEventListener("resize",E)},[]),(0,k.useEffect)(()=>{document.documentElement.dataset.nvimSyncMode=c?"on":"off"},[c]),(0,k.useEffect)(()=>{document.documentElement.dataset.cardClientId=l.current},[]);let kt=E=>{p(E),E||(P(!1),U(!1)),localStorage.setItem("stem-card-nvim-visible",String(E)),Ee({nvim:{visible:E,...E?{}:{expanded:!1,fit:!1}}})},yo=E=>{let N=E&&c;C(N),localStorage.setItem("stem-card-nvim-control-mode",String(N)),Ee({nvim:{control:N}})},vo=E=>{h(E),localStorage.setItem("stem-card-nvim-sync-mode",String(E)),E||(C(!1),localStorage.setItem("stem-card-nvim-control-mode","false")),Ee({nvim:{sync:E,control:E?w:!1}})},jn=E=>{E&&kt(!0),E&&U(!1),P(E),Ee({nvim:{visible:E||_,expanded:E,fit:!1}})},ct=E=>{E&&(kt(!0),P(!1)),U(E),Ee({nvim:{visible:E||_,fit:E,expanded:!1}})},v=E=>{let N=t&&E;ae(N),localStorage.setItem("stem-card-allocator-visible",String(N)),Ee({allocator:{visible:N}})},x=E=>{let N=!!(e.has_spread&&E);q(N),localStorage.setItem("stem-card-diagram-spread",String(N))};(0,k.useEffect)(()=>{let E=0,N=window.requestAnimationFrame(()=>{E=window.requestAnimationFrame(()=>{window.dispatchEvent(new Event("stem:nvim-refresh"))})});return()=>{window.cancelAnimationFrame(N),window.cancelAnimationFrame(E)}},[A,z]),(0,k.useEffect)(()=>{fetch("/api/progress").then(E=>E.ok?E.json():Promise.reject(new Error(`HTTP ${E.status}`))).then(E=>J(E.progress)).catch(E=>bt(`Post\u0119p dzia\u0142a tylko przez serwer karty (${E.message}).`))},[]),(0,k.useEffect)(()=>{let E=!1,N=!1,I={scale:S,viewport:{width:window.innerWidth,height:window.innerHeight},nvim:{visible:_,sync:c,control:w,expanded:A,fit:z},allocator:{visible:F}},K=B=>{let he=B.nvim.visible&&B.nvim.fit,ce=B.nvim.visible&&!he&&B.nvim.expanded;b(Math.max(.25,Math.min(3,B.scale))),p(B.nvim.visible),h(B.nvim.sync),C(B.nvim.sync&&B.nvim.control),P(ce),U(he);let Z=t&&(B.allocator?.visible??!0);ae(Z),localStorage.setItem("stem-card-nvim-visible",String(B.nvim.visible)),localStorage.setItem("stem-card-nvim-sync-mode",String(B.nvim.sync)),localStorage.setItem("stem-card-nvim-control-mode",String(B.nvim.sync&&B.nvim.control)),localStorage.setItem("stem-card-allocator-visible",String(Z));let ee=++g.current;d.current=!0,window.cancelAnimationFrame(y.current),y.current=window.requestAnimationFrame(()=>{if(ee!==g.current)return;let He=Qi();Gi(He,{left:B.scroll.x,top:B.scroll.y});let Ie=Array.from(document.querySelectorAll(".paper .sheet")),St=Ie[Math.max(0,Math.min(Ie.length-1,B.scroll.page_index))]?.querySelector(".sheet-content");St&&Number.isFinite(B.scroll.sheet_scroll_top)&&Gi(St,{top:B.scroll.sheet_scroll_top??0}),y.current=window.requestAnimationFrame(()=>{ee===g.current&&(d.current=!1,m.current&&(m.current=!1,window.dispatchEvent(new Event("stem:viewer-scroll-settled"))))})})},re=async()=>{try{let B=await fetch("/api/control/state",{cache:"no-store"});if(!B.ok||E)return;let he=await B.json(),ce=he.viewer;N||(N=!0,Number(ce?.revision??0)===0&&Ee(I)),ce&&ce.revision>s.current&&(s.current=ce.revision,ce.actor!==l.current&&K(ce));let Z=he.navigation;Z&&Z.revision>u.current&&(u.current=Z.revision,Z.actor!==l.current&&(d.current&&(m.current=!0),window.dispatchEvent(new CustomEvent("stem:navigation-command",{detail:Z}))))}catch{}};re();let W=window.setInterval(re,300);return()=>{E=!0,window.clearInterval(W),g.current+=1,window.cancelAnimationFrame(y.current),d.current=!1,m.current=!1}},[]),(0,k.useEffect)(()=>{Ee({nvim:{connection:V.state,screen_cursor:V.screenCursor,grid:V.grid}})},[V.grid?.columns,V.grid?.rows,V.screenCursor?.column,V.screenCursor?.row,V.state,Ee]),(0,k.useEffect)(()=>{let E=0,N=null,I=Array.from(document.querySelectorAll(".paper .sheet")),K=I.map(W=>W.querySelector(".sheet-content")).filter(W=>!!W),re=W=>{if(d.current)return;let B=W?.currentTarget;B instanceof HTMLElement&&B.classList.contains("sheet-content")&&(N=B),window.clearTimeout(E),E=window.setTimeout(()=>{let he=N?.closest(".sheet"),ce=he?I.indexOf(he):-1,Z=ce>=0?ce:I.reduce((He,Ie,Ke)=>{let St=Ie.getBoundingClientRect(),br=Math.abs((St.top+St.bottom)/2-window.innerHeight/2);return br=0?N:I[Z]?.querySelector(".sheet-content")??null;Ee({scroll:{x:window.scrollX,y:window.scrollY,page_index:Z,sheet_scroll_top:ee?.scrollTop??0},viewport:{width:window.innerWidth,height:window.innerHeight}}),N=null},120)};return window.addEventListener("scroll",re,{passive:!0}),window.addEventListener("resize",re),window.addEventListener("stem:viewer-scroll-settled",re),K.forEach(W=>W.addEventListener("scroll",re,{passive:!0})),re(),()=>{window.clearTimeout(E),window.removeEventListener("scroll",re),window.removeEventListener("resize",re),window.removeEventListener("stem:viewer-scroll-settled",re),K.forEach(W=>W.removeEventListener("scroll",re))}},[Ee]),(0,k.useEffect)(()=>{let E=N=>{if(N.repeat)return;let I=N.altKey&&!N.ctrlKey&&!N.metaKey?N.code:"";if(I==="Digit0"&&e.library){N.preventDefault(),N.stopImmediatePropagation(),te(!1),Dt(!wt);return}if(I==="Digit1"){N.preventDefault(),N.stopImmediatePropagation(),kt(!_);return}if(I==="Digit2"){N.preventDefault(),N.stopImmediatePropagation(),vo(!c);return}if(I==="Digit3"){if(N.preventDefault(),N.stopImmediatePropagation(),!c)return;kt(!0),yo(!w);return}if(I==="Digit4"){N.preventDefault(),N.stopImmediatePropagation(),jn(!A);return}if(I==="Digit5"){N.preventDefault(),N.stopImmediatePropagation(),ct(!z);return}if(I==="Digit6"&&t){N.preventDefault(),N.stopImmediatePropagation(),v(!F);return}if(I==="Digit7"&&e.has_spread){N.preventDefault(),N.stopImmediatePropagation(),x(!me);return}if((N.key==="F1"||N.key==="F2")&&!N.altKey&&!N.ctrlKey&&!N.metaKey&&!N.shiftKey){N.preventDefault(),N.stopImmediatePropagation(),ut(N.key);return}if(N.ctrlKey&&!N.altKey&&!N.metaKey&&!N.shiftKey&&(N.code==="Backquote"||N.key==="`")){N.preventDefault(),N.stopImmediatePropagation(),Ne();return}if(N.key==="F12"){N.preventDefault(),N.stopImmediatePropagation(),Dt(!1),ge(null),se(null),ue(null),D(null),te(K=>!K);return}if(N.key==="Escape"&&(kn||G)){N.preventDefault(),N.stopImmediatePropagation(),ge(null),se(null),ue(null),D(null),te(!1);return}};return window.addEventListener("keydown",E,!0),()=>window.removeEventListener("keydown",E,!0)},[F,me,t,wt,e.has_spread,e.library,w,A,z,c,_,ut,Ne,G,kn]),(0,k.useEffect)(()=>{let E=o.current?.querySelector(".topbar");if(!E)return;let N=()=>{document.documentElement.style.setProperty("--side-panel-top",`${Math.ceil(E.getBoundingClientRect().bottom)}px`)};N();let I=new ResizeObserver(N);return I.observe(E),window.addEventListener("resize",N),()=>{I.disconnect(),window.removeEventListener("resize",N),document.documentElement.style.removeProperty("--side-panel-top")}},[]),(0,k.useEffect)(()=>()=>window.clearTimeout(f.current),[]),(0,k.useEffect)(()=>{let E=N=>{!N.altKey||N.ctrlKey||(N.preventDefault(),b(I=>{let K=Math.max(.25,Math.min(3,I*Math.exp(-Math.max(-120,Math.min(120,N.deltaY))*.001)));return Ee({scale:K}),K}))};return window.addEventListener("wheel",E,{passive:!1}),()=>window.removeEventListener("wheel",E)},[Ee]);let R=async(E,N)=>{try{let I=await fetch(`/api/progress/${encodeURIComponent(E)}`,{method:"PUT",headers:{"content-type":"application/json"},body:JSON.stringify({status:N})});if(!I.ok)throw new Error(await I.text());J((await I.json()).progress),bt("")}catch(I){bt(`Nie zapisano post\u0119pu: ${I instanceof Error?I.message:String(I)}`)}},M=(E,N)=>{R(E,{pending:"approved",approved:"pending"}[N])},$=0,H=e.sections.map(E=>{let N=$;return $+=E.assets.length,{section:E,figureOffset:N}}),j=[];return H.forEach(E=>{let N=E.section.spread_group??"",I=j[j.length-1];if(N&&I?.spreadGroup===N){I.pages.push(E);return}j.push({key:N||E.section.id,spreadGroup:N,spreadLabel:E.section.spread_label??"",spreadColumns:E.section.spread_columns??1,pages:[E]})}),(0,a.jsxs)(a.Fragment,{children:[(0,a.jsxs)("div",{ref:o,className:`viewer-chrome${A?" is-nvim-expanded":""}${z?" is-nvim-fit":""}`,children:[(0,a.jsx)(Mg,{toc:e.toc,runtime:n,onSourceSelect:Ae,libraryAvailable:!!e.library,libraryOpen:wt,setLibraryOpen:Dt,scale:S,setScale:vr,nvimVisible:_,setNvimVisible:kt,nvimSyncMode:c,setNvimSyncMode:vo,nvimControlMode:w,setNvimControlMode:yo,nvimExpanded:A,setNvimExpanded:jn,nvimFit:z,setNvimFit:ct,allocatorVisible:F,setAllocatorVisible:v,hasAllocatorModel:t,diagramSpread:me,setDiagramSpread:x,hasDiagramSpread:!!e.has_spread,nvimSummary:V,progress:Se,progressError:at}),e.viewpoints?.length?(0,a.jsx)(Tg,{viewpoints:e.viewpoints}):null,(0,a.jsx)(Ig,{visible:t&&F,model:e}),(0,a.jsx)(Gg,{visible:_,syncMode:c,controlMode:w,expanded:A,fit:z,scale:S,onSummaryChange:Re}),st&&(0,a.jsx)("output",{className:"viewer-action-toast",children:st})]}),(0,a.jsxs)("main",{ref:i,className:`paper${me?" is-diagram-spread":""}`,children:[(0,a.jsx)(Qg,{front:e.front}),j.map(E=>{let N=Array.from({length:Math.ceil(E.pages.length/E.spreadColumns)},(B,he)=>E.pages.slice(he*E.spreadColumns,(he+1)*E.spreadColumns)),K=Math.max(1,...N.map(B=>B.reduce((he,ce)=>he+(ce.section.page_orientation==="landscape"?297:210),Math.max(0,B.length-1)*2)))*(96/25.4)*S,re=Math.min(1,Math.max(.25,(ze-24)/K)),W=E.pages.map(({section:B,figureOffset:he})=>(0,a.jsx)(lh,{section:B,progress:Se,onCycle:M,onSetStatus:R,figureOffset:he},B.id));return E.spreadGroup?(0,a.jsx)("section",{className:"sheet-spread","data-spread-group":E.spreadGroup,"aria-label":E.spreadLabel||"Wielostronicowy diagram UML",style:{"--spread-columns":E.spreadColumns,"--spread-fit-scale":re},children:W},E.key):W[0]}),e.dictionary&&(0,a.jsxs)(bs,{header:e.dictionary.header_html,footer:e.dictionary.footer_html,className:"dictionary-sheet",children:[(0,a.jsx)("h2",{children:"S\u0142ownik WE/EN/EK/KW"}),(0,a.jsx)(Ft,{html:e.dictionary.content_html})]})]}),(0,a.jsx)(bp,{side:"left",panels:[...e.library?[{id:"library",railLabel:"K",title:"Serie i karty",accent:"#167ca4",content:(0,a.jsx)(uh,{library:e.library,source:r,onNavigate:()=>Dt(!1)})}]:[],{id:"contents",railLabel:"S",title:"Spis tre\u015Bci",accent:"#16805d",content:(0,a.jsx)(ch,{toc:e.toc,onNavigate:()=>{ge(null),se(null)}})}],activeId:Me,pinnedId:yr,onActiveChange:ge,onPinnedChange:se}),(0,a.jsx)(bp,{side:"right",panels:[{id:"classroom",railLabel:"U",title:"Klasa i stanowiska",accent:"#267357",content:(0,a.jsx)(gh,{model:e})},{id:"progress",railLabel:"R",title:"Przebieg i raport",accent:"#b45b26",content:(0,a.jsx)(dh,{progress:Se,error:at})}],activeId:Zt,pinnedId:T,onActiveChange:ue,onPinnedChange:D}),(0,a.jsx)(hh,{open:G,onClose:()=>te(!1)})]})}async function vh(){let e=(0,Sp.createRoot)(document.getElementById("root"));try{let t=await fetch("./card-data.json",{cache:"no-store"});if(!t.ok)throw new Error(`Nie mo\u017Cna wczyta\u0107 card-data.json: ${t.status}`);let n=await t.json();e.render((0,a.jsx)(yh,{model:n}))}catch(t){e.render((0,a.jsxs)("main",{className:"app-load-error",children:[(0,a.jsx)("h1",{children:"Nie uda\u0142o si\u0119 uruchomi\u0107 karty"}),(0,a.jsx)("pre",{children:t instanceof Error?t.message:String(t)})]}))}}vh(); +/*! Bundled license information: + +react/cjs/react.production.min.js: + (** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +scheduler/cjs/scheduler.production.min.js: + (** + * @license React + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +react-dom/cjs/react-dom.production.min.js: + (** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +react/cjs/react-jsx-runtime.production.min.js: + (** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) +*/ diff --git a/web/card-data.json b/web/card-data.json new file mode 100644 index 0000000..50d763f --- /dev/null +++ b/web/card-data.json @@ -0,0 +1,2738 @@ +{ + "schema": "esc-card-react-view.v1", + "card": { + "id": "mpabi-inf-console-bash-08-docker", + "series": "console-bash", + "series_title": "Bash · Console Tools", + "number": "08", + "count": "08", + "slug": "docker", + "title": "L08 · Docker Fundamentals — image, container and storage", + "topic": "Obraz, cykl życia kontenera, volume i bind mount", + "project": "Warsztat terminalowy", + "subject": "Informatyka", + "level": "Rok 1 · Console", + "revision_date": "2026-07-21T00:00:00+02:00", + "status": "Gotowa do wykonania", + "version": "v00.01", + "uuid": "b6d3b00d-6912-502d-95d7-3ee47c60f1db", + "author": "M. Pabiszczak", + "year": "2026" + }, + "library": { + "schema": "stem-card-library.v2", + "workspace": "edu", + "subjects": [ + { + "id": "inf", + "title": "INF · Informatyka", + "current": true, + "levels": [ + { + "id": "year-1", + "title": "Rok 1", + "current": true, + "series": [ + { + "id": "console-bash", + "title": "Bash · Console Tools", + "current": true, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "L01", + "title": "BusyBox — the Swiss Army Knife", + "repo": "lab-console-busybox", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "01", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L02", + "title": "Tmux — sessions, windows and panes", + "repo": "lab-console-tmux", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "02", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L03", + "title": "Neovim — navigation and editing", + "repo": "lab-console-neovim", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L04", + "title": "Git Basics I — status, add and commit", + "repo": "lab-console-git-basics-1", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L05", + "title": "Git Basics II — log, diff and restore", + "repo": "lab-console-git-basics-2", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L06", + "title": "Git Branching I — branch, switch and merge", + "repo": "lab-console-git-branching-1", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L07", + "title": "Git Branching II — rebase and conflicts", + "repo": "lab-console-git-branching-2", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L08", + "title": "Docker Fundamentals", + "repo": "lab-console-docker", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": true, + "number": "08", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + } + ] + }, + { + "id": "rv32i-asm", + "title": "ASM · RV32I From Blinker to RISC-V", + "current": false, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "L01", + "title": "Number Systems and Storage", + "repo": "lab-rv32i-asm-number-systems", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "01", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L02", + "title": "FPGA Bring-up and Programming", + "repo": "lab-rv32i-asm-fpga-bringup", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "02", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L03", + "title": "Blinker and Synchronous Logic", + "repo": "lab-rv32i-asm-blinker", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L04", + "title": "The RISC-V ISA and Instruction Decoder", + "repo": "lab-rv32i-asm-decoder", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L05", + "title": "ALU and the Verilog Assembler", + "repo": "lab-rv32i-asm-alu", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L06", + "title": "Control Flow — Jumps and Branches", + "repo": "lab-rv32i-asm-control-flow", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L07", + "title": "Addresses and Memory", + "repo": "lab-rv32i-asm-addresses-memory", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L08", + "title": "Subroutines and ABI", + "repo": "lab-rv32i-asm-subroutines-abi", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "08", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + }, + { + "id": "L09", + "title": "Load and Store Semantics", + "repo": "lab-rv32i-asm-load-store", + "href": "http://localhost:8080", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "09", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + } + ] + } + ] + }, + { + "id": "c", + "title": "C · Freestanding RV32I and K&R", + "current": false, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "C01", + "title": "ASM, C and GCC", + "repo": "lab-rv32i-strlen-asm-c-gcc", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/852fa3c7-8329-583a-a244-b639d2e04495", + "local_href": "/852fa3c7-8329-583a-a244-b639d2e04495/", + "resource_uuid": "852fa3c7-8329-583a-a244-b639d2e04495", + "available": true, + "current": false, + "number": "01", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Ręczna implementacja RV32I ASM" + }, + { + "id": "task02", + "title": "Referencyjna implementacja w C" + }, + { + "id": "task03", + "title": "ASM wyemitowany przez GCC" + } + ] + }, + { + "id": "C02", + "title": ".bss, .data and static storage", + "repo": "lab-rv32i-strlen-bss-data-stack", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/74afa921-fb51-5017-bcdb-f6e84fb444d8", + "local_href": "/74afa921-fb51-5017-bcdb-f6e84fb444d8/", + "resource_uuid": "74afa921-fb51-5017-bcdb-f6e84fb444d8", + "available": true, + "current": false, + "number": "02", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Wynik strlen w .bss" + }, + { + "id": "task02", + "title": "Wynik i napis w .data" + }, + { + "id": "task03", + "title": "Nieużyta tablica lokalna" + }, + { + "id": "task04", + "title": "Użyta tablica lokalna i stack frame" + } + ] + }, + { + "id": "C03", + "title": "ABI and Stack Frame", + "repo": "lab-rv32i-strlen-abi-stack", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/72b22a0a-3297-5d75-a6ff-d1af48132d6c", + "local_href": "/72b22a0a-3297-5d75-a6ff-d1af48132d6c/", + "resource_uuid": "72b22a0a-3297-5d75-a6ff-d1af48132d6c", + "available": true, + "current": false, + "number": "03", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Argument i wynik bez powrotu z main" + }, + { + "id": "task02", + "title": "Pełny stack frame i zachowanie ra" + } + ] + }, + { + "id": "C04", + "title": "Types, Operators and Expressions", + "repo": "lab-rv32i-c-types-operators-expressions", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/7818aa68-4983-5aca-bf7f-541cfce5c601", + "local_href": "/7818aa68-4983-5aca-bf7f-541cfce5c601/", + "resource_uuid": "7818aa68-4983-5aca-bf7f-541cfce5c601", + "available": true, + "current": false, + "number": "04", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Nazwy i stałe symboliczne" + }, + { + "id": "task02", + "title": "Typy i zakresy" + }, + { + "id": "task03", + "title": "Stałe, długość i enum" + }, + { + "id": "task04", + "title": "Deklaracje, inicjalizacja i const" + }, + { + "id": "task05", + "title": "Arytmetyka i rok przestępny" + }, + { + "id": "task06", + "title": "Porównania i logika" + }, + { + "id": "task07", + "title": "Konwersje: atoi, lower i htoi" + }, + { + "id": "task08", + "title": "Inkrementacja i operacje na napisach" + }, + { + "id": "task09", + "title": "Operatory bitowe" + }, + { + "id": "task10", + "title": "Przypisania i bitcount" + }, + { + "id": "task11", + "title": "Operator warunkowy" + }, + { + "id": "task12", + "title": "Priorytet i kolejność obliczeń" + } + ] + }, + { + "id": "C05", + "title": "Control Flow", + "repo": "lab-rv32i-c-control-flow", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/7512361d-e5e6-5c08-ab18-8be29dacdc10", + "local_href": "/7512361d-e5e6-5c08-ab18-8be29dacdc10/", + "resource_uuid": "7512361d-e5e6-5c08-ab18-8be29dacdc10", + "available": true, + "current": false, + "number": "05", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Instrukcje i bloki" + }, + { + "id": "task02", + "title": "if-else i dangling else" + }, + { + "id": "task03", + "title": "else-if i wyszukiwanie binarne" + }, + { + "id": "task04", + "title": "switch: escape i unescape" + }, + { + "id": "task05", + "title": "while, for i Shell sort" + }, + { + "id": "task06", + "title": "do-while i itoa" + }, + { + "id": "task07", + "title": "break i continue" + }, + { + "id": "task08", + "title": "goto i etykiety" + } + ] + }, + { + "id": "C06", + "title": "Functions and Program Structure", + "repo": "lab-rv32i-c-functions-program-structure", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/9667818d-b691-501d-817c-b58be4cbd594", + "local_href": "/9667818d-b691-501d-817c-b58be4cbd594/", + "resource_uuid": "9667818d-b691-501d-817c-b58be4cbd594", + "available": true, + "current": false, + "number": "06", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Funkcje i strindex" + }, + { + "id": "task02", + "title": "double i atof z wykładnikiem" + }, + { + "id": "task03", + "title": "Zewnętrzny stos RPN" + }, + { + "id": "task04", + "title": "Scope i extern" + }, + { + "id": "task05", + "title": "Nagłówki i prototypy" + }, + { + "id": "task06", + "title": "Static storage" + }, + { + "id": "task07", + "title": "Zmienne register" + }, + { + "id": "task08", + "title": "Struktura blokowa" + }, + { + "id": "task09", + "title": "Inicjalizacja" + }, + { + "id": "task10", + "title": "Rekurencja i quicksort" + }, + { + "id": "task11", + "title": "Preprocesor i makra" + } + ] + }, + { + "id": "C07", + "title": "Pointers, Arrays and Linear Allocator", + "repo": "lab-rv32i-c-pointers-arrays", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/98dbe323-c624-53d6-9fe2-4ee688c44b38", + "local_href": "/98dbe323-c624-53d6-9fe2-4ee688c44b38/", + "resource_uuid": "98dbe323-c624-53d6-9fe2-4ee688c44b38", + "available": true, + "current": false, + "number": "07", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "task01" + }, + { + "id": "task02", + "title": "task02" + }, + { + "id": "task03", + "title": "task03" + }, + { + "id": "task04", + "title": "K&R 5.4 -- Linear Memory Allocator" + }, + { + "id": "task05", + "title": "task05" + }, + { + "id": "task06", + "title": "task06" + }, + { + "id": "task07", + "title": "task07" + }, + { + "id": "task08", + "title": "task08" + }, + { + "id": "task09", + "title": "task09" + }, + { + "id": "task10", + "title": "task10" + }, + { + "id": "task11", + "title": "task11" + }, + { + "id": "task12", + "title": "task12" + } + ] + }, + { + "id": "C08", + "title": "Structures and Linked State", + "repo": "lab-rv32i-c-structures", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/f60fbba3-a788-5f1e-bc24-52af82f227c9", + "local_href": "/f60fbba3-a788-5f1e-bc24-52af82f227c9/", + "resource_uuid": "f60fbba3-a788-5f1e-bc24-52af82f227c9", + "available": true, + "current": false, + "number": "08", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Układ BlockHeader" + }, + { + "id": "task02", + "title": "Nagłówek, payload i odzyskanie właściciela" + }, + { + "id": "task03", + "title": "Intruzywna lista wolnych bloków" + }, + { + "id": "task04", + "title": "Wyrównana arena dwóch bloków" + } + ] + }, + { + "id": "C09", + "title": "smalloc, sbrk and minimal libc", + "repo": "lab-rv32i-c-smalloc-sbrk-libc", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/8e4f8bee-41f0-523b-8ea2-8926bd3d7ab0", + "local_href": "/8e4f8bee-41f0-523b-8ea2-8926bd3d7ab0/", + "resource_uuid": "8e4f8bee-41f0-523b-8ea2-8926bd3d7ab0", + "available": true, + "current": false, + "number": "09", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "strlen z libc bez sterty" + }, + { + "id": "task02", + "title": "Kontrolowany brak dostawcy sbrk" + }, + { + "id": "task03", + "title": "Bounded sbrk i atomowy błąd" + }, + { + "id": "task04", + "title": "Wyrównany smalloc append-only" + }, + { + "id": "task05", + "title": "malloc/free picolibc jako czarna skrzynka" + } + ] + }, + { + "id": "C10", + "title": "RV32I Traps and Interrupt Control", + "repo": "lab-rv32i-c-interrupts", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/8fb9c110-557b-5081-8ae2-a56800f872e8", + "local_href": "/8fb9c110-557b-5081-8ae2-a56800f872e8/", + "resource_uuid": "8fb9c110-557b-5081-8ae2-a56800f872e8", + "available": true, + "current": false, + "number": "10", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "ECALL i wznowienie pod mepc+4" + }, + { + "id": "task02", + "title": "Machine software IRQ i clear źródła" + }, + { + "id": "task03", + "title": "Macierz pending, MSIE i MIE" + } + ] + }, + { + "id": "C11", + "title": "Machine Timer and Periodic Work", + "repo": "lab-rv32i-c-timer", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "11", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "C12", + "title": "UART — polling and interrupts", + "repo": "lab-rv32i-c-uart", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/64f7c516-78b1-58ee-8619-a82d7061079f", + "local_href": "/64f7c516-78b1-58ee-8619-a82d7061079f/", + "resource_uuid": "64f7c516-78b1-58ee-8619-a82d7061079f", + "available": true, + "current": false, + "number": "12", + "version": "v00.01", + "status": "planned", + "tasks": [ + { + "id": "task01", + "title": "Polling jednego bajtu" + }, + { + "id": "task02", + "title": "Odbiór R przez external IRQ 16" + }, + { + "id": "task03", + "title": "Jednokomórkowa skrzynka drop-newest" + } + ] + }, + { + "id": "C13", + "title": "GPIO, Edges and Debounce", + "repo": "lab-rv32i-c-gpio", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "13", + "version": "", + "status": "planned", + "tasks": [] + } + ] + } + ] + }, + { + "id": "year-2", + "title": "Rok 2", + "current": false, + "series": [ + { + "id": "cpp", + "title": "C++", + "current": false, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "CPP01", + "title": "Translation Units, Linkage and Namespaces", + "repo": "lab-cpp-build-namespaces", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "01", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP02", + "title": "Classes, Encapsulation and Invariants", + "repo": "lab-cpp-classes-invariants", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "02", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP03", + "title": "Construction, Destruction and RAII", + "repo": "lab-cpp-lifetime-raii", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP04", + "title": "References, const and Value Categories", + "repo": "lab-cpp-references-const", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP05", + "title": "Copy, Move and the Rule of Zero/Five", + "repo": "lab-cpp-copy-move", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP06", + "title": "Ownership and Resource Types", + "repo": "lab-cpp-ownership", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP07", + "title": "Function and Class Templates", + "repo": "lab-cpp-templates", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP08", + "title": "Static Polymorphism and CRTP", + "repo": "lab-cpp-static-polymorphism", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "08", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP09", + "title": "Inheritance, Interfaces and Dynamic Dispatch", + "repo": "lab-cpp-dynamic-polymorphism", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "09", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP10", + "title": "Operator Overloading and Strong Types", + "repo": "lab-cpp-strong-types", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "10", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP11", + "title": "Containers, Iterators and Algorithms", + "repo": "lab-cpp-containers-iterators", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "11", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP12", + "title": "Lambdas, Function Objects and Callables", + "repo": "lab-cpp-lambdas-callables", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "12", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP13", + "title": "Error Models with and without Exceptions", + "repo": "lab-cpp-error-models", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "13", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP14", + "title": "Object Layout, ABI and Binary Evidence", + "repo": "lab-cpp-abi-evidence", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "14", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "CPP15", + "title": "Resource-type Integration Project", + "repo": "lab-cpp-resource-integration", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "15", + "version": "", + "status": "planned", + "tasks": [] + } + ] + }, + { + "id": "freertos-c", + "title": "FreeRTOS C", + "current": false, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "FC01", + "title": "heap_4: From Linear Cursor to Reusable Blocks", + "repo": "lab-rv32i-freertos-heap4", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/aea09ab7-9bfd-5368-8d1b-c8724871c1a7", + "local_href": "/aea09ab7-9bfd-5368-8d1b-c8724871c1a7/", + "resource_uuid": "aea09ab7-9bfd-5368-8d1b-c8724871c1a7", + "available": true, + "current": false, + "number": "01", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Predict and prove reusable heap_4 blocks" + } + ] + }, + { + "id": "FC02", + "title": "First Task, TCB and Task Stack", + "repo": "lab-rv32i-freertos-c-first-task", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/a2538a1a-3c37-4c14-a6ea-db5aba8fd038", + "local_href": "/a2538a1a-3c37-4c14-a6ea-db5aba8fd038/", + "resource_uuid": "a2538a1a-3c37-4c14-a6ea-db5aba8fd038", + "available": true, + "current": false, + "number": "02", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Pierwszy task FreeRTOS — callback, context, TCB i stos" + } + ] + }, + { + "id": "FC03", + "title": "Tick, Scheduler, Priorities and Time Slicing", + "repo": "lab-rv32i-freertos-c-scheduler", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/2214a13f-34c6-40a1-a561-70a00ec98285", + "local_href": "/2214a13f-34c6-40a1-a561-70a00ec98285/", + "resource_uuid": "2214a13f-34c6-40a1-a561-70a00ec98285", + "available": true, + "current": false, + "number": "03", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Kontrolowany eksperyment schedulera FreeRTOS" + } + ] + }, + { + "id": "FC04", + "title": "Delay, Blocking and Task States", + "repo": "lab-rv32i-freertos-c-delay-state", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/4a7ef04c-ab08-5783-9192-5b02a90f05f8", + "local_href": "/4a7ef04c-ab08-5783-9192-5b02a90f05f8/", + "resource_uuid": "4a7ef04c-ab08-5783-9192-5b02a90f05f8", + "available": true, + "current": false, + "number": "04", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Delay, blokowanie, zawieszenie i self-delete" + } + ] + }, + { + "id": "FC05", + "title": "Queues and Byte-copy Semantics", + "repo": "lab-rv32i-freertos-c-queue", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/8ee8f36a-471b-51bc-97b8-e204bd541c61", + "local_href": "/8ee8f36a-471b-51bc-97b8-e204bd541c61/", + "resource_uuid": "8ee8f36a-471b-51bc-97b8-e204bd541c61", + "available": true, + "current": false, + "number": "05", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Kolejka długości 1: empty, full, copy i FIFO" + } + ] + }, + { + "id": "FC06", + "title": "Software Timers and the Daemon Task", + "repo": "lab-rv32i-freertos-c-software-timers", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/ab188300-f03d-5ddb-8316-b9534876a5bc", + "local_href": "/ab188300-f03d-5ddb-8316-b9534876a5bc/", + "resource_uuid": "ab188300-f03d-5ddb-8316-b9534876a5bc", + "available": true, + "current": false, + "number": "06", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "One-shot, auto-reload i timer daemon" + } + ] + }, + { + "id": "FC07", + "title": "Interrupt-safe API and Semaphore Handoff", + "repo": "lab-rv32i-freertos-c-isr-semaphore", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/a6da1075-8d1f-5615-b0e6-0fc33e59e1e3", + "local_href": "/a6da1075-8d1f-5615-b0e6-0fc33e59e1e3/", + "resource_uuid": "a6da1075-8d1f-5615-b0e6-0fc33e59e1e3", + "available": true, + "current": false, + "number": "07", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "External IRQ → FromISR semaphore → high-priority task" + } + ] + }, + { + "id": "FC08", + "title": "Critical Sections, Mutexes and Priority Inheritance", + "repo": "lab-rv32i-freertos-c-resource-management", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/2392f81a-ed6c-510b-86b5-06c5e2f1c6aa", + "local_href": "/2392f81a-ed6c-510b-86b5-06c5e2f1c6aa/", + "resource_uuid": "2392f81a-ed6c-510b-86b5-06c5e2f1c6aa", + "available": true, + "current": false, + "number": "08", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Low/Medium/High: ownership i dziedziczenie priorytetu" + } + ] + }, + { + "id": "FC09", + "title": "Event Groups and Multi-event Synchronization", + "repo": "lab-rv32i-freertos-c-event-groups", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/3f4c1b8b-d54b-58d5-b92d-87a7233cbd9f", + "local_href": "/3f4c1b8b-d54b-58d5-b92d-87a7233cbd9f/", + "resource_uuid": "3f4c1b8b-d54b-58d5-b92d-87a7233cbd9f", + "available": true, + "current": false, + "number": "09", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "ALL-ready i trzyosobowa bariera" + } + ] + }, + { + "id": "FC10", + "title": "Direct-to-task Notifications", + "repo": "lab-rv32i-freertos-c-notifications", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/3b57334e-2c35-5207-a51d-9321906db078", + "local_href": "/3b57334e-2c35-5207-a51d-9321906db078/", + "resource_uuid": "3b57334e-2c35-5207-a51d-9321906db078", + "available": true, + "current": false, + "number": "10", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Count, overwrite i bit flags na jednym slocie" + } + ] + }, + { + "id": "FC11", + "title": "Static Allocation and Bounded Storage", + "repo": "lab-rv32i-freertos-c-static-allocation", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/f4d7caf3-d665-5a2d-b9eb-39a120a64ff7", + "local_href": "/f4d7caf3-d665-5a2d-b9eb-39a120a64ff7/", + "resource_uuid": "f4d7caf3-d665-5a2d-b9eb-39a120a64ff7", + "available": true, + "current": false, + "number": "11", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Same behavior, two storage policies" + } + ] + }, + { + "id": "FC12", + "title": "Assertions, Hooks, Stack Checks and Runtime Diagnostics", + "repo": "lab-rv32i-freertos-c-diagnostics", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/c3027314-85a4-5e4a-af13-0939c7467a62", + "local_href": "/c3027314-85a4-5e4a-af13-0939c7467a62/", + "resource_uuid": "c3027314-85a4-5e4a-af13-0939c7467a62", + "available": true, + "current": false, + "number": "12", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Diagnose before changing the program" + } + ] + }, + { + "id": "FC13", + "title": "UART Interrupt-to-task Pipeline", + "repo": "lab-rv32i-freertos-c-uart", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/6a838fd1-ab1b-52b6-baaf-659120a4d60a", + "local_href": "/6a838fd1-ab1b-52b6-baaf-659120a4d60a/", + "resource_uuid": "6a838fd1-ab1b-52b6-baaf-659120a4d60a", + "available": true, + "current": false, + "number": "13", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Build and prove a bounded UART RX pipeline" + } + ] + }, + { + "id": "FC14", + "title": "GPIO, Hardware Timer and Deferred Work", + "repo": "lab-rv32i-freertos-c-gpio-timer", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/0cc561df-bf81-5bba-9df1-c872d5b42eb8", + "local_href": "/0cc561df-bf81-5bba-9df1-c872d5b42eb8/", + "resource_uuid": "0cc561df-bf81-5bba-9df1-c872d5b42eb8", + "available": true, + "current": false, + "number": "14", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Prove one machine-timer event from ISR to daemon GPIO work" + } + ] + }, + { + "id": "FC15", + "title": "Integrated Deterministic FreeRTOS C Application", + "repo": "lab-rv32i-freertos-c-integration", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/fe92efeb-3c38-5975-86d8-ed630e1ad3bb", + "local_href": "/fe92efeb-3c38-5975-86d8-ed630e1ad3bb/", + "resource_uuid": "fe92efeb-3c38-5975-86d8-ed630e1ad3bb", + "available": true, + "current": false, + "number": "15", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Prove one complete FreeRTOS C architecture" + } + ] + } + ] + }, + { + "id": "freertos-cpp", + "title": "FreeRTOS C++", + "current": false, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "K02", + "title": "C++ Task Wrapper, CRTP and Lifecycle", + "repo": "lab-rv32i-freertos-task-stack-vector", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/948ce9fd-3d0c-5721-a0ae-ef422008fec8", + "local_href": "/948ce9fd-3d0c-5721-a0ae-ef422008fec8/", + "resource_uuid": "948ce9fd-3d0c-5721-a0ae-ef422008fec8", + "available": true, + "current": false, + "number": "02", + "version": "v00.04", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "C++ Task wrapper — OOP i debugowanie cyklu życia" + } + ] + }, + { + "id": "K03", + "title": "Vector V1 — Destructor, Move, Ownership and RAII", + "repo": "lab-rv32i-freertos-vector-raii", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/de1a2818-4c30-5760-8b31-255114f82e90", + "local_href": "/de1a2818-4c30-5760-8b31-255114f82e90/", + "resource_uuid": "de1a2818-4c30-5760-8b31-255114f82e90", + "available": true, + "current": false, + "number": "03", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Vector V1 — transfer własności i powrót heapu" + } + ] + }, + { + "id": "K04", + "title": "Scheduler States, Priorities and Typed Ticks", + "repo": "lab-rv32i-freertos-scheduler-states", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/853a7dd7-b00b-4f4a-96e8-c160fc5bb04a", + "local_href": "/853a7dd7-b00b-4f4a-96e8-c160fc5bb04a/", + "resource_uuid": "853a7dd7-b00b-4f4a-96e8-c160fc5bb04a", + "available": true, + "current": false, + "number": "04", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Osiem snapshotów decyzji schedulera" + } + ] + }, + { + "id": "K05", + "title": "C++ Heap Bridge, Heap Models and HeapStats", + "repo": "lab-rv32i-freertos-heap-models", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/c456b505-dc5c-4894-b832-bb4c5afd0feb", + "local_href": "/c456b505-dc5c-4894-b832-bb4c5afd0feb/", + "resource_uuid": "c456b505-dc5c-4894-b832-bb4c5afd0feb", + "available": true, + "current": false, + "number": "05", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Fragmentacja, koalescencja i historyczny watermark" + } + ] + }, + { + "id": "K06", + "title": "MemoryResource and FreeRtosAllocator", + "repo": "lab-rv32i-freertos-allocator-resource", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/1af48a2e-8564-4e18-8115-b2c0208169d6", + "local_href": "/1af48a2e-8564-4e18-8115-b2c0208169d6/", + "resource_uuid": "1af48a2e-8564-4e18-8115-b2c0208169d6", + "available": true, + "current": false, + "number": "06", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Dwa zasoby, jeden typed allocator" + } + ] + }, + { + "id": "K07", + "title": "TaskHandle_t, Explicit Start and Static Trampoline", + "repo": "lab-rv32i-freertos-task-wrapper", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/9bffead0-4341-4350-806d-e975b1a3a08f", + "local_href": "/9bffead0-4341-4350-806d-e975b1a3a08f/", + "resource_uuid": "9bffead0-4341-4350-806d-e975b1a3a08f", + "available": true, + "current": false, + "number": "07", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Stabilny obiekt przez statyczny trampoline" + } + ] + }, + { + "id": "K08", + "title": "DynamicTask, StaticTask, Deletion and Lifetime", + "repo": "lab-rv32i-freertos-static-task", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/d9e45417-a37f-487c-813f-fc7a44d4b4f3", + "local_href": "/d9e45417-a37f-487c-813f-fc7a44d4b4f3/", + "resource_uuid": "d9e45417-a37f-487c-813f-fc7a44d4b4f3", + "available": true, + "current": false, + "number": "08", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Dwie polityki storage i pełna oś delete" + } + ] + }, + { + "id": "K09", + "title": "Scheduler Facade and Scoped Kernel Guards", + "repo": "lab-rv32i-freertos-kernel-facade", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/6a2ab068-17e7-4bfa-b726-2df37787ed69", + "local_href": "/6a2ab068-17e7-4bfa-b726-2df37787ed69/", + "resource_uuid": "6a2ab068-17e7-4bfa-b726-2df37787ed69", + "available": true, + "current": false, + "number": "09", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Fasada kernela i restore na każdej ścieżce" + } + ] + }, + { + "id": "K10", + "title": "Queue and StaticQueue", + "repo": "lab-rv32i-freertos-queue", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/66967c2d-bb34-4344-8064-f54d06cce47a", + "local_href": "/66967c2d-bb34-4344-8064-f54d06cce47a/", + "resource_uuid": "66967c2d-bb34-4344-8064-f54d06cce47a", + "available": true, + "current": false, + "number": "10", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Typed byte-copy queue z dwiema politykami storage" + } + ] + }, + { + "id": "K11", + "title": "Mutex, LockGuard and Priority Inheritance", + "repo": "lab-rv32i-freertos-mutex", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/34d7ad61-6b03-4922-8e58-b8b98316e5f7", + "local_href": "/34d7ad61-6b03-4922-8e58-b8b98316e5f7/", + "resource_uuid": "34d7ad61-6b03-4922-8e58-b8b98316e5f7", + "available": true, + "current": false, + "number": "11", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "LockGuard i zmierzone priority inheritance" + } + ] + }, + { + "id": "K12", + "title": "Semaphores and Task Notifications", + "repo": "lab-rv32i-freertos-semaphore-notify", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/fcb6107c-268f-4e86-a8cc-092e5875f6f0", + "local_href": "/fcb6107c-268f-4e86-a8cc-092e5875f6f0/", + "resource_uuid": "fcb6107c-268f-4e86-a8cc-092e5875f6f0", + "available": true, + "current": false, + "number": "12", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Semaphore slots i jednoodbiorcza notification" + } + ] + }, + { + "id": "K13", + "title": "EventGroup and Coordinated State", + "repo": "lab-rv32i-freertos-event-group", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/5fbcd7c6-3a73-4cd2-b76c-bdb016d1daaa", + "local_href": "/5fbcd7c6-3a73-4cd2-b76c-bdb016d1daaa/", + "resource_uuid": "5fbcd7c6-3a73-4cd2-b76c-bdb016d1daaa", + "available": true, + "current": false, + "number": "13", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Typed EventGroup i trzy źródła stanu" + } + ] + }, + { + "id": "K14", + "title": "Software Timer and the Timer Daemon Task", + "repo": "lab-rv32i-freertos-sw-timer", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/78db37df-63e5-4c5b-80ce-f73a411f0b50", + "local_href": "/78db37df-63e5-4c5b-80ce-f73a411f0b50/", + "resource_uuid": "78db37df-63e5-4c5b-80ce-f73a411f0b50", + "available": true, + "current": false, + "number": "14", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "Static one-shot i periodic timer na daemon tasku" + } + ] + }, + { + "id": "K15", + "title": "Explicit FromISR, UART and GPIO Wrappers", + "repo": "lab-rv32i-freertos-isr-drivers", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/02f6cd71-5c85-4883-a237-1d4ba08d9469", + "local_href": "/02f6cd71-5c85-4883-a237-1d4ba08d9469/", + "resource_uuid": "02f6cd71-5c85-4883-a237-1d4ba08d9469", + "available": true, + "current": false, + "number": "15", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "UART queue i GPIO notify z realnego IRQ" + } + ] + }, + { + "id": "K16", + "title": "Integrated Real-time Application and Evidence", + "repo": "lab-rv32i-freertos-integration", + "href": "https://dce7fb9d-7b2f-5d49-96a2-3a30d3070b84.mpabi.pl/05b4f486-a27c-455d-a315-f4e51a4c66b3", + "local_href": "/05b4f486-a27c-455d-a315-f4e51a4c66b3/", + "resource_uuid": "05b4f486-a27c-455d-a315-f4e51a4c66b3", + "available": true, + "current": false, + "number": "16", + "version": "v00.01", + "status": "existing", + "tasks": [ + { + "id": "task01", + "title": "End-to-end RTOS service i evidence report" + } + ] + } + ] + }, + { + "id": "posix-threads", + "title": "POSIX Threads", + "current": false, + "catalog_only": true, + "role": "", + "status": "", + "cards": [ + { + "id": "PT01", + "title": "Process and Thread Execution Context", + "repo": "lab-posix-process-thread", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "01", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT02", + "title": "pthread_create, pthread_join and Lifecycle", + "repo": "lab-posix-pthread-create-join", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "02", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT03", + "title": "Arguments, Return Values and Ownership", + "repo": "lab-posix-thread-arguments", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT04", + "title": "Data Races and the Happens-before Question", + "repo": "lab-posix-data-races", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT05", + "title": "Mutex, Critical Section and Deadlock", + "repo": "lab-posix-mutex", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT06", + "title": "Condition Variables and Predicates", + "repo": "lab-posix-condition-variable", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT07", + "title": "Bounded Producer–Consumer Queue", + "repo": "lab-posix-producer-consumer", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT08", + "title": "POSIX Semaphores", + "repo": "lab-posix-semaphore", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "08", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT09", + "title": "Reader–Writer Lock", + "repo": "lab-posix-rwlock", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "09", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT10", + "title": "C Atomics and Memory Ordering", + "repo": "lab-posix-atomics", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "10", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT11", + "title": "Cancellation, Cleanup and Safe Shutdown", + "repo": "lab-posix-cancellation-cleanup", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "11", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT12", + "title": "Thread-local Storage", + "repo": "lab-posix-thread-local", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "12", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT13", + "title": "Scheduling Policy, Priority and Affinity", + "repo": "lab-posix-scheduling", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "13", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT14", + "title": "GDB, Sanitizers and Race Evidence", + "repo": "lab-posix-thread-debugging", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "14", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "PT15", + "title": "POSIX Threads vs FreeRTOS Integration", + "repo": "lab-posix-freertos-comparison", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "15", + "version": "", + "status": "planned", + "tasks": [] + } + ] + }, + { + "id": "oop", + "title": "OOP and Design Patterns", + "current": false, + "catalog_only": true, + "role": "reference", + "status": "", + "cards": [ + { + "id": "OOP01", + "title": "Strategy — Replaceable Behaviour", + "repo": "lab-cpp-oop-strategy", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "01", + "version": "", + "status": "design", + "tasks": [] + }, + { + "id": "OOP02", + "title": "Adapter — Stable Boundary", + "repo": "lab-cpp-oop-adapter", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "02", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP03", + "title": "Template Method — Fixed Skeleton", + "repo": "lab-cpp-oop-template-method", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP04", + "title": "Static vs Dynamic Dispatch", + "repo": "lab-cpp-oop-dispatch", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP05", + "title": "RAII and Ownership", + "repo": "lab-cpp-oop-raii", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP06", + "title": "State — Explicit Behaviour", + "repo": "lab-cpp-oop-state", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP07", + "title": "Facade — Subsystem Boundary", + "repo": "lab-cpp-oop-facade", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP08", + "title": "Command — Request as Object", + "repo": "lab-cpp-oop-command", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "08", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP09", + "title": "Observer — Subscription and Telemetry", + "repo": "lab-cpp-oop-observer", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "09", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP10", + "title": "Factory Method — Resource Policy", + "repo": "lab-cpp-oop-factory-method", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "10", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP11", + "title": "Builder — Valid Configuration", + "repo": "lab-cpp-oop-builder", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "11", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP12", + "title": "Decorator — Measurement and Diagnostics", + "repo": "lab-cpp-oop-decorator", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "12", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP13", + "title": "Composite — Component Tree", + "repo": "lab-cpp-oop-composite", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "13", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP14", + "title": "Chain of Responsibility", + "repo": "lab-cpp-oop-chain", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "14", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP15", + "title": "Pattern-language Integration", + "repo": "lab-cpp-oop-integration", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "15", + "version": "", + "status": "planned", + "tasks": [] + } + ] + } + ] + }, + { + "id": "year-3", + "title": "Rok 3 · plan", + "current": false, + "series": [ + { + "id": "modern-cpp-stl", + "title": "Modern C++ / STL", + "current": false, + "catalog_only": true, + "role": "", + "status": "scope-reserved", + "cards": [] + }, + { + "id": "std-threads", + "title": "std::thread / std::jthread", + "current": false, + "catalog_only": true, + "role": "", + "status": "scope-reserved", + "cards": [] + }, + { + "id": "boost-asio", + "title": "Boost.Asio", + "current": false, + "catalog_only": true, + "role": "", + "status": "scope-reserved", + "cards": [] + }, + { + "id": "oop", + "title": "OOP and Design Patterns", + "current": false, + "catalog_only": true, + "role": "reference", + "status": "", + "cards": [ + { + "id": "OOP01", + "title": "Strategy — Replaceable Behaviour", + "repo": "lab-cpp-oop-strategy", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "01", + "version": "", + "status": "design", + "tasks": [] + }, + { + "id": "OOP02", + "title": "Adapter — Stable Boundary", + "repo": "lab-cpp-oop-adapter", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "02", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP03", + "title": "Template Method — Fixed Skeleton", + "repo": "lab-cpp-oop-template-method", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP04", + "title": "Static vs Dynamic Dispatch", + "repo": "lab-cpp-oop-dispatch", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP05", + "title": "RAII and Ownership", + "repo": "lab-cpp-oop-raii", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP06", + "title": "State — Explicit Behaviour", + "repo": "lab-cpp-oop-state", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP07", + "title": "Facade — Subsystem Boundary", + "repo": "lab-cpp-oop-facade", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP08", + "title": "Command — Request as Object", + "repo": "lab-cpp-oop-command", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "08", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP09", + "title": "Observer — Subscription and Telemetry", + "repo": "lab-cpp-oop-observer", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "09", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP10", + "title": "Factory Method — Resource Policy", + "repo": "lab-cpp-oop-factory-method", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "10", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP11", + "title": "Builder — Valid Configuration", + "repo": "lab-cpp-oop-builder", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "11", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP12", + "title": "Decorator — Measurement and Diagnostics", + "repo": "lab-cpp-oop-decorator", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "12", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP13", + "title": "Composite — Component Tree", + "repo": "lab-cpp-oop-composite", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "13", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP14", + "title": "Chain of Responsibility", + "repo": "lab-cpp-oop-chain", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "14", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "OOP15", + "title": "Pattern-language Integration", + "repo": "lab-cpp-oop-integration", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "15", + "version": "", + "status": "planned", + "tasks": [] + } + ] + } + ] + }, + { + "id": "year-4", + "title": "Rok 4 · plan", + "current": false, + "series": [ + { + "id": "transformers-gpu", + "title": "Transformers on GPU", + "current": false, + "catalog_only": true, + "role": "", + "status": "scope-reserved", + "cards": [] + } + ] + } + ] + }, + { + "id": "fiz", + "title": "FIZ · Fizyka", + "current": false, + "levels": [ + { + "id": "all", + "title": "Katalog", + "current": false, + "series": [ + { + "id": "fiz", + "title": "Fizyka", + "current": false, + "catalog_only": false, + "role": "", + "status": "", + "cards": [ + { + "id": "ruch-1d", + "title": "Ruch 1D", + "repo": "ruch-1d", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "", + "version": "", + "status": "", + "tasks": [] + }, + { + "id": "ruch-2d", + "title": "Ruch 2D", + "repo": "ruch-2d", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "", + "version": "", + "status": "", + "tasks": [] + }, + { + "id": "ruch-po-okregu", + "title": "Ruch po okręgu", + "repo": "ruch-po-okregu", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "", + "version": "", + "status": "", + "tasks": [] + } + ] + } + ] + } + ] + } + ] + }, + "total_pages": 5, + "front": { + "header_html": "
TITLEL08 · Docker Fundamentals — image, container and storage
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD08/08
SHEET1/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbCARD UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID b6d3b00d-6912-502d-95d7-3ee47c60f1dbM. PabiszczakSTRONA 1/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "goal_title": "Cel karty", + "goal_html": "

Uczeń rozróżnia image od container, przeprowadza cykl życia oraz wyjaśnia trwałość volume i bind mount. Test automatyczny działa jako jawny model offline i nie oznacza uruchomienia prawdziwego silnika.

", + "scope_title": "Zakres tasków", + "scope_html": "", + "scope_columns": [ + "chapter", + "task", + "idea", + "priority", + "status", + "version" + ], + "scope_headers": [ + "Krok", + "Numer tasku", + "Najważniejsza idea", + "Priorytet", + "Status", + "Version" + ], + "scope_rows": [ + { + "chapter": "8", + "task": "Task01", + "idea_html": "

Dockerfile, build, tag i digest

", + "priority": "obowiązkowe", + "status": "ready", + "status_label": "opracowane", + "version": "v00.01", + "key": false + }, + { + "chapter": "8", + "task": "Task02", + "idea_html": "

running, exited, removed

", + "priority": "obowiązkowe", + "status": "ready", + "status_label": "opracowane", + "version": "v00.01", + "key": false + }, + { + "chapter": "8", + "task": "Task03", + "idea_html": "

volume i bind mount

", + "priority": "obowiązkowe", + "status": "ready", + "status_label": "opracowane", + "version": "v00.01", + "key": false + } + ] + }, + "sections": [ + { + "id": "section-1-content", + "index": 1, + "title": "Task01 · Zbuduj specyfikację image", + "show_heading": true, + "toc_entry": true, + "header_html": "
TITLEL08 · Docker Fundamentals — image, container and storage
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD08/08
SHEET2/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbCARD UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID b6d3b00d-6912-502d-95d7-3ee47c60f1dbM. PabiszczakSTRONA 2/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "content_html": "

Przygotuj minimalny Dockerfile, sprawdź cztery instrukcje i wylicz stabilny digest kontekstu. Tag jest nazwą, digest identyfikatorem treści.

", + "tasks": [], + "assets": [ + { + "label": "fig:docker-model", + "logical_label": "fig:docker-model", + "figure_number": 1, + "caption": "Image jest specyfikacją; kontener ma własny cykl życia, a dane mogą żyć niezależnie.", + "alt": "Diagram Dockerfile, image, container, volume i bind mount.", + "href": "figures/docker-model.svg", + "source_href": "", + "width": 0.72, + "kind": "diagram", + "interactive": null, + "tile": null, + "diagram_title": "Image jest specyfikacją; kontener ma własny cykl życia, a dane mogą żyć niezależnie.", + "diagram_block_label": "", + "diagram_description": "", + "diagram_phase_labels": [] + } + ], + "steps": [ + { + "id": "T01-IMAGE", + "title": "Potwierdź składnię, 4 instrukcje, digest i offline-model." + } + ], + "spread_group": "", + "spread_label": "", + "spread_columns": 1, + "spread_index": 1, + "spread_count": 1, + "page_orientation": "portrait" + }, + { + "id": "section-2-content", + "index": 2, + "title": "Task02 · Cykl życia kontenera", + "show_heading": true, + "toc_entry": true, + "header_html": "
TITLEL08 · Docker Fundamentals — image, container and storage
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD08/08
SHEET3/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbCARD UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID b6d3b00d-6912-502d-95d7-3ee47c60f1dbM. PabiszczakSTRONA 3/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "content_html": "

Uruchom instancję image, odczytaj stan running, zatrzymaj ją do exited, a następnie usuń. Stop nie jest tym samym co rm.

", + "tasks": [], + "assets": [], + "steps": [ + { + "id": "T02-LIFECYCLE", + "title": "Zmierz running, exited i removed." + } + ], + "spread_group": "", + "spread_label": "", + "spread_columns": 1, + "spread_index": 1, + "spread_count": 1, + "page_orientation": "portrait" + }, + { + "id": "section-3-content", + "index": 3, + "title": "Task03 · Volume i bind mount", + "show_heading": true, + "toc_entry": true, + "header_html": "
TITLEL08 · Docker Fundamentals — image, container and storage
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD08/08
SHEET4/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbCARD UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID b6d3b00d-6912-502d-95d7-3ee47c60f1dbM. PabiszczakSTRONA 4/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "content_html": "

Zapisz dane w nazwanym volume i ścieżce bind. Po usunięciu kontenera oba zasoby pozostają; volume usuwa się osobno, a bind pozostaje własnością hosta.

", + "tasks": [], + "assets": [], + "steps": [ + { + "id": "T03-STORAGE", + "title": "Potwierdź trwałość i jawne usunięcie volume; zapisz trace.log." + } + ], + "spread_group": "", + "spread_label": "", + "spread_columns": 1, + "spread_index": 1, + "spread_count": 1, + "page_orientation": "portrait" + } + ], + "viewpoints": [], + "has_spread": false, + "has_allocator_model": false, + "dictionary": { + "header_html": "
TITLEL08 · Docker Fundamentals — image, container and storage
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD08/08
SHEET5/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbCARD UUIDb6d3b00d-6912-502d-95d7-3ee47c60f1dbAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID b6d3b00d-6912-502d-95d7-3ee47c60f1dbM. PabiszczakSTRONA 5/5
", + "content_html": "
WE 01 Image, container i storage

Bezpieczny model podstaw konteneryzacji.

OG

EN IP CB08.01 Uczeń analizuje niezmienną specyfikację image.
  • KW IP CB08.01 Wyjaśnia różnicę między tagiem i digestem.

TECH

EK INF04 CB08.01 Uczeń modeluje cykl życia i storage.
  • KW INF04 CB08.01 Potwierdza stany i niezależność danych.
" + }, + "toc": [ + { + "id": "section-1-content", + "label": "1. Task01 · Zbuduj specyfikację image" + }, + { + "id": "section-2-content", + "label": "2. Task02 · Cykl życia kontenera" + }, + { + "id": "section-3-content", + "label": "3. Task03 · Volume i bind mount" + } + ] +} diff --git a/web/figures/docker-model.svg b/web/figures/docker-model.svg new file mode 100644 index 0000000..a4ec188 --- /dev/null +++ b/web/figures/docker-model.svg @@ -0,0 +1,26 @@ + + Model obrazu, kontenera i danych + Dockerfile buduje niezmienny obraz. Obraz uruchamia kontener, który przechodzi przez stany running, exited i removed. Volume i bind mount żyją niezależnie od kontenera. + + + + + + + Dockerfilekontekst build + build + IMAGEtag + digest + run + CONTAINERrunning + stop + exited + rm + + VOLUMEzarządza engine + BIND MOUNTścieżka hosta + + Usunięcie konteneranie usuwa danych. + + diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..8f9f9f5 --- /dev/null +++ b/web/index.html @@ -0,0 +1,15 @@ + + + + + + + Karta pracy + + + + +
+ + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..5204bcd --- /dev/null +++ b/web/style.css @@ -0,0 +1,42 @@ + +:root{--ink:#111;--muted:#666;--line:#b9b9b9;--line-soft:#dddddd;--paper:#fff;--desk:#eeeeee;--og:#008000;--tech:#d36b00;--kw:#0018c8;--we:#e00000;--viewer-canvas-scale:1;--viewer-content-scale:1} +*{box-sizing:border-box} html{scroll-behavior:smooth} body{margin:0;background:var(--desk);color:var(--ink);font-family:"Latin Modern Roman","LM Roman 12",Georgia,"Times New Roman",serif;line-height:1.25} +.topbar{position:sticky;top:0;z-index:20;background:rgba(245,245,245,.94);border-bottom:1px solid #d0d0d0;padding:3px 0;font-family:system-ui,-apple-system,Segoe UI,sans-serif} +.topbar-inner{width:calc(100% - 8px);max-width:none;margin:0 4px;display:flex;align-items:center;justify-content:space-between;gap:6px}.topbar details{min-width:0;flex:1}.topbar summary{cursor:pointer;font-size:16px;line-height:1.35;font-weight:650}.toc-links{display:flex;flex-wrap:wrap;gap:6px;margin-top:5px}.toc-links a{font-size:16px;color:#333;text-decoration:none;border:1px solid #ccc;background:white;border-radius:3px;padding:2px 5px}.viewer-zoom-controls{display:flex;align-items:center;gap:7px;flex:none}.viewer-zoom-status{font:600 16px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:#444;white-space:nowrap}.viewer-zoom-reset{border:1px solid #bbb;border-radius:3px;background:#fff;color:#333;padding:1px 6px;font:600 16px/1.35 system-ui,-apple-system,Segoe UI,sans-serif;cursor:pointer}.viewer-zoom-reset:hover{background:#eee}.viewer-zoom-reset:focus-visible{outline:2px solid #555;outline-offset:1px} +.paper{padding:18px 12px 44px;overflow-x:auto}.sheet{position:relative;width:210mm;min-height:297mm;margin:0 auto 18px;background:var(--paper);box-shadow:0 2px 12px rgba(0,0,0,.13);display:block;padding:0;overflow:visible;zoom:var(--viewer-canvas-scale)}.sheet-content{position:relative;width:100%;min-height:297mm;padding:13mm 21.5mm 15mm;transform:scale(var(--sheet-effective-scale,1));transform-origin:top left}.resource-header-enabled .sheet-content{padding-top:5mm}.sheet-content>.page-resource-header{margin-bottom:3mm} +@media(max-width:900px){.sheet{height:auto}}@media print{.sheet{height:auto}} +.pdf-main{min-width:0;width:100%}.running-header{display:flex;justify-content:space-between;gap:18px;align-items:flex-end;border-bottom:1px solid #111;padding-bottom:3px;margin-bottom:26px;font-size:15px}.running-header span{white-space:nowrap} +.hero h1{font-size:24px;line-height:1.15;margin:0 0 4px;border-bottom:1px solid #111;padding-bottom:4px}.hero .byline{float:right;margin-top:-31px;font-size:15px} +.meta{clear:both;display:grid;grid-template-columns:148px minmax(0,1fr);gap:4px 18px;padding:34px 0 24px;border-bottom:1px solid #111;font-size:17px}.meta dt{color:#333}.meta dd{margin:0;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace} +.front-scope{padding:0 0 3mm;border-bottom:1px solid var(--line-soft)}.front-scope h2{font-size:24px;line-height:1.15;margin:0 0 3mm}.front-scope p{font-size:16px} +.scope-table-wrap{margin:3mm 0;overflow:hidden}.scope-table{width:100%;border-collapse:collapse;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.22}.scope-table th,.scope-table td{border:0;border-bottom:1px solid #888;padding:4px 7px;text-align:left;vertical-align:top}.scope-table th{border-bottom:2px solid #555;background:#f3f3f3;font-weight:700}.scope-table th:first-child,.scope-table td:first-child{width:7%}.scope-table--task th:nth-child(2),.scope-table--task td:nth-child(2){width:11%}.scope-table--task th:nth-child(4),.scope-table--task td:nth-child(4){width:19%}.scope-table--task th:nth-child(5),.scope-table--task td:nth-child(5){width:14%}.scope-table--task th:nth-child(6),.scope-table--task td:nth-child(6){width:10%}.scope-table tr.scope-row--key td{font-weight:700;background:#f4f4f4;border-top:2px solid #111;border-bottom:2px solid #111}.scope-table tr.scope-row--key td:first-child{border-left:2px solid #111}.scope-table tr.scope-row--key td:last-child{border-right:2px solid #111}.scope-table td p{font:inherit;margin:0}.scope-table code{font-size:inherit}.scope-status{display:inline-block;white-space:nowrap;border:1px solid #aaa;border-radius:2px;padding:1px 4px;font-weight:600;background:#f5f5f5}.scope-status[data-status="in-progress"]{border-color:#307da1;background:#eaf5fa;color:#174f69}.scope-status[data-status="ready"]{border-color:#4c7d56;background:#edf6ee;color:#295b33}.scope-status[data-status="draft"],.scope-status[data-status="review"]{border-color:#9a7932;background:#faf4e5;color:#694f16}.scope-status[data-status="blocked"]{border-color:#9c4b4b;background:#faeded;color:#712f2f} +.card-section h2{font-size:24px;line-height:1.15;margin:0 0 4mm}.section-heading{display:flex;align-items:flex-start;gap:0}.section-heading .section-index{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:23px;margin-right:12mm;font-weight:400}.section-heading .section-title{min-width:0}.task-identity{margin-left:auto;padding-left:22px;text-align:right;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:10px;line-height:1.25;color:#555;white-space:nowrap}.task-identity strong,.task-identity-name,.task-identity code{display:block}.task-identity strong{font-size:12px;color:#111}.task-identity-name{max-width:240px;overflow:hidden;text-overflow:ellipsis}.task-identity code{font-size:9px;color:#777} +p{font-size:16px;margin:0 0 3mm}.pdf-main ul{font-size:16px;margin:2mm 0 3mm 7mm;padding:0}.pdf-main li{margin:1.2mm 0} +.pdf-margin{position:absolute;top:34mm;width:18mm;font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:5px;line-height:1.12;color:#333;max-height:245mm;overflow:hidden}.pdf-margin.left{left:2mm;text-align:left}.pdf-margin.right{right:2mm;text-align:left} +.margin-title{font-weight:700;color:#666;margin-bottom:3px;letter-spacing:.02em}.margin-list{display:flex;flex-direction:column;align-items:flex-start}.margin-tag{display:block;margin:0 0 1px;padding:0;border:0;background:transparent;font:inherit;line-height:inherit;text-align:left;cursor:pointer;margin-left:var(--indent)}.margin-tag.we{color:var(--we)}.margin-tag.og,.margin-tag.tech{color:var(--og)}.margin-tag.kw{color:var(--kw)}.margin-tag .local{color:#777}.margin-tag:hover{text-decoration:underline}.margin-empty{color:#aaa} +.step-map{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px;line-height:1.3;color:#5c5c5c;background:transparent;border:0;border-top:1px solid #cfcfcf;border-bottom:1px solid #e0e0e0;border-radius:0;padding:8px 0;margin:0 0 16px}.step-map summary{cursor:pointer;font-weight:400;color:#555} +.tree-legend{display:flex;flex-wrap:wrap;gap:4px;margin:7px 0}.steps{display:grid;gap:6px}.step-row{border-top:1px dashed #c9c9c9;padding-top:6px}.step-row:first-child{border-top:0}.step-title span{font-weight:700;margin-right:7px}.step-trees{display:flex;flex-wrap:wrap;gap:4px;margin-top:3px}.step-refs{display:flex;flex-wrap:wrap;gap:6px;margin-top:3px}.step-refs a{font-size:10px;color:#666} +.tree-token{font:10px/1 "Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;border:1px solid #c8c8c8;background:#fff;border-radius:3px;padding:3px 5px;cursor:pointer}.tree-token.active{background:#111;color:white;border-color:#111} +.equation{display:grid;grid-template-columns:1fr auto;align-items:center;gap:14px;margin:18px 0;padding:4px 0;background:transparent;border:0}.equation .display{font-size:17px}.equation-number{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace;font-size:15px} +figure{margin:24px 0;text-align:center}figure img{max-width:100%;display:block;margin:0 auto;border:0;border-radius:0}figcaption{font-size:14px;color:#444;margin-top:8px;text-align:left}.card-asset.asset-screenshot img{border:1px solid #aaa}.card-asset a{display:block;cursor:zoom-in} +.table-wrap{overflow:auto;margin:14px 0}table{border-collapse:collapse;width:100%;font-size:16px}th,td{border:1px solid #111;padding:5px 8px;text-align:left;vertical-align:top}th{font-weight:400;background:white} +.empty-check{display:inline-block;width:1em;height:1em;border:1px solid #111;vertical-align:-.12em}.answer-line,.dotfill{display:block;border-bottom:1px dotted #111;height:1.45em;min-width:10em}.write-row{display:block;min-height:2.8em} +code{font-family:"Latin Modern Mono",ui-monospace,SFMono-Regular,Consolas,monospace}.tasks{font-size:16px}.task-legacy{margin:2.5mm 0}.task-legacy>strong{display:block;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#555}.task-flow{border:1px solid #222;margin:1mm 0 2mm;background:#fff}.task-flow-header{display:grid;grid-template-columns:auto 1fr auto;align-items:baseline;gap:6px;padding:5px 7px;background:#eaeaea;border-bottom:1px solid #222}.task-flow-header span{font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#222}.task-flow-header h3{font-size:18px;margin:0}.task-flow-header code{font-size:8px;color:#555}.task-block{margin:6px 7px;padding:5px 6px;border:1px solid #777;background:#fff}.task-block header,.task-exercise header{display:flex;align-items:baseline;gap:5px;margin-bottom:3px}.task-block header span,.task-exercise header span{font:700 9px/1.2 "Latin Modern Mono",ui-monospace,monospace;text-transform:uppercase;color:#555}.task-block h4,.task-exercise h4{font-size:16px;margin:0}.block-steps{margin:4px 0 0 16px!important;padding:0;display:grid;gap:3px}.block-steps li{margin:0;border:1px solid #b9b9b9;padding:4px 5px;background:#fff}.block-steps li::marker{font-family:"Latin Modern Mono",ui-monospace,monospace;font-weight:700}.block-steps li p:last-child{margin-bottom:0}.task-exercise{margin:6px 7px;border:1px solid #777;border-left:1px solid #222;padding:5px 6px;background:#fafafa}.task-exercise header{flex-wrap:wrap}.exercise-based-on{margin-left:auto;font-size:8px;color:#777}.exercise-evidence{border-top:1px dashed #bbb;margin-top:5px;padding-top:4px}.exercise-evidence>strong{font-size:11px;text-transform:uppercase}.exercise-criterion{font-size:13px;margin:4px 0 0}.task-flow>footer{padding:5px 7px;border-top:1px solid #222;font-size:12px;background:#f6f6f6}.task-conclusion{margin:8px 0;padding:7px 9px;border:1px solid #315f78;background:#f3f9fc}.task-conclusion>strong{display:block;margin-bottom:3px;font:700 10px/1.2 "Latin Modern Mono",ui-monospace,monospace;color:#315f78}.task-conclusion p{margin:0}.we-block{border-top:1px solid #ddd;padding:8px 0}.we-block summary{cursor:pointer}.dictionary-sheet .pdf-margin{display:none} +.inspector{position:fixed;right:14px;bottom:14px;width:min(360px,calc(100vw - 28px));max-height:46vh;overflow:auto;background:white;border:1px solid #cfcfcf;box-shadow:0 4px 18px rgba(0,0,0,.18);border-radius:6px;padding:12px;font-family:system-ui,-apple-system,Segoe UI,sans-serif;font-size:13px;z-index:30}.inspector h2{font-size:14px;margin:0 0 8px}.inspector .empty{color:#777}.tree-card h3{font-size:15px;margin:6px 0}.tree-card code{display:inline-block;margin:4px 0}.tree-card .line{font-weight:700}.tree-card .line.OG{color:var(--og)}.tree-card .line.TECH{color:var(--tech)}.kw-list{padding-left:18px}.kw-list code{color:var(--kw)} +@media(max-width:900px){.sheet{display:block;min-height:0}.sheet-content{min-height:0;padding:26px 18px}.pdf-margin{position:static;max-height:none;overflow:visible;margin:10px 0;padding:8px;border:1px solid #ddd}.pdf-margin.left{border-left:3px solid var(--tech)}.pdf-margin.right{border-left:3px solid var(--og)}.hero .byline{float:none;margin:0 0 14px}.meta{grid-template-columns:1fr}.section-heading{flex-wrap:wrap}.task-identity{width:100%;padding:6px 0 0}.inspector{position:static;width:auto;max-height:none;margin:0 12px 18px}.topbar{position:sticky;top:0}.viewer-zoom-status{font-size:16px}} +@media print{html,body{background:white}.topbar,.inspector{display:none}.paper{padding:0}.sheet{position:relative;display:block;width:auto;min-height:0;margin:0;box-shadow:none;break-after:page;padding:0;overflow:visible;zoom:1!important}.sheet-content{position:relative;width:auto;min-height:0;padding:0;transform:none!important}.sheet:last-child{break-after:auto}.resource-header-enabled .sheet-content{padding:0}.sheet-content>.page-resource-header,.sheet-content>.page-resource-footer{display:none!important}.pdf-margin{position:absolute;top:0;width:18mm;max-height:none;overflow:hidden;margin:0;padding:0;border:0}.pdf-margin.left{left:-19.5mm}.pdf-margin.right{right:-19.5mm}.task-block,.task-exercise,.block-steps li,figure{break-inside:avoid}} + +.page-resource-header{width:100%;height:28mm;min-height:28mm;max-height:28mm;background:#fff;color:#111;overflow:hidden;font-family:ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;line-height:1.08} +.resource-header-table{width:100%;height:100%;border-collapse:collapse;table-layout:fixed;border:1px solid #111}.resource-header-table>tbody>tr{height:100%}.resource-header-center{height:100%;padding:0;vertical-align:middle;overflow:hidden}.resource-header-grid{display:grid;grid-template-rows:3fr 3fr 3fr 2fr 2fr;width:100%;height:100%}.resource-header-row{display:grid;min-width:0;min-height:0}.resource-header-field{display:flex;flex-direction:column;align-items:stretch;justify-content:space-between;gap:.08mm;min-width:0;padding:.28mm .55mm;border-right:1px solid #888;border-bottom:1px solid #aaa;overflow:hidden;white-space:nowrap}.resource-header-row .resource-header-field:last-child{border-right:0}.resource-header-row:last-child .resource-header-field{border-bottom:0}.resource-header-field a{display:flex;flex-direction:column;align-items:flex-start;gap:.08mm;min-width:0;color:#111;text-decoration:none}.resource-label{align-self:flex-start;font:800 3.2pt/1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;text-transform:uppercase;flex:none}.resource-value{align-self:flex-end;max-width:100%;text-align:right;font:700 6.4pt/1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resource-header-field--title .resource-value{font-size:7.7pt;font-weight:800}.resource-header-field--emphasis .resource-value{font-size:6.6pt;font-weight:800}.resource-header-field--compact{gap:.05mm;padding:.2mm .2mm}.resource-header-field--compact .resource-label{font-size:2.85pt}.resource-header-field--compact .resource-value{font-size:4.45pt}.resource-header-field--identity{display:flex;flex-direction:column;align-items:stretch;justify-content:center;gap:.15mm;padding:.12mm .45mm;font:700 3.55pt/1.1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace}.resource-header-field--identity>span{display:flex;align-items:baseline;gap:.3mm;min-width:0}.resource-header-field--identity b{font-weight:800;flex:none}.resource-header-field--identity .resource-identity-value{margin-left:auto;text-align:right}.resource-header-field--identity .resource-author{margin-left:auto;text-align:right}.resource-header-field--url-row{display:flex;align-items:stretch;justify-content:center;padding:.2mm .55mm}.resource-header-field--url-row a{display:block;overflow:hidden;text-overflow:clip;font:700 4.9pt/1.05 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace}.resource-header-field--url-row b{font-weight:800} +.resource-qr-cell{height:100%;padding:0;vertical-align:middle;overflow:hidden}.resource-qr-cell--left{border-right:1px solid #777}.resource-qr-cell--right{border-left:1px solid #777}.resource-qr{box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:.4mm;color:#333;text-decoration:none}.resource-qr .title-qr{display:block;width:22mm;height:22mm;max-width:22mm;max-height:22mm;flex:none}.resource-qr-empty{text-align:center}.resource-qr-empty span,.resource-qr-empty strong{display:block;font:600 5pt/1.15 ui-monospace,SFMono-Regular,Consolas,monospace} +.page-resource-footer{position:absolute;right:21.5mm;bottom:3mm;left:21.5mm;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:8px;min-height:5mm;border-top:.35mm solid #252525;padding-top:1.2mm;color:#68747a;font:650 7px/1.15 ui-monospace,SFMono-Regular,Consolas,monospace}.page-resource-footer__uuid{overflow:hidden;color:#4d5d65;text-align:left;text-overflow:ellipsis;white-space:nowrap;font:inherit}.page-resource-footer__author{color:#4d5d65;text-align:center;white-space:nowrap}.page-resource-footer__page{color:#283940;text-align:right;white-space:nowrap} + +@page{ + size:A4; + margin:33mm 21.5mm 10mm; + @top-left{content:" TITLE VER. DATETIME \A \A L08 · Docker Fundamentals — image, container and storage 1 21.07.2026 \A PROJECT SERIES CARD SHEET \A \A Warsztat terminalowy Bash · Console Tools 08/08 " counter(page) "/" counter(pages) " \A SUBJ. PROG. CORE SCOPE LEVEL POS. GITEA UUID b6d3b00d-6912-502d-95d7-3ee47c60f1db\A CARD UUID b6d3b00d-6912-502d-95d7-3ee47c60f1db AUTHOR M. Pabiszczak\A Inf. — — — — — \A \A GITEA https://gitea.local/edu-inf/lab-console-docker \A \A HTML http://localhost:8080 ";width:143mm;height:28mm;box-sizing:border-box;padding:.2mm .7mm 0;border-block:1px solid #111;border-left:1px solid #111;background-color:#fff;background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTkgMjgiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjxyZWN0IHdpZHRoPSIxMTkiIGhlaWdodD0iMjgiIGZpbGw9IndoaXRlIi8+PHBhdGggZD0iTTAgNi40NjE1NEgxMTkgTTAgMTIuOTIzMUgxMTkgTTAgMTkuMzg0NkgxMTkgTTAgMjMuNjkyM0gxMTkgTTU5LjUgMFY2LjQ2MTU0IE03Ny41IDBWNi40NjE1NCBNNTkuNSA2LjQ2MTU0VjEyLjkyMzEgTTg5LjUgNi40NjE1NFYxMi45MjMxIE0xMDYuNSA2LjQ2MTU0VjEyLjkyMzEgTTcgMTIuOTIzMVYxOS4zODQ2IE0xMyAxMi45MjMxVjE5LjM4NDYgTTMwLjUgMTIuOTIzMVYxOS4zODQ2IE00MS41IDEyLjkyMzFWMTkuMzg0NiBNNTAuNSAxMi45MjMxVjE5LjM4NDYgTTU5LjUgMTIuOTIzMVYxOS4zODQ2IiBmaWxsPSJub25lIiBzdHJva2U9IiM4ODgiIHN0cm9rZS13aWR0aD0iMC4xNCIvPjwvc3ZnPg==");background-repeat:no-repeat;background-position:center;background-size:100% 100%;white-space:pre;vertical-align:top;text-align:left;font:700 5.2px/2.12308mm ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:-.02px;color:#111;overflow:hidden} + @top-right{content:"";width:24mm;height:28mm;box-sizing:border-box;padding:0;border:1px solid #111;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:22mm 22mm;vertical-align:middle;text-align:center;font:0/0 sans-serif;color:transparent;background-image:none} + @bottom-left{content:"UUID b6d3b00d-6912-502d-95d7-3ee47c60f1db";border-top:.35mm solid #252525;padding-top:1mm;color:#68747a;font:650 7px/1.15 ui-monospace,SFMono-Regular,Consolas,monospace} + @bottom-center{content:"M. Pabiszczak";border-top:.35mm solid #252525;padding-top:1mm;color:#4d5d65;font:650 7px/1.15 ui-monospace,SFMono-Regular,Consolas,monospace} + @bottom-right{content:"STRONA " counter(page) "/" counter(pages);border-top:.35mm solid #252525;padding-top:1mm;color:#283940;font:650 7px/1.15 ui-monospace,SFMono-Regular,Consolas,monospace} +}