@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,217 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generatedFrom": "src/cards.mjs",
|
|
3
|
+
"cards": [
|
|
4
|
+
{
|
|
5
|
+
"id": "first_shot",
|
|
6
|
+
"name": "早撃ち名人",
|
|
7
|
+
"subtitle": "一発で通した率が高い日",
|
|
8
|
+
"rarity": "COMMON",
|
|
9
|
+
"kind": "effect",
|
|
10
|
+
"domain": "dev",
|
|
11
|
+
"tw": "first_shot",
|
|
12
|
+
"effectPermission": "review_trust",
|
|
13
|
+
"quote": null,
|
|
14
|
+
"earned": null,
|
|
15
|
+
"earnedAt": null
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "grit",
|
|
19
|
+
"name": "粘り腰",
|
|
20
|
+
"subtitle": "リトライから成功にこぎ着けた",
|
|
21
|
+
"rarity": "COMMON",
|
|
22
|
+
"kind": "effect",
|
|
23
|
+
"domain": "dev",
|
|
24
|
+
"tw": "grit",
|
|
25
|
+
"effectPermission": null,
|
|
26
|
+
"quote": null,
|
|
27
|
+
"earned": null,
|
|
28
|
+
"earnedAt": null
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "comeback",
|
|
32
|
+
"name": "闘魂の逆走",
|
|
33
|
+
"subtitle": "失敗続きから成功率95%へ立て直し",
|
|
34
|
+
"rarity": "RARE",
|
|
35
|
+
"kind": "effect",
|
|
36
|
+
"domain": "dev",
|
|
37
|
+
"tw": "comeback",
|
|
38
|
+
"effectPermission": "review_trust",
|
|
39
|
+
"quote": null,
|
|
40
|
+
"earned": null,
|
|
41
|
+
"earnedAt": null
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "zero_dt",
|
|
45
|
+
"name": "ゼロダウンタイムの守護者",
|
|
46
|
+
"subtitle": "大仕事を失敗ゼロで完走",
|
|
47
|
+
"rarity": "EPIC",
|
|
48
|
+
"kind": "effect",
|
|
49
|
+
"domain": "dev",
|
|
50
|
+
"tw": "zero_dt",
|
|
51
|
+
"effectPermission": "deploy_prod",
|
|
52
|
+
"quote": "落とさないことが仕事です。派手ではありませんが、そこは譲れません。",
|
|
53
|
+
"earned": null,
|
|
54
|
+
"earnedAt": null
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "iron_streak",
|
|
58
|
+
"name": "不屈の連勤",
|
|
59
|
+
"subtitle": "失敗ゼロの日を継続",
|
|
60
|
+
"rarity": "RARE",
|
|
61
|
+
"kind": "effect",
|
|
62
|
+
"domain": "dev",
|
|
63
|
+
"tw": "iron",
|
|
64
|
+
"effectPermission": "secrets_read",
|
|
65
|
+
"quote": null,
|
|
66
|
+
"earned": null,
|
|
67
|
+
"earnedAt": null
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "clean_submit",
|
|
71
|
+
"name": "一筆入魂",
|
|
72
|
+
"subtitle": "差し戻しゼロで提出",
|
|
73
|
+
"rarity": "RARE",
|
|
74
|
+
"kind": "effect",
|
|
75
|
+
"domain": "tax",
|
|
76
|
+
"tw": "clean",
|
|
77
|
+
"effectPermission": "deleg_l3",
|
|
78
|
+
"quote": null,
|
|
79
|
+
"earned": null,
|
|
80
|
+
"earnedAt": null
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"id": "precision",
|
|
84
|
+
"name": "不敗の検算王",
|
|
85
|
+
"subtitle": "差し戻し率1%未満を継続",
|
|
86
|
+
"rarity": "EPIC",
|
|
87
|
+
"kind": "effect",
|
|
88
|
+
"domain": "tax",
|
|
89
|
+
"tw": "precision",
|
|
90
|
+
"effectPermission": "deleg_l3",
|
|
91
|
+
"quote": "見直しの一手間が、あとの全部を助けます。",
|
|
92
|
+
"earned": null,
|
|
93
|
+
"earnedAt": null
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "owl",
|
|
97
|
+
"name": "夜フクロウ",
|
|
98
|
+
"subtitle": "稼働が夜に寄る型",
|
|
99
|
+
"rarity": "COMMON",
|
|
100
|
+
"kind": "title",
|
|
101
|
+
"domain": null,
|
|
102
|
+
"tw": "owl",
|
|
103
|
+
"effectPermission": null,
|
|
104
|
+
"quote": null,
|
|
105
|
+
"earned": null,
|
|
106
|
+
"earnedAt": null
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"id": "cleaner",
|
|
110
|
+
"name": "整理整頓の鬼",
|
|
111
|
+
"subtitle": "Edit 中心の働き方",
|
|
112
|
+
"rarity": "COMMON",
|
|
113
|
+
"kind": "title",
|
|
114
|
+
"domain": null,
|
|
115
|
+
"tw": "cleaner",
|
|
116
|
+
"effectPermission": null,
|
|
117
|
+
"quote": null,
|
|
118
|
+
"earned": null,
|
|
119
|
+
"earnedAt": null
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"id": "scout",
|
|
123
|
+
"name": "影の立役者",
|
|
124
|
+
"subtitle": "Read・Grep 中心・土台を支える",
|
|
125
|
+
"rarity": "RARE",
|
|
126
|
+
"kind": "title",
|
|
127
|
+
"domain": null,
|
|
128
|
+
"tw": "scout",
|
|
129
|
+
"effectPermission": null,
|
|
130
|
+
"quote": null,
|
|
131
|
+
"earned": null,
|
|
132
|
+
"earnedAt": null
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "builder",
|
|
136
|
+
"name": "建築家",
|
|
137
|
+
"subtitle": "Write 中心・新規を書く",
|
|
138
|
+
"rarity": "RARE",
|
|
139
|
+
"kind": "title",
|
|
140
|
+
"domain": null,
|
|
141
|
+
"tw": "builder",
|
|
142
|
+
"effectPermission": null,
|
|
143
|
+
"quote": null,
|
|
144
|
+
"earned": null,
|
|
145
|
+
"earnedAt": null
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "night3",
|
|
149
|
+
"name": "深夜3時の戦士",
|
|
150
|
+
"subtitle": "午前2〜4時に稼働",
|
|
151
|
+
"rarity": "LEGENDARY",
|
|
152
|
+
"kind": "neta",
|
|
153
|
+
"domain": null,
|
|
154
|
+
"tw": "night3",
|
|
155
|
+
"effectPermission": null,
|
|
156
|
+
"quote": "静かな時間のほうが、はかどることもあります。",
|
|
157
|
+
"earned": null,
|
|
158
|
+
"earnedAt": null
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"id": "one_trick",
|
|
162
|
+
"name": "一芸デー",
|
|
163
|
+
"subtitle": "1ツールが全体の8割超",
|
|
164
|
+
"rarity": "RARE",
|
|
165
|
+
"kind": "neta",
|
|
166
|
+
"domain": null,
|
|
167
|
+
"tw": "onetrick",
|
|
168
|
+
"effectPermission": null,
|
|
169
|
+
"quote": null,
|
|
170
|
+
"earned": null,
|
|
171
|
+
"earnedAt": null
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"id": "two_face",
|
|
175
|
+
"name": "二面性",
|
|
176
|
+
"subtitle": "午前と午後で最多ツールが逆転",
|
|
177
|
+
"rarity": "RARE",
|
|
178
|
+
"kind": "neta",
|
|
179
|
+
"domain": null,
|
|
180
|
+
"tw": "twoface",
|
|
181
|
+
"effectPermission": null,
|
|
182
|
+
"quote": null,
|
|
183
|
+
"earned": null,
|
|
184
|
+
"earnedAt": null
|
|
185
|
+
}
|
|
186
|
+
],
|
|
187
|
+
"permissions": [
|
|
188
|
+
{
|
|
189
|
+
"id": "deploy_prod",
|
|
190
|
+
"name": "本番デプロイ権",
|
|
191
|
+
"subtitle": "production への反映",
|
|
192
|
+
"scope": "deploy:production",
|
|
193
|
+
"unlockable": null
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"id": "secrets_read",
|
|
197
|
+
"name": "Secrets 参照権限",
|
|
198
|
+
"subtitle": "機密値の読み取り",
|
|
199
|
+
"scope": "secrets:read",
|
|
200
|
+
"unlockable": null
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"id": "main_merge",
|
|
204
|
+
"name": "main 直接 merge 権",
|
|
205
|
+
"subtitle": "レビュー省略での merge",
|
|
206
|
+
"scope": "main:merge",
|
|
207
|
+
"unlockable": null
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
"id": "deleg_l3",
|
|
211
|
+
"name": "委任階位 L3",
|
|
212
|
+
"subtitle": "広めの委任(要・資格者承認)",
|
|
213
|
+
"scope": "tax:propose:wide",
|
|
214
|
+
"unlockable": null
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
<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&display=swap" rel="stylesheet" />
|
|
10
|
+
<style>
|
|
11
|
+
:root{--indigo:#4F6BF6;--violet:#8B5CF6;--ink:#161A26;--muted:#8A90A0;--line:#E7E9F2;--green:#0A8F66;}
|
|
12
|
+
*{box-sizing:border-box}
|
|
13
|
+
body{margin:0; background:#EDEFF5; font-family:"Inter","Noto Sans JP",system-ui,sans-serif; color:var(--ink); line-height:1.55}
|
|
14
|
+
.frame{max-width:600px; margin:0 auto; padding:20px 12px 40px}
|
|
15
|
+
.hint{font-size:11px; color:#8A90A0; text-align:center; margin:6px 0 12px}
|
|
16
|
+
|
|
17
|
+
/* ===== 横長デイリーヒーロー(スクショ対象・きのうのデータ) ===== */
|
|
18
|
+
.hero{display:flex; min-height:250px; border-radius:18px; overflow:hidden; box-shadow:0 8px 30px rgba(20,26,38,.14); background:#fff}
|
|
19
|
+
.hL{width:216px; flex:none; background:linear-gradient(150deg,#4F6BF6 0%,#7C5CF6 58%,#8B5CF6 100%); color:#fff; padding:24px 18px; display:flex; flex-direction:column}
|
|
20
|
+
.hL .mk{font-size:11px; font-weight:800; letter-spacing:.03em; display:flex; align-items:center; gap:6px; opacity:.96}
|
|
21
|
+
.hL .mk .d{width:14px; height:14px; border-radius:4px; background:rgba(255,255,255,.9)}
|
|
22
|
+
.hL .av{width:56px; height:56px; border-radius:50%; background:rgba(255,255,255,.16); border:2px solid rgba(255,255,255,.5);
|
|
23
|
+
display:flex; align-items:center; justify-content:center; font-weight:900; font-size:24px; margin-top:auto}
|
|
24
|
+
.hL .name{font-size:24px; font-weight:900; margin-top:9px; letter-spacing:.01em}
|
|
25
|
+
.hL .by{font-size:11px; opacity:.85; font-weight:600}
|
|
26
|
+
.hL .chips{display:flex; gap:6px; flex-wrap:wrap; margin-top:11px}
|
|
27
|
+
.hL .c{font-size:10.5px; font-weight:700; background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.34);
|
|
28
|
+
padding:3px 9px; border-radius:999px}
|
|
29
|
+
|
|
30
|
+
.hR{flex:1; min-width:0; padding:22px 20px 20px; display:flex; flex-direction:column}
|
|
31
|
+
.hR .kick{font-size:12.5px; color:var(--muted); font-weight:700}
|
|
32
|
+
.hR .kick b{color:var(--ink)}
|
|
33
|
+
.stats{display:flex; gap:8px; margin-top:16px}
|
|
34
|
+
.stats .s{flex:1; text-align:center; border:1px solid var(--line); border-radius:11px; padding:15px 4px}
|
|
35
|
+
.stats .v{font-size:23px; font-weight:900; line-height:1}
|
|
36
|
+
.stats .v.g{color:var(--green)}
|
|
37
|
+
.stats .v small{font-size:11px; color:var(--muted); font-weight:700}
|
|
38
|
+
.stats .k{font-size:10px; color:var(--muted); font-weight:600; margin-top:5px}
|
|
39
|
+
.hR .midrow{display:flex; align-items:center; gap:8px; margin-top:16px; flex-wrap:wrap}
|
|
40
|
+
.badge{display:inline-flex; align-items:center; gap:5px; font-size:11px; font-weight:700; color:var(--green);
|
|
41
|
+
background:#E7F5EF; border:1px solid #BFE6D6; padding:4px 10px; border-radius:999px}
|
|
42
|
+
.badge svg{width:12px; height:12px}
|
|
43
|
+
.todaycard{display:inline-flex; align-items:center; gap:6px; font-size:11px; font-weight:700; color:#6D53E0;
|
|
44
|
+
background:#F1EEFF; border:1px solid #E1D9FB; padding:4px 10px; border-radius:999px}
|
|
45
|
+
.hR .foot{margin-top:auto; padding-top:12px; font-size:11.5px; color:var(--muted); font-weight:600}
|
|
46
|
+
.hR .foot b{color:var(--indigo)}
|
|
47
|
+
|
|
48
|
+
/* 下部:パスポートをシェア */
|
|
49
|
+
.bottomshare{margin-top:16px; background:#fff; border:1px solid var(--line); border-radius:16px; padding:22px 20px; text-align:center}
|
|
50
|
+
.bottomshare .bs-t{font-size:15px; font-weight:800}
|
|
51
|
+
.bottomshare .bs-s{font-size:12.5px; color:var(--muted); margin-top:2px}
|
|
52
|
+
.bottomshare .bs-btn{display:inline-flex; align-items:center; gap:8px; text-decoration:none; margin-top:14px;
|
|
53
|
+
font-size:14px; font-weight:800; color:#fff; background:linear-gradient(135deg,#4F6BF6,#8B5CF6);
|
|
54
|
+
border-radius:12px; padding:12px 22px}
|
|
55
|
+
.bottomshare .bs-btn svg{width:16px; height:16px}
|
|
56
|
+
|
|
57
|
+
.scissors{display:flex; align-items:center; gap:10px; color:#B7BECF; font-size:11px; margin:16px 6px 8px}
|
|
58
|
+
.scissors:before,.scissors:after{content:""; flex:1; border-top:1.5px dashed #CBD1DF}
|
|
59
|
+
|
|
60
|
+
.body{background:#fff; border:1px solid var(--line); border-radius:14px; padding:20px 22px; margin-top:2px}
|
|
61
|
+
.body h3{font-size:12px; color:#8A90A0; font-weight:700; margin:0 0 12px}
|
|
62
|
+
.blk{border:1px solid #EEF0F6; border-radius:12px; padding:13px 15px; margin-bottom:10px}
|
|
63
|
+
.blk .t{font-size:14px; font-weight:700} .blk .tool{font-size:10.5px; color:#4F6BF6; font-weight:700; background:#EEF1FE; border:1px solid #DCE3FD; padding:2px 7px; border-radius:6px}
|
|
64
|
+
.blk p{font-size:13px; color:#3A4155; margin:7px 0 0} .blk .m{font-size:11.5px; color:#8A90A0; margin-top:8px}
|
|
65
|
+
.more{text-align:center; font-size:12px; color:#9AA0AE; padding:10px}
|
|
66
|
+
|
|
67
|
+
@media(max-width:460px){ .hero{flex-direction:column} .hL{width:100%; flex-direction:row; align-items:center; gap:12px}
|
|
68
|
+
.hL .av{margin-top:0} .hL .name{margin-top:0} }
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<div class="frame">
|
|
73
|
+
<div class="hint">↓ この横長カードを切り取ってスクショ → 映えるデイリー実績カードに</div>
|
|
74
|
+
|
|
75
|
+
<div class="hero">
|
|
76
|
+
<div class="hL">
|
|
77
|
+
<span class="mk"><span class="d"></span>CLUBS · AIの業務日報</span>
|
|
78
|
+
<div class="av">H</div>
|
|
79
|
+
<div class="name">Hermes</div>
|
|
80
|
+
<div class="by">mayumi のAIエージェント</div>
|
|
81
|
+
<div class="chips"><span class="c">🦉 夜型</span><span class="c">🔥 34日連続</span></div>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="hR">
|
|
84
|
+
<div class="kick">きのうの働き · <b>2026-07-13(日)</b></div>
|
|
85
|
+
<div class="stats">
|
|
86
|
+
<div class="s"><div class="v">5.4<small>h</small></div><div class="k">実働推定</div></div>
|
|
87
|
+
<div class="s"><div class="v">27<small>回</small></div><div class="k">実行</div></div>
|
|
88
|
+
<div class="s"><div class="v g">100<small>%</small></div><div class="k">成功率</div></div>
|
|
89
|
+
<div class="s"><div class="v">0<small>件</small></div><div class="k">失敗</div></div>
|
|
90
|
+
</div>
|
|
91
|
+
<div class="midrow">
|
|
92
|
+
<span class="badge"><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>
|
|
93
|
+
<span class="todaycard">🎯 きょうの称号「一発職人」</span>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="foot">あなたのAIの日報 → <b>clubs.place</b></div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="scissors">ここから下は読む用(スクショ対象外)</div>
|
|
100
|
+
|
|
101
|
+
<div class="body">
|
|
102
|
+
<h3>きのうの業務</h3>
|
|
103
|
+
<div class="blk"><span class="tool">Edit</span><div class="t" style="margin-top:6px">clubs-nippo</div>
|
|
104
|
+
<p>日報メールの描画まわりを整理し、検証状態のバッジを差し込めるようにしました。集計値と本文のハッシュ登録の受け口も用意しています。</p>
|
|
105
|
+
<div class="m">実行 18回 · Edit×7 · Read×6 · Bash×5</div></div>
|
|
106
|
+
<div class="blk"><span class="tool">Bash</span><div class="t" style="margin-top:6px">lemma 連携</div>
|
|
107
|
+
<p>登録APIへの疎通を確認し、鍵が無い場合は自動で照合予定にフォールバックする挙動を検証しました。</p>
|
|
108
|
+
<div class="m">実行 9回 · 成功 9 / 失敗 0</div></div>
|
|
109
|
+
<div class="more">— 進捗と申し送り・気づき が続きます —</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- 下部:パスポートをシェア(シェア導線はここ一本) -->
|
|
113
|
+
<div class="bottomshare">
|
|
114
|
+
<div class="bs-t">実績が溜まってきました</div>
|
|
115
|
+
<div class="bs-s">パスポートを見せる・シェアする</div>
|
|
116
|
+
<a class="bs-btn" href="https://clubs.place/hermes">
|
|
117
|
+
<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>
|
|
118
|
+
パスポートをシェア
|
|
119
|
+
</a>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</body>
|
|
123
|
+
</html>
|
|
@@ -0,0 +1,269 @@
|
|
|
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
|
+
--indigo:#4F6BF6; --violet:#8B5CF6; --green:#0A8F66;
|
|
15
|
+
--gold:#B7862B; --gold-t:#F7EFDD;
|
|
16
|
+
--radius:14px;
|
|
17
|
+
}
|
|
18
|
+
*{box-sizing:border-box}
|
|
19
|
+
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}
|
|
20
|
+
.main{flex:1;min-width:0}
|
|
21
|
+
|
|
22
|
+
.topbar{height:60px;background:#fff;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:12px;padding:0 26px;position:sticky;top:0;z-index:5}
|
|
23
|
+
.topbar h1{font-size:16px;font-weight:800;margin:0}
|
|
24
|
+
.topbar .sp{flex:1}
|
|
25
|
+
.topbar .date{font-size:12px;color:var(--muted)}
|
|
26
|
+
.plan-pill{font-size:11.5px;font-weight:800;color:var(--gold);background:var(--gold-t);border:1px solid #E9D9AE;border-radius:999px;padding:5px 12px}
|
|
27
|
+
.content{padding:24px 26px 44px;max-width:940px;margin:0 auto}
|
|
28
|
+
|
|
29
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 1px 2px rgba(18,22,45,.05)}
|
|
30
|
+
.hero{padding:18px 20px}
|
|
31
|
+
.hero .row{display:flex;align-items:center;gap:13px}
|
|
32
|
+
.hero .av{width:46px;height:46px;border-radius:13px;background:linear-gradient(160deg,#66B9D8,#3E93B5);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:19px;flex:none}
|
|
33
|
+
.hero .nm{font-weight:800;font-size:17px}
|
|
34
|
+
.hero .sub{font-size:11px;color:var(--muted);font-family:"IBM Plex Mono",monospace}
|
|
35
|
+
.stats{display:flex;gap:10px;margin-top:15px;flex-wrap:wrap}
|
|
36
|
+
.stat{flex:1;min-width:140px;background:var(--bg);border:1px solid var(--line);border-radius:11px;padding:11px 12px}
|
|
37
|
+
.stat .l{font-size:10px;font-weight:600;color:var(--muted)}
|
|
38
|
+
.stat .n{font-weight:800;font-size:22px;letter-spacing:-.02em;margin-top:2px;line-height:1}
|
|
39
|
+
.stat .n small{font-size:12px;font-weight:700;color:var(--muted)}
|
|
40
|
+
.stat .k{font-size:9.5px;color:var(--green);font-weight:700;margin-top:3px}
|
|
41
|
+
|
|
42
|
+
.grid2{display:grid;grid-template-columns:1.5fr 1fr;gap:16px;margin-top:16px}
|
|
43
|
+
@media(max-width:880px){.grid2{grid-template-columns:1fr}}
|
|
44
|
+
.pad{padding:16px 18px}
|
|
45
|
+
h3.ct{font-size:12px;font-weight:800;margin:0 0 11px;display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
46
|
+
.chip{font-size:10px;font-weight:700;border-radius:999px;padding:2px 9px;white-space:nowrap}
|
|
47
|
+
.chip.draft{background:var(--gold-t);color:var(--gold)}
|
|
48
|
+
.chip.ok{background:#E4F5EE;color:var(--green)}
|
|
49
|
+
.rstats{font-size:11.5px;color:var(--muted);margin-bottom:9px;font-weight:600}
|
|
50
|
+
.rstats b{color:var(--ink-2)} .rstats .ok{color:var(--green)}
|
|
51
|
+
.line{font-size:13px;padding:7px 0;border-bottom:1px solid var(--line);display:flex;gap:8px;align-items:baseline}
|
|
52
|
+
.line:last-of-type{border-bottom:none}
|
|
53
|
+
.line .n{font-weight:700;color:var(--indigo);min-width:40px;flex:none}
|
|
54
|
+
.line .t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
55
|
+
.line .via{margin-left:auto;font-size:9.5px;color:var(--muted);background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:1px 8px;font-weight:600;flex:none;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
56
|
+
.acts{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
|
|
57
|
+
.acts a{font-weight:700;font-size:12px;border-radius:999px;padding:8px 15px;text-decoration:none}
|
|
58
|
+
.a-ok{background:var(--indigo);color:#fff}
|
|
59
|
+
.a-line{background:#fff;border:1px solid var(--line);color:var(--ink-2)}
|
|
60
|
+
.empty{font-size:13px;color:var(--muted)}
|
|
61
|
+
|
|
62
|
+
.ach{display:flex;gap:9px;flex-wrap:wrap}
|
|
63
|
+
.ach .m{width:58px;text-align:center}
|
|
64
|
+
.ach .mc{height:52px;border-radius:12px;display:flex;align-items:center;justify-content:center;background:#fff;border:1px solid var(--line)}
|
|
65
|
+
.ach .mc svg{width:30px;height:30px}
|
|
66
|
+
.ach .ml{font-size:8.5px;font-weight:700;margin-top:3px;line-height:1.2;color:var(--muted)}
|
|
67
|
+
.morelink{display:block;text-align:center;margin-top:11px;font-size:12px;font-weight:700;color:var(--indigo);text-decoration:none}
|
|
68
|
+
|
|
69
|
+
.share{display:flex;align-items:center;gap:12px;margin-top:16px;background:linear-gradient(120deg,var(--indigo),var(--violet));border-radius:var(--radius);padding:15px 18px;color:#fff;flex-wrap:wrap}
|
|
70
|
+
.share .st{font-weight:800;font-size:14px}
|
|
71
|
+
.share .ss{font-size:11px;opacity:.92}
|
|
72
|
+
.share a{margin-left:auto;background:#fff;color:var(--indigo);border-radius:999px;padding:8px 16px;font-weight:700;font-size:12px;text-decoration:none;white-space:nowrap}
|
|
73
|
+
|
|
74
|
+
.monthly{margin-top:16px;display:flex;align-items:center;gap:12px;background:#fff;border:1px dashed #D7DDF3;border-radius:var(--radius);padding:14px 18px;font-size:13px;color:var(--ink-2)}
|
|
75
|
+
.monthly svg{width:18px;height:18px;color:var(--indigo);flex:none}
|
|
76
|
+
.monthly b{color:var(--ink)}
|
|
77
|
+
.monthly .tag{margin-left:auto;font-size:11px;font-weight:700;color:var(--indigo);background:#EEF1FE;border:1px solid #DCE3FD;padding:3px 10px;border-radius:999px;white-space:nowrap}
|
|
78
|
+
</style>
|
|
79
|
+
</head>
|
|
80
|
+
<body>
|
|
81
|
+
<aside></aside>
|
|
82
|
+
|
|
83
|
+
<div class="main">
|
|
84
|
+
<div class="topbar">
|
|
85
|
+
<h1>ホーム</h1><span class="sp"></span>
|
|
86
|
+
<span class="date" id="today">—</span>
|
|
87
|
+
<span class="plan-pill">個人プラン</span>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="content">
|
|
91
|
+
<div class="card hero">
|
|
92
|
+
<div class="row"><div class="av" id="heroAv">—</div><div><div class="nm" id="heroName">—</div><div class="sub" id="heroSub">—</div></div></div>
|
|
93
|
+
<div class="stats" id="heroStats"></div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="grid2">
|
|
97
|
+
<div class="card pad" id="latestCard"></div>
|
|
98
|
+
<div class="card pad">
|
|
99
|
+
<h3 class="ct">最近の達成 <span class="chip ok">✓ 盛れない</span></h3>
|
|
100
|
+
<div class="ach" id="ach"></div>
|
|
101
|
+
<a class="morelink" href="../cards/achievements.html">実績・図鑑を見る →</a>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="share" id="share" hidden>
|
|
106
|
+
<div><div class="st">実績カードができました</div><div class="ss" id="shareSub">—</div></div>
|
|
107
|
+
<a href="../share/passport.html">シェアする</a>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="monthly">
|
|
111
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.69a4.5 4.5 0 0 1 0 6.36l-3 3a4.5 4.5 0 0 1-6.36-6.36l1.5-1.5M10.81 15.31a4.5 4.5 0 0 1 0-6.36l3-3a4.5 4.5 0 0 1 6.36 6.36l-1.5 1.5"/></svg>
|
|
112
|
+
<span><b>月次レポート</b>:その月の日報をまとめ、オンチェーンに記録して社外に出せる証明書にできます。</span>
|
|
113
|
+
<span class="tag">有償プラン</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<script type="module">
|
|
119
|
+
import { mountSidebar } from "../assets/sidebar.js";
|
|
120
|
+
import { TWEMOJI as TW } from "../assets/twemoji.js";
|
|
121
|
+
|
|
122
|
+
mountSidebar("home");
|
|
123
|
+
|
|
124
|
+
/* データはすべて既存APIから。取れなければ preview.json(実データのスナップショット)。
|
|
125
|
+
モックのサンプル値は持たない。共有URLに作り話の数字を出さないため。 */
|
|
126
|
+
async function load() {
|
|
127
|
+
try {
|
|
128
|
+
const [p, rs, cs, st] = await Promise.all([
|
|
129
|
+
fetch("/api/passport"), fetch("/api/reports"), fetch("/api/cards"), fetch("/api/settings"),
|
|
130
|
+
]);
|
|
131
|
+
if (p.ok && rs.ok && cs.ok && st.ok) {
|
|
132
|
+
const passport = await p.json();
|
|
133
|
+
const { reports } = await rs.json();
|
|
134
|
+
const { cards } = await cs.json();
|
|
135
|
+
const settings = await st.json();
|
|
136
|
+
|
|
137
|
+
// 作業行は日次ドキュメントにしか無い(一覧は集計だけ)。最新日の1本だけ引く。
|
|
138
|
+
const date = reports[0]?.date;
|
|
139
|
+
const r = date ? await fetch(`/api/reports/${date}`) : null;
|
|
140
|
+
const latest = r?.ok ? await r.json() : null;
|
|
141
|
+
|
|
142
|
+
return { live: true, passport, reports, cards, settings, latest };
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
/* 次を試す */
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const r = await fetch("../preview.json");
|
|
150
|
+
if (r.ok) {
|
|
151
|
+
const s = await r.json();
|
|
152
|
+
return {
|
|
153
|
+
live: false, snapshotAt: s.snapshotAt,
|
|
154
|
+
passport: s.passport, reports: s.reports, cards: s.cards,
|
|
155
|
+
settings: s.settings, latest: s.latest ?? null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
/* 何も取れなかった */
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { live: true, passport: null, reports: [], cards: [], settings: null, latest: null };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* 静的配信ではライブのデータが無い。スナップショットであることを必ず明示する。 */
|
|
166
|
+
function snapshotBanner(at) {
|
|
167
|
+
const el = document.createElement("div");
|
|
168
|
+
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";
|
|
169
|
+
el.textContent = `スナップショット(${at} 時点)。サーバに繋がっていないため、記録や保存はできません。`;
|
|
170
|
+
document.body.prepend(el);
|
|
171
|
+
document.body.style.paddingTop = "34px";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
175
|
+
const shortHash = (h) => (h ? `${h.slice(0, 8)}…${h.slice(-4)}` : null);
|
|
176
|
+
|
|
177
|
+
/* 実行回数は metric(例:「実行 15回(Web検索 7 / …)」)から読めたときだけ出す。
|
|
178
|
+
読めない書き方のときは数字を作らない。 */
|
|
179
|
+
const callsOf = (metric) => String(metric ?? "").match(/([\d,]+)\s*回/)?.[1] ?? null;
|
|
180
|
+
|
|
181
|
+
const STATUS = {
|
|
182
|
+
draft: { cls: "draft", label: "下書き・未記録" },
|
|
183
|
+
recorded: { cls: "ok", label: "記録済み" },
|
|
184
|
+
anchored: { cls: "ok", label: "オンチェーン記録済み" },
|
|
185
|
+
pending: { cls: "draft", label: "照合予定" },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
function render(d) {
|
|
189
|
+
const p = d.passport;
|
|
190
|
+
const reports = d.reports ?? [];
|
|
191
|
+
const earned = (d.cards ?? []).filter((c) => c.earned);
|
|
192
|
+
|
|
193
|
+
// toISOString は UTC。JST の朝はまだ前日になるので、ローカル日付で出す。
|
|
194
|
+
document.getElementById("today").textContent = new Date().toLocaleDateString("sv-SE");
|
|
195
|
+
|
|
196
|
+
// ── ヒーロー ────────────────────────────────────────────────
|
|
197
|
+
const name = p?.displayName ?? "—";
|
|
198
|
+
document.getElementById("heroName").textContent = name;
|
|
199
|
+
document.getElementById("heroAv").textContent = [...(p?.agentName ?? "?")][0];
|
|
200
|
+
|
|
201
|
+
// 収集元・docHash・稼働日数は passport には無い。settings と日報の実データから組む。
|
|
202
|
+
const sources = (d.settings?.sources ?? []).join(" · ");
|
|
203
|
+
const hash = shortHash(reports.find((r) => r.docHash)?.docHash);
|
|
204
|
+
document.getElementById("heroSub").textContent =
|
|
205
|
+
[sources || null, hash, reports.length ? `稼働 ${reports.length}日` : null].filter(Boolean).join(" · ") || "—";
|
|
206
|
+
|
|
207
|
+
document.getElementById("heroStats").innerHTML = [
|
|
208
|
+
["累計タスク", (p?.totalCalls ?? 0).toLocaleString(), "✓ ログと一致"],
|
|
209
|
+
["平均成功率", p?.avgSuccess == null ? "—" : `${p.avgSuccess}<small>%</small>`, "盛れない"],
|
|
210
|
+
["連続日数", `${p?.streak ?? 0}<small>日</small>`, "記録済みの連続"],
|
|
211
|
+
["達成カード", earned.length, "パスポートに積算"],
|
|
212
|
+
].map(([l, n, k]) => `<div class="stat"><div class="l">${esc(l)}</div><div class="n">${n}</div><div class="k">${esc(k)}</div></div>`).join("");
|
|
213
|
+
|
|
214
|
+
// ── 直近の日報 ──────────────────────────────────────────────
|
|
215
|
+
const L = reports[0];
|
|
216
|
+
const box = document.getElementById("latestCard");
|
|
217
|
+
if (!L) {
|
|
218
|
+
box.innerHTML = `<h3 class="ct">直近の日報</h3><div class="empty">まだ日報がありません。<code>clubs-nippo</code> を走らせると、ここに出ます。</div>`;
|
|
219
|
+
} else {
|
|
220
|
+
const badge = STATUS[L.status] ?? STATUS.draft;
|
|
221
|
+
// 「上位の作業行」=実行回数の多い順。日次ドキュメントの並びは生成順なので、そのままでは上位にならない。
|
|
222
|
+
// 回数が読めないブロックは末尾へ(順位を作るために数字を捏造しない)。
|
|
223
|
+
const blocks = [...(d.latest?.workBlocks ?? [])]
|
|
224
|
+
.sort((a, b) => Number(callsOf(b.metric)?.replace(/,/g, "") ?? -1) - Number(callsOf(a.metric)?.replace(/,/g, "") ?? -1))
|
|
225
|
+
.slice(0, 3);
|
|
226
|
+
const lines = blocks.map((b) => {
|
|
227
|
+
const n = callsOf(b.metric);
|
|
228
|
+
return `<div class="line">${n ? `<span class="n">${esc(n)}回</span>` : ""}<span class="t">${esc(b.title)}</span>${b.tool ? `<span class="via">${esc(b.tool)}</span>` : ""}</div>`;
|
|
229
|
+
}).join("");
|
|
230
|
+
|
|
231
|
+
box.innerHTML = `
|
|
232
|
+
<h3 class="ct">直近の日報(${esc(L.date.slice(5).replace("-", "/"))}・${esc(L.w)}曜) <span class="chip ${badge.cls}">${esc(badge.label)}</span></h3>
|
|
233
|
+
<div class="rstats"><b>${L.totalCalls}</b> 実行 · 成功率 <b>${L.successRate ?? "—"}%</b> · 実働 約<b>${L.activeHours}h</b>${L.docHash ? ' <span class="ok">✓ ログと一致</span>' : ""}</div>
|
|
234
|
+
${lines}
|
|
235
|
+
<div class="acts">
|
|
236
|
+
<a class="a-ok" href="../reports/edit.html?date=${encodeURIComponent(L.date)}">✏️ 編集して記録する</a>
|
|
237
|
+
<a class="a-line" href="../reports/">日報の一覧</a>
|
|
238
|
+
</div>`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 最近の達成 ──────────────────────────────────────────────
|
|
242
|
+
const recent = [...earned].sort((a, b) => String(b.earnedAt ?? "").localeCompare(String(a.earnedAt ?? ""))).slice(0, 4);
|
|
243
|
+
document.getElementById("ach").innerHTML = recent.length
|
|
244
|
+
? recent.map((c) => `<div class="m"><div class="mc" data-tw="${esc(c.tw)}"></div><div class="ml">${esc(c.name)}</div></div>`).join("")
|
|
245
|
+
: `<div class="empty">まだ獲得したカードはありません。</div>`;
|
|
246
|
+
document.querySelectorAll(".mc[data-tw]").forEach((el) => { el.innerHTML = TW[el.dataset.tw] ?? ""; });
|
|
247
|
+
|
|
248
|
+
// ── 実績カード(直近7日) ───────────────────────────────────
|
|
249
|
+
// 「今週」は日報の実データから積む。無い週は帯ごと出さない(0 の帯を出すと、成果が無いのに
|
|
250
|
+
// カードができたように読める)。
|
|
251
|
+
const week = reports.filter((r) => r.date >= new Date(Date.now() - 6 * 864e5).toISOString().slice(0, 10));
|
|
252
|
+
if (week.length) {
|
|
253
|
+
const calls = week.reduce((s, r) => s + (r.totalCalls ?? 0), 0);
|
|
254
|
+
const rates = week.map((r) => r.successRate).filter((x) => typeof x === "number");
|
|
255
|
+
const avg = rates.length ? Math.round(rates.reduce((a, b) => a + b, 0) / rates.length) : null;
|
|
256
|
+
document.getElementById("shareSub").textContent =
|
|
257
|
+
`${name} の直近7日 ── ${calls}タスク${avg == null ? "" : `・成功率${avg}%`}・連続${p?.streak ?? 0}日`;
|
|
258
|
+
document.getElementById("share").hidden = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
(async () => {
|
|
263
|
+
const d = await load();
|
|
264
|
+
if (d.live === false) snapshotBanner(d.snapshotAt);
|
|
265
|
+
render(d);
|
|
266
|
+
})();
|
|
267
|
+
</script>
|
|
268
|
+
</body>
|
|
269
|
+
</html>
|