fix: address design review ACT NOW items (6 risk gaps)

- Add migrate_config() to ConfigGenerator protocol for schema version upgrades
- Add per-server operation lock to ProcessManager to prevent start/stop races
- Add busy_timeout retry/backoff strategy (exponential: 1s, 2s, 4s) for DB lock exhaustion
- Add ConfigForm testing strategy and error boundary for malformed schemas
- Add schema cache invalidation on adapter version change
- Add ConfigMigrationError to typed adapter exceptions
This commit is contained in:
Tran G. (Revernomad) Khoa
2026-04-16 17:29:19 +07:00
parent 624d7594e2
commit b17d199301
6 changed files with 94 additions and 4 deletions

View File

@@ -895,6 +895,14 @@ frontend/
- Optimistic locking (send `config_version` on save)
- 409 conflict resolution (show diff, allow override/merge)
- Validation errors from adapter (field-level, from Pydantic)
- **Error boundary**: if `jsonSchemaToFields()` encounters an unsupported or malformed schema type (deeply nested objects, unknown formats), render a fallback "This section uses an unsupported field type. Edit raw JSON." with a raw JSON editor instead of crashing the form
**ConfigForm test plan** (critical — highest-risk component):
- **Unit: `jsonSchemaToFields`** — test every supported type mapping (string→text, integer→number, boolean→toggle, enum→select, array→repeatable), test sensitive field masking, test unknown type → fallback descriptor with `type: "raw_json"`
- **Unit: `jsonSchemaToFields`** — test malformed schema input (missing `properties`, nested `$ref`, `oneOf`/`anyOf`) → returns fallback descriptor, never throws
- **Integration: `ConfigForm`** — render with a 2-section schema, verify field rendering, toggle a boolean, verify dirty state, submit and verify request payload
- **Integration: 409 conflict** — after save, mock a 409 response, verify ConflictDialog appears with diff
- **Integration: error boundary** — mount `ConfigForm` with a schema that has unsupported `type: "object"` nested 3 levels deep, verify raw JSON fallback renders instead of crash
**`LogViewer`** — performance-critical:
- Virtualized rendering (react-window or similar)
@@ -929,7 +937,7 @@ All API data uses TanStack Query with appropriate stale times:
| Mods | 5min | — |
| Bans | 2min | — |
| Game types | 30min | — (rarely changes) |
| Config schema | 30min | — (tied to adapter version) |
| Config schema | 30min | — (tied to adapter version) **Invalidation**: when `GET /games/{type}` returns a different `schema_version` than previously cached, TanStack Query invalidates all config schema queries for that game type. This prevents stale forms after an adapter update. |
**Optimistic updates** on:
- Server start/stop → immediately update status in cache, rollback on error
@@ -1071,7 +1079,7 @@ const { hasMissions, hasMods, hasRemoteAdmin } = useCapability(serverId);
interface FieldDescriptor {
name: string;
label: string;
type: "text" | "number" | "boolean" | "select" | "textarea" | "password" | "array";
type: "text" | "number" | "boolean" | "select" | "textarea" | "password" | "array" | "raw_json";
default?: unknown;
min?: number;
max?: number;
@@ -1087,6 +1095,9 @@ function jsonSchemaToFields(
): FieldDescriptor[] {
// Walk schema.properties, map each to a FieldDescriptor
// Mark fields in sensitiveFields as type: "password"
// Unknown types (nested objects, oneOf/anyOf, $ref) → type: "raw_json"
// The form renders a textarea with JSON editing for these fields
// rather than crashing or hiding the field silently
}
```
@@ -1105,6 +1116,31 @@ function jsonSchemaToFields(
Future adapters register themselves; the card list auto-populates from `GET /games`.
### Schema Cache Invalidation
Adapter config schemas are cached with `staleTime: 30min`. When an adapter is updated and its `schema_version` changes, the frontend must not serve a stale schema. The invalidation pattern:
```typescript
// When fetching game type info, compare schema_version
function useGameTypeWithInvalidation(gameType: string) {
const queryClient = useQueryClient();
return useQuery({
queryKey: ["gameType", gameType],
staleTime: 30 * 60 * 1000,
onSuccess: (data) => {
const prevVersion = localStorage.getItem(`schema_version_${gameType}`);
if (prevVersion && prevVersion !== data.schema_version) {
// Adapter was updated — invalidate all config schema queries
queryClient.invalidateQueries({ queryKey: ["configSchema", gameType] });
localStorage.setItem(`schema_version_${gameType}`, data.schema_version);
} else if (!prevVersion) {
localStorage.setItem(`schema_version_${gameType}`, data.schema_version);
}
},
});
}
```
---
## Error Handling UX
@@ -1121,6 +1157,7 @@ Future adapters register themselves; the card list auto-populates from `GET /gam
| WS disconnect | StatusBar indicator + stale data with timestamp |
| WS reconnect | Automatic; no user action needed |
| Server crashed | Toast notification + status dot turns red |
| Malformed adapter schema | Raw JSON fallback in ConfigForm section ("This section uses an unsupported field type. Edit raw JSON.") |
**No `alert()` calls.** All feedback uses toast (transient) or inline (persistent) patterns.