diff --git a/.gitignore b/.gitignore
index 21aae6b..30ac3f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,5 @@ node_modules/
*.streamDeckPlugin
.DS_Store
com.bryce.llamawatch.sdPlugin/bin/
+store-assets/*.png
+store-assets/.gen/
diff --git a/com.bryce.llamawatch.sdPlugin/manifest.json b/com.bryce.llamawatch.sdPlugin/manifest.json
index 7cb39b0..283f491 100644
--- a/com.bryce.llamawatch.sdPlugin/manifest.json
+++ b/com.bryce.llamawatch.sdPlugin/manifest.json
@@ -2,7 +2,7 @@
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
"Name": "llama-watch",
"Version": "1.0.0.0",
- "Author": "Bryce Zuccaro",
+ "Author": "c4ch3c4d3",
"Actions": [
{
"Name": "In-Flight Monitor",
@@ -56,4 +56,4 @@
"Version": "24"
},
"UUID": "com.bryce.llamawatch"
-}
+}
\ No newline at end of file
diff --git a/scripts/store-assets.mts b/scripts/store-assets.mts
new file mode 100644
index 0000000..b4ddf03
--- /dev/null
+++ b/scripts/store-assets.mts
@@ -0,0 +1,188 @@
+import { mkdirSync, rmSync, writeFileSync } from "node:fs";
+import { execFileSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import path from "node:path";
+import { renderGpuGraph, renderInflight, renderUsage } from "../src/lib/render";
+
+const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
+const OUT = path.join(ROOT, "store-assets");
+const TMP = path.join(OUT, ".gen");
+const FONT = "Helvetica, Arial, sans-serif";
+const BG = "#0e1117";
+const PANEL = "#161b26";
+const STROKE = "#242b38";
+const MUTED = "#8b93a5";
+const WHITE = "#e6e9ef";
+const GREEN = "#3fae5a";
+const RED = "#e0453a";
+const AMBER = "#d9a02a";
+
+rmSync(TMP, { recursive: true, force: true });
+mkdirSync(TMP, { recursive: true });
+
+function convert(src: string, dst: string, w: number, h: number): void {
+ execFileSync("rsvg-convert", ["--unlimited", "-w", String(w), "-h", String(h), "-o", dst, src]);
+}
+
+function rasterizeKey(name: string, svg: string, scale = 3): string {
+ const svgPath = path.join(TMP, `${name}.svg`);
+ const pngPath = path.join(TMP, `${name}.png`);
+ writeFileSync(svgPath, svg);
+ convert(svgPath, pngPath, 72 * scale, 72 * scale);
+ return `${name}.png`;
+}
+
+function svg(w: number, h: number, body: string): string {
+ return ``;
+}
+
+function text(content: string, x: number, y: number, size: number, fill: string, weight = "normal", anchor = "middle"): string {
+ return ` ${content}`;
+}
+
+function keyImage(png: string, cx: number, cy: number, size: number): string {
+ return ` `;
+}
+
+function keyWithLabel(png: string, label: string, cx: number, cy: number, size: number): string {
+ return `${keyImage(png, cx, cy, size)}
+
+${text(label, cx, cy + size / 2 + 40, 26, WHITE)}`;
+}
+
+function header(title: string, subtitle: string): string {
+ return `${text(title, 960, 108, 56, WHITE, "bold")}
+${text(subtitle, 960, 164, 28, MUTED)}`;
+}
+
+function footer(caption: string): string {
+ return `${text(caption, 960, 872, 24, MUTED)}`;
+}
+
+const keys: Record = {
+ inflightIdle: renderInflight({ modelName: "DeepSeek-V4-Flash-0731", state: "ready", count: 0, offline: false }),
+ inflightActive: renderInflight({ modelName: "DeepSeek-V4-Flash-0731", state: "ready", count: 3, offline: false, history: [0, 1, 2, 3, 2, 3, 4, 3, 3, 2, 3, 2, 1] }),
+ usageP95: renderUsage({ modelName: "DeepSeek-V4-Flash-0731", stats: { totalRequests: 1950, totalInputTokens: 312177540, totalOutputTokens: 889193, genP95: 378.86 }, primaryStat: "gen_p95", offline: false }),
+ usageReq: renderUsage({ modelName: "All Models", stats: { totalRequests: 1985, totalInputTokens: 312327705, totalOutputTokens: 932710, genP95: 378.86 }, primaryStat: "requests", offline: false }),
+ gpuUtil: renderGpuGraph({ gpuName: "GPU 0 · RTX 5090", metric: "util_percent", value: 67, history: [10, 20, 40, 67, 55, 80, 72, 45, 60], offline: false }),
+ gpuPower: renderGpuGraph({ gpuName: "GPU 1 · RTX 5090", metric: "power", value: 312, history: [100, 150, 200, 312, 280, 350, 220, 190, 260], offline: false }),
+ gpuAll: renderGpuGraph({ gpuName: "ALL GPUS", metric: "temperature", value: 63, history: [55, 58, 61, 63, 60, 65, 64, 62, 66], offline: false }),
+ gpuCombo: renderGpuGraph({ gpuName: "GPU 0+2", metric: "util_percent", value: 71, history: [30, 45, 52, 71, 64, 78, 70, 58, 66], offline: false }),
+};
+
+const pngs: Record = {};
+for (const [name, svg] of Object.entries(keys)) {
+ pngs[name] = rasterizeKey(name, svg);
+}
+
+function writePng(name: string, w: number, h: number, body: string): void {
+ const svgPath = path.join(TMP, `${name}.svg`);
+ writeFileSync(svgPath, svg(w, h, body));
+ convert(svgPath, path.join(OUT, name), w, h);
+ console.log(`wrote store-assets/${name} (${w}x${h})`);
+}
+
+// Thumbnail — 1920x960
+{
+ const s = 240;
+ const cx = [1250, 1570];
+ const cy = [360, 700];
+ const cols = [
+ keyWithLabel(pngs.inflightActive, "In-Flight Monitor", cx[0], cy[0], s),
+ keyWithLabel(pngs.usageReq, "Usage stats", cx[1], cy[0], s),
+ keyWithLabel(pngs.gpuUtil, "GPU Graph", cx[0], cy[1], s),
+ keyWithLabel(pngs.gpuCombo, "GPU combination", cx[1], cy[1], s),
+ ];
+ const bullets = [
+ ["#3fae5a", "In-flight request counts with a 60-second activity spark"],
+ ["#d9a02a", "Usage stats: requests, tokens, and generation-speed P95"],
+ ["#e0453a", "Live GPU graphs — per GPU, all GPUs, or a combination"],
+ ]
+ .map(
+ ([color, t], i) =>
+ ` \n` +
+ text(t, 146, 488 + i * 78, 28, WHITE, "normal", "start"),
+ )
+ .join("\n");
+ writePng(
+ "thumbnail.png",
+ 1920,
+ 960,
+ `${text("llama-watch", 116, 250, 96, WHITE, "bold", "start")}
+${text("Stream Deck keys for your llama-swap LLM server", 116, 322, 30, MUTED, "normal", "start")}
+
+${bullets}
+${cols.join("\n")}`,
+ );
+}
+
+// Gallery 1 — In-Flight Monitor
+{
+ const s = 240;
+ const centers = [470, 810, 1150, 1490];
+ const items = [
+ [pngs.inflightIdle, "IDLE — no requests"],
+ [pngs.inflightActive, "3 in flight + 60s spark"],
+ [pngs.usageP95, "Usage · gen speed P95"],
+ [pngs.usageReq, "Usage · totals"],
+ ];
+ writePng(
+ "gallery-1-inflight.png",
+ 1920,
+ 960,
+ `${header("In-Flight Monitor", "Live request counts, a 60-second activity spark, and usage stats")}
+${items.map(([png, label], i) => keyWithLabel(png as string, label as string, centers[i], 460, s)).join("\n")}
+${footer("Counts update instantly from the SSE feed; the spark shows the last 60 seconds of activity.")}`,
+ );
+}
+
+// Gallery 2 — GPU Graph
+{
+ const s = 240;
+ const centers = [470, 810, 1150, 1490];
+ const items = [
+ [pngs.gpuUtil, "GPU 0 · utilization"],
+ [pngs.gpuPower, "GPU 1 · power draw"],
+ [pngs.gpuAll, "All GPUs · temperature"],
+ [pngs.gpuCombo, "GPU 0+2 · utilization combo"],
+ ];
+ writePng(
+ "gallery-2-gpu.png",
+ 1920,
+ 960,
+ `${header("GPU Graph", "Live metric charts — one GPU, all GPUs, or a custom combination")}
+${items.map(([png, label], i) => keyWithLabel(png as string, label as string, centers[i], 460, s)).join("\n")}
+${footer("Sampled every 5 seconds: utilization, VRAM, temperature, power draw, and fan speed.")}`,
+ );
+}
+
+// Gallery 3 — Setup
+{
+ const field = (label: string, value: string, y: number): string =>
+ `${text(label, 200, y, 22, MUTED, "normal", "start")}
+
+${text(value, 220, y + 22 + 29, 22, WHITE, "normal", "start")}`;
+ const panel = `${text("In-Flight Monitor settings", 200, 240, 30, WHITE, "bold", "start")}
+${field("Base URL", "http://localhost:9292", 300)}
+${field("API Key", "Optional", 395)}
+${field("Model", "DeepSeek-V4-Flash-0731 (ready)", 490)}
+${field("Display", "Usage stats", 585)}
+${field("Primary stat", "Generation speed P95", 680)}
+ `;
+ writePng(
+ "gallery-3-setup.png",
+ 1920,
+ 960,
+ `${header("Point it at your llama-swap instance", "Per-key settings in the property inspector")}
+${panel}
+${keyWithLabel(pngs.usageReq, "Usage stats key", 1420, 430, 300)}
+${footer("Every key is independent: model, display mode, GPU, metric, and combination.")}`,
+ );
+}
+
+// App icon — 288x288
+convert(path.join(ROOT, "com.bryce.llamawatch.sdPlugin/imgs/plugin/icon.svg"), path.join(OUT, "app-icon-288.png"), 288, 288);
+console.log("wrote store-assets/app-icon-288.png (288x288)");
diff --git a/store-assets/listing.md b/store-assets/listing.md
new file mode 100644
index 0000000..5a2de3c
--- /dev/null
+++ b/store-assets/listing.md
@@ -0,0 +1,75 @@
+# Marketplace Listing — llama-watch
+
+Copy the content below into Maker Console when creating the product.
+
+## Product type
+
+Stream Deck plugin
+
+## Name
+
+`llama-watch`
+
+## Author / Organization
+
+Organization: `c4ch3c4d3` (create this in Maker Console; sign the Maker Agreement)
+
+## Description
+
+> Copy exactly (254 chars first 250-char segment is unformatted; total ~900 chars, within the 250–1500 limit):
+
+```
+llama-watch brings live monitoring of your llama-swap LLM server to your Stream Deck. Watch in-flight request counts per model with a real-time activity spark, switch to Usage stats to see request totals, processed and generated tokens, and the generation-speed P95 — per model or across all models. GPU Graph keys render live line charts of GPU utilization, VRAM, temperature, power draw, and fan speed — for one GPU, all GPUs, or a custom combination.
+
+Features
+- In-Flight Monitor: live per-model request count with a 60-second activity spark; Usage stats display (requests, processed/generated tokens, generation-speed P95) per model or all models.
+- GPU Graph: live charts for utilization, VRAM, temperature, power draw, and fan speed — single GPU, all GPUs, or a custom combination.
+- Per-key configuration: base URL and optional API key; every key is independent.
+
+Requirements
+- A running llama-swap instance (open source) and its base URL — default http://localhost:9292.
+- Optional API key for protected instances.
+- macOS 13 or later; Stream Deck 7.1 or later.
+
+Privacy: the plugin reads data only from your configured llama-swap server and sends nothing elsewhere. No analytics, no telemetry.
+```
+
+## Tags
+
+`monitoring`, `gpu`, `llm`, `llama-swap`, `developer tools`
+
+## Price
+
+Free
+
+## Release notes (v1.0.0)
+
+```
+Initial release.
+
+- In-Flight Monitor: live per-model request count with a 60-second activity spark; Usage stats display (requests, processed/generated tokens, generation-speed P95) per model or all models.
+- GPU Graph: live charts for utilization, VRAM, temperature, power draw, and fan speed — single GPU, all GPUs, or a custom combination.
+- Per-key base URL and optional API key settings.
+```
+
+## Media checklist (generated by scripts/store-assets.mts)
+
+- `store-assets/app-icon-288.png` — app icon, 288×288 PNG
+- `store-assets/thumbnail.png` — thumbnail, 1920×960 PNG
+- `store-assets/gallery-1-inflight.png` — gallery, 1920×960 PNG
+- `store-assets/gallery-2-gpu.png` — gallery, 1920×960 PNG
+- `store-assets/gallery-3-setup.png` — gallery, 1920×960 PNG
+
+## Submission steps (Maker Console)
+
+1. maker.elgato.com → create organization `c4ch3c4d3` → sign the Maker Agreement.
+2. Home → Create product → Stream Deck plugin.
+3. Upload `com.bryce.llamawatch.streamDeckPlugin` (rebuilt with Author `c4ch3c4d3`).
+4. Details: name `llama-watch`, description above, tags, price Free.
+5. Media: upload the 5 PNGs above as app icon + thumbnail + 3 gallery items.
+6. Release notes: v1.0.0 above. Submit.
+7. Review: allow 4–10 business days; handle feedback by submitting a revision.
+
+Notes
+- The product name and monetization cannot be changed after creation without contacting maker@elgato.com — confirm `llama-watch` is still available when you create it.
+- No demo video is required (the plugin integrates with the user's own software, not a paid service/hardware).