Live telemetry integration coming soon — chart containers are ready for data binding.
⚡
CPU Usage
Pending
CPU LOAD — AWAITING DATA
Nodes
pve01, pve02, pve03
Interval
30 s
🧠
Memory Usage
Pending
RAM UTILIZATION — AWAITING DATA
Nodes
pve01, pve02, pve03
Interval
30 s
💾
Storage
Pending
DISK CAPACITY — AWAITING DATA
Pools
local-lvm, ceph, nfs
Interval
5 min
🌐
Network I/O
Pending
NETWORK THROUGHPUT — AWAITING DATA
Interfaces
vmbr0, vmbr1
Interval
10 s
Andromeda — Home Lab Mission Control
A self-hosted infrastructure dashboard providing real-time telemetry, service monitoring, and secure access to homelab services.
Architecture
🌐Browser
▶
⚡Pages Function Proxy
▶
☁️Cloudflare Edge
▶
🔒Cloudflare Tunnel
▶
📦cloudflared LXC
▶
🖥️Proxmox VE API
Every API request from the browser is intercepted by a Cloudflare Pages Function running at the edge. The function authenticates the request using a session cookie, injects the Proxmox API token from a secret environment variable, and proxies the call through a Cloudflare Tunnel to a cloudflared LXC container running on the Proxmox node. The Proxmox VE API responds with live cluster data — the browser never sees the credentials.
Tech Stack
🟨
Vanilla JS
No framework — SPA router, DOM rendering, and fetch all hand-rolled.
⚡
Cloudflare Pages Functions
Edge-side API proxy — keeps credentials server-side and out of the browser.
🗄️
Cloudflare D1
SQLite at the edge — stores user accounts with SHA-256 hashed passwords.
🔒
Cloudflare Tunnel
Zero-trust inbound tunnel — no open ports on the home network.
🖥️
Proxmox VE API
RRD telemetry and cluster resource data sourced directly from the hypervisor.
📊
Chart.js
Lightweight canvas charts for CPU, memory, storage, and network I/O.
🌐
Tailscale
WireGuard mesh VPN — direct access to services from any device on the tailnet.
Features
🔑
Auth
Invite-code gated registration and session-cookie login. Passwords are hashed with SHA-256 via the Web Crypto API before storage in Cloudflare D1.
Troubleshooting Story
Login returned 200 but the page never changed. The HttpOnly flag on the session cookie made it invisible to JavaScript, so the router always saw the user as unauthenticated and silently redirected back to the login screen.
📡
Dashboard
Live service health grid showing all Proxmox LXC containers and VMs. Status dots pulse green for online, amber for standby, and grey for offline. Auto-refreshes every 30 seconds.
Troubleshooting Story
The service grid returned "missing API token" even though the environment variable was set. Debug logging revealed a trailing space in the variable name — the code looked up PROXMOX_TOKEN_VALUE but Cloudflare stored PROXMOX_TOKEN_VALUE . Invisible characters, real bug.
🔗
Quick Links
Clickable tiles for every self-hosted service — Nextcloud, Vaultwarden, Jellyfin, AdGuard, Matrix, and more. Each tile shows a live status dot sourced from the Proxmox API. Tailscale URLs are used for direct mesh access.
📈
System Overview
Four live Chart.js charts rendering one hour of RRD telemetry from the Proxmox node: CPU utilisation, memory usage, root disk capacity, and network I/O. Refreshes every 60 seconds.
Troubleshooting Story
Chart.js loaded from a CDN URL pointing to a nonexistent version. The CDN returned an HTML error page, the browser rejected it for MIME type mismatch, and the failed script tag cascaded — breaking every script that loaded after it, including the router and login.
Infrastructure
The homelab runs on a single Proxmox VE node hosting 11 LXC containers — each service isolated in its own container with dedicated resource limits. A dedicated cloudflared container maintains the outbound Cloudflare Tunnel, eliminating the need for any open inbound ports on the home network. Tailscale is installed on the node and key containers, providing a WireGuard-encrypted mesh network for direct device-to-service access from anywhere on the tailnet without routing through the public internet.
Projects & Progress
An active build log, not a finished portfolio. These are the things I'm shipping, debugging, and iterating on right now — documented as they evolve, rough edges and all.
Featured Build
📞
OpenLine
Missed-call recovery SaaS for service businesses.
Live
The Problem
A missed-call recovery SaaS for local service businesses — HVAC, plumbing, electrical. Full-stack build: Next.js App Router, Turso (libSQL) with Drizzle ORM, NextAuth, Stripe billing, deployed on Vercel with Twilio voice/SMS webhooks. Multi-tenant from the start — each business is its own workspace with its own phone number, leads, and conversation history.
Tech Stack
Next.jsTursoTwilioVercel
Current Status
Live webhook pipeline confirmed working end-to-end.
Toll-free SMS verification pending with Twilio.
Dashboard restyled to match the OpenLine brand.
Technical Highlights
Root-causing a silent Twilio dispatch failure
A test call completed successfully by every visible signal — Twilio logged it, billed it, and returned 200 on the status callback — yet the app's primary webhook never fired and no error appeared anywhere. I diagnosed it by cross-referencing three sources: Twilio's Request Inspector (only one webhook dispatched, not two), the app's database (no row created), and Twilio's Debugger (zero logged errors). That narrowed the failure to something happening before the app was ever involved. Confirming through Twilio's REST API showed the account was still in trial mode: trial accounts play an audio message requiring a keypress before dispatching to the voice webhook at all, and the test calls had been too short to trigger it. I verified the fix by sending a properly HMAC-signed synthetic request directly to the production endpoint — isolating the app's own correctness from Twilio's account-level gating before touching anything.
A misleading TypeScript/ESM footgun
One-off database scripts called dotenv.config() before importing the DB client, expecting sequential execution — but bundler import hoisting ran the DB client's module-level code (which reads process.env) before the config call ever executed, despite the source order looking correct. It manifested as a cryptic LibsqlError: URL_INVALID with no obvious connection to environment loading. I fixed it by using Node's native --env-file flag to load environment variables before the module graph evaluates at all — sidestepping the ordering issue entirely rather than patching around it.
Data-integrity cleanup across environments
A production phone number was found attached to a stale test workspace left over from earlier development — invisible from the live account because workspace scoping correctly hid it. I diagnosed it with a direct read-only query script against the production database, then fixed it with a small reassignment script rather than manual SQL, keeping the change scripted and reviewable.
Here's what else I'm building. Smaller write-ups than the featured build above — the shape of each project, its stack, and where it stands.
🌌
Andromeda
A self-hosted mission-control dashboard for a home Proxmox server — live system telemetry, quick links, and infrastructure overview, built to replace logging into Proxmox's own UI for routine checks.
Tech Stack
Cloudflare PagesWorkersD1Cloudflare Tunnel
Status
This is the site you're looking at right now.
Backend telemetry API secured behind Cloudflare Access.
Vanilla JS, no framework.
🏋️
MOG LOG
A training and nutrition tracker built around a specific 4-day upper/lower lifting program — logs sets with automatic double-progression (suggests the next weight once you hit top-of-range reps), tracks daily macros against targets, and turns daily weigh-ins into a weekly-average verdict instead of reacting to daily fluctuation.
Tech Stack
ReactCloudflare WorkersPWA
Status
Installable on Android/iOS home screen.
Works offline.
Live at moglog.timothypehns.tech.
🍽️
Sourced.
An early attempt at a consumer-facing app for restaurant food transparency — letting diners flag whether a restaurant was serving mass-produced, centrally-distributed food versus genuinely locally-sourced ingredients.
Tech Stack
Next.jsVercel
Status
Shelved.
Hit a core validation problem: there's no way for a customer to actually verify where a restaurant sources its food without inside access to the kitchen's supply chain — the entire premise required data no outside user could reliably provide. Kept as a documented lesson in validating the data model before building the product.