DEV Community

linou518
linou518

Posted on

Building a Homelab Dashboard in One File — The Design and Dilemmas of a Single-file SPA

Building a Homelab Dashboard in One File — The Design and Dilemmas of a Single-file SPA

2026-03-29 | techsfree-web


When building a homelab dashboard, the first question is what stack to use. Go proper with React + Vite? Consider SSR with Next.js? Or… put everything in a single HTML file?

We went with the last option. The result: index.html surpassed 1,100 lines.


Why Single-file SPA?

The reason is simple: deployment is a single cp command.

Anyone who's run something in a "production" environment — even a home server — knows the panic when the build pipeline breaks. node_modules missing, unlocked dependencies, wrong paths… every time that happens, you think: "does this dashboard really need all that?"

Run Flask as a backend, serve the HTML from /. Load Vue and Chart.js from a CDN. That's a fully functional SPA. The lightness of managing it with a single systemd service is ideal for a homelab running 24/7.


Keeping 1,000+ Lines Maintainable

The problem is scale. Every new feature bloats the file. Before I knew it, index.html had crossed 1,100 lines.

The reason it's still maintainable: organizing code by view.

<!-- ===== VIEW: overview ===== -->
<div id="view-overview" class="view">
  ...
</div>

<!-- ===== VIEW: nodes ===== -->
<div id="view-nodes" class="view">
  ...
</div>
Enter fullscreen mode Exit fullscreen mode

The JavaScript follows the same pattern — a showView(name) function switches between all views. The active view gets display: block, everything else display: none. No router needed.

function showView(name) {
  document.querySelectorAll('.view').forEach(v => v.style.display = 'none');
  document.getElementById(`view-${name}`).style.display = 'block';
  localStorage.setItem('dashboardCurrentView', name);
  // Initialize each view
  if (name === 'nodes') loadNodes();
  if (name === 'overview') loadOverview();
}
Enter fullscreen mode Exit fullscreen mode

localStorage preserves the current view across page reloads. Subtle, but useful.


The Flask Integration Pattern

The backend is Flask. Define simple API endpoints, call them from the frontend with fetch(). That's it.

@app.route('/api/nodes-status')
def api_nodes_status():
    # Retrieve node status via SSH
    ...
    return jsonify(result)
Enter fullscreen mode Exit fullscreen mode

Frontend:

async function loadNodes() {
  const res = await fetch('/api/nodes-status');
  const data = await res.json();
  renderNodes(data);
}
Enter fullscreen mode Exit fullscreen mode

No CORS configuration needed — it runs on the same origin. That simplicity is the strength of the single-file approach.


When Complexity Creeps In

When bot creation and deletion required 16 steps of async processing, I started feeling the limits of the single-file SPA.

Step progress display, error handling, choosing between SSE and polling… I ended up solving all of it within the "one file" constraint.

The solution: a modal dialog with a log display area, appending each step's result to a <div>. No WebSocket, no SSE — just fetch + 120ms delay animations. It creates enough of a real-time feel.


Things I Deliberately Dropped

  • Build step: Unnecessary. CDN is fine.
  • TypeScript: Autocomplete would be nice, but writing type definitions in a single file has little upside.
  • Component architecture: The Vue SFC temptation was real, but SFCs need a build step. Settled for template literals.
  • Tests: …I haven't written any. Sorry.

When to Graduate

Honestly, the single-file SPA will hit its limits eventually. If my dashboard gets another 2-3 views, I'll probably consider splitting it.

My rough thresholds:

  • When index.html exceeds 2,000 lines
  • When multiple people start editing it
  • When I have the bandwidth to set up a CI/CD pipeline

Until then, I'll enjoy the simplicity of "it runs in one file." Homelab should be fun.


Summary

Aspect Single-file SPA Modern Framework
Deployment Single cp Build pipeline required
Dependency management CDN only node_modules to manage
Dev experience Simple, no type hints Full IDE support
Scale ceiling ~2,000 lines Unlimited
Best for Personal / homelab Team dev / production

"Can you use this in production?" — honestly, questionable. But for a homelab where you're the only user and you fix it yourself when it breaks, this tradeoff is entirely rational.

Top comments (0)