mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
eeab548917
Adds the Housekeeping in-client admin panel — a Modtools-adjacent
surface that runs entirely inside the React client, talking to the
emulator over the existing wire instead of a separate REST/CMS layer.
Surface:
- `src/components/housekeeping/` — panel shell + 5 tabs (Dashboard,
Users, Rooms, Economy, Audit). Each tab drives one domain of the
matching emulator handlers (find/sanction/admin/economy/catalog/
hotel-wide).
- `src/api/housekeeping/` — composer/parser orchestration:
`HousekeepingApi.ts` exposes 30+ typed actions, each one running
through `runHkAction()` which awaits the shared
`HousekeepingActionResultEvent` correlated by action key.
- `src/hooks/housekeeping/` — `useHousekeeping` (the public hook),
`useHousekeepingStore` (useBetween singleton: shared selection +
audit polling + sanction templates), `useHousekeepingActions`,
`useHousekeepingConfirm`.
- `src/api/nitro/awaitMessageEvent.ts` — Promise adapter over
`CommunicationManager.subscribeMessage` with a sync `select`
callback that snapshots the parser INSIDE the subscribe handler
before the renderer recycles the parser instance after the
Promise resolves.
- `public/configuration/housekeeping-texts-{en,it}.example` —
149 EN + 149 IT i18n keys under `housekeeping.*` for every panel
string + every server-side error slug the emulator may emit.
Wiring (additive only):
- `src/components/MainView.tsx` — `<HousekeepingView />` mounted
alongside `<ModToolsView />`.
- `src/api/index.ts`, `src/hooks/index.ts`, `src/api/nitro/index.ts`
— added the `housekeeping` and `awaitMessageEvent` re-exports.
Wire contract: pairs against the Arcturus PR (#120 on
duckietm/Arcturus-Morningstar-Extended) and the renderer PR (#77 on
duckietm/Nitro_Render_V3). Incoming events 9100..9129, outgoing
composers 9200..9207. Permission gate `acc_housekeeping` enforced
server-side; the panel is hidden client-side via
`housekeeping.enabled` in the runtime ui-config.
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import { GetCommunication, IMessageEvent } from '@nitrots/nitro-renderer';
|
|
|
|
export interface AwaitMessageEventInit<T extends IMessageEvent, R = T>
|
|
{
|
|
timeoutMs?: number;
|
|
signal?: AbortSignal;
|
|
accept?: (event: T) => boolean;
|
|
/**
|
|
* Synchronous mapper that runs INSIDE the subscribe callback, while
|
|
* the parser is still valid. Whatever it returns is what the Promise
|
|
* resolves to. **MUST** be used for any read of `event.getParser()` —
|
|
* the renderer recycles parser instances (the `_parser` field is
|
|
* nulled / repopulated for the next packet) so reading the parser
|
|
* AFTER the await microtask gives back null fields. Snapshot the
|
|
* data here, return a plain object/value, then your async code is
|
|
* safe.
|
|
*/
|
|
select?: (event: T) => R;
|
|
}
|
|
|
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
|
|
/**
|
|
* One-shot Promise adapter over the renderer's CommunicationManager.subscribeMessage.
|
|
* Resolves on the first matching event, rejects on timeout / abort / connection error.
|
|
* Used by request-response patterns (e.g. housekeeping lookups) that need a Promise
|
|
* facade over the underlying packet stream.
|
|
*
|
|
* **Read the parser inside `select`, not after the await.** See the
|
|
* AwaitMessageEventInit.select javadoc — the renderer recycles parsers,
|
|
* so post-await reads come back null.
|
|
*/
|
|
export const awaitMessageEvent = <T extends IMessageEvent, R = T>(eventCtor: new (callback: (event: T) => void) => T, init: AwaitMessageEventInit<T, R> = {}): Promise<R> =>
|
|
{
|
|
const { timeoutMs = DEFAULT_TIMEOUT_MS, signal, accept, select } = init;
|
|
|
|
return new Promise<R>((resolve, reject) =>
|
|
{
|
|
if(signal?.aborted)
|
|
{
|
|
reject(new DOMException('aborted', 'AbortError'));
|
|
|
|
return;
|
|
}
|
|
|
|
const communication = GetCommunication();
|
|
|
|
if(!communication || !communication.connection)
|
|
{
|
|
reject(new Error('no_connection'));
|
|
|
|
return;
|
|
}
|
|
|
|
let settled = false;
|
|
let unsubscribe: (() => void) | null = null;
|
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
let onAbort: (() => void) | null = null;
|
|
|
|
const cleanup = () =>
|
|
{
|
|
settled = true;
|
|
if(unsubscribe) unsubscribe();
|
|
unsubscribe = null;
|
|
if(timer) clearTimeout(timer);
|
|
timer = null;
|
|
if(onAbort && signal) signal.removeEventListener('abort', onAbort);
|
|
onAbort = null;
|
|
};
|
|
|
|
unsubscribe = communication.subscribeMessage(eventCtor, event =>
|
|
{
|
|
if(settled) return;
|
|
|
|
if(accept && !accept(event)) return;
|
|
|
|
// Snapshot the data synchronously: post-await reads of the
|
|
// event's parser come back null because the renderer recycles
|
|
// parser instances between packets. If no select supplied,
|
|
// resolve with the raw event for backwards-compat callers
|
|
// that don't touch the parser.
|
|
let snapshot: R;
|
|
|
|
try
|
|
{
|
|
snapshot = select ? select(event) : (event as unknown as R);
|
|
}
|
|
catch(err)
|
|
{
|
|
cleanup();
|
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
|
|
return;
|
|
}
|
|
|
|
cleanup();
|
|
resolve(snapshot);
|
|
});
|
|
|
|
timer = setTimeout(() =>
|
|
{
|
|
if(settled) return;
|
|
cleanup();
|
|
reject(new Error('timeout'));
|
|
}, timeoutMs);
|
|
|
|
if(signal)
|
|
{
|
|
onAbort = () =>
|
|
{
|
|
if(settled) return;
|
|
cleanup();
|
|
reject(new DOMException('aborted', 'AbortError'));
|
|
};
|
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
}
|
|
});
|
|
};
|