mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-25 18:40:06 -04:00
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:
parent
c316a178f9
commit
e0324ce0fc
33 changed files with 302 additions and 407 deletions
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}/>)}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
11
controller/serve.py
Normal 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")
|
||||
|
|
@ -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> → <strong>General</strong> → <strong>VPN & Device Management</strong>.</li>
|
||||
<li>Tap <strong>WROLPi Root CA</strong> → <strong>Install</strong> → enter your passcode.</li>
|
||||
<li>Go to <strong>Settings</strong> → <strong>General</strong> → <strong>About</strong> → <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> → <strong>Security & privacy</strong> → <strong>More security settings</strong> → <strong>Encryption & credentials</strong> → <strong>Install a certificate</strong> → <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> → <strong>Local Machine</strong> → <strong>Next</strong>.</li>
|
||||
<li>Select <strong>Place all certificates in the following store</strong> → <strong>Browse</strong> → <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 → <strong>Settings</strong> → <strong>Privacy & Security</strong>.</li>
|
||||
<li>Scroll to <strong>Certificates</strong> → <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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("/")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
etc/raspberrypios/gnome-disks.desktop
Normal file
7
etc/raspberrypios/gnome-disks.desktop
Normal 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;
|
||||
|
|
@ -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> → <strong>General</strong> → <strong>VPN & Device Management</strong>.</li>
|
||||
<li>Tap <strong>WROLPi Root CA</strong> → <strong>Install</strong> → enter your passcode.</li>
|
||||
<li>Go to <strong>Settings</strong> → <strong>General</strong> → <strong>About</strong> → <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> → <strong>Security & privacy</strong> → <strong>More security settings</strong> → <strong>Encryption & credentials</strong> → <strong>Install a certificate</strong> → <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> → <strong>Local Machine</strong> → <strong>Next</strong>.</li>
|
||||
<li>Select <strong>Place all certificates in the following store</strong> → <strong>Browse</strong> → <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 — 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> → <strong>Privacy & Security</strong>.</li>
|
||||
<li>Scroll to <strong>Certificates</strong> → 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> → <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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
3
help.sh
3
help.sh
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ gcc
|
|||
gcc-doc
|
||||
git
|
||||
git-doc
|
||||
gnome-disk-utility
|
||||
gnupg-agent
|
||||
gpsd
|
||||
gpsd-tools
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue