Skip to content
SLOT-0056 | 2U RACK

Real-time Monitoring for AdGuard Home Sync: A Production Enhancement

Reading Time
5 min
~200 words/min
Word Count
906
2 pages
Published
Oct 5
2025
Updated
Oct 31
2025

Table of Contents

Reading Progress 0%

When you’re running high-availability DNS across a Proxmox cluster, you need to trust that your configuration stays synchronized. I use AdGuard Home Sync to keep my primary and replica DNS servers in sync every 5 minutes, but I had a problem: how do I know it’s actually working without SSH-ing into the server to check logs?

So I built real-time monitoring for it. Took about 6 hours from start to production deployment.

The Problem: No Visibility into Sync Status

I run a 4-node Proxmox cluster at home for DNS, media services, and development environments. AdGuard Home is my DNS solution, and AdGuard Home Sync handles the synchronization between instances perfectly. It’s a solid Go application that syncs configurations on a schedule.

But there was a gap: no real-time visibility.

Want to know if syncs are running? SSH into the server and grep logs:

journalctl -u adguardhome-sync | grep "Sync done"

That doesn’t scale. I needed a dashboard.

The Solution: Live Monitoring API and UI

I built two pieces:

  • Backend (Go): REST API endpoint with sync schedule metadata
  • Frontend (JavaScript): Real-time countdown timers and progress bar

Now I can see sync status at a glance without touching a terminal.

AdGuard Home Sync web interface showing real-time sync progress monitoring with countdown timers and progress bar in dark mode
The enhanced interface showing real-time sync status, countdown timers, and visual progress indication in dark mode

Backend: Exposing Sync Schedule Data

The existing codebase already tracked sync times in the worker struct. I just needed to expose it through an API. The key was making zero breaking changes.

I added a struct to track sync operations:

type syncSchedule struct {
    LastSyncTime    time.Time `json:"lastSyncTime"`
    NextSyncTime    time.Time `json:"nextSyncTime"`
    SyncRunning     bool      `json:"syncRunning"`
    CronExpression  string    `json:"cronExpression,omitempty"`
    IntervalSeconds int       `json:"intervalSeconds"`
}

The new GET /api/v1/sync-schedule endpoint returns this data:

{
  "lastSyncTime": "2025-10-05T17:55:02.828704495-04:00",
  "nextSyncTime": "2025-10-05T18:00:00-04:00",
  "syncRunning": false,
  "cronExpression": "*/5 * * * *",
  "intervalSeconds": 300
}

The intervalSeconds field is calculated dynamically from the cron expression using robfig/cron/v3, so it works with any schedule—every 5 minutes, every hour, whatever.

Frontend: Real-time Updates Without Framework Bloat

The project already used jQuery and Bootstrap. No reason to add React or Vue when you don’t need it.

The frontend polls on two intervals:

  • Every 30 seconds: Fetch fresh data from the API
  • Every 1 second: Update countdown timers and progress bar

Progress bar calculation is straightforward:

const timeSinceSync = (Date.now() - lastSyncTime) / 1000;
const progress = Math.min((timeSinceSync / intervalSeconds) * 100, 100);

I also threw in dark mode with localStorage persistence. The UI works seamlessly in both themes:

Close-up of real-time sync progress indicator showing countdown timers in light theme
Light theme progress indicator
Close-up of real-time sync progress indicator showing countdown timers in dark theme
Dark theme progress indicator

Debugging a 2-Pixel Layout Shift

After implementing the theme toggle, I noticed the “Trigger Sync” button was shifting position when switching themes. Just a couple pixels, but it bugged me.

I used Playwright to measure exact element positions in both themes:

  • Light mode <h1> height: 64px
  • Dark mode <h1> height: 66.2px

Bootstrap’s light and dark themes have different line-height values for headings. Fixed it with CSS normalization:

h1 {
  line-height: 1.2 !important;
  margin: 0 !important;
  padding: 0.5rem 0 !important;
}

Sometimes you just need to measure things instead of guessing.

Production Deployment: Same Day

From idea to production: about 6 hours.

  • Planning & codebase review: 1 hour
  • Backend implementation: 2 hours
  • Frontend implementation: 2 hours
  • Debugging layout shift: 30 minutes
  • Documentation: 30 minutes

Deployed to my Proxmox cluster as a systemd service. The compiled Go binary is lightweight:

  • Memory usage: ~6.3 MB
  • CPU usage: <1% average
  • Sync frequency: 288 syncs/day (every 5 minutes)
  • Average sync duration: ~1.4 seconds
  • Uptime: 100% since October 5, 2025

It’s been rock-solid managing critical DNS infrastructure.

AdGuard Home Sync web interface in light mode showing complete feature set
Live production deployment showing real-time sync logs and status

Why Fork Instead of Upstream Contribution?

I forked bakito/adguardhome-sync (Apache 2.0 license) and created my own fork with proper attribution. Why not contribute upstream?

Simple: I needed this feature today.

  • Upstream contribution takes time—review, discussion, iterations
  • My implementation works for my use case but might need refinement
  • Forking is what open source is for—anyone can enhance tools for their needs

I documented everything in FORK.md and IMPLEMENTATION.md. If the maintainer wants to discuss a pull request, I’m happy to. But I’m equally comfortable maintaining this independently.

What I Learned

A few things stood out from this project:

Read the code first. I spent an hour understanding the architecture before writing anything. Worth it—I knew exactly where to make changes without breaking things.

Work with what’s there. The project used jQuery and Bootstrap. Adding React would have been pointless. Use the existing ecosystem unless you have a good reason not to.

Dependencies are liabilities. Every new dependency is a potential security issue and maintenance burden. I built this without adding a single library.

Document as you go. I created FORK.md, IMPLEMENTATION.md, and screenshot guides. Future me will appreciate it.

Production validates everything. Local testing is fine, but nothing proves a feature works like running it in production. Since October 5th, this has monitored 288 DNS syncs per day without issues.

Wrapping Up

This project shows what you can do when you focus on solving actual problems instead of over-engineering. I enhanced a mature tool, deployed it same-day, and now have real-time visibility into my DNS infrastructure.

Nothing exotic here—Go, JavaScript, jQuery, Bootstrap. No revolutionary approach—REST API, polling, client-side calculations. But it works, it’s maintainable, and it’s running in production.

That’s what matters.

Project Links

If you’re working on similar infrastructure challenges or have questions about Go/JavaScript development, feel free to reach out.

user@eddykawira:~/comments$ ./post_comment.sh

# Leave a Reply

# Note: Your email address will not be published. Required fields are marked *

LIVE
CPU:
MEM: