Migrating Controller from port 8087 to 80 so that it is more discoverable. Reformatting Controller dashboard to make header at the top. Reformatting navigation bar to handle the new links which prevented Admin from being visible in some screens.

This commit is contained in:
Roland 2026-04-07 20:01:49 -06:00 • committed by lrnselfreliance
parent c316a178f9
commit e0324ce0fc
33 changed files with 302 additions and 407 deletions

View file

@ -441,7 +441,7 @@ export function DashboardPage() {
</Grid.Row>
</Grid>
</Media>
<Media at='tablet'>
<Media between={['tablet', 'computer']}>
<Grid>
<Grid.Row columns={2}>
<Grid.Column textAlign='right' width={2}>

View file

@ -39,11 +39,11 @@ const links = [
{text: 'Archive', to: '/archives', key: 'archive'},
{text: 'Docs', to: '/docs', key: 'docs'},
{text: 'Map', to: '/map', key: 'map'},
{text: 'Zim', to: '/zim', key: 'zim'},
{text: 'Files', to: '/files', key: 'files'},
{
text: 'More', key: 'more',
links: [
{text: 'Zim', to: '/zim', key: 'zim'},
{text: 'Inventory', to: '/inventory', key: 'inventory'},
{to: '/more/otp', text: 'One Time Pad', end: true},
{to: '/more/vin', text: 'VIN Decoder', end: true},
@ -265,7 +265,7 @@ export function NavBar() {
</React.Fragment>;
return <>
<Media at='mobile'>
<Media between={['mobile', 'wideTablet']}>
<Menu {...i} attached='top' color={navColor} id='global_navbar'>
{homeLink}
<Menu.Menu position='right'>
@ -279,7 +279,7 @@ export function NavBar() {
</Menu.Menu>
</Menu>
</Media>
<Media greaterThanOrEqual='tablet'>
<Media greaterThanOrEqual='wideTablet'>
<Menu {...i} attached='top' color={navColor} id='global_navbar'>
{homeLink}
{links.map(i => <MenuLink link={i} key={i.key}/>)}

View file

@ -9,11 +9,13 @@ export const NAME = process.env && process.env.REACT_APP_NAME ? process.env.REAC
// Other services on a WROLPi.
const port = window.location.port ? `${window.location.port.replace(/^/, '')}` : '';
export const FILES_MEDIA_URI = `https://${window.location.hostname}:${port}/media/`;
export const FILES_MEDIA_URI = `https://${window.location.hostname}${port ? ':' + port : ''}/media/`;
export const MAP_VIEWER_URI = `https://${window.location.hostname}:8084`;
export const ZIM_VIEWER_URI = `https://${window.location.hostname}:8085`;
export const HELP_VIEWER_URI = `https://${window.location.hostname}:8086`;
export const CONTROLLER_URI = `http://${window.location.hostname}:8087`; // no https
// Docker: 8080; native: 80 (default, no port needed).
const controllerPort = window.location.port === '8443' ? ':8080' : '';
export const CONTROLLER_URI = `http://${window.location.hostname}${controllerPort}`;
export const API_ARCHIVE_UPLOAD_URI = `http://${window.location.hostname}:8081/api/archive/upload`;
export let defaultFileOrder = '-published_datetime'; // Most recently published files first.

View file

@ -57,7 +57,7 @@ export const QueryContext = React.createContext({
export const AppMedia = createMedia({
breakpoints: {
mobile: 0, tablet: 700, computer: 1024,
mobile: 0, tablet: 700, wideTablet: 880, computer: 1024,
}
});
export const mediaStyles = AppMedia.createMediaStyle();

View file

@ -6,8 +6,9 @@ Controller starts with these defaults and loads overrides from
"""
DEFAULT_CONFIG = {
"port": 8087,
"port": 80,
"media_directory": "/media/wrolpi",
"hide_cert_banner": False,
"drives": {
"supported_filesystems": ["ext4", "btrfs", "vfat", "exfat"],
@ -20,8 +21,8 @@ DEFAULT_CONFIG = {
{
"name": "wrolpi-controller",
"systemd_name": "wrolpi-controller",
"port": 8087,
"viewable": True,
"port": 80,
"viewable": False,
"use_https": False,
"description": "Controller API (FastAPI)",
},
@ -87,10 +88,10 @@ DEFAULT_CONFIG = {
{
"name": "caddy",
"systemd_name": "caddy",
"port": 80,
"port": 443,
"viewable": True,
"use_https": False,
"description": "Web proxy",
"use_https": True,
"description": "Web proxy (HTTPS)",
},
# Samba file sharing services
{

View file

@ -18,7 +18,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s %(name)s %(levelname
logger = logging.getLogger(__name__)
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.templating import Jinja2Templates
from controller import __version__
@ -33,9 +33,13 @@ from controller.api.scripts import router as scripts_router
from controller.api.services import router as services_router
from controller.api.status import router as status_router
from controller.lib.config import (
get_config_value,
get_media_directory,
is_docker_mode,
is_primary_drive_mounted,
reload_config_from_drive,
save_config,
update_config,
)
from controller.lib.docker_services import (
can_manage_containers,
@ -217,6 +221,7 @@ async def dashboard(request: Request):
"version": __version__,
"docker_mode": is_docker_mode(),
"drive_mounted": is_primary_drive_mounted(),
"hide_cert_banner": get_config_value("hide_cert_banner", False),
"host": get_host(request),
# Real status data
@ -259,3 +264,26 @@ async def health_check() -> HealthResponse:
docker_mode=is_docker_mode(),
drive_mounted=is_primary_drive_mounted(),
)
@app.get("/ca.crt")
async def download_ca_certificate():
"""Serve the WROLPi Root CA certificate for browser trust setup."""
ca_path = get_media_directory() / "config" / "ssl" / "ca.crt"
if not ca_path.is_file():
from fastapi.responses import JSONResponse
return JSONResponse(status_code=404, content={"error": "CA certificate not found"})
return FileResponse(
path=ca_path,
media_type="application/x-x509-ca-cert",
filename="wrolpi-ca.crt",
headers={"Content-Disposition": 'attachment; filename="wrolpi-ca.crt"'},
)
@app.post("/api/hide-cert-banner")
async def hide_cert_banner():
"""Permanently hide the HTTPS certificate setup banner."""
update_config("hide_cert_banner", True)
save_config()
return {"ok": True}

11
controller/serve.py Normal file
View file

@ -0,0 +1,11 @@
"""
Entry point for the WROLPi Controller.
Starts uvicorn on port 80 (the primary HTTP entry point for users).
Docker maps this to external port 8080; native/RPi uses port 80 directly.
"""
import uvicorn
if __name__ == "__main__":
uvicorn.run("controller.main:app", host="0.0.0.0", port=80, log_level="info")

View file

@ -174,59 +174,6 @@
border-top-color: #000;
}
.upgrade-banner {
background: linear-gradient(135deg, #f39c12, #e67e22);
color: #000;
padding: 1.5rem;
border-radius: 8px;
margin-bottom: 1rem;
text-align: center;
display: none;
}
.upgrade-banner.show {
display: block;
}
.upgrade-banner h2 {
margin: 0 0 0.5rem 0;
font-size: 1.5rem;
}
.upgrade-banner .loading {
width: 2rem;
height: 2rem;
border-color: #333;
border-top-color: #000;
margin: 0.5rem auto;
}
.upgrade-banner .elapsed {
font-size: 1.25rem;
font-weight: bold;
margin: 0.5rem 0;
}
.upgrade-banner .warning {
font-size: 0.9rem;
margin-top: 0.5rem;
opacity: 0.8;
}
.upgrade-banner .success {
color: #27ae60;
font-size: 2rem;
}
.upgrade-banner.complete {
background: linear-gradient(135deg, #27ae60, #2ecc71);
color: #fff;
}
.upgrade-banner.complete .warning {
display: none;
}
/* Status Grid */
.status-grid {
display: grid;
@ -777,6 +724,101 @@
</div>
{% endif %}
<header>
<div>
<h1>Controller - {{ host }} WROLPi</h1>
</div>
</header>
<!-- Certificate trust banner — shown by JS when HTTPS cert is not trusted -->
<div id="cert-banner" class="card" style="display: none; background: #16213e; border: 1px solid #1a3a5c; border-radius: 12px; padding: 1.5rem; margin-bottom: 1rem;">
<h2 style="font-size: 1.1rem; margin-bottom: 0.75rem; color: #4cc9f0;">HTTPS Certificate Setup</h2>
<p style="line-height: 1.6; color: #ccc; margin-bottom: 1rem;">
Install the WROLPi Root CA certificate so your browser trusts all WROLPi HTTPS services.
</p>
<a class="btn btn-info" href="/ca.crt" download="wrolpi-ca.crt" style="margin-bottom: 1rem; display: inline-block;">Download WROLPi Root CA</a>
<details style="margin-bottom: 0.75rem;">
<summary style="cursor: pointer; padding: 0.5rem 0; font-weight: 600; color: #4cc9f0;">iOS / iPadOS</summary>
<div style="padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7;">
<ol style="padding-left: 1.25rem;">
<li>Tap the download button above, then tap <strong>Allow</strong>.</li>
<li>Open <strong>Settings</strong> &rarr; <strong>General</strong> &rarr; <strong>VPN &amp; Device Management</strong>.</li>
<li>Tap <strong>WROLPi Root CA</strong> &rarr; <strong>Install</strong> &rarr; enter your passcode.</li>
<li>Go to <strong>Settings</strong> &rarr; <strong>General</strong> &rarr; <strong>About</strong> &rarr; <strong>Certificate Trust Settings</strong>.</li>
<li>Enable full trust for <strong>WROLPi Root CA</strong>.</li>
</ol>
</div>
</details>
<details style="margin-bottom: 0.75rem;">
<summary style="cursor: pointer; padding: 0.5rem 0; font-weight: 600; color: #4cc9f0;">Android</summary>
<div style="padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7;">
<ol style="padding-left: 1.25rem;">
<li>Tap the download button above.</li>
<li>Open <strong>Settings</strong> &rarr; <strong>Security &amp; privacy</strong> &rarr; <strong>More security settings</strong> &rarr; <strong>Encryption &amp; credentials</strong> &rarr; <strong>Install a certificate</strong> &rarr; <strong>CA certificate</strong>.</li>
<li>Select the downloaded <code style="background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px;">wrolpi-ca.crt</code> file and confirm.</li>
</ol>
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Menu paths vary by manufacturer. Search for <em>"certificate"</em> in Settings if needed.</p>
</div>
</details>
<details style="margin-bottom: 0.75rem;">
<summary style="cursor: pointer; padding: 0.5rem 0; font-weight: 600; color: #4cc9f0;">macOS</summary>
<div style="padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7;">
<ol style="padding-left: 1.25rem;">
<li>Double-click the downloaded <code style="background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px;">wrolpi-ca.crt</code> to open Keychain Access.</li>
<li>Add it to the <strong>System</strong> or <strong>login</strong> keychain.</li>
<li>Find <strong>WROLPi Root CA</strong>, double-click it.</li>
<li>Expand <strong>Trust</strong>, set to <strong>Always Trust</strong>.</li>
<li>Close and enter your password.</li>
</ol>
</div>
</details>
<details style="margin-bottom: 0.75rem;">
<summary style="cursor: pointer; padding: 0.5rem 0; font-weight: 600; color: #4cc9f0;">Windows</summary>
<div style="padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7;">
<ol style="padding-left: 1.25rem;">
<li>Double-click the downloaded <code style="background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px;">wrolpi-ca.crt</code>.</li>
<li>Click <strong>Install Certificate</strong> &rarr; <strong>Local Machine</strong> &rarr; <strong>Next</strong>.</li>
<li>Select <strong>Place all certificates in the following store</strong> &rarr; <strong>Browse</strong> &rarr; <strong>Trusted Root Certification Authorities</strong>.</li>
<li>Click <strong>Finish</strong>.</li>
</ol>
</div>
</details>
<details style="margin-bottom: 0.75rem;">
<summary style="cursor: pointer; padding: 0.5rem 0; font-weight: 600; color: #4cc9f0;">Linux (Debian / Ubuntu)</summary>
<div style="padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7;">
<ol style="padding-left: 1.25rem;">
<li>Copy the certificate: <code style="background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px;">sudo cp wrolpi-ca.crt /usr/local/share/ca-certificates/</code></li>
<li>Update the trust store: <code style="background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px;">sudo update-ca-certificates</code></li>
<li>Restart your browser.</li>
</ol>
</div>
</details>
<details style="margin-bottom: 0.75rem;">
<summary style="cursor: pointer; padding: 0.5rem 0; font-weight: 600; color: #4cc9f0;">Firefox (all platforms)</summary>
<div style="padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7;">
<p style="margin-bottom:0.5rem;">Since Firefox 120, Firefox <strong>automatically trusts</strong> root certificates from the OS store on <strong>Windows, macOS, and Android</strong>.</p>
<p style="margin-bottom:0.5rem;">On <strong>Linux</strong> (or if auto-trust is disabled), import manually:</p>
<ol style="padding-left: 1.25rem;">
<li>Open Firefox &rarr; <strong>Settings</strong> &rarr; <strong>Privacy &amp; Security</strong>.</li>
<li>Scroll to <strong>Certificates</strong> &rarr; <strong>View Certificates</strong>.</li>
<li>In <strong>Authorities</strong> tab, click <strong>Import</strong>.</li>
<li>Select <code style="background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px;">wrolpi-ca.crt</code>, check <strong>Trust this CA to identify websites</strong>.</li>
</ol>
</div>
</details>
<div style="text-align: center; margin-top: 1rem;">
<a class="btn btn-success" id="cert-continue-btn" href="#">Continue to WROLPi (HTTPS)</a>
<button class="btn btn-secondary" onclick="fetch('api/hide-cert-banner',{method:'POST'}).then(function(){document.getElementById('cert-banner').style.display='none';})" style="margin-left: 0.5rem;">Never ask again</button>
</div>
</div>
{% if not drive_mounted and not docker_mode %}
<div id="onboarding-banner" class="onboarding-banner">
<!-- Step 1: Select drive -->
@ -839,6 +881,12 @@
</div>
{% endif %}
{% set app_ready = services | selectattr('name', 'in', ['caddy', 'web']) | selectattr('status', 'equalto', 'running') | list | length > 0 %}
<div style="margin-bottom: 1rem;">
<a id="proceed-btn" class="btn btn-success" style="{% if not app_ready %}display: none;{% endif %} font-size: 1.1rem; padding: 0.6rem 1.5rem;" href="#">Proceed to WROLPi</a>
<a id="view-files-btn" class="btn btn-info" style="{% if not app_ready %}display: none;{% endif %} font-size: 1.1rem; padding: 0.6rem 1.5rem; margin-left: 0.5rem;" href="#">View Files</a>
</div>
<div id="fallback-banner" class="fallback-banner">
<span class="loading"></span>
Main UI is unavailable. Using Controller fallback. Will redirect when ready...
@ -854,21 +902,6 @@
</div>
{% endif %}
<div id="upgrade-banner" class="upgrade-banner">
<h2>Upgrade in Progress</h2>
<span id="upgrade-spinner" class="loading"></span>
<span id="upgrade-success" class="success" style="display: none;">✓</span>
<p id="upgrade-message">Please wait while WROLPi upgrades...</p>
<p class="elapsed">Elapsed: <span id="upgrade-elapsed">0:00</span></p>
</div>
<header>
<div>
<h1>Controller - {{ host }} WROLPi</h1>
<span class="version">v{{ version }}</span>
</div>
</header>
<main>
<section id="status">
<h2>System Status</h2>
@ -955,7 +988,8 @@
</button>
<button class="btn btn-info" onclick="showLogs('{{ service.name }}', true)">Logs</button>
{% if service.viewable and service.status == 'running' and service.port %}
<a href="{{ 'https' if service.use_https else 'http' }}://{{ host }}:{{ service.port }}{{ service.view_path|default('') }}"
{% set is_default_port = (service.use_https and service.port == 443) or (not service.use_https and service.port == 80) %}
<a href="{{ 'https' if service.use_https else 'http' }}://{{ host }}{% if not is_default_port %}:{{ service.port }}{% endif %}{{ service.view_path|default('') }}"
target="_blank" class="btn btn-violet">View</a>
{% endif %}
</td>
@ -1129,6 +1163,42 @@
<script>
/* ===== INLINE JAVASCRIPT - No external files ===== */
// Certificate trust detection.
(function() {
var host = window.location.hostname;
var port = window.location.port;
// Docker: 8080 → 8443; native: 80/empty → 443 (default).
var httpsPort = (port === '8080') ? ':8443' : '';
var httpsBase = 'https://' + host + httpsPort;
// Set the "Continue to WROLPi" link.
var continueBtn = document.getElementById('cert-continue-btn');
if (continueBtn) continueBtn.href = httpsBase;
// Set the "Proceed to WROLPi" and "View Files" links.
var proceedBtn = document.getElementById('proceed-btn');
if (proceedBtn) proceedBtn.href = httpsBase;
var viewFilesBtn = document.getElementById('view-files-btn');
if (viewFilesBtn) viewFilesBtn.href = httpsBase + '/media/';
// Skip cert check if user has permanently hidden the banner.
{% if hide_cert_banner %}return;{% endif %}
// Probe the HTTPS endpoint; show the cert banner if it fails.
var controller = new AbortController();
var timeout = setTimeout(function() { controller.abort(); }, 3000);
fetch(httpsBase + '/api/echo', { signal: controller.signal })
.then(function(r) {
// Cert is trusted — keep banner hidden.
})
.catch(function() {
// Cert not trusted or HTTPS unreachable — show the banner.
var banner = document.getElementById('cert-banner');
if (banner) banner.style.display = 'block';
})
.finally(function() { clearTimeout(timeout); });
})();
async function apiCall(url, method = 'GET', body = null) {
try {
const options = {method};
@ -2326,25 +2396,9 @@
}
// ===== UPGRADE MODE =====
// When ?upgrade=true, poll for completion and redirect to main UI when ready.
try {
if (isUpgradeMode) {
// Show upgrade banner
document.getElementById('upgrade-banner').classList.add('show');
// Track elapsed time
const startTime = Date.now();
const elapsedEl = document.getElementById('upgrade-elapsed');
const updateElapsed = () => {
const elapsed = Math.floor((Date.now() - startTime) / 1000);
const minutes = Math.floor(elapsed / 60);
const seconds = elapsed % 60;
elapsedEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
updateElapsed();
setInterval(updateElapsed, 1000);
// Poll controller's readiness endpoint (same-origin, avoids CORS/cert issues).
const upgradeIsDocker = {{ 'true' if docker_mode else 'false' }};
let upgradeHttpsBase;
if (window.location.protocol === 'https:') {
@ -2354,25 +2408,14 @@
} else {
upgradeHttpsBase = `https://${window.location.hostname}`;
}
// Two-phase upgrade detection:
// Phase 1: Wait for services to go DOWN (confirms upgrade actually started).
// Phase 2: Wait for services to come back UP (upgrade complete).
// State is persisted in sessionStorage so it survives page refreshes.
let upgradeServicesWentDown = sessionStorage.getItem('upgradeServicesWentDown') === 'true';
const markServicesDown = () => {
if (!upgradeServicesWentDown) {
upgradeServicesWentDown = true;
sessionStorage.setItem('upgradeServicesWentDown', 'true');
document.getElementById('upgrade-message').textContent = 'Upgrading... Services restarting.';
}
};
const checkUpgradeComplete = async () => {
try {
// WARNING: This must be a relative URL (no leading slash) so it resolves to
// /controller/api/ready behind Caddy, or /api/ready when accessed directly.
// An absolute '/api/ready' would be routed to Sanic by Caddy, not the Controller.
const resp = await fetch('api/ready', {
method: 'GET',
headers: {'Accept': 'application/json'},
@ -2381,36 +2424,27 @@
if (resp.ok) {
const data = await resp.json();
if (!upgradeServicesWentDown) {
// Phase 1: waiting for services to go down
if (!data.api || !data.app) {
markServicesDown();
upgradeServicesWentDown = true;
sessionStorage.setItem('upgradeServicesWentDown', 'true');
}
} else if (data.api && data.app) {
// Phase 2: services are back up — upgrade complete!
sessionStorage.removeItem('upgradeServicesWentDown');
document.getElementById('upgrade-spinner').style.display = 'none';
document.getElementById('upgrade-success').style.display = 'inline';
document.getElementById('upgrade-message').textContent = 'Upgrade complete! Redirecting...';
document.getElementById('upgrade-banner').classList.add('complete');
// Redirect to main app after 2 seconds
setTimeout(() => {
window.location.href = `${upgradeHttpsBase}/`;
}, 2000);
}
} else {
// Non-ok response means services are going down
markServicesDown();
upgradeServicesWentDown = true;
sessionStorage.setItem('upgradeServicesWentDown', 'true');
}
} catch (e) {
// Fetch failed (connection refused) — services are down
markServicesDown();
upgradeServicesWentDown = true;
sessionStorage.setItem('upgradeServicesWentDown', 'true');
}
};
// Check every 3 seconds (same as old maintenance page)
setInterval(checkUpgradeComplete, 3000);
// Also check immediately
checkUpgradeComplete();
}
} catch (e) {
@ -2595,7 +2629,9 @@
const protocol = service.use_https ? 'https' : 'http';
const host = window.location.hostname;
const viewPath = service.view_path || '';
html += `<a href="${protocol}://${host}:${service.port}${viewPath}" target="_blank" class="btn btn-violet">View</a>`;
const isDefaultPort = (protocol === 'https' && service.port === 443) || (protocol === 'http' && service.port === 80);
const portSuffix = isDefaultPort ? '' : ':' + service.port;
html += `<a href="${protocol}://${host}${portSuffix}${viewPath}" target="_blank" class="btn btn-violet">View</a>`;
}
html += '</td>';
@ -2628,6 +2664,33 @@
}
tbody.innerHTML = html;
// Show "Proceed to WROLPi" when the main UI is reachable.
var proceedBtn = document.getElementById('proceed-btn');
if (proceedBtn) {
// Check if caddy/web is running (native or Docker).
var appReady = services.some(function(s) {
return (s.name === 'caddy' || s.name === 'web') && s.status === 'running';
});
if (appReady) {
var host = window.location.hostname;
var port = window.location.port;
// Docker: 8080 → 8443; native: 80/empty → 443 (default).
var httpsPort = (port === '8080') ? ':8443' : '';
var httpsBase = 'https://' + host + httpsPort;
proceedBtn.href = httpsBase;
proceedBtn.style.display = 'inline-block';
var viewFilesBtn = document.getElementById('view-files-btn');
if (viewFilesBtn) {
viewFilesBtn.href = httpsBase + '/media/';
viewFilesBtn.style.display = 'inline-block';
}
} else {
proceedBtn.style.display = 'none';
var viewFilesBtn2 = document.getElementById('view-files-btn');
if (viewFilesBtn2) viewFilesBtn2.style.display = 'none';
}
}
} catch (e) {
console.error('Failed to update services:', e);
}

View file

@ -54,11 +54,6 @@ class TestDashboardEndpoint:
assert b"Controller" in response.content
assert b"WROLPi" in response.content
def test_dashboard_contains_version(self, test_client):
"""Dashboard should contain version."""
response = test_client.get("/")
assert __version__.encode() in response.content
def test_dashboard_contains_status_cards(self, test_client):
"""Dashboard should contain status cards."""
response = test_client.get("/")
@ -123,13 +118,6 @@ class TestDashboardEndpoint:
tbody_content = tbody_match.group(1)
assert 'id="boot-' not in tbody_content
def test_dashboard_has_upgrade_banner_element(self, test_client):
"""Dashboard should have upgrade banner element for upgrade mode."""
response = test_client.get("/")
content = response.text
assert 'id="upgrade-banner"' in content
assert "Upgrade in Progress" in content
def test_dashboard_has_mount_modal(self, test_client):
"""Dashboard should have mount modal for disk mounting."""
response = test_client.get("/")

View file

@ -38,16 +38,16 @@ class TestGetConfig:
assert "throttle" in config
def test_get_config_default_port(self, reset_runtime_config):
"""Default port should be 8087."""
"""Default port should be 80."""
config = get_config()
assert config["port"] == 8087
assert config["port"] == 80
class TestGetConfigValue:
"""Tests for get_config_value function."""
@pytest.mark.parametrize("key,expected", [
("port", 8087),
("port", 80),
("media_directory", "/media/wrolpi"),
("drives.auto_mount", True),
("hotspot.ssid", "WROLPi"),
@ -145,7 +145,7 @@ class TestReloadConfigFromDrive:
result = reload_config_from_drive()
assert result is False
# Should still have defaults
assert get_config_value("port") == 8087
assert get_config_value("port") == 80
def test_reload_handles_empty_file(
self, reset_runtime_config, mock_config_path
@ -158,7 +158,7 @@ class TestReloadConfigFromDrive:
# File exists and was processed, so returns True
assert result is True
# Config should still have all defaults
assert get_config_value("port") == 8087
assert get_config_value("port") == 80
class TestSaveConfig:
@ -210,7 +210,7 @@ class TestSaveConfig:
assert mock_config_path.exists()
# Reset to default value
update_config("port", 8087) # Default port
update_config("port", 80) # Default port
save_config()
# File should still exist (serves as marker that drive is set up)

View file

@ -14,8 +14,8 @@ class TestDefaultConfig:
assert isinstance(DEFAULT_CONFIG, dict)
def test_default_port(self):
"""Default port should be 8087."""
assert DEFAULT_CONFIG["port"] == 8087
"""Default port should be 80."""
assert DEFAULT_CONFIG["port"] == 80
def test_default_media_directory(self):
"""Default media directory should be /media/wrolpi."""
@ -66,7 +66,7 @@ class TestManagedServicesConfig:
{"systemd_name": "wrolpi-api-dev", "port": 8081, "viewable": True, "show_only_when_running": True}),
("wrolpi-app", {"port": 3000, "viewable": False}),
("wrolpi-app-dev", {"port": 3000, "viewable": False, "show_only_when_running": True}),
("caddy", {"port": 80}),
("caddy", {"port": 443}),
("postgresql", {"port": 5432, "viewable": False}),
("wrolpi-upgrade", {"port": None, "viewable": False, "show_only_when_running": True}),
])

View file

@ -67,7 +67,6 @@ mkdir -p /etc/caddy
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
mkdir -p /var/www
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
# WROLPi needs a few privileged commands.
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi

View file

@ -20,7 +20,7 @@ services:
dockerfile: docker/controller/Dockerfile
restart: unless-stopped
ports:
- "8087:8087"
- "8080:80"
volumes:
# Controller code (for development hot-reload)
- ./controller:/app/controller
@ -35,9 +35,9 @@ services:
- DOCKERIZED=true
- MEDIA_DIRECTORY=/media/wrolpi
# Override CMD to enable auto-reload for development
command: ["uvicorn", "controller.main:app", "--host", "0.0.0.0", "--port", "8087", "--reload", "--reload-dir", "/app/controller"]
command: ["python", "-m", "controller.serve"]
healthcheck:
test: [ 'CMD-SHELL', 'curl -f http://127.0.0.1:8087/api/health || exit 1' ]
test: [ 'CMD-SHELL', 'curl -f http://127.0.0.1:80/api/health || exit 1' ]
interval: 30s
timeout: 5s
start_period: 10s
@ -114,7 +114,6 @@ services:
context: .
dockerfile: docker/web/Dockerfile
ports:
- ${WEB_HOST-0.0.0.0}:${WEB_PORT-8080}:80
- ${WEB_HOST-0.0.0.0}:${WEB_HTTPS_PORT-8443}:443
- '8084:8084'
- '8085:8085'
@ -132,7 +131,7 @@ services:
# Example: EXTRA_SANS=IP:10.0.0.5
- EXTRA_SANS=${EXTRA_SANS:-}
healthcheck:
test: [ 'CMD-SHELL', 'wget --spider http://127.0.0.1:80 || exit 1' ]
test: [ 'CMD-SHELL', 'wget --spider --no-check-certificate https://127.0.0.1:443 || exit 1' ]
interval: 1m
zim: # kiwix-serve

View file

@ -20,17 +20,17 @@ Browse to https://localhost:8443
## Services
| Service | Port(s) | Description |
|----------------|--------------------------------------|-----------------------------|
| **web** | 8080 (HTTP), 8443 (HTTPS), 8084-8086 | Caddy reverse proxy |
| **api** | 8081 | Sanic API server |
| **app** | (via web) | React frontend |
| **controller** | 8087 | System management (FastAPI) |
| **db** | 5432 | PostgreSQL 12 |
| **archive** | 8083 | SingleFile/Readability |
| **zim** | (via web:8085) | Kiwix |
| **help** | (via web:8086) | MkDocs documentation |
| **map** | (via web:8084) | OpenStreetMap tile server |
| Service | Port(s) | Description |
|----------------|--------------------------|-----------------------------|
| **web** | 8443 (HTTPS), 8084-8086 | Caddy reverse proxy |
| **api** | 8081 | Sanic API server |
| **app** | (via web) | React frontend |
| **controller** | 8080 | System management (FastAPI) |
| **db** | 5432 | PostgreSQL 12 |
| **archive** | 8083 | SingleFile/Readability |
| **zim** | (via web:8085) | Kiwix |
| **help** | (via web:8086) | MkDocs documentation |
| **map** | (via web:8084) | OpenStreetMap tile server |
## TLS Certificates
@ -43,12 +43,13 @@ to pick up hostname and IP changes.
1. On first start, the `web` container generates a root CA at `/opt/media/config/ssl/`.
2. A leaf cert signed by the root CA is created at `/etc/ssl/caddy/` inside the container.
3. Caddy serves HTTPS on ports 443, 8084, 8085, and 8086 using the leaf cert.
4. Port 80 serves an HTTP landing page where users can download the root CA.
4. The Controller serves on port 80 with a certificate trust banner when needed.
### Trusting the certificate
1. Browse to `http://<host-ip>:8080` to see the landing page with download link and
platform-specific trust instructions.
1. Browse to `http://<host-ip>` to see the Controller. If the HTTPS certificate is not
yet trusted, a banner will appear with the download link and platform-specific
trust instructions.
2. Download and install the root CA on your device.
3. All HTTPS ports will work without browser warnings.
@ -94,7 +95,7 @@ docker compose up -d --build web
```
All devices will need to re-download and re-trust the new CA from
`http://<host-ip>:8080/ca.crt`.
`http://<host-ip>/ca.crt`.
## Volumes
@ -116,7 +117,6 @@ docker volume create --name=openstreetmap-rendered-tiles
| `MEDIA_DIRECTORY` | `/opt/media` | Media directory inside containers |
| `EXTRA_SANS` | (empty) | Additional SANs for the TLS leaf certificate |
| `WEB_HOST` | `0.0.0.0` | Host bind address for the web service |
| `WEB_PORT` | `8080` | HTTP port |
| `WEB_HTTPS_PORT` | `8443` | HTTPS port |
| `REACT_APP_API` | `0.0.0.0:8081` | API bind address |
| `UID` | `1000` | User ID for api/app containers |

View file

@ -23,8 +23,8 @@ ENV DOCKERIZED=true
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8087/api/health || exit 1
CMD curl -f http://localhost:80/api/health || exit 1
EXPOSE 8087
EXPOSE 80
CMD ["uvicorn", "controller.main:app", "--host", "0.0.0.0", "--port", "8087"]
CMD ["python", "-m", "controller.serve"]

@ -1 +1 @@
Subproject commit 5e9a4306585ea8e8099f7efd8411def97230a49f
Subproject commit 2c75fbb559f6c52aa294fe185e08358d55eab998

View file

@ -5,23 +5,6 @@
auto_https disable_redirects
}
:80 {
handle / {
root * /var/www
rewrite * /landing.html
file_server
}
handle /ca.crt {
root * /opt/media/config/ssl
header Content-Type "application/x-x509-ca-cert"
header Content-Disposition "attachment; filename=\"wrolpi-ca.crt\""
file_server
}
handle {
redir https://{host}:8443{uri} permanent
}
}
:443 {
tls /etc/ssl/caddy/cert.crt /etc/ssl/caddy/cert.key
@ -55,7 +38,7 @@
# Controller - system management API and UI.
handle_path /controller/* {
reverse_proxy controller:8087
reverse_proxy controller:80
}
handle /controller {
redir * /controller/ 301
@ -115,7 +98,7 @@
}
handle_errors 502 503 504 {
redir * "http://{host}:8087/?fallback=true" 302
redir * "http://{host}:8080/?fallback=true" 302
}
}

View file

@ -6,7 +6,6 @@ RUN apk add --no-cache openssl bash
COPY docker/web/Caddyfile /etc/caddy/Caddyfile
COPY docker/web/entrypoint.sh /entrypoint.sh
COPY scripts/generate_certificates.sh /opt/wrolpi/scripts/generate_certificates.sh
COPY etc/raspberrypios/landing.html /var/www/landing.html
RUN chmod +x /entrypoint.sh /opt/wrolpi/scripts/generate_certificates.sh

View file

@ -7,7 +7,7 @@
<p>The web proxy could not find WROLPi! Are your services running?</p>
<p>Start by viewing status of your services at the
<a id="controller_link" href="http://127.0.0.1:8087">Controller</a>
<a id="controller_link" href="http://127.0.0.1:80">Controller</a>
</p>
<pre><b>sudo systemctl status wrolpi-app</b></pre>
@ -87,7 +87,7 @@
replaceHref('api_link', `http://${hostname}:8081`);
replaceHref('map_link', `http://${hostname}:8084`);
replaceHref('kiwix_link', `http://${hostname}:8085`);
replaceHref('controller_link', `http://${hostname}:8087`);
replaceHref('controller_link', `http://${hostname}:80`);
} else {
console.error('Refusing to replace services links because window.location.hostname is empty');
}

View file

@ -5,23 +5,6 @@
auto_https disable_redirects
}
:80 {
handle / {
root * /var/www
rewrite * /landing.html
file_server
}
handle /ca.crt {
root * /media/wrolpi/config/ssl
header Content-Type "application/x-x509-ca-cert"
header Content-Disposition "attachment; filename=\"wrolpi-ca.crt\""
file_server
}
handle {
redir https://{host}{uri} permanent
}
}
:443 {
tls /etc/ssl/wrolpi/cert.crt /etc/ssl/wrolpi/cert.key
@ -55,7 +38,7 @@
# Controller - system management API and UI.
handle_path /controller/* {
reverse_proxy 0.0.0.0:8087
reverse_proxy 0.0.0.0:80
}
handle /controller {
redir * /controller/ 301
@ -115,7 +98,7 @@
}
handle_errors 502 503 504 {
redir * "http://{host}:8087/?fallback=true" 302
redir * "http://{host}/?fallback=true" 302
}
}

View file

@ -6,11 +6,7 @@
auto_https disable_redirects
}
:80 {
redir * http://{host}:8087/ 302
}
:443 {
tls /etc/ssl/wrolpi/cert.crt /etc/ssl/wrolpi/cert.key
redir * http://{host}:8087/ 302
redir * http://{host}/ 302
}

View file

@ -2,5 +2,5 @@
Encoding=UTF-8
Name=WROLPi Controller
Type=Link
URL=http://127.0.0.1:8087
URL=http://127.0.0.1
Icon=/opt/wrolpi/etc/raspberrypios/controller-logo.png

View file

@ -0,0 +1,7 @@
[Desktop Entry]
Type=Application
Name=Disk Management
Comment=Manage disks and partitions
Exec=gnome-disks
Icon=org.gnome.DiskUtility
Categories=System;

View file

@ -1,174 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to WROLPi</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #1a1a2e; color: #eee; min-height: 100vh;
display: flex; justify-content: center; padding: 2rem 1rem;
}
.container { max-width: 640px; width: 100%; }
h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #fff; }
.subtitle { color: #aaa; margin-bottom: 2rem; }
.card {
background: #16213e; border-radius: 12px; padding: 1.5rem;
margin-bottom: 1.5rem; border: 1px solid #1a3a5c;
}
.card h2 { font-size: 1.1rem; margin-bottom: 0.75rem; color: #4cc9f0; }
.card p { line-height: 1.6; color: #ccc; margin-bottom: 1rem; }
.btn {
display: inline-block; padding: 0.75rem 1.5rem; border-radius: 8px;
text-decoration: none; font-weight: 600; font-size: 1rem;
cursor: pointer; border: none; text-align: center;
}
.btn-primary { background: #4cc9f0; color: #1a1a2e; }
.btn-primary:hover { background: #3ab8df; }
.btn-continue { background: #2ecc71; color: #fff; width: 100%; font-size: 1.1rem; }
.btn-continue:hover { background: #27ae60; }
details { margin-bottom: 0.75rem; }
summary {
cursor: pointer; padding: 0.5rem 0; font-weight: 600;
color: #4cc9f0; list-style: none;
}
summary::before { content: "\25b6\00a0"; font-size: 0.8rem; }
details[open] > summary::before { content: "\25bc\00a0"; }
.instructions { padding: 0.75rem 0 0.25rem 0.5rem; color: #bbb; line-height: 1.7; }
.instructions ol { padding-left: 1.25rem; }
.instructions li { margin-bottom: 0.3rem; }
code { background: #0f3460; padding: 0.15rem 0.4rem; border-radius: 4px; font-size: 0.9em; }
#auto-redirect { display: none; text-align: center; color: #2ecc71; margin-bottom: 1rem; }
</style>
</head>
<body>
<div class="container">
<h1>Welcome to WROLPi</h1>
<p class="subtitle">Your offline library needs a one-time certificate setup.</p>
<div id="auto-redirect" class="card">
<p>Certificate already trusted! Redirecting...</p>
</div>
<div class="card">
<h2>Step 1: Download the Root CA Certificate</h2>
<p>Install this certificate on your device to trust all WROLPi HTTPS services permanently.</p>
<a class="btn btn-primary" href="/ca.crt" download="wrolpi-ca.crt">Download WROLPi Root CA</a>
</div>
<div class="card">
<h2>Step 2: Trust the Certificate</h2>
<p>Follow the instructions for your device:</p>
<details>
<summary>iOS / iPadOS</summary>
<div class="instructions">
<ol>
<li>Tap the download button above, then tap <strong>Allow</strong> when prompted.</li>
<li>Open <strong>Settings</strong> &rarr; <strong>General</strong> &rarr; <strong>VPN &amp; Device Management</strong>.</li>
<li>Tap <strong>WROLPi Root CA</strong> &rarr; <strong>Install</strong> &rarr; enter your passcode.</li>
<li>Go to <strong>Settings</strong> &rarr; <strong>General</strong> &rarr; <strong>About</strong> &rarr; <strong>Certificate Trust Settings</strong>.</li>
<li>Enable full trust for <strong>WROLPi Root CA</strong>.</li>
</ol>
</div>
</details>
<details>
<summary>Android</summary>
<div class="instructions">
<ol>
<li>Tap the download button above.</li>
<li>Open <strong>Settings</strong> &rarr; <strong>Security &amp; privacy</strong> &rarr; <strong>More security settings</strong> &rarr; <strong>Encryption &amp; credentials</strong> &rarr; <strong>Install a certificate</strong> &rarr; <strong>CA certificate</strong>.</li>
<li>Select the downloaded <code>wrolpi-ca.crt</code> file and confirm.</li>
</ol>
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Menu paths vary by manufacturer. If you can't find it, search for <em>"certificate"</em> in Settings.</p>
</div>
</details>
<details>
<summary>macOS</summary>
<div class="instructions">
<ol>
<li>Double-click the downloaded <code>wrolpi-ca.crt</code> file to open Keychain Access.</li>
<li>Add it to the <strong>System</strong> or <strong>login</strong> keychain.</li>
<li>Find <strong>WROLPi Root CA</strong> in the keychain, double-click it.</li>
<li>Expand <strong>Trust</strong>, set <em>When using this certificate</em> to <strong>Always Trust</strong>.</li>
<li>Close and enter your password to confirm.</li>
</ol>
</div>
</details>
<details>
<summary>Windows</summary>
<div class="instructions">
<ol>
<li>Double-click the downloaded <code>wrolpi-ca.crt</code> file.</li>
<li>Click <strong>Install Certificate</strong> &rarr; <strong>Local Machine</strong> &rarr; <strong>Next</strong>.</li>
<li>Select <strong>Place all certificates in the following store</strong> &rarr; <strong>Browse</strong> &rarr; <strong>Trusted Root Certification Authorities</strong>.</li>
<li>Click <strong>Finish</strong>.</li>
</ol>
</div>
</details>
<details>
<summary>Linux (Debian / Ubuntu)</summary>
<div class="instructions">
<ol>
<li>Copy the certificate (the <code>.crt</code> extension is required): <code>sudo cp wrolpi-ca.crt /usr/local/share/ca-certificates/</code></li>
<li>Update the trust store: <code>sudo update-ca-certificates</code></li>
<li>Restart your browser. Chromium-based browsers (Chrome, Edge) will use the system store automatically.</li>
</ol>
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Firefox on Linux does <em>not</em> use the system trust store by default &mdash; see the Firefox section below.</p>
</div>
</details>
<details>
<summary>Firefox (all platforms)</summary>
<div class="instructions">
<p style="margin-bottom:0.5rem;">Since Firefox 120, Firefox <strong>automatically trusts</strong> root certificates installed in the OS certificate store on <strong>Windows, macOS, and Android</strong>. If you've already installed the certificate above, Firefox should work with no extra steps on those platforms.</p>
<p style="margin-bottom:0.5rem;">On <strong>Linux</strong> (or if auto-trust is disabled), import manually:</p>
<ol>
<li>Open Firefox, go to <strong>Settings</strong> &rarr; <strong>Privacy &amp; Security</strong>.</li>
<li>Scroll to <strong>Certificates</strong> &rarr; click <strong>View Certificates</strong>.</li>
<li>In the <strong>Authorities</strong> tab, click <strong>Import</strong>.</li>
<li>Select the downloaded <code>wrolpi-ca.crt</code> file.</li>
<li>Check <strong>Trust this CA to identify websites</strong> &rarr; <strong>OK</strong>.</li>
</ol>
<p style="margin-top:0.5rem;font-size:0.9em;color:#999;">Alternatively, enable auto-trust: go to <code>about:config</code> and set <code>security.enterprise_roots.enabled</code> to <code>true</code>.</p>
</div>
</details>
</div>
<div class="card" style="text-align:center;">
<a class="btn btn-continue" id="continue-btn" href="#">Continue to WROLPi</a>
</div>
</div>
<script>
(function() {
var host = window.location.hostname;
var port = window.location.port;
// Docker maps HTTP 8080 → HTTPS 8443; native uses default 443.
var httpsPort = (port === '8080') ? ':8443' : '';
var httpsBase = 'https://' + host + httpsPort;
document.getElementById('continue-btn').href = httpsBase;
// Auto-redirect if cert is already trusted.
var controller = new AbortController();
var timeout = setTimeout(function() { controller.abort(); }, 3000);
fetch(httpsBase + '/api/echo', { signal: controller.signal })
.then(function(r) {
if (r.ok) {
document.getElementById('auto-redirect').style.display = 'block';
setTimeout(function() { window.location.href = httpsBase; }, 1500);
}
})
.catch(function() {})
.finally(function() { clearTimeout(timeout); });
})();
</script>
</body>
</html>

View file

@ -6,7 +6,7 @@ Before=wrolpi-api.service wrolpi-app.service postgresql.service
[Service]
Type=simple
ExecStart=/opt/wrolpi/controller/venv/bin/uvicorn controller.main:app --host 0.0.0.0 --port 8087
ExecStart=/opt/wrolpi/controller/venv/bin/python -m controller.serve
WorkingDirectory=/opt/wrolpi
User=root
Group=root

View file

@ -142,7 +142,7 @@ if [ -f /etc/systemd/system/wrolpi-controller.service ]; then
fi
fi
if curl -s http://0.0.0.0:8087/api/health --max-time 5 >/dev/null 2>&1; then
if curl -s http://0.0.0.0:80/api/health --max-time 5 >/dev/null 2>&1; then
echo "OK: WROLPi Controller health check passed"
else
echo "FAILED: WROLPi Controller health check failed"
@ -263,7 +263,6 @@ check_file /etc/ssl/wrolpi/cert.crt "TLS certificate exists" "TLS certificate do
check_file /etc/ssl/wrolpi/cert.key "TLS key exists" "TLS key does not exist at /etc/ssl/wrolpi/cert.key"
check_file ${MEDIA_DIRECTORY}/config/ssl/ca.crt "Root CA certificate exists" "Root CA certificate does not exist at ${MEDIA_DIRECTORY}/config/ssl/ca.crt"
check_file ${MEDIA_DIRECTORY}/config/ssl/ca.key "Root CA key exists" "Root CA key does not exist at ${MEDIA_DIRECTORY}/config/ssl/ca.key"
check_file /var/www/landing.html "Landing page exists" "Landing page does not exist at /var/www/landing.html"
if [ -f ${MEDIA_DIRECTORY}/config/ssl/ca.crt ] && [ -f /etc/ssl/wrolpi/cert.crt ]; then
if openssl verify -CAfile ${MEDIA_DIRECTORY}/config/ssl/ca.crt /etc/ssl/wrolpi/cert.crt >/dev/null 2>&1; then

View file

@ -30,7 +30,7 @@ from wrolpi.vars import PYTEST, DOCKERIZED
logger = logger.getChild(__name__)
CONTROLLER_SERVICE = 'http://controller:8087'
CONTROLLER_SERVICE = 'http://controller'
__all__ = [
'flag_outdated_zim_files',

View file

@ -23,6 +23,7 @@ gcc
gcc-doc
git
git-doc
gnome-disk-utility
gnupg-agent
gpsd
gpsd-tools

View file

@ -70,7 +70,6 @@ mkdir -p /etc/caddy
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
mkdir -p /var/www
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
# WROLPi needs a few privileged commands.
cp /opt/wrolpi/etc/raspberrypios/90-wrolpi /etc/sudoers.d/90-wrolpi

View file

@ -50,7 +50,6 @@ mkdir -p /etc/caddy
cp /opt/wrolpi/etc/raspberrypios/Caddyfile /etc/caddy/Caddyfile
mkdir -p /var/www
cp /opt/wrolpi/etc/raspberrypios/50x.html /var/www/50x.html
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
# Generate certificate for HTTPS.
/opt/wrolpi/scripts/generate_certificates.sh

View file

@ -129,5 +129,15 @@ if ! command -v smbd &>/dev/null; then
apt-get install -y samba
fi
# Install gnome-disk-utility if not already installed (provides desktop disk management GUI).
if ! command -v gnome-disks &>/dev/null; then
echo "Installing gnome-disk-utility..."
apt-get update
apt-get install -y gnome-disk-utility
fi
# Clean up stale landing page (Controller now serves port 80 directly).
rm -f /var/www/landing.html
# Install any configs, restart services.
/opt/wrolpi/repair.sh

View file

@ -399,7 +399,12 @@ def find_file(directory: pathlib.Path, name: str, depth=1) -> Optional[pathlib.P
if depth == 0:
return
for path in sorted(directory.iterdir()):
try:
entries = sorted(directory.iterdir())
except PermissionError:
return
for path in entries:
if path.is_file() and path.name == name:
return path
if path.is_dir() and (result := find_file(path, name, depth - 1)):
@ -1785,7 +1790,7 @@ async def aiohttp_head(url: str, timeout: int = None, headers: dict = None) -> A
yield response
CONTROLLER_URL = 'http://127.0.0.1:8087'
CONTROLLER_URL = 'http://127.0.0.1'
async def set_system_timezone(timezone: str):

View file

@ -3,9 +3,6 @@
set -x
# Copy landing page for HTTP certificate download.
cp /opt/wrolpi/etc/raspberrypios/landing.html /var/www/landing.html
mkdir -p /etc/caddy /etc/ssl/wrolpi
if [ -f /media/wrolpi/config/ssl/ca.crt ]; then