Server-first label metadata and architecture guidance
This commit is contained in:
@@ -23,9 +23,12 @@
|
|||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
- TypeScript: strict typing, `camelCase`, small focused modules.
|
- TypeScript: strict typing, `camelCase`, small focused modules.
|
||||||
- Python: PEP 8, 4 spaces, `snake_case`, typed Pydantic models.
|
- Python: PEP 8, 4 spaces, `snake_case`, typed Pydantic models.
|
||||||
|
- Architecture: server-first by default. Keep grid/world rules, authoritative validation, and canonical definitions on server whenever practical.
|
||||||
|
- Client scope: UI/UX, rendering, input, and audio presentation. Avoid client-owned gameplay/business rules when server can own them.
|
||||||
- Backward compatibility: not required during current development phase; prefer simpler clean-cut changes over compatibility shims/migrations unless the user asks otherwise.
|
- Backward compatibility: not required during current development phase; prefer simpler clean-cut changes over compatibility shims/migrations unless the user asks otherwise.
|
||||||
- Python docstrings: for `server/app` changes, include module docstring, class docstring, and docstrings for public functions/methods where behavior/contracts matter.
|
- Python docstrings: for `server/app` changes, include module docstring, class docstring, and docstrings for public functions/methods where behavior/contracts matter.
|
||||||
- Shared logic first: when behavior is reused across modes/features, implement it in shared helpers/modules rather than duplicating branch-specific logic.
|
- Shared logic first: when behavior is reused across modes/features, implement it in shared helpers/modules rather than duplicating branch-specific logic.
|
||||||
|
- Keep `main.ts` as orchestration glue. Move reusable feature logic to focused modules; ask before large/structural refactors.
|
||||||
- Keep protocol changes synced in `client/src/network/protocol.ts` and `server/app/models.py`.
|
- Keep protocol changes synced in `client/src/network/protocol.ts` and `server/app/models.py`.
|
||||||
|
|
||||||
## Documentation Maintenance
|
## Documentation Maintenance
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.24 R228";
|
window.CHGRID_WEB_VERSION = "2026.02.25 R229";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ let itemTypeEditableProperties: Partial<Record<ItemType, string[]>> = {};
|
|||||||
let itemTypeCapabilities: Partial<Record<ItemType, string[]>> = {};
|
let itemTypeCapabilities: Partial<Record<ItemType, string[]>> = {};
|
||||||
let itemTypeGlobalProperties: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
let itemTypeGlobalProperties: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
||||||
let itemTypePropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
let itemTypePropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
||||||
|
let propertyLabelByKey: Record<string, string> = {};
|
||||||
|
|
||||||
export let EDITABLE_ITEM_PROPERTY_KEYS = new Set<string>(
|
export let EDITABLE_ITEM_PROPERTY_KEYS = new Set<string>(
|
||||||
Object.values(itemTypeEditableProperties).flatMap((keys) => keys ?? []),
|
Object.values(itemTypeEditableProperties).flatMap((keys) => keys ?? []),
|
||||||
@@ -151,31 +152,14 @@ export function getItemTypeCapabilities(itemType: ItemType): string[] {
|
|||||||
|
|
||||||
/** Returns human-facing label for a property key. */
|
/** Returns human-facing label for a property key. */
|
||||||
export function itemPropertyLabel(key: string): string {
|
export function itemPropertyLabel(key: string): string {
|
||||||
const metadataLabel = Object.values(itemTypePropertyMetadata)
|
const metadataLabel = propertyLabelByKey[key];
|
||||||
.map((entry) => entry?.[key]?.label)
|
|
||||||
.find((label) => typeof label === 'string' && label.trim().length > 0);
|
|
||||||
if (metadataLabel) return metadataLabel;
|
if (metadataLabel) return metadataLabel;
|
||||||
if (key === 'use24Hour') return 'use 24 hour format';
|
const words = key
|
||||||
if (key === 'emitRange') return 'emit range';
|
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||||||
if (key === 'mediaVolume') return 'media volume';
|
.replace(/[_-]+/g, ' ')
|
||||||
if (key === 'emitVolume') return 'emit volume';
|
.trim()
|
||||||
if (key === 'emitSoundSpeed') return 'emit sound speed';
|
.toLowerCase();
|
||||||
if (key === 'emitSoundTempo') return 'emit sound tempo';
|
return words || key;
|
||||||
if (key === 'mediaChannel') return 'media channel';
|
|
||||||
if (key === 'mediaEffect') return 'media effect';
|
|
||||||
if (key === 'mediaEffectValue') return 'media effect value';
|
|
||||||
if (key === 'emitEffect') return 'emit effect';
|
|
||||||
if (key === 'emitEffectValue') return 'emit effect value';
|
|
||||||
if (key === 'instrument') return 'instrument';
|
|
||||||
if (key === 'voiceMode') return 'voice mode';
|
|
||||||
if (key === 'octave') return 'octave';
|
|
||||||
if (key === 'attack') return 'attack';
|
|
||||||
if (key === 'decay') return 'decay';
|
|
||||||
if (key === 'release') return 'release';
|
|
||||||
if (key === 'brightness') return 'brightness';
|
|
||||||
if (key === 'useSound') return 'use sound';
|
|
||||||
if (key === 'emitSound') return 'emit sound';
|
|
||||||
return key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns editable properties for one item instance/type. */
|
/** Returns editable properties for one item instance/type. */
|
||||||
@@ -241,6 +225,7 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
itemTypeCapabilities = {};
|
itemTypeCapabilities = {};
|
||||||
itemTypeGlobalProperties = {};
|
itemTypeGlobalProperties = {};
|
||||||
itemTypePropertyMetadata = {};
|
itemTypePropertyMetadata = {};
|
||||||
|
propertyLabelByKey = {};
|
||||||
rebuildEditablePropertyKeySet();
|
rebuildEditablePropertyKeySet();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -256,6 +241,7 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
const nextCapabilities: Partial<Record<ItemType, string[]>> = {};
|
const nextCapabilities: Partial<Record<ItemType, string[]>> = {};
|
||||||
const nextGlobals: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
const nextGlobals: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
|
||||||
const nextPropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
const nextPropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
|
||||||
|
const nextPropertyLabels: Record<string, string> = {};
|
||||||
|
|
||||||
for (const definition of uiDefinitions.itemTypes) {
|
for (const definition of uiDefinitions.itemTypes) {
|
||||||
if (!definition || typeof definition.type !== 'string') continue;
|
if (!definition || typeof definition.type !== 'string') continue;
|
||||||
@@ -273,7 +259,13 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
nextCapabilities[itemType] = definition.capabilities.filter((entry) => typeof entry === 'string');
|
nextCapabilities[itemType] = definition.capabilities.filter((entry) => typeof entry === 'string');
|
||||||
}
|
}
|
||||||
if (definition.propertyMetadata && typeof definition.propertyMetadata === 'object') {
|
if (definition.propertyMetadata && typeof definition.propertyMetadata === 'object') {
|
||||||
nextPropertyMetadata[itemType] = normalizePropertyMetadataRecord(definition.propertyMetadata);
|
const normalizedMetadata = normalizePropertyMetadataRecord(definition.propertyMetadata);
|
||||||
|
nextPropertyMetadata[itemType] = normalizedMetadata;
|
||||||
|
for (const [propertyKey, propertyMetadata] of Object.entries(normalizedMetadata)) {
|
||||||
|
if (typeof propertyMetadata.label === 'string' && propertyMetadata.label.trim().length > 0) {
|
||||||
|
nextPropertyLabels[propertyKey] = propertyMetadata.label.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (definition.globalProperties && typeof definition.globalProperties === 'object') {
|
if (definition.globalProperties && typeof definition.globalProperties === 'object') {
|
||||||
const normalized: Record<string, string | number | boolean> = {};
|
const normalized: Record<string, string | number | boolean> = {};
|
||||||
@@ -298,6 +290,7 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
|
|||||||
itemTypeCapabilities = nextCapabilities;
|
itemTypeCapabilities = nextCapabilities;
|
||||||
itemTypeGlobalProperties = nextGlobals;
|
itemTypeGlobalProperties = nextGlobals;
|
||||||
itemTypePropertyMetadata = nextPropertyMetadata;
|
itemTypePropertyMetadata = nextPropertyMetadata;
|
||||||
|
propertyLabelByKey = nextPropertyLabels;
|
||||||
itemTypeSequence = explicitOrder ?? discoveredOrder;
|
itemTypeSequence = explicitOrder ?? discoveredOrder;
|
||||||
rebuildEditablePropertyKeySet();
|
rebuildEditablePropertyKeySet();
|
||||||
return itemTypeSequence.length > 0;
|
return itemTypeSequence.length > 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user