Skip to content

refactor(server): extract device & system route groups (Refs #3502)#3527

Merged
Yeraze merged 1 commit into
mainfrom
fix/3502-batch-c
Jun 17, 2026
Merged

refactor(server): extract device & system route groups (Refs #3502)#3527
Yeraze merged 1 commit into
mainfrom
fix/3502-batch-c

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Summary

Continues the server.ts route-extraction effort tracked in #3502. This batch extracts two cleanly-decoupled handler groups; the remaining inline groups are genuinely coupled to server.ts-local state (see below) and were intentionally left.

Groups extracted

routes/deviceRoutes.ts (mounted verbatim at /):

  • GET /device-config
  • GET /device/backup
  • POST /device/reboot
  • POST /device/purge-nodedb

All resolve the per-source manager via resolveSourceManager and delegate to importable services (deviceBackupService, backupFileService, databaseService) — zero server.ts coupling.

routes/systemRoutes.ts (mounted verbatim at /):

  • GET /system/status
  • GET /status
  • GET /version/check
  • POST /system/restart

Supporting helper moves:

  • isRunningInDocker, compareVersions, checkDockerImageExists, and serverStartTime moved to a shared utils/systemInfo.ts. The startup auto-upgrade scheduler (checkForAutoUpgrade, still in server.ts) keeps using compareVersions/checkDockerImageExists via the new import.
  • gracefulShutdown (server-lifecycle: closes the module-local HTTP server + managers) is injected into the route module via setSystemCallbacks(), matching the existing setSettingsCallbacks pattern — the route module never references the HTTP server.

Handler bodies were moved verbatim; auth/validation middleware preserved identically. Orphaned deviceBackupService/backupFileService imports removed from server.ts.

Inline handler count

  • Before: 98 inline apiRouter.* handlers
  • After: 90

Tests

  • New routes/deviceRoutes.test.ts (config/backup/reboot/purge happy + error paths).
  • Rewrote routes/systemRoutes.test.ts as supertest coverage (status/version-check/restart incl. the gracefulShutdown callback and the module-level version-check cache ordering).
  • Full Vitest suite: success=true, 0 failed tests, 0 failed suites (6751 passed). tsc clean. ESLint clean on new files.

Remaining groups — too coupled to extract cleanly (left intentionally)

  • /channels (11) — large mixed-concern group (CRUD + import/export + decode/encode-url + MeshCore push). The 3 channel-move helpers (detectChannelMoves/snapshotChannelsBeforeChange/migrateMessagesIfChannelsMoved) are self-contained, but the handlers also reach into meshcoreManagerRegistry, resolveSourceManager, and message-transform logic. Extractable but warrants its own dedicated PR for reviewability, not a "clean small batch."
  • /nodes (17) — depends on getEffectivePosition + node-enrichment closures over server.ts state.
  • /admin (17) — session-passkey / admin-command state held in server.ts.
  • /config (15) — config-marshalling registry + resolveSourceConnectionConfig.
  • /settings/* (scheduler routes) — coupled to per-source scheduler state; the existing settingsRoutes.ts callback-injection surface would need significant expansion.
  • /messages/send, /poll — manager/connection state.
  • /debug/ip — app/env locals.

🤖 Generated with Claude Code

Continue the server.ts route extraction (issue #3502). Moves two
cleanly-decoupled handler groups out of the inline monolith.

device routes (routes/deviceRoutes.ts), mounted at '/':
  GET  /device-config
  GET  /device/backup
  POST /device/reboot
  POST /device/purge-nodedb

system routes (routes/systemRoutes.ts), mounted at '/':
  GET  /system/status
  GET  /status
  GET  /version/check
  POST /system/restart

The Docker/version pure helpers (isRunningInDocker, compareVersions,
checkDockerImageExists) plus serverStartTime move to a shared
utils/systemInfo.ts, since the startup auto-upgrade scheduler
(checkForAutoUpgrade) still references compareVersions/checkDockerImageExists.
The restart handler's server-lifecycle gracefulShutdown is injected via
setSystemCallbacks() so the route module stays free of the HTTP-server ref.

Handler bodies moved verbatim; middleware preserved. Orphaned
deviceBackupService/backupFileService imports removed from server.ts.

Inline apiRouter handlers: 98 -> 90.

Adds routes/deviceRoutes.test.ts and rewrites routes/systemRoutes.test.ts
as supertest coverage of the new modules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review: refactor(server): extract device & system route groups

Overall this is a clean, well-structured refactor that continues the decomposition of server.ts in a principled way. The callback-injection pattern for gracefulShutdown is a solid design choice. A few issues worth noting:


Bugs / Correctness

1. meshtasticManager singleton usage in systemRoutes.ts (line 93–94)

// systemRoutes.ts:23
import meshtasticManager from '../meshtasticManager.js';
// ...
const connectionStatus = await meshtasticManager.getConnectionStatus();
const localNode = meshtasticManager.getLocalNodeInfo();

The GET /status handler uses the deprecated singleton meshtasticManager instead of resolving from the source registry. The CLAUDE.md and the PR description both highlight that there is no global singleton — per-source scoping is mandatory. This handler silently leaks data (returns only the default source's connection/node info). Given that the PR description lists this route as moved "verbatim," this appears to be a pre-existing issue carried over, but it's worth flagging.

2. versionCheckCache is module-scoped mutable state (systemRoutes.ts:122)

let versionCheckCache: { data: any; timestamp: number } | null = null;

This is fine for production but the test comment in systemRoutes.test.ts:115–117 reveals a non-obvious ordering dependency: the failure test must run before the success test because the success test populates the module-level cache. Vitest randomizes test order when sequence.shuffle is on. A beforeEach(() => { versionCheckCache = null; }) reset (or exposing a resetVersionCheckCache() for tests) would make the test suite order-independent.

3. compareVersions treats pre-release segments as numeric (systemInfo.ts:30)

const aParts = a.split(/[-.]/).map(p => parseInt(p) || 0);

Splitting on both - and . means 4.11.0-beta.1 becomes [4, 11, 0, 0, 1], and parseInt('beta') returns NaN which is coerced to 0. Practically harmless for GitHub release tags (which are pure semver), but silently drops pre-release identifiers. Not a regression — this is existing logic moved verbatim.


Design / Best Practices

4. checkDockerImageExists fetches a token on every call with no caching (systemInfo.ts:57–80)

Each call to checkDockerImageExists fetches an anonymous GHCR token for every tag variant, even when the upstream is called from the 5-minute cached version-check handler. This is benign today but could be tightened with a short-lived token cache if rate-limit pressure arises. Low priority.

5. systemRoutes.ts uses any in the version-check result type (line 122)

let versionCheckCache: { data: any; timestamp: number } | null = null;

A named interface (e.g. VersionCheckResult) would make this self-documenting. Minor.

6. Default callbacks object is mutated at module load (systemRoutes.ts:40–44)

let callbacks: SystemCallbacks = {
  gracefulShutdown: () => {
    logger.warn('gracefulShutdown called before system callbacks were registered');
  },
};

The pattern is consistent with settingsRoutes.ts and prevents a hard crash if setSystemCallbacks is not called, which is good defensive programming. No issue here.


Security

No new attack surface introduced. Auth middleware (requirePermission, optionalAuth) is preserved identically on all moved handlers. The gracefulShutdown route is gated behind requirePermission('settings', 'write'), which is appropriate.

The GET /status endpoint uses optionalAuth() and intentionally exposes version + connection info — this is unchanged behavior.


Test Coverage

Tests are solid and follow the existing supertest pattern. Two specific points:

7. deviceRoutes.test.ts — missing test for null localNodeInfo on backup (line 77)

The backup handler falls back to '!unknown' when getLocalNodeInfo() returns null. There's no test for the null branch, leaving the fallback path untested.

8. systemRoutes.test.ts — version check cache ordering (already noted in bug #2 above)

The comment at line 115–117 acknowledges the ordering dependency. This is a correctness risk if tests are ever run with shuffle enabled.


Summary

Severity Count
Bug (pre-existing, carried verbatim) 1 (singleton meshtasticManager in /status)
Test robustness 2 (cache ordering, null backup path)
Style / minor 2 (any type, no token cache)

The core refactor is clean and the setSystemCallbacks injection pattern is the right approach. The singleton meshtasticManager usage on the /status endpoint is the most notable issue — it predates this PR but now lives in an extracted module, making it more visible and worth addressing.

@Yeraze Yeraze merged commit 31e5e21 into main Jun 17, 2026
19 checks passed
@Yeraze Yeraze deleted the fix/3502-batch-c branch June 17, 2026 18:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant