Add Enigma2 set-top box detection support#230
Conversation
The forced command in authorized_keys is what stops a stolen agent SSH key from getting a shell or running anything other than the allowed bbs-ssh-gate dispatch (borg serve / catalog-write / ping). That key path is solid. The remaining gap was on the local-filesystem side: authorized_keys was previously owned by the bbs-* user (mode 600). If something else gave an attacker shell as that user (web compromise, sudo mistake, etc.), they could rewrite their own authorized_keys and remove the forced command, escalating from 'no shell, ever' to 'full account access on next login'. Now authorized_keys is chowned to root:root after writing. sshd happily reads root-owned authorized_keys, and a local-user shell can no longer rewrite the file. The .ssh directory itself stays user-owned at 700 so legitimate user-level housekeeping still works. Three places updated: - bin/bbs-ssh-helper create-user (new user provisioning) - bin/bbs-ssh-helper update-all-keys (post-upgrade regen) - bin/bbs-update-run (run-on-update permission audit) Existing installs pick this up on the next bbs-update run.
The split-pane art is hidden on phone widths, leaving the form
pane untethered from any branding cue. Add a small standalone
logo above the form on mobile only, ~88px square.
Source priority:
1. branding_icon (the small navbar logo) if set — sized for
a small slot already, looks right at 88px.
2. /images/favicon.png as the default — the new mascot icon.
3. Deliberately NOT branding_login_logo — that artwork is meant
to fill a 500px column and would crowd a phone-width pane.
authView() in Core/Controller.php now also pulls branding_icon
from settings and exposes it as $brandingIcon to the auth view.
The CSS hides the new block on desktop (display: none), and the
existing "<992px" breakpoint shows it above the form.
Stock Ubuntu sshd with StrictModes refuses root-owned authorized_keys in a user's home — privsep can't read it as the authenticating user, auth fails with 'Permission denied (publickey)'. Every bbs-* user on falcon was locked out after the previous change ran in the periodic permission audit. Reverting both call sites and restoring bbs-update-run's chown so existing bad ownership gets fixed on the next 'bbs-update' run. The forced-command + 'restrict' directive inside authorized_keys is the security control — user-owned mode 600 is the standard supported configuration for that pattern. The earlier 'defense in depth' framing was misguided; reverting.
Two issues with the topbar logo flagged in marcpope#223: 1. The PNG was 1536x1024 — 2.4MB of payload to display at 115px wide. Resized to 400px wide (fits 4x retina without going overboard) and stripped/recompressed: 2.4MB -> 82KB (~30x smaller). Display CSS unchanged so the topbar looks the same. 2. Apache wasn't sending Cache-Control on static assets, so browsers issued a conditional GET (with If-Modified-Since) on every page load. On a home server with low upload bandwidth that's noticeable. Added Cache-Control headers via mod_headers in public/.htaccess: 7-day cache for images and fonts, 1-day for css/js (which the layout already cache-busts via ?v=<mtime> when changed).
…pope#223) The 'Backup completed with warnings' notifications were drowning the activity log because two routine, unactionable signals were tripping had_warnings on every backup: 1. SSH's 'Permanently added <host> to the list of known hosts' line fires on every connection because the agent uses UserKnownHostsFile=/dev/null (intentional — avoids stale-key issues across Docker rebuilds). borg captures that line as a WARNING-level log_message, which was promoting every backup to 'completed with warnings'. 2. 'file changed while we backed it up' / 'file vanished' / 'stat: [Errno 2] No such file or directory' are routine on active systems (live mysql, mailspools, log rotation) and aren't actionable. Fix: - Add '-o LogLevel=ERROR' to all four BORG_RSH constructions (BorgCommandBuilder agent path, RemoteSshService runtime + helper, ClientController download). Suppresses the SSH known-hosts notice at the source — borg never sees it as stderr, never logs it. - Agent: classify warning lines as routine vs actionable. If every warning matches a routine pattern, downgrade to a clean 'completed' (no had_warnings, no notification, no warning_log). If even one line is genuinely actionable, behavior is unchanged. Bumps AGENT_VERSION to 2.29.8. Also fixes long unbreakable file paths overflowing the message column on /notifications by adding word-break/overflow-wrap to the cell.
Server Health card (marcpope#225 follow-up): - Fixed-width lbl column (95px) and val column (60px) so every progress bar starts and ends at the same x-coordinate across rows. Was: lbl auto-sized 50–140 + val min-width 80 → bar width drifted between CPU vs /mnt/truenas rows, plus a chunk of dead whitespace between bar end and short values. - Smaller text (lbl 0.82→0.72rem, val 0.78→0.72rem, progress 0.72→0.66rem) so the row reads tighter. - Bar height 1rem → 0.95rem to match the new type. - Row spacing tightened (margin-bottom 8→6, gap 5→8). Schedules page colors: - bbs_agent_color() saturation 55 → 32 and lightness 45 → 38. Per-agent hues stay distinct (137° spacing) but the blocks no longer pop bright against the dark navy of the rest of the app.
Branding tab:
- Navbar Icon copy now says it doubles as the small mobile login
header logo (the bigger Login Page Logo would crowd a phone
pane).
- New 'App Icon / Favorite Icon' upload — single 512x512
transparent PNG that BBS resizes on the fly to every favicon /
apple-touch / PWA size we need. Recommendation surfaced in
the field copy.
- Login Page Theme selector re-enabled (was commented out while
the dark layout was the only option).
Backend:
- New BrandingController::icon($size) handler at
/branding/icon/{size}. Reads branding_app_icon from settings;
if set, decodes + GD-resizes the source preserving alpha,
caches the resized PNG to /var/bbs/cache/branding/, serves
it. If unset, falls through to the bundled
/public/images/favicon-NxN.png. Sizes 16/32/48/96/180/192/512
allowed.
- SettingsController::saveBranding() handles
remove_branding_app_icon and branding_app_icon_data.
- app.php and auth.php layouts now reference /branding/icon/N
for every <link rel='icon'> + apple-touch-icon, so a custom
upload propagates everywhere automatically.
Light login theme:
- Auth layout now respects $defaultTheme (the existing
branding_login_theme override was already wired through
Core/Controller::authView, just no longer hardcoded to
'dark').
- New [data-bs-theme='light'] block: white form pane, dark
text, retoned inputs and focus rings; art pane keeps its
navy radial since the mascot artwork is composed for deep
blue and looks washed out lifted onto white. Divider on the
art pane stays so the split still reads. Binary-stream
background hidden on light.
…ill in light
Card headers were inconsistent between pages — settings used
bg-primary bg-opacity-10 (light blue), clients used bg-body
(transparent), dashboard/schedules/archive-detail used
card-head-gradient (dark navy). In light mode in particular this
read as three different palettes on three pages.
- Standard .card-header is now a subtle neutral surface in both
themes (#f1f3f5 light / #1f2429 dark), so any plain
<div class='card-header'> looks the same everywhere.
- .card-head-gradient and .sched-accent-header keep the dark
navy gradient ONLY in dark mode; in light mode they collapse
to the same neutral header so the page palette stays cohesive.
- Swept the codebase: 11 view files had card-header bg-primary
bg-opacity-10 (settings, queue/detail, profile, users/edit,
notification-services, storage-locations, etc.) or card-header
bg-body (clients/index, clients/detail). Stripped both
overrides; the new base style takes over.
- bg-warning / bg-danger bg-opacity-10 headers stay — those
convey state, not visual hierarchy.
Sidebar version pill (V2.50.1) was using text-body-secondary on
the dark navy sidebar. In light mode that token resolves to a
midtone gray and was nearly illegible. New .sidebar-version-pill
class hardcodes light-on-dark colors regardless of theme, since
the sidebar background is always dark.
Per UX feedback the previous ~16px headers felt too heavy. Single property added to .card-header in style.css — applies universally across dashboard, schedules, settings, and every other card, regardless of theme. Icons scale proportionally via em inheritance.
Storage Locations page (/storage-locations):
- The three sections (Local Storage, Remote SSH, S3 Offsite Sync)
were floating <h5> titles that didn't match the rest of the
app's card-header pattern. Each is now wrapped in a card with
a proper card-header (title + section action button on the
right where applicable). Inner item cards stay as-is.
- Top section renamed Local Storage Locations -> Local Storage
per request.
Table headers, site-wide:
- Were ALL CAPS, 0.85rem, with 0.5px letter-spacing — distinctly
heavier than card headers above them. Switched to mixed case
(text-transform: none) at 0.875rem to match the new
card-header size set in the previous turn. Same change applied
to the dashboard's .v2 .table thead override (was 0.72rem
uppercase).
Topbar:
- Section name (page title) shrunk from 1.25rem -> 1.125rem on
desktop, 1.1rem -> 1rem on mobile (~2px each).
- Username dropdown gets an explicit 0.875rem (was inheriting
Bootstrap's 1rem btn-link). Matches the user's '~2px smaller'
request and aligns with the new card/table type scale.
The /upgrade page renders through authView() because it needs a self-contained layout that doesn't depend on the rest of the app (navbar/sidebar can fail mid-deploy). After the login redesign that meant it inherited the split-pane mascot artwork on the left, which dominated the frame and pushed the upgrade log into a narrow scrolling sliver per the screenshot in marcpope#222. Add a hideAuthArt flag the controller can pass: - UpgradeController sets it to true. - auth.php skips the art pane, the binary-stream background, and the mobile header logo when it's set. - body.auth-no-art rule lets the form pane fill the entire frame width (max-width 1000px) so the upgrade output gets proper reading room. Login pages (login, 2fa, forgot-password, reset-password) don't pass the flag, so they keep the full split-pane treatment.
Bootstrap's <h3> default of 1.75rem (28px) was way too big for the client detail page header — the name dwarfed everything else around it. Inline override to 18px keeps it as a heading-weight title without dominating the panel.
Client detail status tab (#fix from request):
- Replaced the list-group Recent Activity with a proper
table-responsive table: Status | Task | Repo | Time | Date.
- Bumped from 10 -> 20 rows (the table format is denser, so
we have room).
- 14px / 0.875rem text, mixed weight (header bold, body normal),
consistent with the rest of the app.
- Failed-job error_log moved off-row into the status icon's
title attribute so the table stays clean.
- text-truncate on the Repo column with title for long names.
- table-responsive lets the columns wrap horizontally on
mobile.
Site-wide tables:
- Added .table { font-size: 0.875rem } so all table BODY cells
(not just <th>) match the 14px ceiling. Was inheriting
Bootstrap's 1rem default, which made queue/clients/log
pages all read heavier than their card-header above.
The Active & Queued table and the four hero tiles (Clients,
Running, Recovery Points, Errors) were a one-shot snapshot at page
render — the user had to refresh to see jobs queue, run, or
complete. Server Health was the only thing auto-updating.
- New endpoint /dashboard/active-json — trimmed payload (agent
counts, running/queued counts, error count, active-job list)
so the 10s poll is cheap. No charts, no recent jobs, no
archive totals.
- JS polls every 10s and updates: tile-agent-count, online/
offline, running/queued, error count + the danger/success
color class on the Errors tile when it crosses zero.
- Active & Queued table re-renders from the JSON, preserving
the row click-through to /queue/<id> and the inline progress
bar for running jobs.
- 'Recovery Points' tile is intentionally NOT in the fast poll
— it's archive totals that drift slowly and aren't worth
re-querying every 10s. It still updates on page reload.
The .card-head-gradient on Dashboard / Schedules / Archive Detail and .sched-accent-header on Schedules were the only remaining gradients in the app. Per design feedback they read as out of place. All three plus the standard .card-header now use a flat #1e293b (the darkest stop from the prior gradient) in dark mode, so every card header on every page is visually identical. Table <th> in dark mode also picks up the same #1e293b background so a table on a page reads as a continuation of the card header above it — no contrast jump between card-header and the table beneath.
Per design feedback. Updated four matching rules so every dark-mode header (and table <th>) shares one color: - [data-bs-theme=dark] .card-header background #202b3f - [data-bs-theme=dark] .card-head-gradient background #202b3f - [data-bs-theme=dark] .sched-accent-header background #202b3f - [data-bs-theme=dark] .table th background #202b3f Border on each is rgb(24 24 25). Card body background dropped from #212529 to #111214 for stronger separation from the header band.
Per design feedback. Was 0.875rem font / Bootstrap's 0.5rem cell padding — rows packed too loose for the data density on the queue, clients, and dashboard tables. Single override in style.css; applies site-wide across every table.
Adds detection for Enigma2-based set-top boxes (e.g., Dreambox) by checking for characteristic system files and extracting distribution information from multiple sources. Refactors OS detection variables to use local scope, reducing the global namespace footprint. Variables are now declared as local within the detection function and properly passed to output. Implements a fallback chain for Enigma2 detection: first attempts OE-Alliance standard format, then OpenATV-specific format, and finally uses Python's boxbranding module for more robust detection across various firmware images. Co-authored-by: Copilot <copilot@github.com>
marcpope
left a comment
There was a problem hiding this comment.
Thanks for this @MegaV0lt — nice idea, and the local-variable cleanup is a good catch.
Two issues I'd want fixed before merging:
1. Your new check runs too late.
You added the Enigma2 detection as an elif after [ -f /etc/os-release ]. The problem is that most modern Enigma2 images (OpenATV, OpenPLi, etc.) do ship /etc/os-release — so the first branch matches and your new code never runs on those boxes.
Please move the Enigma2 block above the /etc/os-release check. Checking /proc/stb/info/model is cheap, so doing it first is fine.
2. Your fallback chain doesn't actually fall back.
You initialize local distro='Unknown', then in the OE-Alliance block:
distro=$(grep "^creator=" /etc/image-version 2>/dev/null | cut -d= -f2 | tr -d '\r')If /etc/image-version exists but has no creator= line, grep returns nothing and distro becomes an empty string "" — not the original "Unknown". Your two fallback ifs then test [ "$distro" = "Unknown" ], which is false against "", so the OpenATV and boxbranding fallbacks never run.
Easiest fix:
if [ -z "$distro" ] || [ "$distro" = "Unknown" ]; then(in both fallback ifs).
Smaller stuff while you're in there:
- The embedded Python uses an f-string (
f'{getImageDistro()} ...') which needs Python 3.6+. Older Enigma2 images can ship 3.4/3.5. Errors are swallowed silently, so the only symptom is an emptyDetected:line — switching to.format()would be a bit safer. - Trailing spaces on the
filine.
Once those are sorted I'll get it merged. Thanks again for the contribution!
Reorders the OS detection logic to check for Enigma2-based set-top boxes before standard Linux detection methods. This ensures devices with unique Enigma2 environments are properly identified rather than being incorrectly categorized by generic OS detection. Updates Enigma2 distro detection to use empty string checks instead of "Unknown" comparisons for more reliable validation. Replaces f-string formatting with % formatting in the Python snippet to improve compatibility with older Python versions that may be present on Enigma2 devices.
|
Thank you for the review @marcpope. |
Summary
Adds detection for Enigma2-based set-top boxes (e.g., Dreambox) by checking for characteristic system files and extracting distribution information from multiple sources.
Refactors OS detection variables to use local scope, reducing the global namespace footprint. Variables are now declared as local within the detection function and properly passed to output.
Implements a fallback chain for Enigma2 detection: first attempts OE-Alliance standard format, then OpenATV-specific format, and finally uses Python's boxbranding module for more robust detection across various firmware images.
This is only for display in the agent installer script so far. No functional change
Changes
Testing