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