diff --git a/data/claude-usage-history.json b/data/claude-usage-history.json new file mode 100644 index 0000000..3a01a25 --- /dev/null +++ b/data/claude-usage-history.json @@ -0,0 +1,12 @@ +[ + { + "session_percent": 8, + "timestamp": "2026-02-11T05:05:37.770936Z", + "weekly_percent": 42 + }, + { + "session_percent": 9, + "timestamp": "2026-02-11T05:19:36.479005Z", + "weekly_percent": 43 + } +] \ No newline at end of file diff --git a/data/status.json b/data/status.json index 977f1a9..3314525 100644 --- a/data/status.json +++ b/data/status.json @@ -2,15 +2,15 @@ "items": { "claude": { "key": "claude", - "value": "42% used ยท 11:00 PM", + "value": "42% used ยท 12:00 AM", "type": "info", - "updated_at": "2026-02-10T23:00:08.709873416-05:00" + "updated_at": "2026-02-11T00:00:09.222923015-05:00" }, "claude-usage": { "key": "claude-usage", - "value": "๐Ÿ“Š Weekly: 42% used (58% remaining)", + "value": "๐Ÿ“Š Weekly: 43% used (57% remaining)", "type": "info", - "updated_at": "2026-02-10T22:19:41.18380063-05:00" + "updated_at": "2026-02-11T00:19:41.954497545-05:00" }, "claude_weekly": { "key": "claude_weekly", diff --git a/index.html b/index.html index 26e20c7..7260c6a 100644 --- a/index.html +++ b/index.html @@ -853,23 +853,24 @@ if (!container) return; let weeklyPct = 10; - let lastCheck = ''; let resetTime = null; + let lastUpdated = null; + let history = []; if (claudeStatus) { const m = claudeStatus.value.match(/(\d+)%/); if (m) weeklyPct = parseInt(m[1]); - const tm = claudeStatus.value.match(/checked\s+(.+)/); - if (tm) lastCheck = tm[1]; } - // Try to get reset time from usage data + // Get usage data + history from API try { const res = await fetch('/api/claude-usage'); if (res.ok) { const d = await res.json(); if (d.weekly_resets) resetTime = new Date(d.weekly_resets); if (d.weekly_percent !== undefined) weeklyPct = d.weekly_percent; + if (d.last_updated) lastUpdated = d.last_updated; + if (d.history) history = d.history; } } catch(e) {} @@ -877,60 +878,97 @@ if (weeklyPct > 70) pctClass = 'warning'; if (weeklyPct > 85) pctClass = 'error'; - // Chart: 7 days, 4 blocks per day (6h each) = 28 blocks const W = 160, H = 36, PAD = 2; - const BLOCKS = 28; - const blockW = (W - PAD*2) / BLOCKS; - - // Figure out week start (reset - 7 days) const now = new Date(); + const weekMs = 7 * 86400000; + + // Week start = reset time - 7 days let weekStart; if (resetTime) { - weekStart = new Date(resetTime.getTime() - 7 * 86400000); + weekStart = new Date(resetTime.getTime() - weekMs); } else { - // Guess: week resets Friday, so started last Friday const d = new Date(now); - d.setDate(d.getDate() - ((d.getDay() + 2) % 7)); // last Friday - d.setHours(0,0,0,0); + d.setDate(d.getDate() - ((d.getDay() + 2) % 7)); + d.setHours(14,0,0,0); weekStart = d; } + const weekEnd = new Date(weekStart.getTime() + weekMs); - const weekMs = 7 * 86400000; - const elapsed = Math.min(now - weekStart, weekMs); - const nowBlock = Math.min(Math.floor((elapsed / weekMs) * BLOCKS), BLOCKS); - - // Budget line: 0% โ†’ 100% over full week (dashed gray) - const bx0 = PAD, by0 = H - PAD; - const bx1 = PAD + BLOCKS * blockW, by1 = PAD; - - // Actual usage line: linear from 0 to weeklyPct, ending at nowBlock - const ax1 = PAD + nowBlock * blockW; - const ay1 = H - PAD - ((weeklyPct / 100) * (H - PAD*2)); + // Map time to X coordinate + const timeToX = (t) => { + const frac = Math.max(0, Math.min(1, (t - weekStart) / weekMs)); + return PAD + frac * (W - PAD*2); + }; + const pctToY = (p) => H - PAD - (p / 100) * (H - PAD*2); // Day separators let dayLines = ''; for (let d = 1; d < 7; d++) { - const x = PAD + d * 4 * blockW; + const x = timeToX(new Date(weekStart.getTime() + d * 86400000)); dayLines += ``; } - // "Now" marker - const nowX = PAD + nowBlock * blockW; + // Budget line (dashed diagonal) + const bx0 = PAD, by0 = H - PAD; + const bx1 = W - PAD, by1 = PAD; const color = pctClass === 'ok' ? '#059669' : pctClass === 'warning' ? '#B45309' : '#DC2626'; + // Build polyline from history points (filter to current week) + const weekStartMs = weekStart.getTime(); + const points = history + .filter(h => new Date(h.timestamp).getTime() >= weekStartMs) + .map(h => ({ t: new Date(h.timestamp), p: h.weekly_percent })) + .sort((a, b) => a.t - b.t); + + let pathD = ''; + let lastPoint = null; + if (points.length > 0) { + // Start from week start at 0% + pathD = `M${PAD},${H - PAD}`; + // First history point + pathD += ` L${timeToX(points[0].t)},${pctToY(points[0].p)}`; + for (let i = 1; i < points.length; i++) { + pathD += ` L${timeToX(points[i].t)},${pctToY(points[i].p)}`; + } + lastPoint = points[points.length - 1]; + } + + // Current position dot + const nowX = timeToX(now); + const nowY = pctToY(weeklyPct); + if (pathD) { + pathD += ` L${nowX},${nowY}`; + } else { + // No history: draw single line from 0 to current + pathD = `M${PAD},${H - PAD} L${nowX},${nowY}`; + } + const svg = ` ${dayLines} - - + + `; + // Issue 2: last-pull timestamp + let updatedLabel = ''; + if (lastUpdated) { + const updatedDate = new Date(lastUpdated); + const diffMs = now - updatedDate; + const diffMin = Math.floor(diffMs / 60000); + const diffHr = Math.floor(diffMs / 3600000); + if (diffMs < 60000) updatedLabel = 'just now'; + else if (diffMin < 60) updatedLabel = diffMin + 'm ago'; + else if (diffHr < 24) updatedLabel = diffHr + 'h ago'; + else updatedLabel = updatedDate.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); + } + container.innerHTML = ` Claude ${weeklyPct}% ${svg} - ${lastCheck ? `${lastCheck}` : ''} + ${updatedLabel ? `${updatedLabel}` : ''} `; } diff --git a/james-dashboard b/james-dashboard index 8acf4ac..c2e04da 100755 Binary files a/james-dashboard and b/james-dashboard differ diff --git a/james-dashboard-new b/james-dashboard-new deleted file mode 100755 index 8acf4ac..0000000 Binary files a/james-dashboard-new and /dev/null differ diff --git a/server.go b/server.go index fd539a8..5c1718a 100644 --- a/server.go +++ b/server.go @@ -849,7 +849,7 @@ func getAgents(gatewayIP string, gatewayPort string, gatewayToken string) []Agen // Include token for auto-authentication if agentConfig.ID == "main" { agent.Default = true - agent.URL = baseURL + "/chat?token=" + gatewayToken + agent.URL = baseURL + "/chat?session=agent:main:main&token=" + gatewayToken } else { // Control UI reads session param on /chat route agent.URL = baseURL + "/chat?session=agent:" + agentConfig.ID + ":main&token=" + gatewayToken @@ -1339,6 +1339,77 @@ func main() { } }) + // ========== CLAUDE USAGE API ========== + claudeUsageFile := "/home/johan/clawd/memory/claude-usage.json" + claudeHistoryFile := filepath.Join(*dir, "data", "claude-usage-history.json") + + mux.HandleFunc("/api/claude-usage", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + if r.Method != "GET" { + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + return + } + + // Read current usage + result := make(map[string]interface{}) + if data, err := os.ReadFile(claudeUsageFile); err == nil { + json.Unmarshal(data, &result) + } + + // Read history + var history []map[string]interface{} + if data, err := os.ReadFile(claudeHistoryFile); err == nil { + json.Unmarshal(data, &history) + } + result["history"] = history + + json.NewEncoder(w).Encode(result) + }) + + mux.HandleFunc("/api/claude-usage/record", func(w http.ResponseWriter, r *http.Request) { + cors(w) + if r.Method == "OPTIONS" { + return + } + if r.Method != "POST" { + http.Error(w, `{"error": "method not allowed"}`, http.StatusMethodNotAllowed) + return + } + + var entry map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&entry); err != nil { + http.Error(w, `{"error": "invalid JSON"}`, http.StatusBadRequest) + return + } + + // Read existing history + var history []map[string]interface{} + if data, err := os.ReadFile(claudeHistoryFile); err == nil { + json.Unmarshal(data, &history) + } + + // Add timestamp if missing + if _, ok := entry["timestamp"]; !ok { + entry["timestamp"] = time.Now().UTC().Format(time.RFC3339) + } + + history = append(history, entry) + + // Keep last 2000 entries + if len(history) > 2000 { + history = history[len(history)-2000:] + } + + data, _ := json.MarshalIndent(history, "", " ") + os.WriteFile(claudeHistoryFile, data, 0644) + + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]string{"status": "recorded"}) + }) + // ========== AGENTS API ========== mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) { cors(w)