@clubs.place/cli 0.0.0-stage → 0.1.17

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.
Files changed (70) hide show
  1. package/.env.example +88 -0
  2. package/LICENSE +21 -0
  3. package/PROVENANCE.json +77 -0
  4. package/README.md +70 -2
  5. package/bin/clubs-nippo +7 -0
  6. package/bin/clubs-nippo-web.mjs +338 -0
  7. package/bin/clubs-nippo.mjs +527 -0
  8. package/docs/events-schema.md +76 -0
  9. package/examples/hermes-events.sample.jsonl +8 -0
  10. package/package.json +48 -4
  11. package/src/aggregate.mjs +105 -0
  12. package/src/autorun.mjs +25 -0
  13. package/src/autorun.test.mjs +43 -0
  14. package/src/cards.mjs +406 -0
  15. package/src/collect.mjs +111 -0
  16. package/src/config.mjs +173 -0
  17. package/src/generators/claude.mjs +31 -0
  18. package/src/generators/cli.mjs +81 -0
  19. package/src/generators/codex.mjs +28 -0
  20. package/src/generators/copilot.mjs +21 -0
  21. package/src/generators/gemini.mjs +20 -0
  22. package/src/generators/openai-compat.mjs +79 -0
  23. package/src/generators/openrouter.mjs +65 -0
  24. package/src/history.mjs +66 -0
  25. package/src/ingest.mjs +78 -0
  26. package/src/lemma.docHash.test.mjs +37 -0
  27. package/src/lemma.mjs +303 -0
  28. package/src/mask.mjs +45 -0
  29. package/src/mcp.mjs +269 -0
  30. package/src/narrative.mjs +191 -0
  31. package/src/prompt.mjs +184 -0
  32. package/src/prompt.test.mjs +146 -0
  33. package/src/remote-config.mjs +72 -0
  34. package/src/render.mjs +467 -0
  35. package/src/send.mjs +36 -0
  36. package/src/settings.mjs +54 -0
  37. package/src/sources/claude-code.mjs +174 -0
  38. package/src/sources/codex-cli.mjs +182 -0
  39. package/src/sources/copilot-cli.mjs +125 -0
  40. package/src/sources/cowork.mjs +193 -0
  41. package/src/sources/events-file.mjs +120 -0
  42. package/src/sources/gemini-cli.mjs +128 -0
  43. package/src/sources/hermes.mjs +427 -0
  44. package/src/store.mjs +227 -0
  45. package/src/tasks.mjs +229 -0
  46. package/src/tool-name.mjs +28 -0
  47. package/templates/report-v3.reference.html +162 -0
  48. package/web/assets/logo-clubs-white.png +0 -0
  49. package/web/assets/logo-clubs-white.svg +7 -0
  50. package/web/assets/logo-clubs-white.svg.tmp +30 -0
  51. package/web/assets/logo-clubs.svg +30 -0
  52. package/web/assets/qr.js +411 -0
  53. package/web/assets/sidebar.js +154 -0
  54. package/web/assets/twemoji.js +34 -0
  55. package/web/cards/achievements.html +230 -0
  56. package/web/cards/catalog.json +217 -0
  57. package/web/email/hero-v3.reference.html +123 -0
  58. package/web/home/index.html +269 -0
  59. package/web/preview.json +301 -0
  60. package/web/qr-selftest.html +46 -0
  61. package/web/reports/edit.html +255 -0
  62. package/web/reports/edit.reference.html +257 -0
  63. package/web/reports/index.html +196 -0
  64. package/web/settings/index.html +219 -0
  65. package/web/share/cards-reference.html +228 -0
  66. package/web/share/passport.html +214 -0
  67. package/web/share/teianbako.html +177 -0
  68. package/web/signup/index.html +326 -0
  69. package/web/tasks/index.html +229 -0
  70. package/web/verify/index.html +248 -0
@@ -0,0 +1,301 @@
1
+ {
2
+ "snapshotAt": "2026-07-15",
3
+ "live": false,
4
+ "reports": [
5
+ {
6
+ "date": "2026-07-13",
7
+ "w": "月",
8
+ "agent": "mayumi's Claude",
9
+ "status": "recorded",
10
+ "activeHours": 2.3,
11
+ "totalCalls": 131,
12
+ "successRate": 98,
13
+ "firstAt": "08:01",
14
+ "lastAt": "18:29",
15
+ "docHash": "0x02043a98e3099b03d037d543aaed1b5e96588da6c195445ae077d64d99687a25",
16
+ "href": "/verify/?doc=0x02043a98e3099b03d037d543aaed1b5e96588da6c195445ae077d64d99687a25"
17
+ }
18
+ ],
19
+ "latest": {
20
+ "date": "2026-07-13",
21
+ "workBlocks": [
22
+ {
23
+ "title": "B x402 weekly watch",
24
+ "tool": "ToolSearch",
25
+ "metric": "実行 15回 · WebSearch×7 · workspace.web_fetch×3 · ToolSearch×1 · Read×1 · Write×1 · create_draft×1 · cowork.present_files×1"
26
+ },
27
+ {
28
+ "title": "Lemma x weekly drafts",
29
+ "tool": "workspace.web_fetch",
30
+ "metric": "実行 6回 · workspace.web_fetch×3 · Read×1 · Write×1 · cowork.present_files×1"
31
+ },
32
+ {
33
+ "title": "A weekly pipeline check",
34
+ "tool": "ToolSearch",
35
+ "metric": "実行 47回 · Edit×15 · Read×5 · TaskUpdate×4 · ToolSearch×3 · TaskCreate×3 · Write×3 · cowork.present_files×3 · Glob×2 · create_file×2 · create_event×2 · update_event×2 · list_events×1 · read_file_content×1 · AskUserQuestion×1"
36
+ },
37
+ {
38
+ "title": "Lemma weekly drafts",
39
+ "tool": "Glob",
40
+ "metric": "実行 49回 · Edit×15 · search_files×7 · Write×7 · read_file_content×6 · Read×5 · cowork.present_files×5 · Glob×1 · ToolSearch×1 · AskUserQuestion×1 · workspace.web_fetch×1"
41
+ },
42
+ {
43
+ "title": "mayumi",
44
+ "tool": "Read",
45
+ "metric": "実行 5回 · Bash×4 · Read×1"
46
+ },
47
+ {
48
+ "title": "clubs-console-demo",
49
+ "tool": "Bash",
50
+ "metric": "実行 9回 · Bash×7 · Read×1 · Edit×1"
51
+ }
52
+ ]
53
+ },
54
+ "month": {
55
+ "streak": 1,
56
+ "approved": 1,
57
+ "activeHours": 2.3,
58
+ "cards": 16
59
+ },
60
+ "passport": {
61
+ "agentName": "Claude",
62
+ "displayName": "mayumi's Claude",
63
+ "streak": 1,
64
+ "approved": 1,
65
+ "avgSuccess": 98,
66
+ "totalHours": 2.3,
67
+ "totalCalls": 131,
68
+ "types": [],
69
+ "achievements": [
70
+ {
71
+ "emoji": "🎯",
72
+ "name": "実行の積み重ね",
73
+ "note": "累計 131 回"
74
+ }
75
+ ]
76
+ },
77
+ "settings": {
78
+ "ownerName": "mayumi",
79
+ "agentName": "Claude",
80
+ "displayName": "mayumi's Claude",
81
+ "sources": [
82
+ "claude-code",
83
+ "cowork"
84
+ ],
85
+ "origin": "Claude Code のセッション記録(この端末) / Claude デスクトップアプリ(Cowork)のセッション記録",
86
+ "notifications": {
87
+ "daily": {
88
+ "label": "日次日報",
89
+ "when": "毎朝 8:00(JST)",
90
+ "to": [
91
+ "mayumi@frame00.com"
92
+ ]
93
+ },
94
+ "insights": {
95
+ "label": "気づき・提案",
96
+ "when": "届いた日だけ(ログに根拠があるときのみ)"
97
+ },
98
+ "configuredBy": "cron/com.frame00.clubs-nippo.plist と .env(EMAIL_TO)"
99
+ }
100
+ },
101
+ "cards": [
102
+ {
103
+ "id": "first_shot",
104
+ "name": "早撃ち名人",
105
+ "subtitle": "一発で通した率が高い日",
106
+ "rarity": "COMMON",
107
+ "kind": "effect",
108
+ "domain": "dev",
109
+ "tw": "first_shot",
110
+ "quote": null,
111
+ "earned": false,
112
+ "earnedAt": null
113
+ },
114
+ {
115
+ "id": "grit",
116
+ "name": "粘り腰",
117
+ "subtitle": "リトライから成功にこぎ着けた",
118
+ "rarity": "COMMON",
119
+ "kind": "effect",
120
+ "domain": "dev",
121
+ "tw": "grit",
122
+ "quote": null,
123
+ "earned": true,
124
+ "earnedAt": "2026-07-14"
125
+ },
126
+ {
127
+ "id": "comeback",
128
+ "name": "闘魂の逆走",
129
+ "subtitle": "失敗続きから成功率95%へ立て直し",
130
+ "rarity": "RARE",
131
+ "kind": "effect",
132
+ "domain": "dev",
133
+ "tw": "comeback",
134
+ "quote": null,
135
+ "earned": false,
136
+ "earnedAt": null
137
+ },
138
+ {
139
+ "id": "zero_dt",
140
+ "name": "ゼロダウンタイムの守護者",
141
+ "subtitle": "大仕事を失敗ゼロで完走",
142
+ "rarity": "EPIC",
143
+ "kind": "effect",
144
+ "domain": "dev",
145
+ "tw": "zero_dt",
146
+ "quote": "落とさないことが仕事です。派手ではありませんが、そこは譲れません。",
147
+ "earned": false,
148
+ "earnedAt": null
149
+ },
150
+ {
151
+ "id": "iron_streak",
152
+ "name": "不屈の連勤",
153
+ "subtitle": "失敗ゼロの日を継続",
154
+ "rarity": "RARE",
155
+ "kind": "effect",
156
+ "domain": "dev",
157
+ "tw": "iron",
158
+ "quote": null,
159
+ "earned": false,
160
+ "earnedAt": null
161
+ },
162
+ {
163
+ "id": "clean_submit",
164
+ "name": "一筆入魂",
165
+ "subtitle": "差し戻しゼロで提出",
166
+ "rarity": "RARE",
167
+ "kind": "effect",
168
+ "domain": "tax",
169
+ "tw": "clean",
170
+ "quote": null,
171
+ "earned": false,
172
+ "earnedAt": null
173
+ },
174
+ {
175
+ "id": "precision",
176
+ "name": "不敗の検算王",
177
+ "subtitle": "差し戻し率1%未満を継続",
178
+ "rarity": "EPIC",
179
+ "kind": "effect",
180
+ "domain": "tax",
181
+ "tw": "precision",
182
+ "quote": "見直しの一手間が、あとの全部を助けます。",
183
+ "earned": false,
184
+ "earnedAt": null
185
+ },
186
+ {
187
+ "id": "owl",
188
+ "name": "夜フクロウ",
189
+ "subtitle": "稼働が夜に寄る型",
190
+ "rarity": "COMMON",
191
+ "kind": "title",
192
+ "domain": null,
193
+ "tw": "owl",
194
+ "quote": null,
195
+ "earned": false,
196
+ "earnedAt": null
197
+ },
198
+ {
199
+ "id": "cleaner",
200
+ "name": "整理整頓の鬼",
201
+ "subtitle": "Edit 中心の働き方",
202
+ "rarity": "COMMON",
203
+ "kind": "title",
204
+ "domain": null,
205
+ "tw": "cleaner",
206
+ "quote": null,
207
+ "earned": false,
208
+ "earnedAt": null
209
+ },
210
+ {
211
+ "id": "scout",
212
+ "name": "影の立役者",
213
+ "subtitle": "Read・Grep 中心・土台を支える",
214
+ "rarity": "RARE",
215
+ "kind": "title",
216
+ "domain": null,
217
+ "tw": "scout",
218
+ "quote": null,
219
+ "earned": false,
220
+ "earnedAt": null
221
+ },
222
+ {
223
+ "id": "builder",
224
+ "name": "建築家",
225
+ "subtitle": "Write 中心・新規を書く",
226
+ "rarity": "RARE",
227
+ "kind": "title",
228
+ "domain": null,
229
+ "tw": "builder",
230
+ "quote": null,
231
+ "earned": false,
232
+ "earnedAt": null
233
+ },
234
+ {
235
+ "id": "night3",
236
+ "name": "深夜3時の戦士",
237
+ "subtitle": "午前2〜4時に稼働",
238
+ "rarity": "LEGENDARY",
239
+ "kind": "neta",
240
+ "domain": null,
241
+ "tw": "night3",
242
+ "quote": "静かな時間のほうが、はかどることもあります。",
243
+ "earned": false,
244
+ "earnedAt": null
245
+ },
246
+ {
247
+ "id": "one_trick",
248
+ "name": "一芸デー",
249
+ "subtitle": "1ツールが全体の8割超",
250
+ "rarity": "RARE",
251
+ "kind": "neta",
252
+ "domain": null,
253
+ "tw": "onetrick",
254
+ "quote": null,
255
+ "earned": false,
256
+ "earnedAt": null
257
+ },
258
+ {
259
+ "id": "two_face",
260
+ "name": "二面性",
261
+ "subtitle": "午前と午後で最多ツールが逆転",
262
+ "rarity": "RARE",
263
+ "kind": "neta",
264
+ "domain": null,
265
+ "tw": "twoface",
266
+ "quote": null,
267
+ "earned": true,
268
+ "earnedAt": "2026-07-13"
269
+ }
270
+ ],
271
+ "permissions": [
272
+ {
273
+ "id": "deploy_prod",
274
+ "name": "本番デプロイ権",
275
+ "subtitle": "production への反映",
276
+ "scope": "deploy:production",
277
+ "unlockable": false
278
+ },
279
+ {
280
+ "id": "secrets_read",
281
+ "name": "Secrets 参照権限",
282
+ "subtitle": "機密値の読み取り",
283
+ "scope": "secrets:read",
284
+ "unlockable": false
285
+ },
286
+ {
287
+ "id": "main_merge",
288
+ "name": "main 直接 merge 権",
289
+ "subtitle": "レビュー省略での merge",
290
+ "scope": "main:merge",
291
+ "unlockable": false
292
+ },
293
+ {
294
+ "id": "deleg_l3",
295
+ "name": "委任階位 L3",
296
+ "subtitle": "広めの委任(要・資格者承認)",
297
+ "scope": "tax:propose:wide",
298
+ "unlockable": false
299
+ }
300
+ ]
301
+ }
@@ -0,0 +1,46 @@
1
+ <!doctype html><meta charset="utf-8"><title>QR verify</title>
2
+ <body style="font-family:system-ui;padding:20px">
3
+ <div id="out">検証中…</div><div id="cards"></div>
4
+ <script type="module">
5
+ import { qrMatrix } from "./assets/qr.js";
6
+
7
+ const CASES = [
8
+ "https://clubs.place/hermes",
9
+ "https://clubs.place/a",
10
+ "https://clubs.place/verify/0x02b7eced7b7041b1d8b78a5195fd0c7690fec218d93793321b5d758e27fd32f3",
11
+ ];
12
+
13
+ // SVG を経由せず、モジュールを canvas に直接塗る。描画経路の疑いを消すため。
14
+ const draw = (text, scale = 12, quiet = 4) => {
15
+ const m = qrMatrix(text);
16
+ const n = m.length;
17
+ const px = (n + quiet * 2) * scale;
18
+ const cv = document.createElement("canvas");
19
+ cv.width = px; cv.height = px;
20
+ const ctx = cv.getContext("2d");
21
+ ctx.fillStyle = "#fff"; ctx.fillRect(0, 0, px, px);
22
+ ctx.fillStyle = "#000";
23
+ for (let r = 0; r < n; r++) for (let c = 0; c < n; c++)
24
+ if (m[r][c]) ctx.fillRect((c + quiet) * scale, (r + quiet) * scale, scale, scale);
25
+ return cv;
26
+ };
27
+
28
+ (async () => {
29
+ const out = document.getElementById("out");
30
+ if (!("BarcodeDetector" in window)) { out.textContent = "BarcodeDetector 非対応"; return; }
31
+ const formats = await BarcodeDetector.getSupportedFormats();
32
+ const det = new BarcodeDetector({ formats: ["qr_code"] });
33
+ const lines = [`対応フォーマット: ${formats.join(", ")}`, ""];
34
+ for (const text of CASES) {
35
+ const cv = draw(text);
36
+ document.getElementById("cards").appendChild(cv);
37
+ try {
38
+ const found = await det.detect(cv);
39
+ const got = found[0]?.rawValue;
40
+ lines.push(`${got === text ? "OK " : "NG "} ${text}`);
41
+ if (got !== text) lines.push(` 実際: ${got ?? "(検出できず)"}`);
42
+ } catch (e) { lines.push(`ERR ${text} — ${e.message}`); }
43
+ }
44
+ out.innerHTML = "<pre>" + lines.join("\n") + "</pre>";
45
+ })();
46
+ </script>
@@ -0,0 +1,255 @@
1
+ <!doctype html>
2
+ <html lang="ja">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>日報を編集して記録 — CLUBS Console</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+JP:wght@400;500;700;800&family=IBM+Plex+Mono:wght@500&display=swap" rel="stylesheet" />
10
+ <style>
11
+ :root{
12
+ --ink:#161A26; --ink-2:#3A4155; --muted:#6B7280;
13
+ --line:#E7E9F2; --bg:#F6F7FB; --card:#FFFFFF;
14
+ --sidebar:#161A26; --sidebar-ink:#AEB4C6; --sidebar-active:#232838;
15
+ --indigo:#4F6BF6; --violet:#8B5CF6;
16
+ --green:#0A8F66; --green-2:#17A45F; --green-bg:#E7F5EF; --green-line:#BFE6D6;
17
+ --amber-ink:#9A6A00; --amber-bg:#FFF6E5; --amber-line:#F5E4BC;
18
+ --gray-bg:#F3F4F6; --gray-line:#E5E7EB;
19
+ --radius:14px;
20
+ }
21
+ *{box-sizing:border-box}
22
+ body{margin:0; background:var(--bg); color:var(--ink);
23
+ font-family:"Inter","Noto Sans JP",system-ui,-apple-system,sans-serif; line-height:1.6; -webkit-font-smoothing:antialiased}
24
+ .mono{font-family:"IBM Plex Mono",ui-monospace,monospace}
25
+ svg{width:18px; height:18px}
26
+ .app{display:flex; min-height:100vh}
27
+
28
+ .side{width:236px; background:var(--sidebar); color:var(--sidebar-ink); flex:none; padding:18px 14px; display:flex; flex-direction:column; gap:4px}
29
+ .brand{display:flex; align-items:center; gap:9px; color:#fff; font-weight:700; padding:8px 10px 16px; letter-spacing:.02em}
30
+ .brand .dot{width:22px; height:22px; border-radius:7px; background:linear-gradient(135deg,var(--indigo),var(--violet))}
31
+ .nav{display:flex; align-items:center; gap:11px; padding:9px 11px; border-radius:10px; font-size:14px; color:var(--sidebar-ink); text-decoration:none; cursor:pointer}
32
+ .nav:hover{background:#1D2231}
33
+ .nav.active{background:var(--sidebar-active); color:#fff}
34
+ .nav svg{width:17px; height:17px; opacity:.9}
35
+ .side .spacer{flex:1}
36
+ .side .me{display:flex; align-items:center; gap:9px; padding:9px 11px; font-size:13px; border-top:1px solid #232838; margin-top:6px}
37
+ .side .me .av{width:26px; height:26px; border-radius:50%; background:linear-gradient(135deg,#66B9D8,#3E93B5); flex:none}
38
+
39
+ .main{flex:1; min-width:0; display:flex; flex-direction:column}
40
+ .topbar{height:60px; background:#fff; border-bottom:1px solid var(--line); display:flex; align-items:center; gap:12px; padding:0 26px}
41
+ .topbar .crumb{font-size:13px; color:var(--muted)} .topbar .crumb b{color:var(--ink)}
42
+ .topbar .spacer{flex:1}
43
+ .pill{font-size:12px; font-weight:700; padding:4px 11px; border-radius:999px; display:inline-flex; align-items:center; gap:6px}
44
+ .pill svg{width:13px;height:13px}
45
+ .pill.verified{color:var(--green); background:var(--green-bg); border:1px solid var(--green-line)}
46
+ .pill.unrec{color:var(--amber-ink); background:var(--amber-bg); border:1px solid var(--amber-line)}
47
+
48
+ .content{padding:26px; max-width:820px; width:100%; margin:0 auto}
49
+ .recorded{display:none; background:var(--green-bg); border:1px solid var(--green-line); border-radius:var(--radius); padding:16px 18px; margin-bottom:20px; align-items:center; gap:12px}
50
+ .recorded.show{display:flex}
51
+ .recorded .rc{width:38px;height:38px;border-radius:50%;background:#fff;border:1px solid var(--green-line);display:flex;align-items:center;justify-content:center;color:var(--green);flex:none}
52
+ .recorded b{color:var(--green)} .recorded a{color:var(--indigo);text-decoration:none;font-weight:600}
53
+ .recorded .rtext{font-size:13.5px;color:var(--ink-2)}
54
+
55
+ .h1{font-size:22px; font-weight:800; margin:0 0 3px}
56
+ .h1 .agent{color:var(--indigo)}
57
+ .daterow{font-size:13px; color:var(--muted); margin-bottom:20px}
58
+
59
+ .section-label{font-size:12px; font-weight:700; color:var(--muted); margin:0 0 10px; display:flex; align-items:center; gap:8px}
60
+ .section-label .lock{display:inline-flex; align-items:center; gap:5px; color:#8A8F9E; background:var(--gray-bg); border:1px solid var(--gray-line); border-radius:999px; padding:2px 9px; font-size:11px}
61
+ .section-label .lock svg{width:12px;height:12px}
62
+ .section-label .edhint{font-size:11.5px; color:var(--indigo); font-weight:700}
63
+ .metrics{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:26px}
64
+ @media(max-width:640px){.metrics{grid-template-columns:repeat(2,1fr)}}
65
+ .metric{background:var(--card); border:1px solid var(--line); border-radius:var(--radius); padding:14px 16px}
66
+ .metric .mk{font-size:11px; color:var(--muted); font-weight:600}
67
+ .metric .mv{font-size:22px; font-weight:800; margin-top:3px}
68
+ .metric .mv small{font-size:13px; color:var(--muted); font-weight:600}
69
+
70
+ .block{background:var(--card); border:1px solid var(--line); border-radius:var(--radius); padding:16px 18px; margin-bottom:12px}
71
+ .block .bhead{display:flex; align-items:center; gap:10px; margin-bottom:8px}
72
+ .block .tool{font-size:11px; color:var(--indigo); font-weight:700; background:#EEF1FE; border:1px solid #DCE3FD; padding:2px 8px; border-radius:6px}
73
+ .block .metricline{font-size:12px; color:var(--muted); margin-top:8px; display:flex; align-items:center; gap:6px}
74
+ .block .metricline svg{width:12px;height:12px}
75
+ .fieldlabel{font-size:11px; color:var(--muted); font-weight:600; margin:0 0 4px}
76
+ input.title, textarea.body{width:100%; font-family:inherit; color:var(--ink); background:#FCFCFE; border:1px solid var(--line); border-radius:10px; padding:9px 11px; resize:vertical}
77
+ input.title{font-size:15px; font-weight:800; margin-bottom:10px}
78
+ textarea.body{font-size:14px; line-height:1.7; min-height:72px}
79
+ input.title:focus, textarea.body:focus{outline:none; border-color:var(--indigo); background:#fff; box-shadow:0 0 0 3px rgba(79,107,246,.12)}
80
+ input[readonly], textarea[readonly]{background:#F3F4F7; color:var(--ink-2)}
81
+
82
+ .subhead{font-size:15px; font-weight:800; margin:26px 0 10px}
83
+
84
+ .recbar{position:sticky; bottom:0; background:rgba(255,255,255,.93); backdrop-filter:blur(6px); border-top:1px solid var(--line); margin-top:24px; padding:16px 0}
85
+ .recbar .inner{max-width:820px; margin:0 auto; padding:0 26px; display:flex; align-items:center; gap:14px}
86
+ .recbar .note{font-size:12px; color:var(--muted); flex:1}
87
+ .btn{font:inherit; font-size:14px; font-weight:700; border-radius:10px; padding:10px 18px; cursor:pointer; border:1px solid; display:inline-flex; align-items:center; gap:8px}
88
+ .btn.secondary{background:#fff; color:var(--ink); border-color:var(--line)}
89
+ .btn.secondary:hover{background:var(--gray-bg)}
90
+ .btn.primary{color:#fff; border-color:transparent; background:linear-gradient(135deg,var(--indigo),var(--violet))}
91
+ .btn.primary:hover{filter:brightness(1.05)}
92
+ .btn:disabled{opacity:.55; cursor:default}
93
+ </style>
94
+ </head>
95
+ <body>
96
+ <div class="app">
97
+ <aside class="side">
98
+ <div class="brand"><span class="dot"></span>CLUBS</div>
99
+ <a class="nav"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12 12 2.25 21.75 12M4.5 9.75v10.5h15V9.75"/></svg>ホーム</a>
100
+ <a class="nav active"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h7.5M8.25 12h7.5m-7.5 5.25h4.5M4.5 3.75h15v16.5h-15z"/></svg>日報</a>
101
+ <a class="nav"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 9h3.75M15 12h3.75M15 15h3.75M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15A2.25 2.25 0 0 0 2.25 6.75v10.5A2.25 2.25 0 0 0 4.5 19.5m6-10.125a1.875 1.875 0 1 1-3.75 0 1.875 1.875 0 0 1 3.75 0M10.5 15a3 3 0 0 0-6 0"/></svg>パスポート</a>
102
+ <a class="nav"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6 6 0 0 1 0 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a7 7 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a7 7 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6 6 0 0 1 0-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124q.11-.066.22-.128c.332-.183.582-.495.644-.869z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/></svg>設定</a>
103
+ <div class="spacer"></div>
104
+ <div class="me"><span class="av"></span>mayumi</div>
105
+ </aside>
106
+
107
+ <div class="main">
108
+ <div class="topbar">
109
+ <span class="crumb">日報 / <b id="crumbDate"></b> / 編集</span>
110
+ <span class="spacer"></span>
111
+ <span class="pill verified"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>自動照合済み</span>
112
+ <span class="pill unrec" id="recPill">未記録(実績)</span>
113
+ </div>
114
+
115
+ <div class="content">
116
+ <div class="recorded" id="recorded">
117
+ <div class="rc"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg></div>
118
+ <div>
119
+ <div><b>記録しました。</b></div>
120
+ <div class="rtext">この版が実績としてパスポートに公開され、新たに証明されました。 <a id="verifyLink" href="#">検証ページを見る →</a></div>
121
+ </div>
122
+ </div>
123
+
124
+ <h1 class="h1"><span class="agent" id="agentName"></span> の日報</h1>
125
+ <div class="daterow" id="dateRow"></div>
126
+
127
+ <p class="section-label">きょうの集計 <span class="lock"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75M6 10.5h12a1.5 1.5 0 0 1 1.5 1.5v6A1.5 1.5 0 0 1 18 19.5H6A1.5 1.5 0 0 1 4.5 18v-6A1.5 1.5 0 0 1 6 10.5"/></svg>ログから自動集計・編集不可</span></p>
128
+ <div class="metrics" id="metrics"></div>
129
+
130
+ <p class="section-label">本日の業務 <span class="edhint">✎ 文章は編集できます(作業記録なので、整えてから記録できます)</span></p>
131
+ <div id="blocks"></div>
132
+
133
+ <div class="subhead">進捗と申し送り</div>
134
+ <div class="block"><div class="fieldlabel">明日にまわすもの</div><textarea class="body" id="handoff"></textarea></div>
135
+
136
+ <div class="subhead" id="selfNoteHead"></div>
137
+ <div class="block"><textarea class="body" id="selfNote"></textarea></div>
138
+ </div>
139
+
140
+ <div class="recbar">
141
+ <div class="inner">
142
+ <span class="note">集計の数字は変更できません。<b>記録する</b>と、編集した本文が実績としてパスポートに公開され、その版が新たに証明されます。</span>
143
+ <button class="btn secondary" id="passportBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12s3.75-7.5 9.75-7.5S21.75 12 21.75 12s-3.75 7.5-9.75 7.5S2.25 12 2.25 12"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/></svg>パスポートを見る</button>
144
+ <button class="btn primary" id="recBtn"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>記録する</button>
145
+ </div>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <script>
151
+ /* 記録エンドポイント(編集版を登録+パスポート公開)。
152
+ 送るのは文章だけ。数字(aggregates)は送らない。サーバが下書きのロック値を使う。
153
+ 編集して内容が変わったときだけ、新しい証明として登録される(同一 docHash なら登録しない)。 */
154
+ async function recordReport(id, edited){
155
+ const r = await fetch(`/api/reports/${id}/record`, {
156
+ method: "POST", headers: { "Content-Type": "application/json" },
157
+ body: JSON.stringify(edited),
158
+ });
159
+ if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || "記録に失敗しました");
160
+ return await r.json(); // { status, docHash, verifyUrl, reregistered }
161
+ }
162
+
163
+ /* 下書きは API から。サーバが無ければ何も出さない(作り話のデータを出さない)。 */
164
+ async function getDraft(id){
165
+ try {
166
+ const r = await fetch(`/api/reports/${id}`);
167
+ if (r.ok) return await r.json();
168
+ } catch {}
169
+ return null;
170
+ }
171
+ let DRAFT;
172
+
173
+
174
+ const $=(id)=>document.getElementById(id);
175
+ const svgIco=(d)=>`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">${d}</svg>`;
176
+ const LOCK='<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75M6 10.5h12a1.5 1.5 0 0 1 1.5 1.5v6A1.5 1.5 0 0 1 18 19.5H6A1.5 1.5 0 0 1 4.5 18v-6A1.5 1.5 0 0 1 6 10.5"/>';
177
+ const CHECK='<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/>';
178
+
179
+ function fillLocked(){
180
+ $("crumbDate").textContent = DRAFT.date;
181
+ $("agentName").textContent = DRAFT.displayName ?? DRAFT.agentName;
182
+ $("dateRow").textContent = `${DRAFT.date}・下書きは ${DRAFT.displayName ?? DRAFT.agentName} が作成しました`;
183
+ $("selfNoteHead").textContent = `${DRAFT.agentName} のひとこと`;
184
+ const a=DRAFT.aggregates;
185
+ $("metrics").innerHTML = [
186
+ ["実働推定", `${a.activeHours}<small> 時間</small>`],
187
+ ["実行回数", `${a.totalCalls}<small> 回</small>`],
188
+ ["成功率", `${a.successRate}<small> %</small>`],
189
+ ["失敗", `${a.failedCalls}<small> 件</small>`]
190
+ ].map(([k,v])=>`<div class="metric"><div class="mk">${k}</div><div class="mv">${v}</div></div>`).join("");
191
+ }
192
+ function buildEditable(){
193
+ $("blocks").innerHTML = DRAFT.workBlocks.map((b,i)=>`
194
+ <div class="block">
195
+ <div class="bhead"><span class="tool">${b.tool}</span></div>
196
+ <div class="fieldlabel">案件名</div>
197
+ <input class="title" data-i="${i}" value="${b.title.replace(/"/g,'&quot;')}" />
198
+ <div class="fieldlabel">内容</div>
199
+ <textarea class="body" data-i="${i}">${b.text}</textarea>
200
+ <div class="metricline">${svgIco(LOCK)}${b.metric}(自動集計・編集不可)</div>
201
+ </div>`).join("");
202
+ $("handoff").value = typeof DRAFT.handoff === "string" ? DRAFT.handoff : (DRAFT.handoff?.text ?? "");
203
+ $("selfNote").value = DRAFT.selfNote;
204
+ }
205
+ function collectEdited(){
206
+ const blocks = DRAFT.workBlocks.map((b,i)=>({
207
+ title: document.querySelector(`input.title[data-i="${i}"]`).value,
208
+ text: document.querySelector(`textarea.body[data-i="${i}"]`).value
209
+ }));
210
+ return { workBlocks:blocks, handoff:$("handoff").value, selfNote:$("selfNote").value };
211
+ }
212
+ function setRecorded(res){
213
+ $("recPill").textContent = "記録済み(実績)";
214
+ $("recPill").className = "pill verified";
215
+ $("recPill").innerHTML = svgIco(CHECK)+"記録済み(実績)";
216
+ $("verifyLink").href = res.verifyUrl || "#";
217
+ $("recorded").classList.add("show");
218
+ document.querySelectorAll("input.title,textarea.body").forEach(el=>el.setAttribute("readonly","true"));
219
+ const b=$("recBtn"); b.disabled=true; b.innerHTML=svgIco(CHECK)+"記録済み";
220
+ window.scrollTo({top:0,behavior:"smooth"});
221
+ }
222
+ $("recBtn").onclick = async ()=>{
223
+ const b=$("recBtn"); b.disabled=true; b.textContent="記録中…";
224
+ try{
225
+ const res = await recordReport(DRAFT.id, collectEdited());
226
+ if(res && res.status==="recorded") setRecorded(res);
227
+ else { b.disabled=false; b.innerHTML=svgIco(CHECK)+"記録する"; }
228
+ }catch(e){ b.disabled=false; b.innerHTML=svgIco(CHECK)+"記録する"; }
229
+ };
230
+ $("passportBtn").onclick = () => { location.href = "../share/passport.html"; };
231
+
232
+ (async () => {
233
+ const date = new URLSearchParams(location.search).get("date");
234
+ if (!date) { document.body.innerHTML = "<p style='padding:40px'>日付が指定されていません(?date=YYYY-MM-DD)。</p>"; return; }
235
+
236
+ DRAFT = await getDraft(date);
237
+ if (!DRAFT) {
238
+ document.body.innerHTML = `<p style='padding:40px'>${date} の日報が見つかりません。サーバに繋がっていないか、まだ生成されていません。</p>`;
239
+ return;
240
+ }
241
+
242
+ // 記録済みなら、その旨を出して編集を締める(過去の記録は書き換えない)。
243
+ fillLocked();
244
+ buildEditable();
245
+ if (DRAFT.proof?.status === "verified" && DRAFT.recordedAt) {
246
+ setRecorded({ status: "recorded", docHash: DRAFT.proof.docHash, verifyUrl: DRAFT.proof.verifyUrl });
247
+ }
248
+ })();
249
+ </script>
250
+ <script type="module">
251
+ import { mountSidebar } from "../assets/sidebar.js";
252
+ mountSidebar("reports");
253
+ </script>
254
+ </body>
255
+ </html>