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 = ``;
+ // 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)