chore: auto-commit uncommitted changes
This commit is contained in:
parent
9c449e51a0
commit
6ed96a4514
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -2,15 +2,15 @@
|
||||||
"items": {
|
"items": {
|
||||||
"claude": {
|
"claude": {
|
||||||
"key": "claude",
|
"key": "claude",
|
||||||
"value": "42% used · 11:00 PM",
|
"value": "42% used · 12:00 AM",
|
||||||
"type": "info",
|
"type": "info",
|
||||||
"updated_at": "2026-02-10T23:00:08.709873416-05:00"
|
"updated_at": "2026-02-11T00:00:09.222923015-05:00"
|
||||||
},
|
},
|
||||||
"claude-usage": {
|
"claude-usage": {
|
||||||
"key": "claude-usage",
|
"key": "claude-usage",
|
||||||
"value": "📊 Weekly: 42% used (58% remaining)",
|
"value": "📊 Weekly: 43% used (57% remaining)",
|
||||||
"type": "info",
|
"type": "info",
|
||||||
"updated_at": "2026-02-10T22:19:41.18380063-05:00"
|
"updated_at": "2026-02-11T00:19:41.954497545-05:00"
|
||||||
},
|
},
|
||||||
"claude_weekly": {
|
"claude_weekly": {
|
||||||
"key": "claude_weekly",
|
"key": "claude_weekly",
|
||||||
|
|
|
||||||
98
index.html
98
index.html
|
|
@ -853,23 +853,24 @@
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
|
|
||||||
let weeklyPct = 10;
|
let weeklyPct = 10;
|
||||||
let lastCheck = '';
|
|
||||||
let resetTime = null;
|
let resetTime = null;
|
||||||
|
let lastUpdated = null;
|
||||||
|
let history = [];
|
||||||
|
|
||||||
if (claudeStatus) {
|
if (claudeStatus) {
|
||||||
const m = claudeStatus.value.match(/(\d+)%/);
|
const m = claudeStatus.value.match(/(\d+)%/);
|
||||||
if (m) weeklyPct = parseInt(m[1]);
|
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 {
|
try {
|
||||||
const res = await fetch('/api/claude-usage');
|
const res = await fetch('/api/claude-usage');
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const d = await res.json();
|
const d = await res.json();
|
||||||
if (d.weekly_resets) resetTime = new Date(d.weekly_resets);
|
if (d.weekly_resets) resetTime = new Date(d.weekly_resets);
|
||||||
if (d.weekly_percent !== undefined) weeklyPct = d.weekly_percent;
|
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) {}
|
} catch(e) {}
|
||||||
|
|
||||||
|
|
@ -877,60 +878,97 @@
|
||||||
if (weeklyPct > 70) pctClass = 'warning';
|
if (weeklyPct > 70) pctClass = 'warning';
|
||||||
if (weeklyPct > 85) pctClass = 'error';
|
if (weeklyPct > 85) pctClass = 'error';
|
||||||
|
|
||||||
// Chart: 7 days, 4 blocks per day (6h each) = 28 blocks
|
|
||||||
const W = 160, H = 36, PAD = 2;
|
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 now = new Date();
|
||||||
|
const weekMs = 7 * 86400000;
|
||||||
|
|
||||||
|
// Week start = reset time - 7 days
|
||||||
let weekStart;
|
let weekStart;
|
||||||
if (resetTime) {
|
if (resetTime) {
|
||||||
weekStart = new Date(resetTime.getTime() - 7 * 86400000);
|
weekStart = new Date(resetTime.getTime() - weekMs);
|
||||||
} else {
|
} else {
|
||||||
// Guess: week resets Friday, so started last Friday
|
|
||||||
const d = new Date(now);
|
const d = new Date(now);
|
||||||
d.setDate(d.getDate() - ((d.getDay() + 2) % 7)); // last Friday
|
d.setDate(d.getDate() - ((d.getDay() + 2) % 7));
|
||||||
d.setHours(0,0,0,0);
|
d.setHours(14,0,0,0);
|
||||||
weekStart = d;
|
weekStart = d;
|
||||||
}
|
}
|
||||||
|
const weekEnd = new Date(weekStart.getTime() + weekMs);
|
||||||
|
|
||||||
const weekMs = 7 * 86400000;
|
// Map time to X coordinate
|
||||||
const elapsed = Math.min(now - weekStart, weekMs);
|
const timeToX = (t) => {
|
||||||
const nowBlock = Math.min(Math.floor((elapsed / weekMs) * BLOCKS), BLOCKS);
|
const frac = Math.max(0, Math.min(1, (t - weekStart) / weekMs));
|
||||||
|
return PAD + frac * (W - PAD*2);
|
||||||
// Budget line: 0% → 100% over full week (dashed gray)
|
};
|
||||||
const bx0 = PAD, by0 = H - PAD;
|
const pctToY = (p) => H - PAD - (p / 100) * (H - PAD*2);
|
||||||
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));
|
|
||||||
|
|
||||||
// Day separators
|
// Day separators
|
||||||
let dayLines = '';
|
let dayLines = '';
|
||||||
for (let d = 1; d < 7; d++) {
|
for (let d = 1; d < 7; d++) {
|
||||||
const x = PAD + d * 4 * blockW;
|
const x = timeToX(new Date(weekStart.getTime() + d * 86400000));
|
||||||
dayLines += `<line x1="${x}" y1="0" x2="${x}" y2="${H}" stroke="#E5E2DE" stroke-width="0.5"/>`;
|
dayLines += `<line x1="${x}" y1="0" x2="${x}" y2="${H}" stroke="#E5E2DE" stroke-width="0.5"/>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Now" marker
|
// Budget line (dashed diagonal)
|
||||||
const nowX = PAD + nowBlock * blockW;
|
const bx0 = PAD, by0 = H - PAD;
|
||||||
|
const bx1 = W - PAD, by1 = PAD;
|
||||||
|
|
||||||
const color = pctClass === 'ok' ? '#059669' : pctClass === 'warning' ? '#B45309' : '#DC2626';
|
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 = `<svg class="claude-chart-svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
const svg = `<svg class="claude-chart-svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
||||||
${dayLines}
|
${dayLines}
|
||||||
<line x1="${bx0}" y1="${by0}" x2="${bx1}" y2="${by1}" stroke="#E5E2DE" stroke-width="1" stroke-dasharray="3,2"/>
|
<line x1="${bx0}" y1="${by0}" x2="${bx1}" y2="${by1}" stroke="#E5E2DE" stroke-width="1" stroke-dasharray="3,2"/>
|
||||||
<line x1="${PAD}" y1="${H - PAD}" x2="${ax1}" y2="${ay1}" stroke="${color}" stroke-width="2" stroke-linecap="round"/>
|
<path d="${pathD}" stroke="${color}" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<circle cx="${ax1}" cy="${ay1}" r="2.5" fill="${color}"/>
|
<circle cx="${nowX}" cy="${nowY}" r="2.5" fill="${color}"/>
|
||||||
</svg>`;
|
</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 = `
|
container.innerHTML = `
|
||||||
<span class="claude-chart-label">Claude</span>
|
<span class="claude-chart-label">Claude</span>
|
||||||
<span class="claude-chart-pct ${pctClass}">${weeklyPct}%</span>
|
<span class="claude-chart-pct ${pctClass}">${weeklyPct}%</span>
|
||||||
${svg}
|
${svg}
|
||||||
${lastCheck ? `<span class="claude-chart-label">${lastCheck}</span>` : ''}
|
${updatedLabel ? `<span class="claude-chart-label">${updatedLabel}</span>` : ''}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
BIN
james-dashboard
BIN
james-dashboard
Binary file not shown.
Binary file not shown.
73
server.go
73
server.go
|
|
@ -849,7 +849,7 @@ func getAgents(gatewayIP string, gatewayPort string, gatewayToken string) []Agen
|
||||||
// Include token for auto-authentication
|
// Include token for auto-authentication
|
||||||
if agentConfig.ID == "main" {
|
if agentConfig.ID == "main" {
|
||||||
agent.Default = true
|
agent.Default = true
|
||||||
agent.URL = baseURL + "/chat?token=" + gatewayToken
|
agent.URL = baseURL + "/chat?session=agent:main:main&token=" + gatewayToken
|
||||||
} else {
|
} else {
|
||||||
// Control UI reads session param on /chat route
|
// Control UI reads session param on /chat route
|
||||||
agent.URL = baseURL + "/chat?session=agent:" + agentConfig.ID + ":main&token=" + gatewayToken
|
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 ==========
|
// ========== AGENTS API ==========
|
||||||
mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/agents", func(w http.ResponseWriter, r *http.Request) {
|
||||||
cors(w)
|
cors(w)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue