mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +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.
108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { formatCompactNumber, formatRelativePast, formatUptime } from './HousekeepingFormatters';
|
|
|
|
describe('formatUptime', () =>
|
|
{
|
|
it('renders 0/negative/NaN/Infinity as "—"', () =>
|
|
{
|
|
expect(formatUptime(-1)).toBe('—');
|
|
expect(formatUptime(NaN)).toBe('—');
|
|
expect(formatUptime(Infinity)).toBe('—');
|
|
});
|
|
|
|
it('renders seconds only for the fresh-boot case', () =>
|
|
{
|
|
expect(formatUptime(0)).toBe('0s');
|
|
expect(formatUptime(45)).toBe('45s');
|
|
});
|
|
|
|
it('renders minutes (no hour part) when below 1h', () =>
|
|
{
|
|
expect(formatUptime(60)).toBe('1m');
|
|
expect(formatUptime(60 * 59)).toBe('59m');
|
|
});
|
|
|
|
it('renders hours + minutes when below 1 day', () =>
|
|
{
|
|
expect(formatUptime(60 * 60)).toBe('1h 0m');
|
|
expect(formatUptime((60 * 60 * 5) + (60 * 12))).toBe('5h 12m');
|
|
});
|
|
|
|
it('renders days + hours + minutes when over a day', () =>
|
|
{
|
|
const fiveDaysTwelveHoursThreeMinutes = (5 * 86400) + (12 * 3600) + (3 * 60);
|
|
|
|
expect(formatUptime(fiveDaysTwelveHoursThreeMinutes)).toBe('5d 12h 3m');
|
|
});
|
|
});
|
|
|
|
describe('formatRelativePast', () =>
|
|
{
|
|
const NOW = 1_700_000_000_000; // fixed reference
|
|
|
|
it('renders "—" for invalid input', () =>
|
|
{
|
|
expect(formatRelativePast(0, NOW)).toBe('—');
|
|
expect(formatRelativePast(-100, NOW)).toBe('—');
|
|
expect(formatRelativePast(NaN, NOW)).toBe('—');
|
|
});
|
|
|
|
it('renders "now" for the first 5 seconds', () =>
|
|
{
|
|
expect(formatRelativePast(NOW - 1_000, NOW)).toBe('now');
|
|
expect(formatRelativePast(NOW - 4_000, NOW)).toBe('now');
|
|
});
|
|
|
|
it('renders seconds-ago between 5s and 1m', () =>
|
|
{
|
|
expect(formatRelativePast(NOW - 10_000, NOW)).toBe('10s ago');
|
|
expect(formatRelativePast(NOW - 59_000, NOW)).toBe('59s ago');
|
|
});
|
|
|
|
it('renders minutes / hours / days as we cross each unit boundary', () =>
|
|
{
|
|
expect(formatRelativePast(NOW - (60 * 1000), NOW)).toBe('1m ago');
|
|
expect(formatRelativePast(NOW - (3600 * 1000), NOW)).toBe('1h ago');
|
|
expect(formatRelativePast(NOW - (86_400 * 1000), NOW)).toBe('1d ago');
|
|
expect(formatRelativePast(NOW - (3 * 86_400 * 1000), NOW)).toBe('3d ago');
|
|
});
|
|
|
|
it('switches to a fixed ISO-date prefix beyond 7 days', () =>
|
|
{
|
|
const tenDaysAgoMs = NOW - (10 * 86_400 * 1000);
|
|
const expected = new Date(tenDaysAgoMs).toISOString().slice(0, 10);
|
|
|
|
expect(formatRelativePast(tenDaysAgoMs, NOW)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('formatCompactNumber', () =>
|
|
{
|
|
it('returns "—" for non-finite input', () =>
|
|
{
|
|
expect(formatCompactNumber(NaN)).toBe('—');
|
|
expect(formatCompactNumber(Infinity)).toBe('—');
|
|
});
|
|
|
|
it('passes through small values', () =>
|
|
{
|
|
expect(formatCompactNumber(0)).toBe('0');
|
|
expect(formatCompactNumber(42)).toBe('42');
|
|
expect(formatCompactNumber(999)).toBe('999');
|
|
});
|
|
|
|
it('uses K from 1_000 onwards (drops decimals at 10K+ for readability)', () =>
|
|
{
|
|
expect(formatCompactNumber(1_000)).toBe('1.0K');
|
|
expect(formatCompactNumber(1_500)).toBe('1.5K');
|
|
expect(formatCompactNumber(12_345)).toBe('12K');
|
|
});
|
|
|
|
it('uses M from 1_000_000 onwards (drops decimals at 10M+)', () =>
|
|
{
|
|
expect(formatCompactNumber(1_000_000)).toBe('1.0M');
|
|
expect(formatCompactNumber(2_300_000)).toBe('2.3M');
|
|
expect(formatCompactNumber(15_000_000)).toBe('15M');
|
|
});
|
|
});
|