Add settings page, PIN manager, and auto-config
- New settings page at /settings (localhost-only) accessible via tray icon - PIN manager with 6-digit codes, 5-min TTL, single-use - API token uses secrets.token_urlsafe(32) for 256-bit entropy - Config auto-creates config.yaml on first run with detected local IP - Default PIN and token generated randomly on first startup - Added pin_ttl setting (default 300s) - Updated build_exe.py for onefile exe with settings assets bundled - Added start.bat and start.sh for easy development startup
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>PaperDash Settings</title>
|
||||
<link rel="stylesheet" href="settings.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>PaperDash Settings</h1>
|
||||
<p class="subtitle">Changes take effect on next restart</p>
|
||||
|
||||
<form id="settings-form">
|
||||
<div class="field">
|
||||
<label for="dashboard-pin">Dashboard PIN</label>
|
||||
<input
|
||||
type="text"
|
||||
id="dashboard-pin"
|
||||
maxlength="4"
|
||||
placeholder="****"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="api-token">API Token</label>
|
||||
<input
|
||||
type="password"
|
||||
id="api-token"
|
||||
placeholder="Bearer token for scripts"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="host">Host</label>
|
||||
<input type="text" id="host" placeholder="Local IP address" />
|
||||
<p class="hint">Address to bind the server to</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="port">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
id="port"
|
||||
placeholder="8921"
|
||||
min="1"
|
||||
max="65535"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="log-level">Log Level</label>
|
||||
<select id="log-level">
|
||||
<option value="DEBUG">DEBUG</option>
|
||||
<option value="INFO">INFO</option>
|
||||
<option value="WARNING">WARNING</option>
|
||||
<option value="ERROR">ERROR</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" id="reset-btn">Reset</button>
|
||||
<button type="submit" id="save-btn">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("settings-form");
|
||||
const statusEl = document.getElementById("status");
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const res = await fetch("/api/v1/settings");
|
||||
if (!res.ok) throw new Error("Failed to load settings");
|
||||
const data = await res.json();
|
||||
document.getElementById("dashboard-pin").value =
|
||||
data.dashboard_pin || "";
|
||||
document.getElementById("api-token").value = data.api_token || "";
|
||||
document.getElementById("host").value = data.host || "";
|
||||
document.getElementById("port").value = data.port || 8921;
|
||||
document.getElementById("log-level").value = data.log_level || "INFO";
|
||||
} catch (err) {
|
||||
showStatus("Failed to load settings: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSettings(e) {
|
||||
e.preventDefault();
|
||||
const payload = {
|
||||
dashboard_pin: document.getElementById("dashboard-pin").value.trim(),
|
||||
api_token: document.getElementById("api-token").value.trim(),
|
||||
host: document.getElementById("host").value.trim(),
|
||||
port: parseInt(document.getElementById("port").value, 10),
|
||||
log_level: document.getElementById("log-level").value,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/v1/settings", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to save settings");
|
||||
showStatus("Settings saved. Restart the server to apply.", "success");
|
||||
} catch (err) {
|
||||
showStatus("Failed to save: " + err.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
function resetSettings() {
|
||||
document.getElementById("dashboard-pin").value = "";
|
||||
document.getElementById("api-token").value = "";
|
||||
document.getElementById("host").value = "";
|
||||
document.getElementById("port").value = 8921;
|
||||
document.getElementById("log-level").value = "INFO";
|
||||
hideStatus();
|
||||
}
|
||||
|
||||
function showStatus(msg, type) {
|
||||
statusEl.textContent = msg;
|
||||
statusEl.className = "status " + type;
|
||||
if (type === "success") {
|
||||
setTimeout(hideStatus, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function hideStatus() {
|
||||
statusEl.className = "status";
|
||||
statusEl.style.display = "none";
|
||||
}
|
||||
|
||||
form.addEventListener("submit", saveSettings);
|
||||
document
|
||||
.getElementById("reset-btn")
|
||||
.addEventListener("click", resetSettings);
|
||||
loadSettings();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user