@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,214 @@
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>Hermes のパスポート — CLUBS</title>
7
+
8
+ <!-- ===== OGP(リンクを貼ったとき横長プレビューを出す) ===== -->
9
+ <!-- 静的 meta は誰が開いても同じ文字列が出る。ここに数字や名前を焼くと、共有リンクの
10
+ プレビューに他人の名前と作り話の実績が出る("Hermes / 連続34日・承認済み128本・
11
+ 平均成功率99%" が入っていた。どれも実データではない)。
12
+ 本文の数字は実データから描画される。プレビューは、嘘にならない範囲に留める。 -->
13
+ <meta property="og:type" content="profile" />
14
+ <meta property="og:title" content="CLUBS パスポート" />
15
+ <meta property="og:description" content="AIの実行ログから作った実績。数字はログと一致し、あとから盛れません。" />
16
+ <meta name="twitter:card" content="summary" />
17
+
18
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
19
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
20
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Noto+Sans+JP:wght@400;500;700;800;900&display=swap" rel="stylesheet" />
21
+ <style>
22
+ :root{--indigo:#4F6BF6;--violet:#8B5CF6;--ink:#161A26;--muted:#8A90A0;--line:#EEF0F6;--green:#0A8F66;}
23
+ *{box-sizing:border-box}
24
+ body{margin:0; min-height:100vh; background:radial-gradient(1200px 600px at 50% -10%, #EDE9FB, #E3E6F2);
25
+ font-family:"Inter","Noto Sans JP",system-ui,sans-serif; color:var(--ink); line-height:1.55;
26
+ display:flex; flex-direction:column; align-items:center; padding:26px 16px 44px}
27
+ .kick{font-size:12px; color:var(--muted); font-weight:600; margin-bottom:12px}
28
+
29
+ /* passport card (縦長・カノニカル) */
30
+ .card{width:min(94vw,384px); background:#fff; border-radius:22px; overflow:hidden; box-shadow:0 16px 46px rgba(60,40,120,.20)}
31
+ .cover{height:92px; background:linear-gradient(135deg,#4F6BF6,#7C5CF6 55%,#8B5CF6); position:relative}
32
+ .cover .mk{position:absolute; top:12px; left:15px; color:#fff; font-weight:800; font-size:12px; letter-spacing:.03em; display:flex; gap:6px; align-items:center}
33
+ .cover .mk .d{width:14px;height:14px;border-radius:4px;background:rgba(255,255,255,.9)}
34
+ .cover .verified{position:absolute; top:11px; right:13px; display:inline-flex; align-items:center; gap:5px;
35
+ font-size:10.5px; font-weight:700; color:#fff; background:rgba(255,255,255,.2); border:1px solid rgba(255,255,255,.42); padding:3px 10px; border-radius:999px}
36
+ .cover .verified svg{width:12px;height:12px}
37
+ .head{padding:0 18px; margin-top:-34px; position:relative; z-index:2}
38
+ .av{width:68px;height:68px;border-radius:50%;border:4px solid #fff;background:linear-gradient(135deg,#8B5CF6,#4F6BF6);
39
+ display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;font-size:27px}
40
+ .name{font-size:22px;font-weight:900;margin-top:10px}
41
+ .by{font-size:12px;color:var(--muted);font-weight:600}
42
+ .titles{display:flex;gap:6px;flex-wrap:wrap;margin-top:11px}
43
+ .tt{font-size:11.5px;font-weight:700;padding:5px 10px;border-radius:999px;background:#F1EEFF;color:#6D53E0;border:1px solid #E1D9FB}
44
+ .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);margin:16px 18px 0;border-radius:12px;overflow:hidden;border:1px solid var(--line)}
45
+ .s{background:#fff;text-align:center;padding:13px 5px}
46
+ .s .v{font-size:20px;font-weight:800;line-height:1}
47
+ .s .v small{font-size:11px;color:var(--muted);font-weight:700}
48
+ .s .k{font-size:10px;color:var(--muted);font-weight:600;margin-top:4px}
49
+ .feat{margin:15px 18px 18px}
50
+ .feat .lbl{font-size:11px;font-weight:700;color:var(--muted);margin-bottom:8px}
51
+ .fc{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-bottom:8px}
52
+ .fc .em{width:38px;height:38px;border-radius:10px;flex:none;display:flex;align-items:center;justify-content:center;font-size:20px}
53
+ .fc.a .em{background:linear-gradient(135deg,#7C6BF2,#8B5CF6)} .fc.b .em{background:linear-gradient(135deg,#F59E42,#E8730C)}
54
+ .fc .n{font-size:13px;font-weight:800} .fc .c{font-size:11px;color:var(--muted)}
55
+ .fc .rar{margin-left:auto;font-size:9px;font-weight:800;color:#fff;background:#B7BECF;padding:3px 8px;border-radius:999px}
56
+ .fc.a .rar{background:linear-gradient(135deg,#7C6BF2,#8B5CF6)} .fc.b .rar{background:linear-gradient(135deg,#F59E42,#E8730C)}
57
+
58
+ /* share bar */
59
+ .share{width:min(94vw,384px); margin-top:18px}
60
+ .share .primary{width:100%; border:0; border-radius:13px; padding:14px; font:inherit; font-size:15px; font-weight:800; color:#fff;
61
+ background:linear-gradient(135deg,var(--indigo),var(--violet)); cursor:pointer; display:flex; align-items:center; justify-content:center; gap:9px}
62
+ .share .primary svg{width:18px;height:18px}
63
+ .row{display:flex; gap:9px; margin-top:10px}
64
+ .row button{flex:1; font:inherit; font-size:12.5px; font-weight:700; color:var(--ink); background:#fff; border:1px solid var(--line);
65
+ border-radius:12px; padding:11px 6px; cursor:pointer; display:flex; flex-direction:column; align-items:center; gap:6px}
66
+ .row button:hover{border-color:#CBD1E6}
67
+ .row button .ic{width:22px;height:22px;display:flex;align-items:center;justify-content:center}
68
+ .row button.x .ic{background:#161A26;color:#fff;border-radius:6px} .row button.x .ic svg{width:13px;height:13px}
69
+ .row button.line .ic{background:#06C755;color:#fff;border-radius:6px;font-weight:800;font-size:10px}
70
+ .row button .ic svg{width:18px;height:18px}
71
+ .toast{position:fixed; bottom:24px; left:50%; transform:translateX(-50%) translateY(20px); opacity:0; transition:.2s;
72
+ background:#161A26; color:#fff; font-size:13px; font-weight:600; padding:10px 18px; border-radius:999px; pointer-events:none}
73
+ .toast.show{opacity:1; transform:translateX(-50%) translateY(0)}
74
+ .note{font-size:11.5px;color:var(--muted);text-align:center;margin-top:14px;max-width:340px}
75
+ </style>
76
+ </head>
77
+ <body>
78
+ <div class="kick">
79
+ <a href="../reports/" style="color:inherit;text-decoration:none;opacity:.7">← コンソール</a>
80
+ &nbsp;·&nbsp; パスポートをシェア
81
+ </div>
82
+
83
+ <div class="card" id="card">
84
+ <div class="cover">
85
+ <span class="mk"><img src="../assets/logo-clubs-white.svg" alt="CLUBS" style="width:65px;height:17px;vertical-align:middle" /> PASSPORT</span>
86
+ <span class="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>
87
+ </div>
88
+ <div class="head">
89
+ <div class="av" id="pAv">–</div>
90
+ <div class="name" id="pName">–</div>
91
+ <div class="by" id="pBy">–</div>
92
+ <div class="titles" id="pTypes"></div>
93
+ </div>
94
+ <div class="stats" id="pStats"></div>
95
+ <div class="feat" id="pFeat"></div>
96
+ </div>
97
+
98
+ <div class="share">
99
+ <button class="primary" id="nativeShare">
100
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 12a4.5 4.5 0 1 1-.9-2.7m.9 2.7 8.4 4.9m-8.4-7.6 8.4-4.9m0 0a3 3 0 1 0 4.2-1m-4.2 1a3 3 0 0 0 4.2 3.9m-4.2 10.6a3 3 0 1 0 4.2 1"/></svg>
101
+ 実績をシェア
102
+ </button>
103
+ <div class="row">
104
+ <button class="x" id="shareX"><span class="ic"><svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.9 2H22l-7 8 8.3 12H16l-5-7-6 7H2l7.5-8.6L2 2h7.3l4.6 6.4L18.9 2Zm-1.1 18h1.7L7.3 3.8H5.5L17.8 20Z"/></svg></span>X</button>
105
+ <button class="line" id="shareLine"><span class="ic">LINE</span>LINE</button>
106
+ <button id="copyLink"><span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5 21 3m0 0h-5.25M21 3v5.25M10.5 6H6a3 3 0 0 0-3 3v9a3 3 0 0 0 3 3h9a3 3 0 0 0 3-3v-4.5"/></svg></span>リンク</button>
107
+ <button id="saveImg"><span class="ic"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg></span>画像を保存</button>
108
+ </div>
109
+ <div class="note">スクショしてそのまま貼るのもOK。リンクを貼ると横長プレビュー(OGP)が出ます。</div>
110
+ <!-- 見た人の入口。自分のAIでも、という導線を /signup/ へ。 -->
111
+ <a href="../signup/" style="display:block;text-align:center;margin-top:14px;padding:13px;border-radius:13px;font-weight:800;font-size:14px;text-decoration:none;color:#fff;background:linear-gradient(135deg,#4F6BF6,#8B5CF6)">自分のAIでもやってみる →</a>
112
+ <div class="note">つなぐのは最初の一度だけ。あとは毎朝メールに届きます。</div>
113
+ </div>
114
+
115
+ <div class="toast" id="toast">リンクをコピーしました</div>
116
+
117
+ <script type="module">
118
+ /* ■ 差し替え箇所 : パスポートのデータ
119
+ GET /api/passport から累計を読む。サーバが居なければモック表示に落ちる。
120
+ 数字はすべてログ由来(承認済みの日報の積み重ね)で、ここで作らない。 */
121
+
122
+ /* API → preview.json の順。モックは持たない。 */
123
+ const getPassport = async () => {
124
+ try {
125
+ const r = await fetch("/api/passport");
126
+ if (r.ok) return await r.json();
127
+ } catch {}
128
+ try {
129
+ const r = await fetch("../preview.json");
130
+ if (r.ok) {
131
+ const s = await r.json();
132
+ return { ...s.passport, snapshotAt: s.snapshotAt };
133
+ }
134
+ } catch {}
135
+ return null;
136
+ };
137
+
138
+ const $ = (id) => document.getElementById(id);
139
+ const esc = (t) => String(t ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
140
+
141
+ let URL_ = location.href;
142
+ let TEXT = "";
143
+
144
+ const render = (p) => {
145
+ $("pAv").textContent = [...(p.agentName ?? "A")][0];
146
+ $("pName").textContent = p.displayName;
147
+ $("pBy").textContent = `${p.displayName.split("'s")[0]} のAIエージェント`;
148
+
149
+ // 型はログから導けた事実だけ。無ければ何も出さない。
150
+ $("pTypes").innerHTML = (p.types ?? [])
151
+ .map((t) => `<span class="tt">${esc(t.emoji)} ${esc(t.name)}</span>`)
152
+ .join("");
153
+
154
+ $("pStats").innerHTML = [
155
+ `<div class="s"><div class="v">🔥${p.streak}<small>日</small></div><div class="k">連続</div></div>`,
156
+ `<div class="s"><div class="v">${p.approved}<small>本</small></div><div class="k">承認済み</div></div>`,
157
+ `<div class="s"><div class="v">${p.avgSuccess ?? "–"}<small>%</small></div><div class="k">平均成功率</div></div>`,
158
+ ].join("");
159
+
160
+ // 達成はログから条件を満たしたものだけ。レア度は付けない。
161
+ // CLUBS デザインシステム:「アートは証明を飾るが地位は発明しない(レア演出・称号なし)」
162
+ const ach = p.achievements ?? [];
163
+ $("pFeat").innerHTML = ach.length
164
+ ? `<div class="lbl">注目の達成</div>` +
165
+ ach.slice(0, 2).map((a, i) =>
166
+ `<div class="fc ${i === 0 ? "a" : "b"}"><div class="em">${esc(a.emoji)}</div>` +
167
+ `<div><div class="n">${esc(a.name)}</div><div class="c">${esc(a.note ?? "")}</div></div></div>`
168
+ ).join("")
169
+ : "";
170
+
171
+ const slug = (p.displayName ?? "agent").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
172
+ URL_ = `https://clubs.place/${slug}`;
173
+ TEXT = `うちのAIエージェント ${p.displayName} の実績。連続${p.streak}日・平均成功率${p.avgSuccess ?? "–"}%`;
174
+ };
175
+
176
+ const toast = (msg) => {
177
+ const t = $("toast");
178
+ t.textContent = msg;
179
+ t.classList.add("show");
180
+ setTimeout(() => t.classList.remove("show"), 1800);
181
+ };
182
+
183
+ $("nativeShare").onclick = async () => {
184
+ if (navigator.share) {
185
+ try { await navigator.share({ title: `${$("pName").textContent} のパスポート`, text: TEXT, url: URL_ }); } catch {}
186
+ } else {
187
+ try { await navigator.clipboard.writeText(`${TEXT} ${URL_}`); toast("コピーしました"); } catch { toast("共有できませんでした"); }
188
+ }
189
+ };
190
+ $("shareX").onclick = () =>
191
+ window.open(`https://twitter.com/intent/tweet?text=${encodeURIComponent(TEXT)}&url=${encodeURIComponent(URL_)}`, "_blank", "noopener");
192
+ $("shareLine").onclick = () =>
193
+ window.open(`https://social-plugins.line.me/lineit/share?url=${encodeURIComponent(URL_)}&text=${encodeURIComponent(TEXT)}`, "_blank", "noopener");
194
+ $("copyLink").onclick = async () => {
195
+ try { await navigator.clipboard.writeText(URL_); toast("リンクをコピーしました"); } catch { toast("コピーできませんでした"); }
196
+ };
197
+
198
+ /* ■ 差し替え箇所 : 画像を保存
199
+ 縦長カードを画像化する。canvas 化のライブラリ(html2canvas 等)は依存を増やすので入れていない。
200
+ 今はスクショで代用できる旨を伝えるだけ。実装するならサーバ側で PNG を生成するのが筋
201
+ (OGP と同じ経路を使い回せる)。 */
202
+ $("saveImg").onclick = () => toast("画像の保存は未実装です。スクリーンショットで代用してください");
203
+
204
+ const p = await getPassport();
205
+ if (p) {
206
+ render(p);
207
+ if (p.snapshotAt) {
208
+ const n = document.querySelector(".note");
209
+ if (n) n.textContent = `スナップショット(${p.snapshotAt} 時点)。` + n.textContent;
210
+ }
211
+ }
212
+ </script>
213
+ </body>
214
+ </html>
@@ -0,0 +1,177 @@
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>提案箱 — シェア素材(中身は出さない・数字だけ)</title>
7
+ <!-- 静的 meta に数字や名前を焼かない。共有プレビューに作り話が出る。本文は実データから描画する。 -->
8
+ <meta property="og:title" content="CLUBS 提案箱" />
9
+ <meta property="og:description" content="AIが毎日ひとつ提案。承認するだけ。中身は出さず、数字だけ。" />
10
+ <meta name="twitter:card" content="summary" />
11
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
13
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Noto+Sans+JP:wght@400;500;700;800;900&display=swap" rel="stylesheet" />
14
+ <style>
15
+ :root{--indigo:#4F6BF6;--violet:#8B5CF6;--ink:#161A26;--ink-2:#3A4155;--muted:#8A90A0;--line:#E7E9F2;--green:#0A8F66;--green-bg:#E7F5EF;--green-line:#BFE6D6;}
16
+ *{box-sizing:border-box}
17
+ body{margin:0;background:#E9EBF2;font-family:"Inter","Noto Sans JP",system-ui,sans-serif;color:var(--ink);line-height:1.55;padding:26px 18px 60px}
18
+ .wrap{max-width:920px;margin:0 auto}
19
+ h1{font-size:19px;margin:0 0 4px}
20
+ .lead{font-size:13px;color:#5A6072;margin:0 0 24px}
21
+ .row{display:flex;gap:24px;flex-wrap:wrap;align-items:flex-start;justify-content:center}
22
+ .col{display:flex;flex-direction:column;align-items:center}
23
+ .cap{font-size:13px;font-weight:800;margin:14px 0 3px}
24
+ .spec{font-size:11.5px;color:var(--muted);text-align:center;max-width:300px}
25
+ .spec b{color:#5A6072}
26
+
27
+ .agg{width:340px;max-width:92vw;background:#fff;border-radius:22px;overflow:hidden;box-shadow:0 16px 46px rgba(60,40,120,.20)}
28
+ .agg .cover{height:76px;background:linear-gradient(135deg,#4F6BF6,#7C5CF6 55%,#8B5CF6);position:relative}
29
+ .agg .mk{position:absolute;top:12px;left:15px;color:#fff;font-weight:800;font-size:12px;display:flex;gap:6px;align-items:center}
30
+ .agg .mk .d{width:14px;height:14px;border-radius:4px;background:rgba(255,255,255,.9)}
31
+ .agg .v{position:absolute;top:11px;right:13px;font-size:10.5px;font-weight:700;color:#fff;background:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.42);border-radius:999px;padding:3px 10px}
32
+ .agg .head{padding:0 18px}
33
+ .agg .av{width:60px;height:60px;border-radius:50%;border:4px solid #fff;background:linear-gradient(135deg,#8B5CF6,#4F6BF6);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;font-size:24px;margin-top:-30px;position:relative;z-index:2}
34
+ .agg .nm{font-size:18px;font-weight:900;margin-top:8px}
35
+ .agg .by{font-size:11px;color:var(--muted)}
36
+ .agg .big{margin:16px 18px 0;text-align:center;padding:16px 0;border:1px solid var(--line);border-radius:14px;background:#FAFBFF}
37
+ .agg .big .bignum{display:flex;align-items:center;justify-content:center;gap:13px}
38
+ .agg .boxic svg{width:50px;height:50px;display:block;filter:drop-shadow(0 2px 5px rgba(0,0,0,.14))}
39
+ .agg .big .n{font-size:38px;font-weight:900;line-height:1;color:var(--indigo)}
40
+ .agg .big .l{font-size:11px;color:var(--muted);font-weight:700;margin-top:6px}
41
+ .agg .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--line);margin:12px 18px 0;border-radius:12px;overflow:hidden;border:1px solid var(--line)}
42
+ .agg .s{background:#fff;text-align:center;padding:11px 4px}
43
+ .agg .s .n{font-size:17px;font-weight:800} .agg .s .n small{font-size:10px;color:var(--muted);font-weight:700}
44
+ .agg .s .l{font-size:9.5px;color:var(--muted);font-weight:600;margin-top:3px}
45
+ .agg .safe{font-size:10.5px;color:var(--green);font-weight:700;text-align:center;margin:12px 18px 0;background:var(--green-bg);border:1px solid var(--green-line);border-radius:10px;padding:7px}
46
+ .agg .foot{margin:14px 0 0;padding:14px 18px;border-top:1px solid var(--line);font-size:12px;font-weight:800;color:var(--indigo)}
47
+
48
+ .con{width:400px;max-width:92vw;background:#fff;border-radius:22px;overflow:hidden;box-shadow:0 16px 46px rgba(60,40,120,.20)}
49
+ .con .top{background:linear-gradient(150deg,#4F6BF6,#7C5CF6 60%,#8B5CF6);color:#fff;padding:22px 22px 20px}
50
+ .con .mk{font-size:11px;font-weight:800;opacity:.95;display:flex;gap:6px;align-items:center;margin-bottom:12px}
51
+ .con .mk .d{width:14px;height:14px;border-radius:4px;background:rgba(255,255,255,.9)}
52
+ .con h2{font-size:23px;font-weight:900;margin:0;line-height:1.3}
53
+ .con .tophead{display:flex;align-items:center;gap:13px;margin-top:2px}
54
+ .con .boxbig svg{width:54px;height:54px;flex:none;filter:drop-shadow(0 3px 6px rgba(0,0,0,.22))}
55
+ .con .sub{font-size:13px;opacity:.92;margin-top:10px}
56
+ .con .body{padding:18px}
57
+ .demo{border:1px solid var(--line);border-radius:14px;padding:14px;position:relative}
58
+ .demo .tag{position:absolute;top:-9px;left:12px;font-size:9.5px;font-weight:800;color:var(--muted);background:#fff;border:1px solid var(--line);border-radius:999px;padding:2px 8px}
59
+ .demo .dtop{display:flex;gap:10px;align-items:flex-start}
60
+ .demo .ic{width:32px;height:32px;border-radius:9px;flex:none;display:flex;align-items:center;justify-content:center;font-size:16px;background:#EEF1FE}
61
+ .demo .t{font-size:13.5px;font-weight:800} .demo .d{font-size:12px;color:var(--ink-2);margin-top:2px}
62
+ .demo .btns{display:flex;gap:7px;margin-top:11px}
63
+ .demo .btns span{font-size:11px;font-weight:700;border-radius:999px;padding:6px 13px}
64
+ .demo .ok{background:var(--indigo);color:#fff} .demo .no{background:#fff;border:1px solid var(--line);color:var(--muted)}
65
+ .vals{display:flex;flex-direction:column;gap:9px;margin-top:16px}
66
+ .val{display:flex;gap:10px;align-items:center;font-size:12.5px;color:var(--ink-2)}
67
+ .val .vi{width:26px;height:26px;border-radius:8px;flex:none;display:flex;align-items:center;justify-content:center;font-size:14px;background:#F1EEFF}
68
+ .val b{color:var(--ink)}
69
+ .con .cta{display:block;text-align:center;margin-top:18px;background:linear-gradient(135deg,var(--indigo),var(--violet));color:#fff;text-decoration:none;font-weight:800;font-size:15px;border-radius:13px;padding:14px}
70
+ .con .fn{text-align:center;font-size:11px;color:var(--muted);margin-top:10px}
71
+ </style>
72
+ </head>
73
+ <body>
74
+ <div class="wrap">
75
+ <h1>提案箱 — シェア素材(業務内容は出さない)</h1>
76
+ <p class="lead">中身=業務内容はシェアに出さない。ユーザー用は「集計だけ」、マーケ入口は「実データなしの概念+デモ」。日報→パスポートと同じ原則。</p>
77
+
78
+ <div class="row">
79
+
80
+ <!-- ① 集計シェアカード(数字だけ・実データ) -->
81
+ <div class="col">
82
+ <div class="agg">
83
+ <div class="cover"><span class="mk"><span class="d"></span>CLUBS 提案箱</span><span class="v" id="verified" hidden>✓ 検証済み</span></div>
84
+ <div class="head">
85
+ <div class="av" id="av">C</div>
86
+ <div class="nm" id="nm">—</div>
87
+ <div class="by" id="by">実行ログから作った、検証できる実績</div>
88
+ </div>
89
+ <div class="big"><div class="bignum"><span class="boxic"></span><span class="n" id="total">0</span></div><div class="l">AIからの提案(累計)</div></div>
90
+ <div class="grid">
91
+ <div class="s"><div class="n" id="approved">0<small>件</small></div><div class="l">採用</div></div>
92
+ <div class="s"><div class="n" id="rate">0<small>%</small></div><div class="l">採用率</div></div>
93
+ <div class="s"><div class="n" id="streak">🔥0<small>日</small></div><div class="l">連続</div></div>
94
+ </div>
95
+ <div class="safe">🔒 中身は出していません。数字だけ。</div>
96
+ <div class="foot" id="foot">clubs.place</div>
97
+ </div>
98
+ <div class="cap">① 集計シェアカード(ユーザー用)</div>
99
+ <div class="spec"><b>内容ゼロ・数字だけ</b>。提案の本文は一切映らない。自分の実績としてSNSに貼れる安全な形。</div>
100
+ </div>
101
+
102
+ <!-- ② マーケ入口 概念カード(実データなし・概念のみ) -->
103
+ <div class="col">
104
+ <div class="con">
105
+ <div class="top">
106
+ <div class="mk"><span class="d"></span>CLUBS 提案箱</div>
107
+ <div class="tophead">
108
+ <span class="boxbig"></span>
109
+ <h2>AIが毎日、<br>ひとつ提案。</h2>
110
+ </div>
111
+ <div class="sub">あなたの働きを見て気づいたことを、そっと。承認するまで、勝手には動きません。</div>
112
+ </div>
113
+ <div class="body">
114
+ <div class="demo">
115
+ <span class="tag">例(デモ)</span>
116
+ <div class="dtop">
117
+ <div class="ic">🔁</div>
118
+ <div><div class="t">同じ作業を、今月くり返しています</div><div class="d">ワンタップで自動化できます(実行は承認後・あなたの環境で)。</div></div>
119
+ </div>
120
+ <div class="btns"><span class="ok">承認</span><span class="no">あとで</span></div>
121
+ </div>
122
+ <div class="vals">
123
+ <div class="val"><span class="vi">🔎</span><div><b>あなたの働きを見て気づく。</b></div></div>
124
+ <div class="val"><span class="vi">👍</span><div><b>承認するだけでOK。</b></div></div>
125
+ <div class="val"><span class="vi">🔁</span><div><b>あとは自動でおまかせ。</b></div></div>
126
+ </div>
127
+ <a class="cta" href="../signup/">自分のAIでもやってみる →</a>
128
+ <div class="fn">無料ではじめられます · clubs.place</div>
129
+ </div>
130
+ </div>
131
+ <div class="cap">② マーケ入口 概念カード</div>
132
+ <div class="spec"><b>実ユーザーのデータは一切なし</b>。製品の概念+「例(デモ)」の提案だけ。新規獲得の入口・OGP に。</div>
133
+ </div>
134
+
135
+ </div>
136
+ </div>
137
+
138
+ <script type="module">
139
+ import { TWEMOJI as TW } from "../assets/twemoji.js";
140
+
141
+ /* 集計カード①は実データ(提案の stats)から描く。数字はサーバ集計をそのまま出す。
142
+ 取れなければ 0 のまま(作り話の数字は入れない)。②は実データなしの概念カード。 */
143
+ const $ = (id) => document.getElementById(id);
144
+ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
145
+
146
+ async function fill() {
147
+ let stats = null, name = null, agentName = null;
148
+ try {
149
+ const [p, s] = await Promise.all([fetch("/api/proposals"), fetch("/api/settings")]);
150
+ if (p.ok) stats = (await p.json()).stats;
151
+ if (s.ok) { const j = await s.json(); name = j.displayName; agentName = j.agentName; }
152
+ } catch { /* preview へ */ }
153
+ if (!stats) {
154
+ try {
155
+ const r = await fetch("../preview.json");
156
+ if (r.ok) { const j = await r.json(); stats = j.proposals?.stats; name = j.settings?.displayName; agentName = j.settings?.agentName; }
157
+ } catch { /* 0 のまま */ }
158
+ }
159
+
160
+ if (name) { $("nm").textContent = name; $("av").textContent = [...(agentName ?? name ?? "?")][0]; }
161
+ if (stats) {
162
+ $("total").textContent = stats.total ?? 0;
163
+ $("approved").innerHTML = `${stats.approved ?? 0}<small>件</small>`;
164
+ $("rate").innerHTML = `${stats.approvalRate ?? 0}<small>%</small>`;
165
+ $("streak").innerHTML = `🔥${stats.streakDays ?? 0}<small>日</small>`;
166
+ // ✓ は登録された日報がある=提案が実データに紐づくときだけ。0 件なら出さない。
167
+ $("verified").hidden = !(stats.total > 0);
168
+ }
169
+ }
170
+
171
+ // ballot-box を Twemoji でインライン化(外部CDN・素の絵文字は使わない)。
172
+ document.querySelectorAll(".boxic, .boxbig").forEach((el) => { el.innerHTML = TW.ballot || ""; });
173
+
174
+ await fill();
175
+ </script>
176
+ </body>
177
+ </html>