m4nifest0-tech

Search the site

Automation

A hidden CSS bug in my homelab dashboard: the unclosed comment

Rebuilding the self-hosted dashboard with real data, a binary copy-paste that gets corrupted, and an unclosed CSS comment that silently disables an element.
Published on Updated on 5 min readLevel: Intermediate

A self-hosted dashboard (homepage, in the list of self-hosted services) gathers the links and service status of the homelab on a single page. The public repository for mine is a template with placeholders ({{VARIABILE}}, example domains): this guide covers what it took to bring it back to real data, two technical problems I ran into along the way, and why I then added a second tool, Glance, instead of replacing it.

From the public template to real data

The downloadable repository contains the structure (configuration files for services, settings, widgets, bookmarks) but not the real values, for obvious security reasons. To repopulate it I first had to recover the actual configuration from the LXC container that runs the dashboard: get into it, find where the configuration lives (Docker bind mount or a direct path) and read it. From there, I rewrote every file with the real list of services — backup, photo management, uptime checks, DNS with filtering, reverse proxy — keeping only structure and placeholders in the public repository, never the real addresses.

The copy-paste problem: truncated binary data

Transferring plain text from one terminal to another is trivial; transferring an icon (a PNG encoded in base64) is much less so. A first attempt with base64 -w0 (output on a single line) arrived cut off: out of roughly 5,600 expected characters, only 646 came through. The cause was not the encoding but the transport: a single very long line was truncated on its way between the terminal and the client.

The immediate fix was trivial — repeat the encoding without -w0, so the output breaks into several shorter lines and the copy-paste arrives intact. For a heavier icon (a 280 KB avatar), though, even that was not enough: the solution was to read the image directly from the browser already authenticated on the live dashboard, extract its bytes via canvas and rebuild the file locally, finally checking that size and file signature matched the original byte for byte.

Note

For small transfers, a detail like line wrapping makes the difference between a valid file and one that is corrupted with no obvious error: it is worth always checking the size of the file that arrived against the expected one, not just that "something" arrived.

The real bug: an unclosed CSS comment

The dashboard had a watermark in the logo that no longer showed up. No console errors, no logs: the custom CSS (custom.css) simply no longer applied the rule. Inspecting the element live (getComputedStyle on the logo's ::before pseudo-element), the content property was none and position was static: as if that rule had never been written.

The cause was a CSS comment opened and never closed, higher up in the file: everything that came after — including the whole body::before rule that drew the watermark — had been silently swallowed by the comment. Only a second rule stayed active, inside a media query for small screens, which modified an element that no longer had its base definition: no syntax error, no warning, just a piece of the interface gone.

The mistake: a forgotten closing brace or asterisk
/* some legitimate note
.logo-wrapper { ... }        /* <- this line closes the wrong comment */
body::before { content: ""; /* NEVER executed: it stays inside the open comment */
  position: fixed;
  ...
}

An unclosed CSS comment does not raise an error: it silences everything that follows, up to the next */ it meets (even if that was not the intended one). It is one of the sneakiest bugs because the browser reports nothing: the page loads, styles are applied, only one piece is missing. The only way to find it was to compare the live file with a copy rebuilt byte for byte and to read the CSS line by line, backwards from the point where the rule should have applied.

From the cluster inventory to a second dashboard

With direct access to the Proxmox interface, building an accurate inventory of the cluster became easier: querying the cluster APIs returns nodes, virtual machines and containers with the status and address of each. Services assigned via DHCP, however, change address at every reboot unless they are powered off: for the stopped ones the address is not known until they are started again, a limit to keep in mind before relying on an address "discovered" once (better a DHCP reservation or a static address for everything a dashboard has to reach reliably).

From that inventory came the configuration of a second tool, Glance, meant at first to cover only the services actually running at the moment, with a commented-out block ready for the stopped services, to be completed when they are brought back up with a stable address. It does not replace homepage: at this stage of the homelab several dashboards coexist while I evaluate which one to keep.

The lesson

Two practical lessons, useful beyond this specific case: first, when a binary data transfer seems to have "succeeded" but the result behaves strangely, always check the file's size and signature before looking for the bug elsewhere. Second, an interface that stops working with no errors in the console is a strong hint of a silent syntax problem — in CSS, an unclosed comment — not a logic problem: it has to be found by reading the file, not by debugging the behavior.

Related articles