devcoach 2.5.0 → 2.6.0-next.597.g16b8c2e

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,10 +1,10 @@
1
1
  #!/usr/bin/env node
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-UMYQG4J6.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-RUBF2HCU.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>
2
+ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-SCB2FNKK.js';import {w,A,x,v,a,C,T,xa,Ba,ya,N,R,O,S,za,P as P$1,Q as Q$1,H,Ca,na,ra,J as J$1,I,L,D,G,Aa,F,K as K$1,U,t,wa,va,ka,ma,o,pa,oa,qa,ta,sa}from'./chunk-4DCO5JI7.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(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Ye(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var nt=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 Y(r,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">${nt}</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
- ${t}
5
+ ${r}
6
6
  </div>
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">
7
+ </div>`}function J(r,e,g,d,t){return html`<div x-show="${r}" 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">
@@ -14,16 +14,16 @@ import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j
14
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">${r}</div>
17
+ <div class="mt-5 flex justify-end gap-2">${t}</div>
18
18
  </div>
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>
19
+ </div>`}function P(r){let e=(g,d,t)=>html`<a href="${g}" class="text-gray-500 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white transition ${t?"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" />
23
23
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
24
- <title>${t.title}</title>
24
+ <title>${r.title}</title>
25
25
  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
26
- <meta name="devcoach-theme" content="${t.uiTheme}" />
26
+ <meta name="devcoach-theme" content="${r.uiTheme}" />
27
27
  <script>
28
28
  (function () {
29
29
  var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
@@ -38,7 +38,18 @@ import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j
38
38
  <script src="/static/vendor/htmx.min.js"></script>
39
39
  <script src="/static/vendor/alpinejs.min.js" defer></script>
40
40
  <link rel="stylesheet" href="/static/style.css" />
41
- ${t.head??""}
41
+ ${r.head??""}
42
+ <script>
43
+ // Older devcoach on a newer database → the dashboard is read-only: one banner on every page.
44
+ fetch('/ping').then(function (r) { return r.json(); }).then(function (p) {
45
+ if (!p || !p.compat) return;
46
+ var b = document.createElement('div');
47
+ b.setAttribute('role', 'status');
48
+ b.className = 'bg-amber-100 dark:bg-amber-900/40 text-amber-900 dark:text-amber-200 border-b border-amber-300 dark:border-amber-700 px-6 py-2 text-sm';
49
+ b.textContent = '⚠ Read-only: ' + p.compat.message;
50
+ document.body.prepend(b);
51
+ }).catch(function () {});
52
+ </script>
42
53
  </head>
43
54
  <body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen font-mono transition-colors duration-200">
44
55
  <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">
@@ -47,16 +58,16 @@ import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j
47
58
  <a href="/" class="font-extrabold text-lg tracking-tight text-gray-900 dark:text-gray-100 hover:opacity-80 transition"
48
59
  >🎓 dev<span class="text-teal-600 dark:text-teal-300">coach</span></a
49
60
  >
50
- ${e("/knowledge","Profile",t.currentPath==="/knowledge")}
51
- ${e("/lessons","Lessons",t.currentPath.includes("/lessons"))}
52
- ${e("/courses","Courses",t.currentPath.includes("/courses"))}
61
+ ${e("/knowledge","Profile",r.currentPath==="/knowledge")}
62
+ ${e("/lessons","Lessons",r.currentPath.includes("/lessons"))}
63
+ ${e("/courses","Courses",r.currentPath.includes("/courses"))}
53
64
  <div class="ml-auto flex items-center gap-1.5">
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>
65
+ <a href="/settings" title="Settings" ${r.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 ${r.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>
55
66
  <button id="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme"
56
67
  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>
57
68
  </div>
58
69
  </nav>
59
- <main class="${t.wide?"max-w-none":"max-w-7xl"} mx-auto px-4 sm:px-6 py-8">${t.body}</main>
70
+ <main class="${r.wide?"max-w-none":"max-w-7xl"} mx-auto px-4 sm:px-6 py-8">${r.body}</main>
60
71
  <script>
61
72
  function isDark() { return document.documentElement.classList.contains('dark'); }
62
73
  function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
@@ -75,9 +86,9 @@ import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j
75
86
  })();
76
87
  updateThemeIcon(); updateHljsTheme();
77
88
  </script>
78
- ${t.scripts??""}
89
+ ${r.scripts??""}
79
90
  </body>
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`
91
+ </html>`}function Je(r){let e=d=>r.allGroups.filter(t=>t!=="Other").map(t=>html`<option value="${t}" ${t===d?"selected":""}>${t}</option>`),g=html`
81
92
  <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); } }">
82
93
 
83
94
  <div class="flex items-center justify-between mb-6">
@@ -127,16 +138,16 @@ import {a as a$1,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a,e,g,j as j
127
138
  </div>
128
139
 
129
140
  <div class="mb-6 flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400">
130
- <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.total_lessons??0)}</span> lessons total</span>
141
+ <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(r.stats.total_lessons??0)}</span> lessons total</span>
131
142
  <span class="text-gray-300 dark:text-gray-600">·</span>
132
- <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_today??0)}</span> / ${t.maxPerDay} today</span>
143
+ <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(r.stats.lessons_today??0)}</span> / ${r.maxPerDay} today</span>
133
144
  <span class="text-gray-300 dark:text-gray-600">·</span>
134
- <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(t.stats.lessons_this_week??0)}</span> this week</span>
145
+ <span><span class="font-semibold text-gray-800 dark:text-gray-100">${Number(r.stats.lessons_this_week??0)}</span> this week</span>
135
146
  <span class="text-gray-300 dark:text-gray-600">·</span>
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>`}
147
+ ${r.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">${r.rateLimit.reason}</span>`}
137
148
  </div>
138
149
 
139
- ${Object.entries(t.categorised).map(([d,r])=>html`
150
+ ${Object.entries(r.categorised).map(([d,t])=>html`
140
151
  <section class="mb-6 group/section">
141
152
  <div class="flex items-center gap-2 mb-2 border-b border-gray-200 dark:border-gray-800 pb-1">
142
153
  <div class="flex items-center gap-1 flex-1 min-w-0">
@@ -161,7 +172,7 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
161
172
  </div>
162
173
  </div>
163
174
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
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`
175
+ ${t.map(o=>{let n=Ye("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
176
  <div id="${n}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
166
177
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
167
178
  <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>
@@ -196,7 +207,7 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
196
207
  </div>`})}
197
208
  </div>
198
209
  </section>`)}
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 ${$}">
210
+ </div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:r.uiTheme,body:g})}var Fe={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Ge={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},ze={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(r,e={}){let g={period:r.period,category:r.category,level:r.level,project:r.project,repository:r.repository,branch:r.branch,commit:r.commit,feedback:r.feedback,imported:r.imported,shared_by:r.shared_by,search:r.search,date_from:r.date_from,date_to:r.date_to,sort:r.sort,order:r.order};r.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[t,o]of Object.entries(g))o&&d.set(t,o);return `?${d.toString()}`}function Ve(r){let e=r.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),t=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}`:Fe[e.period]??"All time",o=e.feedback?Ge[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"],...r.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
211
  <a href="${h(e,{sort:x,order:k})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${l}
201
212
  ${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
213
  </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" />
@@ -210,7 +221,7 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
210
221
  .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
211
222
  .dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
212
223
  .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
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`
224
+ </style>`,v=r.total===0?"No lessons":r.totalPages===1?`${r.total} lesson${r.total!==1?"s":""}`:`${(r.page-1)*r.perPage+1}\u2013${Math.min(r.page*r.perPage,r.total)} of ${r.total}`,I=JSON.stringify(r.lessons.map(l=>l.id)),R=html`
214
225
  <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()">
215
226
  <form id="filter-form" method="get" action="/lessons">
216
227
  <input type="hidden" name="period" id="h-period" value="${e.period}">
@@ -249,11 +260,11 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
249
260
 
250
261
  <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
251
262
  <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}
263
+ <span>📅</span><span x-text="label">${t}</span>${p}
253
264
  </button>
254
265
  <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">
255
266
  <div class="p-1">
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>`})}
267
+ ${Object.entries(Fe).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>`})}
257
268
  </div>
258
269
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
259
270
  <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"}">
@@ -275,34 +286,34 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
275
286
  </div>
276
287
 
277
288
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
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>
289
+ <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?`${ze[e.level]??""} ${e.level}`:"Level"}${p}</button>
279
290
  <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">
280
291
  ${[["","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>`)}
281
292
  </div>
282
293
  </div>
283
294
 
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">
295
+ ${r.allCategories.length||r.allProjects.length||r.allRepositories.length||r.allBranches.length||r.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
285
296
  <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>
286
297
  <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">
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>`:""}
298
+ ${r.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>${r.allCategories.map(l=>html`<option value="${l}" ${e.category===l?"selected":""}>${l}</option>`)}</select></div>`:""}
299
+ ${r.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>${r.allProjects.map(l=>html`<option value="${l}" ${e.project===l?"selected":""}>${l}</option>`)}</select></div>`:""}
300
+ ${r.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>${r.allRepositories.map(l=>html`<option value="${l}" ${e.repository===l?"selected":""}>${l}</option>`)}</select></div>`:""}
301
+ ${r.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">${r.allBranches.map(l=>html`<option value="${l}">`)}</datalist></div>`:""}
302
+ ${r.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">${r.allCommits.map(l=>html`<option value="${l.slice(0,7)}">`)}</datalist></div>`:""}
292
303
  <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>
293
304
  </div>
294
305
  </div>`:""}
295
306
 
296
307
  ${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>`:""}
297
308
 
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>
309
+ ${Y(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
310
  <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
311
  <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>
301
- <div class="relative" x-data="{ open: ${String(t.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
312
+ <div class="relative" x-data="{ open: ${String(r.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
302
313
  <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>
303
314
  <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">
304
315
  <p class="text-xs text-gray-500 dark:text-gray-400">Paste the lesson code, the link, a URL or the whole text — or drop a <code>.devcoach.md</code> anywhere on this page.</p>
305
- ${t.importError==="invalid"?html`<p class="text-xs text-rose-600 dark:text-rose-400">That doesn't look like a devcoach lesson — copy the whole text again.</p>`:t.importError==="cross"?html`<p class="text-xs text-rose-600 dark:text-rose-400">Imports only work from this dashboard — paste the lesson here.</p>`:""}
316
+ ${r.importError==="invalid"?html`<p class="text-xs text-rose-600 dark:text-rose-400">That doesn't look like a devcoach lesson — copy the whole text again.</p>`:r.importError==="cross"?html`<p class="text-xs text-rose-600 dark:text-rose-400">Imports only work from this dashboard — paste the lesson here.</p>`:""}
306
317
  <textarea name="text" form="import-form" rows="4" placeholder="devcoach:lesson:1:…" 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" x-ref="importText" x-effect="if (open) $nextTick(() => $refs.importText.focus())"></textarea>
307
318
  <div class="flex items-center gap-2">
308
319
  <label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">📄 Choose file<input id="import-file" type="file" name="file" form="import-form" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
@@ -313,14 +324,14 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
313
324
  </div>
314
325
 
315
326
  ${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:""})):""}
327
+ ${g?c(`\u{1F4C5} ${t}`,h(e,{date_from:"",date_to:""})):""}
328
+ ${e.period!=="all"&&!g?c(`\u{1F550} ${t}`,h(e,{period:"all"})):""}
329
+ ${e.feedback?c(Ge[e.feedback]??e.feedback,h(e,{feedback:""})):""}
319
330
  ${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
331
  ${e.imported||e.shared_by?c(n,h(e,{imported:"",shared_by:""})):""}
321
332
  ${e.search?c(`\u{1F50D} "${e.search}"`,h(e,{search:""})):""}
322
333
  ${e.category?c(e.category,h(e,{category:""})):""}
323
- ${e.level?c(`${qe[e.level]??""} ${e.level}`,h(e,{level:""})):""}
334
+ ${e.level?c(`${ze[e.level]??""} ${e.level}`,h(e,{level:""})):""}
324
335
  ${e.project?c(`\u{1F4C1} ${e.project}`,h(e,{project:""})):""}
325
336
  ${e.repository?c(`\u2387 ${e.repository}`,h(e,{repository:""})):""}
326
337
  ${e.branch?c(`\u2387 ${e.branch}`,h(e,{branch:""})):""}
@@ -330,7 +341,7 @@ ${Object.entries(t.categorised).map(([d,r])=>html`
330
341
  <form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
331
342
  <div id="drop-hint" class="hidden fixed inset-0 z-[60] bg-indigo-500/10 border-4 border-dashed border-indigo-400 pointer-events-none items-center justify-center"><p class="bg-white dark:bg-gray-900 text-indigo-700 dark:text-indigo-300 font-semibold rounded-xl px-6 py-3 shadow-lg">Drop to import the lesson</p></div>
332
343
 
333
- ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
344
+ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
334
345
  <table class="w-full text-sm table-fixed" data-resizable="lessons">
335
346
  <colgroup>
336
347
  <col data-col="star" style="width:2.75rem" />
@@ -355,7 +366,7 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
355
366
  </tr>
356
367
  </thead>
357
368
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
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'">
369
+ ${r.lessons.map(l=>{let x=Ye("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'">
359
370
  <td class="px-3 py-3">
360
371
  <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
372
  <input type="hidden" name="starred" value="${l.starred?"0":"1"}" />
@@ -378,12 +389,12 @@ ${t.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
378
389
  </tbody>
379
390
  </table>
380
391
  </div>
381
- ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
382
- <p class="text-xs text-gray-400 dark:text-gray-500">Page ${t.page} of ${t.totalPages}</p>
392
+ ${r.totalPages>1?html`<div class="flex items-center justify-between mt-4">
393
+ <p class="text-xs text-gray-400 dark:text-gray-500">Page ${r.page} of ${r.totalPages}</p>
383
394
  <div class="flex items-center gap-1">
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>`}
395
+ ${r.page>1?html`<a href="${h(e,{page:String(r.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>`}
396
+ ${it(r.page,r.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===r.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>`)}
397
+ ${r.page<r.totalPages?html`<a href="${h(e,{page:String(r.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>`}
387
398
  </div>
388
399
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
389
400
  <p class="text-3xl mb-3">📭</p>
@@ -400,14 +411,14 @@ ${t.totalPages>1?html`<div class="flex items-center justify-between mt-4">
400
411
  </div>
401
412
  </div>
402
413
  </div>
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">
414
+ ${J("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">
404
415
  <template x-for="t in selectedTitles().slice(0, 6)" :key="t"><li class="truncate">· <span x-text="t"></span></li></template>
405
416
  </ul>
406
417
  <p x-show="selected.length > 6" style="display:none" x-text="'…and ' + (selected.length - 6) + ' more'"></p>
407
418
  <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>
408
419
  <form method="post" action="/lessons/delete" class="contents">
409
420
  <template x-for="id in selected" :key="id"><input type="hidden" name="id" :value="id" /></template>
410
- <input type="hidden" name="next" value="/lessons${h(e,{page:String(t.page)})}" />
421
+ <input type="hidden" name="next" value="/lessons${h(e,{page:String(r.page)})}" />
411
422
  <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>
412
423
  </form>`)}
413
424
  <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">
@@ -416,7 +427,7 @@ ${Y("confirmOpen","confirmOpen = false",html`<span x-text="selected.length === 1
416
427
  <div id="share-modal-body" class="space-y-3"><p class="text-xs text-gray-400 dark:text-gray-500">Loading…</p></div>
417
428
  </div>
418
429
  </div>
419
- </div>`,E=html`<script src="/static/vendor/flatpickr.min.js"></script>
430
+ </div>`,L=html`<script src="/static/vendor/flatpickr.min.js"></script>
420
431
  <script src="/static/relative-time.js"></script>
421
432
  <script src="/static/table-resize.js"></script>
422
433
  <script src="/static/share.js"></script>
@@ -425,7 +436,7 @@ function periodPicker() {
425
436
  return {
426
437
  open: false,
427
438
  showCustom: ${raw(g?"true":"false")},
428
- label: ${raw(j(r))},
439
+ label: ${raw(j(t))},
429
440
  fp: null,
430
441
  init() {
431
442
  const self = this;
@@ -456,17 +467,17 @@ function periodPicker() {
456
467
  },
457
468
  };
458
469
  }
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">
470
+ </script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:R,scripts:L})}function it(r,e){let g=[],d=0;for(let t=1;t<=e;t++)(t===1||t===e||t>=r-2&&t<=r+2)&&(d&&t-d>1&&g.push(0),g.push(t),d=t);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(r){let e=`/lessons/${encodeURIComponent(r.id)}/share?format=md&name=${encodeURIComponent(r.name)}&include_context=${r.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${r.text}" data-link="${r.link}" class="space-y-2">
460
471
  <div class="flex flex-wrap gap-2">
461
472
  <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>
462
473
  <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>
463
474
  <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>
464
475
  </div>
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>
466
- </div>`}function W(t,e,g=null){return html`
476
+ <p class="text-[11px] text-gray-400 dark:text-gray-500">${r.name?html`Shared by <span class="text-gray-600 dark:text-gray-300">${r.name}</span>`:"Shared anonymously"} · ${r.includeContext?"includes project, branch and commit":"only the lesson travels \u2014 no paths, no project"}.</p>
477
+ </div>`}function W(r,e,g=null){return html`
467
478
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Share this lesson</p>
468
479
  ${g?html`<p class="text-sm font-semibold text-gray-800 dark:text-gray-100 leading-snug">${g}</p>`:""}
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">
480
+ <form method="post" action="/lessons/${encodeURIComponent(r)}/share" hx-post="/lessons/${encodeURIComponent(r)}/share" hx-target="#share-payloads" hx-swap="outerHTML" hx-trigger="input delay:300ms, change" onsubmit="return false" class="space-y-2.5">
470
481
  <input type="hidden" name="persist" value="0" x-ref="persist" />
471
482
  <div>
472
483
  <label for="share-name" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Your name</label>
@@ -475,10 +486,10 @@ function periodPicker() {
475
486
  <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>
476
487
  </form>
477
488
  ${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`
489
+ `}function We(r){let e=r.lesson,g=r.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",t=e.repository_platform==="local",o=null;t&&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&&!t&&(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`
479
490
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
480
491
  <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>
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>`:""}
492
+ ${r.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">${r.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>`:""}
482
493
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
483
494
  <div class="flex flex-wrap items-center gap-3 mb-2">
484
495
  <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">
@@ -488,7 +499,7 @@ ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
488
499
  </form>
489
500
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.title}</h1>
490
501
  <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")}
502
+ ${Y(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
503
  <div class="relative shrink-0" x-data="{ open: ${String(g.open)} }" @click.outside="open = false" @keydown.escape="open = false">
493
504
  <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>
494
505
  <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">
@@ -502,7 +513,7 @@ ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
502
513
  </span>
503
514
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
504
515
  ${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>`:""}
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>`:""}
516
+ ${r.course?html`<a href="/courses/${encodeURIComponent(r.course.id)}" title="${r.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 · ${r.course.done}/${r.course.total}${r.course.status==="completed"?" \u2713":""}</a>`:""}
506
517
  ${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
518
  ${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
519
  <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>`:""}
@@ -514,8 +525,8 @@ ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
514
525
  <div id="body-content" class="markdown-body"></div>
515
526
  ${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>`:""}
516
527
  ${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>`:""}
528
+ ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${o?html`<a href="${o}" ${t?"":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>`:""}
529
+ ${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}" ${t?"":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>`:""}
519
530
  ${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>`:""}
520
531
  ${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>`:""}
521
532
  ${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>`:""}
@@ -527,7 +538,7 @@ ${t.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
527
538
  </div>`}</div>
528
539
  <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>
529
540
  </div>
530
- ${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
541
+ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
531
542
  <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>
532
543
  <form method="post" action="/lessons/delete" class="contents">
533
544
  <input type="hidden" name="id" value="${e.id}" />
@@ -550,7 +561,7 @@ ${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
550
561
  });
551
562
  document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.summary))}));
552
563
  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">
564
+ </script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:p,scripts:v})}var dt={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 Ze(r){let e=r.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
554
565
  <input type="hidden" name="from" value="lessons" />
555
566
  <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>
556
567
  <div class="flex items-center gap-3">
@@ -562,10 +573,10 @@ ${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
562
573
  <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>
563
574
  <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">
564
575
  <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>
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>`:""}
576
+ ${r.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">${r.error}</div>`:""}
566
577
  ${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
567
578
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
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>
579
+ <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${dt[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
569
580
  </div>
570
581
  <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
571
582
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
@@ -583,11 +594,11 @@ ${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
583
594
  ${e.lesson.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.lesson.task_context}</div>`:""}
584
595
  <form method="post" action="/lessons/import" class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap items-center gap-3">
585
596
  <input type="hidden" name="from" value="lessons" />
586
- <input type="hidden" name="text" value="${t.code}" />
597
+ <input type="hidden" name="text" value="${r.code}" />
587
598
  <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>
588
599
  <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>
589
600
  </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>
601
+ </div>`,t=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,o=html`<script src="/static/vendor/highlight.min.js"></script>
591
602
  <script src="/static/vendor/marked.min.js"></script>
592
603
  <script src="/static/vendor/purify.min.js"></script>
593
604
  <script src="/static/share.js"></script>
@@ -602,12 +613,12 @@ ${Y("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
602
613
  });
603
614
  ${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.summary)}));
604
615
  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`
616
+ </script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:t,body:d,scripts:o})}var lt=[[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"]],Ke={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"},ct={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function Qe(r){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Ke[r]??Ke.abandoned}">${r}</span>`}function Xe(r){let e=html`
606
617
  <div class="flex items-center justify-between mb-6">
607
618
  <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>
619
+ <p class="text-sm text-gray-400 dark:text-gray-500">${r.courses.length} course${r.courses.length!==1?"s":""}</p>
609
620
  </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">
621
+ ${r.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
622
  <table class="w-full text-sm table-fixed">
612
623
  <colgroup><col style="width:46%" /><col style="width:22%" /><col style="width:14%" /><col style="width:8rem" /></colgroup>
613
624
  <thead>
@@ -616,15 +627,15 @@ ${t.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
616
627
  </tr>
617
628
  </thead>
618
629
  <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">
630
+ ${r.courses.map(d=>{let t=d.steps.filter(n=>n.status!=="todo").length,o=d.steps.length?Math.round(t/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
631
  <td class="px-4 py-3">
621
632
  <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
633
  ${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
634
  </td>
624
635
  <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>
636
+ <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">${t}/${d.steps.length}</span></div>
626
637
  </td>
627
- <td class="px-3 py-3">${Ve(d.status)}</td>
638
+ <td class="px-3 py-3">${Qe(d.status)}</td>
628
639
  <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
640
  </tr>`})}
630
641
  </tbody>
@@ -633,21 +644,27 @@ ${t.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
633
644
  <p class="text-3xl mb-3">🎓</p>
634
645
  <p class="text-gray-700 dark:text-gray-200 font-medium">No courses yet.</p>
635
646
  <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`
647
+ </div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:r.uiTheme,body:e,scripts:g})}function et(r){return html`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><title>Read-only — devcoach</title>
648
+ <link rel="stylesheet" href="/static/style.css" /></head>
649
+ <body style="font-family: ui-monospace, monospace; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; line-height: 1.5">
650
+ <h1 style="font-size: 1.25rem">This devcoach is older than your database</h1>
651
+ <p>${r}</p>
652
+ <p><a href="javascript:history.back()">← Back</a></p>
653
+ </body></html>`}function tt(r){let e=r.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===r.selected)??e.steps[0],t=`/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
654
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
638
655
  <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
656
  <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
657
  <span class="text-sm font-semibold text-gray-900 dark:text-white truncate min-w-0">${e.title}</span>
641
- ${Ve(e.status)}
658
+ ${Qe(e.status)}
642
659
  <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")}
660
+ ${Y(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
661
  </div>
645
662
  <div class="grid grid-cols-1 lg:grid-cols-[15rem_minmax(0,1fr)] gap-6 lg:gap-8">
646
663
  <aside id="course-steps" class="lg:sticky lg:top-16 self-start">
647
664
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 px-2 mb-2">Steps</p>
648
665
  ${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
666
  <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>
667
+ <span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-indigo-400"}">${ct[s.status]??"\u25CB"}</span>
651
668
  <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
669
  </a>
653
670
  <div data-step-actions class="flex flex-wrap gap-1.5 mt-2 pl-6 ${d&&s.position===d.position?"":"hidden"}">
@@ -659,7 +676,7 @@ ${t.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
659
676
  <summary class="cursor-pointer font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 select-none">About this course</summary>
660
677
  <div class="mt-2 space-y-2">
661
678
  <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>`:""}
679
+ ${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">${r.lessonTitle??e.lesson_id}</a></p>`:""}
663
680
  ${e.goal?html`<p class="text-gray-600 dark:text-gray-300">${e.goal}</p>`:""}
664
681
  ${e.prerequisites.length?html`<div class="flex flex-wrap items-center gap-1" title="The prerequisite chain explored before writing the course">
665
682
  ${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>`)}
@@ -668,19 +685,19 @@ ${t.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
668
685
  </details>
669
686
  </aside>
670
687
  <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>`}
688
+ ${r.hasDocument?html`<iframe id="course-frame" src="${t}" 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
689
  </div>
673
690
  </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>
691
+ ${J("confirmOpen","confirmOpen = false",html`Delete this course?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
675
692
  <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
693
  <form method="post" action="/courses/delete" class="contents">
677
694
  <input type="hidden" name="id" value="${e.id}" />
678
695
  <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
696
  </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`
697
+ </div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:r.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function rt(r){let e=r.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}"]],t=r.flash,o=html`
681
698
  <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
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.":""}
699
+ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.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"}">
700
+ ${t.imported} lesson${t.imported!==1?"s":""} imported.${t.skipped?` ${t.skipped} skipped (already in DB).`:""}${t.invalid?` ${t.invalid} rejected (failed validation).`:""}${t.groups?` ${t.groups} group${t.groups!==1?"s":""} added.`:""}${t.notebook?" Notebook restored.":""}
684
701
  </div>`:""}
685
702
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
686
703
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
@@ -689,13 +706,13 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
689
706
  <div class="grid grid-cols-2 gap-4">
690
707
  <div>
691
708
  <label for="max-per-day" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Max lessons per day</label>
692
- <input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${t.settings.max_per_day}" 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" />
709
+ <input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${r.settings.max_per_day}" 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" />
693
710
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Lessons in a 24h window.</p>
694
711
  </div>
695
712
  <div>
696
713
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
697
714
  <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">
698
- ${st.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
715
+ ${lt.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
699
716
  </select>
700
717
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
701
718
  </div>
@@ -703,28 +720,28 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
703
720
  <div class="grid grid-cols-2 gap-4">
704
721
  <div>
705
722
  <label for="nudge-every" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Interactions between lessons</label>
706
- <input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${t.settings.nudge_every}" 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" />
723
+ <input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${r.settings.nudge_every}" 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" />
707
724
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cue at most once every N (0 = every turn).</p>
708
725
  </div>
709
726
  <div>
710
727
  <label for="nudge-scope" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count interactions</label>
711
728
  <select id="nudge-scope" name="nudge_scope" 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">
712
- <option value="session" ${t.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
713
- <option value="global" ${t.settings.nudge_scope==="global"?"selected":""}>Globally</option>
729
+ <option value="session" ${r.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
730
+ <option value="global" ${r.settings.nudge_scope==="global"?"selected":""}>Globally</option>
714
731
  </select>
715
732
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Per-chat or across all sessions.</p>
716
733
  </div>
717
734
  </div>
718
735
  <div>
719
736
  <label for="share-name" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Your name (for sharing)</label>
720
- <input id="share-name" type="text" name="share_name" maxlength="80" value="${t.settings.share_name??""}" placeholder="git user.name when empty" autocomplete="off" 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" />
737
+ <input id="share-name" type="text" name="share_name" maxlength="80" value="${r.settings.share_name??""}" placeholder="git user.name when empty" autocomplete="off" 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" />
721
738
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Proposed as the sender when you share a lesson. Empty → your git name.</p>
722
739
  </div>
723
740
  <fieldset class="border-0 p-0 m-0">
724
741
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">UI theme</legend>
725
742
  <div class="flex gap-2">
726
743
  ${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" />
744
+ <input type="radio" name="ui_theme" value="${i}" ${r.settings.ui_theme===i?"checked":""} class="sr-only peer" />
728
745
  <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>
729
746
  </label>`)}
730
747
  </div>
@@ -734,7 +751,7 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
734
751
  <legend class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Home page</legend>
735
752
  <div class="flex gap-2">
736
753
  ${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" />
754
+ <input type="radio" name="ui_home" value="${i}" ${r.settings.ui_home===i?"checked":""} class="sr-only peer" />
738
755
  <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>
739
756
  </label>`)}
740
757
  </div>
@@ -787,11 +804,11 @@ ${r?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${r.invalid?"bg-ye
787
804
  <button type="button" @click="nbMode = 'source'" :class="nbMode === 'source' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition border-l border-gray-200 dark:border-gray-700">Source</button>
788
805
  </div>
789
806
  <a href="/settings/notebook/download" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition">↓ Download</a>
790
- <a href="vscode://file/${t.notebookPath}" title="Open in VS Code" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition"><img src="/static/vscode.svg" class="w-4 h-4" alt="VS Code" />Open in VS Code</a>
807
+ <a href="vscode://file/${r.notebookPath}" title="Open in VS Code" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition"><img src="/static/vscode.svg" class="w-4 h-4" alt="VS Code" />Open in VS Code</a>
791
808
  </div>
792
809
  </div>
793
810
  <div x-show="nbMode === 'preview'" id="notebook-preview-settings" class="markdown-body min-h-24"></div>
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>
811
+ <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">${r.notebookContent}</pre>
795
812
  </div>
796
813
  </div>`,n=html`<script src="/static/vendor/marked.min.js"></script>
797
814
  <script src="/static/vendor/purify.min.js"></script>
@@ -802,10 +819,10 @@ function updateLabel(inputId, labelId, submitId) {
802
819
  document.getElementById(submitId).disabled = !input.files.length;
803
820
  }
804
821
  marked.setOptions({ breaks: true, gfm: true });
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 `
822
+ document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(r.notebookContent))}));
823
+ </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:o,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),bt={".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"},ht="https://ultimaphoenix.github.io",ot={"access-control-allow-origin":ht,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function ft(){return ee===void 0&&(ee=n()),ee}function at(r,e,g){let d=pa({explicit:g.name??null,setting:e,gitUserName:ft()}),t=g.includeContext??false,o=oa(r,{includeContext:t,sharedBy:d}),n=qa(o);return {payload:o,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:sa(o,n),link:ta(o,n)}}}function kt(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function vt(){try{return A(r=>T(r).ui_theme)}catch{return "system"}}function wt(r={}){let e$1=new Hono;e$1.onError((t,o)=>t instanceof w?o.html(et(t.message),503):(console.error(t),o.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let o=decodeURIComponent(t.req.path.slice(8)),n=join(X,o);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":bt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let o=null;try{o=A(n=>{let i=x(n);return i?{...i,message:v(i)}:null});}catch{}return t.json({ok:true,version:a,compat:o},200,ot)}),e$1.options("/ping",t=>t.body(null,204,ot)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:o,hasLessons:n}=A(i=>({home:T(i).ui_home,hasLessons:C(i)>0}));return t.redirect(kt(o,n),302)}),e$1.get("/knowledge",t=>{let{profile:o,stats:n,rateLimit:i,settings:s}=A(p=>({profile:ya(p),stats:Ba(p),rateLimit:xa(p),settings:T(p)})),c=new Map;for(let p of o.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of o.groups)u[p.name]=[];for(let p of o.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Je({categorised:u,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 t=>{let o=await t.req.parseBody(),n=b(o,"topic").trim();if(n){let i=Number.parseInt(b(o,"confidence","5"),10)||5,s=b(o,"group").trim();A(c=>{N(c,n,i),s&&s!=="Other"&&R(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(A(o=>O(o,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let o=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return A(i=>{o&&o!=="Other"?R(i,n,o):S(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let o=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return A(n=>za(n,t.req.param("topic"),o)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let o=b(await t.req.parseBody(),"group_name").trim();return o&&o!=="Other"&&A(n=>P$1(n,o)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(A(o=>Q$1(o,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=A(o=>H(o));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let o=await t.req.parseBody(),n=b(o,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=o.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(o,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=A(m=>Ca(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof na)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let o=(t.req.query("code")??"").trim(),n=null,i=null;if(o)try{n=ra(o);}catch(s){i=s instanceof na?s.message:"That code could not be read.";}return t.html(Ze({code:o,preview:n,error:i,uiTheme:vt()}))}),e$1.get("/lessons",t=>{let o=t.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=o.starred==="1",p=o.imported==="1"?"1":o.imported==="0"?"0":"",m=o.shared_by||null,v=o.sort||"timestamp",I$1=o.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(o.page??"1",10)||1),L$1=o.period||"all",l={period:s||c?null:L$1!=="all"?L$1: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?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=A(k=>{let re=C(k,l),oe=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,oe),{total:re,totalPages:oe,lessons:D(k,{...l,sort:v,order:I$1,page:R,per_page:Z}),allCategories:L(k),allProjects:I(k,"project"),allRepositories:I(k,"repository"),allBranches:I(k,"branch"),allCommits:I(k,"commit_hash"),allSharedBy:J$1(k),theme:T(k).ui_theme}}),$={period:L$1,category:o.category||"",level:i,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:c,sort:v,order:I$1},S=o.error==="invalid"||o.error==="cross"?o.error:null;return t.html(Ve({importOpen:o.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let o=await t.req.parseBody();return A(n=>G(n,t.req.param("lesson_id"),o.starred==="1")),t.redirect(B(b(o,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let o=await t.req.parseBody(),n=b(o,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return A(s=>Aa(s,t.req.param("lesson_id"),i)),t.redirect(B(b(o,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let o=await t.req.parseBody({all:true}),n=o.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return A(s=>F(s,i)),t.redirect(B(b(o,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t$1=>{if(E(t$1))return t$1.text("Forbidden",403);let o,n,i=false;if(t$1.req.method==="POST"){let m=await t$1.req.parseBody();o=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else o=t$1.req.query("name"),n=t$1.req.query("include_context")==="1";let s=A(m=>{let v=K$1(m,t$1.req.param("lesson_id")??"");return v?(i&&o!==void 0&&U(m,"share_name",t(o)),{lesson:v,settingName:T(m).share_name}):null});if(!s)return t$1.text("Lesson not found",404);let{state:c,payload:u}=at(s.lesson,s.settingName,{name:o,includeContext:n}),p=t$1.req.query("format");return p==="text"?t$1.text(c.text):p==="link"?t$1.text(c.link):p==="md"?new Response(wa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${va(u)}"`}}):p==="panel"?t$1.html(W(s.lesson.id,c,s.lesson.title)):t$1.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let o=A(s=>{let c=K$1(s,t.req.param("lesson_id"));if(!c)return null;let u=T(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 t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(We({lesson:o.lesson,uiTheme:o.uiTheme,share:at(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
824
  <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};
825
+ `}catch{return ""}})();return e$1.get("/courses",t=>{let o=A(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?K$1(n,i.lesson_id)?.title??null:null})),uiTheme:T(n).ui_theme}));return t.html(Xe(o))}),e$1.get("/courses/:course_id/index.html",t=>{let o=t.req.param("course_id");if(!a$1.test(o))return t.notFound();let n=e(o);if(!n)return t.notFound();try{let i=Buffer.concat([readFileSync(n),Buffer.from(d)]);return new Response(new Uint8Array(i),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let o=t.req.param("course_id"),n=A(c=>{let u=g(c,o);return u?{course:u,lessonTitle:u.lesson_id?K$1(c,u.lesson_id)?.title??null:null,uiTheme:T(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(tt({...n,selected:s,hasDocument:e(o)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let o=await t.req.parseBody(),n=b(o,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return A(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(o,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let o=await t.req.parseBody();return A(n=>l(n,b(o,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=A(o=>ka(o));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let o=(await t.req.parseBody()).file;if(!(o instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await o.arrayBuffer()),i=A(s=>ma(s,n));return t.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 t=existsSync(o)?readFileSync(o,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let o$1=await t.req.parseBody();return mkdirSync(dirname(o),{recursive:true}),writeFileSync(o,b(o$1,"content"),"utf8"),t.redirect(B(b(o$1,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t$1=>{let o=await t$1.req.parseBody(),n=Number.parseInt(b(o,"max_per_day","2"),10)||2,i=Number.parseInt(b(o,"min_gap_minutes","240"),10),s=b(o,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(o,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(o,"nudge_every","10"),10)||0),p=b(o,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),A(m=>{U(m,"max_per_day",String(n)),U(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),U(m,"ui_theme",s),U(m,"ui_home",c),U(m,"nudge_every",String(u)),U(m,"nudge_scope",p),U(m,"share_name",t(b(o,"share_name")));}),t$1.redirect("/settings",303)}),e$1.get("/settings",t=>{let o$1=A(c=>T(c)),n=existsSync(o)?readFileSync(o,"utf8"):"",i=t.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 t.html(rt({settings:o$1,notebookContent:n,notebookPath:o,uiTheme:o$1.ui_theme,flash:s}))}),e$1}function $t(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),o=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(o),d();}),g.closeIdleConnections?.();})}async function _t(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/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 St(r,e,g=_t){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,o=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$2.yellow("devcoach UI is already running at")} ${a$2.cyan(c(t))}${o}
826
+ ${a$2.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$2.red(`\u2717 Port ${r} is already in use by another process`)}
827
+ ${a$2.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$2.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$2.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function qt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$2.dim(`devcoach UI stopping (${s})\u2026`)),$t(o).then(()=>{c&&process.exit(0);});},t=wt({onShutdown:()=>d("stop requested",false)}),o=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c$1=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$2.cyan(c(c$1))}`),process.stdout.isTTY&&console.log(a$2.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]);St(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),o}async function Ft(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
828
+ export{wt as createApp,St as explainListenError,$t as gracefulShutdown,_t as pingUi,kt as resolveHomePath,qt as startUi,Ft as stopUi};