devcoach 2.4.0 → 2.5.0-next.580.g7777f02

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-FIIE7I2N.js';import {T as T$1,p,r,J as J$1,ba,fa,ca,D,H,E as E$1,I,da,F as F$1,G as G$1,x,ga,U,Y as Y$1,z as z$1,y as y$1,B as B$1,s,v,ea,u,A,K,n,aa,$,R,S,k,W,V as V$1,X,_,Z}from'./chunk-CTBMPKAV.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function T(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Le(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,o=>`_${(o.codePointAt(0)??0).toString(16)}`)}var Ge=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Ee(t,e="",o=""){return html`<div class="relative ${e}" ${raw(o)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
- <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100">${Ge}</button>
2
+ import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j$1,l,n as n$1}from'./chunk-3NJXATLN.js';import {ha,u,w,N,sa,wa,ta,H,L as L$1,I,M,ua,J as J$1,K as K$1,B as B$1,xa,ia,ma,D,C,F,x,A,va,z,E,O,s,ra,qa,ea,ga,n,ka,ja,la,oa,na}from'./chunk-W26MDJJQ.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Ge(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var rt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function J(t,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
+ <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100">${rt}</button>
4
4
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-56 p-1" style="display:none">
5
5
  ${t}
6
6
  </div>
7
- </div>`}function Re(t,e,o,r,s){return html`<div x-show="${t}" style="display:none" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="fixed inset-0 z-[80] flex items-center justify-center px-4">
7
+ </div>`}function Y(t,e,g,d,r){return html`<div x-show="${t}" style="display:none" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="fixed inset-0 z-[80] flex items-center justify-center px-4">
8
8
  <div class="absolute inset-0 bg-gray-900/50 dark:bg-black/60" @click="${e}"></div>
9
9
  <div role="alertdialog" aria-modal="true" class="relative w-full max-w-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl p-5">
10
10
  <div class="flex items-start gap-3">
11
11
  <span class="w-9 h-9 rounded-full bg-rose-100 dark:bg-rose-900/40 text-rose-600 dark:text-rose-300 inline-flex items-center justify-center shrink-0" aria-hidden="true">🗑</span>
12
12
  <div class="min-w-0 flex-1">
13
- <h2 class="text-base font-semibold text-gray-900 dark:text-white">${o}</h2>
14
- <div class="mt-1 text-sm text-gray-500 dark:text-gray-400 space-y-2">${r}</div>
13
+ <h2 class="text-base font-semibold text-gray-900 dark:text-white">${g}</h2>
14
+ <div class="mt-1 text-sm text-gray-500 dark:text-gray-400 space-y-2">${d}</div>
15
15
  </div>
16
16
  </div>
17
- <div class="mt-5 flex justify-end gap-2">${s}</div>
17
+ <div class="mt-5 flex justify-end gap-2">${r}</div>
18
18
  </div>
19
- </div>`}function E(t){let e=(o,r,s)=>html`<a href="${o}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${s?"text-gray-900 dark:text-white font-semibold":""}">${r}</a>`;return html`<!DOCTYPE html>
19
+ </div>`}function P(t){let e=(g,d,r)=>html`<a href="${g}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${r?"text-gray-900 dark:text-white font-semibold":""}">${d}</a>`;return html`<!DOCTYPE html>
20
20
  <html lang="en">
21
21
  <head>
22
22
  <meta charset="UTF-8" />
@@ -49,13 +49,14 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-F
49
49
  >
50
50
  ${e("/knowledge","Profile",t.currentPath==="/knowledge")}
51
51
  ${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
52
+ ${e("/courses","Courses",t.currentPath.includes("/courses"))}
52
53
  <div class="ml-auto flex items-center gap-1.5">
53
54
  <a href="/settings" title="Settings" ${t.currentPath==="/settings"?raw('aria-current="page"'):""} class="h-8 px-2.5 inline-flex items-center gap-1.5 rounded-lg border text-sm font-medium transition ${t.currentPath==="/settings"?"bg-white dark:bg-gray-900 border-indigo-400 text-gray-900 dark:text-white":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100"}"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.56 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.65 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6h.09A1.7 1.7 0 0 0 10.1 3.1V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.09a1.7 1.7 0 0 0 1.56 1.03H21a2 2 0 1 1 0 4h-.09A1.7 1.7 0 0 0 19.4 15Z"/></svg><span>Settings</span></a>
54
55
  <button id="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme"
55
56
  class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 text-base leading-none"></button>
56
57
  </div>
57
58
  </nav>
58
- <main class="max-w-7xl mx-auto px-4 sm:px-6 py-8">${t.body}</main>
59
+ <main class="${t.wide?"max-w-none":"max-w-7xl"} mx-auto px-4 sm:px-6 py-8">${t.body}</main>
59
60
  <script>
60
61
  function isDark() { return document.documentElement.classList.contains('dark'); }
61
62
  function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
@@ -76,7 +77,7 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-F
76
77
  </script>
77
78
  ${t.scripts??""}
78
79
  </body>
79
- </html>`}function Me(t){let e=r=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===r?"selected":""}>${s}</option>`),o=html`
80
+ </html>`}function ze(t){let e=d=>t.allGroups.filter(r=>r!=="Other").map(r=>html`<option value="${r}" ${r===d?"selected":""}>${r}</option>`),g=html`
80
81
  <div id="knowledge-map" x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
81
82
 
82
83
  <div class="flex items-center justify-between mb-6">
@@ -135,12 +136,12 @@ import {a,c as c$1}from'./chunk-QU6UQJWC.js';import {c,d,a as a$1}from'./chunk-F
135
136
  ${t.rateLimit.allowed?html`<span class="text-green-600 dark:text-green-400 font-medium">Available now</span>`:html`<span class="text-yellow-600 dark:text-yellow-400">${t.rateLimit.reason}</span>`}
136
137
  </div>
137
138
 
138
- ${Object.entries(t.categorised).map(([r,s])=>html`
139
+ ${Object.entries(t.categorised).map(([d,r])=>html`
139
140
  <section class="mb-6 group/section">
140
141
  <div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
141
142
  <div class="flex items-center gap-1 flex-1 min-w-0">
142
- <h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${r}</h2>
143
- ${r!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(r)}/delete" hx-post="/groups/${encodeURIComponent(r)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove group &quot;${r}&quot;? Topics will move to Other.">
143
+ <h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${d}</h2>
144
+ ${d!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(d)}/delete" hx-post="/groups/${encodeURIComponent(d)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove group &quot;${d}&quot;? Topics will move to Other.">
144
145
  <button type="submit" class="text-xs text-gray-300 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/section:opacity-100" title="Delete group">×</button>
145
146
  </form>`:""}
146
147
  </div>
@@ -148,7 +149,7 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
148
149
  <button type="button" @click="open = !open" class="text-xs text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition px-1">+ topic</button>
149
150
  <div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-3 w-64" style="display:none">
150
151
  <form method="post" action="/knowledge" hx-post="/knowledge" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="space-y-2">
151
- <input type="hidden" name="group" value="${r!=="Other"?r:""}" />
152
+ <input type="hidden" name="group" value="${d!=="Other"?d:""}" />
152
153
  <input type="text" name="topic" placeholder="topic_id (e.g. rust_lifetimes)" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
153
154
  <div class="flex items-center gap-2">
154
155
  <label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
@@ -160,45 +161,45 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
160
161
  </div>
161
162
  </div>
162
163
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
163
- ${s.map(n=>{let l=Le("topic-row-",n.topic),g=n.confidence,c=Math.trunc(g/10*100),i=g>=7?"bg-green-500":g>=4?"bg-yellow-500":"bg-red-500",p=g>=7?"text-green-600 dark:text-green-400":g>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
164
- <div id="${l}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
164
+ ${r.map(o=>{let n=Ge("topic-row-",o.topic),i=o.confidence,s=Math.trunc(i/10*100),c=i>=7?"bg-green-500":i>=4?"bg-yellow-500":"bg-red-500",u=i>=7?"text-green-600 dark:text-green-400":i>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
165
+ <div id="${n}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
165
166
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
166
167
  <button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-indigo-500 dark:hover:text-indigo-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
167
168
  <div x-show="open" @click.outside="open = false" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-1 w-40" style="display:none">
168
- <form method="post" action="/knowledge/${encodeURIComponent(n.topic)}/group" hx-post="/knowledge/${encodeURIComponent(n.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
169
+ <form method="post" action="/knowledge/${encodeURIComponent(o.topic)}/group" hx-post="/knowledge/${encodeURIComponent(o.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
169
170
  <select name="group" onchange="this.form.requestSubmit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
170
- <option value="Other" ${r==="Other"?"selected":""}>Other (ungrouped)</option>
171
- ${e(r)}
171
+ <option value="Other" ${d==="Other"?"selected":""}>Other (ungrouped)</option>
172
+ ${e(d)}
172
173
  </select>
173
174
  </form>
174
175
  </div>
175
176
  </div>
176
177
  <div class="flex items-center gap-1 flex-1 min-w-0">
177
- <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(n.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${n.topic}">${n.topic}</a>
178
- <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${n.topic}">${n.topic}</span>
179
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(n.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${n.topic} from your knowledge map?" class="shrink-0 flex items-center">
178
+ <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(o.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${o.topic}">${o.topic}</a>
179
+ <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${o.topic}">${o.topic}</span>
180
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(o.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(o.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${o.topic} from your knowledge map?" class="shrink-0 flex items-center">
180
181
  <button type="submit" class="text-xs text-gray-200 dark:text-gray-700 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Remove topic">×</button>
181
182
  </form>
182
183
  </div>
183
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${l}" hx-select="#${l}" hx-swap="outerHTML" class="shrink-0 flex items-center">
184
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(o.topic)}" hx-post="/knowledge/${encodeURIComponent(o.topic)}" hx-target="#${n}" hx-select="#${n}" hx-swap="outerHTML" class="shrink-0 flex items-center">
184
185
  <input type="hidden" name="delta" value="-1" />
185
186
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 transition w-5 text-center leading-none">−</button>
186
187
  </form>
187
188
  <div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
188
- <div class="${i} h-full rounded-full transition-all" style="width: ${c}%"></div>
189
+ <div class="${c} h-full rounded-full transition-all" style="width: ${s}%"></div>
189
190
  </div>
190
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(n.topic)}" hx-post="/knowledge/${encodeURIComponent(n.topic)}" hx-target="#${l}" hx-select="#${l}" hx-swap="outerHTML" class="shrink-0 flex items-center">
191
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(o.topic)}" hx-post="/knowledge/${encodeURIComponent(o.topic)}" hx-target="#${n}" hx-select="#${n}" hx-swap="outerHTML" class="shrink-0 flex items-center">
191
192
  <input type="hidden" name="delta" value="1" />
192
193
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-green-600 dark:hover:text-green-400 transition w-5 text-center leading-none">+</button>
193
194
  </form>
194
- <span class="text-xs font-bold ${p} w-5 text-right shrink-0">${g}</span>
195
+ <span class="text-xs font-bold ${u} w-5 text-right shrink-0">${i}</span>
195
196
  </div>`})}
196
197
  </div>
197
198
  </section>`)}
198
- </div>`;return E({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:t.uiTheme,body:o})}var Te={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},je={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},Pe={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function y(t,e={}){let o={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,imported:t.imported,shared_by:t.shared_by,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(o.starred="1"),Object.assign(o,e);let r=new URLSearchParams;for(let[s,n]of Object.entries(o))n&&r.set(s,n);return `?${r.toString()}`}function Be(t){let e=t.s,o=!!(e.date_from||e.date_to),r=e.period!=="all"&&!o||o||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),s=o?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Te[e.period]??"All time",n=e.feedback?je[e.feedback]??"Feedback":"Feedback",l=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",g=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...t.allSharedBy.map(d=>["1",d,`\u{1F91D} from ${d}`])],c={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},i=(d,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${d}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,p=(d,x,b="")=>{let w=e.sort===x,j=w&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${b}">
199
- <a href="${y(e,{sort:x,order:j})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${d}
200
- ${w?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
201
- </a></th>`},h=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,P=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
199
+ </div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:t.uiTheme,body:g})}var Ne={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},He={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},qe={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(t,e={}){let g={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,imported:t.imported,shared_by:t.shared_by,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[r,o]of Object.entries(g))o&&d.set(r,o);return `?${d.toString()}`}function Ke(t){let e=t.s,g=!!(e.date_from||e.date_to),d=e.period!=="all"&&!g||g||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),r=g?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Ne[e.period]??"All time",o=e.feedback?He[e.feedback]??"Feedback":"Feedback",n=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",i=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...t.allSharedBy.map(l=>["1",l,`\u{1F91D} from ${l}`])],s={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},c=(l,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${l}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,u=(l,x,$="")=>{let S=e.sort===x,k=S&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${$}">
200
+ <a href="${h(e,{sort:x,order:k})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${l}
201
+ ${S?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
202
+ </a></th>`},p=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,m=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
202
203
  <style>
203
204
  .dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
204
205
  .dark .flatpickr-day { color:#e5e7eb; }
@@ -209,7 +210,7 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
209
210
  .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
210
211
  .dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
211
212
  .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
212
- </style>`,_=t.total===0?"No lessons":t.totalPages===1?`${t.total} lesson${t.total!==1?"s":""}`:`${(t.page-1)*t.perPage+1}\u2013${Math.min(t.page*t.perPage,t.total)} of ${t.total}`,$=JSON.stringify(t.lessons.map(d=>d.id)),R=html`
213
+ </style>`,v=t.total===0?"No lessons":t.totalPages===1?`${t.total} lesson${t.total!==1?"s":""}`:`${(t.page-1)*t.perPage+1}\u2013${Math.min(t.page*t.perPage,t.total)} of ${t.total}`,I=JSON.stringify(t.lessons.map(l=>l.id)),R=html`
213
214
  <div :class="selectMode && 'dc-selecting'" x-data="{ selectMode: false, selected: [], confirmOpen: false, shareOpen: false, sharePos: { left: 0, top: null, bottom: null }, openShare(btn) { const r = btn.getBoundingClientRect(); const w = 320; const left = Math.max(8, Math.min(r.right - w, innerWidth - w - 8)); const below = innerHeight - r.bottom > 380; this.sharePos = { left, top: below ? r.bottom + 6 : null, bottom: below ? null : innerHeight - r.top + 6 }; this.shareOpen = true }, toggle(id) { const i = this.selected.indexOf(id); if (i < 0) this.selected.push(id); else this.selected.splice(i, 1) }, setAll(ids, on) { this.selected = on ? ids.slice() : [] }, selectedTitles() { const rows = Array.from(document.querySelectorAll('tr[data-id]')); return this.selected.map((id) => { const r = rows.find((x) => x.dataset.id === id); return r ? r.dataset.title : id }) }, leave() { this.selectMode = false; this.selected = []; this.shareOpen = false; this.confirmOpen = false } }" @keydown.escape.window="leave()">
214
215
  <form id="filter-form" method="get" action="/lessons">
215
216
  <input type="hidden" name="period" id="h-period" value="${e.period}">
@@ -232,7 +233,7 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
232
233
  <input type="text" name="search" value="${e.search}" placeholder="Search lessons…" autocomplete="off" class="w-full pl-9 pr-10 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />
233
234
  ${e.search?html`<button type="submit" name="search" value="" class="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-xl leading-none">×</button>`:""}
234
235
  </div>
235
- <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${_}</p>
236
+ <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${v}</p>
236
237
  </div>
237
238
 
238
239
  <div class="flex flex-wrap items-center gap-2 mb-3">
@@ -240,22 +241,22 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
240
241
  class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.starred?"bg-yellow-400 text-yellow-900 border-yellow-400":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-yellow-400 hover:text-yellow-500"}">★ Starred</button>
241
242
 
242
243
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
243
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.imported||e.shared_by?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${l}${h}</button>
244
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.imported||e.shared_by?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${n}${p}</button>
244
245
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-52 p-1 overflow-hidden" style="display:none">
245
- ${g.map(([d,x,b])=>{let w=e.imported===d&&e.shared_by===x;return html`<button type="button" data-imported="${d}" data-shared-by="${x}" onclick="var d=this.dataset; document.getElementById('h-imported').value=d.imported; document.getElementById('h-shared-by').value=d.sharedBy; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${w?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}"><span class="truncate">${b}</span>${w?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`})}
246
+ ${i.map(([l,x,$])=>{let S=e.imported===l&&e.shared_by===x;return html`<button type="button" data-imported="${l}" data-shared-by="${x}" onclick="var d=this.dataset; document.getElementById('h-imported').value=d.imported; document.getElementById('h-shared-by').value=d.sharedBy; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${S?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}"><span class="truncate">${$}</span>${S?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`})}
246
247
  </div>
247
248
  </div>
248
249
 
249
250
  <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
250
- <button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${o||e.period!=="all"?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">
251
- <span>📅</span><span x-text="label">${s}</span>${h}
251
+ <button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${g||e.period!=="all"?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">
252
+ <span>📅</span><span x-text="label">${r}</span>${p}
252
253
  </button>
253
254
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
254
255
  <div class="p-1">
255
- ${Object.entries(Te).map(([d,x])=>{let b=e.period===d&&!o;return html`<button type="button" @click="selectPreset('${d}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${b?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${b?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
256
+ ${Object.entries(Ne).map(([l,x])=>{let $=e.period===l&&!g;return html`<button type="button" @click="selectPreset('${l}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${$?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${$?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
256
257
  </div>
257
258
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
258
- <button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${o?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
259
+ <button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${g?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
259
260
  <span>Custom range</span>
260
261
  <svg class="w-3 h-3 opacity-60 transition-transform" :class="showCustom ? 'rotate-180' : ''" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>
261
262
  </button>
@@ -267,36 +268,36 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
267
268
  </div>
268
269
 
269
270
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
270
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${n}${h}</button>
271
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${o}${p}</button>
271
272
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-44 p-1 overflow-hidden" style="display:none">
272
- ${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([d,x])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${d}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.feedback===d?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.feedback===d?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
273
+ ${[["","All feedback"],["know","\u2713 Knew it"],["understood","\u{1F4A1} Understood"],["dont_know","\u2717 Couldn't follow"],["none","\u2014 No response"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.feedback===l?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.feedback===l?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
273
274
  </div>
274
275
  </div>
275
276
 
276
277
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
277
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${Pe[e.level]??""} ${e.level}`:"Level"}${h}</button>
278
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${qe[e.level]??""} ${e.level}`:"Level"}${p}</button>
278
279
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
279
- ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([d,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${d}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===d?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.level===d?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
280
+ ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===l?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.level===l?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
280
281
  </div>
281
282
  </div>
282
283
 
283
284
  ${t.allCategories.length||t.allProjects.length||t.allRepositories.length||t.allBranches.length||t.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
284
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${h}</button>
285
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${p}</button>
285
286
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 p-3 space-y-2.5" style="display:none">
286
- ${t.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${t.allCategories.map(d=>html`<option value="${d}" ${e.category===d?"selected":""}>${d}</option>`)}</select></div>`:""}
287
- ${t.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${t.allProjects.map(d=>html`<option value="${d}" ${e.project===d?"selected":""}>${d}</option>`)}</select></div>`:""}
288
- ${t.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All repositories</option>${t.allRepositories.map(d=>html`<option value="${d}" ${e.repository===d?"selected":""}>${d}</option>`)}</select></div>`:""}
289
- ${t.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="branch-list">${t.allBranches.map(d=>html`<option value="${d}">`)}</datalist></div>`:""}
290
- ${t.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="commit-list">${t.allCommits.map(d=>html`<option value="${d.slice(0,7)}">`)}</datalist></div>`:""}
287
+ ${t.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${t.allCategories.map(l=>html`<option value="${l}" ${e.category===l?"selected":""}>${l}</option>`)}</select></div>`:""}
288
+ ${t.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${t.allProjects.map(l=>html`<option value="${l}" ${e.project===l?"selected":""}>${l}</option>`)}</select></div>`:""}
289
+ ${t.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All repositories</option>${t.allRepositories.map(l=>html`<option value="${l}" ${e.repository===l?"selected":""}>${l}</option>`)}</select></div>`:""}
290
+ ${t.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="branch-list">${t.allBranches.map(l=>html`<option value="${l}">`)}</datalist></div>`:""}
291
+ ${t.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="commit-list">${t.allCommits.map(l=>html`<option value="${l.slice(0,7)}">`)}</datalist></div>`:""}
291
292
  <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Apply</button>
292
293
  </div>
293
294
  </div>`:""}
294
295
 
295
- ${r?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
296
+ ${d?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
296
297
 
297
- ${Ee(html`<button type="button" @click="selectMode = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lessons…</button>
298
- <button type="button" @click="window.resetLessonColumns && window.resetLessonColumns(); open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><svg class="w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><rect x="2.5" y="4" width="15" height="12" rx="2"/><path d="M7.5 4v12M12.5 4v12"/></svg>Reset column widths</button>`,r?"":"ml-auto",'x-show="!selectMode"')}
299
- <button type="button" x-show="selectMode" style="display:none" @click="leave()" class="${r?"":"ml-auto"} inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-rose-400 hover:text-rose-600 dark:hover:text-rose-400">✕ Cancel</button>
298
+ ${J(html`<button type="button" @click="selectMode = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lessons…</button>
299
+ <button type="button" @click="window.resetLessonColumns && window.resetLessonColumns(); open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><svg class="w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><rect x="2.5" y="4" width="15" height="12" rx="2"/><path d="M7.5 4v12M12.5 4v12"/></svg>Reset column widths</button>`,d?"":"ml-auto",'x-show="!selectMode"')}
300
+ <button type="button" x-show="selectMode" style="display:none" @click="leave()" class="${d?"":"ml-auto"} inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-rose-400 hover:text-rose-600 dark:hover:text-rose-400">✕ Cancel</button>
300
301
  <div class="relative" x-data="{ open: ${String(t.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
301
302
  <button type="button" @click="open = !open" title="Import a lesson someone shared" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Import</button>
302
303
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-80 p-3 space-y-2.5" style="display:none">
@@ -311,19 +312,19 @@ ${Object.entries(t.categorised).map(([r,s])=>html`
311
312
  </div>
312
313
  </div>
313
314
 
314
- ${r?html`<div class="flex flex-wrap gap-1.5 mb-4">
315
- ${o?i(`\u{1F4C5} ${s}`,y(e,{date_from:"",date_to:""})):""}
316
- ${e.period!=="all"&&!o?i(`\u{1F550} ${s}`,y(e,{period:"all"})):""}
317
- ${e.feedback?i(je[e.feedback]??e.feedback,y(e,{feedback:""})):""}
318
- ${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${y({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
319
- ${e.imported||e.shared_by?i(l,y(e,{imported:"",shared_by:""})):""}
320
- ${e.search?i(`\u{1F50D} "${e.search}"`,y(e,{search:""})):""}
321
- ${e.category?i(e.category,y(e,{category:""})):""}
322
- ${e.level?i(`${Pe[e.level]??""} ${e.level}`,y(e,{level:""})):""}
323
- ${e.project?i(`\u{1F4C1} ${e.project}`,y(e,{project:""})):""}
324
- ${e.repository?i(`\u2387 ${e.repository}`,y(e,{repository:""})):""}
325
- ${e.branch?i(`\u2387 ${e.branch}`,y(e,{branch:""})):""}
326
- ${e.commit?i(e.commit.slice(0,7),y(e,{commit:""})):""}
315
+ ${d?html`<div class="flex flex-wrap gap-1.5 mb-4">
316
+ ${g?c(`\u{1F4C5} ${r}`,h(e,{date_from:"",date_to:""})):""}
317
+ ${e.period!=="all"&&!g?c(`\u{1F550} ${r}`,h(e,{period:"all"})):""}
318
+ ${e.feedback?c(He[e.feedback]??e.feedback,h(e,{feedback:""})):""}
319
+ ${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${h({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
320
+ ${e.imported||e.shared_by?c(n,h(e,{imported:"",shared_by:""})):""}
321
+ ${e.search?c(`\u{1F50D} "${e.search}"`,h(e,{search:""})):""}
322
+ ${e.category?c(e.category,h(e,{category:""})):""}
323
+ ${e.level?c(`${qe[e.level]??""} ${e.level}`,h(e,{level:""})):""}
324
+ ${e.project?c(`\u{1F4C1} ${e.project}`,h(e,{project:""})):""}
325
+ ${e.repository?c(`\u2387 ${e.repository}`,h(e,{repository:""})):""}
326
+ ${e.branch?c(`\u2387 ${e.branch}`,h(e,{branch:""})):""}
327
+ ${e.commit?c(e.commit.slice(0,7),h(e,{commit:""})):""}
327
328
  </div>`:""}
328
329
  </form>
329
330
  <form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
@@ -343,36 +344,36 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
343
344
  </colgroup>
344
345
  <thead>
345
346
  <tr class="bg-gray-50 dark:bg-gray-800/60 border-b border-gray-200 dark:border-gray-800 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
346
- <th class="px-3 py-3"><input type="checkbox" tabindex="-1" aria-label="Select all on this page" class="dc-check" x-show="selectMode" style="display:none" :checked="selected.length > 0 && ${$}.every((id) => selected.includes(id))" @change="setAll(${$}, $event.target.checked)" /></th>
347
- ${p("Date","timestamp")}
348
- ${p("Topic","topic_id","hidden sm:table-cell")}
349
- ${p("Title","title")}
350
- ${p("Level","level")}
347
+ <th class="px-3 py-3"><input type="checkbox" tabindex="-1" aria-label="Select all on this page" class="dc-check" x-show="selectMode" style="display:none" :checked="selected.length > 0 && ${I}.every((id) => selected.includes(id))" @change="setAll(${I}, $event.target.checked)" /></th>
348
+ ${u("Date","timestamp")}
349
+ ${u("Topic","topic_id","hidden sm:table-cell")}
350
+ ${u("Title","title")}
351
+ ${u("Level","level")}
351
352
  <th class="px-3 py-3 hidden lg:table-cell relative overflow-hidden text-ellipsis">Categories</th>
352
- ${p("Feedback","feedback","hidden xl:table-cell")}
353
+ ${u("Feedback","feedback","hidden xl:table-cell")}
353
354
  <th class="px-2 py-3 w-8"><span class="sr-only">Share</span></th>
354
355
  </tr>
355
356
  </thead>
356
357
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
357
- ${t.lessons.map(d=>{let x=Le("lesson-row-",d.id),b=d.timestamp.slice(0,10),w=d.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${d.id}" data-title="${d.title}" data-href="/lessons/${encodeURIComponent(d.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-indigo-50 dark:bg-indigo-900/20'">
358
+ ${t.lessons.map(l=>{let x=Ge("lesson-row-",l.id),$=l.timestamp.slice(0,10),S=l.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${l.id}" data-title="${l.title}" data-href="/lessons/${encodeURIComponent(l.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-indigo-50 dark:bg-indigo-900/20'">
358
359
  <td class="px-3 py-3">
359
- <form x-show="!selectMode" method="post" action="/lessons/${encodeURIComponent(d.id)}/star" hx-post="/lessons/${encodeURIComponent(d.id)}/star" hx-target="#${x}" hx-select="#${x}" hx-swap="outerHTML">
360
- <input type="hidden" name="starred" value="${d.starred?"0":"1"}" />
361
- <input type="hidden" name="next" value="/lessons${y(e)}" />
362
- <button type="submit" title="${d.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${d.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${d.starred?"\u2605":"\u2606"}</button>
360
+ <form x-show="!selectMode" method="post" action="/lessons/${encodeURIComponent(l.id)}/star" hx-post="/lessons/${encodeURIComponent(l.id)}/star" hx-target="#${x}" hx-select="#${x}" hx-swap="outerHTML">
361
+ <input type="hidden" name="starred" value="${l.starred?"0":"1"}" />
362
+ <input type="hidden" name="next" value="/lessons${h(e)}" />
363
+ <button type="submit" title="${l.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${l.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${l.starred?"\u2605":"\u2606"}</button>
363
364
  </form>
364
365
  <input type="checkbox" tabindex="-1" aria-hidden="true" class="dc-check pointer-events-none" x-show="selectMode" style="display:none" :checked="selected.includes($el.closest('tr').dataset.id)" />
365
366
  </td>
366
367
  <td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
367
- <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${d.timestamp}">${b}</span>
368
- <div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${w}</div>
368
+ <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${l.timestamp}">${$}</span>
369
+ <div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${S}</div>
369
370
  </td>
370
- <td class="px-3 py-3 hidden sm:table-cell"><span class="block truncate text-xs font-mono text-cyan-600 dark:text-cyan-400">${d.topic_id}</span>${d.imported?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate max-w-[10rem]" title="This lesson was shared with you">🤝 ${d.shared_by??"anonymous"}</span>`:""}</td>
371
- <td class="px-3 py-3"><a href="/lessons/${encodeURIComponent(d.id)}" class="font-semibold text-[15px] leading-snug text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${d.title}</a></td>
372
- <td class="px-3 py-3"><a href="${y(e,{level:d.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${c[d.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${d.level}</a></td>
373
- <td class="px-3 py-3 hidden lg:table-cell"><div class="flex flex-wrap gap-1">${d.categories.map(j=>html`<a href="${y(e,{category:j})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${j}</a>`)}</div></td>
374
- <td class="px-3 py-3 hidden xl:table-cell">${d.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:d.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
375
- <td class="px-2 py-3 text-center"><button type="button" hx-get="/lessons/${encodeURIComponent(d.id)}/share?format=panel" hx-target="#share-modal-body" hx-swap="innerHTML" @click="openShare($el)" title="Share this lesson" aria-label="Share this lesson" class="inline-flex text-gray-400 dark:text-gray-500 hover:text-indigo-500 dark:hover:text-indigo-400 transition opacity-0 group-hover:opacity-100 focus-visible:opacity-100"><svg class="w-4 h-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 14 14 6M8 6h6v6"/></svg></button></td>
371
+ <td class="px-3 py-3 hidden sm:table-cell"><span class="block truncate text-xs font-mono text-cyan-600 dark:text-cyan-400">${l.topic_id}</span>${l.imported?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate max-w-[10rem]" title="This lesson was shared with you">🤝 ${l.shared_by??"anonymous"}</span>`:""}</td>
372
+ <td class="px-3 py-3"><a href="/lessons/${encodeURIComponent(l.id)}" class="font-semibold text-[15px] leading-snug text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${l.title}</a></td>
373
+ <td class="px-3 py-3"><a href="${h(e,{level:l.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${s[l.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${l.level}</a></td>
374
+ <td class="px-3 py-3 hidden lg:table-cell"><div class="flex flex-wrap gap-1">${l.categories.map(k=>html`<a href="${h(e,{category:k})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${k}</a>`)}</div></td>
375
+ <td class="px-3 py-3 hidden xl:table-cell">${l.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Knew it</span>`:l.feedback==="understood"?html`<span class="text-xs text-indigo-500 dark:text-indigo-400 font-medium">💡 Understood</span>`:l.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Couldn't follow</span>`:""}</td>
376
+ <td class="px-2 py-3 text-center"><button type="button" hx-get="/lessons/${encodeURIComponent(l.id)}/share?format=panel" hx-target="#share-modal-body" hx-swap="innerHTML" @click="openShare($el)" title="Share this lesson" aria-label="Share this lesson" class="inline-flex text-gray-400 dark:text-gray-500 hover:text-indigo-500 dark:hover:text-indigo-400 transition opacity-0 group-hover:opacity-100 focus-visible:opacity-100"><svg class="w-4 h-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 14 14 6M8 6h6v6"/></svg></button></td>
376
377
  </tr>`})}
377
378
  </tbody>
378
379
  </table>
@@ -380,14 +381,14 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
380
381
  ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
381
382
  <p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
382
383
  <div class="flex items-center gap-1">
383
- ${t.page>1?html`<a href="${y(e,{page:String(t.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
384
- ${Fe(t.page,t.totalPages).map(d=>d===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:d===t.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${d}</span>`:html`<a href="${y(e,{page:String(d)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${d}</a>`)}
385
- ${t.page<t.totalPages?html`<a href="${y(e,{page:String(t.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
384
+ ${t.page>1?html`<a href="${h(e,{page:String(t.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
385
+ ${ot(t.page,t.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===t.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${l}</span>`:html`<a href="${h(e,{page:String(l)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${l}</a>`)}
386
+ ${t.page<t.totalPages?html`<a href="${h(e,{page:String(t.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
386
387
  </div>
387
388
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
388
389
  <p class="text-3xl mb-3">📭</p>
389
390
  <p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
390
- ${r?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
391
+ ${d?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
391
392
  </div>`}
392
393
  <div x-show="selectMode" style="display:none" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" class="fixed inset-x-0 bottom-0 z-50 border-t border-gray-200 dark:border-gray-800 bg-white/95 dark:bg-gray-900/95 backdrop-blur px-4 sm:px-6 py-3">
393
394
  <div class="max-w-7xl mx-auto flex items-center gap-3">
@@ -399,14 +400,14 @@ ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
399
400
  </div>
400
401
  </div>
401
402
  </div>
402
- ${Re("confirmOpen","confirmOpen = false",html`<span x-text="selected.length === 1 ? 'Delete this lesson?' : 'Delete ' + selected.length + ' lessons?'"></span>`,html`<ul class="max-h-40 overflow-y-auto space-y-1 text-gray-700 dark:text-gray-200">
403
+ ${Y("confirmOpen","confirmOpen = false",html`<span x-text="selected.length === 1 ? 'Delete this lesson?' : 'Delete ' + selected.length + ' lessons?'"></span>`,html`<ul class="max-h-40 overflow-y-auto space-y-1 text-gray-700 dark:text-gray-200">
403
404
  <template x-for="t in selectedTitles().slice(0, 6)" :key="t"><li class="truncate">· <span x-text="t"></span></li></template>
404
405
  </ul>
405
406
  <p x-show="selected.length > 6" style="display:none" x-text="'…and ' + (selected.length - 6) + ' more'"></p>
406
407
  <p>They will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
407
408
  <form method="post" action="/lessons/delete" class="contents">
408
409
  <template x-for="id in selected" :key="id"><input type="hidden" name="id" :value="id" /></template>
409
- <input type="hidden" name="next" value="/lessons${y(e,{page:String(t.page)})}" />
410
+ <input type="hidden" name="next" value="/lessons${h(e,{page:String(t.page)})}" />
410
411
  <button type="submit" :disabled="!selected.length" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500" x-text="selected.length === 1 ? '🗑 Delete lesson' : '🗑 Delete ' + selected.length + ' lessons'">🗑 Delete</button>
411
412
  </form>`)}
412
413
  <div x-show="shareOpen" style="display:none" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" class="fixed inset-0 z-[70] pointer-events-none">
@@ -415,7 +416,7 @@ ${Re("confirmOpen","confirmOpen = false",html`<span x-text="selected.length ===
415
416
  <div id="share-modal-body" class="space-y-3"><p class="text-xs text-gray-400 dark:text-gray-500">Loading…</p></div>
416
417
  </div>
417
418
  </div>
418
- </div>`,k=html`<script src="/static/vendor/flatpickr.min.js"></script>
419
+ </div>`,E=html`<script src="/static/vendor/flatpickr.min.js"></script>
419
420
  <script src="/static/relative-time.js"></script>
420
421
  <script src="/static/table-resize.js"></script>
421
422
  <script src="/static/share.js"></script>
@@ -423,8 +424,8 @@ ${Re("confirmOpen","confirmOpen = false",html`<span x-text="selected.length ===
423
424
  function periodPicker() {
424
425
  return {
425
426
  open: false,
426
- showCustom: ${raw(o?"true":"false")},
427
- label: ${raw(T(s))},
427
+ showCustom: ${raw(g?"true":"false")},
428
+ label: ${raw(j(r))},
428
429
  fp: null,
429
430
  init() {
430
431
  const self = this;
@@ -432,7 +433,7 @@ function periodPicker() {
432
433
  if (val && !this.fp) {
433
434
  this.fp = flatpickr(this.$refs.fp, {
434
435
  mode: 'range', dateFormat: 'Y-m-d', inline: false,
435
- ${raw(e.date_from?`defaultDate: [${T(e.date_from)}${e.date_to?`, ${T(e.date_to)}`:""}],`:"")}
436
+ ${raw(e.date_from?`defaultDate: [${j(e.date_from)}${e.date_to?`, ${j(e.date_to)}`:""}],`:"")}
436
437
  onChange(dates) {
437
438
  if (dates.length === 2) {
438
439
  document.getElementById('h-date-from').value = self.fp.formatDate(dates[0], 'Y-m-d');
@@ -455,16 +456,16 @@ function periodPicker() {
455
456
  },
456
457
  };
457
458
  }
458
- </script>`;return E({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:P,body:R,scripts:k})}function Fe(t,e){let o=[],r=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(r&&s-r>1&&o.push(0),o.push(s),r=s);return o}var G={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function F(t){let e=`/lessons/${encodeURIComponent(t.id)}/share?format=md&name=${encodeURIComponent(t.name)}&include_context=${t.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${t.text}" data-link="${t.link}" class="space-y-2">
459
+ </script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:m,body:R,scripts:E})}function ot(t,e){let g=[],d=0;for(let r=1;r<=e;r++)(r===1||r===e||r>=t-2&&r<=t+2)&&(d&&r-d>1&&g.push(0),g.push(r),d=r);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(t){let e=`/lessons/${encodeURIComponent(t.id)}/share?format=md&name=${encodeURIComponent(t.name)}&include_context=${t.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${t.text}" data-link="${t.link}" class="space-y-2">
459
460
  <div class="flex flex-wrap gap-2">
460
461
  <button type="button" onclick="copyShare('text', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">📋 Copy text</button>
461
462
  <button type="button" onclick="copyShare('link', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">🔗 Copy link</button>
462
463
  <a href="${e}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">⬇ Download .md</a>
463
464
  </div>
464
465
  <p class="text-[11px] text-gray-400 dark:text-gray-500">${t.name?html`Shared by <span class="text-gray-600 dark:text-gray-300">${t.name}</span>`:"Shared anonymously"} · ${t.includeContext?"includes project, branch and commit":"only the lesson travels \u2014 no paths, no project"}.</p>
465
- </div>`}function z(t,e,o=null){return html`
466
+ </div>`}function W(t,e,g=null){return html`
466
467
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Share this lesson</p>
467
- ${o?html`<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 leading-snug">${o}</p>`:""}
468
+ ${g?html`<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 leading-snug">${g}</p>`:""}
468
469
  <form method="post" action="/lessons/${encodeURIComponent(t)}/share" hx-post="/lessons/${encodeURIComponent(t)}/share" hx-target="#share-payloads" hx-swap="outerHTML" hx-trigger="input delay:300ms, change" onsubmit="return false" class="space-y-2.5">
469
470
  <input type="hidden" name="persist" value="0" x-ref="persist" />
470
471
  <div>
@@ -473,8 +474,8 @@ function periodPicker() {
473
474
  </div>
474
475
  <label class="flex items-start gap-2 text-xs text-gray-600 dark:text-gray-300 cursor-pointer"><input type="checkbox" name="include_context" value="1" ${e.includeContext?"checked":""} class="mt-0.5" /><span>Include where it happened <span class="text-gray-400 dark:text-gray-500">(project, branch, commit — never local paths)</span></span></label>
475
476
  </form>
476
- ${F(e)}
477
- `}function Oe(t){let e=t.lesson,o=t.share,r=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",s=e.repository_platform==="local",n=null;s&&e.repository?n=`vscode://file/${e.repository}`:e.repository_platform&&G[e.repository_platform]&&e.repository&&(n=`https://${G[e.repository_platform]}/${e.repository}`);let l=null;n&&e.commit_hash&&!s&&(l=e.repository_platform==="gitlab"?`${n}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${n}/commits/${e.commit_hash}`:`${n}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),c=e.timestamp.slice(0,10),i=e.timestamp.slice(0,16).replace("T"," "),p=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,h=html`
477
+ ${V(e)}
478
+ `}function Je(t){let e=t.lesson,g=t.share,d=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",r=e.repository_platform==="local",o=null;r&&e.repository?o=`vscode://file/${e.repository}`:e.repository_platform&&K[e.repository_platform]&&e.repository&&(o=`https://${K[e.repository_platform]}/${e.repository}`);let n=null;o&&e.commit_hash&&!r&&(n=e.repository_platform==="gitlab"?`${o}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${o}/commits/${e.commit_hash}`:`${o}/commit/${e.commit_hash}`);let i=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),s=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),u=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,p=html`
478
479
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
479
480
  <div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
480
481
  ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300">${t.imported==="new"?html`✓ Imported “${e.title}”${e.shared_by?html`, shared by ${e.shared_by}`:""}. It's in your log now — feedback works as usual and it never counts against your daily limit.`:html`“${e.title}” was already in your log — nothing changed.`}</div>`:""}
@@ -486,24 +487,25 @@ ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
486
487
  <button type="submit" title="${e.starred?"Unstar":"Star"}" class="w-6 text-center text-xl leading-none transition ${e.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${e.starred?"\u2605":"\u2606"}</button>
487
488
  </form>
488
489
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
489
- <a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${r} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
490
- ${Ee(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lesson…</button>`,"shrink-0")}
491
- <div class="relative shrink-0" x-data="{ open: ${String(o.open)} }" @click.outside="open = false" @keydown.escape="open = false">
490
+ <a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${d} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
491
+ ${J(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lesson…</button>`,"shrink-0")}
492
+ <div class="relative shrink-0" x-data="{ open: ${String(g.open)} }" @click.outside="open = false" @keydown.escape="open = false">
492
493
  <button type="button" @click="open = !open" title="Share this lesson" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">↗ Share</button>
493
494
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-80 p-4 space-y-3" style="display:none">
494
- ${z(e.id,o)}
495
+ ${W(e.id,g)}
495
496
  </div>
496
497
  </div>
497
498
  </div>
498
499
  <div id="lesson-meta" class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
499
- <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${c}</span>
500
- <span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${i}</span>
500
+ <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${s}</span>
501
+ <span class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${c}</span>
501
502
  </span>
502
503
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
503
504
  ${e.imported?html`<span title="This lesson was shared with you">🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span></span>`:""}
504
- ${e.categories.map($=>html`<a href="/lessons?category=${encodeURIComponent($)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${$}</a>`)}
505
- ${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I know this</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ I don't know this</span>`}
506
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(p)}><input type="hidden" name="feedback" value="clear" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button></form>`:""}
505
+ ${t.course?html`<a href="/courses/${encodeURIComponent(t.course.id)}" title="${t.course.title}" class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 hover:border-indigo-400 transition">🎓 Course · ${t.course.done}/${t.course.total}${t.course.status==="completed"?" \u2713":""}</a>`:""}
506
+ ${e.categories.map(I=>html`<a href="/lessons?category=${encodeURIComponent(I)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${I}</a>`)}
507
+ ${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I knew this</span>`:e.feedback==="understood"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700">💡 Understood</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ Couldn't follow</span>`}
508
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="clear" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button></form>`:""}
507
509
  </div>
508
510
  <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
509
511
  <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
@@ -511,27 +513,28 @@ ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
511
513
  </div>
512
514
  <div id="body-content" class="markdown-body"></div>
513
515
  ${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
514
- ${g?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
515
- ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${n?html`<a href="${n}" ${s?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
516
- ${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&G[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${n?html`<a href="${n}" ${s?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
516
+ ${i?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
517
+ ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${o?html`<a href="${o}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
518
+ ${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&K[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${o?html`<a href="${o}" ${r?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
517
519
  ${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
518
- ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${l?html`<a href="${l}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
520
+ ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${n?html`<a href="${n}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
519
521
  ${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
520
522
  </div></div>`:""}
521
523
  <div id="lesson-feedback">${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
522
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(p)}><input type="hidden" name="feedback" value="know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I know this</button></form>
523
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(p)}><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ I don't know this</button></form>
524
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I knew this</button></form>
525
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="understood" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900 hover:text-indigo-700 dark:hover:text-white">💡 Understood</button></form>
526
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ Couldn't follow</button></form>
524
527
  </div>`}</div>
525
528
  <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">ID: ${e.id}</div>
526
529
  </div>
527
- ${Re("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
530
+ ${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
528
531
  <p>${e.imported?html`It was shared with you${e.shared_by?html` by ${e.shared_by}`:""}; you can import it again later. `:""}It will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
529
532
  <form method="post" action="/lessons/delete" class="contents">
530
533
  <input type="hidden" name="id" value="${e.id}" />
531
534
  <input type="hidden" name="next" value="/lessons" />
532
535
  <button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500">🗑 Delete lesson</button>
533
536
  </form>`)}
534
- </div>`,P=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,_=html`<script src="/static/vendor/highlight.min.js"></script>
537
+ </div>`,m=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,v=html`<script src="/static/vendor/highlight.min.js"></script>
535
538
  <script src="/static/vendor/marked.min.js"></script>
536
539
  <script src="/static/vendor/purify.min.js"></script>
537
540
  <script src="/static/relative-time.js"></script>
@@ -545,9 +548,9 @@ ${Re("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class
545
548
  },
546
549
  breaks: true, gfm: true,
547
550
  });
548
- document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(T(e.summary))}));
549
- document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(T(e.body))}));
550
- </script>`;return E({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:P,body:h,scripts:_})}var ze={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function Ae(t){let e=t.preview,o=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
551
+ document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.summary))}));
552
+ document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.body))}));
553
+ </script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:m,body:p,scripts:v})}var at={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function Ye(t){let e=t.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
551
554
  <input type="hidden" name="from" value="lessons" />
552
555
  <textarea name="text" rows="5" placeholder="devcoach:lesson:1:… — or the link, a URL, or the whole card" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono"></textarea>
553
556
  <div class="flex items-center gap-3">
@@ -555,18 +558,18 @@ ${Re("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class
555
558
  <button type="submit" class="ml-auto bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">Import</button>
556
559
  </div>
557
560
  <p class="text-xs text-gray-400 dark:text-gray-500">You can also drop the file anywhere on this page.</p>
558
- </form>`,r=html`
561
+ </form>`,d=html`
559
562
  <div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
560
563
  <div class="${e?"max-w-5xl":"max-w-3xl"} mx-auto bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
561
564
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-4">${e?"Someone shared a lesson with you":"Import a shared lesson"}</p>
562
565
  ${t.error?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-rose-50 dark:bg-rose-900/20 border-rose-200 dark:border-rose-700 text-rose-700 dark:text-rose-300">${t.error}</div>`:""}
563
566
  ${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
564
567
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
565
- <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${ze[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
568
+ <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${at[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
566
569
  </div>
567
570
  <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
568
571
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
569
- ${e.lesson.categories.map(l=>html`<span class="inline-block bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 border border-gray-200 dark:border-gray-700">${l}</span>`)}
572
+ ${e.lesson.categories.map(n=>html`<span class="inline-block bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 border border-gray-200 dark:border-gray-700">${n}</span>`)}
570
573
  <span>🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span> · ${e.shared_at.slice(0,10)}</span>
571
574
  </div>
572
575
  <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
@@ -583,8 +586,8 @@ ${Re("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class
583
586
  <input type="hidden" name="text" value="${t.code}" />
584
587
  <button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">+ Add to my lessons</button>
585
588
  <p class="text-xs text-gray-400 dark:text-gray-500">Nothing is saved until you click. It joins your log like your own lessons and never counts against the daily limit.</p>
586
- </form>`:o}
587
- </div>`,s=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,n=html`<script src="/static/vendor/highlight.min.js"></script>
589
+ </form>`:g}
590
+ </div>`,r=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,o=html`<script src="/static/vendor/highlight.min.js"></script>
588
591
  <script src="/static/vendor/marked.min.js"></script>
589
592
  <script src="/static/vendor/purify.min.js"></script>
590
593
  <script src="/static/share.js"></script>
@@ -597,12 +600,87 @@ ${Re("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class
597
600
  },
598
601
  breaks: true, gfm: true,
599
602
  });
600
- ${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${T(e.lesson.summary)}));
601
- document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${T(e.lesson.body??"")}));`):""}
602
- </script>`;return E({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:s,body:r,scripts:n})}var Ke=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]];function Ne(t){let e=t.settings.min_gap_minutes,o=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],r=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],s=t.flash,n=html`
603
+ ${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.summary)}));
604
+ document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.body??"")}));`):""}
605
+ </script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:r,body:d,scripts:o})}var st=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]],Fe={active:"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-700",completed:"bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-700",abandoned:"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"},nt={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function Ve(t){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Fe[t]??Fe.abandoned}">${t}</span>`}function We(t){let e=html`
606
+ <div class="flex items-center justify-between mb-6">
607
+ <h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Courses</h1>
608
+ <p class="text-sm text-gray-400 dark:text-gray-500">${t.courses.length} course${t.courses.length!==1?"s":""}</p>
609
+ </div>
610
+ ${t.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
611
+ <table class="w-full text-sm table-fixed">
612
+ <colgroup><col style="width:46%" /><col style="width:22%" /><col style="width:14%" /><col style="width:8rem" /></colgroup>
613
+ <thead>
614
+ <tr class="bg-gray-50 dark:bg-gray-800/60 border-b border-gray-200 dark:border-gray-800 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
615
+ <th class="px-4 py-3">Course</th><th class="px-3 py-3">Progress</th><th class="px-3 py-3">Status</th><th class="px-3 py-3">Updated</th>
616
+ </tr>
617
+ </thead>
618
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
619
+ ${t.courses.map(d=>{let r=d.steps.filter(n=>n.status!=="todo").length,o=d.steps.length?Math.round(r/d.steps.length*100):0;return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors cursor-pointer" tabindex="0" role="link" data-href="/courses/${encodeURIComponent(d.id)}" @click="if (!$event.target.closest('a, button')) window.location = $el.dataset.href" @keydown.enter="window.location = $el.dataset.href">
620
+ <td class="px-4 py-3">
621
+ <a href="/courses/${encodeURIComponent(d.id)}" class="font-semibold text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${d.title}</a>
622
+ ${d.lesson_id?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate">from <a href="/lessons/${encodeURIComponent(d.lesson_id)}" class="hover:text-indigo-500 hover:underline">${d.lesson_title??d.lesson_id}</a></span>`:""}
623
+ </td>
624
+ <td class="px-3 py-3">
625
+ <div class="flex items-center gap-2"><div class="flex-1 h-1.5 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden"><div class="h-full bg-indigo-500" style="width:${o}%"></div></div><span class="text-xs tabular-nums text-gray-500 dark:text-gray-400 shrink-0">${r}/${d.steps.length}</span></div>
626
+ </td>
627
+ <td class="px-3 py-3">${Ve(d.status)}</td>
628
+ <td class="px-3 py-3 whitespace-nowrap text-gray-400 dark:text-gray-500 tabular-nums"><span data-ts="${d.updated_at}">${d.updated_at.slice(0,10)}</span></td>
629
+ </tr>`})}
630
+ </tbody>
631
+ </table>
632
+ </div>`:html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 p-8 text-center">
633
+ <p class="text-3xl mb-3">🎓</p>
634
+ <p class="text-gray-700 dark:text-gray-200 font-medium">No courses yet.</p>
635
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 max-w-md mx-auto">A course starts in your agent: answer <span class="font-mono">n</span> (couldn't follow) under a lesson card and say yes to the offer, or run <span class="font-mono">/devcoach:course</span> on any lesson or concept. The AI first asks what you already know, then writes the course here.</p>
636
+ </div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:t.uiTheme,body:e,scripts:g})}function Ze(t){let e=t.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===t.selected)??e.steps[0],r=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,o=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400",i=html`
637
+ <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
638
+ <div class="sticky top-0 z-20 -mx-4 sm:-mx-6 -mt-8 mb-5 px-4 sm:px-6 h-12 flex items-center gap-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur border-b border-gray-200 dark:border-gray-800">
639
+ <a href="/courses" title="Back to courses" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition shrink-0">←</a>
640
+ <span class="text-sm font-semibold text-gray-900 dark:text-white truncate min-w-0">${e.title}</span>
641
+ ${Ve(e.status)}
642
+ <span class="text-xs text-gray-500 dark:text-gray-400 shrink-0">${g}/${e.steps.length} steps</span>
643
+ ${J(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete course…</button>`,"ml-auto shrink-0")}
644
+ </div>
645
+ <div class="grid grid-cols-1 lg:grid-cols-[15rem_minmax(0,1fr)] gap-6 lg:gap-8">
646
+ <aside id="course-steps" class="lg:sticky lg:top-16 self-start">
647
+ <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 px-2 mb-2">Steps</p>
648
+ ${e.steps.length?html`<ol class="space-y-0.5">${e.steps.map(s=>html`<li data-anchor="${s.anchor}" class="rounded-lg px-2 py-1.5 ${d&&s.position===d.position?"bg-indigo-50 dark:bg-indigo-900/30":""}">
649
+ <a href="/courses/${encodeURIComponent(e.id)}?step=${s.position}" data-anchor="${s.anchor}" class="flex items-start gap-2 text-sm">
650
+ <span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-indigo-400"}">${nt[s.status]??"\u25CB"}</span>
651
+ <span class="min-w-0 flex-1"><span class="block text-gray-800 dark:text-gray-100">${s.position}. ${s.title}</span><span class="block text-[11px] text-gray-400 dark:text-gray-500">${s.kind}${s.status!=="todo"?` \xB7 ${s.status}`:""}</span></span>
652
+ </a>
653
+ <div data-step-actions class="flex flex-wrap gap-1.5 mt-2 pl-6 ${d&&s.position===d.position?"":"hidden"}">
654
+ ${s.status!=="done"?o(s.position,"done","\u2713 Mark done",`${n} hover:border-green-400 hover:text-green-700 dark:hover:text-green-300`):o(s.position,"todo","\u21BA Reopen",n)}
655
+ ${s.status==="todo"?o(s.position,"skipped","Skip",n):""}
656
+ </div>
657
+ </li>`)}</ol>`:html`<p class="text-sm text-gray-500 dark:text-gray-400 px-2">No steps registered yet.</p>`}
658
+ <details class="mt-4 px-2 text-xs text-gray-500 dark:text-gray-400">
659
+ <summary class="cursor-pointer font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 select-none">About this course</summary>
660
+ <div class="mt-2 space-y-2">
661
+ <p>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></p>
662
+ ${e.lesson_id?html`<p>from <a href="/lessons/${encodeURIComponent(e.lesson_id)}" class="text-gray-700 dark:text-gray-200 hover:text-indigo-500 hover:underline">${t.lessonTitle??e.lesson_id}</a></p>`:""}
663
+ ${e.goal?html`<p class="text-gray-600 dark:text-gray-300">${e.goal}</p>`:""}
664
+ ${e.prerequisites.length?html`<div class="flex flex-wrap items-center gap-1" title="The prerequisite chain explored before writing the course">
665
+ ${e.prerequisites.map(s=>html`<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full border ${s.known?"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800":"bg-rose-50 dark:bg-rose-900/20 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800"}">${s.known?"\u2713":"\u2717"} ${s.concept}</span>`)}
666
+ </div>`:""}
667
+ </div>
668
+ </details>
669
+ </aside>
670
+ <div class="min-w-0">
671
+ ${t.hasDocument?html`<iframe id="course-frame" src="${r}" title="${e.title}" sandbox="allow-scripts allow-forms" referrerpolicy="no-referrer" scrolling="no" class="w-full block min-h-[40vh]"></iframe>`:html`<div class="p-8 text-center text-sm text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl"><p class="text-3xl mb-3">✍️</p>The course document hasn't been written yet. Ask your agent to continue the course — it writes <span class="font-mono">index.html</span> in the course folder, and it appears here.</div>`}
672
+ </div>
673
+ </div>
674
+ ${Y("confirmOpen","confirmOpen = false",html`Delete this course?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
675
+ <p>The course, its steps and its document folder will be removed. This cannot be undone.${e.lesson_id?" The lesson it grew from stays.":""}</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
676
+ <form method="post" action="/courses/delete" class="contents">
677
+ <input type="hidden" name="id" value="${e.id}" />
678
+ <button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500">🗑 Delete course</button>
679
+ </form>`)}
680
+ </div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:t.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function Qe(t){let e=t.settings.min_gap_minutes,g=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],d=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],r=t.flash,o=html`
603
681
  <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
604
- ${s?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${s.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
605
- ${s.imported} lesson${s.imported!==1?"s":""} imported.${s.skipped?` ${s.skipped} skipped (already in DB).`:""}${s.invalid?` ${s.invalid} rejected (failed validation).`:""}${s.groups?` ${s.groups} group${s.groups!==1?"s":""} added.`:""}${s.notebook?" Notebook restored.":""}
682
+ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
683
+ ${r.imported} lesson${r.imported!==1?"s":""} imported.${r.skipped?` ${r.skipped} skipped (already in DB).`:""}${r.invalid?` ${r.invalid} rejected (failed validation).`:""}${r.groups?` ${r.groups} group${r.groups!==1?"s":""} added.`:""}${r.notebook?" Notebook restored.":""}
606
684
  </div>`:""}
607
685
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
608
686
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
@@ -617,7 +695,7 @@ ${s?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${s.invalid?"bg-ye
617
695
  <div>
618
696
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
619
697
  <select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
620
- ${Ke.map(([g,c])=>html`<option value="${g}" ${e===g?"selected":""}>${c}</option>`)}
698
+ ${st.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
621
699
  </select>
622
700
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
623
701
  </div>
@@ -645,9 +723,9 @@ ${s?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${s.invalid?"bg-ye
645
723
  <fieldset class="border-0 p-0 m-0">
646
724
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
647
725
  <div class="flex gap-2">
648
- ${o.map(([g,c,i])=>html`<label class="flex-1 cursor-pointer">
649
- <input type="radio" name="ui_theme" value="${g}" ${t.settings.ui_theme===g?"checked":""} class="sr-only peer" />
650
- <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${i}</span>${c}</span>
726
+ ${g.map(([i,s,c])=>html`<label class="flex-1 cursor-pointer">
727
+ <input type="radio" name="ui_theme" value="${i}" ${t.settings.ui_theme===i?"checked":""} class="sr-only peer" />
728
+ <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
651
729
  </label>`)}
652
730
  </div>
653
731
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
@@ -655,9 +733,9 @@ ${s?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${s.invalid?"bg-ye
655
733
  <fieldset class="border-0 p-0 m-0">
656
734
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Home page</legend>
657
735
  <div class="flex gap-2">
658
- ${r.map(([g,c,i])=>html`<label class="flex-1 cursor-pointer">
659
- <input type="radio" name="ui_home" value="${g}" ${t.settings.ui_home===g?"checked":""} class="sr-only peer" />
660
- <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${i}</span>${c}</span>
736
+ ${d.map(([i,s,c])=>html`<label class="flex-1 cursor-pointer">
737
+ <input type="radio" name="ui_home" value="${i}" ${t.settings.ui_home===i?"checked":""} class="sr-only peer" />
738
+ <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
661
739
  </label>`)}
662
740
  </div>
663
741
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Auto opens Lessons once you have one, the knowledge map before.</p>
@@ -715,7 +793,7 @@ ${s?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${s.invalid?"bg-ye
715
793
  <div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
716
794
  <pre x-show="nbMode === 'source'" style="display:none" class="text-xs font-mono bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded p-3 overflow-x-auto whitespace-pre-wrap text-gray-700 dark:text-gray-200 min-h-24">${t.notebookContent}</pre>
717
795
  </div>
718
- </div>`,l=html`<script src="/static/vendor/marked.min.js"></script>
796
+ </div>`,n=html`<script src="/static/vendor/marked.min.js"></script>
719
797
  <script src="/static/vendor/purify.min.js"></script>
720
798
  <script>
721
799
  function updateLabel(inputId, labelId, submitId) {
@@ -724,8 +802,10 @@ function updateLabel(inputId, labelId, submitId) {
724
802
  document.getElementById(submitId).disabled = !input.files.length;
725
803
  }
726
804
  marked.setOptions({ breaks: true, gfm: true });
727
- document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(T(t.notebookContent))}));
728
- </script>`;return E({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:n,scripts:l})}var J=25;function M(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function m(t,e,o=""){let r=t[e];return typeof r=="string"?r:o}var Y=dirname(fileURLToPath(import.meta.url)),Ue=[join(Y,"../assets/static"),join(Y,"../../assets/static")].find(t=>existsSync(t))??join(Y,"../assets/static"),Xe={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},et="https://ultimaphoenix.github.io",He={"access-control-allow-origin":et,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function B(t){let e=t.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var V;function tt(){return V===void 0&&(V=a$1()),V}function De(t,e,o){let r=W({explicit:o.name??null,setting:e,gitUserName:tt()}),s=o.includeContext??false,n=V$1(t,{includeContext:s,sharedBy:r}),l=X(n);return {payload:n,state:{id:t.id,open:o.open??false,name:r??"",includeContext:s,text:Z(n,l),link:_(n,l)}}}function rt(t,e){return t==="lessons"?"/lessons":t==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function ot(){try{return p(t=>J$1(t).ui_theme)}catch{return "system"}}function at(t={}){let e=new Hono;return e.get("/static/*",o=>{let r=decodeURIComponent(o.req.path.slice(8)),s=join(Ue,r);if(!s.startsWith(Ue))return o.notFound();try{let n=readFileSync(s);return new Response(new Uint8Array(n),{headers:{"content-type":Xe[extname(s)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return o.notFound()}}),e.get("/ping",o=>o.json({ok:true,version:T$1},200,He)),e.options("/ping",o=>o.body(null,204,He)),e.post("/shutdown",o=>B(o)?o.text("Forbidden",403):(setTimeout(()=>t.onShutdown?.(),50),o.json({ok:true}))),e.get("/shutdown",o=>o.text("Method Not Allowed",405)),e.get("/",o=>{let{home:r$1,hasLessons:s}=p(n=>({home:J$1(n).ui_home,hasLessons:r(n)>0}));return o.redirect(rt(r$1,s),302)}),e.get("/knowledge",o=>{let{profile:r,stats:s,rateLimit:n,settings:l}=p(i=>({profile:ca(i),stats:fa(i),rateLimit:ba(i),settings:J$1(i)})),g=new Map;for(let i of r.groups)for(let p of i.topics)g.set(p,i.name);let c={};for(let i of r.groups)c[i.name]=[];for(let i of r.knowledge){let p=g.get(i.topic)??"Other",h=c[p]??[];h.push(i),c[p]=h;}return o.html(Me({categorised:c,allGroups:r.groups.map(i=>i.name),stats:s,rateLimit:n,maxPerDay:l.max_per_day,uiTheme:l.ui_theme}))}),e.post("/knowledge",async o=>{let r=await o.req.parseBody(),s=m(r,"topic").trim();if(s){let n=Number.parseInt(m(r,"confidence","5"),10)||5,l=m(r,"group").trim();p(g=>{D(g,s,n),l&&l!=="Other"&&H(g,s,l);});}return o.redirect("/knowledge",303)}),e.post("/knowledge/:topic/delete",o=>(p(r=>E$1(r,o.req.param("topic"))),o.redirect("/knowledge",303))),e.post("/knowledge/:topic/group",async o=>{let r=m(await o.req.parseBody(),"group").trim(),s=o.req.param("topic");return p(n=>{r&&r!=="Other"?H(n,s,r):I(n,s);}),o.redirect("/knowledge",303)}),e.post("/knowledge/:topic",async o=>{let r=Number.parseInt(m(await o.req.parseBody(),"delta","0"),10)||0;return p(s=>da(s,o.req.param("topic"),r)),o.redirect("/knowledge",303)}),e.post("/groups",async o=>{let r=m(await o.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&p(s=>F$1(s,r)),o.redirect("/knowledge",303)}),e.post("/groups/:group_name/delete",o=>(p(r=>G$1(r,o.req.param("group_name"))),o.redirect("/knowledge",303))),e.get("/lessons/export",()=>{let o=p(r=>x(r));return new Response(JSON.stringify(o,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e.post("/lessons/import",async o=>{let r=await o.req.parseBody(),s=m(r,"from")==="lessons",n=i=>o.redirect(s?`/lessons?import=1&error=${i}`:"/settings?imported=0&skipped=0&invalid=1",303);if(B(o))return n("cross");let l=r.file,c$1=(l instanceof File&&l.size>0?await l.text():null)??m(r,"text").trim();try{c(c$1)&&!c$1.includes("#devcoach:lesson:")&&(c$1=await d(c$1));let i=p(p=>ga(p,c$1));return i.kind==="lessons"||!i.lesson?o.redirect(`/settings?imported=${i.inserted}&skipped=${i.duplicated}&invalid=${i.invalid}`,303):o.redirect(`/lessons/${encodeURIComponent(i.lesson.id)}?imported=${i.inserted?"1":"dup"}`,303)}catch(i){if(i instanceof U)return n("invalid");throw i}}),e.get("/lessons/import",o=>{let r=(o.req.query("code")??"").trim(),s=null,n=null;if(r)try{s=Y$1(r);}catch(l){n=l instanceof U?l.message:"That code could not be read.";}return o.html(Ae({code:r,preview:s,error:n,uiTheme:ot()}))}),e.get("/lessons",o=>{let r$1=o.req.query(),s$1=new Set(["junior","mid","senior"]),n=r$1.level&&s$1.has(r$1.level)?r$1.level:"",l=r$1.date_from||"",g=r$1.date_to||"",c=r$1.starred==="1",i=r$1.imported==="1"?"1":r$1.imported==="0"?"0":"",p$1=r$1.shared_by||null,h=r$1.sort||"timestamp",P=r$1.order==="asc"?"asc":"desc",_=Math.max(1,Number.parseInt(r$1.page??"1",10)||1),$=r$1.period||"all",R={period:l||g?null:$!=="all"?$:null,category:r$1.category||null,level:n||null,project:r$1.project||null,repository:r$1.repository||null,branch:r$1.branch||null,commit:r$1.commit||null,starred:c?true:null,imported:p$1||i==="1"?true:i==="0"?false:null,shared_by:p$1,search:r$1.search||null,feedback:r$1.feedback||null,date_from:l||null,date_to:g||null},k=p(b=>{let w=r(b,R),j=Math.max(1,Math.ceil(w/J));return _=Math.min(_,j),{total:w,totalPages:j,lessons:s(b,{...R,sort:h,order:P,page:_,per_page:J}),allCategories:B$1(b),allProjects:y$1(b,"project"),allRepositories:y$1(b,"repository"),allBranches:y$1(b,"branch"),allCommits:y$1(b,"commit_hash"),allSharedBy:z$1(b),theme:J$1(b).ui_theme}}),d={period:$,category:r$1.category||"",level:n,project:r$1.project||"",repository:r$1.repository||"",branch:r$1.branch||"",commit:r$1.commit||"",starred:c,imported:p$1?"1":i,shared_by:p$1??"",search:r$1.search||"",feedback:r$1.feedback||"",date_from:l,date_to:g,sort:h,order:P},x=r$1.error==="invalid"||r$1.error==="cross"?r$1.error:null;return o.html(Be({importOpen:r$1.import==="1",importError:x,lessons:k.lessons,allCategories:k.allCategories,allProjects:k.allProjects,allRepositories:k.allRepositories,allBranches:k.allBranches,allCommits:k.allCommits,allSharedBy:k.allSharedBy,s:d,page:_,perPage:J,total:k.total,totalPages:k.totalPages,uiTheme:k.theme}))}),e.post("/lessons/:lesson_id/star",async o=>{let r=await o.req.parseBody();return p(s=>v(s,o.req.param("lesson_id"),r.starred==="1")),o.redirect(M(m(r,"next")||void 0),303)}),e.post("/lessons/:lesson_id/feedback",async o=>{let r=await o.req.parseBody(),s=m(r,"feedback"),n=s===""||s==="clear"?null:s;return p(l=>ea(l,o.req.param("lesson_id"),n)),o.redirect(M(m(r,"next")||void 0),303)}),e.post("/lessons/delete",async o=>{if(B(o))return o.text("Forbidden",403);let r=await o.req.parseBody({all:true}),s=r.id,n=(Array.isArray(s)?s:[s]).filter(l=>typeof l=="string"&&l!=="");return p(l=>u(l,n)),o.redirect(M(m(r,"next")||void 0),303)}),e.on(["GET","POST"],"/lessons/:lesson_id/share",async o=>{if(B(o))return o.text("Forbidden",403);let r,s,n$1=false;if(o.req.method==="POST"){let p=await o.req.parseBody();r=m(p,"name"),s=m(p,"include_context")==="1",n$1=m(p,"persist")==="1";}else r=o.req.query("name"),s=o.req.query("include_context")==="1";let l=p(p=>{let h=A(p,o.req.param("lesson_id")??"");return h?(n$1&&r!==void 0&&K(p,"share_name",n(r)),{lesson:h,settingName:J$1(p).share_name}):null});if(!l)return o.text("Lesson not found",404);let{state:g,payload:c}=De(l.lesson,l.settingName,{name:r,includeContext:s}),i=o.req.query("format");return i==="text"?o.text(g.text):i==="link"?o.text(g.link):i==="md"?new Response(aa(c),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${$(c)}"`}}):i==="panel"?o.html(z(l.lesson.id,g,l.lesson.title)):o.html(F(g))}),e.get("/lessons/:lesson_id",o=>{let r=p(l=>{let g=A(l,o.req.param("lesson_id"));if(!g)return null;let c=J$1(l);return {lesson:g,settingName:c.share_name,uiTheme:c.ui_theme}});if(!r)return o.html("<h1>Lesson not found</h1>",404);let s=o.req.query(),n=s.imported==="1"?"new":s.imported==="dup"?"dup":null;return o.html(Oe({lesson:r.lesson,uiTheme:r.uiTheme,share:De(r.lesson,r.settingName,{open:s.share==="1"}).state,imported:n}))}),e.get("/settings/export",()=>{let o=p(r=>R(r));return new Response(new Uint8Array(o),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e.post("/settings/import",async o=>{let r=(await o.req.parseBody()).file;if(!(r instanceof File))return o.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=new Uint8Array(await r.arrayBuffer()),n=p(l=>S(l,s));return o.redirect(`/settings?imported=${n.lessons}&skipped=${n.skipped}&invalid=${n.invalid}&groups=${n.groups}&notebook=${n.learning_state}`,303)}),e.get("/settings/notebook/download",()=>{let o=existsSync(k)?readFileSync(k,"utf8"):"";return new Response(o,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e.post("/settings/notebook",async o=>{let r=await o.req.parseBody();return mkdirSync(dirname(k),{recursive:true}),writeFileSync(k,m(r,"content"),"utf8"),o.redirect(M(m(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),e.post("/settings",async o=>{let r=await o.req.parseBody(),s=Number.parseInt(m(r,"max_per_day","2"),10)||2,n$1=Number.parseInt(m(r,"min_gap_minutes","240"),10),l=m(r,"ui_theme","system");["system","dark","light"].includes(l)||(l="system");let g=m(r,"ui_home","auto");["auto","lessons","knowledge"].includes(g)||(g="auto");let c=Math.max(0,Number.parseInt(m(r,"nudge_every","10"),10)||0),i=m(r,"nudge_scope","session");return i!=="session"&&i!=="global"&&(i="session"),p(p=>{K(p,"max_per_day",String(s)),K(p,"min_gap_minutes",String(Number.isNaN(n$1)?240:n$1)),K(p,"ui_theme",l),K(p,"ui_home",g),K(p,"nudge_every",String(c)),K(p,"nudge_scope",i),K(p,"share_name",n(m(r,"share_name")));}),o.redirect("/settings",303)}),e.get("/settings",o=>{let r=p(g=>J$1(g)),s=existsSync(k)?readFileSync(k,"utf8"):"",n=o.req.query(),l=n.imported!==void 0?{imported:Number(n.imported??0),skipped:Number(n.skipped??0),invalid:Number(n.invalid??0),groups:Number(n.groups??0),notebook:Number(n.notebook??0)}:null;return o.html(Ne({settings:r,notebookContent:s,notebookPath:k,uiTheme:r.ui_theme,flash:l}))}),e}function st(t,e=2e3){let o=t;return new Promise(r=>{let s=setTimeout(()=>{o.closeAllConnections?.();},e),n=setInterval(()=>o.closeIdleConnections?.(),100);t.close(()=>{clearTimeout(s),clearInterval(n),r();}),o.closeIdleConnections?.();})}async function nt(t,e=fetch){try{let o=await e(`http://127.0.0.1:${t}/ping`,{signal:AbortSignal.timeout(1500)});if(!o.ok)return null;let r=await o.json();return r.ok===!0?{version:String(r.version??"")}:null}catch{return null}}async function it(t,e,o=nt){if(e.code==="EADDRINUSE"){let r=await o(t);if(r){let s=`http://localhost:${t}`,n=r.version?` (v${r.version})`:"";return {existingUrl:s,exitCode:0,message:`${a.yellow("devcoach UI is already running at")} ${a.cyan(c$1(s))}${n}
729
- ${a.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a.red(`\u2717 Port ${t} is already in use by another process`)}
730
- ${a.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a.red(`\u2717 Port ${t} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a.red(`\u2717 Could not start the devcoach UI on port ${t}: ${e.message}`)}}function $t(t,e={}){let o=false,r=(c,i)=>{if(o){i&&process.exit(130);return}o=true,process.stdout.isTTY&&console.log(a.dim(`devcoach UI stopping (${c})\u2026`)),st(n).then(()=>{i&&process.exit(0);});},s=at({onShutdown:()=>r("stop requested",false)}),n=serve({fetch:s.fetch,port:t,hostname:"127.0.0.1"},c=>{let i=`http://localhost:${c.port}`;console.log(`devcoach UI running at ${a.cyan(c$1(i))}`),process.stdout.isTTY&&console.log(a.dim("Press Ctrl+C to stop")),e.onReady?.(i);}),l=["SIGINT","SIGTERM","SIGHUP"],g={SIGINT:()=>r("SIGINT",true),SIGTERM:()=>r("SIGTERM",true),SIGHUP:()=>r("SIGHUP",true)};if(e.handleSignals!==false)for(let c of l)process.on(c,g[c]);return n.once("error",c=>{for(let i of l)process.off(i,g[i]);it(t,c).then(i=>{if(e.onListenError){e.onListenError(i);return}console.error(i.message),process.exit(i.exitCode);});}),n}async function St(t,e=fetch){try{return (await e(`http://127.0.0.1:${t}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
731
- export{at as createApp,it as explainListenError,st as gracefulShutdown,nt as pingUi,rt as resolveHomePath,$t as startUi,St as stopUi};
805
+ document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(t.notebookContent))}));
806
+ </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:o,scripts:n})}var Z=25;function B(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function y(t,e,g=""){let d=t[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(t=>existsSync(t))??join(Q,"../assets/static"),ut={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},mt="https://ultimaphoenix.github.io",Xe={"access-control-allow-origin":mt,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function L(t){let e=t.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function xt(){return ee===void 0&&(ee=n$1()),ee}function et(t,e,g){let d=ka({explicit:g.name??null,setting:e,gitUserName:xt()}),r=g.includeContext??false,o=ja(t,{includeContext:r,sharedBy:d}),n=la(o);return {payload:o,state:{id:t.id,open:g.open??false,name:d??"",includeContext:r,text:na(o,n),link:oa(o,n)}}}function yt(t,e){return t==="lessons"?"/lessons":t==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function bt(){try{return u(t=>N(t).ui_theme)}catch{return "system"}}function ht(t={}){let e$1=new Hono;e$1.get("/static/*",r=>{let o=decodeURIComponent(r.req.path.slice(8)),n=join(X,o);if(!n.startsWith(X))return r.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":ut[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return r.notFound()}}),e$1.get("/ping",r=>r.json({ok:true,version:ha},200,Xe)),e$1.options("/ping",r=>r.body(null,204,Xe)),e$1.post("/shutdown",r=>L(r)?r.text("Forbidden",403):(setTimeout(()=>t.onShutdown?.(),50),r.json({ok:true}))),e$1.get("/shutdown",r=>r.text("Method Not Allowed",405)),e$1.get("/",r=>{let{home:o,hasLessons:n}=u(i=>({home:N(i).ui_home,hasLessons:w(i)>0}));return r.redirect(yt(o,n),302)}),e$1.get("/knowledge",r=>{let{profile:o,stats:n,rateLimit:i,settings:s}=u(p=>({profile:ta(p),stats:wa(p),rateLimit:sa(p),settings:N(p)})),c=new Map;for(let p of o.groups)for(let m of p.topics)c.set(m,p.name);let u$1={};for(let p of o.groups)u$1[p.name]=[];for(let p of o.knowledge){let m=c.get(p.topic)??"Other",v=u$1[m]??[];v.push(p),u$1[m]=v;}return r.html(ze({categorised:u$1,allGroups:o.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async r=>{let o=await r.req.parseBody(),n=y(o,"topic").trim();if(n){let i=Number.parseInt(y(o,"confidence","5"),10)||5,s=y(o,"group").trim();u(c=>{H(c,n,i),s&&s!=="Other"&&L$1(c,n,s);});}return r.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",r=>(u(o=>I(o,r.req.param("topic"))),r.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async r=>{let o=y(await r.req.parseBody(),"group").trim(),n=r.req.param("topic");return u(i=>{o&&o!=="Other"?L$1(i,n,o):M(i,n);}),r.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async r=>{let o=Number.parseInt(y(await r.req.parseBody(),"delta","0"),10)||0;return u(n=>ua(n,r.req.param("topic"),o)),r.redirect("/knowledge",303)}),e$1.post("/groups",async r=>{let o=y(await r.req.parseBody(),"group_name").trim();return o&&o!=="Other"&&u(n=>J$1(n,o)),r.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",r=>(u(o=>K$1(o,r.req.param("group_name"))),r.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let r=u(o=>B$1(o));return new Response(JSON.stringify(r,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async r=>{let o=await r.req.parseBody(),n=y(o,"from")==="lessons",i=p=>r.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(L(r))return i("cross");let s=o.file,u$1=(s instanceof File&&s.size>0?await s.text():null)??y(o,"text").trim();try{p(u$1)&&!u$1.includes("#devcoach:lesson:")&&(u$1=await q(u$1));let p$1=u(m=>xa(m,u$1));return p$1.kind==="lessons"||!p$1.lesson?r.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):r.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof ia)return i("invalid");throw p}}),e$1.get("/lessons/import",r=>{let o=(r.req.query("code")??"").trim(),n=null,i=null;if(o)try{n=ma(o);}catch(s){i=s instanceof ia?s.message:"That code could not be read.";}return r.html(Ye({code:o,preview:n,error:i,uiTheme:bt()}))}),e$1.get("/lessons",r=>{let o=r.req.query(),n=new Set(["junior","mid","senior"]),i=o.level&&n.has(o.level)?o.level:"",s=o.date_from||"",c=o.date_to||"",u$1=o.starred==="1",p=o.imported==="1"?"1":o.imported==="0"?"0":"",m=o.shared_by||null,v=o.sort||"timestamp",I=o.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(o.page??"1",10)||1),E=o.period||"all",l={period:s||c?null:E!=="all"?E:null,category:o.category||null,level:i||null,project:o.project||null,repository:o.repository||null,branch:o.branch||null,commit:o.commit||null,starred:u$1?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:o.search||null,feedback:o.feedback||null,date_from:s||null,date_to:c||null},x$1=u(k=>{let re=w(k,l),oe=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,oe),{total:re,totalPages:oe,lessons:x(k,{...l,sort:v,order:I,page:R,per_page:Z}),allCategories:F(k),allProjects:C(k,"project"),allRepositories:C(k,"repository"),allBranches:C(k,"branch"),allCommits:C(k,"commit_hash"),allSharedBy:D(k),theme:N(k).ui_theme}}),$={period:E,category:o.category||"",level:i,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:u$1,imported:m?"1":p,shared_by:m??"",search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:c,sort:v,order:I},S=o.error==="invalid"||o.error==="cross"?o.error:null;return r.html(Ke({importOpen:o.import==="1",importError:S,lessons:x$1.lessons,allCategories:x$1.allCategories,allProjects:x$1.allProjects,allRepositories:x$1.allRepositories,allBranches:x$1.allBranches,allCommits:x$1.allCommits,allSharedBy:x$1.allSharedBy,s:$,page:R,perPage:Z,total:x$1.total,totalPages:x$1.totalPages,uiTheme:x$1.theme}))}),e$1.post("/lessons/:lesson_id/star",async r=>{let o=await r.req.parseBody();return u(n=>A(n,r.req.param("lesson_id"),o.starred==="1")),r.redirect(B(y(o,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async r=>{let o=await r.req.parseBody(),n=y(o,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return r.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return u(s=>va(s,r.req.param("lesson_id"),i)),r.redirect(B(y(o,"next")||void 0),303)}),e$1.post("/lessons/delete",async r=>{if(L(r))return r.text("Forbidden",403);let o=await r.req.parseBody({all:true}),n=o.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return u(s=>z(s,i)),r.redirect(B(y(o,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async r=>{if(L(r))return r.text("Forbidden",403);let o,n,i=false;if(r.req.method==="POST"){let m=await r.req.parseBody();o=y(m,"name"),n=y(m,"include_context")==="1",i=y(m,"persist")==="1";}else o=r.req.query("name"),n=r.req.query("include_context")==="1";let s$1=u(m=>{let v=E(m,r.req.param("lesson_id")??"");return v?(i&&o!==void 0&&O(m,"share_name",s(o)),{lesson:v,settingName:N(m).share_name}):null});if(!s$1)return r.text("Lesson not found",404);let{state:c,payload:u$1}=et(s$1.lesson,s$1.settingName,{name:o,includeContext:n}),p=r.req.query("format");return p==="text"?r.text(c.text):p==="link"?r.text(c.link):p==="md"?new Response(ra(u$1),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${qa(u$1)}"`}}):p==="panel"?r.html(W(s$1.lesson.id,c,s$1.lesson.title)):r.html(V(c))}),e$1.get("/lessons/:lesson_id",r=>{let o=u(s=>{let c=E(s,r.req.param("lesson_id"));if(!c)return null;let u=N(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!o)return r.html("<h1>Lesson not found</h1>",404);let n=r.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return r.html(Je({lesson:o.lesson,uiTheme:o.uiTheme,share:et(o.lesson,o.settingName,{open:n.share==="1"}).state,imported:i,course:o.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d=(()=>{try{return `
807
+ <script>${readFileSync(join(X,"course-frame.js"),"utf8")}</script>
808
+ `}catch{return ""}})();return e$1.get("/courses",r=>{let o=u(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?E(n,i.lesson_id)?.title??null:null})),uiTheme:N(n).ui_theme}));return r.html(We(o))}),e$1.get("/courses/:course_id/index.html",r=>{let o=r.req.param("course_id");if(!a.test(o))return r.notFound();let n=e(o);if(!n)return r.notFound();try{let i=Buffer.concat([readFileSync(n),Buffer.from(d)]);return new Response(new Uint8Array(i),{headers:new Headers(g$1)})}catch{return r.notFound()}}),e$1.get("/courses/:course_id",r=>{let o=r.req.param("course_id"),n=u(c=>{let u=g(c,o);return u?{course:u,lessonTitle:u.lesson_id?E(c,u.lesson_id)?.title??null:null,uiTheme:N(c).ui_theme}:null});if(!n)return r.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(r.req.query("step")??"",10)||i;return r.html(Ze({...n,selected:s,hasDocument:e(o)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async r=>{if(L(r))return r.text("Forbidden",403);let o=await r.req.parseBody(),n=y(o,"status");if(!["todo","done","skipped"].includes(n))return r.text("Unknown status",400);let i=Number.parseInt(r.req.param("position"),10),s=r.req.param("course_id");return u(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?r.redirect(B(y(o,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):r.text("Step not found",404)}),e$1.post("/courses/delete",async r=>{if(L(r))return r.text("Forbidden",403);let o=await r.req.parseBody();return u(n=>l(n,y(o,"id"))),r.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let r=u(o=>ea(o));return new Response(new Uint8Array(r),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async r=>{let o=(await r.req.parseBody()).file;if(!(o instanceof File))return r.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await o.arrayBuffer()),i=u(s=>ga(s,n));return r.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let r=existsSync(n)?readFileSync(n,"utf8"):"";return new Response(r,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async r=>{let o=await r.req.parseBody();return mkdirSync(dirname(n),{recursive:true}),writeFileSync(n,y(o,"content"),"utf8"),r.redirect(B(y(o,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async r=>{let o=await r.req.parseBody(),n=Number.parseInt(y(o,"max_per_day","2"),10)||2,i=Number.parseInt(y(o,"min_gap_minutes","240"),10),s$1=y(o,"ui_theme","system");["system","dark","light"].includes(s$1)||(s$1="system");let c=y(o,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u$1=Math.max(0,Number.parseInt(y(o,"nudge_every","10"),10)||0),p=y(o,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),u(m=>{O(m,"max_per_day",String(n)),O(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),O(m,"ui_theme",s$1),O(m,"ui_home",c),O(m,"nudge_every",String(u$1)),O(m,"nudge_scope",p),O(m,"share_name",s(y(o,"share_name")));}),r.redirect("/settings",303)}),e$1.get("/settings",r=>{let o=u(c=>N(c)),n$1=existsSync(n)?readFileSync(n,"utf8"):"",i=r.req.query(),s=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return r.html(Qe({settings:o,notebookContent:n$1,notebookPath:n,uiTheme:o.ui_theme,flash:s}))}),e$1}function ft(t,e=2e3){let g=t;return new Promise(d=>{let r=setTimeout(()=>{g.closeAllConnections?.();},e),o=setInterval(()=>g.closeIdleConnections?.(),100);t.close(()=>{clearTimeout(r),clearInterval(o),d();}),g.closeIdleConnections?.();})}async function kt(t,e=fetch){try{let g=await e(`http://127.0.0.1:${t}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function vt(t,e,g=kt){if(e.code==="EADDRINUSE"){let d=await g(t);if(d){let r=`http://localhost:${t}`,o=d.version?` (v${d.version})`:"";return {existingUrl:r,exitCode:0,message:`${a$1.yellow("devcoach UI is already running at")} ${a$1.cyan(c(r))}${o}
809
+ ${a$1.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$1.red(`\u2717 Port ${t} is already in use by another process`)}
810
+ ${a$1.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$1.red(`\u2717 Port ${t} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$1.red(`\u2717 Could not start the devcoach UI on port ${t}: ${e.message}`)}}function At(t,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$1.dim(`devcoach UI stopping (${s})\u2026`)),ft(o).then(()=>{c&&process.exit(0);});},r=ht({onShutdown:()=>d("stop requested",false)}),o=serve({fetch:r.fetch,port:t,hostname:"127.0.0.1"},s=>{let c$1=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$1.cyan(c(c$1))}`),process.stdout.isTTY&&console.log(a$1.dim("Press Ctrl+C to stop")),e.onReady?.(c$1);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return o.once("error",s=>{for(let c of n)process.off(c,i[c]);vt(t,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),o}async function Dt(t,e=fetch){try{return (await e(`http://127.0.0.1:${t}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
811
+ export{ht as createApp,vt as explainListenError,ft as gracefulShutdown,kt as pingUi,yt as resolveHomePath,At as startUi,Dt as stopUi};