Switch admin/API flows to article IDs with legacy job fallback
This commit is contained in:
parent
3cc2828bc0
commit
efcf87d00c
5 changed files with 205 additions and 67 deletions
|
|
@ -4,28 +4,86 @@ require __DIR__ . '/../auth.php';
|
||||||
mvlog_require_login();
|
mvlog_require_login();
|
||||||
|
|
||||||
// Web-server side handler to toggle "Show" for an MVLog job and send a web-push immediately if appropriate.
|
// Web-server side handler to toggle "Show" for an MVLog job and send a web-push immediately if appropriate.
|
||||||
// Usage (POST): job=<job> show=1|0
|
// Preferred usage (POST): id=<article_id> show=1|0
|
||||||
|
// Legacy fallback (POST): job=<job_dir_name> show=1|0
|
||||||
|
|
||||||
$MVLOG_ROOT = dirname(__DIR__);
|
$MVLOG_ROOT = dirname(__DIR__);
|
||||||
$job = $_POST['job'] ?? '';
|
$id = trim((string)($_POST['id'] ?? ''));
|
||||||
|
$job = trim((string)($_POST['job'] ?? ''));
|
||||||
$show = isset($_POST['show']) ? filter_var($_POST['show'], FILTER_VALIDATE_BOOLEAN) : true;
|
$show = isset($_POST['show']) ? filter_var($_POST['show'], FILTER_VALIDATE_BOOLEAN) : true;
|
||||||
|
|
||||||
// Basic validation
|
function mvlog_article_id_is_valid(string $id): bool {
|
||||||
if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) {
|
return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id);
|
||||||
http_response_code(400);
|
|
||||||
echo 'invalid job';
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$jobdir = "$MVLOG_ROOT/in-dir/$job";
|
function mvlog_legacy_job_is_valid(string $job): bool {
|
||||||
if (!is_dir($jobdir)) {
|
return (bool)preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_read_article_id(string $jobdir): string {
|
||||||
|
$path = $jobdir . '/.mvlog-id';
|
||||||
|
if (!is_file($path)) return '';
|
||||||
|
$id = strtolower(trim((string)@file_get_contents($path)));
|
||||||
|
return mvlog_article_id_is_valid($id) ? $id : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_find_jobdir_by_id(string $mvlogRoot, string $articleId): ?string {
|
||||||
|
if (!mvlog_article_id_is_valid($articleId)) return null;
|
||||||
|
|
||||||
|
$cacheFile = $mvlogRoot . '/cache/articles-index.json';
|
||||||
|
if (is_file($cacheFile)) {
|
||||||
|
$cache = json_decode((string)@file_get_contents($cacheFile), true);
|
||||||
|
if (is_array($cache) && is_array($cache['by_id'] ?? null)) {
|
||||||
|
$name = (string)($cache['by_id'][$articleId] ?? '');
|
||||||
|
if ($name !== '') {
|
||||||
|
$candidate = $mvlogRoot . '/in-dir/' . basename($name);
|
||||||
|
if (is_dir($candidate)) return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob($mvlogRoot . '/in-dir/*', GLOB_ONLYDIR) ?: [] as $dir) {
|
||||||
|
if (mvlog_read_article_id($dir) === $articleId) return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_resolve_jobdir(string $mvlogRoot, string $id, string $job, ?string &$resolvedJob, ?string &$resolvedId): ?string {
|
||||||
|
$resolvedJob = null;
|
||||||
|
$resolvedId = null;
|
||||||
|
|
||||||
|
if ($id !== '' && mvlog_article_id_is_valid($id)) {
|
||||||
|
$dir = mvlog_find_jobdir_by_id($mvlogRoot, strtolower($id));
|
||||||
|
if ($dir !== null) {
|
||||||
|
$resolvedJob = basename($dir);
|
||||||
|
$resolvedId = mvlog_read_article_id($dir);
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($job !== '' && mvlog_legacy_job_is_valid($job)) {
|
||||||
|
$dir = $mvlogRoot . '/in-dir/' . basename($job);
|
||||||
|
if (is_dir($dir)) {
|
||||||
|
$resolvedJob = basename($dir);
|
||||||
|
$resolvedId = mvlog_read_article_id($dir);
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolvedJob = null;
|
||||||
|
$resolvedId = null;
|
||||||
|
$jobdir = mvlog_resolve_jobdir($MVLOG_ROOT, strtolower($id), $job, $resolvedJob, $resolvedId);
|
||||||
|
if ($jobdir === null) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo 'job not found';
|
echo 'job not found';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$enabled_file = "$jobdir/.movmaker-enabled";
|
$enabled_file = "$jobdir/.movmaker-enabled";
|
||||||
$cache_file = "$MVLOG_ROOT/cache/push_notifications.json";
|
|
||||||
|
|
||||||
if ($show) {
|
if ($show) {
|
||||||
file_put_contents($enabled_file, date(DATE_ATOM) . PHP_EOL, LOCK_EX);
|
file_put_contents($enabled_file, date(DATE_ATOM) . PHP_EOL, LOCK_EX);
|
||||||
|
|
@ -35,20 +93,16 @@ if ($show) {
|
||||||
if (is_file(__DIR__ . '/../lib/send_push.php')) {
|
if (is_file(__DIR__ . '/../lib/send_push.php')) {
|
||||||
include_once __DIR__ . '/../lib/send_push.php';
|
include_once __DIR__ . '/../lib/send_push.php';
|
||||||
try {
|
try {
|
||||||
$sent = try_send_show_notification($job, $jobdir, $MVLOG_ROOT, '/var/log/mvlog_notify.log');
|
$sent = try_send_show_notification($resolvedId ?: $resolvedJob, $jobdir, $MVLOG_ROOT, '/var/log/mvlog_notify.log');
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
error_log("[mvlog] notify exception for $job: " . $e->getMessage() . "\n", 3, '/var/log/mvlog_notify.log');
|
error_log("[mvlog] notify exception for " . ($resolvedId ?: $resolvedJob) . ": " . $e->getMessage() . "\n", 3, '/var/log/mvlog_notify.log');
|
||||||
$sent = false;
|
$sent = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error_log("[mvlog] send_push helper missing, cannot send notification for $job\n", 3, '/var/log/mvlog_notify.log');
|
error_log("[mvlog] send_push helper missing, cannot send notification for " . ($resolvedId ?: $resolvedJob) . "\n", 3, '/var/log/mvlog_notify.log');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($sent) {
|
echo $sent ? 'enabled' : 'enabled (no notification)';
|
||||||
echo 'enabled';
|
|
||||||
} else {
|
|
||||||
echo 'enabled (no notification)';
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
@unlink($enabled_file);
|
@unlink($enabled_file);
|
||||||
echo 'disabled';
|
echo 'disabled';
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,12 @@ foreach ($dirs as $dir) {
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
$stateFile = $dir . '/.movmaker-state.json';
|
$stateFile = $dir . '/.movmaker-state.json';
|
||||||
$lockDir = $dir . '/.movmaker-lock';
|
$lockDir = $dir . '/.movmaker-lock';
|
||||||
|
$idFile = $dir . '/.mvlog-id';
|
||||||
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
|
||||||
$status = is_array($state) ? (string)($state['status'] ?? '') : '';
|
$status = is_array($state) ? (string)($state['status'] ?? '') : '';
|
||||||
|
$articleId = is_array($state) ? strtolower((string)($state['article_id'] ?? '')) : '';
|
||||||
|
if ($articleId === '' && is_file($idFile)) $articleId = strtolower(trim((string)file_get_contents($idFile)));
|
||||||
|
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) $articleId = '';
|
||||||
$locked = is_dir($lockDir);
|
$locked = is_dir($lockDir);
|
||||||
if ($locked || $status === 'processing') {
|
if ($locked || $status === 'processing') {
|
||||||
$started = '';
|
$started = '';
|
||||||
|
|
@ -19,6 +23,7 @@ foreach ($dirs as $dir) {
|
||||||
if ($started === '' && is_array($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
|
if ($started === '' && is_array($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
|
||||||
$jobs[] = [
|
$jobs[] = [
|
||||||
'name' => $name,
|
'name' => $name,
|
||||||
|
'id' => $articleId,
|
||||||
'status' => $status ?: ($locked ? 'processing' : 'unknown'),
|
'status' => $status ?: ($locked ? 'processing' : 'unknown'),
|
||||||
'started_at' => $started,
|
'started_at' => $started,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,57 @@ mvlog_require_login();
|
||||||
$MVLOG_ROOT = dirname(__DIR__);
|
$MVLOG_ROOT = dirname(__DIR__);
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
$job = $_POST['job'] ?? '';
|
$id = strtolower(trim((string)($_POST['id'] ?? '')));
|
||||||
|
$job = trim((string)($_POST['job'] ?? ''));
|
||||||
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16;
|
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16;
|
||||||
|
|
||||||
if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) {
|
function mvlog_article_id_is_valid(string $id): bool {
|
||||||
http_response_code(400);
|
return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id);
|
||||||
echo json_encode(['status'=>'error','message'=>'invalid job']);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$jobdir = $MVLOG_ROOT . '/in-dir/' . $job;
|
function mvlog_legacy_job_is_valid(string $job): bool {
|
||||||
if (!is_dir($jobdir)) {
|
return (bool)preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_read_article_id(string $jobdir): string {
|
||||||
|
$path = $jobdir . '/.mvlog-id';
|
||||||
|
if (!is_file($path)) return '';
|
||||||
|
$value = strtolower(trim((string)@file_get_contents($path)));
|
||||||
|
return mvlog_article_id_is_valid($value) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_find_jobdir_by_id(string $mvlogRoot, string $articleId): ?string {
|
||||||
|
if (!mvlog_article_id_is_valid($articleId)) return null;
|
||||||
|
|
||||||
|
$cacheFile = $mvlogRoot . '/cache/articles-index.json';
|
||||||
|
if (is_file($cacheFile)) {
|
||||||
|
$cache = json_decode((string)@file_get_contents($cacheFile), true);
|
||||||
|
if (is_array($cache) && is_array($cache['by_id'] ?? null)) {
|
||||||
|
$name = (string)($cache['by_id'][$articleId] ?? '');
|
||||||
|
if ($name !== '') {
|
||||||
|
$candidate = $mvlogRoot . '/in-dir/' . basename($name);
|
||||||
|
if (is_dir($candidate)) return $candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (glob($mvlogRoot . '/in-dir/*', GLOB_ONLYDIR) ?: [] as $dir) {
|
||||||
|
if (mvlog_read_article_id($dir) === $articleId) return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobdir = null;
|
||||||
|
if ($id !== '' && mvlog_article_id_is_valid($id)) {
|
||||||
|
$jobdir = mvlog_find_jobdir_by_id($MVLOG_ROOT, $id);
|
||||||
|
}
|
||||||
|
if ($jobdir === null && $job !== '' && mvlog_legacy_job_is_valid($job)) {
|
||||||
|
$candidate = $MVLOG_ROOT . '/in-dir/' . basename($job);
|
||||||
|
if (is_dir($candidate)) $jobdir = $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($jobdir === null || !is_dir($jobdir)) {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['status'=>'error','message'=>'job not found']);
|
echo json_encode(['status'=>'error','message'=>'job not found']);
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* send_push.php
|
* send_push.php
|
||||||
* Helper function to send "Show enabled" web-push notifications from the webserver.
|
* Helper function to send "Show enabled" web-push notifications from the webserver.
|
||||||
*
|
*
|
||||||
* Function: try_send_show_notification(string $job, string $jobdir, string $mvlog_root = null, string $logfile = null): bool
|
* Function: try_send_show_notification(string $articleRefOrJob, string $jobdir, string $mvlog_root = null, string $logfile = null): bool
|
||||||
*
|
*
|
||||||
* Returns true on successful send and cache update, false otherwise.
|
* Returns true on successful send and cache update, false otherwise.
|
||||||
*/
|
*/
|
||||||
|
|
@ -73,7 +73,20 @@ function mvlog_nice_title(string $s): string
|
||||||
return trim(ucwords(str_replace(['_', '-'], ' ', $s)));
|
return trim(ucwords(str_replace(['_', '-'], ' ', $s)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile = null)
|
function mvlog_article_id_is_valid(string $id): bool
|
||||||
|
{
|
||||||
|
return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mvlog_read_article_id(string $jobdir): string
|
||||||
|
{
|
||||||
|
$path = rtrim($jobdir, '/') . '/.mvlog-id';
|
||||||
|
if (!is_file($path)) return '';
|
||||||
|
$value = strtolower(trim((string)@file_get_contents($path)));
|
||||||
|
return mvlog_article_id_is_valid($value) ? $value : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function try_send_show_notification($articleRefOrJob, $jobdir, $mvlog_root = null, $logfile = null)
|
||||||
{
|
{
|
||||||
if ($mvlog_root === null) $mvlog_root = dirname(__DIR__);
|
if ($mvlog_root === null) $mvlog_root = dirname(__DIR__);
|
||||||
if ($logfile === null) $logfile = '/var/log/mvlog_notify.log';
|
if ($logfile === null) $logfile = '/var/log/mvlog_notify.log';
|
||||||
|
|
@ -81,6 +94,11 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
$subs_file = $mvlog_root . '/cache/push_subscriptions.json';
|
$subs_file = $mvlog_root . '/cache/push_subscriptions.json';
|
||||||
$cache_file = $mvlog_root . '/cache/push_notifications.json';
|
$cache_file = $mvlog_root . '/cache/push_notifications.json';
|
||||||
|
|
||||||
|
$job = basename((string)$jobdir);
|
||||||
|
$articleRef = strtolower(trim((string)$articleRefOrJob));
|
||||||
|
$articleId = mvlog_article_id_is_valid($articleRef) ? $articleRef : mvlog_read_article_id((string)$jobdir);
|
||||||
|
$identity = $articleId !== '' ? $articleId : $job;
|
||||||
|
|
||||||
// Don't notify for hidden jobs
|
// Don't notify for hidden jobs
|
||||||
if (is_file($jobdir . '/.mvlog-hidden')) {
|
if (is_file($jobdir . '/.mvlog-hidden')) {
|
||||||
error_log("[mvlog] job $job hidden, skipping push\n", 3, $logfile);
|
error_log("[mvlog] job $job hidden, skipping push\n", 3, $logfile);
|
||||||
|
|
@ -89,17 +107,17 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
|
|
||||||
// Gather subscriptions
|
// Gather subscriptions
|
||||||
if (!is_file($subs_file)) {
|
if (!is_file($subs_file)) {
|
||||||
error_log("[mvlog] subscriptions file missing, skipping push for $job\n", 3, $logfile);
|
error_log("[mvlog] subscriptions file missing, skipping push for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$subs_raw = @file_get_contents($subs_file);
|
$subs_raw = @file_get_contents($subs_file);
|
||||||
if (!is_string($subs_raw) || trim($subs_raw) === '') {
|
if (!is_string($subs_raw) || trim($subs_raw) === '') {
|
||||||
error_log("[mvlog] subscriptions empty, skipping push for $job\n", 3, $logfile);
|
error_log("[mvlog] subscriptions empty, skipping push for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$subs = json_decode($subs_raw, true);
|
$subs = json_decode($subs_raw, true);
|
||||||
if (!is_array($subs) || count($subs) === 0) {
|
if (!is_array($subs) || count($subs) === 0) {
|
||||||
error_log("[mvlog] subscriptions invalid/empty, skipping push for $job\n", 3, $logfile);
|
error_log("[mvlog] subscriptions invalid/empty, skipping push for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,21 +127,22 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
$cache = json_decode((string)@file_get_contents($cache_file), true) ?: [];
|
$cache = json_decode((string)@file_get_contents($cache_file), true) ?: [];
|
||||||
}
|
}
|
||||||
$shown = isset($cache['shown']) && is_array($cache['shown']) ? $cache['shown'] : [];
|
$shown = isset($cache['shown']) && is_array($cache['shown']) ? $cache['shown'] : [];
|
||||||
if (isset($shown[$job])) {
|
// Transition support: honor both new (ID) and legacy (job name) keys.
|
||||||
error_log("[mvlog] push already sent for $job, skipping\n", 3, $logfile);
|
if (isset($shown[$identity]) || ($identity !== $job && isset($shown[$job]))) {
|
||||||
|
error_log("[mvlog] push already sent for $identity (job=$job), skipping\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve push config consistently (same source model as push_config.php)
|
// Resolve push config consistently (same source model as push_config.php)
|
||||||
$cfg = mvlog_load_push_config($mvlog_root);
|
$cfg = mvlog_load_push_config($mvlog_root);
|
||||||
if (!$cfg) {
|
if (!$cfg) {
|
||||||
error_log("[mvlog] push config not found/invalid, skipping push for $job\n", 3, $logfile);
|
error_log("[mvlog] push config not found/invalid, skipping push for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tmp_push_config = mvlog_write_temp_push_json($cfg);
|
$tmp_push_config = mvlog_write_temp_push_json($cfg);
|
||||||
if (!$tmp_push_config) {
|
if (!$tmp_push_config) {
|
||||||
error_log("[mvlog] failed to create temp push config for $job\n", 3, $logfile);
|
error_log("[mvlog] failed to create temp push config for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,7 +154,7 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
|
|
||||||
$state_file = $jobdir . '/.movmaker-state.json';
|
$state_file = $jobdir . '/.movmaker-state.json';
|
||||||
if (is_file($state_file)) {
|
if (is_file($state_file)) {
|
||||||
$state = json_decode(@file_get_contents($state_file), true) ?: [];
|
$state = json_decode((string)@file_get_contents($state_file), true) ?: [];
|
||||||
if (!empty($state['output']) && is_string($state['output'])) {
|
if (!empty($state['output']) && is_string($state['output'])) {
|
||||||
$out = pathinfo($state['output'], PATHINFO_FILENAME);
|
$out = pathinfo($state['output'], PATHINFO_FILENAME);
|
||||||
if ($out) {
|
if ($out) {
|
||||||
|
|
@ -158,7 +177,7 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
'title' => $articleTitle,
|
'title' => $articleTitle,
|
||||||
'body' => 'New MVLog article is now visible',
|
'body' => 'New MVLog article is now visible',
|
||||||
'url' => $filterUrl,
|
'url' => $filterUrl,
|
||||||
'tag' => "mvlog-show-{$job}",
|
'tag' => "mvlog-show-{$identity}",
|
||||||
], JSON_UNESCAPED_UNICODE);
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
|
||||||
// Call node send_push.js with subscriptions on stdin
|
// Call node send_push.js with subscriptions on stdin
|
||||||
|
|
@ -176,7 +195,7 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
$proc = proc_open($cmd, $descriptorspec, $pipes);
|
$proc = proc_open($cmd, $descriptorspec, $pipes);
|
||||||
if (!is_resource($proc)) {
|
if (!is_resource($proc)) {
|
||||||
@unlink($tmp_push_config);
|
@unlink($tmp_push_config);
|
||||||
error_log("[mvlog] failed to start node send_push process for $job\n", 3, $logfile);
|
error_log("[mvlog] failed to start node send_push process for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,29 +210,33 @@ function try_send_show_notification($job, $jobdir, $mvlog_root = null, $logfile
|
||||||
@unlink($tmp_push_config);
|
@unlink($tmp_push_config);
|
||||||
|
|
||||||
if ($rc !== 0) {
|
if ($rc !== 0) {
|
||||||
error_log("[mvlog] send_push failed for $job rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile);
|
error_log("[mvlog] send_push failed for $identity rc=$rc stdout=" . trim((string)$stdout) . " stderr=" . trim((string)$stderr) . "\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// On success, record in cache['shown']
|
// On success, record in cache['shown']
|
||||||
if (!is_array($cache)) $cache = [];
|
if (!is_array($cache)) $cache = [];
|
||||||
if (!isset($cache['shown']) || !is_array($cache['shown'])) $cache['shown'] = [];
|
if (!isset($cache['shown']) || !is_array($cache['shown'])) $cache['shown'] = [];
|
||||||
$cache['shown'][$job] = ['sent_at' => date(DATE_ATOM)];
|
$cache['shown'][$identity] = [
|
||||||
|
'sent_at' => date(DATE_ATOM),
|
||||||
|
'job' => $job,
|
||||||
|
'article_id' => $articleId,
|
||||||
|
];
|
||||||
|
|
||||||
// Atomic write: write to tmp then rename
|
// Atomic write: write to tmp then rename
|
||||||
$tmp = $cache_file . '.tmp.' . bin2hex(random_bytes(6));
|
$tmp = $cache_file . '.tmp.' . bin2hex(random_bytes(6));
|
||||||
$json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
$json = json_encode($cache, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
|
||||||
if (@file_put_contents($tmp, $json, LOCK_EX) === false) {
|
if (@file_put_contents($tmp, $json, LOCK_EX) === false) {
|
||||||
error_log("[mvlog] failed to write cache temp file for $job\n", 3, $logfile);
|
error_log("[mvlog] failed to write cache temp file for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@chmod($tmp, 0664);
|
@chmod($tmp, 0664);
|
||||||
if (!@rename($tmp, $cache_file)) {
|
if (!@rename($tmp, $cache_file)) {
|
||||||
@unlink($tmp);
|
@unlink($tmp);
|
||||||
error_log("[mvlog] failed to atomically write cache file for $job\n", 3, $logfile);
|
error_log("[mvlog] failed to atomically write cache file for $identity\n", 3, $logfile);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
error_log("[mvlog] show-notification sent: $job (cfg=" . ($cfg['_source'] ?? 'unknown') . ")\n", 3, $logfile);
|
error_log("[mvlog] show-notification sent: identity=$identity job=$job (cfg=" . ($cfg['_source'] ?? 'unknown') . ")\n", 3, $logfile);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
66
new.php
66
new.php
|
|
@ -84,7 +84,7 @@ function load_articles_index($base, $dirs = null){
|
||||||
foreach ($dirs as $dir) {
|
foreach ($dirs as $dir) {
|
||||||
$name = basename((string)$dir);
|
$name = basename((string)$dir);
|
||||||
$id = read_article_id($dir);
|
$id = read_article_id($dir);
|
||||||
if (($id === '' || !empty($byId[$id])) && !is_dir($dir . '/.movmaker-lock')) {
|
if ($id === '' || !empty($byId[$id])) {
|
||||||
$id = ensure_article_id($dir, $byId);
|
$id = ensure_article_id($dir, $byId);
|
||||||
}
|
}
|
||||||
if ($id !== '' && empty($byId[$id])) $byId[$id] = $name;
|
if ($id !== '' && empty($byId[$id])) $byId[$id] = $name;
|
||||||
|
|
@ -561,13 +561,14 @@ function active_worker_jobs($config){
|
||||||
foreach (input_dirs($config['uploads_dir']) as $dir) {
|
foreach (input_dirs($config['uploads_dir']) as $dir) {
|
||||||
$name = basename($dir);
|
$name = basename($dir);
|
||||||
$state = input_dir_state($dir);
|
$state = input_dir_state($dir);
|
||||||
|
$articleId = (string)($state['article_id'] ?? '') ?: ensure_article_id($dir);
|
||||||
$status = (string)($state['status'] ?? '');
|
$status = (string)($state['status'] ?? '');
|
||||||
$locked = is_dir($dir . '/.movmaker-lock');
|
$locked = is_dir($dir . '/.movmaker-lock');
|
||||||
if ($locked || $status === 'processing') {
|
if ($locked || $status === 'processing') {
|
||||||
$started = '';
|
$started = '';
|
||||||
if (is_file($dir . '/.movmaker-lock/started_at')) $started = trim((string)file_get_contents($dir . '/.movmaker-lock/started_at'));
|
if (is_file($dir . '/.movmaker-lock/started_at')) $started = trim((string)file_get_contents($dir . '/.movmaker-lock/started_at'));
|
||||||
if ($started === '' && !empty($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
|
if ($started === '' && !empty($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
|
||||||
$jobs[] = ['name'=>$name,'status'=>$status ?: ($locked ? 'processing' : 'unknown'),'started_at'=>$started];
|
$jobs[] = ['name'=>$name,'id'=>$articleId,'status'=>$status ?: ($locked ? 'processing' : 'unknown'),'started_at'=>$started];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $jobs;
|
return $jobs;
|
||||||
|
|
@ -763,7 +764,7 @@ try {
|
||||||
if ($visibleNow && $canShow) {
|
if ($visibleNow && $canShow) {
|
||||||
if (is_file(__DIR__ . '/lib/send_push.php')) {
|
if (is_file(__DIR__ . '/lib/send_push.php')) {
|
||||||
include_once __DIR__ . '/lib/send_push.php';
|
include_once __DIR__ . '/lib/send_push.php';
|
||||||
try_send_show_notification(basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log');
|
try_send_show_notification($articleId ?: basename($dir), $dir, __DIR__, '/var/log/mvlog_notify.log');
|
||||||
} else {
|
} else {
|
||||||
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "\n", 3, '/var/log/mvlog_notify.log');
|
error_log("[mvlog] send_push helper missing, cannot send notification for " . basename($dir) . "\n", 3, '/var/log/mvlog_notify.log');
|
||||||
}
|
}
|
||||||
|
|
@ -942,7 +943,7 @@ $runningJobs = active_worker_jobs($config);
|
||||||
<?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
<?php if($err): ?><div class="err"><?=h($err)?></div><?php endif; ?>
|
||||||
|
|
||||||
<?php if($tab==='videos'): ?><section><h2>Videos without input dir</h2><?php if(!$orphanVideos): ?><p>No orphan videos.</p><?php endif; ?><div class="admin-list"><?php foreach($orphanVideosPage as $v): $vn=basename($v); ?><form method="post" class="admin-item" onsubmit="return confirm('Delete this generated video?') js-ajax-form"><div><strong><?=h($vn)?></strong><br><span><?=h(round(filesize($v)/1048576,1))?> MB</span></div><input type="hidden" name="action" value="delete_video"><input type="hidden" name="video" value="<?=h($vn)?>"><button type="submit">Delete video</button></form><?php endforeach; ?></div><?=page_links('videos',$videoPage,$videoPages)?></section><?php endif; ?>
|
<?php if($tab==='videos'): ?><section><h2>Videos without input dir</h2><?php if(!$orphanVideos): ?><p>No orphan videos.</p><?php endif; ?><div class="admin-list"><?php foreach($orphanVideosPage as $v): $vn=basename($v); ?><form method="post" class="admin-item" onsubmit="return confirm('Delete this generated video?') js-ajax-form"><div><strong><?=h($vn)?></strong><br><span><?=h(round(filesize($v)/1048576,1))?> MB</span></div><input type="hidden" name="action" value="delete_video"><input type="hidden" name="video" value="<?=h($vn)?>"><button type="submit">Delete video</button></form><?php endforeach; ?></div><?=page_links('videos',$videoPage,$videoPages)?></section><?php endif; ?>
|
||||||
<?php if($tab==='edit' && !$editDir): ?><section><form method="get" class="admin-search-form"><input type="hidden" name="tab" value="edit"><div class="admin-search-wrap"><input name="q" value="<?=h($rawKeyword)?>" placeholder="Search title/description or use date=... / location=..." aria-label="Search input dirs"><a class="admin-search-clear <?=$searchEmpty ? 'is-empty' : ''?>" href="new.php?tab=edit" aria-label="Clear filter" title="Clear filter">×</a></div><div class="admin-search-filters"><label class="admin-filter-item" title="Preview"><input type="checkbox" name="f_preview" value="1" <?=$filterPreview?'checked':''?>><span>P</span></label><label class="admin-filter-item" title="Render"><input type="checkbox" name="f_render" value="1" <?=$filterRender?'checked':''?>><span>R</span></label><label class="admin-filter-item" title="Show"><input type="checkbox" name="f_shown" value="1" <?=$filterShown?'checked':''?>><span>S</span></label></div><button type="submit" class="admin-search-submit" aria-label="Filter"><span class="icon" aria-hidden="true">🔍</span><span class="sr-only">Filter</span></button></form><?php if(!$dirs): ?><p>No input directories yet.</p><?php endif; ?><div class="admin-list video-list"><?php foreach($dirsPage as $d): $runStatus=input_dir_status($d); $running=input_dir_running($d); $enabled=input_dir_enabled($d); $preview=input_dir_preview($d); $state=input_dir_state($d); $fullOutput=basename((string)($state['output'] ?? '')); $previewOutput=basename((string)($state['preview_output'] ?? '')); $hasFullVideo=$fullOutput !== '' && is_file($config['videos_dir'].'/'.$fullOutput); $hasPreviewVideo=$previewOutput !== '' && is_file($config['videos_dir'].'/'.$previewOutput); $displayOutput=($preview && $hasPreviewVideo) ? $previewOutput : ($hasFullVideo ? $fullOutput : ($hasPreviewVideo ? $previewOutput : '')); $hasVideo=$displayOutput !== ''; $displayVideoPath=$hasVideo ? $config['videos_dir'].'/'.$displayOutput : ''; $previewVideo=$hasVideo && $displayOutput === $previewOutput; $canShow=$hasFullVideo && !$preview; $visible=$canShow && input_dir_visible($d); $info=$dirInfo[basename($d)] ?? input_dir_info($d); $data=read_data($d); $meta=cached_video_metadata($displayOutput, $data); $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?><article class="video-row admin-video-row<?= $visible ? ' shown' : '' ?>" data-job="<?=h(basename($d))?>"><div><?php if($hasVideo): ?><div class="video-wrapper"><video controls preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"></video><?php $map_n = ($displayOutput !== '' ? pathinfo($displayOutput, PATHINFO_FILENAME) : basename($d)) . '.png'; if(is_file($config['videos_dir'].'/'.$map_n)): ?><a class="map-image-link" href="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>"><img class="map-image" src="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>" alt="Map"></a><?php endif; ?></div><?php else: ?><div class="video-placeholder"><img src="assets/img/moto_travel.png" alt=""><span>No video yet</span></div><?php endif; ?></div><div class="video-info"><div class="admin-row-top"><div class="admin-switches">
|
<?php if($tab==='edit' && !$editDir): ?><section><form method="get" class="admin-search-form"><input type="hidden" name="tab" value="edit"><div class="admin-search-wrap"><input name="q" value="<?=h($rawKeyword)?>" placeholder="Search title/description or use date=... / location=..." aria-label="Search input dirs"><a class="admin-search-clear <?=$searchEmpty ? 'is-empty' : ''?>" href="new.php?tab=edit" aria-label="Clear filter" title="Clear filter">×</a></div><div class="admin-search-filters"><label class="admin-filter-item" title="Preview"><input type="checkbox" name="f_preview" value="1" <?=$filterPreview?'checked':''?>><span>P</span></label><label class="admin-filter-item" title="Render"><input type="checkbox" name="f_render" value="1" <?=$filterRender?'checked':''?>><span>R</span></label><label class="admin-filter-item" title="Show"><input type="checkbox" name="f_shown" value="1" <?=$filterShown?'checked':''?>><span>S</span></label></div><button type="submit" class="admin-search-submit" aria-label="Filter"><span class="icon" aria-hidden="true">🔍</span><span class="sr-only">Filter</span></button></form><?php if(!$dirs): ?><p>No input directories yet.</p><?php endif; ?><div class="admin-list video-list"><?php foreach($dirsPage as $d): $runStatus=input_dir_status($d); $running=input_dir_running($d); $enabled=input_dir_enabled($d); $preview=input_dir_preview($d); $state=input_dir_state($d); $fullOutput=basename((string)($state['output'] ?? '')); $previewOutput=basename((string)($state['preview_output'] ?? '')); $hasFullVideo=$fullOutput !== '' && is_file($config['videos_dir'].'/'.$fullOutput); $hasPreviewVideo=$previewOutput !== '' && is_file($config['videos_dir'].'/'.$previewOutput); $displayOutput=($preview && $hasPreviewVideo) ? $previewOutput : ($hasFullVideo ? $fullOutput : ($hasPreviewVideo ? $previewOutput : '')); $hasVideo=$displayOutput !== ''; $displayVideoPath=$hasVideo ? $config['videos_dir'].'/'.$displayOutput : ''; $previewVideo=$hasVideo && $displayOutput === $previewOutput; $canShow=$hasFullVideo && !$preview; $visible=$canShow && input_dir_visible($d); $info=$dirInfo[basename($d)] ?? input_dir_info($d); $data=read_data($d); $meta=cached_video_metadata($displayOutput, $data); $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?><article class="video-row admin-video-row<?= $visible ? ' shown' : '' ?>" data-job="<?=h(basename($d))?>" data-id="<?=h(read_article_id($d))?>"><div><?php if($hasVideo): ?><div class="video-wrapper"><video controls preload="metadata" src="<?=h($config['public_videos'].'/'.rawurlencode($displayOutput))?>"></video><?php $map_n = ($displayOutput !== '' ? pathinfo($displayOutput, PATHINFO_FILENAME) : basename($d)) . '.png'; if(is_file($config['videos_dir'].'/'.$map_n)): ?><a class="map-image-link" href="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>"><img class="map-image" src="<?=h($config['public_videos'].'/'.rawurlencode($map_n))?>" alt="Map"></a><?php endif; ?></div><?php else: ?><div class="video-placeholder"><img src="assets/img/moto_travel.png" alt=""><span>No video yet</span></div><?php endif; ?></div><div class="video-info"><div class="admin-row-top"><div class="admin-switches">
|
||||||
<form method="post" class="inline-form switch-form" data-switch="preview">
|
<form method="post" class="inline-form switch-form" data-switch="preview">
|
||||||
<input type="hidden" name="action" value="set_preview">
|
<input type="hidden" name="action" value="set_preview">
|
||||||
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
<input type="hidden" name="dir" value="<?=h(basename($d))?>">
|
||||||
|
|
@ -1319,8 +1320,10 @@ async function handleSwitchToggle(form, checkbox){
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySwitchPayload(payload){
|
function applySwitchPayload(payload){
|
||||||
if(!payload || !payload.dir) return;
|
if(!payload) return;
|
||||||
const row = document.querySelector('.admin-video-row[data-job="'+payload.dir+'"]');
|
let row = null;
|
||||||
|
if(payload.id) row = document.querySelector('.admin-video-row[data-id="'+payload.id+'"]');
|
||||||
|
if(!row && payload.dir) row = document.querySelector('.admin-video-row[data-job="'+payload.dir+'"]');
|
||||||
if(!row) return;
|
if(!row) return;
|
||||||
function setSwitch(type, value){
|
function setSwitch(type, value){
|
||||||
const target = row.querySelector('form[data-switch="'+type+'"] input[type="checkbox"]');
|
const target = row.querySelector('form[data-switch="'+type+'"] input[type="checkbox"]');
|
||||||
|
|
@ -1366,12 +1369,13 @@ function applySwitchPayload(payload){
|
||||||
if (m > 0) return m + 'm ' + s + 's';
|
if (m > 0) return m + 'm ' + s + 's';
|
||||||
return s + 's';
|
return s + 's';
|
||||||
}
|
}
|
||||||
function syncEditButtons(runningJobs){
|
function syncEditButtons(runningIds, runningJobs){
|
||||||
document.querySelectorAll('.admin-video-row[data-job]').forEach(function(row){
|
document.querySelectorAll('.admin-video-row[data-job]').forEach(function(row){
|
||||||
const job=row.dataset.job || '';
|
const job=row.dataset.job || '';
|
||||||
|
const articleId=row.dataset.id || '';
|
||||||
const edit=row.querySelector('[data-edit-action="1"]');
|
const edit=row.querySelector('[data-edit-action="1"]');
|
||||||
if(!edit) return;
|
if(!edit) return;
|
||||||
const running=runningJobs.has(job);
|
const running=(articleId && runningIds.has(articleId)) || runningJobs.has(job);
|
||||||
if(edit.tagName === 'A'){
|
if(edit.tagName === 'A'){
|
||||||
if(running){
|
if(running){
|
||||||
if(!edit.dataset.editHref) edit.dataset.editHref = edit.getAttribute('href') || '';
|
if(!edit.dataset.editHref) edit.dataset.editHref = edit.getAttribute('href') || '';
|
||||||
|
|
@ -1397,9 +1401,10 @@ function applySwitchPayload(payload){
|
||||||
const r=await fetch('job_status.php',{cache:'no-store'});
|
const r=await fetch('job_status.php',{cache:'no-store'});
|
||||||
if(!r.ok) throw new Error('status failed');
|
if(!r.ok) throw new Error('status failed');
|
||||||
const data=await r.json();
|
const data=await r.json();
|
||||||
const jobs=(data.jobs||[]).filter(function(job){ return job && job.name; });
|
const jobs=(data.jobs||[]).filter(function(job){ return job && (job.name || job.id); });
|
||||||
const runningJobs=new Set(jobs.map(function(job){ return String(job.name); }));
|
const runningIds=new Set(jobs.map(function(job){ return String(job.id || ''); }).filter(function(v){ return v !== ''; }));
|
||||||
syncEditButtons(runningJobs);
|
const runningJobs=new Set(jobs.map(function(job){ return String(job.name || ''); }).filter(function(v){ return v !== ''; }));
|
||||||
|
syncEditButtons(runningIds, runningJobs);
|
||||||
if(!jobs.length){
|
if(!jobs.length){
|
||||||
box.hidden=true;
|
box.hidden=true;
|
||||||
box.innerHTML='';
|
box.innerHTML='';
|
||||||
|
|
@ -1418,7 +1423,6 @@ function applySwitchPayload(payload){
|
||||||
<footer>Input dirs are saved under <code>in-dir/</code>.</footer>
|
<footer>Input dirs are saved under <code>in-dir/</code>.</footer>
|
||||||
<footer class="site-footer admin-footer"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div id="job-status" class="job-status"<?= empty($runningJobs) ? ' hidden' : '' ?>><?php foreach($runningJobs as $job): ?><span class="job-dot"></span><span><?=h(str_replace('_', ' ', $job['name']))?></span><?php if(!empty($job['started_at'])): ?><span class="job-age">(<?=h(format_job_age($job['started_at']))?>)</span><?php endif; ?><?php endforeach; ?></div><div class="admin-right"><a class="logout-link" href="logout.php">Log off</a></div></footer>
|
<footer class="site-footer admin-footer"><div class="admin-left"><nav class="tabs"><a class="<?= $tab==='new'?'active':'' ?>" href="new.php?tab=new">New</a><a class="<?= $tab==='edit'?'active':'' ?>" href="new.php?tab=edit">Edit</a><a class="<?= $tab==='videos'?'active':'' ?>" href="new.php?tab=videos">Videos</a></nav></div><div id="job-status" class="job-status"<?= empty($runningJobs) ? ' hidden' : '' ?>><?php foreach($runningJobs as $job): ?><span class="job-dot"></span><span><?=h(str_replace('_', ' ', $job['name']))?></span><?php if(!empty($job['started_at'])): ?><span class="job-age">(<?=h(format_job_age($job['started_at']))?>)</span><?php endif; ?><?php endforeach; ?></div><div class="admin-right"><a class="logout-link" href="logout.php">Log off</a></div></footer>
|
||||||
<script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script>
|
<script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script>
|
||||||
<script>(function(){const box=document.getElementById('job-status');if(!box)return;function fmt(job){const name=(job.name||'job').replace(/_/g,' ');const started=job.started_at ? new Date(job.started_at) : null;const ageText=started && !Number.isNaN(started.getTime()) ? ' <span class="job-age">(' + formatAge(started) + ')</span>' : '';return '<span class="job-dot"></span><span>'+name+'</span>'+ageText;}function formatAge(started){const seconds=Math.max(0,Math.floor((Date.now()-started.getTime())/1000));const h=Math.floor(seconds/3600);const m=Math.floor((seconds%3600)/60);const s=seconds%60;if(h>0) return h+'h '+m+'m';if(m>0) return m+'m '+s+'s';return s+'s';}function syncEditButtons(runningJobs){document.querySelectorAll('.admin-video-row[data-job]').forEach(function(row){const job=row.dataset.job || '';const edit=row.querySelector('[data-edit-action="1"]');if(!edit) return;const running=runningJobs.has(job);if(edit.tagName==='A'){if(running){if(!edit.dataset.editHref) edit.dataset.editHref = edit.getAttribute('href') || '';edit.removeAttribute('href');edit.classList.add('disabled');edit.setAttribute('aria-disabled','true');edit.title='Rendering now';}else{const href=edit.dataset.editHref || ('?edit=' + encodeURIComponent(job));edit.setAttribute('href', href);edit.classList.remove('disabled');edit.removeAttribute('aria-disabled');edit.title='Edit';}}else{edit.classList.toggle('disabled', running);edit.title=running ? 'Rendering now' : '';}});}async function updateJobs(){try{const r=await fetch('job_status.php',{cache:'no-store'});if(!r.ok) throw new Error('status failed');const data=await r.json();const jobs=(data.jobs||[]).filter(function(job){ return job && job.name; });const runningJobs=new Set(jobs.map(function(job){ return String(job.name); }));syncEditButtons(runningJobs);if(!jobs.length){box.hidden=true;box.innerHTML='';return;}box.hidden=false;box.innerHTML=jobs.map(fmt).join('');}catch(e){if(box.innerHTML.trim()!=='') box.hidden=false;}}updateJobs();setInterval(updateJobs,30000);})();</script>
|
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
function attachDescribeButtons(){
|
function attachDescribeButtons(){
|
||||||
|
|
@ -1426,12 +1430,14 @@ function applySwitchPayload(payload){
|
||||||
if (row.querySelector('.describe-button')) return;
|
if (row.querySelector('.describe-button')) return;
|
||||||
var actions = row.querySelector('.admin-actions');
|
var actions = row.querySelector('.admin-actions');
|
||||||
if (!actions) return;
|
if (!actions) return;
|
||||||
var job = row.dataset.job;
|
var job = row.dataset.job || '';
|
||||||
if (!job) return;
|
var articleId = row.dataset.id || '';
|
||||||
|
if (!job && !articleId) return;
|
||||||
var describe = document.createElement('a');
|
var describe = document.createElement('a');
|
||||||
describe.className = 'button describe-button';
|
describe.className = 'button describe-button';
|
||||||
describe.href = '#';
|
describe.href = '#';
|
||||||
describe.dataset.job = job;
|
describe.dataset.job = job;
|
||||||
|
describe.dataset.id = articleId;
|
||||||
describe.textContent = 'Describe';
|
describe.textContent = 'Describe';
|
||||||
var first = actions.querySelector('.button');
|
var first = actions.querySelector('.button');
|
||||||
if (first) actions.insertBefore(describe, first);
|
if (first) actions.insertBefore(describe, first);
|
||||||
|
|
@ -1439,7 +1445,7 @@ function applySwitchPayload(payload){
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function showModal(job, description, rawJson){
|
function showModal(job, articleId, description, rawJson){
|
||||||
var existing = document.getElementById('mvlog-gemini-modal');
|
var existing = document.getElementById('mvlog-gemini-modal');
|
||||||
if (existing) existing.remove();
|
if (existing) existing.remove();
|
||||||
var overlay = document.createElement('div');
|
var overlay = document.createElement('div');
|
||||||
|
|
@ -1473,8 +1479,11 @@ function applySwitchPayload(payload){
|
||||||
useBtn.addEventListener('click', function(){
|
useBtn.addEventListener('click', function(){
|
||||||
var editForm = document.getElementById('edit-form');
|
var editForm = document.getElementById('edit-form');
|
||||||
if (editForm){
|
if (editForm){
|
||||||
|
var idInput = editForm.querySelector('input[name="id"]');
|
||||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||||
if (dirInput && dirInput.value === job){
|
var sameById = !!(articleId && idInput && idInput.value === articleId);
|
||||||
|
var sameByDir = !!(job && dirInput && dirInput.value === job);
|
||||||
|
if (sameById || sameByDir){
|
||||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||||
if (textarea){
|
if (textarea){
|
||||||
textarea.value = description || '';
|
textarea.value = description || '';
|
||||||
|
|
@ -1484,8 +1493,12 @@ function applySwitchPayload(payload){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try { localStorage.setItem('mvlog_gemini_description_' + job, description || ''); } catch(e){}
|
var storageKey = 'mvlog_gemini_description_' + (articleId || job);
|
||||||
window.location.href = 'new.php?tab=edit&edit=' + encodeURIComponent(job);
|
try { localStorage.setItem(storageKey, description || ''); } catch(e){}
|
||||||
|
var href = 'new.php?tab=edit';
|
||||||
|
if (articleId) href += '&id=' + encodeURIComponent(articleId);
|
||||||
|
if (job) href += '&edit=' + encodeURIComponent(job);
|
||||||
|
window.location.href = href;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1493,8 +1506,9 @@ function applySwitchPayload(payload){
|
||||||
var el = (e.target && e.target.closest && e.target.closest('.describe-button')) || (e.target && e.target.classList && e.target.classList.contains && e.target.classList.contains('describe-button') ? e.target : null);
|
var el = (e.target && e.target.closest && e.target.closest('.describe-button')) || (e.target && e.target.classList && e.target.classList.contains && e.target.classList.contains('describe-button') ? e.target : null);
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var job = el.dataset.job;
|
var job = el.dataset.job || '';
|
||||||
if (!job) return;
|
var articleId = el.dataset.id || '';
|
||||||
|
if (!job && !articleId) return;
|
||||||
el.classList.add('disabled');
|
el.classList.add('disabled');
|
||||||
var oldText = el.textContent;
|
var oldText = el.textContent;
|
||||||
el.textContent = 'Generating...';
|
el.textContent = 'Generating...';
|
||||||
|
|
@ -1502,12 +1516,12 @@ function applySwitchPayload(payload){
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
headers: {'Content-Type':'application/x-www-form-urlencoded'},
|
||||||
body: 'job=' + encodeURIComponent(job) + '&max_frames=16'
|
body: 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16'
|
||||||
}).then(function(r){ return r.json().catch(()=>({})); })
|
}).then(function(r){ return r.json().catch(()=>({})); })
|
||||||
.then(function(data){
|
.then(function(data){
|
||||||
if (data && data.status === 'ok'){
|
if (data && data.status === 'ok'){
|
||||||
var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || '';
|
var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || '';
|
||||||
showModal(job, desc, data.raw_json || null);
|
showModal(job, articleId, desc, data.raw_json || null);
|
||||||
} else {
|
} else {
|
||||||
var msg = (data && data.message) ? data.message : 'No response';
|
var msg = (data && data.message) ? data.message : 'No response';
|
||||||
if (data && data.log) {
|
if (data && data.log) {
|
||||||
|
|
@ -1539,10 +1553,12 @@ function applySwitchPayload(payload){
|
||||||
var editForm = document.getElementById('edit-form');
|
var editForm = document.getElementById('edit-form');
|
||||||
if (!editForm) return;
|
if (!editForm) return;
|
||||||
var dirInput = editForm.querySelector('input[name="dir"]');
|
var dirInput = editForm.querySelector('input[name="dir"]');
|
||||||
if (!dirInput) return;
|
var idInput = editForm.querySelector('input[name="id"]');
|
||||||
var job = dirInput.value;
|
if (!dirInput && !idInput) return;
|
||||||
|
var job = dirInput ? dirInput.value : '';
|
||||||
|
var articleId = idInput ? idInput.value : '';
|
||||||
try {
|
try {
|
||||||
var key = 'mvlog_gemini_description_' + job;
|
var key = 'mvlog_gemini_description_' + (articleId || job);
|
||||||
var desc = localStorage.getItem(key);
|
var desc = localStorage.getItem(key);
|
||||||
if (desc) {
|
if (desc) {
|
||||||
var textarea = editForm.querySelector('textarea[name="description"]');
|
var textarea = editForm.querySelector('textarea[name="description"]');
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue