Server-first label metadata and architecture guidance

This commit is contained in:
Jage9
2026-02-24 19:06:08 -05:00
parent 59db0a0dda
commit 60c0ced7b7
3 changed files with 22 additions and 26 deletions

View File

@@ -23,9 +23,12 @@
## Coding Style & Naming Conventions
- TypeScript: strict typing, `camelCase`, small focused modules.
- 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.
- 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.
- 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`.
## Documentation Maintenance

View File

@@ -1,5 +1,5 @@
// Maintainer-controlled web client version.
// 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.
window.CHGRID_TIME_ZONE = "America/Detroit";

View File

@@ -35,6 +35,7 @@ let itemTypeEditableProperties: Partial<Record<ItemType, string[]>> = {};
let itemTypeCapabilities: Partial<Record<ItemType, string[]>> = {};
let itemTypeGlobalProperties: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
let itemTypePropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
let propertyLabelByKey: Record<string, string> = {};
export let EDITABLE_ITEM_PROPERTY_KEYS = new Set<string>(
Object.values(itemTypeEditableProperties).flatMap((keys) => keys ?? []),
@@ -151,31 +152,14 @@ export function getItemTypeCapabilities(itemType: ItemType): string[] {
/** Returns human-facing label for a property key. */
export function itemPropertyLabel(key: string): string {
const metadataLabel = Object.values(itemTypePropertyMetadata)
.map((entry) => entry?.[key]?.label)
.find((label) => typeof label === 'string' && label.trim().length > 0);
const metadataLabel = propertyLabelByKey[key];
if (metadataLabel) return metadataLabel;
if (key === 'use24Hour') return 'use 24 hour format';
if (key === 'emitRange') return 'emit range';
if (key === 'mediaVolume') return 'media volume';
if (key === 'emitVolume') return 'emit volume';
if (key === 'emitSoundSpeed') return 'emit sound speed';
if (key === 'emitSoundTempo') return 'emit sound tempo';
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;
const words = key
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[_-]+/g, ' ')
.trim()
.toLowerCase();
return words || key;
}
/** Returns editable properties for one item instance/type. */
@@ -241,6 +225,7 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
itemTypeCapabilities = {};
itemTypeGlobalProperties = {};
itemTypePropertyMetadata = {};
propertyLabelByKey = {};
rebuildEditablePropertyKeySet();
return false;
}
@@ -256,6 +241,7 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
const nextCapabilities: Partial<Record<ItemType, string[]>> = {};
const nextGlobals: Partial<Record<ItemType, Record<string, string | number | boolean>>> = {};
const nextPropertyMetadata: Partial<Record<ItemType, Record<string, ItemPropertyMetadata>>> = {};
const nextPropertyLabels: Record<string, string> = {};
for (const definition of uiDefinitions.itemTypes) {
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');
}
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') {
const normalized: Record<string, string | number | boolean> = {};
@@ -298,6 +290,7 @@ export function applyServerItemUiDefinitions(uiDefinitions: UiDefinitionsPayload
itemTypeCapabilities = nextCapabilities;
itemTypeGlobalProperties = nextGlobals;
itemTypePropertyMetadata = nextPropertyMetadata;
propertyLabelByKey = nextPropertyLabels;
itemTypeSequence = explicitOrder ?? discoveredOrder;
rebuildEditablePropertyKeySet();
return itemTypeSequence.length > 0;