feat(260924-i8v): Proxmox-Kachel als Durchstich mit Balken und Zusammenfassung

- Typ proxmox am Ende von WIDGET_TYPES, WIDGET_MODULE_SLUGS = { proxmox: 'proxmox' }
- gemeinsame Statusteile (proxmox-status, HealthBar, status-styles) nach components/proxmox/
- HealthBar variant compact: 6 px, ohne Legende, aria-hidden
- Registry 3/4/8/8, Katalog-/Registry-/API-Tests auf die erste Modul-Kachel umgestellt
- ProxmoxWidget liest nur listServers, zeigt Lade-, Leer-, Fehlerzustand

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 13:24:51 +02:00
parent 8bfa4fc467
commit a906c6705d
19 changed files with 556 additions and 69 deletions
@@ -27,6 +27,8 @@ vi.mock('next-intl', () => ({
'pictureFrame.description': 'Bilder als Diashow',
'xframe.name': 'XFrame',
'xframe.description': 'Webseite einbetten',
'proxmox.name': 'Proxmox',
'proxmox.description': 'Zustand Ihrer Proxmox-Server auf einen Blick',
},
common: {
close: 'Schließen',
@@ -40,8 +42,9 @@ import { WIDGET_TYPES } from '@tessera/shared';
import { WIDGET_REGISTRY } from './widget-registry';
import { WidgetCatalogModal } from './widget-catalog-modal';
// Alle neun Kacheln sind heute Plattform-Kacheln ohne moduleSlug, also zeigt
// der Katalog sie auch bei leerer Modulliste vollstaendig an.
// Neun Kacheln sind Plattform-Kacheln ohne moduleSlug und erscheinen auch bei
// leerer Modulliste; die Proxmox-Kachel (quick-260924-i8v) nur mit Zugriff
// auf das Modul 'proxmox'.
const baseProps = {
isOpen: true,
onClose: vi.fn(),
@@ -141,44 +144,39 @@ describe('WidgetCatalogModal', () => {
* sie aus der Registry ab und filtert nach Modulzugriff.
*/
describe('WidgetCatalogModal: Liste kommt aus der Registry (quick-260922-m1h)', () => {
it('zeigt alle neun Kacheln in der Reihenfolge der Registry', () => {
render(<WidgetCatalogModal {...baseProps} />);
function catalogTypes(): (string | null)[] {
const dialog = screen.getByRole('dialog', { name: 'Widget hinzufügen' });
const cards = Array.from(
return Array.from(
dialog.querySelectorAll<HTMLButtonElement>('button[data-widget-type]'),
);
).map((c) => c.getAttribute('data-widget-type'));
}
expect(cards.map((c) => c.getAttribute('data-widget-type'))).toEqual([
...WIDGET_TYPES,
]);
it("zeigt mit Zugriff auf 'proxmox' alle zehn Kacheln in der Reihenfolge der Registry, Proxmox zuletzt", () => {
render(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={['proxmox']} />);
expect(catalogTypes()).toEqual([...WIDGET_TYPES]);
expect(catalogTypes().at(-1)).toBe('proxmox');
expect(Object.keys(WIDGET_REGISTRY)).toEqual([...WIDGET_TYPES]);
});
it('eine Kachel MIT moduleSlug fehlt, wenn das Modul nicht zugaenglich ist, und erscheint, wenn doch', () => {
// Die Registry traegt heute keine Modul-Kachel — fuer den Nachweis am
// echten Bauteil wird clock voruebergehend zu einer gemacht.
WIDGET_REGISTRY.clock.moduleSlug = 'proxmox';
try {
const { rerender } = render(<WidgetCatalogModal {...baseProps} />);
expect(screen.queryByRole('button', { name: /Uhr/ })).toBeNull();
it('zeigt ohne Modulzugriff die neun Plattform-Kacheln ohne Proxmox', () => {
render(<WidgetCatalogModal {...baseProps} />);
rerender(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={['proxmox']} />);
expect(screen.getByRole('button', { name: /Uhr/ })).toBeInTheDocument();
} finally {
WIDGET_REGISTRY.clock.moduleSlug = undefined;
}
expect(catalogTypes()).toEqual(WIDGET_TYPES.filter((type) => type !== 'proxmox'));
});
it('fail-closed: schlaegt der Modulabruf fehl (null), verschwinden Kacheln MIT moduleSlug, Plattform-Kacheln bleiben', () => {
WIDGET_REGISTRY.clock.moduleSlug = 'proxmox';
try {
render(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={null} />);
it('die Kachel „Proxmox“ fehlt, wenn das Modul nicht zugaenglich ist, und erscheint, wenn doch', () => {
const { rerender } = render(<WidgetCatalogModal {...baseProps} />);
expect(screen.queryByRole('button', { name: /Proxmox/ })).toBeNull();
expect(screen.queryByRole('button', { name: /Uhr/ })).toBeNull();
expect(screen.getByRole('button', { name: /Notiz/ })).toBeInTheDocument();
} finally {
WIDGET_REGISTRY.clock.moduleSlug = undefined;
}
rerender(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={['proxmox']} />);
expect(screen.getByRole('button', { name: /Proxmox/ })).toBeInTheDocument();
});
it('fail-closed: schlaegt der Modulabruf fehl (null), fehlt „Proxmox“, Plattform-Kacheln bleiben', () => {
render(<WidgetCatalogModal {...baseProps} accessibleModuleSlugs={null} />);
expect(screen.queryByRole('button', { name: /Proxmox/ })).toBeNull();
expect(screen.getByRole('button', { name: /Notiz/ })).toBeInTheDocument();
});
});