Add config editing and service restart to the web UI #69

Merged
danny merged 4 commits from issue25_webui_config_edit into main 2026-07-07 13:50:07 +01:00
danny commented 2026-07-07 11:43:40 +01:00 (Migrated from gitlab.keyop.co.uk)

Closes #25

Background

Issue #22 added a read-only view of the running configuration to the web UI. Issue #25 (split out from #22's own hedge) adds the ability to actually edit that configuration and restart the service to apply it.

Fix

  • internal/config: extracted LoadFromBytes (the same defaults-merging/queue-number-assignment/validation pipeline LoadFromFile runs, minus the file read) and added ParseRawBytes (a bare unmarshal - no defaults, no validation), plus a new Configuration.ConfigPath field (yaml:"-", set by LoadFromFile) so the web UI knows which file to write an edit back to. Added json:"..." tags matching every existing yaml:"..." tag across Configuration/ConnectionDefaults/NetworkDefaults/Connection/WebUI, so config.Configuration can be reused directly as the edit feature's wire format instead of a separate DTO.
  • internal/nfq_forwarder/webui_config_edit.go (new): four new routes, all behind the existing session/CSRF middleware -
    • GET /api/config/raw - the actual on-disk config, unresolved (via ParseRawBytes), unlike the resolved view GET /api/config (issue #22) shows.
    • POST /api/config/validate - accepts either {"yaml": "..."} or {"config": {...}} (whichever edit mode the client currently holds) and always echoes back both representations, computed server-side (yaml.Marshal/ParseRawBytes) - there's no in-browser YAML parser to do this client-side, so this is what lets the UI's raw/structured toggle stay in sync.
    • POST /api/config/save - same validation, then an atomic write (temp file in the same directory + os.Rename, preserving the existing file's permissions) of exactly the submitted, validated YAML - never the resolved/expanded form, and never at all if invalid. Does not hot-swap the running appConfig - this project has no live-reload path (see docs/nfq_forwarder.service's ExecReload), so a save alone has no effect until a restart.
    • POST /api/config/restart - runs systemctl --user restart --no-block nfq_forwarder.service behind a new restartServiceFunc package-level variable (matching the existing sshDialFunc/addDNATRulesFunc indirection pattern) for testability. --no-block matters: this process is asking systemd's separate manager daemon to restart it, so the call must return before that teardown happens, letting the HTTP response actually reach the browser.
  • internal/nfq_forwarder/webui/templates/config_edit.html + webui/static/config_edit.js (new): a toggle between a raw YAML <textarea> and a structured per-field form (global settings, web UI settings, defaults incl. dynamic by_network rows, and a dynamic connections list) - switching modes round-trips through /api/config/validate. Separate "Validate", "Save" and "Restart service" actions, with explicit messaging that saving alone doesn't apply the change.
  • Added an "Edit configuration" link from the existing read-only config page.

Testing

  • internal/config: new tests for LoadFromBytes, ParseRawBytes, and LoadFromFile setting ConfigPath - package stays at 100% coverage.
  • internal/nfq_forwarder/webui_config_edit_test.go (new): every new route's session/CSRF requirements; GET /api/config/raw returning the unresolved shape and failing cleanly on a missing/malformed file; POST /api/config/validate for both submission modes, including that a semantically-invalid-but-parseable config still echoes back Config (for the structured form to display) while a YAML syntax error omits it; POST /api/config/save confirming a valid submission is written byte-for-byte with permissions preserved, and an invalid one leaves the file untouched; POST /api/config/restart via a stubbed restartServiceFunc covering both success and a surfaced failure - no real systemctl ever invoked.
  • make lint (golangci-lint + yamllint + gitleaks) and make test (go test -race -cover ./...) both clean.
  • Manually smoke-tested the full raw-fetch -> save -> on-disk-content -> restart flow against a real httptest server and a real temp config file; restart correctly surfaced a real systemctl "unit not found" error in this sandbox (no such unit installed here) rather than hanging or crashing.

Docs

Updated README.md, docs/README.md, and CLAUDE.md (new Design-notes bullet + Testing section additions, plus a new real-host verification checklist item for the restart flow, matching the project's existing untested-in-sandbox web UI items).

Closes #25 ## Background Issue #22 added a read-only view of the running configuration to the web UI. Issue #25 (split out from #22's own hedge) adds the ability to actually **edit** that configuration and **restart the service** to apply it. ## Fix - `internal/config`: extracted `LoadFromBytes` (the same defaults-merging/queue-number-assignment/validation pipeline `LoadFromFile` runs, minus the file read) and added `ParseRawBytes` (a bare unmarshal - no defaults, no validation), plus a new `Configuration.ConfigPath` field (`yaml:"-"`, set by `LoadFromFile`) so the web UI knows which file to write an edit back to. Added `json:"..."` tags matching every existing `yaml:"..."` tag across `Configuration`/`ConnectionDefaults`/`NetworkDefaults`/`Connection`/`WebUI`, so `config.Configuration` can be reused directly as the edit feature's wire format instead of a separate DTO. - `internal/nfq_forwarder/webui_config_edit.go` (new): four new routes, all behind the existing session/CSRF middleware - - `GET /api/config/raw` - the actual on-disk config, unresolved (via `ParseRawBytes`), unlike the resolved view `GET /api/config` (issue #22) shows. - `POST /api/config/validate` - accepts either `{"yaml": "..."}` or `{"config": {...}}` (whichever edit mode the client currently holds) and always echoes back **both** representations, computed server-side (`yaml.Marshal`/`ParseRawBytes`) - there's no in-browser YAML parser to do this client-side, so this is what lets the UI's raw/structured toggle stay in sync. - `POST /api/config/save` - same validation, then an atomic write (temp file in the same directory + `os.Rename`, preserving the existing file's permissions) of exactly the submitted, validated YAML - never the resolved/expanded form, and never at all if invalid. Does **not** hot-swap the running `appConfig` - this project has no live-reload path (see `docs/nfq_forwarder.service`'s `ExecReload`), so a save alone has no effect until a restart. - `POST /api/config/restart` - runs `systemctl --user restart --no-block nfq_forwarder.service` behind a new `restartServiceFunc` package-level variable (matching the existing `sshDialFunc`/`addDNATRulesFunc` indirection pattern) for testability. `--no-block` matters: this process is asking systemd's separate manager daemon to restart it, so the call must return before that teardown happens, letting the HTTP response actually reach the browser. - `internal/nfq_forwarder/webui/templates/config_edit.html` + `webui/static/config_edit.js` (new): a toggle between a raw YAML `<textarea>` and a structured per-field form (global settings, web UI settings, defaults incl. dynamic `by_network` rows, and a dynamic connections list) - switching modes round-trips through `/api/config/validate`. Separate "Validate", "Save" and "Restart service" actions, with explicit messaging that saving alone doesn't apply the change. - Added an "Edit configuration" link from the existing read-only config page. ## Testing - `internal/config`: new tests for `LoadFromBytes`, `ParseRawBytes`, and `LoadFromFile` setting `ConfigPath` - package stays at 100% coverage. - `internal/nfq_forwarder/webui_config_edit_test.go` (new): every new route's session/CSRF requirements; `GET /api/config/raw` returning the unresolved shape and failing cleanly on a missing/malformed file; `POST /api/config/validate` for both submission modes, including that a semantically-invalid-but-parseable config still echoes back `Config` (for the structured form to display) while a YAML syntax error omits it; `POST /api/config/save` confirming a valid submission is written byte-for-byte with permissions preserved, and an invalid one leaves the file untouched; `POST /api/config/restart` via a stubbed `restartServiceFunc` covering both success and a surfaced failure - no real `systemctl` ever invoked. - `make lint` (golangci-lint + yamllint + gitleaks) and `make test` (`go test -race -cover ./...`) both clean. - Manually smoke-tested the full raw-fetch -> save -> on-disk-content -> restart flow against a real `httptest` server and a real temp config file; restart correctly surfaced a real `systemctl` "unit not found" error in this sandbox (no such unit installed here) rather than hanging or crashing. ## Docs Updated `README.md`, `docs/README.md`, and `CLAUDE.md` (new Design-notes bullet + Testing section additions, plus a new real-host verification checklist item for the restart flow, matching the project's existing untested-in-sandbox web UI items).
danny (Migrated from gitlab.keyop.co.uk) approved these changes 2026-07-07 11:43:40 +01:00
danny commented 2026-07-07 11:43:57 +01:00 (Migrated from gitlab.keyop.co.uk)

mentioned in issue #25

mentioned in issue #25
danny commented 2026-07-07 12:33:31 +01:00 (Migrated from gitlab.keyop.co.uk)

added 1 commit

  • 2824beaa - Default to structured editing and add compact per-connection save

Compare with previous version

added 1 commit <ul><li>2824beaa - Default to structured editing and add compact per-connection save</li></ul> [Compare with previous version](/keyop/go/nfq_forwarder/-/merge_requests/28/diffs?diff_id=306&start_sha=6ecbdd9b1ca8549c0a284bb73f29d22856929f07)
danny commented 2026-07-07 12:56:35 +01:00 (Migrated from gitlab.keyop.co.uk)

added 1 commit

  • c7edfbee - Fix compact table header alignment, compact by_network, add connection reordering

Compare with previous version

added 1 commit <ul><li>c7edfbee - Fix compact table header alignment, compact by_network, add connection reordering</li></ul> [Compare with previous version](/keyop/go/nfq_forwarder/-/merge_requests/28/diffs?diff_id=309&start_sha=2824beaaf61fa897840946b51ee3fb1839ebfe93)
danny commented 2026-07-07 13:08:10 +01:00 (Migrated from gitlab.keyop.co.uk)

added 1 commit

  • 3ab6a387 - Compact global/web UI/defaults fields, drag-and-drop connection reordering

Compare with previous version

added 1 commit <ul><li>3ab6a387 - Compact global/web UI/defaults fields, drag-and-drop connection reordering</li></ul> [Compare with previous version](/keyop/go/nfq_forwarder/-/merge_requests/28/diffs?diff_id=311&start_sha=c7edfbeefa45ddf1f907f25d49b7a6c18cf0d196)
danny commented 2026-07-07 13:13:13 +01:00 (Migrated from gitlab.keyop.co.uk)

UI refinements added after initial review feedback:

  1. Structured form is now the default edit mode (raw YAML is the opt-in toggle, moved to the right); the connections table became a compact one-row-per-connection grid instead of full labeled fields per row, with an inline Save button on each row (disabled until that row's own fields are modified) so editing several connections doesn't require scrolling to the bottom action bar.
  2. Fixed a header/column misalignment in the compact tables (header and data rows were computing max-content action-button column widths independently, since they were separate CSS grids - fixed by sharing one grid across header + rows via display: contents). Applied the same compact one-row-per-rule treatment to the defaults.by_network table. Made the Global settings / Web UI / Defaults field cards more compact too (tighter spacing, smaller inputs).
  3. Replaced the connection row up/down buttons with drag-and-drop reordering (native HTML5 drag-and-drop, no library) - drag the handle to reorder; the saved YAML preserves whatever order the rows end up in.

All changes are on the same branch/MR (!69), lint and the full test suite stay clean throughout. Currently awaiting review.

**UI refinements added after initial review feedback:** 1. Structured form is now the default edit mode (raw YAML is the opt-in toggle, moved to the right); the connections table became a compact one-row-per-connection grid instead of full labeled fields per row, with an inline **Save** button on each row (disabled until that row's own fields are modified) so editing several connections doesn't require scrolling to the bottom action bar. 2. Fixed a header/column misalignment in the compact tables (header and data rows were computing `max-content` action-button column widths independently, since they were separate CSS grids - fixed by sharing one grid across header + rows via `display: contents`). Applied the same compact one-row-per-rule treatment to the `defaults.by_network` table. Made the Global settings / Web UI / Defaults field cards more compact too (tighter spacing, smaller inputs). 3. Replaced the connection row up/down buttons with drag-and-drop reordering (native HTML5 drag-and-drop, no library) - drag the handle to reorder; the saved YAML preserves whatever order the rows end up in. All changes are on the same branch/MR (!69), lint and the full test suite stay clean throughout. Currently awaiting review.
danny commented 2026-07-07 13:27:41 +01:00 (Migrated from gitlab.keyop.co.uk)

assigned to @danny

assigned to @danny
danny commented 2026-07-07 13:27:42 +01:00 (Migrated from gitlab.keyop.co.uk)

requested review from @danny

requested review from @danny
danny commented 2026-07-07 13:28:16 +01:00 (Migrated from gitlab.keyop.co.uk)

This seems to work and meet the specifications given.

Approving and merging, will become v1.1.0.

This seems to work and meet the specifications given. Approving and merging, will become v1.1.0.
danny commented 2026-07-07 13:28:16 +01:00 (Migrated from gitlab.keyop.co.uk)

approved this merge request

approved this merge request
danny commented 2026-07-07 13:50:07 +01:00 (Migrated from gitlab.keyop.co.uk)

mentioned in commit 551f561100

mentioned in commit 551f5611002c2bb2523ff601c1064d2bee389298
danny (Migrated from gitlab.keyop.co.uk) merged commit 551f561100 into main 2026-07-07 13:50:07 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
keyop-go/nfq_forwarder!69
No description provided.