From e0324ce0fce1c75fa5d9af4bc6c0609af5aa263a Mon Sep 17 00:00:00 2001 From: Roland Date: Tue, 7 Apr 2026 20:01:49 -0600 Subject: [PATCH] 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. --- app/src/DashboardPage.js | 2 +- app/src/components/Nav.js | 6 +- app/src/components/Vars.js | 6 +- app/src/contexts/contexts.js | 2 +- controller/defaults.py | 13 +- controller/main.py | 30 +- controller/serve.py | 11 + controller/templates/index.html | 291 +++++++++++------- controller/test/test_api.py | 12 - controller/test/test_config.py | 12 +- controller/test/test_defaults.py | 6 +- .../hooks/normal/9999-wrolpi.hook.chroot | 1 - docker-compose.yml | 9 +- docker/README.md | 32 +- docker/controller/Dockerfile | 6 +- docker/help/wrolpi-help | 2 +- docker/web/Caddyfile | 21 +- docker/web/Dockerfile | 1 - etc/raspberrypios/50x.html | 4 +- etc/raspberrypios/Caddyfile | 21 +- etc/raspberrypios/Caddyfile.onboarding | 6 +- etc/raspberrypios/controller.desktop | 2 +- etc/raspberrypios/gnome-disks.desktop | 7 + etc/raspberrypios/landing.html | 174 ----------- etc/raspberrypios/wrolpi-controller.service | 2 +- help.sh | 3 +- modules/zim/lib.py | 2 +- pi-gen/stage2/04-wrolpi/01-packages-nr | 1 + pi-gen/stage2/04-wrolpi/03-run-chroot.sh | 1 - repair.sh | 1 - scripts/upgrade.sh | 10 + wrolpi/common.py | 9 +- wrolpi/scripts/first_startup.sh | 3 - 33 files changed, 302 insertions(+), 407 deletions(-) create mode 100644 controller/serve.py create mode 100644 etc/raspberrypios/gnome-disks.desktop delete mode 100644 etc/raspberrypios/landing.html diff --git a/app/src/DashboardPage.js b/app/src/DashboardPage.js index abbbbf78..b4215fca 100644 --- a/app/src/DashboardPage.js +++ b/app/src/DashboardPage.js @@ -441,7 +441,7 @@ export function DashboardPage() { - + diff --git a/app/src/components/Nav.js b/app/src/components/Nav.js index 23c40a9e..d47497ac 100644 --- a/app/src/components/Nav.js +++ b/app/src/components/Nav.js @@ -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() { ; return <> - + {homeLink} @@ -279,7 +279,7 @@ export function NavBar() { - + {homeLink} {links.map(i => )} diff --git a/app/src/components/Vars.js b/app/src/components/Vars.js index 2b3169dd..6c895a4e 100644 --- a/app/src/components/Vars.js +++ b/app/src/components/Vars.js @@ -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. diff --git a/app/src/contexts/contexts.js b/app/src/contexts/contexts.js index 6a960c9a..973a0f97 100644 --- a/app/src/contexts/contexts.js +++ b/app/src/contexts/contexts.js @@ -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(); diff --git a/controller/defaults.py b/controller/defaults.py index 33438ec2..b585a1b9 100644 --- a/controller/defaults.py +++ b/controller/defaults.py @@ -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 { diff --git a/controller/main.py b/controller/main.py index 41077450..db2a9d79 100644 --- a/controller/main.py +++ b/controller/main.py @@ -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} diff --git a/controller/serve.py b/controller/serve.py new file mode 100644 index 00000000..be56ccf5 --- /dev/null +++ b/controller/serve.py @@ -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") diff --git a/controller/templates/index.html b/controller/templates/index.html index 101d4265..70cf4263 100644 --- a/controller/templates/index.html +++ b/controller/templates/index.html @@ -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 @@ {% endif %} +
+
+

Controller - {{ host }} WROLPi

+
+
+ + + + {% if not drive_mounted and not docker_mode %}
@@ -839,6 +881,12 @@
{% endif %} +{% set app_ready = services | selectattr('name', 'in', ['caddy', 'web']) | selectattr('status', 'equalto', 'running') | list | length > 0 %} + +
Main UI is unavailable. Using Controller fallback. Will redirect when ready... @@ -854,21 +902,6 @@
{% endif %} -
-

Upgrade in Progress

- - -

Please wait while WROLPi upgrades...

-

Elapsed: 0:00

-
- -
-
-

Controller - {{ host }} WROLPi

- v{{ version }} -
-
-

System Status

@@ -955,7 +988,8 @@ {% if service.viewable and service.status == 'running' and service.port %} - View {% endif %} @@ -1129,6 +1163,42 @@ - - diff --git a/etc/raspberrypios/wrolpi-controller.service b/etc/raspberrypios/wrolpi-controller.service index 57ae116a..3465cb71 100644 --- a/etc/raspberrypios/wrolpi-controller.service +++ b/etc/raspberrypios/wrolpi-controller.service @@ -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 diff --git a/help.sh b/help.sh index 8accd3ce..a62414d0 100755 --- a/help.sh +++ b/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 diff --git a/modules/zim/lib.py b/modules/zim/lib.py index 4c855bd6..2e791410 100644 --- a/modules/zim/lib.py +++ b/modules/zim/lib.py @@ -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', diff --git a/pi-gen/stage2/04-wrolpi/01-packages-nr b/pi-gen/stage2/04-wrolpi/01-packages-nr index 60bbb059..f1754fda 100644 --- a/pi-gen/stage2/04-wrolpi/01-packages-nr +++ b/pi-gen/stage2/04-wrolpi/01-packages-nr @@ -23,6 +23,7 @@ gcc gcc-doc git git-doc +gnome-disk-utility gnupg-agent gpsd gpsd-tools diff --git a/pi-gen/stage2/04-wrolpi/03-run-chroot.sh b/pi-gen/stage2/04-wrolpi/03-run-chroot.sh index 9cb33813..b20965c8 100755 --- a/pi-gen/stage2/04-wrolpi/03-run-chroot.sh +++ b/pi-gen/stage2/04-wrolpi/03-run-chroot.sh @@ -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 diff --git a/repair.sh b/repair.sh index 2f15106a..a088ef26 100755 --- a/repair.sh +++ b/repair.sh @@ -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 diff --git a/scripts/upgrade.sh b/scripts/upgrade.sh index 54d1c8c6..e5721642 100755 --- a/scripts/upgrade.sh +++ b/scripts/upgrade.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 diff --git a/wrolpi/common.py b/wrolpi/common.py index 66f10caf..c6fa9d24 100644 --- a/wrolpi/common.py +++ b/wrolpi/common.py @@ -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): diff --git a/wrolpi/scripts/first_startup.sh b/wrolpi/scripts/first_startup.sh index f86fb070..64fca063 100755 --- a/wrolpi/scripts/first_startup.sh +++ b/wrolpi/scripts/first_startup.sh @@ -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