v1.9.1: fix world map rendering and resolve eight small TODO items

World map:
- Fix zero-padded ISO numeric ids leaving 31 countries unrendered
  (Brazil, Australia, Belgium, Austria, Algeria and more)
- Fix Russia and Fiji smearing across the full map width at the antimeridian
  by unwrapping ring longitudes and drawing them at both edges
- Crop to 84N-60S, add evenodd fill rule, 174 countries rendered

Improvements:
- Logger::tail() reads backwards in chunks instead of loading the whole file
- External links get rel=noopener noreferrer in footer and Markdown content
- formatDisplayName() cleaned up and guarded against empty input
- Export statistics as CSV (Excel BOM) or JSON
- GeoIP database auto-updates when older than 35 days
- Editor shortcuts Ctrl/Cmd+S to save and Ctrl/Cmd+N for a new page
- Live search filter in the admin content browser
- Content versioning with timestamped .bak copies in content/-backups/

Also removes eight stale TODO entries that were already implemented
This commit is contained in:
2026-07-29 15:53:35 +02:00
parent 5357bc8915
commit 0fe5c75eae
12 changed files with 772 additions and 281 deletions
+58 -7
View File
@@ -123,20 +123,71 @@ class Logger {
/**
* Get last N lines from log file
*
*
* Reads the file backwards in chunks so large log files never have to be
* loaded into memory in their entirety.
*
* @param int $lines Number of lines to retrieve
* @return array Log lines
* @return array Log lines (including trailing newlines, oldest first)
*/
public static function tail($lines = 100) {
if (!self::$logFile || !file_exists(self::$logFile)) {
return [];
}
$file = @file(self::$logFile);
if ($file === false) {
$lines = max(1, (int)$lines);
$handle = @fopen(self::$logFile, 'rb');
if ($handle === false) {
return [];
}
return array_slice($file, -$lines);
if (fseek($handle, 0, SEEK_END) !== 0) {
fclose($handle);
return [];
}
$fileSize = ftell($handle);
if ($fileSize === false || $fileSize === 0) {
fclose($handle);
return [];
}
$chunkSize = 8192;
$position = $fileSize;
$buffer = '';
$newlineCount = 0;
// Read backwards until we have enough newlines or reach the start
while ($position > 0 && $newlineCount <= $lines) {
$readSize = (int)min($chunkSize, $position);
$position -= $readSize;
if (fseek($handle, $position, SEEK_SET) !== 0) {
break;
}
$chunk = fread($handle, $readSize);
if ($chunk === false) {
break;
}
$buffer = $chunk . $buffer;
$newlineCount = substr_count($buffer, "\n");
}
fclose($handle);
if ($buffer === '') {
return [];
}
// Split while keeping the newline characters, matching file() behaviour
$result = preg_split('/(?<=\n)/', $buffer, -1, PREG_SPLIT_NO_EMPTY);
if ($result === false) {
return [];
}
return array_slice($result, -$lines);
}
}