@clubs.place/cli 0.0.0-stage → 0.1.18
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.
- package/.env.example +88 -0
- package/LICENSE +21 -0
- package/PROVENANCE.json +77 -0
- package/README.md +70 -2
- package/bin/clubs-nippo +7 -0
- package/bin/clubs-nippo-web.mjs +338 -0
- package/bin/clubs-nippo.mjs +527 -0
- package/docs/events-schema.md +76 -0
- package/examples/hermes-events.sample.jsonl +8 -0
- package/package.json +44 -4
- package/src/aggregate.mjs +105 -0
- package/src/autorun.mjs +25 -0
- package/src/autorun.test.mjs +43 -0
- package/src/cards.mjs +406 -0
- package/src/collect.mjs +111 -0
- package/src/config.mjs +173 -0
- package/src/generators/claude.mjs +31 -0
- package/src/generators/cli.mjs +81 -0
- package/src/generators/codex.mjs +28 -0
- package/src/generators/copilot.mjs +21 -0
- package/src/generators/gemini.mjs +20 -0
- package/src/generators/openai-compat.mjs +79 -0
- package/src/generators/openrouter.mjs +65 -0
- package/src/history.mjs +66 -0
- package/src/ingest.mjs +78 -0
- package/src/lemma.docHash.test.mjs +37 -0
- package/src/lemma.mjs +303 -0
- package/src/mask.mjs +45 -0
- package/src/mcp.mjs +269 -0
- package/src/narrative.mjs +191 -0
- package/src/prompt.mjs +184 -0
- package/src/prompt.test.mjs +146 -0
- package/src/remote-config.mjs +72 -0
- package/src/render.mjs +467 -0
- package/src/send.mjs +36 -0
- package/src/settings.mjs +54 -0
- package/src/sources/claude-code.mjs +174 -0
- package/src/sources/codex-cli.mjs +182 -0
- package/src/sources/copilot-cli.mjs +125 -0
- package/src/sources/cowork.mjs +193 -0
- package/src/sources/events-file.mjs +120 -0
- package/src/sources/gemini-cli.mjs +128 -0
- package/src/sources/hermes.mjs +427 -0
- package/src/store.mjs +227 -0
- package/src/tasks.mjs +229 -0
- package/src/tool-name.mjs +28 -0
- package/templates/report-v3.reference.html +162 -0
- package/web/assets/logo-clubs-white.png +0 -0
- package/web/assets/logo-clubs-white.svg +7 -0
- package/web/assets/logo-clubs-white.svg.tmp +30 -0
- package/web/assets/logo-clubs.svg +30 -0
- package/web/assets/qr.js +411 -0
- package/web/assets/sidebar.js +154 -0
- package/web/assets/twemoji.js +34 -0
- package/web/cards/achievements.html +230 -0
- package/web/cards/catalog.json +217 -0
- package/web/email/hero-v3.reference.html +123 -0
- package/web/home/index.html +269 -0
- package/web/preview.json +301 -0
- package/web/qr-selftest.html +46 -0
- package/web/reports/edit.html +255 -0
- package/web/reports/edit.reference.html +257 -0
- package/web/reports/index.html +196 -0
- package/web/settings/index.html +219 -0
- package/web/share/cards-reference.html +228 -0
- package/web/share/passport.html +214 -0
- package/web/share/teianbako.html +177 -0
- package/web/signup/index.html +326 -0
- package/web/tasks/index.html +229 -0
- package/web/verify/index.html +248 -0
|
@@ -0,0 +1,326 @@
|
|
|
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>あなたのAIの日報 — CLUBS</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;900&family=Noto+Sans+JP:wght@400;500;700;800;900&family=IBM+Plex+Mono:wght@500&display=swap" rel="stylesheet" />
|
|
10
|
+
<style>
|
|
11
|
+
:root{--indigo:#4F6BF6;--violet:#8B5CF6;--ink:#161A26;--ink-2:#3A4155;--muted:#8A90A0;--line:#E7E9F2;--green:#0A8F66;--green-bg:#E7F5EF;--green-line:#BFE6D6;}
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
body{margin:0;min-height:100vh;background:radial-gradient(1000px 500px at 50% -8%,#EDE9FB,#E3E6F2);
|
|
14
|
+
font-family:"Inter","Noto Sans JP",system-ui,sans-serif;color:var(--ink);line-height:1.6;
|
|
15
|
+
display:flex;flex-direction:column;align-items:center;padding:22px 14px 44px}
|
|
16
|
+
.mono{font-family:"IBM Plex Mono",monospace}
|
|
17
|
+
.brand{display:flex;align-items:center;justify-content:center;margin-bottom:14px}
|
|
18
|
+
.brand img{height:22px;width:auto;display:block}
|
|
19
|
+
|
|
20
|
+
.card{width:420px;max-width:100%;background:#fff;border-radius:22px;overflow:hidden;box-shadow:0 20px 60px rgba(60,40,120,.20);border:1px solid #E5E1F0}
|
|
21
|
+
.screen{padding:16px 22px 24px;min-height:520px;display:flex;flex-direction:column}
|
|
22
|
+
|
|
23
|
+
.steps{display:flex;gap:6px;justify-content:center;padding:8px 0 16px}
|
|
24
|
+
.steps i{width:26px;height:4px;border-radius:99px;background:#E3E1EE}
|
|
25
|
+
.steps i.on{background:linear-gradient(90deg,var(--indigo),var(--violet))}
|
|
26
|
+
.back{font-size:13px;color:var(--muted);background:none;border:0;text-align:left;font-weight:600;align-self:flex-start;cursor:pointer;margin-bottom:6px;padding:0}
|
|
27
|
+
|
|
28
|
+
.kicker{font-size:12px;font-weight:700;color:var(--indigo);margin-bottom:4px}
|
|
29
|
+
h2{font-size:22px;font-weight:900;margin:0 0 6px;letter-spacing:.01em;text-wrap:balance}
|
|
30
|
+
.lead{font-size:13.5px;color:var(--ink-2);margin:0 0 18px}
|
|
31
|
+
|
|
32
|
+
.btn{display:flex;align-items:center;justify-content:center;gap:9px;width:100%;border:0;border-radius:13px;padding:14px;font:inherit;font-size:15px;font-weight:800;cursor:pointer;text-decoration:none}
|
|
33
|
+
.btn.primary{color:#fff;background:linear-gradient(135deg,var(--indigo),var(--violet))}
|
|
34
|
+
.btn.ghost{background:#fff;color:var(--ink);border:1.5px solid var(--line)}
|
|
35
|
+
.btn.dark{background:#161A26;color:#fff}
|
|
36
|
+
.btn svg{width:18px;height:18px}
|
|
37
|
+
.btn + .btn{margin-top:10px}
|
|
38
|
+
.rec{margin-left:6px;font-size:10px;font-weight:800;background:rgba(255,255,255,.22);border:1px solid rgba(255,255,255,.4);border-radius:999px;padding:2px 7px}
|
|
39
|
+
.note{font-size:11.5px;color:var(--muted);text-align:center;margin-top:14px}
|
|
40
|
+
.grow{flex:1}
|
|
41
|
+
|
|
42
|
+
.scard{border:1px solid var(--line);border-radius:18px;overflow:hidden;margin-bottom:16px}
|
|
43
|
+
.scover{height:66px;background:linear-gradient(135deg,#4F6BF6,#7C5CF6 55%,#8B5CF6);position:relative}
|
|
44
|
+
.scover .v{position:absolute;top:10px;right:12px;font-size:10px;font-weight:700;color:#fff;background:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.4);border-radius:999px;padding:3px 9px}
|
|
45
|
+
.shead{padding:0 16px 14px}
|
|
46
|
+
.sav{width:56px;height:56px;border-radius:50%;border:3px solid #fff;background:linear-gradient(135deg,#8B5CF6,#4F6BF6);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;font-size:22px;margin-top:-28px;position:relative;z-index:2}
|
|
47
|
+
.sname{font-size:18px;font-weight:900;margin-top:8px}
|
|
48
|
+
.sby{font-size:11px;color:var(--muted)}
|
|
49
|
+
.sstats{display:flex;gap:14px;margin-top:12px;border-top:1px solid var(--line);padding-top:11px}
|
|
50
|
+
.sstats .n{font-weight:900;font-size:16px} .sstats .n .fl{color:#E8730C}
|
|
51
|
+
.sstats .l{font-size:9px;color:var(--muted);font-weight:600}
|
|
52
|
+
.fromthreads{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:700;color:var(--ink-2);background:#F3F0FF;border:1px solid #E2D9FB;border-radius:999px;padding:4px 11px;margin-bottom:12px;align-self:flex-start}
|
|
53
|
+
.vrow{display:flex;gap:6px;margin:2px 0 16px;flex-wrap:wrap}
|
|
54
|
+
.vchip{font-size:11px;font-weight:700;color:var(--ink-2);background:#F6F7FB;border:1px solid var(--line);border-radius:999px;padding:5px 10px}
|
|
55
|
+
|
|
56
|
+
.aicard{display:flex;align-items:center;gap:12px;border:1.5px solid var(--line);border-radius:14px;padding:14px;margin-bottom:10px;cursor:pointer;background:#fff;width:100%;text-align:left;font:inherit}
|
|
57
|
+
.aicard.sel{border-color:var(--indigo);box-shadow:0 0 0 3px rgba(79,107,246,.12)}
|
|
58
|
+
.aicard .ic{width:40px;height:40px;border-radius:11px;flex:none;display:flex;align-items:center;justify-content:center;font-size:20px;background:#EEF1FE}
|
|
59
|
+
.aicard .t{font-size:14.5px;font-weight:800} .aicard .d{font-size:11.5px;color:var(--muted)}
|
|
60
|
+
.aicard .rc{margin-left:auto;font-size:9px;font-weight:800;color:#fff;background:linear-gradient(135deg,var(--indigo),var(--violet));border-radius:999px;padding:2px 8px}
|
|
61
|
+
.adv{display:block;text-align:center;font-size:12.5px;font-weight:700;color:var(--indigo);background:none;border:0;cursor:pointer;margin-top:4px;width:100%;font-family:inherit}
|
|
62
|
+
|
|
63
|
+
/* advanced: events-file */
|
|
64
|
+
.soon{margin-left:auto;font-size:9px;font-weight:800;color:#8A6520;background:#F7EFDD;border:1px solid #E6D6BF;border-radius:999px;padding:2px 8px}
|
|
65
|
+
.steplist{margin:2px 0 0;padding-left:0;list-style:none}
|
|
66
|
+
.steplist li{position:relative;padding:9px 0 9px 30px;font-size:13px;border-bottom:1px solid var(--line)}
|
|
67
|
+
.steplist li:last-child{border-bottom:0}
|
|
68
|
+
.steplist .no{position:absolute;left:0;top:8px;width:21px;height:21px;border-radius:50%;background:#EEF1FE;color:var(--indigo);font-size:11px;font-weight:800;display:flex;align-items:center;justify-content:center}
|
|
69
|
+
.code{background:#161A26;color:#E8ECF7;border-radius:11px;padding:12px 13px;font-family:"IBM Plex Mono",monospace;font-size:11px;line-height:1.7;overflow-x:auto;white-space:pre;margin:12px 0}
|
|
70
|
+
.mcprow{display:flex;align-items:center;gap:10px;background:#F6F7FB;border:1px solid var(--line);border-radius:12px;padding:12px;margin-top:6px;font-size:12.5px;color:var(--ink-2)}
|
|
71
|
+
.mcprow .ic{width:30px;height:30px;border-radius:9px;background:#EEF1FE;display:flex;align-items:center;justify-content:center;flex:none}
|
|
72
|
+
|
|
73
|
+
.consent2{border:1px solid var(--line);border-radius:16px;padding:22px 18px 18px;text-align:center;box-shadow:0 2px 10px rgba(20,26,38,.05)}
|
|
74
|
+
.conn{display:flex;align-items:center;justify-content:center;gap:14px;margin-bottom:14px}
|
|
75
|
+
.conn .b{width:48px;height:48px;border-radius:13px;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:900;font-size:18px}
|
|
76
|
+
.conn .clubs{background:linear-gradient(135deg,var(--indigo),var(--violet))}
|
|
77
|
+
.conn .ai{background:linear-gradient(160deg,#66B9D8,#3E93B5)}
|
|
78
|
+
.conn .link{color:#B7BECF;font-size:20px}
|
|
79
|
+
.ctitle{font-size:16.5px;font-weight:800} .ctitle b{color:var(--indigo)}
|
|
80
|
+
.csub{font-size:12px;color:var(--muted);margin-top:5px}
|
|
81
|
+
.plist{text-align:left;margin-top:16px}
|
|
82
|
+
.pl{display:flex;align-items:center;gap:11px;font-size:13.5px;padding:9px 0;border-bottom:1px solid var(--line)}
|
|
83
|
+
.pl:last-child{border-bottom:0}
|
|
84
|
+
.pc{width:20px;height:20px;border-radius:50%;background:var(--green-bg);color:var(--green);display:flex;align-items:center;justify-content:center;flex:none}
|
|
85
|
+
.pc svg{width:13px;height:13px}
|
|
86
|
+
.pexcl{font-size:11.5px;color:var(--ink-2);text-align:left;margin-top:14px;background:#F6F7FB;border:1px solid var(--line);border-radius:10px;padding:10px 12px}
|
|
87
|
+
.pexcl b{color:var(--green)}
|
|
88
|
+
|
|
89
|
+
.seal{width:72px;height:72px;border-radius:50%;background:var(--green-bg);border:2px solid var(--green-line);display:flex;align-items:center;justify-content:center;color:var(--green);margin:18px auto 14px}
|
|
90
|
+
.seal svg{width:36px;height:36px}
|
|
91
|
+
</style>
|
|
92
|
+
</head>
|
|
93
|
+
<body>
|
|
94
|
+
<div class="brand"><img src="../assets/logo-clubs.svg" alt="CLUBS" /></div>
|
|
95
|
+
<div class="card">
|
|
96
|
+
<div class="steps" id="steps"></div>
|
|
97
|
+
<div class="screen" id="screen"></div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<script type="module">
|
|
101
|
+
/* ユーザー登録導線(Threads 流入 → Seal 登録 → AI 連携)。
|
|
102
|
+
*
|
|
103
|
+
* 認証は Seal に委譲する方針で、このページは資格情報を一切受け取らない
|
|
104
|
+
* (パスワード欄もトークン入力も持たない)。Seal・アカウント・連携トークンの
|
|
105
|
+
* バックエンドは未実装のため、ここは導線の UI。各ボタンは次の画面へ進める。
|
|
106
|
+
* バックエンドが入ったら、その地点で Seal 呼び出し/トークン発行に差し替える。
|
|
107
|
+
*
|
|
108
|
+
* 数字は作らない: 入口の実績カードは preview.json(実データのスナップショット)から
|
|
109
|
+
* 引く。取れなければ数字を伏せる。見本の作り話(34日・128本・99%)は出さない。
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
const chk = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>';
|
|
113
|
+
const TOTAL = 5; // 主経路のステップ数(入口=0 … 完了=4)
|
|
114
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
115
|
+
|
|
116
|
+
/* アプリ内ブラウザ(Threads / Instagram / LINE 等の WKWebView)検出。
|
|
117
|
+
* これらは既存セッションが無く、マジックリンクや GitHub OAuth が別タブで分断されがち。
|
|
118
|
+
* → メール(OTP)を前面に、パスキー/GitHub は「ブラウザで開く」に降格する。 */
|
|
119
|
+
const isInApp = (() => {
|
|
120
|
+
const ua = navigator.userAgent || "";
|
|
121
|
+
if (/Instagram|Threads|Barcelona|FBAN|FBAV|FB_IAB|Line\/|LINE|TikTok|Twitter|KAKAOTALK/i.test(ua)) return true;
|
|
122
|
+
// iOS の非 Safari WKWebView(Safari を名乗らない WebKit)
|
|
123
|
+
if (/(iPhone|iPod|iPad)/i.test(ua) && /AppleWebKit/i.test(ua) && !/Safari/i.test(ua)) return true;
|
|
124
|
+
return false;
|
|
125
|
+
})();
|
|
126
|
+
|
|
127
|
+
const openInBrowser = () => {
|
|
128
|
+
// webview から外部ブラウザへ抜ける最善努力。確実な手段は環境依存なので、
|
|
129
|
+
// 開けない場合に備えて同タブのメール導線を常に残す。
|
|
130
|
+
try { window.open(location.href, "_system"); } catch { /* noop */ }
|
|
131
|
+
try { window.open(location.href, "_blank"); } catch { /* noop */ }
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
let step = 0;
|
|
135
|
+
const $ = (id) => document.getElementById(id);
|
|
136
|
+
function renderSteps() {
|
|
137
|
+
$("steps").innerHTML = Array.from({ length: TOTAL - 1 }, (_, i) => `<i class="${i < step ? "on" : ""}"></i>`).join("");
|
|
138
|
+
}
|
|
139
|
+
function nav(html, s) {
|
|
140
|
+
if (typeof s === "number") step = s;
|
|
141
|
+
$("screen").innerHTML = html;
|
|
142
|
+
renderSteps();
|
|
143
|
+
$("screen").querySelectorAll("[data-go]").forEach((el) =>
|
|
144
|
+
el.addEventListener("click", () => go(el.dataset.go)),
|
|
145
|
+
);
|
|
146
|
+
$("screen").querySelectorAll("[data-open]").forEach((el) =>
|
|
147
|
+
el.addEventListener("click", openInBrowser),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
function go(name) { SCREENS[name](); }
|
|
151
|
+
|
|
152
|
+
/* 入口カードの実データ。preview.json → passport(累計)から。無ければ数字を伏せる。 */
|
|
153
|
+
let ENTRY = null;
|
|
154
|
+
async function loadEntry() {
|
|
155
|
+
try {
|
|
156
|
+
const r = await fetch("../preview.json");
|
|
157
|
+
if (r.ok) {
|
|
158
|
+
const s = await r.json();
|
|
159
|
+
const p = s.passport ?? {};
|
|
160
|
+
ENTRY = {
|
|
161
|
+
name: p.displayName ?? null,
|
|
162
|
+
initial: [...(p.agentName ?? "?")][0],
|
|
163
|
+
streak: p.streak ?? null,
|
|
164
|
+
approved: p.approved ?? null,
|
|
165
|
+
avgSuccess: p.avgSuccess ?? null,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
} catch { /* 数字を伏せる */ }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const backBtn = (name) => `<button class="back" data-go="${name}">← もどる</button>`;
|
|
172
|
+
|
|
173
|
+
const SCREENS = {
|
|
174
|
+
// 0: 入口(Threads のシェア画面から流入)
|
|
175
|
+
0() {
|
|
176
|
+
const e = ENTRY;
|
|
177
|
+
const stat = (n, unit, l) =>
|
|
178
|
+
n == null ? "" : `<div><div class="n">${unit === "🔥" ? '<span class="fl">🔥</span>' : ""}${n}<span style="font-size:10px">${unit === "🔥" ? "日" : unit}</span></div><div class="l">${l}</div></div>`;
|
|
179
|
+
const stats = e ? [stat(e.streak, "🔥", "連続"), stat(e.approved, "本", "承認済み"), stat(e.avgSuccess, "%", "平均成功率")].filter(Boolean).join("") : "";
|
|
180
|
+
nav(`
|
|
181
|
+
<span class="fromthreads">👀 シェアで見た実績</span>
|
|
182
|
+
<div class="scard">
|
|
183
|
+
<div class="scover"><span class="v">✓ 検証済み</span></div>
|
|
184
|
+
<div class="shead">
|
|
185
|
+
<div class="sav">${e?.initial ? esc(e.initial) : "C"}</div>
|
|
186
|
+
<div class="sname">${e?.name ? esc(e.name) : "あなたのAI"}</div>
|
|
187
|
+
<div class="sby">実行ログから作った、検証できる実績</div>
|
|
188
|
+
${stats ? `<div class="sstats">${stats}</div>` : ""}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<h2>あなたのAIの日報が、<br>毎日届く。</h2>
|
|
192
|
+
<p class="lead">つなぐのは最初の一度だけ。あとは毎朝、メールに届きます。作業記録に、業務報告にも。</p>
|
|
193
|
+
<div class="vrow"><span class="vchip">⚡ 数秒でつなぐ</span><span class="vchip">🤖 あとは自動</span><span class="vchip">🔒 データは外に出ない</span></div>
|
|
194
|
+
<div class="grow"></div>
|
|
195
|
+
<button class="btn primary" data-go="1">自分のAIでもやってみる →</button>
|
|
196
|
+
<div class="note">無料ではじめられます。</div>
|
|
197
|
+
`, 0);
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
// 1: サインアップ(Seal 委譲・パスワードレス)
|
|
201
|
+
1() {
|
|
202
|
+
// メール(OTP)は常に先頭・おすすめ。アプリ内ブラウザではパスキー/GitHub を
|
|
203
|
+
// 「ブラウザで開く」に降格する(同タブで完結するメールが最も確実なため)。
|
|
204
|
+
const email = `
|
|
205
|
+
<button class="btn primary" data-go="2">
|
|
206
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/></svg>
|
|
207
|
+
メールで続ける <span class="rec">おすすめ</span>
|
|
208
|
+
</button>`;
|
|
209
|
+
const passkey = isInApp
|
|
210
|
+
? `<button class="btn ghost" data-open>
|
|
211
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75A2.25 2.25 0 0 0 1.5 12.75v6.75a2.25 2.25 0 0 0 2.25 2.25Z"/></svg>
|
|
212
|
+
パスキーで登録(ブラウザで開く)
|
|
213
|
+
</button>`
|
|
214
|
+
: `<button class="btn ghost" data-go="2">
|
|
215
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.03 5.912 3 3 0 0 0-2.83.999L10 17.5H8v2H6v2H3.5a1 1 0 0 1-1-1v-1.914a1 1 0 0 1 .293-.707l6.348-6.348a1 1 0 0 0 .21-1.117A6 6 0 1 1 21.75 8.25Z"/></svg>
|
|
216
|
+
パスキーで登録
|
|
217
|
+
</button>`;
|
|
218
|
+
const github = isInApp
|
|
219
|
+
? `<button class="btn dark" data-open>
|
|
220
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.09.68-.22.68-.49v-1.7c-2.78.62-3.37-1.37-3.37-1.37-.46-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.36 1.12 2.94.86.09-.67.35-1.12.63-1.38-2.22-.26-4.56-1.14-4.56-5.05 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05a9.35 9.35 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.92-2.34 4.78-4.57 5.04.36.32.68.94.68 1.9v2.82c0 .27.18.59.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z"/></svg>
|
|
221
|
+
GitHub で続ける(ブラウザで開く)
|
|
222
|
+
</button>`
|
|
223
|
+
: `<button class="btn dark" data-go="2">
|
|
224
|
+
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.58 2 12.25c0 4.53 2.87 8.37 6.84 9.73.5.09.68-.22.68-.49v-1.7c-2.78.62-3.37-1.37-3.37-1.37-.46-1.18-1.11-1.5-1.11-1.5-.91-.63.07-.62.07-.62 1 .07 1.53 1.06 1.53 1.06.9 1.57 2.36 1.12 2.94.86.09-.67.35-1.12.63-1.38-2.22-.26-4.56-1.14-4.56-5.05 0-1.12.39-2.03 1.03-2.75-.1-.26-.45-1.3.1-2.71 0 0 .84-.28 2.75 1.05a9.35 9.35 0 0 1 5 0c1.91-1.33 2.75-1.05 2.75-1.05.55 1.41.2 2.45.1 2.71.64.72 1.03 1.63 1.03 2.75 0 3.92-2.34 4.78-4.57 5.04.36.32.68.94.68 1.9v2.82c0 .27.18.59.69.49A10.26 10.26 0 0 0 22 12.25C22 6.58 17.52 2 12 2Z"/></svg>
|
|
225
|
+
GitHub で続ける
|
|
226
|
+
</button>`;
|
|
227
|
+
nav(`
|
|
228
|
+
${backBtn("0")}
|
|
229
|
+
<span class="kicker">はじめる(無料)</span>
|
|
230
|
+
<h2>数秒ではじめる</h2>
|
|
231
|
+
<p class="lead">パスワードなし。あなたのAIのパスポートが作られます。</p>
|
|
232
|
+
${email}
|
|
233
|
+
${passkey}
|
|
234
|
+
${github}
|
|
235
|
+
<div class="note">${isInApp ? "Threads などのアプリ内ブラウザでは、メールがスムーズです。" : "登録はメールが最もスムーズです。"}</div>
|
|
236
|
+
`, 1);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// 2: AI 連携(選ぶ)
|
|
240
|
+
2() {
|
|
241
|
+
nav(`
|
|
242
|
+
${backBtn("1")}
|
|
243
|
+
<span class="kicker">あとひと息</span>
|
|
244
|
+
<h2>AIをつなぐ</h2>
|
|
245
|
+
<p class="lead">選ぶだけ。難しい設定はありません。あとから増やせます。</p>
|
|
246
|
+
<button class="aicard sel" data-go="3">
|
|
247
|
+
<div class="ic">🧑💻</div><div><div class="t">Claude Code</div><div class="d">手元の Claude Code の作業を日報に</div></div>
|
|
248
|
+
<span class="rc">おすすめ</span>
|
|
249
|
+
</button>
|
|
250
|
+
<button class="aicard" data-go="3">
|
|
251
|
+
<div class="ic">🖥️</div><div><div class="t">Cowork</div><div class="d">Claude デスクトップの Cowork を日報に</div></div>
|
|
252
|
+
</button>
|
|
253
|
+
<button class="adv" data-go="adv">その他のAIを繋ぐ →</button>
|
|
254
|
+
<div class="grow"></div>
|
|
255
|
+
<button class="btn primary" data-go="3">Claude Code をつなぐ →</button>
|
|
256
|
+
`, 2);
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// adv: その他のAI(events-file 案内・MCP は近日)
|
|
260
|
+
adv() {
|
|
261
|
+
nav(`
|
|
262
|
+
${backBtn("2")}
|
|
263
|
+
<span class="kicker">その他のAI</span>
|
|
264
|
+
<h2>他のAIをつなぐ</h2>
|
|
265
|
+
<p class="lead">Claude Code・Cowork 以外のAI(Telegram で動く Hermes など)は、実行記録をファイルに書き出して日報にできます。</p>
|
|
266
|
+
<ul class="steplist">
|
|
267
|
+
<li><span class="no">1</span>AI に、1行1イベントの記録ファイル(JSONL)を書き出させる</li>
|
|
268
|
+
<li><span class="no">2</span>そのファイルの場所を日報ツールに教える</li>
|
|
269
|
+
<li><span class="no">3</span>あとは Claude Code と同じ。毎朝メールに届く</li>
|
|
270
|
+
</ul>
|
|
271
|
+
<div class="code">{"ts":"2026-07-13T09:12:00+09:00",
|
|
272
|
+
"tool":"telegram.reply",
|
|
273
|
+
"ok":true,
|
|
274
|
+
"summary":"問い合わせに返信"}</div>
|
|
275
|
+
<div class="mcprow">
|
|
276
|
+
<span class="ic">🔌</span>
|
|
277
|
+
<div><b>MCP でワンクリック接続</b><br>対応アプリからそのまま繋げるようにします。</div>
|
|
278
|
+
<span class="soon">近日</span>
|
|
279
|
+
</div>
|
|
280
|
+
<div class="grow"></div>
|
|
281
|
+
<button class="btn ghost" data-go="2">← AIの選択にもどる</button>
|
|
282
|
+
<div class="note">書き出しの詳しい形式は、接続後の設定画面で確認できます。</div>
|
|
283
|
+
`, 2);
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
// 3: 接続を許可(OAuth 風)
|
|
287
|
+
3() {
|
|
288
|
+
nav(`
|
|
289
|
+
${backBtn("2")}
|
|
290
|
+
<div class="consent2">
|
|
291
|
+
<div class="conn"><span class="b clubs">C</span><span class="link">→</span><span class="b ai">C</span></div>
|
|
292
|
+
<div class="ctitle">CLUBS が <b>Claude</b> に接続します</div>
|
|
293
|
+
<div class="csub">次の情報が CLUBS に共有されます</div>
|
|
294
|
+
<div class="plist">
|
|
295
|
+
<div class="pl"><span class="pc">${chk}</span>承認した日報の本文</div>
|
|
296
|
+
<div class="pl"><span class="pc">${chk}</span>集計値(実行回数・成功率など)</div>
|
|
297
|
+
<div class="pl"><span class="pc">${chk}</span>署名(改ざん検知用)</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="pexcl"><b>作業の中身にはアクセスしません。</b>コマンドやファイルの中身・出力は共有されず、データは外に出ません。</div>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="grow"></div>
|
|
302
|
+
<button class="btn primary" data-go="4">許可</button>
|
|
303
|
+
<button class="btn ghost" data-go="2">キャンセル</button>
|
|
304
|
+
<div class="note">「許可」で接続します。接続はいつでも解除できます。</div>
|
|
305
|
+
`, 3);
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
// 4: 完了
|
|
309
|
+
4() {
|
|
310
|
+
nav(`
|
|
311
|
+
<div class="seal"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg></div>
|
|
312
|
+
<h2 style="text-align:center">接続できました</h2>
|
|
313
|
+
<p class="lead" style="text-align:center">明日の朝、最初の日報が届きます。<br>今日ぶんを今すぐ見ることもできます。</p>
|
|
314
|
+
<div class="grow"></div>
|
|
315
|
+
<a class="btn primary" href="../home/">ホームへ</a>
|
|
316
|
+
<a class="btn ghost" href="../reports/">今日の日報をプレビュー</a>
|
|
317
|
+
<div class="note">通知は毎朝の日報だけ。設定でいつでも変えられます。</div>
|
|
318
|
+
`, 4);
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
await loadEntry();
|
|
323
|
+
go("0");
|
|
324
|
+
</script>
|
|
325
|
+
</body>
|
|
326
|
+
</html>
|
|
@@ -0,0 +1,229 @@
|
|
|
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>AIから(タスク・提案箱) — 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
|
+
--indigo:#4F6BF6; --violet:#8B5CF6; --green:#0A8F66; --gold:#B7862B;
|
|
15
|
+
--amber-ink:#9A6A00; --amber-bg:#FFF6E5; --amber-line:#F5E4BC;
|
|
16
|
+
--red-ink:#B4453B; --red-bg:#FDECEA; --red-line:#F3C9C4;
|
|
17
|
+
--gray-bg:#F3F4F6; --radius:14px;
|
|
18
|
+
}
|
|
19
|
+
*{box-sizing:border-box}
|
|
20
|
+
body{margin:0;background:var(--bg);color:var(--ink);font-family:"Inter","Noto Sans JP",system-ui,sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased;display:flex;min-height:100vh}
|
|
21
|
+
.mono{font-family:"IBM Plex Mono",monospace}
|
|
22
|
+
.main{flex:1;min-width:0;display:flex;flex-direction:column}
|
|
23
|
+
.topbar{height:60px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:16px;padding:0 26px;position:sticky;top:0;z-index:5}
|
|
24
|
+
.tab{font-size:14px;font-weight:700;color:var(--muted);cursor:pointer;padding:6px 2px;border-bottom:2px solid transparent;background:none;border-top:0;border-left:0;border-right:0;font-family:inherit}
|
|
25
|
+
.tab.on{color:var(--ink);border-color:var(--indigo)}
|
|
26
|
+
.tab .c{font-size:11px;font-weight:800;background:var(--gray-bg);color:var(--muted);border-radius:999px;padding:1px 7px;margin-left:5px}
|
|
27
|
+
.content{padding:24px 26px 44px;max-width:840px;width:100%;margin:0 auto}
|
|
28
|
+
.intro{font-size:12.5px;color:var(--muted);background:#fff;border:1px solid var(--line);border-radius:12px;padding:11px 14px;margin-bottom:18px}
|
|
29
|
+
.intro b{color:var(--ink-2)}
|
|
30
|
+
.empty{font-size:13px;color:var(--muted);background:#fff;border:1px dashed var(--line);border-radius:12px;padding:22px 16px;text-align:center}
|
|
31
|
+
|
|
32
|
+
.item{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:15px 17px;margin-bottom:11px}
|
|
33
|
+
.item .top{display:flex;align-items:flex-start;gap:11px}
|
|
34
|
+
.item .ico{width:34px;height:34px;border-radius:9px;flex:none;display:flex;align-items:center;justify-content:center;font-size:17px;background:#EEF1FE}
|
|
35
|
+
.item .t{font-size:14.5px;font-weight:800;line-height:1.4}
|
|
36
|
+
.item .d{font-size:12.5px;color:var(--ink-2);margin-top:3px}
|
|
37
|
+
.item .meta{display:flex;align-items:center;gap:8px;margin-top:9px;flex-wrap:wrap;font-size:11px;color:var(--muted)}
|
|
38
|
+
.chip{font-size:10.5px;font-weight:700;border-radius:999px;padding:3px 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid}
|
|
39
|
+
.chip.open{color:var(--amber-ink);background:var(--amber-bg);border-color:var(--amber-line)}
|
|
40
|
+
.chip.done,.chip.approved{color:var(--green);background:#E7F5EF;border-color:#BFE6D6}
|
|
41
|
+
.chip.rejected{color:var(--red-ink);background:var(--red-bg);border-color:var(--red-line)}
|
|
42
|
+
.chip.hold{color:var(--amber-ink);background:var(--amber-bg);border-color:var(--amber-line)}
|
|
43
|
+
.chip.carry{color:#6D53E0;background:#F1EEFF;border-color:#E1D9FB}
|
|
44
|
+
.src{font-family:"IBM Plex Mono",monospace;font-size:10.5px;color:var(--muted)}
|
|
45
|
+
.assignee{margin-left:auto;display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted)}
|
|
46
|
+
.pro{font-size:8px;font-weight:800;background:var(--gold);color:#2A2008;border-radius:999px;padding:1px 6px}
|
|
47
|
+
.acts{display:flex;gap:7px;margin-top:11px;flex-wrap:wrap}
|
|
48
|
+
.btn{font:inherit;font-size:12px;font-weight:700;border-radius:999px;padding:7px 13px;cursor:pointer;border:1px solid var(--line);background:#fff;color:var(--ink-2)}
|
|
49
|
+
.btn.ok{background:var(--indigo);color:#fff;border-color:transparent}
|
|
50
|
+
.btn.no{color:var(--red-ink);border-color:var(--red-line)}
|
|
51
|
+
.doneline .t{text-decoration:line-through;color:var(--muted)}
|
|
52
|
+
.foot-note{font-size:11.5px;color:var(--muted);margin-top:8px;background:#fff;border:1px dashed #D7DDF3;border-radius:12px;padding:11px 14px}
|
|
53
|
+
.foot-note b{color:var(--ink-2)}
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<aside></aside>
|
|
58
|
+
<div class="main">
|
|
59
|
+
<div class="topbar">
|
|
60
|
+
<button class="tab on" id="tabTasks">タスク<span class="c" id="cTasks">0</span></button>
|
|
61
|
+
<button class="tab" id="tabProps">提案箱<span class="c" id="cProps">0</span></button>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="content" id="content"></div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<script type="module">
|
|
67
|
+
import { mountSidebar } from "../assets/sidebar.js";
|
|
68
|
+
mountSidebar("tasks");
|
|
69
|
+
|
|
70
|
+
/* 日報の「申し送り」「気づき・提案」を、流さずにコンソールへ蓄積したビュー。
|
|
71
|
+
数字・件数はサーバの確定データ由来(盛れない)。✓ は「日報が改ざんされていない」
|
|
72
|
+
ことの証明であって、タスク/提案の当否は保証しない。 */
|
|
73
|
+
|
|
74
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
75
|
+
const md = (d) => (d ? d.slice(5).replace("-", "/") : "");
|
|
76
|
+
|
|
77
|
+
let TASKS = [];
|
|
78
|
+
let PROPS = [];
|
|
79
|
+
let LIVE = true;
|
|
80
|
+
|
|
81
|
+
/* ライブ API → 無ければ preview.json(実データのスナップショット)。作り話は持たない。 */
|
|
82
|
+
async function load() {
|
|
83
|
+
try {
|
|
84
|
+
const [t, p] = await Promise.all([fetch("/api/tasks"), fetch("/api/proposals")]);
|
|
85
|
+
if (t.ok && p.ok) {
|
|
86
|
+
TASKS = (await t.json()).items ?? [];
|
|
87
|
+
PROPS = (await p.json()).items ?? [];
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
} catch { /* 次へ */ }
|
|
91
|
+
try {
|
|
92
|
+
const r = await fetch("../preview.json");
|
|
93
|
+
if (r.ok) {
|
|
94
|
+
const s = await r.json();
|
|
95
|
+
TASKS = s.tasks ?? [];
|
|
96
|
+
PROPS = s.proposals?.items ?? [];
|
|
97
|
+
LIVE = false;
|
|
98
|
+
if (s.snapshotAt) banner(s.snapshotAt);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
} catch { /* 空 */ }
|
|
102
|
+
}
|
|
103
|
+
function banner(at) {
|
|
104
|
+
const el = document.createElement("div");
|
|
105
|
+
el.style.cssText = "position:fixed;top:0;left:0;right:0;z-index:99;background:#F7EFDD;color:#8A6520;font-size:12px;font-weight:700;padding:8px 14px;text-align:center;border-bottom:1px solid #E6D6BF";
|
|
106
|
+
el.textContent = `スナップショット(${at} 時点)。サーバに繋がっていないため、状態の変更はできません。`;
|
|
107
|
+
document.body.prepend(el);
|
|
108
|
+
document.body.style.paddingTop = "34px";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const TASK_LABEL = { open: { cls: "open", t: "未着手" }, done: { cls: "done", t: "完了" } };
|
|
112
|
+
const PROP_LABEL = { open: { cls: "open", t: "未対応" }, approved: { cls: "approved", t: "承認" }, rejected: { cls: "rejected", t: "却下" }, hold: { cls: "hold", t: "保留" } };
|
|
113
|
+
|
|
114
|
+
const carryBadge = (n) => (n >= 2 ? `<span class="chip carry">繰越 ${n}日</span>` : "");
|
|
115
|
+
|
|
116
|
+
function renderTasks() {
|
|
117
|
+
const open = TASKS.filter((t) => t.status !== "done").length;
|
|
118
|
+
document.getElementById("cTasks").textContent = open;
|
|
119
|
+
const body = TASKS.length
|
|
120
|
+
? TASKS.map((t) => {
|
|
121
|
+
const L = TASK_LABEL[t.status] ?? TASK_LABEL.open;
|
|
122
|
+
const done = t.status === "done";
|
|
123
|
+
return `<div class="item ${done ? "doneline" : ""}">
|
|
124
|
+
<div class="top">
|
|
125
|
+
<div class="ico">${done ? "✅" : "📌"}</div>
|
|
126
|
+
<div style="flex:1">
|
|
127
|
+
<div class="t">${esc(t.text)}</div>
|
|
128
|
+
<div class="meta">
|
|
129
|
+
<span class="chip ${L.cls}">${L.t}</span>
|
|
130
|
+
${carryBadge(t.carryDays ?? 1)}
|
|
131
|
+
<span class="src">${md(t.sourceDate)} の申し送り</span>
|
|
132
|
+
<span class="assignee">担当 <span class="pro">PRO</span></span>
|
|
133
|
+
</div>
|
|
134
|
+
${LIVE ? `<div class="acts"><button class="btn ${done ? "" : "ok"}" data-task="${esc(t.id)}" data-status="${done ? "open" : "done"}">${done ? "未着手に戻す" : "完了にする"}</button></div>` : ""}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>`;
|
|
138
|
+
}).join("")
|
|
139
|
+
: `<div class="empty">まだタスクはありません。日報を<b>記録</b>すると、申し送りがここに溜まります。</div>`;
|
|
140
|
+
|
|
141
|
+
document.getElementById("content").innerHTML = `
|
|
142
|
+
<div class="intro">日報の <b>申し送り</b> から自動で溜まります。AIのやり残しが流れず、繰り越されます。<b>状態・担当</b>はチームで共有できます(PRO)。</div>
|
|
143
|
+
${body}
|
|
144
|
+
<div class="foot-note">タスクは日報の申し送りの<b>ビュー</b>です。✓(Verified with Lemma)は「日報が改ざんされていない」ことを示すもので、タスクの当否そのものは保証しません。</div>`;
|
|
145
|
+
|
|
146
|
+
document.querySelectorAll("[data-task]").forEach((b) =>
|
|
147
|
+
b.addEventListener("click", () => toggleTask(b.dataset.task, b.dataset.status)),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderProps() {
|
|
152
|
+
const open = PROPS.filter((p) => p.status === "open").length;
|
|
153
|
+
document.getElementById("cProps").textContent = open;
|
|
154
|
+
const body = PROPS.length
|
|
155
|
+
? PROPS.map((p) => {
|
|
156
|
+
const L = PROP_LABEL[p.status] ?? PROP_LABEL.open;
|
|
157
|
+
const isOpen = p.status === "open";
|
|
158
|
+
return `<div class="item">
|
|
159
|
+
<div class="top">
|
|
160
|
+
<div class="ico">${esc(p.icon || "💡")}</div>
|
|
161
|
+
<div style="flex:1">
|
|
162
|
+
<div class="t">${esc(p.title)}</div>
|
|
163
|
+
${p.desc ? `<div class="d">${esc(p.desc)}</div>` : ""}
|
|
164
|
+
<div class="meta">
|
|
165
|
+
<span class="chip ${L.cls}">${L.t}</span>
|
|
166
|
+
${carryBadge(p.carryDays ?? 1)}
|
|
167
|
+
<span class="src">${md(p.sourceDate)} の気づき</span>
|
|
168
|
+
${!isOpen ? `<span class="assignee">承認者 <span class="pro">PRO</span></span>` : ""}
|
|
169
|
+
</div>
|
|
170
|
+
${isOpen && LIVE ? `<div class="acts">
|
|
171
|
+
<button class="btn ok" data-prop="${esc(p.id)}" data-decision="approved">承認(記録する)</button>
|
|
172
|
+
<button class="btn" data-prop="${esc(p.id)}" data-decision="hold">保留</button>
|
|
173
|
+
<button class="btn no" data-prop="${esc(p.id)}" data-decision="rejected">却下</button>
|
|
174
|
+
</div>` : ""}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>`;
|
|
178
|
+
}).join("")
|
|
179
|
+
: `<div class="empty">まだ提案はありません。日報に<b>気づき・提案</b>が出て、それを記録すると溜まります。</div>`;
|
|
180
|
+
|
|
181
|
+
document.getElementById("content").innerHTML = `
|
|
182
|
+
<div class="intro">日報の <b>気づき・提案</b> から溜まります。<b>提案どまり</b>——承認しても記録されるだけで、実行はオプトインです(勝手には動きません)。<b>承認者の記録</b>はチームの監査に使えます(PRO)。</div>
|
|
183
|
+
${body}
|
|
184
|
+
<div class="foot-note"><b>承認 = 決定の記録</b>(誰がいつ承認/却下したか)。実行を自動化したい場合だけ、設定でオプトイン。税理士業務などでは自動実行はオフのままにできます。</div>`;
|
|
185
|
+
|
|
186
|
+
document.querySelectorAll("[data-prop]").forEach((b) =>
|
|
187
|
+
b.addEventListener("click", () => decide(b.dataset.prop, b.dataset.decision)),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function toggleTask(id, status) {
|
|
192
|
+
try {
|
|
193
|
+
const r = await fetch(`/api/tasks/${id}/status`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "content-type": "application/json" },
|
|
196
|
+
body: JSON.stringify({ status }),
|
|
197
|
+
});
|
|
198
|
+
if (r.ok) { const { item } = await r.json(); replace(TASKS, item); renderTasks(); }
|
|
199
|
+
} catch { /* noop */ }
|
|
200
|
+
}
|
|
201
|
+
async function decide(id, decision) {
|
|
202
|
+
try {
|
|
203
|
+
const r = await fetch(`/api/proposals/${id}/decision`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: { "content-type": "application/json" },
|
|
206
|
+
body: JSON.stringify({ decision }),
|
|
207
|
+
});
|
|
208
|
+
if (r.ok) { const { item } = await r.json(); replace(PROPS, item); renderProps(); }
|
|
209
|
+
} catch { /* noop */ }
|
|
210
|
+
}
|
|
211
|
+
const replace = (arr, item) => { const i = arr.findIndex((x) => x.id === item.id); if (i >= 0) arr[i] = item; };
|
|
212
|
+
|
|
213
|
+
let tab = "tasks";
|
|
214
|
+
function show(t) {
|
|
215
|
+
tab = t;
|
|
216
|
+
document.getElementById("tabTasks").classList.toggle("on", t === "tasks");
|
|
217
|
+
document.getElementById("tabProps").classList.toggle("on", t === "props");
|
|
218
|
+
(t === "tasks" ? renderTasks : renderProps)();
|
|
219
|
+
}
|
|
220
|
+
document.getElementById("tabTasks").onclick = () => show("tasks");
|
|
221
|
+
document.getElementById("tabProps").onclick = () => show("props");
|
|
222
|
+
|
|
223
|
+
await load();
|
|
224
|
+
document.getElementById("cTasks").textContent = TASKS.filter((t) => t.status !== "done").length;
|
|
225
|
+
document.getElementById("cProps").textContent = PROPS.filter((p) => p.status === "open").length;
|
|
226
|
+
show("tasks");
|
|
227
|
+
</script>
|
|
228
|
+
</body>
|
|
229
|
+
</html>
|