Skip to content

Add Redis DB selection to the Add Instance modal - #166

Merged
dngrtech merged 17 commits into
mainfrom
feature/redis-db-selection
Aug 7, 2026
Merged

Add Redis DB selection to the Add Instance modal#166
dngrtech merged 17 commits into
mainfrom
feature/redis-db-selection

Conversation

@dngrtech

@dngrtech dngrtech commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a narrow Redis DB dropdown to the Add New QL Instance modal, letting the operator choose which Redis logical database a new instance stores its minqlx state in.

Until now the DB index was never stored anywhere — it was recomputed from the game port (port - 27959) in two independent places, which meant the two could drift apart and the operator had no way to influence it.

Design

  • Backward compatible. QLInstance.redis_db is nullable and NULL means "derive from the port", which is exactly what every pre-existing instance already did. A regression test asserts a NULL instance produces a byte-identical qlds_args string. No data migration, no instance restarts.
  • DB 0 stays reserved for QLSM's own state. Selectable range is 1..MAX_INSTANCES_PER_HOST (8).
  • The list stays short. The dropdown offers 1..upper where upper = min(8, max(instance_count + 1, highest_occupied, selected)) — a host with no instances offers just 1, a host with one offers 1, 2. It never dumps all 8 up front. The highest_occupied and selected terms keep an occupied DB visible above the baseline and guarantee the current value is always present in its own list.
  • The default tracks the port — matching how the value was always derived implicitly — until the operator picks one explicitly, after which it sticks. All three port-change paths (dropdown, preset load, editing net_port in server.cfg) already funnel through setPort, so one effect covers them.
  • An occupied DB is flagged, not blocked. An amber info icon reads Used by <instance name> on hover, but the option stays fully selectable — sharing a DB between two instances is a supported choice. There is no uniqueness check and no warning copy.
  • Creation-only. There is no edit path for the field, the same way there is none for the port.
  • Presets deliberately unchanged. Redis DB is placement identity, like port and host — not reusable configuration.

Side effect worth noting: the two duplicated derivation formulas are now a single ui.constants.resolve_redis_db() helper, so they can no longer drift.

Test plan

  • Backend: 1355 passed / 2 failed / 50 errors. Zero regressions — every failure was diffed against base commit b788ca5 in a separate worktree and confirmed pre-existing (socketio / rcon-fleet).
  • Frontend: 551 passed / 3 failed. The 3 are pre-existing HooksTab.test.jsx delete-confirmation tests in a file this branch never touches.
  • cd frontend-react && pnpm build succeeds.

New coverage: resolve_redis_db unit tests, a backward-compat qlds_args regression guard, status-poller resolution tests, six API validation tests, 12 buildRedisDbOptions unit tests, and five dropdown render tests — including the key one asserting that clicking an occupied DB still fires onRedisDbChange.

Known limitation

docs/qlds_args_reference.md still claims qlx_redisDatabase comes from instance.id. That has never been true — it has always been port-derived. The file is gitignored (.gitignore:134 docs/*), so the correction could not be committed here. docs/user/operations/edit-configs.md carried the same stale claim and was fixed.

https://claude.ai/code/session_01Ab7PPM5Du7xVeAsq6aickq

@dngrtech
dngrtech marked this pull request as draft August 6, 2026 19:54
@dngrtech
dngrtech marked this pull request as ready for review August 6, 2026 19:54
@dngrtech
dngrtech marked this pull request as draft August 6, 2026 20:55
@dngrtech
dngrtech marked this pull request as ready for review August 6, 2026 20:55
@dngrtech
dngrtech marked this pull request as draft August 6, 2026 21:02
@dngrtech
dngrtech marked this pull request as ready for review August 6, 2026 21:02
@dngrtech

dngrtech commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Posted manually: the Claude Code Review workflow could not run — GitHub Actions has been in a major outage since 2026-08-06 15:22 UTC, with webhooks throttled to ~15%, so the opened and ready_for_review events for this PR were dropped. Same prompt and checklist as the workflow, run locally against main...HEAD.

PR #166 Review — Redis DB Selection for Add Instance

Reviewed git diff main...HEAD (25 files, +792/-18) plus the surrounding source (ui/constants.py, ui/routes/instance_routes.py, ui/task_logic/ansible_instance_mgmt.py, ui/task_logic/server_status_poll.py, ui/models.py, ui/database.py, frontend-react/src/components/addInstance/AddInstanceForm.jsx, InstanceBasicInfoForm.jsx, redisDbOptions.js, frontend-react/src/constants/hostLimits.js) and the migration chain. Ran the backend suite (tests/test_resolve_redis_db.py, test_redis_db_resolution.py, test_instance_redis_db_api.py, test_task_deploy_instance.py, test_task_server_status_poll.py — 57 passed) and the frontend suite (redisDbOptions.test.js, InstanceBasicInfoForm.redisDb.test.jsx, InstanceBasicInfoForm.test.jsx, AddInstanceForm.test.jsx — 41 passed). All green, no regressions.

Strengths

  • Single point of derivation. ui/constants.py:37-47 (resolve_redis_db) replaces two independently-drifting copies of port - REDIS_DB_PORT_OFFSET (ui/task_logic/ansible_instance_mgmt.py:121, ui/task_logic/server_status_poll.py:28). Both call sites were updated in the same commit (9a820bd), and both int-coerce their inputs (int(instance.redis_db) / int(instance.port)), which correctly avoids a str - int crash if a caller passes a string DB from JSON.
  • Backward compatibility is genuinely verified, not just asserted. tests/test_redis_db_resolution.py:750-760 checks the entire _build_qlds_args_string output is byte-identical for a redis_db=None instance, not just the redis flag — this is the right level of paranoia for a change that touches a shared arg-builder.
  • Truthiness bug guarded against. tests/test_resolve_redis_db.py:813-816 and the frontend equivalent (effectiveRedisDb, redisDbOptions.test.js:409-411) both explicitly test that a stored 1 isn't treated as "missing" — a realistic footgun given if instance.redis_db: would have silently broken DB 1.
  • Validation matches project convention. _validate_redis_db (ui/routes/instance_routes.py:238-249) checks type before range, rejects bool explicitly (since bool is an int subclass in Python — an easy miss), and returns (value, error) consistent with the other validators in the same file (_validate_qlx_plugins, _validate_enabled_hooks_payload).
  • Migration is clean. Single head (755d969fcce8), correct down_revision, nullable column, reversible downgrade().
  • Docs are thorough and accurate. docs/technical.md, docs/architecture.md, docs/api_reference.md, and docs/user/* were all updated in the same PR, including fixing a previously-stale claim ("derived from port" → now conditional on resolve_redis_db). Version bump (1.23.0) is consistent across VERSION, docs/user/version.json, and docs/user/releases.md, and the changelog entry links PR Add Redis DB selection to the Add Instance modal #166.
  • Duplicate DB sharing is a deliberate, tested, documented choice (tests/test_instance_redis_db_api.py:676-696, docs/api_reference.md:331), not an oversight — good to see the intent spelled out rather than left ambiguous.

Issues

Critical (Must Fix)

None found.

Important (Should Fix)

  1. Stale Redis DB value survives a host switch until the port re-syncs. frontend-react/src/components/addInstance/AddInstanceForm.jsx:307-361 resets redisDbTouched.current = false on handleHostChange, but never resets the redisDb state itself. redisDb is only recomputed by the effect at AddInstanceForm.jsx:548-553, which is a no-op when port is '' (parseInt('', 10) is NaN, guarded by the early return). Per the same function (lines 354-360), when a user switches hosts mid-session and the previously-selected port isn't in the new host's available-port list, port gets cleared to '' — but redisDb keeps whatever numeric value it had for the old host. Meanwhile the Redis DB Listbox is only disabled={!selectedHostId} (InstanceBasicInfoForm.jsx:142), not gated on port, so it stays interactive and visibly shows the stale number. redisDbOptions (AddInstanceForm.jsx:564-567) is rebuilt against the new host's instances but with selectedDb still equal to the stale number, which can inflate the option list far beyond what the new host needs (e.g., carrying over selectedDb=7 from a busy old host onto a brand-new host with zero instances renders 7 options instead of 1). This can't reach the backend with bad data (submit is blocked while port is empty, and the value self-corrects the moment a port is picked), but it's a real, reproducible UI-correctness bug, not just a hypothetical — a QA pass on "switch host mid-form" will show it. Fix: reset redisDb (e.g., to 1 or null) at the same point redisDbTouched.current is reset in handleHostChange.

  2. No test covers redis_db in the actual submit payload or the host-switch reset behavior. frontend-react/src/components/addInstance/AddInstanceForm.jsx:926 adds redis_db: redisDb to the submitted object, and redisDbTouched.current = false is set on host change (line 310) — neither is exercised by any test. AddInstanceForm.test.jsx (21 tests, unmodified by this PR) has zero redis_db/redisDb references; the new coverage lives entirely in InstanceBasicInfoForm.redisDb.test.jsx (component-level, mocked Listbox, no submit path) and redisDbOptions.test.js (pure function). The integration path — port syncing, touched-tracking, host-switch reset, and what actually lands in onSubmit's payload — is untested, which is exactly the surface where issue Remove serverchecker.py preset script #1 lives. Add at least one AddInstanceForm test asserting the submitted redis_db value, and one exercising a host switch after a port was already selected.

Minor (Nice to Have)

  1. MAX_REDIS_DB duplicates an existing constant instead of importing it. frontend-react/src/components/addInstance/redisDbOptions.js:9 hardcodes MAX_REDIS_DB = 8, but frontend-react/src/constants/hostLimits.js:4 already exports MAX_INSTANCES_PER_HOST = 8 for exactly this purpose (with a comment noting it mirrors the backend and must be kept in sync). Now there are two independent frontend copies of the same backend-derived number instead of one. Same applies to REDIS_DB_PORT_OFFSET = 27959 (redisDbOptions.js:8), which has no existing frontend counterpart but is the same kind of magic-number duplication CLAUDE.md's own ui/constants.py docstring warns against ("everything that depends on it derives from these values ... so the layers cannot drift apart"). Suggest importing MAX_INSTANCES_PER_HOST from hostLimits.js for maxInstances's default rather than re-declaring MAX_REDIS_DB.
  2. resolve_redis_db returning a value below 1 is still only guarded downstream, not at the source. ui/task_logic/server_status_poll.py:29-31 raises ValueError if the derived DB is < 1, but nothing bounds the upper end (e.g. a redis_db written directly via a future migration/script bypassing _validate_redis_db, or a manually-edited row, could exceed MAX_INSTANCES_PER_HOST). This is pre-existing behavior (not introduced by this PR — the same asymmetric check existed before against the port-derived value), so not a regression, but worth a follow-up since resolve_redis_db is now the single trusted source multiple callers lean on.
  3. ui/database.py:49-50create_instance's new redis_db=None keyword is appended after qlx_plugins with mixed comma/indentation style change; purely cosmetic, no functional issue.

Assessment

Ready to merge? With fixes (Important #1 recommended before merge; #2 can be a fast follow if time-constrained).

Reasoning: Backend design and test coverage are solid — the shared resolve_redis_db helper, byte-identical backward-compat test, and truthiness guard are all evidence of careful work. The one real bug is confined to the frontend's host-switch edge case, is UX-only (never reaches the API with bad data), and has a small, well-understood fix; everything else is a documentation/DRY nit.

rage added 10 commits August 6, 2026 19:48
Substitutes for browser verification: checks the label/value, the
disabled-until-host-selected state, the option count/order, the
InfoTooltip "used by" trigger on occupied DBs, and -- the key design
intent -- that an occupied DB stays selectable and fires onChange.
Documents the optional redis_db field (api_reference.md), points the
architecture/technical notes at resolve_redis_db() as the single
derivation source instead of the inline formula, and adds a short
user-facing note on the Redis DB dropdown to the instance-creation
page. Also corrects edit-configs.md, which still described
qlx_redisDatabase as always port-derived.

docs/qlds_args_reference.md was also corrected locally (it wrongly
attributed qlx_redisDatabase to instance.id) but that file is
gitignored (docs/* with an explicit allow-list) and isn't part of
this commit.
@dngrtech
dngrtech force-pushed the feature/redis-db-selection branch from f1110d8 to 11907c5 Compare August 7, 2026 02:49
rage added 3 commits August 6, 2026 19:51
The auto-derive effect only watched `port`, so switching hosts left a
stale Redis DB value on screen when the new host kept the same port
number but should re-derive, or cleared the port entirely.
… singleton

create_app() was called without RCON_ENABLED: False, so it hit the real
SocketIO+Redis init path and mutated the module-level socketio object
with a message queue -- breaking every later test that calls
socketio.test_client() in the full-suite run.
The Redis DB picker no longer re-derives from the chosen port; it defaults
to the lowest free DB for the host on initial load and then stays fixed
until the operator changes it explicitly, independent of port/host changes.
Also surfaces the Redis DB on the instance details drawer and adds a thin
scrollbar style to the drawer bodies.
@dngrtech
dngrtech marked this pull request as draft August 7, 2026 04:41
@dngrtech
dngrtech marked this pull request as ready for review August 7, 2026 04:41

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #166 — Redis DB Selection at Instance Creation (v1.24.0)

This PR lets operators explicitly choose a Redis logical DB (1–8) when deploying a new instance, instead of always deriving it from the port number. Pre-existing instances with no stored value continue to derive it from the port, preserving backward compatibility.


Strengths

  • Single source of truth for DB resolution. ui/constants.py:resolve_redis_db() and its JS mirror effectiveRedisDb() in redisDbOptions.js eliminate the prior duplicated port - REDIS_DB_PORT_OFFSET arithmetic that existed in both ansible_instance_mgmt.py and server_status_poll.py. This was the actual bug-surface — now there is one place to change.

  • Bulletproof input validation. _validate_redis_db() at ui/routes/instance_routes.py:1460–1472 correctly handles Python's bool-is-a-subclass-of-int gotcha by checking isinstance(raw, bool) first. Zero (QLSM-reserved), >8, floats, and strings are all caught with descriptive 400 errors.

  • Backward compatibility by design. The nullable column with NULL = derive from port means every pre-existing instance gets the same DB it always had, with no migration data-fill required. The dedicated test test_qlds_args_are_byte_identical_when_redis_db_is_null at tests/test_redis_db_resolution.py:50 guards this regression point.

  • Thorough test coverage at all layers. Python API integration tests (test_instance_redis_db_api.py), unit tests for the resolution function (test_resolve_redis_db.py), Ansible arg-builder and status-poller integration (test_redis_db_resolution.py), JS unit tests for all three helper functions (redisDbOptions.test.js), and component tests including the headless-UI JSDOM workaround (InstanceBasicInfoForm.redisDb.test.jsx).

  • UX is non-intrusive. The DB field auto-selects the next free DB on host selection, stays disabled until a host is picked, and shows a warning tooltip on occupied DBs without blocking selection — matching the documented intent that sharing is a deliberate, supported choice.

  • Documentation is complete. API reference, architecture doc, technical doc, user guide, and release notes are all updated coherently.


Issues

Critical (Must Fix)

None.

Important (Should Fix)

None.

Minor (Nice to Have)

1. Constants duplicated across language boundary
frontend-react/src/components/addInstance/redisDbOptions.js:1–2

export const REDIS_DB_PORT_OFFSET = 27959;
export const MAX_REDIS_DB = 8;

These mirror ui/constants.py's REDIS_DB_PORT_OFFSET and MAX_INSTANCES_PER_HOST. There is no enforcement that they stay in sync — a change on the Python side silently leaves the frontend wrong. The existing pattern in this codebase for other constants (e.g. BASE_GAME_PORT) has the same shape, so this isn't unique to this PR. Worth a comment in both files pointing to the other, so whoever changes one is reminded to check the other.

2. Awkward field label in the details modal
frontend-react/src/components/instances/InstanceDetailsModal.jsx:933

<Field label="Redis DB Instance">

"Redis DB Instance" reads oddly — "Redis DB" would match the deploy-form label and the API field name. Minor cosmetic inconsistency.

3. nextFreeRedisDb silently wraps to DB 1 when all 8 are occupied
frontend-react/src/components/addInstance/redisDbOptions.js:52–54

const free = options.find((option) => !option.inUse);
return free ? free.db : 1;

When all DBs are taken, the function returns 1 (which is occupied) without any signal to the caller. The warning tooltip on the button will show, so the user isn't completely in the dark, but a caller that wants to distinguish "free DB found" from "everything full" cannot do so from the return value alone. Given that MAX_INSTANCES_PER_HOST = 8 and the host can only ever have 8 instances, this scenario is only reachable when the host is full — at which point the deploy form should presumably already be blocking submission for unrelated reasons (no available ports). Low real-world impact.

4. resolve_redis_db does not re-validate stored values
ui/constants.py:1388–1390

if instance.redis_db is not None:
    return int(instance.redis_db)

A DB value that bypassed the API (direct DB write, data migration, future code path) could be 0 or >8 and would pass through silently. The status poller does have a db < 1 guard that raises ValueError, so extreme bad values are caught there, but 0 would surface as a runtime error rather than a clean validation failure. Not a concern for normal operation; noted for completeness.


Assessment

Ready to merge? Yes

Reasoning: The core abstraction (resolve_redis_db) is clean, the migration is safe (nullable, no backfill needed), input validation is correct, and the test suite covers the failure modes that matter — including the backward-compat regression test for the byte-identical arg string. The minor notes above are cosmetic or theoretical; none block production use.

rage added 4 commits August 6, 2026 21:51
Matches the Add Instance form's label and the API field name.
Redis DB now defaults to the lowest free DB and stays fixed regardless
of port changes, rather than tracking the port.
Highlights that admins can give each instance its own DB or share
one across instances that need to share plugin state.
The field was described as merely "auto-selected," undersetting that
it's a full dropdown offering every DB 1-8, freely pickable at any time.
@dngrtech
dngrtech merged commit 6b7c374 into main Aug 7, 2026
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