diff --git a/src/css/options.css b/src/css/options.css new file mode 100644 index 0000000..5712143 --- /dev/null +++ b/src/css/options.css @@ -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; + } +} diff --git a/src/includes/api/admin.php b/src/includes/api/admin.php new file mode 100644 index 0000000..3ae5649 --- /dev/null +++ b/src/includes/api/admin.php @@ -0,0 +1,20 @@ +'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']); +} diff --git a/src/includes/api/cache.php b/src/includes/api/cache.php new file mode 100644 index 0000000..9abd3b1 --- /dev/null +++ b/src/includes/api/cache.php @@ -0,0 +1,16 @@ +'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']); diff --git a/src/includes/api/config.php b/src/includes/api/config.php new file mode 100644 index 0000000..10fade9 --- /dev/null +++ b/src/includes/api/config.php @@ -0,0 +1,100 @@ + '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, " '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']); +} +?> diff --git a/src/includes/api/logs.php b/src/includes/api/logs.php new file mode 100644 index 0000000..f86017e --- /dev/null +++ b/src/includes/api/logs.php @@ -0,0 +1,14 @@ +'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]); diff --git a/src/includes/api/system.php b/src/includes/api/system.php new file mode 100644 index 0000000..d8d5dd2 --- /dev/null +++ b/src/includes/api/system.php @@ -0,0 +1,55 @@ +'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']); +} diff --git a/src/js/options.js b/src/js/options.js new file mode 100644 index 0000000..2e7c6a4 --- /dev/null +++ b/src/js/options.js @@ -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); + }); +}); diff --git a/src/pages/options.php b/src/pages/options.php new file mode 100644 index 0000000..52bb38b --- /dev/null +++ b/src/pages/options.php @@ -0,0 +1,80 @@ + + + + + +Options Panel + + + +
+ +

System Options

+ +
+

System Status

+
+
Wi-Fi: Loading…
+
Uptime: –
+
Load: –
+
Memory: –
+
Services: –
+
+
+ +
+ +
+

Wi-Fi & Network

+ + +
+ +
+

Services

+ + + + +
+ +
+

Configuration File

+ + + +
+
+ +
+

Cache

+ +
+ +
+

Logs

+ +
(none)
+
+ +
+

Admin Password

+ + + + +
+ +
+ + + + +