All notable changes to Easy DevOps are documented here. Format follows Keep a Changelog.
- New named channel system — Discord webhooks and Telegram bots are registered once in Settings → Notifications with a name and UUID. Channels are stored in SQLite under
notification_channelsand reused across any number of domains and event types. - Settings → Notifications tab — settings page split into General and Notifications tabs. The Notifications tab lists all configured channels with Test / Edit / Delete actions per row.
- Channel CRUD API — five new endpoints under
/api/settings/channels:GETlist all channelsPOSTcreate a channel (generates UUID, validates type-specific fields)PUT /:idupdate name or credentials (type is immutable after creation)DELETE /:idremove a channelPOST /:id/testsend a live test notification to a single channel
- Discord message prefix — optional "Message Prefix" field on Discord channels. Value is sent as the
contentfield of the webhook (not inside the embed), so role mentions like<@&RoleId>are resolved by Discord correctly.
- Domain edit form gains a Notifications accordion section showing two event types:
cert_expiry(SSL certificate expiry) anddomain_health(backend port check). Each event has an enable toggle and a channel checkbox list. nginx_downis intentionally absent from per-domain config — it is a global event and is routed using the union of all configured channel IDs across all domains.- Per-domain config stored in SQLite under
domain_notification_config. Default is enabled with no external channels (dashboard-only). - API:
GET /api/domains/:name/notificationsandPUT /api/domains/:name/notifications. - Domain list and create/update responses now include a
notificationsfield.
shouldSendNotification(domain, eventType, currentStatus)incore/domainNotifier.jsimplements a state machine that prevents repeated alerts for the same condition.- First check: records baseline state, sends no notification.
- Down: sends alert only on the
up → downtransition. - Still down: no repeated notification.
- Recovery: sends a "back online" notification on the
down → uptransition. - State persisted in SQLite under
notification_state— survives server restarts.
createNginxRecoveredEvent()incore/events.js—severity: 'success', used when nginx comes back up.createDomainDownEvent(domain, port)andcreateDomainRecoveredEvent(domain, port)— carry the backend port in the message for clarity.- Both Discord and Telegram render recovery events with distinct messaging from down events.
- Bell icon in the navbar with an unread badge counter.
- Socket.io
notification:newevents for: nginx status, SSL cert expiry (within 30 days), domain health. - Sliding notification panel with severity colouring and timestamps. "Clear all" button. Badge resets when the panel is opened.
- Deduplication by event
id— replacing the same event in place instead of appending duplicates.
- New sidebar page. Streams
tail -fover Socket.io forerror.logandaccess.log. - Toggle between logs with a button group; auto-scroll to newest line; Pause/Resume.
- Colour-coded output: red for
[error]/[crit], yellow for[warn], green for 2xx/304. - Max 500 lines buffered; cleared on log switch.
- Server polls each domain's
backendHost:portvia HTTP HEAD every 60 seconds. - Live green
● Up/ red● Downbadges in the domains table. - External URL backends (starting with
http) are skipped. - Health check failures emit
notification:newto the browser panel and dispatch to configured external channels.
- Export:
GET /api/settings/backup— downloads config and domains as JSON. Dashboard password excluded. - Import:
POST /api/settings/restore— restores config and domains from a backup file. Confirmed via SweetAlert2 dialog.
core/events.js— addedcreateNginxRecoveredEvent(),createDomainDownEvent(domain, port),createDomainRecoveredEvent(domain, port). ExistingcreateDomainHealthEventkept as a legacy wrapper. All event IDs are now unique per emission (timestamp suffix) to prevent false deduplication.socketManager.js— nginx and domain health handlers rewritten to useshouldSendNotification()for state-aware dispatch.getAllDomainChannelIds(eventType)used for global events (nginx_down) instead of a null-domain lookup that always returned empty.core/notifier.js— rewritten to the named-channel model.sendNotification(event, channelIds)resolves each UUID to a channel object and dispatches to the matchingchannels/{type}.jsmodule. Empty or nullchannelIdsis a no-op (dashboard-only path).core/domainNotifier.js— replaced type-name arrays with UUID arrays (channelIds). AddedgetAllDomainChannelIds(eventType)(union across all domains) andshouldSendNotificationstate machine.- Discord channel — message prefix routed to webhook
contentfield instead of embed description, enabling Discord mention resolution. - Routes refactored into controllers —
dashboard/routes/domains.jsanddashboard/routes/settings.jsextracted intodashboard/controllers/domainsController.jsanddashboard/controllers/settingsController.js. - Settings page — split into "General Settings" and "Notifications" tabs via
settingsTabstate.
- Random session secret — generated once via
crypto.randomBytes(32), stored in SQLitesession_secretkey. - Request body size limit —
express.json({ limit: '1mb' }). - Login rate limiting —
express-rate-limitonPOST /login: max 10 attempts per 15 minutes.
- Domain health notifications never fired —
emitDomainHealthonly emitted thedomain:healthsocket event for UI badges; it never calledsendNotificationor emittednotification:new. Both are now called for every down domain. - nginx_down never reached external channels —
getEffectiveChannelIds(null, 'nginx_down')always returned[]because anulldomain falls back to defaults which have emptychannelIds. Replaced withgetAllDomainChannelIds('nginx_down')which unions channel IDs from all domain configs. - CLI detection spam —
runDetection()now caches results for 60 seconds. Subprocesses (nginx -v,npm --version, etc.) no longer run on every main menu return. - Sessions survive restarts —
session-file-storepersists sessions to disk. - Vue data() return object truncated — a duplicate closing brace in
app.jscausedpermissions,backup,channels,channelModal, andnotificationsto be declared outside thedata()return object, making them non-reactive. Fixed by removing the extra brace.
- Root cause:
data/easy-devops.sqlitewas stored inside the npm package directory (src/core/../../data/). When runningnpm install -g easy-devopsto update, npm replaces the entire package folder, deleting the database and all user configuration (domains, passwords, paths, ACME email). - Fix: The database is now stored in a persistent user-level directory that npm never touches:
- Linux/macOS:
~/.config/easy-devops/easy-devops.sqlite - Windows:
%APPDATA%\easy-devops\easy-devops.sqlite
- Linux/macOS:
- Migration: On first run after updating, Easy DevOps automatically detects an existing database at the old package-relative location and copies it to the new path. The old file is renamed to
easy-devops.sqlite.migratedso the migration only runs once. No manual action required.
core/db.js—DATA_DIRnow resolves to the user home config directory instead of the package-relativedata/folder.osmodule imported for cross-platform home directory detection.
- Root cause:
nginx -tonly reads world-readable config files (/etc/nginx/nginx.confis 644). It was never necessary to run it withsudo. All sudo prefixes have been removed from everynginx -tcall acrosscli/managers/nginx-manager.js,cli/managers/domain-manager.js,dashboard/routes/domains.js, anddashboard/lib/nginx-service.js. - Why it was breaking: The dashboard runs headless (no TTY), so
sudo nginx -ttriggered "a terminal is required to read the password" → the test was always reported as failed even with valid configs.
- Root cause: When run as a non-root user,
nginx -twrites "syntax is ok" to stderr then attempts to write/run/nginx.pid— which requires root. This causes nginx to exit with code 1 even though the config is perfectly valid. - Fix:
isNginxTestOk(result)added tocore/platform.js. Returnstrueifresult.successOR if the output contains"syntax is ok". All 11 nginx config-test result checks now use this function instead of checking exit code directly. - Tip: Always use
isNginxTestOk(result)— never rely onresult.successalone when checkingnginx -tresults on Linux.
- The dashboard API runs as a background Express server with no attached terminal. All
sudo systemctlcalls indashboard/lib/nginx-service.jsnow usesudo -n(non-interactive). If NOPASSWD is not configured, the call fails immediately with a clear message instead of hanging. isSudoPermissionError()updated to catch"sudo:"prefix in output (coverssudo: a password is required,sudo: a terminal is required, and other sudo error variants).
- Problem: On Linux,
sudo -n systemctl start/stop/reload/restart nginxalways fails until NOPASSWD sudoers rules are configured. Previously there was no way to configure this from the app. - Fix: New one-time setup flow:
- CLI:
Settings → Setup Linux Permissions— usesrunInteractive('sudo -v')to authenticate, then writes/etc/sudoers.d/easy-devopswithNOPASSWDrules for systemctl and the detected nginx binary path. - Dashboard:
Settings → Linux Permissions card— password field + "Setup Permissions" button. UsesPOST /api/settings/permissions/setupwhich pipes the password tosudo -Sviaspawnstdin (no terminal needed). - New
core/permissions.jsmodule:setupLinuxPermissions(),checkPermissionsConfigured(). - Status badge shows "✓ Configured" or "⚠ Required". CLI menu shows "✅ configured" or "⚠ required" in the menu item label.
- CLI:
which nginxis called viafindNginxPath()in bothcore/permissions.jsanddashboard/lib/nginx-service.jsto detect the real nginx binary path at runtime.- The SUDO_RULES written to
/etc/sudoers.d/easy-devopsinclude the detected path (e.g./usr/sbin/nginx) rather than hard-coding/usr/bin/nginx. - Dashboard nginx test/start/save-config flows use the detected path for
nginx -t.
- On Linux, creating directories under
/etc/easy-devops/requires root.ssl-manager.jsmkdir calls now usesudo -n mkdir -pfollowed bysudo -n chownto restore ownership, instead of failing withEACCES.
core/permissions.js— new module withsetupLinuxPermissions()andcheckPermissionsConfigured().GET /api/settings/permissions— returns{ configured: boolean }.POST /api/settings/permissions/setup— accepts{ password }, runssudo -Ssetup, returns{ success: true }or{ error }.isNginxTestOk(result)exported fromcore/platform.js— the correct semantic check for nginx config validity on all platforms.
This release is a major leap from the 0.x series. The most significant change is
the complete removal of external ACME binaries (certbot, wacs.exe) in favour of
a pure Node.js implementation using acme-client. Almost every part of the
project was touched — SSL, domains, nginx config, the dashboard UI, the CLI, and
the Linux installer.
certbotDirconfig key renamed tosslDir— if you have an existingdata/easy-devops.sqliteconfig, the stored key name must be updated. The CLI and dashboard will show an empty SSL directory until you re-enter the path in Settings.- Certificate paths changed — certs are no longer stored in certbot's
/etc/letsencrypt/live/tree. New layout:- Linux:
/etc/easy-devops/ssl/{domain}/fullchain.pem(andprivkey.pem) - Windows:
C:\easy-devops\ssl\{domain}\fullchain.pem(andprivkey.pem) - ACME account key:
{sslDir}/.account/account.key
- Linux:
acmeEmailconfig field required — certificate issuance now fails immediately if no email is configured. Set it once in Settings or via the CLI and it is reused for every subsequent issuance.
- Replaced certbot (Linux) and wacs.exe / win-acme (Windows) with the
acme-clientnpm package. No external binaries required on any platform. - HTTP-01 challenge: Easy DevOps stops nginx, spins up a temporary Node.js HTTP server on port 80 to serve the ACME token, then restarts nginx after validation completes. No webroot configuration needed.
- DNS-01 challenge: Async callback flow. The CLI prompts the user to add the
_acme-challengeTXT record and press Enter; the dashboard shows the record details and waits for a/create-confirmcall before proceeding. DNS propagation is verified viadns.promises.resolveTxtbefore continuing. - Wildcard certificates (
*.example.com): Supported with automatic DNS-01 enforcement. Attempting HTTP-01 for a wildcard returns an error early. CertificatealtNamesis set to[domain, *.domain]automatically. acmeEmailfield added to config, CLI settings menu, and dashboard Settings panel.
- External URL backends — the Backend Host field now accepts full URLs
(
https://myapp.vercel.app/). When a URL is detected, port validation is skipped and the generated nginx conf usesproxy_passwith the URL directly. TheHostheader is set to the upstream hostname instead of$host. proxy_ssl_server_name on— automatically inserted for HTTPS URL backends that are external hostnames (not IPs, not localhost). Enables SNI for services like Vercel and Railway. Omitted when nginx itself is terminating SSL.- Enable / Disable domain — domains can be toggled without deleting their
configuration. Disabling renames
.conf→.conf.disabled; enabling does the reverse (with nginx config test + rollback on failure).- CLI: new "Enable / Disable Domain" menu option with status indicator.
- Dashboard: Enable/Disable button on each domain row; disabled domains are grayed out with a "Disabled" badge.
- Wildcard domain support (
*.example.com) — new Wildcard checkbox in both CLI add-domain flow and dashboard domain form.- Generates
server_name *.example.com example.com;in nginx conf. - Forces DNS-01 validation for any SSL certificate on wildcard domains.
- Hides HTTP-01 option in the cert creation section when wildcard is checked.
- The
*.prefix is added automatically by the system; users type only the bare domain (example.com). - Wildcard badge shown on domain cards in the dashboard.
- Generates
- Delete SSL cert with domain — when deleting a domain whose SSL is enabled, a SweetAlert2 checkbox offers to also remove the certificate files from disk.
- SweetAlert2 replaces all browser
confirm()andalert()calls. Confirmation dialogs are styled to match the current theme (dark/light). The domain-delete dialog includes a checkbox to also delete SSL files. - Light / dark mode toggle — fully working. CSS custom properties
(
--body-bg,--sidebar-bg,--main-bg, etc.) are set per theme via a.light-modeclass on<body>. The sidebar, main content area, and all components respond correctly. - New color scheme — background
#161616, accent#d64a29(burnt orange). Surface color updated to#1e1e1e(neutral dark, not blue-tinted). Scrollbars, focus rings, button glows, and stat card hovers all derive from the accent via--color-primary-rgb. - Accent color picker updated — the first circle now shows the new
#d64a29orange-red as the default accent. - Multi-level subdomain support — the domain name validator now accepts any
depth (
abo.farghaly.dev,a.b.c.example.com). Each label is validated individually (alphanumeric, hyphens allowed in the middle, no leading or trailing hyphens). - Domain form — wildcard UX — when the Wildcard checkbox is checked:
- The www-subdomain toggle is hidden (incompatible with wildcard).
- An amber info box explains that DNS-01 is required.
- The cert method selector hides the HTTP option.
- An inline hint tells the user to type the bare domain only.
core/platform.js(new) — single source of truth for:isWindows,getNginxExe(nginxDir),nginxTestCmd(nginxDir),nginxReloadCmd(nginxDir),combineOutput(result).core/validators.js(new) — shared input validation:validateDomainName,validatePort,validateEmail,validateUpstreamType,validateMaxBodySize,validatePositiveInteger.getConfDDirexported fromcore/nginx-conf-generator.jsand imported bydashboard/lib/nginx-service.js— no longer defined twice.
core/config.js—certbotDirrenamed tosslDirthroughout;acmeEmailadded to defaults (empty string).core/nginx-conf-generator.js— importsisWindowsfromcore/platform.jsinstead of declaring it;getConfDDiris now exported.dashboard/lib/nginx-service.js— importsisWindows,getNginxExe,nginxTestCmd,combineOutputfromcore/platform.jsandgetConfDDirfromcore/nginx-conf-generator.js. ExtractedassertNginxInstalled()helper to avoid repeating the nginx binary check.dashboard/routes/domains.js— removed four duplicated nginx helpers and five duplicated validators; imports fromcore/platform.jsandcore/validators.js.dashboard/routes/settings.js— inline port/email validation replaced with calls tovalidatePort/validateEmailfromcore/validators.js.cli/managers/domain-manager.js— removed local copies ofisWindows,getNginxExe,nginxTestCmd,nginxReloadCmd,combineOutput; imports fromcore/platform.js.cli/managers/nginx-manager.js— removed localconst isWindows; imports fromcore/platform.js.cli/managers/ssl-manager.js— removed localconst isWindows; imports fromcore/platform.js.domain-manager.jsnginx test command on Windows now uses the correct explicit-c conf/nginx.confflag (was missing it, unlike the dashboard route).
- Multi-level subdomains rejected —
abo.farghaly.devand similar domains were incorrectly rejected byvalidateDomainName. Fixed by splitting on.and validating each label separately. proxy_ssl_server_nameadded for IPs and localhost — the directive was generated for any HTTPS URL includinghttps://192.168.1.1. Now only added for named external hosts; IPs andlocalhostare excluded.- Wildcard
*.prefix in domain input — users typing*.example.comwould store the literal*in the name. The validator now strips*.defensively; the CLIfilterdoes the same; the dashboard form shows a hint. nvm installflooding the terminal — on Linux,nvm installprinted megabytes of download/compile output during installation. Output is now redirected to a temp file and only printed on failure.- Installer step shown three times —
install.shprinted all 7 steps as[ ] pendingupfront, then reprinted each step as[→] runningand[✓] done— three lines per step. The upfront pending block is removed; steps now appear once each as they run. nvm-bootstrap.shstray step messages — internalstep_done/step_runningcalls insidebootstrap_nvminterleaved withinstall.sh's step display, creating confusing mixed output. Changed to plainprintfcalls.picker.shCtrl-C leaves terminal in raw mode —trap '_tty_cleanup; return 2' INT TERMinside a bash function:returnfrom a trap handler does not exit the enclosing function. The terminal was left in raw mode on interruption. Fixed with a_cancelledflag; the loop checks it;return 2is called after cleanup.picker.shsttycrash on empty saved state —stty "$old_stty"called even whenstty -gfailed (e.g. non-TTY or dumb terminal), passing an empty string tostty. Added[ -n "$old_stty" ]guard.- Removed
console.log("startResult", ...)debug output fromdashboard/lib/nginx-service.jsrestart()function.
- All certbot and wacs.exe / win-acme integration code (executables, install helpers, spawn pipes, stdin auto-answer tricks).
- The "Install certbot" menu option from the CLI SSL manager.
- Local duplicate definitions of
isWindows,getNginxExe,nginxTestCmd,nginxReloadCmd,combineOutputindomain-manager.js,dashboard/routes/domains.js, anddashboard/lib/nginx-service.js.
Previous release before this session. No changelog was maintained for 0.x versions. Key capabilities at that point: nginx manager, domain manager, basic SSL via certbot/wacs.exe, Node.js version switching via nvm, web dashboard with authentication.