claude-finops 0.4.1 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-finops",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Local FinOps dashboard for Claude Code: what you used, what it cost, why it cost that much, and what to change. Reads your own transcripts, no API key, no data leaves the machine.",
5
5
  "bin": {
6
6
  "claude-finops": "bin/claude-finops.js"
package/run.py CHANGED
@@ -93,6 +93,7 @@ HELP = """Claude FinOps Command Center
93
93
  claude-finops --set-key store a provider API key (prompts, never echoes)
94
94
  claude-finops --keys list which provider keys are configured
95
95
  claude-finops --share write ../claude-finops.zip (code only, never your data)
96
+ claude-finops --version print the installed version, and whether a newer one is out
96
97
  claude-finops --help this message
97
98
 
98
99
  Environment:
@@ -104,6 +105,38 @@ Environment:
104
105
  """
105
106
 
106
107
 
108
+ def _version():
109
+ from finops.update import _installed
110
+ return _installed()
111
+
112
+
113
+ def version():
114
+ """Which copy is this, and is it current?
115
+
116
+ The second half matters more than the first: people run a global install and
117
+ a repo checkout side by side, and the usual confusion is not "what version
118
+ am I on" but "why does the one I am looking at not have the feature".
119
+ """
120
+ from finops.update import check, disabled, _key
121
+ # Asking outright is worth a fresh request: a day-old cached answer is the
122
+ # one thing this command must not give you.
123
+ u = check(force=True)
124
+ print(f" claude-finops {u['current'] or 'unknown'}")
125
+ print(f" installed at {ROOT}")
126
+ if u.get("update_available"):
127
+ print(f" update {u['latest']} is out - {u['command']}")
128
+ elif disabled():
129
+ print(" update check is off (NO_UPDATE_NOTIFIER)")
130
+ elif u.get("latest") and _key(u["current"]) > _key(u["latest"]):
131
+ # A checkout mid-release is ahead of what is published. Saying "up to
132
+ # date" there would hide exactly the gap you are looking for.
133
+ print(f" update ahead of npm (published latest is {u['latest']})")
134
+ elif u.get("latest"):
135
+ print(f" update up to date (npm latest is {u['latest']})")
136
+ else:
137
+ print(" update could not reach the npm registry")
138
+
139
+
107
140
  def where():
108
141
  from finops.paths import (HOME_DIR, DB_PATH, LOCAL_SETTINGS_PATH, SECRETS_PATH,
109
142
  LOGFILE, CONFIG_DIR)
@@ -180,6 +213,8 @@ def main():
180
213
  migrate()
181
214
  if "--help" in args or "-h" in args:
182
215
  return print(HELP)
216
+ if "--version" in args or "-v" in args or "-V" in args:
217
+ return version()
183
218
  if "--where" in args:
184
219
  return where()
185
220
  if "--keys" in args:
@@ -192,6 +227,16 @@ def main():
192
227
  return
193
228
  if "--share" in args:
194
229
  return share()
230
+ # A flag we do not know used to fall straight through and start the
231
+ # dashboard, so a typo (or a flag from a newer release than the one you
232
+ # have installed) looked like the command silently doing nothing.
233
+ known = {"--rebuild", "--detach", "--foreground"}
234
+ unknown = [a for a in args if a.startswith("-") and a not in known]
235
+ if unknown:
236
+ print(f"Unknown option: {unknown[0]}")
237
+ print(f"This is claude-finops {_version() or 'unknown'}; "
238
+ f"run --help to see what it supports.")
239
+ sys.exit(2)
195
240
  stop() # replace a previous detached server
196
241
  port = os.environ.get("PORT", "8787")
197
242
  source = os.environ.get("CLAUDE_PROJECTS", os.path.join(os.path.expanduser("~"), ".claude", "projects"))
package/web/app.js CHANGED
@@ -485,7 +485,10 @@ async function openSession(id) {
485
485
  /* ---------- charts added to table-first pages ---------- */
486
486
  // Inserts a chart card after the page's KPI row (or at the top) and draws into it.
487
487
  function addChart(page, title, draw, opts = {}) {
488
- const c = h(card(title, '<div class="cchart"></div>', opts));
488
+ // .chart carries every chart style (text fill, gridlines, axes); .cchart is
489
+ // just the hook this helper looks up. Missing .chart left SVG labels at the
490
+ // browser default fill — black text, invisible on the dark theme.
491
+ const c = h(card(title, '<div class="chart cchart"></div>', opts));
489
492
  const anchor = opts.after ? page.querySelector(opts.after) : page.querySelector(':scope > .grid');
490
493
  if (anchor) anchor.after(c); else page.prepend(c);
491
494
  draw(c.querySelector('.cchart'));
package/web/styles.css CHANGED
@@ -76,15 +76,19 @@ button, input, select { font: inherit; color: inherit; }
76
76
 
77
77
  /* ---------- shell ---------- */
78
78
  .app { display: grid; grid-template-columns: 216px 1fr; min-height: 100vh; }
79
+ /* The sidebar itself no longer scrolls: the nav does. A sticky footer inside a
80
+ scrolling column still has the rest of the list running underneath it, which
81
+ showed as nav items bleeding through the account block. */
79
82
  .sidebar { background: var(--surface); border-right: 1px solid var(--border);
80
- padding: 14px 0; position: sticky; top:0; height:100vh; overflow-y:auto;
83
+ padding: 14px 0; position: sticky; top:0; height:100vh; overflow:hidden;
81
84
  display:flex; flex-direction:column; }
82
85
  .brand { padding: 0 16px 14px; border-bottom: 1px solid var(--border); margin-bottom: 10px; }
83
86
  .brand .mark { display:flex; align-items:center; gap:8px; font-weight:660; font-size:14px;
84
87
  letter-spacing:-0.01em; }
85
88
  .brand .dot { width:9px; height:9px; border-radius:3px; background:var(--s2); flex:none; }
86
89
  .brand .sub { font-size:10.5px; color:var(--muted); margin-top:3px; letter-spacing:.02em; }
87
- .nav { display:flex; flex-direction:column; gap:1px; padding: 0 8px; }
90
+ .nav { display:flex; flex-direction:column; gap:1px; padding: 0 8px;
91
+ flex:1 1 auto; min-height:0; overflow-y:auto; }
88
92
  .nav .group { font-size:9.5px; text-transform:uppercase; letter-spacing:.09em; color:var(--muted);
89
93
  padding: 13px 8px 5px; font-weight:600; }
90
94
  .nav a { display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px;
@@ -401,8 +405,7 @@ table.tbl .sub { font-size:10.5px; color:var(--muted); }
401
405
  so every screenshot carries the name of the account it came from. */
402
406
  /* Sticky, because the nav is taller than the viewport and an identity you have
403
407
  to scroll to find is one nobody checks. */
404
- .who { margin-top:auto; position:sticky; bottom:0; z-index:2;
405
- display:flex; align-items:center; gap:9px;
408
+ .who { flex:none; display:flex; align-items:center; gap:9px;
406
409
  padding:12px 16px; background:var(--surface);
407
410
  border-top:1px solid var(--border); }
408
411
  .who:empty { display:none; }