diff --git a/apps/web/src/app/(portal)/modules/proxmox/page.tsx b/apps/web/src/app/(portal)/modules/proxmox/page.tsx
index 3a341e8..569fd94 100644
--- a/apps/web/src/app/(portal)/modules/proxmox/page.tsx
+++ b/apps/web/src/app/(portal)/modules/proxmox/page.tsx
@@ -11,7 +11,9 @@ import { ServerCard } from './components/ServerCard';
* Modulseite (Aufgabe 6) — liest ausschliesslich aus dem Zwischenlager, das
* `GET servers` liefert; kein Live-Zugriff bei Proxmox von hier aus (D-05).
* "Jetzt aktualisieren" loest je Server eine Abfrage aus und laedt die
- * Liste danach neu — waehrend des Laufs ist der Knopf gesperrt.
+ * Liste danach neu — waehrend des Laufs ist der Knopf gesperrt. Der Knopf
+ * erscheint nur fuer Admins, weil `POST servers/:id/poll` `@Roles(ADMIN,
+ * SUPER_ADMIN)` verlangt; fuer andere waere er wirkungslos (260923-le6).
*/
export default function ProxmoxPage() {
const t = useTranslations('proxmox');
@@ -51,7 +53,7 @@ export default function ProxmoxPage() {
{t('description')}
- {servers !== null && servers.length > 0 && (
+ {isAdmin && servers !== null && servers.length > 0 && (
{servers.map((server) => (
-
+
))}
diff --git a/apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx b/apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx
new file mode 100644
index 0000000..3c471cc
--- /dev/null
+++ b/apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx
@@ -0,0 +1,178 @@
+import { cleanup, render, screen, waitFor } from '@testing-library/react';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+import type { ProxmoxServer } from '@/lib/proxmox-api';
+
+// Mock @/lib/proxmox-api — die Seite ruft `listServers` beim Laden auf,
+// "Jetzt aktualisieren" ruft `pollServer` je Server.
+const mockListServers = vi.fn();
+const mockPollServer = vi.fn();
+
+vi.mock('@/lib/proxmox-api', () => ({
+ listServers: (...args: unknown[]) => mockListServers(...args),
+ pollServer: (...args: unknown[]) => mockPollServer(...args),
+}));
+
+// Mock the auth store — mirrors settings-roles.test.tsx (selector-passthrough).
+const mockAuthStore = vi.fn();
+vi.mock('@/lib/stores/auth-store', () => ({
+ useAuthStore: (selector: (state: unknown) => unknown) => mockAuthStore(selector),
+}));
+
+vi.mock('next/link', () => ({
+ default: ({ href, children, ...rest }: { href: string; children: React.ReactNode }) => (
+
+ {children}
+
+ ),
+}));
+
+vi.mock('next-intl', () => ({
+ useTranslations: () => (key: string, params?: Record) => {
+ const translations: Record = {
+ title: 'Proxmox',
+ description: 'Zustand Ihrer Proxmox-Server (PVE/PBS/PMG) auf einen Blick.',
+ loading: 'Lade Serverliste...',
+ loadError: 'Die Serverliste konnte nicht geladen werden.',
+ emptyState:
+ 'Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an.',
+ 'card.refresh': 'Jetzt aktualisieren',
+ 'card.refreshing': 'Wird aktualisiert...',
+ 'card.settingsLink': 'Zu den Einstellungen',
+ 'card.unknownValue': 'unbekannt',
+ 'card.lastPolledLabel': 'Letzte Abfrage',
+ 'card.notPolledYet': 'Noch keine Abfrage gelaufen. Klicken Sie oben auf „{refreshLabel}“.',
+ 'card.notPolledYetAutomatic':
+ 'Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.',
+ };
+ let result = translations[key] ?? key;
+ if (params) {
+ for (const [k, v] of Object.entries(params)) {
+ result = result.replace(`{${k}}`, String(v));
+ }
+ }
+ return result;
+ },
+}));
+
+function mockUser(user: { role: 'SUPER_ADMIN' | 'ADMIN' | 'USER' } | null) {
+ mockAuthStore.mockImplementation((selector: (state: { user: typeof user }) => unknown) =>
+ selector({ user }),
+ );
+}
+
+// Ein nie abgefragter Server (wie im Befund-2-Test von ServerCard.test.tsx):
+// leere Zwischenlagerzeile direkt nach `createServer`.
+function makeUnpolledServer(overrides: Partial = {}): ProxmoxServer {
+ return {
+ id: 'srv-1',
+ tenantId: 't1',
+ name: 'pve-1',
+ productType: 'pve',
+ baseUrl: 'https://pve.intern',
+ authMethod: 'token',
+ tokenId: 'root@pam!x',
+ username: null,
+ tlsRejectUnauthorized: true,
+ isActive: true,
+ pollIntervalMin: 5,
+ position: 0,
+ createdAt: '2026-01-01T00:00:00.000Z',
+ updatedAt: '2026-01-01T00:00:00.000Z',
+ status: {
+ id: 's1',
+ serverId: 'srv-1',
+ lastPolledAt: null,
+ lastOkAt: null,
+ reachable: false,
+ errorKind: null,
+ errorDetail: null,
+ rawSample: null,
+ updatedAt: '2026-01-01T00:00:00.000Z',
+ metrics: null,
+ },
+ ...overrides,
+ } as ProxmoxServer;
+}
+
+afterEach(() => {
+ cleanup();
+ mockListServers.mockReset();
+ mockPollServer.mockReset();
+ mockAuthStore.mockReset();
+});
+
+describe('ProxmoxPage role gating (260923-le6)', () => {
+ it('Rolle USER: kein Knopf "Jetzt aktualisieren", Karten-Hinweis ist der automatische Text', async () => {
+ mockUser({ role: 'USER' });
+ mockListServers.mockResolvedValue([makeUnpolledServer()]);
+
+ const { default: ProxmoxPage } = await import('./page');
+ render( );
+
+ await screen.findByText('pve-1');
+
+ expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
+ expect(
+ screen.getByText(
+ 'Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.',
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it('kein Benutzer geladen (user: null): kein Knopf', async () => {
+ mockUser(null);
+ mockListServers.mockResolvedValue([makeUnpolledServer()]);
+
+ const { default: ProxmoxPage } = await import('./page');
+ render( );
+
+ await screen.findByText('pve-1');
+
+ expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
+ });
+
+ it('Rolle ADMIN: Knopf sichtbar, Karten-Hinweis ist der Admin-Text mit Knopfverweis', async () => {
+ mockUser({ role: 'ADMIN' });
+ mockListServers.mockResolvedValue([makeUnpolledServer()]);
+
+ const { default: ProxmoxPage } = await import('./page');
+ render( );
+
+ await screen.findByText('pve-1');
+
+ expect(screen.getByRole('button', { name: 'Jetzt aktualisieren' })).toBeInTheDocument();
+ expect(
+ screen.getByText('Noch keine Abfrage gelaufen. Klicken Sie oben auf „Jetzt aktualisieren“.'),
+ ).toBeInTheDocument();
+ });
+
+ it('Rolle SUPER_ADMIN: Knopf sichtbar', async () => {
+ mockUser({ role: 'SUPER_ADMIN' });
+ mockListServers.mockResolvedValue([makeUnpolledServer()]);
+
+ const { default: ProxmoxPage } = await import('./page');
+ render( );
+
+ await screen.findByText('pve-1');
+
+ expect(screen.getByRole('button', { name: 'Jetzt aktualisieren' })).toBeInTheDocument();
+ });
+
+ it('leere Serverliste bei ADMIN: weiterhin kein Knopf', async () => {
+ mockUser({ role: 'ADMIN' });
+ mockListServers.mockResolvedValue([]);
+
+ const { default: ProxmoxPage } = await import('./page');
+ render( );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText(
+ 'Noch kein Server eingetragen. Legen Sie in den Moduleinstellungen einen Server an.',
+ ),
+ ).toBeInTheDocument();
+ });
+
+ expect(screen.queryByRole('button', { name: 'Jetzt aktualisieren' })).not.toBeInTheDocument();
+ });
+});