Fix code-review findings across domain, persistence, server and frontend
Full-codebase review turned up five real correctness/security bugs and
a dozen smaller inconsistencies; all are fixed here with matching test
coverage:
- BMK uniqueness silently allowed German-umlaut duplicates ("Ä1" vs
"ä1") because the DB's normalized index only folds ASCII case. Added
a shared Unicode-aware pre-check used by every circuit/component
insert and rename path (one of which had no pre-check at all).
- CircuitDeviceRow.simultaneityFactor had no upper bound at the row
level (command model and snapshot/restore schema), unlike every
sibling entity, letting a bad value silently corrupt power totals.
- Grid cell editing silently misread German thousands-separator input
("1.500" parsed as 1.5); "." is now rejected outright with a clear
message instead of guessing.
- The editor's shared command runner (runCommand/applyHistory) had no
re-entrancy guard, so a double click/drop could fire the same
command twice and race a BMK collision or revision conflict. Added a
synchronous ref guard plus isSaving on the buttons that lacked it.
- GET .../next-identifier leaked circuit-numbering state for sections
in other projects (no ownership check, 400 instead of 404). Moved
under /projects/:projectId and scoped it.
Also: added the missing circuits.section_id / circuit_device_rows.
circuit_id indexes (migration 0006), gave FormModal a focus trap /
Escape-to-close / focus restore and rebuilt ProjectSettingsModal on
top of it instead of duplicated markup, removed dead code (3 orphaned
domain model files, an unused persistence helper, a wrapper only used
by its own test), pointed the project page at GET /projects/:id
instead of listing+filtering client-side, closed the gap between the
documented 18 MB CSV limit and the ~17.17 MiB actually enforced, added
missing upper bounds on several free-text fields, filled in nine
missing German labels in the revision timeline, replaced a
key-order-fragile JSON.stringify equality check with a real field
comparison, made an implicit sort-order assumption in three
renumbering helpers explicit, cleared the sidebar's target selection
when it no longer resolves after a tree reload, and fixed
updateGlobalDevice to check-then-write instead of write-then-check.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fa96be2d42
commit
b45dc5002d
48 changed files with 3263 additions and 526 deletions
|
|
@ -144,6 +144,9 @@ function assertCircuitDeviceRowUpdateFieldValue(
|
|||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${field} must be a non-negative finite number.`);
|
||||
}
|
||||
if (field === "simultaneityFactor" && value > 1) {
|
||||
throw new Error("simultaneityFactor must not exceed 1.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (field === "cosPhi") {
|
||||
|
|
|
|||
|
|
@ -134,6 +134,9 @@ export function assertCircuitDeviceRowInsertProjectCommand(
|
|||
row.simultaneityFactor,
|
||||
"row.simultaneityFactor"
|
||||
);
|
||||
if (row.simultaneityFactor > 1) {
|
||||
throw new Error("row.simultaneityFactor must not exceed 1.");
|
||||
}
|
||||
if (row.cosPhi !== null) {
|
||||
assertFiniteNumber(row.cosPhi, "row.cosPhi");
|
||||
if (row.cosPhi <= 0) {
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
export interface CircuitDeviceRow {
|
||||
id: string;
|
||||
circuitId: string;
|
||||
linkedProjectDeviceId?: string;
|
||||
sortOrder: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
phaseType?: string;
|
||||
connectionKind?: string;
|
||||
costGroup?: string;
|
||||
category?: string;
|
||||
level?: string;
|
||||
roomId?: string;
|
||||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
manualQuantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
remark?: string;
|
||||
overriddenFields?: string;
|
||||
}
|
||||
|
||||
|
|
@ -71,15 +71,33 @@ export function assertCircuitProtectionUpdateProjectCommand(
|
|||
if (target !== null) {
|
||||
assertCircuitProtectionSnapshot(target, circuitId);
|
||||
}
|
||||
if (JSON.stringify(expected) === JSON.stringify(target)) {
|
||||
if (circuitProtectionSnapshotsEqual(expected, target)) {
|
||||
throw new Error("Circuit protection update must change state.");
|
||||
}
|
||||
}
|
||||
|
||||
function circuitProtectionSnapshotsEqual(
|
||||
left: CircuitProtectionSnapshot | null,
|
||||
right: CircuitProtectionSnapshot | null
|
||||
): boolean {
|
||||
if (left === null || right === null) {
|
||||
return left === right;
|
||||
}
|
||||
return (
|
||||
left.circuitId === right.circuitId &&
|
||||
left.type === right.type &&
|
||||
left.ratedCurrentA === right.ratedCurrentA &&
|
||||
left.fuseUtilizationCategory === right.fuseUtilizationCategory &&
|
||||
left.tripCharacteristic === right.tripCharacteristic &&
|
||||
left.rcdType === right.rcdType &&
|
||||
left.ratedResidualCurrentMa === right.ratedResidualCurrentMa
|
||||
);
|
||||
}
|
||||
|
||||
export function assertCircuitProtectionSnapshot(
|
||||
value: unknown,
|
||||
circuitId: string
|
||||
) {
|
||||
): asserts value is CircuitProtectionSnapshot {
|
||||
if (
|
||||
!isPlainObject(value) ||
|
||||
Object.keys(value).length !== 7 ||
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
export interface CircuitSection {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
key: string;
|
||||
displayName: string;
|
||||
prefix: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
export interface Circuit {
|
||||
id: string;
|
||||
circuitListId: string;
|
||||
sectionId: string;
|
||||
equipmentIdentifier: string;
|
||||
displayName?: string;
|
||||
sortOrder: number;
|
||||
cableType?: string;
|
||||
cableCrossSection?: string;
|
||||
cableLength?: number;
|
||||
rcdAssignment?: string;
|
||||
terminalDesignation?: string;
|
||||
voltage?: number;
|
||||
controlRequirement?: string;
|
||||
status?: string;
|
||||
isReserve: boolean;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ const circuitDeviceRowSchema = z.preprocess(
|
|||
quantity: finiteNumberSchema.nonnegative(),
|
||||
manualQuantity: finiteNumberSchema.nonnegative(),
|
||||
powerPerUnit: finiteNumberSchema.nonnegative(),
|
||||
simultaneityFactor: finiteNumberSchema.nonnegative(),
|
||||
simultaneityFactor: finiteNumberSchema.min(0).max(1),
|
||||
cosPhi: finiteNumberSchema.positive().nullable(),
|
||||
remark: nullableStringSchema,
|
||||
overriddenFields: nullableStringSchema,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue