devcoach 0.8.3 → 0.9.0

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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r,U,w,P,Q,j,G}from'./chunk-227BHE2B.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 C(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function S(t){let e=(r,a,s)=>html`<a href="${r}" 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":""}">${a}</a>`;return html`<!DOCTYPE html>
2
+ import {m,F,P,T,Q,z,D,A as A$1,E,R,B as B$1,C as C$1,t,u,o,v,x as x$1,p,r,S as S$1,w,N,O,j,G}from'./chunk-ECN4MNGM.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 C(t){return JSON.stringify(t??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function ne(t,e){return t+e.replace(/[^A-Za-z0-9_-]/g,r=>`_${(r.codePointAt(0)??0).toString(16)}`)}function S(t){let e=(r,o,s)=>html`<a href="${r}" 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":""}">${o}</a>`;return html`<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8" />
@@ -27,8 +27,8 @@ import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r
27
27
  <nav class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-3 flex items-center gap-6">
28
28
  <!-- Brand wordmark, mirrors the docs site (website/src/css/custom.css): "dev" in the
29
29
  theme foreground, "coach" in the teal accent (#0d9488 / #5eead4 = teal-600/300). -->
30
- <span class="font-extrabold text-lg tracking-tight text-gray-900 dark:text-gray-100"
31
- >🎓 dev<span class="text-teal-600 dark:text-teal-300">coach</span></span
30
+ <a href="/" class="font-extrabold text-lg tracking-tight text-gray-900 dark:text-gray-100 hover:opacity-80 transition"
31
+ >🎓 dev<span class="text-teal-600 dark:text-teal-300">coach</span></a
32
32
  >
33
33
  ${e("/","Profile",t.currentPath==="/")}
34
34
  ${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
@@ -60,8 +60,8 @@ import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r
60
60
  </script>
61
61
  ${t.scripts??""}
62
62
  </body>
63
- </html>`}function se(t){let e=a=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===a?"selected":""}>${s}</option>`),r=html`
64
- <div x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
63
+ </html>`}function ie(t){let e=o=>t.allGroups.filter(s=>s!=="Other").map(s=>html`<option value="${s}" ${s===o?"selected":""}>${s}</option>`),r=html`
64
+ <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); } }">
65
65
 
66
66
  <div class="flex items-center justify-between mb-6">
67
67
  <h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Knowledge Map</h1>
@@ -70,7 +70,7 @@ import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r
70
70
  <button type="button" @click="open = !open"
71
71
  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">+ Add group</button>
72
72
  <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-56" style="display:none">
73
- <form method="post" action="/groups" class="flex gap-2">
73
+ <form method="post" action="/groups" hx-post="/groups" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="flex gap-2">
74
74
  <input type="text" name="group_name" placeholder="Group name…" required class="flex-1 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" />
75
75
  <button type="submit" class="px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
76
76
  </form>
@@ -79,7 +79,7 @@ import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r
79
79
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
80
80
  <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 bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add topic</button>
81
81
  <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-4 w-72" style="display:none">
82
- <form method="post" action="/knowledge" class="space-y-3">
82
+ <form method="post" action="/knowledge" hx-post="/knowledge" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="space-y-3">
83
83
  <div>
84
84
  <label for="add-topic-id" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Topic ID</label>
85
85
  <input id="add-topic-id" type="text" name="topic" placeholder="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" />
@@ -119,20 +119,20 @@ import {m as m$1,F,R as R$1,V,S as S$1,z,D,A,E,T,B as B$1,C as C$1,t,u,o,v,x,p,r
119
119
  ${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>`}
120
120
  </div>
121
121
 
122
- ${Object.entries(t.categorised).map(([a,s])=>html`
122
+ ${Object.entries(t.categorised).map(([o,s])=>html`
123
123
  <section class="mb-6 group/section">
124
124
  <div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
125
125
  <div class="flex items-center gap-1 flex-1 min-w-0">
126
- <h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${a}</h2>
127
- ${a!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(a)}/delete" onsubmit="return confirm('Remove group &quot;${a}&quot;? Topics will move to Other.')">
126
+ <h2 class="text-xs font-semibold uppercase tracking-widest text-gray-400 dark:text-gray-500 shrink-0">${o}</h2>
127
+ ${o!=="Other"?html`<form x-show="editMode" style="display:none" method="post" action="/groups/${encodeURIComponent(o)}/delete" hx-post="/groups/${encodeURIComponent(o)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove group &quot;${o}&quot;? Topics will move to Other.">
128
128
  <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>
129
129
  </form>`:""}
130
130
  </div>
131
131
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
132
132
  <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>
133
133
  <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">
134
- <form method="post" action="/knowledge" class="space-y-2">
135
- <input type="hidden" name="group" value="${a!=="Other"?a:""}" />
134
+ <form method="post" action="/knowledge" hx-post="/knowledge" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="space-y-2">
135
+ <input type="hidden" name="group" value="${o!=="Other"?o:""}" />
136
136
  <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" />
137
137
  <div class="flex items-center gap-2">
138
138
  <label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
@@ -144,15 +144,15 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
144
144
  </div>
145
145
  </div>
146
146
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
147
- ${s.map(i=>{let g=i.confidence,l=Math.trunc(g/10*100),d=g>=7?"bg-green-500":g>=4?"bg-yellow-500":"bg-red-500",y=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`
148
- <div class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
147
+ ${s.map(i=>{let g=ne("topic-row-",i.topic),d=i.confidence,l=Math.trunc(d/10*100),m=d>=7?"bg-green-500":d>=4?"bg-yellow-500":"bg-red-500",b=d>=7?"text-green-600 dark:text-green-400":d>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
148
+ <div id="${g}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
149
149
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
150
150
  <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>
151
151
  <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">
152
- <form method="post" action="/knowledge/${encodeURIComponent(i.topic)}/group">
153
- <select name="group" onchange="this.form.submit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
154
- <option value="Other" ${a==="Other"?"selected":""}>Other (ungrouped)</option>
155
- ${e(a)}
152
+ <form method="post" action="/knowledge/${encodeURIComponent(i.topic)}/group" hx-post="/knowledge/${encodeURIComponent(i.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
153
+ <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">
154
+ <option value="Other" ${o==="Other"?"selected":""}>Other (ungrouped)</option>
155
+ ${e(o)}
156
156
  </select>
157
157
  </form>
158
158
  </div>
@@ -160,29 +160,29 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
160
160
  <div class="flex items-center gap-1 flex-1 min-w-0">
161
161
  <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(i.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="${i.topic}">${i.topic}</a>
162
162
  <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${i.topic}">${i.topic}</span>
163
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}/delete" onsubmit="return confirm('Remove ${i.topic} from your knowledge map?')" class="shrink-0 flex items-center">
163
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(i.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${i.topic} from your knowledge map?" class="shrink-0 flex items-center">
164
164
  <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>
165
165
  </form>
166
166
  </div>
167
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" class="shrink-0 flex items-center">
167
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" hx-post="/knowledge/${encodeURIComponent(i.topic)}" hx-target="#${g}" hx-select="#${g}" hx-swap="outerHTML" class="shrink-0 flex items-center">
168
168
  <input type="hidden" name="delta" value="-1" />
169
169
  <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>
170
170
  </form>
171
171
  <div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
172
- <div class="${d} h-full rounded-full transition-all" style="width: ${l}%"></div>
172
+ <div class="${m} h-full rounded-full transition-all" style="width: ${l}%"></div>
173
173
  </div>
174
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" class="shrink-0 flex items-center">
174
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(i.topic)}" hx-post="/knowledge/${encodeURIComponent(i.topic)}" hx-target="#${g}" hx-select="#${g}" hx-swap="outerHTML" class="shrink-0 flex items-center">
175
175
  <input type="hidden" name="delta" value="1" />
176
176
  <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>
177
177
  </form>
178
- <span class="text-xs font-bold ${y} w-5 text-right shrink-0">${g}</span>
178
+ <span class="text-xs font-bold ${b} w-5 text-right shrink-0">${d}</span>
179
179
  </div>`})}
180
180
  </div>
181
181
  </section>`)}
182
- </div>`;return S({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:r})}var re={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},ae={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},oe={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function m(t,e={}){let r={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(r.starred="1"),Object.assign(r,e);let a=new URLSearchParams;for(let[s,i]of Object.entries(r))i&&a.set(s,i);return `?${a.toString()}`}function ne(t){let e=t.s,r=!!(e.date_from||e.date_to),a=e.period!=="all"&&!r||r||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),s=r?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:re[e.period]??"All time",i=e.feedback?ae[e.feedback]??"Feedback":"Feedback",g={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"},l=(n,c)=>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">${n}<a href="${c}" 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>`,d=(n,c,h="")=>{let f=e.sort===c,ce=f&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${h}">
183
- <a href="${m(e,{sort:c,order:ce})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${n}
184
- ${f?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>`}
185
- </a></th>`},y=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>`,u=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
182
+ </div>`;return S({title:"Profile \u2014 devcoach",currentPath:"/",uiTheme:t.uiTheme,body:r})}var oe={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},ae={know:"\u2713 Known",dont_know:"\u2717 Don't know",none:"\u2014 No response"},se={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function x(t,e={}){let r={period:t.period,category:t.category,level:t.level,project:t.project,repository:t.repository,branch:t.branch,commit:t.commit,feedback:t.feedback,search:t.search,date_from:t.date_from,date_to:t.date_to,sort:t.sort,order:t.order};t.starred&&(r.starred="1"),Object.assign(r,e);let o=new URLSearchParams;for(let[s,i]of Object.entries(r))i&&o.set(s,i);return `?${o.toString()}`}function de(t){let e=t.s,r=!!(e.date_from||e.date_to),o=e.period!=="all"&&!r||r||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.search||e.feedback),s=r?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:oe[e.period]??"All time",i=e.feedback?ae[e.feedback]??"Feedback":"Feedback",g={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"},d=(n,c)=>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">${n}<a href="${c}" 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>`,l=(n,c,f="")=>{let w=e.sort===c,j=w&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap ${f}">
183
+ <a href="${x(e,{sort:c,order:j})}" class="inline-flex items-center gap-1 hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${n}
184
+ ${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>`}
185
+ </a></th>`},m=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>`,b=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
186
186
  <style>
187
187
  .dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
188
188
  .dark .flatpickr-day { color:#e5e7eb; }
@@ -193,7 +193,7 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
193
193
  .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
194
194
  .dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
195
195
  .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
196
- </style>`,w=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}`,$=html`
196
+ </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}`,$=html`
197
197
  <form id="filter-form" method="get" action="/lessons">
198
198
  <input type="hidden" name="period" id="h-period" value="${e.period}">
199
199
  <input type="hidden" name="date_from" id="h-date-from" value="${e.date_from}">
@@ -213,7 +213,7 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
213
213
  <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" />
214
214
  ${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>`:""}
215
215
  </div>
216
- <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${w}</p>
216
+ <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${v}</p>
217
217
  </div>
218
218
 
219
219
  <div class="flex flex-wrap items-center gap-2 mb-3">
@@ -222,11 +222,11 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
222
222
 
223
223
  <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
224
224
  <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 ${r||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"}">
225
- <span>📅</span><span x-text="label">${s}</span>${y}
225
+ <span>📅</span><span x-text="label">${s}</span>${m}
226
226
  </button>
227
227
  <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">
228
228
  <div class="p-1">
229
- ${Object.entries(re).map(([n,c])=>{let h=e.period===n&&!r;return html`<button type="button" @click="selectPreset('${n}', '${c}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${h?"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"}">${c}${h?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
229
+ ${Object.entries(oe).map(([n,c])=>{let f=e.period===n&&!r;return html`<button type="button" @click="selectPreset('${n}', '${c}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${f?"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"}">${c}${f?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
230
230
  </div>
231
231
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
232
232
  <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 ${r?"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"}">
@@ -241,21 +241,21 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
241
241
  </div>
242
242
 
243
243
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
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.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"}">${i}${y}</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.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"}">${i}${m}</button>
245
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-44 p-1 overflow-hidden" style="display:none">
246
246
  ${[["","All feedback"],["know","\u2713 Known"],["dont_know","\u2717 Don't know"],["none","\u2014 No response"]].map(([n,c])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${n}'; 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===n?"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"}">${c}${e.feedback===n?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
247
247
  </div>
248
248
  </div>
249
249
 
250
250
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
251
- <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?`${oe[e.level]??""} ${e.level}`:"Level"}${y}</button>
251
+ <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?`${se[e.level]??""} ${e.level}`:"Level"}${m}</button>
252
252
  <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">
253
253
  ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([n,c])=>html`<button type="button" onclick="document.getElementById('h-level').value='${n}'; 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===n?"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"}">${c}${e.level===n?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
254
254
  </div>
255
255
  </div>
256
256
 
257
257
  ${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">
258
- <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${y}</button>
258
+ <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${m}</button>
259
259
  <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">
260
260
  ${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(n=>html`<option value="${n}" ${e.category===n?"selected":""}>${n}</option>`)}</select></div>`:""}
261
261
  ${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(n=>html`<option value="${n}" ${e.project===n?"selected":""}>${n}</option>`)}</select></div>`:""}
@@ -266,21 +266,21 @@ ${Object.entries(t.categorised).map(([a,s])=>html`
266
266
  </div>
267
267
  </div>`:""}
268
268
 
269
- ${a?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
269
+ ${o?html`<a href="/lessons" class="ml-auto text-xs text-gray-400 hover:text-gray-700 dark:hover:text-white transition">Clear all</a>`:""}
270
270
  </div>
271
271
 
272
- ${a?html`<div class="flex flex-wrap gap-1.5 mb-4">
273
- ${r?l(`\u{1F4C5} ${s}`,m(e,{date_from:"",date_to:""})):""}
274
- ${e.period!=="all"&&!r?l(`\u{1F550} ${s}`,m(e,{period:"all"})):""}
275
- ${e.feedback?l(ae[e.feedback]??e.feedback,m(e,{feedback:""})):""}
276
- ${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="${m({...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>`:""}
277
- ${e.search?l(`\u{1F50D} "${e.search}"`,m(e,{search:""})):""}
278
- ${e.category?l(e.category,m(e,{category:""})):""}
279
- ${e.level?l(`${oe[e.level]??""} ${e.level}`,m(e,{level:""})):""}
280
- ${e.project?l(`\u{1F4C1} ${e.project}`,m(e,{project:""})):""}
281
- ${e.repository?l(`\u2387 ${e.repository}`,m(e,{repository:""})):""}
282
- ${e.branch?l(`\u2387 ${e.branch}`,m(e,{branch:""})):""}
283
- ${e.commit?l(e.commit.slice(0,7),m(e,{commit:""})):""}
272
+ ${o?html`<div class="flex flex-wrap gap-1.5 mb-4">
273
+ ${r?d(`\u{1F4C5} ${s}`,x(e,{date_from:"",date_to:""})):""}
274
+ ${e.period!=="all"&&!r?d(`\u{1F550} ${s}`,x(e,{period:"all"})):""}
275
+ ${e.feedback?d(ae[e.feedback]??e.feedback,x(e,{feedback:""})):""}
276
+ ${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="${x({...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>`:""}
277
+ ${e.search?d(`\u{1F50D} "${e.search}"`,x(e,{search:""})):""}
278
+ ${e.category?d(e.category,x(e,{category:""})):""}
279
+ ${e.level?d(`${se[e.level]??""} ${e.level}`,x(e,{level:""})):""}
280
+ ${e.project?d(`\u{1F4C1} ${e.project}`,x(e,{project:""})):""}
281
+ ${e.repository?d(`\u2387 ${e.repository}`,x(e,{repository:""})):""}
282
+ ${e.branch?d(`\u2387 ${e.branch}`,x(e,{branch:""})):""}
283
+ ${e.commit?d(e.commit.slice(0,7),x(e,{commit:""})):""}
284
284
  </div>`:""}
285
285
  </form>
286
286
 
@@ -289,31 +289,31 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
289
289
  <thead>
290
290
  <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">
291
291
  <th class="w-8 px-3 py-3"></th>
292
- ${d("Date","timestamp")}
293
- ${d("Topic","topic_id","hidden sm:table-cell")}
294
- ${d("Title","title")}
295
- ${d("Level","level")}
292
+ ${l("Date","timestamp")}
293
+ ${l("Topic","topic_id","hidden sm:table-cell")}
294
+ ${l("Title","title")}
295
+ ${l("Level","level")}
296
296
  <th class="px-3 py-3 hidden lg:table-cell">Categories</th>
297
- ${d("Feedback","feedback","hidden xl:table-cell")}
297
+ ${l("Feedback","feedback","hidden xl:table-cell")}
298
298
  </tr>
299
299
  </thead>
300
300
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
301
- ${t.lessons.map(n=>{let c=n.timestamp.slice(0,10),h=n.timestamp.slice(0,16).replace("T"," ");return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(n.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(n.id)}'" role="link">
301
+ ${t.lessons.map(n=>{let c=ne("lesson-row-",n.id),f=n.timestamp.slice(0,10),w=n.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${c}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" onclick="window.location='/lessons/${encodeURIComponent(n.id)}'" onkeydown="if(event.key==='Enter')window.location='/lessons/${encodeURIComponent(n.id)}'" role="link">
302
302
  <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">
303
- <form method="post" action="/lessons/${encodeURIComponent(n.id)}/star">
303
+ <form method="post" action="/lessons/${encodeURIComponent(n.id)}/star" hx-post="/lessons/${encodeURIComponent(n.id)}/star" hx-target="#${c}" hx-select="#${c}" hx-swap="outerHTML">
304
304
  <input type="hidden" name="starred" value="${n.starred?"0":"1"}" />
305
- <input type="hidden" name="next" value="/lessons${m(e)}" />
305
+ <input type="hidden" name="next" value="/lessons${x(e)}" />
306
306
  <button type="submit" title="${n.starred?"Unstar":"Star"}" class="w-6 text-lg text-center leading-none transition ${n.starred?"text-yellow-400 hover:text-yellow-300":"text-gray-300 dark:text-gray-600 hover:text-yellow-400"}">${n.starred?"\u2605":"\u2606"}</button>
307
307
  </form>
308
308
  </td>
309
309
  <td class="px-3 py-3 whitespace-nowrap tabular-nums relative group/date">
310
- <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${n.timestamp}">${c}</span>
311
- <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">${h}</div>
310
+ <span class="text-gray-400 dark:text-gray-500 cursor-default" data-ts="${n.timestamp}">${f}</span>
311
+ <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>
312
312
  </td>
313
313
  <td class="px-3 py-3 hidden sm:table-cell"><span class="text-xs font-mono text-cyan-600 dark:text-cyan-400">${n.topic_id}</span></td>
314
314
  <td class="px-3 py-3 max-w-xs"><a href="/lessons/${encodeURIComponent(n.id)}" class="font-medium text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${n.title}</a></td>
315
- <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${m(e,{level:n.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${g[n.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${n.level}</a></td>
316
- <td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${n.categories.map(f=>html`<a href="${m(e,{category:f})}" 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">${f}</a>`)}</div></td>
315
+ <td class="px-3 py-3" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><a href="${x(e,{level:n.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${g[n.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${n.level}</a></td>
316
+ <td class="px-3 py-3 hidden lg:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()"><div class="flex flex-wrap gap-1">${n.categories.map(j=>html`<a href="${x(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>
317
317
  <td class="px-3 py-3 hidden xl:table-cell" onclick="event.stopPropagation()" onkeydown="event.stopPropagation()">${n.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Known</span>`:n.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Unknown</span>`:""}</td>
318
318
  </tr>`})}
319
319
  </tbody>
@@ -322,15 +322,15 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
322
322
  ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
323
323
  <p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
324
324
  <div class="flex items-center gap-1">
325
- ${t.page>1?html`<a href="${m(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>`}
326
- ${pe(t.page,t.totalPages).map(n=>n===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:n===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">${n}</span>`:html`<a href="${m(e,{page:String(n)})}" 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">${n}</a>`)}
327
- ${t.page<t.totalPages?html`<a href="${m(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>`}
325
+ ${t.page>1?html`<a href="${x(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>`}
326
+ ${me(t.page,t.totalPages).map(n=>n===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:n===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">${n}</span>`:html`<a href="${x(e,{page:String(n)})}" 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">${n}</a>`)}
327
+ ${t.page<t.totalPages?html`<a href="${x(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>`}
328
328
  </div>
329
329
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
330
330
  <p class="text-3xl mb-3">📭</p>
331
331
  <p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
332
- ${a?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
333
- </div>`}`,x=html`<script src="/static/vendor/flatpickr.min.js"></script>
332
+ ${o?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
333
+ </div>`}`,u=html`<script src="/static/vendor/flatpickr.min.js"></script>
334
334
  <script src="/static/relative-time.js"></script>
335
335
  <script>
336
336
  function periodPicker() {
@@ -368,11 +368,11 @@ function periodPicker() {
368
368
  },
369
369
  };
370
370
  }
371
- </script>`;return S({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:u,body:$,scripts:x})}function pe(t,e){let r=[],a=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(a&&s-a>1&&r.push(0),r.push(s),a=s);return r}var R={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function ie(t){let e=t.lesson,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",a=e.repository_platform==="local",s=null;a&&e.repository?s=`vscode://file/${e.repository}`:e.repository_platform&&R[e.repository_platform]&&e.repository&&(s=`https://${R[e.repository_platform]}/${e.repository}`);let i=null;s&&e.commit_hash&&!a&&(i=e.repository_platform==="gitlab"?`${s}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${s}/commits/${e.commit_hash}`:`${s}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),l=e.timestamp.slice(0,10),d=e.timestamp.slice(0,16).replace("T"," "),y=html`
371
+ </script>`;return S({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:t.uiTheme,head:b,body:$,scripts:u})}function me(t,e){let r=[],o=0;for(let s=1;s<=e;s++)(s===1||s===e||s>=t-2&&s<=t+2)&&(o&&s-o>1&&r.push(0),r.push(s),o=s);return r}var L={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function le(t){let e=t.lesson,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",o=e.repository_platform==="local",s=null;o&&e.repository?s=`vscode://file/${e.repository}`:e.repository_platform&&L[e.repository_platform]&&e.repository&&(s=`https://${L[e.repository_platform]}/${e.repository}`);let i=null;s&&e.commit_hash&&!o&&(i=e.repository_platform==="gitlab"?`${s}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${s}/commits/${e.commit_hash}`:`${s}/commit/${e.commit_hash}`);let g=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),d=e.timestamp.slice(0,10),l=e.timestamp.slice(0,16).replace("T"," "),m=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,b=html`
372
372
  <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>
373
373
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
374
374
  <div class="flex flex-wrap items-center gap-3 mb-2">
375
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/star">
375
+ <form id="lesson-star" method="post" action="/lessons/${encodeURIComponent(e.id)}/star" hx-post="/lessons/${encodeURIComponent(e.id)}/star" hx-target="#lesson-star" hx-select="#lesson-star" hx-swap="outerHTML">
376
376
  <input type="hidden" name="starred" value="${e.starred?"0":"1"}" />
377
377
  <input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" />
378
378
  <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>
@@ -380,14 +380,14 @@ function periodPicker() {
380
380
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
381
381
  <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>
382
382
  </div>
383
- <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
384
- <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${l}</span>
385
- <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">${d}</span>
383
+ <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">
384
+ <span class="relative group/date cursor-default">🗓 <span data-ts="${e.timestamp}">${d}</span>
385
+ <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">${l}</span>
386
386
  </span>
387
387
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
388
- ${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>`)}
388
+ ${e.categories.map(u=>html`<a href="/lessons?category=${encodeURIComponent(u)}" 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">${u}</a>`)}
389
389
  ${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>`}
390
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><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>`:""}
390
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(m)}><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>`:""}
391
391
  </div>
392
392
  <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">
393
393
  <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
@@ -396,18 +396,18 @@ function periodPicker() {
396
396
  <div id="body-content" class="markdown-body"></div>
397
397
  ${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>`:""}
398
398
  ${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">
399
- ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${s?html`<a href="${s}" ${a?"":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>`:""}
400
- ${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&&R[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="" />`:""}${s?html`<a href="${s}" ${a?"":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>`:""}
399
+ ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${s?html`<a href="${s}" ${o?"":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>`:""}
400
+ ${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&&L[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="" />`:""}${s?html`<a href="${s}" ${o?"":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>`:""}
401
401
  ${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>`:""}
402
402
  ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${i?html`<a href="${i}" 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>`:""}
403
403
  ${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>`:""}
404
404
  </div></div>`:""}
405
- ${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
406
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><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>
407
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback"><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>
408
- </div>`}
405
+ <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">
406
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(m)}><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>
407
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(m)}><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>
408
+ </div>`}</div>
409
409
  <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>
410
- </div>`,u=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,w=html`<script src="/static/vendor/highlight.min.js"></script>
410
+ </div>`,v=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,$=html`<script src="/static/vendor/highlight.min.js"></script>
411
411
  <script src="/static/vendor/marked.min.js"></script>
412
412
  <script src="/static/relative-time.js"></script>
413
413
  <script>
@@ -421,10 +421,10 @@ function periodPicker() {
421
421
  });
422
422
  document.getElementById('summary-content').innerHTML = marked.parse(${raw(C(e.summary))});
423
423
  document.getElementById('body-content').innerHTML = marked.parse(${raw(C(e.body))});
424
- </script>`;return S({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:u,body:y,scripts:w})}var me=[[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 de(t){let e=t.settings.min_gap_minutes,r=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],a=t.flash,s=html`
424
+ </script>`;return S({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:t.uiTheme,head:v,body:b,scripts:$})}var xe=[[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 ge(t){let e=t.settings.min_gap_minutes,r=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],o=t.flash,s=html`
425
425
  <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
426
- ${a?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${a.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"}">
427
- ${a.imported} lesson${a.imported!==1?"s":""} imported.${a.skipped?` ${a.skipped} skipped (already in DB).`:""}${a.invalid?` ${a.invalid} rejected (failed validation).`:""}${a.groups?` ${a.groups} group${a.groups!==1?"s":""} added.`:""}${a.notebook?" Notebook restored.":""}
426
+ ${o?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${o.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"}">
427
+ ${o.imported} lesson${o.imported!==1?"s":""} imported.${o.skipped?` ${o.skipped} skipped (already in DB).`:""}${o.invalid?` ${o.invalid} rejected (failed validation).`:""}${o.groups?` ${o.groups} group${o.groups!==1?"s":""} added.`:""}${o.notebook?" Notebook restored.":""}
428
428
  </div>`:""}
429
429
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
430
430
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
@@ -439,7 +439,7 @@ ${a?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${a.invalid?"bg-ye
439
439
  <div>
440
440
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
441
441
  <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">
442
- ${me.map(([g,l])=>html`<option value="${g}" ${e===g?"selected":""}>${l}</option>`)}
442
+ ${xe.map(([g,d])=>html`<option value="${g}" ${e===g?"selected":""}>${d}</option>`)}
443
443
  </select>
444
444
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
445
445
  </div>
@@ -462,9 +462,9 @@ ${a?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${a.invalid?"bg-ye
462
462
  <fieldset class="border-0 p-0 m-0">
463
463
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
464
464
  <div class="flex gap-2">
465
- ${r.map(([g,l,d])=>html`<label class="flex-1 cursor-pointer">
465
+ ${r.map(([g,d,l])=>html`<label class="flex-1 cursor-pointer">
466
466
  <input type="radio" name="ui_theme" value="${g}" ${t.settings.ui_theme===g?"checked":""} class="sr-only peer" />
467
- <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">${d}</span>${l}</span>
467
+ <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">${l}</span>${d}</span>
468
468
  </label>`)}
469
469
  </div>
470
470
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
@@ -531,5 +531,5 @@ function updateLabel(inputId, labelId, submitId) {
531
531
  }
532
532
  marked.setOptions({ breaks: true, gfm: true });
533
533
  document.getElementById('notebook-preview-settings').innerHTML = marked.parse(${raw(C(t.notebookContent))});
534
- </script>`;return S({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:s,scripts:i})}var L=25;function B(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function b(t,e,r=""){let a=t[e];return typeof a=="string"?a:r}var N=dirname(fileURLToPath(import.meta.url)),le=[join(N,"../assets/static"),join(N,"../../assets/static")].find(t=>existsSync(t))??join(N,"../assets/static"),ke={".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"};function ve(){try{return m$1(t=>F(t).ui_theme)}catch{return "system"}}function we(){let t$1=new Hono;return t$1.get("/static/*",e=>{let r=decodeURIComponent(e.req.path.slice(8)),a=join(le,r);if(!a.startsWith(le))return e.notFound();try{let s=readFileSync(a);return new Response(new Uint8Array(s),{headers:{"content-type":ke[extname(a)]??"application/octet-stream"}})}catch{return e.notFound()}}),t$1.get("/",e=>{let{profile:r,stats:a,rateLimit:s,settings:i}=m$1(d=>({profile:S$1(d),stats:V(d),rateLimit:R$1(d),settings:F(d)})),g=new Map;for(let d of r.groups)for(let y of d.topics)g.set(y,d.name);let l={};for(let d of r.groups)l[d.name]=[];for(let d of r.knowledge){let y=g.get(d.topic)??"Other",u=l[y]??[];u.push(d),l[y]=u;}return e.html(se({categorised:l,allGroups:r.groups.map(d=>d.name),stats:a,rateLimit:s,maxPerDay:i.max_per_day,uiTheme:i.ui_theme}))}),t$1.post("/knowledge",async e=>{let r=await e.req.parseBody(),a=b(r,"topic").trim();if(a){let s=Number.parseInt(b(r,"confidence","5"),10)||5,i=b(r,"group").trim();m$1(g=>{z(g,a,s),i&&i!=="Other"&&D(g,a,i);});}return e.redirect("/",303)}),t$1.post("/knowledge/:topic/delete",e=>(m$1(r=>A(r,e.req.param("topic"))),e.redirect("/",303))),t$1.post("/knowledge/:topic/group",async e=>{let r=b(await e.req.parseBody(),"group").trim(),a=e.req.param("topic");return m$1(s=>{r&&r!=="Other"?D(s,a,r):E(s,a);}),e.redirect("/",303)}),t$1.post("/knowledge/:topic",async e=>{let r=Number.parseInt(b(await e.req.parseBody(),"delta","0"),10)||0;return m$1(a=>T(a,e.req.param("topic"),r)),e.redirect("/",303)}),t$1.post("/groups",async e=>{let r=b(await e.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&m$1(a=>B$1(a,r)),e.redirect("/",303)}),t$1.post("/groups/:group_name/delete",e=>(m$1(r=>C$1(r,e.req.param("group_name"))),e.redirect("/",303))),t$1.get("/lessons/export",()=>{let e=m$1(r=>t(r));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t$1.post("/lessons/import",async e=>{let r=(await e.req.parseBody()).file,a;try{a=JSON.parse(r instanceof File?await r.text():String(r));}catch{return e.redirect("/settings?imported=0&skipped=0&invalid=1",303)}if(!Array.isArray(a))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=m$1(i=>u(i,a));return e.redirect(`/settings?imported=${s.inserted}&skipped=${s.duplicated}&invalid=${s.invalid}`,303)}),t$1.get("/lessons",e=>{let r=e.req.query(),a=new Set(["junior","mid","senior"]),s=r.level&&a.has(r.level)?r.level:"",i=r.date_from||"",g=r.date_to||"",l=r.starred==="1",d=r.sort||"timestamp",y=r.order==="asc"?"asc":"desc",u=Math.max(1,Number.parseInt(r.page??"1",10)||1),w=r.period||"all",$={period:i||g?null:w!=="all"?w:null,category:r.category||null,level:s||null,project:r.project||null,repository:r.repository||null,branch:r.branch||null,commit:r.commit||null,starred:l?true:null,search:r.search||null,feedback:r.feedback||null,date_from:i||null,date_to:g||null},x$1=m$1(c=>{let h=o(c,$),f=Math.max(1,Math.ceil(h/L));return u=Math.min(u,f),{total:h,totalPages:f,lessons:p(c,{...$,sort:d,order:y,page:u,per_page:L}),allCategories:x(c),allProjects:v(c,"project"),allRepositories:v(c,"repository"),allBranches:v(c,"branch"),allCommits:v(c,"commit_hash"),theme:F(c).ui_theme}}),n={period:w,category:r.category||"",level:s,project:r.project||"",repository:r.repository||"",branch:r.branch||"",commit:r.commit||"",starred:l,search:r.search||"",feedback:r.feedback||"",date_from:i,date_to:g,sort:d,order:y};return e.html(ne({lessons:x$1.lessons,allCategories:x$1.allCategories,allProjects:x$1.allProjects,allRepositories:x$1.allRepositories,allBranches:x$1.allBranches,allCommits:x$1.allCommits,s:n,page:u,perPage:L,total:x$1.total,totalPages:x$1.totalPages,uiTheme:x$1.theme}))}),t$1.post("/lessons/:lesson_id/star",async e=>{let r$1=await e.req.parseBody();return m$1(a=>r(a,e.req.param("lesson_id"),r$1.starred==="1")),e.redirect(B(b(r$1,"next")||void 0),303)}),t$1.post("/lessons/:lesson_id/feedback",async e=>{let r=await e.req.parseBody(),a=b(r,"feedback"),s=a===""||a==="clear"?null:a;return m$1(i=>U(i,e.req.param("lesson_id"),s)),e.redirect(B(b(r,"next")||void 0),303)}),t$1.get("/lessons/:lesson_id",e=>{let r=m$1(a=>w(a,e.req.param("lesson_id")));return r?e.html(ie({lesson:r,uiTheme:ve()})):e.html("<h1>Lesson not found</h1>",404)}),t$1.get("/settings/export",()=>{let e=m$1(r=>P(r));return new Response(new Uint8Array(e),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),t$1.post("/settings/import",async e=>{let r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let a=new Uint8Array(await r.arrayBuffer()),s=m$1(i=>Q(i,a));return e.redirect(`/settings?imported=${s.lessons}&skipped=${s.skipped}&invalid=${s.invalid}&groups=${s.groups}&notebook=${s.learning_state}`,303)}),t$1.get("/settings/notebook/download",()=>{let e=existsSync(j)?readFileSync(j,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t$1.post("/settings/notebook",async e=>{let r=await e.req.parseBody();return mkdirSync(dirname(j),{recursive:true}),writeFileSync(j,b(r,"content"),"utf8"),e.redirect(B(b(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),t$1.post("/settings",async e=>{let r=await e.req.parseBody(),a=Number.parseInt(b(r,"max_per_day","2"),10)||2,s=Number.parseInt(b(r,"min_gap_minutes","240"),10),i=b(r,"ui_theme","system");["system","dark","light"].includes(i)||(i="system");let g=Math.max(0,Number.parseInt(b(r,"nudge_every","10"),10)||0),l=b(r,"nudge_scope","session");return l!=="session"&&l!=="global"&&(l="session"),m$1(d=>{G(d,"max_per_day",String(a)),G(d,"min_gap_minutes",String(Number.isNaN(s)?240:s)),G(d,"ui_theme",i),G(d,"nudge_every",String(g)),G(d,"nudge_scope",l);}),e.redirect("/settings",303)}),t$1.get("/settings",e=>{let r=m$1(g=>F(g)),a=existsSync(j)?readFileSync(j,"utf8"):"",s=e.req.query(),i=s.imported!==void 0?{imported:Number(s.imported??0),skipped:Number(s.skipped??0),invalid:Number(s.invalid??0),groups:Number(s.groups??0),notebook:Number(s.notebook??0)}:null;return e.html(de({settings:r,notebookContent:a,notebookPath:j,uiTheme:r.ui_theme,flash:i}))}),t$1}function Le(t){let e=we();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},r=>{console.log(`devcoach UI running at http://localhost:${r.port}`);});}
535
- export{we as createApp,Le as startUi};
534
+ </script>`;return S({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:t.uiTheme,body:s,scripts:i})}var M=25;function B(t,e="/lessons"){return t?.startsWith("/")&&!t.startsWith("//")?t:e}function y(t,e,r=""){let o=t[e];return typeof o=="string"?o:r}var A=dirname(fileURLToPath(import.meta.url)),ce=[join(A,"../assets/static"),join(A,"../../assets/static")].find(t=>existsSync(t))??join(A,"../assets/static"),ve={".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"};function we(){try{return m(t=>F(t).ui_theme)}catch{return "system"}}function $e(){let t$1=new Hono;return t$1.get("/static/*",e=>{let r=decodeURIComponent(e.req.path.slice(8)),o=join(ce,r);if(!o.startsWith(ce))return e.notFound();try{let s=readFileSync(o);return new Response(new Uint8Array(s),{headers:{"content-type":ve[extname(o)]??"application/octet-stream"}})}catch{return e.notFound()}}),t$1.get("/",e=>{let{profile:r,stats:o,rateLimit:s,settings:i}=m(l=>({profile:Q(l),stats:T(l),rateLimit:P(l),settings:F(l)})),g=new Map;for(let l of r.groups)for(let m of l.topics)g.set(m,l.name);let d={};for(let l of r.groups)d[l.name]=[];for(let l of r.knowledge){let m=g.get(l.topic)??"Other",b=d[m]??[];b.push(l),d[m]=b;}return e.html(ie({categorised:d,allGroups:r.groups.map(l=>l.name),stats:o,rateLimit:s,maxPerDay:i.max_per_day,uiTheme:i.ui_theme}))}),t$1.post("/knowledge",async e=>{let r=await e.req.parseBody(),o=y(r,"topic").trim();if(o){let s=Number.parseInt(y(r,"confidence","5"),10)||5,i=y(r,"group").trim();m(g=>{z(g,o,s),i&&i!=="Other"&&D(g,o,i);});}return e.redirect("/",303)}),t$1.post("/knowledge/:topic/delete",e=>(m(r=>A$1(r,e.req.param("topic"))),e.redirect("/",303))),t$1.post("/knowledge/:topic/group",async e=>{let r=y(await e.req.parseBody(),"group").trim(),o=e.req.param("topic");return m(s=>{r&&r!=="Other"?D(s,o,r):E(s,o);}),e.redirect("/",303)}),t$1.post("/knowledge/:topic",async e=>{let r=Number.parseInt(y(await e.req.parseBody(),"delta","0"),10)||0;return m(o=>R(o,e.req.param("topic"),r)),e.redirect("/",303)}),t$1.post("/groups",async e=>{let r=y(await e.req.parseBody(),"group_name").trim();return r&&r!=="Other"&&m(o=>B$1(o,r)),e.redirect("/",303)}),t$1.post("/groups/:group_name/delete",e=>(m(r=>C$1(r,e.req.param("group_name"))),e.redirect("/",303))),t$1.get("/lessons/export",()=>{let e=m(r=>t(r));return new Response(JSON.stringify(e,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),t$1.post("/lessons/import",async e=>{let r=(await e.req.parseBody()).file,o;try{o=JSON.parse(r instanceof File?await r.text():String(r));}catch{return e.redirect("/settings?imported=0&skipped=0&invalid=1",303)}if(!Array.isArray(o))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let s=m(i=>u(i,o));return e.redirect(`/settings?imported=${s.inserted}&skipped=${s.duplicated}&invalid=${s.invalid}`,303)}),t$1.get("/lessons",e=>{let r=e.req.query(),o$1=new Set(["junior","mid","senior"]),s=r.level&&o$1.has(r.level)?r.level:"",i=r.date_from||"",g=r.date_to||"",d=r.starred==="1",l=r.sort||"timestamp",m$1=r.order==="asc"?"asc":"desc",b=Math.max(1,Number.parseInt(r.page??"1",10)||1),v$1=r.period||"all",$={period:i||g?null:v$1!=="all"?v$1:null,category:r.category||null,level:s||null,project:r.project||null,repository:r.repository||null,branch:r.branch||null,commit:r.commit||null,starred:d?true:null,search:r.search||null,feedback:r.feedback||null,date_from:i||null,date_to:g||null},u=m(c=>{let f=o(c,$),w=Math.max(1,Math.ceil(f/M));return b=Math.min(b,w),{total:f,totalPages:w,lessons:p(c,{...$,sort:l,order:m$1,page:b,per_page:M}),allCategories:x$1(c),allProjects:v(c,"project"),allRepositories:v(c,"repository"),allBranches:v(c,"branch"),allCommits:v(c,"commit_hash"),theme:F(c).ui_theme}}),n={period:v$1,category:r.category||"",level:s,project:r.project||"",repository:r.repository||"",branch:r.branch||"",commit:r.commit||"",starred:d,search:r.search||"",feedback:r.feedback||"",date_from:i,date_to:g,sort:l,order:m$1};return e.html(de({lessons:u.lessons,allCategories:u.allCategories,allProjects:u.allProjects,allRepositories:u.allRepositories,allBranches:u.allBranches,allCommits:u.allCommits,s:n,page:b,perPage:M,total:u.total,totalPages:u.totalPages,uiTheme:u.theme}))}),t$1.post("/lessons/:lesson_id/star",async e=>{let r$1=await e.req.parseBody();return m(o=>r(o,e.req.param("lesson_id"),r$1.starred==="1")),e.redirect(B(y(r$1,"next")||void 0),303)}),t$1.post("/lessons/:lesson_id/feedback",async e=>{let r=await e.req.parseBody(),o=y(r,"feedback"),s=o===""||o==="clear"?null:o;return m(i=>S$1(i,e.req.param("lesson_id"),s)),e.redirect(B(y(r,"next")||void 0),303)}),t$1.get("/lessons/:lesson_id",e=>{let r=m(o=>w(o,e.req.param("lesson_id")));return r?e.html(le({lesson:r,uiTheme:we()})):e.html("<h1>Lesson not found</h1>",404)}),t$1.get("/settings/export",()=>{let e=m(r=>N(r));return new Response(new Uint8Array(e),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),t$1.post("/settings/import",async e=>{let r=(await e.req.parseBody()).file;if(!(r instanceof File))return e.redirect("/settings?imported=0&skipped=0&invalid=1",303);let o=new Uint8Array(await r.arrayBuffer()),s=m(i=>O(i,o));return e.redirect(`/settings?imported=${s.lessons}&skipped=${s.skipped}&invalid=${s.invalid}&groups=${s.groups}&notebook=${s.learning_state}`,303)}),t$1.get("/settings/notebook/download",()=>{let e=existsSync(j)?readFileSync(j,"utf8"):"";return new Response(e,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),t$1.post("/settings/notebook",async e=>{let r=await e.req.parseBody();return mkdirSync(dirname(j),{recursive:true}),writeFileSync(j,y(r,"content"),"utf8"),e.redirect(B(y(r,"next")||void 0,"/settings?notebook_saved=1"),303)}),t$1.post("/settings",async e=>{let r=await e.req.parseBody(),o=Number.parseInt(y(r,"max_per_day","2"),10)||2,s=Number.parseInt(y(r,"min_gap_minutes","240"),10),i=y(r,"ui_theme","system");["system","dark","light"].includes(i)||(i="system");let g=Math.max(0,Number.parseInt(y(r,"nudge_every","10"),10)||0),d=y(r,"nudge_scope","session");return d!=="session"&&d!=="global"&&(d="session"),m(l=>{G(l,"max_per_day",String(o)),G(l,"min_gap_minutes",String(Number.isNaN(s)?240:s)),G(l,"ui_theme",i),G(l,"nudge_every",String(g)),G(l,"nudge_scope",d);}),e.redirect("/settings",303)}),t$1.get("/settings",e=>{let r=m(g=>F(g)),o=existsSync(j)?readFileSync(j,"utf8"):"",s=e.req.query(),i=s.imported!==void 0?{imported:Number(s.imported??0),skipped:Number(s.skipped??0),invalid:Number(s.invalid??0),groups:Number(s.groups??0),notebook:Number(s.notebook??0)}:null;return e.html(ge({settings:r,notebookContent:o,notebookPath:j,uiTheme:r.ui_theme,flash:i}))}),t$1}function Me(t){let e=$e();serve({fetch:e.fetch,port:t,hostname:"127.0.0.1"},r=>{console.log(`devcoach UI running at http://localhost:${r.port}`);});}
535
+ export{$e as createApp,Me as startUi};
package/dist/bin.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- var r=process.argv[2]??"",t=new Set(["stop-hook","prompt-hook","onboard-hook","lesson-ready"]);try{if(t.has(r)){let{runHook:o}=await import('./hooks-G7D4KE3D.js');o(r);}else {let{runCli:o}=await import('./commands-D66GG4RO.js');await o();}}catch(o){console.error(o),process.exit(1);}
2
+ var r=process.argv[2]??"",t=new Set(["stop-hook","prompt-hook","onboard-hook","lesson-ready"]);try{if(t.has(r)){let{runHook:o}=await import('./hooks-OZQBLTCR.js');o(r);}else {let{runCli:o}=await import('./commands-U7PVT6PV.js');await o();}}catch(o){console.error(o),process.exit(1);}
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {i,m,L as L$1,W,h as h$1,l,V}from'./chunk-ECN4MNGM.js';import {existsSync,mkdirSync,statSync,writeFileSync,appendFileSync,fstatSync,readFileSync}from'fs';import {join}from'path';function C(o){try{let e=JSON.parse(o);return {stop_hook_active:e?.stop_hook_active===!0,permission_mode:typeof e?.permission_mode=="string"?e.permission_mode:null,session_id:typeof e?.session_id=="string"?e.session_id:null}}catch{return {stop_hook_active:false,permission_mode:null,session_id:null}}}function h(){let o={stop_hook_active:false,permission_mode:null,session_id:null};try{let e=fstatSync(0);return !e.isFIFO()&&!e.isFile()&&!e.isSocket()?o:C(readFileSync(0,"utf8"))}catch{return o}}function H(o){return o.stop_hook_active||o.permission_mode==="plan"}function s(o,e,t){if(process.env.DEVCOACH_HOOK_DEBUG==="1")try{mkdirSync(h$1,{recursive:!0});let n=join(h$1,"hook.log");existsSync(n)&&statSync(n).size>256*1024&&writeFileSync(n,""),appendFileSync(n,`${new Date().toISOString()} ${o} ${(e??"-").slice(0,8)} ${t}
3
+ `);}catch{}}function d(o,e){let t={decision:"block",reason:o};e&&(t.systemMessage=e),process.stdout.write(`${JSON.stringify(t)}
4
+ `),process.exit(0);}var k="devcoach: the user has no coaching profile yet. Ask them how they want to set it up and STRONGLY recommend Automatic \u2014 devcoach scans their full Claude Code history across all their projects (read devcoach://onboarding for the detected stack and per-project evidence) so the profile reflects what they actually build. Also offer Guided (a short conversation to map topics and confidence levels) and Import (restore from a backup), but lead with Automatic; do not pick for them. After they choose, build the profile and save it by calling the devcoach `complete_onboarding` MCP tool (not a shell command) before ending your turn.";function D(o=h()){if(H(o)&&process.exit(0),existsSync(i)){let e;try{e=m(t=>L$1(t).knowledge_ready);}catch(t){s("onboard-hook",o.session_id,`error: ${t}`),process.exit(0);}e&&process.exit(0);}s("onboard-hook",o.session_id,"onboarding cue"),d(k);}var a=10;function P(o){let e=o%a===0,t=Math.ceil(o/a)*a;return `devcoach: a lesson is due for the technical work just completed. Do it now \u2014 do not acknowledge this message and do not explain what you are about to do.
5
+
6
+ Invoke the \`devcoach\` skill (Skill tool) NOW and follow it to deliver ONE lesson. Three hard rules:
7
+ - The lesson card MUST be printed as your visible reply BEFORE calling log_lesson.
8
+ - After log_lesson returns, output NOTHING else \u2014 the card is the end of the reply.
9
+ - If the work just completed does not warrant a lesson (pure questions, chat, nothing technical), call the devcoach \`skip_lesson\` tool with a one-line reason and output nothing.
10
+ ${e?`This is lesson #${o} \u2014 a notebook checkpoint (every ${a} lessons): after log_lesson returns the feedback, also call update_notebook with the revised notebook markdown and record "(updated after ${o} lessons)".`:`Do NOT call update_notebook this time \u2014 the notebook is refreshed only every ${a} lessons (next checkpoint at lesson #${t}).`}
11
+
12
+ If the devcoach skill is not available, fall back to: read devcoach://briefing (one read: profile, taught topics, notebook); pick ONE untaught profile topic at or above the user's confidence band; write the card as plain markdown (no '>' blockquote) between two band headings \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\` and \`### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [topic] \xB7 [level] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\`, with \`**[Title]** \xB7 [Category] \xB7 [Level]\`, 3\u20136 short paragraphs tied to the task, and a \`\u{1F4A1} *Senior tip:*\` line; THEN call log_lesson (body = clean markdown without bands or the title line).
13
+
14
+ The rate-limit check is already done by this hook \u2014 skip it. Work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026'). Output only the lesson card (or nothing). No preamble, no meta-commentary.`}function T(o,e,t){let n=e.permission_mode==="plan";if(e.stop_hook_active)return {kind:"silent",note:"hook-forced continuation"};if(!L$1(o).knowledge_ready)return !t.onboardCue||n?{kind:"silent",note:"onboarding not complete"}:{kind:"onboard",note:"onboarding cue (empty knowledge)"};let i=V(o,e.session_id,{planMode:n});return i.cue?{kind:"cue",nextLessonNumber:i.nextLessonNumber,note:i.reason}:{kind:"silent",note:i.reason}}function _(o,e,t){let n;try{n=m(i=>l(i,()=>T(i,e,t)));}catch(i){s(o,e.session_id,`error: ${i}`),process.exit(0);}switch(s(o,e.session_id,n.note),n.kind){case "onboard":d(k);break;case "cue":d(P(n.nextLessonNumber),"\u{1F393} devcoach: preparing a lesson\u2026");break;}process.exit(0);}function E(o=h()){existsSync(i)||process.exit(0),_("lesson-ready",o,{onboardCue:false});}function A(o=h()){existsSync(i)||((o.stop_hook_active||o.permission_mode==="plan")&&process.exit(0),s("stop-hook",o.session_id,"onboarding cue (no DB)"),d(k)),_("stop-hook",o,{onboardCue:true});}var N="devcoach: the lesson pacing threshold will be reached at the end of this turn. Resolve it before finishing:\n- If this task is technical: invoke the `devcoach` skill (Skill tool), then end your reply with ONE lesson card (the `### \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u{1F393} devcoach \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500` block) followed by a log_lesson call. The card must be the LAST visible text \u2014 after log_lesson returns, output nothing else.\n- If it is not technical: silently call the devcoach `skip_lesson` tool with a one-line reason and output nothing about it.\n- Either way, work in silence: never narrate devcoach tool calls or resource reads ('checking your profile\u2026').";function $(o=h()){o.permission_mode==="plan"&&process.exit(0),existsSync(i)||process.exit(0);let e;try{e=m(t=>W(t,o.session_id).wouldCue);}catch(t){s("prompt-hook",o.session_id,`error: ${t}`),process.exit(0);}s("prompt-hook",o.session_id,e?"priming":"no prime"),e||process.exit(0),process.stdout.write(`${JSON.stringify({hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:N}})}
15
+ `),process.exit(0);}function L(o){switch(o){case "stop-hook":A();break;case "prompt-hook":$();break;case "onboard-hook":D();break;case "lesson-ready":E();break;default:process.exit(0);}}export{C as a,D as b,P as c,E as d,A as e,$ as f,L as g};