mirror of
https://github.com/dtzp555-max/olp.git
synced 2026-07-21 21:15:10 +00:00
## F4 — bin/olp.mjs + olp-plugin/index.js migrate to quota_v2 Codex post-v0.5.0 review Q4: both `olp usage` CLI and `/olp usage` plugin handler read legacy `body.quota` shape, which never has `percent_used` or meaningful `available` data → display always fell through to "no quota api" even when anthropic live quota was visible on the dashboard (D81/D82). Authority: codex review Q4 (PR #58); ADR 0008 Amendment 2 (quota_v2 shape: { provider, status, utilization, reset, representative_claim, fallback_percentage, overage, failure?, failure_kind?, backoff_until? }). ### bin/olp.mjs cmdUsage - When `body.quota_v2` present (non-empty array, server v0.5.0+): render per-provider rows with status badge (live/stale/unreachable/unavailable), 5h + 7d utilization % (color-coded: green <50% / yellow 50-80% / red ≥80%), reset countdowns, binding claim, ⚠ stale / ❌ unreachable annotations. - When `body.quota_v2` absent: fall through to existing legacy `body.quota` rendering (preserves backwards compat with pre-v0.5.0 servers). - Added `formatResetCountdown(epochSeconds)` — 5-range formatter (past / <1h / <24h / <7d / ≥7d), ported from dashboard.html D82. Exported for tests. Kept in bin/olp.mjs (not lib/) per spec guidance. - Added `formatAgo(diffMs)` — small helper for stale-row age display. ### olp-plugin/index.js fmtUsage() - Same quota_v2 migration. Plain-text output (no ANSI), one line per provider. Legacy body.quota fallback preserved. - Added `pluginFormatResetCountdown(epochSeconds)` — intentionally duplicated from bin/olp.mjs (olp-plugin ships as a separate package and must not import from bin/). Exported for tests. - Also fixed fmtUsage to read `w.request_count` (dashboard-data shape) in addition to legacy `w.requests` (OCP-era shape) for completeness. ### Backwards compat - Pre-v0.5.0 server returns body.quota only → both surfaces use legacy display (unchanged behaviour). - v0.5.0+ server returns both quota and quota_v2 → both surfaces prefer quota_v2. - Legacy code paths kept (5-10 lines each, not deleted). ## v1.x roadmap #7 — AUTH_MISSING tuple path test coverage — CLOSED The dedicated test was already shipped at D56 (test-features.mjs line 6255: 'engine: AUTH_MISSING terminates chain, fallbackDetail tuple records trigger_type:"auth_missing" (D56, v1.x roadmap #7)'). This commit closes the roadmap entry with a date stamp and PR reference. Authority: docs/v1x-roadmap.md § "#7 — AUTH_MISSING tuple path test coverage (D40 follow-up)". ## Tests Suite 40 (9 new tests — 40a through 40i): 40a — cmdUsage parses quota_v2 live rows (mock server) 40b — cmdUsage falls back to legacy body.quota when quota_v2 absent 40c — olp-plugin fmtUsage parses quota_v2 live row 40d — olp-plugin fmtUsage falls back to legacy body.quota 40e — formatResetCountdown covers all 5 time ranges 40f — pluginFormatResetCountdown covers past/<1h/<24h/<7d/≥7d 40g — cmdUsage renders quota_v2 stale row with ⚠ stale note 40h — cmdUsage renders quota_v2 unreachable row with ❌ indicator 40i — cmdUsage renders quota_v2 unavailable rows correctly 759 → 768 tests, 0 fail. Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+86
-1
@@ -226,6 +226,53 @@ function formatMs(ms) {
|
||||
return `${Math.floor(ms / 3600000)}h${Math.floor((ms % 3600000) / 60000)}m`;
|
||||
}
|
||||
|
||||
/** formatAgo(diffMs) — "N min ago" / "Nh ago" from a millisecond diff. */
|
||||
function formatAgo(diffMs) {
|
||||
if (typeof diffMs !== 'number' || diffMs < 0) return 'just now';
|
||||
const sec = Math.floor(diffMs / 1000);
|
||||
if (sec < 60) return `${sec}s ago`;
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `${min}m ago`;
|
||||
return `${Math.floor(min / 60)}h ago`;
|
||||
}
|
||||
|
||||
/**
|
||||
* formatResetCountdown(epochSeconds) → human-readable reset countdown string.
|
||||
*
|
||||
* Mirrors dashboard.html formatResetCountdown(). Five ranges:
|
||||
* past / < 1h / < 24h / < 7d / ≥ 7d
|
||||
*
|
||||
* Authority: ADR 0008 Amendment 2 (quota_v2 shape); ported from
|
||||
* dashboard.html (D82). No external deps. Pure formatter.
|
||||
*
|
||||
* @param {number|null} epochSeconds — Unix epoch seconds for reset time
|
||||
* @returns {string}
|
||||
*/
|
||||
export function formatResetCountdown(epochSeconds) {
|
||||
if (epochSeconds == null) return '—';
|
||||
const nowMs = Date.now();
|
||||
const targetMs = epochSeconds * 1000;
|
||||
const diffMs = targetMs - nowMs;
|
||||
if (diffMs <= 0) return 'resetting now';
|
||||
const diffMin = Math.floor(diffMs / 60000);
|
||||
const diffHr = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHr / 24);
|
||||
if (diffMin < 60) return `resets in ${diffMin}m`;
|
||||
if (diffHr < 24) {
|
||||
const remMin = diffMin - diffHr * 60;
|
||||
if (remMin === 0) return `resets in ${diffHr}h`;
|
||||
return `resets in ${diffHr}h ${remMin}m`;
|
||||
}
|
||||
const target = new Date(targetMs);
|
||||
const timeStr = target.toLocaleString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
|
||||
if (diffDay < 7) {
|
||||
const dayStr = target.toLocaleString('en-US', { weekday: 'short' });
|
||||
return `resets ${dayStr} ${timeStr}`;
|
||||
}
|
||||
const dateStr = target.toLocaleString('en-US', { month: 'short', day: 'numeric' });
|
||||
return `resets ${dateStr} ${timeStr}`;
|
||||
}
|
||||
|
||||
// ── Subcommand: status ────────────────────────────────────────────────────
|
||||
|
||||
async function cmdStatus(flags, io) {
|
||||
@@ -329,7 +376,45 @@ async function cmdUsage(flags, io) {
|
||||
} else {
|
||||
io.log(' (no 24h usage data — server may not have processed any requests yet)');
|
||||
}
|
||||
if (Array.isArray(body.quota) && body.quota.length > 0) {
|
||||
// F4 (v0.5.1 codex post-release review Q4): prefer quota_v2 when present
|
||||
// (server v0.5.0+), fall back to legacy quota array on older servers.
|
||||
// Authority: ADR 0008 Amendment 2 (quota_v2 shape).
|
||||
if (Array.isArray(body.quota_v2) && body.quota_v2.length > 0) {
|
||||
io.log('');
|
||||
io.log(colorize('Per-provider quota (live)', ANSI.bold, io.useColor));
|
||||
io.log('─'.repeat(60));
|
||||
for (const p of body.quota_v2) {
|
||||
const label = String(p.provider ?? '?').toUpperCase().padEnd(12);
|
||||
const status = p.status ?? 'unavailable';
|
||||
if (status === 'unavailable') {
|
||||
io.log(` ${colorize(label, ANSI.gray, io.useColor)} unavailable ${p.reason ?? 'no public quota api'}`);
|
||||
} else if (status === 'unreachable') {
|
||||
const fk = p.failure?.kind ?? 'unknown';
|
||||
const fm = p.failure?.message ?? 'probe failed';
|
||||
io.log(` ${colorize(label, ANSI.red, io.useColor)} ❌ no cached data — failure: ${fk} (${fm})`);
|
||||
} else {
|
||||
// live or stale
|
||||
const staleWarn = status === 'stale'
|
||||
? colorize(` ⚠ stale${p.last_fresh_at ? ` (${formatAgo(Date.now() - p.last_fresh_at)})` : ''} failure: ${p.failure?.kind ?? 'unknown'}`, ANSI.yellow, io.useColor)
|
||||
: '';
|
||||
const util = p.utilization ?? {};
|
||||
const reset = p.reset ?? {};
|
||||
const parts = [];
|
||||
for (const window of ['5h', '7d']) {
|
||||
const frac = util[window];
|
||||
const resetEpoch = reset[window];
|
||||
if (frac != null) {
|
||||
const pct = `${Math.round(frac * 100)}%`;
|
||||
const rst = formatResetCountdown(resetEpoch);
|
||||
parts.push(`${window}: ${colorize(pct, frac >= 0.8 ? ANSI.red : frac >= 0.5 ? ANSI.yellow : ANSI.green, io.useColor)} (${rst})`);
|
||||
}
|
||||
}
|
||||
const binding = p.representative_claim ? ` binding: ${p.representative_claim.replace('_', '-')}` : '';
|
||||
io.log(` ${colorize(label, ANSI.bold, io.useColor)} ${colorize(status, status === 'live' ? ANSI.green : ANSI.yellow, io.useColor).padEnd(6)} ${parts.join(' ')}${binding}${staleWarn}`);
|
||||
}
|
||||
}
|
||||
} else if (Array.isArray(body.quota) && body.quota.length > 0) {
|
||||
// Legacy fallback for pre-v0.5.0 servers
|
||||
io.log('');
|
||||
io.log(colorize('Per-provider quota', ANSI.bold, io.useColor));
|
||||
io.log('─'.repeat(60));
|
||||
|
||||
Reference in New Issue
Block a user