My NAS had been throwing file errors on the cache NVMes for weeks. Not the kind you can ignore. Real BTRFS corruption, one unclean shutdown away from a full pool rebuild. I ran every diagnostic I could think of. Reseated the drives. Swapped SATA/M.2 slots. Ran btrfs scrub more times than I want to admit. Every test came back pointing at the drives, but two brand-new NVMes throwing identical errors is not a drive problem. It’s a motherboard problem. The board was quietly corrupting writes on the way to the M.2 slot, and I’d been chasing symptoms for a month.

Prime Day landed a decent-but-not-great discount on the UGREEN DXP4800 Pro. Decent-but-not-great because AI training has been eating every NVMe and DDR module the industry can make, and the flow-through to consumer prices is real. The days of a Prime Day NAS discount making you feel clever are, at least for now, over. I’d been eyeing UGREEN NASes for a while. Small, quiet, Intel iGPU for hardware transcoding, and the community had confirmed Unraid boots on them without heroics. Pulled the trigger. The migration went shockingly clean: shut down the old box, moved the array drives and both cache NVMes to the UGREEN, plugged in the same Unraid USB, powered on. Docker came up. Shares came up. A few hours later the whole thing was back to 100%. Not the interesting part of the story.

The interesting part started when I sat down with a coffee and looked at the front of the NAS. Four disk LEDs, a network LED, and a power LED. All just sitting there. The disk LEDs were solid white. Network LED did some kernel-driven flicker on link. Power LED was static. This is a NAS. It’s supposed to tell me things.

Quick search turned up ich777’s unraid-ugreenleds-driver plugin. If you’ve spent any time in the Unraid community you know ich777. The person is a one-human plugin factory, keeping half the third-party ecosystem alive (NVIDIA driver, Coral TPU, AMD Vendor Reset, this UGREEN driver, and many more). Their plugin here does exactly what it says: ships the led-ugreen kernel module and a daemon that lights up disk LEDs on I/O. Rock-solid foundation. The bits I was reaching for on top (array state on the power LED, network activity as something more expressive than a kernel-driven flicker, a settings page instead of an SSH-edited config file) just weren’t the goal of the upstream project. That’s a fork opportunity, not a complaint. I wanted the LEDs to mean things, and ich777 had done all the hard work of getting me a stable substrate to build meaning on top of.

I didn’t know how Unraid plugins worked. Asked Claude.

This is a fork of ich777’s UGREEN LED driver plugin that adds semantic behavior on top of the existing daemon: the power LED becomes an array-health indicator, the network LED breathes at a speed proportional to traffic, and the disk LEDs turn into a progress bar during parity checks. The kernel module, the disk mapping, the LED sysfs plumbing (the actually hard parts) are still ich777’s and miskcoo’s work. This fork adds a meaning layer on top plus a proper settings UI so you don’t have to SSH in to change a color.

Why This Exists

The problem: UGREEN NASes ship with a nice front-panel LED array that Unraid has no idea how to drive. The one existing plugin gives you decoration, not information.

Existing solutions didn’t fit:

  • ich777’s unraid-ugreenleds-driver: The reason any of this is possible. Ships the led-ugreen kernel module, maps disks to LEDs, blinks on I/O. But the daemon reads a config file (edit over SSH), power LED is unmanaged, network LED is kernel-driven flicker, and there’s no notion of array state.
  • miskcoo’s ugreen_dx4600_leds_controller: The upstream C++ controller. Same substrate, same limitation — it’s a driver, not an information display.
  • Manual sysfs pokes: echo 1 > /sys/class/leds/disk1/shot works but that’s not a system, that’s a one-off.
  • BIOS/vendor firmware LEDs: UGREEN’s stock firmware has its own animations. Doesn’t run under Unraid.

The gap: A layer on top of the driver that turns idle LEDs into an ambient status display — array health, network activity, parity progress, UPS state — and a webGUI to configure it.

The goal: Look at the NAS from across the room and know if it’s fine, warned, or on fire. Look during a parity check and see progress without opening the webGUI. Never edit a config file over SSH again.

The Plan

Here’s the implementation plan Claude produced after I described the goal and pointed it at the upstream repo. I’d never written an Unraid plugin, so the “how Unraid plugins actually work” section of the plan was as valuable as the code.


Plan: ugreen-leds-plan.md

UGREEN LEDs Fork — Implementation Plan

Overview

Fork of ich777/unraid-ugreenleds-driver. Extend the existing bash daemon with four coordinated LED behaviors, add a dynamix-native settings page under Settings → Utilities → UGREEN LEDs, package it as a proper .plg that installs cleanly and migrates existing configs.

Tech Stack

ToolPurpose
Bash 5LED daemon (extends existing script)
PHP 8Settings page (Unraid runs its webGUI in nginx+PHP)
HTML + dynamix CSSSettings UI, matches native Unraid look
sysfs /sys/class/leds/LED control (kernel led-ugreen module already handles the hardware)
/var/local/emhttp/*.iniRead array/disk state from Unraid’s own emhttp cache
apcupsdUPS status for on-battery alert
Slackware .txzPackage format Unraid uses for plugin installs
.plg manifestXML file with install/uninstall scripts and <FILE> entries

How Unraid Plugins Actually Work

The whole thing runs off a .plg XML manifest that packages up <INSTALL> / <REMOVE> bash scripts and pointers to .txz payloads. Config lives at /boot/config/plugins/<name>/ (persists on the USB); the webGUI files live at /usr/local/emhttp/plugins/<name>/ which is on tmpfs and wiped every reboot, so the .plg re-installs them from /boot on each boot. A .page file registers a menu entry (small header, --- separator, PHP/HTML body), and if you want the page to look native you match Unraid’s dynamix CSS conventions or it reads like a 2005 admin panel.

Project Structure

unraid-ugreenleds-driver/
├── ugreenleds-driver.plg              # XML manifest, INSTALL script, <FILE> entries
├── source/
│   └── usr/
│       ├── bin/
│       │   └── ugreen-leds            # bash daemon (extended)
│       └── local/emhttp/plugins/ugreenleds-driver/
│           ├── ugreenleds-driver.page # Settings page (dynamix header + PHP)
│           └── include/
│               ├── apply.sh           # Restart daemon on Apply
│               └── preview.php        # Live color preview endpoint
└── packages/
    └── ugreenleds-driver-YYYY.MM.DD.txz  # Slackware package built from source/

Key Decisions

  1. Fork, don’t rewrite. ich777’s daemon already handles disk mapping, the kernel module, and the boring plumbing. Extend it. Credit them.
  2. Four subshell processes for the four behaviors — array health, netdev breathing, parity progress, UPS alert — coordinate via flag files in /var/run/ugreen-leds.{alert,parity}. Priority: alert > parity > normal. Each subshell checks the flags before writing.
  3. Read array state from /var/local/emhttp/disks.ini, not by calling mdcmd. The Main page shows red/yellow/green dots; parse the same file it does. Match the GUI by construction rather than reimplementing the logic.
  4. Netdev breathing at 25fps — read /sys/class/net/*/statistics/{tx,rx}_bytes, compute a rate, map to a period. Faster traffic = faster breath.
  5. Parity progress as a bar chart across disk LEDs — read progress from mdResync in /proc/mdstat, light 0–N disk LEDs to represent 0–100%, leave the current-progress LED breathing.
  6. UPS alert takes over everything — if apcupsd reports ONBATT, blink all LEDs red and set a flag. When power restores, restore state in place (don’t kill/restart the daemon, or you lose the disk-activity trigger state).
  7. Dynamix-native settings page — plain PHP writing KEY="value" back to settings.cfg. Live color preview writes sysfs directly on <input type="color"> change; on page leave, revert.

Implementation Phases

  1. Understand the plugin format: Read the upstream .plg, /boot/config/plugins/, and one of ich777’s plugins that already has a settings page (nvidia-driver) as a reference.
  2. Extend the daemon: Add night-mode window, netdev breathing subshell, array-health subshell, parity-progress subshell, UPS-alert subshell. Flag-file coordination.
  3. Settings page: .page file with dynamix header + PHP. Sections for Disk / Network / Night Mode / Alerts. Live color preview via a tiny AJAX endpoint (preview.php).
  4. Apply script: Kill daemon, remove old cron entries, regenerate night-mode cron from settings, restart daemon via at now -M.
  5. .plg manifest: Update <FILE> entries for the new files, add migration logic (add new settings keys to existing settings.cfg without clobbering user values), bump the version.
  6. Package: makepkg a fresh .txz, MD5 it, drop into packages/.
  7. Test on the actual NAS: install from the local .plg URL, verify menu appears under Settings → Utilities, verify LEDs actually behave.

Verification

  • Plugin installs from raw GitHub URL and appears under Plugins
  • Settings page renders at Settings → Utilities → UGREEN LEDs with all sections
  • Power LED goes green with a healthy array, amber if a disk is warning, red if a disk is red or the array won’t start
  • Netdev LED breathes faster while iperf3 runs, slower when idle
  • Start a parity check → disk LEDs turn into a filling bar
  • Yank UPS power → all LEDs blink red until power restores, then return to normal without the daemon restarting

How Claude Code Actually Worked

The morning was research first. I described what I wanted the LEDs to do and pointed Claude at ich777’s repo. It came back with the “How Unraid Plugins Actually Work” section that’s in the plan above — the .plg XML, the tmpfs-vs-USB split, the .page header format, the fact that dynamix has its own CSS conventions and if you ignore them your page looks foreign. None of that was obvious from the upstream README, and figuring it out from scratch would have eaten the whole day.

The plan above was already written on Saturday — I’d sketched it out, gone through what settings would need dropdowns vs sliders vs color pickers, and estimated half a day. Sunday morning I sat down and Claude turned the plan into code. The .plg manifest and the settings page were mechanical. The daemon extensions took the actual thought — four subshells that all want to write to the same LEDs need a coordination story, and “priority alert > parity > normal via flag files in /var/run” was the first clean answer that stayed clean across restart, kill -9, and UPS-restore edge cases.

The single commit that shipped this: 1,048 line changes across the daemon, the .page file, the apply script, the .plg manifest, the README, and a fresh .txz package. One commit because I tested the whole thing end-to-end on the live NAS before pushing — install from local .plg, verify each behavior, uninstall, reinstall from GitHub, verify migration from the upstream config. When it all worked, I pushed.

Total: ~4 hours on Sunday (plus the plan on Saturday). Most of it was the daemon coordination logic. The PHP was assembly.

Results

Build time: One day (research + plan Saturday, ~4 hours build Sunday)

First run: Installed from the local .plg, refreshed the webGUI, and the settings page was there under Settings → Utilities → UGREEN LEDs. Toggled each feature on from the UI and confirmed the daemon picked up the change: netdev LED switched to breathing, power LED painted the array-health color, disk LEDs went into activity-only mode. That’s enough to know the plumbing works. The behaviors that need the real world to happen — the SATA failure, the multi-hour parity check, the UPS on-battery — those live under “Looking forward” below.

Looking forward to seeing it work in the wild: the plugin only landed today, so the interesting moments haven’t happened yet — the first amber that catches a SMART warning before I’ve opened the webGUI, the first monthly parity check where the disk LEDs march left-to-right over hours, the first time I glance at the NAS from across the room and clock the state without a browser tab. That’s what the LEDs are for now. I’ll know they’re earning their keep the first time one of them tells me something I would otherwise have missed.

What shipped vs the plan:

The plan was: settings UI + night mode + configurable invert. What shipped added: array-health on power LED, netdev breathing, parity progress bar, and UPS-on-battery alert with in-place state restore. All four semantic behaviors landed in the daemon during the same afternoon. The plan’s scope was “make it configurable”; the actual scope became “make it mean something.” Once the settings page existed, adding one more toggle for a new subshell was cheap.

Limitations:

  • Only tested on the DXP4800 Pro. Other supported UGREEN models (DXP6800, DXP2800, etc.) should work — same LED sysfs interface — but I can’t verify.
  • This plugin is Unraid-only. The .plg manifest, the dynamix UI framework, /boot/config/plugins/, the /var/local/emhttp/disks.ini health source — none of that exists on TrueNAS or OpenMediaVault, and porting the plugin isn’t the right shape of work. The plan above is the transferable artifact: hand it to Claude with “do the same thing on TrueNAS Scale” or “OMV 7,” ask it to research how that OS’s plugin/service system works, and let it rewrite the daemon and settings page against the equivalent primitives. The subshell coordination and the sysfs LED interface carry over unchanged.
  • Parity progress bar is coarse (4 disks = 25% resolution). Fine.
  • No test coverage. It’s a bash script that talks to sysfs. I test it by looking at the front of the NAS.

What’s next: Nothing. LEDs mean things now. I’m publishing this because I think it’s useful, not because I’m signing up to be an Unraid plugin maintainer. Anyone who wants variations (a different NAS OS, a different array-state source, a different LED behavior) should fork or branch it and go.


Tech stack: Bash 5, PHP 8, dynamix CSS, sysfs LED interface, led-ugreen kernel module (upstream), apcupsd Build time: 1 day (plan Saturday, build ~4 hours Sunday) Lines of code: ~1,050 added on top of the upstream fork Credit: ich777 for the driver plugin, miskcoo for the underlying LED controller work