Compare commits
6 Commits
0352fffabe
...
c3fd4d9f57
| Author | SHA256 | Date | |
|---|---|---|---|
| c3fd4d9f57 | |||
| 0ce0b2b842 | |||
| 187ac702bf | |||
| 04bf1d358f | |||
| 26b9785ba3 | |||
| 0a5637f12a |
108
src/css/options.css
Normal file
108
src/css/options.css
Normal file
@@ -0,0 +1,108 @@
|
||||
/* css/options.css - matches SPA theme */
|
||||
|
||||
/* Container for the options page */
|
||||
.options-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center; /* center horizontally */
|
||||
width: 100%;
|
||||
padding: 20px 18px;
|
||||
box-sizing: border-box;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 60vh;
|
||||
font-family: Inter, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Cards / sections inside options page */
|
||||
.option-card {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 2px 6px var(--shadow);
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Headings inside cards */
|
||||
.option-card h1,
|
||||
.option-card h2,
|
||||
.option-card h3,
|
||||
.option-card h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Buttons inside cards */
|
||||
.option-card button {
|
||||
margin: 0.25rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.option-card button:hover {
|
||||
background: var(--accentHover);
|
||||
}
|
||||
|
||||
/* Inputs (password, text) and textarea (config/logs) */
|
||||
.option-card input[type="password"],
|
||||
.option-card input[type="text"],
|
||||
.option-card textarea {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
margin: 0.25rem 0.5rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Config editor & logs box */
|
||||
#configEditor,
|
||||
#logsBox {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
height: 200px;
|
||||
padding: 0.5rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
overflow-x: auto;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Flash animation for updates */
|
||||
.updated {
|
||||
background-color: rgba(0, 200, 0, 0.2);
|
||||
transition: background 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Responsive tweaks */
|
||||
@media (max-width: 720px) {
|
||||
.option-card input[type="password"],
|
||||
.option-card input[type="text"],
|
||||
.option-card textarea {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
20
src/includes/api/admin.php
Normal file
20
src/includes/api/admin.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (empty($_SESSION['is_admin'])) exit(json_encode(['error'=>'Not authorized']));
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
$old = $_POST['oldPass'] ?? '';
|
||||
$new = $_POST['newPass'] ?? '';
|
||||
|
||||
$stored_hash = file_get_contents(__DIR__.'/../../admin.pass'); // store hashed password
|
||||
|
||||
if($action === 'change_pass'){
|
||||
if(!password_verify($old, $stored_hash)) exit(json_encode(['error'=>'Current password incorrect']));
|
||||
$hash = password_hash($new, PASSWORD_DEFAULT);
|
||||
file_put_contents(__DIR__.'/../../admin.pass', $hash);
|
||||
echo json_encode(['message'=>'Password changed successfully']);
|
||||
}else{
|
||||
echo json_encode(['error'=>'Unknown action']);
|
||||
}
|
||||
16
src/includes/api/cache.php
Normal file
16
src/includes/api/cache.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (empty($_SESSION['is_admin'])) exit(json_encode(['error'=>'Not authorized']));
|
||||
|
||||
$cacheDir = __DIR__.'/../../cache';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if(!is_dir($cacheDir)){
|
||||
echo json_encode(['error'=>'Cache folder not found']); exit;
|
||||
}
|
||||
|
||||
$files = glob($cacheDir.'/*');
|
||||
foreach($files as $f){ if(is_file($f)) unlink($f); }
|
||||
|
||||
echo json_encode(['message'=>'Cache cleared']);
|
||||
100
src/includes/api/config.php
Normal file
100
src/includes/api/config.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
// includes/api/config.php
|
||||
session_start();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if (empty($_SESSION['is_admin'])) {
|
||||
echo json_encode(['error' => 'Unauthorized']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$configFile = __DIR__ . '/../config.php';
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
function makeDiff($old, $new) {
|
||||
$oldLines = explode("\n", $old);
|
||||
$newLines = explode("\n", $new);
|
||||
$diff = [];
|
||||
|
||||
$max = max(count($oldLines), count($newLines));
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
$o = $oldLines[$i] ?? '';
|
||||
$n = $newLines[$i] ?? '';
|
||||
if ($o !== $n) {
|
||||
$diff[] = sprintf("Line %d:\n- %s\n+ %s", $i + 1, $o, $n);
|
||||
}
|
||||
}
|
||||
return implode("\n\n", $diff);
|
||||
}
|
||||
|
||||
function syntaxCheck($content) {
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'cfgcheck_');
|
||||
file_put_contents($tmpFile, "<?php\n" . $content);
|
||||
exec("php -l " . escapeshellarg($tmpFile), $output, $ret);
|
||||
//unlink($tmpFile);
|
||||
if ($ret !== 0) return implode("\n", $output);
|
||||
return '';
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'load':
|
||||
if (!file_exists($configFile)) {
|
||||
echo json_encode(['error' => 'Config file not found']);
|
||||
exit;
|
||||
}
|
||||
$content = file_get_contents($configFile);
|
||||
echo json_encode(['content' => $content]);
|
||||
break;
|
||||
|
||||
case 'preview-save':
|
||||
$newData = $_POST['data'] ?? '';
|
||||
if (!file_exists($configFile)) {
|
||||
echo json_encode(['error' => 'Config file not found']);
|
||||
exit;
|
||||
}
|
||||
$oldData = file_get_contents($configFile);
|
||||
$diff = makeDiff($oldData, $newData);
|
||||
|
||||
// Syntax check
|
||||
$syntaxErr = syntaxCheck($newData);
|
||||
if ($syntaxErr) {
|
||||
echo json_encode(['error' => "Syntax error detected:\n$syntaxErr"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (trim($diff) === '') {
|
||||
echo json_encode(['message' => 'No changes detected']);
|
||||
} else {
|
||||
echo json_encode(['diff' => $diff]);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'save':
|
||||
$newData = $_POST['data'] ?? '';
|
||||
if (trim($newData) === '') {
|
||||
echo json_encode(['error' => 'Config data cannot be empty']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Syntax check before saving
|
||||
$syntaxErr = syntaxCheck($newData);
|
||||
if ($syntaxErr) {
|
||||
echo json_encode(['error' => "Syntax error detected:\n$syntaxErr"]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Backup original
|
||||
copy($configFile, $configFile . '.bak_' . date('Ymd_His'));
|
||||
|
||||
$result = file_put_contents($configFile, $newData);
|
||||
if ($result === false) {
|
||||
echo json_encode(['error' => 'Failed to write config file']);
|
||||
} else {
|
||||
echo json_encode(['message' => 'Configuration saved successfully']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['error' => 'Invalid action']);
|
||||
}
|
||||
?>
|
||||
14
src/includes/api/logs.php
Normal file
14
src/includes/api/logs.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (empty($_SESSION['is_admin'])) exit(json_encode(['error'=>'Not authorized']));
|
||||
|
||||
$logFile = __DIR__.'/../../logs/system.log';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if(!file_exists($logFile)){
|
||||
echo json_encode(['error'=>'Log file not found']); exit;
|
||||
}
|
||||
|
||||
$content = file_get_contents($logFile);
|
||||
echo json_encode(['content'=>$content]);
|
||||
55
src/includes/api/system.php
Normal file
55
src/includes/api/system.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
session_start();
|
||||
if (empty($_SESSION['is_admin'])) exit(json_encode(['error'=>'Not authorized']));
|
||||
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
switch($action){
|
||||
case 'system-info':
|
||||
$uptime = shell_exec('uptime -p');
|
||||
$load = sys_getloadavg()[0] ?? 'N/A';
|
||||
$mem = round(memory_get_usage()/1024/1024,2) . ' MB';
|
||||
echo json_encode([
|
||||
'uptime'=>$uptime,
|
||||
'load'=>$load,
|
||||
'memory'=>$mem
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'wifi-status':
|
||||
$status = trim(shell_exec('uci get wireless.@wifi-iface[0].disabled')) === '1' ? 'disabled' : 'enabled';
|
||||
echo json_encode(['message'=>$status]);
|
||||
break;
|
||||
|
||||
case 'wifi-toggle':
|
||||
shell_exec('uci set wireless.@wifi-iface[0].disabled=$( [ $(uci get wireless.@wifi-iface[0].disabled) -eq 1 ] && echo 0 || echo 1 ) && wifi');
|
||||
echo json_encode(['message'=>'Wi-Fi toggled']);
|
||||
break;
|
||||
|
||||
case 'service-status':
|
||||
$service = escapeshellarg($_POST['service'] ?? '');
|
||||
if(!$service) { echo json_encode(['error'=>'No service specified']); exit; }
|
||||
$status = shell_exec("service $service status");
|
||||
echo json_encode(['message'=> $status]);
|
||||
break;
|
||||
|
||||
case 'service-start':
|
||||
case 'service-stop':
|
||||
case 'service-restart':
|
||||
$service = escapeshellarg($_POST['service'] ?? '');
|
||||
if(!$service) { echo json_encode(['error'=>'No service specified']); exit; }
|
||||
$cmd = str_replace('service-', '', $action);
|
||||
shell_exec("service $service $cmd");
|
||||
echo json_encode(['message'=>"Service $service $cmd executed"]);
|
||||
break;
|
||||
|
||||
case 'system-reboot':
|
||||
shell_exec('reboot');
|
||||
echo json_encode(['message'=>'Rebooting device']);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['error'=>'Unknown action']);
|
||||
}
|
||||
@@ -1,39 +1,74 @@
|
||||
<?php
|
||||
// includes/config.php
|
||||
// Global configuration for the site. Edit values as needed.
|
||||
|
||||
// === Site Setting ===
|
||||
$ROOT_DIR='/home/reclusejay/repos/camera-gallery/src';
|
||||
$CONFIG = [
|
||||
|
||||
// === General Site Info ===
|
||||
'site_name' => 'My Photo Gallery',
|
||||
'nav_order' => ['home', 'gallery', 'upload'],
|
||||
'nav_admin' => ['shell', 'gethash'],
|
||||
'nav_hidden' => ['admin','infophp'],
|
||||
'site_name' => 'The Photo Gallery',
|
||||
'default_theme' => 'dark', // 'dark' or 'light'
|
||||
'nav_order' => ['home', 'gallery', 'upload'],
|
||||
'nav_admin' => ['shell', 'options'],
|
||||
'nav_hidden' => ['admin','infophp','gethash'],
|
||||
'timezone' => 'UTC',
|
||||
|
||||
// === Paths ===
|
||||
'base_dir' => $ROOT_DIR,
|
||||
'gallery_dir' => $ROOT_DIR.'/img/sorted/jpg',
|
||||
'thumb_dir' => $ROOT_DIR.'/cache/thumbs',
|
||||
'cache_dir' => $ROOT_DIR.'/cache',
|
||||
'log_dir' => $ROOT_DIR.'/logs',
|
||||
'upload_dir' => $ROOT_DIR.'/img/uploads',
|
||||
'index_file' => $ROOT_DIR.'/img/.index',
|
||||
|
||||
|
||||
// === Gallery & Caching ===
|
||||
'max_per_page' => 100,
|
||||
'thumb_max_size' => 300,
|
||||
'thumb_quality' => 80,
|
||||
'cache_file' => $ROOT_DIR.'/cache/gallery.json',
|
||||
|
||||
// === Uploads ===
|
||||
'max_parallel_uploads' => 2, // max simultaneous uploads
|
||||
|
||||
// === Shell ===
|
||||
'blacklist_commands' => ['rm', 'shutdown', 'reboot', 'passwd', 'dd', ':(){'],
|
||||
'default_dir' => '/home',
|
||||
|
||||
|
||||
|
||||
|
||||
];
|
||||
|
||||
// === JavaScript Variables ===
|
||||
$CONFIG += [
|
||||
// === Index System Wide ===
|
||||
'index_admin_status_update_interval' => 50000,
|
||||
|
||||
// === Home Page ===
|
||||
'home_chart_color_threshold_medium' => '70',
|
||||
'home_chart_color_threshold_high' => '80',
|
||||
|
||||
// === Shell Page ===
|
||||
'shell_print_howto_to_logconsole' => TRUE,
|
||||
|
||||
// === Options Page ===
|
||||
'options_status_interval' => 100000,
|
||||
'options_status_services' => ['uhttpd', 'dnsmasq', 'dropbear'],
|
||||
|
||||
];
|
||||
|
||||
// === ! DO NOT CHANGE ! ===
|
||||
$CONFIG += [
|
||||
// === Paths ===
|
||||
'base_dir' => $ROOT_DIR, // Location of root directory for the web server usually (Define at the top $ROOT_DIR)
|
||||
'gallery_dir' => $ROOT_DIR.'/img/sorted/jpg', // Location of the images
|
||||
'upload_dir' => $ROOT_DIR.'/img/uploads', // Location for uploaded images
|
||||
'index_file' => $ROOT_DIR.'/img/.index', // Location of the index file for image naming
|
||||
'cache_dir' => $ROOT_DIR.'/cache', // Location of the cache directory
|
||||
'thumb_dir' => $ROOT_DIR.'/cache/thumbs', // Location for generated thumbnail
|
||||
'exif_dir' => $ROOT_DIR.'/cache/exif', // Location of exif file, info extracted from images
|
||||
'cache_file' => $ROOT_DIR.'/cache/gallery.json', // Location of the gallery cache file
|
||||
'log_dir' => $ROOT_DIR.'/logs', // Location of the log files
|
||||
// Admin Hashed Password
|
||||
'upload_password' => '$2y$12$Fb7u3H.428PoPCy/pxhqpu.poqjmDbiyzJtRJs/CEcCEPPMOYBLCm', // The bcrypt hash for admin password ! DO NOT USE PLAIN TEXT PASSWORD !
|
||||
// Exclude empty or useless exif information
|
||||
'remove_nodes' => ['MAKERNOTE', 'INTEROP', 'THUMBNAIL'],
|
||||
'remove_tags' => ['SectionsFound','UserComment','UserCommentEncoding','Thumbnail.FileType','Thumbnail.MimeType','ComponentsConfiguration','FileSource','SceneType'],
|
||||
'exif_dir' => $ROOT_DIR.'/cache/exif',
|
||||
|
||||
// === Uploads ===
|
||||
'upload_password' => '$2y$12$Fb7u3H.428PoPCy/pxhqpu.poqjmDbiyzJtRJs/CEcCEPPMOYBLCm', // bcrypt hash
|
||||
'max_parallel_uploads' => 2, // max simultaneous uploads
|
||||
|
||||
];
|
||||
|
||||
// Apply timezone globally
|
||||
date_default_timezone_set($CONFIG['timezone']);
|
||||
|
||||
?>
|
||||
@@ -17,7 +17,7 @@ if ($cmd === '') {
|
||||
}
|
||||
|
||||
// Restrict dangerous commands for safety
|
||||
$blacklist = ['rm', 'shutdown', 'reboot', 'passwd', 'dd', ':(){'];
|
||||
$blacklist = $CONFIG['blacklist_commands'];
|
||||
foreach ($blacklist as $bad) {
|
||||
if (stripos($cmd, $bad) !== false) {
|
||||
echo json_encode(['output' => "⚠️ Command '$bad' not allowed"]);
|
||||
@@ -30,7 +30,7 @@ $descriptor = [
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w']
|
||||
];
|
||||
$process = proc_open($cmd, $descriptor, $pipes, '/home');
|
||||
$process = proc_open($cmd, $descriptor, $pipes, $CONFIG['default_dir']);
|
||||
if (is_resource($process)) {
|
||||
$output = stream_get_contents($pipes[1]);
|
||||
$error = stream_get_contents($pipes[2]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
// index.php
|
||||
// Start session and include config
|
||||
session_start();
|
||||
// index.php - SPA
|
||||
require_once __DIR__ . '/includes/config.php';
|
||||
|
||||
// Replicate what admin_auth.php does for the "check" action
|
||||
@@ -17,7 +18,7 @@ $nav_bar_admin = [];
|
||||
if (is_dir($pages_dir)) {
|
||||
foreach (glob($pages_dir . '/*.php') as $file) {
|
||||
$base = strtolower(basename($file, '.php'));
|
||||
if (in_array($base, ['_template', 'error'])) continue; // skip special files
|
||||
if (in_array($base, ['_template', 'error', 'log'])) continue; // skip special files
|
||||
if (in_array($base, array_map('strtolower', $nav_hidden))) continue;
|
||||
$label = ucfirst($base);
|
||||
|
||||
@@ -30,7 +31,7 @@ if (is_dir($pages_dir)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Optional: order nav_bar according to $nav_order
|
||||
// Order nav_bar according to $nav_order
|
||||
$ordered = [];
|
||||
foreach ($nav_order as $o) {
|
||||
foreach ($nav_bar as $key => $label) {
|
||||
@@ -51,6 +52,16 @@ unset($nav_order,$nav_admin,$nav_hidden);
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title><?= htmlspecialchars($CONFIG['site_name']) ?></title>
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<script>
|
||||
const CONFIG = {
|
||||
options_status_interval: <?= ($CONFIG['options_status_interval'] ?? 30000) ?>,
|
||||
options_status_services: <?= ($CONFIG['options_status_services'] ?? []) ?>,
|
||||
shell_print_howto_to_logconsole: <?= ($CONFIG['shell_print_howto_to_logconsole'] ?? false) ?>,
|
||||
home_chart_color_threshold_high: <?= ($CONFIG['home_chart_color_threshold_high'] ?? 70) ?>,
|
||||
home_chart_color_threshold_medium: <?= ($CONFIG['home_chart_color_threshold_medium'] ?? 50) ?>,
|
||||
index_admin_status_update_interval: <?= ($CONFIG['index_admin_status_update_interval'] ?? 50) ?>,
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
@@ -94,10 +105,7 @@ unset($nav_order,$nav_admin,$nav_hidden);
|
||||
|
||||
<!-- GLightbox -->
|
||||
<link rel="stylesheet" href="dist/css/glightbox.min.css">
|
||||
<script src="dist/js/glightbox.min.js"></script>
|
||||
|
||||
|
||||
|
||||
<script src="dist/js/glightbox.min.js" defer></script>
|
||||
<script src="js/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -68,7 +68,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
await checkAdmin();
|
||||
setInterval(checkAdmin, 30000);
|
||||
setInterval(checkAdmin, CONFIG.index_admin_status_update_interval);
|
||||
})();
|
||||
|
||||
// --- Page loader ---
|
||||
|
||||
@@ -60,9 +60,9 @@ document.addEventListener('page-loaded', e => {
|
||||
}
|
||||
|
||||
// Color thresholds
|
||||
if (numVal >= 85) {
|
||||
if (numVal >= CONFIG.home_chart_color_threshold_high) {
|
||||
elements[key].className = 'high';
|
||||
} else if (numVal >= 75) {
|
||||
} else if (numVal >= CONFIG.home_chart_color_threshold_medium) {
|
||||
elements[key].className = 'medium';
|
||||
} else {
|
||||
elements[key].className = 'low';
|
||||
|
||||
183
src/js/options.js
Normal file
183
src/js/options.js
Normal file
@@ -0,0 +1,183 @@
|
||||
document.addEventListener('page-loaded', e => {
|
||||
if (e.detail.page !== 'options') return;
|
||||
|
||||
// ---- STATUS WIDGET ----
|
||||
const wifi = document.getElementById('statusWifi');
|
||||
const uptime = document.getElementById('statusUptime');
|
||||
const load = document.getElementById('statusLoad');
|
||||
const mem = document.getElementById('statusMem');
|
||||
const services = document.getElementById('statusServices');
|
||||
const btnRefresh = document.getElementById('btnRefreshStatus');
|
||||
|
||||
async function updateStatus() {
|
||||
try {
|
||||
const res = await fetch('includes/api/system.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'system-info' })
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.error) throw new Error(j.error);
|
||||
|
||||
wifi.textContent = j.wifi || '–';
|
||||
uptime.textContent = j.uptime || '–';
|
||||
load.textContent = j.load || '–';
|
||||
mem.textContent = j.memory || '–';
|
||||
|
||||
const svcList = CONFIG.options_status_services;
|
||||
const results = [];
|
||||
for (const svc of svcList) {
|
||||
const r = await fetch('includes/api/system.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'service-status', service: svc })
|
||||
});
|
||||
const text = await r.text();
|
||||
results.push(`${svc}: ${text.includes('running') ? '🟢' : '🔴'}`);
|
||||
}
|
||||
services.textContent = results.join(' ');
|
||||
} catch (err) {
|
||||
wifi.textContent = uptime.textContent = load.textContent = mem.textContent = 'Error';
|
||||
services.textContent = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
btnRefresh?.addEventListener('click', updateStatus);
|
||||
updateStatus();
|
||||
setInterval(updateStatus, CONFIG.options_status_interval);
|
||||
|
||||
// ---- CONFIG EDITOR ----
|
||||
const editor = document.getElementById('configEditor');
|
||||
const msg = document.getElementById('configMessage');
|
||||
const btnLoadConfig = document.getElementById('btnLoadConfig');
|
||||
const btnSaveConfig = document.getElementById('btnSaveConfig');
|
||||
|
||||
if (!btnSaveConfig.dataset.bound) {
|
||||
btnSaveConfig.dataset.bound = 'true';
|
||||
|
||||
btnLoadConfig?.addEventListener('click', async () => {
|
||||
msg.textContent = 'Loading...';
|
||||
try {
|
||||
const res = await fetch('includes/api/config.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'load' })
|
||||
});
|
||||
const j = await res.json();
|
||||
if (j.error) msg.textContent = j.error;
|
||||
else {
|
||||
editor.value = j.content || '';
|
||||
msg.textContent = 'Config loaded.';
|
||||
}
|
||||
} catch (err) {
|
||||
msg.textContent = 'Error loading config: ' + err.message;
|
||||
}
|
||||
});
|
||||
|
||||
btnSaveConfig?.addEventListener('click', async () => {
|
||||
msg.textContent = 'Checking changes...';
|
||||
try {
|
||||
const data = editor.value;
|
||||
|
||||
// Preview changes & syntax check
|
||||
const previewRes = await fetch('includes/api/config.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'preview-save', data })
|
||||
});
|
||||
const preview = await previewRes.json();
|
||||
|
||||
if (preview.error) {
|
||||
msg.textContent = preview.error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (preview.message) {
|
||||
msg.textContent = preview.message;
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMsg = `⚙️ Changes detected (${preview.diff.split('\n\n').length} lines):\n\n` +
|
||||
preview.diff.slice(0, 1000) +
|
||||
(preview.diff.length > 1000 ? '\n\n...preview truncated...' : '') +
|
||||
`\n\nSave these changes?`;
|
||||
|
||||
if (!confirm(confirmMsg)) {
|
||||
msg.textContent = 'Save canceled.';
|
||||
return;
|
||||
}
|
||||
|
||||
msg.textContent = 'Saving...';
|
||||
const resSave = await fetch('includes/api/config.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'save', data })
|
||||
});
|
||||
const jSave = await resSave.json();
|
||||
msg.textContent = jSave.message || jSave.error || 'Saved successfully.';
|
||||
if (jSave.message && jSave.message.includes('successfully')) {
|
||||
editor.value = '';
|
||||
msg.textContent = '';
|
||||
}
|
||||
} catch (err) {
|
||||
msg.textContent = 'Error: ' + err.message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- CACHE ----
|
||||
const cacheBtn = document.getElementById('btnClearCache');
|
||||
cacheBtn?.addEventListener('click', async () => {
|
||||
if (!confirm('Clear cache folder?')) return;
|
||||
const res = await fetch('includes/api/cache.php', { method: 'POST' });
|
||||
const j = await res.json();
|
||||
alert(j.message || j.error);
|
||||
});
|
||||
|
||||
// ---- LOGS ----
|
||||
const logsBtn = document.getElementById('btnViewLogs');
|
||||
const logsBox = document.getElementById('logsBox');
|
||||
logsBtn?.addEventListener('click', async () => {
|
||||
const res = await fetch('includes/api/logs.php');
|
||||
const j = await res.json();
|
||||
logsBox.textContent = j.content || j.error;
|
||||
});
|
||||
|
||||
// ---- WIFI / LAN ----
|
||||
const wifiBtn = document.getElementById('btnWifiToggle');
|
||||
const lanBtn = document.getElementById('btnLanRestart');
|
||||
|
||||
wifiBtn?.addEventListener('click', async () => {
|
||||
await fetch('includes/api/system.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'wifi-toggle' })
|
||||
});
|
||||
updateStatus();
|
||||
});
|
||||
|
||||
lanBtn?.addEventListener('click', async () => {
|
||||
await fetch('includes/api/system.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ action: 'service-restart', service: 'network' })
|
||||
});
|
||||
updateStatus();
|
||||
});
|
||||
|
||||
// ---- PASSWORD CHANGE ----
|
||||
const oldPass = document.getElementById('oldPass');
|
||||
const newPass = document.getElementById('newPass');
|
||||
const confirmPass = document.getElementById('confirmPass');
|
||||
const passBtn = document.getElementById('btnChangePass');
|
||||
|
||||
passBtn?.addEventListener('click', async () => {
|
||||
if (newPass.value !== confirmPass.value) {
|
||||
alert('New passwords do not match');
|
||||
return;
|
||||
}
|
||||
const res = await fetch('includes/api/admin.php', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
action: 'change_pass',
|
||||
oldPass: oldPass.value,
|
||||
newPass: newPass.value
|
||||
})
|
||||
});
|
||||
const j = await res.json();
|
||||
alert(j.message || j.error);
|
||||
});
|
||||
});
|
||||
@@ -79,12 +79,13 @@
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[shell.js] Shell initialized.');
|
||||
console.log('💡 Usage');
|
||||
console.log('Shortcut Action');
|
||||
console.log('↑ / ↓ Browse previous commands');
|
||||
console.log('Ctrl + L Clear the screen');
|
||||
console.log('Enter Execute command');
|
||||
if (CONFIG.shell_print_howto_to_logconsole){
|
||||
console.log('[shell.js] Shell initialized.');
|
||||
console.log('💡 Usage');
|
||||
console.log('Shortcut Action');
|
||||
console.log('↑ / ↓ Browse previous commands');
|
||||
console.log('Ctrl + L Clear the screen');
|
||||
console.log('Enter Execute command');
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
80
src/pages/options.php
Normal file
80
src/pages/options.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
session_start();
|
||||
require_once __DIR__ . '/../includes/config.php';
|
||||
if (empty($_SESSION['is_admin'])) {
|
||||
header('Location: login.php');
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Options Panel</title>
|
||||
<link rel="stylesheet" href="css/options.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="options-container">
|
||||
|
||||
<h1>System Options</h1>
|
||||
|
||||
<section class="option-card status-widget" id="systemStatus">
|
||||
<h2>System Status</h2>
|
||||
<div class="status-grid">
|
||||
<div><strong>Wi-Fi:</strong> <span id="statusWifi">Loading…</span></div>
|
||||
<div><strong>Uptime:</strong> <span id="statusUptime">–</span></div>
|
||||
<div><strong>Load:</strong> <span id="statusLoad">–</span></div>
|
||||
<div><strong>Memory:</strong> <span id="statusMem">–</span></div>
|
||||
<div><strong>Services:</strong> <span id="statusServices">–</span></div>
|
||||
</div>
|
||||
<br>
|
||||
<button id="btnRefreshStatus" class="btn small">🔄 Refresh</button>
|
||||
</section>
|
||||
|
||||
<section class="option-card">
|
||||
<h2>Wi-Fi & Network</h2>
|
||||
<button id="btnWifiToggle">Toggle Wi-Fi</button>
|
||||
<button id="btnLanRestart">Restart LAN</button>
|
||||
</section>
|
||||
|
||||
<section class="option-card">
|
||||
<h2>Services</h2>
|
||||
<input type="text" id="serviceName" placeholder="Service name (e.g., uhttpd)">
|
||||
<button id="btnServiceStart">Start</button>
|
||||
<button id="btnServiceStop">Stop</button>
|
||||
<button id="btnServiceRestart">Restart</button>
|
||||
</section>
|
||||
|
||||
<section class="option-card">
|
||||
<h2>Configuration File</h2>
|
||||
<textarea id="configEditor" placeholder="Config contents here..."></textarea>
|
||||
<button id="btnLoadConfig">Load</button>
|
||||
<button id="btnSaveConfig">Save</button>
|
||||
<div id="configMessage"></div>
|
||||
</section>
|
||||
|
||||
<section class="option-card">
|
||||
<h2>Cache</h2>
|
||||
<button id="btnClearCache">Clear Cache</button>
|
||||
</section>
|
||||
|
||||
<section class="option-card">
|
||||
<h2>Logs</h2>
|
||||
<button id="btnViewLogs">View Logs</button>
|
||||
<pre id="logsBox">(none)</pre>
|
||||
</section>
|
||||
|
||||
<section class="option-card">
|
||||
<h2>Admin Password</h2>
|
||||
<input type="password" id="oldPass" placeholder="Old password">
|
||||
<input type="password" id="newPass" placeholder="New password">
|
||||
<input type="password" id="confirmPass" placeholder="Confirm new password">
|
||||
<button id="btnChangePass">Change Password</button>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="js/options.js" defer></script>
|
||||
<link rel="stylesheet" href="css/options.css">
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,9 +17,6 @@ if (empty($_SESSION['is_admin'])) {
|
||||
<input id="shellInput" class="shell-input" type="text" placeholder="Enter command..." autocomplete="off" />
|
||||
<button id="runBtn" class="btn-run" type="submit">Run</button>
|
||||
</form>
|
||||
|
||||
<script src="js/shell.js" defer></script>
|
||||
<link rel="stylesheet" href="css/shell.css">
|
||||
</section>
|
||||
|
||||
<link rel="stylesheet" href="css/shell.css">
|
||||
|
||||
Reference in New Issue
Block a user