Research console (Next 16) + control-plane API + mld deployment for www.socialcrawl.co
- apps/dashboard: SocialCrawl console — observatory, sessions console (decisions, page state, entities, runtime APIs, media, world-model graph, live events), entity evidence pages, runtime API explorer, connectors, missions launcher; passcode gate + authenticated API proxy. - apps/api: node:http control plane (overview, sessions, entities search, schemas, platforms, SSE stream, job runner spawning worker processes), token-protected mutations. - worker: bounded action execution (60 s), PLAY_VIDEO no longer awaits play(); --job linking. - storage: jsonb sanitiser, linkJob; util: unicode thousands separators, stopwords. - deploy/social-runtime-crawler.mld.json: PM2 api 8350 + web 8351, MacLustr Tunnel route. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
46 changed files +7,607 −105
modified
.gitignore
+5 −0
@@ -14,3 +14,8 @@ data/platform_model/*/ | ||
| 14 | 14 | |
| 15 | 15 | # Compiled (learned) connectors are versioned intentionally; regression fixtures may be large. |
| 16 | 16 | connectors/*/regression_tests/large/ |
| 17 | + | |
| 18 | +# Next.js dashboard | |
| 19 | +apps/dashboard/.next/ | |
| 20 | +apps/dashboard/next-env.d.ts | |
| 21 | +*.tsbuildinfo | |
modified
apps/api/package.json
+5 −1
@@ -5,6 +5,10 @@ | ||
| 5 | 5 | "type": "module", |
| 6 | 6 | "dependencies": { |
| 7 | 7 | "@src/shared": "workspace:*", |
| 8 | − "@src/storage": "workspace:*" | |
| 8 | + "@src/storage": "workspace:*", | |
| 9 | + "@src/platform-model": "workspace:*" | |
| 10 | + }, | |
| 11 | + "scripts": { | |
| 12 | + "start": "tsx src/server.ts" | |
| 9 | 13 | } |
| 10 | 14 | } |
added
apps/api/src/jobs.ts
+102 −0
@@ -0,0 +1,102 @@ | ||
| 1 | +import { spawn, type ChildProcess } from "node:child_process"; | |
| 2 | +import fs from "node:fs"; | |
| 3 | +import path from "node:path"; | |
| 4 | +import { fileURLToPath } from "node:url"; | |
| 5 | +import { createLogger, isPlatform, newId, type AgentMode, type AppConfig } from "@src/shared"; | |
| 6 | +import type { PostgresStore } from "@src/storage"; | |
| 7 | + | |
| 8 | +const log = createLogger("jobs"); | |
| 9 | +const here = path.dirname(fileURLToPath(import.meta.url)); | |
| 10 | +const repoRoot = path.resolve(here, "..", "..", ".."); | |
| 11 | + | |
| 12 | +export interface JobRequest { | |
| 13 | + platform: string; | |
| 14 | + goal?: string; | |
| 15 | + query?: string; | |
| 16 | + mode?: AgentMode; | |
| 17 | + seed?: string; | |
| 18 | + minutes?: number; | |
| 19 | + actions?: number; | |
| 20 | + media?: number; | |
| 21 | + headless?: boolean; | |
| 22 | + account?: string; | |
| 23 | +} | |
| 24 | + | |
| 25 | +/** | |
| 26 | + * Job runner (§63 control plane, single-node version): spawns the worker CLI as a child process, | |
| 27 | + * streams its log to data/jobs/<id>.log and tracks status in crawl_jobs. One browser per job. | |
| 28 | + */ | |
| 29 | +export class JobRunner { | |
| 30 | + private running = new Map<string, ChildProcess>(); | |
| 31 | + readonly logsDir: string; | |
| 32 | + constructor(private readonly store: PostgresStore, private readonly cfg: AppConfig, private readonly maxConcurrent = 2) { | |
| 33 | + this.logsDir = path.join(cfg.dataDir, "jobs"); | |
| 34 | + fs.mkdirSync(this.logsDir, { recursive: true }); | |
| 35 | + } | |
| 36 | + | |
| 37 | + get activeCount() { | |
| 38 | + return this.running.size; | |
| 39 | + } | |
| 40 | + | |
| 41 | + async start(req: JobRequest): Promise<{ job_id: string }> { | |
| 42 | + if (!isPlatform(req.platform)) throw new Error("unknown platform"); | |
| 43 | + if (!["youtube", "reddit"].includes(req.platform)) throw new Error(`no adapter for ${req.platform} yet (Phase 1 = youtube, reddit)`); | |
| 44 | + if (this.running.size >= this.maxConcurrent) throw new Error(`max ${this.maxConcurrent} concurrent jobs`); | |
| 45 | + const job_id = newId("job"); | |
| 46 | + const mode = req.mode ?? "research"; | |
| 47 | + const minutes = clamp(req.minutes ?? 10, 1, 120); | |
| 48 | + const actions = clamp(req.actions ?? 60, 1, 1000); | |
| 49 | + const media = clamp(req.media ?? 1, 0, 4); | |
| 50 | + const goal = req.goal?.trim() || (req.query ? `Discover public content about “${req.query}”` : `Observe the ${req.platform} feed`); | |
| 51 | + const args = ["node_modules/tsx/dist/cli.mjs", "apps/worker/src/cli.ts", mode === "learn" ? "learn" : "crawl", req.platform, "--job", job_id, "--goal", goal, "--mode", mode, "--minutes", String(minutes), "--actions", String(actions), "--media", String(media), "--account", req.account?.trim() || "research-01"]; | |
| 52 | + if (req.query) args.push("--query", req.query); | |
| 53 | + if (req.seed) args.push("--seed", req.seed); | |
| 54 | + if (req.headless !== false) args.push("--headless"); | |
| 55 | + | |
| 56 | + await this.store.pool.query(`insert into crawl_jobs(job_id, platform, mode, goal, budget, status) values ($1,$2,$3,$4,$5,'queued')`, [job_id, req.platform, mode, goal, JSON.stringify({ minutes, actions, media, query: req.query ?? null, seed: req.seed ?? null, headless: req.headless !== false, account: req.account ?? "research-01" })]); | |
| 57 | + | |
| 58 | + const logFile = path.join(this.logsDir, `${job_id}.log`); | |
| 59 | + const out = fs.openSync(logFile, "a"); | |
| 60 | + const child = spawn(process.execPath, args, { cwd: repoRoot, env: { ...process.env, SRC_LOG_PRETTY: "1", SRC_LOG_LEVEL: "info" }, stdio: ["ignore", out, out] }); | |
| 61 | + this.running.set(job_id, child); | |
| 62 | + await this.store.pool.query(`update crawl_jobs set status='running' where job_id=$1`, [job_id]); | |
| 63 | + log.info("job started", { job_id, platform: req.platform, mode, pid: child.pid }); | |
| 64 | + child.on("exit", async (code, signal) => { | |
| 65 | + this.running.delete(job_id); | |
| 66 | + fs.closeSync(out); | |
| 67 | + const status = signal ? "stopped" : code === 0 ? "done" : "failed"; | |
| 68 | + let result: unknown = null; | |
| 69 | + try { | |
| 70 | + const sid = (await this.store.pool.query(`select session_id from crawl_jobs where job_id=$1`, [job_id])).rows[0]?.session_id as string | undefined; | |
| 71 | + if (sid) { | |
| 72 | + const f = path.join(this.cfg.sessionsDir, sid, "summary.json"); | |
| 73 | + if (fs.existsSync(f)) result = JSON.parse(fs.readFileSync(f, "utf8")); | |
| 74 | + } | |
| 75 | + } catch { | |
| 76 | + /* ignore */ | |
| 77 | + } | |
| 78 | + await this.store.pool.query(`update crawl_jobs set status=$2, finished_at=now(), result=$3 where job_id=$1`, [job_id, status, result ? JSON.stringify(result) : null]).catch(() => {}); | |
| 79 | + log.info("job finished", { job_id, status, code, signal }); | |
| 80 | + }); | |
| 81 | + return { job_id }; | |
| 82 | + } | |
| 83 | + | |
| 84 | + stop(job_id: string): boolean { | |
| 85 | + const child = this.running.get(job_id); | |
| 86 | + if (!child) return false; | |
| 87 | + child.kill("SIGTERM"); | |
| 88 | + setTimeout(() => child.kill("SIGKILL"), 15_000).unref(); | |
| 89 | + return true; | |
| 90 | + } | |
| 91 | + | |
| 92 | + logTail(job_id: string, lines = 200): string { | |
| 93 | + const f = path.join(this.logsDir, `${job_id}.log`); | |
| 94 | + if (!fs.existsSync(f)) return ""; | |
| 95 | + const all = fs.readFileSync(f, "utf8").split("\n"); | |
| 96 | + return all.slice(-lines).join("\n"); | |
| 97 | + } | |
| 98 | +} | |
| 99 | + | |
| 100 | +function clamp(n: number, lo: number, hi: number) { | |
| 101 | + return Math.max(lo, Math.min(hi, Number.isFinite(n) ? n : lo)); | |
| 102 | +} | |
added
apps/api/src/queries.ts
+188 −0
@@ -0,0 +1,188 @@ | ||
| 1 | +import fs from "node:fs"; | |
| 2 | +import path from "node:path"; | |
| 3 | +import type { PostgresStore } from "@src/storage"; | |
| 4 | +import { PLATFORMS, type AppConfig, type Platform } from "@src/shared"; | |
| 5 | +import { PlatformModel } from "@src/platform-model"; | |
| 6 | + | |
| 7 | +/** Read-side queries for the research console. All read PostgreSQL; files (models, manifests, session dirs) complete them. */ | |
| 8 | +export class Queries { | |
| 9 | + constructor(private readonly store: PostgresStore, private readonly cfg: AppConfig) {} | |
| 10 | + private get q() { | |
| 11 | + return this.store.pool; | |
| 12 | + } | |
| 13 | + | |
| 14 | + async overview() { | |
| 15 | + const [totals, byType, running, recentSessions, series, surfaces, topActions, patterns] = await Promise.all([ | |
| 16 | + this.q.query(`select | |
| 17 | + (select count(*) from sessions)::int as sessions, | |
| 18 | + (select count(*) from entities)::int as entities, | |
| 19 | + (select count(*) from media)::int as media, | |
| 20 | + (select count(*) from schema_patterns)::int as schemas, | |
| 21 | + (select count(*) from actions)::int as actions, | |
| 22 | + (select count(*) from observations)::int as observations, | |
| 23 | + (select count(*) from relationships)::int as relationships, | |
| 24 | + (select count(*) from observations where event_type='CONNECTOR_PATTERN_LEARNED')::int as patterns_learned, | |
| 25 | + (select count(*) from feed_items)::int as feed_items`), | |
| 26 | + this.q.query(`select platform, entity_type, count(*)::int as n from entities group by 1,2 order by 3 desc`), | |
| 27 | + this.q.query(`select session_id, platform, account_alias, started_at, health from sessions where ended_at is null and started_at > now() - interval '6 hours' order by started_at desc`), | |
| 28 | + this.q.query(`select s.session_id, s.platform, s.goal, s.mode, s.started_at, s.ended_at, s.health, | |
| 29 | + (select count(*) from actions a where a.session_id=s.session_id)::int as actions, | |
| 30 | + (select count(distinct fingerprint) from entity_observations eo where eo.session_id=s.session_id)::int as entities | |
| 31 | + from sessions s order by started_at desc limit 8`), | |
| 32 | + this.q.query(`select date_trunc('hour', ts) + (floor(extract(minute from ts)/10)*10) * interval '1 minute' as bucket, | |
| 33 | + count(*) filter (where event_type in ('VIDEO_DISCOVERED','POST_DISCOVERED','PROFILE_DISCOVERED','ENTITY_DISCOVERED'))::int as entities, | |
| 34 | + count(*) filter (where event_type='NETWORK_RESPONSE_OBSERVED')::int as responses, | |
| 35 | + count(*) filter (where event_type in ('ACTION_EXECUTED','ACTION_FAILED'))::int as actions, | |
| 36 | + count(*) filter (where event_type='NETWORK_SCHEMA_DISCOVERED')::int as schemas | |
| 37 | + from observations where ts > now() - interval '48 hours' group by 1 order by 1`), | |
| 38 | + this.q.query(`select coalesce(sum((payload->>'dom_entities')::int),0)::int as dom, coalesce(sum((payload->>'network_entities')::int),0)::int as network, | |
| 39 | + coalesce(sum((payload->>'both_surfaces')::int),0)::int as both, coalesce(sum((payload->>'field_agreements')::int),0)::int as agreements, | |
| 40 | + coalesce(sum(jsonb_array_length(coalesce(payload->'field_conflicts','[]'::jsonb))),0)::int as conflicts | |
| 41 | + from observations where event_type='PAGE_OPENED' and payload ? 'dom_entities'`), | |
| 42 | + this.q.query(`select action_type, count(*)::int as n, avg(expected_gain)::float as avg_gain, avg(case when success then 1 else 0 end)::float as success_rate, | |
| 43 | + avg((after_state->>'new_entities')::float) as avg_new_entities from actions group by 1 order by 2 desc`), | |
| 44 | + this.q.query(`select platform, count(*)::int as n, avg((payload->>'confidence')::float) as conf from observations where event_type='CONNECTOR_PATTERN_LEARNED' group by 1`), | |
| 45 | + ]); | |
| 46 | + return { | |
| 47 | + totals: totals.rows[0], | |
| 48 | + by_type: byType.rows, | |
| 49 | + running: running.rows, | |
| 50 | + recent_sessions: recentSessions.rows, | |
| 51 | + series: series.rows, | |
| 52 | + surfaces: surfaces.rows[0], | |
| 53 | + actions: topActions.rows, | |
| 54 | + patterns_by_platform: patterns.rows, | |
| 55 | + platforms: this.platforms(), | |
| 56 | + }; | |
| 57 | + } | |
| 58 | + | |
| 59 | + platforms() { | |
| 60 | + return PLATFORMS.map((p) => { | |
| 61 | + const file = path.join(this.cfg.platformModelDir, p, "platform_model.json"); | |
| 62 | + const manifest = path.join(process.cwd(), "connectors", p, "manifest.json"); | |
| 63 | + if (!fs.existsSync(file)) return { platform: p, learned: false, adapter: p === "youtube" || p === "reddit", confidence: 0, page_types: 0, entity_types: 0, navigation_actions: 0, network_schemas: 0, media_patterns: 0, sessions: 0, has_manifest: fs.existsSync(manifest), profiles: this.profilesFor(p) }; | |
| 64 | + const m = new PlatformModel(p, this.cfg.platformModelDir); | |
| 65 | + return { learned: true, adapter: p === "youtube" || p === "reddit", has_manifest: fs.existsSync(manifest), profiles: this.profilesFor(p), ...m.summary() }; | |
| 66 | + }); | |
| 67 | + } | |
| 68 | + | |
| 69 | + profilesFor(p: Platform) { | |
| 70 | + if (!fs.existsSync(this.cfg.profilesDir)) return []; | |
| 71 | + return fs | |
| 72 | + .readdirSync(this.cfg.profilesDir) | |
| 73 | + .filter((d) => d.startsWith(`${p}-`)) | |
| 74 | + .map((d) => ({ alias: d.slice(p.length + 1), has_state: fs.existsSync(path.join(this.cfg.profilesDir, d, "Default", "Cookies")) || fs.existsSync(path.join(this.cfg.profilesDir, d, "Default", "Network", "Cookies")) })); | |
| 75 | + } | |
| 76 | + | |
| 77 | + platformDetail(p: Platform) { | |
| 78 | + const file = path.join(this.cfg.platformModelDir, p, "platform_model.json"); | |
| 79 | + const manifest = path.join(process.cwd(), "connectors", p, "manifest.json"); | |
| 80 | + return { | |
| 81 | + platform: p, | |
| 82 | + model: fs.existsSync(file) ? JSON.parse(fs.readFileSync(file, "utf8")) : null, | |
| 83 | + manifest: fs.existsSync(manifest) ? JSON.parse(fs.readFileSync(manifest, "utf8")) : null, | |
| 84 | + summary: fs.existsSync(file) ? new PlatformModel(p, this.cfg.platformModelDir).summary() : null, | |
| 85 | + profiles: this.profilesFor(p), | |
| 86 | + }; | |
| 87 | + } | |
| 88 | + | |
| 89 | + async sessions(limit = 100) { | |
| 90 | + return ( | |
| 91 | + await this.q.query( | |
| 92 | + `select s.*, (select count(*) from actions a where a.session_id=s.session_id)::int as actions, | |
| 93 | + (select count(distinct fingerprint) from entity_observations eo where eo.session_id=s.session_id)::int as entities, | |
| 94 | + (select count(*) from observations o where o.session_id=s.session_id and o.event_type='NETWORK_SCHEMA_DISCOVERED')::int as schemas, | |
| 95 | + (select count(*) from observations o where o.session_id=s.session_id and o.event_type='CONNECTOR_PATTERN_LEARNED')::int as patterns, | |
| 96 | + (select payload->>'reason' from observations o where o.session_id=s.session_id and o.event_type='BUDGET_EXHAUSTED' limit 1) as ended_because | |
| 97 | + from sessions s order by started_at desc limit $1`, | |
| 98 | + [limit], | |
| 99 | + ) | |
| 100 | + ).rows; | |
| 101 | + } | |
| 102 | + | |
| 103 | + async session(id: string) { | |
| 104 | + const row = (await this.q.query(`select * from sessions where session_id=$1`, [id])).rows[0]; | |
| 105 | + if (!row) return null; | |
| 106 | + const dir = path.join(this.cfg.sessionsDir, id); | |
| 107 | + const read = (f: string) => { | |
| 108 | + try { | |
| 109 | + return JSON.parse(fs.readFileSync(path.join(dir, f), "utf8")); | |
| 110 | + } catch { | |
| 111 | + return null; | |
| 112 | + } | |
| 113 | + }; | |
| 114 | + const counts = (await this.q.query(`select event_type, count(*)::int as n from observations where session_id=$1 group by 1`, [id])).rows; | |
| 115 | + return { ...row, job: read("job.json"), summary: read("summary.json"), event_counts: counts }; | |
| 116 | + } | |
| 117 | + | |
| 118 | + async pages(id: string) { | |
| 119 | + return (await this.q.query(`select step, ts, payload from observations where session_id=$1 and event_type='PAGE_OPENED' and payload ? 'page_type' order by ts`, [id])).rows; | |
| 120 | + } | |
| 121 | + | |
| 122 | + async world(id: string) { | |
| 123 | + const dir = path.join(this.cfg.sessionsDir, id, "world_model.json"); | |
| 124 | + if (fs.existsSync(dir)) return JSON.parse(fs.readFileSync(dir, "utf8")); | |
| 125 | + // Reconstruct from the database for sessions crawled on another machine. | |
| 126 | + const nodes = (await this.q.query(`select e.fingerprint, e.entity_type as type, e.name, e.url, e.platform_id, min(eo.step) as first_seen_step, max(eo.step) as last_seen_step, count(*)::int as seen_count from entities e join entity_observations eo on eo.fingerprint=e.fingerprint where eo.session_id=$1 group by e.fingerprint limit 1500`, [id])).rows; | |
| 127 | + const edges = (await this.q.query(`select from_fp as "from", to_fp as "to", rel_type as type, step from relationships where session_id=$1 limit 4000`, [id])).rows; | |
| 128 | + return { nodes, edges }; | |
| 129 | + } | |
| 130 | + | |
| 131 | + async searchEntities(o: { q?: string; type?: string; platform?: string; limit: number; offset: number; sort?: string }) { | |
| 132 | + const params: unknown[] = []; | |
| 133 | + const where: string[] = []; | |
| 134 | + if (o.q) { | |
| 135 | + params.push(`%${o.q}%`); | |
| 136 | + where.push(`(name ilike $${params.length} or text_excerpt ilike $${params.length} or author ilike $${params.length} or platform_id ilike $${params.length})`); | |
| 137 | + } | |
| 138 | + if (o.type) { | |
| 139 | + params.push(o.type); | |
| 140 | + where.push(`entity_type = $${params.length}`); | |
| 141 | + } | |
| 142 | + if (o.platform) { | |
| 143 | + params.push(o.platform); | |
| 144 | + where.push(`platform = $${params.length}`); | |
| 145 | + } | |
| 146 | + const w = where.length ? `where ${where.join(" and ")}` : ""; | |
| 147 | + const order = o.sort === "views" ? `(metrics->>'views')::numeric desc nulls last` : o.sort === "seen" ? `seen_count desc` : `last_seen desc`; | |
| 148 | + params.push(o.limit, o.offset); | |
| 149 | + const rows = (await this.q.query(`select *, (select count(*) from entity_observations eo where eo.fingerprint=entities.fingerprint)::int as observations from entities ${w} order by ${order} limit $${params.length - 1} offset $${params.length}`, params)).rows; | |
| 150 | + const total = (await this.q.query(`select count(*)::int as n from entities ${w}`, params.slice(0, -2))).rows[0]?.n ?? 0; | |
| 151 | + return { rows, total }; | |
| 152 | + } | |
| 153 | + | |
| 154 | + async entity(fp: string) { | |
| 155 | + const e = (await this.q.query(`select * from entities where fingerprint=$1`, [fp])).rows[0]; | |
| 156 | + if (!e) return null; | |
| 157 | + const observations = (await this.q.query(`select eo.session_id, eo.step, eo.ts, eo.surfaces, eo.snapshot, s.goal from entity_observations eo left join sessions s on s.session_id=eo.session_id where eo.fingerprint=$1 order by eo.ts desc limit 100`, [fp])).rows; | |
| 158 | + const relations = (await this.q.query(`select r.rel_type, r.from_fp, r.to_fp, r.step, coalesce(a.name, a.platform_id) as from_name, coalesce(b.name, b.platform_id) as to_name, a.entity_type as from_type, b.entity_type as to_type from relationships r left join entities a on a.fingerprint=r.from_fp left join entities b on b.fingerprint=r.to_fp where r.from_fp=$1 or r.to_fp=$1 limit 200`, [fp])).rows; | |
| 159 | + const media = (await this.q.query(`select * from media where fingerprint=$1`, [fp])).rows[0] ?? null; | |
| 160 | + const feed = (await this.q.query(`select session_id, step, page_type, feed_position, visible, ts from feed_items where fingerprint=$1 order by ts desc limit 50`, [fp])).rows; | |
| 161 | + return { entity: e, observations, relations, media, feed }; | |
| 162 | + } | |
| 163 | + | |
| 164 | + async schemas(platform?: string) { | |
| 165 | + const params: unknown[] = []; | |
| 166 | + let w = ""; | |
| 167 | + if (platform) { | |
| 168 | + params.push(platform); | |
| 169 | + w = `where platform=$1`; | |
| 170 | + } | |
| 171 | + return (await this.q.query(`select * from schema_patterns ${w} order by observed_count desc limit 300`, params)).rows; | |
| 172 | + } | |
| 173 | + | |
| 174 | + async liveEvents(since: string, session?: string, limit = 200) { | |
| 175 | + const params: unknown[] = [since]; | |
| 176 | + let w = `ts > $1`; | |
| 177 | + if (session) { | |
| 178 | + params.push(session); | |
| 179 | + w += ` and session_id=$2`; | |
| 180 | + } | |
| 181 | + params.push(limit); | |
| 182 | + return (await this.q.query(`select event_id, session_id, platform, event_type, step, ts, payload, provenance from observations where ${w} order by ts asc limit $${params.length}`, params)).rows; | |
| 183 | + } | |
| 184 | + | |
| 185 | + async jobs(limit = 50) { | |
| 186 | + return (await this.q.query(`select j.*, s.health, (select count(*) from actions a where a.session_id=j.session_id)::int as actions, (select count(distinct fingerprint) from entity_observations eo where eo.session_id=j.session_id)::int as entities from crawl_jobs j left join sessions s on s.session_id=j.session_id order by created_at desc limit $1`, [limit])).rows; | |
| 187 | + } | |
| 188 | +} | |
modified
apps/api/src/server.ts
+115 −74
@@ -2,108 +2,149 @@ import http from "node:http"; | ||
| 2 | 2 | import fs from "node:fs"; |
| 3 | 3 | import path from "node:path"; |
| 4 | 4 | import { fileURLToPath } from "node:url"; |
| 5 | −import { createLogger, loadConfig } from "@src/shared"; | |
| 6 | −import { JsonlEventLog } from "@src/events"; | |
| 5 | +import { createLogger, isPlatform, loadConfig } from "@src/shared"; | |
| 7 | 6 | import { PostgresStore } from "@src/storage"; |
| 7 | +import { Queries } from "./queries.ts"; | |
| 8 | +import { JobRunner } from "./jobs.ts"; | |
| 8 | 9 | |
| 9 | 10 | /** |
| 10 | − * Research/Debug dashboard API (§53, §82). Zero framework: node:http + a static page. | |
| 11 | − * Reads PostgreSQL when configured, else the JSONL session logs. | |
| 11 | + * Control-plane + research API (§53, §63, §82). Plain node:http, JSON, SSE. | |
| 12 | + * Consumed by the Next.js console (apps/dashboard) through its authenticated proxy; mutations require SRC_API_TOKEN. | |
| 12 | 13 | */ |
| 13 | −const log = createLogger("dashboard"); | |
| 14 | +const log = createLogger("api"); | |
| 14 | 15 | const cfg = loadConfig(); |
| 15 | 16 | const here = path.dirname(fileURLToPath(import.meta.url)); |
| 16 | 17 | const publicDir = path.join(here, "..", "public"); |
| 17 | −let store: PostgresStore | undefined; | |
| 18 | −try { | |
| 19 | − if (cfg.databaseUrl) store = await PostgresStore.connect(cfg.databaseUrl); | |
| 20 | −} catch (err) { | |
| 21 | − log.warn("no database — serving JSONL logs", { err: (err as Error).message }); | |
| 18 | +const API_TOKEN = process.env.SRC_API_TOKEN ?? ""; | |
| 19 | +const PORT = Number(process.env.SRC_API_PORT ?? cfg.dashboardPort); | |
| 20 | + | |
| 21 | +if (!cfg.databaseUrl) { | |
| 22 | + console.error("SRC_DATABASE_URL is required for the API"); | |
| 23 | + process.exit(1); | |
| 22 | 24 | } |
| 25 | +const store = await PostgresStore.connect(cfg.databaseUrl); | |
| 26 | +await store.migrate(); | |
| 27 | +const queries = new Queries(store, cfg); | |
| 28 | +const jobs = new JobRunner(store, cfg, Number(process.env.SRC_MAX_JOBS ?? 2)); | |
| 23 | 29 | |
| 24 | 30 | function json(res: http.ServerResponse, data: unknown, status = 200) { |
| 25 | − res.writeHead(status, { "content-type": "application/json; charset=utf-8", "access-control-allow-origin": "*" }); | |
| 31 | + res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }); | |
| 26 | 32 | res.end(JSON.stringify(data)); |
| 27 | 33 | } |
| 28 | − | |
| 29 | −function sessionDirs(): string[] { | |
| 30 | − return fs.existsSync(cfg.sessionsDir) ? fs.readdirSync(cfg.sessionsDir).filter((d) => fs.existsSync(path.join(cfg.sessionsDir, d, "events.jsonl"))).sort().reverse() : []; | |
| 31 | −} | |
| 32 | − | |
| 33 | −function jsonlSessions() { | |
| 34 | − return sessionDirs().map((id) => { | |
| 35 | − const job = readJson(path.join(cfg.sessionsDir, id, "job.json")) as Record<string, unknown> | undefined; | |
| 36 | − const summary = readJson(path.join(cfg.sessionsDir, id, "summary.json")) as Record<string, unknown> | undefined; | |
| 37 | − return { session_id: id, platform: job?.platform, mode: job?.mode, goal: job?.goal, account_alias: job?.account_alias, started_at: fs.statSync(path.join(cfg.sessionsDir, id)).birthtime, health: summary ? "stopped" : "running", actions: summary?.steps ?? null, entities: summary?.entities ?? null, ended_because: summary?.ended_because }; | |
| 34 | +function readBody(req: http.IncomingMessage): Promise<string> { | |
| 35 | + return new Promise((resolve, reject) => { | |
| 36 | + let s = ""; | |
| 37 | + req.on("data", (c) => { | |
| 38 | + s += c; | |
| 39 | + if (s.length > 1e6) reject(new Error("body too large")); | |
| 40 | + }); | |
| 41 | + req.on("end", () => resolve(s)); | |
| 42 | + req.on("error", reject); | |
| 38 | 43 | }); |
| 39 | 44 | } |
| 40 | − | |
| 41 | −function readJson(file: string): unknown { | |
| 42 | − try { | |
| 43 | − return JSON.parse(fs.readFileSync(file, "utf8")); | |
| 44 | − } catch { | |
| 45 | − return undefined; | |
| 46 | − } | |
| 45 | +function authorized(req: http.IncomingMessage): boolean { | |
| 46 | + if (!API_TOKEN) return true; // dev | |
| 47 | + return req.headers["x-src-token"] === API_TOKEN; | |
| 47 | 48 | } |
| 48 | 49 | |
| 49 | 50 | const server = http.createServer(async (req, res) => { |
| 50 | 51 | const url = new URL(req.url ?? "/", "http://localhost"); |
| 51 | 52 | const parts = url.pathname.split("/").filter(Boolean); |
| 53 | + const method = req.method ?? "GET"; | |
| 52 | 54 | try { |
| 53 | − if (parts[0] === "api") { | |
| 54 | − if (parts[1] === "sessions" && !parts[2]) return json(res, store ? await store.listSessions() : jsonlSessions()); | |
| 55 | − if (parts[1] === "sessions" && parts[2]) { | |
| 56 | − const id = parts[2]; | |
| 57 | − const dir = path.join(cfg.sessionsDir, id); | |
| 58 | − const sub = parts[3]; | |
| 59 | − if (sub === "events") { | |
| 60 | − const types = url.searchParams.get("types")?.split(",").filter(Boolean); | |
| 61 | − const limit = Number(url.searchParams.get("limit") ?? 300); | |
| 62 | − if (store) return json(res, await store.sessionEvents(id, { types, limit })); | |
| 63 | − const evs = JsonlEventLog.read(dir).filter((e) => !types?.length || types.includes(e.event_type)); | |
| 64 | − return json(res, evs.slice(-limit).reverse()); | |
| 65 | − } | |
| 66 | − if (sub === "actions") return json(res, store ? await store.sessionActions(id) : JsonlEventLog.read(dir).filter((e) => e.event_type === "ACTION_PLANNED").map((e) => ({ step: e.step, ...(e.payload as Record<string, unknown>) }))); | |
| 67 | − if (sub === "entities") { | |
| 68 | − if (store) return json(res, await store.sessionEntities(id)); | |
| 69 | − const seen = new Map<string, unknown>(); | |
| 70 | − for (const e of JsonlEventLog.read(dir)) if (/DISCOVERED|ENTITY_OBSERVED/.test(e.event_type) && (e.payload as { entity?: { fingerprint: string } }).entity) seen.set((e.payload as { entity: { fingerprint: string } }).entity.fingerprint, { ...(e.payload as { entity: object }).entity, last_step: e.step, provenance: e.provenance }); | |
| 71 | − return json(res, [...seen.values()].reverse()); | |
| 72 | − } | |
| 73 | − if (sub === "media") return json(res, store ? await store.sessionMedia(id) : JsonlEventLog.read(dir).filter((e) => e.event_type === "MEDIA_DISCOVERED").map((e) => e.payload)); | |
| 74 | − if (sub === "world") return json(res, readJson(path.join(dir, "world_model.json")) ?? { nodes: [], edges: [] }); | |
| 75 | − if (sub === "summary") return json(res, { job: readJson(path.join(dir, "job.json")), summary: readJson(path.join(dir, "summary.json")) }); | |
| 76 | − if (sub === "schemas") { | |
| 77 | − const platform = (readJson(path.join(dir, "job.json")) as { platform?: string } | undefined)?.platform; | |
| 78 | − if (store) return json(res, await store.schemas(platform)); | |
| 79 | − return json(res, JsonlEventLog.read(dir).filter((e) => e.event_type === "NETWORK_SCHEMA_DISCOVERED").map((e) => e.payload)); | |
| 55 | + if (parts[0] !== "api" && parts[0] !== "media") { | |
| 56 | + // legacy static console (apps/api/public) — kept for local debugging | |
| 57 | + const file = path.join(publicDir, parts.length ? parts.join("/") : "index.html"); | |
| 58 | + if (!file.startsWith(publicDir) || !fs.existsSync(file)) return json(res, { error: "not found" }, 404); | |
| 59 | + res.writeHead(200, { "content-type": file.endsWith(".html") ? "text/html; charset=utf-8" : "application/octet-stream" }); | |
| 60 | + return fs.createReadStream(file).pipe(res); | |
| 61 | + } | |
| 62 | + if (parts[0] === "media") { | |
| 63 | + const file = path.join(cfg.mediaDir, ...parts.slice(1).map(decodeURIComponent)); | |
| 64 | + if (!file.startsWith(cfg.mediaDir) || !fs.existsSync(file)) return json(res, { error: "not found" }, 404); | |
| 65 | + res.writeHead(200, { "content-type": "image/jpeg", "cache-control": "public, max-age=86400" }); | |
| 66 | + return fs.createReadStream(file).pipe(res); | |
| 67 | + } | |
| 68 | + if (method !== "GET" && !authorized(req)) return json(res, { error: "unauthorized" }, 401); | |
| 69 | + const [, r1, r2, r3] = parts; | |
| 70 | + | |
| 71 | + if (r1 === "health") return json(res, { ok: true, jobs_running: jobs.activeCount, db: true, ts: new Date().toISOString() }); | |
| 72 | + if (r1 === "overview") return json(res, await queries.overview()); | |
| 73 | + if (r1 === "platforms" && !r2) return json(res, queries.platforms()); | |
| 74 | + if (r1 === "platforms" && r2) return isPlatform(r2) ? json(res, queries.platformDetail(r2)) : json(res, { error: "unknown platform" }, 404); | |
| 75 | + if (r1 === "sessions" && !r2) return json(res, await queries.sessions(Number(url.searchParams.get("limit") ?? 100))); | |
| 76 | + if (r1 === "sessions" && r2) { | |
| 77 | + const id = r2; | |
| 78 | + if (!r3) { | |
| 79 | + const s = await queries.session(id); | |
| 80 | + return s ? json(res, s) : json(res, { error: "not found" }, 404); | |
| 81 | + } | |
| 82 | + if (r3 === "events") return json(res, await store.sessionEvents(id, { types: url.searchParams.get("types")?.split(",").filter(Boolean), limit: Number(url.searchParams.get("limit") ?? 300) })); | |
| 83 | + if (r3 === "actions") return json(res, await store.sessionActions(id)); | |
| 84 | + if (r3 === "entities") return json(res, await store.sessionEntities(id, Number(url.searchParams.get("limit") ?? 500))); | |
| 85 | + if (r3 === "media") return json(res, await store.sessionMedia(id)); | |
| 86 | + if (r3 === "pages") return json(res, await queries.pages(id)); | |
| 87 | + if (r3 === "world") return json(res, await queries.world(id)); | |
| 88 | + if (r3 === "schemas") { | |
| 89 | + const s = await queries.session(id); | |
| 90 | + return json(res, await queries.schemas(s?.platform)); | |
| 91 | + } | |
| 92 | + if (r3 === "relationships") return json(res, await store.sessionRelationships(id)); | |
| 93 | + } | |
| 94 | + if (r1 === "entities" && !r2) return json(res, await queries.searchEntities({ q: url.searchParams.get("q") ?? undefined, type: url.searchParams.get("type") ?? undefined, platform: url.searchParams.get("platform") ?? undefined, sort: url.searchParams.get("sort") ?? undefined, limit: Math.min(200, Number(url.searchParams.get("limit") ?? 50)), offset: Number(url.searchParams.get("offset") ?? 0) })); | |
| 95 | + if (r1 === "entities" && r2) { | |
| 96 | + const e = await queries.entity(decodeURIComponent(r2)); | |
| 97 | + return e ? json(res, e) : json(res, { error: "not found" }, 404); | |
| 98 | + } | |
| 99 | + if (r1 === "schemas") return json(res, await queries.schemas(url.searchParams.get("platform") ?? undefined)); | |
| 100 | + if (r1 === "stats") return json(res, await store.entityStats()); | |
| 101 | + | |
| 102 | + if (r1 === "stream") { | |
| 103 | + // Server-Sent Events: poll the observations table and push new rows (§53 live panels). | |
| 104 | + res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive", "x-accel-buffering": "no" }); | |
| 105 | + let since = url.searchParams.get("since") ?? new Date(Date.now() - 60_000).toISOString(); | |
| 106 | + const session = url.searchParams.get("session") ?? undefined; | |
| 107 | + const types = url.searchParams.get("types")?.split(",").filter(Boolean); | |
| 108 | + let alive = true; | |
| 109 | + req.on("close", () => (alive = false)); | |
| 110 | + res.write(`event: hello\ndata: ${JSON.stringify({ since })}\n\n`); | |
| 111 | + while (alive) { | |
| 112 | + try { | |
| 113 | + const rows = await queries.liveEvents(since, session, 200); | |
| 114 | + for (const row of rows) { | |
| 115 | + since = new Date(row.ts).toISOString(); | |
| 116 | + if (types && !types.includes(row.event_type)) continue; | |
| 117 | + res.write(`event: observation\ndata: ${JSON.stringify(row)}\n\n`); | |
| 118 | + } | |
| 119 | + res.write(`: ping ${Date.now()}\n\n`); | |
| 120 | + } catch (err) { | |
| 121 | + res.write(`event: error\ndata: ${JSON.stringify({ error: (err as Error).message })}\n\n`); | |
| 80 | 122 | } |
| 123 | + await new Promise((r) => setTimeout(r, 1500)); | |
| 81 | 124 | } |
| 82 | − if (parts[1] === "platform-model" && parts[2]) { | |
| 83 | − const f = path.join(cfg.platformModelDir, parts[2], "platform_model.json"); | |
| 84 | − return json(res, readJson(f) ?? { error: "no model yet" }); | |
| 125 | + return res.end(); | |
| 126 | + } | |
| 127 | + | |
| 128 | + if (r1 === "jobs" && method === "GET" && !r2) return json(res, await queries.jobs()); | |
| 129 | + if (r1 === "jobs" && method === "POST" && !r2) { | |
| 130 | + const body = JSON.parse((await readBody(req)) || "{}"); | |
| 131 | + try { | |
| 132 | + return json(res, await jobs.start(body), 201); | |
| 133 | + } catch (err) { | |
| 134 | + return json(res, { error: (err as Error).message }, 400); | |
| 85 | 135 | } |
| 86 | − if (parts[1] === "entity" && parts[2]) return json(res, store ? await store.entity(decodeURIComponent(parts[2])) : { error: "needs database" }); | |
| 87 | − if (parts[1] === "stats") return json(res, store ? await store.entityStats() : []); | |
| 88 | − return json(res, { error: "not found" }, 404); | |
| 89 | 136 | } |
| 90 | − if (parts[0] === "media") { | |
| 91 | − // serve sampled frames | |
| 92 | − const file = path.join(cfg.mediaDir, ...parts.slice(1)); | |
| 93 | − if (!file.startsWith(cfg.mediaDir) || !fs.existsSync(file)) return json(res, { error: "not found" }, 404); | |
| 94 | − res.writeHead(200, { "content-type": "image/jpeg" }); | |
| 95 | − fs.createReadStream(file).pipe(res); | |
| 96 | − return; | |
| 137 | + if (r1 === "jobs" && r2 && r3 === "log") { | |
| 138 | + res.writeHead(200, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); | |
| 139 | + return res.end(jobs.logTail(r2, Number(url.searchParams.get("lines") ?? 200))); | |
| 97 | 140 | } |
| 98 | − const file = path.join(publicDir, parts.length ? parts.join("/") : "index.html"); | |
| 99 | − if (!file.startsWith(publicDir) || !fs.existsSync(file)) return json(res, { error: "not found" }, 404); | |
| 100 | − const ext = path.extname(file); | |
| 101 | − res.writeHead(200, { "content-type": ext === ".html" ? "text/html; charset=utf-8" : ext === ".js" ? "text/javascript" : ext === ".css" ? "text/css" : "application/octet-stream" }); | |
| 102 | − fs.createReadStream(file).pipe(res); | |
| 141 | + if (r1 === "jobs" && r2 && r3 === "stop" && method === "POST") return json(res, { stopped: jobs.stop(r2) }); | |
| 142 | + | |
| 143 | + return json(res, { error: "not found" }, 404); | |
| 103 | 144 | } catch (err) { |
| 104 | 145 | log.error("request failed", { url: req.url, err: (err as Error).message }); |
| 105 | 146 | json(res, { error: (err as Error).message }, 500); |
| 106 | 147 | } |
| 107 | 148 | }); |
| 108 | 149 | |
| 109 | −server.listen(cfg.dashboardPort, () => log.info(`dashboard → http://localhost:${cfg.dashboardPort} (${store ? "postgres" : "jsonl"})`)); | |
| 150 | +server.listen(PORT, "127.0.0.1", () => log.info(`api → http://127.0.0.1:${PORT} (token ${API_TOKEN ? "on" : "off"})`)); | |
added
apps/dashboard/next.config.ts
+13 −0
@@ -0,0 +1,13 @@ | ||
| 1 | +import type { NextConfig } from "next"; | |
| 2 | + | |
| 3 | +const nextConfig: NextConfig = { | |
| 4 | + reactStrictMode: true, | |
| 5 | + poweredByHeader: false, | |
| 6 | + images: { unoptimized: true }, | |
| 7 | + experimental: { serverActions: { allowedOrigins: ["www.socialcrawl.co", "socialcrawl.co", "localhost:8351"] } }, | |
| 8 | + async headers() { | |
| 9 | + return [{ source: "/(.*)", headers: [{ key: "X-Frame-Options", value: "DENY" }, { key: "X-Content-Type-Options", value: "nosniff" }, { key: "Referrer-Policy", value: "same-origin" }] }]; | |
| 10 | + }, | |
| 11 | +}; | |
| 12 | + | |
| 13 | +export default nextConfig; | |
added
apps/dashboard/package.json
+31 −0
@@ -0,0 +1,31 @@ | ||
| 1 | +{ | |
| 2 | + "name": "@src/dashboard", | |
| 3 | + "version": "0.1.0", | |
| 4 | + "private": true, | |
| 5 | + "type": "module", | |
| 6 | + "scripts": { | |
| 7 | + "dev": "next dev -p 8351", | |
| 8 | + "build": "next build", | |
| 9 | + "start": "next start -p 8351 -H 0.0.0.0", | |
| 10 | + "typecheck": "tsc -p tsconfig.json --noEmit" | |
| 11 | + }, | |
| 12 | + "dependencies": { | |
| 13 | + "clsx": "^2.1.1", | |
| 14 | + "d3-force": "^3.0.0", | |
| 15 | + "framer-motion": "^12.23.0", | |
| 16 | + "lucide-react": "^1.0.0", | |
| 17 | + "next": "16.3.4", | |
| 18 | + "react": "19.2.8", | |
| 19 | + "react-dom": "19.2.8", | |
| 20 | + "recharts": "^3.0.0" | |
| 21 | + }, | |
| 22 | + "devDependencies": { | |
| 23 | + "@tailwindcss/postcss": "^4", | |
| 24 | + "@types/d3-force": "^3.0.10", | |
| 25 | + "@types/node": "^24.0.0", | |
| 26 | + "@types/react": "^19", | |
| 27 | + "@types/react-dom": "^19", | |
| 28 | + "tailwindcss": "^4", | |
| 29 | + "typescript": "^5.9.3" | |
| 30 | + } | |
| 31 | +} | |
added
apps/dashboard/postcss.config.mjs
+2 −0
@@ -0,0 +1,2 @@ | ||
| 1 | +const config = { plugins: { "@tailwindcss/postcss": {} } }; | |
| 2 | +export default config; | |
added
apps/dashboard/public/brand/icon.svg
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><rect width="48" height="48" rx="10" fill="#05070b"/><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop offset="0" stop-color="#38e1ff"/><stop offset="1" stop-color="#9d7bff"/></linearGradient></defs><ellipse cx="24" cy="24" rx="19" ry="10" fill="none" stroke="url(#g)" stroke-width="2.2" transform="rotate(-24 24 24)"/><ellipse cx="24" cy="24" rx="19" ry="10" fill="none" stroke="url(#g)" stroke-width="1.2" opacity=".5" transform="rotate(36 24 24)"/><circle cx="24" cy="24" r="5.5" fill="url(#g)"/><circle cx="40" cy="16" r="2.2" fill="#ffb347"/></svg> | |
added
apps/dashboard/src/app/access/page.tsx
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +import { cookies } from "next/headers"; | |
| 2 | +import { redirect } from "next/navigation"; | |
| 3 | +import { COOKIE, checkPasscode, gateEnabled, makeToken } from "@/lib/auth"; | |
| 4 | +import { Brand } from "@/components/Brand"; | |
| 5 | + | |
| 6 | +export const dynamic = "force-dynamic"; | |
| 7 | + | |
| 8 | +async function login(formData: FormData) { | |
| 9 | + "use server"; | |
| 10 | + const code = String(formData.get("passcode") ?? ""); | |
| 11 | + const next = String(formData.get("next") ?? "/"); | |
| 12 | + if (!checkPasscode(code)) redirect(`/access?error=1&next=${encodeURIComponent(next)}`); | |
| 13 | + const jar = await cookies(); | |
| 14 | + jar.set(COOKIE, makeToken(), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: 30 * 24 * 3600 }); | |
| 15 | + redirect(next.startsWith("/") ? next : "/"); | |
| 16 | +} | |
| 17 | + | |
| 18 | +export default async function AccessPage({ searchParams }: { searchParams: Promise<{ error?: string; next?: string }> }) { | |
| 19 | + const sp = await searchParams; | |
| 20 | + if (!gateEnabled()) redirect("/"); | |
| 21 | + return ( | |
| 22 | + <main className="min-h-screen grid place-items-center grid-bg px-6"> | |
| 23 | + <form action={login} className="panel w-full max-w-sm p-8 space-y-6"> | |
| 24 | + <Brand size="lg" /> | |
| 25 | + <div className="space-y-1"> | |
| 26 | + <h1 className="text-lg font-semibold">Research console</h1> | |
| 27 | + <p className="text-sm text-fg-2">This console observes live social runtimes and can start browser sessions on the cluster. Enter the access code.</p> | |
| 28 | + </div> | |
| 29 | + <input type="hidden" name="next" value={sp.next ?? "/"} /> | |
| 30 | + <label className="block space-y-2"> | |
| 31 | + <span className="text-xs uppercase tracking-widest text-dim">Access code</span> | |
| 32 | + <input name="passcode" type="password" autoFocus required className="w-full rounded-lg bg-bg-2 border border-line-2 px-3 py-2 mono text-fg outline-none focus:border-cyan/60 focus:glow-cyan" /> | |
| 33 | + </label> | |
| 34 | + {sp.error && <p className="text-sm text-red">Wrong code.</p>} | |
| 35 | + <button className="w-full rounded-lg bg-cyan/90 hover:bg-cyan text-bg font-semibold py-2 transition">Enter</button> | |
| 36 | + </form> | |
| 37 | + </main> | |
| 38 | + ); | |
| 39 | +} | |
added
apps/dashboard/src/app/api/[...path]/route.ts
+28 −0
@@ -0,0 +1,28 @@ | ||
| 1 | +import { cookies } from "next/headers"; | |
| 2 | +import { NextResponse, type NextRequest } from "next/server"; | |
| 3 | +import { COOKIE, verifyToken } from "@/lib/auth"; | |
| 4 | + | |
| 5 | +export const dynamic = "force-dynamic"; | |
| 6 | +const API_URL = process.env.API_URL ?? "http://127.0.0.1:8350"; | |
| 7 | +const API_TOKEN = process.env.SRC_API_TOKEN ?? ""; | |
| 8 | + | |
| 9 | +/** Authenticated pass-through to the control-plane API (adds the server token, streams SSE as-is). */ | |
| 10 | +async function forward(req: NextRequest, path: string[]) { | |
| 11 | + const jar = await cookies(); | |
| 12 | + if (!verifyToken(jar.get(COOKIE)?.value)) return NextResponse.json({ error: "unauthorized" }, { status: 401 }); | |
| 13 | + const target = new URL(`/api/${path.join("/")}`, API_URL); | |
| 14 | + target.search = req.nextUrl.search; | |
| 15 | + const headers: Record<string, string> = { "x-src-token": API_TOKEN }; | |
| 16 | + if (req.headers.get("content-type")) headers["content-type"] = req.headers.get("content-type")!; | |
| 17 | + const upstream = await fetch(target, { method: req.method, headers, body: req.method === "GET" || req.method === "HEAD" ? undefined : await req.text(), cache: "no-store", // @ts-expect-error — undici option for streaming request bodies | |
| 18 | + duplex: "half" }); | |
| 19 | + const ct = upstream.headers.get("content-type") ?? "application/json"; | |
| 20 | + return new Response(upstream.body, { status: upstream.status, headers: { "content-type": ct, "cache-control": "no-store", ...(ct.includes("event-stream") ? { connection: "keep-alive", "x-accel-buffering": "no" } : {}) } }); | |
| 21 | +} | |
| 22 | + | |
| 23 | +export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { | |
| 24 | + return forward(req, (await ctx.params).path); | |
| 25 | +} | |
| 26 | +export async function POST(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { | |
| 27 | + return forward(req, (await ctx.params).path); | |
| 28 | +} | |
added
apps/dashboard/src/app/entities/[fp]/page.tsx
+173 −0
@@ -0,0 +1,173 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { use } from "react"; | |
| 4 | +import Link from "next/link"; | |
| 5 | +import { ExternalLink } from "lucide-react"; | |
| 6 | +import { useApi } from "@/lib/api"; | |
| 7 | +import { fmtDate, fmtDuration, fmtNum, truncate } from "@/lib/format"; | |
| 8 | +import { Empty, Metric, Panel, Skeleton, SurfaceChips, Table, Tag, TypeTag } from "@/components/ui"; | |
| 9 | +import type { EntityRow } from "@/components/EntityTable"; | |
| 10 | + | |
| 11 | +interface Detail { | |
| 12 | + entity: EntityRow & { first_seen: string; last_seen: string; seen_count: number; first_session: string }; | |
| 13 | + observations: { session_id: string; step: number; ts: string; surfaces: string[]; snapshot: { name?: string; author?: string; metrics?: Record<string, number>; context?: string; provenance?: { surface: string; confidence: number; detail?: string }[] }; goal: string | null }[]; | |
| 14 | + relations: { rel_type: string; from_fp: string; to_fp: string; from_name: string | null; to_name: string | null; from_type: string; to_type: string; step: number }[]; | |
| 15 | + media: { duration_s: number | null; width: number | null; height: number | null; delivery: { kind: string; hostnames: string[] } | null; frames: string[] | null; thumbnail_url: string | null } | null; | |
| 16 | + feed: { session_id: string; step: number; page_type: string; feed_position: number; visible: boolean; ts: string }[]; | |
| 17 | +} | |
| 18 | + | |
| 19 | +export default function EntityPage({ params }: { params: Promise<{ fp: string }> }) { | |
| 20 | + const { fp } = use(params); | |
| 21 | + const { data, loading, error } = useApi<Detail>(`/entities/${encodeURIComponent(decodeURIComponent(fp))}`); | |
| 22 | + if (loading) return <Skeleton rows={10} />; | |
| 23 | + if (error || !data) return <Empty>{error ?? "not found"}</Empty>; | |
| 24 | + const e = data.entity; | |
| 25 | + const type = e.entity_type ?? e.type ?? "?"; | |
| 26 | + return ( | |
| 27 | + <div className="space-y-4"> | |
| 28 | + <div className="text-xs text-dim mono"> | |
| 29 | + <Link href="/entities" className="hover:text-cyan">entities</Link> / {e.fingerprint} | |
| 30 | + </div> | |
| 31 | + <div className="flex flex-wrap items-start gap-4"> | |
| 32 | + <div className="min-w-0"> | |
| 33 | + <div className="flex items-center gap-2"> | |
| 34 | + <TypeTag type={type} /> | |
| 35 | + <Tag>{e.platform}</Tag> | |
| 36 | + {e.url && ( | |
| 37 | + <a href={e.url} target="_blank" rel="noreferrer" className="text-dim hover:text-cyan flex items-center gap-1 text-xs"> | |
| 38 | + open on platform <ExternalLink className="h-3.5 w-3.5" /> | |
| 39 | + </a> | |
| 40 | + )} | |
| 41 | + </div> | |
| 42 | + <h1 className="text-xl font-semibold tracking-tight mt-2 max-w-3xl">{e.name ?? e.text_excerpt ?? e.platform_id}</h1> | |
| 43 | + {e.author && <div className="text-sm text-fg-2 mt-1">by {e.author}</div>} | |
| 44 | + {e.text_excerpt && e.name && <p className="text-sm text-fg-2 mt-2 max-w-3xl">{e.text_excerpt}</p>} | |
| 45 | + </div> | |
| 46 | + </div> | |
| 47 | + | |
| 48 | + <div className="grid grid-cols-1 xl:grid-cols-3 gap-4"> | |
| 49 | + <Panel title="Evidence per field" sub="every canonical value points back to the surface that produced it (§38)" className="xl:col-span-2" pad={false}> | |
| 50 | + <div className="p-4"> | |
| 51 | + <Table head={["field", "value", "evidence"]} dense> | |
| 52 | + {Object.entries(e.fields ?? {}).map(([k, f]) => ( | |
| 53 | + <tr key={k}> | |
| 54 | + <td className="mono text-fg-2 whitespace-nowrap">{k}</td> | |
| 55 | + <td className="max-w-[480px] break-words">{typeof f.value === "string" ? f.value : JSON.stringify(f.value)}</td> | |
| 56 | + <td> | |
| 57 | + <SurfaceChips provenance={f.provenance} /> | |
| 58 | + {f.provenance?.[0]?.detail && <div className="text-[10px] text-dim mono truncate max-w-[280px]">{f.provenance[0].detail}</div>} | |
| 59 | + </td> | |
| 60 | + </tr> | |
| 61 | + ))} | |
| 62 | + </Table> | |
| 63 | + </div> | |
| 64 | + </Panel> | |
| 65 | + <div className="space-y-4"> | |
| 66 | + <Panel title="Canonical record"> | |
| 67 | + <Metric label="platform id" value={e.platform_id ?? "—"} /> | |
| 68 | + <Metric label="confidence" value={e.confidence ? Number(e.confidence).toFixed(2) : "—"} /> | |
| 69 | + <Metric label="observed" value={`${e.seen_count}× in ${new Set(data.observations.map((o) => o.session_id)).size} session(s)`} /> | |
| 70 | + <Metric label="first seen" value={fmtDate(e.first_seen)} /> | |
| 71 | + <Metric label="last seen" value={fmtDate(e.last_seen)} /> | |
| 72 | + {e.metrics && | |
| 73 | + Object.entries(e.metrics).map(([k, v]) => ( | |
| 74 | + <Metric key={k} label={k} value={fmtNum(v)} /> | |
| 75 | + ))} | |
| 76 | + {data.media && ( | |
| 77 | + <> | |
| 78 | + <Metric label="duration" value={fmtDuration(data.media.duration_s)} /> | |
| 79 | + {data.media.width ? <Metric label="resolution" value={`${data.media.width}×${data.media.height}`} /> : null} | |
| 80 | + {data.media.delivery && <Metric label="delivery" value={`${data.media.delivery.kind} · ${data.media.delivery.hostnames.slice(0, 2).join(", ")}`} />} | |
| 81 | + </> | |
| 82 | + )} | |
| 83 | + </Panel> | |
| 84 | + {data.media?.frames?.length ? ( | |
| 85 | + <Panel title="Sampled frames" sub="0 / 25 / 50 / 75 / 100 % (LEVEL 2)"> | |
| 86 | + <div className="grid grid-cols-5 gap-1"> | |
| 87 | + {data.media.frames.map((f) => ( | |
| 88 | + // eslint-disable-next-line @next/next/no-img-element | |
| 89 | + <img key={f} src={`/media/${f.split("/media/")[1] ?? ""}`} alt="" className="aspect-video object-cover rounded" /> | |
| 90 | + ))} | |
| 91 | + </div> | |
| 92 | + </Panel> | |
| 93 | + ) : data.media?.thumbnail_url || e.media?.thumbnail_url ? ( | |
| 94 | + <Panel title="Thumbnail"> | |
| 95 | + {/* eslint-disable-next-line @next/next/no-img-element */} | |
| 96 | + <img src={data.media?.thumbnail_url ?? e.media?.thumbnail_url ?? ""} alt="" className="rounded w-full" /> | |
| 97 | + </Panel> | |
| 98 | + ) : null} | |
| 99 | + </div> | |
| 100 | + </div> | |
| 101 | + | |
| 102 | + <div className="grid grid-cols-1 xl:grid-cols-2 gap-4"> | |
| 103 | + <Panel title="Observations" sub="raw sightings, never overwritten (§37)" pad={false}> | |
| 104 | + <div className="p-4"> | |
| 105 | + <Table head={["when", "session", "step", "surfaces", "context", "name as seen"]} dense> | |
| 106 | + {data.observations.map((o, i) => ( | |
| 107 | + <tr key={i}> | |
| 108 | + <td className="mono text-dim whitespace-nowrap">{fmtDate(o.ts)}</td> | |
| 109 | + <td> | |
| 110 | + <Link href={`/sessions/${o.session_id}`} className="mono text-[11px] hover:text-cyan"> | |
| 111 | + {truncate(o.goal ?? o.session_id, 32)} | |
| 112 | + </Link> | |
| 113 | + </td> | |
| 114 | + <td className="mono">#{o.step}</td> | |
| 115 | + <td> | |
| 116 | + <SurfaceChips provenance={o.snapshot.provenance ?? o.surfaces.map((s) => ({ surface: s, confidence: 0 }))} /> | |
| 117 | + </td> | |
| 118 | + <td className="text-dim text-[11px]">{o.snapshot.context}</td> | |
| 119 | + <td className="text-fg-2 max-w-[260px] truncate">{o.snapshot.name}</td> | |
| 120 | + </tr> | |
| 121 | + ))} | |
| 122 | + </Table> | |
| 123 | + </div> | |
| 124 | + </Panel> | |
| 125 | + <div className="space-y-4"> | |
| 126 | + <Panel title="Relationships" pad={false}> | |
| 127 | + <div className="p-4"> | |
| 128 | + {data.relations.length ? ( | |
| 129 | + <Table head={["from", "relation", "to"]} dense> | |
| 130 | + {data.relations.map((r, i) => ( | |
| 131 | + <tr key={i}> | |
| 132 | + <td className="max-w-[240px] truncate"> | |
| 133 | + <TypeTag type={r.from_type ?? "?"} /> <Link href={`/entities/${encodeURIComponent(r.from_fp)}`} className="hover:text-cyan">{truncate(r.from_name ?? r.from_fp, 40)}</Link> | |
| 134 | + </td> | |
| 135 | + <td> | |
| 136 | + <Tag color="#9d7bff">{r.rel_type}</Tag> | |
| 137 | + </td> | |
| 138 | + <td className="max-w-[240px] truncate"> | |
| 139 | + <TypeTag type={r.to_type ?? "?"} /> <Link href={`/entities/${encodeURIComponent(r.to_fp)}`} className="hover:text-cyan">{truncate(r.to_name ?? r.to_fp, 40)}</Link> | |
| 140 | + </td> | |
| 141 | + </tr> | |
| 142 | + ))} | |
| 143 | + </Table> | |
| 144 | + ) : ( | |
| 145 | + <Empty>no relationships recorded</Empty> | |
| 146 | + )} | |
| 147 | + </div> | |
| 148 | + </Panel> | |
| 149 | + <Panel title="Feed exposure" sub="where it ranked when the crawler saw it (§44)" pad={false}> | |
| 150 | + <div className="p-4"> | |
| 151 | + {data.feed.length ? ( | |
| 152 | + <Table head={["when", "page", "position", "visible"]} dense> | |
| 153 | + {data.feed.map((f, i) => ( | |
| 154 | + <tr key={i}> | |
| 155 | + <td className="mono text-dim whitespace-nowrap">{fmtDate(f.ts)}</td> | |
| 156 | + <td> | |
| 157 | + <Tag color="#38e1ff">{f.page_type}</Tag> | |
| 158 | + </td> | |
| 159 | + <td className="mono">#{f.feed_position + 1}</td> | |
| 160 | + <td className="mono">{f.visible ? "yes" : "below fold"}</td> | |
| 161 | + </tr> | |
| 162 | + ))} | |
| 163 | + </Table> | |
| 164 | + ) : ( | |
| 165 | + <Empty>not seen in a feed</Empty> | |
| 166 | + )} | |
| 167 | + </div> | |
| 168 | + </Panel> | |
| 169 | + </div> | |
| 170 | + </div> | |
| 171 | + </div> | |
| 172 | + ); | |
| 173 | +} | |
added
apps/dashboard/src/app/entities/page.tsx
+85 −0
@@ -0,0 +1,85 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useState } from "react"; | |
| 4 | +import { Search } from "lucide-react"; | |
| 5 | +import { useApi } from "@/lib/api"; | |
| 6 | +import { fmtNum } from "@/lib/format"; | |
| 7 | +import { Empty, Panel, Skeleton } from "@/components/ui"; | |
| 8 | +import { EntityTable, type EntityRow } from "@/components/EntityTable"; | |
| 9 | + | |
| 10 | +const TYPES = ["", "video", "channel", "profile", "post", "comment", "community", "hashtag", "page"]; | |
| 11 | +const PLATFORMS = ["", "youtube", "reddit", "facebook", "instagram", "tiktok", "x"]; | |
| 12 | + | |
| 13 | +export default function Entities() { | |
| 14 | + const [q, setQ] = useState(""); | |
| 15 | + const [type, setType] = useState(""); | |
| 16 | + const [platform, setPlatform] = useState(""); | |
| 17 | + const [sort, setSort] = useState("last_seen"); | |
| 18 | + const [page, setPage] = useState(0); | |
| 19 | + const limit = 50; | |
| 20 | + const params = new URLSearchParams({ limit: String(limit), offset: String(page * limit), sort }); | |
| 21 | + if (q) params.set("q", q); | |
| 22 | + if (type) params.set("type", type); | |
| 23 | + if (platform) params.set("platform", platform); | |
| 24 | + const { data, loading } = useApi<{ rows: EntityRow[]; total: number }>(`/entities?${params}`); | |
| 25 | + const sel = "rounded-lg bg-bg-2 border border-line-2 px-2 py-1.5 text-sm text-fg outline-none focus:border-cyan/60"; | |
| 26 | + return ( | |
| 27 | + <div className="space-y-4"> | |
| 28 | + <div className="flex flex-wrap items-end gap-3"> | |
| 29 | + <div> | |
| 30 | + <h1 className="text-2xl font-semibold tracking-tight">Entities</h1> | |
| 31 | + <p className="text-sm text-fg-2">Canonical entities with per-field evidence. {data ? <span className="mono">{fmtNum(data.total)} match</span> : null}</p> | |
| 32 | + </div> | |
| 33 | + <div className="ml-auto flex flex-wrap gap-2 items-center"> | |
| 34 | + <label className="relative"> | |
| 35 | + <Search className="h-4 w-4 absolute left-2 top-2 text-dim" /> | |
| 36 | + <input | |
| 37 | + value={q} | |
| 38 | + onChange={(e) => { | |
| 39 | + setQ(e.target.value); | |
| 40 | + setPage(0); | |
| 41 | + }} | |
| 42 | + placeholder="search name, text, author, id…" | |
| 43 | + className={`${sel} pl-8 w-72 mono`} | |
| 44 | + /> | |
| 45 | + </label> | |
| 46 | + <select value={type} onChange={(e) => (setType(e.target.value), setPage(0))} className={sel}> | |
| 47 | + {TYPES.map((t) => ( | |
| 48 | + <option key={t} value={t}> | |
| 49 | + {t || "all types"} | |
| 50 | + </option> | |
| 51 | + ))} | |
| 52 | + </select> | |
| 53 | + <select value={platform} onChange={(e) => (setPlatform(e.target.value), setPage(0))} className={sel}> | |
| 54 | + {PLATFORMS.map((p) => ( | |
| 55 | + <option key={p} value={p}> | |
| 56 | + {p || "all platforms"} | |
| 57 | + </option> | |
| 58 | + ))} | |
| 59 | + </select> | |
| 60 | + <select value={sort} onChange={(e) => setSort(e.target.value)} className={sel}> | |
| 61 | + <option value="last_seen">recently seen</option> | |
| 62 | + <option value="seen">most observed</option> | |
| 63 | + <option value="views">most views</option> | |
| 64 | + </select> | |
| 65 | + </div> | |
| 66 | + </div> | |
| 67 | + <Panel pad={false}> | |
| 68 | + <div className="p-4">{loading ? <Skeleton rows={10} /> : data?.rows.length ? <EntityTable rows={data.rows} showPlatform /> : <Empty>Nothing matches.</Empty>}</div> | |
| 69 | + {data && data.total > limit && ( | |
| 70 | + <div className="flex items-center gap-3 px-4 py-3 border-t border-line text-xs"> | |
| 71 | + <button disabled={page === 0} onClick={() => setPage((p) => p - 1)} className="px-2 py-1 rounded border border-line disabled:opacity-40 hover:border-cyan/50"> | |
| 72 | + ← prev | |
| 73 | + </button> | |
| 74 | + <span className="mono text-dim"> | |
| 75 | + {page * limit + 1}–{Math.min(data.total, (page + 1) * limit)} of {data.total} | |
| 76 | + </span> | |
| 77 | + <button disabled={(page + 1) * limit >= data.total} onClick={() => setPage((p) => p + 1)} className="px-2 py-1 rounded border border-line disabled:opacity-40 hover:border-cyan/50"> | |
| 78 | + next → | |
| 79 | + </button> | |
| 80 | + </div> | |
| 81 | + )} | |
| 82 | + </Panel> | |
| 83 | + </div> | |
| 84 | + ); | |
| 85 | +} | |
added
apps/dashboard/src/app/globals.css
+87 −0
@@ -0,0 +1,87 @@ | ||
| 1 | +@import "tailwindcss"; | |
| 2 | + | |
| 3 | +@theme { | |
| 4 | + --color-bg: #05070b; | |
| 5 | + --color-bg-2: #0a0e15; | |
| 6 | + --color-panel: #0d1219; | |
| 7 | + --color-panel-2: #111823; | |
| 8 | + --color-line: #1a2331; | |
| 9 | + --color-line-2: #243042; | |
| 10 | + --color-fg: #e6edf5; | |
| 11 | + --color-fg-2: #a9b6c6; | |
| 12 | + --color-dim: #6b7a8c; | |
| 13 | + --color-cyan: #38e1ff; | |
| 14 | + --color-amber: #ffb347; | |
| 15 | + --color-green: #43e69a; | |
| 16 | + --color-red: #ff5c7a; | |
| 17 | + --color-violet: #9d7bff; | |
| 18 | + --color-pink: #ff7ad9; | |
| 19 | + --font-sans: var(--font-sans), ui-sans-serif, system-ui, sans-serif; | |
| 20 | + --font-mono: var(--font-mono), ui-monospace, "SF Mono", Menlo, monospace; | |
| 21 | +} | |
| 22 | + | |
| 23 | +:root { | |
| 24 | + color-scheme: dark; | |
| 25 | +} | |
| 26 | + | |
| 27 | +html, | |
| 28 | +body { | |
| 29 | + background: var(--color-bg); | |
| 30 | + color: var(--color-fg); | |
| 31 | + font-family: var(--font-sans); | |
| 32 | + -webkit-font-smoothing: antialiased; | |
| 33 | +} | |
| 34 | + | |
| 35 | +body { | |
| 36 | + background-image: | |
| 37 | + radial-gradient(1200px 600px at 10% -10%, rgba(56, 225, 255, 0.07), transparent 60%), | |
| 38 | + radial-gradient(900px 500px at 100% 0%, rgba(157, 123, 255, 0.07), transparent 60%); | |
| 39 | + background-attachment: fixed; | |
| 40 | +} | |
| 41 | + | |
| 42 | +.grid-bg { | |
| 43 | + background-image: | |
| 44 | + linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px), | |
| 45 | + linear-gradient(90deg, rgba(255, 255, 255, 0.025) 1px, transparent 1px); | |
| 46 | + background-size: 28px 28px; | |
| 47 | + mask-image: radial-gradient(ellipse at top, black 40%, transparent 85%); | |
| 48 | +} | |
| 49 | + | |
| 50 | +.panel { | |
| 51 | + background: linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0.005)), var(--color-panel); | |
| 52 | + border: 1px solid var(--color-line); | |
| 53 | + border-radius: 14px; | |
| 54 | + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 10px 30px -20px rgba(0, 0, 0, 0.8); | |
| 55 | +} | |
| 56 | + | |
| 57 | +.glow-cyan { box-shadow: 0 0 0 1px rgba(56, 225, 255, 0.25), 0 0 24px -6px rgba(56, 225, 255, 0.5); } | |
| 58 | + | |
| 59 | +.mono { font-family: var(--font-mono); font-feature-settings: "tnum" 1, "zero" 1; } | |
| 60 | + | |
| 61 | +.scrollbar-thin::-webkit-scrollbar { width: 8px; height: 8px; } | |
| 62 | +.scrollbar-thin::-webkit-scrollbar-thumb { background: var(--color-line-2); border-radius: 8px; } | |
| 63 | +.scrollbar-thin::-webkit-scrollbar-track { background: transparent; } | |
| 64 | + | |
| 65 | +@keyframes pulse-dot { | |
| 66 | + 0%, 100% { transform: scale(1); opacity: 1; } | |
| 67 | + 50% { transform: scale(1.6); opacity: 0.35; } | |
| 68 | +} | |
| 69 | +.live-dot::after { | |
| 70 | + content: ""; | |
| 71 | + position: absolute; | |
| 72 | + inset: 0; | |
| 73 | + border-radius: 9999px; | |
| 74 | + background: currentColor; | |
| 75 | + animation: pulse-dot 1.6s ease-in-out infinite; | |
| 76 | +} | |
| 77 | + | |
| 78 | +@keyframes shimmer { from { background-position: -200% 0; } to { background-position: 200% 0; } } | |
| 79 | +.skeleton { | |
| 80 | + background: linear-gradient(90deg, rgba(255,255,255,0.03) 25%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.03) 75%); | |
| 81 | + background-size: 200% 100%; | |
| 82 | + animation: shimmer 1.4s linear infinite; | |
| 83 | + border-radius: 6px; | |
| 84 | +} | |
| 85 | + | |
| 86 | +a { color: inherit; } | |
| 87 | +::selection { background: rgba(56, 225, 255, 0.3); } | |
added
apps/dashboard/src/app/jobs/page.tsx
+213 −0
@@ -0,0 +1,213 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useEffect, useState } from "react"; | |
| 4 | +import Link from "next/link"; | |
| 5 | +import { Play, Square } from "lucide-react"; | |
| 6 | +import { api, useApi } from "@/lib/api"; | |
| 7 | +import { ago, truncate } from "@/lib/format"; | |
| 8 | +import { Empty, Panel, Skeleton, StatusPill, Table, Tag } from "@/components/ui"; | |
| 9 | + | |
| 10 | +interface Job { | |
| 11 | + job_id: string; | |
| 12 | + session_id: string | null; | |
| 13 | + platform: string; | |
| 14 | + mode: string; | |
| 15 | + goal: string; | |
| 16 | + budget: { minutes: number; actions: number; media: number; query?: string | null; headless: boolean; account: string }; | |
| 17 | + status: string; | |
| 18 | + created_at: string; | |
| 19 | + finished_at: string | null; | |
| 20 | + result: { steps?: number; entities?: number; videos?: number; ended_because?: string } | null; | |
| 21 | + actions: number; | |
| 22 | + entities: number; | |
| 23 | +} | |
| 24 | + | |
| 25 | +const PRESETS = [ | |
| 26 | + { label: "Topic · AI Québec on YouTube", platform: "youtube", mode: "topic", query: "intelligence artificielle Québec", goal: "Discover public Quebec creators and channels discussing artificial intelligence", minutes: 10, actions: 60, media: 2 }, | |
| 27 | + { label: "Learn · YouTube runtime", platform: "youtube", mode: "learn", query: "", goal: "", minutes: 15, actions: 80, media: 1 }, | |
| 28 | + { label: "Observe · YouTube home feed", platform: "youtube", mode: "observe", query: "", goal: "Observe the recommendation feed without navigating", minutes: 5, actions: 30, media: 0 }, | |
| 29 | + { label: "Research · Reddit r/Quebec AI", platform: "reddit", mode: "research", query: "intelligence artificielle Québec", goal: "Discover public Reddit posts and users discussing AI in Quebec", minutes: 10, actions: 60, media: 1 }, | |
| 30 | +]; | |
| 31 | + | |
| 32 | +export default function Missions() { | |
| 33 | + const jobs = useApi<Job[]>("/jobs", { refreshMs: 5000 }); | |
| 34 | + const [form, setForm] = useState({ platform: "youtube", mode: "research", query: "intelligence artificielle Québec", goal: "Discover public Quebec creators discussing AI", minutes: 10, actions: 60, media: 1, headless: true, account: "research-01" }); | |
| 35 | + const [busy, setBusy] = useState(false); | |
| 36 | + const [msg, setMsg] = useState<string | null>(null); | |
| 37 | + const [logJob, setLogJob] = useState<string | null>(null); | |
| 38 | + const input = "w-full rounded-lg bg-bg-2 border border-line-2 px-3 py-2 text-sm outline-none focus:border-cyan/60"; | |
| 39 | + | |
| 40 | + async function launch() { | |
| 41 | + setBusy(true); | |
| 42 | + setMsg(null); | |
| 43 | + try { | |
| 44 | + const r = await api<{ job_id: string }>("/jobs", { method: "POST", body: JSON.stringify(form) }); | |
| 45 | + setMsg(`mission ${r.job_id} launched`); | |
| 46 | + setLogJob(r.job_id); | |
| 47 | + jobs.refresh(); | |
| 48 | + } catch (e) { | |
| 49 | + setMsg((e as Error).message); | |
| 50 | + } finally { | |
| 51 | + setBusy(false); | |
| 52 | + } | |
| 53 | + } | |
| 54 | + async function stop(id: string) { | |
| 55 | + await api(`/jobs/${id}/stop`, { method: "POST", body: "{}" }); | |
| 56 | + jobs.refresh(); | |
| 57 | + } | |
| 58 | + | |
| 59 | + return ( | |
| 60 | + <div className="space-y-4"> | |
| 61 | + <div> | |
| 62 | + <h1 className="text-2xl font-semibold tracking-tight">Missions</h1> | |
| 63 | + <p className="text-sm text-fg-2 max-w-2xl">A mission opens one browser on the cluster node, gives the agent a goal and a budget, and lets it explore. Read-only: the crawler never likes, follows, comments or subscribes.</p> | |
| 64 | + </div> | |
| 65 | + <div className="grid grid-cols-1 xl:grid-cols-3 gap-4"> | |
| 66 | + <Panel title="New mission" className="xl:col-span-1"> | |
| 67 | + <div className="space-y-3"> | |
| 68 | + <div className="flex flex-wrap gap-1"> | |
| 69 | + {PRESETS.map((p) => ( | |
| 70 | + <button key={p.label} onClick={() => setForm((f) => ({ ...f, ...p }))} className="text-[11px] rounded-md border border-line px-2 py-1 hover:border-cyan/50 text-fg-2 hover:text-fg"> | |
| 71 | + {p.label} | |
| 72 | + </button> | |
| 73 | + ))} | |
| 74 | + </div> | |
| 75 | + <div className="grid grid-cols-2 gap-2"> | |
| 76 | + <label className="text-xs text-dim"> | |
| 77 | + platform | |
| 78 | + <select value={form.platform} onChange={(e) => setForm({ ...form, platform: e.target.value })} className={input}> | |
| 79 | + <option value="youtube">youtube</option> | |
| 80 | + <option value="reddit">reddit</option> | |
| 81 | + </select> | |
| 82 | + </label> | |
| 83 | + <label className="text-xs text-dim"> | |
| 84 | + mode | |
| 85 | + <select value={form.mode} onChange={(e) => setForm({ ...form, mode: e.target.value })} className={input}> | |
| 86 | + {["research", "topic", "observe", "profile", "learn"].map((m) => ( | |
| 87 | + <option key={m}>{m}</option> | |
| 88 | + ))} | |
| 89 | + </select> | |
| 90 | + </label> | |
| 91 | + </div> | |
| 92 | + <label className="text-xs text-dim block"> | |
| 93 | + topic / search query | |
| 94 | + <input value={form.query} onChange={(e) => setForm({ ...form, query: e.target.value })} className={input} placeholder="e.g. intelligence artificielle Québec" /> | |
| 95 | + </label> | |
| 96 | + <label className="text-xs text-dim block"> | |
| 97 | + goal (what the agent optimises for) | |
| 98 | + <textarea value={form.goal} onChange={(e) => setForm({ ...form, goal: e.target.value })} rows={2} className={input} /> | |
| 99 | + </label> | |
| 100 | + <div className="grid grid-cols-3 gap-2"> | |
| 101 | + <label className="text-xs text-dim"> | |
| 102 | + minutes | |
| 103 | + <input type="number" min={1} max={120} value={form.minutes} onChange={(e) => setForm({ ...form, minutes: Number(e.target.value) })} className={`${input} mono`} /> | |
| 104 | + </label> | |
| 105 | + <label className="text-xs text-dim"> | |
| 106 | + actions | |
| 107 | + <input type="number" min={1} max={1000} value={form.actions} onChange={(e) => setForm({ ...form, actions: Number(e.target.value) })} className={`${input} mono`} /> | |
| 108 | + </label> | |
| 109 | + <label className="text-xs text-dim"> | |
| 110 | + media level | |
| 111 | + <select value={form.media} onChange={(e) => setForm({ ...form, media: Number(e.target.value) })} className={input}> | |
| 112 | + <option value={0}>0 · metadata</option> | |
| 113 | + <option value={1}>1 · + thumbnail</option> | |
| 114 | + <option value={2}>2 · + frames</option> | |
| 115 | + </select> | |
| 116 | + </label> | |
| 117 | + </div> | |
| 118 | + <div className="grid grid-cols-2 gap-2"> | |
| 119 | + <label className="text-xs text-dim"> | |
| 120 | + account profile | |
| 121 | + <input value={form.account} onChange={(e) => setForm({ ...form, account: e.target.value })} className={`${input} mono`} /> | |
| 122 | + </label> | |
| 123 | + <label className="text-xs text-dim flex items-end gap-2 pb-2"> | |
| 124 | + <input type="checkbox" checked={form.headless} onChange={(e) => setForm({ ...form, headless: e.target.checked })} /> headless browser | |
| 125 | + </label> | |
| 126 | + </div> | |
| 127 | + <button disabled={busy} onClick={launch} className="w-full rounded-lg bg-cyan/90 hover:bg-cyan disabled:opacity-50 text-bg font-semibold py-2 flex items-center justify-center gap-2"> | |
| 128 | + <Play className="h-4 w-4" /> Launch mission | |
| 129 | + </button> | |
| 130 | + {msg && <div className="text-xs text-fg-2 mono">{msg}</div>} | |
| 131 | + </div> | |
| 132 | + </Panel> | |
| 133 | + <Panel title="Missions" className="xl:col-span-2" pad={false} right={jobs.data ? `${jobs.data.filter((j) => j.status === "running").length} running` : ""}> | |
| 134 | + <div className="p-4"> | |
| 135 | + {jobs.loading ? ( | |
| 136 | + <Skeleton rows={6} /> | |
| 137 | + ) : jobs.data?.length ? ( | |
| 138 | + <Table head={["status", "platform", "goal", "mode", "budget", "steps", "entities", "created", ""]} dense> | |
| 139 | + {jobs.data.map((j) => ( | |
| 140 | + <tr key={j.job_id}> | |
| 141 | + <td> | |
| 142 | + <StatusPill status={j.status} /> | |
| 143 | + </td> | |
| 144 | + <td> | |
| 145 | + <Tag>{j.platform}</Tag> | |
| 146 | + </td> | |
| 147 | + <td className="max-w-[360px]"> | |
| 148 | + {j.session_id ? ( | |
| 149 | + <Link href={`/sessions/${j.session_id}`} className="hover:text-cyan"> | |
| 150 | + {truncate(j.goal, 80)} | |
| 151 | + </Link> | |
| 152 | + ) : ( | |
| 153 | + truncate(j.goal, 80) | |
| 154 | + )} | |
| 155 | + {j.budget?.query && <div className="text-[10.5px] text-dim">“{j.budget.query}”</div>} | |
| 156 | + </td> | |
| 157 | + <td className="text-fg-2">{j.mode}</td> | |
| 158 | + <td className="mono text-[11px] text-dim"> | |
| 159 | + {j.budget?.minutes}m · {j.budget?.actions}a · L{j.budget?.media} | |
| 160 | + </td> | |
| 161 | + <td className="mono">{j.actions || j.result?.steps || 0}</td> | |
| 162 | + <td className="mono">{j.entities || j.result?.entities || 0}</td> | |
| 163 | + <td className="text-dim whitespace-nowrap">{ago(j.created_at)}</td> | |
| 164 | + <td className="whitespace-nowrap"> | |
| 165 | + <button onClick={() => setLogJob(j.job_id)} className="text-[11px] text-dim hover:text-cyan mr-2"> | |
| 166 | + log | |
| 167 | + </button> | |
| 168 | + {j.status === "running" && ( | |
| 169 | + <button onClick={() => stop(j.job_id)} className="text-[11px] text-red hover:text-red/80 inline-flex items-center gap-1"> | |
| 170 | + <Square className="h-3 w-3" /> stop | |
| 171 | + </button> | |
| 172 | + )} | |
| 173 | + </td> | |
| 174 | + </tr> | |
| 175 | + ))} | |
| 176 | + </Table> | |
| 177 | + ) : ( | |
| 178 | + <Empty>No missions yet — launch one on the left.</Empty> | |
| 179 | + )} | |
| 180 | + </div> | |
| 181 | + </Panel> | |
| 182 | + </div> | |
| 183 | + {logJob && <JobLog id={logJob} onClose={() => setLogJob(null)} />} | |
| 184 | + </div> | |
| 185 | + ); | |
| 186 | +} | |
| 187 | + | |
| 188 | +function JobLog({ id, onClose }: { id: string; onClose: () => void }) { | |
| 189 | + const [text, setText] = useState(""); | |
| 190 | + useEffect(() => { | |
| 191 | + let alive = true; | |
| 192 | + let timer: ReturnType<typeof setTimeout> | undefined; | |
| 193 | + const tick = async () => { | |
| 194 | + try { | |
| 195 | + const r = await fetch(`/api/jobs/${id}/log?lines=300`, { cache: "no-store" }); | |
| 196 | + if (alive) setText(await r.text()); | |
| 197 | + } catch { | |
| 198 | + /* ignore */ | |
| 199 | + } | |
| 200 | + if (alive) timer = setTimeout(tick, 3000); | |
| 201 | + }; | |
| 202 | + tick(); | |
| 203 | + return () => { | |
| 204 | + alive = false; | |
| 205 | + if (timer) clearTimeout(timer); | |
| 206 | + }; | |
| 207 | + }, [id]); | |
| 208 | + return ( | |
| 209 | + <Panel title={`Worker log · ${id}`} right={<button onClick={onClose} className="hover:text-cyan">close</button>}> | |
| 210 | + <pre className="mono text-[11px] leading-5 text-fg-2 whitespace-pre-wrap max-h-[420px] overflow-auto scrollbar-thin">{text || "…"}</pre> | |
| 211 | + </Panel> | |
| 212 | + ); | |
| 213 | +} | |
added
apps/dashboard/src/app/layout.tsx
+24 −0
@@ -0,0 +1,24 @@ | ||
| 1 | +import type { Metadata } from "next"; | |
| 2 | +import { Inter, JetBrains_Mono } from "next/font/google"; | |
| 3 | +import "./globals.css"; | |
| 4 | +import { Shell } from "@/components/Shell"; | |
| 5 | + | |
| 6 | +const sans = Inter({ subsets: ["latin"], variable: "--font-sans", display: "swap" }); | |
| 7 | +const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono", display: "swap" }); | |
| 8 | + | |
| 9 | +export const metadata: Metadata = { | |
| 10 | + title: { default: "SocialCrawl — Social Runtime Mining console", template: "%s · SocialCrawl" }, | |
| 11 | + description: "Research console of the Social Runtime Crawler: live observation of social-media runtimes, information-gain navigation, learned connectors.", | |
| 12 | + robots: { index: false, follow: false }, | |
| 13 | + icons: { icon: "/brand/icon.svg" }, | |
| 14 | +}; | |
| 15 | + | |
| 16 | +export default function RootLayout({ children }: { children: React.ReactNode }) { | |
| 17 | + return ( | |
| 18 | + <html lang="en" className={`${sans.variable} ${mono.variable}`}> | |
| 19 | + <body className="min-h-screen"> | |
| 20 | + <Shell>{children}</Shell> | |
| 21 | + </body> | |
| 22 | + </html> | |
| 23 | + ); | |
| 24 | +} | |
added
apps/dashboard/src/app/media/[...path]/route.ts
+15 −0
@@ -0,0 +1,15 @@ | ||
| 1 | +import { cookies } from "next/headers"; | |
| 2 | +import { NextResponse, type NextRequest } from "next/server"; | |
| 3 | +import { COOKIE, verifyToken } from "@/lib/auth"; | |
| 4 | + | |
| 5 | +export const dynamic = "force-dynamic"; | |
| 6 | +const API_URL = process.env.API_URL ?? "http://127.0.0.1:8350"; | |
| 7 | + | |
| 8 | +/** Sampled video frames, served through the gate. */ | |
| 9 | +export async function GET(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) { | |
| 10 | + const jar = await cookies(); | |
| 11 | + if (!verifyToken(jar.get(COOKIE)?.value)) return NextResponse.json({ error: "unauthorized" }, { status: 401 }); | |
| 12 | + const { path } = await ctx.params; | |
| 13 | + const upstream = await fetch(new URL(`/media/${path.map(encodeURIComponent).join("/")}`, API_URL), { cache: "no-store" }); | |
| 14 | + return new Response(upstream.body, { status: upstream.status, headers: { "content-type": upstream.headers.get("content-type") ?? "image/jpeg", "cache-control": "private, max-age=3600" } }); | |
| 15 | +} | |
added
apps/dashboard/src/app/page.tsx
+126 −0
@@ -0,0 +1,126 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import Link from "next/link"; | |
| 4 | +import { ArrowRight } from "lucide-react"; | |
| 5 | +import { useApi } from "@/lib/api"; | |
| 6 | +import { ago, fmtNum, truncate } from "@/lib/format"; | |
| 7 | +import { Bar, Empty, Kpi, Panel, Skeleton, StatusPill, Tag } from "@/components/ui"; | |
| 8 | +import { ActivityChart, SurfaceBars, TypeDonut, YieldBars } from "@/components/charts"; | |
| 9 | +import { LiveFeed } from "@/components/LiveFeed"; | |
| 10 | + | |
| 11 | +interface Overview { | |
| 12 | + totals: { sessions: number; entities: number; media: number; schemas: number; actions: number; observations: number; relationships: number; patterns_learned: number; feed_items: number }; | |
| 13 | + by_type: { platform: string; entity_type: string; n: number }[]; | |
| 14 | + running: { session_id: string; platform: string; started_at: string }[]; | |
| 15 | + recent_sessions: { session_id: string; platform: string; goal: string | null; mode: string | null; started_at: string; ended_at: string | null; health: string; actions: number; entities: number }[]; | |
| 16 | + series: { bucket: string; entities: number; responses: number; actions: number; schemas: number }[]; | |
| 17 | + surfaces: { dom: number; network: number; both: number; agreements: number; conflicts: number }; | |
| 18 | + actions: { action_type: string; n: number; avg_gain: number | null; success_rate: number | null; avg_new_entities: number | null }[]; | |
| 19 | + platforms: { platform: string; learned: boolean; adapter: boolean; confidence: number; page_types: number; entity_types: number; network_schemas: number; sessions: number; has_manifest: boolean }[]; | |
| 20 | +} | |
| 21 | + | |
| 22 | +export default function Observatory() { | |
| 23 | + const { data, loading } = useApi<Overview>("/overview", { refreshMs: 8000 }); | |
| 24 | + const t = data?.totals; | |
| 25 | + const s = data?.surfaces; | |
| 26 | + const agreement = s && s.agreements + s.conflicts > 0 ? s.agreements / (s.agreements + s.conflicts) : null; | |
| 27 | + const spark = (k: keyof Overview["series"][number]) => (data?.series ?? []).slice(-30).map((r) => Number(r[k])); | |
| 28 | + return ( | |
| 29 | + <div className="space-y-6"> | |
| 30 | + <div className="flex flex-wrap items-end gap-4"> | |
| 31 | + <div> | |
| 32 | + <h1 className="text-2xl font-semibold tracking-tight">Observatory</h1> | |
| 33 | + <p className="text-sm text-fg-2 mt-1 max-w-2xl">The crawler does not download pages. It watches social applications run inside an authenticated browser, fuses what the network and the DOM reveal, and learns the platform as it goes.</p> | |
| 34 | + </div> | |
| 35 | + <div className="ml-auto flex gap-2"> | |
| 36 | + {data?.running.map((r) => ( | |
| 37 | + <Link key={r.session_id} href={`/sessions/${r.session_id}`} className="panel px-3 py-1.5 text-xs flex items-center gap-2 hover:border-amber/50"> | |
| 38 | + <StatusPill status="running" /> {r.platform} <span className="text-dim">{ago(r.started_at)}</span> | |
| 39 | + </Link> | |
| 40 | + ))} | |
| 41 | + <Link href="/jobs" className="rounded-lg bg-cyan/90 hover:bg-cyan text-bg text-sm font-semibold px-3 py-1.5 flex items-center gap-1"> | |
| 42 | + New mission <ArrowRight className="h-4 w-4" /> | |
| 43 | + </Link> | |
| 44 | + </div> | |
| 45 | + </div> | |
| 46 | + | |
| 47 | + <div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3"> | |
| 48 | + <Kpi label="entities" value={fmtNum(t?.entities)} hint={`${fmtNum(t?.relationships)} relationships`} spark={spark("entities")} /> | |
| 49 | + <Kpi label="media" value={fmtNum(t?.media)} hint="videos & images with metadata" accent="pink" /> | |
| 50 | + <Kpi label="runtime schemas" value={fmtNum(t?.schemas)} hint={`${fmtNum(t?.patterns_learned)} patterns learned`} accent="violet" spark={spark("schemas")} /> | |
| 51 | + <Kpi label="actions" value={fmtNum(t?.actions)} hint={`${fmtNum(t?.sessions)} sessions`} accent="amber" spark={spark("actions")} /> | |
| 52 | + <Kpi label="observations" value={fmtNum(t?.observations)} hint="raw, append-only" accent="fg" spark={spark("responses")} /> | |
| 53 | + <Kpi label="surface agreement" value={agreement === null ? "—" : `${Math.round(agreement * 100)}%`} hint={s ? `${fmtNum(s.both)} entities seen on both surfaces` : ""} accent="green" /> | |
| 54 | + </div> | |
| 55 | + | |
| 56 | + <div className="grid grid-cols-1 xl:grid-cols-3 gap-4"> | |
| 57 | + <Panel title="Activity" sub="10-minute buckets, last 48 h" className="xl:col-span-2"> | |
| 58 | + {loading ? <Skeleton rows={6} /> : data?.series.length ? <ActivityChart data={data.series} /> : <Empty>No activity yet — launch a mission.</Empty>} | |
| 59 | + </Panel> | |
| 60 | + <Panel title="Entity mix" sub="canonical entities by type"> | |
| 61 | + {data?.by_type.length ? <TypeDonut data={data.by_type} /> : <Empty>—</Empty>} | |
| 62 | + </Panel> | |
| 63 | + </div> | |
| 64 | + | |
| 65 | + <div className="grid grid-cols-1 xl:grid-cols-3 gap-4"> | |
| 66 | + <Panel title="Live observation stream" sub="every event, as the browser sees it" className="xl:col-span-2" pad={false}> | |
| 67 | + <div className="p-2"> | |
| 68 | + <LiveFeed max={80} height={380} /> | |
| 69 | + </div> | |
| 70 | + </Panel> | |
| 71 | + <div className="space-y-4"> | |
| 72 | + <Panel title="Network vs DOM" sub="where entities were confirmed"> | |
| 73 | + {s ? <SurfaceBars dom={s.dom} network={s.network} both={s.both} /> : <Skeleton />} | |
| 74 | + <div className="text-xs text-dim mt-2"> | |
| 75 | + field agreements <span className="mono text-fg">{fmtNum(s?.agreements)}</span> · conflicts <span className="mono text-fg">{fmtNum(s?.conflicts)}</span> | |
| 76 | + </div> | |
| 77 | + </Panel> | |
| 78 | + <Panel title="Action yield" sub="average new entities per action type"> | |
| 79 | + {data?.actions.length ? <YieldBars data={data.actions} height={190} /> : <Empty>—</Empty>} | |
| 80 | + </Panel> | |
| 81 | + </div> | |
| 82 | + </div> | |
| 83 | + | |
| 84 | + <div className="grid grid-cols-1 xl:grid-cols-2 gap-4"> | |
| 85 | + <Panel title="Recent sessions" right={<Link href="/sessions" className="hover:text-cyan">all →</Link>} pad={false}> | |
| 86 | + {data?.recent_sessions.length ? ( | |
| 87 | + <ul className="divide-y divide-line/60"> | |
| 88 | + {data.recent_sessions.map((r) => ( | |
| 89 | + <li key={r.session_id}> | |
| 90 | + <Link href={`/sessions/${r.session_id}`} className="flex items-center gap-3 px-4 py-2.5 hover:bg-white/[0.03]"> | |
| 91 | + <StatusPill status={r.ended_at ? "done" : r.health} /> | |
| 92 | + <Tag>{r.platform}</Tag> | |
| 93 | + <span className="text-sm truncate">{truncate(r.goal ?? "(no goal)", 70)}</span> | |
| 94 | + <span className="ml-auto mono text-xs text-dim whitespace-nowrap"> | |
| 95 | + {r.actions} steps · {r.entities} ent · {ago(r.started_at)} | |
| 96 | + </span> | |
| 97 | + </Link> | |
| 98 | + </li> | |
| 99 | + ))} | |
| 100 | + </ul> | |
| 101 | + ) : ( | |
| 102 | + <Empty>No sessions yet.</Empty> | |
| 103 | + )} | |
| 104 | + </Panel> | |
| 105 | + <Panel title="Connectors" sub="learned platform knowledge" right={<Link href="/platforms" className="hover:text-cyan">details →</Link>}> | |
| 106 | + <div className="space-y-3"> | |
| 107 | + {data?.platforms | |
| 108 | + .filter((p) => p.adapter || p.learned) | |
| 109 | + .map((p) => ( | |
| 110 | + <Link key={p.platform} href={`/platforms/${p.platform}`} className="block group"> | |
| 111 | + <div className="flex items-center gap-3 text-sm"> | |
| 112 | + <span className="w-20 capitalize group-hover:text-cyan">{p.platform}</span> | |
| 113 | + <Bar value={p.confidence / 100} color={p.confidence >= 70 ? "#43e69a" : p.confidence >= 40 ? "#38e1ff" : "#ffb347"} className="flex-1" /> | |
| 114 | + <span className="mono text-xs w-10 text-right">{p.confidence}%</span> | |
| 115 | + </div> | |
| 116 | + <div className="text-[10.5px] text-dim mono ml-[92px]"> | |
| 117 | + {p.page_types} page types · {p.entity_types} entity types · {p.network_schemas} schemas · {p.sessions} sessions{p.has_manifest ? " · compiled" : ""} | |
| 118 | + </div> | |
| 119 | + </Link> | |
| 120 | + ))} | |
| 121 | + </div> | |
| 122 | + </Panel> | |
| 123 | + </div> | |
| 124 | + </div> | |
| 125 | + ); | |
| 126 | +} | |
added
apps/dashboard/src/app/platforms/[platform]/page.tsx
+198 −0
@@ -0,0 +1,198 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { use } from "react"; | |
| 4 | +import Link from "next/link"; | |
| 5 | +import { useApi } from "@/lib/api"; | |
| 6 | +import { fmtDate } from "@/lib/format"; | |
| 7 | +import { Bar, Empty, Metric, Panel, Skeleton, Table, Tag } from "@/components/ui"; | |
| 8 | + | |
| 9 | +interface Detail { | |
| 10 | + platform: string; | |
| 11 | + summary: { confidence: number; page_types: number; entity_types: number; navigation_actions: number; network_schemas: number; media_patterns: number; sessions: number } | null; | |
| 12 | + model: { | |
| 13 | + updated_at: string; | |
| 14 | + response_patterns: Record<string, { shape_hash: string; hostname: string; path_pattern: string; method: string; graphql_operation?: string; likely_entity_types: Record<string, number>; observed_count: number; entity_yield_total: number; confidence: number; triggered_by: Record<string, number>; first_seen: string; last_seen: string }>; | |
| 15 | + action_patterns: Record<string, { action_type: string; from_page_type: string; observed_count: number; avg_new_entities: number; avg_network_responses: number; to_page_types: Record<string, number>; failures: number }>; | |
| 16 | + page_types: Record<string, { page_type: string; observed_count: number; url_patterns: Record<string, number>; avg_entities: number; entity_types: Record<string, number>; avg_confidence: number }>; | |
| 17 | + media_patterns: Record<string, { hostname: string; kind: string; observed_count: number }>; | |
| 18 | + sessions_learned: string[]; | |
| 19 | + } | null; | |
| 20 | + manifest: { compiled_at: string; confidence: number; network_patterns: unknown[] } | null; | |
| 21 | + profiles: { alias: string; has_state: boolean }[]; | |
| 22 | +} | |
| 23 | + | |
| 24 | +export default function PlatformPage({ params }: { params: Promise<{ platform: string }> }) { | |
| 25 | + const { platform } = use(params); | |
| 26 | + const { data, loading } = useApi<Detail>(`/platforms/${platform}`, { refreshMs: 15_000 }); | |
| 27 | + if (loading) return <Skeleton rows={10} />; | |
| 28 | + if (!data) return <Empty>unknown platform</Empty>; | |
| 29 | + const m = data.model; | |
| 30 | + const rps = m ? Object.values(m.response_patterns).filter((r) => Object.keys(r.likely_entity_types).length).sort((a, b) => b.confidence - a.confidence) : []; | |
| 31 | + const aps = m ? Object.values(m.action_patterns).filter((a) => a.from_page_type !== "ANY").sort((a, b) => b.observed_count - a.observed_count) : []; | |
| 32 | + const pts = m ? Object.values(m.page_types).sort((a, b) => b.observed_count - a.observed_count) : []; | |
| 33 | + return ( | |
| 34 | + <div className="space-y-4"> | |
| 35 | + <div className="text-xs text-dim mono"> | |
| 36 | + <Link href="/platforms" className="hover:text-cyan">connectors</Link> / {platform} | |
| 37 | + </div> | |
| 38 | + <div className="flex flex-wrap items-end gap-4"> | |
| 39 | + <h1 className="text-2xl font-semibold tracking-tight capitalize">{platform}</h1> | |
| 40 | + {data.summary && ( | |
| 41 | + <div className="ml-auto flex items-center gap-3 w-72"> | |
| 42 | + <span className="text-xs text-dim">confidence</span> | |
| 43 | + <Bar value={data.summary.confidence / 100} className="flex-1" color={data.summary.confidence >= 70 ? "#43e69a" : "#38e1ff"} /> | |
| 44 | + <span className="mono">{data.summary.confidence}%</span> | |
| 45 | + </div> | |
| 46 | + )} | |
| 47 | + </div> | |
| 48 | + {!m ? ( | |
| 49 | + <Panel> | |
| 50 | + <Empty> | |
| 51 | + Nothing learned yet on {platform}. {["youtube", "reddit"].includes(platform) ? "Launch a learning mission from the Missions page." : "No adapter yet — this platform is scheduled for a later phase."} | |
| 52 | + </Empty> | |
| 53 | + </Panel> | |
| 54 | + ) : ( | |
| 55 | + <> | |
| 56 | + <div className="grid grid-cols-1 xl:grid-cols-3 gap-4"> | |
| 57 | + <Panel title="Learned model"> | |
| 58 | + <Metric label="page types" value={data.summary?.page_types} /> | |
| 59 | + <Metric label="entity types" value={data.summary?.entity_types} /> | |
| 60 | + <Metric label="navigation actions" value={data.summary?.navigation_actions} /> | |
| 61 | + <Metric label="network schemas" value={data.summary?.network_schemas} /> | |
| 62 | + <Metric label="media patterns" value={data.summary?.media_patterns} /> | |
| 63 | + <Metric label="sessions learned from" value={m.sessions_learned.length} /> | |
| 64 | + <Metric label="updated" value={fmtDate(m.updated_at)} /> | |
| 65 | + <Metric label="compiled manifest" value={data.manifest ? `${fmtDate(data.manifest.compiled_at)} · ${data.manifest.network_patterns.length} patterns` : "not yet (needs ≥ 40 %)"} /> | |
| 66 | + <Metric label="browser profiles" value={data.profiles.length ? data.profiles.map((p) => `${p.alias}${p.has_state ? " ✓" : ""}`).join(", ") : "none — run pnpm login"} /> | |
| 67 | + </Panel> | |
| 68 | + <Panel title="Page types" sub="what the crawler recognises, and what each page usually yields" className="xl:col-span-2" pad={false}> | |
| 69 | + <div className="p-4"> | |
| 70 | + <Table head={["page type", "seen", "avg entities", "avg conf", "url patterns", "entity mix"]} dense> | |
| 71 | + {pts.map((p) => ( | |
| 72 | + <tr key={p.page_type}> | |
| 73 | + <td> | |
| 74 | + <Tag color="#38e1ff">{p.page_type}</Tag> | |
| 75 | + </td> | |
| 76 | + <td className="mono">{p.observed_count}</td> | |
| 77 | + <td className="mono">{p.avg_entities.toFixed(1)}</td> | |
| 78 | + <td className="mono">{p.avg_confidence.toFixed(2)}</td> | |
| 79 | + <td className="mono text-[11px] text-fg-2"> | |
| 80 | + {Object.entries(p.url_patterns) | |
| 81 | + .sort((a, b) => b[1] - a[1]) | |
| 82 | + .slice(0, 3) | |
| 83 | + .map(([k, v]) => `${k} (${v})`) | |
| 84 | + .join(" ")} | |
| 85 | + </td> | |
| 86 | + <td className="text-[11px]"> | |
| 87 | + {Object.entries(p.entity_types) | |
| 88 | + .sort((a, b) => b[1] - a[1]) | |
| 89 | + .slice(0, 4) | |
| 90 | + .map(([k, v]) => ( | |
| 91 | + <Tag key={k} className="mr-1"> | |
| 92 | + {k} {v} | |
| 93 | + </Tag> | |
| 94 | + ))} | |
| 95 | + </td> | |
| 96 | + </tr> | |
| 97 | + ))} | |
| 98 | + </Table> | |
| 99 | + </div> | |
| 100 | + </Panel> | |
| 101 | + </div> | |
| 102 | + <Panel title="Runtime API map" sub="action → response shape → entity types (§71) — the learned frontend API of the platform" pad={false}> | |
| 103 | + <div className="p-4"> | |
| 104 | + {rps.length ? ( | |
| 105 | + <Table head={["endpoint", "entity types", "triggered by", "seen", "yield", "confidence"]} dense> | |
| 106 | + {rps.map((r) => ( | |
| 107 | + <tr key={r.shape_hash}> | |
| 108 | + <td className="mono text-[11.5px]"> | |
| 109 | + <Tag color="#9d7bff" className="mr-1"> | |
| 110 | + {r.method} | |
| 111 | + </Tag> | |
| 112 | + {r.hostname} | |
| 113 | + <span className="text-fg-2">{r.path_pattern}</span> | |
| 114 | + {r.graphql_operation && <Tag color="#ff7ad9" className="ml-1">{r.graphql_operation}</Tag>} | |
| 115 | + <div className="text-[10px] text-dim">#{r.shape_hash.slice(0, 10)}</div> | |
| 116 | + </td> | |
| 117 | + <td> | |
| 118 | + {Object.entries(r.likely_entity_types).map(([t, n]) => ( | |
| 119 | + <Tag key={t} color="#38e1ff" className="mr-1"> | |
| 120 | + {t} {n} | |
| 121 | + </Tag> | |
| 122 | + ))} | |
| 123 | + </td> | |
| 124 | + <td> | |
| 125 | + {Object.entries(r.triggered_by).map(([a, n]) => ( | |
| 126 | + <Tag key={a} color="#ffb347" className="mr-1"> | |
| 127 | + {a.toLowerCase()} {n} | |
| 128 | + </Tag> | |
| 129 | + ))} | |
| 130 | + </td> | |
| 131 | + <td className="mono">{r.observed_count}</td> | |
| 132 | + <td className="mono">{(r.entity_yield_total / Math.max(1, r.observed_count)).toFixed(1)} ent/resp</td> | |
| 133 | + <td className="w-32"> | |
| 134 | + <Bar value={r.confidence} color={r.confidence >= 0.5 ? "#43e69a" : "#ffb347"} /> | |
| 135 | + <div className="mono text-[10px] text-dim">{r.confidence.toFixed(2)}</div> | |
| 136 | + </td> | |
| 137 | + </tr> | |
| 138 | + ))} | |
| 139 | + </Table> | |
| 140 | + ) : ( | |
| 141 | + <Empty>no entity-bearing schema learned yet</Empty> | |
| 142 | + )} | |
| 143 | + </div> | |
| 144 | + </Panel> | |
| 145 | + <div className="grid grid-cols-1 xl:grid-cols-2 gap-4"> | |
| 146 | + <Panel title="Navigation patterns" sub="action from a page type → where it leads and what it yields (§30)" pad={false}> | |
| 147 | + <div className="p-4"> | |
| 148 | + <Table head={["action", "from", "→ to", "seen", "avg new ent", "avg resp", "fail"]} dense> | |
| 149 | + {aps.map((a) => ( | |
| 150 | + <tr key={a.action_type + a.from_page_type}> | |
| 151 | + <td> | |
| 152 | + <Tag color="#ffb347">{a.action_type.toLowerCase()}</Tag> | |
| 153 | + </td> | |
| 154 | + <td> | |
| 155 | + <Tag color="#38e1ff">{a.from_page_type}</Tag> | |
| 156 | + </td> | |
| 157 | + <td className="text-[11px]"> | |
| 158 | + {Object.entries(a.to_page_types) | |
| 159 | + .sort((x, y) => y[1] - x[1]) | |
| 160 | + .map(([k, v]) => `${k} ${v}`) | |
| 161 | + .join(", ")} | |
| 162 | + </td> | |
| 163 | + <td className="mono">{a.observed_count}</td> | |
| 164 | + <td className="mono">{a.avg_new_entities.toFixed(1)}</td> | |
| 165 | + <td className="mono">{a.avg_network_responses.toFixed(1)}</td> | |
| 166 | + <td className="mono">{a.failures}</td> | |
| 167 | + </tr> | |
| 168 | + ))} | |
| 169 | + </Table> | |
| 170 | + </div> | |
| 171 | + </Panel> | |
| 172 | + <Panel title="Media delivery patterns" pad={false}> | |
| 173 | + <div className="p-4"> | |
| 174 | + {Object.values(m.media_patterns).length ? ( | |
| 175 | + <Table head={["host", "kind", "seen"]} dense> | |
| 176 | + {Object.values(m.media_patterns) | |
| 177 | + .sort((a, b) => b.observed_count - a.observed_count) | |
| 178 | + .map((mp) => ( | |
| 179 | + <tr key={mp.hostname + mp.kind}> | |
| 180 | + <td className="mono text-[11.5px]">{mp.hostname}</td> | |
| 181 | + <td> | |
| 182 | + <Tag color="#ff7ad9">{mp.kind}</Tag> | |
| 183 | + </td> | |
| 184 | + <td className="mono">{mp.observed_count}</td> | |
| 185 | + </tr> | |
| 186 | + ))} | |
| 187 | + </Table> | |
| 188 | + ) : ( | |
| 189 | + <Empty>none yet</Empty> | |
| 190 | + )} | |
| 191 | + </div> | |
| 192 | + </Panel> | |
| 193 | + </div> | |
| 194 | + </> | |
| 195 | + )} | |
| 196 | + </div> | |
| 197 | + ); | |
| 198 | +} | |
added
apps/dashboard/src/app/platforms/page.tsx
+82 −0
@@ -0,0 +1,82 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import Link from "next/link"; | |
| 4 | +import { useApi } from "@/lib/api"; | |
| 5 | +import { Bar, Panel, Skeleton, Tag } from "@/components/ui"; | |
| 6 | + | |
| 7 | +interface P { | |
| 8 | + platform: string; | |
| 9 | + learned: boolean; | |
| 10 | + adapter: boolean; | |
| 11 | + confidence: number; | |
| 12 | + page_types: number; | |
| 13 | + entity_types: number; | |
| 14 | + navigation_actions: number; | |
| 15 | + network_schemas: number; | |
| 16 | + media_patterns: number; | |
| 17 | + sessions: number; | |
| 18 | + has_manifest: boolean; | |
| 19 | + profiles: { alias: string; has_state: boolean }[]; | |
| 20 | +} | |
| 21 | + | |
| 22 | +const PHASE: Record<string, string> = { youtube: "Phase 1", reddit: "Phase 1", facebook: "Phase 2", instagram: "Phase 2", tiktok: "Phase 3", x: "Phase 3", linkedin: "Phase 4", threads: "Phase 4" }; | |
| 23 | + | |
| 24 | +export default function Platforms() { | |
| 25 | + const { data, loading } = useApi<P[]>("/platforms", { refreshMs: 15_000 }); | |
| 26 | + return ( | |
| 27 | + <div className="space-y-4"> | |
| 28 | + <div> | |
| 29 | + <h1 className="text-2xl font-semibold tracking-tight">Connectors</h1> | |
| 30 | + <p className="text-sm text-fg-2 max-w-2xl">A connector is learned state, not hand-written truth: page types, response schemas, action → observation patterns and media patterns accumulated across sessions, compiled into a manifest once confidence is sufficient.</p> | |
| 31 | + </div> | |
| 32 | + {loading ? ( | |
| 33 | + <Skeleton rows={6} /> | |
| 34 | + ) : ( | |
| 35 | + <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4"> | |
| 36 | + {data?.map((p) => ( | |
| 37 | + <Link key={p.platform} href={`/platforms/${p.platform}`} className="block"> | |
| 38 | + <Panel className="h-full hover:border-cyan/40 transition"> | |
| 39 | + <div className="flex items-center gap-2"> | |
| 40 | + <span className="text-lg font-semibold capitalize">{p.platform}</span> | |
| 41 | + <span className="ml-auto text-[10px] uppercase tracking-widest text-dim">{PHASE[p.platform]}</span> | |
| 42 | + </div> | |
| 43 | + <div className="flex gap-1 mt-2 flex-wrap"> | |
| 44 | + <Tag color={p.adapter ? "#43e69a" : "#6b7a8c"}>{p.adapter ? "adapter" : "no adapter"}</Tag> | |
| 45 | + <Tag color={p.learned ? "#38e1ff" : "#6b7a8c"}>{p.learned ? "learned" : "unlearned"}</Tag> | |
| 46 | + {p.has_manifest && <Tag color="#9d7bff">compiled</Tag>} | |
| 47 | + {p.profiles.map((pr) => ( | |
| 48 | + <Tag key={pr.alias} color={pr.has_state ? "#ffb347" : "#6b7a8c"}> | |
| 49 | + profile {pr.alias} | |
| 50 | + </Tag> | |
| 51 | + ))} | |
| 52 | + </div> | |
| 53 | + <div className="mt-4"> | |
| 54 | + <div className="flex justify-between text-xs text-dim mb-1"> | |
| 55 | + <span>connector confidence</span> | |
| 56 | + <span className="mono text-fg">{p.confidence}%</span> | |
| 57 | + </div> | |
| 58 | + <Bar value={p.confidence / 100} color={p.confidence >= 70 ? "#43e69a" : p.confidence >= 40 ? "#38e1ff" : "#ffb347"} /> | |
| 59 | + </div> | |
| 60 | + <dl className="grid grid-cols-2 gap-x-3 gap-y-1 mt-4 text-xs"> | |
| 61 | + {[ | |
| 62 | + ["page types", p.page_types], | |
| 63 | + ["entity types", p.entity_types], | |
| 64 | + ["navigation actions", p.navigation_actions], | |
| 65 | + ["network schemas", p.network_schemas], | |
| 66 | + ["media patterns", p.media_patterns], | |
| 67 | + ["sessions", p.sessions], | |
| 68 | + ].map(([k, v]) => ( | |
| 69 | + <div key={k as string} className="flex justify-between border-b border-line/60 py-1"> | |
| 70 | + <dt className="text-dim">{k as string}</dt> | |
| 71 | + <dd className="mono">{v as number}</dd> | |
| 72 | + </div> | |
| 73 | + ))} | |
| 74 | + </dl> | |
| 75 | + </Panel> | |
| 76 | + </Link> | |
| 77 | + ))} | |
| 78 | + </div> | |
| 79 | + )} | |
| 80 | + </div> | |
| 81 | + ); | |
| 82 | +} | |
added
apps/dashboard/src/app/schemas/page.tsx
+97 −0
@@ -0,0 +1,97 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useState } from "react"; | |
| 4 | +import { useApi } from "@/lib/api"; | |
| 5 | +import { fmtDate } from "@/lib/format"; | |
| 6 | +import { Empty, Panel, Skeleton, Table, Tag } from "@/components/ui"; | |
| 7 | + | |
| 8 | +interface Schema { | |
| 9 | + platform: string; | |
| 10 | + shape_hash: string; | |
| 11 | + fingerprint: { hostname: string; path_pattern: string; method: string; graphql_operation?: string; observed_entity_types?: string[] }; | |
| 12 | + schema: { candidate_entity_types: { type: string; confidence: number; path: string }[]; fields: { path: string; semantic?: { kind: string; confidence: number }; example?: string; types?: string[] }[]; object_count: number; repeated_object_paths: string[] }; | |
| 13 | + observed_count: number; | |
| 14 | + sample_url: string; | |
| 15 | + first_seen: string; | |
| 16 | + last_seen: string; | |
| 17 | +} | |
| 18 | + | |
| 19 | +export default function Schemas() { | |
| 20 | + const [platform, setPlatform] = useState(""); | |
| 21 | + const { data, loading } = useApi<Schema[]>(`/schemas${platform ? `?platform=${platform}` : ""}`, { refreshMs: 15_000 }); | |
| 22 | + const platforms = [...new Set((data ?? []).map((s) => s.platform))]; | |
| 23 | + return ( | |
| 24 | + <div className="space-y-4"> | |
| 25 | + <div className="flex flex-wrap items-end gap-3"> | |
| 26 | + <div> | |
| 27 | + <h1 className="text-2xl font-semibold tracking-tight">Runtime APIs</h1> | |
| 28 | + <p className="text-sm text-fg-2 max-w-2xl">Response shapes the browser received while a human-like session unfolded. Endpoints are fingerprinted by (host, path pattern, method, structure hash) and their fields are semantically inferred — nothing here was written by hand.</p> | |
| 29 | + </div> | |
| 30 | + <select value={platform} onChange={(e) => setPlatform(e.target.value)} className="ml-auto rounded-lg bg-bg-2 border border-line-2 px-2 py-1.5 text-sm"> | |
| 31 | + <option value="">all platforms</option> | |
| 32 | + {["youtube", "reddit", ...platforms.filter((p) => !["youtube", "reddit"].includes(p))].map((p) => ( | |
| 33 | + <option key={p} value={p}> | |
| 34 | + {p} | |
| 35 | + </option> | |
| 36 | + ))} | |
| 37 | + </select> | |
| 38 | + </div> | |
| 39 | + {loading ? ( | |
| 40 | + <Skeleton rows={8} /> | |
| 41 | + ) : data?.length ? ( | |
| 42 | + <div className="space-y-3"> | |
| 43 | + {data.map((sc) => ( | |
| 44 | + <details key={sc.platform + sc.shape_hash} className="panel open:glow-cyan/30"> | |
| 45 | + <summary className="cursor-pointer px-4 py-3 flex flex-wrap items-center gap-2 text-sm"> | |
| 46 | + <Tag>{sc.platform}</Tag> | |
| 47 | + <Tag color="#9d7bff">{sc.fingerprint.method}</Tag> | |
| 48 | + <span className="mono"> | |
| 49 | + {sc.fingerprint.hostname} | |
| 50 | + <span className="text-fg-2">{sc.fingerprint.path_pattern}</span> | |
| 51 | + </span> | |
| 52 | + {sc.fingerprint.graphql_operation && <Tag color="#ff7ad9">{sc.fingerprint.graphql_operation}</Tag>} | |
| 53 | + <span className="ml-auto flex gap-1 flex-wrap"> | |
| 54 | + {sc.schema.candidate_entity_types.slice(0, 5).map((c) => ( | |
| 55 | + <Tag key={c.type + c.path} color="#38e1ff" title={c.path}> | |
| 56 | + {c.type} {Number(c.confidence).toFixed(2)} | |
| 57 | + </Tag> | |
| 58 | + ))} | |
| 59 | + </span> | |
| 60 | + <span className="mono text-xs text-dim whitespace-nowrap"> | |
| 61 | + seen {sc.observed_count}× · {sc.schema.object_count} objects · #{sc.shape_hash.slice(0, 8)} | |
| 62 | + </span> | |
| 63 | + </summary> | |
| 64 | + <div className="px-4 pb-4 space-y-3"> | |
| 65 | + <div className="text-[11px] text-dim mono break-all"> | |
| 66 | + sample {sc.sample_url} · first {fmtDate(sc.first_seen)} · last {fmtDate(sc.last_seen)} | |
| 67 | + </div> | |
| 68 | + {sc.schema.repeated_object_paths?.length ? ( | |
| 69 | + <div className="text-xs text-fg-2"> | |
| 70 | + repeated object arrays (entity lists): <span className="mono">{sc.schema.repeated_object_paths.slice(0, 5).join(" · ")}</span> | |
| 71 | + </div> | |
| 72 | + ) : null} | |
| 73 | + <Table head={["field path", "inferred semantic", "conf", "types", "example"]} dense> | |
| 74 | + {sc.schema.fields.slice(0, 80).map((f) => ( | |
| 75 | + <tr key={f.path}> | |
| 76 | + <td className="mono text-[11px] text-fg-2 max-w-[560px] truncate" title={f.path}> | |
| 77 | + {f.path} | |
| 78 | + </td> | |
| 79 | + <td>{f.semantic ? <Tag color={f.semantic.kind === "identifier" ? "#ffb347" : f.semantic.kind.includes("url") ? "#ff7ad9" : "#43e69a"}>{f.semantic.kind}</Tag> : "—"}</td> | |
| 80 | + <td className="mono text-[11px]">{f.semantic ? Number(f.semantic.confidence).toFixed(2) : ""}</td> | |
| 81 | + <td className="mono text-[11px] text-dim">{f.types?.join("|")}</td> | |
| 82 | + <td className="mono text-[11px] text-dim max-w-[320px] truncate">{f.example}</td> | |
| 83 | + </tr> | |
| 84 | + ))} | |
| 85 | + </Table> | |
| 86 | + </div> | |
| 87 | + </details> | |
| 88 | + ))} | |
| 89 | + </div> | |
| 90 | + ) : ( | |
| 91 | + <Panel> | |
| 92 | + <Empty>No schemas discovered yet.</Empty> | |
| 93 | + </Panel> | |
| 94 | + )} | |
| 95 | + </div> | |
| 96 | + ); | |
| 97 | +} | |
added
apps/dashboard/src/app/sessions/[id]/page.tsx
+405 −0
@@ -0,0 +1,405 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { use, useMemo, useState } from "react"; | |
| 4 | +import Link from "next/link"; | |
| 5 | +import clsx from "clsx"; | |
| 6 | +import { ExternalLink } from "lucide-react"; | |
| 7 | +import { useApi } from "@/lib/api"; | |
| 8 | +import { ago, eventColor, fmtDate, fmtDuration, fmtTime, truncate } from "@/lib/format"; | |
| 9 | +import { Bar, Empty, Metric, Panel, Skeleton, StatusPill, Tag, Table } from "@/components/ui"; | |
| 10 | +import { GainChart } from "@/components/charts"; | |
| 11 | +import { LiveFeed } from "@/components/LiveFeed"; | |
| 12 | +import { WorldGraph, type GraphEdge, type GraphNode } from "@/components/WorldGraph"; | |
| 13 | +import { EntityTable, type EntityRow } from "@/components/EntityTable"; | |
| 14 | + | |
| 15 | +interface Session { | |
| 16 | + session_id: string; | |
| 17 | + platform: string; | |
| 18 | + account_alias: string; | |
| 19 | + mode: string | null; | |
| 20 | + goal: string | null; | |
| 21 | + started_at: string; | |
| 22 | + ended_at: string | null; | |
| 23 | + health: string; | |
| 24 | + job: { budget?: Record<string, number>; query?: string; media_level?: number } | null; | |
| 25 | + summary: { steps: number; entities: number; videos: number; network_responses: number; schemas: number; patterns_learned: number; ended_because: string; world: { nodes: number; edges: number; visited: number; multi_surface: number; by_type: Record<string, number> }; connector: { confidence: number; page_types: number; network_schemas: number } } | null; | |
| 26 | + event_counts: { event_type: string; n: number }[]; | |
| 27 | +} | |
| 28 | +interface Action { | |
| 29 | + step: number; | |
| 30 | + action_type: string; | |
| 31 | + label: string; | |
| 32 | + target_url: string | null; | |
| 33 | + planner: string; | |
| 34 | + expected_gain: number; | |
| 35 | + novelty: number; | |
| 36 | + relevance: number; | |
| 37 | + reason: string; | |
| 38 | + scores: { action_id: string; information_gain: number; penalties: string[] }[]; | |
| 39 | + before_state: { url: string; page_type: string; visible_entities: number } | null; | |
| 40 | + after_state: { url: string; page_type: string; visible_entities: number; new_entities: number; network_responses: number; dom_nodes_added: number } | null; | |
| 41 | + success: boolean; | |
| 42 | + error: string | null; | |
| 43 | + duration_ms: number; | |
| 44 | + ts: string; | |
| 45 | +} | |
| 46 | +interface PageRow { | |
| 47 | + step: number; | |
| 48 | + ts: string; | |
| 49 | + payload: { url: string; title: string; page_type: string; confidence: number; signals: string[]; entities: number; dom_entities: number; network_entities: number; both_surfaces: number; field_agreements: number; field_conflicts: { field: string; network: unknown; dom: unknown }[]; videos: number; summary: string }; | |
| 50 | +} | |
| 51 | +interface Media { | |
| 52 | + fingerprint: string; | |
| 53 | + media_type: string; | |
| 54 | + platform_media_id: string | null; | |
| 55 | + title: string | null; | |
| 56 | + author: string | null; | |
| 57 | + duration_s: number | null; | |
| 58 | + width: number | null; | |
| 59 | + height: number | null; | |
| 60 | + thumbnail_url: string | null; | |
| 61 | + page_url: string | null; | |
| 62 | + delivery: { kind: string; hostnames: string[]; manifest_url?: string } | null; | |
| 63 | + frames: string[] | null; | |
| 64 | +} | |
| 65 | +interface Schema { | |
| 66 | + shape_hash: string; | |
| 67 | + fingerprint: { hostname: string; path_pattern: string; method: string; graphql_operation?: string }; | |
| 68 | + schema: { candidate_entity_types: { type: string; confidence: number; path: string }[]; fields: { path: string; semantic?: { kind: string; confidence: number }; example?: string }[]; object_count: number }; | |
| 69 | + observed_count: number; | |
| 70 | + sample_url: string; | |
| 71 | +} | |
| 72 | + | |
| 73 | +const TABS = ["decisions", "pages", "entities", "runtime apis", "media", "world model", "events"] as const; | |
| 74 | + | |
| 75 | +export default function SessionConsole({ params }: { params: Promise<{ id: string }> }) { | |
| 76 | + const { id } = use(params); | |
| 77 | + const [tab, setTab] = useState<(typeof TABS)[number]>("decisions"); | |
| 78 | + const s = useApi<Session>(`/sessions/${id}`, { refreshMs: 10_000 }); | |
| 79 | + const live = !s.data?.ended_at; | |
| 80 | + const actions = useApi<Action[]>(`/sessions/${id}/actions`, { refreshMs: live ? 5000 : undefined }); | |
| 81 | + const pages = useApi<PageRow[]>(`/sessions/${id}/pages`, { refreshMs: live ? 5000 : undefined }); | |
| 82 | + const entities = useApi<EntityRow[]>(tab === "entities" ? `/sessions/${id}/entities?limit=500` : null, { refreshMs: live ? 8000 : undefined }); | |
| 83 | + const media = useApi<Media[]>(tab === "media" ? `/sessions/${id}/media` : null); | |
| 84 | + const schemas = useApi<Schema[]>(tab === "runtime apis" ? `/sessions/${id}/schemas` : null); | |
| 85 | + const world = useApi<{ nodes: GraphNode[]; edges: GraphEdge[] }>(tab === "world model" ? `/sessions/${id}/world` : null, { refreshMs: live ? 10_000 : undefined }); | |
| 86 | + const [selStep, setSelStep] = useState<number | null>(null); | |
| 87 | + | |
| 88 | + const gainSeries = useMemo(() => (actions.data ?? []).map((a) => ({ step: a.step, expected_gain: Number(a.expected_gain), novelty: Number(a.novelty), relevance: Number(a.relevance), action_type: a.action_type, new_entities: a.after_state?.new_entities })), [actions.data]); | |
| 89 | + const sess = s.data; | |
| 90 | + const sum = sess?.summary; | |
| 91 | + const lastPage = pages.data?.[pages.data.length - 1]; | |
| 92 | + const selPage = selStep !== null ? pages.data?.find((p) => p.step === selStep) : lastPage; | |
| 93 | + | |
| 94 | + return ( | |
| 95 | + <div className="space-y-4"> | |
| 96 | + <div className="flex flex-wrap items-start gap-3"> | |
| 97 | + <div className="min-w-0"> | |
| 98 | + <div className="flex items-center gap-2 text-xs text-dim mono"> | |
| 99 | + <Link href="/sessions" className="hover:text-cyan">sessions</Link> / {id} | |
| 100 | + </div> | |
| 101 | + <h1 className="text-xl font-semibold tracking-tight mt-1 truncate max-w-3xl">{sess?.goal ?? "…"}</h1> | |
| 102 | + <div className="flex flex-wrap items-center gap-2 mt-2 text-xs"> | |
| 103 | + {sess && <StatusPill status={sess.ended_at ? (sess.health === "auth_required" ? "auth_required" : "done") : sess.health} />} | |
| 104 | + {sess && <Tag>{sess.platform}</Tag>} | |
| 105 | + {sess?.mode && <Tag color="#ffb347">{sess.mode}</Tag>} | |
| 106 | + {sess?.job?.query && <Tag color="#9d7bff">“{sess.job.query}”</Tag>} | |
| 107 | + {sess && <span className="text-dim">started {fmtDate(sess.started_at)} · {sess.ended_at ? `ended ${ago(sess.ended_at)}` : "running"}</span>} | |
| 108 | + {sum && <span className="text-dim">· ended because <span className="text-fg-2">{sum.ended_because}</span></span>} | |
| 109 | + </div> | |
| 110 | + </div> | |
| 111 | + <div className="ml-auto grid grid-cols-3 sm:grid-cols-6 gap-2 text-center"> | |
| 112 | + {[ | |
| 113 | + ["steps", actions.data?.length ?? sum?.steps], | |
| 114 | + ["entities", sum?.entities ?? sess?.event_counts.filter((c) => /DISCOVERED/.test(c.event_type)).reduce((a, c) => a + c.n, 0)], | |
| 115 | + ["videos", sum?.videos], | |
| 116 | + ["responses", sum?.network_responses ?? sess?.event_counts.find((c) => c.event_type === "NETWORK_RESPONSE_OBSERVED")?.n], | |
| 117 | + ["schemas", sum?.schemas ?? sess?.event_counts.find((c) => c.event_type === "NETWORK_SCHEMA_DISCOVERED")?.n], | |
| 118 | + ["learned", sum?.patterns_learned ?? sess?.event_counts.find((c) => c.event_type === "CONNECTOR_PATTERN_LEARNED")?.n], | |
| 119 | + ].map(([k, v]) => ( | |
| 120 | + <div key={k as string} className="panel px-3 py-2"> | |
| 121 | + <div className="mono text-lg text-cyan">{v ?? "—"}</div> | |
| 122 | + <div className="text-[10px] uppercase tracking-widest text-dim">{k as string}</div> | |
| 123 | + </div> | |
| 124 | + ))} | |
| 125 | + </div> | |
| 126 | + </div> | |
| 127 | + | |
| 128 | + <div className="flex gap-1 border-b border-line text-sm"> | |
| 129 | + {TABS.map((t) => ( | |
| 130 | + <button key={t} onClick={() => setTab(t)} className={clsx("px-3 py-2 -mb-px border-b-2 capitalize transition", tab === t ? "border-cyan text-cyan" : "border-transparent text-fg-2 hover:text-fg")}> | |
| 131 | + {t} | |
| 132 | + </button> | |
| 133 | + ))} | |
| 134 | + </div> | |
| 135 | + | |
| 136 | + {tab === "decisions" && ( | |
| 137 | + <div className="grid grid-cols-1 xl:grid-cols-3 gap-4"> | |
| 138 | + <div className="xl:col-span-2 space-y-4"> | |
| 139 | + <Panel title="Information gain per step" sub="what the agent expected of each move"> | |
| 140 | + {gainSeries.length ? <GainChart data={gainSeries} /> : <Empty>no decisions yet</Empty>} | |
| 141 | + </Panel> | |
| 142 | + <Panel title="Agent decisions" sub="why did the crawler do that? — click a step to see its page state" pad={false}> | |
| 143 | + {actions.loading ? ( | |
| 144 | + <div className="p-4"> | |
| 145 | + <Skeleton rows={6} /> | |
| 146 | + </div> | |
| 147 | + ) : ( | |
| 148 | + <ul className="divide-y divide-line/60 max-h-[720px] overflow-auto scrollbar-thin"> | |
| 149 | + {(actions.data ?? []) | |
| 150 | + .slice() | |
| 151 | + .reverse() | |
| 152 | + .map((a) => ( | |
| 153 | + <li key={a.step} onClick={() => setSelStep(a.step)} className={clsx("px-4 py-2.5 cursor-pointer hover:bg-white/[0.03]", selStep === a.step && "bg-cyan/5")}> | |
| 154 | + <div className="flex items-center gap-2 text-sm"> | |
| 155 | + <span className="mono text-dim w-8">#{a.step}</span> | |
| 156 | + <Tag color={a.success ? "#43e69a" : "#ff5c7a"}>{a.action_type.replace(/_/g, " ").toLowerCase()}</Tag> | |
| 157 | + <span className="truncate text-fg">{truncate(a.label.replace(/^Open \w+ E\d+: /, ""), 80)}</span> | |
| 158 | + <span className="ml-auto mono text-xs text-amber whitespace-nowrap">gain {Number(a.expected_gain).toFixed(3)}</span> | |
| 159 | + </div> | |
| 160 | + <div className="flex flex-wrap gap-x-4 text-[11px] text-dim mono mt-1 ml-10"> | |
| 161 | + <span>nov {Number(a.novelty).toFixed(2)}</span> | |
| 162 | + <span>rel {Number(a.relevance).toFixed(2)}</span> | |
| 163 | + <span>{a.planner}</span> | |
| 164 | + {a.after_state && ( | |
| 165 | + <span> | |
| 166 | + → {a.after_state.page_type} · +{a.after_state.new_entities} ent · {a.after_state.network_responses} resp · {a.duration_ms} ms | |
| 167 | + </span> | |
| 168 | + )} | |
| 169 | + {a.error && <span className="text-red">{truncate(a.error, 60)}</span>} | |
| 170 | + </div> | |
| 171 | + <div className="text-xs text-fg-2 mt-1 ml-10">{a.reason}</div> | |
| 172 | + </li> | |
| 173 | + ))} | |
| 174 | + </ul> | |
| 175 | + )} | |
| 176 | + </Panel> | |
| 177 | + </div> | |
| 178 | + <div className="space-y-4"> | |
| 179 | + <Panel title={selStep !== null ? `Page state after step ${selStep}` : "Current page state"} sub="semantic DOM representation given to the planner"> | |
| 180 | + {selPage ? ( | |
| 181 | + <div className="space-y-2"> | |
| 182 | + <div className="flex items-center gap-2 text-xs"> | |
| 183 | + <Tag color="#38e1ff">{selPage.payload.page_type}</Tag> | |
| 184 | + <span className="mono text-dim">conf {Number(selPage.payload.confidence).toFixed(2)}</span> | |
| 185 | + <a href={selPage.payload.url} target="_blank" rel="noreferrer" className="ml-auto text-dim hover:text-cyan"> | |
| 186 | + <ExternalLink className="h-3.5 w-3.5" /> | |
| 187 | + </a> | |
| 188 | + </div> | |
| 189 | + <div className="text-[11px] text-dim break-all">{selPage.payload.url}</div> | |
| 190 | + <div className="grid grid-cols-4 gap-2 text-center text-[10.5px] mono"> | |
| 191 | + {[ | |
| 192 | + ["entities", selPage.payload.entities], | |
| 193 | + ["dom", selPage.payload.dom_entities], | |
| 194 | + ["network", selPage.payload.network_entities], | |
| 195 | + ["both", selPage.payload.both_surfaces], | |
| 196 | + ].map(([k, v]) => ( | |
| 197 | + <div key={k as string} className="rounded bg-bg-2 py-1"> | |
| 198 | + <div className="text-fg">{v as number}</div> | |
| 199 | + <div className="text-dim">{k as string}</div> | |
| 200 | + </div> | |
| 201 | + ))} | |
| 202 | + </div> | |
| 203 | + {selPage.payload.field_conflicts?.length > 0 && ( | |
| 204 | + <div className="text-[11px] text-amber"> | |
| 205 | + {selPage.payload.field_conflicts.length} field conflict(s): {selPage.payload.field_conflicts.map((c) => c.field).join(", ")} | |
| 206 | + </div> | |
| 207 | + )} | |
| 208 | + <pre className="mono text-[11px] leading-5 text-fg-2 whitespace-pre-wrap max-h-[520px] overflow-auto scrollbar-thin bg-bg-2 rounded p-3">{selPage.payload.summary}</pre> | |
| 209 | + </div> | |
| 210 | + ) : ( | |
| 211 | + <Empty>no page state yet</Empty> | |
| 212 | + )} | |
| 213 | + </Panel> | |
| 214 | + <Panel title="Budget"> | |
| 215 | + {sess?.job?.budget ? ( | |
| 216 | + <div> | |
| 217 | + <Metric label="max minutes" value={sess.job.budget.max_minutes} /> | |
| 218 | + <Metric label="max actions" value={sess.job.budget.max_actions} /> | |
| 219 | + <Metric label="max profiles opened" value={sess.job.budget.max_profiles} /> | |
| 220 | + <Metric label="max videos opened" value={sess.job.budget.max_videos} /> | |
| 221 | + <Metric label="media level" value={sess.job.media_level ?? 1} /> | |
| 222 | + <div className="mt-3 text-[10.5px] text-dim">steps used</div> | |
| 223 | + <Bar value={(actions.data?.length ?? 0) / Math.max(1, sess.job.budget.max_actions)} color="#ffb347" /> | |
| 224 | + </div> | |
| 225 | + ) : ( | |
| 226 | + <Skeleton rows={3} /> | |
| 227 | + )} | |
| 228 | + </Panel> | |
| 229 | + </div> | |
| 230 | + </div> | |
| 231 | + )} | |
| 232 | + | |
| 233 | + {tab === "pages" && ( | |
| 234 | + <Panel title="Navigation path" sub="each page the browser reached, classified" pad={false}> | |
| 235 | + <div className="p-4"> | |
| 236 | + {pages.data?.length ? ( | |
| 237 | + <Table head={["step", "time", "page type", "conf", "url", "entities (dom / net / both)", "agreements", "videos"]} dense> | |
| 238 | + {pages.data.map((p) => ( | |
| 239 | + <tr key={`${p.step}-${p.ts}`} onClick={() => setSelStep(p.step)} className="cursor-pointer"> | |
| 240 | + <td className="mono">#{p.step}</td> | |
| 241 | + <td className="mono text-dim">{fmtTime(p.ts)}</td> | |
| 242 | + <td> | |
| 243 | + <Tag color="#38e1ff">{p.payload.page_type}</Tag> | |
| 244 | + </td> | |
| 245 | + <td className="mono">{Number(p.payload.confidence).toFixed(2)}</td> | |
| 246 | + <td className="max-w-[420px] truncate text-fg-2"> | |
| 247 | + <a href={p.payload.url} target="_blank" rel="noreferrer" className="hover:text-cyan"> | |
| 248 | + {truncate(p.payload.url, 80)} | |
| 249 | + </a> | |
| 250 | + </td> | |
| 251 | + <td className="mono"> | |
| 252 | + {p.payload.entities} <span className="text-dim">({p.payload.dom_entities} / {p.payload.network_entities} / {p.payload.both_surfaces})</span> | |
| 253 | + </td> | |
| 254 | + <td className="mono"> | |
| 255 | + {p.payload.field_agreements} | |
| 256 | + {p.payload.field_conflicts?.length ? <span className="text-amber"> / {p.payload.field_conflicts.length}✗</span> : ""} | |
| 257 | + </td> | |
| 258 | + <td className="mono">{p.payload.videos}</td> | |
| 259 | + </tr> | |
| 260 | + ))} | |
| 261 | + </Table> | |
| 262 | + ) : ( | |
| 263 | + <Empty>no pages yet</Empty> | |
| 264 | + )} | |
| 265 | + </div> | |
| 266 | + </Panel> | |
| 267 | + )} | |
| 268 | + | |
| 269 | + {tab === "entities" && ( | |
| 270 | + <Panel title="Entities observed in this session" sub="every field keeps its provenance (network / dom) and confidence" pad={false}> | |
| 271 | + <div className="p-4">{entities.loading ? <Skeleton rows={8} /> : entities.data?.length ? <EntityTable rows={entities.data} /> : <Empty>no entities yet</Empty>}</div> | |
| 272 | + </Panel> | |
| 273 | + )} | |
| 274 | + | |
| 275 | + {tab === "runtime apis" && ( | |
| 276 | + <Panel title="Runtime API discovery" sub="response shapes fingerprinted on this platform — no endpoint was hardcoded" pad={false}> | |
| 277 | + <div className="p-4 space-y-3"> | |
| 278 | + {schemas.loading ? ( | |
| 279 | + <Skeleton rows={6} /> | |
| 280 | + ) : ( | |
| 281 | + (schemas.data ?? []).map((sc) => ( | |
| 282 | + <details key={sc.shape_hash} className="rounded-lg border border-line bg-bg-2/40 open:bg-bg-2/70"> | |
| 283 | + <summary className="cursor-pointer px-3 py-2 flex flex-wrap items-center gap-2 text-sm"> | |
| 284 | + <Tag color="#9d7bff">{sc.fingerprint.method}</Tag> | |
| 285 | + <span className="mono text-fg"> | |
| 286 | + {sc.fingerprint.hostname} | |
| 287 | + <span className="text-fg-2">{sc.fingerprint.path_pattern}</span> | |
| 288 | + </span> | |
| 289 | + {sc.fingerprint.graphql_operation && <Tag color="#ff7ad9">{sc.fingerprint.graphql_operation}</Tag>} | |
| 290 | + <span className="ml-auto flex gap-1"> | |
| 291 | + {sc.schema.candidate_entity_types.slice(0, 4).map((c) => ( | |
| 292 | + <Tag key={c.type + c.path} color="#38e1ff"> | |
| 293 | + {c.type} {Number(c.confidence).toFixed(2)} | |
| 294 | + </Tag> | |
| 295 | + ))} | |
| 296 | + </span> | |
| 297 | + <span className="mono text-xs text-dim">seen {sc.observed_count}× · {sc.schema.object_count} objects · #{sc.shape_hash.slice(0, 8)}</span> | |
| 298 | + </summary> | |
| 299 | + <div className="px-3 pb-3"> | |
| 300 | + <div className="text-[11px] text-dim mono mb-2 break-all">sample: {sc.sample_url}</div> | |
| 301 | + <Table head={["field path", "inferred semantic", "conf", "example"]} dense> | |
| 302 | + {sc.schema.fields.slice(0, 60).map((f) => ( | |
| 303 | + <tr key={f.path}> | |
| 304 | + <td className="mono text-[11px] text-fg-2 max-w-[520px] truncate">{f.path}</td> | |
| 305 | + <td>{f.semantic ? <Tag color="#43e69a">{f.semantic.kind}</Tag> : "—"}</td> | |
| 306 | + <td className="mono text-[11px]">{f.semantic ? Number(f.semantic.confidence).toFixed(2) : ""}</td> | |
| 307 | + <td className="mono text-[11px] text-dim max-w-[300px] truncate">{f.example}</td> | |
| 308 | + </tr> | |
| 309 | + ))} | |
| 310 | + </Table> | |
| 311 | + </div> | |
| 312 | + </details> | |
| 313 | + )) | |
| 314 | + )} | |
| 315 | + </div> | |
| 316 | + </Panel> | |
| 317 | + )} | |
| 318 | + | |
| 319 | + {tab === "media" && ( | |
| 320 | + <Panel title="Media" sub="videos detected on DOM, network and entity surfaces" pad={false}> | |
| 321 | + <div className="p-4 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3"> | |
| 322 | + {media.loading ? ( | |
| 323 | + <Skeleton rows={6} /> | |
| 324 | + ) : media.data?.length ? ( | |
| 325 | + media.data.map((m) => ( | |
| 326 | + <div key={m.fingerprint} className="rounded-lg border border-line bg-bg-2/40 overflow-hidden"> | |
| 327 | + {m.frames?.length ? ( | |
| 328 | + <div className="grid grid-cols-5 gap-px bg-line"> | |
| 329 | + {m.frames.map((f) => ( | |
| 330 | + // eslint-disable-next-line @next/next/no-img-element | |
| 331 | + <img key={f} src={`/media/${f.split("/media/")[1] ?? ""}`} alt="" className="aspect-video object-cover w-full" /> | |
| 332 | + ))} | |
| 333 | + </div> | |
| 334 | + ) : m.thumbnail_url ? ( | |
| 335 | + // eslint-disable-next-line @next/next/no-img-element | |
| 336 | + <img src={m.thumbnail_url} alt="" className="aspect-video object-cover w-full" /> | |
| 337 | + ) : ( | |
| 338 | + <div className="aspect-video grid place-items-center text-dim text-xs">no frame</div> | |
| 339 | + )} | |
| 340 | + <div className="p-3 space-y-1"> | |
| 341 | + <div className="text-sm truncate">{m.title ?? m.platform_media_id}</div> | |
| 342 | + <div className="text-[11px] text-dim mono flex flex-wrap gap-x-3"> | |
| 343 | + <span>{m.author ?? ""}</span> | |
| 344 | + <span>{fmtDuration(m.duration_s)}</span> | |
| 345 | + {m.width ? <span>{m.width}×{m.height}</span> : null} | |
| 346 | + {m.delivery?.kind && <span>{m.delivery.kind}</span>} | |
| 347 | + {m.delivery?.hostnames?.length ? <span>{m.delivery.hostnames.slice(0, 2).join(", ")}</span> : null} | |
| 348 | + </div> | |
| 349 | + <Link href={`/entities/${encodeURIComponent(m.fingerprint)}`} className="text-[11px] text-cyan">evidence →</Link> | |
| 350 | + </div> | |
| 351 | + </div> | |
| 352 | + )) | |
| 353 | + ) : ( | |
| 354 | + <Empty>no media yet</Empty> | |
| 355 | + )} | |
| 356 | + </div> | |
| 357 | + </Panel> | |
| 358 | + )} | |
| 359 | + | |
| 360 | + {tab === "world model" && ( | |
| 361 | + <Panel title="Social world model" sub="graph of entities and relationships discovered in this session — orange ring = visited by the crawler" pad={false}> | |
| 362 | + <div className="p-2">{world.data ? <WorldGraph nodes={world.data.nodes} edges={world.data.edges} height={620} /> : <Skeleton rows={8} />}</div> | |
| 363 | + </Panel> | |
| 364 | + )} | |
| 365 | + | |
| 366 | + {tab === "events" && ( | |
| 367 | + <div className="grid grid-cols-1 xl:grid-cols-4 gap-4"> | |
| 368 | + <Panel title="Event mix"> | |
| 369 | + <ul className="space-y-1 text-xs mono"> | |
| 370 | + {(sess?.event_counts ?? []) | |
| 371 | + .slice() | |
| 372 | + .sort((a, b) => b.n - a.n) | |
| 373 | + .map((c) => ( | |
| 374 | + <li key={c.event_type} className="flex justify-between gap-2"> | |
| 375 | + <Tag color={eventColor(c.event_type)}>{c.event_type.toLowerCase()}</Tag> | |
| 376 | + <span>{c.n}</span> | |
| 377 | + </li> | |
| 378 | + ))} | |
| 379 | + </ul> | |
| 380 | + </Panel> | |
| 381 | + <Panel title="Raw observation stream" sub={live ? "live" : "session ended — showing the tail"} className="xl:col-span-3" pad={false}> | |
| 382 | + <div className="p-2">{live ? <LiveFeed session={id} max={200} height={640} /> : <EventsTail id={id} />}</div> | |
| 383 | + </Panel> | |
| 384 | + </div> | |
| 385 | + )} | |
| 386 | + </div> | |
| 387 | + ); | |
| 388 | +} | |
| 389 | + | |
| 390 | +function EventsTail({ id }: { id: string }) { | |
| 391 | + const { data } = useApi<{ event_id: string; event_type: string; step: number; ts: string; payload: Record<string, unknown> }[]>(`/sessions/${id}/events?limit=400`); | |
| 392 | + if (!data) return <Skeleton rows={8} />; | |
| 393 | + return ( | |
| 394 | + <ul className="mono text-[11.5px] space-y-0.5 max-h-[640px] overflow-auto scrollbar-thin"> | |
| 395 | + {data.map((e) => ( | |
| 396 | + <li key={e.event_id} className="flex gap-2 leading-5 px-1"> | |
| 397 | + <span className="text-dim">{fmtTime(e.ts)}</span> | |
| 398 | + <span className="text-dim w-7 text-right">#{e.step ?? 0}</span> | |
| 399 | + <Tag color={eventColor(e.event_type)}>{e.event_type.replace(/_/g, " ").toLowerCase()}</Tag> | |
| 400 | + <span className="text-fg-2 truncate">{truncate(JSON.stringify(e.payload), 160)}</span> | |
| 401 | + </li> | |
| 402 | + ))} | |
| 403 | + </ul> | |
| 404 | + ); | |
| 405 | +} | |
added
apps/dashboard/src/app/sessions/page.tsx
+69 −0
@@ -0,0 +1,69 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import Link from "next/link"; | |
| 4 | +import { useApi } from "@/lib/api"; | |
| 5 | +import { ago, fmtDate, truncate } from "@/lib/format"; | |
| 6 | +import { Empty, Panel, Skeleton, StatusPill, Table, Tag } from "@/components/ui"; | |
| 7 | + | |
| 8 | +interface Row { | |
| 9 | + session_id: string; | |
| 10 | + platform: string; | |
| 11 | + account_alias: string; | |
| 12 | + mode: string | null; | |
| 13 | + goal: string | null; | |
| 14 | + started_at: string; | |
| 15 | + ended_at: string | null; | |
| 16 | + health: string; | |
| 17 | + actions: number; | |
| 18 | + entities: number; | |
| 19 | + schemas: number; | |
| 20 | + patterns: number; | |
| 21 | + ended_because: string | null; | |
| 22 | +} | |
| 23 | + | |
| 24 | +export default function Sessions() { | |
| 25 | + const { data, loading } = useApi<Row[]>("/sessions?limit=200", { refreshMs: 10_000 }); | |
| 26 | + return ( | |
| 27 | + <div className="space-y-4"> | |
| 28 | + <h1 className="text-2xl font-semibold tracking-tight">Sessions</h1> | |
| 29 | + <Panel pad={false}> | |
| 30 | + <div className="p-4"> | |
| 31 | + {loading ? ( | |
| 32 | + <Skeleton rows={8} /> | |
| 33 | + ) : data?.length ? ( | |
| 34 | + <Table head={["status", "platform", "goal", "mode", "steps", "entities", "schemas", "learned", "started", "ended"]}> | |
| 35 | + {data.map((r) => ( | |
| 36 | + <tr key={r.session_id}> | |
| 37 | + <td> | |
| 38 | + <StatusPill status={r.ended_at ? (r.health === "auth_required" ? "auth_required" : "done") : r.health} /> | |
| 39 | + </td> | |
| 40 | + <td> | |
| 41 | + <Tag>{r.platform}</Tag> | |
| 42 | + <div className="text-[10px] text-dim mono">{r.account_alias}</div> | |
| 43 | + </td> | |
| 44 | + <td className="max-w-[420px]"> | |
| 45 | + <Link href={`/sessions/${r.session_id}`} className="hover:text-cyan"> | |
| 46 | + {truncate(r.goal ?? "(no goal)", 90)} | |
| 47 | + </Link> | |
| 48 | + <div className="text-[10px] text-dim mono">{r.session_id}</div> | |
| 49 | + </td> | |
| 50 | + <td className="text-fg-2">{r.mode ?? "—"}</td> | |
| 51 | + <td className="mono">{r.actions}</td> | |
| 52 | + <td className="mono">{r.entities}</td> | |
| 53 | + <td className="mono">{r.schemas}</td> | |
| 54 | + <td className="mono">{r.patterns}</td> | |
| 55 | + <td className="text-dim whitespace-nowrap" title={fmtDate(r.started_at)}> | |
| 56 | + {ago(r.started_at)} | |
| 57 | + </td> | |
| 58 | + <td className="text-dim whitespace-nowrap">{r.ended_because ?? (r.ended_at ? "—" : "running")}</td> | |
| 59 | + </tr> | |
| 60 | + ))} | |
| 61 | + </Table> | |
| 62 | + ) : ( | |
| 63 | + <Empty>No sessions yet.</Empty> | |
| 64 | + )} | |
| 65 | + </div> | |
| 66 | + </Panel> | |
| 67 | + </div> | |
| 68 | + ); | |
| 69 | +} | |
added
apps/dashboard/src/components/Brand.tsx
+33 −0
@@ -0,0 +1,33 @@ | ||
| 1 | +import clsx from "clsx"; | |
| 2 | + | |
| 3 | +/** Social Runtime Crawler mark: an eye-like orbit — the crawler *watches* a runtime rather than fetching pages. */ | |
| 4 | +export function Mark({ className }: { className?: string }) { | |
| 5 | + return ( | |
| 6 | + <svg viewBox="0 0 48 48" className={className} aria-hidden> | |
| 7 | + <defs> | |
| 8 | + <linearGradient id="srcg" x1="0" x2="1" y1="0" y2="1"> | |
| 9 | + <stop offset="0" stopColor="#38e1ff" /> | |
| 10 | + <stop offset="1" stopColor="#9d7bff" /> | |
| 11 | + </linearGradient> | |
| 12 | + </defs> | |
| 13 | + <ellipse cx="24" cy="24" rx="20" ry="11" fill="none" stroke="url(#srcg)" strokeWidth="2.2" transform="rotate(-24 24 24)" /> | |
| 14 | + <ellipse cx="24" cy="24" rx="20" ry="11" fill="none" stroke="url(#srcg)" strokeWidth="1.2" opacity=".5" transform="rotate(36 24 24)" /> | |
| 15 | + <circle cx="24" cy="24" r="5.5" fill="url(#srcg)" /> | |
| 16 | + <circle cx="41" cy="16" r="2.2" fill="#ffb347" /> | |
| 17 | + </svg> | |
| 18 | + ); | |
| 19 | +} | |
| 20 | + | |
| 21 | +export function Brand({ size = "md" }: { size?: "sm" | "md" | "lg" }) { | |
| 22 | + return ( | |
| 23 | + <div className={clsx("flex items-center gap-3", size === "lg" && "gap-4")}> | |
| 24 | + <Mark className={clsx(size === "sm" ? "h-6 w-6" : size === "lg" ? "h-12 w-12" : "h-8 w-8")} /> | |
| 25 | + <div className="leading-tight"> | |
| 26 | + <div className={clsx("font-semibold tracking-tight", size === "lg" ? "text-xl" : "text-sm")}> | |
| 27 | + Social<span className="text-cyan">Crawl</span> | |
| 28 | + </div> | |
| 29 | + <div className={clsx("text-dim uppercase tracking-[0.2em]", size === "lg" ? "text-[11px]" : "text-[9px]")}>Social Runtime Mining</div> | |
| 30 | + </div> | |
| 31 | + </div> | |
| 32 | + ); | |
| 33 | +} | |
added
apps/dashboard/src/components/EntityTable.tsx
+79 −0
@@ -0,0 +1,79 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import Link from "next/link"; | |
| 4 | +import { ExternalLink } from "lucide-react"; | |
| 5 | +import { fmtDuration, fmtNum, truncate } from "@/lib/format"; | |
| 6 | +import { SurfaceChips, Table, TypeTag } from "./ui"; | |
| 7 | + | |
| 8 | +export interface EntityRow { | |
| 9 | + fingerprint: string; | |
| 10 | + entity_type?: string; | |
| 11 | + type?: string; | |
| 12 | + platform: string; | |
| 13 | + platform_id?: string | null; | |
| 14 | + url?: string | null; | |
| 15 | + name?: string | null; | |
| 16 | + text_excerpt?: string | null; | |
| 17 | + text?: string | null; | |
| 18 | + author?: string | null; | |
| 19 | + metrics?: Record<string, number> | null; | |
| 20 | + media?: { has_video?: boolean; duration_s?: number; thumbnail_url?: string } | null; | |
| 21 | + fields?: Record<string, { value: unknown; provenance: { surface: string; confidence: number; detail?: string }[] }> | null; | |
| 22 | + provenance?: { surface: string; confidence: number; detail?: string }[] | null; | |
| 23 | + confidence?: number | null; | |
| 24 | + seen_count?: number; | |
| 25 | + last_seen?: string; | |
| 26 | + last_step?: number; | |
| 27 | +} | |
| 28 | + | |
| 29 | +export function provenanceOf(e: EntityRow) { | |
| 30 | + if (e.provenance?.length) return e.provenance; | |
| 31 | + if (e.fields) return Object.values(e.fields).flatMap((f) => f.provenance ?? []); | |
| 32 | + return []; | |
| 33 | +} | |
| 34 | + | |
| 35 | +export function EntityTable({ rows, showPlatform = false, dense = true }: { rows: EntityRow[]; showPlatform?: boolean; dense?: boolean }) { | |
| 36 | + return ( | |
| 37 | + <Table head={["type", "entity", "author", "metrics", "evidence", "conf", ""]} dense={dense}> | |
| 38 | + {rows.map((e) => { | |
| 39 | + const prov = provenanceOf(e); | |
| 40 | + const conf = e.confidence ?? Math.max(0, ...prov.map((p) => p.confidence)); | |
| 41 | + const type = e.entity_type ?? e.type ?? "?"; | |
| 42 | + return ( | |
| 43 | + <tr key={e.fingerprint}> | |
| 44 | + <td> | |
| 45 | + <TypeTag type={type} /> | |
| 46 | + {showPlatform && <div className="text-[10px] text-dim mono mt-0.5">{e.platform}</div>} | |
| 47 | + </td> | |
| 48 | + <td className="max-w-[420px]"> | |
| 49 | + <Link href={`/entities/${encodeURIComponent(e.fingerprint)}`} className="text-fg hover:text-cyan"> | |
| 50 | + {truncate(e.name ?? e.text ?? e.text_excerpt ?? e.platform_id ?? e.fingerprint, 90)} | |
| 51 | + </Link> | |
| 52 | + {e.media?.has_video && e.media.duration_s ? <span className="ml-2 text-[10.5px] mono text-dim">{fmtDuration(e.media.duration_s)}</span> : null} | |
| 53 | + <div className="text-[10.5px] mono text-dim truncate">{e.platform_id}</div> | |
| 54 | + </td> | |
| 55 | + <td className="text-fg-2 whitespace-nowrap">{truncate(e.author ?? "", 28)}</td> | |
| 56 | + <td className="mono text-[11px] text-fg-2 whitespace-nowrap"> | |
| 57 | + {e.metrics && Object.keys(e.metrics).length | |
| 58 | + ? Object.entries(e.metrics) | |
| 59 | + .map(([k, v]) => `${fmtNum(v)} ${k}`) | |
| 60 | + .join(" · ") | |
| 61 | + : "—"} | |
| 62 | + </td> | |
| 63 | + <td> | |
| 64 | + <SurfaceChips provenance={prov} /> | |
| 65 | + </td> | |
| 66 | + <td className="mono text-[11px]">{conf ? conf.toFixed(2) : "—"}</td> | |
| 67 | + <td> | |
| 68 | + {e.url && ( | |
| 69 | + <a href={e.url} target="_blank" rel="noreferrer" className="text-dim hover:text-cyan" title="open on platform"> | |
| 70 | + <ExternalLink className="h-3.5 w-3.5" /> | |
| 71 | + </a> | |
| 72 | + )} | |
| 73 | + </td> | |
| 74 | + </tr> | |
| 75 | + ); | |
| 76 | + })} | |
| 77 | + </Table> | |
| 78 | + ); | |
| 79 | +} | |
added
apps/dashboard/src/components/LiveFeed.tsx
+78 −0
@@ -0,0 +1,78 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import Link from "next/link"; | |
| 4 | +import { AnimatePresence, motion } from "framer-motion"; | |
| 5 | +import { useLive, type LiveEvent } from "@/lib/api"; | |
| 6 | +import { eventColor, fmtTime, truncate } from "@/lib/format"; | |
| 7 | +import { Tag } from "./ui"; | |
| 8 | + | |
| 9 | +export function describeEvent(e: LiveEvent): string { | |
| 10 | + const p = e.payload ?? {}; | |
| 11 | + switch (e.event_type) { | |
| 12 | + case "ACTION_PLANNED": { | |
| 13 | + const a = p.action as { type?: string; label?: string } | undefined; | |
| 14 | + return `${a?.type ?? ""} — ${truncate(a?.label, 80)} · gain ${Number(p.expected_information_gain ?? 0).toFixed(3)} · ${truncate(String(p.reason ?? ""), 90)}`; | |
| 15 | + } | |
| 16 | + case "ACTION_EXECUTED": | |
| 17 | + case "ACTION_FAILED": { | |
| 18 | + const a = p.action as { type?: string } | undefined; | |
| 19 | + const after = p.after as { new_entities?: number; network_responses?: number } | undefined; | |
| 20 | + return `${a?.type ?? ""} ${p.ok ? "ok" : "failed"}${p.error ? ` (${truncate(String(p.error), 60)})` : ""} · +${after?.new_entities ?? 0} entities · ${after?.network_responses ?? 0} responses · ${p.duration_ms} ms`; | |
| 21 | + } | |
| 22 | + case "PAGE_OPENED": | |
| 23 | + return p.page_type ? `${p.page_type} (${Number(p.confidence ?? 0).toFixed(2)}) ${truncate(String(p.url ?? ""), 70)} · dom ${p.dom_entities} · net ${p.network_entities} · both ${p.both_surfaces}` : `${truncate(String(p.url ?? ""), 80)}`; | |
| 24 | + case "NETWORK_RESPONSE_OBSERVED": | |
| 25 | + return `${p.kind} ${p.method} ${p.hostname}${p.path_pattern}${p.graphql_operation ? ` [${p.graphql_operation}]` : ""} · ${p.entity_count} entities · ${p.body_size} B`; | |
| 26 | + case "NETWORK_SCHEMA_DISCOVERED": { | |
| 27 | + const fp = p.fingerprint as { hostname?: string; path_pattern?: string } | undefined; | |
| 28 | + const sc = p.schema as { candidate_entity_types?: { type: string; confidence: number }[] } | undefined; | |
| 29 | + return `new shape ${fp?.hostname}${fp?.path_pattern} → ${(sc?.candidate_entity_types ?? []).map((c) => `${c.type} ${c.confidence.toFixed(2)}`).join(", ") || "?"}`; | |
| 30 | + } | |
| 31 | + case "CONNECTOR_PATTERN_LEARNED": | |
| 32 | + return `learned ${p.hostname}${p.path_pattern} → ${Object.keys((p.likely_entity_types as object) ?? {}).join(",")} · conf ${Number(p.confidence ?? 0).toFixed(2)} · seen ${p.observed_count}×`; | |
| 33 | + case "DOM_CHANGED": | |
| 34 | + return `+${p.added} −${p.removed} nodes · ${Object.entries((p.regions as Record<string, number>) ?? {}).map(([k, v]) => `${k} ${v}`).join(" ")}${p.modal_opened ? " · modal" : ""}${Number(p.new_videos) ? ` · ${p.new_videos} video` : ""}`; | |
| 35 | + case "MEDIA_DISCOVERED": | |
| 36 | + return `${p.media_type} ${truncate(String(p.title ?? p.platform_media_id ?? ""), 60)} · ${p.duration_s ? Math.round(Number(p.duration_s)) + "s" : ""} ${p.width ? `${p.width}×${p.height}` : ""} ${(p.delivery as { kind?: string } | undefined)?.kind ?? ""}`; | |
| 37 | + case "LOOP_DETECTED": | |
| 38 | + return `loop ${p.pattern} at ${truncate(String(p.url ?? ""), 60)} — breaking out`; | |
| 39 | + case "CONNECTOR_DEGRADED": | |
| 40 | + return `expected ${p.expected} entities on ${p.page_type}, observed ${p.observed} — ${p.action}`; | |
| 41 | + case "BUDGET_EXHAUSTED": | |
| 42 | + return `session ended: ${p.reason}`; | |
| 43 | + default: { | |
| 44 | + const ent = p.entity as { type?: string; name?: string; platform_id?: string; author?: string } | undefined; | |
| 45 | + if (ent) return `${ent.type}: ${truncate(ent.name ?? ent.platform_id ?? "", 80)}${ent.author ? ` — ${truncate(ent.author, 30)}` : ""}`; | |
| 46 | + return truncate(JSON.stringify(p), 120); | |
| 47 | + } | |
| 48 | + } | |
| 49 | +} | |
| 50 | + | |
| 51 | +export function LiveFeed({ session, types, max = 60, height = 420 }: { session?: string; types?: string[]; max?: number; height?: number }) { | |
| 52 | + const { events, connected } = useLive({ session, types, max }); | |
| 53 | + return ( | |
| 54 | + <div className="overflow-hidden" style={{ height }}> | |
| 55 | + {!connected && events.length === 0 && <div className="text-xs text-dim p-2">connecting to the observation stream…</div>} | |
| 56 | + {connected && events.length === 0 && <div className="text-xs text-dim p-2">stream connected — waiting for observations (start a mission)</div>} | |
| 57 | + <ul className="space-y-0.5 mono text-[11.5px]"> | |
| 58 | + <AnimatePresence initial={false}> | |
| 59 | + {events.map((e) => ( | |
| 60 | + <motion.li key={e.event_id} initial={{ opacity: 0, y: -6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }} transition={{ duration: 0.18 }} className="flex gap-2 items-start leading-5 rounded px-1 hover:bg-white/[0.03]"> | |
| 61 | + <span className="text-dim shrink-0">{fmtTime(e.ts)}</span> | |
| 62 | + <span className="text-dim shrink-0 w-7 text-right">#{e.step ?? 0}</span> | |
| 63 | + <Tag color={eventColor(e.event_type)} className="shrink-0"> | |
| 64 | + {e.event_type.replace(/_/g, " ").toLowerCase()} | |
| 65 | + </Tag> | |
| 66 | + <span className="text-fg-2 truncate">{describeEvent(e)}</span> | |
| 67 | + {!session && ( | |
| 68 | + <Link href={`/sessions/${e.session_id}`} className="ml-auto shrink-0 text-dim hover:text-cyan"> | |
| 69 | + {e.platform} | |
| 70 | + </Link> | |
| 71 | + )} | |
| 72 | + </motion.li> | |
| 73 | + ))} | |
| 74 | + </AnimatePresence> | |
| 75 | + </ul> | |
| 76 | + </div> | |
| 77 | + ); | |
| 78 | +} | |
added
apps/dashboard/src/components/Shell.tsx
+78 −0
@@ -0,0 +1,78 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import Link from "next/link"; | |
| 4 | +import { usePathname } from "next/navigation"; | |
| 5 | +import clsx from "clsx"; | |
| 6 | +import { Activity, Braces, Database, Eye, Layers, Network, PlayCircle, Radar } from "lucide-react"; | |
| 7 | +import { Brand } from "./Brand"; | |
| 8 | +import { useApi, useLive } from "@/lib/api"; | |
| 9 | + | |
| 10 | +const NAV = [ | |
| 11 | + { href: "/", label: "Observatory", icon: Radar }, | |
| 12 | + { href: "/sessions", label: "Sessions", icon: Eye }, | |
| 13 | + { href: "/entities", label: "Entities", icon: Database }, | |
| 14 | + { href: "/schemas", label: "Runtime APIs", icon: Braces }, | |
| 15 | + { href: "/platforms", label: "Connectors", icon: Layers }, | |
| 16 | + { href: "/jobs", label: "Missions", icon: PlayCircle }, | |
| 17 | +]; | |
| 18 | + | |
| 19 | +export function Shell({ children }: { children: React.ReactNode }) { | |
| 20 | + const path = usePathname(); | |
| 21 | + if (path.startsWith("/access")) return <>{children}</>; | |
| 22 | + return ( | |
| 23 | + <div className="flex min-h-screen"> | |
| 24 | + <aside className="hidden md:flex w-60 shrink-0 flex-col border-r border-line bg-bg-2/60 backdrop-blur sticky top-0 h-screen"> | |
| 25 | + <div className="px-5 py-5 border-b border-line"> | |
| 26 | + <Link href="/"> | |
| 27 | + <Brand /> | |
| 28 | + </Link> | |
| 29 | + </div> | |
| 30 | + <nav className="p-3 space-y-1"> | |
| 31 | + {NAV.map((n) => { | |
| 32 | + const active = n.href === "/" ? path === "/" : path.startsWith(n.href); | |
| 33 | + return ( | |
| 34 | + <Link key={n.href} href={n.href} className={clsx("flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition", active ? "bg-cyan/10 text-cyan" : "text-fg-2 hover:bg-white/5 hover:text-fg")}> | |
| 35 | + <n.icon className="h-4 w-4" /> | |
| 36 | + {n.label} | |
| 37 | + </Link> | |
| 38 | + ); | |
| 39 | + })} | |
| 40 | + </nav> | |
| 41 | + <div className="mt-auto p-4 text-[11px] text-dim leading-relaxed border-t border-line"> | |
| 42 | + <div className="flex items-center gap-2 mb-2"> | |
| 43 | + <Network className="h-3.5 w-3.5" /> MacLustr · read-only crawler | |
| 44 | + </div> | |
| 45 | + Observes only what the authenticated account can legitimately see. No likes, follows, comments. | |
| 46 | + </div> | |
| 47 | + </aside> | |
| 48 | + <div className="flex-1 min-w-0 flex flex-col"> | |
| 49 | + <TopBar /> | |
| 50 | + <main className="flex-1 p-4 md:p-6 space-y-6">{children}</main> | |
| 51 | + </div> | |
| 52 | + </div> | |
| 53 | + ); | |
| 54 | +} | |
| 55 | + | |
| 56 | +function TopBar() { | |
| 57 | + const { data } = useApi<{ jobs_running: number; ts: string }>("/health", { refreshMs: 10_000 }); | |
| 58 | + const { connected, rate } = useLive({ max: 1 }); | |
| 59 | + return ( | |
| 60 | + <header className="sticky top-0 z-20 flex items-center gap-4 px-4 md:px-6 h-14 border-b border-line bg-bg/70 backdrop-blur"> | |
| 61 | + <div className="md:hidden"> | |
| 62 | + <Brand size="sm" /> | |
| 63 | + </div> | |
| 64 | + <div className="ml-auto flex items-center gap-5 text-xs"> | |
| 65 | + <span className="flex items-center gap-2 text-fg-2"> | |
| 66 | + <span className={clsx("relative inline-block h-2 w-2 rounded-full", connected ? "bg-green text-green live-dot" : "bg-dim")} /> | |
| 67 | + {connected ? "live" : "offline"} | |
| 68 | + <span className="mono text-dim">{rate}/min</span> | |
| 69 | + </span> | |
| 70 | + <span className="flex items-center gap-2 text-fg-2"> | |
| 71 | + <Activity className="h-3.5 w-3.5 text-amber" /> | |
| 72 | + <span className="mono">{data?.jobs_running ?? 0}</span> running | |
| 73 | + </span> | |
| 74 | + <span className="mono text-dim hidden sm:inline">{data?.ts ? new Date(data.ts).toLocaleTimeString("en-CA", { hour12: false }) : ""}</span> | |
| 75 | + </div> | |
| 76 | + </header> | |
| 77 | + ); | |
| 78 | +} | |
added
apps/dashboard/src/components/WorldGraph.tsx
+237 −0
@@ -0,0 +1,237 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useEffect, useMemo, useRef, useState } from "react"; | |
| 4 | +import { forceCenter, forceCollide, forceLink, forceManyBody, forceSimulation, forceX, forceY, type SimulationLinkDatum, type SimulationNodeDatum } from "d3-force"; | |
| 5 | +import { TYPE_COLORS, truncate } from "@/lib/format"; | |
| 6 | + | |
| 7 | +export interface GraphNode { | |
| 8 | + fingerprint: string; | |
| 9 | + type: string; | |
| 10 | + name?: string | null; | |
| 11 | + url?: string | null; | |
| 12 | + visited?: boolean; | |
| 13 | + seen_count?: number; | |
| 14 | + surfaces?: string[]; | |
| 15 | +} | |
| 16 | +export interface GraphEdge { | |
| 17 | + from: string; | |
| 18 | + to: string; | |
| 19 | + type: string; | |
| 20 | +} | |
| 21 | + | |
| 22 | +interface SimNode extends SimulationNodeDatum, GraphNode { | |
| 23 | + r: number; | |
| 24 | +} | |
| 25 | +type SimLink = SimulationLinkDatum<SimNode> & { type: string }; | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * Social World Model (§23) rendered on a canvas with a d3-force layout. | |
| 29 | + * Node size = how often the entity was observed; ring = visited; colour = entity type. | |
| 30 | + */ | |
| 31 | +export function WorldGraph({ nodes, edges, height = 520, onSelect }: { nodes: GraphNode[]; edges: GraphEdge[]; height?: number; onSelect?: (n: GraphNode | null) => void }) { | |
| 32 | + const canvasRef = useRef<HTMLCanvasElement>(null); | |
| 33 | + const [hover, setHover] = useState<SimNode | null>(null); | |
| 34 | + const [selected, setSelected] = useState<SimNode | null>(null); | |
| 35 | + const simRef = useRef<ReturnType<typeof forceSimulation<SimNode>> | null>(null); | |
| 36 | + const transform = useRef({ x: 0, y: 0, k: 1 }); | |
| 37 | + | |
| 38 | + const data = useMemo(() => { | |
| 39 | + const limit = 600; | |
| 40 | + const ns: SimNode[] = nodes.slice(0, limit).map((n) => ({ ...n, r: 3 + Math.min(9, Math.sqrt(n.seen_count ?? 1) * 1.8) + (n.visited ? 2 : 0) })); | |
| 41 | + const ids = new Set(ns.map((n) => n.fingerprint)); | |
| 42 | + const ls: SimLink[] = edges.filter((e) => ids.has(e.from) && ids.has(e.to)).map((e) => ({ source: e.from, target: e.to, type: e.type })); | |
| 43 | + return { ns, ls }; | |
| 44 | + }, [nodes, edges]); | |
| 45 | + | |
| 46 | + useEffect(() => { | |
| 47 | + const canvas = canvasRef.current; | |
| 48 | + if (!canvas) return; | |
| 49 | + const parent = canvas.parentElement!; | |
| 50 | + const dpr = window.devicePixelRatio || 1; | |
| 51 | + const W = parent.clientWidth; | |
| 52 | + const H = height; | |
| 53 | + canvas.width = W * dpr; | |
| 54 | + canvas.height = H * dpr; | |
| 55 | + canvas.style.width = `${W}px`; | |
| 56 | + canvas.style.height = `${H}px`; | |
| 57 | + const ctx = canvas.getContext("2d")!; | |
| 58 | + ctx.scale(dpr, dpr); | |
| 59 | + | |
| 60 | + const sim = forceSimulation<SimNode>(data.ns) | |
| 61 | + .force("link", forceLink<SimNode, SimLink>(data.ls).id((d) => d.fingerprint).distance(38).strength(0.4)) | |
| 62 | + .force("charge", forceManyBody().strength(-45)) | |
| 63 | + .force("center", forceCenter(W / 2, H / 2)) | |
| 64 | + .force("x", forceX(W / 2).strength(0.03)) | |
| 65 | + .force("y", forceY(H / 2).strength(0.03)) | |
| 66 | + .force("collide", forceCollide<SimNode>((d) => d.r + 2)) | |
| 67 | + .alphaDecay(0.03); | |
| 68 | + simRef.current = sim; | |
| 69 | + | |
| 70 | + const draw = () => { | |
| 71 | + const { x: tx, y: ty, k } = transform.current; | |
| 72 | + ctx.clearRect(0, 0, W, H); | |
| 73 | + ctx.save(); | |
| 74 | + ctx.translate(tx, ty); | |
| 75 | + ctx.scale(k, k); | |
| 76 | + ctx.lineWidth = 0.6 / k; | |
| 77 | + for (const l of data.ls) { | |
| 78 | + const s = l.source as SimNode; | |
| 79 | + const t = l.target as SimNode; | |
| 80 | + if (s.x === undefined || t.x === undefined) continue; | |
| 81 | + ctx.strokeStyle = l.type === "AUTHORED" ? "rgba(157,123,255,0.35)" : "rgba(255,255,255,0.08)"; | |
| 82 | + ctx.beginPath(); | |
| 83 | + ctx.moveTo(s.x!, s.y!); | |
| 84 | + ctx.lineTo(t.x!, t.y!); | |
| 85 | + ctx.stroke(); | |
| 86 | + } | |
| 87 | + for (const n of data.ns) { | |
| 88 | + if (n.x === undefined) continue; | |
| 89 | + const color = TYPE_COLORS[n.type] ?? "#6b7a8c"; | |
| 90 | + const isSel = selected?.fingerprint === n.fingerprint || hover?.fingerprint === n.fingerprint; | |
| 91 | + ctx.beginPath(); | |
| 92 | + ctx.arc(n.x!, n.y!, n.r, 0, Math.PI * 2); | |
| 93 | + ctx.fillStyle = isSel ? color : `${color}cc`; | |
| 94 | + ctx.fill(); | |
| 95 | + if (n.visited) { | |
| 96 | + ctx.beginPath(); | |
| 97 | + ctx.arc(n.x!, n.y!, n.r + 3, 0, Math.PI * 2); | |
| 98 | + ctx.strokeStyle = "#ffb347"; | |
| 99 | + ctx.lineWidth = 1.2 / k; | |
| 100 | + ctx.stroke(); | |
| 101 | + } | |
| 102 | + if (isSel || (n.seen_count ?? 0) > 4 || n.visited) { | |
| 103 | + ctx.fillStyle = isSel ? "#e6edf5" : "rgba(230,237,245,0.7)"; | |
| 104 | + ctx.font = `${11 / k}px ui-monospace, monospace`; | |
| 105 | + ctx.fillText(truncate(n.name ?? n.fingerprint.split(":").pop() ?? "", 34), n.x! + n.r + 4, n.y! + 3); | |
| 106 | + } | |
| 107 | + } | |
| 108 | + ctx.restore(); | |
| 109 | + }; | |
| 110 | + sim.on("tick", draw); | |
| 111 | + draw(); | |
| 112 | + | |
| 113 | + const pick = (ev: MouseEvent): SimNode | null => { | |
| 114 | + const rect = canvas.getBoundingClientRect(); | |
| 115 | + const { x: tx, y: ty, k } = transform.current; | |
| 116 | + const mx = (ev.clientX - rect.left - tx) / k; | |
| 117 | + const my = (ev.clientY - rect.top - ty) / k; | |
| 118 | + let best: SimNode | null = null; | |
| 119 | + let bd = Infinity; | |
| 120 | + for (const n of data.ns) { | |
| 121 | + if (n.x === undefined) continue; | |
| 122 | + const d = Math.hypot(n.x! - mx, n.y! - my); | |
| 123 | + if (d < n.r + 4 && d < bd) { | |
| 124 | + bd = d; | |
| 125 | + best = n; | |
| 126 | + } | |
| 127 | + } | |
| 128 | + return best; | |
| 129 | + }; | |
| 130 | + let dragging: SimNode | null = null; | |
| 131 | + let panning = false; | |
| 132 | + let last = { x: 0, y: 0 }; | |
| 133 | + const onMove = (ev: MouseEvent) => { | |
| 134 | + if (dragging) { | |
| 135 | + const rect = canvas.getBoundingClientRect(); | |
| 136 | + const { x: tx, y: ty, k } = transform.current; | |
| 137 | + dragging.fx = (ev.clientX - rect.left - tx) / k; | |
| 138 | + dragging.fy = (ev.clientY - rect.top - ty) / k; | |
| 139 | + sim.alpha(0.3).restart(); | |
| 140 | + return; | |
| 141 | + } | |
| 142 | + if (panning) { | |
| 143 | + transform.current.x += ev.clientX - last.x; | |
| 144 | + transform.current.y += ev.clientY - last.y; | |
| 145 | + last = { x: ev.clientX, y: ev.clientY }; | |
| 146 | + draw(); | |
| 147 | + return; | |
| 148 | + } | |
| 149 | + const h = pick(ev); | |
| 150 | + setHover(h); | |
| 151 | + canvas.style.cursor = h ? "pointer" : "grab"; | |
| 152 | + draw(); | |
| 153 | + }; | |
| 154 | + const onDown = (ev: MouseEvent) => { | |
| 155 | + const h = pick(ev); | |
| 156 | + if (h) { | |
| 157 | + dragging = h; | |
| 158 | + } else { | |
| 159 | + panning = true; | |
| 160 | + last = { x: ev.clientX, y: ev.clientY }; | |
| 161 | + } | |
| 162 | + }; | |
| 163 | + const onUp = (ev: MouseEvent) => { | |
| 164 | + if (dragging) { | |
| 165 | + dragging.fx = null; | |
| 166 | + dragging.fy = null; | |
| 167 | + setSelected(dragging); | |
| 168 | + onSelect?.(dragging); | |
| 169 | + dragging = null; | |
| 170 | + } else if (panning) { | |
| 171 | + panning = false; | |
| 172 | + if (Math.hypot(ev.clientX - last.x, ev.clientY - last.y) < 3) { | |
| 173 | + setSelected(null); | |
| 174 | + onSelect?.(null); | |
| 175 | + } | |
| 176 | + } | |
| 177 | + }; | |
| 178 | + const onWheel = (ev: WheelEvent) => { | |
| 179 | + ev.preventDefault(); | |
| 180 | + const rect = canvas.getBoundingClientRect(); | |
| 181 | + const mx = ev.clientX - rect.left; | |
| 182 | + const my = ev.clientY - rect.top; | |
| 183 | + const k0 = transform.current.k; | |
| 184 | + const k1 = Math.max(0.3, Math.min(4, k0 * (ev.deltaY < 0 ? 1.1 : 0.9))); | |
| 185 | + transform.current.x = mx - ((mx - transform.current.x) * k1) / k0; | |
| 186 | + transform.current.y = my - ((my - transform.current.y) * k1) / k0; | |
| 187 | + transform.current.k = k1; | |
| 188 | + draw(); | |
| 189 | + }; | |
| 190 | + canvas.addEventListener("mousemove", onMove); | |
| 191 | + canvas.addEventListener("mousedown", onDown); | |
| 192 | + window.addEventListener("mouseup", onUp); | |
| 193 | + canvas.addEventListener("wheel", onWheel, { passive: false }); | |
| 194 | + return () => { | |
| 195 | + sim.stop(); | |
| 196 | + canvas.removeEventListener("mousemove", onMove); | |
| 197 | + canvas.removeEventListener("mousedown", onDown); | |
| 198 | + window.removeEventListener("mouseup", onUp); | |
| 199 | + canvas.removeEventListener("wheel", onWheel); | |
| 200 | + }; | |
| 201 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 202 | + }, [data, height]); | |
| 203 | + | |
| 204 | + const legend = useMemo(() => { | |
| 205 | + const c: Record<string, number> = {}; | |
| 206 | + for (const n of nodes) c[n.type] = (c[n.type] ?? 0) + 1; | |
| 207 | + return Object.entries(c).sort((a, b) => b[1] - a[1]); | |
| 208 | + }, [nodes]); | |
| 209 | + | |
| 210 | + return ( | |
| 211 | + <div className="relative"> | |
| 212 | + <canvas ref={canvasRef} className="w-full rounded-lg bg-bg-2/60" style={{ height }} /> | |
| 213 | + <div className="absolute left-3 top-3 flex flex-wrap gap-2 text-[10.5px] mono"> | |
| 214 | + {legend.map(([t, n]) => ( | |
| 215 | + <span key={t} className="inline-flex items-center gap-1 rounded bg-bg/70 px-1.5 py-0.5 border border-line"> | |
| 216 | + <span className="h-2 w-2 rounded-full" style={{ background: TYPE_COLORS[t] ?? "#6b7a8c" }} /> {t} {n} | |
| 217 | + </span> | |
| 218 | + ))} | |
| 219 | + <span className="inline-flex items-center gap-1 rounded bg-bg/70 px-1.5 py-0.5 border border-line"> | |
| 220 | + <span className="h-2 w-2 rounded-full border border-amber" /> visited | |
| 221 | + </span> | |
| 222 | + </div> | |
| 223 | + {(hover || selected) && ( | |
| 224 | + <div className="absolute right-3 bottom-3 max-w-xs panel p-3 text-xs space-y-1"> | |
| 225 | + <div className="text-fg font-medium truncate">{(hover ?? selected)!.name ?? (hover ?? selected)!.fingerprint}</div> | |
| 226 | + <div className="text-dim mono">{(hover ?? selected)!.type} · seen {(hover ?? selected)!.seen_count ?? 1}× · {(hover ?? selected)!.surfaces?.join("+") ?? ""}</div> | |
| 227 | + {(hover ?? selected)!.url && ( | |
| 228 | + <a className="text-cyan truncate block" href={(hover ?? selected)!.url!} target="_blank" rel="noreferrer"> | |
| 229 | + {(hover ?? selected)!.url} | |
| 230 | + </a> | |
| 231 | + )} | |
| 232 | + </div> | |
| 233 | + )} | |
| 234 | + <div className="absolute right-3 top-3 text-[10.5px] text-dim mono">{nodes.length} nodes · {edges.length} edges · scroll to zoom · drag to move</div> | |
| 235 | + </div> | |
| 236 | + ); | |
| 237 | +} | |
added
apps/dashboard/src/components/charts.tsx
+110 −0
@@ -0,0 +1,110 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, Legend, Line, LineChart, Pie, PieChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; | |
| 4 | +import { TYPE_COLORS, fmtNum } from "@/lib/format"; | |
| 5 | + | |
| 6 | +const AXIS = { stroke: "#6b7a8c", fontSize: 10.5, fontFamily: "var(--font-mono)" } as const; | |
| 7 | +const TT = { contentStyle: { background: "#0d1219", border: "1px solid #243042", borderRadius: 8, fontSize: 12, fontFamily: "var(--font-mono)" }, labelStyle: { color: "#a9b6c6" }, itemStyle: { color: "#e6edf5" } } as const; | |
| 8 | + | |
| 9 | +export function ActivityChart({ data, height = 220 }: { data: { bucket: string; entities: number; responses: number; actions: number; schemas: number }[]; height?: number }) { | |
| 10 | + const rows = data.map((d) => ({ ...d, t: new Date(d.bucket).toLocaleTimeString("en-CA", { hour12: false, hour: "2-digit", minute: "2-digit" }) })); | |
| 11 | + return ( | |
| 12 | + <ResponsiveContainer width="100%" height={height}> | |
| 13 | + <AreaChart data={rows} margin={{ top: 8, right: 8, left: -14, bottom: 0 }}> | |
| 14 | + <defs> | |
| 15 | + <linearGradient id="gEnt" x1="0" x2="0" y1="0" y2="1"> | |
| 16 | + <stop offset="0" stopColor="#38e1ff" stopOpacity={0.5} /> | |
| 17 | + <stop offset="1" stopColor="#38e1ff" stopOpacity={0} /> | |
| 18 | + </linearGradient> | |
| 19 | + <linearGradient id="gResp" x1="0" x2="0" y1="0" y2="1"> | |
| 20 | + <stop offset="0" stopColor="#9d7bff" stopOpacity={0.4} /> | |
| 21 | + <stop offset="1" stopColor="#9d7bff" stopOpacity={0} /> | |
| 22 | + </linearGradient> | |
| 23 | + </defs> | |
| 24 | + <CartesianGrid stroke="#1a2331" vertical={false} /> | |
| 25 | + <XAxis dataKey="t" tick={AXIS} tickLine={false} axisLine={false} minTickGap={40} /> | |
| 26 | + <YAxis tick={AXIS} tickLine={false} axisLine={false} tickFormatter={(v) => fmtNum(v)} /> | |
| 27 | + <Tooltip {...TT} /> | |
| 28 | + <Area type="monotone" dataKey="responses" name="network responses" stroke="#9d7bff" fill="url(#gResp)" strokeWidth={1.5} /> | |
| 29 | + <Area type="monotone" dataKey="entities" name="entities discovered" stroke="#38e1ff" fill="url(#gEnt)" strokeWidth={1.8} /> | |
| 30 | + <Line type="monotone" dataKey="actions" name="actions" stroke="#ffb347" strokeWidth={1.2} dot={false} /> | |
| 31 | + </AreaChart> | |
| 32 | + </ResponsiveContainer> | |
| 33 | + ); | |
| 34 | +} | |
| 35 | + | |
| 36 | +export function TypeDonut({ data, height = 200 }: { data: { entity_type: string; n: number }[]; height?: number }) { | |
| 37 | + const agg = new Map<string, number>(); | |
| 38 | + for (const d of data) agg.set(d.entity_type, (agg.get(d.entity_type) ?? 0) + Number(d.n)); | |
| 39 | + const rows = [...agg.entries()].map(([name, value]) => ({ name, value })).sort((a, b) => b.value - a.value); | |
| 40 | + return ( | |
| 41 | + <ResponsiveContainer width="100%" height={height}> | |
| 42 | + <PieChart> | |
| 43 | + <Pie data={rows} dataKey="value" nameKey="name" innerRadius="58%" outerRadius="85%" paddingAngle={2} stroke="none"> | |
| 44 | + {rows.map((r) => ( | |
| 45 | + <Cell key={r.name} fill={TYPE_COLORS[r.name] ?? "#6b7a8c"} /> | |
| 46 | + ))} | |
| 47 | + </Pie> | |
| 48 | + <Tooltip {...TT} /> | |
| 49 | + <Legend iconType="circle" iconSize={7} wrapperStyle={{ fontSize: 11, fontFamily: "var(--font-mono)", color: "#a9b6c6" }} /> | |
| 50 | + </PieChart> | |
| 51 | + </ResponsiveContainer> | |
| 52 | + ); | |
| 53 | +} | |
| 54 | + | |
| 55 | +export function GainChart({ data, height = 200 }: { data: { step: number; expected_gain: number; novelty: number; relevance: number; action_type: string; new_entities?: number }[]; height?: number }) { | |
| 56 | + return ( | |
| 57 | + <ResponsiveContainer width="100%" height={height}> | |
| 58 | + <LineChart data={data} margin={{ top: 8, right: 8, left: -18, bottom: 0 }}> | |
| 59 | + <CartesianGrid stroke="#1a2331" vertical={false} /> | |
| 60 | + <XAxis dataKey="step" tick={AXIS} tickLine={false} axisLine={false} /> | |
| 61 | + <YAxis tick={AXIS} tickLine={false} axisLine={false} domain={[0, 1]} /> | |
| 62 | + <Tooltip {...TT} labelFormatter={(l) => `step ${l}`} /> | |
| 63 | + <Line type="monotone" dataKey="expected_gain" name="information gain" stroke="#ffb347" strokeWidth={2} dot={{ r: 2 }} /> | |
| 64 | + <Line type="monotone" dataKey="novelty" name="novelty" stroke="#38e1ff" strokeWidth={1.2} dot={false} /> | |
| 65 | + <Line type="monotone" dataKey="relevance" name="relevance" stroke="#9d7bff" strokeWidth={1.2} dot={false} /> | |
| 66 | + </LineChart> | |
| 67 | + </ResponsiveContainer> | |
| 68 | + ); | |
| 69 | +} | |
| 70 | + | |
| 71 | +export function YieldBars({ data, height = 200 }: { data: { action_type: string; n: number; avg_new_entities: number | null; success_rate: number | null }[]; height?: number }) { | |
| 72 | + const rows = data.filter((d) => d.n > 0).map((d) => ({ ...d, avg_new_entities: Number(d.avg_new_entities ?? 0), label: d.action_type.replace(/_/g, " ").toLowerCase() })); | |
| 73 | + return ( | |
| 74 | + <ResponsiveContainer width="100%" height={height}> | |
| 75 | + <BarChart data={rows} layout="vertical" margin={{ top: 4, right: 12, left: 10, bottom: 0 }}> | |
| 76 | + <CartesianGrid stroke="#1a2331" horizontal={false} /> | |
| 77 | + <XAxis type="number" tick={AXIS} tickLine={false} axisLine={false} /> | |
| 78 | + <YAxis type="category" dataKey="label" tick={AXIS} tickLine={false} axisLine={false} width={110} /> | |
| 79 | + <Tooltip {...TT} /> | |
| 80 | + <Bar dataKey="avg_new_entities" name="avg new entities / action" radius={[0, 4, 4, 0]}> | |
| 81 | + {rows.map((r) => ( | |
| 82 | + <Cell key={r.action_type} fill={r.action_type.startsWith("OPEN") ? "#38e1ff" : r.action_type === "SEARCH" ? "#ffb347" : "#9d7bff"} /> | |
| 83 | + ))} | |
| 84 | + </Bar> | |
| 85 | + </BarChart> | |
| 86 | + </ResponsiveContainer> | |
| 87 | + ); | |
| 88 | +} | |
| 89 | + | |
| 90 | +export function SurfaceBars({ dom, network, both, height = 120 }: { dom: number; network: number; both: number; height?: number }) { | |
| 91 | + const rows = [ | |
| 92 | + { name: "DOM only", value: Math.max(0, dom - both), fill: "#43e69a" }, | |
| 93 | + { name: "Both surfaces", value: both, fill: "#38e1ff" }, | |
| 94 | + { name: "Network only", value: Math.max(0, network - both), fill: "#9d7bff" }, | |
| 95 | + ]; | |
| 96 | + return ( | |
| 97 | + <ResponsiveContainer width="100%" height={height}> | |
| 98 | + <BarChart data={rows} margin={{ top: 4, right: 8, left: -18, bottom: 0 }}> | |
| 99 | + <XAxis dataKey="name" tick={AXIS} tickLine={false} axisLine={false} /> | |
| 100 | + <YAxis tick={AXIS} tickLine={false} axisLine={false} tickFormatter={(v) => fmtNum(v)} /> | |
| 101 | + <Tooltip {...TT} /> | |
| 102 | + <Bar dataKey="value" radius={[4, 4, 0, 0]}> | |
| 103 | + {rows.map((r) => ( | |
| 104 | + <Cell key={r.name} fill={r.fill} /> | |
| 105 | + ))} | |
| 106 | + </Bar> | |
| 107 | + </BarChart> | |
| 108 | + </ResponsiveContainer> | |
| 109 | + ); | |
| 110 | +} | |
added
apps/dashboard/src/components/ui.tsx
+135 −0
@@ -0,0 +1,135 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import clsx from "clsx"; | |
| 4 | +import type { ReactNode } from "react"; | |
| 5 | +import { TYPE_COLORS } from "@/lib/format"; | |
| 6 | + | |
| 7 | +export function Panel({ title, sub, right, children, className, pad = true }: { title?: ReactNode; sub?: ReactNode; right?: ReactNode; children: ReactNode; className?: string; pad?: boolean }) { | |
| 8 | + return ( | |
| 9 | + <section className={clsx("panel flex flex-col min-w-0", className)}> | |
| 10 | + {(title || right) && ( | |
| 11 | + <header className="flex items-center gap-3 px-4 py-3 border-b border-line"> | |
| 12 | + <div className="min-w-0"> | |
| 13 | + {title && <h2 className="text-[11px] uppercase tracking-[0.18em] text-fg-2 font-medium">{title}</h2>} | |
| 14 | + {sub && <p className="text-xs text-dim truncate">{sub}</p>} | |
| 15 | + </div> | |
| 16 | + {right && <div className="ml-auto shrink-0 text-xs text-dim">{right}</div>} | |
| 17 | + </header> | |
| 18 | + )} | |
| 19 | + <div className={clsx("min-w-0 flex-1", pad && "p-4")}>{children}</div> | |
| 20 | + </section> | |
| 21 | + ); | |
| 22 | +} | |
| 23 | + | |
| 24 | +export function Kpi({ label, value, hint, accent = "cyan", spark }: { label: string; value: ReactNode; hint?: ReactNode; accent?: "cyan" | "amber" | "green" | "violet" | "pink" | "red" | "fg"; spark?: number[] }) { | |
| 25 | + const color = { cyan: "text-cyan", amber: "text-amber", green: "text-green", violet: "text-violet", pink: "text-pink", red: "text-red", fg: "text-fg" }[accent]; | |
| 26 | + return ( | |
| 27 | + <div className="panel p-4 relative overflow-hidden"> | |
| 28 | + <div className="text-[11px] uppercase tracking-[0.18em] text-dim">{label}</div> | |
| 29 | + <div className={clsx("mono text-3xl font-semibold mt-1 tabular-nums", color)}>{value}</div> | |
| 30 | + {hint && <div className="text-xs text-fg-2 mt-1">{hint}</div>} | |
| 31 | + {spark && spark.length > 1 && <Spark values={spark} className="absolute right-3 bottom-3 h-8 w-24 opacity-70" />} | |
| 32 | + </div> | |
| 33 | + ); | |
| 34 | +} | |
| 35 | + | |
| 36 | +export function Spark({ values, className, color = "currentColor" }: { values: number[]; className?: string; color?: string }) { | |
| 37 | + const max = Math.max(1, ...values); | |
| 38 | + const w = 100; | |
| 39 | + const h = 30; | |
| 40 | + const pts = values.map((v, i) => `${(i / Math.max(1, values.length - 1)) * w},${h - (v / max) * (h - 2) - 1}`).join(" "); | |
| 41 | + return ( | |
| 42 | + <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none" className={className}> | |
| 43 | + <polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" vectorEffect="non-scaling-stroke" /> | |
| 44 | + </svg> | |
| 45 | + ); | |
| 46 | +} | |
| 47 | + | |
| 48 | +export function Tag({ children, color, className, title }: { children: ReactNode; color?: string; className?: string; title?: string }) { | |
| 49 | + const c = color ?? "#6b7a8c"; | |
| 50 | + return ( | |
| 51 | + <span title={title} className={clsx("inline-flex items-center rounded-md px-1.5 py-0.5 text-[10.5px] font-medium mono leading-4 whitespace-nowrap", className)} style={{ color: c, background: `${c}1f`, border: `1px solid ${c}33` }}> | |
| 52 | + {children} | |
| 53 | + </span> | |
| 54 | + ); | |
| 55 | +} | |
| 56 | + | |
| 57 | +export function TypeTag({ type }: { type: string }) { | |
| 58 | + return <Tag color={TYPE_COLORS[type] ?? "#6b7a8c"}>{type}</Tag>; | |
| 59 | +} | |
| 60 | + | |
| 61 | +export function SurfaceChips({ provenance }: { provenance?: { surface: string; confidence: number; detail?: string }[] | null }) { | |
| 62 | + if (!provenance?.length) return <span className="text-dim">—</span>; | |
| 63 | + const best = new Map<string, { confidence: number; detail?: string }>(); | |
| 64 | + for (const p of provenance) if (!best.has(p.surface) || best.get(p.surface)!.confidence < p.confidence) best.set(p.surface, p); | |
| 65 | + const colors: Record<string, string> = { network: "#38e1ff", dom: "#43e69a", visual: "#ff7ad9", media: "#9d7bff", accessibility: "#ffb347", navigation: "#6b7a8c" }; | |
| 66 | + return ( | |
| 67 | + <span className="inline-flex gap-1 flex-wrap"> | |
| 68 | + {[...best.entries()].map(([s, p]) => ( | |
| 69 | + <Tag key={s} color={colors[s] ?? "#6b7a8c"} title={p.detail}> | |
| 70 | + {s} {Math.round(p.confidence * 100)} | |
| 71 | + </Tag> | |
| 72 | + ))} | |
| 73 | + </span> | |
| 74 | + ); | |
| 75 | +} | |
| 76 | + | |
| 77 | +export function Bar({ value, color = "#38e1ff", className }: { value: number; color?: string; className?: string }) { | |
| 78 | + return ( | |
| 79 | + <div className={clsx("h-1.5 rounded-full bg-white/5 overflow-hidden", className)}> | |
| 80 | + <div className="h-full rounded-full transition-all" style={{ width: `${Math.max(0, Math.min(100, value * 100))}%`, background: color }} /> | |
| 81 | + </div> | |
| 82 | + ); | |
| 83 | +} | |
| 84 | + | |
| 85 | +export function Empty({ children }: { children: ReactNode }) { | |
| 86 | + return <div className="text-sm text-dim py-10 text-center">{children}</div>; | |
| 87 | +} | |
| 88 | + | |
| 89 | +export function Skeleton({ rows = 4 }: { rows?: number }) { | |
| 90 | + return ( | |
| 91 | + <div className="space-y-2"> | |
| 92 | + {Array.from({ length: rows }).map((_, i) => ( | |
| 93 | + <div key={i} className="skeleton h-4" style={{ width: `${70 + ((i * 13) % 30)}%` }} /> | |
| 94 | + ))} | |
| 95 | + </div> | |
| 96 | + ); | |
| 97 | +} | |
| 98 | + | |
| 99 | +export function Table({ head, children, dense }: { head: ReactNode[]; children: ReactNode; dense?: boolean }) { | |
| 100 | + return ( | |
| 101 | + <div className="overflow-auto scrollbar-thin -mx-4 px-4"> | |
| 102 | + <table className={clsx("w-full text-sm", dense ? "text-[12.5px]" : "")}> | |
| 103 | + <thead> | |
| 104 | + <tr className="text-left text-[10.5px] uppercase tracking-widest text-dim"> | |
| 105 | + {head.map((h, i) => ( | |
| 106 | + <th key={i} className="px-2 py-2 font-medium border-b border-line whitespace-nowrap"> | |
| 107 | + {h} | |
| 108 | + </th> | |
| 109 | + ))} | |
| 110 | + </tr> | |
| 111 | + </thead> | |
| 112 | + <tbody className="[&>tr]:border-b [&>tr]:border-line/60 [&>tr:hover]:bg-white/[0.025] [&>tr>td]:px-2 [&>tr>td]:py-1.5 [&>tr>td]:align-top">{children}</tbody> | |
| 113 | + </table> | |
| 114 | + </div> | |
| 115 | + ); | |
| 116 | +} | |
| 117 | + | |
| 118 | +export function Metric({ label, value }: { label: string; value: ReactNode }) { | |
| 119 | + return ( | |
| 120 | + <div className="flex items-baseline justify-between gap-3 text-sm border-b border-line/60 py-1.5"> | |
| 121 | + <span className="text-dim">{label}</span> | |
| 122 | + <span className="mono text-fg">{value}</span> | |
| 123 | + </div> | |
| 124 | + ); | |
| 125 | +} | |
| 126 | + | |
| 127 | +export function StatusPill({ status }: { status: string }) { | |
| 128 | + const map: Record<string, string> = { running: "#ffb347", healthy: "#ffb347", queued: "#6b7a8c", done: "#43e69a", stopped: "#6b7a8c", failed: "#ff5c7a", auth_required: "#ff5c7a", crashed: "#ff5c7a", degraded: "#ff5c7a" }; | |
| 129 | + return ( | |
| 130 | + <Tag color={map[status] ?? "#6b7a8c"}> | |
| 131 | + {status === "running" || status === "healthy" ? <span className="relative inline-block h-1.5 w-1.5 rounded-full bg-amber text-amber live-dot mr-1" /> : null} | |
| 132 | + {status} | |
| 133 | + </Tag> | |
| 134 | + ); | |
| 135 | +} | |
added
apps/dashboard/src/lib/api.ts
+94 −0
@@ -0,0 +1,94 @@ | ||
| 1 | +"use client"; | |
| 2 | + | |
| 3 | +import { useCallback, useEffect, useRef, useState } from "react"; | |
| 4 | + | |
| 5 | +/** Client-side data layer: every call goes through the Next proxy (/api/*) which adds the API token. */ | |
| 6 | +export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> { | |
| 7 | + const res = await fetch(`/api${path}`, { ...init, headers: { "content-type": "application/json", ...(init?.headers ?? {}) }, cache: "no-store" }); | |
| 8 | + if (!res.ok) { | |
| 9 | + let msg = `${res.status}`; | |
| 10 | + try { | |
| 11 | + msg = ((await res.json()) as { error?: string }).error ?? msg; | |
| 12 | + } catch { | |
| 13 | + /* ignore */ | |
| 14 | + } | |
| 15 | + throw new Error(msg); | |
| 16 | + } | |
| 17 | + return (await res.json()) as T; | |
| 18 | +} | |
| 19 | + | |
| 20 | +export function useApi<T>(path: string | null, opts: { refreshMs?: number; deps?: unknown[] } = {}) { | |
| 21 | + const [data, setData] = useState<T | null>(null); | |
| 22 | + const [error, setError] = useState<string | null>(null); | |
| 23 | + const [loading, setLoading] = useState(!!path); | |
| 24 | + const [tick, setTick] = useState(0); | |
| 25 | + const refresh = useCallback(() => setTick((t) => t + 1), []); | |
| 26 | + useEffect(() => { | |
| 27 | + if (!path) return; | |
| 28 | + let alive = true; | |
| 29 | + api<T>(path) | |
| 30 | + .then((d) => { | |
| 31 | + if (!alive) return; | |
| 32 | + setData(d); | |
| 33 | + setError(null); | |
| 34 | + }) | |
| 35 | + .catch((e: Error) => alive && setError(e.message)) | |
| 36 | + .finally(() => alive && setLoading(false)); | |
| 37 | + const id = opts.refreshMs ? setInterval(() => setTick((t) => t + 1), opts.refreshMs) : undefined; | |
| 38 | + return () => { | |
| 39 | + alive = false; | |
| 40 | + if (id) clearInterval(id); | |
| 41 | + }; | |
| 42 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 43 | + }, [path, tick, ...(opts.deps ?? [])]); | |
| 44 | + return { data, error, loading, refresh }; | |
| 45 | +} | |
| 46 | + | |
| 47 | +export interface LiveEvent { | |
| 48 | + event_id: string; | |
| 49 | + session_id: string; | |
| 50 | + platform: string; | |
| 51 | + event_type: string; | |
| 52 | + step: number | null; | |
| 53 | + ts: string; | |
| 54 | + payload: Record<string, unknown>; | |
| 55 | + provenance?: { surface: string; confidence: number }[] | null; | |
| 56 | +} | |
| 57 | + | |
| 58 | +/** Server-Sent Events subscription (live feed). Keeps the last `max` events. */ | |
| 59 | +export function useLive(opts: { session?: string; types?: string[]; max?: number; enabled?: boolean } = {}) { | |
| 60 | + const [events, setEvents] = useState<LiveEvent[]>([]); | |
| 61 | + const [connected, setConnected] = useState(false); | |
| 62 | + const [rate, setRate] = useState(0); // events per minute (rolling) | |
| 63 | + const stamps = useRef<number[]>([]); | |
| 64 | + const max = opts.max ?? 250; | |
| 65 | + const enabled = opts.enabled !== false; | |
| 66 | + useEffect(() => { | |
| 67 | + if (!enabled) return; | |
| 68 | + const params = new URLSearchParams(); | |
| 69 | + if (opts.session) params.set("session", opts.session); | |
| 70 | + if (opts.types?.length) params.set("types", opts.types.join(",")); | |
| 71 | + const es = new EventSource(`/api/stream?${params}`); | |
| 72 | + es.onopen = () => setConnected(true); | |
| 73 | + es.onerror = () => setConnected(false); | |
| 74 | + es.addEventListener("observation", (m) => { | |
| 75 | + const ev = JSON.parse((m as MessageEvent).data) as LiveEvent; | |
| 76 | + const now = Date.now(); | |
| 77 | + stamps.current.push(now); | |
| 78 | + stamps.current = stamps.current.filter((t) => now - t < 60_000); | |
| 79 | + setRate(stamps.current.length); | |
| 80 | + setEvents((prev) => [ev, ...prev].slice(0, max)); | |
| 81 | + }); | |
| 82 | + const id = setInterval(() => { | |
| 83 | + const now = Date.now(); | |
| 84 | + stamps.current = stamps.current.filter((t) => now - t < 60_000); | |
| 85 | + setRate(stamps.current.length); | |
| 86 | + }, 5000); | |
| 87 | + return () => { | |
| 88 | + es.close(); | |
| 89 | + clearInterval(id); | |
| 90 | + }; | |
| 91 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 92 | + }, [opts.session, opts.types?.join(","), enabled, max]); | |
| 93 | + return { events, connected, rate }; | |
| 94 | +} | |
added
apps/dashboard/src/lib/auth.ts
+36 −0
@@ -0,0 +1,36 @@ | ||
| 1 | +import { createHmac, timingSafeEqual } from "node:crypto"; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Passcode gate. The console is public on the Internet (www.socialcrawl.co) and can launch browsers | |
| 5 | + * on a cluster node, so every page and API call requires a signed cookie obtained with SRC_DASHBOARD_PASSCODE. | |
| 6 | + */ | |
| 7 | +export const COOKIE = "src_access"; | |
| 8 | +const SECRET = process.env.SRC_DASHBOARD_SECRET ?? process.env.SRC_API_TOKEN ?? "dev-secret"; | |
| 9 | +const PASSCODE = process.env.SRC_DASHBOARD_PASSCODE ?? ""; | |
| 10 | + | |
| 11 | +export function gateEnabled(): boolean { | |
| 12 | + return PASSCODE.length > 0; | |
| 13 | +} | |
| 14 | + | |
| 15 | +export function checkPasscode(input: string): boolean { | |
| 16 | + if (!PASSCODE) return true; | |
| 17 | + const a = Buffer.from(input); | |
| 18 | + const b = Buffer.from(PASSCODE); | |
| 19 | + return a.length === b.length && timingSafeEqual(a, b); | |
| 20 | +} | |
| 21 | + | |
| 22 | +export function makeToken(): string { | |
| 23 | + const exp = Date.now() + 30 * 24 * 3600 * 1000; | |
| 24 | + const body = `${exp}`; | |
| 25 | + return `${body}.${createHmac("sha256", SECRET).update(body).digest("hex")}`; | |
| 26 | +} | |
| 27 | + | |
| 28 | +export function verifyToken(token: string | undefined): boolean { | |
| 29 | + if (!PASSCODE) return true; | |
| 30 | + if (!token) return false; | |
| 31 | + const [body, sig] = token.split("."); | |
| 32 | + if (!body || !sig) return false; | |
| 33 | + const expect = createHmac("sha256", SECRET).update(body).digest("hex"); | |
| 34 | + if (expect.length !== sig.length || !timingSafeEqual(Buffer.from(expect), Buffer.from(sig))) return false; | |
| 35 | + return Number(body) > Date.now(); | |
| 36 | +} | |
added
apps/dashboard/src/lib/format.ts
+84 −0
@@ -0,0 +1,84 @@ | ||
| 1 | +export function fmtNum(n: number | string | null | undefined, digits = 0): string { | |
| 2 | + if (n === null || n === undefined || n === "") return "—"; | |
| 3 | + const v = Number(n); | |
| 4 | + if (Number.isNaN(v)) return String(n); | |
| 5 | + if (Math.abs(v) >= 1e9) return (v / 1e9).toFixed(1) + "B"; | |
| 6 | + if (Math.abs(v) >= 1e6) return (v / 1e6).toFixed(1) + "M"; | |
| 7 | + if (Math.abs(v) >= 1e4) return (v / 1e3).toFixed(1) + "k"; | |
| 8 | + return v.toLocaleString("en-US", { maximumFractionDigits: digits }); | |
| 9 | +} | |
| 10 | + | |
| 11 | +export function fmtPct(n: number | null | undefined, digits = 0): string { | |
| 12 | + if (n === null || n === undefined || Number.isNaN(Number(n))) return "—"; | |
| 13 | + return `${(Number(n) * 100).toFixed(digits)}%`; | |
| 14 | +} | |
| 15 | + | |
| 16 | +export function fmtDuration(s: number | null | undefined): string { | |
| 17 | + if (!s && s !== 0) return "—"; | |
| 18 | + const v = Math.round(Number(s)); | |
| 19 | + const h = Math.floor(v / 3600); | |
| 20 | + const m = Math.floor((v % 3600) / 60); | |
| 21 | + const sec = v % 60; | |
| 22 | + return h ? `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}` : `${m}:${String(sec).padStart(2, "0")}`; | |
| 23 | +} | |
| 24 | + | |
| 25 | +export function fmtTime(ts: string | Date | null | undefined): string { | |
| 26 | + if (!ts) return "—"; | |
| 27 | + const d = new Date(ts); | |
| 28 | + return d.toLocaleTimeString("en-CA", { hour12: false }); | |
| 29 | +} | |
| 30 | + | |
| 31 | +export function fmtDate(ts: string | Date | null | undefined): string { | |
| 32 | + if (!ts) return "—"; | |
| 33 | + const d = new Date(ts); | |
| 34 | + return d.toLocaleString("en-CA", { hour12: false, month: "short", day: "2-digit", hour: "2-digit", minute: "2-digit" }); | |
| 35 | +} | |
| 36 | + | |
| 37 | +export function ago(ts: string | Date | null | undefined): string { | |
| 38 | + if (!ts) return "—"; | |
| 39 | + const diff = (Date.now() - new Date(ts).getTime()) / 1000; | |
| 40 | + if (diff < 60) return `${Math.max(0, Math.round(diff))}s ago`; | |
| 41 | + if (diff < 3600) return `${Math.round(diff / 60)}m ago`; | |
| 42 | + if (diff < 86400) return `${Math.round(diff / 3600)}h ago`; | |
| 43 | + return `${Math.round(diff / 86400)}d ago`; | |
| 44 | +} | |
| 45 | + | |
| 46 | +export function truncate(s: string | null | undefined, n: number): string { | |
| 47 | + if (!s) return ""; | |
| 48 | + return s.length > n ? s.slice(0, n - 1) + "…" : s; | |
| 49 | +} | |
| 50 | + | |
| 51 | +export const TYPE_COLORS: Record<string, string> = { | |
| 52 | + video: "#38e1ff", | |
| 53 | + channel: "#9d7bff", | |
| 54 | + profile: "#9d7bff", | |
| 55 | + person: "#9d7bff", | |
| 56 | + post: "#ffb347", | |
| 57 | + comment: "#ff7ad9", | |
| 58 | + community: "#43e69a", | |
| 59 | + page: "#43e69a", | |
| 60 | + hashtag: "#6b7a8c", | |
| 61 | + image: "#38e1ff", | |
| 62 | + organization: "#43e69a", | |
| 63 | +}; | |
| 64 | + | |
| 65 | +export const EVENT_COLORS: Record<string, string> = { | |
| 66 | + ACTION_PLANNED: "#ffb347", | |
| 67 | + ACTION_EXECUTED: "#43e69a", | |
| 68 | + ACTION_FAILED: "#ff5c7a", | |
| 69 | + PAGE_OPENED: "#38e1ff", | |
| 70 | + NETWORK_SCHEMA_DISCOVERED: "#9d7bff", | |
| 71 | + CONNECTOR_PATTERN_LEARNED: "#9d7bff", | |
| 72 | + CONNECTOR_DEGRADED: "#ff5c7a", | |
| 73 | + CONNECTOR_REPAIRED: "#43e69a", | |
| 74 | + LOOP_DETECTED: "#ff5c7a", | |
| 75 | + AUTH_REQUIRED: "#ff5c7a", | |
| 76 | + MEDIA_DISCOVERED: "#ff7ad9", | |
| 77 | + VIDEO_DISCOVERED: "#38e1ff", | |
| 78 | + POST_DISCOVERED: "#ffb347", | |
| 79 | + PROFILE_DISCOVERED: "#9d7bff", | |
| 80 | +}; | |
| 81 | + | |
| 82 | +export function eventColor(t: string): string { | |
| 83 | + return EVENT_COLORS[t] ?? "#6b7a8c"; | |
| 84 | +} | |
added
apps/dashboard/src/proxy.ts
+24 −0
@@ -0,0 +1,24 @@ | ||
| 1 | +import { NextResponse, type NextRequest } from "next/server"; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Edge gate: redirect unauthenticated visitors to /access. Token verification (HMAC) happens in the | |
| 5 | + * Node route handlers; here we only check presence + expiry to keep the edge runtime crypto-free. | |
| 6 | + */ | |
| 7 | +export function proxy(req: NextRequest) { | |
| 8 | + const { pathname } = req.nextUrl; | |
| 9 | + if (pathname.startsWith("/access") || pathname.startsWith("/_next") || pathname === "/favicon.ico" || pathname.startsWith("/brand")) return NextResponse.next(); | |
| 10 | + const gate = process.env.SRC_DASHBOARD_PASSCODE; | |
| 11 | + if (!gate) return NextResponse.next(); | |
| 12 | + const token = req.cookies.get("src_access")?.value; | |
| 13 | + const exp = token ? Number(token.split(".")[0]) : 0; | |
| 14 | + if (!token || !(exp > Date.now())) { | |
| 15 | + if (pathname.startsWith("/api")) return NextResponse.json({ error: "unauthorized" }, { status: 401 }); | |
| 16 | + const url = req.nextUrl.clone(); | |
| 17 | + url.pathname = "/access"; | |
| 18 | + url.searchParams.set("next", pathname); | |
| 19 | + return NextResponse.redirect(url); | |
| 20 | + } | |
| 21 | + return NextResponse.next(); | |
| 22 | +} | |
| 23 | + | |
| 24 | +export const config = { matcher: ["/((?!_next/static|_next/image).*)"] }; | |
added
apps/dashboard/tsconfig.json
+41 −0
@@ -0,0 +1,41 @@ | ||
| 1 | +{ | |
| 2 | + "compilerOptions": { | |
| 3 | + "target": "ES2022", | |
| 4 | + "lib": [ | |
| 5 | + "dom", | |
| 6 | + "dom.iterable", | |
| 7 | + "esnext" | |
| 8 | + ], | |
| 9 | + "allowJs": true, | |
| 10 | + "skipLibCheck": true, | |
| 11 | + "strict": true, | |
| 12 | + "noEmit": true, | |
| 13 | + "esModuleInterop": true, | |
| 14 | + "module": "esnext", | |
| 15 | + "moduleResolution": "bundler", | |
| 16 | + "resolveJsonModule": true, | |
| 17 | + "isolatedModules": true, | |
| 18 | + "jsx": "react-jsx", | |
| 19 | + "incremental": true, | |
| 20 | + "plugins": [ | |
| 21 | + { | |
| 22 | + "name": "next" | |
| 23 | + } | |
| 24 | + ], | |
| 25 | + "paths": { | |
| 26 | + "@/*": [ | |
| 27 | + "./src/*" | |
| 28 | + ] | |
| 29 | + } | |
| 30 | + }, | |
| 31 | + "include": [ | |
| 32 | + "next-env.d.ts", | |
| 33 | + "**/*.ts", | |
| 34 | + "**/*.tsx", | |
| 35 | + ".next/types/**/*.ts", | |
| 36 | + ".next/dev/types/**/*.ts" | |
| 37 | + ], | |
| 38 | + "exclude": [ | |
| 39 | + "node_modules" | |
| 40 | + ] | |
| 41 | +} | |
added
apps/worker/qa/screenshots.mjs
+12 −0
@@ -0,0 +1,12 @@ | ||
| 1 | +import { chromium } from "playwright"; | |
| 2 | +const b = await chromium.launch(); const p = await b.newPage({ viewport: { width: 1500, height: 1000 } }); | |
| 3 | +await p.goto("http://localhost:8351/access"); await p.fill('input[name=passcode]', "test1234"); await p.click("button"); await p.waitForURL("http://localhost:8351/"); | |
| 4 | +await p.waitForTimeout(3500); await p.screenshot({ path: "/tmp/shot-home.png", fullPage: true }); | |
| 5 | +const sid = (await (await fetch("http://127.0.0.1:8350/api/sessions?limit=1")).json())[0].session_id; | |
| 6 | +await p.goto(`http://localhost:8351/sessions/${sid}`); await p.waitForTimeout(3000); await p.screenshot({ path: "/tmp/shot-session.png", fullPage: true }); | |
| 7 | +await p.click("text=world model"); await p.waitForTimeout(3500); await p.screenshot({ path: "/tmp/shot-world.png" }); | |
| 8 | +await p.goto(`http://localhost:8351/platforms/youtube`); await p.waitForTimeout(2500); await p.screenshot({ path: "/tmp/shot-platform.png", fullPage: true }); | |
| 9 | +await p.goto(`http://localhost:8351/jobs`); await p.waitForTimeout(2500); await p.screenshot({ path: "/tmp/shot-jobs.png" }); | |
| 10 | +const errs = []; p.on("console", m => { if (m.type()==="error") errs.push(m.text()); }); | |
| 11 | +await p.goto(`http://localhost:8351/entities`); await p.waitForTimeout(2500); await p.screenshot({ path: "/tmp/shot-entities.png" }); | |
| 12 | +console.log("errors:", errs.slice(0,5)); await b.close(); | |
modified
apps/worker/src/cli.ts
+1 −1
@@ -69,7 +69,7 @@ async function main() { | ||
| 69 | 69 | const goal = str("goal") ?? (isLearn ? `Learn how ${platform} surfaces content: page types, entity structures, response schemas, navigation` : query ? `Discover public content about “${query}”` : `Observe the ${platform} feed`); |
| 70 | 70 | if (args.flags.headless) cfg.headless = true; |
| 71 | 71 | const job: CrawlJob = { |
| 72 | − job_id: newId("job"), | |
| 72 | + job_id: str("job") ?? newId("job"), | |
| 73 | 73 | platform, |
| 74 | 74 | account_alias: str("account", "research-01")!, |
| 75 | 75 | mode, |
modified
apps/worker/src/engine.ts
+1 −0
@@ -76,6 +76,7 @@ export class CrawlEngine { | ||
| 76 | 76 | this.store = await openStore(cfg.databaseUrl); |
| 77 | 77 | this.store?.attach(this.bus); |
| 78 | 78 | fs.writeFileSync(path.join(this.sessionDir, "job.json"), JSON.stringify({ ...job, session_id: this.session.id }, null, 2)); |
| 79 | + await this.store?.linkJob(job, this.session.id).catch(() => {}); | |
| 79 | 80 | |
| 80 | 81 | if (!this.session.hasProfile()) log.warn(`No saved browser profile for ${job.platform}/${job.account_alias}. Run: pnpm login ${job.platform} --account ${job.account_alias}`); |
| 81 | 82 | |
modified
apps/worker/src/executor.ts
+0 −0
Binary file not shown.
modified
connectors/youtube/manifest.json
+2993 −14
@@ -1,14 +1,15 @@ | ||
| 1 | 1 | { |
| 2 | 2 | "platform": "youtube", |
| 3 | − "compiled_at": "2026-09-11T21:32:58.338Z", | |
| 4 | − "learned_from_sessions": 4, | |
| 3 | + "compiled_at": "2026-09-12T02:35:03.398Z", | |
| 4 | + "learned_from_sessions": 5, | |
| 5 | 5 | "confidence": 45, |
| 6 | 6 | "page_types": [ |
| 7 | 7 | { |
| 8 | 8 | "page_type": "UNKNOWN", |
| 9 | 9 | "url_patterns": [ |
| 10 | 10 | "/", |
| 11 | − "/results" | |
| 11 | + "/results", | |
| 12 | + "/watch" | |
| 12 | 13 | ], |
| 13 | 14 | "avg_entities": 0 |
| 14 | 15 | }, |
@@ -29,12 +30,2971 @@ | ||
| 29 | 30 | { |
| 30 | 31 | "page_type": "VIDEO_DETAIL", |
| 31 | 32 | "url_patterns": [ |
| 32 | − "/watch" | |
| 33 | + "/watch", | |
| 34 | + "/shorts/*" | |
| 33 | 35 | ], |
| 34 | − "avg_entities": 39 | |
| 36 | + "avg_entities": 34.1 | |
| 37 | + } | |
| 38 | + ], | |
| 39 | + "network_patterns": [ | |
| 40 | + { | |
| 41 | + "shape_hash": "e716c418dfe5cbd3", | |
| 42 | + "status": "provisional", | |
| 43 | + "hostname": "www.youtube.com", | |
| 44 | + "path_pattern": "/youtubei/v1/search", | |
| 45 | + "method": "POST", | |
| 46 | + "likely_entity_types": { | |
| 47 | + "video": 2, | |
| 48 | + "post": 2, | |
| 49 | + "channel": 2 | |
| 50 | + }, | |
| 51 | + "confidence": 0.39, | |
| 52 | + "triggered_by": { | |
| 53 | + "SEARCH": 2 | |
| 54 | + }, | |
| 55 | + "key_fields": [ | |
| 56 | + { | |
| 57 | + "path": "responseContext.mainAppWebResponseContext.loggedOut", | |
| 58 | + "semantic": { | |
| 59 | + "kind": "boolean", | |
| 60 | + "confidence": 0.95 | |
| 61 | + }, | |
| 62 | + "example": "true" | |
| 63 | + }, | |
| 64 | + { | |
| 65 | + "path": "responseContext.responseId", | |
| 66 | + "semantic": { | |
| 67 | + "kind": "identifier", | |
| 68 | + "confidence": 0.95 | |
| 69 | + }, | |
| 70 | + "example": "IhMI486x5bznlgMVYhaRAR0OOzWR" | |
| 71 | + }, | |
| 72 | + { | |
| 73 | + "path": "responseContext.webResponseContextExtensionData.hasDecorated", | |
| 74 | + "semantic": { | |
| 75 | + "kind": "boolean", | |
| 76 | + "confidence": 0.95 | |
| 77 | + }, | |
| 78 | + "example": "true" | |
| 79 | + }, | |
| 80 | + { | |
| 81 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.videoId", | |
| 82 | + "semantic": { | |
| 83 | + "kind": "identifier", | |
| 84 | + "confidence": 0.95 | |
| 85 | + }, | |
| 86 | + "example": "1RqbM2LW3wE" | |
| 87 | + }, | |
| 88 | + { | |
| 89 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.thumbnail.thumbnails.[].url", | |
| 90 | + "semantic": { | |
| 91 | + "kind": "thumbnail_url", | |
| 92 | + "confidence": 0.92 | |
| 93 | + }, | |
| 94 | + "example": "https://i.ytimg.com/vi/1RqbM2LW3wE/hq720.jpg?sqp=-oaymwEnCOgCEMoBSFryq4qpAxkIARU" | |
| 95 | + }, | |
| 96 | + { | |
| 97 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.title.runs.[].text", | |
| 98 | + "semantic": { | |
| 99 | + "kind": "text", | |
| 100 | + "confidence": 0.85 | |
| 101 | + }, | |
| 102 | + "example": "Intelligence artificielle: que nous réserve 2026?" | |
| 103 | + }, | |
| 104 | + { | |
| 105 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.longBylineText.runs.[].text", | |
| 106 | + "semantic": { | |
| 107 | + "kind": "text", | |
| 108 | + "confidence": 0.85 | |
| 109 | + }, | |
| 110 | + "example": "TVA Nouvelles" | |
| 111 | + }, | |
| 112 | + { | |
| 113 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.longBylineText.runs.[].navigationEndpoint.browseEndpoint.browseId", | |
| 114 | + "semantic": { | |
| 115 | + "kind": "identifier", | |
| 116 | + "confidence": 0.95 | |
| 117 | + }, | |
| 118 | + "example": "UCgKzdfWnTie2OD4bfaZKaQw" | |
| 119 | + }, | |
| 120 | + { | |
| 121 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.publishedTimeText.simpleText", | |
| 122 | + "semantic": { | |
| 123 | + "kind": "text", | |
| 124 | + "confidence": 0.85 | |
| 125 | + }, | |
| 126 | + "example": "il y a 8 mois" | |
| 127 | + }, | |
| 128 | + { | |
| 129 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.lengthText.simpleText", | |
| 130 | + "semantic": { | |
| 131 | + "kind": "duration", | |
| 132 | + "confidence": 0.9 | |
| 133 | + }, | |
| 134 | + "example": "9:18" | |
| 135 | + }, | |
| 136 | + { | |
| 137 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.viewCountText.simpleText", | |
| 138 | + "semantic": { | |
| 139 | + "kind": "text", | |
| 140 | + "confidence": 0.85 | |
| 141 | + }, | |
| 142 | + "example": "17 686 visionnements" | |
| 143 | + }, | |
| 144 | + { | |
| 145 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.navigationEndpoint.watchEndpoint.videoId", | |
| 146 | + "semantic": { | |
| 147 | + "kind": "identifier", | |
| 148 | + "confidence": 0.95 | |
| 149 | + }, | |
| 150 | + "example": "1RqbM2LW3wE" | |
| 151 | + }, | |
| 152 | + { | |
| 153 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.navigationEndpoint.watchEndpoint.watchEndpointSupportedOnesieConfig.html5PlaybackOnesieConfig.commonConfig.url", | |
| 154 | + "semantic": { | |
| 155 | + "kind": "url", | |
| 156 | + "confidence": 0.9 | |
| 157 | + }, | |
| 158 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ay.googlevideo.com/initplayback?source=youtube&oei" | |
| 159 | + }, | |
| 160 | + { | |
| 161 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.ownerText.runs.[].text", | |
| 162 | + "semantic": { | |
| 163 | + "kind": "text", | |
| 164 | + "confidence": 0.85 | |
| 165 | + }, | |
| 166 | + "example": "TVA Nouvelles" | |
| 167 | + }, | |
| 168 | + { | |
| 169 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.ownerText.runs.[].navigationEndpoint.browseEndpoint.browseId", | |
| 170 | + "semantic": { | |
| 171 | + "kind": "identifier", | |
| 172 | + "confidence": 0.95 | |
| 173 | + }, | |
| 174 | + "example": "UCgKzdfWnTie2OD4bfaZKaQw" | |
| 175 | + }, | |
| 176 | + { | |
| 177 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.shortBylineText.runs.[].text", | |
| 178 | + "semantic": { | |
| 179 | + "kind": "text", | |
| 180 | + "confidence": 0.85 | |
| 181 | + }, | |
| 182 | + "example": "TVA Nouvelles" | |
| 183 | + }, | |
| 184 | + { | |
| 185 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.shortBylineText.runs.[].navigationEndpoint.browseEndpoint.browseId", | |
| 186 | + "semantic": { | |
| 187 | + "kind": "identifier", | |
| 188 | + "confidence": 0.95 | |
| 189 | + }, | |
| 190 | + "example": "UCgKzdfWnTie2OD4bfaZKaQw" | |
| 191 | + }, | |
| 192 | + { | |
| 193 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.showActionMenu", | |
| 194 | + "semantic": { | |
| 195 | + "kind": "boolean", | |
| 196 | + "confidence": 0.95 | |
| 197 | + }, | |
| 198 | + "example": "false" | |
| 199 | + }, | |
| 200 | + { | |
| 201 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.shortViewCountText.simpleText", | |
| 202 | + "semantic": { | |
| 203 | + "kind": "text", | |
| 204 | + "confidence": 0.85 | |
| 205 | + }, | |
| 206 | + "example": "17 k visionnements" | |
| 207 | + }, | |
| 208 | + { | |
| 209 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.menu.menuRenderer.items.[].menuServiceItemRenderer.text.runs.[].text", | |
| 210 | + "semantic": { | |
| 211 | + "kind": "text", | |
| 212 | + "confidence": 0.85 | |
| 213 | + }, | |
| 214 | + "example": "Ajouter à la file d'attente" | |
| 215 | + }, | |
| 216 | + { | |
| 217 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.menu.menuRenderer.items.[].menuServiceItemRenderer.serviceEndpoint.commandMetadata.webCommandMetadata.sendPost", | |
| 218 | + "semantic": { | |
| 219 | + "kind": "boolean", | |
| 220 | + "confidence": 0.95 | |
| 221 | + }, | |
| 222 | + "example": "true" | |
| 223 | + }, | |
| 224 | + { | |
| 225 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.menu.menuRenderer.items.[].menuServiceItemRenderer.serviceEndpoint.signalServiceEndpoint.actions.[].addToPlaylistCommand.openMiniplayer", | |
| 226 | + "semantic": { | |
| 227 | + "kind": "boolean", | |
| 228 | + "confidence": 0.95 | |
| 229 | + }, | |
| 230 | + "example": "true" | |
| 231 | + }, | |
| 232 | + { | |
| 233 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.menu.menuRenderer.items.[].menuServiceItemRenderer.serviceEndpoint.signalServiceEndpoint.actions.[].addToPlaylistCommand.videoId", | |
| 234 | + "semantic": { | |
| 235 | + "kind": "identifier", | |
| 236 | + "confidence": 0.95 | |
| 237 | + }, | |
| 238 | + "example": "1RqbM2LW3wE" | |
| 239 | + }, | |
| 240 | + { | |
| 241 | + "path": "contents.twoColumnSearchResultsRenderer.primaryContents.sectionListRenderer.contents.[].itemSectionRenderer.contents.[].videoRenderer.menu.menuRenderer.items.[].menuServiceItemRenderer.serviceEndpoint.signalServiceEndpoint.actions.[].addToPlaylistCommand.onCreateListCommand.commandMetadata.webCommandMetadata.sendPost", | |
| 242 | + "semantic": { | |
| 243 | + "kind": "boolean", | |
| 244 | + "confidence": 0.95 | |
| 245 | + }, | |
| 246 | + "example": "true" | |
| 247 | + } | |
| 248 | + ] | |
| 249 | + }, | |
| 250 | + { | |
| 251 | + "shape_hash": "ad92d388ecab3c2a", | |
| 252 | + "status": "provisional", | |
| 253 | + "hostname": "www.youtube.com", | |
| 254 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 255 | + "method": "POST", | |
| 256 | + "likely_entity_types": { | |
| 257 | + "video": 2, | |
| 258 | + "channel": 2, | |
| 259 | + "post": 2 | |
| 260 | + }, | |
| 261 | + "confidence": 0.39, | |
| 262 | + "triggered_by": { | |
| 263 | + "OPEN_VIDEO": 2 | |
| 264 | + }, | |
| 265 | + "key_fields": [ | |
| 266 | + { | |
| 267 | + "path": "[].responseContext.responseId", | |
| 268 | + "semantic": { | |
| 269 | + "kind": "identifier", | |
| 270 | + "confidence": 0.95 | |
| 271 | + }, | |
| 272 | + "example": "IhMIlbyp5rznlgMV4NSUCR0s4BSn" | |
| 273 | + }, | |
| 274 | + { | |
| 275 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 276 | + "semantic": { | |
| 277 | + "kind": "boolean", | |
| 278 | + "confidence": 0.95 | |
| 279 | + }, | |
| 280 | + "example": "true" | |
| 281 | + }, | |
| 282 | + { | |
| 283 | + "path": "[].playerResponse.responseContext.responseId", | |
| 284 | + "semantic": { | |
| 285 | + "kind": "identifier", | |
| 286 | + "confidence": 0.95 | |
| 287 | + }, | |
| 288 | + "example": "IhMIlbyp5rznlgMV4NSUCR0s4BSn" | |
| 289 | + }, | |
| 290 | + { | |
| 291 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 292 | + "semantic": { | |
| 293 | + "kind": "boolean", | |
| 294 | + "confidence": 0.95 | |
| 295 | + }, | |
| 296 | + "example": "true" | |
| 297 | + }, | |
| 298 | + { | |
| 299 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 300 | + "semantic": { | |
| 301 | + "kind": "boolean", | |
| 302 | + "confidence": 0.95 | |
| 303 | + }, | |
| 304 | + "example": "true" | |
| 305 | + }, | |
| 306 | + { | |
| 307 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 308 | + "semantic": { | |
| 309 | + "kind": "duration", | |
| 310 | + "confidence": 0.75 | |
| 311 | + }, | |
| 312 | + "example": "46763300" | |
| 313 | + }, | |
| 314 | + { | |
| 315 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 316 | + "semantic": { | |
| 317 | + "kind": "duration", | |
| 318 | + "confidence": 0.75 | |
| 319 | + }, | |
| 320 | + "example": "358591" | |
| 321 | + }, | |
| 322 | + { | |
| 323 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 324 | + "semantic": { | |
| 325 | + "kind": "boolean", | |
| 326 | + "confidence": 0.95 | |
| 327 | + }, | |
| 328 | + "example": "true" | |
| 329 | + }, | |
| 330 | + { | |
| 331 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 332 | + "semantic": { | |
| 333 | + "kind": "boolean", | |
| 334 | + "confidence": 0.95 | |
| 335 | + }, | |
| 336 | + "example": "true" | |
| 337 | + }, | |
| 338 | + { | |
| 339 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 340 | + "semantic": { | |
| 341 | + "kind": "boolean", | |
| 342 | + "confidence": 0.95 | |
| 343 | + }, | |
| 344 | + "example": "true" | |
| 345 | + }, | |
| 346 | + { | |
| 347 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 348 | + "semantic": { | |
| 349 | + "kind": "media_url", | |
| 350 | + "confidence": 0.93 | |
| 351 | + }, | |
| 352 | + "example": "https://rr2---sn-cxaaj5o5q5-t0ay.googlevideo.com/videoplayback?expire=1789183857" | |
| 353 | + }, | |
| 354 | + { | |
| 355 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 356 | + "semantic": { | |
| 357 | + "kind": "boolean", | |
| 358 | + "confidence": 0.95 | |
| 359 | + }, | |
| 360 | + "example": "true" | |
| 361 | + }, | |
| 362 | + { | |
| 363 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 364 | + "semantic": { | |
| 365 | + "kind": "boolean", | |
| 366 | + "confidence": 0.95 | |
| 367 | + }, | |
| 368 | + "example": "true" | |
| 369 | + }, | |
| 370 | + { | |
| 371 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 372 | + "semantic": { | |
| 373 | + "kind": "boolean", | |
| 374 | + "confidence": 0.95 | |
| 375 | + }, | |
| 376 | + "example": "true" | |
| 377 | + }, | |
| 378 | + { | |
| 379 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 380 | + "semantic": { | |
| 381 | + "kind": "boolean", | |
| 382 | + "confidence": 0.95 | |
| 383 | + }, | |
| 384 | + "example": "true" | |
| 385 | + }, | |
| 386 | + { | |
| 387 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 388 | + "semantic": { | |
| 389 | + "kind": "url", | |
| 390 | + "confidence": 0.9 | |
| 391 | + }, | |
| 392 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=TtU427br-sk&ei=EXOka" | |
| 393 | + }, | |
| 394 | + { | |
| 395 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 396 | + "semantic": { | |
| 397 | + "kind": "url", | |
| 398 | + "confidence": 0.9 | |
| 399 | + }, | |
| 400 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=TtU427br-sk&ei=EXOk" | |
| 401 | + }, | |
| 402 | + { | |
| 403 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 404 | + "semantic": { | |
| 405 | + "kind": "url", | |
| 406 | + "confidence": 0.9 | |
| 407 | + }, | |
| 408 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=TtU427br-sk&ei=EXOk" | |
| 409 | + }, | |
| 410 | + { | |
| 411 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 412 | + "semantic": { | |
| 413 | + "kind": "url", | |
| 414 | + "confidence": 0.9 | |
| 415 | + }, | |
| 416 | + "example": "https://www.youtube.com/ptracking?ei=EXOkatXvEOCp0_wPrMDTuAo&oid=m-Wz5fxH9AWP_fn" | |
| 417 | + }, | |
| 418 | + { | |
| 419 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 420 | + "semantic": { | |
| 421 | + "kind": "url", | |
| 422 | + "confidence": 0.9 | |
| 423 | + }, | |
| 424 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=TtU427br-sk&ei=EXOkatXvEO" | |
| 425 | + }, | |
| 426 | + { | |
| 427 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 428 | + "semantic": { | |
| 429 | + "kind": "url", | |
| 430 | + "confidence": 0.9 | |
| 431 | + }, | |
| 432 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=TtU427br-sk&ei=EXOkatXvEOCp0_wPr" | |
| 433 | + }, | |
| 434 | + { | |
| 435 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 436 | + "semantic": { | |
| 437 | + "kind": "timestamp", | |
| 438 | + "confidence": 0.7 | |
| 439 | + }, | |
| 440 | + "example": "5" | |
| 441 | + }, | |
| 442 | + { | |
| 443 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 444 | + "semantic": { | |
| 445 | + "kind": "url", | |
| 446 | + "confidence": 0.9 | |
| 447 | + }, | |
| 448 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 449 | + }, | |
| 450 | + { | |
| 451 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.elapsedMediaTimeSeconds", | |
| 452 | + "semantic": { | |
| 453 | + "kind": "timestamp", | |
| 454 | + "confidence": 0.7 | |
| 455 | + }, | |
| 456 | + "example": "0" | |
| 457 | + }, | |
| 458 | + { | |
| 459 | + "path": "[].playerResponse.playbackTracking.googleRemarketingUrl.baseUrl", | |
| 460 | + "semantic": { | |
| 461 | + "kind": "url", | |
| 462 | + "confidence": 0.9 | |
| 463 | + }, | |
| 464 | + "example": "https://www.google.com/pagead/1p-user-list/962985656/?backend=innertube&cname=1&" | |
| 465 | + } | |
| 466 | + ] | |
| 467 | + }, | |
| 468 | + { | |
| 469 | + "shape_hash": "b8cf160ca9b65b8a", | |
| 470 | + "status": "provisional", | |
| 471 | + "hostname": "www.youtube.com", | |
| 472 | + "path_pattern": "/youtubei/v1/next", | |
| 473 | + "method": "POST", | |
| 474 | + "likely_entity_types": { | |
| 475 | + "post": 2, | |
| 476 | + "comment": 2, | |
| 477 | + "channel": 2, | |
| 478 | + "video": 2 | |
| 479 | + }, | |
| 480 | + "confidence": 0.39, | |
| 481 | + "triggered_by": { | |
| 482 | + "OPEN_VIDEO": 2 | |
| 483 | + }, | |
| 484 | + "key_fields": [ | |
| 485 | + { | |
| 486 | + "path": "responseContext.mainAppWebResponseContext.loggedOut", | |
| 487 | + "semantic": { | |
| 488 | + "kind": "boolean", | |
| 489 | + "confidence": 0.95 | |
| 490 | + }, | |
| 491 | + "example": "true" | |
| 492 | + }, | |
| 493 | + { | |
| 494 | + "path": "responseContext.responseId", | |
| 495 | + "semantic": { | |
| 496 | + "kind": "identifier", | |
| 497 | + "confidence": 0.95 | |
| 498 | + }, | |
| 499 | + "example": "IhMI3--M57znlgMVqRHLBB0OIjPf" | |
| 500 | + }, | |
| 501 | + { | |
| 502 | + "path": "responseContext.webResponseContextExtensionData.hasDecorated", | |
| 503 | + "semantic": { | |
| 504 | + "kind": "boolean", | |
| 505 | + "confidence": 0.95 | |
| 506 | + }, | |
| 507 | + "example": "true" | |
| 508 | + }, | |
| 509 | + { | |
| 510 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.targetId", | |
| 511 | + "semantic": { | |
| 512 | + "kind": "identifier", | |
| 513 | + "confidence": 0.95 | |
| 514 | + }, | |
| 515 | + "example": "comments-section" | |
| 516 | + }, | |
| 517 | + { | |
| 518 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.authorThumbnail.thumbnails.[].url", | |
| 519 | + "semantic": { | |
| 520 | + "kind": "url", | |
| 521 | + "confidence": 0.9 | |
| 522 | + }, | |
| 523 | + "example": "https://yt3.ggpht.com/a/default-user=s48-c-k-c0x00ffffff-no-rj" | |
| 524 | + }, | |
| 525 | + { | |
| 526 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.placeholderText.runs.[].text", | |
| 527 | + "semantic": { | |
| 528 | + "kind": "text", | |
| 529 | + "confidence": 0.85 | |
| 530 | + }, | |
| 531 | + "example": "Ajouter un commentaire…" | |
| 532 | + }, | |
| 533 | + { | |
| 534 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.prepareAccountEndpoint.commandMetadata.webCommandMetadata.ignoreNavigation", | |
| 535 | + "semantic": { | |
| 536 | + "kind": "boolean", | |
| 537 | + "confidence": 0.95 | |
| 538 | + }, | |
| 539 | + "example": "true" | |
| 540 | + }, | |
| 541 | + { | |
| 542 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.prepareAccountEndpoint.modalEndpoint.modal.modalWithTitleAndButtonRenderer.title.runs.[].text", | |
| 543 | + "semantic": { | |
| 544 | + "kind": "text", | |
| 545 | + "confidence": 0.85 | |
| 546 | + }, | |
| 547 | + "example": "Vous voulez vous joindre à la conversation?" | |
| 548 | + }, | |
| 549 | + { | |
| 550 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.prepareAccountEndpoint.modalEndpoint.modal.modalWithTitleAndButtonRenderer.content.runs.[].text", | |
| 551 | + "semantic": { | |
| 552 | + "kind": "text", | |
| 553 | + "confidence": 0.85 | |
| 554 | + }, | |
| 555 | + "example": "Connectez-vous pour continuer" | |
| 556 | + }, | |
| 557 | + { | |
| 558 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.prepareAccountEndpoint.modalEndpoint.modal.modalWithTitleAndButtonRenderer.button.buttonRenderer.isDisabled", | |
| 559 | + "semantic": { | |
| 560 | + "kind": "boolean", | |
| 561 | + "confidence": 0.95 | |
| 562 | + }, | |
| 563 | + "example": "false" | |
| 564 | + }, | |
| 565 | + { | |
| 566 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.prepareAccountEndpoint.modalEndpoint.modal.modalWithTitleAndButtonRenderer.button.buttonRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url", | |
| 567 | + "semantic": { | |
| 568 | + "kind": "url", | |
| 569 | + "confidence": 0.9 | |
| 570 | + }, | |
| 571 | + "example": "https://accounts.google.com/ServiceLogin?service=youtube&uilel=3&passive=true&co" | |
| 572 | + }, | |
| 573 | + { | |
| 574 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.prepareAccountEndpoint.modalEndpoint.modal.modalWithTitleAndButtonRenderer.button.buttonRenderer.navigationEndpoint.signInEndpoint.hack", | |
| 575 | + "semantic": { | |
| 576 | + "kind": "boolean", | |
| 577 | + "confidence": 0.95 | |
| 578 | + }, | |
| 579 | + "example": "true" | |
| 580 | + }, | |
| 581 | + { | |
| 582 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.emojiPicker.emojiPickerRenderer.categories.[].emojiPickerCategoryRenderer.imageLoadingLazy", | |
| 583 | + "semantic": { | |
| 584 | + "kind": "boolean", | |
| 585 | + "confidence": 0.95 | |
| 586 | + }, | |
| 587 | + "example": "true" | |
| 588 | + }, | |
| 589 | + { | |
| 590 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.emojiPicker.emojiPickerRenderer.categoryButtons.[].emojiPickerCategoryButtonRenderer.targetId", | |
| 591 | + "semantic": { | |
| 592 | + "kind": "identifier", | |
| 593 | + "confidence": 0.95 | |
| 594 | + }, | |
| 595 | + "example": "emoji-picker-category-button-people" | |
| 596 | + }, | |
| 597 | + { | |
| 598 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.emojiPicker.emojiPickerRenderer.searchPlaceholderText.runs.[].text", | |
| 599 | + "semantic": { | |
| 600 | + "kind": "text", | |
| 601 | + "confidence": 0.85 | |
| 602 | + }, | |
| 603 | + "example": "Rechercher un emoji" | |
| 604 | + }, | |
| 605 | + { | |
| 606 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.emojiPicker.emojiPickerRenderer.searchNoResultsText.runs.[].text", | |
| 607 | + "semantic": { | |
| 608 | + "kind": "text", | |
| 609 | + "confidence": 0.85 | |
| 610 | + }, | |
| 611 | + "example": "Aucun émoji trouvé" | |
| 612 | + }, | |
| 613 | + { | |
| 614 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.emojiPicker.emojiPickerRenderer.pickSkinToneText.runs.[].text", | |
| 615 | + "semantic": { | |
| 616 | + "kind": "text", | |
| 617 | + "confidence": 0.85 | |
| 618 | + }, | |
| 619 | + "example": "Sélectionner le teint de l'emoji" | |
| 620 | + }, | |
| 621 | + { | |
| 622 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.disabledText", | |
| 623 | + "semantic": { | |
| 624 | + "kind": "text", | |
| 625 | + "confidence": 0.85 | |
| 626 | + }, | |
| 627 | + "example": "Les commentaires sont désactivés." | |
| 628 | + }, | |
| 629 | + { | |
| 630 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.createRenderer.commentSimpleboxRenderer.disabledTextUrl", | |
| 631 | + "semantic": { | |
| 632 | + "kind": "url", | |
| 633 | + "confidence": 0.9 | |
| 634 | + }, | |
| 635 | + "example": "https://support.google.com/youtube/answer/9706180" | |
| 636 | + }, | |
| 637 | + { | |
| 638 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.sortMenu.sortFilterSubMenuRenderer.subMenuItems.[].title", | |
| 639 | + "semantic": { | |
| 640 | + "kind": "title", | |
| 641 | + "confidence": 0.85 | |
| 642 | + }, | |
| 643 | + "example": "Principaux" | |
| 644 | + }, | |
| 645 | + { | |
| 646 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.sortMenu.sortFilterSubMenuRenderer.subMenuItems.[].selected", | |
| 647 | + "semantic": { | |
| 648 | + "kind": "boolean", | |
| 649 | + "confidence": 0.95 | |
| 650 | + }, | |
| 651 | + "example": "true" | |
| 652 | + }, | |
| 653 | + { | |
| 654 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.sortMenu.sortFilterSubMenuRenderer.subMenuItems.[].serviceEndpoint.commandMetadata.webCommandMetadata.sendPost", | |
| 655 | + "semantic": { | |
| 656 | + "kind": "boolean", | |
| 657 | + "confidence": 0.95 | |
| 658 | + }, | |
| 659 | + "example": "true" | |
| 660 | + }, | |
| 661 | + { | |
| 662 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.sortMenu.sortFilterSubMenuRenderer.subMenuItems.[].serviceEndpoint.continuationCommand.command.showReloadUiCommand.targetId", | |
| 663 | + "semantic": { | |
| 664 | + "kind": "identifier", | |
| 665 | + "confidence": 0.95 | |
| 666 | + }, | |
| 667 | + "example": "comments-section" | |
| 668 | + }, | |
| 669 | + { | |
| 670 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.sortMenu.sortFilterSubMenuRenderer.subMenuItems.[].subtitle", | |
| 671 | + "semantic": { | |
| 672 | + "kind": "title", | |
| 673 | + "confidence": 0.85 | |
| 674 | + }, | |
| 675 | + "example": "Affiche les commentaires en vedette" | |
| 676 | + }, | |
| 677 | + { | |
| 678 | + "path": "onResponseReceivedEndpoints.[].reloadContinuationItemsCommand.continuationItems.[].commentsHeaderRenderer.sortMenu.sortFilterSubMenuRenderer.title", | |
| 679 | + "semantic": { | |
| 680 | + "kind": "title", | |
| 681 | + "confidence": 0.85 | |
| 682 | + }, | |
| 683 | + "example": "Trier par" | |
| 684 | + } | |
| 685 | + ] | |
| 686 | + }, | |
| 687 | + { | |
| 688 | + "shape_hash": "094367e8f9ba7873", | |
| 689 | + "status": "provisional", | |
| 690 | + "hostname": "www.youtube.com", | |
| 691 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 692 | + "method": "POST", | |
| 693 | + "likely_entity_types": { | |
| 694 | + "video": 2, | |
| 695 | + "channel": 2, | |
| 696 | + "post": 2 | |
| 697 | + }, | |
| 698 | + "confidence": 0.39, | |
| 699 | + "triggered_by": { | |
| 700 | + "OPEN_VIDEO": 2 | |
| 701 | + }, | |
| 702 | + "key_fields": [ | |
| 703 | + { | |
| 704 | + "path": "[].responseContext.responseId", | |
| 705 | + "semantic": { | |
| 706 | + "kind": "identifier", | |
| 707 | + "confidence": 0.95 | |
| 708 | + }, | |
| 709 | + "example": "IhMIvqLNhb3nlgMVloTkBh0Cth8C" | |
| 710 | + }, | |
| 711 | + { | |
| 712 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 713 | + "semantic": { | |
| 714 | + "kind": "boolean", | |
| 715 | + "confidence": 0.95 | |
| 716 | + }, | |
| 717 | + "example": "true" | |
| 718 | + }, | |
| 719 | + { | |
| 720 | + "path": "[].playerResponse.responseContext.responseId", | |
| 721 | + "semantic": { | |
| 722 | + "kind": "identifier", | |
| 723 | + "confidence": 0.95 | |
| 724 | + }, | |
| 725 | + "example": "IhMIvqLNhb3nlgMVloTkBh0Cth8C" | |
| 726 | + }, | |
| 727 | + { | |
| 728 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 729 | + "semantic": { | |
| 730 | + "kind": "boolean", | |
| 731 | + "confidence": 0.95 | |
| 732 | + }, | |
| 733 | + "example": "true" | |
| 734 | + }, | |
| 735 | + { | |
| 736 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 737 | + "semantic": { | |
| 738 | + "kind": "boolean", | |
| 739 | + "confidence": 0.95 | |
| 740 | + }, | |
| 741 | + "example": "true" | |
| 742 | + }, | |
| 743 | + { | |
| 744 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 745 | + "semantic": { | |
| 746 | + "kind": "duration", | |
| 747 | + "confidence": 0.75 | |
| 748 | + }, | |
| 749 | + "example": "54128576" | |
| 750 | + }, | |
| 751 | + { | |
| 752 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 753 | + "semantic": { | |
| 754 | + "kind": "duration", | |
| 755 | + "confidence": 0.75 | |
| 756 | + }, | |
| 757 | + "example": "184050" | |
| 758 | + }, | |
| 759 | + { | |
| 760 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 761 | + "semantic": { | |
| 762 | + "kind": "boolean", | |
| 763 | + "confidence": 0.95 | |
| 764 | + }, | |
| 765 | + "example": "true" | |
| 766 | + }, | |
| 767 | + { | |
| 768 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 769 | + "semantic": { | |
| 770 | + "kind": "boolean", | |
| 771 | + "confidence": 0.95 | |
| 772 | + }, | |
| 773 | + "example": "true" | |
| 774 | + }, | |
| 775 | + { | |
| 776 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 777 | + "semantic": { | |
| 778 | + "kind": "boolean", | |
| 779 | + "confidence": 0.95 | |
| 780 | + }, | |
| 781 | + "example": "true" | |
| 782 | + }, | |
| 783 | + { | |
| 784 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 785 | + "semantic": { | |
| 786 | + "kind": "media_url", | |
| 787 | + "confidence": 0.93 | |
| 788 | + }, | |
| 789 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ar.googlevideo.com/videoplayback?expire=1789183922" | |
| 790 | + }, | |
| 791 | + { | |
| 792 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 793 | + "semantic": { | |
| 794 | + "kind": "boolean", | |
| 795 | + "confidence": 0.95 | |
| 796 | + }, | |
| 797 | + "example": "true" | |
| 798 | + }, | |
| 799 | + { | |
| 800 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 801 | + "semantic": { | |
| 802 | + "kind": "boolean", | |
| 803 | + "confidence": 0.95 | |
| 804 | + }, | |
| 805 | + "example": "true" | |
| 806 | + }, | |
| 807 | + { | |
| 808 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 809 | + "semantic": { | |
| 810 | + "kind": "boolean", | |
| 811 | + "confidence": 0.95 | |
| 812 | + }, | |
| 813 | + "example": "true" | |
| 814 | + }, | |
| 815 | + { | |
| 816 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 817 | + "semantic": { | |
| 818 | + "kind": "boolean", | |
| 819 | + "confidence": 0.95 | |
| 820 | + }, | |
| 821 | + "example": "true" | |
| 822 | + }, | |
| 823 | + { | |
| 824 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 825 | + "semantic": { | |
| 826 | + "kind": "url", | |
| 827 | + "confidence": 0.9 | |
| 828 | + }, | |
| 829 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=hmG52cE7S5U&ei=UnOka" | |
| 830 | + }, | |
| 831 | + { | |
| 832 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 833 | + "semantic": { | |
| 834 | + "kind": "url", | |
| 835 | + "confidence": 0.9 | |
| 836 | + }, | |
| 837 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=hmG52cE7S5U&ei=UnOk" | |
| 838 | + }, | |
| 839 | + { | |
| 840 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 841 | + "semantic": { | |
| 842 | + "kind": "url", | |
| 843 | + "confidence": 0.9 | |
| 844 | + }, | |
| 845 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=hmG52cE7S5U&ei=UnOk" | |
| 846 | + }, | |
| 847 | + { | |
| 848 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 849 | + "semantic": { | |
| 850 | + "kind": "url", | |
| 851 | + "confidence": 0.9 | |
| 852 | + }, | |
| 853 | + "example": "https://www.youtube.com/ptracking?ei=UnOkar6xNZaJkucPguz-EA&oid=WLeG0gE_kkpZY0oK" | |
| 854 | + }, | |
| 855 | + { | |
| 856 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 857 | + "semantic": { | |
| 858 | + "kind": "url", | |
| 859 | + "confidence": 0.9 | |
| 860 | + }, | |
| 861 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=hmG52cE7S5U&ei=UnOkar6xNZ" | |
| 862 | + }, | |
| 863 | + { | |
| 864 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 865 | + "semantic": { | |
| 866 | + "kind": "url", | |
| 867 | + "confidence": 0.9 | |
| 868 | + }, | |
| 869 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=hmG52cE7S5U&ei=UnOkar6xNZaJkucPg" | |
| 870 | + }, | |
| 871 | + { | |
| 872 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 873 | + "semantic": { | |
| 874 | + "kind": "timestamp", | |
| 875 | + "confidence": 0.7 | |
| 876 | + }, | |
| 877 | + "example": "5" | |
| 878 | + }, | |
| 879 | + { | |
| 880 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 881 | + "semantic": { | |
| 882 | + "kind": "url", | |
| 883 | + "confidence": 0.9 | |
| 884 | + }, | |
| 885 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 886 | + }, | |
| 887 | + { | |
| 888 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.elapsedMediaTimeSeconds", | |
| 889 | + "semantic": { | |
| 890 | + "kind": "timestamp", | |
| 891 | + "confidence": 0.7 | |
| 892 | + }, | |
| 893 | + "example": "0" | |
| 894 | + }, | |
| 895 | + { | |
| 896 | + "path": "[].playerResponse.playbackTracking.googleRemarketingUrl.baseUrl", | |
| 897 | + "semantic": { | |
| 898 | + "kind": "url", | |
| 899 | + "confidence": 0.9 | |
| 900 | + }, | |
| 901 | + "example": "https://www.google.com/pagead/1p-user-list/962985656/?backend=innertube&cname=1&" | |
| 902 | + } | |
| 903 | + ] | |
| 904 | + }, | |
| 905 | + { | |
| 906 | + "shape_hash": "ffec634da7d6e380", | |
| 907 | + "status": "provisional", | |
| 908 | + "hostname": "www.youtube.com", | |
| 909 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 910 | + "method": "POST", | |
| 911 | + "likely_entity_types": { | |
| 912 | + "video": 2, | |
| 913 | + "channel": 2, | |
| 914 | + "post": 2 | |
| 915 | + }, | |
| 916 | + "confidence": 0.39, | |
| 917 | + "triggered_by": { | |
| 918 | + "OPEN_VIDEO": 2 | |
| 919 | + }, | |
| 920 | + "key_fields": [ | |
| 921 | + { | |
| 922 | + "path": "[].responseContext.responseId", | |
| 923 | + "semantic": { | |
| 924 | + "kind": "identifier", | |
| 925 | + "confidence": 0.95 | |
| 926 | + }, | |
| 927 | + "example": "IhMI2by7kr3nlgMVNYHkBh23ayjA" | |
| 928 | + }, | |
| 929 | + { | |
| 930 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 931 | + "semantic": { | |
| 932 | + "kind": "boolean", | |
| 933 | + "confidence": 0.95 | |
| 934 | + }, | |
| 935 | + "example": "true" | |
| 936 | + }, | |
| 937 | + { | |
| 938 | + "path": "[].playerResponse.responseContext.responseId", | |
| 939 | + "semantic": { | |
| 940 | + "kind": "identifier", | |
| 941 | + "confidence": 0.95 | |
| 942 | + }, | |
| 943 | + "example": "IhMI2by7kr3nlgMVNYHkBh23ayjA" | |
| 944 | + }, | |
| 945 | + { | |
| 946 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 947 | + "semantic": { | |
| 948 | + "kind": "boolean", | |
| 949 | + "confidence": 0.95 | |
| 950 | + }, | |
| 951 | + "example": "true" | |
| 952 | + }, | |
| 953 | + { | |
| 954 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 955 | + "semantic": { | |
| 956 | + "kind": "boolean", | |
| 957 | + "confidence": 0.95 | |
| 958 | + }, | |
| 959 | + "example": "true" | |
| 960 | + }, | |
| 961 | + { | |
| 962 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 963 | + "semantic": { | |
| 964 | + "kind": "duration", | |
| 965 | + "confidence": 0.75 | |
| 966 | + }, | |
| 967 | + "example": "523368691" | |
| 968 | + }, | |
| 969 | + { | |
| 970 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 971 | + "semantic": { | |
| 972 | + "kind": "duration", | |
| 973 | + "confidence": 0.75 | |
| 974 | + }, | |
| 975 | + "example": "2348466" | |
| 976 | + }, | |
| 977 | + { | |
| 978 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 979 | + "semantic": { | |
| 980 | + "kind": "boolean", | |
| 981 | + "confidence": 0.95 | |
| 982 | + }, | |
| 983 | + "example": "true" | |
| 984 | + }, | |
| 985 | + { | |
| 986 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 987 | + "semantic": { | |
| 988 | + "kind": "boolean", | |
| 989 | + "confidence": 0.95 | |
| 990 | + }, | |
| 991 | + "example": "true" | |
| 992 | + }, | |
| 993 | + { | |
| 994 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 995 | + "semantic": { | |
| 996 | + "kind": "boolean", | |
| 997 | + "confidence": 0.95 | |
| 998 | + }, | |
| 999 | + "example": "true" | |
| 1000 | + }, | |
| 1001 | + { | |
| 1002 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 1003 | + "semantic": { | |
| 1004 | + "kind": "media_url", | |
| 1005 | + "confidence": 0.93 | |
| 1006 | + }, | |
| 1007 | + "example": "https://rr4---sn-cxaaj5o5q5-t0ay.googlevideo.com/videoplayback?expire=1789183949" | |
| 1008 | + }, | |
| 1009 | + { | |
| 1010 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 1011 | + "semantic": { | |
| 1012 | + "kind": "boolean", | |
| 1013 | + "confidence": 0.95 | |
| 1014 | + }, | |
| 1015 | + "example": "true" | |
| 1016 | + }, | |
| 1017 | + { | |
| 1018 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 1019 | + "semantic": { | |
| 1020 | + "kind": "boolean", | |
| 1021 | + "confidence": 0.95 | |
| 1022 | + }, | |
| 1023 | + "example": "true" | |
| 1024 | + }, | |
| 1025 | + { | |
| 1026 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 1027 | + "semantic": { | |
| 1028 | + "kind": "boolean", | |
| 1029 | + "confidence": 0.95 | |
| 1030 | + }, | |
| 1031 | + "example": "true" | |
| 1032 | + }, | |
| 1033 | + { | |
| 1034 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 1035 | + "semantic": { | |
| 1036 | + "kind": "boolean", | |
| 1037 | + "confidence": 0.95 | |
| 1038 | + }, | |
| 1039 | + "example": "true" | |
| 1040 | + }, | |
| 1041 | + { | |
| 1042 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 1043 | + "semantic": { | |
| 1044 | + "kind": "url", | |
| 1045 | + "confidence": 0.9 | |
| 1046 | + }, | |
| 1047 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=QYRiiWEsTeI&ei=bXOka" | |
| 1048 | + }, | |
| 1049 | + { | |
| 1050 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 1051 | + "semantic": { | |
| 1052 | + "kind": "url", | |
| 1053 | + "confidence": 0.9 | |
| 1054 | + }, | |
| 1055 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=QYRiiWEsTeI&ei=bXOk" | |
| 1056 | + }, | |
| 1057 | + { | |
| 1058 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 1059 | + "semantic": { | |
| 1060 | + "kind": "url", | |
| 1061 | + "confidence": 0.9 | |
| 1062 | + }, | |
| 1063 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=QYRiiWEsTeI&ei=bXOk" | |
| 1064 | + }, | |
| 1065 | + { | |
| 1066 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 1067 | + "semantic": { | |
| 1068 | + "kind": "url", | |
| 1069 | + "confidence": 0.9 | |
| 1070 | + }, | |
| 1071 | + "example": "https://www.youtube.com/ptracking?ei=bXOkapnSM7WCkucPt9ehgQw&oid=XG4oyHHzHGAFkGU" | |
| 1072 | + }, | |
| 1073 | + { | |
| 1074 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 1075 | + "semantic": { | |
| 1076 | + "kind": "url", | |
| 1077 | + "confidence": 0.9 | |
| 1078 | + }, | |
| 1079 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=QYRiiWEsTeI&ei=bXOkapnSM7" | |
| 1080 | + }, | |
| 1081 | + { | |
| 1082 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 1083 | + "semantic": { | |
| 1084 | + "kind": "url", | |
| 1085 | + "confidence": 0.9 | |
| 1086 | + }, | |
| 1087 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=QYRiiWEsTeI&ei=bXOkapnSM7WCkucPt" | |
| 1088 | + }, | |
| 1089 | + { | |
| 1090 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 1091 | + "semantic": { | |
| 1092 | + "kind": "timestamp", | |
| 1093 | + "confidence": 0.7 | |
| 1094 | + }, | |
| 1095 | + "example": "5" | |
| 1096 | + }, | |
| 1097 | + { | |
| 1098 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 1099 | + "semantic": { | |
| 1100 | + "kind": "url", | |
| 1101 | + "confidence": 0.9 | |
| 1102 | + }, | |
| 1103 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 1104 | + }, | |
| 1105 | + { | |
| 1106 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.elapsedMediaTimeSeconds", | |
| 1107 | + "semantic": { | |
| 1108 | + "kind": "timestamp", | |
| 1109 | + "confidence": 0.7 | |
| 1110 | + }, | |
| 1111 | + "example": "0" | |
| 1112 | + }, | |
| 1113 | + { | |
| 1114 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].baseUrl", | |
| 1115 | + "semantic": { | |
| 1116 | + "kind": "url", | |
| 1117 | + "confidence": 0.9 | |
| 1118 | + }, | |
| 1119 | + "example": "https://www.youtube.com/api/timedtext?v=QYRiiWEsTeI&ei=bXOkapnSM7WCkucPt9ehgQw&c" | |
| 1120 | + } | |
| 1121 | + ] | |
| 1122 | + }, | |
| 1123 | + { | |
| 1124 | + "shape_hash": "9a96a965bd5dc746", | |
| 1125 | + "status": "provisional", | |
| 1126 | + "hostname": "www.youtube.com", | |
| 1127 | + "path_pattern": "/youtubei/v1/guide", | |
| 1128 | + "method": "POST", | |
| 1129 | + "likely_entity_types": { | |
| 1130 | + "video": 3, | |
| 1131 | + "post": 3 | |
| 1132 | + }, | |
| 1133 | + "confidence": 0.29, | |
| 1134 | + "triggered_by": { | |
| 1135 | + "RETURN_TO_FEED": 2, | |
| 1136 | + "OPEN_CHANNEL": 1 | |
| 1137 | + }, | |
| 1138 | + "key_fields": [ | |
| 1139 | + { | |
| 1140 | + "path": "responseContext.mainAppWebResponseContext.loggedOut", | |
| 1141 | + "semantic": { | |
| 1142 | + "kind": "boolean", | |
| 1143 | + "confidence": 0.95 | |
| 1144 | + }, | |
| 1145 | + "example": "true" | |
| 1146 | + }, | |
| 1147 | + { | |
| 1148 | + "path": "responseContext.responseId", | |
| 1149 | + "semantic": { | |
| 1150 | + "kind": "identifier", | |
| 1151 | + "confidence": 0.95 | |
| 1152 | + }, | |
| 1153 | + "example": "IhMI2IilqbznlgMVio_lBx1nLSES" | |
| 1154 | + }, | |
| 1155 | + { | |
| 1156 | + "path": "responseContext.webResponseContextExtensionData.hasDecorated", | |
| 1157 | + "semantic": { | |
| 1158 | + "kind": "boolean", | |
| 1159 | + "confidence": 0.95 | |
| 1160 | + }, | |
| 1161 | + "example": "true" | |
| 1162 | + }, | |
| 1163 | + { | |
| 1164 | + "path": "items.[].guideSectionRenderer.items.[].guideEntryRenderer.navigationEndpoint.browseEndpoint.browseId", | |
| 1165 | + "semantic": { | |
| 1166 | + "kind": "identifier", | |
| 1167 | + "confidence": 0.95 | |
| 1168 | + }, | |
| 1169 | + "example": "FEwhat_to_watch" | |
| 1170 | + }, | |
| 1171 | + { | |
| 1172 | + "path": "items.[].guideSectionRenderer.items.[].guideEntryRenderer.isPrimary", | |
| 1173 | + "semantic": { | |
| 1174 | + "kind": "boolean", | |
| 1175 | + "confidence": 0.95 | |
| 1176 | + }, | |
| 1177 | + "example": "true" | |
| 1178 | + }, | |
| 1179 | + { | |
| 1180 | + "path": "items.[].guideSectionRenderer.items.[].guideEntryRenderer.serviceEndpoint.reelWatchEndpoint.updateKey", | |
| 1181 | + "semantic": { | |
| 1182 | + "kind": "timestamp", | |
| 1183 | + "confidence": 0.7 | |
| 1184 | + }, | |
| 1185 | + "example": "EhhTSE9SVFNfU0VFRExFU1NfRU5EUE9JTlQg5gEoAQ%3D%3D" | |
| 1186 | + }, | |
| 1187 | + { | |
| 1188 | + "path": "items.[].guideSectionRenderer.items.[].guideEntryRenderer.entryData.guideEntryData.guideEntryId", | |
| 1189 | + "semantic": { | |
| 1190 | + "kind": "identifier", | |
| 1191 | + "confidence": 0.95 | |
| 1192 | + }, | |
| 1193 | + "example": "subscriptions-channels-header" | |
| 1194 | + }, | |
| 1195 | + { | |
| 1196 | + "path": "items.[].guideSectionRenderer.items.[].guideEntryRenderer.targetId", | |
| 1197 | + "semantic": { | |
| 1198 | + "kind": "identifier", | |
| 1199 | + "confidence": 0.95 | |
| 1200 | + }, | |
| 1201 | + "example": "subscriptions-guide-item" | |
| 1202 | + }, | |
| 1203 | + { | |
| 1204 | + "path": "items.[].guideSigninPromoRenderer.descriptiveText.simpleText", | |
| 1205 | + "semantic": { | |
| 1206 | + "kind": "text", | |
| 1207 | + "confidence": 0.85 | |
| 1208 | + }, | |
| 1209 | + "example": "Connectez-vous pour aimer des vidéos, publier des commentaires et vous abonner." | |
| 1210 | + }, | |
| 1211 | + { | |
| 1212 | + "path": "items.[].guideSigninPromoRenderer.signInButton.buttonRenderer.isDisabled", | |
| 1213 | + "semantic": { | |
| 1214 | + "kind": "boolean", | |
| 1215 | + "confidence": 0.95 | |
| 1216 | + }, | |
| 1217 | + "example": "false" | |
| 1218 | + }, | |
| 1219 | + { | |
| 1220 | + "path": "items.[].guideSigninPromoRenderer.signInButton.buttonRenderer.navigationEndpoint.commandMetadata.webCommandMetadata.url", | |
| 1221 | + "semantic": { | |
| 1222 | + "kind": "url", | |
| 1223 | + "confidence": 0.9 | |
| 1224 | + }, | |
| 1225 | + "example": "https://accounts.google.com/ServiceLogin?service=youtube&uilel=3&passive=true&co" | |
| 1226 | + }, | |
| 1227 | + { | |
| 1228 | + "path": "items.[].guideSigninPromoRenderer.signInButton.buttonRenderer.navigationEndpoint.signInEndpoint.hack", | |
| 1229 | + "semantic": { | |
| 1230 | + "kind": "boolean", | |
| 1231 | + "confidence": 0.95 | |
| 1232 | + }, | |
| 1233 | + "example": "true" | |
| 1234 | + }, | |
| 1235 | + { | |
| 1236 | + "path": "items.[].guideSectionRenderer.items.[].guideCollapsibleEntryRenderer.expandableItems.[].guideEntryRenderer.navigationEndpoint.browseEndpoint.browseId", | |
| 1237 | + "semantic": { | |
| 1238 | + "kind": "identifier", | |
| 1239 | + "confidence": 0.95 | |
| 1240 | + }, | |
| 1241 | + "example": "UCYfdidRxbB8Qhf0Nx7ioOYw" | |
| 1242 | + }, | |
| 1243 | + { | |
| 1244 | + "path": "items.[].guideSectionRenderer.formattedTitle.simpleText", | |
| 1245 | + "semantic": { | |
| 1246 | + "kind": "text", | |
| 1247 | + "confidence": 0.85 | |
| 1248 | + }, | |
| 1249 | + "example": "Explorer" | |
| 1250 | + }, | |
| 1251 | + { | |
| 1252 | + "path": "items.[].guideSectionRenderer.items.[].guideEntryRenderer.navigationEndpoint.urlEndpoint.url", | |
| 1253 | + "semantic": { | |
| 1254 | + "kind": "url", | |
| 1255 | + "confidence": 0.9 | |
| 1256 | + }, | |
| 1257 | + "example": "https://music.youtube.com" | |
| 1258 | + } | |
| 1259 | + ] | |
| 1260 | + }, | |
| 1261 | + { | |
| 1262 | + "shape_hash": "3c89ecaf0eb838d0", | |
| 1263 | + "status": "provisional", | |
| 1264 | + "hostname": "www.youtube.com", | |
| 1265 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 1266 | + "method": "POST", | |
| 1267 | + "likely_entity_types": { | |
| 1268 | + "video": 1, | |
| 1269 | + "channel": 1, | |
| 1270 | + "post": 1 | |
| 1271 | + }, | |
| 1272 | + "confidence": 0.22, | |
| 1273 | + "triggered_by": { | |
| 1274 | + "OPEN_VIDEO": 1 | |
| 1275 | + }, | |
| 1276 | + "key_fields": [ | |
| 1277 | + { | |
| 1278 | + "path": "[].responseContext.responseId", | |
| 1279 | + "semantic": { | |
| 1280 | + "kind": "identifier", | |
| 1281 | + "confidence": 0.95 | |
| 1282 | + }, | |
| 1283 | + "example": "IhMI89fHi73nlgMVDprkBh0vlBK2" | |
| 1284 | + }, | |
| 1285 | + { | |
| 1286 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 1287 | + "semantic": { | |
| 1288 | + "kind": "boolean", | |
| 1289 | + "confidence": 0.95 | |
| 1290 | + }, | |
| 1291 | + "example": "true" | |
| 1292 | + }, | |
| 1293 | + { | |
| 1294 | + "path": "[].playerResponse.responseContext.responseId", | |
| 1295 | + "semantic": { | |
| 1296 | + "kind": "identifier", | |
| 1297 | + "confidence": 0.95 | |
| 1298 | + }, | |
| 1299 | + "example": "IhMI89fHi73nlgMVDprkBh0vlBK2" | |
| 1300 | + }, | |
| 1301 | + { | |
| 1302 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 1303 | + "semantic": { | |
| 1304 | + "kind": "boolean", | |
| 1305 | + "confidence": 0.95 | |
| 1306 | + }, | |
| 1307 | + "example": "true" | |
| 1308 | + }, | |
| 1309 | + { | |
| 1310 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 1311 | + "semantic": { | |
| 1312 | + "kind": "boolean", | |
| 1313 | + "confidence": 0.95 | |
| 1314 | + }, | |
| 1315 | + "example": "true" | |
| 1316 | + }, | |
| 1317 | + { | |
| 1318 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 1319 | + "semantic": { | |
| 1320 | + "kind": "duration", | |
| 1321 | + "confidence": 0.75 | |
| 1322 | + }, | |
| 1323 | + "example": "3008965991" | |
| 1324 | + }, | |
| 1325 | + { | |
| 1326 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 1327 | + "semantic": { | |
| 1328 | + "kind": "duration", | |
| 1329 | + "confidence": 0.75 | |
| 1330 | + }, | |
| 1331 | + "example": "1952742" | |
| 1332 | + }, | |
| 1333 | + { | |
| 1334 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 1335 | + "semantic": { | |
| 1336 | + "kind": "boolean", | |
| 1337 | + "confidence": 0.95 | |
| 1338 | + }, | |
| 1339 | + "example": "true" | |
| 1340 | + }, | |
| 1341 | + { | |
| 1342 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].audioTrack.displayName", | |
| 1343 | + "semantic": { | |
| 1344 | + "kind": "display_name", | |
| 1345 | + "confidence": 0.7 | |
| 1346 | + }, | |
| 1347 | + "example": "Anglais (US)" | |
| 1348 | + }, | |
| 1349 | + { | |
| 1350 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].audioTrack.audioIsDefault", | |
| 1351 | + "semantic": { | |
| 1352 | + "kind": "boolean", | |
| 1353 | + "confidence": 0.95 | |
| 1354 | + }, | |
| 1355 | + "example": "false" | |
| 1356 | + }, | |
| 1357 | + { | |
| 1358 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].audioTrack.isAutoDubbed", | |
| 1359 | + "semantic": { | |
| 1360 | + "kind": "boolean", | |
| 1361 | + "confidence": 0.95 | |
| 1362 | + }, | |
| 1363 | + "example": "true" | |
| 1364 | + }, | |
| 1365 | + { | |
| 1366 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 1367 | + "semantic": { | |
| 1368 | + "kind": "boolean", | |
| 1369 | + "confidence": 0.95 | |
| 1370 | + }, | |
| 1371 | + "example": "true" | |
| 1372 | + }, | |
| 1373 | + { | |
| 1374 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 1375 | + "semantic": { | |
| 1376 | + "kind": "boolean", | |
| 1377 | + "confidence": 0.95 | |
| 1378 | + }, | |
| 1379 | + "example": "true" | |
| 1380 | + }, | |
| 1381 | + { | |
| 1382 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 1383 | + "semantic": { | |
| 1384 | + "kind": "media_url", | |
| 1385 | + "confidence": 0.93 | |
| 1386 | + }, | |
| 1387 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ar.googlevideo.com/videoplayback?expire=1789183935" | |
| 1388 | + }, | |
| 1389 | + { | |
| 1390 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 1391 | + "semantic": { | |
| 1392 | + "kind": "boolean", | |
| 1393 | + "confidence": 0.95 | |
| 1394 | + }, | |
| 1395 | + "example": "true" | |
| 1396 | + }, | |
| 1397 | + { | |
| 1398 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 1399 | + "semantic": { | |
| 1400 | + "kind": "boolean", | |
| 1401 | + "confidence": 0.95 | |
| 1402 | + }, | |
| 1403 | + "example": "true" | |
| 1404 | + }, | |
| 1405 | + { | |
| 1406 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 1407 | + "semantic": { | |
| 1408 | + "kind": "boolean", | |
| 1409 | + "confidence": 0.95 | |
| 1410 | + }, | |
| 1411 | + "example": "true" | |
| 1412 | + }, | |
| 1413 | + { | |
| 1414 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 1415 | + "semantic": { | |
| 1416 | + "kind": "boolean", | |
| 1417 | + "confidence": 0.95 | |
| 1418 | + }, | |
| 1419 | + "example": "true" | |
| 1420 | + }, | |
| 1421 | + { | |
| 1422 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 1423 | + "semantic": { | |
| 1424 | + "kind": "url", | |
| 1425 | + "confidence": 0.9 | |
| 1426 | + }, | |
| 1427 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=lVmAzyP67oE&ei=X3Oka" | |
| 1428 | + }, | |
| 1429 | + { | |
| 1430 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 1431 | + "semantic": { | |
| 1432 | + "kind": "url", | |
| 1433 | + "confidence": 0.9 | |
| 1434 | + }, | |
| 1435 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=lVmAzyP67oE&ei=X3Ok" | |
| 1436 | + }, | |
| 1437 | + { | |
| 1438 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 1439 | + "semantic": { | |
| 1440 | + "kind": "url", | |
| 1441 | + "confidence": 0.9 | |
| 1442 | + }, | |
| 1443 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=lVmAzyP67oE&ei=X3Ok" | |
| 1444 | + }, | |
| 1445 | + { | |
| 1446 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 1447 | + "semantic": { | |
| 1448 | + "kind": "url", | |
| 1449 | + "confidence": 0.9 | |
| 1450 | + }, | |
| 1451 | + "example": "https://www.youtube.com/ptracking?ei=X3OkarOsFo60kucPr6jKsAs&oid=ef6_Stpd9GrV7UO" | |
| 1452 | + }, | |
| 1453 | + { | |
| 1454 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 1455 | + "semantic": { | |
| 1456 | + "kind": "url", | |
| 1457 | + "confidence": 0.9 | |
| 1458 | + }, | |
| 1459 | + "example": "https://s.youtube.com/api/stats/qoe?cat=mta&cl=974780265&docid=lVmAzyP67oE&ei=X3" | |
| 1460 | + }, | |
| 1461 | + { | |
| 1462 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 1463 | + "semantic": { | |
| 1464 | + "kind": "url", | |
| 1465 | + "confidence": 0.9 | |
| 1466 | + }, | |
| 1467 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=lVmAzyP67oE&ei=X3OkarOsFo60kucPr" | |
| 1468 | + }, | |
| 1469 | + { | |
| 1470 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 1471 | + "semantic": { | |
| 1472 | + "kind": "timestamp", | |
| 1473 | + "confidence": 0.7 | |
| 1474 | + }, | |
| 1475 | + "example": "5" | |
| 1476 | + } | |
| 1477 | + ] | |
| 1478 | + }, | |
| 1479 | + { | |
| 1480 | + "shape_hash": "7602103111a512bc", | |
| 1481 | + "status": "provisional", | |
| 1482 | + "hostname": "www.youtube.com", | |
| 1483 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 1484 | + "method": "POST", | |
| 1485 | + "likely_entity_types": { | |
| 1486 | + "video": 1, | |
| 1487 | + "channel": 1, | |
| 1488 | + "post": 1 | |
| 1489 | + }, | |
| 1490 | + "confidence": 0.22, | |
| 1491 | + "triggered_by": { | |
| 1492 | + "OPEN_VIDEO": 1 | |
| 1493 | + }, | |
| 1494 | + "key_fields": [ | |
| 1495 | + { | |
| 1496 | + "path": "[].responseContext.responseId", | |
| 1497 | + "semantic": { | |
| 1498 | + "kind": "identifier", | |
| 1499 | + "confidence": 0.95 | |
| 1500 | + }, | |
| 1501 | + "example": "IhMI-_r6jb3nlgMVDYXkBh3m1w07" | |
| 1502 | + }, | |
| 1503 | + { | |
| 1504 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 1505 | + "semantic": { | |
| 1506 | + "kind": "boolean", | |
| 1507 | + "confidence": 0.95 | |
| 1508 | + }, | |
| 1509 | + "example": "true" | |
| 1510 | + }, | |
| 1511 | + { | |
| 1512 | + "path": "[].playerResponse.responseContext.responseId", | |
| 1513 | + "semantic": { | |
| 1514 | + "kind": "identifier", | |
| 1515 | + "confidence": 0.95 | |
| 1516 | + }, | |
| 1517 | + "example": "IhMI-_r6jb3nlgMVDYXkBh3m1w07" | |
| 1518 | + }, | |
| 1519 | + { | |
| 1520 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 1521 | + "semantic": { | |
| 1522 | + "kind": "boolean", | |
| 1523 | + "confidence": 0.95 | |
| 1524 | + }, | |
| 1525 | + "example": "true" | |
| 1526 | + }, | |
| 1527 | + { | |
| 1528 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 1529 | + "semantic": { | |
| 1530 | + "kind": "boolean", | |
| 1531 | + "confidence": 0.95 | |
| 1532 | + }, | |
| 1533 | + "example": "true" | |
| 1534 | + }, | |
| 1535 | + { | |
| 1536 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 1537 | + "semantic": { | |
| 1538 | + "kind": "duration", | |
| 1539 | + "confidence": 0.75 | |
| 1540 | + }, | |
| 1541 | + "example": "923323161" | |
| 1542 | + }, | |
| 1543 | + { | |
| 1544 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 1545 | + "semantic": { | |
| 1546 | + "kind": "duration", | |
| 1547 | + "confidence": 0.75 | |
| 1548 | + }, | |
| 1549 | + "example": "3068880" | |
| 1550 | + }, | |
| 1551 | + { | |
| 1552 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 1553 | + "semantic": { | |
| 1554 | + "kind": "boolean", | |
| 1555 | + "confidence": 0.95 | |
| 1556 | + }, | |
| 1557 | + "example": "true" | |
| 1558 | + }, | |
| 1559 | + { | |
| 1560 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].audioTrack.displayName", | |
| 1561 | + "semantic": { | |
| 1562 | + "kind": "display_name", | |
| 1563 | + "confidence": 0.7 | |
| 1564 | + }, | |
| 1565 | + "example": "Anglais" | |
| 1566 | + }, | |
| 1567 | + { | |
| 1568 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].audioTrack.audioIsDefault", | |
| 1569 | + "semantic": { | |
| 1570 | + "kind": "boolean", | |
| 1571 | + "confidence": 0.95 | |
| 1572 | + }, | |
| 1573 | + "example": "false" | |
| 1574 | + }, | |
| 1575 | + { | |
| 1576 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 1577 | + "semantic": { | |
| 1578 | + "kind": "boolean", | |
| 1579 | + "confidence": 0.95 | |
| 1580 | + }, | |
| 1581 | + "example": "true" | |
| 1582 | + }, | |
| 1583 | + { | |
| 1584 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 1585 | + "semantic": { | |
| 1586 | + "kind": "boolean", | |
| 1587 | + "confidence": 0.95 | |
| 1588 | + }, | |
| 1589 | + "example": "true" | |
| 1590 | + }, | |
| 1591 | + { | |
| 1592 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 1593 | + "semantic": { | |
| 1594 | + "kind": "media_url", | |
| 1595 | + "confidence": 0.93 | |
| 1596 | + }, | |
| 1597 | + "example": "https://rr1---sn-cxaaj5o5q5-t0a6.googlevideo.com/videoplayback?expire=1789183940" | |
| 1598 | + }, | |
| 1599 | + { | |
| 1600 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 1601 | + "semantic": { | |
| 1602 | + "kind": "boolean", | |
| 1603 | + "confidence": 0.95 | |
| 1604 | + }, | |
| 1605 | + "example": "true" | |
| 1606 | + }, | |
| 1607 | + { | |
| 1608 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 1609 | + "semantic": { | |
| 1610 | + "kind": "boolean", | |
| 1611 | + "confidence": 0.95 | |
| 1612 | + }, | |
| 1613 | + "example": "true" | |
| 1614 | + }, | |
| 1615 | + { | |
| 1616 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 1617 | + "semantic": { | |
| 1618 | + "kind": "boolean", | |
| 1619 | + "confidence": 0.95 | |
| 1620 | + }, | |
| 1621 | + "example": "true" | |
| 1622 | + }, | |
| 1623 | + { | |
| 1624 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 1625 | + "semantic": { | |
| 1626 | + "kind": "boolean", | |
| 1627 | + "confidence": 0.95 | |
| 1628 | + }, | |
| 1629 | + "example": "true" | |
| 1630 | + }, | |
| 1631 | + { | |
| 1632 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 1633 | + "semantic": { | |
| 1634 | + "kind": "url", | |
| 1635 | + "confidence": 0.9 | |
| 1636 | + }, | |
| 1637 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=XZWRcP5LWoM&ei=ZHOka" | |
| 1638 | + }, | |
| 1639 | + { | |
| 1640 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 1641 | + "semantic": { | |
| 1642 | + "kind": "url", | |
| 1643 | + "confidence": 0.9 | |
| 1644 | + }, | |
| 1645 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=XZWRcP5LWoM&ei=ZHOk" | |
| 1646 | + }, | |
| 1647 | + { | |
| 1648 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 1649 | + "semantic": { | |
| 1650 | + "kind": "url", | |
| 1651 | + "confidence": 0.9 | |
| 1652 | + }, | |
| 1653 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=XZWRcP5LWoM&ei=ZHOk" | |
| 1654 | + }, | |
| 1655 | + { | |
| 1656 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 1657 | + "semantic": { | |
| 1658 | + "kind": "url", | |
| 1659 | + "confidence": 0.9 | |
| 1660 | + }, | |
| 1661 | + "example": "https://www.youtube.com/ptracking?ei=ZHOkavu4GI2KkucP5q-32AM&oid=AkXOB7W-CcJiy62" | |
| 1662 | + }, | |
| 1663 | + { | |
| 1664 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 1665 | + "semantic": { | |
| 1666 | + "kind": "url", | |
| 1667 | + "confidence": 0.9 | |
| 1668 | + }, | |
| 1669 | + "example": "https://s.youtube.com/api/stats/qoe?cat=mta&cl=974780265&docid=XZWRcP5LWoM&ei=ZH" | |
| 1670 | + }, | |
| 1671 | + { | |
| 1672 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 1673 | + "semantic": { | |
| 1674 | + "kind": "url", | |
| 1675 | + "confidence": 0.9 | |
| 1676 | + }, | |
| 1677 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=XZWRcP5LWoM&ei=ZHOkavu4GI2KkucP5" | |
| 1678 | + }, | |
| 1679 | + { | |
| 1680 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 1681 | + "semantic": { | |
| 1682 | + "kind": "timestamp", | |
| 1683 | + "confidence": 0.7 | |
| 1684 | + }, | |
| 1685 | + "example": "5" | |
| 1686 | + }, | |
| 1687 | + { | |
| 1688 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 1689 | + "semantic": { | |
| 1690 | + "kind": "url", | |
| 1691 | + "confidence": 0.9 | |
| 1692 | + }, | |
| 1693 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 1694 | + } | |
| 1695 | + ] | |
| 1696 | + }, | |
| 1697 | + { | |
| 1698 | + "shape_hash": "68e05e1c3c83f379", | |
| 1699 | + "status": "provisional", | |
| 1700 | + "hostname": "www.youtube.com", | |
| 1701 | + "path_pattern": "/youtubei/v1/next", | |
| 1702 | + "method": "POST", | |
| 1703 | + "likely_entity_types": { | |
| 1704 | + "comment": 1, | |
| 1705 | + "channel": 1, | |
| 1706 | + "video": 1 | |
| 1707 | + }, | |
| 1708 | + "confidence": 0.22, | |
| 1709 | + "triggered_by": { | |
| 1710 | + "OPEN_VIDEO": 1 | |
| 1711 | + }, | |
| 1712 | + "key_fields": [ | |
| 1713 | + { | |
| 1714 | + "path": "responseContext.mainAppWebResponseContext.loggedOut", | |
| 1715 | + "semantic": { | |
| 1716 | + "kind": "boolean", | |
| 1717 | + "confidence": 0.95 | |
| 1718 | + }, | |
| 1719 | + "example": "true" | |
| 1720 | + }, | |
| 1721 | + { | |
| 1722 | + "path": "responseContext.responseId", | |
| 1723 | + "semantic": { | |
| 1724 | + "kind": "identifier", | |
| 1725 | + "confidence": 0.95 | |
| 1726 | + }, | |
| 1727 | + "example": "IhMI186jkL3nlgMVORXLBB2u3zne" | |
| 1728 | + }, | |
| 1729 | + { | |
| 1730 | + "path": "responseContext.webResponseContextExtensionData.hasDecorated", | |
| 1731 | + "semantic": { | |
| 1732 | + "kind": "boolean", | |
| 1733 | + "confidence": 0.95 | |
| 1734 | + }, | |
| 1735 | + "example": "true" | |
| 1736 | + }, | |
| 1737 | + { | |
| 1738 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.continuationItems.[].commentThreadRenderer.isModeratedElqComment", | |
| 1739 | + "semantic": { | |
| 1740 | + "kind": "boolean", | |
| 1741 | + "confidence": 0.95 | |
| 1742 | + }, | |
| 1743 | + "example": "false" | |
| 1744 | + }, | |
| 1745 | + { | |
| 1746 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.continuationItems.[].commentThreadRenderer.commentViewModel.commentViewModel.commentId", | |
| 1747 | + "semantic": { | |
| 1748 | + "kind": "identifier", | |
| 1749 | + "confidence": 0.95 | |
| 1750 | + }, | |
| 1751 | + "example": "UgwXrux_QAEMRaAD0w54AaABAg" | |
| 1752 | + }, | |
| 1753 | + { | |
| 1754 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.continuationItems.[].commentThreadRenderer.replies.commentRepliesRenderer.hideReplies.buttonRenderer.text.runs.[].text", | |
| 1755 | + "semantic": { | |
| 1756 | + "kind": "text", | |
| 1757 | + "confidence": 0.85 | |
| 1758 | + }, | |
| 1759 | + "example": "Masquer les réponses" | |
| 1760 | + }, | |
| 1761 | + { | |
| 1762 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.continuationItems.[].commentThreadRenderer.replies.commentRepliesRenderer.targetId", | |
| 1763 | + "semantic": { | |
| 1764 | + "kind": "identifier", | |
| 1765 | + "confidence": 0.95 | |
| 1766 | + }, | |
| 1767 | + "example": "comment-replies-item-Ugzg0kiZiRZUEQ1l0RB4AaABAg" | |
| 1768 | + }, | |
| 1769 | + { | |
| 1770 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.continuationItems.[].commentThreadRenderer.replies.commentRepliesRenderer.subThreads.[].continuationItemRenderer.continuationEndpoint.commandMetadata.webCommandMetadata.sendPost", | |
| 1771 | + "semantic": { | |
| 1772 | + "kind": "boolean", | |
| 1773 | + "confidence": 0.95 | |
| 1774 | + }, | |
| 1775 | + "example": "true" | |
| 1776 | + }, | |
| 1777 | + { | |
| 1778 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.continuationItems.[].continuationItemRenderer.continuationEndpoint.commandMetadata.webCommandMetadata.sendPost", | |
| 1779 | + "semantic": { | |
| 1780 | + "kind": "boolean", | |
| 1781 | + "confidence": 0.95 | |
| 1782 | + }, | |
| 1783 | + "example": "true" | |
| 1784 | + }, | |
| 1785 | + { | |
| 1786 | + "path": "onResponseReceivedEndpoints.[].appendContinuationItemsAction.targetId", | |
| 1787 | + "semantic": { | |
| 1788 | + "kind": "identifier", | |
| 1789 | + "confidence": 0.95 | |
| 1790 | + }, | |
| 1791 | + "example": "comments-section" | |
| 1792 | + }, | |
| 1793 | + { | |
| 1794 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.properties.commentId", | |
| 1795 | + "semantic": { | |
| 1796 | + "kind": "identifier", | |
| 1797 | + "confidence": 0.95 | |
| 1798 | + }, | |
| 1799 | + "example": "UgwXrux_QAEMRaAD0w54AaABAg" | |
| 1800 | + }, | |
| 1801 | + { | |
| 1802 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.properties.content.content", | |
| 1803 | + "semantic": { | |
| 1804 | + "kind": "text", | |
| 1805 | + "confidence": 0.85 | |
| 1806 | + }, | |
| 1807 | + "example": "Adorei o seu vidio." | |
| 1808 | + }, | |
| 1809 | + { | |
| 1810 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.properties.publishedTime", | |
| 1811 | + "semantic": { | |
| 1812 | + "kind": "timestamp", | |
| 1813 | + "confidence": 0.7 | |
| 1814 | + }, | |
| 1815 | + "example": "il y a 3 mois" | |
| 1816 | + }, | |
| 1817 | + { | |
| 1818 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.channelId", | |
| 1819 | + "semantic": { | |
| 1820 | + "kind": "identifier", | |
| 1821 | + "confidence": 0.95 | |
| 1822 | + }, | |
| 1823 | + "example": "UCI0tDQv_Tt-jnPAuK6FPmfA" | |
| 1824 | + }, | |
| 1825 | + { | |
| 1826 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.displayName", | |
| 1827 | + "semantic": { | |
| 1828 | + "kind": "username", | |
| 1829 | + "confidence": 0.7 | |
| 1830 | + }, | |
| 1831 | + "example": "@AdrianConceição-p8t" | |
| 1832 | + }, | |
| 1833 | + { | |
| 1834 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.avatarThumbnailUrl", | |
| 1835 | + "semantic": { | |
| 1836 | + "kind": "url", | |
| 1837 | + "confidence": 0.9 | |
| 1838 | + }, | |
| 1839 | + "example": "https://yt3.ggpht.com/3y6bu5PDGsPKf4sfHi6k2GhKeU5QWX1VjlbuEymaj7vJlmov4Wye-gws0N" | |
| 1840 | + }, | |
| 1841 | + { | |
| 1842 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.isVerified", | |
| 1843 | + "semantic": { | |
| 1844 | + "kind": "boolean", | |
| 1845 | + "confidence": 0.95 | |
| 1846 | + }, | |
| 1847 | + "example": "false" | |
| 1848 | + }, | |
| 1849 | + { | |
| 1850 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.isCurrentUser", | |
| 1851 | + "semantic": { | |
| 1852 | + "kind": "boolean", | |
| 1853 | + "confidence": 0.95 | |
| 1854 | + }, | |
| 1855 | + "example": "false" | |
| 1856 | + }, | |
| 1857 | + { | |
| 1858 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.isCreator", | |
| 1859 | + "semantic": { | |
| 1860 | + "kind": "boolean", | |
| 1861 | + "confidence": 0.95 | |
| 1862 | + }, | |
| 1863 | + "example": "false" | |
| 1864 | + }, | |
| 1865 | + { | |
| 1866 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.channelCommand.innertubeCommand.browseEndpoint.browseId", | |
| 1867 | + "semantic": { | |
| 1868 | + "kind": "identifier", | |
| 1869 | + "confidence": 0.95 | |
| 1870 | + }, | |
| 1871 | + "example": "UCI0tDQv_Tt-jnPAuK6FPmfA" | |
| 1872 | + }, | |
| 1873 | + { | |
| 1874 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.author.isArtist", | |
| 1875 | + "semantic": { | |
| 1876 | + "kind": "boolean", | |
| 1877 | + "confidence": 0.95 | |
| 1878 | + }, | |
| 1879 | + "example": "false" | |
| 1880 | + }, | |
| 1881 | + { | |
| 1882 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.toolbar.likeCountLiked", | |
| 1883 | + "semantic": { | |
| 1884 | + "kind": "count", | |
| 1885 | + "confidence": 0.9 | |
| 1886 | + }, | |
| 1887 | + "example": "3" | |
| 1888 | + }, | |
| 1889 | + { | |
| 1890 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.toolbar.likeCountNotliked", | |
| 1891 | + "semantic": { | |
| 1892 | + "kind": "count", | |
| 1893 | + "confidence": 0.9 | |
| 1894 | + }, | |
| 1895 | + "example": "2" | |
| 1896 | + }, | |
| 1897 | + { | |
| 1898 | + "path": "frameworkUpdates.entityBatchUpdate.mutations.[].payload.commentEntityPayload.toolbar.likeCountA11y", | |
| 1899 | + "semantic": { | |
| 1900 | + "kind": "count", | |
| 1901 | + "confidence": 0.9 | |
| 1902 | + }, | |
| 1903 | + "example": "2 mentions J'aime" | |
| 1904 | + } | |
| 1905 | + ] | |
| 1906 | + }, | |
| 1907 | + { | |
| 1908 | + "shape_hash": "1a02f0ac8f8d4371", | |
| 1909 | + "status": "provisional", | |
| 1910 | + "hostname": "www.youtube.com", | |
| 1911 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 1912 | + "method": "POST", | |
| 1913 | + "likely_entity_types": { | |
| 1914 | + "video": 1, | |
| 1915 | + "post": 1, | |
| 1916 | + "channel": 1 | |
| 1917 | + }, | |
| 1918 | + "confidence": 0.22, | |
| 1919 | + "triggered_by": { | |
| 1920 | + "OPEN_VIDEO": 1 | |
| 1921 | + }, | |
| 1922 | + "key_fields": [ | |
| 1923 | + { | |
| 1924 | + "path": "[].responseContext.responseId", | |
| 1925 | + "semantic": { | |
| 1926 | + "kind": "identifier", | |
| 1927 | + "confidence": 0.95 | |
| 1928 | + }, | |
| 1929 | + "example": "IhMIxI2kkL3nlgMVn4HkBh1GThmw" | |
| 1930 | + }, | |
| 1931 | + { | |
| 1932 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 1933 | + "semantic": { | |
| 1934 | + "kind": "boolean", | |
| 1935 | + "confidence": 0.95 | |
| 1936 | + }, | |
| 1937 | + "example": "true" | |
| 1938 | + }, | |
| 1939 | + { | |
| 1940 | + "path": "[].playerResponse.responseContext.responseId", | |
| 1941 | + "semantic": { | |
| 1942 | + "kind": "identifier", | |
| 1943 | + "confidence": 0.95 | |
| 1944 | + }, | |
| 1945 | + "example": "IhMIxI2kkL3nlgMVn4HkBh1GThmw" | |
| 1946 | + }, | |
| 1947 | + { | |
| 1948 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 1949 | + "semantic": { | |
| 1950 | + "kind": "boolean", | |
| 1951 | + "confidence": 0.95 | |
| 1952 | + }, | |
| 1953 | + "example": "true" | |
| 1954 | + }, | |
| 1955 | + { | |
| 1956 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 1957 | + "semantic": { | |
| 1958 | + "kind": "boolean", | |
| 1959 | + "confidence": 0.95 | |
| 1960 | + }, | |
| 1961 | + "example": "true" | |
| 1962 | + }, | |
| 1963 | + { | |
| 1964 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 1965 | + "semantic": { | |
| 1966 | + "kind": "duration", | |
| 1967 | + "confidence": 0.75 | |
| 1968 | + }, | |
| 1969 | + "example": "176692547" | |
| 1970 | + }, | |
| 1971 | + { | |
| 1972 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 1973 | + "semantic": { | |
| 1974 | + "kind": "duration", | |
| 1975 | + "confidence": 0.75 | |
| 1976 | + }, | |
| 1977 | + "example": "3509572" | |
| 1978 | + }, | |
| 1979 | + { | |
| 1980 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 1981 | + "semantic": { | |
| 1982 | + "kind": "boolean", | |
| 1983 | + "confidence": 0.95 | |
| 1984 | + }, | |
| 1985 | + "example": "true" | |
| 1986 | + }, | |
| 1987 | + { | |
| 1988 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 1989 | + "semantic": { | |
| 1990 | + "kind": "boolean", | |
| 1991 | + "confidence": 0.95 | |
| 1992 | + }, | |
| 1993 | + "example": "true" | |
| 1994 | + }, | |
| 1995 | + { | |
| 1996 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 1997 | + "semantic": { | |
| 1998 | + "kind": "boolean", | |
| 1999 | + "confidence": 0.95 | |
| 2000 | + }, | |
| 2001 | + "example": "true" | |
| 2002 | + }, | |
| 2003 | + { | |
| 2004 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 2005 | + "semantic": { | |
| 2006 | + "kind": "media_url", | |
| 2007 | + "confidence": 0.93 | |
| 2008 | + }, | |
| 2009 | + "example": "https://rr5---sn-cxaaj5o5q5-t0ay.googlevideo.com/videoplayback?expire=1789183945" | |
| 2010 | + }, | |
| 2011 | + { | |
| 2012 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 2013 | + "semantic": { | |
| 2014 | + "kind": "boolean", | |
| 2015 | + "confidence": 0.95 | |
| 2016 | + }, | |
| 2017 | + "example": "true" | |
| 2018 | + }, | |
| 2019 | + { | |
| 2020 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 2021 | + "semantic": { | |
| 2022 | + "kind": "boolean", | |
| 2023 | + "confidence": 0.95 | |
| 2024 | + }, | |
| 2025 | + "example": "true" | |
| 2026 | + }, | |
| 2027 | + { | |
| 2028 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 2029 | + "semantic": { | |
| 2030 | + "kind": "boolean", | |
| 2031 | + "confidence": 0.95 | |
| 2032 | + }, | |
| 2033 | + "example": "true" | |
| 2034 | + }, | |
| 2035 | + { | |
| 2036 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 2037 | + "semantic": { | |
| 2038 | + "kind": "boolean", | |
| 2039 | + "confidence": 0.95 | |
| 2040 | + }, | |
| 2041 | + "example": "true" | |
| 2042 | + }, | |
| 2043 | + { | |
| 2044 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 2045 | + "semantic": { | |
| 2046 | + "kind": "url", | |
| 2047 | + "confidence": 0.9 | |
| 2048 | + }, | |
| 2049 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=jL4ANKq2MHQ&ei=aXOka" | |
| 2050 | + }, | |
| 2051 | + { | |
| 2052 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 2053 | + "semantic": { | |
| 2054 | + "kind": "url", | |
| 2055 | + "confidence": 0.9 | |
| 2056 | + }, | |
| 2057 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=jL4ANKq2MHQ&ei=aXOk" | |
| 2058 | + }, | |
| 2059 | + { | |
| 2060 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 2061 | + "semantic": { | |
| 2062 | + "kind": "url", | |
| 2063 | + "confidence": 0.9 | |
| 2064 | + }, | |
| 2065 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=jL4ANKq2MHQ&ei=aXOk" | |
| 2066 | + }, | |
| 2067 | + { | |
| 2068 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 2069 | + "semantic": { | |
| 2070 | + "kind": "url", | |
| 2071 | + "confidence": 0.9 | |
| 2072 | + }, | |
| 2073 | + "example": "https://www.youtube.com/ptracking?ei=aXOkaoS1EJ-DkucPxpzlgAs&oid=jMMLR90nQCMutT8" | |
| 2074 | + }, | |
| 2075 | + { | |
| 2076 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 2077 | + "semantic": { | |
| 2078 | + "kind": "url", | |
| 2079 | + "confidence": 0.9 | |
| 2080 | + }, | |
| 2081 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=jL4ANKq2MHQ&ei=aXOkaoS1EJ" | |
| 2082 | + }, | |
| 2083 | + { | |
| 2084 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 2085 | + "semantic": { | |
| 2086 | + "kind": "url", | |
| 2087 | + "confidence": 0.9 | |
| 2088 | + }, | |
| 2089 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=jL4ANKq2MHQ&ei=aXOkaoS1EJ-DkucPx" | |
| 2090 | + }, | |
| 2091 | + { | |
| 2092 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 2093 | + "semantic": { | |
| 2094 | + "kind": "timestamp", | |
| 2095 | + "confidence": 0.7 | |
| 2096 | + }, | |
| 2097 | + "example": "5" | |
| 2098 | + }, | |
| 2099 | + { | |
| 2100 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 2101 | + "semantic": { | |
| 2102 | + "kind": "url", | |
| 2103 | + "confidence": 0.9 | |
| 2104 | + }, | |
| 2105 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 2106 | + }, | |
| 2107 | + { | |
| 2108 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.elapsedMediaTimeSeconds", | |
| 2109 | + "semantic": { | |
| 2110 | + "kind": "timestamp", | |
| 2111 | + "confidence": 0.7 | |
| 2112 | + }, | |
| 2113 | + "example": "0" | |
| 2114 | + }, | |
| 2115 | + { | |
| 2116 | + "path": "[].playerResponse.playbackTracking.googleRemarketingUrl.baseUrl", | |
| 2117 | + "semantic": { | |
| 2118 | + "kind": "url", | |
| 2119 | + "confidence": 0.9 | |
| 2120 | + }, | |
| 2121 | + "example": "https://www.google.com/pagead/1p-user-list/962985656/?backend=innertube&cname=1&" | |
| 2122 | + } | |
| 2123 | + ] | |
| 2124 | + }, | |
| 2125 | + { | |
| 2126 | + "shape_hash": "ae99380e3e3aef0c", | |
| 2127 | + "status": "provisional", | |
| 2128 | + "hostname": "www.youtube.com", | |
| 2129 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 2130 | + "method": "POST", | |
| 2131 | + "likely_entity_types": { | |
| 2132 | + "video": 1, | |
| 2133 | + "channel": 1, | |
| 2134 | + "post": 1 | |
| 2135 | + }, | |
| 2136 | + "confidence": 0.22, | |
| 2137 | + "triggered_by": { | |
| 2138 | + "OPEN_VIDEO": 1 | |
| 2139 | + }, | |
| 2140 | + "key_fields": [ | |
| 2141 | + { | |
| 2142 | + "path": "[].responseContext.responseId", | |
| 2143 | + "semantic": { | |
| 2144 | + "kind": "identifier", | |
| 2145 | + "confidence": 0.95 | |
| 2146 | + }, | |
| 2147 | + "example": "IhMI9uTVlL3nlgMVE4auBR3LsgFe" | |
| 2148 | + }, | |
| 2149 | + { | |
| 2150 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 2151 | + "semantic": { | |
| 2152 | + "kind": "boolean", | |
| 2153 | + "confidence": 0.95 | |
| 2154 | + }, | |
| 2155 | + "example": "true" | |
| 2156 | + }, | |
| 2157 | + { | |
| 2158 | + "path": "[].playerResponse.responseContext.responseId", | |
| 2159 | + "semantic": { | |
| 2160 | + "kind": "identifier", | |
| 2161 | + "confidence": 0.95 | |
| 2162 | + }, | |
| 2163 | + "example": "IhMI9uTVlL3nlgMVE4auBR3LsgFe" | |
| 2164 | + }, | |
| 2165 | + { | |
| 2166 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 2167 | + "semantic": { | |
| 2168 | + "kind": "boolean", | |
| 2169 | + "confidence": 0.95 | |
| 2170 | + }, | |
| 2171 | + "example": "true" | |
| 2172 | + }, | |
| 2173 | + { | |
| 2174 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 2175 | + "semantic": { | |
| 2176 | + "kind": "boolean", | |
| 2177 | + "confidence": 0.95 | |
| 2178 | + }, | |
| 2179 | + "example": "true" | |
| 2180 | + }, | |
| 2181 | + { | |
| 2182 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 2183 | + "semantic": { | |
| 2184 | + "kind": "duration", | |
| 2185 | + "confidence": 0.75 | |
| 2186 | + }, | |
| 2187 | + "example": "734584581" | |
| 2188 | + }, | |
| 2189 | + { | |
| 2190 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 2191 | + "semantic": { | |
| 2192 | + "kind": "duration", | |
| 2193 | + "confidence": 0.75 | |
| 2194 | + }, | |
| 2195 | + "example": "7197823" | |
| 2196 | + }, | |
| 2197 | + { | |
| 2198 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 2199 | + "semantic": { | |
| 2200 | + "kind": "boolean", | |
| 2201 | + "confidence": 0.95 | |
| 2202 | + }, | |
| 2203 | + "example": "true" | |
| 2204 | + }, | |
| 2205 | + { | |
| 2206 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 2207 | + "semantic": { | |
| 2208 | + "kind": "boolean", | |
| 2209 | + "confidence": 0.95 | |
| 2210 | + }, | |
| 2211 | + "example": "true" | |
| 2212 | + }, | |
| 2213 | + { | |
| 2214 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 2215 | + "semantic": { | |
| 2216 | + "kind": "media_url", | |
| 2217 | + "confidence": 0.93 | |
| 2218 | + }, | |
| 2219 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ay.googlevideo.com/videoplayback?expire=1789183954" | |
| 2220 | + }, | |
| 2221 | + { | |
| 2222 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 2223 | + "semantic": { | |
| 2224 | + "kind": "boolean", | |
| 2225 | + "confidence": 0.95 | |
| 2226 | + }, | |
| 2227 | + "example": "true" | |
| 2228 | + }, | |
| 2229 | + { | |
| 2230 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 2231 | + "semantic": { | |
| 2232 | + "kind": "boolean", | |
| 2233 | + "confidence": 0.95 | |
| 2234 | + }, | |
| 2235 | + "example": "true" | |
| 2236 | + }, | |
| 2237 | + { | |
| 2238 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 2239 | + "semantic": { | |
| 2240 | + "kind": "boolean", | |
| 2241 | + "confidence": 0.95 | |
| 2242 | + }, | |
| 2243 | + "example": "true" | |
| 2244 | + }, | |
| 2245 | + { | |
| 2246 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 2247 | + "semantic": { | |
| 2248 | + "kind": "boolean", | |
| 2249 | + "confidence": 0.95 | |
| 2250 | + }, | |
| 2251 | + "example": "true" | |
| 2252 | + }, | |
| 2253 | + { | |
| 2254 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 2255 | + "semantic": { | |
| 2256 | + "kind": "url", | |
| 2257 | + "confidence": 0.9 | |
| 2258 | + }, | |
| 2259 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=Fa2sDP_RYsA&ei=cnOka" | |
| 2260 | + }, | |
| 2261 | + { | |
| 2262 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 2263 | + "semantic": { | |
| 2264 | + "kind": "url", | |
| 2265 | + "confidence": 0.9 | |
| 2266 | + }, | |
| 2267 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=Fa2sDP_RYsA&ei=cnOk" | |
| 2268 | + }, | |
| 2269 | + { | |
| 2270 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 2271 | + "semantic": { | |
| 2272 | + "kind": "url", | |
| 2273 | + "confidence": 0.9 | |
| 2274 | + }, | |
| 2275 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=Fa2sDP_RYsA&ei=cnOk" | |
| 2276 | + }, | |
| 2277 | + { | |
| 2278 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 2279 | + "semantic": { | |
| 2280 | + "kind": "url", | |
| 2281 | + "confidence": 0.9 | |
| 2282 | + }, | |
| 2283 | + "example": "https://www.youtube.com/ptracking?ei=cnOkavbjHJOMut0Py-WG8AU&oid=WLeG0gE_kkpZY0o" | |
| 2284 | + }, | |
| 2285 | + { | |
| 2286 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 2287 | + "semantic": { | |
| 2288 | + "kind": "url", | |
| 2289 | + "confidence": 0.9 | |
| 2290 | + }, | |
| 2291 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=Fa2sDP_RYsA&ei=cnOkavbjHJ" | |
| 2292 | + }, | |
| 2293 | + { | |
| 2294 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 2295 | + "semantic": { | |
| 2296 | + "kind": "url", | |
| 2297 | + "confidence": 0.9 | |
| 2298 | + }, | |
| 2299 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=Fa2sDP_RYsA&ei=cnOkavbjHJOMut0Py" | |
| 2300 | + }, | |
| 2301 | + { | |
| 2302 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 2303 | + "semantic": { | |
| 2304 | + "kind": "timestamp", | |
| 2305 | + "confidence": 0.7 | |
| 2306 | + }, | |
| 2307 | + "example": "5" | |
| 2308 | + }, | |
| 2309 | + { | |
| 2310 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 2311 | + "semantic": { | |
| 2312 | + "kind": "url", | |
| 2313 | + "confidence": 0.9 | |
| 2314 | + }, | |
| 2315 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 2316 | + }, | |
| 2317 | + { | |
| 2318 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.elapsedMediaTimeSeconds", | |
| 2319 | + "semantic": { | |
| 2320 | + "kind": "timestamp", | |
| 2321 | + "confidence": 0.7 | |
| 2322 | + }, | |
| 2323 | + "example": "0" | |
| 2324 | + }, | |
| 2325 | + { | |
| 2326 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].baseUrl", | |
| 2327 | + "semantic": { | |
| 2328 | + "kind": "url", | |
| 2329 | + "confidence": 0.9 | |
| 2330 | + }, | |
| 2331 | + "example": "https://www.youtube.com/api/timedtext?v=Fa2sDP_RYsA&ei=cnOkavbjHJOMut0Py-WG8AU&c" | |
| 2332 | + }, | |
| 2333 | + { | |
| 2334 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].name.simpleText", | |
| 2335 | + "semantic": { | |
| 2336 | + "kind": "text", | |
| 2337 | + "confidence": 0.85 | |
| 2338 | + }, | |
| 2339 | + "example": "Français (générés automatiquement)" | |
| 2340 | + } | |
| 2341 | + ] | |
| 2342 | + }, | |
| 2343 | + { | |
| 2344 | + "shape_hash": "5d481f850a3bd8ee", | |
| 2345 | + "status": "provisional", | |
| 2346 | + "hostname": "www.youtube.com", | |
| 2347 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 2348 | + "method": "POST", | |
| 2349 | + "likely_entity_types": { | |
| 2350 | + "video": 1, | |
| 2351 | + "channel": 1, | |
| 2352 | + "post": 1 | |
| 2353 | + }, | |
| 2354 | + "confidence": 0.22, | |
| 2355 | + "triggered_by": { | |
| 2356 | + "OPEN_VIDEO": 1 | |
| 2357 | + }, | |
| 2358 | + "key_fields": [ | |
| 2359 | + { | |
| 2360 | + "path": "[].responseContext.responseId", | |
| 2361 | + "semantic": { | |
| 2362 | + "kind": "identifier", | |
| 2363 | + "confidence": 0.95 | |
| 2364 | + }, | |
| 2365 | + "example": "IhMI7I_0lr3nlgMVPKHkBh3E-zwt" | |
| 2366 | + }, | |
| 2367 | + { | |
| 2368 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 2369 | + "semantic": { | |
| 2370 | + "kind": "boolean", | |
| 2371 | + "confidence": 0.95 | |
| 2372 | + }, | |
| 2373 | + "example": "true" | |
| 2374 | + }, | |
| 2375 | + { | |
| 2376 | + "path": "[].playerResponse.responseContext.responseId", | |
| 2377 | + "semantic": { | |
| 2378 | + "kind": "identifier", | |
| 2379 | + "confidence": 0.95 | |
| 2380 | + }, | |
| 2381 | + "example": "IhMI7I_0lr3nlgMVPKHkBh3E-zwt" | |
| 2382 | + }, | |
| 2383 | + { | |
| 2384 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 2385 | + "semantic": { | |
| 2386 | + "kind": "boolean", | |
| 2387 | + "confidence": 0.95 | |
| 2388 | + }, | |
| 2389 | + "example": "true" | |
| 2390 | + }, | |
| 2391 | + { | |
| 2392 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 2393 | + "semantic": { | |
| 2394 | + "kind": "boolean", | |
| 2395 | + "confidence": 0.95 | |
| 2396 | + }, | |
| 2397 | + "example": "true" | |
| 2398 | + }, | |
| 2399 | + { | |
| 2400 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 2401 | + "semantic": { | |
| 2402 | + "kind": "duration", | |
| 2403 | + "confidence": 0.75 | |
| 2404 | + }, | |
| 2405 | + "example": "33822999" | |
| 2406 | + }, | |
| 2407 | + { | |
| 2408 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 2409 | + "semantic": { | |
| 2410 | + "kind": "duration", | |
| 2411 | + "confidence": 0.75 | |
| 2412 | + }, | |
| 2413 | + "example": "1560025" | |
| 2414 | + }, | |
| 2415 | + { | |
| 2416 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 2417 | + "semantic": { | |
| 2418 | + "kind": "boolean", | |
| 2419 | + "confidence": 0.95 | |
| 2420 | + }, | |
| 2421 | + "example": "true" | |
| 2422 | + }, | |
| 2423 | + { | |
| 2424 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 2425 | + "semantic": { | |
| 2426 | + "kind": "boolean", | |
| 2427 | + "confidence": 0.95 | |
| 2428 | + }, | |
| 2429 | + "example": "true" | |
| 2430 | + }, | |
| 2431 | + { | |
| 2432 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 2433 | + "semantic": { | |
| 2434 | + "kind": "boolean", | |
| 2435 | + "confidence": 0.95 | |
| 2436 | + }, | |
| 2437 | + "example": "true" | |
| 2438 | + }, | |
| 2439 | + { | |
| 2440 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 2441 | + "semantic": { | |
| 2442 | + "kind": "media_url", | |
| 2443 | + "confidence": 0.93 | |
| 2444 | + }, | |
| 2445 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ar.googlevideo.com/videoplayback?expire=1789183959" | |
| 2446 | + }, | |
| 2447 | + { | |
| 2448 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 2449 | + "semantic": { | |
| 2450 | + "kind": "boolean", | |
| 2451 | + "confidence": 0.95 | |
| 2452 | + }, | |
| 2453 | + "example": "true" | |
| 2454 | + }, | |
| 2455 | + { | |
| 2456 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 2457 | + "semantic": { | |
| 2458 | + "kind": "boolean", | |
| 2459 | + "confidence": 0.95 | |
| 2460 | + }, | |
| 2461 | + "example": "true" | |
| 2462 | + }, | |
| 2463 | + { | |
| 2464 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 2465 | + "semantic": { | |
| 2466 | + "kind": "boolean", | |
| 2467 | + "confidence": 0.95 | |
| 2468 | + }, | |
| 2469 | + "example": "true" | |
| 2470 | + }, | |
| 2471 | + { | |
| 2472 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 2473 | + "semantic": { | |
| 2474 | + "kind": "boolean", | |
| 2475 | + "confidence": 0.95 | |
| 2476 | + }, | |
| 2477 | + "example": "true" | |
| 2478 | + }, | |
| 2479 | + { | |
| 2480 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 2481 | + "semantic": { | |
| 2482 | + "kind": "url", | |
| 2483 | + "confidence": 0.9 | |
| 2484 | + }, | |
| 2485 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=upq306ci98I&ei=d3Oka" | |
| 2486 | + }, | |
| 2487 | + { | |
| 2488 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 2489 | + "semantic": { | |
| 2490 | + "kind": "url", | |
| 2491 | + "confidence": 0.9 | |
| 2492 | + }, | |
| 2493 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=upq306ci98I&ei=d3Ok" | |
| 2494 | + }, | |
| 2495 | + { | |
| 2496 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 2497 | + "semantic": { | |
| 2498 | + "kind": "url", | |
| 2499 | + "confidence": 0.9 | |
| 2500 | + }, | |
| 2501 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=upq306ci98I&ei=d3Ok" | |
| 2502 | + }, | |
| 2503 | + { | |
| 2504 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 2505 | + "semantic": { | |
| 2506 | + "kind": "url", | |
| 2507 | + "confidence": 0.9 | |
| 2508 | + }, | |
| 2509 | + "example": "https://www.youtube.com/ptracking?ei=d3Okaqz4CbzCkucPxPfz6QI&oid=EwVG-FSZ-fJALhv" | |
| 2510 | + }, | |
| 2511 | + { | |
| 2512 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 2513 | + "semantic": { | |
| 2514 | + "kind": "url", | |
| 2515 | + "confidence": 0.9 | |
| 2516 | + }, | |
| 2517 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=upq306ci98I&ei=d3Okaqz4Cb" | |
| 2518 | + }, | |
| 2519 | + { | |
| 2520 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 2521 | + "semantic": { | |
| 2522 | + "kind": "url", | |
| 2523 | + "confidence": 0.9 | |
| 2524 | + }, | |
| 2525 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=upq306ci98I&ei=d3Okaqz4CbzCkucPx" | |
| 2526 | + }, | |
| 2527 | + { | |
| 2528 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 2529 | + "semantic": { | |
| 2530 | + "kind": "timestamp", | |
| 2531 | + "confidence": 0.7 | |
| 2532 | + }, | |
| 2533 | + "example": "5" | |
| 2534 | + }, | |
| 2535 | + { | |
| 2536 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.baseUrl", | |
| 2537 | + "semantic": { | |
| 2538 | + "kind": "url", | |
| 2539 | + "confidence": 0.9 | |
| 2540 | + }, | |
| 2541 | + "example": "https://www.youtube.com/pagead/viewthroughconversion/962985656/?backend=innertub" | |
| 2542 | + }, | |
| 2543 | + { | |
| 2544 | + "path": "[].playerResponse.playbackTracking.youtubeRemarketingUrl.elapsedMediaTimeSeconds", | |
| 2545 | + "semantic": { | |
| 2546 | + "kind": "timestamp", | |
| 2547 | + "confidence": 0.7 | |
| 2548 | + }, | |
| 2549 | + "example": "0" | |
| 2550 | + }, | |
| 2551 | + { | |
| 2552 | + "path": "[].playerResponse.playbackTracking.googleRemarketingUrl.baseUrl", | |
| 2553 | + "semantic": { | |
| 2554 | + "kind": "url", | |
| 2555 | + "confidence": 0.9 | |
| 2556 | + }, | |
| 2557 | + "example": "https://www.google.com/pagead/1p-user-list/962985656/?backend=innertube&cname=1&" | |
| 2558 | + } | |
| 2559 | + ] | |
| 2560 | + }, | |
| 2561 | + { | |
| 2562 | + "shape_hash": "ec24830f0e0f4c6a", | |
| 2563 | + "status": "provisional", | |
| 2564 | + "hostname": "www.youtube.com", | |
| 2565 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 2566 | + "method": "POST", | |
| 2567 | + "likely_entity_types": { | |
| 2568 | + "video": 1, | |
| 2569 | + "post": 1, | |
| 2570 | + "channel": 1 | |
| 2571 | + }, | |
| 2572 | + "confidence": 0.22, | |
| 2573 | + "triggered_by": { | |
| 2574 | + "OPEN_VIDEO": 1 | |
| 2575 | + }, | |
| 2576 | + "key_fields": [ | |
| 2577 | + { | |
| 2578 | + "path": "[].responseContext.responseId", | |
| 2579 | + "semantic": { | |
| 2580 | + "kind": "identifier", | |
| 2581 | + "confidence": 0.95 | |
| 2582 | + }, | |
| 2583 | + "example": "IhMIsti_m73nlgMVbazkBh0L1Sz8" | |
| 2584 | + }, | |
| 2585 | + { | |
| 2586 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 2587 | + "semantic": { | |
| 2588 | + "kind": "boolean", | |
| 2589 | + "confidence": 0.95 | |
| 2590 | + }, | |
| 2591 | + "example": "true" | |
| 2592 | + }, | |
| 2593 | + { | |
| 2594 | + "path": "[].playerResponse.responseContext.responseId", | |
| 2595 | + "semantic": { | |
| 2596 | + "kind": "identifier", | |
| 2597 | + "confidence": 0.95 | |
| 2598 | + }, | |
| 2599 | + "example": "IhMIsti_m73nlgMVbazkBh0L1Sz8" | |
| 2600 | + }, | |
| 2601 | + { | |
| 2602 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 2603 | + "semantic": { | |
| 2604 | + "kind": "boolean", | |
| 2605 | + "confidence": 0.95 | |
| 2606 | + }, | |
| 2607 | + "example": "true" | |
| 2608 | + }, | |
| 2609 | + { | |
| 2610 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 2611 | + "semantic": { | |
| 2612 | + "kind": "boolean", | |
| 2613 | + "confidence": 0.95 | |
| 2614 | + }, | |
| 2615 | + "example": "true" | |
| 2616 | + }, | |
| 2617 | + { | |
| 2618 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 2619 | + "semantic": { | |
| 2620 | + "kind": "duration", | |
| 2621 | + "confidence": 0.75 | |
| 2622 | + }, | |
| 2623 | + "example": "2139831769" | |
| 2624 | + }, | |
| 2625 | + { | |
| 2626 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 2627 | + "semantic": { | |
| 2628 | + "kind": "duration", | |
| 2629 | + "confidence": 0.75 | |
| 2630 | + }, | |
| 2631 | + "example": "1125191" | |
| 2632 | + }, | |
| 2633 | + { | |
| 2634 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 2635 | + "semantic": { | |
| 2636 | + "kind": "boolean", | |
| 2637 | + "confidence": 0.95 | |
| 2638 | + }, | |
| 2639 | + "example": "true" | |
| 2640 | + }, | |
| 2641 | + { | |
| 2642 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 2643 | + "semantic": { | |
| 2644 | + "kind": "boolean", | |
| 2645 | + "confidence": 0.95 | |
| 2646 | + }, | |
| 2647 | + "example": "true" | |
| 2648 | + }, | |
| 2649 | + { | |
| 2650 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 2651 | + "semantic": { | |
| 2652 | + "kind": "boolean", | |
| 2653 | + "confidence": 0.95 | |
| 2654 | + }, | |
| 2655 | + "example": "true" | |
| 2656 | + }, | |
| 2657 | + { | |
| 2658 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 2659 | + "semantic": { | |
| 2660 | + "kind": "media_url", | |
| 2661 | + "confidence": 0.93 | |
| 2662 | + }, | |
| 2663 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ar.googlevideo.com/videoplayback?expire=1789183968" | |
| 2664 | + }, | |
| 2665 | + { | |
| 2666 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 2667 | + "semantic": { | |
| 2668 | + "kind": "boolean", | |
| 2669 | + "confidence": 0.95 | |
| 2670 | + }, | |
| 2671 | + "example": "true" | |
| 2672 | + }, | |
| 2673 | + { | |
| 2674 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 2675 | + "semantic": { | |
| 2676 | + "kind": "boolean", | |
| 2677 | + "confidence": 0.95 | |
| 2678 | + }, | |
| 2679 | + "example": "true" | |
| 2680 | + }, | |
| 2681 | + { | |
| 2682 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 2683 | + "semantic": { | |
| 2684 | + "kind": "boolean", | |
| 2685 | + "confidence": 0.95 | |
| 2686 | + }, | |
| 2687 | + "example": "true" | |
| 2688 | + }, | |
| 2689 | + { | |
| 2690 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 2691 | + "semantic": { | |
| 2692 | + "kind": "boolean", | |
| 2693 | + "confidence": 0.95 | |
| 2694 | + }, | |
| 2695 | + "example": "true" | |
| 2696 | + }, | |
| 2697 | + { | |
| 2698 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 2699 | + "semantic": { | |
| 2700 | + "kind": "url", | |
| 2701 | + "confidence": 0.9 | |
| 2702 | + }, | |
| 2703 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=AOyfVBVEIt4&ei=gHOka" | |
| 2704 | + }, | |
| 2705 | + { | |
| 2706 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 2707 | + "semantic": { | |
| 2708 | + "kind": "url", | |
| 2709 | + "confidence": 0.9 | |
| 2710 | + }, | |
| 2711 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=AOyfVBVEIt4&ei=gHOk" | |
| 2712 | + }, | |
| 2713 | + { | |
| 2714 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 2715 | + "semantic": { | |
| 2716 | + "kind": "url", | |
| 2717 | + "confidence": 0.9 | |
| 2718 | + }, | |
| 2719 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=AOyfVBVEIt4&ei=gHOk" | |
| 2720 | + }, | |
| 2721 | + { | |
| 2722 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 2723 | + "semantic": { | |
| 2724 | + "kind": "url", | |
| 2725 | + "confidence": 0.9 | |
| 2726 | + }, | |
| 2727 | + "example": "https://www.youtube.com/ptracking?ei=gHOkarKYMO3YkucPi6qz4Q8&oid=S9JN2l7skMZq-F8" | |
| 2728 | + }, | |
| 2729 | + { | |
| 2730 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 2731 | + "semantic": { | |
| 2732 | + "kind": "url", | |
| 2733 | + "confidence": 0.9 | |
| 2734 | + }, | |
| 2735 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=AOyfVBVEIt4&ei=gHOkarKYMO" | |
| 2736 | + }, | |
| 2737 | + { | |
| 2738 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 2739 | + "semantic": { | |
| 2740 | + "kind": "url", | |
| 2741 | + "confidence": 0.9 | |
| 2742 | + }, | |
| 2743 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=AOyfVBVEIt4&ei=gHOkarKYMO3YkucPi" | |
| 2744 | + }, | |
| 2745 | + { | |
| 2746 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 2747 | + "semantic": { | |
| 2748 | + "kind": "timestamp", | |
| 2749 | + "confidence": 0.7 | |
| 2750 | + }, | |
| 2751 | + "example": "5" | |
| 2752 | + }, | |
| 2753 | + { | |
| 2754 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].baseUrl", | |
| 2755 | + "semantic": { | |
| 2756 | + "kind": "url", | |
| 2757 | + "confidence": 0.9 | |
| 2758 | + }, | |
| 2759 | + "example": "https://www.youtube.com/api/timedtext?v=AOyfVBVEIt4&ei=gHOkarKYMO3YkucPi6qz4Q8&c" | |
| 2760 | + }, | |
| 2761 | + { | |
| 2762 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].name.simpleText", | |
| 2763 | + "semantic": { | |
| 2764 | + "kind": "text", | |
| 2765 | + "confidence": 0.85 | |
| 2766 | + }, | |
| 2767 | + "example": "Anglais (générés automatiquement)" | |
| 2768 | + }, | |
| 2769 | + { | |
| 2770 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].isTranslatable", | |
| 2771 | + "semantic": { | |
| 2772 | + "kind": "boolean", | |
| 2773 | + "confidence": 0.95 | |
| 2774 | + }, | |
| 2775 | + "example": "true" | |
| 2776 | + } | |
| 2777 | + ] | |
| 2778 | + }, | |
| 2779 | + { | |
| 2780 | + "shape_hash": "ef8c8cb030c1e3ed", | |
| 2781 | + "status": "provisional", | |
| 2782 | + "hostname": "www.youtube.com", | |
| 2783 | + "path_pattern": "/youtubei/v1/get_watch", | |
| 2784 | + "method": "POST", | |
| 2785 | + "likely_entity_types": { | |
| 2786 | + "video": 1, | |
| 2787 | + "channel": 1, | |
| 2788 | + "post": 1 | |
| 2789 | + }, | |
| 2790 | + "confidence": 0.22, | |
| 2791 | + "triggered_by": { | |
| 2792 | + "OPEN_VIDEO": 1 | |
| 2793 | + }, | |
| 2794 | + "key_fields": [ | |
| 2795 | + { | |
| 2796 | + "path": "[].responseContext.responseId", | |
| 2797 | + "semantic": { | |
| 2798 | + "kind": "identifier", | |
| 2799 | + "confidence": 0.95 | |
| 2800 | + }, | |
| 2801 | + "example": "IhMIk9Hynb3nlgMV8YDkBh3UOQxb" | |
| 2802 | + }, | |
| 2803 | + { | |
| 2804 | + "path": "[].playerResponse.responseContext.mainAppWebResponseContext.loggedOut", | |
| 2805 | + "semantic": { | |
| 2806 | + "kind": "boolean", | |
| 2807 | + "confidence": 0.95 | |
| 2808 | + }, | |
| 2809 | + "example": "true" | |
| 2810 | + }, | |
| 2811 | + { | |
| 2812 | + "path": "[].playerResponse.responseContext.responseId", | |
| 2813 | + "semantic": { | |
| 2814 | + "kind": "identifier", | |
| 2815 | + "confidence": 0.95 | |
| 2816 | + }, | |
| 2817 | + "example": "IhMIk9Hynb3nlgMV8YDkBh3UOQxb" | |
| 2818 | + }, | |
| 2819 | + { | |
| 2820 | + "path": "[].playerResponse.responseContext.webResponseContextExtensionData.hasDecorated", | |
| 2821 | + "semantic": { | |
| 2822 | + "kind": "boolean", | |
| 2823 | + "confidence": 0.95 | |
| 2824 | + }, | |
| 2825 | + "example": "true" | |
| 2826 | + }, | |
| 2827 | + { | |
| 2828 | + "path": "[].playerResponse.playabilityStatus.playableInEmbed", | |
| 2829 | + "semantic": { | |
| 2830 | + "kind": "boolean", | |
| 2831 | + "confidence": 0.95 | |
| 2832 | + }, | |
| 2833 | + "example": "true" | |
| 2834 | + }, | |
| 2835 | + { | |
| 2836 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].contentLength", | |
| 2837 | + "semantic": { | |
| 2838 | + "kind": "duration", | |
| 2839 | + "confidence": 0.75 | |
| 2840 | + }, | |
| 2841 | + "example": "1450852620" | |
| 2842 | + }, | |
| 2843 | + { | |
| 2844 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].approxDurationMs", | |
| 2845 | + "semantic": { | |
| 2846 | + "kind": "duration", | |
| 2847 | + "confidence": 0.75 | |
| 2848 | + }, | |
| 2849 | + "example": "806806" | |
| 2850 | + }, | |
| 2851 | + { | |
| 2852 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].highReplication", | |
| 2853 | + "semantic": { | |
| 2854 | + "kind": "boolean", | |
| 2855 | + "confidence": 0.95 | |
| 2856 | + }, | |
| 2857 | + "example": "true" | |
| 2858 | + }, | |
| 2859 | + { | |
| 2860 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isDrc", | |
| 2861 | + "semantic": { | |
| 2862 | + "kind": "boolean", | |
| 2863 | + "confidence": 0.95 | |
| 2864 | + }, | |
| 2865 | + "example": "true" | |
| 2866 | + }, | |
| 2867 | + { | |
| 2868 | + "path": "[].playerResponse.streamingData.adaptiveFormats.[].isVb", | |
| 2869 | + "semantic": { | |
| 2870 | + "kind": "boolean", | |
| 2871 | + "confidence": 0.95 | |
| 2872 | + }, | |
| 2873 | + "example": "true" | |
| 2874 | + }, | |
| 2875 | + { | |
| 2876 | + "path": "[].playerResponse.streamingData.serverAbrStreamingUrl", | |
| 2877 | + "semantic": { | |
| 2878 | + "kind": "media_url", | |
| 2879 | + "confidence": 0.93 | |
| 2880 | + }, | |
| 2881 | + "example": "https://rr1---sn-cxaaj5o5q5-t0ad.googlevideo.com/videoplayback?expire=1789183973" | |
| 2882 | + }, | |
| 2883 | + { | |
| 2884 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.playerAdParams.showContentThumbnail", | |
| 2885 | + "semantic": { | |
| 2886 | + "kind": "boolean", | |
| 2887 | + "confidence": 0.95 | |
| 2888 | + }, | |
| 2889 | + "example": "true" | |
| 2890 | + }, | |
| 2891 | + { | |
| 2892 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showCompanion", | |
| 2893 | + "semantic": { | |
| 2894 | + "kind": "boolean", | |
| 2895 | + "confidence": 0.95 | |
| 2896 | + }, | |
| 2897 | + "example": "true" | |
| 2898 | + }, | |
| 2899 | + { | |
| 2900 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.showInstream", | |
| 2901 | + "semantic": { | |
| 2902 | + "kind": "boolean", | |
| 2903 | + "confidence": 0.95 | |
| 2904 | + }, | |
| 2905 | + "example": "true" | |
| 2906 | + }, | |
| 2907 | + { | |
| 2908 | + "path": "[].playerResponse.playerAds.[].playerLegacyDesktopWatchAdsRenderer.useGut", | |
| 2909 | + "semantic": { | |
| 2910 | + "kind": "boolean", | |
| 2911 | + "confidence": 0.95 | |
| 2912 | + }, | |
| 2913 | + "example": "true" | |
| 2914 | + }, | |
| 2915 | + { | |
| 2916 | + "path": "[].playerResponse.playbackTracking.videostatsPlaybackUrl.baseUrl", | |
| 2917 | + "semantic": { | |
| 2918 | + "kind": "url", | |
| 2919 | + "confidence": 0.9 | |
| 2920 | + }, | |
| 2921 | + "example": "https://s.youtube.com/api/stats/playback?cl=974780265&docid=64fmQxx7hiU&ei=hXOka" | |
| 2922 | + }, | |
| 2923 | + { | |
| 2924 | + "path": "[].playerResponse.playbackTracking.videostatsDelayplayUrl.baseUrl", | |
| 2925 | + "semantic": { | |
| 2926 | + "kind": "url", | |
| 2927 | + "confidence": 0.9 | |
| 2928 | + }, | |
| 2929 | + "example": "https://s.youtube.com/api/stats/delayplay?cl=974780265&docid=64fmQxx7hiU&ei=hXOk" | |
| 2930 | + }, | |
| 2931 | + { | |
| 2932 | + "path": "[].playerResponse.playbackTracking.videostatsWatchtimeUrl.baseUrl", | |
| 2933 | + "semantic": { | |
| 2934 | + "kind": "url", | |
| 2935 | + "confidence": 0.9 | |
| 2936 | + }, | |
| 2937 | + "example": "https://s.youtube.com/api/stats/watchtime?cl=974780265&docid=64fmQxx7hiU&ei=hXOk" | |
| 2938 | + }, | |
| 2939 | + { | |
| 2940 | + "path": "[].playerResponse.playbackTracking.ptrackingUrl.baseUrl", | |
| 2941 | + "semantic": { | |
| 2942 | + "kind": "url", | |
| 2943 | + "confidence": 0.9 | |
| 2944 | + }, | |
| 2945 | + "example": "https://www.youtube.com/ptracking?ei=hXOkatP6MfGBkucP1POw2AU&oid=1krK__BKXwDOY6x" | |
| 2946 | + }, | |
| 2947 | + { | |
| 2948 | + "path": "[].playerResponse.playbackTracking.qoeUrl.baseUrl", | |
| 2949 | + "semantic": { | |
| 2950 | + "kind": "url", | |
| 2951 | + "confidence": 0.9 | |
| 2952 | + }, | |
| 2953 | + "example": "https://s.youtube.com/api/stats/qoe?cl=974780265&docid=64fmQxx7hiU&ei=hXOkatP6Mf" | |
| 2954 | + }, | |
| 2955 | + { | |
| 2956 | + "path": "[].playerResponse.playbackTracking.atrUrl.baseUrl", | |
| 2957 | + "semantic": { | |
| 2958 | + "kind": "url", | |
| 2959 | + "confidence": 0.9 | |
| 2960 | + }, | |
| 2961 | + "example": "https://s.youtube.com/api/stats/atr?c=WEB&docid=64fmQxx7hiU&ei=hXOkatP6MfGBkucP1" | |
| 2962 | + }, | |
| 2963 | + { | |
| 2964 | + "path": "[].playerResponse.playbackTracking.atrUrl.elapsedMediaTimeSeconds", | |
| 2965 | + "semantic": { | |
| 2966 | + "kind": "timestamp", | |
| 2967 | + "confidence": 0.7 | |
| 2968 | + }, | |
| 2969 | + "example": "5" | |
| 2970 | + }, | |
| 2971 | + { | |
| 2972 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].baseUrl", | |
| 2973 | + "semantic": { | |
| 2974 | + "kind": "url", | |
| 2975 | + "confidence": 0.9 | |
| 2976 | + }, | |
| 2977 | + "example": "https://www.youtube.com/api/timedtext?v=64fmQxx7hiU&ei=hXOkatP6MfGBkucP1POw2AU&c" | |
| 2978 | + }, | |
| 2979 | + { | |
| 2980 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].name.simpleText", | |
| 2981 | + "semantic": { | |
| 2982 | + "kind": "text", | |
| 2983 | + "confidence": 0.85 | |
| 2984 | + }, | |
| 2985 | + "example": "Anglais (générés automatiquement)" | |
| 2986 | + }, | |
| 2987 | + { | |
| 2988 | + "path": "[].playerResponse.captions.playerCaptionsTracklistRenderer.captionTracks.[].isTranslatable", | |
| 2989 | + "semantic": { | |
| 2990 | + "kind": "boolean", | |
| 2991 | + "confidence": 0.95 | |
| 2992 | + }, | |
| 2993 | + "example": "true" | |
| 2994 | + } | |
| 2995 | + ] | |
| 35 | 2996 | } |
| 36 | 2997 | ], |
| 37 | − "network_patterns": [], | |
| 38 | 2998 | "navigation": [ |
| 39 | 2999 | { |
| 40 | 3000 | "action": "WAIT_FOR_CONTENT", |
@@ -76,19 +3036,19 @@ | ||
| 76 | 3036 | "action": "OPEN_VIDEO", |
| 77 | 3037 | "from": "SEARCH_RESULTS", |
| 78 | 3038 | "to": { |
| 79 | − "VIDEO_DETAIL": 3 | |
| 3039 | + "VIDEO_DETAIL": 4 | |
| 80 | 3040 | }, |
| 81 | − "avg_new_entities": 25.7, | |
| 82 | − "observed": 3 | |
| 3041 | + "avg_new_entities": 19.8, | |
| 3042 | + "observed": 4 | |
| 83 | 3043 | }, |
| 84 | 3044 | { |
| 85 | 3045 | "action": "OPEN_VIDEO", |
| 86 | 3046 | "from": "VIDEO_DETAIL", |
| 87 | 3047 | "to": { |
| 88 | − "VIDEO_DETAIL": 10 | |
| 3048 | + "VIDEO_DETAIL": 11 | |
| 89 | 3049 | }, |
| 90 | − "avg_new_entities": 29.3, | |
| 91 | − "observed": 10 | |
| 3050 | + "avg_new_entities": 26.6, | |
| 3051 | + "observed": 11 | |
| 92 | 3052 | }, |
| 93 | 3053 | { |
| 94 | 3054 | "action": "SEARCH", |
@@ -98,13 +3058,22 @@ | ||
| 98 | 3058 | }, |
| 99 | 3059 | "avg_new_entities": 1, |
| 100 | 3060 | "observed": 1 |
| 3061 | + }, | |
| 3062 | + { | |
| 3063 | + "action": "PLAY_VIDEO", | |
| 3064 | + "from": "VIDEO_DETAIL", | |
| 3065 | + "to": { | |
| 3066 | + "UNKNOWN": 1 | |
| 3067 | + }, | |
| 3068 | + "avg_new_entities": 0, | |
| 3069 | + "observed": 1 | |
| 101 | 3070 | } |
| 102 | 3071 | ], |
| 103 | 3072 | "media": [ |
| 104 | 3073 | { |
| 105 | 3074 | "hostname": "www.youtube.com", |
| 106 | 3075 | "kind": "media_segment", |
| 107 | − "observed_count": 12 | |
| 3076 | + "observed_count": 16 | |
| 108 | 3077 | }, |
| 109 | 3078 | { |
| 110 | 3079 | "hostname": "rr4---sn-q4fl6nde.googlevideo.com", |
@@ -114,7 +3083,7 @@ | ||
| 114 | 3083 | { |
| 115 | 3084 | "hostname": "i.ytimg.com", |
| 116 | 3085 | "kind": "image", |
| 117 | − "observed_count": 172 | |
| 3086 | + "observed_count": 181 | |
| 118 | 3087 | }, |
| 119 | 3088 | { |
| 120 | 3089 | "hostname": "rr5---sn-q4fl6n6z.googlevideo.com", |
@@ -160,6 +3129,16 @@ | ||
| 160 | 3129 | "hostname": "rr1---sn-cxaaj5o5q5-t0ad.googlevideo.com", |
| 161 | 3130 | "kind": "media_segment", |
| 162 | 3131 | "observed_count": 1 |
| 3132 | + }, | |
| 3133 | + { | |
| 3134 | + "hostname": "rr1---sn-cxaaj5o5q5-t9cs.googlevideo.com", | |
| 3135 | + "kind": "media_segment", | |
| 3136 | + "observed_count": 2 | |
| 3137 | + }, | |
| 3138 | + { | |
| 3139 | + "hostname": "rr2---sn-ntqe6n7k.googlevideo.com", | |
| 3140 | + "kind": "media_segment", | |
| 3141 | + "observed_count": 6 | |
| 163 | 3142 | } |
| 164 | 3143 | ] |
| 165 | 3144 | } |
| \ No newline at end of file | ||
added
deploy/social-runtime-crawler.mld.json
+102 −0
@@ -0,0 +1,102 @@ | ||
| 1 | +{ | |
| 2 | + "app": "social-runtime-crawler", | |
| 3 | + "label": "SocialCrawl — Social Runtime Crawler (research console + control plane + browser worker)", | |
| 4 | + "domain": "www.socialcrawl.co", | |
| 5 | + "port": 8351, | |
| 6 | + "health_path": "/access", | |
| 7 | + "dir": "~/apps/social-runtime-crawler", | |
| 8 | + "extra_paths": [], | |
| 9 | + "sync_excludes": [ | |
| 10 | + "node_modules/", | |
| 11 | + ".git/", | |
| 12 | + ".env", | |
| 13 | + ".env.*", | |
| 14 | + "!.env.example", | |
| 15 | + "*.tsbuildinfo", | |
| 16 | + ".DS_Store", | |
| 17 | + ".claude/", | |
| 18 | + "data/", | |
| 19 | + "apps/dashboard/.next/", | |
| 20 | + "apps/dashboard/next-env.d.ts", | |
| 21 | + "deploy/" | |
| 22 | + ], | |
| 23 | + "requires": { | |
| 24 | + "runtimes": ["pm2", "node", "pnpm", "postgresql@17"], | |
| 25 | + "ram_gb": 8, | |
| 26 | + "ports": [8350, 8351] | |
| 27 | + }, | |
| 28 | + "ram_mb_observed": 1500, | |
| 29 | + "size_mb": 40, | |
| 30 | + "placement": { | |
| 31 | + "pin": null, | |
| 32 | + "prefer": "M3U96a", | |
| 33 | + "avoid": ["M1M32", "M3U96b", "M2U64", "M1M64", "m2m16b", "m1m16", "m2m16c", "m4me", "m4mf", "m4mg", "m4mh", "m1m16b", "m4mi", "m4mj", "m4mk", "m2m8a", "m2m8b"], | |
| 34 | + "reason": "Chromium headless par mission (≈1–2 Go chacun) + Postgres 17 local ; nœud LAN avec session graphique pour les logins humains (Partage d'écran) ; M2U64 sans session graphique évité" | |
| 35 | + }, | |
| 36 | + "processes": [ | |
| 37 | + { | |
| 38 | + "name": "socialcrawl-api", | |
| 39 | + "manager": "pm2", | |
| 40 | + "script": "/opt/homebrew/bin/node", | |
| 41 | + "args": ["node_modules/tsx/dist/cli.mjs", "apps/api/src/server.ts"], | |
| 42 | + "interpreter": null, | |
| 43 | + "cwd": "{{HOME}}/apps/social-runtime-crawler", | |
| 44 | + "env": { | |
| 45 | + "NODE_ENV": "production", | |
| 46 | + "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin", | |
| 47 | + "SRC_DATABASE_URL": "postgres://localhost:5432/social_runtime", | |
| 48 | + "SRC_DATA_DIR": "{{HOME}}/apps/social-runtime-crawler/data", | |
| 49 | + "SRC_API_PORT": "8350", | |
| 50 | + "SRC_API_TOKEN": "e9618b61c36567d8f01beb39218ab3108e8042c438d31ee4", | |
| 51 | + "SRC_MAX_JOBS": "2", | |
| 52 | + "SRC_HEADLESS": "true", | |
| 53 | + "SRC_BROWSER_CHANNEL": "chromium", | |
| 54 | + "SRC_LLM_PROVIDER": "none", | |
| 55 | + "SRC_LOG_LEVEL": "info", | |
| 56 | + "SRC_LOG_PRETTY": "1" | |
| 57 | + }, | |
| 58 | + "cron_restart": null, | |
| 59 | + "autorestart": true, | |
| 60 | + "max_memory_restart": "1G" | |
| 61 | + }, | |
| 62 | + { | |
| 63 | + "name": "socialcrawl-web", | |
| 64 | + "manager": "pm2", | |
| 65 | + "script": "/opt/homebrew/bin/node", | |
| 66 | + "args": ["node_modules/next/dist/bin/next", "start", "-p", "8351", "-H", "0.0.0.0"], | |
| 67 | + "interpreter": null, | |
| 68 | + "cwd": "{{HOME}}/apps/social-runtime-crawler/apps/dashboard", | |
| 69 | + "env": { | |
| 70 | + "NODE_ENV": "production", | |
| 71 | + "PATH": "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin", | |
| 72 | + "API_URL": "http://127.0.0.1:8350", | |
| 73 | + "SRC_API_TOKEN": "e9618b61c36567d8f01beb39218ab3108e8042c438d31ee4", | |
| 74 | + "SRC_DASHBOARD_PASSCODE": "OZVCyj4m0Gepql", | |
| 75 | + "SRC_DASHBOARD_SECRET": "2b150a27dcbcf0cfdfd275cdfc85587bb15e9a29b864845f2a59e424e3e0e332", | |
| 76 | + "NEXT_TELEMETRY_DISABLED": "1" | |
| 77 | + }, | |
| 78 | + "cron_restart": null, | |
| 79 | + "autorestart": true, | |
| 80 | + "max_memory_restart": "1G" | |
| 81 | + } | |
| 82 | + ], | |
| 83 | + "tunnel": { | |
| 84 | + "domain": "www.socialcrawl.co", | |
| 85 | + "gateway": "BHS64", | |
| 86 | + "redirects": ["socialcrawl.co"], | |
| 87 | + "websocket": false, | |
| 88 | + "note": "SSE (/api/stream) passe par Caddy sans config spéciale ; apex socialcrawl.co n'a pas encore d'enregistrement A (à ajouter chez GoDaddy pour la redirection)" | |
| 89 | + }, | |
| 90 | + "launchd": [], | |
| 91 | + "env_overrides": {}, | |
| 92 | + "hooks": { | |
| 93 | + "post_sync": [ | |
| 94 | + "export PATH=\"/opt/homebrew/bin:$PATH\"; pnpm install --frozen-lockfile --silent && echo ' deps ok'", | |
| 95 | + "export PATH=\"/opt/homebrew/bin:$PATH\"; (psql -d postgres -Atc \"select 1 from pg_database where datname='social_runtime'\" | grep -q 1 || createdb social_runtime) && SRC_DATABASE_URL=postgres://localhost:5432/social_runtime pnpm db:migrate 2>&1 | tail -1 && echo ' db ok'", | |
| 96 | + "export PATH=\"/opt/homebrew/bin:$PATH\"; pnpm --filter @src/worker exec playwright install chromium 2>&1 | tail -1; echo ' chromium ok'", | |
| 97 | + "export PATH=\"/opt/homebrew/bin:$PATH\"; mkdir -p data/browser_profiles data/sessions data/platform_model data/media data/jobs; export NEXT_TELEMETRY_DISABLED=1 API_URL=http://127.0.0.1:8350; pnpm --filter @src/dashboard build 2>&1 | tail -3 && echo ' web build ok'" | |
| 98 | + ], | |
| 99 | + "post_start": [] | |
| 100 | + }, | |
| 101 | + "notes": "v0.1 (2026-09-12) : console de recherche Next 16 (gate par code d'accès SRC_DASHBOARD_PASSCODE, proxy /api → API loopback 8350 avec SRC_API_TOKEN), API de contrôle node:http (missions = processus worker Playwright enfants, SSE /api/stream), Postgres 17 local `social_runtime`. Les profils navigateur authentifiés vivent dans data/browser_profiles sur le nœud : login humain via Partage d'écran + `pnpm login <platform>`. Adaptateurs YouTube + Reddit. Lecture seule." | |
| 102 | +} | |
modified
package.json
+5 −2
@@ -14,11 +14,14 @@ | ||
| 14 | 14 | "crawl": "tsx apps/worker/src/cli.ts crawl", |
| 15 | 15 | "learn": "tsx apps/worker/src/cli.ts learn", |
| 16 | 16 | "replay": "tsx apps/worker/src/cli.ts replay", |
| 17 | − "dashboard": "tsx apps/api/src/server.ts", | |
| 18 | 17 | "db:migrate": "tsx packages/storage/src/migrate.ts", |
| 19 | 18 | "typecheck": "tsc -p tsconfig.json --noEmit", |
| 20 | 19 | "test": "vitest run", |
| 21 | − "test:watch": "vitest" | |
| 20 | + "test:watch": "vitest", | |
| 21 | + "api": "tsx apps/api/src/server.ts", | |
| 22 | + "dashboard:dev": "pnpm --filter @src/dashboard dev", | |
| 23 | + "dashboard:build": "pnpm --filter @src/dashboard build", | |
| 24 | + "dashboard:start": "pnpm --filter @src/dashboard start" | |
| 22 | 25 | }, |
| 23 | 26 | "devDependencies": { |
| 24 | 27 | "@types/node": "^24.0.0", |
modified
packages/media/src/index.ts
+2 −0
@@ -97,7 +97,9 @@ export async function sampleVideoFrames(page: Page, media: ObservedMedia, mediaD | ||
| 97 | 97 | const duration = media.duration_s ?? (await video.evaluate((v: HTMLVideoElement) => (Number.isFinite(v.duration) ? v.duration : 0)).catch(() => 0)); |
| 98 | 98 | const positions = [0, 0.25, 0.5, 0.75, 0.98].slice(0, maxFrames); |
| 99 | 99 | const files: string[] = []; |
| 100 | + const deadline = Date.now() + 20_000; // frame sampling is best-effort and bounded | |
| 100 | 101 | for (const p of positions) { |
| 102 | + if (Date.now() > deadline) break; | |
| 101 | 103 | try { |
| 102 | 104 | if (duration > 2) { |
| 103 | 105 | await video.evaluate((v: HTMLVideoElement, t: number) => { |
modified
packages/storage/src/index.ts
+0 −0
Binary file not shown.
modified
pnpm-lock.yaml
+1358 −12
@@ -19,10 +19,13 @@ importers: | ||
| 19 | 19 | version: 5.9.3 |
| 20 | 20 | vitest: |
| 21 | 21 | specifier: ^3.2.0 |
| 22 | − version: 3.2.7(@types/node@24.13.4)(tsx@4.23.13) | |
| 22 | + version: 3.2.7(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13) | |
| 23 | 23 | |
| 24 | 24 | apps/api: |
| 25 | 25 | dependencies: |
| 26 | + '@src/platform-model': | |
| 27 | + specifier: workspace:* | |
| 28 | + version: link:../../packages/platform-model | |
| 26 | 29 | '@src/shared': |
| 27 | 30 | specifier: workspace:* |
| 28 | 31 | version: link:../../packages/shared |
@@ -30,6 +33,55 @@ importers: | ||
| 30 | 33 | specifier: workspace:* |
| 31 | 34 | version: link:../../packages/storage |
| 32 | 35 | |
| 36 | + apps/dashboard: | |
| 37 | + dependencies: | |
| 38 | + clsx: | |
| 39 | + specifier: ^2.1.1 | |
| 40 | + version: 2.1.1 | |
| 41 | + d3-force: | |
| 42 | + specifier: ^3.0.0 | |
| 43 | + version: 3.0.0 | |
| 44 | + framer-motion: | |
| 45 | + specifier: ^12.23.0 | |
| 46 | + version: 12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) | |
| 47 | + lucide-react: | |
| 48 | + specifier: ^1.0.0 | |
| 49 | + version: 1.44.0(react@19.2.8) | |
| 50 | + next: | |
| 51 | + specifier: 16.3.4 | |
| 52 | + version: 16.3.4(@types/node@24.13.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) | |
| 53 | + react: | |
| 54 | + specifier: 19.2.8 | |
| 55 | + version: 19.2.8 | |
| 56 | + react-dom: | |
| 57 | + specifier: 19.2.8 | |
| 58 | + version: 19.2.8(react@19.2.8) | |
| 59 | + recharts: | |
| 60 | + specifier: ^3.0.0 | |
| 61 | + version: 3.10.1(@types/react@19.3.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1) | |
| 62 | + devDependencies: | |
| 63 | + '@tailwindcss/postcss': | |
| 64 | + specifier: ^4 | |
| 65 | + version: 4.3.3 | |
| 66 | + '@types/d3-force': | |
| 67 | + specifier: ^3.0.10 | |
| 68 | + version: 3.0.10 | |
| 69 | + '@types/node': | |
| 70 | + specifier: ^24.0.0 | |
| 71 | + version: 24.13.4 | |
| 72 | + '@types/react': | |
| 73 | + specifier: ^19 | |
| 74 | + version: 19.3.0 | |
| 75 | + '@types/react-dom': | |
| 76 | + specifier: ^19 | |
| 77 | + version: 19.3.0(@types/react@19.3.0) | |
| 78 | + tailwindcss: | |
| 79 | + specifier: ^4 | |
| 80 | + version: 4.3.3 | |
| 81 | + typescript: | |
| 82 | + specifier: ^5.9.3 | |
| 83 | + version: 5.9.3 | |
| 84 | + | |
| 33 | 85 | apps/worker: |
| 34 | 86 | dependencies: |
| 35 | 87 | '@src/agent': |
@@ -170,6 +222,10 @@ importers: | ||
| 170 | 222 | |
| 171 | 223 | packages: |
| 172 | 224 | |
| 225 | + '@alloc/quick-lru@5.3.0': | |
| 226 | + resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} | |
| 227 | + engines: {node: '>=10'} | |
| 228 | + | |
| 173 | 229 | '@anthropic-ai/sdk@0.125.0': |
| 174 | 230 | resolution: {integrity: sha512-Hq5wYlXupzJ9M1Fzqjqa3hObcuigEXVZJqSYDgeZGM3wF4qrNERM5Z1OOAeoT9rlod8awJ3uYyJjbQXp1ckIGg==} |
| 175 | 231 | hasBin: true |
@@ -183,6 +239,9 @@ packages: | ||
| 183 | 239 | resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} |
| 184 | 240 | engines: {node: '>=6.9.0'} |
| 185 | 241 | |
| 242 | + '@emnapi/runtime@1.11.3': | |
| 243 | + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} | |
| 244 | + | |
| 186 | 245 | '@esbuild/aix-ppc64@0.28.2': |
| 187 | 246 | resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} |
| 188 | 247 | engines: {node: '>=18'} |
@@ -339,9 +398,184 @@ packages: | ||
| 339 | 398 | cpu: [x64] |
| 340 | 399 | os: [win32] |
| 341 | 400 | |
| 401 | + '@img/colour@1.1.0': | |
| 402 | + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} | |
| 403 | + engines: {node: '>=18'} | |
| 404 | + | |
| 405 | + '@img/sharp-darwin-arm64@0.35.4': | |
| 406 | + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} | |
| 407 | + engines: {node: '>=20.9.0'} | |
| 408 | + cpu: [arm64] | |
| 409 | + os: [darwin] | |
| 410 | + | |
| 411 | + '@img/sharp-darwin-x64@0.35.4': | |
| 412 | + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} | |
| 413 | + engines: {node: '>=20.9.0'} | |
| 414 | + cpu: [x64] | |
| 415 | + os: [darwin] | |
| 416 | + | |
| 417 | + '@img/sharp-freebsd-wasm32@0.35.4': | |
| 418 | + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} | |
| 419 | + engines: {node: '>=20.9.0'} | |
| 420 | + os: [freebsd] | |
| 421 | + | |
| 422 | + '@img/sharp-libvips-darwin-arm64@1.3.3': | |
| 423 | + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} | |
| 424 | + cpu: [arm64] | |
| 425 | + os: [darwin] | |
| 426 | + | |
| 427 | + '@img/sharp-libvips-darwin-x64@1.3.3': | |
| 428 | + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} | |
| 429 | + cpu: [x64] | |
| 430 | + os: [darwin] | |
| 431 | + | |
| 432 | + '@img/sharp-libvips-linux-arm64@1.3.3': | |
| 433 | + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} | |
| 434 | + cpu: [arm64] | |
| 435 | + os: [linux] | |
| 436 | + libc: [glibc] | |
| 437 | + | |
| 438 | + '@img/sharp-libvips-linux-arm@1.3.3': | |
| 439 | + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} | |
| 440 | + cpu: [arm] | |
| 441 | + os: [linux] | |
| 442 | + libc: [glibc] | |
| 443 | + | |
| 444 | + '@img/sharp-libvips-linux-ppc64@1.3.3': | |
| 445 | + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} | |
| 446 | + cpu: [ppc64] | |
| 447 | + os: [linux] | |
| 448 | + libc: [glibc] | |
| 449 | + | |
| 450 | + '@img/sharp-libvips-linux-riscv64@1.3.3': | |
| 451 | + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} | |
| 452 | + cpu: [riscv64] | |
| 453 | + os: [linux] | |
| 454 | + libc: [glibc] | |
| 455 | + | |
| 456 | + '@img/sharp-libvips-linux-s390x@1.3.3': | |
| 457 | + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} | |
| 458 | + cpu: [s390x] | |
| 459 | + os: [linux] | |
| 460 | + libc: [glibc] | |
| 461 | + | |
| 462 | + '@img/sharp-libvips-linux-x64@1.3.3': | |
| 463 | + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} | |
| 464 | + cpu: [x64] | |
| 465 | + os: [linux] | |
| 466 | + libc: [glibc] | |
| 467 | + | |
| 468 | + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': | |
| 469 | + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} | |
| 470 | + cpu: [arm64] | |
| 471 | + os: [linux] | |
| 472 | + libc: [musl] | |
| 473 | + | |
| 474 | + '@img/sharp-libvips-linuxmusl-x64@1.3.3': | |
| 475 | + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} | |
| 476 | + cpu: [x64] | |
| 477 | + os: [linux] | |
| 478 | + libc: [musl] | |
| 479 | + | |
| 480 | + '@img/sharp-linux-arm64@0.35.4': | |
| 481 | + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} | |
| 482 | + engines: {node: '>=20.9.0'} | |
| 483 | + cpu: [arm64] | |
| 484 | + os: [linux] | |
| 485 | + libc: [glibc] | |
| 486 | + | |
| 487 | + '@img/sharp-linux-arm@0.35.4': | |
| 488 | + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} | |
| 489 | + engines: {node: '>=20.9.0'} | |
| 490 | + cpu: [arm] | |
| 491 | + os: [linux] | |
| 492 | + libc: [glibc] | |
| 493 | + | |
| 494 | + '@img/sharp-linux-ppc64@0.35.4': | |
| 495 | + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} | |
| 496 | + engines: {node: '>=20.9.0'} | |
| 497 | + cpu: [ppc64] | |
| 498 | + os: [linux] | |
| 499 | + libc: [glibc] | |
| 500 | + | |
| 501 | + '@img/sharp-linux-riscv64@0.35.4': | |
| 502 | + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} | |
| 503 | + engines: {node: '>=20.9.0'} | |
| 504 | + cpu: [riscv64] | |
| 505 | + os: [linux] | |
| 506 | + libc: [glibc] | |
| 507 | + | |
| 508 | + '@img/sharp-linux-s390x@0.35.4': | |
| 509 | + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} | |
| 510 | + engines: {node: '>=20.9.0'} | |
| 511 | + cpu: [s390x] | |
| 512 | + os: [linux] | |
| 513 | + libc: [glibc] | |
| 514 | + | |
| 515 | + '@img/sharp-linux-x64@0.35.4': | |
| 516 | + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} | |
| 517 | + engines: {node: '>=20.9.0'} | |
| 518 | + cpu: [x64] | |
| 519 | + os: [linux] | |
| 520 | + libc: [glibc] | |
| 521 | + | |
| 522 | + '@img/sharp-linuxmusl-arm64@0.35.4': | |
| 523 | + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} | |
| 524 | + engines: {node: '>=20.9.0'} | |
| 525 | + cpu: [arm64] | |
| 526 | + os: [linux] | |
| 527 | + libc: [musl] | |
| 528 | + | |
| 529 | + '@img/sharp-linuxmusl-x64@0.35.4': | |
| 530 | + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} | |
| 531 | + engines: {node: '>=20.9.0'} | |
| 532 | + cpu: [x64] | |
| 533 | + os: [linux] | |
| 534 | + libc: [musl] | |
| 535 | + | |
| 536 | + '@img/sharp-wasm32@0.35.4': | |
| 537 | + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} | |
| 538 | + engines: {node: '>=20.9.0'} | |
| 539 | + | |
| 540 | + '@img/sharp-webcontainers-wasm32@0.35.4': | |
| 541 | + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} | |
| 542 | + engines: {node: '>=20.9.0'} | |
| 543 | + cpu: [wasm32] | |
| 544 | + | |
| 545 | + '@img/sharp-win32-arm64@0.35.4': | |
| 546 | + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} | |
| 547 | + engines: {node: '>=20.9.0'} | |
| 548 | + cpu: [arm64] | |
| 549 | + os: [win32] | |
| 550 | + | |
| 551 | + '@img/sharp-win32-ia32@0.35.4': | |
| 552 | + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} | |
| 553 | + engines: {node: ^20.9.0} | |
| 554 | + cpu: [ia32] | |
| 555 | + os: [win32] | |
| 556 | + | |
| 557 | + '@img/sharp-win32-x64@0.35.4': | |
| 558 | + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} | |
| 559 | + engines: {node: '>=20.9.0'} | |
| 560 | + cpu: [x64] | |
| 561 | + os: [win32] | |
| 562 | + | |
| 563 | + '@jridgewell/gen-mapping@0.3.13': | |
| 564 | + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} | |
| 565 | + | |
| 566 | + '@jridgewell/remapping@2.3.5': | |
| 567 | + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} | |
| 568 | + | |
| 569 | + '@jridgewell/resolve-uri@3.1.2': | |
| 570 | + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} | |
| 571 | + engines: {node: '>=6.0.0'} | |
| 572 | + | |
| 342 | 573 | '@jridgewell/sourcemap-codec@1.6.0': |
| 343 | 574 | resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} |
| 344 | 575 | |
| 576 | + '@jridgewell/trace-mapping@0.3.31': | |
| 577 | + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} | |
| 578 | + | |
| 345 | 579 | '@napi-rs/lzma-linux-x64-gnu@1.5.1': |
| 346 | 580 | resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} |
| 347 | 581 | engines: {node: ^22.20 || ^24.12 || >=25} |
@@ -349,6 +583,72 @@ packages: | ||
| 349 | 583 | os: [linux] |
| 350 | 584 | libc: [glibc] |
| 351 | 585 | |
| 586 | + '@next/env@16.3.4': | |
| 587 | + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} | |
| 588 | + | |
| 589 | + '@next/swc-darwin-arm64@16.3.4': | |
| 590 | + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} | |
| 591 | + engines: {node: '>= 10'} | |
| 592 | + cpu: [arm64] | |
| 593 | + os: [darwin] | |
| 594 | + | |
| 595 | + '@next/swc-darwin-x64@16.3.4': | |
| 596 | + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} | |
| 597 | + engines: {node: '>= 10'} | |
| 598 | + cpu: [x64] | |
| 599 | + os: [darwin] | |
| 600 | + | |
| 601 | + '@next/swc-linux-arm64-gnu@16.3.4': | |
| 602 | + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} | |
| 603 | + engines: {node: '>= 10'} | |
| 604 | + cpu: [arm64] | |
| 605 | + os: [linux] | |
| 606 | + libc: [glibc] | |
| 607 | + | |
| 608 | + '@next/swc-linux-arm64-musl@16.3.4': | |
| 609 | + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} | |
| 610 | + engines: {node: '>= 10'} | |
| 611 | + cpu: [arm64] | |
| 612 | + os: [linux] | |
| 613 | + libc: [musl] | |
| 614 | + | |
| 615 | + '@next/swc-linux-x64-gnu@16.3.4': | |
| 616 | + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} | |
| 617 | + engines: {node: '>= 10'} | |
| 618 | + cpu: [x64] | |
| 619 | + os: [linux] | |
| 620 | + libc: [glibc] | |
| 621 | + | |
| 622 | + '@next/swc-linux-x64-musl@16.3.4': | |
| 623 | + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} | |
| 624 | + engines: {node: '>= 10'} | |
| 625 | + cpu: [x64] | |
| 626 | + os: [linux] | |
| 627 | + libc: [musl] | |
| 628 | + | |
| 629 | + '@next/swc-win32-arm64-msvc@16.3.4': | |
| 630 | + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} | |
| 631 | + engines: {node: '>= 10'} | |
| 632 | + cpu: [arm64] | |
| 633 | + os: [win32] | |
| 634 | + | |
| 635 | + '@next/swc-win32-x64-msvc@16.3.4': | |
| 636 | + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} | |
| 637 | + engines: {node: '>= 10'} | |
| 638 | + cpu: [x64] | |
| 639 | + os: [win32] | |
| 640 | + | |
| 641 | + '@reduxjs/toolkit@2.12.0': | |
| 642 | + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} | |
| 643 | + peerDependencies: | |
| 644 | + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 | |
| 645 | + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 | |
| 646 | + peerDependenciesMeta: | |
| 647 | + react: | |
| 648 | + optional: true | |
| 649 | + react-redux: | |
| 650 | + optional: true | |
| 651 | + | |
| 352 | 652 | '@rollup/rollup-android-arm-eabi@4.63.1': |
| 353 | 653 | resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} |
| 354 | 654 | cpu: [arm] |
@@ -490,9 +790,140 @@ packages: | ||
| 490 | 790 | '@stablelib/base64@1.0.1': |
| 491 | 791 | resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} |
| 492 | 792 | |
| 793 | + '@standard-schema/spec@1.1.0': | |
| 794 | + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} | |
| 795 | + | |
| 796 | + '@standard-schema/utils@0.3.0': | |
| 797 | + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} | |
| 798 | + | |
| 799 | + '@swc/helpers@0.5.23': | |
| 800 | + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} | |
| 801 | + | |
| 802 | + '@tailwindcss/node@4.3.3': | |
| 803 | + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} | |
| 804 | + | |
| 805 | + '@tailwindcss/oxide-android-arm64@4.3.3': | |
| 806 | + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} | |
| 807 | + engines: {node: '>= 20'} | |
| 808 | + cpu: [arm64] | |
| 809 | + os: [android] | |
| 810 | + | |
| 811 | + '@tailwindcss/oxide-darwin-arm64@4.3.3': | |
| 812 | + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} | |
| 813 | + engines: {node: '>= 20'} | |
| 814 | + cpu: [arm64] | |
| 815 | + os: [darwin] | |
| 816 | + | |
| 817 | + '@tailwindcss/oxide-darwin-x64@4.3.3': | |
| 818 | + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} | |
| 819 | + engines: {node: '>= 20'} | |
| 820 | + cpu: [x64] | |
| 821 | + os: [darwin] | |
| 822 | + | |
| 823 | + '@tailwindcss/oxide-freebsd-x64@4.3.3': | |
| 824 | + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} | |
| 825 | + engines: {node: '>= 20'} | |
| 826 | + cpu: [x64] | |
| 827 | + os: [freebsd] | |
| 828 | + | |
| 829 | + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': | |
| 830 | + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} | |
| 831 | + engines: {node: '>= 20'} | |
| 832 | + cpu: [arm] | |
| 833 | + os: [linux] | |
| 834 | + | |
| 835 | + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': | |
| 836 | + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} | |
| 837 | + engines: {node: '>= 20'} | |
| 838 | + cpu: [arm64] | |
| 839 | + os: [linux] | |
| 840 | + libc: [glibc] | |
| 841 | + | |
| 842 | + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': | |
| 843 | + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} | |
| 844 | + engines: {node: '>= 20'} | |
| 845 | + cpu: [arm64] | |
| 846 | + os: [linux] | |
| 847 | + libc: [musl] | |
| 848 | + | |
| 849 | + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': | |
| 850 | + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} | |
| 851 | + engines: {node: '>= 20'} | |
| 852 | + cpu: [x64] | |
| 853 | + os: [linux] | |
| 854 | + libc: [glibc] | |
| 855 | + | |
| 856 | + '@tailwindcss/oxide-linux-x64-musl@4.3.3': | |
| 857 | + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} | |
| 858 | + engines: {node: '>= 20'} | |
| 859 | + cpu: [x64] | |
| 860 | + os: [linux] | |
| 861 | + libc: [musl] | |
| 862 | + | |
| 863 | + '@tailwindcss/oxide-wasm32-wasi@4.3.3': | |
| 864 | + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} | |
| 865 | + engines: {node: '>=14.0.0'} | |
| 866 | + cpu: [wasm32] | |
| 867 | + bundledDependencies: | |
| 868 | + - '@napi-rs/wasm-runtime' | |
| 869 | + - '@emnapi/core' | |
| 870 | + - '@emnapi/runtime' | |
| 871 | + - '@tybys/wasm-util' | |
| 872 | + - '@emnapi/wasi-threads' | |
| 873 | + - tslib | |
| 874 | + | |
| 875 | + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': | |
| 876 | + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} | |
| 877 | + engines: {node: '>= 20'} | |
| 878 | + cpu: [arm64] | |
| 879 | + os: [win32] | |
| 880 | + | |
| 881 | + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': | |
| 882 | + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} | |
| 883 | + engines: {node: '>= 20'} | |
| 884 | + cpu: [x64] | |
| 885 | + os: [win32] | |
| 886 | + | |
| 887 | + '@tailwindcss/oxide@4.3.3': | |
| 888 | + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} | |
| 889 | + engines: {node: '>= 20'} | |
| 890 | + | |
| 891 | + '@tailwindcss/postcss@4.3.3': | |
| 892 | + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} | |
| 893 | + | |
| 493 | 894 | '@types/chai@5.2.3': |
| 494 | 895 | resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} |
| 495 | 896 | |
| 897 | + '@types/d3-array@3.2.2': | |
| 898 | + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} | |
| 899 | + | |
| 900 | + '@types/d3-color@3.1.3': | |
| 901 | + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} | |
| 902 | + | |
| 903 | + '@types/d3-ease@3.0.2': | |
| 904 | + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} | |
| 905 | + | |
| 906 | + '@types/d3-force@3.0.10': | |
| 907 | + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} | |
| 908 | + | |
| 909 | + '@types/d3-interpolate@3.0.4': | |
| 910 | + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} | |
| 911 | + | |
| 912 | + '@types/d3-path@3.1.1': | |
| 913 | + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} | |
| 914 | + | |
| 915 | + '@types/d3-scale@4.0.9': | |
| 916 | + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} | |
| 917 | + | |
| 918 | + '@types/d3-shape@3.2.0': | |
| 919 | + resolution: {integrity: sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==} | |
| 920 | + | |
| 921 | + '@types/d3-time@3.0.4': | |
| 922 | + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} | |
| 923 | + | |
| 924 | + '@types/d3-timer@3.0.2': | |
| 925 | + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} | |
| 926 | + | |
| 496 | 927 | '@types/deep-eql@4.0.2': |
| 497 | 928 | resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} |
| 498 | 929 | |
@@ -505,6 +936,17 @@ packages: | ||
| 505 | 936 | '@types/pg@8.23.1': |
| 506 | 937 | resolution: {integrity: sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==} |
| 507 | 938 | |
| 939 | + '@types/react-dom@19.3.0': | |
| 940 | + resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} | |
| 941 | + peerDependencies: | |
| 942 | + '@types/react': ^19.3.0 | |
| 943 | + | |
| 944 | + '@types/react@19.3.0': | |
| 945 | + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} | |
| 946 | + | |
| 947 | + '@types/use-sync-external-store@0.0.6': | |
| 948 | + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} | |
| 949 | + | |
| 508 | 950 | '@vitest/expect@3.2.7': |
| 509 | 951 | resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==} |
| 510 | 952 | |
@@ -538,10 +980,18 @@ packages: | ||
| 538 | 980 | resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} |
| 539 | 981 | engines: {node: '>=12'} |
| 540 | 982 | |
| 983 | + baseline-browser-mapping@2.11.22: | |
| 984 | + resolution: {integrity: sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA==} | |
| 985 | + engines: {node: '>=6.0.0'} | |
| 986 | + hasBin: true | |
| 987 | + | |
| 541 | 988 | cac@6.7.14: |
| 542 | 989 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} |
| 543 | 990 | engines: {node: '>=8'} |
| 544 | 991 | |
| 992 | + caniuse-lite@1.0.30001810: | |
| 993 | + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} | |
| 994 | + | |
| 545 | 995 | chai@5.3.3: |
| 546 | 996 | resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} |
| 547 | 997 | engines: {node: '>=18'} |
@@ -550,6 +1000,72 @@ packages: | ||
| 550 | 1000 | resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} |
| 551 | 1001 | engines: {node: '>= 16'} |
| 552 | 1002 | |
| 1003 | + client-only@0.0.1: | |
| 1004 | + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} | |
| 1005 | + | |
| 1006 | + clsx@2.1.1: | |
| 1007 | + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} | |
| 1008 | + engines: {node: '>=6'} | |
| 1009 | + | |
| 1010 | + csstype@3.2.3: | |
| 1011 | + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} | |
| 1012 | + | |
| 1013 | + d3-array@3.2.4: | |
| 1014 | + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} | |
| 1015 | + engines: {node: '>=12'} | |
| 1016 | + | |
| 1017 | + d3-color@3.1.0: | |
| 1018 | + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} | |
| 1019 | + engines: {node: '>=12'} | |
| 1020 | + | |
| 1021 | + d3-dispatch@3.0.1: | |
| 1022 | + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} | |
| 1023 | + engines: {node: '>=12'} | |
| 1024 | + | |
| 1025 | + d3-ease@3.0.1: | |
| 1026 | + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} | |
| 1027 | + engines: {node: '>=12'} | |
| 1028 | + | |
| 1029 | + d3-force@3.0.0: | |
| 1030 | + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} | |
| 1031 | + engines: {node: '>=12'} | |
| 1032 | + | |
| 1033 | + d3-format@3.1.2: | |
| 1034 | + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} | |
| 1035 | + engines: {node: '>=12'} | |
| 1036 | + | |
| 1037 | + d3-interpolate@3.0.1: | |
| 1038 | + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} | |
| 1039 | + engines: {node: '>=12'} | |
| 1040 | + | |
| 1041 | + d3-path@3.1.0: | |
| 1042 | + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} | |
| 1043 | + engines: {node: '>=12'} | |
| 1044 | + | |
| 1045 | + d3-quadtree@3.0.1: | |
| 1046 | + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} | |
| 1047 | + engines: {node: '>=12'} | |
| 1048 | + | |
| 1049 | + d3-scale@4.0.2: | |
| 1050 | + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} | |
| 1051 | + engines: {node: '>=12'} | |
| 1052 | + | |
| 1053 | + d3-shape@3.2.0: | |
| 1054 | + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} | |
| 1055 | + engines: {node: '>=12'} | |
| 1056 | + | |
| 1057 | + d3-time-format@4.1.0: | |
| 1058 | + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} | |
| 1059 | + engines: {node: '>=12'} | |
| 1060 | + | |
| 1061 | + d3-time@3.1.0: | |
| 1062 | + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} | |
| 1063 | + engines: {node: '>=12'} | |
| 1064 | + | |
| 1065 | + d3-timer@3.0.1: | |
| 1066 | + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} | |
| 1067 | + engines: {node: '>=12'} | |
| 1068 | + | |
| 553 | 1069 | debug@4.4.3: |
| 554 | 1070 | resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} |
| 555 | 1071 | engines: {node: '>=6.0'} |
@@ -559,13 +1075,27 @@ packages: | ||
| 559 | 1075 | supports-color: |
| 560 | 1076 | optional: true |
| 561 | 1077 | |
| 1078 | + decimal.js-light@2.5.1: | |
| 1079 | + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} | |
| 1080 | + | |
| 562 | 1081 | deep-eql@5.0.2: |
| 563 | 1082 | resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} |
| 564 | 1083 | engines: {node: '>=6'} |
| 565 | 1084 | |
| 1085 | + detect-libc@2.1.2: | |
| 1086 | + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} | |
| 1087 | + engines: {node: '>=8'} | |
| 1088 | + | |
| 1089 | + enhanced-resolve@5.24.5: | |
| 1090 | + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} | |
| 1091 | + engines: {node: '>=10.13.0'} | |
| 1092 | + | |
| 566 | 1093 | es-module-lexer@1.7.0: |
| 567 | 1094 | resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} |
| 568 | 1095 | |
| 1096 | + es-toolkit@1.52.0: | |
| 1097 | + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} | |
| 1098 | + | |
| 569 | 1099 | esbuild@0.28.2: |
| 570 | 1100 | resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} |
| 571 | 1101 | engines: {node: '>=18'} |
@@ -574,6 +1104,9 @@ packages: | ||
| 574 | 1104 | estree-walker@3.0.3: |
| 575 | 1105 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} |
| 576 | 1106 | |
| 1107 | + eventemitter3@5.0.4: | |
| 1108 | + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} | |
| 1109 | + | |
| 577 | 1110 | expect-type@1.4.0: |
| 578 | 1111 | resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} |
| 579 | 1112 | engines: {node: '>=12.0.0'} |
@@ -590,11 +1123,39 @@ packages: | ||
| 590 | 1123 | picomatch: |
| 591 | 1124 | optional: true |
| 592 | 1125 | |
| 1126 | + framer-motion@12.43.0: | |
| 1127 | + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} | |
| 1128 | + peerDependencies: | |
| 1129 | + '@emotion/is-prop-valid': '*' | |
| 1130 | + react: ^18.0.0 || ^19.0.0 | |
| 1131 | + react-dom: ^18.0.0 || ^19.0.0 | |
| 1132 | + peerDependenciesMeta: | |
| 1133 | + '@emotion/is-prop-valid': | |
| 1134 | + optional: true | |
| 1135 | + react: | |
| 1136 | + optional: true | |
| 1137 | + react-dom: | |
| 1138 | + optional: true | |
| 1139 | + | |
| 593 | 1140 | fsevents@2.3.3: |
| 594 | 1141 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} |
| 595 | 1142 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} |
| 596 | 1143 | os: [darwin] |
| 597 | 1144 | |
| 1145 | + graceful-fs@4.2.11: | |
| 1146 | + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} | |
| 1147 | + | |
| 1148 | + immer@11.1.18: | |
| 1149 | + resolution: {integrity: sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==} | |
| 1150 | + | |
| 1151 | + internmap@2.0.3: | |
| 1152 | + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} | |
| 1153 | + engines: {node: '>=12'} | |
| 1154 | + | |
| 1155 | + jiti@2.7.0: | |
| 1156 | + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} | |
| 1157 | + hasBin: true | |
| 1158 | + | |
| 598 | 1159 | js-tokens@9.0.1: |
| 599 | 1160 | resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} |
| 600 | 1161 | |
@@ -602,12 +1163,97 @@ packages: | ||
| 602 | 1163 | resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} |
| 603 | 1164 | engines: {node: '>=16'} |
| 604 | 1165 | |
| 605 | − loupe@3.2.1: | |
| 606 | − resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} | |
| 1166 | + lightningcss-android-arm64@1.32.0: | |
| 1167 | + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} | |
| 1168 | + engines: {node: '>= 12.0.0'} | |
| 1169 | + cpu: [arm64] | |
| 1170 | + os: [android] | |
| 1171 | + | |
| 1172 | + lightningcss-darwin-arm64@1.32.0: | |
| 1173 | + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} | |
| 1174 | + engines: {node: '>= 12.0.0'} | |
| 1175 | + cpu: [arm64] | |
| 1176 | + os: [darwin] | |
| 1177 | + | |
| 1178 | + lightningcss-darwin-x64@1.32.0: | |
| 1179 | + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} | |
| 1180 | + engines: {node: '>= 12.0.0'} | |
| 1181 | + cpu: [x64] | |
| 1182 | + os: [darwin] | |
| 1183 | + | |
| 1184 | + lightningcss-freebsd-x64@1.32.0: | |
| 1185 | + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} | |
| 1186 | + engines: {node: '>= 12.0.0'} | |
| 1187 | + cpu: [x64] | |
| 1188 | + os: [freebsd] | |
| 1189 | + | |
| 1190 | + lightningcss-linux-arm-gnueabihf@1.32.0: | |
| 1191 | + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} | |
| 1192 | + engines: {node: '>= 12.0.0'} | |
| 1193 | + cpu: [arm] | |
| 1194 | + os: [linux] | |
| 1195 | + | |
| 1196 | + lightningcss-linux-arm64-gnu@1.32.0: | |
| 1197 | + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} | |
| 1198 | + engines: {node: '>= 12.0.0'} | |
| 1199 | + cpu: [arm64] | |
| 1200 | + os: [linux] | |
| 1201 | + libc: [glibc] | |
| 1202 | + | |
| 1203 | + lightningcss-linux-arm64-musl@1.32.0: | |
| 1204 | + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} | |
| 1205 | + engines: {node: '>= 12.0.0'} | |
| 1206 | + cpu: [arm64] | |
| 1207 | + os: [linux] | |
| 1208 | + libc: [musl] | |
| 1209 | + | |
| 1210 | + lightningcss-linux-x64-gnu@1.32.0: | |
| 1211 | + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} | |
| 1212 | + engines: {node: '>= 12.0.0'} | |
| 1213 | + cpu: [x64] | |
| 1214 | + os: [linux] | |
| 1215 | + libc: [glibc] | |
| 1216 | + | |
| 1217 | + lightningcss-linux-x64-musl@1.32.0: | |
| 1218 | + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} | |
| 1219 | + engines: {node: '>= 12.0.0'} | |
| 1220 | + cpu: [x64] | |
| 1221 | + os: [linux] | |
| 1222 | + libc: [musl] | |
| 1223 | + | |
| 1224 | + lightningcss-win32-arm64-msvc@1.32.0: | |
| 1225 | + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} | |
| 1226 | + engines: {node: '>= 12.0.0'} | |
| 1227 | + cpu: [arm64] | |
| 1228 | + os: [win32] | |
| 1229 | + | |
| 1230 | + lightningcss-win32-x64-msvc@1.32.0: | |
| 1231 | + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} | |
| 1232 | + engines: {node: '>= 12.0.0'} | |
| 1233 | + cpu: [x64] | |
| 1234 | + os: [win32] | |
| 1235 | + | |
| 1236 | + lightningcss@1.32.0: | |
| 1237 | + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} | |
| 1238 | + engines: {node: '>= 12.0.0'} | |
| 1239 | + | |
| 1240 | + loupe@3.2.1: | |
| 1241 | + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} | |
| 1242 | + | |
| 1243 | + lucide-react@1.44.0: | |
| 1244 | + resolution: {integrity: sha512-2egNApH4hX4j/qdCgRublh88+9u3mEhz9iSlW5ckm4kaQEqZbXbMr0l5u5JZLy8nmWRx2dbHGQkEDYz6C9aCgw==} | |
| 1245 | + peerDependencies: | |
| 1246 | + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 | |
| 607 | 1247 | |
| 608 | 1248 | magic-string@0.30.21: |
| 609 | 1249 | resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} |
| 610 | 1250 | |
| 1251 | + motion-dom@12.43.0: | |
| 1252 | + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} | |
| 1253 | + | |
| 1254 | + motion-utils@12.39.0: | |
| 1255 | + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} | |
| 1256 | + | |
| 611 | 1257 | ms@2.1.3: |
| 612 | 1258 | resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} |
| 613 | 1259 | |
@@ -616,6 +1262,27 @@ packages: | ||
| 616 | 1262 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} |
| 617 | 1263 | hasBin: true |
| 618 | 1264 | |
| 1265 | + next@16.3.4: | |
| 1266 | + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} | |
| 1267 | + engines: {node: '>=20.9.0'} | |
| 1268 | + hasBin: true | |
| 1269 | + peerDependencies: | |
| 1270 | + '@opentelemetry/api': ^1.1.0 | |
| 1271 | + '@playwright/test': ^1.51.1 | |
| 1272 | + babel-plugin-react-compiler: '*' | |
| 1273 | + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 | |
| 1274 | + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 | |
| 1275 | + sass: ^1.3.0 | |
| 1276 | + peerDependenciesMeta: | |
| 1277 | + '@opentelemetry/api': | |
| 1278 | + optional: true | |
| 1279 | + '@playwright/test': | |
| 1280 | + optional: true | |
| 1281 | + babel-plugin-react-compiler: | |
| 1282 | + optional: true | |
| 1283 | + sass: | |
| 1284 | + optional: true | |
| 1285 | + | |
| 619 | 1286 | pathe@2.0.3: |
| 620 | 1287 | resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} |
| 621 | 1288 | |
@@ -674,6 +1341,10 @@ packages: | ||
| 674 | 1341 | engines: {node: '>=20'} |
| 675 | 1342 | hasBin: true |
| 676 | 1343 | |
| 1344 | + postcss@8.5.23: | |
| 1345 | + resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} | |
| 1346 | + engines: {node: ^10 || ^12 || >=14} | |
| 1347 | + | |
| 677 | 1348 | postcss@8.5.28: |
| 678 | 1349 | resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} |
| 679 | 1350 | engines: {node: ^10 || ^12 || >=14} |
@@ -694,11 +1365,71 @@ packages: | ||
| 694 | 1365 | resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} |
| 695 | 1366 | engines: {node: '>=0.10.0'} |
| 696 | 1367 | |
| 1368 | + react-dom@19.2.8: | |
| 1369 | + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} | |
| 1370 | + peerDependencies: | |
| 1371 | + react: ^19.2.8 | |
| 1372 | + | |
| 1373 | + react-is@19.3.0: | |
| 1374 | + resolution: {integrity: sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==} | |
| 1375 | + | |
| 1376 | + react-redux@9.3.0: | |
| 1377 | + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} | |
| 1378 | + peerDependencies: | |
| 1379 | + '@types/react': ^18.2.25 || ^19 | |
| 1380 | + react: ^18.0 || ^19 | |
| 1381 | + redux: ^5.0.0 | |
| 1382 | + peerDependenciesMeta: | |
| 1383 | + '@types/react': | |
| 1384 | + optional: true | |
| 1385 | + redux: | |
| 1386 | + optional: true | |
| 1387 | + | |
| 1388 | + react@19.2.8: | |
| 1389 | + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} | |
| 1390 | + engines: {node: '>=0.10.0'} | |
| 1391 | + | |
| 1392 | + recharts@3.10.1: | |
| 1393 | + resolution: {integrity: sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==} | |
| 1394 | + engines: {node: '>=18'} | |
| 1395 | + peerDependencies: | |
| 1396 | + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 | |
| 1397 | + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 | |
| 1398 | + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 | |
| 1399 | + | |
| 1400 | + redux-thunk@3.1.0: | |
| 1401 | + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} | |
| 1402 | + peerDependencies: | |
| 1403 | + redux: ^5.0.0 | |
| 1404 | + | |
| 1405 | + redux@5.0.1: | |
| 1406 | + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} | |
| 1407 | + | |
| 1408 | + reselect@5.2.0: | |
| 1409 | + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} | |
| 1410 | + | |
| 697 | 1411 | rollup@4.63.1: |
| 698 | 1412 | resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} |
| 699 | 1413 | engines: {node: '>=18.0.0', npm: '>=8.0.0'} |
| 700 | 1414 | hasBin: true |
| 701 | 1415 | |
| 1416 | + scheduler@0.27.0: | |
| 1417 | + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} | |
| 1418 | + | |
| 1419 | + semver@7.8.5: | |
| 1420 | + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} | |
| 1421 | + engines: {node: '>=10'} | |
| 1422 | + hasBin: true | |
| 1423 | + | |
| 1424 | + sharp@0.35.4: | |
| 1425 | + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} | |
| 1426 | + engines: {node: '>=20.9.0'} | |
| 1427 | + peerDependencies: | |
| 1428 | + '@types/node': '*' | |
| 1429 | + peerDependenciesMeta: | |
| 1430 | + '@types/node': | |
| 1431 | + optional: true | |
| 1432 | + | |
| 702 | 1433 | siginfo@2.0.0: |
| 703 | 1434 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} |
| 704 | 1435 | |
@@ -722,6 +1453,29 @@ packages: | ||
| 722 | 1453 | strip-literal@3.1.0: |
| 723 | 1454 | resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} |
| 724 | 1455 | |
| 1456 | + styled-jsx@5.1.6: | |
| 1457 | + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} | |
| 1458 | + engines: {node: '>= 12.0.0'} | |
| 1459 | + peerDependencies: | |
| 1460 | + '@babel/core': '*' | |
| 1461 | + babel-plugin-macros: '*' | |
| 1462 | + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' | |
| 1463 | + peerDependenciesMeta: | |
| 1464 | + '@babel/core': | |
| 1465 | + optional: true | |
| 1466 | + babel-plugin-macros: | |
| 1467 | + optional: true | |
| 1468 | + | |
| 1469 | + tailwindcss@4.3.3: | |
| 1470 | + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} | |
| 1471 | + | |
| 1472 | + tapable@2.3.3: | |
| 1473 | + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} | |
| 1474 | + engines: {node: '>=6'} | |
| 1475 | + | |
| 1476 | + tiny-invariant@1.3.3: | |
| 1477 | + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} | |
| 1478 | + | |
| 725 | 1479 | tinybench@2.9.0: |
| 726 | 1480 | resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} |
| 727 | 1481 | |
@@ -747,6 +1501,9 @@ packages: | ||
| 747 | 1501 | ts-algebra@2.0.0: |
| 748 | 1502 | resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} |
| 749 | 1503 | |
| 1504 | + tslib@2.8.1: | |
| 1505 | + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} | |
| 1506 | + | |
| 750 | 1507 | tsx@4.23.13: |
| 751 | 1508 | resolution: {integrity: sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==} |
| 752 | 1509 | engines: {node: '>=18.0.0'} |
@@ -760,6 +1517,14 @@ packages: | ||
| 760 | 1517 | undici-types@7.18.2: |
| 761 | 1518 | resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} |
| 762 | 1519 | |
| 1520 | + use-sync-external-store@1.7.0: | |
| 1521 | + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} | |
| 1522 | + peerDependencies: | |
| 1523 | + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 | |
| 1524 | + | |
| 1525 | + victory-vendor@37.3.6: | |
| 1526 | + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} | |
| 1527 | + | |
| 763 | 1528 | vite-node@3.2.4: |
| 764 | 1529 | resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} |
| 765 | 1530 | engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} |
@@ -844,6 +1609,8 @@ packages: | ||
| 844 | 1609 | |
| 845 | 1610 | snapshots: |
| 846 | 1611 | |
| 1612 | + '@alloc/quick-lru@5.3.0': {} | |
| 1613 | + | |
| 847 | 1614 | '@anthropic-ai/sdk@0.125.0': |
| 848 | 1615 | dependencies: |
| 849 | 1616 | json-schema-to-ts: 3.1.1 |
@@ -851,6 +1618,11 @@ snapshots: | ||
| 851 | 1618 | |
| 852 | 1619 | '@babel/runtime@7.29.7': {} |
| 853 | 1620 | |
| 1621 | + '@emnapi/runtime@1.11.3': | |
| 1622 | + dependencies: | |
| 1623 | + tslib: 2.8.1 | |
| 1624 | + optional: true | |
| 1625 | + | |
| 854 | 1626 | '@esbuild/aix-ppc64@0.28.2': |
| 855 | 1627 | optional: true |
| 856 | 1628 | |
@@ -929,11 +1701,173 @@ snapshots: | ||
| 929 | 1701 | '@esbuild/win32-x64@0.28.2': |
| 930 | 1702 | optional: true |
| 931 | 1703 | |
| 1704 | + '@img/colour@1.1.0': | |
| 1705 | + optional: true | |
| 1706 | + | |
| 1707 | + '@img/sharp-darwin-arm64@0.35.4': | |
| 1708 | + optionalDependencies: | |
| 1709 | + '@img/sharp-libvips-darwin-arm64': 1.3.3 | |
| 1710 | + optional: true | |
| 1711 | + | |
| 1712 | + '@img/sharp-darwin-x64@0.35.4': | |
| 1713 | + optionalDependencies: | |
| 1714 | + '@img/sharp-libvips-darwin-x64': 1.3.3 | |
| 1715 | + optional: true | |
| 1716 | + | |
| 1717 | + '@img/sharp-freebsd-wasm32@0.35.4': | |
| 1718 | + dependencies: | |
| 1719 | + '@img/sharp-wasm32': 0.35.4 | |
| 1720 | + optional: true | |
| 1721 | + | |
| 1722 | + '@img/sharp-libvips-darwin-arm64@1.3.3': | |
| 1723 | + optional: true | |
| 1724 | + | |
| 1725 | + '@img/sharp-libvips-darwin-x64@1.3.3': | |
| 1726 | + optional: true | |
| 1727 | + | |
| 1728 | + '@img/sharp-libvips-linux-arm64@1.3.3': | |
| 1729 | + optional: true | |
| 1730 | + | |
| 1731 | + '@img/sharp-libvips-linux-arm@1.3.3': | |
| 1732 | + optional: true | |
| 1733 | + | |
| 1734 | + '@img/sharp-libvips-linux-ppc64@1.3.3': | |
| 1735 | + optional: true | |
| 1736 | + | |
| 1737 | + '@img/sharp-libvips-linux-riscv64@1.3.3': | |
| 1738 | + optional: true | |
| 1739 | + | |
| 1740 | + '@img/sharp-libvips-linux-s390x@1.3.3': | |
| 1741 | + optional: true | |
| 1742 | + | |
| 1743 | + '@img/sharp-libvips-linux-x64@1.3.3': | |
| 1744 | + optional: true | |
| 1745 | + | |
| 1746 | + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': | |
| 1747 | + optional: true | |
| 1748 | + | |
| 1749 | + '@img/sharp-libvips-linuxmusl-x64@1.3.3': | |
| 1750 | + optional: true | |
| 1751 | + | |
| 1752 | + '@img/sharp-linux-arm64@0.35.4': | |
| 1753 | + optionalDependencies: | |
| 1754 | + '@img/sharp-libvips-linux-arm64': 1.3.3 | |
| 1755 | + optional: true | |
| 1756 | + | |
| 1757 | + '@img/sharp-linux-arm@0.35.4': | |
| 1758 | + optionalDependencies: | |
| 1759 | + '@img/sharp-libvips-linux-arm': 1.3.3 | |
| 1760 | + optional: true | |
| 1761 | + | |
| 1762 | + '@img/sharp-linux-ppc64@0.35.4': | |
| 1763 | + optionalDependencies: | |
| 1764 | + '@img/sharp-libvips-linux-ppc64': 1.3.3 | |
| 1765 | + optional: true | |
| 1766 | + | |
| 1767 | + '@img/sharp-linux-riscv64@0.35.4': | |
| 1768 | + optionalDependencies: | |
| 1769 | + '@img/sharp-libvips-linux-riscv64': 1.3.3 | |
| 1770 | + optional: true | |
| 1771 | + | |
| 1772 | + '@img/sharp-linux-s390x@0.35.4': | |
| 1773 | + optionalDependencies: | |
| 1774 | + '@img/sharp-libvips-linux-s390x': 1.3.3 | |
| 1775 | + optional: true | |
| 1776 | + | |
| 1777 | + '@img/sharp-linux-x64@0.35.4': | |
| 1778 | + optionalDependencies: | |
| 1779 | + '@img/sharp-libvips-linux-x64': 1.3.3 | |
| 1780 | + optional: true | |
| 1781 | + | |
| 1782 | + '@img/sharp-linuxmusl-arm64@0.35.4': | |
| 1783 | + optionalDependencies: | |
| 1784 | + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 | |
| 1785 | + optional: true | |
| 1786 | + | |
| 1787 | + '@img/sharp-linuxmusl-x64@0.35.4': | |
| 1788 | + optionalDependencies: | |
| 1789 | + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 | |
| 1790 | + optional: true | |
| 1791 | + | |
| 1792 | + '@img/sharp-wasm32@0.35.4': | |
| 1793 | + dependencies: | |
| 1794 | + '@emnapi/runtime': 1.11.3 | |
| 1795 | + optional: true | |
| 1796 | + | |
| 1797 | + '@img/sharp-webcontainers-wasm32@0.35.4': | |
| 1798 | + dependencies: | |
| 1799 | + '@img/sharp-wasm32': 0.35.4 | |
| 1800 | + optional: true | |
| 1801 | + | |
| 1802 | + '@img/sharp-win32-arm64@0.35.4': | |
| 1803 | + optional: true | |
| 1804 | + | |
| 1805 | + '@img/sharp-win32-ia32@0.35.4': | |
| 1806 | + optional: true | |
| 1807 | + | |
| 1808 | + '@img/sharp-win32-x64@0.35.4': | |
| 1809 | + optional: true | |
| 1810 | + | |
| 1811 | + '@jridgewell/gen-mapping@0.3.13': | |
| 1812 | + dependencies: | |
| 1813 | + '@jridgewell/sourcemap-codec': 1.6.0 | |
| 1814 | + '@jridgewell/trace-mapping': 0.3.31 | |
| 1815 | + | |
| 1816 | + '@jridgewell/remapping@2.3.5': | |
| 1817 | + dependencies: | |
| 1818 | + '@jridgewell/gen-mapping': 0.3.13 | |
| 1819 | + '@jridgewell/trace-mapping': 0.3.31 | |
| 1820 | + | |
| 1821 | + '@jridgewell/resolve-uri@3.1.2': {} | |
| 1822 | + | |
| 932 | 1823 | '@jridgewell/sourcemap-codec@1.6.0': {} |
| 933 | 1824 | |
| 1825 | + '@jridgewell/trace-mapping@0.3.31': | |
| 1826 | + dependencies: | |
| 1827 | + '@jridgewell/resolve-uri': 3.1.2 | |
| 1828 | + '@jridgewell/sourcemap-codec': 1.6.0 | |
| 1829 | + | |
| 934 | 1830 | '@napi-rs/lzma-linux-x64-gnu@1.5.1': |
| 935 | 1831 | optional: true |
| 936 | 1832 | |
| 1833 | + '@next/env@16.3.4': {} | |
| 1834 | + | |
| 1835 | + '@next/swc-darwin-arm64@16.3.4': | |
| 1836 | + optional: true | |
| 1837 | + | |
| 1838 | + '@next/swc-darwin-x64@16.3.4': | |
| 1839 | + optional: true | |
| 1840 | + | |
| 1841 | + '@next/swc-linux-arm64-gnu@16.3.4': | |
| 1842 | + optional: true | |
| 1843 | + | |
| 1844 | + '@next/swc-linux-arm64-musl@16.3.4': | |
| 1845 | + optional: true | |
| 1846 | + | |
| 1847 | + '@next/swc-linux-x64-gnu@16.3.4': | |
| 1848 | + optional: true | |
| 1849 | + | |
| 1850 | + '@next/swc-linux-x64-musl@16.3.4': | |
| 1851 | + optional: true | |
| 1852 | + | |
| 1853 | + '@next/swc-win32-arm64-msvc@16.3.4': | |
| 1854 | + optional: true | |
| 1855 | + | |
| 1856 | + '@next/swc-win32-x64-msvc@16.3.4': | |
| 1857 | + optional: true | |
| 1858 | + | |
| 1859 | + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.3.0)(react@19.2.8)(redux@5.0.1))(react@19.2.8)': | |
| 1860 | + dependencies: | |
| 1861 | + '@standard-schema/spec': 1.1.0 | |
| 1862 | + '@standard-schema/utils': 0.3.0 | |
| 1863 | + immer: 11.1.18 | |
| 1864 | + redux: 5.0.1 | |
| 1865 | + redux-thunk: 3.1.0(redux@5.0.1) | |
| 1866 | + reselect: 5.2.0 | |
| 1867 | + optionalDependencies: | |
| 1868 | + react: 19.2.8 | |
| 1869 | + react-redux: 9.3.0(@types/react@19.3.0)(react@19.2.8)(redux@5.0.1) | |
| 1870 | + | |
| 937 | 1871 | '@rollup/rollup-android-arm-eabi@4.63.1': |
| 938 | 1872 | optional: true |
| 939 | 1873 | |
@@ -1011,11 +1945,114 @@ snapshots: | ||
| 1011 | 1945 | |
| 1012 | 1946 | '@stablelib/base64@1.0.1': {} |
| 1013 | 1947 | |
| 1948 | + '@standard-schema/spec@1.1.0': {} | |
| 1949 | + | |
| 1950 | + '@standard-schema/utils@0.3.0': {} | |
| 1951 | + | |
| 1952 | + '@swc/helpers@0.5.23': | |
| 1953 | + dependencies: | |
| 1954 | + tslib: 2.8.1 | |
| 1955 | + | |
| 1956 | + '@tailwindcss/node@4.3.3': | |
| 1957 | + dependencies: | |
| 1958 | + '@jridgewell/remapping': 2.3.5 | |
| 1959 | + enhanced-resolve: 5.24.5 | |
| 1960 | + jiti: 2.7.0 | |
| 1961 | + lightningcss: 1.32.0 | |
| 1962 | + magic-string: 0.30.21 | |
| 1963 | + source-map-js: 1.2.1 | |
| 1964 | + tailwindcss: 4.3.3 | |
| 1965 | + | |
| 1966 | + '@tailwindcss/oxide-android-arm64@4.3.3': | |
| 1967 | + optional: true | |
| 1968 | + | |
| 1969 | + '@tailwindcss/oxide-darwin-arm64@4.3.3': | |
| 1970 | + optional: true | |
| 1971 | + | |
| 1972 | + '@tailwindcss/oxide-darwin-x64@4.3.3': | |
| 1973 | + optional: true | |
| 1974 | + | |
| 1975 | + '@tailwindcss/oxide-freebsd-x64@4.3.3': | |
| 1976 | + optional: true | |
| 1977 | + | |
| 1978 | + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': | |
| 1979 | + optional: true | |
| 1980 | + | |
| 1981 | + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': | |
| 1982 | + optional: true | |
| 1983 | + | |
| 1984 | + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': | |
| 1985 | + optional: true | |
| 1986 | + | |
| 1987 | + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': | |
| 1988 | + optional: true | |
| 1989 | + | |
| 1990 | + '@tailwindcss/oxide-linux-x64-musl@4.3.3': | |
| 1991 | + optional: true | |
| 1992 | + | |
| 1993 | + '@tailwindcss/oxide-wasm32-wasi@4.3.3': | |
| 1994 | + optional: true | |
| 1995 | + | |
| 1996 | + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': | |
| 1997 | + optional: true | |
| 1998 | + | |
| 1999 | + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': | |
| 2000 | + optional: true | |
| 2001 | + | |
| 2002 | + '@tailwindcss/oxide@4.3.3': | |
| 2003 | + optionalDependencies: | |
| 2004 | + '@tailwindcss/oxide-android-arm64': 4.3.3 | |
| 2005 | + '@tailwindcss/oxide-darwin-arm64': 4.3.3 | |
| 2006 | + '@tailwindcss/oxide-darwin-x64': 4.3.3 | |
| 2007 | + '@tailwindcss/oxide-freebsd-x64': 4.3.3 | |
| 2008 | + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 | |
| 2009 | + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 | |
| 2010 | + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 | |
| 2011 | + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 | |
| 2012 | + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 | |
| 2013 | + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 | |
| 2014 | + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 | |
| 2015 | + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 | |
| 2016 | + | |
| 2017 | + '@tailwindcss/postcss@4.3.3': | |
| 2018 | + dependencies: | |
| 2019 | + '@alloc/quick-lru': 5.3.0 | |
| 2020 | + '@tailwindcss/node': 4.3.3 | |
| 2021 | + '@tailwindcss/oxide': 4.3.3 | |
| 2022 | + postcss: 8.5.28 | |
| 2023 | + tailwindcss: 4.3.3 | |
| 2024 | + | |
| 1014 | 2025 | '@types/chai@5.2.3': |
| 1015 | 2026 | dependencies: |
| 1016 | 2027 | '@types/deep-eql': 4.0.2 |
| 1017 | 2028 | assertion-error: 2.0.1 |
| 1018 | 2029 | |
| 2030 | + '@types/d3-array@3.2.2': {} | |
| 2031 | + | |
| 2032 | + '@types/d3-color@3.1.3': {} | |
| 2033 | + | |
| 2034 | + '@types/d3-ease@3.0.2': {} | |
| 2035 | + | |
| 2036 | + '@types/d3-force@3.0.10': {} | |
| 2037 | + | |
| 2038 | + '@types/d3-interpolate@3.0.4': | |
| 2039 | + dependencies: | |
| 2040 | + '@types/d3-color': 3.1.3 | |
| 2041 | + | |
| 2042 | + '@types/d3-path@3.1.1': {} | |
| 2043 | + | |
| 2044 | + '@types/d3-scale@4.0.9': | |
| 2045 | + dependencies: | |
| 2046 | + '@types/d3-time': 3.0.4 | |
| 2047 | + | |
| 2048 | + '@types/d3-shape@3.2.0': | |
| 2049 | + dependencies: | |
| 2050 | + '@types/d3-path': 3.1.1 | |
| 2051 | + | |
| 2052 | + '@types/d3-time@3.0.4': {} | |
| 2053 | + | |
| 2054 | + '@types/d3-timer@3.0.2': {} | |
| 2055 | + | |
| 1019 | 2056 | '@types/deep-eql@4.0.2': {} |
| 1020 | 2057 | |
| 1021 | 2058 | '@types/estree@1.0.9': {} |
@@ -1030,6 +2067,16 @@ snapshots: | ||
| 1030 | 2067 | pg-protocol: 1.16.0 |
| 1031 | 2068 | pg-types: 2.2.0 |
| 1032 | 2069 | |
| 2070 | + '@types/react-dom@19.3.0(@types/react@19.3.0)': | |
| 2071 | + dependencies: | |
| 2072 | + '@types/react': 19.3.0 | |
| 2073 | + | |
| 2074 | + '@types/react@19.3.0': | |
| 2075 | + dependencies: | |
| 2076 | + csstype: 3.2.3 | |
| 2077 | + | |
| 2078 | + '@types/use-sync-external-store@0.0.6': {} | |
| 2079 | + | |
| 1033 | 2080 | '@vitest/expect@3.2.7': |
| 1034 | 2081 | dependencies: |
| 1035 | 2082 | '@types/chai': 5.2.3 |
@@ -1038,13 +2085,13 @@ snapshots: | ||
| 1038 | 2085 | chai: 5.3.3 |
| 1039 | 2086 | tinyrainbow: 2.0.0 |
| 1040 | 2087 | |
| 1041 | − '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.4)(tsx@4.23.13))': | |
| 2088 | + '@vitest/mocker@3.2.7(vite@7.3.6(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13))': | |
| 1042 | 2089 | dependencies: |
| 1043 | 2090 | '@vitest/spy': 3.2.7 |
| 1044 | 2091 | estree-walker: 3.0.3 |
| 1045 | 2092 | magic-string: 0.30.21 |
| 1046 | 2093 | optionalDependencies: |
| 1047 | − vite: 7.3.6(@types/node@24.13.4)(tsx@4.23.13) | |
| 2094 | + vite: 7.3.6(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13) | |
| 1048 | 2095 | |
| 1049 | 2096 | '@vitest/pretty-format@3.2.7': |
| 1050 | 2097 | dependencies: |
@@ -1074,8 +2121,12 @@ snapshots: | ||
| 1074 | 2121 | |
| 1075 | 2122 | assertion-error@2.0.1: {} |
| 1076 | 2123 | |
| 2124 | + baseline-browser-mapping@2.11.22: {} | |
| 2125 | + | |
| 1077 | 2126 | cac@6.7.14: {} |
| 1078 | 2127 | |
| 2128 | + caniuse-lite@1.0.30001810: {} | |
| 2129 | + | |
| 1079 | 2130 | chai@5.3.3: |
| 1080 | 2131 | dependencies: |
| 1081 | 2132 | assertion-error: 2.0.1 |
@@ -1086,14 +2137,79 @@ snapshots: | ||
| 1086 | 2137 | |
| 1087 | 2138 | check-error@2.1.3: {} |
| 1088 | 2139 | |
| 2140 | + client-only@0.0.1: {} | |
| 2141 | + | |
| 2142 | + clsx@2.1.1: {} | |
| 2143 | + | |
| 2144 | + csstype@3.2.3: {} | |
| 2145 | + | |
| 2146 | + d3-array@3.2.4: | |
| 2147 | + dependencies: | |
| 2148 | + internmap: 2.0.3 | |
| 2149 | + | |
| 2150 | + d3-color@3.1.0: {} | |
| 2151 | + | |
| 2152 | + d3-dispatch@3.0.1: {} | |
| 2153 | + | |
| 2154 | + d3-ease@3.0.1: {} | |
| 2155 | + | |
| 2156 | + d3-force@3.0.0: | |
| 2157 | + dependencies: | |
| 2158 | + d3-dispatch: 3.0.1 | |
| 2159 | + d3-quadtree: 3.0.1 | |
| 2160 | + d3-timer: 3.0.1 | |
| 2161 | + | |
| 2162 | + d3-format@3.1.2: {} | |
| 2163 | + | |
| 2164 | + d3-interpolate@3.0.1: | |
| 2165 | + dependencies: | |
| 2166 | + d3-color: 3.1.0 | |
| 2167 | + | |
| 2168 | + d3-path@3.1.0: {} | |
| 2169 | + | |
| 2170 | + d3-quadtree@3.0.1: {} | |
| 2171 | + | |
| 2172 | + d3-scale@4.0.2: | |
| 2173 | + dependencies: | |
| 2174 | + d3-array: 3.2.4 | |
| 2175 | + d3-format: 3.1.2 | |
| 2176 | + d3-interpolate: 3.0.1 | |
| 2177 | + d3-time: 3.1.0 | |
| 2178 | + d3-time-format: 4.1.0 | |
| 2179 | + | |
| 2180 | + d3-shape@3.2.0: | |
| 2181 | + dependencies: | |
| 2182 | + d3-path: 3.1.0 | |
| 2183 | + | |
| 2184 | + d3-time-format@4.1.0: | |
| 2185 | + dependencies: | |
| 2186 | + d3-time: 3.1.0 | |
| 2187 | + | |
| 2188 | + d3-time@3.1.0: | |
| 2189 | + dependencies: | |
| 2190 | + d3-array: 3.2.4 | |
| 2191 | + | |
| 2192 | + d3-timer@3.0.1: {} | |
| 2193 | + | |
| 1089 | 2194 | debug@4.4.3: |
| 1090 | 2195 | dependencies: |
| 1091 | 2196 | ms: 2.1.3 |
| 1092 | 2197 | |
| 2198 | + decimal.js-light@2.5.1: {} | |
| 2199 | + | |
| 1093 | 2200 | deep-eql@5.0.2: {} |
| 1094 | 2201 | |
| 2202 | + detect-libc@2.1.2: {} | |
| 2203 | + | |
| 2204 | + enhanced-resolve@5.24.5: | |
| 2205 | + dependencies: | |
| 2206 | + graceful-fs: 4.2.11 | |
| 2207 | + tapable: 2.3.3 | |
| 2208 | + | |
| 1095 | 2209 | es-module-lexer@1.7.0: {} |
| 1096 | 2210 | |
| 2211 | + es-toolkit@1.52.0: {} | |
| 2212 | + | |
| 1097 | 2213 | esbuild@0.28.2: |
| 1098 | 2214 | optionalDependencies: |
| 1099 | 2215 | '@esbuild/aix-ppc64': 0.28.2 |
@@ -1127,6 +2243,8 @@ snapshots: | ||
| 1127 | 2243 | dependencies: |
| 1128 | 2244 | '@types/estree': 1.0.9 |
| 1129 | 2245 | |
| 2246 | + eventemitter3@5.0.4: {} | |
| 2247 | + | |
| 1130 | 2248 | expect-type@1.4.0: {} |
| 1131 | 2249 | |
| 1132 | 2250 | fast-sha256@1.3.0: {} |
@@ -1135,9 +2253,26 @@ snapshots: | ||
| 1135 | 2253 | optionalDependencies: |
| 1136 | 2254 | picomatch: 4.0.7 |
| 1137 | 2255 | |
| 2256 | + framer-motion@12.43.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): | |
| 2257 | + dependencies: | |
| 2258 | + motion-dom: 12.43.0 | |
| 2259 | + motion-utils: 12.39.0 | |
| 2260 | + tslib: 2.8.1 | |
| 2261 | + optionalDependencies: | |
| 2262 | + react: 19.2.8 | |
| 2263 | + react-dom: 19.2.8(react@19.2.8) | |
| 2264 | + | |
| 1138 | 2265 | fsevents@2.3.3: |
| 1139 | 2266 | optional: true |
| 1140 | 2267 | |
| 2268 | + graceful-fs@4.2.11: {} | |
| 2269 | + | |
| 2270 | + immer@11.1.18: {} | |
| 2271 | + | |
| 2272 | + internmap@2.0.3: {} | |
| 2273 | + | |
| 2274 | + jiti@2.7.0: {} | |
| 2275 | + | |
| 1141 | 2276 | js-tokens@9.0.1: {} |
| 1142 | 2277 | |
| 1143 | 2278 | json-schema-to-ts@3.1.1: |
@@ -1145,16 +2280,100 @@ snapshots: | ||
| 1145 | 2280 | '@babel/runtime': 7.29.7 |
| 1146 | 2281 | ts-algebra: 2.0.0 |
| 1147 | 2282 | |
| 2283 | + lightningcss-android-arm64@1.32.0: | |
| 2284 | + optional: true | |
| 2285 | + | |
| 2286 | + lightningcss-darwin-arm64@1.32.0: | |
| 2287 | + optional: true | |
| 2288 | + | |
| 2289 | + lightningcss-darwin-x64@1.32.0: | |
| 2290 | + optional: true | |
| 2291 | + | |
| 2292 | + lightningcss-freebsd-x64@1.32.0: | |
| 2293 | + optional: true | |
| 2294 | + | |
| 2295 | + lightningcss-linux-arm-gnueabihf@1.32.0: | |
| 2296 | + optional: true | |
| 2297 | + | |
| 2298 | + lightningcss-linux-arm64-gnu@1.32.0: | |
| 2299 | + optional: true | |
| 2300 | + | |
| 2301 | + lightningcss-linux-arm64-musl@1.32.0: | |
| 2302 | + optional: true | |
| 2303 | + | |
| 2304 | + lightningcss-linux-x64-gnu@1.32.0: | |
| 2305 | + optional: true | |
| 2306 | + | |
| 2307 | + lightningcss-linux-x64-musl@1.32.0: | |
| 2308 | + optional: true | |
| 2309 | + | |
| 2310 | + lightningcss-win32-arm64-msvc@1.32.0: | |
| 2311 | + optional: true | |
| 2312 | + | |
| 2313 | + lightningcss-win32-x64-msvc@1.32.0: | |
| 2314 | + optional: true | |
| 2315 | + | |
| 2316 | + lightningcss@1.32.0: | |
| 2317 | + dependencies: | |
| 2318 | + detect-libc: 2.1.2 | |
| 2319 | + optionalDependencies: | |
| 2320 | + lightningcss-android-arm64: 1.32.0 | |
| 2321 | + lightningcss-darwin-arm64: 1.32.0 | |
| 2322 | + lightningcss-darwin-x64: 1.32.0 | |
| 2323 | + lightningcss-freebsd-x64: 1.32.0 | |
| 2324 | + lightningcss-linux-arm-gnueabihf: 1.32.0 | |
| 2325 | + lightningcss-linux-arm64-gnu: 1.32.0 | |
| 2326 | + lightningcss-linux-arm64-musl: 1.32.0 | |
| 2327 | + lightningcss-linux-x64-gnu: 1.32.0 | |
| 2328 | + lightningcss-linux-x64-musl: 1.32.0 | |
| 2329 | + lightningcss-win32-arm64-msvc: 1.32.0 | |
| 2330 | + lightningcss-win32-x64-msvc: 1.32.0 | |
| 2331 | + | |
| 1148 | 2332 | loupe@3.2.1: {} |
| 1149 | 2333 | |
| 2334 | + lucide-react@1.44.0(react@19.2.8): | |
| 2335 | + dependencies: | |
| 2336 | + react: 19.2.8 | |
| 2337 | + | |
| 1150 | 2338 | magic-string@0.30.21: |
| 1151 | 2339 | dependencies: |
| 1152 | 2340 | '@jridgewell/sourcemap-codec': 1.6.0 |
| 1153 | 2341 | |
| 2342 | + motion-dom@12.43.0: | |
| 2343 | + dependencies: | |
| 2344 | + motion-utils: 12.39.0 | |
| 2345 | + | |
| 2346 | + motion-utils@12.39.0: {} | |
| 2347 | + | |
| 1154 | 2348 | ms@2.1.3: {} |
| 1155 | 2349 | |
| 1156 | 2350 | nanoid@3.3.19: {} |
| 1157 | 2351 | |
| 2352 | + next@16.3.4(@types/node@24.13.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): | |
| 2353 | + dependencies: | |
| 2354 | + '@next/env': 16.3.4 | |
| 2355 | + '@swc/helpers': 0.5.23 | |
| 2356 | + baseline-browser-mapping: 2.11.22 | |
| 2357 | + caniuse-lite: 1.0.30001810 | |
| 2358 | + postcss: 8.5.23 | |
| 2359 | + react: 19.2.8 | |
| 2360 | + react-dom: 19.2.8(react@19.2.8) | |
| 2361 | + styled-jsx: 5.1.6(react@19.2.8) | |
| 2362 | + optionalDependencies: | |
| 2363 | + '@next/swc-darwin-arm64': 16.3.4 | |
| 2364 | + '@next/swc-darwin-x64': 16.3.4 | |
| 2365 | + '@next/swc-linux-arm64-gnu': 16.3.4 | |
| 2366 | + '@next/swc-linux-arm64-musl': 16.3.4 | |
| 2367 | + '@next/swc-linux-x64-gnu': 16.3.4 | |
| 2368 | + '@next/swc-linux-x64-musl': 16.3.4 | |
| 2369 | + '@next/swc-win32-arm64-msvc': 16.3.4 | |
| 2370 | + '@next/swc-win32-x64-msvc': 16.3.4 | |
| 2371 | + sharp: 0.35.4(@types/node@24.13.4) | |
| 2372 | + transitivePeerDependencies: | |
| 2373 | + - '@babel/core' | |
| 2374 | + - '@types/node' | |
| 2375 | + - babel-plugin-macros | |
| 2376 | + | |
| 1158 | 2377 | pathe@2.0.3: {} |
| 1159 | 2378 | |
| 1160 | 2379 | pathval@2.0.1: {} |
@@ -1204,6 +2423,12 @@ snapshots: | ||
| 1204 | 2423 | dependencies: |
| 1205 | 2424 | playwright-core: 1.63.0 |
| 1206 | 2425 | |
| 2426 | + postcss@8.5.23: | |
| 2427 | + dependencies: | |
| 2428 | + nanoid: 3.3.19 | |
| 2429 | + picocolors: 1.1.1 | |
| 2430 | + source-map-js: 1.2.1 | |
| 2431 | + | |
| 1207 | 2432 | postcss@8.5.28: |
| 1208 | 2433 | dependencies: |
| 1209 | 2434 | nanoid: 3.3.19 |
@@ -1220,6 +2445,52 @@ snapshots: | ||
| 1220 | 2445 | dependencies: |
| 1221 | 2446 | xtend: 4.0.2 |
| 1222 | 2447 | |
| 2448 | + react-dom@19.2.8(react@19.2.8): | |
| 2449 | + dependencies: | |
| 2450 | + react: 19.2.8 | |
| 2451 | + scheduler: 0.27.0 | |
| 2452 | + | |
| 2453 | + react-is@19.3.0: {} | |
| 2454 | + | |
| 2455 | + react-redux@9.3.0(@types/react@19.3.0)(react@19.2.8)(redux@5.0.1): | |
| 2456 | + dependencies: | |
| 2457 | + '@types/use-sync-external-store': 0.0.6 | |
| 2458 | + react: 19.2.8 | |
| 2459 | + use-sync-external-store: 1.7.0(react@19.2.8) | |
| 2460 | + optionalDependencies: | |
| 2461 | + '@types/react': 19.3.0 | |
| 2462 | + redux: 5.0.1 | |
| 2463 | + | |
| 2464 | + react@19.2.8: {} | |
| 2465 | + | |
| 2466 | + recharts@3.10.1(@types/react@19.3.0)(react-dom@19.2.8(react@19.2.8))(react-is@19.3.0)(react@19.2.8)(redux@5.0.1): | |
| 2467 | + dependencies: | |
| 2468 | + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.3.0)(react@19.2.8)(redux@5.0.1))(react@19.2.8) | |
| 2469 | + clsx: 2.1.1 | |
| 2470 | + decimal.js-light: 2.5.1 | |
| 2471 | + es-toolkit: 1.52.0 | |
| 2472 | + eventemitter3: 5.0.4 | |
| 2473 | + immer: 11.1.18 | |
| 2474 | + react: 19.2.8 | |
| 2475 | + react-dom: 19.2.8(react@19.2.8) | |
| 2476 | + react-is: 19.3.0 | |
| 2477 | + react-redux: 9.3.0(@types/react@19.3.0)(react@19.2.8)(redux@5.0.1) | |
| 2478 | + reselect: 5.2.0 | |
| 2479 | + tiny-invariant: 1.3.3 | |
| 2480 | + use-sync-external-store: 1.7.0(react@19.2.8) | |
| 2481 | + victory-vendor: 37.3.6 | |
| 2482 | + transitivePeerDependencies: | |
| 2483 | + - '@types/react' | |
| 2484 | + - redux | |
| 2485 | + | |
| 2486 | + redux-thunk@3.1.0(redux@5.0.1): | |
| 2487 | + dependencies: | |
| 2488 | + redux: 5.0.1 | |
| 2489 | + | |
| 2490 | + redux@5.0.1: {} | |
| 2491 | + | |
| 2492 | + reselect@5.2.0: {} | |
| 2493 | + | |
| 1223 | 2494 | rollup@4.63.1: |
| 1224 | 2495 | dependencies: |
| 1225 | 2496 | '@types/estree': 1.0.9 |
@@ -1252,6 +2523,45 @@ snapshots: | ||
| 1252 | 2523 | '@rollup/rollup-win32-x64-msvc': 4.63.1 |
| 1253 | 2524 | fsevents: 2.3.3 |
| 1254 | 2525 | |
| 2526 | + scheduler@0.27.0: {} | |
| 2527 | + | |
| 2528 | + semver@7.8.5: | |
| 2529 | + optional: true | |
| 2530 | + | |
| 2531 | + sharp@0.35.4(@types/node@24.13.4): | |
| 2532 | + dependencies: | |
| 2533 | + '@img/colour': 1.1.0 | |
| 2534 | + detect-libc: 2.1.2 | |
| 2535 | + semver: 7.8.5 | |
| 2536 | + optionalDependencies: | |
| 2537 | + '@img/sharp-darwin-arm64': 0.35.4 | |
| 2538 | + '@img/sharp-darwin-x64': 0.35.4 | |
| 2539 | + '@img/sharp-freebsd-wasm32': 0.35.4 | |
| 2540 | + '@img/sharp-libvips-darwin-arm64': 1.3.3 | |
| 2541 | + '@img/sharp-libvips-darwin-x64': 1.3.3 | |
| 2542 | + '@img/sharp-libvips-linux-arm': 1.3.3 | |
| 2543 | + '@img/sharp-libvips-linux-arm64': 1.3.3 | |
| 2544 | + '@img/sharp-libvips-linux-ppc64': 1.3.3 | |
| 2545 | + '@img/sharp-libvips-linux-riscv64': 1.3.3 | |
| 2546 | + '@img/sharp-libvips-linux-s390x': 1.3.3 | |
| 2547 | + '@img/sharp-libvips-linux-x64': 1.3.3 | |
| 2548 | + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 | |
| 2549 | + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 | |
| 2550 | + '@img/sharp-linux-arm': 0.35.4 | |
| 2551 | + '@img/sharp-linux-arm64': 0.35.4 | |
| 2552 | + '@img/sharp-linux-ppc64': 0.35.4 | |
| 2553 | + '@img/sharp-linux-riscv64': 0.35.4 | |
| 2554 | + '@img/sharp-linux-s390x': 0.35.4 | |
| 2555 | + '@img/sharp-linux-x64': 0.35.4 | |
| 2556 | + '@img/sharp-linuxmusl-arm64': 0.35.4 | |
| 2557 | + '@img/sharp-linuxmusl-x64': 0.35.4 | |
| 2558 | + '@img/sharp-webcontainers-wasm32': 0.35.4 | |
| 2559 | + '@img/sharp-win32-arm64': 0.35.4 | |
| 2560 | + '@img/sharp-win32-ia32': 0.35.4 | |
| 2561 | + '@img/sharp-win32-x64': 0.35.4 | |
| 2562 | + '@types/node': 24.13.4 | |
| 2563 | + optional: true | |
| 2564 | + | |
| 1255 | 2565 | siginfo@2.0.0: {} |
| 1256 | 2566 | |
| 1257 | 2567 | source-map-js@1.2.1: {} |
@@ -1271,6 +2581,17 @@ snapshots: | ||
| 1271 | 2581 | dependencies: |
| 1272 | 2582 | js-tokens: 9.0.1 |
| 1273 | 2583 | |
| 2584 | + styled-jsx@5.1.6(react@19.2.8): | |
| 2585 | + dependencies: | |
| 2586 | + client-only: 0.0.1 | |
| 2587 | + react: 19.2.8 | |
| 2588 | + | |
| 2589 | + tailwindcss@4.3.3: {} | |
| 2590 | + | |
| 2591 | + tapable@2.3.3: {} | |
| 2592 | + | |
| 2593 | + tiny-invariant@1.3.3: {} | |
| 2594 | + | |
| 1274 | 2595 | tinybench@2.9.0: {} |
| 1275 | 2596 | |
| 1276 | 2597 | tinyexec@0.3.2: {} |
@@ -1288,6 +2609,8 @@ snapshots: | ||
| 1288 | 2609 | |
| 1289 | 2610 | ts-algebra@2.0.0: {} |
| 1290 | 2611 | |
| 2612 | + tslib@2.8.1: {} | |
| 2613 | + | |
| 1291 | 2614 | tsx@4.23.13: |
| 1292 | 2615 | dependencies: |
| 1293 | 2616 | esbuild: 0.28.2 |
@@ -1298,13 +2621,34 @@ snapshots: | ||
| 1298 | 2621 | |
| 1299 | 2622 | undici-types@7.18.2: {} |
| 1300 | 2623 | |
| 1301 | − vite-node@3.2.4(@types/node@24.13.4)(tsx@4.23.13): | |
| 2624 | + use-sync-external-store@1.7.0(react@19.2.8): | |
| 2625 | + dependencies: | |
| 2626 | + react: 19.2.8 | |
| 2627 | + | |
| 2628 | + victory-vendor@37.3.6: | |
| 2629 | + dependencies: | |
| 2630 | + '@types/d3-array': 3.2.2 | |
| 2631 | + '@types/d3-ease': 3.0.2 | |
| 2632 | + '@types/d3-interpolate': 3.0.4 | |
| 2633 | + '@types/d3-scale': 4.0.9 | |
| 2634 | + '@types/d3-shape': 3.2.0 | |
| 2635 | + '@types/d3-time': 3.0.4 | |
| 2636 | + '@types/d3-timer': 3.0.2 | |
| 2637 | + d3-array: 3.2.4 | |
| 2638 | + d3-ease: 3.0.1 | |
| 2639 | + d3-interpolate: 3.0.1 | |
| 2640 | + d3-scale: 4.0.2 | |
| 2641 | + d3-shape: 3.2.0 | |
| 2642 | + d3-time: 3.1.0 | |
| 2643 | + d3-timer: 3.0.1 | |
| 2644 | + | |
| 2645 | + vite-node@3.2.4(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13): | |
| 1302 | 2646 | dependencies: |
| 1303 | 2647 | cac: 6.7.14 |
| 1304 | 2648 | debug: 4.4.3 |
| 1305 | 2649 | es-module-lexer: 1.7.0 |
| 1306 | 2650 | pathe: 2.0.3 |
| 1307 | − vite: 7.3.6(@types/node@24.13.4)(tsx@4.23.13) | |
| 2651 | + vite: 7.3.6(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13) | |
| 1308 | 2652 | transitivePeerDependencies: |
| 1309 | 2653 | - '@types/node' |
| 1310 | 2654 | - jiti |
@@ -1319,7 +2663,7 @@ snapshots: | ||
| 1319 | 2663 | - tsx |
| 1320 | 2664 | - yaml |
| 1321 | 2665 | |
| 1322 | − vite@7.3.6(@types/node@24.13.4)(tsx@4.23.13): | |
| 2666 | + vite@7.3.6(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13): | |
| 1323 | 2667 | dependencies: |
| 1324 | 2668 | esbuild: 0.28.2 |
| 1325 | 2669 | fdir: 6.5.0(picomatch@4.0.7) |
@@ -1330,13 +2674,15 @@ snapshots: | ||
| 1330 | 2674 | optionalDependencies: |
| 1331 | 2675 | '@types/node': 24.13.4 |
| 1332 | 2676 | fsevents: 2.3.3 |
| 2677 | + jiti: 2.7.0 | |
| 2678 | + lightningcss: 1.32.0 | |
| 1333 | 2679 | tsx: 4.23.13 |
| 1334 | 2680 | |
| 1335 | − vitest@3.2.7(@types/node@24.13.4)(tsx@4.23.13): | |
| 2681 | + vitest@3.2.7(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13): | |
| 1336 | 2682 | dependencies: |
| 1337 | 2683 | '@types/chai': 5.2.3 |
| 1338 | 2684 | '@vitest/expect': 3.2.7 |
| 1339 | − '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.4)(tsx@4.23.13)) | |
| 2685 | + '@vitest/mocker': 3.2.7(vite@7.3.6(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13)) | |
| 1340 | 2686 | '@vitest/pretty-format': 3.2.7 |
| 1341 | 2687 | '@vitest/runner': 3.2.7 |
| 1342 | 2688 | '@vitest/snapshot': 3.2.7 |
@@ -1354,8 +2700,8 @@ snapshots: | ||
| 1354 | 2700 | tinyglobby: 0.2.17 |
| 1355 | 2701 | tinypool: 1.1.1 |
| 1356 | 2702 | tinyrainbow: 2.0.0 |
| 1357 | − vite: 7.3.6(@types/node@24.13.4)(tsx@4.23.13) | |
| 1358 | − vite-node: 3.2.4(@types/node@24.13.4)(tsx@4.23.13) | |
| 2703 | + vite: 7.3.6(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13) | |
| 2704 | + vite-node: 3.2.4(@types/node@24.13.4)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.13) | |
| 1359 | 2705 | why-is-node-running: 2.3.0 |
| 1360 | 2706 | optionalDependencies: |
| 1361 | 2707 | '@types/node': 24.13.4 |
modified
tsconfig.json
+1 −1
@@ -30,5 +30,5 @@ | ||
| 30 | 30 | } |
| 31 | 31 | }, |
| 32 | 32 | "include": ["apps/**/*.ts", "packages/**/*.ts", "scripts/**/*.ts"], |
| 33 | − "exclude": ["node_modules", "**/node_modules", "data", "connectors"] | |
| 33 | + "exclude": ["node_modules", "**/node_modules", "data", "connectors", "apps/dashboard"] | |
| 34 | 34 | } |
| 35 | 35 | |