added terminal interface as default. old website as no-js fallback

This commit is contained in:
root
2026-01-21 09:58:53 -07:00
parent 81d9ddfd03
commit 6f709886ed
14 changed files with 1569 additions and 19 deletions

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
$rootDir = dirname(__DIR__, 2);
if (!is_dir($rootDir)) {
http_response_code(500);
echo json_encode(['error' => 'Invalid root directory'], JSON_UNESCAPED_SLASHES);
exit;
}
function json_out(array $data, int $status = 200): void
{
http_response_code($status);
try {
echo json_encode(
$data,
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
} catch (Throwable $e) {
http_response_code(500);
echo '{"error":"JSON encoding failed"}';
}
exit;
}

58
terminal/api/menu.php Normal file
View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
require __DIR__ . '/terminal/_bootstrap.php';
$file = $rootDir . '/includes/sidebar.md';
if (!is_file($file)) {
json_out(['sections' => []]);
}
$md = file_get_contents($file);
if ($md === false) {
json_out(['sections' => []]);
}
$lines = preg_split('/\r\n|\n|\r/', $md) ?: [];
$sections = [];
$current = null;
foreach ($lines as $line) {
$raw = rtrim($line);
// Section header: "- Title"
if (preg_match('/^\-\s{2,}(.+)$/', $raw, $m)) {
if ($current !== null) {
$sections[] = $current;
}
$current = [
'title' => trim($m[1]),
'items' => [],
];
continue;
}
// Menu item: " - [Text](Href)"
if ($current !== null && preg_match('/^\s+\-\s{2,}\[(.+?)\]\((.+?)\)\s*$/', $raw, $m)) {
$text = trim($m[1]);
$href = trim($m[2]);
$internal = str_starts_with($href, '/');
$slug = $internal ? ltrim(parse_url($href, PHP_URL_PATH) ?? '', '/') : '';
$current['items'][] = [
'text' => $text,
'href' => $href,
'internal' => $internal,
'slug' => $slug,
];
continue;
}
}
if ($current !== null) {
$sections[] = $current;
}
json_out(['sections' => $sections]);

53
terminal/api/page.php Normal file
View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
require __DIR__ . '/_bootstrap.php';
$p = $_GET['p'] ?? '';
if (!is_string($p)) {
json_out(['error' => 'Invalid page'], 400);
}
$slug = trim($p);
$slug = ltrim($slug, '/');
$slug = preg_replace('/\?.*$/', '', $slug) ?? $slug;
if ($slug === '' || !preg_match('/^[A-Za-z0-9][A-Za-z0-9_-]*$/', $slug)) {
json_out(['error' => 'Invalid page'], 400);
}
if (preg_match('/^success\d+$/', $slug) === 1) {
json_out(['error' => 'Not found'], 404);
}
$file = $rootDir . '/articles/' . $slug . '.md';
if (!is_file($file)) {
json_out(['error' => 'Not found'], 404);
}
$md = file_get_contents($file);
if ($md === false) {
json_out(['error' => 'Failed to read page'], 500);
}
$parsedownPath = $rootDir . '/parsedown-1.7.3/Parsedown.php';
$extraPath = $rootDir . '/parsedown-extra-0.7.1/ParsedownExtra.php';
if (is_file($parsedownPath)) {
require_once $parsedownPath;
}
if (is_file($extraPath)) {
require_once $extraPath;
}
$html = '';
if (class_exists('ParsedownExtra')) {
$pd = new ParsedownExtra();
$html = $pd->text($md);
}
json_out([
'slug' => $slug,
'markdown' => $md,
'html' => $html,
]);

51
terminal/api/pages.php Normal file
View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
require __DIR__ . '/_bootstrap.php';
$articlesDir = $rootDir . '/articles';
if (!is_dir($articlesDir)) {
json_out(['pages' => []]);
}
$pages = [];
$files = glob($articlesDir . '/*.md') ?: [];
sort($files);
foreach ($files as $file) {
$slug = basename($file, '.md');
if (preg_match('/^success\d+$/', $slug) === 1) {
continue;
}
$title = $slug;
$fh = fopen($file, 'rb');
if ($fh !== false) {
for ($i = 0; $i < 20; $i++) {
$line = fgets($fh);
if ($line === false) {
break;
}
$line = trim($line);
if ($line === '') {
continue;
}
if (preg_match('/^#\s+(.+)$/', $line, $m)) {
$title = trim($m[1]);
break;
}
$title = mb_substr($line, 0, 80);
break;
}
fclose($fh);
}
$pages[] = [
'slug' => $slug,
'title' => $title,
];
}
json_out(['pages' => $pages]);

49
terminal/api/server.php Normal file
View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
require __DIR__ . '/_bootstrap.php';
$candidates = [
$rootDir . '/report',
$rootDir . '/includes/report',
];
$report = null;
foreach ($candidates as $cand) {
if (is_file($cand)) {
$report = $cand;
break;
}
}
if ($report === null) {
json_out(['rows' => [], 'lastUpdated' => null]);
}
$rows = [];
$fh = fopen($report, 'rb');
if ($fh === false) {
json_out(['rows' => [], 'lastUpdated' => null]);
}
while (($line = fgets($fh)) !== false) {
$line = trim($line);
if ($line === '') {
continue;
}
$parts = str_getcsv($line);
if (count($parts) < 3) {
continue;
}
$rows[] = [
'host' => (string)$parts[0],
'check' => (string)$parts[1],
'status' => (string)$parts[2],
];
}
fclose($fh);
$mtime = @filemtime($report);
$last = $mtime ? gmdate('c', (int)$mtime) : null;
json_out(['rows' => $rows, 'lastUpdated' => $last]);

42
terminal/api/users.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
require __DIR__ . '/_bootstrap.php';
$siteRoot = '//' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
$skelIndex = '/etc/skel/public_html/index.html';
$skelIndexCksum = is_file($skelIndex) ? @md5_file($skelIndex) : null;
$users = [];
$homes = glob('/home/*', GLOB_ONLYDIR) ?: [];
foreach ($homes as $homeDir) {
$user = basename($homeDir);
if ($user === '' || $user === 'lost+found') {
continue;
}
$userIndex = $homeDir . '/public_html/index.html';
$userPub = $homeDir . '/public_html';
if (!is_dir($userPub)) {
continue;
}
$hasCustomIndex = false;
if (is_file($userIndex) && $skelIndexCksum !== null) {
$userCksum = @md5_file($userIndex);
$hasCustomIndex = ($userCksum !== false && $userCksum !== $skelIndexCksum);
} elseif (is_file($userIndex)) {
$hasCustomIndex = true;
}
$users[] = [
'username' => $user,
'url' => $siteRoot . '/~' . rawurlencode($user) . '/',
'hasContent' => $hasCustomIndex,
];
}
json_out(['users' => $users]);