From e76cdf6336b3432b884d64c7d1a1817ad66014db Mon Sep 17 00:00:00 2001 From: user Date: Tue, 21 Jul 2026 17:55:11 +0200 Subject: [PATCH] feat(L02): add tmux sessions and panes card --- .gitea/workflows/render-pdf.yml | 63 + .gitignore | 30 + Makefile | 11 + README.md | 25 + answers/tmux-evidence.md | 10 + doc/assets/tmux-model.png | Bin 0 -> 21076 bytes doc/assets/tmux-model.svg | 1 + doc/generated/main.tex | 356 +++ doc/main.tex | 7 + doc/pdf/.gitkeep | 1 + json/card_source.json | 22 + 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/tasks/task01_session_lifecycle.sh | 9 + src/tasks/task02_windows_and_panes.sh | 11 + src/tasks/task03_detach_attach_state.sh | 14 + stem-card.yaml | 9 + tests/card_contract.test.mjs | 5 + tests/card_state_db.test.mjs | 67 + tests/test_tasks.sh | 5 + tools/card-action.sh | 14 + web/app.css | 1 + web/app.js | 61 + web/card-data.json | 2660 +++++++++++++++++++++++ web/figures/tmux-model.svg | 1 + web/index.html | 15 + web/style.css | 42 + 38 files changed, 7109 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/tmux-evidence.md create mode 100644 doc/assets/tmux-model.png create mode 100644 doc/assets/tmux-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/tasks/task01_session_lifecycle.sh create mode 100755 src/tasks/task02_windows_and_panes.sh create mode 100755 src/tasks/task03_detach_attach_state.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/tmux-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..90f7f2d --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +TASK ?= task01_session_lifecycle +.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..7eca074 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# L02 · Tmux — sessions, windows and panes + +Karta uczy pracy z trwałym serwerem terminalowym. Taski używają osobnego +socketu `tmux -L`, więc nie widzą ani nie modyfikują sesji użytkownika. + +## Taski + +| Task | Dowód PASS | +| --- | --- | +| `task01_session_lifecycle` | utworzenie, wykrycie i kontrolowane zamknięcie sesji `lab` | +| `task02_windows_and_panes` | dwa nazwane okna oraz dwa panele w oknie `shell` | +| `task03_detach_attach_state` | stan serwera zachowany między klientami: 2 okna, 3 panele, marker `ready` | + +```bash +stemctl card use console-bash L02 +stemctl test native-amd64 console-bash L02 1 +stemctl test native-amd64 console-bash L02 2 +stemctl debug native-amd64 console-bash L02 3 +``` + +Wyniki wpisz do [answers/tmux-evidence.md](answers/tmux-evidence.md). + +Skróty interaktywne do przećwiczenia po testach: `Ctrl-b c` tworzy okno, +`Ctrl-b %` dzieli pionowo, `Ctrl-b "` poziomo, `Ctrl-b d` odłącza klienta. +Ponowne dołączenie wykonuje `tmux attach -t NAZWA`. diff --git a/answers/tmux-evidence.md b/answers/tmux-evidence.md new file mode 100644 index 0000000..e7ea06f --- /dev/null +++ b/answers/tmux-evidence.md @@ -0,0 +1,10 @@ +# L02 · Tmux — evidence + +Uczeń: ____________________ Data: ____________________ + +- Task01: nazwa sesji, liczba sesji i wersja tmux: +- Task02: nazwy okien i liczba paneli w `shell`: +- Task03: marker odczytany przez kolejnego klienta, liczba okien/paneli: +- Ścieżka `trace.log`: + +Wyjaśnij różnicę między serwerem tmux, sesją, oknem, panelem i klientem. diff --git a/doc/assets/tmux-model.png b/doc/assets/tmux-model.png new file mode 100644 index 0000000000000000000000000000000000000000..bb6ea42e29f6ff5b8a83af8d880cd37b0d2d80b0 GIT binary patch literal 21076 zcmeFZbyQW++ckO+0VP#L8k820?iT4T>6Gs7I4B?@rF4h1bmyVFyE~=3o|# zGukK(F3;^GG#nui%(llr2wk?hM&L_)CsB1LWm^*`R|5xQh^wnBgSm~RqoIMFF@vpx zY2pDN0R-|EA}R7w#Vu)X!BzY7IceANl(F_j)cdsD+%#WA`@`sRhv;b5>e^~!3k!`r zjPmq&;{KeOmWMrPUHJ6uN6Zy{$JdvQ@DdBfDRMH)tPd4kh|yOy-6#}G?Tw5~ zU?H;bt@y1sdOyj=?llTP!(=QNSS z6ZTCFFXALmX$$%l>~*&h?k_5`x~D(XN$IE;?-w)vxrY+Wp+~5msx|!q75{H=U+VvL-(0$6 z{?BC3bkuk-)aNdU zDcSk`kiSlBXr9ugxt#wU&)<*-&pCHV`T@N2i1CSh(f@wuDBhFNa)^^fh#w&pma2NO z`8XcqRe77P&WEmQuOxL~9+^e{0unmUy@%Cww%PP`Fq)8rWTCb?MXLW43oSlFY6x8N zw+4&maR-gT%H4%a4PZ_4MjKkk^15Ra_$IDZ`Q7YLZN;<{1u zW~rg>r_J;IGLJWRI{e?>x+j(&?f=f&Al9(}(W!BxOj>K%`VlJMKHnb#V(v-)=a17I z#uAcU>UE2zQ`S%Eo3gdryfcRIR9JNf(5+}U9k1ISTwLb;sE*yj-oj>NpeKt!{Al_k zRJvItuf6^YH^EdwVCg&`da zhmAZ??vT^iq9?<-E*RR$#HfXb^I5yvHC`CLCg+wfJ=pK!El)@c4GoF*ta4Q=UxBa- z*9mkJcrN*VkJ~Hh$J_|St)_Ok!RM_Dhw7~R;CJ1%)#%-D9r=BNMz^~9C%=l7UOL?n znGWhvk+Wi3($Vcqp0y3t3#V_4Td?O01~`zdlbM@ut82z7=WF6v+<4$}Q5DTJU{?~* zMl(vE|BXg1AQxbcUkLnGLrfevIvw|Oqi;CVMm8iA)yK$c(b+eopI#R!K*jcIVYxR2z!zHIBoh$G-`lZjrw|!dD~j?<*`MU zv4zXqE#EsFnK>Ha7bM*7F*9gBNNI}61dlK4Ghwx$J!u!l!FvShp_9+FCO+kH6NxTq zs`+R#r*_UJEjic~C?$Goa2=Woz5)Y%=dfh%p?=#pxZDfvB0Xs&$@rt?bolY{vvv66 z8E6?O4O(Uo#cw`r9R=z4%gAm||9uH9Sn6CPrzV;E9%y-DAeF`JUFV`d?t96^r0|+M z+|9+3<8ZzF(1)CrF28yAjfqJ=k)XfJ2LedR<&k}(UfpKt?v2-p9p8dZtv7XyfpX(= zYjlb5X@!+~VsUopWXe+tsvMTpHAZd{uk_&tXN#+og@Wg^`ux_5!YZ`vu=>g=Wcp~S z0W4{_(a6v9_4V~P>We)sVT@GaX}TYv`<9Nl3|oT>CALOp61OoJX!NHu)zT6}{pMoF9i~tp88Ur zW;^Uy(V+R5gW4+su(bvP1Ekd2K63bcNBd{KPPUTIQ(4uj_4*B$k[_x!;3Emf@ux_9%K_XTT<2&28N zf0}nOU8;D@o7^fo=qAf8)?7fIf-UFxAko@vt^uL5Q%poel=ZBQv3p>Et|TNcuZMhh zr9~e@VQebVqw?~lmW zR`v-w&zOX>%RC8~=EK#kO8fY@e^Qe6n?m)WKZ29n>-?|nKy6UoVxVB9L*pf+tE;dc z)FRe0Ff|nQFrmU0OdP5_`XSu+IZwT)dnvuJ5XYi2VTE)FOAX^m{#d@WC#WTeCA<1B z9HE*;I#d4gO%vbqH6H}dc1Cpxe}p9qbXB;ck=A3Ci2Es zcen@x$M2dCkJCA*sbljTp|nn4u4zoNvo8e`_pBB3CqPk`uWpG%eOdx$c@vhBjiDxo z>4GGX>c^&c$s#i>&Z_FSYX`>jTnlDk!bl!@&f-JIX~e#B@0 zrJqmqMVb!KcSevtAtHk1$>*>(?VJh@3X@&VS%R;&y*z9>x{%`KJ(~K%9iHSvcJ<|J zDpa#7%w>RJd0Vlr`x*r{760+jdGcYtXiB1JsYVo|sh9p{H3QeC*v*5mY9Em;R_a0X z3JIhy#4|j_=%{_@M+0`-N@NRL9$}hJXMVw);qJ=ER47{#OO_5qcFLz%ajeE??Ts{i zUZ$8~e|z}PyI?h&w|A(RJcSejeN!CyJDzY~ovm{0Tfl{8F1^ZS6zkEi8{F(ZIzF30Zm5`zlxn;e2_s zLtj&3(Dj0JCsw`JJ@OW#W$Q#6e^R6=wCl#&^V+h{h6-a>`ilc9N?0;-n)K}(FCK$j zm&IDa4~ECR7<<#6{^R!B>Kd;fgm|18qBeG`o+K<5U9(b~3DyWR=o&2hYfxlmGMisK zaVffM9NuWXYB$=vyW%aULU+%;l;zX(Yqm(jfJv<<tax;=6icQArRW-17L1XqWj?M#N6^UQ6%5fZmEqokwdy`f z*fN8gP*e z--_*&W}QDca?jT_>n}YMPuC8SI@S3x0)#)V)v;gO)np^CT_metknjgcWo-@(@FL`D z<`Eg9oVRxmE7Z#XC}%dO?I>w`+el84ldUW$G&UJmBSQ9v%@FtST(`EK$G}WqNL%=o z!OQBew=gxVCUMFEx*0zlpIr{g_vVla>&3KgcOSURce(2Ll;1s7FmY7a!Mc9Veo>RD zdTMnXhwZyAxm3gZ3z(D5m`slK%ev1^s_c^wr;GqjZ4UUXnN2kaRNmz)z@$3F!=|L= zBpJ&{K;ADODj%25)pO=9%#Xj$8pTWfnWA!Zwyf#)bncRPMLgQ8-%cHwPgY&@2JDFKkD#6p74_}OW?#(lFmL$xDR zAw30I8QU;nGG8>y!Pt)8Slr5sOz(r{U|xATTcKKHg3qD~y&7_O*yZln#LQO*5*yGU zm7NU@XgB&qZk{7PKe}wG|3+==OkWps$nC7^7?cwOjedd{dT2gYAfUWuAx9t|;6GZS z`(eL=huteEEOg852q}B;J_sXt%fj(^ahJv8L73Q#q4=~QH;>HoQ&bv zEx1Sd+8^iY$Fnus$hzFBtS5hcfRm7cY7X$R5_t2QAGDFpJQn%2lev9d1qq+3ZuMXH zY1h3OJZ9Z`uh%K-`F^B-Jc{5m9j+!+@>mMFAg<==K72u&NUxW}a19%u-P$I7Dv|Tq zdf*Thj$0vsh_jVu%`s4{6NUiMFvB=*Bau?6}E`pqWa z`i|T$jitO_#D0mRW5n-dH4(?HEcV9Dy00m)q`H!dnlsigXV9XHlF!XyI9>Epng_~qV3y@WOZV_E(9H<^JAG4`ulAv0zI|vD*JGd~@3rUDcq>4L zoT(wkdm+WO*w{lUZCx{t>W-IjiNV;(bL^%7ucWEaD$>06I_<#|lS`JqDx?WZdbZLX z^g?J%`B-e13VnOwvnR@K+m}^r1QK_(xaa_+$(T)$QP`2c)b%xWbxBA=|A|k1#eUpj z1dHdJQDF7__leX$jjXIh^v`Mc1U;k(X%Ei##!rfcC!}!H%iVJ>Iuku%+80%mS9~1< ziY8Q4A^2A$q~6Df@I4-hoT-GB9hIHarN=?-?H$&PqLHK9>iJ|5D@iQ-DTj)sBqDiuFn_yWfMh z3lxLcnwR*ZpfmtDpbsw{yQ@4p6^5m!ZL|0F_mVL(77tf=mK`qc8JrBCSTxV|MPBk$ z8*C>))oZ+ni>qzu4R4*&d}N^)r2HMxC4`yAMs_;~8>Kw#_eX`9?lR&cDr{i!k_3vk z0}2S2FI8Mo^2aTBs;viSp)QU)+BB?98vWI*o&1i3hYR)=78Xir9A&2t!AXLgmk+zy zR8r{Zug5WtOm`1HCDq!+|C@elBWm8MH<7mz7!>pzv@*RtzZ8_l#%f(YIhIS@lZ6FS z_R!&%kjQkTs}+s?RooXoHA6=3{F0_HTqn7JyR?ao{93Rd^cy3HOU9j3g5^GQHgy%A zClKM|lZ||p%q-I<3NQ=G{b^YHx{9jx{NNpBes_1j(RONLJco&|ORXvX`eKcZY`?5* z4bhvHbHZu`+R$s8NfG*i!|=TK%Cks4bw9x;2T3th6;$*c)X>?REzhz+N_d8e}OQxC{*jcBY-~Nzq)RmCZ zL|%e7W-By%%)h-4)DDQC>wV*Sb+QbXWO{&r{J!v+9|J8=UXYU0@`M2}*4F(mnrKt&&p6h&|1*#ugZCdrx~TNQwy9tF7d*NdBibPr zkF1*r1q35y^<%@*u!zV&@wsS8YHeZiAkrqRxuhaChr3E{I*=M7#Zf#v(JelA#Gn0ZHHC0$L$~T2P zHp4_|k7f_b{HBWFjEflssP%PhMM%h#vGTGqd}e3NjO+p9m6KM}14ik~DZLEPtmUd$ zaLjwh1kz}H`GTE0mX7PpbM|HI!h3dV->Lv_G@s6Rfz1`pzul4&&T4EV*m!M3q$f)p z3c3rhspzTM(z59V)(^elA@vA_Vv$fYbL05BgyWfB(RX4Ubby*__|SihhffeutaRM> z7lW}u*IsYyVeRooV~uriny1*1s7na0&u{z6xy)?0LGbZ>q`ccWbPt;iiOF z`=5)@@9UZnn(IE)r<%_MVDar_eO}`MOK2gl!fCUlW_ILdV?zR{EJQX=UzNgw?SOjD zVVx|c(=Dojx7T&-UI?HHW=eH9Knk>rYsV%^qlk4=NQTBIWA~%c)gg#W6MU<0c{sib zWMtc~i#)S`F8NfirvDd~;1p(==jA{ZQjBuOCSa}t9u^w0Z>RT-x7J_R&euv$q0Pfi z-y$L+v36l~PIpUq=EG<-l8E*zhthBCXY`Wl-J-psf6h1XOeoC+6_$$%x4FN$1?iF4P<}UC1a$2;pH5`v)aPF?bWT7I#~+e^ z;HvE(wDEu{*$=>6cTbOKp{gSQC;O3=uwI-8p&C1o4@(A0QVA}}pj$ORWvj08+ccjO zEUqE_1>@sCDppeTl(d<=)2ryJ28W)_yj6J80wlXkLt zGG#meWwN@d#A6H-k}!cqe4z{y4CH2o9UsyN%KTxfBw|^utxZOG{w8`QJWo} z$~NcUM~p{ln0pRjF=*Ie;__Si!}zPOBDg^3gn(^g&o09{SgCi(Ss@Rh*5P6nBJ(|J|4O*IdXalRV&3h3EXdP?S)WQ7(TsPS)r7`E_ zE8$ZY>!pYl?+3pM@PT}~VRG7^_!hMVo}wAwbOt%nQ%d&|r^c&MPm zYPu1RnEA-{ZUwtRqA~Cj=sNN zVyIz>b-c{LdS7j`;oigYvE;X;v9duunKn&K0CF`a#Z?BpiCCrO>so%$e|cSxNQlWK zN?UmFka}N#N_VGq^WggolffTJC8r*YNMHzuj0AWfOiWi8HeGC?_=zVcG~(gW=w6k@ zE9Du1?x;b~rIVbL(dk%JI{ceFfu@Cd}7jc1w!@k0w_qL;sg{!^wlB=)GwTx~8`73<ZGVV14qX}JJ5GN|=-?Ztm{|WB%!vd`1X6RJHlM;hD5>V3leGraS0Do(`aanHW zb;)~sa&p=;IM~uG^s$mQTAlB5O?c_{f^AclxDB8{b4Kh1G_|ZfE4azHm%YK9h8QUX@cbR;Rff*)DbT)UboA9@w?2E!6JZm6^On}DtB`cSHg za_`_~kaZ&`^5=hI_rAC3J=4=aG$#%O(-%5>%bJP(7+DzdbbX<}m)>79fm#xlwU>*# zJha!-rg1V+A-rVGb9aBxpURD})VqP!aIk&QJ3fLKcX|9zICbcp)J-nQ6-nMYoYssa z$>T7r1;z7Ribd2!E@d#qU!&?^7vg`Qb?eXV%*f3i%FApIYUcJ{+fh?%y(BKghUfzy zLoS6|ymNF?f|!-|6EQKdC>Ix+6C1i?r-wf0=;&x!6)U2@UN~Wx7d+QrtfQr5Wwf72 zzR#Oh*IHdw-zla3#cUr$K|yI~XdD!oac}7NSrP30fX?xYkAE+RIn>zDco)07^BIgU zXQj>OQpF*UGaT$Eu(J~l6S-m{q?4{Cm%`F)WDQ}kP8=!xgtm`=24~I7h+TujiezME zX|@!vm`NC0n}xmxE9d11e06dtp*#7SF5lNKqOmOPtA_+XbUyzJXfvA9CEQ$j%C#tw zD0Lu)oLpl`=PjgT9fn@TL0w2a*5BH^ONsd}%}X)jZs|<_`F*ci9v9y0{+2X-tAY>a zOAQFga~xxfVRY@MkKzwd3#FbsS6_{%%*ibM(&nOSH7(cSV4Nwf`_KQs$I)JYmG1U# zl#=m3QXJ3--QqJ*r2qLl^`QMnKLSJOA_E+wgA65^xa#t0^> z9ZYpN{J+$%pa?nS+W*#{z`P0-MU#@Zcj!li(rL>2bnf7h|2-QX@F@AY2Zy-)`aNli z@BT~4i*f?bGq_N{f4#stx4fm3UXw`vApbuK7#RYX4q!FTEw2B=(Jtg-c6KUhu69WN zmqfh(C$+9{m*;blra-O?TsMq{Fuao?< z{yk&p_{5rM7nR+?=)@LJKf?z&3bmc+tDeOuF0rK)Sa%DZAH&}uGa5a>8L!$F(G@@I(` zj}XQK9aK`2-#WXNzPs4U-a=KER<-`4jb9B{X^yCSMcI?@D1vMwwrJ2+1)KG)JZ)w^ zIX%^*(dQ3sqyCnsmp_YENX|l=fHCLNWY_F5Q}ys*ZO{3UPs~P_L9lqbxkSLWNeMat z?vNup=22m;SzMEN+zY~%QZk5_B7ednrg*Nmia!uQZFj%fGw=FGK3~6QCsahFbJ%r{ z8lqiCAP0KoqQ#ny_H@{z9CGNR1G=C@kpD(@#WpH=>X^P-ip*ZHD=bjb|9HiZq95LJ z0Bnnxrj&Pe5gSc}&g184ZrX{pBWaE+Zk>pkjBYS}Ym1n`aBGWOynGV+%!}?b4O6OS z;b!=`)=Y^Z>6`5AhM8>qtf20jLC5_r=iZ#>;2lzas-l zCQx@_Hba3%A#WVKz$L2%#}|g=hKp8XQPn(Eqc&St@uvmz70F3^+Kb-h7yzh_-yVv- ze&e;m60BB0O-mJK@tKwtEX58wOMeC!`ZElh>sSH ztV00(4t`ddez*}J|HrmI{4s8uZJ4YeLGc?baoa#1Wnnc}sz>(obfE-lsQeZqg8p#k zkQ;n|3Y7d!X((xXxmJ}6S~gXu({9>zy>6KW_BAaI;-WXUZ>81)<%bWE7N6-#H+OX~evr9M1=Z63x*Id7Ywftv4GqbG==41_8VAwSiL7}D#rH?9*(SW~XGvnU zovX8n*`bXJtzHZe7Yi0Eux*RW3YMx+2v#>;SVlfQQcy;ME`#G_y+@Xy_>;f8i{K=!#XO3jjdO;S@ zeNGm0M7X9XotS$)(_i_IPtKMrJDuI@4e0NYd>h7MxToKVanSSgTuZ&)nX}STB@58` zT)eZaF8cCBd)DezHfT_r#f~_D2YGX;^EnTVKqx`HZu_JjrMn76M0~+a+`cJm~!W}3ohSnFbQZKX?=Ch$;>)^h{KD3U{mHt|BpZ()9^@*S}^BtN#ccJ`VR2H+M@ zU6?rv)MhsQ-hE(Ghl@9w?A?2y>9RqWrdrA3F2`7QCXbAJyAk|#(M%Ux1tHVWv)8PR z*YN_j&PG5g+~axOUdUStF*4m6Ha%mL;^jWs4=Js%(oTF>6L?2POJFJO_;7vs7SfYv zZZ+K)i!pbJ@GCL4i*&b2v9W+x@Pl4objh<#dD1S=(BK7i?y8b z@;-M(EB+(4>F?y^PjYe$-kmZz4%cM>Ers2%-*2-!(3<1pC)GP8gT~5u&-e1mX;h)= z&huBO>+Wt-TKl>Fwd%!cef(P9^7cm%DPJf4+TF|>*Hu`4)ZoPJmx;PkWx(={&O)+b z2y^p@q5>8E_x)13Xq!+-Xb2!i4G!e{YVq9zm(kShTysfgdYu@`%GeRg$_PhA z%kEH34bCUCXK(pgvC&vrF^RFVKc;P-ECWurr*FXcy7JE|4a}a_Z?!vM=X!2Xe?T5c8EALkMf#-Jf1vKUz0HRck(} z)KWjbrw0VtQ~klHmw*ii>P)Ghhs9L+R`+wvRDt}#Y>X;%hhbAmAyI%UZx+ec-E7Ka}pSE+Pfy6N~MU(@%>zsv@c zUaumQB&6h8dS{=IBO*Tb2vIQN^eLO+TshzZ-~la_%K+hM>1RX8}I73I$JhznAWmD=3j@Bi|i&7dJEkT7TP`^i=|F_1cbVF}z2`TBJY4i}kw zV-g8I#zM}JUTeUjJbK#9lvh2Ze!{J;A>3lGtpT6j)HFx&@tPPH_=heg>mko*b;`=LzjEs8M08_6tM@gS*4GNO~ z6II9#T1sNyGMgeI9{m2zWE2{;P7Fs@R_!EWyOQdSzHqT$vBk7i-q0B-?Pl8jz3wT& z;$^UX8EtI>ml;L#)NN27J|Urjl~K3vad`~^$yXqM(N_L!@WKJwPNl&{%Aj?EIsEXV zIqeIGA#@D%?%v*(*8UK{tp1yAM1s7m&H(YNnC3ZFFU!E@i|TT5A%jmK%xN0 z&IT}etgc3yy->ga?GK9Ix6!Y>NxKlJpD-0h2th$`pZ-6ts1 zMyt>$6ZjHV|3M{&H_*Aga=M}aOlx7YSWDmn98dCNK-+nxJo?BY+;_>1oY?3SUg??& z+^u>NxNgh)ref*vRx}TGN=?}YyFK@6;6uC9&hyfcOR4`66!ePyvBDKCg|yGKU;haw}$`*hv+~HkqNDT%dan`knsneCZ#W zFAxlXu!7c4`sQU2Y=7;LrDvdT&8SR87==Y~uWF7z;D=$2^rXpW}un zYh@(Ho1B6fDzqB+V_VZR>ZQxofcD(X zvPwfF@iR{p(2D%(Dc2}JlHRi_0PLFna!re9SY=;#zZW+zol}u$#w?KliHwXe*(VQY z(R76)AQL>gEBZ_uEQcq1Bm4syW)^~Ehh@sEC~gkmhe1KXU6W#tca}409D#EFc4y<< zR9K#={+ULYq*$B!3QRy9M`#8qEC}TeSRZG@B;jpapFz8ND3=`f`WdOGST-LNM0ez< zqD@pasru+5@tNejyP7fDLB4uaN(-y_Q)>jYETwlQ&P-QuN#AgL&o8Gu62_(s+M9D7 zbU&nJj9Py!^323W6t$lpQ)Oi;)qbM}A`j4tN{ufK_Sv$2Gn;S6=c*)-ydx+7w9oZ7 zYFs5GB;@Ea&mSz`+$G7ockhvWLFKUHyW#`Z1jH94_KDTVvQ*66=cU%qYu%6-<+11o z1`t4dFF!pX0dxl>GvS3=1!@devHoE#if?E5}* zkxHXS7boq5oj(D)Uv)6Dmy=b?ExR+h8aqJ-tug zcwlE@^(e1g8>2}SnCtRXvw$y=jLYGK9(d9`vYjh!v|}mrIe#Y<<@o_)m|2$udp>FR zwY1vvMmzjrbT_!?)W=!&Vl1K4?_Ue%OF~()l1qUhMsgv+YR2)$`nbwn1N$ee?=lZb z9dw-fhQ>XX13QYdGhcZ}j_W+=+NGUt<|)Hcc+iAYoQSksN&h&n)lpF97}!{RE3U=` zx|-E&Y3f-H*H=qz>i;x*gdP~fe4Hnhu7Rb8|0E}E76v@qvi0@NfIdJwUbro;(htqC zkr(uOBL9vzGh|E}W2t2VlWem!yR1+5B` zX)n!VkqQY7Wm2RC-9>hl+HS_mJ>F@L8r#l179t{I10Vm@>L=O5SkMei&uCg0m+ z?~vMV8VCI4w@!C=WaHv!z^jjN;RUR;$=R7Z+rcPlwAAB@;4fZ&NNyZj?TB)*H_{K6 zYglw;8^pX2>7rzEpT327vHc-6H1(3rkW1hJI3~0>Cmcy%A)LXB?^2w=72g+dI$2hQ zS0E@Q>9k?$3V*6#j5U-&=Zm8FPhv z0|83gPO-$9ta@YP;^TfLC3H5EBq(l5b*~$+c3ro55S*bLs^XFBMk^}b?J0yK0v#ahSAf@}~N46;!=Ud*sxx%q`O-cXSVu-e( z@$;Yktwuz!q8ZSkEIg%1O>8aiyEjs(*|ZW84X?eRZ&n5WU$VRf$6Ocw_Y3PfEa4%bnd zM0RIqg8aV?_I`~!QaK2HGc6>(|7xE8$IvdLO$L$6+6%6w45xBF-}#>-4IlNKv&MWk zHnuUFdwvDiX%`M>0JjO`h;~c2|ARYQUcOrR|9Jgxz+nO8f|yVX#1gQhJq|iD$)B+F zIlt5S=l^!piEK%o0{VV#FS0o7h(+lJaEyT4f#s>CG@2$SZ7m8t7(DoY4PF2Srw14X z`SKrnbC=!&(0=O})EJ#;fXg}0(dVhYL@@&(j^8*2|7WKEO+fzhYuuIE3OD};KS%`a zmb|(M#p8j6b(Qx;5mexpE6R2B}G zsq>sCHYZQJ@$pMNddV!*ivR8o{MdTow8^KubteJ%E^thvbS0L%hKKvLYkA7g|I0xN z*i(An+&C^|nX9Ps^eITLT3@Qmzz+8EmS3>)dtj6~E7dsu1u*YHd;6oELhF)fv%BX7 z@MJ51?cnS}%pQM%sc z6tg02a6R{s=+58u|5?iw zp)O1uPj}AXq4e|moSeH)$j$wSdgtzL)#j%gLg}rmgO{1>NqKp2{rgp%w+PaF5wwiqcW2`{DS~^K zsWm5Uu{?7=KB~xn_hga_4~={KB*c?568-FaRH^=o7f2VXM#d&Qp&dY}t_`F%AxYtx zVQB8C9s8kPu41}-Tg>9@)Kd116BUtrJ%+;7#hS{fwD|JO#F=G$t6pT)GJr-n3&p2 z4IKGA(c7EVCA+)GCF))9oqb45sW7rvhmX~x@`Pq56k1~#$<{jAL-zOHFBG9MeBd>k ziuYOLnS$P?R9MC`mAa-m1r{M?=YO_njb$V=S#m*_8L)$9cz)f+;^Fm1zt+G<(q(m~ z4ZV!ZgS79>$*nJ0b_a#G^~2Cp4l&wi=Wu=L(6$Ve^JurLZC<$A;YxX;(4ymows!w2 z(R=fYNsZ%d9JxVH3?}L-&hiF`X-kJW<5%R~?_T7g)+BYOAB3A0pudl&a!Q$16>p3|N z2V+5*x-gNO0Xrpz6i{d2$Lc#4K`k%Q;^ zu@`67KR^F|$rNw$^BsahzRJAzJsq=WRYYubfcF4T)FCgCA)yvTI2*mK(p!y;v#9m& z7o4anmvv(!g`%yu5Ebs3q+|ht>Z$rQg=%KVd)%Q{4_(4{*-%7elrhao%&s_<|Y3J`0OwF-iVzGRZnY^9|qc&d~P|ca@7SU!cnlbPx3}?@=2b z{W~M!RAp=_)m7%JJE!8kz?_Pqf4d?4?0a^0O$D&xOaZe(+1;KMSY(QaZUF^f??U7# zZ(B%bj4EWEqSMj4$CV-=sdae#Y3v(fRC-dFC)&&1hqs)}FZ)n&-Y-EZ-jHl?aaukZ z{4lqsUYzGbs>GN~y?05P%i~P)ohlpY;&AVbxyE@ksft1?ZJxdTScepa0o8^)c z(oLuhKldDE&}$zajTp7&U71WLqe%VU%rSFPJR4s4ApiT%^#~fw)z7tVu-~U| zF|l!W<$ZJnFF?$|OhTz!(#Bdd^zgN;y!Y&RpZ^Cf5wR;AkO%hO`xx4Ko~QQ4^bemh zn=~OWJdGH)Ol8{}#fWJ!lYL7IiPd}(@{@~H^PXM1am81Y=d!JmgP@kYJN`6#ERZ$V z@Kto?B(An@ZL$cBsw%7dXYD{<={-|bRn}^WBp17TXy1~L)O`8^#Ava$Bkwm%advuz z--Ljnkk?@&kP)n*SpCAk1+DI|;B2o{EW6x=U&FOy5?uC~-QIQuORsY}3+*iCQM-@_ zBw>^tV@S z2HICnEEmoMiXh}782#u2!hrvCsHd$8&psS4`2&mu#Vj8;i^+;9I)@CFiyJWzUZ zOwLFdq-?l|Qb|ypj&0|?FWc&Kn=+?0g{ygCU~Zts#cu<7gVSK2JC`|pc9v+6xzK=V zCQu<4z8n5ol=QF6&Fk1V`?XxAw8tL<*$(0RTJwty->3cHtJiXZ+H)?eoxPMIMS5Sq z2EDi0uhZZ87m^jr`S?lyi&SsGi*GsTZKHjGQf<_RyK0e+W8#r6>}NsN(3cvnWY6Py zo&?r~9^bChS$SF7nVh_Ec(sO5Q?~5*<>D}N{>;jzu)8Z{{y$iX=X%vnH*IxsaJAY-b+kTy1n~;ge*^XM$a!+rLZh4b(K#Ey$U1Uyz94zud`d- zJ?WN~U0LrIST{9Ravy))>vY&x7@A*uvqm%^HDNJ0g!id6s4WT##lyJ?C)@b)Y%4qT zNo4{^M7eN6Us_JwL;JD!w~G)mxQ6uANr3OB|68X6r+FTiHrHA0s0+-e@&rF=8F8`e zmF6`s#lL#jz>Gg2i}ebxpuXMSlVtE>Mk1G{Yxo|LqoV(b!Rz5A2o$rW#7Od6{am*6 zlej#$hHFfNzBjL`yk0^k1tuz>Q*xnNzhh&hRL8B+Wn|d9;uO7^%PKlO``N%v*JdM- z=CvzD%1Lp*JuYU}oa@A%_~tk)eiuo6{FX7gX7vfr+Lkzan4DpUfyHxO9m+ddLWd(9`E$wNNA_R&MlO>RDV$Kih731@ULp z`r)~Cd9;_#ol%{y5qMWXZv(%lhCJ`W`t8gcQ76r5kj944{Pz!*xTkaGUj4nD+3+`i z<&W#x6l>EpIFN15Hhb%-_@9VfzQSL

?eQr`*udy9#Y;OIC}tgd#PagzH!!CDoo zUC3PcZUix*T&=lGn36X92JbQCC1Eb{puUzCe=Z&tsHyPLxrOgGF$kGkf&{}x=$4kx zS^Z@nWwV3>x%I7k-1i*1-zOzhA~bh4QRyOzA?WWahx*yF=Lk2g)Ku)`>KtYvl_StB zRAH;(LLIAP`+l4yPD$2#DSDcC%HJ(u_5LK48BUoo=@45Ruffs%)M==niQo6z=bq*G zBMR7OaF@Y-QAN2q4b@#?pl1Fn3H>{w$9_%*Zv=F73bEB(%8~> z#%pI?9?Q1rA;*0ES#zm9-JdGk=07sr`-g1Q?^7K3krgi%9%e6@52g`T!%165`|6u7 zPFY6Z>YcZ{yy+NjEo*km3ens0q}kBp7~D5vz~@4S_bt(H723u?If z@6Wt$lG5&z{x^$am9f)?A<+*5Wyo1cq+)0?mEjN-oYvkXy^z4bjZ&1<*l zLP-BRPRi#~RmofeXT)U(B42h^#e7^iK<2En9*>D1q&P-SMjhtk?;4RR#NH)Y+0g6x zE3Zw?-&x`VBylAZmzTWMJsWe%+O;-v&A*4ZW1%O{b=`Dvz$fr`$7=b?c2b6A#`IO$ zi=44MlV%LmitfZypWJqwXiU^(%1U~ufJ#v z1;xAPk`m8mx&>}w_iGXFB13%*t(OFrn!1x&w^AIp7I}w~PLuj{6M|3hD=*m3HedW) z@=X5m;CG)f&V-r4{%wyv{5PL1vuw`E^U{t;HbatsPU!!}uvBp5y+1H#jG5EE&wO`T z1HW^LN~Ln?7r9xee4W7#>T`C~-+SJ>EiW{+4vkkuO+gQWKFfEeOuujT3{?*2B=0^i zEv~*jF8=g{{_D>9Xk2QWFDd_vk_CYkTBz^mSm&q7EfR~t@+eUwiT!LT{qNYAvvgpJ zZQ@^K_vJg5s)yC%#80=$28riCS>3!Y9PBT?Ly!;lw=_kOLD2L(M|0z6dymw$t*mpA z!(+&q1F!i#GSv8$S*Bi+Y9J}R`~=-N)muW@{@s8mQjMkM5f3B9NVV5*1Uz!G``c67 z;1Dr;vyHW)y08Y^)X7GDYT30t+|-w>9(*w*k`E{BD3=OHv&bIarjpGSAKjWhp4H!O zsT?lIee%H!8~tK|dGO9*d06gt+>Ryj#5z?!m8nvz1xH9{^Cdn1t8R_z(WU_%*Rakz zBDh7>aOP{M-NZ@t7F^)4Lhd%xTm*yo<8@59IX!K&qLPHN4@0TpEJ{NlX+vaZB=>@a zrPiA6yoek8<3kg9pzc@7A`}#8S)9M+)(d`zB)C`M)k!g4|GOlQ+X-JDCl7v%T8#$? z9eq|YoZV2JYlOT@DY-PbyQM8}u_^NFCBN`}(Au~uX$KLuw4?b;d+Z#gSod|Zb4!Gn z%Fp+)v?lFbP&*{elGib#fj3EL2&%gmQ^v&aT<+F=-?BMK5Oj7+?39O%YHP||`L;YP zqc*=LZkxuf_?`2oTJPhzTaN}Q-s|@yoZux-kLD;Gbp{jF!Ch918^6YltlbOx3tJtF zoRP|0PY#@2KZNHko>JcYQm|U%Nu{c)#J8>tO_Uj#D_(Zw&KtV>StB_%3oH7Iis3%f z9zulTTb~_Ku*4_4lPn#C$0Wjw880qjb&mL~nd3pqi-9`yntOV*ICJ?NgQx-eoWJPt zj@u7DP84kE)QAV}KN5gZrN8e_ZH2#M;H5!Wn6(;*!#^DV^KJ2sL2I_nEik42T13wu z<~T2Y!mVEEKYyJHA|BR5R01>3L=`%A8%~t*r=miM+J;?W(m($?r&%FmsFzqQQ8wYMuPV=_Xj?2m(gze_24pt<$Xc;$#Ubv zFDZs}f2_%vCnoFg!Se!IUP;|m;3v(L;m`T!McLj)J-9Mr&;b4|^n*95ajK8RX#e%Y z2v%48E6fG>rw{{Gt8L{U#H)nT8vWhPg+_&#k-LT9tQI zSH4Z52CAJ_Zh_@`UQ)81HmFNJ@g=(6_}A;-Pj81Sc{w9Z8QT+PZ@7>y1_bO0HuSuN zTb(&q{}xeLTuFTCl1tkh7LZXK5kbcjme79klRCrm+!cOiE^f9W$VExb*RXZubX1 zd&)|9v8XNEZ!SXt%EKnf)WiE9C)SCQ4J%Ym$e(g&4Qk5#5dE;dya83862v7VZN$ZI&?|oM& z#gJCNFR9=^ThEJGY6f@GLdt=pq4X^2-1$bF)V?6tK=F~lQf+byT4UFU_~FLCvaAr@ zTLNTh8dLZMw?{_MF=JEsV6M)$^O>&td9_sxM|`)#;{T(WGk=G2f8)4xtkEJ8vhO+( z6%Au8YX)T*>u4$>OV**Wo)VdCaSmlLWUH}-glx&y80)cyA!8Y1Fhgd>KIiG2>-zo; z-=ClRdOpu}f8O`|em%duheeVuxFGcJ!@^GlCsp8?juFhR^wA91TqB{a-|4o=eM1S( zTPq&)#_a{jq#~^UgQ>%Qy^tTWqNB#|Sy5GCW6^17s1Ppr+$-?IGqdqXjg4Kk-g;k! zph>gH6wh?eOSDw*(@aN+%$eE|?e&b3bmrfpD)o5kwcQTwhOJVPQPPZtQ);ik|(4v(GK88ZoL4nFVgH5D)1DRm2a&>O(cL-kZt)Ziz{Rkft%u0J0WESFOJ^0C+Pw{T8P>|vox z9`piQf0mc3@>?cRC*V&^_w8s=s0WB`*--~@^6*DKG|g-2tW{)lG9jiLYAVBdQobDO z9t3lQ5VxJyW6n@QSNJ&N^|r(Wv(NxN{DHj(9PQ_yUo9ovwNQd&S*f(HMLM6PrJm$F z_}&pGCl_##$Ylfe=kvU>5^_~&xd7R6jB1`2b)s0ZrSKB3js1nCQcx>V8W0EtAxW1m zxSFf)k9~VLCHa@mNZ&O8!!OmJF|##<)l-^W`-|nBH4b;Vut>D1jjPo*+(CwmU$&Vg zWmWE;Sl@cCi&C(rCKOitlchw1}ne#QVjdvZHi}Fe-12&F-EWcoPS>-{c!#4 z&6ARbz#)GCzh@rLb2lY&Frqq07OSsr7F6u~` z7d9QM*Jc;mcc`ajtG_o=98PP&RWf(v04ym8AEA<& z9TA&XNB64y#z?XrvlTgq#49RCM^HQCc3`jvep(BJjfDalAN3TU-`V0iWFE72s~Atc zTsEFMbUA0t9?(hMnK?%t6yEp-OcT`uS+BnFXVjiyLKJUe-8{Ck=)IC8aLqwInpkG^ z6C9>xhjq*3!*8(+N@WSE;}qffDqSFR20i;S(}uRkX@EButtNtM#=$1Ih^oT3_aJ2a zi@(%%e&z^NVQ%!dim$=I&u$MMIqx!f<$G@@P+$(ghMu`vgob>+LnNf;)1H)v9Y!(rD=}noWXU|vfm2IS@x@Nq7&1bih?k<5DCAWc2uATkJhClBq z4`A8A6%S#F_JWaDpVmZb9Q1>5^f2h!r1?%ZdJ@D^v9p&3he~%4#OBKtyTr*!^; zr|LpB_Uv;a1NpIUPwRNHcSw!}$?;2{Gbu;r^_5C^_8cg$48hl@F&7>55@sKCr>FJN zJbZ9Z5FER05hgmPfC2McLIS*LuBDop&>H~l={x0VjmC)&Hk7#{K zq@TX#E5|UO_$F9D8v)J0=+pjjI}`K4+JNtc=(QAL8?YB`gd{U}ic;_&=l%TI?P8?H zGl}#cSnZdfDa3bMC}HWnZh|xzELFWvQJUS3P(d6fR}VgJUS#g&oU}_-ygNlk=ZDWU zsNmI&Z^Dya@LJ^RWt|;@bCbb?By|2> zbg1a=lu)=R=#8HMjyDRtdVgcN!_6fnp5EwiD7U8U~S%xA8GBYHPa$Qyahr zS;FVf%NDAQ(-G?-_PJdTl2>RKs8~ty+d>@fw)dNSYVYlz+tMtDZ$${3{9#`ujG0a> z{1{i5O6Z}PViX=VfRpULac6pU+~rx<{x(eJuC9+6w}mE=5*Bm{wX(S(+*Ydit`LTN z$a}&16VNdytC$0>JJgkZ*w-&A92s|7J>H%stA zm|*Ab^u4PvM%0gc%(RrfVWC{uoNB`sNPN^ovctEQEAyJDx@dvyV5`g>XryDH}25Model tmuxKlienci łączą się z serwerem, który utrzymuje sesje zawierające okna i panele.CLIENTattach/detachSERVERsocketSESSIONwindowsWINDOWpanesdetach zamyka klienta — serwer i procesy pozostają diff --git a/doc/generated/main.tex b/doc/generated/main.tex new file mode 100644 index 0000000..d662114 --- /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}{02} +\newcommand{\CardCount}{08} +\newcommand{\CardSlug}{tmux} +\newcommand{\CardVersion}{v00.01} +\newcommand{\CardStatus}{Gotowa do wykonania} +\newcommand{\DocumentUUID}{2e06affe-9ecc-5813-8313-fd7b91744f3b} + +\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 L02 · Tmux — sessions, windows and panes}\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 02/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 2e06affe-9ecc-5813-8313-fd7b91744f3b}\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 2e06affe-9ecc-5813-8313-fd7b91744f3b}\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-tmux}{\nolinkurl{https://gitea.local/edu-inf/lab-console-tmux}}}} \\% + \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ń tworzy izolowaną sesję tmux, organizuje okna i panele oraz dowodzi trwałości stanu między klientami. + +\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{2} & \texttt{Task01} & cykl życia sesji & obowiązkowe & opracowane & \texttt{v00.01} \\ \hline +\texttt{2} & \texttt{Task02} & okna i panele & obowiązkowe & opracowane & \texttt{v00.01} \\ \hline +\texttt{2} & \texttt{Task03} & detach/attach i trwałość & 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 · Cykl życia sesji} +\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}{CB02.WE01.OG.IP.01 | WE 01: Tmux. Bezpieczna organizacja pracy terminalowej w tmux. | EN IP\textCR CB02.01: Uczeń analizuje hierarchię tmux. | KW IP CB02.01: Rozróżnia serwer, klienta, sesję,\textCR okno i panel.} \pdftooltip[width=\textwidth]{D2}{CB02.WE01.TECH.I4.01 | WE 01: Tmux. Bezpieczna organizacja pracy terminalowej w tmux. | EK\textCR INF04 CB02.01: Uczeń automatyzuje layout terminala. | KW INF04 CB02.01: Potwierdza liczby\textCR okien/paneli i trwały marker.}\par +\vspace{0.10em}% +\noindent K1: Zmierz jedną sesję lab na izolowanym sockecie.\quad D2\par +\par\vspace{0.18em}% +\endgroup + +Utwórz sesję lab na osobnym sockecie, sprawdź ją przez has-session i zamknij wyłącznie testowy serwer. +\begin{figure}[H] + \centering + \includegraphics[width=0.9\linewidth]{\detokenize{../assets/tmux-model.png}} + \caption{Klient łączy się z serwerem, który utrzymuje sesje, okna i panele.} + \label{fig:tmux-model} +\end{figure} +\ESCSectionBlockEnd + +\ESCSectionBlockStart +\section{Task02 · Okna i panele} +\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}{CB02.WE01.OG.IP.01 | WE 01: Tmux. Bezpieczna organizacja pracy terminalowej w tmux. | EN IP\textCR CB02.01: Uczeń analizuje hierarchię tmux. | KW IP CB02.01: Rozróżnia serwer, klienta, sesję,\textCR okno i panel.} \pdftooltip[width=\textwidth]{D2}{CB02.WE01.TECH.I4.01 | WE 01: Tmux. Bezpieczna organizacja pracy terminalowej w tmux. | EK\textCR INF04 CB02.01: Uczeń automatyzuje layout terminala. | KW INF04 CB02.01: Potwierdza liczby\textCR okien/paneli i trwały marker.}\par +\vspace{0.10em}% +\noindent K1: Potwierdź 2 okna i 2 panele shell.\quad D2\par +\par\vspace{0.18em}% +\endgroup + +Utwórz okna shell i editor, a shell podziel na dwa panele. Nazwy i liczności są dowodem layoutu. +\ESCSectionBlockEnd + +\ESCSectionBlockStart +\section{Task03 · Detach i attach} +\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}{CB02.WE01.OG.IP.01 | WE 01: Tmux. Bezpieczna organizacja pracy terminalowej w tmux. | EN IP\textCR CB02.01: Uczeń analizuje hierarchię tmux. | KW IP CB02.01: Rozróżnia serwer, klienta, sesję,\textCR okno i panel.} \pdftooltip[width=\textwidth]{D2}{CB02.WE01.TECH.I4.01 | WE 01: Tmux. Bezpieczna organizacja pracy terminalowej w tmux. | EK\textCR INF04 CB02.01: Uczeń automatyzuje layout terminala. | KW INF04 CB02.01: Potwierdza liczby\textCR okien/paneli i trwały marker.}\par +\vspace{0.10em}% +\noindent K1: Potwierdź marker ready, 2 okna i 3 panele work.\quad D2\par +\par\vspace{0.18em}% +\endgroup + +Niezależne procesy klienta zapisują i odczytują marker z tego samego serwera. To mierzalny odpowiednik detach/attach bez interaktywnego TTY. +\ESCSectionBlockEnd + +\end{document} diff --git a/doc/main.tex b/doc/main.tex new file mode 100644 index 0000000..fa86b19 --- /dev/null +++ b/doc/main.tex @@ -0,0 +1,7 @@ +\newcommand{\PublisherDomain}{mpabi} +\newcommand{\CardArea}{inf} +\newcommand{\CardSeries}{console-bash} +\newcommand{\CardNumber}{02} +\newcommand{\CardSlug}{tmux} +\newcommand{\CardVersion}{v00.01} +\newcommand{\DocumentUUID}{2e06affe-9ecc-5813-8313-fd7b91744f3b} 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..5247487 --- /dev/null +++ b/json/card_source.json @@ -0,0 +1,22 @@ +{ + "$schema": "../../../../tools/card-layouts/schemas/card-source.schema.json", "schema": "esc-card-source.v1", + "card": {"id": "mpabi-inf-console-bash-02-tmux", "series": "console-bash", "series_title": "Bash · Console Tools", "number": "02", "count": "08", "slug": "tmux", "title": "L02 · Tmux — sessions, windows and panes", "topic": "Serwer, sesje, okna, panele i trwałość stanu", "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": "2e06affe-9ecc-5813-8313-fd7b91744f3b", "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-tmux", "revision": "1", "issued_on": "21.07.2026", "series": "CONSOLE-BASH-02", "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ń tworzy izolowaną sesję tmux, organizuje okna i panele oraz dowodzi trwałości stanu między klientami.", "scope_title": "Zakres tasków", "scope_table": {"headers": ["Krok", "Numer tasku", "Najważniejsza idea", "Priorytet", "Status", "Version"], "rows": [{"chapter": "2", "task": "Task01", "idea_tex": "cykl życia sesji", "priority": "obowiązkowe", "status": "ready", "version": "v00.01"}, {"chapter": "2", "task": "Task02", "idea_tex": "okna i panele", "priority": "obowiązkowe", "status": "ready", "version": "v00.01"}, {"chapter": "2", "task": "Task03", "idea_tex": "detach/attach i trwałość", "priority": "obowiązkowe", "status": "ready", "version": "v00.01"}]}}, + "learning_effects": {"CB02.EN01": {"text": "Uczeń wyjaśnia relację klient–serwer–sesja–okno–panel.", "label": "Model tmux", "bloom_level": "Analiza", "assessment_criteria": ["CB02.KW01"]}, "CB02.EK01": {"text": "Uczeń tworzy i mierzy izolowany układ tmux.", "label": "Obsługa tmux", "bloom_level": "Zastosowanie", "assessment_criteria": ["CB02.KW01"]}}, + "assessment_criteria": {"CB02.KW01": {"text": "Sesja, layout dwóch okien oraz trwały stan dwóch klientów dają trzy komunikaty PASS.", "learning_effects": ["CB02.EN01", "CB02.EK01"]}}, + "educational_requirements": {"CB02.WE01": {"text": "Bezpieczna organizacja pracy terminalowej w tmux.", "label": "Tmux", "learning_effects": ["CB02.EN01", "CB02.EK01"], "learning_tree": {"schema": "we-learning-tree.v1", "policy": "Testy używają osobnego socketu i nigdy nie dotykają sesji użytkownika.", "ogolne": [{"tree_id": "CB02.WE01.OG.IP.01", "effect_ref": "CB02.EN01", "display": "EN IP CB02.01", "text": "Uczeń analizuje hierarchię tmux.", "kw": [{"criterion_ref": "CB02.KW01", "display": "KW IP CB02.01", "text": "Rozróżnia serwer, klienta, sesję, okno i panel."}]}], "zawodowe": [{"tree_id": "CB02.WE01.TECH.I4.01", "effect_ref": "CB02.EK01", "display": "EK INF04 CB02.01", "text": "Uczeń automatyzuje layout terminala.", "kw": [{"criterion_ref": "CB02.KW01", "display": "KW INF04 CB02.01", "text": "Potwierdza liczby okien/paneli i trwały marker."}]}]}}}, + "sections": [ + {"title": "Task01 · Cykl życia sesji", "content_kind": "prose", "content_tex": "Utwórz sesję lab na osobnym sockecie, sprawdź ją przez has-session i zamknij wyłącznie testowy serwer.", "educational_requirement_refs": ["CB02.WE01"], "learning_effect_refs": ["CB02.EN01", "CB02.EK01"], "assessment_criterion_refs": ["CB02.KW01"], "area_tree_refs": ["CB02.WE01.OG.IP.01", "CB02.WE01.TECH.I4.01"], "steps": [{"id": "T01-SESSION", "title": "Zmierz jedną sesję lab na izolowanym sockecie.", "tree_refs": ["CB02.WE01.TECH.I4.01"]}], "assets": [{"path": "assets/tmux-model.png", "html_path": "assets/tmux-model.svg", "caption": "Klient łączy się z serwerem, który utrzymuje sesje, okna i panele.", "label": "fig:tmux-model", "alt": "Diagram klient, serwer, sesja, okno i panele.", "kind": "diagram", "width": 0.9}]}, + {"title": "Task02 · Okna i panele", "content_kind": "prose", "content_tex": "Utwórz okna shell i editor, a shell podziel na dwa panele. Nazwy i liczności są dowodem layoutu.", "educational_requirement_refs": ["CB02.WE01"], "learning_effect_refs": ["CB02.EN01", "CB02.EK01"], "assessment_criterion_refs": ["CB02.KW01"], "area_tree_refs": ["CB02.WE01.OG.IP.01", "CB02.WE01.TECH.I4.01"], "steps": [{"id": "T02-LAYOUT", "title": "Potwierdź 2 okna i 2 panele shell.", "tree_refs": ["CB02.WE01.TECH.I4.01"]}]}, + {"title": "Task03 · Detach i attach", "content_kind": "prose", "content_tex": "Niezależne procesy klienta zapisują i odczytują marker z tego samego serwera. To mierzalny odpowiednik detach/attach bez interaktywnego TTY.", "educational_requirement_refs": ["CB02.WE01"], "learning_effect_refs": ["CB02.EN01", "CB02.EK01"], "assessment_criterion_refs": ["CB02.KW01"], "area_tree_refs": ["CB02.WE01.OG.IP.01", "CB02.WE01.TECH.I4.01"], "steps": [{"id": "T03-PERSIST", "title": "Potwierdź marker ready, 2 okna i 3 panele work.", "tree_refs": ["CB02.WE01.TECH.I4.01"]}]} + ], + "tasks": {"task01": {"prompt_tex": "Utwórz i sprawdź izolowaną sesję.", "criterion": "session=lab sessions=1 isolated_socket=1", "assessment_criterion_ref": "CB02.KW01"}, "task02": {"prompt_tex": "Utwórz dwa okna i podziel shell.", "criterion": "windows=2 shell_panes=2 names=editor,shell", "assessment_criterion_ref": "CB02.KW01"}, "task03": {"prompt_tex": "Potwierdź stan zachowany między klientami.", "criterion": "reattach_state=preserved windows=2 work_panes=3 marker=ready", "assessment_criterion_ref": "CB02.KW01"}}, + "tasks_order": ["task01", "task02", "task03"], "mission": "Zorganizować trwały i mierzalny warsztat terminalowy tmux.", "objectives": ["Utworzysz sesję.", "Zbudujesz layout okien i paneli.", "Wyjaśnisz detach/attach."], + "agenda": [{"time": "0--20 min", "work": "Sesja i socket."}, {"time": "20--40 min", "work": "Okna i panele."}, {"time": "40--60 min", "work": "Trwałość stanu i trace."}], + "tech_stack": {"software": [{"name": "tmux", "use": "serwer terminalowy"}, {"name": "POSIX shell", "use": "izolowany test"}, {"name": "stemctl", "use": "profil native-amd64"}], "hardware": [{"name": "Brak", "use": "laboratorium kontenerowe"}]}, + "container_test": {"title": "Test native-amd64", "commands": ["stemctl test native-amd64 console-bash L02 1", "stemctl test native-amd64 console-bash L02 2", "stemctl debug native-amd64 console-bash L02 3"], "pass_condition": "Trzy komunikaty PASS i trace.log."}, + "safety_rules": [{"title": "Osobny socket", "body": "Każdy task używa unikatowego tmux -L."}, {"title": "Kontrolowane procesy", "body": "Trap zamyka wyłącznie serwer testowy."}], + "hardware_procedure": [{"step": "1", "action": "Utwórz sesję.", "condition": "Jedna sesja lab."}, {"step": "2", "action": "Zbuduj layout.", "condition": "Dwa okna i dwa panele."}, {"step": "3", "action": "Sprawdź trwałość.", "condition": "Marker ready odczytany przez kolejnego klienta."}], "references": [] +} diff --git a/lesson-flow.yaml b/lesson-flow.yaml new file mode 100644 index 0000000..64b859f --- /dev/null +++ b/lesson-flow.yaml @@ -0,0 +1,37 @@ +schema: 1 +card: console-bash/L02 +title: Tmux — sessions, windows and panes +steps: + - id: session + task: task01_session_lifecycle + action: test + profile: native-amd64 + target: native + command: stemctl test native-amd64 console-bash L02 1 + board: + show: [tmux_server, socket_label, session_lab, client_command] + highlight: [isolated_socket, new_session, has_session, kill_server] + question: Co działa dalej po zamknięciu pojedynczego klienta terminala? + evidence: [tmux_version, session_count, isolated_socket] + - id: layout + task: task02_windows_and_panes + action: test + profile: native-amd64 + target: native + command: stemctl test native-amd64 console-bash L02 2 + board: + show: [session_lab, windows, shell_panes, active_window] + highlight: [window_shell, window_editor, horizontal_split] + question: Czym różni się okno od panelu? + evidence: [window_count, pane_count, window_names] + - id: persistence + task: task03_detach_attach_state + action: debug + profile: native-amd64 + target: native + command: stemctl debug native-amd64 console-bash L02 3 + board: + show: [client_1, server_state, client_2, marker, trace_log] + highlight: [detach_model, attach_model, persistent_server] + question: Dlaczego drugi klient widzi marker ustawiony przez pierwszy? + evidence: [reattach_state, windows, panes, marker, trace_log] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..59272f7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,811 @@ +{ + "name": "mpabi-inf-console-bash-02-tmux", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mpabi-inf-console-bash-02-tmux", + "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..2d78aaf --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "mpabi-inf-console-bash-02-tmux", + "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/tasks/task01_session_lifecycle.sh b/src/tasks/task01_session_lifecycle.sh new file mode 100755 index 0000000..2830878 --- /dev/null +++ b/src/tasks/task01_session_lifecycle.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +socket="cb02-session-$$"; cleanup(){ tmux -L "$socket" kill-server 2>/dev/null || true; }; trap cleanup EXIT +tmux -L "$socket" new-session -d -s lab -n shell 'sleep 60' +tmux -L "$socket" has-session -t lab +session_count="$(tmux -L "$socket" list-sessions -F '#{session_name}' | wc -l)" +session_name="$(tmux -L "$socket" display-message -p -t lab '#{session_name}')" +[[ "$session_count" -eq 1 && "$session_name" == lab ]] +printf 'PASS task01 session=lab sessions=1 isolated_socket=1 tmux=%s\n' "$(tmux -V | tr ' ' '-')" diff --git a/src/tasks/task02_windows_and_panes.sh b/src/tasks/task02_windows_and_panes.sh new file mode 100755 index 0000000..9104135 --- /dev/null +++ b/src/tasks/task02_windows_and_panes.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +set -euo pipefail +socket="cb02-layout-$$"; cleanup(){ tmux -L "$socket" kill-server 2>/dev/null || true; }; trap cleanup EXIT +tmux -L "$socket" new-session -d -s lab -n shell 'sleep 60' +tmux -L "$socket" split-window -h -t lab:shell 'sleep 60' +tmux -L "$socket" new-window -d -t lab -n editor 'sleep 60' +window_count="$(tmux -L "$socket" list-windows -t lab -F '#{window_name}' | wc -l)" +pane_count="$(tmux -L "$socket" list-panes -t lab:shell -F '#{pane_id}' | wc -l)" +window_names="$(tmux -L "$socket" list-windows -t lab -F '#{window_name}' | sort | paste -sd, -)" +[[ "$window_count" -eq 2 && "$pane_count" -eq 2 && "$window_names" == editor,shell ]] +printf 'PASS task02 windows=2 shell_panes=2 names=%s split=horizontal\n' "$window_names" diff --git a/src/tasks/task03_detach_attach_state.sh b/src/tasks/task03_detach_attach_state.sh new file mode 100755 index 0000000..9eabc34 --- /dev/null +++ b/src/tasks/task03_detach_attach_state.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +socket="cb02-persist-$$"; cleanup(){ tmux -L "$socket" kill-server 2>/dev/null || true; }; trap cleanup EXIT +tmux -L "$socket" new-session -d -s persist -n work 'sleep 60' +tmux -L "$socket" split-window -h -t persist:work 'sleep 60' +tmux -L "$socket" split-window -v -t persist:work.1 'sleep 60' +tmux -L "$socket" new-window -d -t persist -n logs 'sleep 60' +tmux -L "$socket" set-option -t persist @lesson_marker ready +# Każda poniższa komenda jest nowym procesem klienta połączonym z tym samym serwerem. +marker="$(tmux -L "$socket" show-options -v -t persist @lesson_marker)" +windows="$(tmux -L "$socket" list-windows -t persist -F '#{window_id}' | wc -l)" +panes="$(tmux -L "$socket" list-panes -t persist:work -F '#{pane_id}' | wc -l)" +[[ "$marker" == ready && "$windows" -eq 2 && "$panes" -eq 3 ]] +printf 'PASS task03 reattach_state=preserved windows=2 work_panes=3 marker=ready\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..ca596ef --- /dev/null +++ b/tests/card_contract.test.mjs @@ -0,0 +1,5 @@ +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('L02 identity and task order are stable',()=>{assert.equal(source.card.number,'02');assert.equal(source.card.uuid,'2e06affe-9ecc-5813-8313-fd7b91744f3b');assert.deepEqual(source.tasks_order,['task01','task02','task03']);}); +test('session, window, pane and persistence evidence are explicit',()=>{const text=JSON.stringify(source);for(const token of ['session','window','pane','reattach','trace.log'])assert.match(text,new RegExp(token,'i'));}); +test('all task sources exist',async()=>{for(const name of ['task01_session_lifecycle','task02_windows_and_panes','task03_detach_attach_state'])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..67b0845 --- /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 is required}" +case "$task" in task01_session_lifecycle|task02_windows_and_panes|task03_detach_attach_state) ;; *) 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..c5f4556 --- /dev/null +++ b/tools/card-action.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail +action="${1:?action is 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_session_lifecycle task02_windows_and_panes task03_detach_attach_state) +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; }; printf 'Unknown task: %s\n' "$1" >&2; 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"; command -v tmux >/dev/null; printf 'BUILD tasks=3 tmux=%s\n' "$(tmux -V | tr ' ' '-')" ;; + 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" ;; + *) printf 'Unsupported profile/target/action: %s/%s/%s\n' "$profile" "$target" "$action" >&2; 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..34c8908 --- /dev/null +++ b/web/card-data.json @@ -0,0 +1,2660 @@ +{ + "schema": "esc-card-react-view.v1", + "card": { + "id": "mpabi-inf-console-bash-02-tmux", + "series": "console-bash", + "series_title": "Bash · Console Tools", + "number": "02", + "count": "08", + "slug": "tmux", + "title": "L02 · Tmux — sessions, windows and panes", + "topic": "Serwer, sesje, okna, panele i trwałość stanu", + "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": "2e06affe-9ecc-5813-8313-fd7b91744f3b", + "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": true, + "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": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "03", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L04", + "title": "Git Basics I — status, add and commit", + "repo": "lab-console-git-basics-1", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "04", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L05", + "title": "Git Basics II — log, diff and restore", + "repo": "lab-console-git-basics-2", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "05", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L06", + "title": "Git Branching I — branch, switch and merge", + "repo": "lab-console-git-branching-1", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "06", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L07", + "title": "Git Branching II — rebase and conflicts", + "repo": "lab-console-git-branching-2", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "07", + "version": "", + "status": "planned", + "tasks": [] + }, + { + "id": "L08", + "title": "Docker Fundamentals", + "repo": "lab-console-docker", + "href": "", + "local_href": "", + "resource_uuid": "", + "available": false, + "current": false, + "number": "08", + "version": "", + "status": "planned", + "tasks": [] + } + ] + }, + { + "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": "
TITLEL02 · Tmux — sessions, windows and panes
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD02/08
SHEET1/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUID2e06affe-9ecc-5813-8313-fd7b91744f3bCARD UUID2e06affe-9ecc-5813-8313-fd7b91744f3bAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID 2e06affe-9ecc-5813-8313-fd7b91744f3bM. PabiszczakSTRONA 1/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "goal_title": "Cel karty", + "goal_html": "

Uczeń tworzy izolowaną sesję tmux, organizuje okna i panele oraz dowodzi trwałości stanu między klientami.

", + "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": "2", + "task": "Task01", + "idea_html": "

cykl życia sesji

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

okna i panele

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

detach/attach i trwałość

", + "priority": "obowiązkowe", + "status": "ready", + "status_label": "opracowane", + "version": "v00.01", + "key": false + } + ] + }, + "sections": [ + { + "id": "section-1-content", + "index": 1, + "title": "Task01 · Cykl życia sesji", + "show_heading": true, + "toc_entry": true, + "header_html": "
TITLEL02 · Tmux — sessions, windows and panes
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD02/08
SHEET2/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUID2e06affe-9ecc-5813-8313-fd7b91744f3bCARD UUID2e06affe-9ecc-5813-8313-fd7b91744f3bAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID 2e06affe-9ecc-5813-8313-fd7b91744f3bM. PabiszczakSTRONA 2/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "content_html": "

Utwórz sesję lab na osobnym sockecie, sprawdź ją przez has-session i zamknij wyłącznie testowy serwer.

", + "tasks": [], + "assets": [ + { + "label": "fig:tmux-model", + "logical_label": "fig:tmux-model", + "figure_number": 1, + "caption": "Klient łączy się z serwerem, który utrzymuje sesje, okna i panele.", + "alt": "Diagram klient, serwer, sesja, okno i panele.", + "href": "figures/tmux-model.svg", + "source_href": "", + "width": 0.9, + "kind": "diagram", + "interactive": null, + "tile": null, + "diagram_title": "Klient łączy się z serwerem, który utrzymuje sesje, okna i panele.", + "diagram_block_label": "", + "diagram_description": "", + "diagram_phase_labels": [] + } + ], + "steps": [ + { + "id": "T01-SESSION", + "title": "Zmierz jedną sesję lab na izolowanym sockecie." + } + ], + "spread_group": "", + "spread_label": "", + "spread_columns": 1, + "spread_index": 1, + "spread_count": 1, + "page_orientation": "portrait" + }, + { + "id": "section-2-content", + "index": 2, + "title": "Task02 · Okna i panele", + "show_heading": true, + "toc_entry": true, + "header_html": "
TITLEL02 · Tmux — sessions, windows and panes
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD02/08
SHEET3/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUID2e06affe-9ecc-5813-8313-fd7b91744f3bCARD UUID2e06affe-9ecc-5813-8313-fd7b91744f3bAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID 2e06affe-9ecc-5813-8313-fd7b91744f3bM. PabiszczakSTRONA 3/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "content_html": "

Utwórz okna shell i editor, a shell podziel na dwa panele. Nazwy i liczności są dowodem layoutu.

", + "tasks": [], + "assets": [], + "steps": [ + { + "id": "T02-LAYOUT", + "title": "Potwierdź 2 okna i 2 panele shell." + } + ], + "spread_group": "", + "spread_label": "", + "spread_columns": 1, + "spread_index": 1, + "spread_count": 1, + "page_orientation": "portrait" + }, + { + "id": "section-3-content", + "index": 3, + "title": "Task03 · Detach i attach", + "show_heading": true, + "toc_entry": true, + "header_html": "
TITLEL02 · Tmux — sessions, windows and panes
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD02/08
SHEET4/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUID2e06affe-9ecc-5813-8313-fd7b91744f3bCARD UUID2e06affe-9ecc-5813-8313-fd7b91744f3bAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID 2e06affe-9ecc-5813-8313-fd7b91744f3bM. PabiszczakSTRONA 4/5
", + "left_margin_html": "
TECH
", + "right_margin_html": "
OG
", + "content_html": "

Niezależne procesy klienta zapisują i odczytują marker z tego samego serwera. To mierzalny odpowiednik detach/attach bez interaktywnego TTY.

", + "tasks": [], + "assets": [], + "steps": [ + { + "id": "T03-PERSIST", + "title": "Potwierdź marker ready, 2 okna i 3 panele work." + } + ], + "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": "
TITLEL02 · Tmux — sessions, windows and panes
VER.1
DATETIME21.07.2026
PROJECTWarsztat terminalowy
SERIESBash · Console Tools
CARD02/08
SHEET5/5
SUBJ.Inf.
PROG.
CORE
SCOPE
LEVEL
POS.
GITEA UUID2e06affe-9ecc-5813-8313-fd7b91744f3bCARD UUID2e06affe-9ecc-5813-8313-fd7b91744f3bAUTHOR  M. Pabiszczak
QRHTML wyłączony
", + "footer_html": "
UUID 2e06affe-9ecc-5813-8313-fd7b91744f3bM. PabiszczakSTRONA 5/5
", + "content_html": "
WE 01 Tmux

Bezpieczna organizacja pracy terminalowej w tmux.

OG

EN IP CB02.01 Uczeń analizuje hierarchię tmux.
  • KW IP CB02.01 Rozróżnia serwer, klienta, sesję, okno i panel.

TECH

EK INF04 CB02.01 Uczeń automatyzuje layout terminala.
  • KW INF04 CB02.01 Potwierdza liczby okien/paneli i trwały marker.
" + }, + "toc": [ + { + "id": "section-1-content", + "label": "1. Task01 · Cykl życia sesji" + }, + { + "id": "section-2-content", + "label": "2. Task02 · Okna i panele" + }, + { + "id": "section-3-content", + "label": "3. Task03 · Detach i attach" + } + ] +} diff --git a/web/figures/tmux-model.svg b/web/figures/tmux-model.svg new file mode 100644 index 0000000..09fad0c --- /dev/null +++ b/web/figures/tmux-model.svg @@ -0,0 +1 @@ +Model tmuxKlienci łączą się z serwerem, który utrzymuje sesje zawierające okna i panele.CLIENTattach/detachSERVERsocketSESSIONwindowsWINDOWpanesdetach zamyka klienta — serwer i procesy pozostają 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..31adb62 --- /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 L02 · Tmux — sessions, windows and panes 1 21.07.2026 \A PROJECT SERIES CARD SHEET \A \A Warsztat terminalowy Bash · Console Tools 02/08 " counter(page) "/" counter(pages) " \A SUBJ. PROG. CORE SCOPE LEVEL POS. GITEA UUID 2e06affe-9ecc-5813-8313-fd7b91744f3b\A CARD UUID 2e06affe-9ecc-5813-8313-fd7b91744f3b AUTHOR M. Pabiszczak\A Inf. — — — — — \A \A GITEA https://gitea.local/edu-inf/lab-console-tmux \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 2e06affe-9ecc-5813-8313-fd7b91744f3b";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} +}