2026-04-18 13:42:08 -04:00
|
|
|
import express from 'express';
|
|
|
|
|
import pool from '../db/pool.js';
|
|
|
|
|
import { getAmppConfig } from '../ampp/client.js';
|
2026-05-20 15:48:14 -04:00
|
|
|
import { rebuildS3Client, buildTestClient, testS3Connection, getS3Bucket } from '../s3/client.js';
|
2026-04-18 13:42:08 -04:00
|
|
|
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
|
2026-05-20 15:48:14 -04:00
|
|
|
// ── S3 / Object Storage ───────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
router.get('/s3', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`SELECT key, value FROM settings
|
|
|
|
|
WHERE key IN ('s3_endpoint','s3_bucket','s3_access_key','s3_secret_key','s3_region')`
|
|
|
|
|
);
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
// Env defaults — surface what's actually wired so the UI doesn't show
|
|
|
|
|
// "not configured" when the container was started with S3_* env vars.
|
|
|
|
|
const envEndpoint = process.env.S3_ENDPOINT || '';
|
|
|
|
|
const envBucket = process.env.S3_BUCKET || getS3Bucket();
|
|
|
|
|
const envAccessKey = process.env.S3_ACCESS_KEY || '';
|
|
|
|
|
const envSecretKey = process.env.S3_SECRET_KEY || '';
|
|
|
|
|
const envRegion = process.env.S3_REGION || 'us-east-1';
|
|
|
|
|
|
2026-05-20 15:48:14 -04:00
|
|
|
const out = {
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
s3_endpoint: envEndpoint,
|
|
|
|
|
s3_bucket: envBucket,
|
|
|
|
|
s3_access_key: envAccessKey,
|
|
|
|
|
s3_secret_key_exists: !!envSecretKey,
|
|
|
|
|
s3_region: envRegion,
|
|
|
|
|
source: envEndpoint ? 'env' : 'unset',
|
2026-05-20 15:48:14 -04:00
|
|
|
};
|
|
|
|
|
for (const { key, value } of result.rows) {
|
|
|
|
|
if (key === 's3_secret_key') {
|
feat: SDK deployment UI, proxy encoding global settings, S3 env fallback
- Settings: drop AMPP tab, rename GPU/Transcoding → Proxy encoding
with explicit 'applied to every ingested file' wording, expose
CPU codec/preset options when GPU is off
- New Capture SDKs tab (Settings): upload Blackmagic / AJA / Deltacast
SDK archives (.zip / .tar.gz) staged to /sdk/<vendor>/ inside mam-api;
BMD is fully wired into the FFmpeg build pipeline, AJA + Deltacast
staging-only pending FFmpeg patches
- mam-api: new /api/v1/sdk routes (multer upload, extract, list, delete);
Dockerfile gets unzip+tar; docker-compose mounts /mnt/NVME/MAM/sdk:/sdk
- proxy worker now reads proxy-encoding settings from DB on every job,
builds args for libx264 / NVENC / VAAPI, falls back to libx264 on
hardware-encode failure
- settings GET /s3 falls back to S3_* env vars when DB is empty so the
UI reflects what's actually wired (fixes 'not configured' false alarm)
2026-05-22 22:58:32 -04:00
|
|
|
if (value) { out.s3_secret_key_exists = true; out.source = 'db'; }
|
|
|
|
|
} else if (value) {
|
|
|
|
|
out[key] = value;
|
|
|
|
|
out.source = 'db';
|
2026-05-20 15:48:14 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.json(out);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/s3', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const { s3_endpoint, s3_bucket, s3_access_key, s3_secret_key, s3_region } = req.body;
|
|
|
|
|
|
|
|
|
|
if (!s3_endpoint) return res.status(400).json({ error: 's3_endpoint is required' });
|
|
|
|
|
if (!s3_bucket) return res.status(400).json({ error: 's3_bucket is required' });
|
|
|
|
|
|
|
|
|
|
const keys = { s3_endpoint, s3_bucket, s3_region: s3_region || 'us-east-1' };
|
|
|
|
|
if (s3_access_key) keys.s3_access_key = s3_access_key;
|
|
|
|
|
if (s3_secret_key) keys.s3_secret_key = s3_secret_key;
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(keys)) {
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
|
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`,
|
|
|
|
|
[key, value.trim()]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch the full current config (including any previously saved secret)
|
|
|
|
|
const saved = await pool.query(
|
|
|
|
|
`SELECT key, value FROM settings
|
|
|
|
|
WHERE key IN ('s3_endpoint','s3_bucket','s3_access_key','s3_secret_key','s3_region')
|
|
|
|
|
AND value IS NOT NULL AND value <> ''`
|
|
|
|
|
);
|
|
|
|
|
const cfg = {};
|
|
|
|
|
for (const { key, value } of saved.rows) {
|
|
|
|
|
switch (key) {
|
|
|
|
|
case 's3_endpoint': cfg.endpoint = value; break;
|
|
|
|
|
case 's3_bucket': cfg.bucket = value; break;
|
|
|
|
|
case 's3_access_key': cfg.accessKey = value; break;
|
|
|
|
|
case 's3_secret_key': cfg.secretKey = value; break;
|
|
|
|
|
case 's3_region': cfg.region = value; break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rebuildS3Client(cfg);
|
|
|
|
|
|
|
|
|
|
res.json({ message: 'S3 settings saved and applied' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Test with the values the browser just typed (before saving), or with saved
|
|
|
|
|
// creds if the fields are left blank. Secret is sent only if user typed it.
|
|
|
|
|
router.post('/s3/test', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const { s3_endpoint, s3_bucket, s3_access_key, s3_secret_key, s3_region } = req.body;
|
|
|
|
|
|
|
|
|
|
// Merge submitted values with anything already saved in the DB
|
|
|
|
|
const saved = await pool.query(
|
|
|
|
|
`SELECT key, value FROM settings
|
|
|
|
|
WHERE key IN ('s3_endpoint','s3_bucket','s3_access_key','s3_secret_key','s3_region')
|
|
|
|
|
AND value IS NOT NULL AND value <> ''`
|
|
|
|
|
);
|
|
|
|
|
const fromDb = {};
|
|
|
|
|
for (const { key, value } of saved.rows) fromDb[key] = value;
|
|
|
|
|
|
|
|
|
|
const cfg = {
|
|
|
|
|
endpoint: (s3_endpoint || fromDb.s3_endpoint || '').trim(),
|
|
|
|
|
bucket: (s3_bucket || fromDb.s3_bucket || getS3Bucket()).trim(),
|
|
|
|
|
accessKey: (s3_access_key || fromDb.s3_access_key || '').trim(),
|
|
|
|
|
secretKey: (s3_secret_key || fromDb.s3_secret_key || '').trim(),
|
|
|
|
|
region: (s3_region || fromDb.s3_region || 'us-east-1').trim(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!cfg.endpoint) return res.status(400).json({ error: 'S3 endpoint is required' });
|
|
|
|
|
if (!cfg.bucket) return res.status(400).json({ error: 'S3 bucket is required' });
|
|
|
|
|
|
|
|
|
|
const client = buildTestClient(cfg);
|
|
|
|
|
const result = await testS3Connection(client, cfg.bucket);
|
|
|
|
|
res.json(result);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(400).json({ ok: false, error: err.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── AMPP integration ───────────────────────────────────────────────────────────
|
2026-05-20 14:18:43 -04:00
|
|
|
|
2026-04-18 13:42:08 -04:00
|
|
|
router.get('/ampp', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
"SELECT key, value FROM settings WHERE key IN ('ampp_base_url', 'ampp_token')"
|
|
|
|
|
);
|
|
|
|
|
const out = {};
|
|
|
|
|
for (const row of result.rows) {
|
|
|
|
|
if (row.key === 'ampp_token') {
|
2026-05-20 14:18:43 -04:00
|
|
|
out.ampp_token_exists = true;
|
2026-04-18 13:42:08 -04:00
|
|
|
} else {
|
|
|
|
|
out[row.key] = row.value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.json(out);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/ampp', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const { ampp_base_url, ampp_token } = req.body;
|
2026-05-20 14:18:43 -04:00
|
|
|
if (!ampp_base_url) return res.status(400).json({ error: 'ampp_base_url is required' });
|
2026-04-18 13:42:08 -04:00
|
|
|
const baseUrl = ampp_base_url.trim().replace(/\/$/, '');
|
|
|
|
|
await pool.query(
|
2026-05-20 14:18:43 -04:00
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ('ampp_base_url', $1, NOW())
|
2026-04-18 13:42:08 -04:00
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
|
|
|
|
|
[baseUrl]
|
|
|
|
|
);
|
|
|
|
|
if (ampp_token) {
|
|
|
|
|
await pool.query(
|
2026-05-20 14:18:43 -04:00
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ('ampp_token', $1, NOW())
|
2026-04-18 13:42:08 -04:00
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
|
|
|
|
|
[ampp_token.trim()]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
res.json({ message: 'AMPP settings saved' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/ampp/test', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const config = await getAmppConfig();
|
2026-05-20 14:18:43 -04:00
|
|
|
if (!config) return res.status(400).json({ error: 'AMPP credentials not configured' });
|
2026-04-18 13:42:08 -04:00
|
|
|
const testUrl = `${config.ampp_base_url}/api/v1/store/folder/folders?limit=1`;
|
|
|
|
|
const testRes = await fetch(testUrl, {
|
2026-05-20 14:18:43 -04:00
|
|
|
headers: { Authorization: `Bearer ${config.ampp_token}`, 'Content-Type': 'application/json' },
|
2026-04-18 13:42:08 -04:00
|
|
|
});
|
2026-05-20 14:18:43 -04:00
|
|
|
if (!testRes.ok) return res.status(400).json({ error: `AMPP returned HTTP ${testRes.status}` });
|
|
|
|
|
res.json({ message: 'AMPP connection successful' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
res.status(400).json({ error: `Connection failed: ${err.message}` });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-20 15:48:14 -04:00
|
|
|
// ── Hardware inventory ────────────────────────────────────────────────────────
|
2026-05-20 14:18:43 -04:00
|
|
|
|
|
|
|
|
router.get('/hardware', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`SELECT id, hostname, role, ip_address, capabilities,
|
|
|
|
|
EXTRACT(EPOCH FROM (NOW() - last_seen))::int AS stale_seconds
|
|
|
|
|
FROM cluster_nodes
|
|
|
|
|
ORDER BY role, hostname`
|
|
|
|
|
);
|
|
|
|
|
const nodes = result.rows.map(n => ({
|
|
|
|
|
id: n.id,
|
|
|
|
|
hostname: n.hostname,
|
|
|
|
|
role: n.role,
|
|
|
|
|
ip_address: n.ip_address,
|
|
|
|
|
online: n.stale_seconds < 120,
|
|
|
|
|
capabilities: n.capabilities || {},
|
|
|
|
|
}));
|
|
|
|
|
res.json({ nodes });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-20 15:48:14 -04:00
|
|
|
// ── GPU / Transcoding ─────────────────────────────────────────────────────────
|
2026-05-20 14:18:43 -04:00
|
|
|
|
|
|
|
|
router.get('/transcoding', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`SELECT key, value FROM settings
|
2026-05-20 23:41:42 -04:00
|
|
|
WHERE key IN ('gpu_transcode_enabled','gpu_codec','gpu_preset','gpu_bitrate_mbps','gpu_node',
|
|
|
|
|
'gpu_extension','gpu_framerate','gpu_rc_mode','gpu_audio_codec','gpu_audio_bitrate_kbps')`
|
2026-05-20 14:18:43 -04:00
|
|
|
);
|
|
|
|
|
const out = {
|
2026-05-20 23:41:42 -04:00
|
|
|
gpu_transcode_enabled: 'false',
|
|
|
|
|
gpu_codec: 'h264_nvenc',
|
|
|
|
|
gpu_preset: 'p4',
|
|
|
|
|
gpu_bitrate_mbps: '8',
|
|
|
|
|
gpu_node: '',
|
|
|
|
|
gpu_extension: 'mp4',
|
|
|
|
|
gpu_framerate: 'passthrough',
|
|
|
|
|
gpu_rc_mode: 'cbr',
|
|
|
|
|
gpu_audio_codec: 'aac',
|
|
|
|
|
gpu_audio_bitrate_kbps: '192',
|
2026-05-20 14:18:43 -04:00
|
|
|
};
|
|
|
|
|
for (const { key, value } of result.rows) out[key] = value;
|
|
|
|
|
res.json(out);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-18 13:42:08 -04:00
|
|
|
|
2026-05-20 14:18:43 -04:00
|
|
|
router.put('/transcoding', async (req, res, next) => {
|
|
|
|
|
try {
|
2026-05-20 23:41:42 -04:00
|
|
|
const allowed = [
|
|
|
|
|
'gpu_transcode_enabled', 'gpu_codec', 'gpu_preset', 'gpu_bitrate_mbps', 'gpu_node',
|
|
|
|
|
'gpu_extension', 'gpu_framerate', 'gpu_rc_mode', 'gpu_audio_codec', 'gpu_audio_bitrate_kbps',
|
|
|
|
|
];
|
2026-05-20 14:18:43 -04:00
|
|
|
for (const key of allowed) {
|
|
|
|
|
if (req.body[key] !== undefined) {
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
|
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`,
|
|
|
|
|
[key, String(req.body[key])]
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-18 13:42:08 -04:00
|
|
|
}
|
2026-05-20 14:18:43 -04:00
|
|
|
res.json({ message: 'Transcoding settings saved' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-18 13:42:08 -04:00
|
|
|
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
// ── Growing files (SMB landing zone) ─────────────────────────────────────────
|
|
|
|
|
// Lets capture write its master output to a fast local SMB share instead of
|
|
|
|
|
// streaming directly to S3. Premiere can mount the share and edit the file
|
|
|
|
|
// while it's still being written; the promotion worker later moves the
|
|
|
|
|
// finalized file to S3 and flips the asset to status='ready'.
|
|
|
|
|
|
2026-05-31 14:50:31 -04:00
|
|
|
// Growing-files mode is now a PER-RECORDER setting (recorders.growing_enabled);
|
|
|
|
|
// the legacy global `growing_enabled` key is no longer read at recorder start.
|
|
|
|
|
// These global keys describe the shared SMB landing-zone infrastructure only:
|
|
|
|
|
// - growing_path container mount point (default /growing)
|
|
|
|
|
// - growing_smb_url smb://... display string for editors (Premiere)
|
|
|
|
|
// - growing_smb_mount //host/share CIFS source the capture container mounts
|
|
|
|
|
// - growing_smb_username SMB user for the system-side CIFS mount
|
|
|
|
|
// - growing_smb_password SMB password (WRITE-ONLY; never returned)
|
|
|
|
|
// - growing_smb_vers CIFS protocol version (default 3.0)
|
|
|
|
|
// - growing_promote_after_seconds idle threshold before S3 promotion
|
|
|
|
|
const GROWING_KEYS = [
|
|
|
|
|
'growing_path', 'growing_smb_url', 'growing_smb_mount',
|
|
|
|
|
'growing_smb_username', 'growing_smb_vers', 'growing_promote_after_seconds',
|
|
|
|
|
];
|
|
|
|
|
// growing_smb_password is handled separately: stored on PUT but NEVER returned
|
|
|
|
|
// on GET (only a *_exists flag), mirroring s3_secret_key.
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
|
|
|
|
|
router.get('/growing', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
`SELECT key, value FROM settings WHERE key = ANY($1)`,
|
2026-05-31 14:50:31 -04:00
|
|
|
[[...GROWING_KEYS, 'growing_smb_password']]
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
);
|
|
|
|
|
const out = {
|
|
|
|
|
growing_path: '/growing',
|
|
|
|
|
growing_smb_url: '',
|
2026-05-31 14:50:31 -04:00
|
|
|
growing_smb_mount: '',
|
|
|
|
|
growing_smb_username: '',
|
|
|
|
|
growing_smb_vers: '3.0',
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
growing_promote_after_seconds: '8',
|
2026-05-31 14:50:31 -04:00
|
|
|
growing_smb_password_exists: false,
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
};
|
2026-05-31 14:50:31 -04:00
|
|
|
for (const { key, value } of result.rows) {
|
|
|
|
|
if (key === 'growing_smb_password') {
|
|
|
|
|
out.growing_smb_password_exists = !!(value && value.length);
|
|
|
|
|
} else {
|
|
|
|
|
out[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
res.json(out);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/growing', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
for (const key of GROWING_KEYS) {
|
|
|
|
|
if (req.body[key] !== undefined) {
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ($1, $2, NOW())
|
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`,
|
|
|
|
|
[key, String(req.body[key])]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-31 14:50:31 -04:00
|
|
|
// SMB password is write-only. A non-empty value sets/replaces it. To remove
|
|
|
|
|
// it, send growing_smb_password_clear:true. A blank/omitted password field
|
|
|
|
|
// leaves the stored value untouched (so operators don't retype it on every
|
|
|
|
|
// save).
|
|
|
|
|
if (req.body.growing_smb_password_clear === true) {
|
|
|
|
|
await pool.query(`DELETE FROM settings WHERE key = 'growing_smb_password'`);
|
|
|
|
|
} else if (typeof req.body.growing_smb_password === 'string' && req.body.growing_smb_password.length > 0) {
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ('growing_smb_password', $1, NOW())
|
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
|
|
|
|
|
[req.body.growing_smb_password]
|
|
|
|
|
);
|
|
|
|
|
}
|
feat: live HLS preview, proxy worker fixes, Settings tabs, growing-files + Premier panel
- worker/proxy: scale-to-even filter, analyzeduration 100M, skip images, hasAudio
- worker/promotion: SMB landing zone -> S3 on idle, queues proxy job, status='ready'
- web-ui screens-ingest: HlsPreview component replaces fake LiveStrip/FauxFrame
- web-ui screens-admin: functional Settings tabs (S3, GPU, Growing, SDI, AMPP)
- mam-api /settings/growing: GET/PUT growing-files config
- mam-api /assets/:id/live-path: SMB UNC/POSIX path for live growing assets
- capture-manager: GROWING_ENABLED -> write hires to /growing instead of S3 stream
- recorders.js: pass GROWING_ENABLED to capture container, bind /growing mount
- docker-compose: mount /mnt/NVME/MAM/wild-dragon-growing on mam-api + worker
- premiere-plugin: Mount Live button, Relink-to-HiRes, live->ready status poll
2026-05-22 19:12:53 -04:00
|
|
|
res.json({ message: 'Growing-files settings saved' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-20 15:48:14 -04:00
|
|
|
// ── Capture service routing ───────────────────────────────────────────────────
|
2026-05-20 14:18:43 -04:00
|
|
|
|
|
|
|
|
router.get('/capture-service', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const result = await pool.query(
|
|
|
|
|
"SELECT value FROM settings WHERE key = 'capture_service_url'"
|
|
|
|
|
);
|
|
|
|
|
res.json({ capture_service_url: result.rows[0]?.value || '' });
|
2026-04-18 13:42:08 -04:00
|
|
|
} catch (err) {
|
2026-05-20 14:18:43 -04:00
|
|
|
next(err);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/capture-service', async (req, res, next) => {
|
|
|
|
|
try {
|
|
|
|
|
const url = (req.body.capture_service_url || '').trim().replace(/\/$/, '');
|
|
|
|
|
await pool.query(
|
|
|
|
|
`INSERT INTO settings (key, value, updated_at) VALUES ('capture_service_url', $1, NOW())
|
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()`,
|
|
|
|
|
[url]
|
|
|
|
|
);
|
|
|
|
|
res.json({ message: 'Capture service URL saved' });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
next(err);
|
2026-04-18 13:42:08 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default router;
|