mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
feat(housekeeping): in-client admin panel
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.
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
findTemplateById, HK_SANCTION_TEMPLATES, HousekeepingSanctionType, templatesByType
|
||||
} from './HousekeepingSanctionTemplates';
|
||||
|
||||
describe('HK_SANCTION_TEMPLATES', () =>
|
||||
{
|
||||
it('has a unique id for every template', () =>
|
||||
{
|
||||
const ids = HK_SANCTION_TEMPLATES.map(t => t.id);
|
||||
const unique = new Set(ids);
|
||||
|
||||
expect(unique.size).toBe(ids.length);
|
||||
});
|
||||
|
||||
it('covers every sanction type at least once', () =>
|
||||
{
|
||||
const types = new Set(HK_SANCTION_TEMPLATES.map(t => t.type));
|
||||
|
||||
expect(types.has(HousekeepingSanctionType.BAN)).toBe(true);
|
||||
expect(types.has(HousekeepingSanctionType.MUTE)).toBe(true);
|
||||
expect(types.has(HousekeepingSanctionType.KICK)).toBe(true);
|
||||
expect(types.has(HousekeepingSanctionType.TRADE_LOCK)).toBe(true);
|
||||
});
|
||||
|
||||
it('uses durationValue=0 for KICK templates only (kick is instant, no duration)', () =>
|
||||
{
|
||||
for(const template of HK_SANCTION_TEMPLATES)
|
||||
{
|
||||
if(template.type === HousekeepingSanctionType.KICK) expect(template.durationValue).toBe(0);
|
||||
else expect(template.durationValue).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('every template has a non-empty default reason (avoids empty-reason validation failures)', () =>
|
||||
{
|
||||
for(const template of HK_SANCTION_TEMPLATES)
|
||||
{
|
||||
expect(template.defaultReason.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('findTemplateById', () =>
|
||||
{
|
||||
it('returns the matching template', () =>
|
||||
{
|
||||
expect(findTemplateById('ban_24h')?.type).toBe(HousekeepingSanctionType.BAN);
|
||||
expect(findTemplateById('ban_24h')?.durationValue).toBe(24);
|
||||
});
|
||||
|
||||
it('returns null for an unknown id', () =>
|
||||
{
|
||||
expect(findTemplateById('does-not-exist')).toBeNull();
|
||||
expect(findTemplateById('')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('templatesByType', () =>
|
||||
{
|
||||
it('filters the list down to a single type', () =>
|
||||
{
|
||||
const bans = templatesByType(HousekeepingSanctionType.BAN);
|
||||
|
||||
expect(bans.length).toBeGreaterThan(0);
|
||||
expect(bans.every(t => t.type === HousekeepingSanctionType.BAN)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns an empty list for unknown types (defensive)', () =>
|
||||
{
|
||||
expect(templatesByType('unknown' as never)).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user