From d861a9c2c0fe9296945111033c5bf4e34ff37387 Mon Sep 17 00:00:00 2001 From: "itarun.p" Date: Mon, 20 Jul 2026 06:55:59 +0700 Subject: [PATCH 1/6] refactor(local-only): remove cloud upload and device pairing from the CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step of making this fork local-only. Two things go away here: the queue upload path and the browser-pairing command that existed to obtain the credential it needed. `sync` no longer resolves a runtime cloud config, no longer calls drainQueueToCloud, and no longer POSTs to /functions/tokentracker-ingest. Parsing is untouched — the local queue still fills exactly as before, it simply has nowhere to be sent. Removing the upload call alone would have left a trap: the queue offset only ever advanced on a successful upload, so `pendingBytes` would have been permanently positive and the auto-retry scheduler would have rebooked itself forever with nothing to retry. The whole retry chain goes with it — scheduleAutoRetry, clearAutoRetry, spawnAutoRetryProcess, buildAutoRetryScript, coerceRetryMs, the auto.retry.json artifact and the upload throttle state. `device-login` is deleted outright, along with its CLI registration, help text and test. Its only purpose was pairing a headless session with a browser sign-in to mint a device token. Help text now says what is true: sync parses into the local queue and nothing is uploaded. Zero new test failures — 778/782, the same 4 pre-existing parseKiroCliIncremental failures that are red on main (#65). The count drops from 795 because device-login.test.js is gone with its subject. Refs #67 (local-only direction) --- src/cli.js | 13 +- src/commands/device-login.js | 161 ------------------------- src/commands/sync.js | 226 ----------------------------------- test/device-login.test.js | 134 --------------------- 4 files changed, 2 insertions(+), 532 deletions(-) delete mode 100644 src/commands/device-login.js delete mode 100644 test/device-login.test.js diff --git a/src/cli.js b/src/cli.js index cf1c098b..d313c149 100644 --- a/src/cli.js +++ b/src/cli.js @@ -5,7 +5,6 @@ const { cmdDiagnostics } = require("./commands/diagnostics"); const { cmdDoctor } = require("./commands/doctor"); const { cmdUninstall } = require("./commands/uninstall"); const { cmdServe } = require("./commands/serve"); -const { cmdDeviceLogin } = require("./commands/device-login"); const { cmdWrapped } = require("./commands/wrapped"); async function run(argv) { @@ -44,9 +43,6 @@ async function run(argv) { case "uninstall": await cmdUninstall(rest); return; - case "device-login": - await cmdDeviceLogin(rest); - return; case "wrapped": await cmdWrapped(rest); return; @@ -70,23 +66,18 @@ function printHelp() { " npx --yes @ipv9/tokentracker-cli [--debug] diagnostics [--out diagnostics.json]", " npx --yes @ipv9/tokentracker-cli [--debug] doctor [--json] [--out doctor.json] [--base-url ]", " npx --yes @ipv9/tokentracker-cli [--debug] uninstall [--purge]", - " npx --yes @ipv9/tokentracker-cli [--debug] device-login [--json] [--base-url ]", " npx --yes @ipv9/tokentracker-cli [--debug] wrapped [--year 2026] [--json]", "", "Notes:", - " - init: consent first, local setup next, browser sign-in last.", + " - init: consent first, then local setup. This build is local-only.", " - --yes skips the consent menu (non-interactive safe).", " - --dry-run previews changes without writing files.", - " - optional: --link-code skips browser login when provided by Dashboard.", " - Every Code notify installs when ~/.code/config.toml exists.", " - OpenClaw hook auto-links when OpenClaw is installed (requires gateway restart).", - " - auto sync waits for a device token.", - " - optional: --dashboard-url for hosted landing.", " - serve prints the local dashboard URL; pass --open to ask the OS to open a browser.", - " - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl, then uploads token deltas only when cloud credentials are configured.", + " - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl into the local queue. Nothing is uploaded.", " - --from-openclaw marks sync runs triggered by OpenClaw hooks.", " - --debug shows original backend errors.", - " - device-login pairs a headless CLI / SSH session with a browser sign-in (15-min code).", "", ].join("\n"), ); diff --git a/src/commands/device-login.js b/src/commands/device-login.js deleted file mode 100644 index 1d8cb2ad..00000000 --- a/src/commands/device-login.js +++ /dev/null @@ -1,161 +0,0 @@ -"use strict"; - -const os = require("node:os"); -const path = require("node:path"); -const fs = require("node:fs/promises"); - -const { readJson, writeJson } = require("../lib/fs"); -const { resolveTrackerPaths } = require("../lib/tracker-paths"); - -const DEFAULT_BASE_URL = "https://srctyff5.us-east.insforge.app"; -const POLL_INTERVAL_MS = 5_000; -const ABSOLUTE_TIMEOUT_MS = 16 * 60 * 1000; // matches the 15-min server window with a small buffer - -function readBaseUrl(config) { - return ( - process.env.TOKENTRACKER_BASE_URL || - process.env.TOKENTRACKER_API_URL || - config?.baseUrl || - DEFAULT_BASE_URL - ); -} - -async function authorize({ baseUrl, clientInfo }) { - const res = await fetch(`${baseUrl}/functions/tokentracker-device-flow-authorize`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ client_info: clientInfo }), - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(`authorize failed (HTTP ${res.status}): ${text.slice(0, 200)}`); - } - return res.json(); -} - -async function pollOnce({ baseUrl, deviceCode }) { - const res = await fetch(`${baseUrl}/functions/tokentracker-device-flow-poll`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ device_code: deviceCode }), - }); - const data = await res.json().catch(() => ({})); - // 404 = unknown, 410 = expired, 200 = {status, user_id?, device_token?}. - // Anything else (502 from a misconfigured edge, 5xx during deploy, …) must - // bubble up as a network-style error — masking it as "unknown" would tell - // the user their device_code was evicted when it wasn't. - if (!res.ok && res.status !== 404 && res.status !== 410) { - throw new Error(`poll HTTP ${res.status}: ${(data?.error ?? "").toString().slice(0, 200)}`); - } - return { - status: data.status ?? "unknown", - user_id: data.user_id ?? null, - deviceToken: data.device_token ?? data.deviceToken ?? null, - deviceId: data.device_id ?? data.deviceId ?? null, - httpStatus: res.status, - }; -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -async function cmdDeviceLogin(argv = [], options = {}) { - const opts = parseArgs(argv); - const home = options.home || os.homedir(); - const sleepFn = options.sleep || sleep; - const { trackerDir } = await resolveTrackerPaths({ home }); - const configPath = path.join(trackerDir, "config.json"); - const config = (await readJson(configPath)) || {}; - const baseUrl = opts.baseUrl || readBaseUrl(config); - - const clientInfo = `${os.platform()}-${os.arch()} ${os.hostname()}`; - process.stdout.write(`Requesting device code from ${baseUrl}...\n`); - const authResp = await authorize({ baseUrl, clientInfo }); - - if (opts.json) { - process.stdout.write(JSON.stringify(authResp, null, 2) + "\n"); - } else { - process.stdout.write( - [ - "", - " Sign in from a browser:", - ` ${authResp.verification_uri_complete || authResp.verification_uri}`, - "", - ` Or visit ${authResp.verification_uri} and enter the code:`, - "", - ` ${authResp.user_code}`, - "", - ` This code expires in ${Math.round(authResp.expires_in / 60)} minutes.`, - " Polling every 5 seconds until you approve…", - "", - ].join("\n"), - ); - } - - const startedAt = Date.now(); - let consecutiveErrors = 0; - const MAX_BACKOFF_MS = 30_000; - while (Date.now() - startedAt < ABSOLUTE_TIMEOUT_MS) { - // Exponential backoff on consecutive network failures (capped at 30s) so - // a flaky network doesn't hammer the server at the full 5s cadence for - // the entire 15-minute window. ±20% jitter on retries prevents - // thundering-herd reconnects when many CLIs lose connectivity at once. - let wait = - consecutiveErrors === 0 - ? POLL_INTERVAL_MS - : Math.min(POLL_INTERVAL_MS * Math.pow(2, consecutiveErrors - 1), MAX_BACKOFF_MS); - if (consecutiveErrors > 0) { - const jitter = wait * 0.2 * (Math.random() * 2 - 1); - wait = Math.max(POLL_INTERVAL_MS, wait + jitter); - } - await sleepFn(wait); - let result; - try { - result = await pollOnce({ baseUrl, deviceCode: authResp.device_code }); - consecutiveErrors = 0; - } catch (e) { - consecutiveErrors++; - process.stderr.write(`poll error (retry ${consecutiveErrors}): ${e?.message || e}\n`); - continue; - } - if (result.status === "approved" && result.user_id) { - if (!result.deviceToken) { - throw new Error("device login approved but server did not return a device token"); - } - const next = { - ...config, - baseUrl, - user_id: result.user_id, - deviceToken: result.deviceToken, - deviceId: result.deviceId || config.deviceId, - device_login_at: new Date().toISOString(), - }; - await writeJson(configPath, next); - process.stdout.write(`\n✓ Approved. device token written to ${configPath}\n`); - return; - } - if (result.status === "expired") { - throw new Error("device_code expired — re-run `tracker device-login`"); - } - if (result.status === "unknown") { - throw new Error("device_code is unknown — server may have evicted it"); - } - // status === "pending" → just keep polling silently - } - throw new Error("device-login timed out without approval"); -} - -function parseArgs(argv) { - const out = { json: false, baseUrl: null }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a === "--json") out.json = true; - else if (a === "--base-url") { - out.baseUrl = argv[++i] || null; - } else throw new Error(`Unknown option: ${a}`); - } - return out; -} - -module.exports = { cmdDeviceLogin, authorize, pollOnce }; diff --git a/src/commands/sync.js b/src/commands/sync.js index d4406742..bc01cd7e 100644 --- a/src/commands/sync.js +++ b/src/commands/sync.js @@ -62,13 +62,6 @@ const { } = require("../lib/rollout"); const { computeClaudeGroundTruthBuckets } = require("../lib/claude-categorizer"); const { createProgress, renderBar, formatNumber, formatBytes } = require("../lib/progress"); -const { - normalizeState: normalizeUploadState, - decideAutoUpload, - recordUploadFailure, - recordUploadSuccess, - parseRetryAfterMs, -} = require("../lib/upload-throttle"); const { isCursorInstalled, extractCursorSessionToken, @@ -146,14 +139,11 @@ async function cmdSync(argv) { const queueStatePath = path.join(trackerDir, "queue.state.json"); const projectQueuePath = path.join(trackerDir, "project.queue.jsonl"); const projectQueueStatePath = path.join(trackerDir, "project.queue.state.json"); - const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json"); const grokSignalPath = path.join(trackerDir, "grok-last-session.json"); const legacyGrokSignalPath = path.join(trackerDir, "tracker", "grok-last-session.json"); const config = await readJson(configPath); const cursors = (await readJson(cursorsPath)) || { version: 1, files: {}, updatedAt: null }; - const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath)); - let uploadThrottleState = uploadThrottle; let grokHookSignal = null; let grokHookSignalPath = null; for (const candidate of [grokSignalPath, legacyGrokSignalPath]) { @@ -928,69 +918,7 @@ async function cmdSync(argv) { progress?.stop(); - const runtime = resolveRuntimeConfig({ config: config || {}, env: process.env }); - - let uploadResult = { inserted: 0, skipped: 0 }; - let uploadAttempted = false; - if (runtime.deviceToken && runtime.baseUrl) { - uploadAttempted = true; - try { - uploadResult = await drainQueueToCloud({ - baseUrl: runtime.baseUrl, - deviceToken: runtime.deviceToken, - queuePath, - queueStatePath, - maxBatches: opts.drain ? 100 : 5, - batchSize: 200, - }); - // Record success so the exponential backoff step resets — otherwise - // a single past failure keeps us pessimistically throttled forever. - uploadThrottleState = recordUploadSuccess({ - nowMs: Date.now(), - state: uploadThrottleState, - }); - await writeJson(uploadThrottlePath, uploadThrottleState); - } catch (e) { - // Persist a backoff on 429 / 5xx so the next auto-sync waits instead - // of retrying immediately and making the rate-limit worse. The - // throttle module already parses Retry-After when we surface it on - // the error object (drainQueueToCloud stamps err.status + err.retryAfterMs). - uploadThrottleState = recordUploadFailure({ - nowMs: Date.now(), - state: uploadThrottleState, - error: e, - }); - await writeJson(uploadThrottlePath, uploadThrottleState); - if (!opts.auto) { - process.stderr.write(`Upload error: ${e?.message || e}\n`); - } - } - } - - const afterState = (await readJson(queueStatePath)) || { offset: 0 }; - const queueSize = await safeStatSize(queuePath); - const projectAfterState = (await readJson(projectQueueStatePath)) || { offset: 0 }; - const projectQueueSize = await safeStatSize(projectQueuePath); - const pendingBytes = - Math.max(0, queueSize - Number(afterState.offset || 0)) + - Math.max(0, projectQueueSize - Number(projectAfterState.offset || 0)); - - if (pendingBytes <= 0) { - await clearAutoRetry(trackerDir); - } else if (opts.auto && uploadAttempted) { - const retryAtMs = Number(uploadThrottleState?.nextAllowedAtMs || 0); - if (retryAtMs > Date.now()) { - await scheduleAutoRetry({ - trackerDir, - retryAtMs, - reason: "backlog", - pendingBytes, - source: "auto-backlog", - autoRetryNoSpawn: runtime.autoRetryNoSpawn, - }); - } - } const totalParsed = parseResult.filesProcessed + @@ -1045,9 +973,6 @@ async function cmdSync(argv) { const summary = { totalParsed, totalBuckets, - upload: uploadResult, - uploadAttempted, - pendingBytes, }; if (!opts.auto) { @@ -1056,12 +981,6 @@ async function cmdSync(argv) { "Sync finished:", `- Parsed files: ${totalParsed}`, `- New 30-min buckets queued: ${totalBuckets}`, - runtime.deviceToken - ? `- Uploaded: ${uploadResult.inserted} inserted, ${uploadResult.skipped} skipped` - : "- Uploaded: skipped (no device token)", - runtime.deviceToken && pendingBytes > 0 && !opts.drain - ? `- Remaining: ${formatBytes(pendingBytes)} pending (run sync again, or use --drain)` - : null, "", ] .filter(Boolean) @@ -1276,102 +1195,10 @@ function deriveAutoSkipReason({ decision, state }) { return "throttled"; } -async function scheduleAutoRetry({ - trackerDir, - retryAtMs, - reason, - pendingBytes, - source, - autoRetryNoSpawn, -}) { - const retryMs = coerceRetryMs(retryAtMs); - if (!retryMs) return { scheduled: false, retryAtMs: 0 }; - - const retryPath = path.join(trackerDir, AUTO_RETRY_FILENAME); - const nowMs = Date.now(); - const existing = await readJson(retryPath); - const existingMs = coerceRetryMs(existing?.retryAtMs); - if (existingMs && existingMs >= retryMs - 1000) { - return { scheduled: false, retryAtMs: existingMs }; - } - - const payload = { - version: 1, - retryAtMs: retryMs, - retryAt: new Date(retryMs).toISOString(), - reason: typeof reason === "string" && reason.length > 0 ? reason : "throttled", - pendingBytes: Math.max(0, Number(pendingBytes || 0)), - scheduledAt: new Date(nowMs).toISOString(), - source: typeof source === "string" ? source : "auto", - }; - - await writeJson(retryPath, payload); - const delayMs = Math.min(AUTO_RETRY_MAX_DELAY_MS, Math.max(0, retryMs - nowMs)); - if (delayMs <= 0) return { scheduled: false, retryAtMs: retryMs }; - if (autoRetryNoSpawn) { - return { scheduled: false, retryAtMs: retryMs }; - } - spawnAutoRetryProcess({ - retryPath, - trackerBinPath: path.join(trackerDir, "app", "bin", "tracker.js"), - fallbackPkg: "@ipv9/tokentracker-cli", - delayMs, - }); - return { scheduled: true, retryAtMs: retryMs }; -} -async function clearAutoRetry(trackerDir) { - const retryPath = path.join(trackerDir, AUTO_RETRY_FILENAME); - await fs.unlink(retryPath).catch(() => {}); -} -function spawnAutoRetryProcess({ retryPath, trackerBinPath, fallbackPkg, delayMs }) { - const script = buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs }); - try { - const child = cp.spawn(process.execPath, ["-e", script], { - detached: true, - stdio: "ignore", - env: process.env, - }); - child.unref(); - } catch (_e) {} -} - -function buildAutoRetryScript({ retryPath, trackerBinPath, fallbackPkg, delayMs }) { - return ( - `'use strict';\n` + - `const fs = require('node:fs');\n` + - `const cp = require('node:child_process');\n` + - `const retryPath = ${JSON.stringify(retryPath)};\n` + - `const trackerBinPath = ${JSON.stringify(trackerBinPath)};\n` + - `const fallbackPkg = ${JSON.stringify(fallbackPkg)};\n` + - `const delayMs = ${Math.max(0, Math.floor(delayMs || 0))};\n` + - `setTimeout(() => {\n` + - ` let retryAtMs = 0;\n` + - ` try {\n` + - ` const raw = fs.readFileSync(retryPath, 'utf8');\n` + - ` retryAtMs = Number(JSON.parse(raw).retryAtMs || 0);\n` + - ` } catch (_) {}\n` + - ` if (!retryAtMs || Date.now() + 1000 < retryAtMs) process.exit(0);\n` + - ` const argv = ['sync', '--auto', '--from-retry'];\n` + - ` const cmd = fs.existsSync(trackerBinPath)\n` + - ` ? [process.execPath, trackerBinPath, ...argv]\n` + - ` : ['npx', '--yes', fallbackPkg, ...argv];\n` + - ` try {\n` + - ` const child = cp.spawn(cmd[0], cmd.slice(1), { detached: true, stdio: 'ignore', env: process.env });\n` + - ` child.unref();\n` + - ` } catch (_) {}\n` + - `}, delayMs);\n` - ); -} - -function coerceRetryMs(v) { - const n = Number(v); - if (!Number.isFinite(n) || n <= 0) return 0; - return Math.floor(n); -} async function writeOpenclawSignal(trackerDir) { const openclawSignalPath = path.join(trackerDir, "openclaw.signal"); @@ -1382,63 +1209,10 @@ async function writeOpenclawSignal(trackerDir) { } } -const AUTO_RETRY_FILENAME = "auto.retry.json"; const AUTO_RETRY_MAX_DELAY_MS = 2 * 60 * 60 * 1000; -const INGEST_SLUG = "tokentracker-ingest"; const MAX_INGEST_BUCKETS = 500; -async function drainQueueToCloud({ baseUrl, deviceToken, queuePath, queueStatePath, maxBatches = 5, batchSize = 200 }) { - const state = (await readJson(queueStatePath)) || { offset: 0 }; - let offset = Number(state.offset || 0); - let inserted = 0; - let skipped = 0; - - const queueSize = await safeStatSize(queuePath); - const limit = Math.min(Math.max(1, Math.floor(Number(batchSize || 200))), MAX_INGEST_BUCKETS); - - for (let batch = 0; batch < maxBatches; batch++) { - if (offset >= queueSize) break; - const result = await readQueueBatch(queuePath, offset, limit); - if (result.buckets.length === 0) break; - - const root = baseUrl.replace(/\/$/, ""); - const anonKey = process.env.TOKENTRACKER_INSFORGE_ANON_KEY || ""; - const headers = { - "Content-Type": "application/json", - Accept: "application/json", - Authorization: `Bearer ${deviceToken}`, - }; - if (anonKey) headers.apikey = anonKey; - const res = await fetch(`${root}/functions/${INGEST_SLUG}`, { - method: "POST", - headers, - body: JSON.stringify({ hourly: result.buckets }), - }); - - const rawText = await res.text().catch(() => ""); - let data = {}; - try { data = JSON.parse(rawText); } catch { data = {}; } - if (!res.ok) { - const err = new Error(`HTTP ${res.status}: ${rawText.substring(0, 500)}`); - err.status = res.status; - const retryAfter = res.headers?.get?.("Retry-After") ?? null; - const retryAfterMs = parseRetryAfterMs(retryAfter); - if (retryAfterMs !== null) err.retryAfterMs = retryAfterMs; - throw err; - } - - inserted += Number(data?.inserted || 0); - skipped += Number(data?.skipped || 0); - - offset = result.nextOffset; - state.offset = offset; - state.updatedAt = new Date().toISOString(); - await writeJson(queueStatePath, state); - } - - return { inserted, skipped }; -} async function readQueueBatch(queuePath, startOffset, maxBuckets) { const st = await fs.stat(queuePath).catch(() => null); diff --git a/test/device-login.test.js b/test/device-login.test.js deleted file mode 100644 index 19b54340..00000000 --- a/test/device-login.test.js +++ /dev/null @@ -1,134 +0,0 @@ -"use strict"; - -const test = require("node:test"); -const assert = require("node:assert/strict"); -const fs = require("node:fs/promises"); -const os = require("node:os"); -const path = require("node:path"); - -const { cmdDeviceLogin, pollOnce } = require("../src/commands/device-login"); - -test("pollOnce maps approved device token fields from server response", async () => { - const originalFetch = global.fetch; - global.fetch = async (_url, _opts) => ({ - ok: true, - status: 200, - async json() { - return { - status: "approved", - user_id: "user-1", - device_token: "device-token-1", - device_id: "device-1", - }; - }, - }); - try { - const result = await pollOnce({ baseUrl: "https://example.invalid", deviceCode: "abc" }); - assert.equal(result.status, "approved"); - assert.equal(result.user_id, "user-1"); - assert.equal(result.deviceToken, "device-token-1"); - assert.equal(result.deviceId, "device-1"); - } finally { - global.fetch = originalFetch; - } -}); - -test("cmdDeviceLogin persists the approved device token used by sync", async () => { - const home = await fs.mkdtemp(path.join(os.tmpdir(), "device-login-")); - const calls = []; - const originalFetch = global.fetch; - const originalStdoutWrite = process.stdout.write; - global.fetch = async (url, opts) => { - calls.push({ url: String(url), body: opts?.body ? JSON.parse(opts.body) : null }); - if (String(url).endsWith("/tokentracker-device-flow-authorize")) { - return { - ok: true, - status: 200, - async json() { - return { - device_code: "d".repeat(64), - user_code: "ABCD-2345", - verification_uri: "https://www.tokentracker.cc/device", - verification_uri_complete: "https://www.tokentracker.cc/device?user_code=ABCD-2345", - expires_in: 900, - interval: 5, - }; - }, - }; - } - return { - ok: true, - status: 200, - async json() { - return { - status: "approved", - user_id: "user-1", - device_token: "device-token-1", - device_id: "device-1", - }; - }, - }; - }; - process.stdout.write = () => true; - - try { - await cmdDeviceLogin(["--base-url", "https://example.invalid"], { home, sleep: async () => {} }); - const config = JSON.parse( - await fs.readFile(path.join(home, ".tokentracker", "tracker", "config.json"), "utf8"), - ); - assert.equal(config.user_id, "user-1"); - assert.equal(config.deviceToken, "device-token-1"); - assert.equal(config.deviceId, "device-1"); - assert.equal(config.baseUrl, "https://example.invalid"); - assert.equal(calls.length, 2); - } finally { - process.stdout.write = originalStdoutWrite; - global.fetch = originalFetch; - await fs.rm(home, { recursive: true, force: true }); - } -}); - -test("cmdDeviceLogin rejects approved responses without a usable device token", async () => { - const home = await fs.mkdtemp(path.join(os.tmpdir(), "device-login-missing-token-")); - const originalFetch = global.fetch; - const originalStdoutWrite = process.stdout.write; - global.fetch = async (url) => { - if (String(url).endsWith("/tokentracker-device-flow-authorize")) { - return { - ok: true, - status: 200, - async json() { - return { - device_code: "d".repeat(64), - user_code: "ABCD-2345", - verification_uri: "https://www.tokentracker.cc/device", - expires_in: 900, - }; - }, - }; - } - return { - ok: true, - status: 200, - async json() { - return { status: "approved", user_id: "user-1" }; - }, - }; - }; - process.stdout.write = () => true; - - try { - await assert.rejects( - () => cmdDeviceLogin(["--base-url", "https://example.invalid"], { home, sleep: async () => {} }), - /server did not return a device token/, - ); - await assert.rejects( - () => fs.readFile(path.join(home, ".tokentracker", "tracker", "config.json"), "utf8"), - { code: "ENOENT" }, - ); - } finally { - process.stdout.write = originalStdoutWrite; - global.fetch = originalFetch; - await fs.rm(home, { recursive: true, force: true }); - } -}); From 1cec2305e1c9520a4a53f5aee794dbb980d46241 Mon Sep 17 00:00:00 2001 From: "itarun.p" Date: Tue, 21 Jul 2026 07:07:00 +0700 Subject: [PATCH 2/6] refactor(local-only): remove the cloud surface from the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second phase of the local-only change. Deletes the Leaderboard, Login, Device pairing, Marketing landing and Share features from the dashboard, along with the InsForge auth context and every helper that existed to serve them. The file count hid the real work. `lib/auth-token` was imported by six data hooks (usage, trend, model-breakdown, heatmap, project-summary, pulse), so the dashboard had been gating its *local* API fetches on a *cloud* access token. Removing the token meant unpicking that gate in each one; leaving it would have left the hooks waiting for a token that can never arrive, and the dashboard permanently empty. Two tests were INVERTED rather than deleted, because their subjects did not disappear — the intended behavior reversed: - `app-route-pathname-guard` asserted /leaderboard and /login exist; it now asserts they stay gone, along with the pages behind them. - `dashboard-missing-jwt-guard` asserted the hooks refuse to fetch without a token; it now asserts they do not gate on one. Deleting it would have left the reversal unprotected. Test files whose subject was genuinely removed were deleted outright. Also rewires the HTML meta. `index.html` reads its title and Open Graph tags from copy keys, and those keys lived under `landing.meta.*`. Dropping them with the landing page left the dashboard shipping an empty `` — silently, because a missing copy key only warns and the build still exits 0. They are now `app.meta.*` with local-appropriate values, and `share.html` is removed from the Vite inputs and the Vercel rewrites rather than being built as a dead entry. Verified: `npm test` 789 pass, `npm --prefix dashboard test` 233 pass, `npm run dashboard:build` exit 0 with no missing-key warnings, `<title>` renders as TokenTracker, and `grep -rlEi "insforge|deviceToken|tokentracker\.cc|leaderboard" dashboard/src` returns nothing. The CLI side (local-api auth proxy, runtime-config, doctor, diagnostics, status, init) is still to do. --- dashboard/share.html | 139 -- dashboard/src/App.jsx | 207 +-- dashboard/src/App.navigation-preload.test.jsx | 82 +- dashboard/src/App.preload.test.jsx | 172 +- .../components/InsforgeUserHeaderControls.jsx | 194 --- .../src/components/LeaderboardAvatar.jsx | 76 - .../LeaderboardProviderColumnHeader.jsx | 34 - .../LeaderboardProviderColumnHeader.test.jsx | 14 - .../src/components/LeaderboardSkeleton.jsx | 116 -- .../src/components/LeaderboardSummaryCard.jsx | 116 -- dashboard/src/components/LoginCard.jsx | 357 ---- dashboard/src/components/LoginModal.jsx | 68 - .../src/components/SortableColumnHeader.jsx | 91 - .../leaderboard/LeaderboardProfileModal.jsx | 682 -------- .../components/settings/AccountSection.jsx | 139 -- .../settings/AccountSectionParts.jsx | 273 --- .../settings/AccountSectionUtils.js | 26 - .../settings/useAccountProfileSettings.js | 290 ---- dashboard/src/content/copy.csv | 1511 +++++++---------- dashboard/src/content/i18n/ja/core.json | 56 - dashboard/src/content/i18n/ja/dashboard.json | 43 +- dashboard/src/content/i18n/ja/marketing.json | 171 -- dashboard/src/content/i18n/ko/core.json | 56 - dashboard/src/content/i18n/ko/dashboard.json | 43 +- dashboard/src/content/i18n/ko/marketing.json | 171 -- dashboard/src/content/i18n/zh-TW/core.json | 56 - .../src/content/i18n/zh-TW/dashboard.json | 43 +- .../src/content/i18n/zh-TW/marketing.json | 171 -- dashboard/src/content/i18n/zh/core.json | 56 - dashboard/src/content/i18n/zh/dashboard.json | 43 +- dashboard/src/content/i18n/zh/marketing.json | 171 -- .../src/contexts/InsforgeAuthContext.d.ts | 32 - .../src/contexts/InsforgeAuthContext.jsx | 319 ---- dashboard/src/contexts/LoginModalContext.jsx | 23 - .../__tests__/InsforgeAuthContext.test.jsx | 104 -- dashboard/src/hooks/use-activity-heatmap.ts | 23 +- dashboard/src/hooks/use-cloud-usage-sync.ts | 59 - dashboard/src/hooks/use-column-order.js | 82 - .../src/hooks/use-project-usage-summary.ts | 28 +- dashboard/src/hooks/use-pulse.ts | 15 +- dashboard/src/hooks/use-trend-data.test.tsx | 5 - dashboard/src/hooks/use-trend-data.ts | 26 +- dashboard/src/hooks/use-usage-data.ts | 34 +- .../src/hooks/use-usage-model-breakdown.ts | 20 +- .../__tests__/api-public-visibility.test.ts | 110 -- .../src/lib/__tests__/auth-token.test.ts | 75 - .../lib/__tests__/leaderboard-columns.test.js | 10 - .../src/lib/__tests__/leaderboard-ui.test.js | 102 -- dashboard/src/lib/api.ts | 262 --- dashboard/src/lib/auth-token.d.ts | 17 - dashboard/src/lib/auth-token.js | 115 -- dashboard/src/lib/cloud-sync-prefs.ts | 80 - dashboard/src/lib/cloud-sync.ts | 188 -- dashboard/src/lib/config.ts | 29 +- dashboard/src/lib/copy.ts | 8 - dashboard/src/lib/dashboard-preload.d.ts | 35 +- dashboard/src/lib/dashboard-preload.js | 234 +-- .../lib/dashboard-preload.leaderboard.test.js | 373 ---- .../src/lib/dashboard-preload.silent.test.js | 55 - dashboard/src/lib/dashboard-preload.test.js | 135 +- dashboard/src/lib/insforge-config.ts | 79 - dashboard/src/lib/install-status.js | 13 - dashboard/src/lib/leaderboard-columns.js | 69 - dashboard/src/lib/mock-data.ts | 207 --- dashboard/src/main.jsx | 5 +- dashboard/src/pages/DashboardPage.jsx | 275 +-- dashboard/src/pages/DevicePage.jsx | 155 -- dashboard/src/pages/IpCheckPage.jsx | 4 +- dashboard/src/pages/LandingPage.jsx | 108 -- dashboard/src/pages/LeaderboardPage.jsx | 922 ---------- dashboard/src/pages/LeaderboardPage.test.jsx | 519 ------ .../src/pages/LeaderboardProfilePage.jsx | 133 -- dashboard/src/pages/LoginPage.jsx | 420 ----- .../src/pages/NativeAuthCallbackPage.jsx | 161 -- dashboard/src/pages/SkillsPage.jsx | 5 +- dashboard/src/ui/components/Sidebar.jsx | 17 +- dashboard/src/ui/components/Sidebar.test.jsx | 16 +- .../dashboard/components/ActivityHeatmap.jsx | 18 +- .../dashboard/components/CommandPalette.jsx | 2 +- .../ui/dashboard/components/HeroSummary.jsx | 14 +- .../ui/dashboard/components/LikeButton.jsx | 244 --- .../ui/dashboard/components/MacAppBanner.jsx | 145 -- .../ui/dashboard/components/TrendMonitor.jsx | 4 +- .../__tests__/ActivityHeatmap.test.jsx | 4 +- .../src/ui/dashboard/views/DashboardView.jsx | 28 +- dashboard/src/ui/marketing/LogoCarousel.jsx | 149 -- .../src/ui/marketing/MarketingLanding.jsx | 693 -------- dashboard/src/ui/marketing/agent-logos.js | 24 - .../ui/marketing/components/BorderGlow.jsx | 239 --- .../src/ui/marketing/components/LaserFlow.jsx | 440 ----- .../src/ui/marketing/components/LightRays.jsx | 384 ----- .../ui/marketing/components/SpotlightCard.jsx | 48 - .../ui/marketing/components/TiltedCard.jsx | 90 - dashboard/src/ui/share/HeatmapStrip.jsx | 67 - dashboard/src/ui/share/ShareCard.d.ts | 11 - dashboard/src/ui/share/ShareCard.jsx | 46 - dashboard/src/ui/share/ShareModal.tsx | 469 ----- .../src/ui/share/build-share-card-data.ts | 274 --- dashboard/src/ui/share/capture-share-card.ts | 117 -- dashboard/src/ui/share/native-save.ts | 129 -- .../src/ui/share/share-card-constants.ts | 7 - dashboard/src/ui/share/use-share-card-data.ts | 113 -- .../ui/share/variants/AnnualReportCard.jsx | 240 --- .../src/ui/share/variants/BroadsheetCard.jsx | 430 ----- dashboard/src/vite-env.d.ts | 4 - dashboard/vercel.json | 1 - dashboard/vite.config.js | 33 +- test/app-route-pathname-guard.test.js | 26 +- test/auth-token.test.js | 87 - test/cloud-sync-prefs.test.js | 56 - test/cloud-sync-rotation.test.js | 45 - test/dashboard-missing-jwt-guard.test.js | 24 +- test/install-status.test.js | 38 - test/landing-cta-copy.test.js | 44 - test/landing-screenshot.test.js | 107 -- test/localization-regressions.test.js | 2 +- test/mock-leaderboard.test.js | 40 - test/settings-account-user-id.test.js | 28 - test/share-card-data.test.js | 126 -- 119 files changed, 806 insertions(+), 15657 deletions(-) delete mode 100644 dashboard/share.html delete mode 100644 dashboard/src/components/InsforgeUserHeaderControls.jsx delete mode 100644 dashboard/src/components/LeaderboardAvatar.jsx delete mode 100644 dashboard/src/components/LeaderboardProviderColumnHeader.jsx delete mode 100644 dashboard/src/components/LeaderboardProviderColumnHeader.test.jsx delete mode 100644 dashboard/src/components/LeaderboardSkeleton.jsx delete mode 100644 dashboard/src/components/LeaderboardSummaryCard.jsx delete mode 100644 dashboard/src/components/LoginCard.jsx delete mode 100644 dashboard/src/components/LoginModal.jsx delete mode 100644 dashboard/src/components/SortableColumnHeader.jsx delete mode 100644 dashboard/src/components/leaderboard/LeaderboardProfileModal.jsx delete mode 100644 dashboard/src/components/settings/AccountSection.jsx delete mode 100644 dashboard/src/components/settings/AccountSectionParts.jsx delete mode 100644 dashboard/src/components/settings/AccountSectionUtils.js delete mode 100644 dashboard/src/components/settings/useAccountProfileSettings.js delete mode 100644 dashboard/src/content/i18n/ja/marketing.json delete mode 100644 dashboard/src/content/i18n/ko/marketing.json delete mode 100644 dashboard/src/content/i18n/zh-TW/marketing.json delete mode 100644 dashboard/src/content/i18n/zh/marketing.json delete mode 100644 dashboard/src/contexts/InsforgeAuthContext.d.ts delete mode 100644 dashboard/src/contexts/InsforgeAuthContext.jsx delete mode 100644 dashboard/src/contexts/LoginModalContext.jsx delete mode 100644 dashboard/src/contexts/__tests__/InsforgeAuthContext.test.jsx delete mode 100644 dashboard/src/hooks/use-cloud-usage-sync.ts delete mode 100644 dashboard/src/hooks/use-column-order.js delete mode 100644 dashboard/src/lib/__tests__/api-public-visibility.test.ts delete mode 100644 dashboard/src/lib/__tests__/auth-token.test.ts delete mode 100644 dashboard/src/lib/__tests__/leaderboard-columns.test.js delete mode 100644 dashboard/src/lib/__tests__/leaderboard-ui.test.js delete mode 100644 dashboard/src/lib/auth-token.d.ts delete mode 100644 dashboard/src/lib/auth-token.js delete mode 100644 dashboard/src/lib/cloud-sync-prefs.ts delete mode 100644 dashboard/src/lib/cloud-sync.ts delete mode 100644 dashboard/src/lib/dashboard-preload.leaderboard.test.js delete mode 100644 dashboard/src/lib/dashboard-preload.silent.test.js delete mode 100644 dashboard/src/lib/insforge-config.ts delete mode 100644 dashboard/src/lib/install-status.js delete mode 100644 dashboard/src/lib/leaderboard-columns.js delete mode 100644 dashboard/src/pages/DevicePage.jsx delete mode 100644 dashboard/src/pages/LandingPage.jsx delete mode 100644 dashboard/src/pages/LeaderboardPage.jsx delete mode 100644 dashboard/src/pages/LeaderboardPage.test.jsx delete mode 100644 dashboard/src/pages/LeaderboardProfilePage.jsx delete mode 100644 dashboard/src/pages/LoginPage.jsx delete mode 100644 dashboard/src/pages/NativeAuthCallbackPage.jsx delete mode 100644 dashboard/src/ui/dashboard/components/LikeButton.jsx delete mode 100644 dashboard/src/ui/dashboard/components/MacAppBanner.jsx delete mode 100644 dashboard/src/ui/marketing/LogoCarousel.jsx delete mode 100644 dashboard/src/ui/marketing/MarketingLanding.jsx delete mode 100644 dashboard/src/ui/marketing/agent-logos.js delete mode 100644 dashboard/src/ui/marketing/components/BorderGlow.jsx delete mode 100644 dashboard/src/ui/marketing/components/LaserFlow.jsx delete mode 100644 dashboard/src/ui/marketing/components/LightRays.jsx delete mode 100644 dashboard/src/ui/marketing/components/SpotlightCard.jsx delete mode 100644 dashboard/src/ui/marketing/components/TiltedCard.jsx delete mode 100644 dashboard/src/ui/share/HeatmapStrip.jsx delete mode 100644 dashboard/src/ui/share/ShareCard.d.ts delete mode 100644 dashboard/src/ui/share/ShareCard.jsx delete mode 100644 dashboard/src/ui/share/ShareModal.tsx delete mode 100644 dashboard/src/ui/share/build-share-card-data.ts delete mode 100644 dashboard/src/ui/share/capture-share-card.ts delete mode 100644 dashboard/src/ui/share/native-save.ts delete mode 100644 dashboard/src/ui/share/share-card-constants.ts delete mode 100644 dashboard/src/ui/share/use-share-card-data.ts delete mode 100644 dashboard/src/ui/share/variants/AnnualReportCard.jsx delete mode 100644 dashboard/src/ui/share/variants/BroadsheetCard.jsx delete mode 100644 test/auth-token.test.js delete mode 100644 test/cloud-sync-prefs.test.js delete mode 100644 test/cloud-sync-rotation.test.js delete mode 100644 test/install-status.test.js delete mode 100644 test/landing-cta-copy.test.js delete mode 100644 test/landing-screenshot.test.js delete mode 100644 test/mock-leaderboard.test.js delete mode 100644 test/settings-account-user-id.test.js delete mode 100644 test/share-card-data.test.js diff --git a/dashboard/share.html b/dashboard/share.html deleted file mode 100644 index a28c5139..00000000 --- a/dashboard/share.html +++ /dev/null @@ -1,139 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="UTF-8" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>__TOKENTRACKER_TITLE__ - - - - - - - - - - - - - - - - - - - - - - - -
-

Token Tracker Share Dashboard

-

- This page renders a shareable dashboard snapshot for Token Tracker token tracking data, including model - usage trends and project-level summaries. It is designed for quick sharing with teammates, - collaborators, or stakeholders who need clear usage context. -

-

- The shared view keeps the same core signal style as the main dashboard: concise metrics, readable - trend framing, and lightweight navigation. Use it when you want to communicate usage outcomes without - requiring the recipient to open the full private workspace. -

-

- In practice, teams use this page during sprint reviews, weekly engineering updates, and cost-analysis - discussions. Instead of manually summarizing logs, you can share one URL that highlights model usage, - project activity, and trend direction in a format that is easy to scan. -

- -

What this share page includes

-
    -
  • Snapshot-oriented presentation for token usage highlights.
  • -
  • Model and project usage context suitable for async reporting.
  • -
  • Fast navigation back to primary Token Tracker pages.
  • -
-

- If you are reviewing usage impact after adopting a new coding workflow, this page helps you present - changes clearly: which models were used, where token volume concentrated, and whether the trend is - rising or stabilizing. It is optimized for communication and fast interpretation. -

-

- For deeper analysis and operational actions, recipients can jump back to the main dashboard from the - links below, then inspect richer views such as leaderboard ranking, project-level usage, and historical - trends over custom time ranges. -

-

- Example use cases include comparing token efficiency before and after a model swap, validating budget - controls for a weekly sprint, and checking if newly introduced automation scripts are causing - unexpected model spikes. Teams often use this page when they want a concise status snapshot before - a planning meeting, because it keeps language and focus simple while still preserving key operational - context. -

-

- You can also reference this page as supporting material in PRs or postmortems: it helps answer - questions like “Did we improve or increase cost this cycle?”, “Which projects drove the largest increase?”, - and “What follow-up guardrails should we add next?”. -

- -

Quick links

- -
- -
- - - diff --git a/dashboard/src/App.jsx b/dashboard/src/App.jsx index 7e5746ec..52e3877a 100644 --- a/dashboard/src/App.jsx +++ b/dashboard/src/App.jsx @@ -1,59 +1,30 @@ -import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef } from "react"; +import React, { lazy, Suspense, useCallback, useMemo, useRef } from "react"; import { useLocation } from "react-router-dom"; import { Analytics } from "@vercel/analytics/react"; import { SpeedInsights } from "@vercel/speed-insights/react"; import { ErrorBoundary } from "./components/ErrorBoundary.jsx"; import { useLocale } from "./hooks/useLocale.js"; import { ThemeProvider } from "./ui/foundation/ThemeProvider.jsx"; -import { useInsforgeAuth } from "./contexts/InsforgeAuthContext.jsx"; -import { LoginModalProvider } from "./contexts/LoginModalContext.jsx"; -import { LoginModal } from "./components/LoginModal.jsx"; -import { getBackendBaseUrl, getLeaderboardBaseUrl } from "./lib/config"; -import { isMockEnabled } from "./lib/mock-data"; +import { getBackendBaseUrl } from "./lib/config"; import { isScreenshotModeEnabled } from "./lib/screenshot-mode"; -import { useCloudUsageSync } from "./hooks/use-cloud-usage-sync"; import { AppLayout } from "./ui/components/Sidebar.jsx"; import { CommandPalette } from "./ui/dashboard/components/CommandPalette.jsx"; import { ToastProvider } from "./ui/components/Toast.jsx"; import { - getLeaderboardPreloadContextKey, markDashboardMainContentVisible, preloadDashboardPageResource, - preloadDashboardPageResources, - preloadLeaderboardDefaultState, } from "./lib/dashboard-preload.js"; -// NativeAuthCallbackPage must be eager-imported: its module-level code -// captures the OAuth `insforge_code` query param synchronously at app -// boot, BEFORE the InsForge SDK's detectAuthCallback() strips it. Lazy -// loading delays the module until route render, by which point the -// param has already been removed — the page then falls through to the -// "Sign-in incomplete" failure state. -import { NativeAuthCallbackPage } from "./pages/NativeAuthCallbackPage.jsx"; // Pages are lazy-loaded so each route ships in its own chunk; keeps the -// initial main bundle small (was 1.9 MB before splitting, all 11 pages -// were bundled together). Routes are mutually exclusive, so only one +// initial main bundle small. Routes are mutually exclusive, so only one // chunk loads per navigation. const DashboardPage = lazy(() => import("./pages/DashboardPage.jsx").then((m) => ({ default: m.DashboardPage })), ); const IpCheckPage = lazy(() => import("./pages/IpCheckPage.jsx")); -const LandingPage = lazy(() => - import("./pages/LandingPage.jsx").then((m) => ({ default: m.LandingPage })), -); -const LeaderboardPage = lazy(() => - import("./pages/LeaderboardPage.jsx").then((m) => ({ default: m.LeaderboardPage })), -); -const LeaderboardProfilePage = lazy(() => - import("./pages/LeaderboardProfilePage.jsx").then((m) => ({ default: m.LeaderboardProfilePage })), -); const LimitsPage = lazy(() => import("./pages/LimitsPage.jsx").then((m) => ({ default: m.LimitsPage })), ); -const LoginPage = lazy(() => - import("./pages/LoginPage.jsx").then((m) => ({ default: m.LoginPage })), -); -const DevicePage = lazy(() => import("./pages/DevicePage.jsx")); const WrappedPage = lazy(() => import("./pages/WrappedPage.jsx")); const SettingsPage = lazy(() => import("./pages/SettingsPage.jsx").then((m) => ({ default: m.SettingsPage })), @@ -77,81 +48,24 @@ export default function App() { // across the tree — without unmounting lazy-loaded pages. const { resolvedLocale } = useLocale(); const location = useLocation(); - const insforge = useInsforgeAuth(); - useCloudUsageSync(); const dashboardMainContentVisibleRef = useRef(false); const dashboardResourcePreloadStartedRef = useRef(false); - const leaderboardStatePreloadContextKeysRef = useRef(new Set()); - const mockEnabled = isMockEnabled(); const enableVercelInsights = shouldEnableVercelInsights(); const screenshotMode = useMemo(() => { if (typeof window === "undefined") return false; return isScreenshotModeEnabled(window.location.search); }, []); const pathname = location?.pathname || "/"; - const pageUrl = new URL(window.location.href); - const sharePathname = pageUrl.pathname.replace(/\/+$/, "") || "/"; - const shareMatch = sharePathname.match(/^\/share\/([^/?#]+)$/i); - const tokenFromPath = shareMatch?.[1] || null; - const tokenFromQuery = pageUrl.searchParams.get("token") || null; - const publicToken = tokenFromPath || tokenFromQuery; - const publicMode = - sharePathname === "/share" || - sharePathname === "/share.html" || - sharePathname.startsWith("/share/"); - - const isLocalMode = - typeof window !== "undefined" && - (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); + const baseUrl = getBackendBaseUrl(); const normalizedPath = pathname.replace(/\/+$/, "") || "/"; const isDashboardDefaultPath = normalizedPath === "/" || normalizedPath === "/dashboard"; - const isLeaderboardPath = normalizedPath === "/leaderboard"; - // Standalone shareable profile page: /u/:userId (public, anonymous-visible). - const profileMatch = normalizedPath.match(/^\/u\/([^/]+)$/i); - const profileUserId = profileMatch ? profileMatch[1] : null; - - const cloudAuthSignedIn = Boolean(insforge.enabled && insforge.signedIn); - const signedIn = isLocalMode || cloudAuthSignedIn; - const sessionSoftExpired = false; - const baseUrl = getBackendBaseUrl(); - const isAuthGateTriggered = !signedIn && !mockEnabled && !isLocalMode; - const leaderboardAccessMode = mockEnabled - ? "mock" - : insforge.loading - ? "unavailable" - : cloudAuthSignedIn - ? "cloud" - : signedIn - ? "local" - : "unavailable"; - - const tryPreloadLeaderboardDefaultState = useCallback(() => { - if (isLocalMode) return; - if (!dashboardMainContentVisibleRef.current) return; - if (!mockEnabled && insforge.loading) return; - if (!mockEnabled && !signedIn) return; - const preloadOptions = { - accessMode: leaderboardAccessMode, - baseUrl: getLeaderboardBaseUrl(), - mockEnabled, - signedIn, - authLoading: Boolean(insforge.loading), - userId: cloudAuthSignedIn ? insforge.user?.id || null : null, - }; - const contextKey = getLeaderboardPreloadContextKey(preloadOptions); - if (leaderboardStatePreloadContextKeysRef.current.has(contextKey)) return; - leaderboardStatePreloadContextKeysRef.current.add(contextKey); - void preloadLeaderboardDefaultState(preloadOptions); - }, [ - cloudAuthSignedIn, - insforge.loading, - insforge.user?.id, - isLocalMode, - leaderboardAccessMode, - mockEnabled, - signedIn, - ]); + const isLimitsPath = normalizedPath === "/limits"; + const isSettingsPath = normalizedPath === "/settings"; + const isSkillsPath = normalizedPath === "/skills"; + const isWidgetsPath = normalizedPath === "/widgets"; + const isIpCheckPath = normalizedPath === "/ip-check"; + const isWrappedPath = normalizedPath === "/wrapped"; const handleDashboardMainContentVisible = useCallback(() => { if (!isDashboardDefaultPath) return; @@ -161,51 +75,12 @@ export default function App() { } if (!dashboardResourcePreloadStartedRef.current) { dashboardResourcePreloadStartedRef.current = true; - if (isLocalMode) { - void preloadDashboardPageResource("limits"); - } else { - void preloadDashboardPageResources(); - } + void preloadDashboardPageResource("limits"); } - tryPreloadLeaderboardDefaultState(); - }, [ - isDashboardDefaultPath, - isLocalMode, - tryPreloadLeaderboardDefaultState, - ]); - - useEffect(() => { - tryPreloadLeaderboardDefaultState(); - }, [tryPreloadLeaderboardDefaultState]); - - const authObject = useMemo(() => { - if (!insforge.enabled || !cloudAuthSignedIn) return null; - return { - getAccessToken: () => insforge.getAccessToken(), - name: insforge.displayName || "", - userId: insforge.user?.id || null, - }; - }, [cloudAuthSignedIn, insforge]); - - let gate = isLocalMode || mockEnabled || screenshotMode ? "dashboard" : "landing"; - if (normalizedPath === "/landing") gate = "landing"; - if (normalizedPath === "/dashboard") gate = "dashboard"; - if (isLeaderboardPath) gate = "dashboard"; - if (profileUserId) gate = "dashboard"; - - const isLimitsPath = normalizedPath === "/limits"; - const isSettingsPath = normalizedPath === "/settings"; - const isSkillsPath = normalizedPath === "/skills"; - const isWidgetsPath = normalizedPath === "/widgets"; - const isIpCheckPath = normalizedPath === "/ip-check"; - if (isLimitsPath || isSettingsPath || isSkillsPath || isWidgetsPath || isIpCheckPath) gate = "dashboard"; + }, [isDashboardDefaultPath]); let PageComponent = DashboardPage; - if (profileUserId) { - PageComponent = LeaderboardProfilePage; - } else if (normalizedPath === "/leaderboard") { - PageComponent = LeaderboardPage; - } else if (isLimitsPath) { + if (isLimitsPath) { PageComponent = LimitsPage; } else if (isSettingsPath) { PageComponent = SettingsPage; @@ -218,68 +93,38 @@ export default function App() { } const showSidebar = - !publicMode && - !isAuthGateTriggered && - (normalizedPath === "/dashboard" || - normalizedPath === "/" || - isLeaderboardPath || - isLimitsPath || - isSettingsPath || - isSkillsPath || - isWidgetsPath || - isIpCheckPath); + normalizedPath === "/dashboard" || + normalizedPath === "/" || + isLimitsPath || + isSettingsPath || + isSkillsPath || + isWidgetsPath || + isIpCheckPath; let content = null; - if (normalizedPath === "/auth/callback" || normalizedPath === "/auth/native-callback") { - content = ; - } else if (normalizedPath === "/login") { - content = ; - } else if (normalizedPath === "/device") { - // Headless-CLI device-flow approval page. Standalone (no sidebar) so - // unsigned visitors hit the embedded sign-in CTA without sidebar nav - // confusion. Auth check happens inside DevicePage itself. - content = ; - } else if (normalizedPath === "/wrapped") { + if (isWrappedPath) { // Year-end Wrapped page. Reads from /functions/tokentracker-wrapped // (provided by the local CLI server) — no auth required. content = ; - } else if (gate === "landing") { - content = ; } else { const pageNode = ( (insforge.enabled ? insforge.signOut() : Promise.resolve())} - publicMode={publicMode} - publicToken={publicToken} - userId={profileUserId} - signInUrl="/login" - signUpUrl="/login" onMainContentVisible={handleDashboardMainContentVisible} /> ); - if (showSidebar) { - content = {pageNode}; - } else { - content = pageNode; - } + content = showSidebar ? {pageNode} : pageNode; } return ( - - {content} - {showSidebar ? : null} - - {enableVercelInsights ? : null} - {enableVercelInsights ? : null} - + {content} + {showSidebar ? : null} + {enableVercelInsights ? : null} + {enableVercelInsights ? : null} diff --git a/dashboard/src/App.navigation-preload.test.jsx b/dashboard/src/App.navigation-preload.test.jsx index a8f18dd0..03ab30ec 100644 --- a/dashboard/src/App.navigation-preload.test.jsx +++ b/dashboard/src/App.navigation-preload.test.jsx @@ -4,24 +4,13 @@ import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "./App.jsx"; -import { - preloadDashboardPageResource, - preloadDashboardPageResources, - preloadLeaderboardDefaultState, -} from "./lib/dashboard-preload.js"; +import { preloadDashboardPageResource } from "./lib/dashboard-preload.js"; const TEXT = { dashboard: "Dashboard page", - device: "Device page", ipCheck: "IP check", - landing: "Landing page", - leaderboard: "Leaderboard page", - leaderboardNav: "Leaderboard nav", limits: "Limits page", limitsNav: "Limits nav", - login: "Login page", - nativeCallback: "Native callback", - profile: "Profile page", reveal: "reveal main content", settings: "Settings page", skills: "Skills page", @@ -29,54 +18,19 @@ const TEXT = { wrapped: "Wrapped page", }; -const insforgeMock = vi.hoisted(() => ({ - enabled: true, - signedIn: true, - loading: false, - user: { id: "user-1" }, - displayName: "Ada", - getAccessToken: vi.fn(), - signOut: vi.fn(), -})); - const pending = vi.hoisted(() => new Promise(() => {})); vi.mock("./lib/dashboard-preload.js", () => ({ - getLeaderboardPreloadContextKey: vi.fn((options = {}) => - [ - options.accessMode || "", - options.baseUrl || "", - String(Boolean(options.mockEnabled)), - String(Boolean(options.signedIn)), - String(Boolean(options.authLoading)), - options.userId || "null", - ].join("|"), - ), markDashboardMainContentVisible: vi.fn(), preloadDashboardPageResource: vi.fn(() => pending), - preloadDashboardPageResources: vi.fn(() => pending), - preloadLeaderboardDefaultState: vi.fn(() => pending), })); vi.mock("./hooks/useLocale.js", () => ({ useLocale: () => ({ resolvedLocale: "en" }), })); -vi.mock("./contexts/InsforgeAuthContext.jsx", () => ({ - useInsforgeAuth: () => insforgeMock, -})); - -vi.mock("./hooks/use-cloud-usage-sync", () => ({ - useCloudUsageSync: vi.fn(), -})); - -vi.mock("./lib/mock-data", () => ({ - isMockEnabled: () => false, -})); - vi.mock("./lib/config", () => ({ getBackendBaseUrl: () => "", - getLeaderboardBaseUrl: () => "https://edge.example", })); vi.mock("./lib/screenshot-mode", () => ({ @@ -91,14 +45,6 @@ vi.mock("./ui/foundation/ThemeProvider.jsx", () => ({ ThemeProvider: ({ children }) => <>{children}, })); -vi.mock("./contexts/LoginModalContext.jsx", () => ({ - LoginModalProvider: ({ children }) => <>{children}, -})); - -vi.mock("./components/LoginModal.jsx", () => ({ - LoginModal: () => null, -})); - vi.mock("@vercel/analytics/react", () => ({ Analytics: () => null, })); @@ -114,7 +60,6 @@ vi.mock("./ui/components/Sidebar.jsx", async () => {
{children}
@@ -141,19 +86,7 @@ vi.mock("./pages/LimitsPage.jsx", () => ({ LimitsPage: () =>
{TEXT.limits}
, })); -vi.mock("./pages/LeaderboardPage.jsx", () => ({ - LeaderboardPage: () =>
{TEXT.leaderboard}
, -})); - -vi.mock("./pages/NativeAuthCallbackPage.jsx", () => ({ - NativeAuthCallbackPage: () =>
{TEXT.nativeCallback}
, -})); - vi.mock("./pages/IpCheckPage.jsx", () => ({ default: () =>
{TEXT.ipCheck}
})); -vi.mock("./pages/LandingPage.jsx", () => ({ LandingPage: () =>
{TEXT.landing}
})); -vi.mock("./pages/LeaderboardProfilePage.jsx", () => ({ LeaderboardProfilePage: () =>
{TEXT.profile}
})); -vi.mock("./pages/LoginPage.jsx", () => ({ LoginPage: () =>
{TEXT.login}
})); -vi.mock("./pages/DevicePage.jsx", () => ({ default: () =>
{TEXT.device}
})); vi.mock("./pages/WrappedPage.jsx", () => ({ default: () =>
{TEXT.wrapped}
})); vi.mock("./pages/SettingsPage.jsx", () => ({ SettingsPage: () =>
{TEXT.settings}
})); vi.mock("./pages/SkillsPage.jsx", () => ({ SkillsPage: () =>
{TEXT.skills}
})); @@ -174,8 +107,6 @@ async function startPendingPreload(user) { await user.click(screen.getByRole("button", { name: TEXT.reveal })); await waitFor(() => { expect(preloadDashboardPageResource).toHaveBeenCalledWith("limits"); - expect(preloadDashboardPageResources).not.toHaveBeenCalled(); - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); }); } @@ -195,15 +126,4 @@ describe("App navigation while preload is pending", () => { expect(await screen.findByText(TEXT.limits)).toBeInTheDocument(); }); - - it("switches to /leaderboard without waiting for pending preload promises", async () => { - const user = userEvent.setup(); - await startPendingPreload(user); - - await act(async () => { - await user.click(screen.getByRole("link", { name: TEXT.leaderboardNav })); - }); - - expect(await screen.findByText(TEXT.leaderboard)).toBeInTheDocument(); - }); }); diff --git a/dashboard/src/App.preload.test.jsx b/dashboard/src/App.preload.test.jsx index 6476a19b..4dbbdf80 100644 --- a/dashboard/src/App.preload.test.jsx +++ b/dashboard/src/App.preload.test.jsx @@ -4,24 +4,12 @@ import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import { beforeEach, describe, expect, it, vi } from "vitest"; import App from "./App.jsx"; -import { - markDashboardMainContentVisible, - preloadDashboardPageResource, - preloadDashboardPageResources, - preloadLeaderboardDefaultState, -} from "./lib/dashboard-preload.js"; +import { markDashboardMainContentVisible, preloadDashboardPageResource } from "./lib/dashboard-preload.js"; const TEXT = { dashboard: "Dashboard page", - device: "Device page", ipCheck: "IP check", - landing: "Landing page", - leaderboard: "Leaderboard page", limits: "Limits page", - limitsNav: "Limits nav", - login: "Login page", - nativeCallback: "Native callback", - profile: "Profile page", reveal: "reveal main content", settings: "Settings page", skills: "Skills page", @@ -29,52 +17,17 @@ const TEXT = { wrapped: "Wrapped page", }; -const insforgeMock = vi.hoisted(() => ({ - enabled: true, - signedIn: true, - loading: false, - user: { id: "user-1" }, - displayName: "Ada", - getAccessToken: vi.fn(), - signOut: vi.fn(), -})); - vi.mock("./lib/dashboard-preload.js", () => ({ - getLeaderboardPreloadContextKey: vi.fn((options = {}) => - [ - options.accessMode || "", - options.baseUrl || "", - String(Boolean(options.mockEnabled)), - String(Boolean(options.signedIn)), - String(Boolean(options.authLoading)), - options.userId || "null", - ].join("|"), - ), markDashboardMainContentVisible: vi.fn(), preloadDashboardPageResource: vi.fn(() => Promise.resolve(null)), - preloadDashboardPageResources: vi.fn(() => Promise.resolve([])), - preloadLeaderboardDefaultState: vi.fn(() => Promise.resolve(null)), })); vi.mock("./hooks/useLocale.js", () => ({ useLocale: () => ({ resolvedLocale: "en" }), })); -vi.mock("./contexts/InsforgeAuthContext.jsx", () => ({ - useInsforgeAuth: () => insforgeMock, -})); - -vi.mock("./hooks/use-cloud-usage-sync", () => ({ - useCloudUsageSync: vi.fn(), -})); - -vi.mock("./lib/mock-data", () => ({ - isMockEnabled: () => false, -})); - vi.mock("./lib/config", () => ({ getBackendBaseUrl: () => "", - getLeaderboardBaseUrl: () => "https://edge.example", })); vi.mock("./lib/screenshot-mode", () => ({ @@ -89,14 +42,6 @@ vi.mock("./ui/foundation/ThemeProvider.jsx", () => ({ ThemeProvider: ({ children }) => <>{children}, })); -vi.mock("./contexts/LoginModalContext.jsx", () => ({ - LoginModalProvider: ({ children }) => <>{children}, -})); - -vi.mock("./components/LoginModal.jsx", () => ({ - LoginModal: () => null, -})); - vi.mock("@vercel/analytics/react", () => ({ Analytics: () => null, })); @@ -109,7 +54,7 @@ vi.mock("./ui/components/Sidebar.jsx", () => ({ AppLayout: ({ children }) => ( @@ -140,24 +85,7 @@ vi.mock("./pages/LimitsPage.jsx", () => ({ }, })); -vi.mock("./pages/LeaderboardPage.jsx", () => ({ - LeaderboardPage: ({ onMainContentVisible }) => { - React.useEffect(() => { - onMainContentVisible?.(); - }, [onMainContentVisible]); - return
{TEXT.leaderboard}
; - }, -})); - -vi.mock("./pages/NativeAuthCallbackPage.jsx", () => ({ - NativeAuthCallbackPage: () =>
{TEXT.nativeCallback}
, -})); - vi.mock("./pages/IpCheckPage.jsx", () => ({ default: () =>
{TEXT.ipCheck}
})); -vi.mock("./pages/LandingPage.jsx", () => ({ LandingPage: () =>
{TEXT.landing}
})); -vi.mock("./pages/LeaderboardProfilePage.jsx", () => ({ LeaderboardProfilePage: () =>
{TEXT.profile}
})); -vi.mock("./pages/LoginPage.jsx", () => ({ LoginPage: () =>
{TEXT.login}
})); -vi.mock("./pages/DevicePage.jsx", () => ({ default: () =>
{TEXT.device}
})); vi.mock("./pages/WrappedPage.jsx", () => ({ default: () =>
{TEXT.wrapped}
})); vi.mock("./pages/SettingsPage.jsx", () => ({ SettingsPage: () =>
{TEXT.settings}
})); vi.mock("./pages/SkillsPage.jsx", () => ({ SkillsPage: () =>
{TEXT.skills}
})); @@ -175,117 +103,31 @@ function renderApp(initialPath = "/dashboard") { describe("App deferred dashboard preload", () => { beforeEach(() => { vi.clearAllMocks(); - insforgeMock.enabled = true; - insforgeMock.signedIn = true; - insforgeMock.loading = false; - insforgeMock.user = { id: "user-1" }; - insforgeMock.displayName = "Ada"; window.history.pushState({}, "", "/"); }); - it("does not start target preload before the dashboard main content is visible", async () => { + it("does not start limits preload before the dashboard main content is visible", async () => { const user = userEvent.setup(); renderApp("/dashboard"); expect(await screen.findByText(TEXT.dashboard)).toBeInTheDocument(); - expect(preloadDashboardPageResources).not.toHaveBeenCalled(); - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); + expect(preloadDashboardPageResource).not.toHaveBeenCalled(); await user.click(screen.getByRole("button", { name: TEXT.reveal })); await waitFor(() => { expect(markDashboardMainContentVisible).toHaveBeenCalledTimes(1); expect(preloadDashboardPageResource).toHaveBeenCalledWith("limits"); - expect(preloadDashboardPageResources).not.toHaveBeenCalled(); - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); }); }); - it.each([ - ["/limits", TEXT.limits], - ["/leaderboard", TEXT.leaderboard], - ])("does not start dashboard preload for deep-linked %s", async (path, pageText) => { - renderApp(path); + it("does not start dashboard preload for a deep-linked /limits route", async () => { + renderApp("/limits"); - expect(await screen.findByText(pageText)).toBeInTheDocument(); + expect(await screen.findByText(TEXT.limits)).toBeInTheDocument(); await waitFor(() => { expect(markDashboardMainContentVisible).not.toHaveBeenCalled(); - expect(preloadDashboardPageResources).not.toHaveBeenCalled(); - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); - }); - }); - - it("skips leaderboard state preload in local dashboard mode", async () => { - const user = userEvent.setup(); - insforgeMock.signedIn = false; - insforgeMock.user = null; - - renderApp("/dashboard"); - - expect(await screen.findByText(TEXT.dashboard)).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: TEXT.reveal })); - - await waitFor(() => { - expect(preloadDashboardPageResource).toHaveBeenCalledWith("limits"); - expect(preloadDashboardPageResources).not.toHaveBeenCalled(); - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); - }); - }); - - it("keeps leaderboard preload disabled even when the local auth context changes", async () => { - const user = userEvent.setup(); - insforgeMock.signedIn = false; - insforgeMock.user = null; - const view = renderApp("/dashboard"); - - expect(await screen.findByText(TEXT.dashboard)).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: TEXT.reveal })); - - await waitFor(() => { - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); - }); - - insforgeMock.signedIn = true; - insforgeMock.user = { id: "user-cloud" }; - view.rerender( - - - , - ); - - await waitFor(() => { - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); - }); - }); - - it("waits for cloud auth to settle before preloading leaderboard default state", async () => { - const user = userEvent.setup(); - insforgeMock.loading = true; - insforgeMock.signedIn = false; - insforgeMock.user = null; - const view = renderApp("/dashboard"); - - expect(await screen.findByText(TEXT.dashboard)).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: TEXT.reveal })); - - await waitFor(() => { - expect(preloadDashboardPageResource).toHaveBeenCalledWith("limits"); - expect(preloadDashboardPageResources).not.toHaveBeenCalled(); - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); - }); - - insforgeMock.loading = false; - insforgeMock.signedIn = true; - insforgeMock.user = { id: "user-late" }; - view.rerender( - - - , - ); - - await waitFor(() => { - expect(preloadLeaderboardDefaultState).not.toHaveBeenCalled(); }); }); }); diff --git a/dashboard/src/components/InsforgeUserHeaderControls.jsx b/dashboard/src/components/InsforgeUserHeaderControls.jsx deleted file mode 100644 index d75b6acd..00000000 --- a/dashboard/src/components/InsforgeUserHeaderControls.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import React, { useMemo } from "react"; -import { useNavigate } from "react-router-dom"; -import { useInsforgeAuth } from "../contexts/InsforgeAuthContext.jsx"; -import { useLoginModal } from "../contexts/LoginModalContext.jsx"; -import { useLocale } from "../hooks/useLocale.js"; -import { isNativeApp } from "../lib/native-bridge.js"; -import { copy } from "../lib/copy"; -import { cn } from "../lib/cn"; - -function pickAvatarUrl(user) { - if (!user || typeof user !== "object") return null; - const meta = user.user_metadata && typeof user.user_metadata === "object" ? user.user_metadata : {}; - const prof = user.profile && typeof user.profile === "object" ? user.profile : {}; - const u = meta.avatar_url || meta.picture || prof.avatar_url || user.avatar_url; - return typeof u === "string" && u.trim() ? u.trim() : null; -} - -// In TokenTrackerBar WKWebView, third-party avatar CDNs (lh3.googleusercontent.com, -// avatars.githubusercontent.com) intermittently fail to load even when they -// render fine in Safari. Route them through the local CLI server, which fetches -// via Node and serves the bytes as same-origin — that path is reliable. -// On Vercel (no local server) we keep the original URL. -function resolveAvatarSrc(url) { - if (!url) return null; - if (!isNativeApp()) return url; - return `/api/avatar-proxy?url=${encodeURIComponent(url)}`; -} - -function initialsFromName(name) { - const s = String(name || "").trim(); - if (!s) return "?"; - const parts = s.split(/\s+/).filter(Boolean); - if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); - return s.slice(0, 2).toUpperCase(); -} - -/** - * Compact identity control. Avatar click navigates to /settings — all account - * preferences live there now. Sign-in shows the login modal as before. - */ -export function InsforgeUserHeaderControls({ className, variant = "header", collapsed = false, onAfterAction }) { - // Subscribe to locale so labels re-render on language switch. - useLocale(); - const isSidebar = variant === "sidebar"; - const { enabled, loading, signedIn, user, displayName } = useInsforgeAuth(); - const { openLoginModal } = useLoginModal(); - const navigate = useNavigate(); - const avatarUrl = useMemo(() => pickAvatarUrl(user), [user]); - const avatarSrc = useMemo(() => resolveAvatarSrc(avatarUrl), [avatarUrl]); - const [avatarFailed, setAvatarFailed] = React.useState(false); - - React.useEffect(() => { - setAvatarFailed(false); - }, [avatarSrc]); - - if (!enabled) return null; - - if (loading) { - return ( -
- ); - } - - if (!signedIn) { - if (isSidebar) { - return ( - - ); - } - return ( - - ); - } - - const handleClick = () => { - navigate("/settings"); - onAfterAction?.(); - }; - - return ( -
- -
- ); -} diff --git a/dashboard/src/components/LeaderboardAvatar.jsx b/dashboard/src/components/LeaderboardAvatar.jsx deleted file mode 100644 index 486ebbd1..00000000 --- a/dashboard/src/components/LeaderboardAvatar.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from "react"; -import { cn } from "../lib/cn"; - -function hashHue(input) { - let h = 0; - const s = String(input ?? ""); - for (let i = 0; i < s.length; i += 1) { - h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; - } - return Math.abs(h) % 360; -} - -function initialsFromName(displayName) { - const t = String(displayName ?? "").trim(); - if (!t) return "?"; - const parts = t.split(/\s+/).filter(Boolean); - if (parts.length >= 2) { - const a = parts[0][0] || ""; - const b = parts[1][0] || ""; - return `${a}${b}`.toUpperCase(); - } - return t.slice(0, 2).toUpperCase(); -} - -const SIZE_CLASS = { - sm: "h-7 w-7 min-h-7 min-w-7 text-[10px]", - md: "h-8 w-8 min-h-8 min-w-8 text-[11px]", - lg: "h-14 w-14 min-h-14 min-w-14 text-base", - xl: "h-[68px] w-[68px] min-h-[68px] min-w-[68px] text-xl", -}; - -/** - * Avatar for leaderboard rows: remote URL when present, otherwise deterministic initials + color. - */ -export function LeaderboardAvatar({ - avatarUrl, - displayName, - seed, - size = "md", - className, -}) { - const dim = SIZE_CLASS[size] || SIZE_CLASS.md; - const hue = hashHue(seed ?? displayName ?? ""); - const safeUrl = typeof avatarUrl === "string" ? avatarUrl.trim() : ""; - const [failed, setFailed] = React.useState(false); - - React.useEffect(() => { - setFailed(false); - }, [safeUrl]); - - if (safeUrl && !failed) { - return ( - setFailed(true)} - className={cn("rounded-full object-cover ring-1 ring-white/10", dim, className)} - /> - ); - } - - return ( -
- {initialsFromName(displayName)} -
- ); -} diff --git a/dashboard/src/components/LeaderboardProviderColumnHeader.jsx b/dashboard/src/components/LeaderboardProviderColumnHeader.jsx deleted file mode 100644 index 21a586e3..00000000 --- a/dashboard/src/components/LeaderboardProviderColumnHeader.jsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; - -/** - * Brand SVGs that ship as solid black/dark glyphs (no built-in light variant). - * Invert them in dark mode so the icon stays visible on a dark background. - */ -const INVERT_IN_DARK = new Set([ - "/brand-logos/cursor.svg", - "/brand-logos/kimi.svg", - "/brand-logos/kiro.svg", - "/brand-logos/copilot.svg", -]); - -/** - * Single provider column header: brand icon + label from copy registry. - */ -export function LeaderboardProviderColumnHeader({ iconSrc, label }) { - return ( - - {iconSrc ? ( - - ) : null} - {label} - - ); -} diff --git a/dashboard/src/components/LeaderboardProviderColumnHeader.test.jsx b/dashboard/src/components/LeaderboardProviderColumnHeader.test.jsx deleted file mode 100644 index e5d190b0..00000000 --- a/dashboard/src/components/LeaderboardProviderColumnHeader.test.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { render } from "@testing-library/react"; -import { LeaderboardProviderColumnHeader } from "./LeaderboardProviderColumnHeader.jsx"; - -describe("LeaderboardProviderColumnHeader", () => { - it("inverts the monochrome Kimi logo in dark mode", () => { - const { container } = render( - , - ); - - const img = container.querySelector('img[src="/brand-logos/kimi.svg"]'); - expect(img).toHaveClass("dark:invert"); - }); -}); diff --git a/dashboard/src/components/LeaderboardSkeleton.jsx b/dashboard/src/components/LeaderboardSkeleton.jsx deleted file mode 100644 index b42288f8..00000000 --- a/dashboard/src/components/LeaderboardSkeleton.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react"; -import { cn } from "../lib/cn"; -import { - LB_STICKY_TH_RANK, - LB_STICKY_TH_USER, - LEADERBOARD_TOKEN_COLUMNS, - lbStickyTdRank, - lbStickyTdUser, -} from "../lib/leaderboard-columns.js"; - -function Bone({ className }) { - return ( -
- ); -} - -function SkeletonRow({ index }) { - // First 3 rows slightly wider to mimic top-ranked users with longer names/numbers - const nameW = index < 3 ? "w-24" : index < 8 ? "w-20" : "w-16"; - const totalW = index < 3 ? "w-16" : "w-12"; - const cellW = index < 5 ? "w-14" : "w-10"; - - return ( - - {/* Rank */} - - - - {/* User: avatar + name */} - -
- - -
- - {/* Total */} - - - - {/* Cost */} - - - - {/* Provider columns */} - {LEADERBOARD_TOKEN_COLUMNS.map((col) => ( - - - - ))} - - ); -} - -/** - * Skeleton loader that mirrors the leaderboard table structure. - * Renders `rows` shimmer rows with the same sticky columns and cell layout. - */ -export function LeaderboardSkeleton({ rows = 10 }) { - return ( -
- - - - - - - - {LEADERBOARD_TOKEN_COLUMNS.map((col) => ( - - ))} - - - - {Array.from({ length: rows }, (_, i) => ( - - ))} - -
- - - - - - - - - -
-
- ); -} diff --git a/dashboard/src/components/LeaderboardSummaryCard.jsx b/dashboard/src/components/LeaderboardSummaryCard.jsx deleted file mode 100644 index 353a28e6..00000000 --- a/dashboard/src/components/LeaderboardSummaryCard.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from "react"; -import { ArrowRight } from "lucide-react"; -import { copy } from "../lib/copy"; -import { cn } from "../lib/cn"; -import { LeaderboardAvatar } from "./LeaderboardAvatar.jsx"; - -function computePercentile(rank, total) { - const r = Number(rank); - const t = Number(total); - if (!Number.isFinite(r) || r < 1) return null; - if (!Number.isFinite(t) || t < 1) return null; - return Math.min(100, Math.max(1, Math.ceil((r / t) * 100))); -} - -function trimName(value) { - return typeof value === "string" ? value.trim() : ""; -} - -function isAnon(value) { - const v = trimName(value); - if (!v) return true; - return v.toLowerCase() === "anonymous"; -} - -/** - * Compact identity pill that lives in the page header next to the period - * toggle. Click → jumps to the user's row when not already on that page. - */ -export function LeaderboardMeChip({ - me, - totalEntries, - meLabel, - onOpenProfile, - onJumpToMe, - canJump, - className, -}) { - const rank = me && typeof me.rank === "number" ? me.rank : null; - if (!rank) return null; - - const total = Number(totalEntries) || 0; - const percentile = computePercentile(rank, total); - - const rawName = trimName(me?.display_name); - const headlineName = !rawName || isAnon(rawName) ? meLabel || "You" : rawName; - const avatarSeed = me?.user_id || headlineName; - - // Click opens the user's own profile modal (where the embeddable badge lives). - // Falls back to the legacy jump-to-row when no profile handler is available. - const handleClick = onOpenProfile || (canJump ? onJumpToMe : null); - const interactive = typeof handleClick === "function"; - const Tag = interactive ? "button" : "div"; - const interactiveProps = interactive - ? { - type: "button", - onClick: handleClick, - "aria-label": copy( - onOpenProfile - ? "leaderboard.summary.view_profile" - : "leaderboard.summary.jump_to_me", - ), - } - : {}; - - return ( - - - - {headlineName} - - - #{rank.toLocaleString()} - - {percentile != null && ( - - {copy("leaderboard.summary.percentile", { p: String(percentile) })} - - )} - {interactive ? ( - - ) : ( - - - - - )} - - ); -} diff --git a/dashboard/src/components/LoginCard.jsx b/dashboard/src/components/LoginCard.jsx deleted file mode 100644 index 4a3808ea..00000000 --- a/dashboard/src/components/LoginCard.jsx +++ /dev/null @@ -1,357 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Mail } from "lucide-react"; -import { useInsforgeAuth } from "../contexts/InsforgeAuthContext.jsx"; -import { useLocale } from "../hooks/useLocale.js"; -import { copy } from "../lib/copy"; -import { cn } from "../lib/cn"; - -const GOOGLE_ICON = ( - - - - - - -); - -const GITHUB_ICON = ( - - - -); - -const PROVIDER_ICONS = { google: GOOGLE_ICON, github: GITHUB_ICON }; -const PROVIDER_LABELS = { - google: "Google", - github: "GitHub", - microsoft: "Microsoft", - discord: "Discord", - apple: "Apple", -}; - -function providerLabel(key) { - return PROVIDER_LABELS[key] || String(key || "").replace(/-/g, " "); -} - -export function LoginCard({ - title, - subtitle, - className = "", - onSuccess, - hideLogo = false, -}) { - useLocale(); - const { - enabled, - signedIn, - refreshUser, - signInWithPassword, - signUp, - signInWithOAuth, - getPublicAuthConfig, - } = useInsforgeAuth(); - - const [mode, setMode] = useState("signin"); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [name, setName] = useState(""); - const [busy, setBusy] = useState(false); - const [error, setError] = useState(null); - const [banner, setBanner] = useState(null); - const [emailExpanded, setEmailExpanded] = useState(false); - const [configLoading, setConfigLoading] = useState(true); - const [oauthProviders, setOauthProviders] = useState([]); - const [passwordMinLength, setPasswordMinLength] = useState(8); - - // Load auth config when component mounts - useEffect(() => { - if (!enabled) return; - let active = true; - setConfigLoading(true); - (async () => { - const { data, error: cfgErr } = await getPublicAuthConfig(); - if (!active) return; - if (cfgErr || !data) { - setOauthProviders(["google", "github"]); - } else { - const providers = Array.isArray(data.oAuthProviders) ? data.oAuthProviders : []; - const custom = Array.isArray(data.customOAuthProviders) ? data.customOAuthProviders : []; - setOauthProviders([...providers, ...custom]); - if (typeof data.passwordMinLength === "number" && data.passwordMinLength > 0) { - setPasswordMinLength(data.passwordMinLength); - } - } - setConfigLoading(false); - })(); - return () => { - active = false; - }; - }, [enabled, getPublicAuthConfig]); - - // Handle successful login - useEffect(() => { - if (signedIn && onSuccess) { - onSuccess(); - } - }, [signedIn, onSuccess]); - - const redirectUrl = useMemo(() => { - if (typeof window === "undefined") return ""; - const isNativeContext = Boolean(window.webkit?.messageHandlers?.nativeOAuth); - return isNativeContext - ? `${window.location.origin}/auth/callback` - : `${window.location.origin}/`; - }, []); - - const handleOAuth = useCallback(async (provider) => { - setError(null); - setBusy(true); - try { - const { error: err } = await signInWithOAuth(provider, redirectUrl); - if (err) setError(err.message || String(err)); - } finally { - setBusy(false); - } - }, [signInWithOAuth, redirectUrl]); - - const handleEmailAuth = useCallback(async (e) => { - e.preventDefault(); - setError(null); - setBusy(true); - try { - if (mode === "signup") { - const { data, error: err } = await signUp({ - email: email.trim(), - password, - name: name.trim() || undefined, - }); - if (err) { - setError(err.message || String(err)); - return; - } - if (data?.requireEmailVerification) { - setBanner(copy("login.verify_email_pending")); - setMode("signin"); - return; - } - await refreshUser(); - return; - } - const { error: err } = await signInWithPassword({ email: email.trim(), password }); - if (err) { - setError(err.message || String(err)); - return; - } - await refreshUser(); - } finally { - setBusy(false); - } - }, [mode, email, password, name, signUp, signInWithPassword, refreshUser]); - - if (!enabled) return null; - - return ( -
- {/* Header */} -
- {!hideLogo && ( -
- - - {copy("shared.app_name")} - -
- )} -

- {title || copy("login_modal.subtitle")} -

- {subtitle && ( -

- {subtitle} -

- )} -
- - {/* Banner */} - {banner && ( -
- {banner} -
- )} - - {/* Error */} - {error && ( -

- {error} -

- )} - - {/* OAuth buttons */} -
- {configLoading ? ( -
-
-
-
- ) : ( - oauthProviders.map((p) => ( - - )) - )} -
- - {/* Email section */} - {!emailExpanded ? ( - <> -
-
- -
-
- - {copy("login.divider")} - -
-
- - - ) : ( - <> - {/* Divider */} -
-
- -
-
- - {copy("login_modal.divider_email")} - -
-
- - {/* Sign in / Sign up toggle */} -
- - -
- - {/* Email form */} -
- {mode === "signup" && ( -
- - setName(e.target.value)} - className="w-full h-10 rounded-lg border border-oai-gray-200 dark:border-oai-gray-800 bg-oai-gray-50 dark:bg-oai-gray-900 px-3 text-sm text-oai-black dark:text-white placeholder-oai-gray-400 dark:placeholder-oai-gray-600 focus:outline-none focus:ring-2 focus:ring-oai-brand-500" - placeholder={copy("login.field.name_placeholder")} - /> -
- )} -
- - setEmail(e.target.value)} - className="w-full h-10 rounded-lg border border-oai-gray-200 dark:border-oai-gray-800 bg-oai-gray-50 dark:bg-oai-gray-900 px-3 text-sm text-oai-black dark:text-white placeholder-oai-gray-400 dark:placeholder-oai-gray-600 focus:outline-none focus:ring-2 focus:ring-oai-brand-500" - /> -
-
- - setPassword(e.target.value)} - className="w-full h-10 rounded-lg border border-oai-gray-200 dark:border-oai-gray-800 bg-oai-gray-50 dark:bg-oai-gray-900 px-3 text-sm text-oai-black dark:text-white placeholder-oai-gray-400 dark:placeholder-oai-gray-600 focus:outline-none focus:ring-2 focus:ring-oai-brand-500" - /> - {mode === "signup" && ( -

- {copy("login.password_hint", { min: String(passwordMinLength) })} -

- )} -
- -
- - )} -
- ); -} diff --git a/dashboard/src/components/LoginModal.jsx b/dashboard/src/components/LoginModal.jsx deleted file mode 100644 index 6777d8cb..00000000 --- a/dashboard/src/components/LoginModal.jsx +++ /dev/null @@ -1,68 +0,0 @@ -import React, { useEffect } from "react"; -import { AnimatePresence, motion } from "motion/react"; -import { X } from "lucide-react"; -import { useInsforgeAuth } from "../contexts/InsforgeAuthContext.jsx"; -import { useLoginModal } from "../contexts/LoginModalContext.jsx"; -import { LoginCard } from "./LoginCard.jsx"; - -export function LoginModal() { - const { isOpen, closeLoginModal } = useLoginModal(); - const { enabled } = useInsforgeAuth(); - - // Handle Escape key - useEffect(() => { - if (!isOpen) return; - const onKey = (e) => { - if (e.key === "Escape") closeLoginModal(); - }; - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [isOpen, closeLoginModal]); - - if (!enabled) return null; - - return ( - - {isOpen && ( - - {/* Backdrop */} - - - {/* Card Wrapper */} - e.stopPropagation()} - > - {/* Close button */} - - - {/* LoginCard Form */} - - - - )} - - ); -} diff --git a/dashboard/src/components/SortableColumnHeader.jsx b/dashboard/src/components/SortableColumnHeader.jsx deleted file mode 100644 index 0af67466..00000000 --- a/dashboard/src/components/SortableColumnHeader.jsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useLayoutEffect } from "react"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { cn } from "../lib/cn"; - -/** - * Draggable column header. In addition to its own sortable transform, - * it imperatively mirrors the same transform/transition to every - * `` in the table so the entire column - * translates together in real time — not just after drop. - */ -export function SortableColumnHeader({ id, thClassName, children }) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id }); - - const transformStr = CSS.Transform.toString(transform) || ""; - - useLayoutEffect(() => { - // `id` is always a safe snake_case identifier from LEADERBOARD_TOKEN_COLUMNS, - // so a raw attribute selector is fine — no escaping needed. - const cells = document.querySelectorAll(`td[data-column-key="${id}"]`); - const liftShadow = - "0 2px 10px rgba(15, 23, 42, 0.10), 0 1px 3px rgba(15, 23, 42, 0.06)"; - cells.forEach((cell) => { - cell.style.transform = transformStr; - cell.style.transition = transition || ""; - cell.style.zIndex = isDragging ? "20" : ""; - cell.style.position = transformStr ? "relative" : ""; - cell.style.boxShadow = isDragging ? liftShadow : ""; - }); - return () => { - cells.forEach((cell) => { - cell.style.transform = ""; - cell.style.transition = ""; - cell.style.zIndex = ""; - cell.style.position = ""; - cell.style.boxShadow = ""; - }); - }; - }, [id, transformStr, transition, isDragging]); - - const style = { - transform: transformStr, - transition, - position: transformStr ? "relative" : undefined, - zIndex: isDragging ? 20 : undefined, - boxShadow: isDragging - ? "0 2px 10px rgba(15, 23, 42, 0.10), 0 1px 3px rgba(15, 23, 42, 0.06)" - : undefined, - }; - - return ( - -
- - {children} -
- - ); -} diff --git a/dashboard/src/components/leaderboard/LeaderboardProfileModal.jsx b/dashboard/src/components/leaderboard/LeaderboardProfileModal.jsx deleted file mode 100644 index 4202c4e2..00000000 --- a/dashboard/src/components/leaderboard/LeaderboardProfileModal.jsx +++ /dev/null @@ -1,682 +0,0 @@ -import { Dialog } from "@base-ui/react/dialog"; -import { X, ArrowUpRight, Check, Link2, Code2 } from "lucide-react"; -import React, { useEffect, useMemo, useState } from "react"; -import { Link } from "react-router-dom"; -import { copy } from "../../lib/copy"; -import { formatCompactNumber, formatUsdCurrency } from "../../lib/format"; -import { useCurrency } from "../../hooks/useCurrency.js"; -import { getLeaderboardProfile } from "../../lib/api"; -import { resolveAuthAccessTokenWithRetry } from "../../lib/auth-token"; -import { buildActivityHeatmap } from "../../lib/activity-heatmap"; -import { LeaderboardAvatar } from "../LeaderboardAvatar.jsx"; -import { ProviderIcon } from "../../ui/dashboard/components/ProviderIcon.jsx"; -import { ActivityHeatmap } from "../../ui/dashboard/components/ActivityHeatmap.jsx"; -import { cn } from "../../lib/cn"; -import { isNativeApp } from "../../lib/native-bridge.js"; -import { LikeButton } from "../../ui/dashboard/components/LikeButton.jsx"; -import { getInsforgeRemoteUrl } from "../../lib/insforge-config"; -import { safeWriteClipboard } from "../../lib/safe-browser"; -import { useInsforgeAuth } from "../../contexts/InsforgeAuthContext.jsx"; - -function formatCost(value, currency, rate) { - const n = Number(value); - if (!Number.isFinite(n) || n < 0) return "—"; - if (n > 0 && n < 0.01) { - const symbol = currency === "USD" ? "$" : ""; - return `<${symbol}0.01`; - } - return formatUsdCurrency(n, { decimals: 2, currency, rate }); -} - -/** - * Compact cost for the stat strip: stays exact under $1000 (so "$94.83" reads - * naturally), then collapses to K/M/B so a million-dollar total doesn't blow - * out the 4-column grid alongside a 3-character token count like "56.4B". - */ -function formatCostCompact(value, currency, rate) { - const n = Number(value); - if (!Number.isFinite(n) || n < 0) return "—"; - if (n < 1000) return formatCost(n, currency, rate); - const converted = currency === "USD" ? n : n * (rate || 1); - const symbol = currency === "USD" ? "$" : ""; - return `${symbol}${formatCompactNumber(converted, { decimals: 1 })}`; -} - -function formatTokens(value) { - const n = Number(value); - if (!Number.isFinite(n) || n <= 0) return "0"; - return formatCompactNumber(n, { decimals: 1 }); -} - -/** - * Adapt the edge function's daily series ({date, total_tokens}[]) into the - * shape consumed by the dashboard heatmap and trend components. Heatmap - * goes through `buildActivityHeatmap` (same path the main dashboard uses); - * TrendMonitor consumes the rows directly via `day` + `total_tokens`. - */ -function buildHeatmapForModal(daily) { - const arr = Array.isArray(daily) ? daily : []; - if (arr.length === 0) return null; - // Forward `models` so the heatmap's hover tooltip can render the per-day - // model breakdown (same as the main dashboard heatmap). - const dailyRows = arr.map((d) => ({ - day: d.date, - total_tokens: d.total_tokens, - models: d.models || null, - })); - const lastDate = arr[arr.length - 1]?.date; - return buildActivityHeatmap({ dailyRows, weeks: 52, to: lastDate }); -} - -const shimmerStyle = ` - @keyframes tt-shimmer { - 100% { transform: translateX(100%); } - } - .tt-shimmer-bar { - position: relative; - overflow: hidden; - } - .tt-shimmer-bar::after { - position: absolute; - top: 0; right: 0; bottom: 0; left: 0; - transform: translateX(-100%); - background-image: linear-gradient( - 90deg, - rgba(0, 0, 0, 0) 0%, - rgba(0, 0, 0, 0.02) 20%, - rgba(0, 0, 0, 0.06) 60%, - rgba(0, 0, 0, 0) 100% - ); - animation: tt-shimmer 1.6s infinite; - content: ''; - } - .dark .tt-shimmer-bar::after { - background-image: linear-gradient( - 90deg, - rgba(255, 255, 255, 0) 0%, - rgba(255, 255, 255, 0.02) 20%, - rgba(255, 255, 255, 0.06) 60%, - rgba(255, 255, 255, 0) 100% - ); - } -`; - -/** - * Skeleton that mirrors the real profile layout (header → stat strip → - * fact list → heatmap → provider list). Same heights as the loaded view - * to avoid layout shift on resolve. - */ -export function ProfileSkeleton({ variant = "modal" }) { - const bar = "rounded bg-oai-gray-200/50 dark:bg-oai-gray-800/40 tt-shimmer-bar"; - const isPage = variant === "page"; - return ( -
-