From 8306992533191759c12c03a9a64a676f6bd1cbce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Wed, 3 Dec 2025 19:03:19 +0100 Subject: [PATCH] Simple Managment webapp [LLM] --- webapp/angular.json | 29 +---- webapp/src/app/core/models/message.model.ts | 1 + .../src/app/core/models/sender-name.model.ts | 3 +- .../features/auth/login/login.component.html | 2 +- .../channel-detail.component.html | 28 +--- .../channel-detail.component.scss | 21 ++- .../channel-detail.component.ts | 41 +++--- .../message-list/message-list.component.html | 16 ++- .../message-list/message-list.component.scss | 11 +- .../message-list/message-list.component.ts | 46 ++++--- .../sender-list/sender-list.component.html | 123 ++++++++++++------ .../sender-list/sender-list.component.ts | 58 +++++++-- .../main-layout/main-layout.component.html | 4 +- .../main-layout/main-layout.component.scss | 12 +- webapp/{public => src/assets}/favicon.ico | Bin 15 files changed, 233 insertions(+), 162 deletions(-) rename webapp/{public => src/assets}/favicon.ico (100%) diff --git a/webapp/angular.json b/webapp/angular.json index cbb6394..0dc0d1a 100644 --- a/webapp/angular.json +++ b/webapp/angular.json @@ -48,10 +48,7 @@ "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "assets": [ - { - "glob": "**/*", - "input": "public" - } + {"input": "src/assets", "output": ".", "glob": "**/*" } ], "styles": [ "src/styles.scss" @@ -96,30 +93,6 @@ } }, "defaultConfiguration": "development" - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n" - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "polyfills": [ - "zone.js", - "zone.js/testing" - ], - "tsConfig": "tsconfig.spec.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "public" - } - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - } } } } diff --git a/webapp/src/app/core/models/message.model.ts b/webapp/src/app/core/models/message.model.ts index 89de14b..78ab067 100644 --- a/webapp/src/app/core/models/message.model.ts +++ b/webapp/src/app/core/models/message.model.ts @@ -32,4 +32,5 @@ export interface MessageListResponse { messages: Message[]; next_page_token: string; page_size: number; + total_count: number; } diff --git a/webapp/src/app/core/models/sender-name.model.ts b/webapp/src/app/core/models/sender-name.model.ts index 5b7ede8..6307781 100644 --- a/webapp/src/app/core/models/sender-name.model.ts +++ b/webapp/src/app/core/models/sender-name.model.ts @@ -1,9 +1,10 @@ export interface SenderNameStatistics { name: string; + first_timestamp: string; last_timestamp: string; count: number; } export interface SenderNameListResponse { - senders: SenderNameStatistics[]; + sender_names: SenderNameStatistics[]; } diff --git a/webapp/src/app/features/auth/login/login.component.html b/webapp/src/app/features/auth/login/login.component.html index 7468219..b2c73a3 100644 --- a/webapp/src/app/features/auth/login/login.component.html +++ b/webapp/src/app/features/auth/login/login.component.html @@ -1,7 +1,7 @@
} @@ -238,15 +234,3 @@ - - - - -

Scan this QR code with the SimpleCloudNotifier app to subscribe to this channel.

-
-
diff --git a/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss b/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss index db9dcc0..e22c986 100644 --- a/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss +++ b/webapp/src/app/features/channels/channel-detail/channel-detail.component.scss @@ -44,9 +44,28 @@ } } +.qr-section { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid #f0f0f0; + + label { + display: block; + font-weight: 500; + margin-bottom: 12px; + color: #333; + } + + app-qr-code-display { + display: flex; + justify-content: center; + } +} + .qr-hint { text-align: center; color: #666; font-size: 13px; - margin-top: 16px; + margin-top: 12px; + margin-bottom: 0; } diff --git a/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts b/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts index 21d57a5..0989501 100644 --- a/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts +++ b/webapp/src/app/features/channels/channel-detail/channel-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, signal, OnInit } from '@angular/core'; +import { Component, inject, signal, computed, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; @@ -70,9 +70,20 @@ export class ChannelDetailComponent implements OnInit { editDescription = ''; saving = signal(false); - // QR modal - showQrModal = signal(false); - qrCodeData = signal(''); + // QR code data (computed from channel) + qrCodeData = computed(() => { + const channel = this.channel(); + if (!channel || !channel.subscribe_key) return ''; + + return [ + '@scn.channel.subscribe', + 'v1', + channel.display_name, + channel.owner_user_id, + channel.channel_id, + channel.subscribe_key + ].join('\n'); + }); ngOnInit(): void { const channelId = this.route.snapshot.paramMap.get('id'); @@ -211,28 +222,6 @@ export class ChannelDetailComponent implements OnInit { }); } - // QR Code - showQrCode(): void { - const channel = this.channel(); - if (!channel || !channel.subscribe_key) return; - - const qrText = [ - '@scn.channel.subscribe', - 'v1', - channel.display_name, - channel.owner_user_id, - channel.channel_id, - channel.subscribe_key - ].join('\n'); - - this.qrCodeData.set(qrText); - this.showQrModal.set(true); - } - - closeQrModal(): void { - this.showQrModal.set(false); - } - getSubscriptionStatus(): { label: string; color: string } { const channel = this.channel(); if (!channel) return { label: 'Unknown', color: 'default' }; diff --git a/webapp/src/app/features/messages/message-list/message-list.component.html b/webapp/src/app/features/messages/message-list/message-list.component.html index 31398fc..46b599c 100644 --- a/webapp/src/app/features/messages/message-list/message-list.component.html +++ b/webapp/src/app/features/messages/message-list/message-list.component.html @@ -110,12 +110,14 @@ - @if (nextPageToken()) { -
- -
- } +
+ +
diff --git a/webapp/src/app/features/messages/message-list/message-list.component.scss b/webapp/src/app/features/messages/message-list/message-list.component.scss index 99868a6..f41ad08 100644 --- a/webapp/src/app/features/messages/message-list/message-list.component.scss +++ b/webapp/src/app/features/messages/message-list/message-list.component.scss @@ -42,8 +42,17 @@ margin-top: 4px; } -.load-more { +.pagination-controls { display: flex; justify-content: center; + align-items: center; + gap: 16px; padding: 16px 0; + + .page-indicator { + font-size: 14px; + color: #666; + min-width: 80px; + text-align: center; + } } diff --git a/webapp/src/app/features/messages/message-list/message-list.component.ts b/webapp/src/app/features/messages/message-list/message-list.component.ts index e9422bc..89dc2fc 100644 --- a/webapp/src/app/features/messages/message-list/message-list.component.ts +++ b/webapp/src/app/features/messages/message-list/message-list.component.ts @@ -11,6 +11,7 @@ import { NzEmptyModule } from 'ng-zorro-antd/empty'; import { NzSpinModule } from 'ng-zorro-antd/spin'; import { NzCardModule } from 'ng-zorro-antd/card'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; +import { NzPaginationModule } from 'ng-zorro-antd/pagination'; import { ApiService } from '../../../core/services/api.service'; import { AuthService } from '../../../core/services/auth.service'; import { Message, MessageListParams } from '../../../core/models'; @@ -31,6 +32,7 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; NzSpinModule, NzCardModule, NzToolTipModule, + NzPaginationModule, RelativeTimePipe, ], templateUrl: './message-list.component.html', @@ -43,7 +45,11 @@ export class MessageListComponent implements OnInit { messages = signal([]); loading = signal(false); - nextPageToken = signal(null); + + // Pagination + currentPage = signal(1); + pageSize = 50; + totalCount = signal(0); // Filters searchText = ''; @@ -80,11 +86,11 @@ export class MessageListComponent implements OnInit { }); } - loadMessages(append = false): void { + loadMessages(): void { this.loading.set(true); const params: MessageListParams = { - page_size: 50, + page_size: this.pageSize, trimmed: true, }; @@ -97,22 +103,17 @@ export class MessageListComponent implements OnInit { if (this.channelFilter.length > 0) { params.channel = this.channelFilter.join(','); } - if (append && this.nextPageToken()) { - params.next_page_token = this.nextPageToken()!; + + // Use page-index based pagination: $1 = page 1, $2 = page 2, etc. + const page = this.currentPage(); + if (page > 1) { + params.next_page_token = `$${page}`; } this.apiService.getMessages(params).subscribe({ next: (response) => { - if (append) { - this.messages.update(msgs => [...msgs, ...response.messages]); - } else { - this.messages.set(response.messages); - } - this.nextPageToken.set( - response.next_page_token && response.next_page_token !== '@end' - ? response.next_page_token - : null - ); + this.messages.set(response.messages); + this.totalCount.set(response.total_count); this.loading.set(false); }, error: () => { @@ -123,37 +124,44 @@ export class MessageListComponent implements OnInit { applyFilters(): void { this.appliedSearchText = this.searchText; + this.currentPage.set(1); this.loadMessages(); } onPriorityFilterChange(filters: string[] | null): void { this.priorityFilter = filters ?? []; + this.currentPage.set(1); this.loadMessages(); } onChannelFilterChange(filters: string[] | null): void { this.channelFilter = filters ?? []; + this.currentPage.set(1); this.loadMessages(); } clearSearch(): void { this.searchText = ''; this.appliedSearchText = ''; + this.currentPage.set(1); this.loadMessages(); } clearChannelFilter(): void { this.channelFilter = []; + this.currentPage.set(1); this.loadMessages(); } removeChannelFilter(channel: string): void { this.channelFilter = this.channelFilter.filter(c => c !== channel); + this.currentPage.set(1); this.loadMessages(); } clearPriorityFilter(): void { this.priorityFilter = []; + this.currentPage.set(1); this.loadMessages(); } @@ -162,6 +170,7 @@ export class MessageListComponent implements OnInit { this.appliedSearchText = ''; this.channelFilter = []; this.priorityFilter = []; + this.currentPage.set(1); this.loadMessages(); } @@ -175,10 +184,9 @@ export class MessageListComponent implements OnInit { return channel?.text?.toString() ?? internalName; } - loadMore(): void { - if (this.nextPageToken()) { - this.loadMessages(true); - } + goToPage(page: number): void { + this.currentPage.set(page); + this.loadMessages(); } viewMessage(message: Message): void { diff --git a/webapp/src/app/features/senders/sender-list/sender-list.component.html b/webapp/src/app/features/senders/sender-list/sender-list.component.html index cf8bdcc..cb22f6b 100644 --- a/webapp/src/app/features/senders/sender-list/sender-list.component.html +++ b/webapp/src/app/features/senders/sender-list/sender-list.component.html @@ -1,50 +1,95 @@
- - - - - Sender Name - Message Count - Last Used - - - - @for (sender of senders(); track sender.name) { - - - {{ sender.name || '(No name)' }} - - {{ sender.count }} - - - {{ sender.last_timestamp | relativeTime }} - - - - } @empty { - - - - - - } - - + + + + + + + Sender Name + Message Count + Last Used + + + + @for (sender of mySenders(); track sender.name) { + + + {{ sender.name || '(No name)' }} + + {{ sender.count }} + + + {{ sender.last_timestamp | relativeTime }} + + + + } @empty { + + + + + + } + + + + + + + + + + Sender Name + Message Count + Last Used + + + + @for (sender of allSenders(); track sender.name) { + + + {{ sender.name || '(No name)' }} + + {{ sender.count }} + + + {{ sender.last_timestamp | relativeTime }} + + + + } @empty { + + + + + + } + + + +
diff --git a/webapp/src/app/features/senders/sender-list/sender-list.component.ts b/webapp/src/app/features/senders/sender-list/sender-list.component.ts index 6e1a3e0..a337c50 100644 --- a/webapp/src/app/features/senders/sender-list/sender-list.component.ts +++ b/webapp/src/app/features/senders/sender-list/sender-list.component.ts @@ -6,7 +6,9 @@ import { NzIconModule } from 'ng-zorro-antd/icon'; import { NzEmptyModule } from 'ng-zorro-antd/empty'; import { NzCardModule } from 'ng-zorro-antd/card'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; +import { NzTabsModule } from 'ng-zorro-antd/tabs'; import { ApiService } from '../../../core/services/api.service'; +import { AuthService } from '../../../core/services/auth.service'; import { SenderNameStatistics } from '../../../core/models'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; @@ -21,6 +23,7 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; NzEmptyModule, NzCardModule, NzToolTipModule, + NzTabsModule, RelativeTimePipe, ], templateUrl: './sender-list.component.html', @@ -28,24 +31,61 @@ import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; }) export class SenderListComponent implements OnInit { private apiService = inject(ApiService); + private authService = inject(AuthService); - senders = signal([]); - loading = signal(false); + mySenders = signal([]); + allSenders = signal([]); + loadingMy = signal(false); + loadingAll = signal(false); + activeTab = signal(0); ngOnInit(): void { - this.loadSenders(); + this.loadMySenders(); } - loadSenders(): void { - this.loading.set(true); - this.apiService.getSenderNames().subscribe({ + onTabChange(index: number): void { + this.activeTab.set(index); + if (index === 0 && this.mySenders().length === 0) { + this.loadMySenders(); + } else if (index === 1 && this.allSenders().length === 0) { + this.loadAllSenders(); + } + } + + loadMySenders(): void { + const userId = this.authService.getUserId(); + if (!userId) return; + + this.loadingMy.set(true); + this.apiService.getUserSenderNames(userId).subscribe({ next: (response) => { - this.senders.set(response.senders); - this.loading.set(false); + this.mySenders.set(response.sender_names); + this.loadingMy.set(false); }, error: () => { - this.loading.set(false); + this.loadingMy.set(false); } }); } + + loadAllSenders(): void { + this.loadingAll.set(true); + this.apiService.getSenderNames().subscribe({ + next: (response) => { + this.allSenders.set(response.sender_names); + this.loadingAll.set(false); + }, + error: () => { + this.loadingAll.set(false); + } + }); + } + + refresh(): void { + if (this.activeTab() === 0) { + this.loadMySenders(); + } else { + this.loadAllSenders(); + } + } } diff --git a/webapp/src/app/layout/main-layout/main-layout.component.html b/webapp/src/app/layout/main-layout/main-layout.component.html index 2d69632..319ff13 100644 --- a/webapp/src/app/layout/main-layout/main-layout.component.html +++ b/webapp/src/app/layout/main-layout/main-layout.component.html @@ -9,10 +9,9 @@ [nzCollapsedWidth]="80" >
    @@ -62,7 +61,6 @@ Logout - diff --git a/webapp/src/app/layout/main-layout/main-layout.component.scss b/webapp/src/app/layout/main-layout/main-layout.component.scss index fca0aef..d3d18d8 100644 --- a/webapp/src/app/layout/main-layout/main-layout.component.scss +++ b/webapp/src/app/layout/main-layout/main-layout.component.scss @@ -18,6 +18,7 @@ display: flex; align-items: center; justify-content: center; + gap: 10px; padding: 0 16px; background: #001529; color: #fff; @@ -25,6 +26,12 @@ font-weight: 600; overflow: hidden; white-space: nowrap; + + .sidebar-logo-img { + height: 32px; + width: auto; + flex-shrink: 0; + } } .app-header { @@ -64,11 +71,6 @@ color: #666; font-size: 13px; } - - .header-logo { - height: 36px; - width: auto; - } } .content-area { diff --git a/webapp/public/favicon.ico b/webapp/src/assets/favicon.ico similarity index 100% rename from webapp/public/favicon.ico rename to webapp/src/assets/favicon.ico