@ijfw/memory-server 1.3.0
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/bin/ijfw +27 -0
- package/bin/ijfw-dashboard +180 -0
- package/bin/ijfw-dispatch-plan +41 -0
- package/bin/ijfw-memorize +273 -0
- package/bin/ijfw-memory +51 -0
- package/fixtures/demo-target.js +28 -0
- package/package.json +53 -0
- package/src/api-client.js +190 -0
- package/src/audit-roster.js +315 -0
- package/src/caps.js +37 -0
- package/src/cold-scan-runner.mjs +37 -0
- package/src/compute/edges.js +155 -0
- package/src/compute/extract.js +560 -0
- package/src/compute/fts5.js +420 -0
- package/src/compute/graph-auto-index.js +191 -0
- package/src/compute/graph-lock.js +114 -0
- package/src/compute/index.js +18 -0
- package/src/compute/migration-runner.js +116 -0
- package/src/compute/migrations/001-initial.js +23 -0
- package/src/compute/migrations/002-porter-stemming-source.js +139 -0
- package/src/compute/migrations/003-tier-semantic.js +69 -0
- package/src/compute/migrations/004-kg-tables.js +83 -0
- package/src/compute/migrations/005-stale-candidate.js +72 -0
- package/src/compute/python-resolver.js +106 -0
- package/src/compute/runner-vm.js +185 -0
- package/src/compute/runner.js +416 -0
- package/src/compute/sandbox-detect.js +122 -0
- package/src/compute/sandbox-linux.js +164 -0
- package/src/compute/sandbox-macos.js +167 -0
- package/src/compute/sandbox-windows.js +63 -0
- package/src/compute/schema.sql +118 -0
- package/src/compute/staleness.js +239 -0
- package/src/compute/synonyms.js +367 -0
- package/src/compute/traverse.js +180 -0
- package/src/cost/aggregator.js +229 -0
- package/src/cost/pricing.js +134 -0
- package/src/cost/readers/claude.js +179 -0
- package/src/cost/readers/codex.js +131 -0
- package/src/cost/readers/gemini.js +111 -0
- package/src/cost/savings.js +243 -0
- package/src/cross-dispatcher.js +437 -0
- package/src/cross-orchestrator-cli.js +1885 -0
- package/src/cross-orchestrator.js +598 -0
- package/src/cross-project-search.js +114 -0
- package/src/dashboard-client.html +1180 -0
- package/src/dashboard-server.js +895 -0
- package/src/design-companion.js +81 -0
- package/src/dispatch/colon-syntax.js +732 -0
- package/src/dispatch-planner.js +235 -0
- package/src/dream/cooldown.js +105 -0
- package/src/dream/runner.mjs +373 -0
- package/src/dream/staleness-wiring.js +195 -0
- package/src/feedback-detector.js +57 -0
- package/src/hero-line.js +115 -0
- package/src/importers/claude-mem.js +152 -0
- package/src/importers/cli.js +311 -0
- package/src/importers/common.js +84 -0
- package/src/importers/discover.js +235 -0
- package/src/importers/rtk.js +107 -0
- package/src/intent-router.js +221 -0
- package/src/lib/atomic-io.js +201 -0
- package/src/lib/cache.js +33 -0
- package/src/lib/npm-view.js +104 -0
- package/src/lib/status-card.js +95 -0
- package/src/lib/token.js +85 -0
- package/src/memory/fts5.js +349 -0
- package/src/memory/migration-runner.js +116 -0
- package/src/memory/migrations/001-fts5-init.js +26 -0
- package/src/memory/migrations/002-tier-semantic.js +60 -0
- package/src/memory/migrations/003-stale-candidate.js +60 -0
- package/src/memory/reader.js +300 -0
- package/src/memory/recall-counter.js +76 -0
- package/src/memory/schema.sql +79 -0
- package/src/memory/search.js +431 -0
- package/src/memory/staleness.js +237 -0
- package/src/memory/tier-promotion.js +377 -0
- package/src/memory/tokenize.js +63 -0
- package/src/project-type-detector.js +866 -0
- package/src/prompt-check.js +171 -0
- package/src/ralph-allowlist.js +88 -0
- package/src/receipts.js +129 -0
- package/src/redactor.js +107 -0
- package/src/sandbox.js +275 -0
- package/src/sanitizer.js +69 -0
- package/src/scan-resume.js +167 -0
- package/src/schema.js +82 -0
- package/src/search-bm25.js +108 -0
- package/src/server.js +1414 -0
- package/src/swarm-config.js +80 -0
- package/src/trident/dispatch.js +211 -0
- package/src/trident/lens-health.js +253 -0
- package/src/update-apply.js +79 -0
- package/src/update-check.js +136 -0
- package/src/vectors.js +178 -0
- package/templates/design/bento-grid.md +84 -0
- package/templates/design/brutalist-luxe.md +82 -0
- package/templates/design/cinematic-dark.md +82 -0
- package/templates/design/data-dense-dashboard.md +88 -0
- package/templates/design/editorial-warm.md +81 -0
- package/templates/design/glassmorphic.md +84 -0
- package/templates/design/magazine-editorial.md +84 -0
- package/templates/design/maximalist-vibrant.md +85 -0
- package/templates/design/neo-swiss-tech.md +85 -0
- package/templates/design/swiss-minimal.md +80 -0
- package/templates/design/terminal-native.md +83 -0
- package/templates/design/warm-organic.md +84 -0
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>IJFW Cockpit</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg-deep:#020203; --bg-base:#0b0b0d; --bg-elevated:#121216;
|
|
10
|
+
--surface:rgba(255,255,255,0.04); --border:rgba(255,255,255,0.08);
|
|
11
|
+
--fg:#EDEDEF; --fg-dim:#8A8F98;
|
|
12
|
+
--accent:#5E6AD2; --claude:#D97757; --codex:#10A37F; --gemini:#4285F4;
|
|
13
|
+
--success:#2ecc71; --warn:#f59e0b; --danger:#ef4444;
|
|
14
|
+
--radius:8px; --radius-sm:4px; --trans:180ms ease;
|
|
15
|
+
--sidebar-w:240px;
|
|
16
|
+
}
|
|
17
|
+
[data-theme="light"] {
|
|
18
|
+
--bg-deep:#ffffff; --bg-base:#f4f4f6; --bg-elevated:#ffffff;
|
|
19
|
+
--surface:rgba(0,0,0,0.03); --border:rgba(0,0,0,0.1);
|
|
20
|
+
--fg:#111113; --fg-dim:#6b7280;
|
|
21
|
+
}
|
|
22
|
+
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
|
23
|
+
html,body{height:100%;overflow:hidden}
|
|
24
|
+
body{font-family:system-ui,-apple-system,"Segoe UI",Roboto,Inter,sans-serif;background:var(--bg-base);color:var(--fg);font-size:14px;line-height:1.5}
|
|
25
|
+
button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}
|
|
26
|
+
input{font-family:inherit;border:none;outline:none;background:none;color:inherit}
|
|
27
|
+
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|
28
|
+
|
|
29
|
+
/* ---- SHELL ---- */
|
|
30
|
+
.shell{display:flex;height:100vh;overflow:hidden}
|
|
31
|
+
|
|
32
|
+
/* ---- SIDEBAR ---- */
|
|
33
|
+
.sidebar{
|
|
34
|
+
width:var(--sidebar-w);flex-shrink:0;
|
|
35
|
+
background:var(--bg-deep);border-right:1px solid var(--border);
|
|
36
|
+
display:flex;flex-direction:column;
|
|
37
|
+
transition:width 220ms ease;overflow:hidden;
|
|
38
|
+
}
|
|
39
|
+
.sidebar.col{width:52px}
|
|
40
|
+
.sb-header{
|
|
41
|
+
height:56px;display:flex;align-items:center;gap:10px;
|
|
42
|
+
padding:0 14px;border-bottom:1px solid var(--border);flex-shrink:0;
|
|
43
|
+
}
|
|
44
|
+
.sb-brand{font-size:16px;font-weight:700;color:var(--fg);white-space:nowrap}
|
|
45
|
+
.sb-brand em{color:var(--accent);font-style:normal}
|
|
46
|
+
.sb-toggle{
|
|
47
|
+
margin-left:auto;width:28px;height:28px;border-radius:var(--radius-sm);
|
|
48
|
+
display:flex;align-items:center;justify-content:center;
|
|
49
|
+
color:var(--fg-dim);flex-shrink:0;
|
|
50
|
+
transition:background var(--trans);
|
|
51
|
+
}
|
|
52
|
+
.sb-toggle:hover{background:var(--surface);color:var(--fg)}
|
|
53
|
+
.sb-toggle svg{transition:transform 220ms ease}
|
|
54
|
+
.sidebar.col .sb-toggle svg{transform:scaleX(-1)}
|
|
55
|
+
|
|
56
|
+
.sb-nav{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0}
|
|
57
|
+
.sb-group-label{
|
|
58
|
+
font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;
|
|
59
|
+
color:var(--fg-dim);opacity:.5;padding:12px 16px 4px;white-space:nowrap;overflow:hidden;
|
|
60
|
+
}
|
|
61
|
+
.sidebar.col .sb-group-label{opacity:0}
|
|
62
|
+
.sb-item{
|
|
63
|
+
display:flex;align-items:center;gap:10px;padding:7px 14px;
|
|
64
|
+
font-size:13px;font-weight:500;color:var(--fg-dim);cursor:pointer;
|
|
65
|
+
border-left:2px solid transparent;white-space:nowrap;overflow:hidden;
|
|
66
|
+
transition:background var(--trans),color var(--trans),border-color var(--trans);
|
|
67
|
+
min-height:36px;
|
|
68
|
+
}
|
|
69
|
+
.sb-item:hover{background:var(--surface);color:var(--fg)}
|
|
70
|
+
.sb-item.on{background:rgba(94,106,210,0.08);color:var(--fg);border-left-color:var(--accent)}
|
|
71
|
+
.sb-icon{flex-shrink:0;width:18px;text-align:center;font-size:14px}
|
|
72
|
+
.sb-label{overflow:hidden;text-overflow:ellipsis}
|
|
73
|
+
.sidebar.col .sb-label{display:none}
|
|
74
|
+
.sidebar.col .sb-group-label span{display:none}
|
|
75
|
+
|
|
76
|
+
.sb-footer{
|
|
77
|
+
padding:12px 14px;border-top:1px solid var(--border);
|
|
78
|
+
display:flex;align-items:center;gap:8px;flex-shrink:0;
|
|
79
|
+
}
|
|
80
|
+
.sb-avatar{width:28px;height:28px;border-radius:50%;background:rgba(94,106,210,0.2);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--accent);flex-shrink:0}
|
|
81
|
+
.sb-user{font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden}
|
|
82
|
+
.sidebar.col .sb-user{display:none}
|
|
83
|
+
|
|
84
|
+
/* ---- MAIN ---- */
|
|
85
|
+
.main-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden}
|
|
86
|
+
.main-header{
|
|
87
|
+
height:56px;display:flex;align-items:center;padding:0 20px;
|
|
88
|
+
background:var(--bg-deep);border-bottom:1px solid var(--border);
|
|
89
|
+
flex-shrink:0;gap:10px;
|
|
90
|
+
}
|
|
91
|
+
.breadcrumb{font-size:13px;color:var(--fg-dim)}
|
|
92
|
+
.breadcrumb b{color:var(--fg)}
|
|
93
|
+
.main-header .spacer{flex:1}
|
|
94
|
+
.tier-pill{padding:4px 10px;border-radius:var(--radius-sm);background:rgba(94,106,210,0.12);font-size:11px;font-weight:600;color:var(--accent)}
|
|
95
|
+
.icon-btn{padding:5px 10px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;color:var(--fg-dim);transition:background var(--trans),color var(--trans)}
|
|
96
|
+
.icon-btn:hover{background:var(--surface);color:var(--fg)}
|
|
97
|
+
|
|
98
|
+
.main-content{flex:1;overflow-y:auto;padding:24px}
|
|
99
|
+
|
|
100
|
+
/* ---- SECTIONS (shown/hidden) ---- */
|
|
101
|
+
.section{display:none}
|
|
102
|
+
.section.on{display:block}
|
|
103
|
+
|
|
104
|
+
/* ---- LAYOUT ---- */
|
|
105
|
+
.hgrid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
|
|
106
|
+
@media(max-width:900px){.hgrid{grid-template-columns:repeat(2,1fr)}}
|
|
107
|
+
.row{display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px}
|
|
108
|
+
.col{flex:1;min-width:0}
|
|
109
|
+
.card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:16px}
|
|
110
|
+
.ctitle{font-size:14px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px}
|
|
111
|
+
.iinfo{font-size:11px;color:var(--fg-dim);cursor:pointer;opacity:.6;transition:opacity var(--trans)}
|
|
112
|
+
.iinfo:hover{opacity:1}
|
|
113
|
+
.hcard{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
|
|
114
|
+
.hlabel{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--fg-dim);margin-bottom:6px}
|
|
115
|
+
.hval{font-size:32px;font-weight:700;line-height:1;letter-spacing:-0.5px;margin-bottom:4px}
|
|
116
|
+
.hsub{font-size:12px;color:var(--fg-dim)}
|
|
117
|
+
.hsub b{color:var(--fg);font-weight:600}
|
|
118
|
+
table{width:100%;border-collapse:collapse}
|
|
119
|
+
th{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.06em;color:var(--fg-dim);padding:6px 8px 8px;border-bottom:1px solid var(--border);text-align:left}
|
|
120
|
+
td{padding:8px;font-size:13px;border-bottom:1px solid var(--border);vertical-align:middle}
|
|
121
|
+
tr:last-child td{border-bottom:none}
|
|
122
|
+
tr:hover td{background:var(--surface)}
|
|
123
|
+
.nr{text-align:right;font-variant-numeric:tabular-nums;font-family:ui-monospace,monospace}
|
|
124
|
+
.chip{display:inline-flex;align-items:center;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600}
|
|
125
|
+
.cc{background:rgba(217,119,87,0.15);color:var(--claude)}
|
|
126
|
+
.co{background:rgba(94,106,210,0.15);color:var(--accent)}
|
|
127
|
+
.disc{font-size:11px;color:var(--fg-dim);padding:8px 10px;background:rgba(245,158,11,0.06);border:1px solid rgba(245,158,11,0.15);border-radius:var(--radius-sm);margin-top:8px;line-height:1.5}
|
|
128
|
+
.disc em{font-style:normal;color:var(--warn);font-weight:600}
|
|
129
|
+
.btn-g{padding:6px 12px;border-radius:var(--radius-sm);font-size:12px;font-weight:500;border:1px solid var(--border);color:var(--fg-dim);transition:background var(--trans),color var(--trans),border-color var(--trans)}
|
|
130
|
+
.btn-g:hover{background:var(--surface);color:var(--fg)}
|
|
131
|
+
.pulse{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--success);animation:pa 2s ease-in-out infinite;vertical-align:middle}
|
|
132
|
+
@keyframes pa{0%,100%{opacity:1}50%{opacity:.3}}
|
|
133
|
+
.wbar{height:20px;background:var(--border);border-radius:10px;overflow:hidden;display:flex;margin-bottom:6px}
|
|
134
|
+
.wseg{background:var(--claude);height:100%}
|
|
135
|
+
.wmeta{display:flex;justify-content:space-between;font-size:12px;color:var(--fg-dim)}
|
|
136
|
+
.wok{color:var(--success);font-weight:600}
|
|
137
|
+
.feed-row{display:flex;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);font-size:12px}
|
|
138
|
+
.feed-row:last-child{border-bottom:none}
|
|
139
|
+
.fts{color:var(--fg-dim);white-space:nowrap;font-family:ui-monospace,monospace}
|
|
140
|
+
.ftype{min-width:52px;padding:1px 6px;border-radius:3px;font-size:11px;font-weight:600;text-align:center;align-self:flex-start;margin-top:1px}
|
|
141
|
+
.ftype.recall{background:rgba(94,106,210,0.15);color:var(--accent)}
|
|
142
|
+
.fbody{flex:1;color:var(--fg)}
|
|
143
|
+
.empty{text-align:center;padding:40px 20px;color:var(--fg-dim)}
|
|
144
|
+
.empty-icon{font-size:32px;margin-bottom:10px;opacity:.4}
|
|
145
|
+
|
|
146
|
+
/* Memory tree */
|
|
147
|
+
.mem-layout{display:flex;gap:0;height:calc(100vh - 180px);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
|
148
|
+
.mem-l{width:300px;flex-shrink:0;border-right:1px solid var(--border);display:flex;flex-direction:column;background:var(--bg-elevated)}
|
|
149
|
+
.mem-r{flex:1;overflow-y:auto;background:var(--bg-base);padding:20px}
|
|
150
|
+
.mem-search{padding:10px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px}
|
|
151
|
+
.mem-search input{flex:1;font-size:13px}
|
|
152
|
+
.mem-search input::placeholder{color:var(--fg-dim)}
|
|
153
|
+
.mem-list{flex:1;overflow-y:auto}
|
|
154
|
+
.tier-hdr{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:0.07em;color:var(--fg-dim);padding:10px 12px 4px;display:flex;justify-content:space-between;cursor:pointer}
|
|
155
|
+
.tier-hdr:hover{color:var(--fg)}
|
|
156
|
+
.mfile{display:flex;align-items:center;justify-content:space-between;padding:7px 20px 7px 24px;cursor:pointer;transition:background var(--trans);border-left:2px solid transparent}
|
|
157
|
+
.mfile:hover{background:var(--surface)}
|
|
158
|
+
.mfile.on{background:rgba(94,106,210,0.08);border-left-color:var(--accent)}
|
|
159
|
+
.mfname{font-size:12px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}
|
|
160
|
+
.mfrec{font-size:11px;color:var(--fg-dim);white-space:nowrap}
|
|
161
|
+
.pv-title{font-size:18px;font-weight:700;margin-bottom:8px}
|
|
162
|
+
.pv-meta{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:12px;font-size:12px;color:var(--fg-dim)}
|
|
163
|
+
.pv-meta b{color:var(--fg);font-weight:600}
|
|
164
|
+
.ptag{display:inline-block;padding:2px 7px;border-radius:3px;font-size:11px;background:var(--surface);border:1px solid var(--border);color:var(--fg-dim);margin:0 3px 3px 0}
|
|
165
|
+
.pv-body{font-size:14px;line-height:1.7;color:var(--fg-dim)}
|
|
166
|
+
.pv-body p{margin-bottom:10px}
|
|
167
|
+
.pv-body strong{color:var(--fg)}
|
|
168
|
+
.pv-actions{display:flex;gap:8px;margin-top:16px;padding-top:12px;border-top:1px solid var(--border)}
|
|
169
|
+
.mem-preview{display:none}
|
|
170
|
+
.mem-preview.on{display:block}
|
|
171
|
+
|
|
172
|
+
/* Settings */
|
|
173
|
+
.sgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
|
|
174
|
+
.scard{background:var(--bg-elevated);border:1px solid var(--border);border-radius:var(--radius);padding:16px}
|
|
175
|
+
.scard h3{font-size:14px;font-weight:600;margin-bottom:12px}
|
|
176
|
+
.srow{display:flex;align-items:center;justify-content:space-between;padding:6px 0;border-bottom:1px solid var(--border)}
|
|
177
|
+
.srow:last-child{border-bottom:none}
|
|
178
|
+
.slbl{font-size:13px;color:var(--fg-dim)}
|
|
179
|
+
.sval{font-size:13px;font-weight:500}
|
|
180
|
+
.tog{position:relative;width:36px;height:20px;cursor:pointer}
|
|
181
|
+
.tog input{opacity:0;width:0;height:0;position:absolute}
|
|
182
|
+
.tog-track{position:absolute;inset:0;border-radius:10px;background:var(--border);transition:background var(--trans)}
|
|
183
|
+
.tog input:checked + .tog-track{background:var(--accent)}
|
|
184
|
+
.tog-thumb{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:white;transition:transform var(--trans)}
|
|
185
|
+
.tog input:checked ~ .tog-thumb{transform:translateX(16px)}
|
|
186
|
+
.spark{height:80px;margin:8px 0}
|
|
187
|
+
.spark svg{width:100%;height:80px;display:block}
|
|
188
|
+
::-webkit-scrollbar{width:6px;height:6px}
|
|
189
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
190
|
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
191
|
+
::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.2)}
|
|
192
|
+
@media(max-width:700px){
|
|
193
|
+
.sidebar{display:none}
|
|
194
|
+
.mem-layout{flex-direction:column;height:auto}
|
|
195
|
+
.mem-l{width:100%;border-right:none;border-bottom:1px solid var(--border);max-height:280px}
|
|
196
|
+
}
|
|
197
|
+
</style>
|
|
198
|
+
</head>
|
|
199
|
+
<body>
|
|
200
|
+
<div class="shell">
|
|
201
|
+
|
|
202
|
+
<!-- SIDEBAR -->
|
|
203
|
+
<aside class="sidebar" id="sidebar" role="navigation" aria-label="Main navigation">
|
|
204
|
+
<div class="sb-header">
|
|
205
|
+
<div class="sb-brand">IJ<em>FW</em></div>
|
|
206
|
+
<button class="sb-toggle" id="sbToggle" aria-label="Collapse sidebar" aria-expanded="true" aria-controls="sidebar">
|
|
207
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
|
208
|
+
<line x1="2" y1="4" x2="14" y2="4"/><line x1="2" y1="8" x2="14" y2="8"/><line x1="2" y1="12" x2="14" y2="12"/>
|
|
209
|
+
</svg>
|
|
210
|
+
</button>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<nav class="sb-nav" id="sbNav">
|
|
214
|
+
<div class="sb-group-label"><span>Overview</span></div>
|
|
215
|
+
<button class="sb-item on" data-section="today" aria-current="page"><span class="sb-icon">■</span><span class="sb-label">Overview</span></button>
|
|
216
|
+
<button class="sb-item" data-section="live"><span class="sb-icon">⬤</span><span class="sb-label">Live Feed</span></button>
|
|
217
|
+
|
|
218
|
+
<div class="sb-group-label"><span>Cost</span></div>
|
|
219
|
+
<button class="sb-item" data-section="spend"><span class="sb-icon">$</span><span class="sb-label">Usage</span></button>
|
|
220
|
+
|
|
221
|
+
<div class="sb-group-label"><span>Memory</span></div>
|
|
222
|
+
<button class="sb-item" data-section="memsearch"><span class="sb-icon">🔍</span><span class="sb-label">Memory</span></button>
|
|
223
|
+
<button class="sb-item" data-section="sessions"><span class="sb-icon">📋</span><span class="sb-label">Sessions</span></button>
|
|
224
|
+
<button class="sb-item" data-section="handoffs"><span class="sb-icon">🕓</span><span class="sb-label">Handoffs</span></button>
|
|
225
|
+
|
|
226
|
+
<div class="sb-group-label"><span>Projects</span></div>
|
|
227
|
+
<button class="sb-item" data-section="allprojects"><span class="sb-icon">☰</span><span class="sb-label">Projects (17)</span></button>
|
|
228
|
+
|
|
229
|
+
<div class="sb-group-label"><span>Audits</span></div>
|
|
230
|
+
<button class="sb-item" data-section="trident"><span class="sb-icon">▲</span><span class="sb-label">Cross-AI Reviews</span></button>
|
|
231
|
+
|
|
232
|
+
<div class="sb-group-label"><span>Settings</span></div>
|
|
233
|
+
<button class="sb-item" data-section="subs"><span class="sb-icon">⚗</span><span class="sb-label">Settings</span></button>
|
|
234
|
+
</nav>
|
|
235
|
+
|
|
236
|
+
<div class="sb-footer">
|
|
237
|
+
<div class="sb-avatar">SD</div>
|
|
238
|
+
<div class="sb-user">Sean Donahoe</div>
|
|
239
|
+
</div>
|
|
240
|
+
</aside>
|
|
241
|
+
|
|
242
|
+
<!-- MAIN -->
|
|
243
|
+
<div class="main-wrap">
|
|
244
|
+
<header class="main-header" role="banner">
|
|
245
|
+
<div class="breadcrumb" id="breadcrumb"><span style="color:var(--fg-dim)">Overview</span> <b>Today</b></div>
|
|
246
|
+
<div class="spacer"></div>
|
|
247
|
+
<span class="tier-pill">MAX 20x</span>
|
|
248
|
+
<button class="icon-btn" id="themeBtn" aria-label="Toggle theme">☾ Theme</button>
|
|
249
|
+
</header>
|
|
250
|
+
|
|
251
|
+
<main class="main-content" id="mainContent">
|
|
252
|
+
|
|
253
|
+
<!-- ======== TODAY ======== -->
|
|
254
|
+
<div class="section on" data-section="today">
|
|
255
|
+
<div class="card" style="padding:12px 16px;margin-bottom:16px;background:rgba(94,106,210,0.06);border-color:rgba(94,106,210,0.2)">
|
|
256
|
+
<span style="font-size:13px;color:var(--fg-dim)">New here? Try asking your AI to <b style="color:var(--fg)">"help me plan"</b> or <b style="color:var(--fg)">"review my code"</b> -- IJFW handles the rest. Memory and costs appear here as you work.</span>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="hgrid">
|
|
259
|
+
<div class="hcard">
|
|
260
|
+
<div class="hlabel">Today's Cost</div>
|
|
261
|
+
<div class="hval" data-live="cost-today">$981</div>
|
|
262
|
+
<div class="hsub" data-live="cost-today-sub"><b>5</b> sessions, 5 projects</div>
|
|
263
|
+
<div style="margin-top:6px"><span style="font-size:11px;color:var(--fg-dim)">Claude, measured from API tokens</span></div>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="hcard">
|
|
266
|
+
<div class="hlabel"><span class="pulse"></span> Active Session</div>
|
|
267
|
+
<div class="hval" style="font-size:20px;padding-top:6px">ijfw</div>
|
|
268
|
+
<div class="hsub">claude-opus-4-6 -- <b>current</b></div>
|
|
269
|
+
</div>
|
|
270
|
+
<div class="hcard">
|
|
271
|
+
<div class="hlabel">Cache Efficiency</div>
|
|
272
|
+
<div class="hval" style="color:var(--success)">97.0%</div>
|
|
273
|
+
<div class="hsub">2.71B tokens served from cache (30d)</div>
|
|
274
|
+
<div class="disc">Claude Code caches automatically. IJFW measures your efficiency.</div>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="hcard">
|
|
277
|
+
<div class="hlabel">30-Day Actual</div>
|
|
278
|
+
<div class="hval" data-live="cost-30d">$6,546</div>
|
|
279
|
+
<div class="hsub" style="color:var(--fg-dim)">Claude measured</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<div class="card">
|
|
283
|
+
<div class="ctitle">Today by Project</div>
|
|
284
|
+
<table aria-label="Today spend by project">
|
|
285
|
+
<thead><tr><th>Project</th><th>Platform</th><th class="nr">Spend</th></tr></thead>
|
|
286
|
+
<tbody id="today-project-tbody">
|
|
287
|
+
<tr><td>ijfw</td><td><span class="chip cc">Claude</span></td><td class="nr">$392</td></tr>
|
|
288
|
+
<tr><td>wayland</td><td><span class="chip cc">Claude</span></td><td class="nr">$241</td></tr>
|
|
289
|
+
<tr><td>aion</td><td><span class="chip cc">Claude</span></td><td class="nr">$223</td></tr>
|
|
290
|
+
<tr><td>socialmedia</td><td><span class="chip cc">Claude</span></td><td class="nr">$124</td></tr>
|
|
291
|
+
<tr><td style="color:var(--fg-dim)">others</td><td></td><td class="nr" style="color:var(--fg-dim)">$1</td></tr>
|
|
292
|
+
</tbody>
|
|
293
|
+
</table>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<!-- ======== LIVE FEED ======== -->
|
|
298
|
+
<div class="section" data-section="live">
|
|
299
|
+
<div class="card">
|
|
300
|
+
<div class="ctitle">Live Feed <span class="pulse"></span> <span style="font-size:11px;color:var(--fg-dim);margin-left:auto">Real data from ~/.ijfw/observations.jsonl (50 entries)</span></div>
|
|
301
|
+
<div class="feed-row"><span class="fts">15:37</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
302
|
+
<div class="feed-row"><span class="fts">15:19</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
303
|
+
<div class="feed-row"><span class="fts">15:16</span><span class="ftype recall">recall</span><span class="fbody">session_start -- decisions (mcp)</span></div>
|
|
304
|
+
<div class="feed-row"><span class="fts">15:15</span><span class="ftype recall">recall</span><span class="fbody">session_start -- decisions (mcp)</span></div>
|
|
305
|
+
<div class="feed-row"><span class="fts">15:05</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
306
|
+
<div class="feed-row"><span class="fts">14:59</span><span class="ftype recall">recall</span><span class="fbody">session_start -- decisions (mcp)</span></div>
|
|
307
|
+
<div class="feed-row"><span class="fts">14:58</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
308
|
+
<div class="feed-row"><span class="fts">13:48</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
309
|
+
<div class="feed-row"><span class="fts">13:47</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
310
|
+
<div class="feed-row"><span class="fts">13:46</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
311
|
+
<div class="feed-row"><span class="fts">13:33</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
312
|
+
<div class="feed-row"><span class="fts">13:29</span><span class="ftype recall">recall</span><span class="fbody">decisions -- session_start (mcp)</span></div>
|
|
313
|
+
<div style="padding:8px 0;font-size:12px;color:var(--fg-dim)">Showing memory-recall events. Tool call and file edit events populate as sessions run.</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<!-- ======== USAGE LIMIT (was 5H WINDOW) ======== -->
|
|
318
|
+
<div class="section" data-section="window">
|
|
319
|
+
<div class="card">
|
|
320
|
+
<div class="ctitle">Usage Limit</div>
|
|
321
|
+
<div style="margin-bottom:16px">
|
|
322
|
+
<div class="wbar"><div class="wseg" style="width:47%"></div><div style="flex:1;background:var(--surface)"></div></div>
|
|
323
|
+
<div class="wmeta"><span>2h 21m used of 5h window</span><span class="wok">2h 39m remaining -- 53%</span></div>
|
|
324
|
+
</div>
|
|
325
|
+
<div style="font-size:13px;color:var(--fg-dim);margin-bottom:16px">
|
|
326
|
+
<button class="btn-g" style="font-size:11px;padding:2px 8px" onclick="goSection('accounts')">+ Add account label</button>
|
|
327
|
+
<span style="margin-left:8px;font-size:12px">Account labels are optional -- add one to track multiple subscriptions.</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="disc">Usage window shown as an estimate based on today's token spend vs. your daily average.</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<!-- ======== SPEND ======== -->
|
|
334
|
+
<div class="section" data-section="spend">
|
|
335
|
+
<div class="card">
|
|
336
|
+
<div class="ctitle">Spend by Project (30 days, Claude measured)</div>
|
|
337
|
+
<table aria-label="Spend by project 30 days">
|
|
338
|
+
<thead><tr><th>Project</th><th>Last Active</th><th class="nr">30d Cost</th><th class="nr">%</th></tr></thead>
|
|
339
|
+
<tbody id="spend-project-tbody">
|
|
340
|
+
<tr><td><strong>ijfw</strong></td><td>2026-04-16</td><td class="nr">$2,736</td><td class="nr">42%</td></tr>
|
|
341
|
+
<tr><td>wayland</td><td>2026-04-16</td><td class="nr">$2,126</td><td class="nr">32%</td></tr>
|
|
342
|
+
<tr><td>Flow</td><td>2026-03-27</td><td class="nr">$662</td><td class="nr">10%</td></tr>
|
|
343
|
+
<tr><td>socialmedia</td><td>2026-04-14</td><td class="nr">$605</td><td class="nr">9%</td></tr>
|
|
344
|
+
<tr><td>Crucible</td><td>2026-03-25</td><td class="nr">$110</td><td class="nr">2%</td></tr>
|
|
345
|
+
<tr><td>hearth</td><td>2026-03-30</td><td class="nr">$89</td><td class="nr">1%</td></tr>
|
|
346
|
+
<tr><td>PiPLLM</td><td>2026-03-18</td><td class="nr">$45</td><td class="nr"><1%</td></tr>
|
|
347
|
+
<tr><td>openclaw</td><td>2026-04-14</td><td class="nr">$45</td><td class="nr"><1%</td></tr>
|
|
348
|
+
<tr><td>mindgraph</td><td>2026-04-08</td><td class="nr">$40</td><td class="nr"><1%</td></tr>
|
|
349
|
+
<tr><td>aion</td><td>2026-04-16</td><td class="nr">$23</td><td class="nr"><1%</td></tr>
|
|
350
|
+
<tr><td>ContentDev</td><td>2026-04-07</td><td class="nr">$19</td><td class="nr"><1%</td></tr>
|
|
351
|
+
<tr><td>dev</td><td>2026-03-31</td><td class="nr">$13</td><td class="nr"><1%</td></tr>
|
|
352
|
+
<tr><td>pip</td><td>2026-04-11</td><td class="nr">$10</td><td class="nr"><1%</td></tr>
|
|
353
|
+
<tr><td>twitterclean</td><td>2026-04-05</td><td class="nr">$8</td><td class="nr"><1%</td></tr>
|
|
354
|
+
<tr><td>deepcode</td><td>2026-03-26</td><td class="nr">$7</td><td class="nr"><1%</td></tr>
|
|
355
|
+
<tr><td>TestBEd</td><td>2026-04-13</td><td class="nr">$5</td><td class="nr"><1%</td></tr>
|
|
356
|
+
<tr><td>tradecanyon-sentinal</td><td>2026-03-24</td><td class="nr">$4</td><td class="nr"><1%</td></tr>
|
|
357
|
+
</tbody>
|
|
358
|
+
</table>
|
|
359
|
+
<div class="disc">Claude cost is measured precisely from API tokens. Codex and Gemini costs are flat-rate subscriptions -- shown separately for clarity.</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<!-- ======== MODELS ======== -->
|
|
364
|
+
<div class="section" data-section="models">
|
|
365
|
+
<div class="card">
|
|
366
|
+
<div class="ctitle">By Model (Claude measured, 30d)</div>
|
|
367
|
+
<table aria-label="Spend by model">
|
|
368
|
+
<thead><tr><th>Model</th><th class="nr">30d Cost</th><th class="nr">Cache hit</th><th class="nr">Sessions</th></tr></thead>
|
|
369
|
+
<tbody id="spend-model-tbody">
|
|
370
|
+
<tr><td><span class="chip co">Opus 4.6</span></td><td class="nr">$6,544</td><td class="nr">97.0%</td><td class="nr">--</td></tr>
|
|
371
|
+
<tr><td><span class="chip" style="background:var(--surface);color:var(--fg-dim)">Sonnet 4.6</span></td><td class="nr">$2</td><td class="nr">--</td><td class="nr">--</td></tr>
|
|
372
|
+
<tr><td colspan="4" style="font-size:12px;color:var(--fg-dim);padding-top:10px">Codex and Gemini are flat-rate subscriptions -- shown separately from per-token Claude costs.</td></tr>
|
|
373
|
+
</tbody>
|
|
374
|
+
</table>
|
|
375
|
+
<div class="disc">Opus 4.6 is your primary model -- 99.9% of Claude cost. Cache hit rate 97.0% is measured from 2.71B cache_read tokens over 30 days.</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
|
|
379
|
+
<!-- ======== ACTIVITY ======== -->
|
|
380
|
+
<div class="section" data-section="activity">
|
|
381
|
+
<div class="card">
|
|
382
|
+
<div class="ctitle">Activity Types</div>
|
|
383
|
+
<div class="empty">
|
|
384
|
+
<div class="empty-icon">📈</div>
|
|
385
|
+
<p>Activity tracking activates as you work. Types and one-shot rates appear here.</p>
|
|
386
|
+
<p style="margin-top:8px;font-size:12px;color:var(--fg-dim)">Activity classification populates automatically from observed tool usage.</p>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<!-- ======== TREND ======== -->
|
|
392
|
+
<div class="section" data-section="trend">
|
|
393
|
+
<div class="card">
|
|
394
|
+
<div class="ctitle">30-Day Cost Trend (Claude measured)</div>
|
|
395
|
+
<div class="spark" aria-label="30-day cost sparkline">
|
|
396
|
+
<svg viewBox="0 0 700 80" preserveAspectRatio="none" role="img" aria-label="Upward cost trend over 30 days">
|
|
397
|
+
<defs>
|
|
398
|
+
<linearGradient id="g2" x1="0" y1="0" x2="0" y2="1">
|
|
399
|
+
<stop offset="0%" stop-color="#D97757" stop-opacity="0.35"/>
|
|
400
|
+
<stop offset="100%" stop-color="#D97757" stop-opacity="0.03"/>
|
|
401
|
+
</linearGradient>
|
|
402
|
+
</defs>
|
|
403
|
+
<polygon points="0,74 35,70 70,68 105,63 140,58 175,53 210,48 245,44 280,39 315,33 350,28 385,22 420,18 455,14 490,10 525,7 560,5 595,3 630,2 665,1 700,1 700,80 0,80" fill="url(#g2)"/>
|
|
404
|
+
<polyline points="0,74 35,70 70,68 105,63 140,58 175,53 210,48 245,44 280,39 315,33 350,28 385,22 420,18 455,14 490,10 525,7 560,5 595,3 630,2 665,1 700,1" fill="none" stroke="#D97757" stroke-width="2"/>
|
|
405
|
+
</svg>
|
|
406
|
+
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--fg-dim);margin-top:4px">
|
|
407
|
+
<span id="trend-start-label">Mar 17</span><span id="trend-total-label">Total: $6,546 Claude (30d measured)</span><span id="trend-end-label">Apr 16</span>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="disc">Claude measured cost only. Codex/Gemini subscriptions ($270/mo combined) are flat-rate and excluded from this trend.</div>
|
|
411
|
+
</div>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<!-- ======== MEMORY SEARCH ======== -->
|
|
415
|
+
<div class="section" data-section="memsearch">
|
|
416
|
+
<div class="card">
|
|
417
|
+
<div class="ctitle" id="memsearch-title">Memory Search -- all 35 files</div>
|
|
418
|
+
<div class="mem-layout">
|
|
419
|
+
<div class="mem-l">
|
|
420
|
+
<div class="mem-search">
|
|
421
|
+
<span style="color:var(--fg-dim)">/</span>
|
|
422
|
+
<input type="search" id="memSearchB" placeholder="Search 35 files..." aria-label="Search memory" oninput="onMemSearch(this.value)">
|
|
423
|
+
</div>
|
|
424
|
+
<div class="mem-list" id="memListB" role="listbox" aria-label="Memory files">
|
|
425
|
+
<div class="tier-hdr" id="mem-tier-hdr">Project Memory (35)</div>
|
|
426
|
+
<div class="mfile on" role="option" aria-selected="true" tabindex="0" data-key="loop" onclick="pickMemB(this,'loop')"><span class="mfname">Donahoe Loop <span class="ptag" style="margin-left:4px">pattern</span></span><span class="mfrec" title="Recalled 170 times across sessions">170 recalls</span></div>
|
|
427
|
+
<div class="mfile" role="option" tabindex="0" data-key="posframe" onclick="pickMemB(this,'posframe')"><span class="mfname">Positive Framing is Sacred <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">12 recalls</span></div>
|
|
428
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Memory Tiers <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">8 recalls</span></div>
|
|
429
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Brainstorm is a Conversation <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">6 recalls</span></div>
|
|
430
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Cross-AI Combo Policy <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">5 recalls</span></div>
|
|
431
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Workflow Owns Execution <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">4 recalls</span></div>
|
|
432
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Cross-Audit UX <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">4 recalls</span></div>
|
|
433
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Ownership Discipline <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">3 recalls</span></div>
|
|
434
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Always Recommend and Show Progress <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">3 recalls</span></div>
|
|
435
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Dual-Mode Workflow <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">2 recalls</span></div>
|
|
436
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Workflow Clarity and Navigation <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">2 recalls</span></div>
|
|
437
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">No Proxy Rule <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">2 recalls</span></div>
|
|
438
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Hook Event Semantics <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">2 recalls</span></div>
|
|
439
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Cross-Audit Must Auto-Fire <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
440
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Claude-Side Specialist Swarm <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
441
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Phase 7 Scope <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
442
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">IJFW Forge Portability <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
443
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Installer Must Merge <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
444
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Plugin Auto-Register MCP <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
445
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Prelude Tool <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
446
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">FTS5 Deferred <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
447
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Live Todo Visibility <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
448
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Don't Make Me Think Cross-Audit <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
449
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Book-Tool Recursion <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
450
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Session State 2026-04-14 <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
451
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Migration Philosophy <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
452
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Mockups Must Be Real HTML <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
453
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Auto-Invoke Design Skills <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
454
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Design Requires Approval Before Build <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
455
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Prompt Improver <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
456
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Cross-Project Search <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
457
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Never Publish Without Live E2E <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
458
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">No Hanging Items <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
459
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">Reports Must Be Self-Contained <span class="ptag" style="margin-left:4px">feedback</span></span><span class="mfrec">1 recall</span></div>
|
|
460
|
+
<div class="mfile" role="option" tabindex="0" onclick="pickMemB(this,'generic')"><span class="mfname">IJFW Design Dispatcher <span class="ptag" style="margin-left:4px">project</span></span><span class="mfrec">1 recall</span></div>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="mem-r" id="memRightB">
|
|
464
|
+
<div class="mem-preview on" data-key="loop">
|
|
465
|
+
<div class="pv-title">Donahoe Loop</div>
|
|
466
|
+
<div class="pv-meta"><span>Project Memory</span><span>Recalls: <b>170</b></span><span>~1.2 KB</span><span>Last: <b>2026-04-16 15:37</b></span></div>
|
|
467
|
+
<div style="margin-bottom:10px"><span class="ptag">pattern</span><span class="ptag">core</span><span class="ptag">IJFW</span></div>
|
|
468
|
+
<div class="pv-body">
|
|
469
|
+
<p><strong>Donahoe Loop</strong> -- canonical IJFW loop: audit gates between every stage, Multi-AI Trident handoff at each gate.</p>
|
|
470
|
+
<p>Plan -- Audit -- Execute -- Verify -- Ship, with cross-AI review at each gate.</p>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="pv-actions"><button class="btn-g">Copy path</button><button class="btn-g">Open in editor</button></div>
|
|
473
|
+
</div>
|
|
474
|
+
<div class="mem-preview" data-key="posframe">
|
|
475
|
+
<div class="pv-title">Positive Framing is Sacred</div>
|
|
476
|
+
<div class="pv-meta"><span>Project Memory</span><span>Recalls: <b>12</b></span><span>~0.8 KB</span></div>
|
|
477
|
+
<div style="margin-bottom:10px"><span class="ptag">feedback</span><span class="ptag">UX</span></div>
|
|
478
|
+
<div class="pv-body"><p><strong>Positive framing is sacred</strong> -- no negatives ever in user-facing surfaces. Sutherland reframe rule: reposition every message as opportunity.</p></div>
|
|
479
|
+
<div class="pv-actions"><button class="btn-g">Copy path</button><button class="btn-g">Open in editor</button></div>
|
|
480
|
+
</div>
|
|
481
|
+
<div class="mem-preview" data-key="generic">
|
|
482
|
+
<div class="pv-title" id="bGfTitle">--</div>
|
|
483
|
+
<div class="pv-meta"><span>Project Memory</span><span id="bGfRec">1 recall</span></div>
|
|
484
|
+
<div class="pv-body"><p>Open in editor to view full file content.</p></div>
|
|
485
|
+
<div class="pv-actions"><button class="btn-g">Copy path</button><button class="btn-g">Open in editor</button></div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<!-- ======== SESSIONS ======== -->
|
|
493
|
+
<div class="section" data-section="sessions">
|
|
494
|
+
<div class="card">
|
|
495
|
+
<div class="ctitle">Sessions (48 in 30 days)</div>
|
|
496
|
+
<table aria-label="Session list">
|
|
497
|
+
<thead><tr><th>Date</th><th>Project</th><th>Model</th><th>Trident</th></tr></thead>
|
|
498
|
+
<tbody>
|
|
499
|
+
<tr style="background:rgba(94,106,210,0.05)"><td>2026-04-16</td><td><strong>ijfw</strong></td><td style="color:var(--fg-dim);font-size:12px">opus-4-6</td><td style="color:var(--success)">active</td></tr>
|
|
500
|
+
<tr><td>2026-04-16</td><td>wayland</td><td style="color:var(--fg-dim);font-size:12px">opus-4-6</td><td style="color:var(--fg-dim)">--</td></tr>
|
|
501
|
+
<tr><td>2026-04-16</td><td>aion</td><td style="color:var(--fg-dim);font-size:12px">opus-4-6</td><td style="color:var(--fg-dim)">--</td></tr>
|
|
502
|
+
<tr><td>2026-04-16</td><td>socialmedia</td><td style="color:var(--fg-dim);font-size:12px">opus-4-6</td><td style="color:var(--fg-dim)">--</td></tr>
|
|
503
|
+
<tr><td>2026-04-15</td><td>ijfw</td><td style="color:var(--fg-dim);font-size:12px">opus-4-6</td><td style="color:var(--fg-dim)">--</td></tr>
|
|
504
|
+
<tr><td colspan="4" style="color:var(--fg-dim);font-size:11px">48 total sessions over 30 days.</td></tr>
|
|
505
|
+
</tbody>
|
|
506
|
+
</table>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<!-- ======== HANDOFFS ======== -->
|
|
511
|
+
<div class="section" data-section="handoffs">
|
|
512
|
+
<div class="card">
|
|
513
|
+
<div class="ctitle">Handoff Documents</div>
|
|
514
|
+
<div id="handoffs-content">
|
|
515
|
+
<div class="empty">
|
|
516
|
+
<div class="empty-icon">🕓</div>
|
|
517
|
+
<p>No handoffs yet. Handoffs are created automatically at session end -- start a coding session to see them here.</p>
|
|
518
|
+
<p style="margin-top:8px;font-size:12px;color:var(--fg-dim)">Handoff documents appear here as you create them with /handoff.</p>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
</div>
|
|
523
|
+
|
|
524
|
+
<!-- ======== ALL PROJECTS ======== -->
|
|
525
|
+
<div class="section" data-section="allprojects">
|
|
526
|
+
<div class="card">
|
|
527
|
+
<div class="ctitle">All Projects (17 with spend, Claude measured 30d)</div>
|
|
528
|
+
<table aria-label="All projects">
|
|
529
|
+
<thead><tr><th>Project</th><th>Last Active</th><th class="nr">30d Cost</th><th>Model</th></tr></thead>
|
|
530
|
+
<tbody id="allprojects-tbody">
|
|
531
|
+
<tr><td><strong>ijfw</strong> <span style="font-size:11px;color:var(--accent)">current</span></td><td>2026-04-16</td><td class="nr">$2,736</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
532
|
+
<tr><td>wayland</td><td>2026-04-16</td><td class="nr">$2,126</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
533
|
+
<tr><td>Flow</td><td>2026-03-27</td><td class="nr">$662</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
534
|
+
<tr><td>socialmedia</td><td>2026-04-14</td><td class="nr">$605</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
535
|
+
<tr><td>Crucible</td><td>2026-03-25</td><td class="nr">$110</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
536
|
+
<tr><td>hearth</td><td>2026-03-30</td><td class="nr">$89</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
537
|
+
<tr><td>PiPLLM</td><td>2026-03-18</td><td class="nr">$45</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
538
|
+
<tr><td>openclaw</td><td>2026-04-14</td><td class="nr">$45</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
539
|
+
<tr><td>mindgraph</td><td>2026-04-08</td><td class="nr">$40</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
540
|
+
<tr><td>aion</td><td>2026-04-16</td><td class="nr">$23</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
541
|
+
<tr><td>ContentDev</td><td>2026-04-07</td><td class="nr">$19</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
542
|
+
<tr><td>dev</td><td>2026-03-31</td><td class="nr">$13</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
543
|
+
<tr><td>pip</td><td>2026-04-11</td><td class="nr">$10</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
544
|
+
<tr><td>twitterclean</td><td>2026-04-05</td><td class="nr">$8</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
545
|
+
<tr><td>deepcode</td><td>2026-03-26</td><td class="nr">$7</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
546
|
+
<tr><td>TestBEd</td><td>2026-04-13</td><td class="nr">$5</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
547
|
+
<tr><td>tradecanyon-sentinal</td><td>2026-03-24</td><td class="nr">$4</td><td><span class="chip co">Opus 4.6</span></td></tr>
|
|
548
|
+
</tbody>
|
|
549
|
+
</table>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<!-- ======== CROSS-AI REVIEWS ======== -->
|
|
554
|
+
<div class="section" data-section="trident">
|
|
555
|
+
<div class="card">
|
|
556
|
+
<div class="ctitle">Cross-AI Review History</div>
|
|
557
|
+
<div class="empty">
|
|
558
|
+
<div class="empty-icon">✓</div>
|
|
559
|
+
<p>Clean slate this week. Run /cross-audit to generate your first cross-AI review.</p>
|
|
560
|
+
<p style="margin-top:8px;font-size:12px;color:var(--fg-dim)">Cross-AI reviews get a second opinion from other AI engines -- findings appear here.</p>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
|
|
565
|
+
<!-- ======== SUBSCRIPTIONS ======== -->
|
|
566
|
+
<div class="section" data-section="subs">
|
|
567
|
+
<div class="sgrid">
|
|
568
|
+
<div class="scard">
|
|
569
|
+
<h3>Platform Subscriptions</h3>
|
|
570
|
+
<div id="subs-config-rows">
|
|
571
|
+
<div class="srow"><span class="slbl">Claude Max 20x</span><span class="sval">$200/mo</span></div>
|
|
572
|
+
<div class="srow"><span class="slbl">Codex Pro</span><span class="sval">$20/mo</span></div>
|
|
573
|
+
<div class="srow"><span class="slbl">Gemini AI Ultra</span><span class="sval">$249.99/mo</span></div>
|
|
574
|
+
<div class="srow"><span class="slbl">Combined flat-rate</span><span class="sval">$469.99/mo</span></div>
|
|
575
|
+
<div class="srow"><span class="slbl">Billing day</span><span class="sval">1st of month</span></div>
|
|
576
|
+
</div>
|
|
577
|
+
<div class="srow"><span class="slbl">Prices pinned</span><span class="sval" data-live="prices-pinned-date">2026-04-16</span></div>
|
|
578
|
+
<div class="disc">IJFW uses your subscription tier to calculate accurate cost and savings projections. No tier configured? Defaults work fine.</div>
|
|
579
|
+
</div>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<!-- ======== ACCOUNTS (merged into subs section via direct link) ======== -->
|
|
584
|
+
<div class="section" data-section="accounts">
|
|
585
|
+
<div class="scard" style="max-width:420px">
|
|
586
|
+
<h3>Account Labels</h3>
|
|
587
|
+
<p style="font-size:13px;color:var(--fg-dim);margin-bottom:12px">Labels let you track which subscription generated which sessions.</p>
|
|
588
|
+
<div style="color:var(--fg-dim);font-size:13px;margin-bottom:12px">Account labels are optional. Most users never need them. Add one if you rotate between multiple Max accounts.</div>
|
|
589
|
+
<button class="btn-g">+ Add account label</button>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
<!-- ======== DISPLAY ======== -->
|
|
594
|
+
<div class="section" data-section="display">
|
|
595
|
+
<div class="sgrid">
|
|
596
|
+
<div class="scard">
|
|
597
|
+
<h3>Display</h3>
|
|
598
|
+
<div class="srow">
|
|
599
|
+
<span class="slbl">Light mode</span>
|
|
600
|
+
<label class="tog" aria-label="Toggle light mode">
|
|
601
|
+
<input type="checkbox" id="themeCbB" onchange="applyTheme(this.checked)">
|
|
602
|
+
<div class="tog-track"></div>
|
|
603
|
+
<div class="tog-thumb"></div>
|
|
604
|
+
</label>
|
|
605
|
+
</div>
|
|
606
|
+
<div class="srow"><span class="slbl">Currency</span><span class="sval">USD</span></div>
|
|
607
|
+
<div class="srow"><span class="slbl">Platform filter</span><span class="sval" style="color:var(--fg-dim);font-size:12px">All platforms</span></div>
|
|
608
|
+
<div class="srow"><span class="slbl">Density</span><span class="sval" style="color:var(--fg-dim);font-size:12px">Comfortable</span></div>
|
|
609
|
+
<div class="srow"><span class="slbl">Cost methodology</span><a href="/api/savings/methodology" target="_blank" class="sval" style="color:var(--accent);font-size:12px;text-decoration:underline">View methodology</a></div>
|
|
610
|
+
</div>
|
|
611
|
+
<div class="scard">
|
|
612
|
+
<h3>Data Confidence</h3>
|
|
613
|
+
<div class="srow"><span class="slbl">Claude cost</span><span class="sval" style="color:var(--success)">Measured</span></div>
|
|
614
|
+
<div class="srow"><span class="slbl">Cache hit rate</span><span class="sval" style="color:var(--success)">Measured</span></div>
|
|
615
|
+
<div class="srow"><span class="slbl">Codex cost</span><span class="sval" style="color:var(--warn)">Flat-rate</span></div>
|
|
616
|
+
<div class="srow"><span class="slbl">Gemini cost</span><span class="sval" style="color:var(--warn)">Flat-rate</span></div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
</main>
|
|
622
|
+
</div><!-- /main-wrap -->
|
|
623
|
+
</div><!-- /shell -->
|
|
624
|
+
|
|
625
|
+
<script>
|
|
626
|
+
// ====== LUCIDE ICONS (vendored, ISC license, https://lucide.dev) ======
|
|
627
|
+
const ICONS = {"activity":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2\"/></svg>","bar-chart-2":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 21v-6\"/><path d=\"M12 21V3\"/><path d=\"M19 21V9\"/></svg>","clock":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><path d=\"M12 6v6l4 2\"/></svg>","credit-card":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"20\" height=\"14\" x=\"2\" y=\"5\" rx=\"2\"/><line x1=\"2\" x2=\"22\" y1=\"10\" y2=\"10\"/></svg>","dollar-sign":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"12\" x2=\"12\" y1=\"2\" y2=\"22\"/><path d=\"M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6\"/></svg>","file-text":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8l6 6v12a2 2 0 0 1-2 2z\"/><path d=\"M14 2v6h6\"/><path d=\"M10 9H8\"/><path d=\"M16 13H8\"/><path d=\"M16 17H8\"/></svg>","folder-open":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2\"/></svg>","folder":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/></svg>","home":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8\"/><path d=\"M3 10a2 2 0 0 1 .709-1.528l7-6a2 2 0 0 1 2.582 0l7 6A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z\"/></svg>","layers":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z\"/><path d=\"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12\"/><path d=\"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17\"/></svg>","search":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m21 21-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></svg>","settings":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915\"/><circle cx=\"12\" cy=\"12\" r=\"3\"/></svg>","shield":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z\"/></svg>","trending-up":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 7h6v6\"/><path d=\"m22 7-8.5 8.5-5-5L2 17\"/></svg>","users":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2\"/><path d=\"M16 3.128a4 4 0 0 1 0 7.744\"/><path d=\"M22 21v-2a4 4 0 0 0-3-3.87\"/><circle cx=\"9\" cy=\"7\" r=\"4\"/></svg>","zap":"<svg class=\"icon\" xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\"/></svg>"};
|
|
628
|
+
|
|
629
|
+
var ICON_MAP = {today:'home',live:'activity',window:'clock',spend:'dollar-sign',models:'layers',activity:'bar-chart-2',trend:'trending-up',memsearch:'search',sessions:'clock',handoffs:'file-text',allprojects:'folder',trident:'shield',subs:'credit-card',accounts:'users',display:'settings'};
|
|
630
|
+
|
|
631
|
+
document.querySelectorAll('.sb-icon').forEach(function(el) {
|
|
632
|
+
var sec = el.closest('.sb-item');
|
|
633
|
+
if (sec) {
|
|
634
|
+
var key = sec.dataset.section;
|
|
635
|
+
var ic = ICON_MAP[key];
|
|
636
|
+
if (ic && ICONS[ic]) el.innerHTML = ICONS[ic];
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// ====== SIDEBAR ======
|
|
641
|
+
var collapsed = false;
|
|
642
|
+
var sbToggle = document.getElementById('sbToggle');
|
|
643
|
+
var sidebar = document.getElementById('sidebar');
|
|
644
|
+
|
|
645
|
+
sbToggle.addEventListener('click', function() {
|
|
646
|
+
collapsed = !collapsed;
|
|
647
|
+
sidebar.classList.toggle('col', collapsed);
|
|
648
|
+
sbToggle.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
|
649
|
+
sbToggle.setAttribute('aria-label', collapsed ? 'Expand sidebar' : 'Collapse sidebar');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// ====== SECTION ROUTING ======
|
|
653
|
+
var breadcrumbs = {
|
|
654
|
+
today: 'Overview > Today', live: 'Overview > Live Feed', window: 'Overview > Usage Limit',
|
|
655
|
+
spend: 'Cost > Usage', models: 'Cost > Models', activity: 'Cost > Activity', trend: 'Cost > Trend',
|
|
656
|
+
memsearch: 'Memory > Memory', sessions: 'Memory > Sessions', handoffs: 'Memory > Handoffs',
|
|
657
|
+
allprojects: 'Projects > All',
|
|
658
|
+
trident: 'Audits > Trident',
|
|
659
|
+
subs: 'Settings > Subscriptions', accounts: 'Settings > Accounts', display: 'Settings > Display',
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
function goSection(key) {
|
|
663
|
+
document.querySelectorAll('.sb-item').forEach(function(i) { i.classList.remove('on'); i.removeAttribute('aria-current'); });
|
|
664
|
+
document.querySelectorAll('.section').forEach(function(s) { s.classList.remove('on'); });
|
|
665
|
+
var btn = document.querySelector('.sb-item[data-section="'+key+'"]');
|
|
666
|
+
var sec = document.querySelector('.section[data-section="'+key+'"]');
|
|
667
|
+
if (btn) { btn.classList.add('on'); btn.setAttribute('aria-current','page'); }
|
|
668
|
+
if (sec) sec.classList.add('on');
|
|
669
|
+
var bc = breadcrumbs[key] || key;
|
|
670
|
+
var parts = bc.split(' > ');
|
|
671
|
+
var bel = document.getElementById('breadcrumb');
|
|
672
|
+
bel.textContent = '';
|
|
673
|
+
parts.forEach(function(p, i) {
|
|
674
|
+
var s = document.createElement('span');
|
|
675
|
+
if (i < parts.length - 1) { s.style.color = 'var(--fg-dim)'; s.textContent = p + ' > '; }
|
|
676
|
+
else { var b = document.createElement('b'); b.textContent = p; s.appendChild(b); }
|
|
677
|
+
bel.appendChild(s);
|
|
678
|
+
});
|
|
679
|
+
history.replaceState(null, '', '#' + key);
|
|
680
|
+
if (collapsed) { sidebar.classList.remove('col'); collapsed = false; sbToggle.setAttribute('aria-expanded','true'); }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
document.querySelectorAll('.sb-item').forEach(function(item) {
|
|
684
|
+
item.addEventListener('click', function() { goSection(item.dataset.section); });
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// hash routing
|
|
688
|
+
(function() {
|
|
689
|
+
var h = window.location.hash.replace(/^#/,'');
|
|
690
|
+
if (h && document.querySelector('.sb-item[data-section="'+h+'"]')) goSection(h);
|
|
691
|
+
})();
|
|
692
|
+
|
|
693
|
+
// ====== THEME ======
|
|
694
|
+
document.getElementById('themeBtn').addEventListener('click', function() {
|
|
695
|
+
var light = document.documentElement.getAttribute('data-theme') !== 'light';
|
|
696
|
+
applyTheme(light);
|
|
697
|
+
document.getElementById('themeCbB').checked = light;
|
|
698
|
+
});
|
|
699
|
+
function applyTheme(l) {
|
|
700
|
+
document.documentElement.setAttribute('data-theme', l ? 'light' : 'dark');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ====== LIVE SSE (observation stream) ======
|
|
704
|
+
if (typeof EventSource !== 'undefined') {
|
|
705
|
+
var es = new EventSource('/stream');
|
|
706
|
+
// 1.2.9: server emits unnamed SSE frames (no `event:` line), so the
|
|
707
|
+
// EventSource default channel is the only one that fires. addEventListener
|
|
708
|
+
// for a named event would never trigger.
|
|
709
|
+
es.onmessage = function(e) {
|
|
710
|
+
try {
|
|
711
|
+
var obs = JSON.parse(e.data);
|
|
712
|
+
var feed = document.querySelector('[data-section="live"] .scard');
|
|
713
|
+
if (feed) {
|
|
714
|
+
var row = document.createElement('div');
|
|
715
|
+
row.style.cssText = 'padding:6px 0;border-bottom:1px solid var(--border);font-size:13px';
|
|
716
|
+
row.textContent = (obs.platform || 'claude') + ' | ' + (obs.type || 'change') + ' | ' + (obs.title || 'New observation');
|
|
717
|
+
feed.prepend(row);
|
|
718
|
+
}
|
|
719
|
+
} catch(err) {}
|
|
720
|
+
};
|
|
721
|
+
es.addEventListener('close', function() { es.close(); });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// ====== MEMORY (search section) ======
|
|
725
|
+
function pickMemB(el, key) {
|
|
726
|
+
document.querySelectorAll('#memListB .mfile').forEach(function(f) { f.classList.remove('on'); f.setAttribute('aria-selected','false'); });
|
|
727
|
+
el.classList.add('on');
|
|
728
|
+
el.setAttribute('aria-selected','true');
|
|
729
|
+
document.querySelectorAll('#memRightB .mem-preview').forEach(function(p) { p.classList.remove('on'); });
|
|
730
|
+
var spec = document.querySelector('#memRightB .mem-preview[data-key="'+key+'"]');
|
|
731
|
+
if (spec) {
|
|
732
|
+
spec.classList.add('on');
|
|
733
|
+
} else {
|
|
734
|
+
var g = document.querySelector('#memRightB .mem-preview[data-key="generic"]');
|
|
735
|
+
g.classList.add('on');
|
|
736
|
+
document.getElementById('bGfTitle').textContent = el.querySelector('.mfname').textContent;
|
|
737
|
+
document.getElementById('bGfRec').textContent = el.querySelector('.mfrec').textContent;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function filterMemB(q) {
|
|
741
|
+
q = q.toLowerCase();
|
|
742
|
+
document.querySelectorAll('#memListB .mfile').forEach(function(f) {
|
|
743
|
+
f.style.display = (!q || f.querySelector('.mfname').textContent.toLowerCase().includes(q)) ? '' : 'none';
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ====== LIVE API LOADERS ======
|
|
748
|
+
|
|
749
|
+
// Error handler for JS errors (appended to console, never visible to user)
|
|
750
|
+
window.addEventListener('error', function(e) {
|
|
751
|
+
console.warn('[ijfw-dashboard] uncaught:', e.message);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// --- helpers ---
|
|
755
|
+
function fmtDollars(n) {
|
|
756
|
+
if (n == null || isNaN(n)) return '$0';
|
|
757
|
+
if (n >= 1000) return '$' + (n / 1000).toFixed(1) + 'k';
|
|
758
|
+
if (n >= 10) return '$' + Math.round(n);
|
|
759
|
+
return '$' + n.toFixed(2);
|
|
760
|
+
}
|
|
761
|
+
// 1.2.9: server emits cost_usd / theoretical_cost_usd, never `cost` -- read
|
|
762
|
+
// the canonical fields with a Max-plan fallback to theoretical so flat-rate
|
|
763
|
+
// users see something other than $0.
|
|
764
|
+
function extractCost(row) {
|
|
765
|
+
if (!row) return 0;
|
|
766
|
+
if (row.cost_usd != null) return Number(row.cost_usd) || 0;
|
|
767
|
+
if (row.theoretical_cost_usd != null) return Number(row.theoretical_cost_usd) || 0;
|
|
768
|
+
if (row.measuredCost != null) return Number(row.measuredCost) || 0;
|
|
769
|
+
if (row.cost != null) return Number(row.cost) || 0;
|
|
770
|
+
if (row.value != null) return Number(row.value) || 0;
|
|
771
|
+
return 0;
|
|
772
|
+
}
|
|
773
|
+
// Server returns cache_read_tokens + input_tokens; derive hit-rate per row.
|
|
774
|
+
function extractCacheHit(row) {
|
|
775
|
+
if (!row) return null;
|
|
776
|
+
if (row.cache_hit != null) return Number(row.cache_hit);
|
|
777
|
+
var cr = Number(row.cache_read_tokens) || 0;
|
|
778
|
+
var inp = Number(row.input_tokens) || 0;
|
|
779
|
+
if (cr + inp === 0) return null;
|
|
780
|
+
return cr / (cr + inp);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function noData(el) {
|
|
784
|
+
if (!el) return;
|
|
785
|
+
el.textContent = '-- no live data --';
|
|
786
|
+
el.style.color = 'var(--fg-dim)';
|
|
787
|
+
el.style.fontSize = '12px';
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// --- 1. Cost today ---
|
|
791
|
+
async function loadCostToday() {
|
|
792
|
+
try {
|
|
793
|
+
var r = await fetch('/api/cost/today');
|
|
794
|
+
var d = await r.json();
|
|
795
|
+
var cost = extractCost(d);
|
|
796
|
+
var el = document.querySelector('[data-live="cost-today"]');
|
|
797
|
+
if (el) el.textContent = fmtDollars(cost);
|
|
798
|
+
var sub = document.querySelector('[data-live="cost-today-sub"]');
|
|
799
|
+
if (sub && cost === 0) sub.textContent = 'no data yet today';
|
|
800
|
+
} catch(e) {}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// --- 2. Cost 30d ---
|
|
804
|
+
async function loadCost30d() {
|
|
805
|
+
try {
|
|
806
|
+
var r = await fetch('/api/cost/period?days=30');
|
|
807
|
+
var d = await r.json();
|
|
808
|
+
var cost = extractCost(d);
|
|
809
|
+
var el = document.querySelector('[data-live="cost-30d"]');
|
|
810
|
+
if (el) el.textContent = fmtDollars(cost);
|
|
811
|
+
} catch(e) {}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// --- 3. Per-project table (today) ---
|
|
815
|
+
async function loadTodayProjects() {
|
|
816
|
+
try {
|
|
817
|
+
var r = await fetch('/api/cost/by?dim=project&period=1d');
|
|
818
|
+
var rows = await r.json();
|
|
819
|
+
var tbody = document.getElementById('today-project-tbody');
|
|
820
|
+
if (!tbody) return;
|
|
821
|
+
if (!Array.isArray(rows) || rows.length === 0) return;
|
|
822
|
+
tbody.innerHTML = '';
|
|
823
|
+
rows.forEach(function(row) {
|
|
824
|
+
var tr = document.createElement('tr');
|
|
825
|
+
var td1 = document.createElement('td'); td1.textContent = row.key || row.project || '--';
|
|
826
|
+
var td2 = document.createElement('td');
|
|
827
|
+
var chip = document.createElement('span'); chip.className = 'chip cc'; chip.textContent = 'Claude';
|
|
828
|
+
td2.appendChild(chip);
|
|
829
|
+
var td3 = document.createElement('td'); td3.className = 'nr'; td3.textContent = fmtDollars(row.cost || 0);
|
|
830
|
+
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3);
|
|
831
|
+
tbody.appendChild(tr);
|
|
832
|
+
});
|
|
833
|
+
} catch(e) {}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// --- 4. Per-project table (30d spend) ---
|
|
837
|
+
async function loadSpendProjects() {
|
|
838
|
+
try {
|
|
839
|
+
var r = await fetch('/api/cost/by?dim=project&period=30d');
|
|
840
|
+
var rows = await r.json();
|
|
841
|
+
var tbody = document.getElementById('spend-project-tbody');
|
|
842
|
+
if (!tbody) return;
|
|
843
|
+
if (!Array.isArray(rows) || rows.length === 0) return;
|
|
844
|
+
var total = rows.reduce(function(s, x) { return s + (x.cost || 0); }, 0);
|
|
845
|
+
tbody.innerHTML = '';
|
|
846
|
+
rows.forEach(function(row) {
|
|
847
|
+
var tr = document.createElement('tr');
|
|
848
|
+
var cost = row.cost || 0;
|
|
849
|
+
var pct = total > 0 ? Math.round(cost / total * 100) : 0;
|
|
850
|
+
var td1 = document.createElement('td'); td1.textContent = row.key || row.project || '--';
|
|
851
|
+
var td2 = document.createElement('td'); td2.textContent = row.last_active || '--';
|
|
852
|
+
var td3 = document.createElement('td'); td3.className = 'nr'; td3.textContent = fmtDollars(cost);
|
|
853
|
+
var td4 = document.createElement('td'); td4.className = 'nr'; td4.textContent = pct < 1 ? '<1%' : pct + '%';
|
|
854
|
+
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4);
|
|
855
|
+
tbody.appendChild(tr);
|
|
856
|
+
});
|
|
857
|
+
// Update allprojects tbody with same data
|
|
858
|
+
var all = document.getElementById('allprojects-tbody');
|
|
859
|
+
if (all) {
|
|
860
|
+
all.innerHTML = '';
|
|
861
|
+
rows.forEach(function(row) {
|
|
862
|
+
var tr = document.createElement('tr');
|
|
863
|
+
var td1 = document.createElement('td'); td1.textContent = row.key || row.project || '--';
|
|
864
|
+
var td2 = document.createElement('td'); td2.textContent = row.last_active || '--';
|
|
865
|
+
var td3 = document.createElement('td'); td3.className = 'nr'; td3.textContent = fmtDollars(extractCost(row));
|
|
866
|
+
var td4 = document.createElement('td');
|
|
867
|
+
var chip = document.createElement('span'); chip.className = 'chip co'; chip.textContent = 'Opus 4.6';
|
|
868
|
+
td4.appendChild(chip);
|
|
869
|
+
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4);
|
|
870
|
+
all.appendChild(tr);
|
|
871
|
+
});
|
|
872
|
+
var cTitle = document.querySelector('[data-section="allprojects"] .ctitle');
|
|
873
|
+
if (cTitle) cTitle.textContent = 'All Projects (' + rows.length + ' with spend, Claude measured 30d)';
|
|
874
|
+
var sbAll = document.querySelector('.sb-item[data-section="allprojects"] .sb-label');
|
|
875
|
+
if (sbAll) sbAll.textContent = 'All (' + rows.length + ')';
|
|
876
|
+
}
|
|
877
|
+
} catch(e) {}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// --- 5. Per-model table (30d) ---
|
|
881
|
+
async function loadSpendModels() {
|
|
882
|
+
try {
|
|
883
|
+
var r = await fetch('/api/cost/by?dim=model&period=30d');
|
|
884
|
+
var rows = await r.json();
|
|
885
|
+
var tbody = document.getElementById('spend-model-tbody');
|
|
886
|
+
if (!tbody) return;
|
|
887
|
+
if (!Array.isArray(rows) || rows.length === 0) return;
|
|
888
|
+
tbody.innerHTML = '';
|
|
889
|
+
rows.forEach(function(row) {
|
|
890
|
+
var tr = document.createElement('tr');
|
|
891
|
+
var td1 = document.createElement('td');
|
|
892
|
+
var chip = document.createElement('span'); chip.className = 'chip co'; chip.textContent = row.key || row.model || '--';
|
|
893
|
+
td1.appendChild(chip);
|
|
894
|
+
var td2 = document.createElement('td'); td2.className = 'nr'; td2.textContent = fmtDollars(extractCost(row));
|
|
895
|
+
var hit = extractCacheHit(row);
|
|
896
|
+
var td3 = document.createElement('td'); td3.className = 'nr'; td3.textContent = hit != null ? (hit * 100).toFixed(1) + '%' : '--';
|
|
897
|
+
var sessions = row.sessions != null ? row.sessions : row.count;
|
|
898
|
+
var td4 = document.createElement('td'); td4.className = 'nr'; td4.textContent = sessions != null ? String(sessions) : '--';
|
|
899
|
+
tr.appendChild(td1); tr.appendChild(td2); tr.appendChild(td3); tr.appendChild(td4);
|
|
900
|
+
tbody.appendChild(tr);
|
|
901
|
+
});
|
|
902
|
+
} catch(e) {}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// --- 6 & 7. Memory file list + search ---
|
|
906
|
+
var _memFiles = [];
|
|
907
|
+
|
|
908
|
+
function renderMemList(files) {
|
|
909
|
+
var list = document.getElementById('memListB');
|
|
910
|
+
var hdr = document.getElementById('mem-tier-hdr');
|
|
911
|
+
if (!list) return;
|
|
912
|
+
// Remove all mfile elements, keep tier headers
|
|
913
|
+
list.querySelectorAll('.mfile').forEach(function(el) { el.remove(); });
|
|
914
|
+
if (!files || files.length === 0) {
|
|
915
|
+
var empty = document.createElement('div');
|
|
916
|
+
empty.style.cssText = 'padding:16px;font-size:12px;color:var(--fg-dim)';
|
|
917
|
+
empty.textContent = 'No memory files found.';
|
|
918
|
+
list.appendChild(empty);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (hdr) hdr.textContent = 'Project Memory (' + files.length + ')';
|
|
922
|
+
var title = document.getElementById('memsearch-title');
|
|
923
|
+
if (title) title.textContent = 'Memory Search -- all ' + files.length + ' files';
|
|
924
|
+
var searchInput = document.getElementById('memSearchB');
|
|
925
|
+
if (searchInput) searchInput.placeholder = 'Search ' + files.length + ' files...';
|
|
926
|
+
// Clear existing and rebuild
|
|
927
|
+
var frag = document.createDocumentFragment();
|
|
928
|
+
files.forEach(function(f) {
|
|
929
|
+
var div = document.createElement('div');
|
|
930
|
+
div.className = 'mfile';
|
|
931
|
+
div.setAttribute('role', 'option');
|
|
932
|
+
div.setAttribute('tabindex', '0');
|
|
933
|
+
div.dataset.path = f.path || '';
|
|
934
|
+
var nameSpan = document.createElement('span'); nameSpan.className = 'mfname';
|
|
935
|
+
nameSpan.textContent = f.name || f.path || '--';
|
|
936
|
+
var recSpan = document.createElement('span'); recSpan.className = 'mfrec';
|
|
937
|
+
recSpan.textContent = f.recall_count ? f.recall_count + (f.recall_count === 1 ? ' recall' : ' recalls') : '0 recalls';
|
|
938
|
+
div.appendChild(nameSpan); div.appendChild(recSpan);
|
|
939
|
+
div.addEventListener('click', function() { pickMemFile(div, f); });
|
|
940
|
+
div.addEventListener('keydown', function(e) { if (e.key === 'Enter' || e.key === ' ') pickMemFile(div, f); });
|
|
941
|
+
frag.appendChild(div);
|
|
942
|
+
});
|
|
943
|
+
list.appendChild(frag);
|
|
944
|
+
// Select first
|
|
945
|
+
var first = list.querySelector('.mfile');
|
|
946
|
+
if (first) first.click();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function pickMemFile(el, f) {
|
|
950
|
+
document.querySelectorAll('#memListB .mfile').forEach(function(x) { x.classList.remove('on'); x.setAttribute('aria-selected','false'); });
|
|
951
|
+
el.classList.add('on'); el.setAttribute('aria-selected','true');
|
|
952
|
+
var right = document.getElementById('memRightB');
|
|
953
|
+
if (!right) return;
|
|
954
|
+
// Hide all static previews
|
|
955
|
+
right.querySelectorAll('.mem-preview').forEach(function(p) { p.classList.remove('on'); });
|
|
956
|
+
// Use generic preview, populate with file info
|
|
957
|
+
var g = right.querySelector('.mem-preview[data-key="generic"]');
|
|
958
|
+
if (!g) return;
|
|
959
|
+
g.classList.add('on');
|
|
960
|
+
var titleEl = document.getElementById('bGfTitle');
|
|
961
|
+
if (titleEl) titleEl.textContent = f.name || '--';
|
|
962
|
+
var recEl = document.getElementById('bGfRec');
|
|
963
|
+
if (recEl) { var rc = f.recall_count || 0; recEl.textContent = rc + (rc === 1 ? ' recall' : ' recalls'); }
|
|
964
|
+
// Fetch file content
|
|
965
|
+
if (f.path) {
|
|
966
|
+
var bodyEl = g.querySelector('.pv-body');
|
|
967
|
+
if (bodyEl) bodyEl.textContent = 'Loading...';
|
|
968
|
+
fetch('/api/memory/file?path=' + encodeURIComponent(f.path))
|
|
969
|
+
.then(function(r) { return r.json(); })
|
|
970
|
+
.then(function(d) {
|
|
971
|
+
if (bodyEl) {
|
|
972
|
+
if (d.body) {
|
|
973
|
+
bodyEl.textContent = d.body.slice(0, 2000);
|
|
974
|
+
} else {
|
|
975
|
+
bodyEl.textContent = '-- no content available --';
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
})
|
|
979
|
+
.catch(function() {
|
|
980
|
+
if (bodyEl) bodyEl.textContent = '-- could not load file --';
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async function loadMemoryFiles() {
|
|
986
|
+
try {
|
|
987
|
+
var r = await fetch('/api/memory');
|
|
988
|
+
var d = await r.json();
|
|
989
|
+
_memFiles = d.files || [];
|
|
990
|
+
renderMemList(_memFiles);
|
|
991
|
+
} catch(e) {}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
var _memSearchTimer = null;
|
|
995
|
+
function onMemSearch(q) {
|
|
996
|
+
clearTimeout(_memSearchTimer);
|
|
997
|
+
if (!q) { renderMemList(_memFiles); return; }
|
|
998
|
+
_memSearchTimer = setTimeout(async function() {
|
|
999
|
+
try {
|
|
1000
|
+
var r = await fetch('/api/memory/search?q=' + encodeURIComponent(q));
|
|
1001
|
+
var d = await r.json();
|
|
1002
|
+
renderMemList(d.results || []);
|
|
1003
|
+
} catch(e) { filterMemB(q); }
|
|
1004
|
+
}, 200);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// --- 8. Projects list ---
|
|
1008
|
+
async function loadProjects() {
|
|
1009
|
+
try {
|
|
1010
|
+
var r = await fetch('/api/projects');
|
|
1011
|
+
var d = await r.json();
|
|
1012
|
+
var projects = d.projects || [];
|
|
1013
|
+
if (projects.length === 0) return;
|
|
1014
|
+
// Update sidebar count
|
|
1015
|
+
var sbAll = document.querySelector('.sb-item[data-section="allprojects"] .sb-label');
|
|
1016
|
+
if (sbAll) sbAll.textContent = 'All (' + projects.length + ')';
|
|
1017
|
+
} catch(e) {}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// --- 9. Handoffs ---
|
|
1021
|
+
async function loadHandoffs() {
|
|
1022
|
+
try {
|
|
1023
|
+
var r = await fetch('/api/memory/search?q=HANDOFF&limit=10');
|
|
1024
|
+
var d = await r.json();
|
|
1025
|
+
var results = (d.results || []).filter(function(f) {
|
|
1026
|
+
return (f.name || f.path || '').toUpperCase().includes('HANDOFF');
|
|
1027
|
+
});
|
|
1028
|
+
var container = document.getElementById('handoffs-content');
|
|
1029
|
+
if (!container) return;
|
|
1030
|
+
if (results.length === 0) return; // keep placeholder
|
|
1031
|
+
container.innerHTML = '';
|
|
1032
|
+
results.forEach(function(f) {
|
|
1033
|
+
var card = document.createElement('div');
|
|
1034
|
+
card.style.cssText = 'padding:12px;border-bottom:1px solid var(--border);cursor:pointer';
|
|
1035
|
+
var name = document.createElement('div');
|
|
1036
|
+
name.style.cssText = 'font-weight:600;margin-bottom:4px';
|
|
1037
|
+
name.textContent = f.name || f.path || '--';
|
|
1038
|
+
var meta = document.createElement('div');
|
|
1039
|
+
meta.style.cssText = 'font-size:12px;color:var(--fg-dim)';
|
|
1040
|
+
meta.textContent = 'Recalls: ' + (f.recall_count || 0) + 'x';
|
|
1041
|
+
card.appendChild(name); card.appendChild(meta);
|
|
1042
|
+
if (f.path) {
|
|
1043
|
+
card.addEventListener('click', function() {
|
|
1044
|
+
fetch('/api/memory/file?path=' + encodeURIComponent(f.path))
|
|
1045
|
+
.then(function(r2) { return r2.json(); })
|
|
1046
|
+
.then(function(fd) {
|
|
1047
|
+
if (!fd.body) return;
|
|
1048
|
+
var pre = document.createElement('pre');
|
|
1049
|
+
pre.style.cssText = 'margin-top:8px;font-size:12px;color:var(--fg-dim);white-space:pre-wrap;word-break:break-word';
|
|
1050
|
+
pre.textContent = fd.body.slice(0, 3000);
|
|
1051
|
+
card.appendChild(pre);
|
|
1052
|
+
})
|
|
1053
|
+
.catch(function() {});
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
container.appendChild(card);
|
|
1057
|
+
});
|
|
1058
|
+
} catch(e) {}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// --- 10 & 11. Config display + save ---
|
|
1062
|
+
async function loadConfig() {
|
|
1063
|
+
try {
|
|
1064
|
+
var r = await fetch('/api/config');
|
|
1065
|
+
var d = await r.json();
|
|
1066
|
+
// Prices pinned date
|
|
1067
|
+
var ppEl = document.querySelector('[data-live="prices-pinned-date"]');
|
|
1068
|
+
if (ppEl && d.prices_pinned_date) ppEl.textContent = d.prices_pinned_date;
|
|
1069
|
+
// Subscriptions
|
|
1070
|
+
var subsRows = document.getElementById('subs-config-rows');
|
|
1071
|
+
if (subsRows && d.subscriptions && Object.keys(d.subscriptions).length > 0) {
|
|
1072
|
+
subsRows.innerHTML = '';
|
|
1073
|
+
var combined = 0;
|
|
1074
|
+
Object.entries(d.subscriptions).forEach(function(entry) {
|
|
1075
|
+
var platform = entry[0]; var cfg = entry[1];
|
|
1076
|
+
var price = cfg.monthly_price || cfg.price || 0;
|
|
1077
|
+
combined += price;
|
|
1078
|
+
var row = document.createElement('div'); row.className = 'srow';
|
|
1079
|
+
var lbl = document.createElement('span'); lbl.className = 'slbl';
|
|
1080
|
+
lbl.textContent = platform.charAt(0).toUpperCase() + platform.slice(1);
|
|
1081
|
+
var val = document.createElement('span'); val.className = 'sval';
|
|
1082
|
+
val.textContent = '$' + price + '/mo';
|
|
1083
|
+
row.appendChild(lbl); row.appendChild(val);
|
|
1084
|
+
subsRows.appendChild(row);
|
|
1085
|
+
});
|
|
1086
|
+
if (combined > 0) {
|
|
1087
|
+
var combRow = document.createElement('div'); combRow.className = 'srow';
|
|
1088
|
+
var cLbl = document.createElement('span'); cLbl.className = 'slbl'; cLbl.textContent = 'Combined flat-rate';
|
|
1089
|
+
var cVal = document.createElement('span'); cVal.className = 'sval'; cVal.textContent = '$' + combined.toFixed(2) + '/mo';
|
|
1090
|
+
combRow.appendChild(cLbl); combRow.appendChild(cVal);
|
|
1091
|
+
subsRows.appendChild(combRow);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// Tier pill in header
|
|
1095
|
+
var tierPill = document.querySelector('.tier-pill');
|
|
1096
|
+
if (tierPill && d.subscriptions && d.subscriptions.claude) {
|
|
1097
|
+
var tier = d.subscriptions.claude.tier || d.subscriptions.claude.name || 'MAX';
|
|
1098
|
+
tierPill.textContent = tier;
|
|
1099
|
+
}
|
|
1100
|
+
} catch(e) {}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// --- 16. Trend sparkline ---
|
|
1104
|
+
async function loadTrendSparkline() {
|
|
1105
|
+
try {
|
|
1106
|
+
var r = await fetch('/api/cost/history?days=30');
|
|
1107
|
+
var series = await r.json();
|
|
1108
|
+
if (!Array.isArray(series) || series.length < 2) return;
|
|
1109
|
+
var values = series.map(extractCost);
|
|
1110
|
+
var maxV = Math.max.apply(null, values) || 1;
|
|
1111
|
+
var w = 700; var h = 80; var n = values.length;
|
|
1112
|
+
var pts = values.map(function(v, i) {
|
|
1113
|
+
var x = Math.round(i / (n - 1) * w);
|
|
1114
|
+
var y = Math.round((1 - v / maxV) * (h - 10) + 4);
|
|
1115
|
+
return x + ',' + y;
|
|
1116
|
+
}).join(' ');
|
|
1117
|
+
var polyline = document.querySelector('[data-section="trend"] polyline');
|
|
1118
|
+
var polygon = document.querySelector('[data-section="trend"] polygon');
|
|
1119
|
+
if (polyline) polyline.setAttribute('points', pts);
|
|
1120
|
+
if (polygon) {
|
|
1121
|
+
var first = pts.split(' ')[0];
|
|
1122
|
+
var last = pts.split(' ')[n - 1];
|
|
1123
|
+
polygon.setAttribute('points', pts + ' ' + last.split(',')[0] + ',' + h + ' 0,' + h);
|
|
1124
|
+
}
|
|
1125
|
+
var total = values.reduce(function(s, v) { return s + v; }, 0);
|
|
1126
|
+
var startEl = document.getElementById('trend-start-label');
|
|
1127
|
+
var endEl = document.getElementById('trend-end-label');
|
|
1128
|
+
var totEl = document.getElementById('trend-total-label');
|
|
1129
|
+
if (startEl && series[0]) startEl.textContent = (series[0].date || '').slice(5) || '';
|
|
1130
|
+
if (endEl && series[series.length - 1]) endEl.textContent = (series[series.length - 1].date || '').slice(5) || '';
|
|
1131
|
+
if (totEl) totEl.textContent = 'Total: ' + fmtDollars(total) + ' Claude (30d measured)';
|
|
1132
|
+
} catch(e) {}
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// --- 15. 5h window block ---
|
|
1136
|
+
async function loadBlockUsage() {
|
|
1137
|
+
try {
|
|
1138
|
+
var r = await fetch('/api/cost/block');
|
|
1139
|
+
var d = await r.json();
|
|
1140
|
+
if (!d || d.error) return;
|
|
1141
|
+
var bar = document.querySelector('[data-section="window"] .wseg');
|
|
1142
|
+
var meta = document.querySelector('[data-section="window"] .wmeta');
|
|
1143
|
+
if (!bar || !meta) return;
|
|
1144
|
+
// 1.2.9: server emits { start, end, window_minutes, used_tok, used_usd, used_usd_theoretical }.
|
|
1145
|
+
// Derive elapsed minutes from start; previous code read usedMinutes/windowMinutes/remainingMinutes
|
|
1146
|
+
// which the server has never emitted, so the panel rendered 0% used / 5h remaining forever.
|
|
1147
|
+
var windowMin = Number(d.window_minutes) || 300;
|
|
1148
|
+
var startMs = d.start ? new Date(d.start).getTime() : NaN;
|
|
1149
|
+
var nowMs = Date.now();
|
|
1150
|
+
var elapsedMin = isFinite(startMs) ? Math.max(0, Math.min(windowMin, Math.floor((nowMs - startMs) / 60000))) : 0;
|
|
1151
|
+
var remainMin = Math.max(0, windowMin - elapsedMin);
|
|
1152
|
+
var pct = Math.min(100, Math.round(elapsedMin / windowMin * 100));
|
|
1153
|
+
bar.style.width = pct + '%';
|
|
1154
|
+
var used = document.createElement('span');
|
|
1155
|
+
used.textContent = Math.floor(elapsedMin / 60) + 'h ' + (elapsedMin % 60) + 'm into 5h window';
|
|
1156
|
+
var remaining = document.createElement('span');
|
|
1157
|
+
remaining.className = 'wok';
|
|
1158
|
+
remaining.textContent = Math.floor(remainMin / 60) + 'h ' + (remainMin % 60) + 'm remaining -- ' + (100 - pct) + '%';
|
|
1159
|
+
meta.innerHTML = '';
|
|
1160
|
+
meta.appendChild(used); meta.appendChild(remaining);
|
|
1161
|
+
} catch(e) {}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ====== RUN ALL LOADERS ======
|
|
1165
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1166
|
+
loadCostToday();
|
|
1167
|
+
loadCost30d();
|
|
1168
|
+
loadTodayProjects();
|
|
1169
|
+
loadSpendProjects();
|
|
1170
|
+
loadSpendModels();
|
|
1171
|
+
loadMemoryFiles();
|
|
1172
|
+
loadProjects();
|
|
1173
|
+
loadHandoffs();
|
|
1174
|
+
loadConfig();
|
|
1175
|
+
loadTrendSparkline();
|
|
1176
|
+
loadBlockUsage();
|
|
1177
|
+
});
|
|
1178
|
+
</script>
|
|
1179
|
+
</body>
|
|
1180
|
+
</html>
|