Prepare Elgato Marketplace listing: brand author, store assets, listing text
- manifest Author -> c4ch3c4d3 and repackage .streamDeckPlugin - scripts/store-assets.mts renders real key SVGs into store PNGs (app icon 288, thumbnail + 3 gallery 1920x960) - store-assets/listing.md: description, tags, release notes, submission checklist
This commit is contained in:
@@ -2,3 +2,5 @@ node_modules/
|
|||||||
*.streamDeckPlugin
|
*.streamDeckPlugin
|
||||||
.DS_Store
|
.DS_Store
|
||||||
com.bryce.llamawatch.sdPlugin/bin/
|
com.bryce.llamawatch.sdPlugin/bin/
|
||||||
|
store-assets/*.png
|
||||||
|
store-assets/.gen/
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
|
"$schema": "https://schemas.elgato.com/streamdeck/plugins/manifest.json",
|
||||||
"Name": "llama-watch",
|
"Name": "llama-watch",
|
||||||
"Version": "1.0.0.0",
|
"Version": "1.0.0.0",
|
||||||
"Author": "Bryce Zuccaro",
|
"Author": "c4ch3c4d3",
|
||||||
"Actions": [
|
"Actions": [
|
||||||
{
|
{
|
||||||
"Name": "In-Flight Monitor",
|
"Name": "In-Flight Monitor",
|
||||||
|
|||||||
@@ -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 `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
|
||||||
|
<rect width="${w}" height="${h}" fill="${BG}"/>
|
||||||
|
${body}
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function text(content: string, x: number, y: number, size: number, fill: string, weight = "normal", anchor = "middle"): string {
|
||||||
|
return ` <text x="${x}" y="${y}" text-anchor="${anchor}" font-family="${FONT}" font-size="${size}" font-weight="${weight}" fill="${fill}">${content}</text>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyImage(png: string, cx: number, cy: number, size: number): string {
|
||||||
|
return ` <image href="${png}" x="${cx - size / 2}" y="${cy - size / 2}" width="${size}" height="${size}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyWithLabel(png: string, label: string, cx: number, cy: number, size: number): string {
|
||||||
|
return `${keyImage(png, cx, cy, size)}
|
||||||
|
<rect x="${cx - size / 2 - 6}" y="${cy + size / 2 + 14}" width="${size + 12}" height="1" fill="${STROKE}"/>
|
||||||
|
${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<string, string> = {
|
||||||
|
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<string, string> = {};
|
||||||
|
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) =>
|
||||||
|
` <circle cx="116" cy="${480 + i * 78}" r="10" fill="${color}"/>\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")}
|
||||||
|
<rect x="116" y="350" width="620" height="3" fill="${GREEN}"/>
|
||||||
|
${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")}
|
||||||
|
<rect x="200" y="${y + 22}" width="700" height="44" rx="10" fill="${BG}" stroke="${STROKE}" stroke-width="2"/>
|
||||||
|
${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)}
|
||||||
|
<rect x="160" y="170" width="760" height="640" rx="24" fill="${PANEL}" stroke="${STROKE}" stroke-width="3"/>`;
|
||||||
|
writePng(
|
||||||
|
"gallery-3-setup.png",
|
||||||
|
1920,
|
||||||
|
960,
|
||||||
|
`${header("Point it at your llama-swap instance", "Per-key settings in the property inspector")}
|
||||||
|
<g transform="translate(0,0)">${panel}</g>
|
||||||
|
${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)");
|
||||||
@@ -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).
|
||||||
Reference in New Issue
Block a user