305 lines
10 KiB
JavaScript
305 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
// Frigate Classification Page Inspector
|
|
// Phase 1: Fetch API data via HTTP to understand the data model
|
|
// Phase 2: Fetch the classification page HTML and parse its DOM structure
|
|
// No browser needed — uses plain HTTP requests.
|
|
|
|
import { spawn } from "child_process";
|
|
import http from "http";
|
|
|
|
const KUBE_CONFIG = `${process.cwd()}/config`;
|
|
const LOCAL_PORT = 15000;
|
|
const FRIGATE_NS = "frigate";
|
|
const FRIGATE_SVC = "svc/frigate";
|
|
const FRIGATE_PORT = 80;
|
|
const BASE_URL = `http://localhost:${LOCAL_PORT}`;
|
|
|
|
async function startPortForward() {
|
|
console.log(
|
|
`[port-forward] Starting: kubectl port-forward ${FRIGATE_SVC} ${LOCAL_PORT}:${FRIGATE_PORT} -n ${FRIGATE_NS}`,
|
|
);
|
|
const proc = spawn(
|
|
"kubectl",
|
|
[
|
|
"--kubeconfig",
|
|
KUBE_CONFIG,
|
|
"port-forward",
|
|
FRIGATE_SVC,
|
|
`${LOCAL_PORT}:${FRIGATE_PORT}`,
|
|
"-n",
|
|
FRIGATE_NS,
|
|
],
|
|
{ stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const timer = setTimeout(
|
|
() => reject(new Error("Port-forward timed out")),
|
|
15000,
|
|
);
|
|
proc.stdout.on("data", (data) => {
|
|
if (data.toString().includes("Forwarding from")) {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
}
|
|
});
|
|
proc.stderr.on("data", (data) => {
|
|
console.error(`[port-forward stderr] ${data.toString().trim()}`);
|
|
});
|
|
proc.on("error", (err) => {
|
|
clearTimeout(timer);
|
|
reject(err);
|
|
});
|
|
proc.on("exit", (code) => {
|
|
if (code !== null && code !== 0) {
|
|
clearTimeout(timer);
|
|
reject(new Error(`port-forward exited with code ${code}`));
|
|
}
|
|
});
|
|
});
|
|
|
|
console.log("[port-forward] Ready");
|
|
return proc;
|
|
}
|
|
|
|
function httpGet(path) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = `${BASE_URL}${path}`;
|
|
http.get(url, (res) => {
|
|
let body = "";
|
|
res.on("data", (chunk) => (body += chunk));
|
|
res.on("end", () =>
|
|
resolve({ status: res.statusCode, body, headers: res.headers }),
|
|
);
|
|
}).on("error", (err) => reject(err));
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
let portForwardProc = null;
|
|
|
|
try {
|
|
portForwardProc = await startPortForward();
|
|
|
|
// ================================================================
|
|
// API INSPECTION
|
|
// ================================================================
|
|
console.log("\n" + "=".repeat(80));
|
|
console.log("API INSPECTION");
|
|
console.log("=".repeat(80));
|
|
|
|
// Get config to find model names
|
|
const configResp = await httpGet("/api/config");
|
|
let modelNames = [];
|
|
if (configResp.status === 200) {
|
|
try {
|
|
const config = JSON.parse(configResp.body);
|
|
// Custom classification models are under config.classification.custom
|
|
const classificationModels = config.classification?.custom || {};
|
|
modelNames = Object.keys(classificationModels);
|
|
console.log(
|
|
`\n[API] /api/config - Classification models: ${JSON.stringify(modelNames)}`,
|
|
);
|
|
console.log(
|
|
`[API] Classification config:\n${JSON.stringify(config.classification, null, 2)}`,
|
|
);
|
|
} catch (e) {
|
|
console.log(`[API] /api/config - Failed to parse: ${e.message}`);
|
|
console.log(
|
|
`[API] Raw (first 500): ${configResp.body.slice(0, 500)}`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log(`[API] /api/config - HTTP ${configResp.status}`);
|
|
}
|
|
|
|
for (const model of modelNames) {
|
|
console.log(`\n--- Model: ${model} ---`);
|
|
const encodedModel = encodeURIComponent(model);
|
|
|
|
// Dataset endpoint
|
|
const datasetResp = await httpGet(
|
|
`/api/classification/${encodedModel}/dataset`,
|
|
);
|
|
if (datasetResp.status === 200) {
|
|
try {
|
|
const dataset = JSON.parse(datasetResp.body);
|
|
// Dataset response: { categories: { catName: [files...] }, training_metadata: {...} }
|
|
const cats = dataset.categories || dataset;
|
|
const categories = Object.keys(cats);
|
|
console.log(`[API] /api/classification/${model}/dataset`);
|
|
console.log(` Categories: ${JSON.stringify(categories)}`);
|
|
for (const cat of categories) {
|
|
const items = Array.isArray(cats[cat]) ? cats[cat] : [];
|
|
console.log(
|
|
` "${cat}": ${items.length} items, first 3: ${JSON.stringify(items.slice(0, 3))}`,
|
|
);
|
|
}
|
|
if (dataset.training_metadata) {
|
|
console.log(` Training metadata: ${JSON.stringify(dataset.training_metadata, null, 2)}`);
|
|
}
|
|
} catch (e) {
|
|
console.log(` Failed to parse dataset: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`[API] /api/classification/${model}/dataset - HTTP ${datasetResp.status}: ${datasetResp.body.slice(0, 200)}`,
|
|
);
|
|
}
|
|
|
|
// Train endpoint
|
|
const trainResp = await httpGet(
|
|
`/api/classification/${encodedModel}/train`,
|
|
);
|
|
if (trainResp.status === 200) {
|
|
try {
|
|
const train = JSON.parse(trainResp.body);
|
|
const entries = Array.isArray(train) ? train : Object.entries(train);
|
|
console.log(`[API] /api/classification/${model}/train`);
|
|
console.log(
|
|
` Type: ${Array.isArray(train) ? "array" : typeof train}, length/keys: ${Array.isArray(train) ? train.length : Object.keys(train).length}`,
|
|
);
|
|
console.log(
|
|
` First 5 entries:\n${JSON.stringify(entries.slice(0, 5), null, 2)}`,
|
|
);
|
|
} catch (e) {
|
|
console.log(` Failed to parse train: ${e.message}`);
|
|
}
|
|
} else {
|
|
console.log(
|
|
`[API] /api/classification/${model}/train - HTTP ${trainResp.status}: ${trainResp.body.slice(0, 200)}`,
|
|
);
|
|
}
|
|
|
|
// Try to get a thumbnail URL to understand the image src pattern
|
|
if (trainResp.status === 200) {
|
|
try {
|
|
const train = JSON.parse(trainResp.body);
|
|
const firstFile = Array.isArray(train) ? train[0] : null;
|
|
if (firstFile) {
|
|
// Try various thumbnail URL patterns
|
|
const patterns = [
|
|
`/api/classification/${encodedModel}/train/${firstFile}/thumbnail.jpg`,
|
|
`/api/classification/${encodedModel}/train/${firstFile}`,
|
|
`/clips/${encodedModel}/train/${firstFile}`,
|
|
];
|
|
for (const p of patterns) {
|
|
const resp = await httpGet(p);
|
|
console.log(
|
|
` Thumbnail URL test: ${p} -> HTTP ${resp.status} (content-type: ${resp.headers["content-type"]}, size: ${resp.body.length})`,
|
|
);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// HTML/DOM INSPECTION
|
|
// ================================================================
|
|
console.log("\n" + "=".repeat(80));
|
|
console.log("HTML / DOM INSPECTION");
|
|
console.log("=".repeat(80));
|
|
|
|
// Fetch the main classification page HTML
|
|
const classifPageResp = await httpGet("/classification");
|
|
console.log(
|
|
`\n[HTML] /classification - HTTP ${classifPageResp.status} (${classifPageResp.body.length} bytes)`,
|
|
);
|
|
|
|
// This is likely a React SPA, so the HTML will be minimal. Let's check.
|
|
const html = classifPageResp.body;
|
|
console.log(`[HTML] First 2000 chars:\n${html.slice(0, 2000)}`);
|
|
|
|
// Check for any JS bundle references (to find source maps or component names)
|
|
const scriptMatches = html.match(/<script[^>]*src="([^"]+)"[^>]*>/g) || [];
|
|
console.log(`\n[HTML] Script tags: ${scriptMatches.length}`);
|
|
for (const s of scriptMatches) {
|
|
console.log(` ${s}`);
|
|
}
|
|
|
|
// Fetch the main JS bundle to look for classification component code
|
|
const jsMatch = html.match(/src="(\/assets\/[^"]+\.js)"/);
|
|
if (jsMatch) {
|
|
console.log(`\n[JS] Fetching main bundle: ${jsMatch[1]}`);
|
|
const jsResp = await httpGet(jsMatch[1]);
|
|
if (jsResp.status === 200) {
|
|
const js = jsResp.body;
|
|
console.log(`[JS] Bundle size: ${js.length} bytes`);
|
|
|
|
// Search for classification-related code patterns
|
|
const searchTerms = [
|
|
"classify image as",
|
|
"Classify image as",
|
|
"categorize",
|
|
"/classification/",
|
|
"dataset/categorize",
|
|
"training_file",
|
|
"train/delete",
|
|
"ModelTraining",
|
|
"classification",
|
|
];
|
|
for (const term of searchTerms) {
|
|
const idx = js.indexOf(term);
|
|
if (idx !== -1) {
|
|
const context = js.slice(Math.max(0, idx - 200), idx + 200);
|
|
console.log(`\n[JS] Found "${term}" at offset ${idx}:`);
|
|
console.log(` ...${context}...`);
|
|
}
|
|
}
|
|
|
|
// Look for the dropdown/select implementation
|
|
const selectTerms = [
|
|
"combobox",
|
|
"listbox",
|
|
"SelectTrigger",
|
|
"SelectContent",
|
|
"SelectItem",
|
|
"Select>",
|
|
"DropdownMenu",
|
|
];
|
|
for (const term of selectTerms) {
|
|
const idx = js.indexOf(term);
|
|
if (idx !== -1) {
|
|
const context = js.slice(Math.max(0, idx - 150), idx + 150);
|
|
console.log(`\n[JS] Found "${term}" at offset ${idx}:`);
|
|
console.log(` ...${context}...`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check if there are multiple JS chunks
|
|
const allJsMatches =
|
|
html.match(/src="(\/assets\/[^"]+\.js)"/g) || [];
|
|
console.log(`\n[JS] All JS assets: ${allJsMatches.length}`);
|
|
for (const m of allJsMatches) {
|
|
const path = m.match(/src="([^"]+)"/)?.[1];
|
|
if (path) console.log(` ${path}`);
|
|
}
|
|
|
|
// Try to fetch the Frigate source for classification view from GitHub
|
|
console.log("\n" + "=".repeat(80));
|
|
console.log("FRIGATE VERSION");
|
|
console.log("=".repeat(80));
|
|
|
|
const versionResp = await httpGet("/api/version");
|
|
if (versionResp.status === 200) {
|
|
console.log(`[API] Frigate version: ${versionResp.body}`);
|
|
}
|
|
|
|
console.log("\n" + "=".repeat(80));
|
|
console.log("INSPECTION COMPLETE");
|
|
console.log("=".repeat(80));
|
|
} catch (err) {
|
|
console.error(`\n[ERROR] ${err.message}`);
|
|
console.error(err.stack);
|
|
} finally {
|
|
if (portForwardProc) {
|
|
console.log("\n[cleanup] Killing port-forward...");
|
|
portForwardProc.kill("SIGTERM");
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|