feat(nextcloud-status): Clouds verwalten, Logos, stündliche Prüfung und Sortierung

- Ändern, Entfernen, Logo-Upload und -Abruf, Sammelprüfung nur mit Verwalten
- Stündlicher Auftrag beim Start registriert, Prüfung je Cloud an ihren Mandanten gebunden
- Sortierung nach Kundenname, Status, Version und Support-Ende, je Benutzer gemerkt
- Formular zum Anlegen und Bearbeiten, Zugriffsinventar angepasst

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-02 14:54:58 +02:00
parent 5ef7b0c6cc
commit 6698ef1a92
23 changed files with 2061 additions and 30 deletions
@@ -15,15 +15,37 @@ describe('NextcloudStatusController Metadaten', () => {
expect(Reflect.getMetadata(GUARDS_METADATA, NextcloudStatusController)).toContain(ModuleGuard);
});
it('list bleibt auf Benutzen-Ebene', () => {
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto.list as object)).toBeUndefined();
expect(Reflect.getMetadata(ROLES_KEY, proto.list as object)).toBeUndefined();
it.each(['list', 'logo'])('%s bleibt auf Benutzen-Ebene', (name) => {
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name] as object)).toBeUndefined();
expect(Reflect.getMetadata(ROLES_KEY, proto[name] as object)).toBeUndefined();
});
it.each(['create', 'checkOne'])('%s verlangt Verwalten ohne Rollen-Decorator', (name) => {
it.each([
'create',
'update',
'remove',
'checkAll',
'checkOne',
'uploadLogo',
'removeLogo',
])('%s verlangt Verwalten ohne Rollen-Decorator', (name) => {
const fn = proto[name] as object;
expect(Reflect.getMetadata(MODULE_MANAGE_KEY, fn)).toBe(true);
expect(Reflect.getMetadata(MODULE_SLUG_KEY, fn)).toBe('nextcloud-status');
expect(Reflect.getMetadata(ROLES_KEY, fn)).toBeUndefined();
});
it('die statische Route POST instances/check steht vor jedem Handler mit :id (kein 404-Shadowing)', () => {
const names = Object.getOwnPropertyNames(NextcloudStatusController.prototype).filter(
(n) => n !== 'constructor' && typeof proto[n] === 'function',
);
const pathOf = (n: string) => Reflect.getMetadata('path', proto[n] as object) as string;
expect(pathOf('checkAll')).toBe('instances/check');
const checkIndex = names.indexOf('checkAll');
const idHandlers = names.filter((n) => (pathOf(n) ?? '').includes(':id'));
expect(idHandlers.length).toBeGreaterThan(0);
for (const name of idHandlers) {
expect(checkIndex, `checkAll vor ${name}`).toBeLessThan(names.indexOf(name));
}
});
});