Log and always regenerate MVLog maps on save

This commit is contained in:
hbrain 2026-06-02 10:58:21 +00:00
parent 37e43b6bca
commit 6ba4050d9a
2 changed files with 50 additions and 26 deletions

View file

@ -485,6 +485,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
<?php endforeach; ?>
</div>
<?php if ($articleId === ''): ?>
<div class="pages">
<?php for($i=1;$i<=$pages;$i++):
$linkParams = $baseParams;
@ -493,6 +494,7 @@ $siteHeaderTitle = $headerTitles[array_rand($headerTitles)];
<a class="<?=$i===$page?'active':''?>" href="<?=h(query_url($linkParams))?>"><?=$i?></a>
<?php endfor; ?>
</div>
<?php endif; ?>
</section>
</main>
<?php include __DIR__ . '/_footer.php'; ?>

74
new.php
View file

@ -75,14 +75,14 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
$text = $normalize($text);
return $text === '' ? '' : wordwrap($text, $width, "\n", false);
};
$clampWrappedText = static function ($text, int $width, int $maxLines) use ($normalize): string {
$clampWrappedText = static function ($text, int $width, int $maxLines, bool $cutLongWords = false) use ($normalize): string {
$text = $normalize($text);
if ($text === '') return '';
$words = preg_split('/\s+/u', $text, -1, PREG_SPLIT_NO_EMPTY) ?: [];
if (!$words) return '';
$candidate = implode(' ', $words);
while (true) {
$wrapped = wordwrap($candidate, $width, "\n", false);
$wrapped = wordwrap($candidate, $width, "\n", $cutLongWords);
$lineCount = substr_count($wrapped, "\n") + 1;
if ($lineCount <= $maxLines || count($words) <= 1) return $wrapped;
array_pop($words);
@ -94,8 +94,8 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
$bestSize = 95;
$bestWidth = 28;
$bestLines = 1;
for ($size = 94; $size >= 60; $size -= 4) {
$width = max(16, min(46, (int)round(24 + (94 - $size) / 2.7)));
for ($size = 84; $size >= 54; $size -= 4) {
$width = max(16, min(46, (int)round(23 + (84 - $size) / 2.7)));
$candidate = $wrapText($text, $width);
$lines = substr_count($candidate, "\n") + 1;
$bestText = $candidate;
@ -108,14 +108,14 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
};
[$titleText, $titleFontSize, $titleWrapWidth, $titleLines] = $fitTitleText($title);
$teaserText = $clampWrappedText($teaser, 38, 6);
$teaserText = $clampWrappedText($teaser, 34, 6, true);
$teaserLines = max(1, substr_count($teaserText, "\n") + 1);
// Move the whole block a bit right while keeping left-aligned text.
$left = max(18, (int)round($imgW * 0.055));
$titleY = max(24, (int)round($imgH * 0.06));
// Move the whole block a bit left and higher while keeping left-aligned text.
$left = max(14, (int)round($imgW * 0.046));
$titleY = max(20, (int)round($imgH * 0.062));
$titleLineStep = max(68, (int)round($imgH * 0.043));
$teaserY = max((int)round($imgH * 0.25), $titleY + ($titleLines * $titleLineStep) + max(56, (int)round($imgH * 0.03)));
$teaserY = max((int)round($imgH * 0.235), $titleY + ($titleLines * $titleLineStep) + max(54, (int)round($imgH * 0.03)));
$overlayBase = tempnam(sys_get_temp_dir(), 'thumb_overlay_');
$outputPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
@ -158,7 +158,7 @@ function apply_thumbnail_overlay($imagePath, $title, $teaser) {
foreach ($qualities as $quality) {
$attemptPath = dirname($imagePath) . '/' . basename($imagePath) . '.tmp.' . uniqid('', true) . '.jpg';
$cmd2 = sprintf(
'/usr/bin/convert -filter Lanczos -define filter:blur=0.85 %s -resize %dx%d^ -gravity center -extent %dx%d -modulate 62,76 %s -composite -strip -interlace Plane -sampling-factor 4:2:0 -quality %d %s 2>&1',
'/usr/bin/convert -filter Lanczos -define filter:blur=0.85 %s -resize %dx%d^ -gravity center -extent %dx%d -modulate 70,82 %s -composite -strip -interlace Plane -sampling-factor 4:2:0 -quality %d %s 2>&1',
escapeshellarg($imagePath),
$imgW,
$imgH,
@ -813,9 +813,13 @@ function input_dir_output($dir){
return basename((string)($state['output'] ?? ''));
}
function regenerate_input_dir_maps($dir, $config){
$logFile = '/var/log/mvlog_map.log';
$python = trim((string)shell_exec('command -v python3 2>/dev/null'));
$script = __DIR__ . '/bin/gpsmap.py';
if ($python === '' || !is_file($script)) return;
if ($python === '' || !is_file($script)) {
file_put_contents($logFile, '[mvlog] map regenerate skipped for ' . basename((string)$dir) . ' (python=' . ($python !== '' ? 'yes' : 'no') . ', script=' . (is_file($script) ? 'yes' : 'no') . ")\n", FILE_APPEND);
return;
}
$state = input_dir_state($dir);
$targets = [];
@ -831,14 +835,19 @@ function regenerate_input_dir_maps($dir, $config){
}
$targets = array_values(array_unique($targets));
file_put_contents($logFile, '[mvlog] map regenerate requested for ' . basename((string)$dir) . ' targets=' . implode(', ', array_map('basename', $targets)) . "\n", FILE_APPEND);
foreach ($targets as $outPng) {
file_put_contents($logFile, '[mvlog] map regenerate start for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . "\n", FILE_APPEND);
$cmd = escapeshellarg($python) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($dir) . ' ' . escapeshellarg($outPng) . ' 2>&1';
$output = (string)shell_exec($cmd);
if (is_file($outPng)) {
@chmod($outPng, 0664);
clearstatcache(true, $outPng);
$size = @filesize($outPng);
file_put_contents($logFile, '[mvlog] map regenerate done for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' bytes=' . ($size !== false ? $size : 'unknown') . "\n", FILE_APPEND);
} else {
error_log('[mvlog] map regenerate failed for '.basename((string)$dir).' target='.basename((string)$outPng).' output='.trim($output)."\n", 3, '/var/log/mvlog_map.log');
file_put_contents($logFile, '[mvlog] map regenerate failed for ' . basename((string)$dir) . ' target=' . basename((string)$outPng) . ' output=' . trim($output) . "\n", FILE_APPEND);
}
}
}
@ -1099,11 +1108,15 @@ try {
$dir = resolve_input_dir_from_request($config['uploads_dir'], $_POST['id'] ?? '', $_POST['dir'] ?? '', $articleIndex);
$articleId = ensure_state_article_id($dir);
if (input_dir_running($dir)) throw new RuntimeException('This input directory is being rendered. Editing is disabled until the job completes.');
write_data($dir, $_POST);
save_uploads('media', $dir, $allowedMedia);
save_uploads('audio', $dir, $allowedAudio);
// Always regenerate map image(s) on Save changes.
regenerate_input_dir_maps($dir, $config);
file_put_contents('/var/log/mvlog_map.log', '[mvlog] save changes pressed for ' . basename($dir) . ' id=' . $articleId . "\n", FILE_APPEND);
try {
write_data($dir, $_POST);
save_uploads('media', $dir, $allowedMedia);
save_uploads('audio', $dir, $allowedAudio);
} finally {
// Always regenerate map image(s) whenever Save changes is pressed.
regenerate_input_dir_maps($dir, $config);
}
if (!empty($_POST['ajax'])) ajax_json(['ok'=>true, 'message'=>'Updated input directory: in-dir/' . basename($dir), 'dir'=>basename($dir), 'id'=>$articleId]);
header('Location: new.php?tab=edit');
exit;
@ -1368,9 +1381,11 @@ function setThumbButtonsDisabled(disabled){
buttons[i].classList.toggle('is-busy-target', disabled);
}
}
function showThumbBusy(){
function showThumbBusy(label){
var overlay = ensureThumbBusyOverlay();
setThumbButtonsDisabled(true);
var text = overlay.querySelector('.label');
if(text && label) text.textContent = label;
overlay.classList.add('show');
}
function hideThumbBusy(){
@ -1380,7 +1395,7 @@ function hideThumbBusy(){
function setThumb(name){
var ctx = getEditDirContext();
if(!ctx.dir){ showToast('Internal error: missing input dir', false); return; }
showThumbBusy();
showThumbBusy('Creating thumbnail…');
var body = 'action=set_video_thumb&dir=' + encodeURIComponent(ctx.dir) + '&id=' + encodeURIComponent(ctx.id) + '&file=' + encodeURIComponent(name) + '&ajax=1';
fetch('new.php', {
method: 'POST',
@ -1617,7 +1632,7 @@ function handleAjaxFormSubmit(form, redirectTab) {
data.set('ajax', '1');
const action = String(data.get('action') || '');
const verb = action === 'update' ? 'Saving'
const verb = action === 'update' ? 'Saving changes…'
: action === 'delete_video' ? 'Deleting'
: action === 'delete_dir' ? 'Deleting'
: action === 'delete_file' ? 'Deleting'
@ -1626,18 +1641,22 @@ function handleAjaxFormSubmit(form, redirectTab) {
let timer = 0;
let success = false;
const useBusyOverlay = action === 'update';
if (submit) {
submit.dataset.label = submit.dataset.label || submit.textContent.trim() || 'Submit';
submit.disabled = true;
submit.style.opacity = '0.78';
submit.style.cursor = 'wait';
let dots = 0;
submit.textContent = verb;
timer = window.setInterval(function(){
dots = (dots + 1) % 4;
submit.textContent = verb + '.'.repeat(dots);
}, 320);
if (!useBusyOverlay) {
let dots = 0;
submit.textContent = verb;
timer = window.setInterval(function(){
dots = (dots + 1) % 4;
submit.textContent = verb + '.'.repeat(dots);
}, 320);
}
}
if (useBusyOverlay) showThumbBusy('Saving changes…');
try{
const res = await fetch('new.php', { method: 'POST', body: data, credentials: 'same-origin', headers: { 'Accept': 'application/json' }});
@ -1645,9 +1664,11 @@ function handleAjaxFormSubmit(form, redirectTab) {
if(!res.ok || !json.ok) throw new Error(json.error || 'Operation failed');
success = true;
showToast(json.message || 'Operation successful', true);
if (useBusyOverlay) setTimeout(hideThumbBusy, 120);
setTimeout(() => { window.location.href = 'new.php?tab=' + (json.tab || redirectTab); }, 450);
} catch(e) {
showToast(e.message || 'Operation failed', false);
if (useBusyOverlay) setTimeout(hideThumbBusy, 120);
} finally {
if (timer) window.clearInterval(timer);
if(submit && !success) {
@ -1656,6 +1677,7 @@ function handleAjaxFormSubmit(form, redirectTab) {
submit.style.cursor = '';
submit.textContent = submit.dataset.label || 'Submit';
}
if (useBusyOverlay && !success) hideThumbBusy();
}
});
}