From 1f9abb8574fc2f418efe6b8450e82fad2b77a4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Thu, 26 Mar 2026 17:05:51 +0100 Subject: [PATCH] WebApp: Fix channel-detail page for non-owned channels --- webapp/src/app/core/models/channel.model.ts | 3 + webapp/src/app/core/models/client.model.ts | 2 + webapp/src/app/core/models/key-token.model.ts | 4 + webapp/src/app/core/services/api.service.ts | 10 +++ .../channel-detail.component.html | 49 ++++++----- .../channel-detail.component.scss | 11 +++ .../channel-detail.component.ts | 70 +++++++++++----- .../client-detail.component.html | 34 ++++---- .../client-detail/client-detail.component.ts | 36 ++++++-- .../keys/key-detail/key-detail.component.html | 84 ++++++++++--------- .../keys/key-detail/key-detail.component.ts | 60 ++++++++++--- 11 files changed, 248 insertions(+), 115 deletions(-) diff --git a/webapp/src/app/core/models/channel.model.ts b/webapp/src/app/core/models/channel.model.ts index 23b00a1..7a15018 100644 --- a/webapp/src/app/core/models/channel.model.ts +++ b/webapp/src/app/core/models/channel.model.ts @@ -21,6 +21,9 @@ export interface ChannelPreview { owner_user_id: string; internal_name: string; display_name: string; + description_name: string | null; + messages_sent: number; + subscription: Subscription | null; } export type ChannelSelector = 'owned' | 'subscribed' | 'all' | 'subscribed_any' | 'all_any'; diff --git a/webapp/src/app/core/models/client.model.ts b/webapp/src/app/core/models/client.model.ts index 817efe8..8006b62 100644 --- a/webapp/src/app/core/models/client.model.ts +++ b/webapp/src/app/core/models/client.model.ts @@ -19,8 +19,10 @@ export interface ClientListResponse { export interface ClientPreview { client_id: string; + user_id: string; name: string | null; type: ClientType; + timestamp_created: string; agent_model: string; agent_version: string; } diff --git a/webapp/src/app/core/models/key-token.model.ts b/webapp/src/app/core/models/key-token.model.ts index dc5b7d7..0d843e8 100644 --- a/webapp/src/app/core/models/key-token.model.ts +++ b/webapp/src/app/core/models/key-token.model.ts @@ -14,6 +14,10 @@ export interface KeyToken { export interface KeyTokenPreview { keytoken_id: string; name: string; + owner_user_id: string; + all_channels: boolean; + channels: string[]; + permissions: string; } export type TokenPermission = 'A' | 'CR' | 'CS' | 'UR'; diff --git a/webapp/src/app/core/services/api.service.ts b/webapp/src/app/core/services/api.service.ts index 2518d65..8f77086 100644 --- a/webapp/src/app/core/services/api.service.ts +++ b/webapp/src/app/core/services/api.service.ts @@ -6,6 +6,8 @@ import { User, UserWithExtra, UserPreview, + ChannelPreview, + KeyTokenPreview, Message, MessageListParams, MessageListResponse, @@ -98,6 +100,14 @@ export class ApiService { return this.http.get(`${this.baseUrl}/preview/clients/${clientId}`); } + getChannelPreview(channelId: string): Observable { + return this.http.get(`${this.baseUrl}/preview/channels/${channelId}`); + } + + getKeyPreview(keyId: string): Observable { + return this.http.get(`${this.baseUrl}/preview/keys/${keyId}`); + } + // Channel endpoints getChannels(userId: string, selector?: ChannelSelector): Observable { let params = new HttpParams(); diff --git a/webapp/src/app/features/channels/channel-detail/channel-detail.component.html b/webapp/src/app/features/channels/channel-detail/channel-detail.component.html index 0a9e780..cef12b7 100644 --- a/webapp/src/app/features/channels/channel-detail/channel-detail.component.html +++ b/webapp/src/app/features/channels/channel-detail/channel-detail.component.html @@ -3,7 +3,7 @@
- } @else if (channel()) { + } @else if (channelData()) {
- + - {{ channel()!.channel_id }} + {{ channelData()!.channel_id }} - {{ channel()!.internal_name }} + {{ channelData()!.internal_name }} @@ -46,29 +46,36 @@ - {{ channel()!.owner_user_id }} + @if (resolvedOwner()) { +
{{ resolvedOwner()!.displayName }}
+
{{ channelData()!.owner_user_id }}
+ } @else { + {{ channelData()!.owner_user_id }} + }
- @if (channel()!.description_name) { + @if (channelData()!.description_name) { - {{ channel()!.description_name }} + {{ channelData()!.description_name }} } - {{ channel()!.messages_sent }} + {{ channelData()!.messages_sent }} - - @if (channel()!.timestamp_lastsent) { -
{{ channel()!.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}
-
{{ channel()!.timestamp_lastsent | relativeTime }}
- } @else { - Never - } -
- -
{{ channel()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
-
{{ channel()!.timestamp_created | relativeTime }}
-
- @if (isOwner() && channel()!.subscribe_key) { + @if (channel()) { + + @if (channel()!.timestamp_lastsent) { +
{{ channel()!.timestamp_lastsent | date:'yyyy-MM-dd HH:mm:ss' }}
+
{{ channel()!.timestamp_lastsent | relativeTime }}
+ } @else { + Never + } +
+ +
{{ channel()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
+
{{ channel()!.timestamp_created | relativeTime }}
+
+ } + @if (isOwner() && channel()?.subscribe_key) {
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 815c35f..fa7cf08 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 @@ -109,6 +109,17 @@ overflow-y: clip; } +.owner-name { + font-weight: 500; + color: #333; +} + +.owner-id { + font-size: 11px; + color: #999; + margin-top: 2px; +} + .text-muted { color: #999; } 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 40e9551..f1905f3 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 @@ -21,7 +21,7 @@ import { AuthService } from '../../../core/services/auth.service'; import { NotificationService } from '../../../core/services/notification.service'; import { SettingsService } from '../../../core/services/settings.service'; import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service'; -import { ChannelWithSubscription, Subscription, Message } from '../../../core/models'; +import { ChannelWithSubscription, ChannelPreview, Subscription, Message } from '../../../core/models'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; import { CopyToClipboardDirective } from '../../../shared/directives/copy-to-clipboard.directive'; import { QrCodeDisplayComponent } from '../../../shared/components/qr-code-display/qr-code-display.component'; @@ -68,9 +68,11 @@ export class ChannelDetailComponent implements OnInit { private userCacheService = inject(UserCacheService); channel = signal(null); + channelPreview = signal(null); subscriptions = signal([]); messages = signal([]); userNames = signal>(new Map()); + resolvedOwner = signal(null); loading = signal(true); loadingSubscriptions = signal(false); loadingMessages = signal(false); @@ -115,14 +117,26 @@ export class ChannelDetailComponent implements OnInit { if (!userId) return; this.loading.set(true); - this.apiService.getChannel(userId, channelId).subscribe({ - next: (channel) => { - this.channel.set(channel); - this.loading.set(false); - if (this.isOwner()) { - this.loadSubscriptions(channelId); + this.apiService.getChannelPreview(channelId).subscribe({ + next: (preview) => { + this.channelPreview.set(preview); + this.resolveOwner(preview.owner_user_id); + if (preview.owner_user_id === userId) { + this.apiService.getChannel(userId, channelId).subscribe({ + next: (channel) => { + this.channel.set(channel); + this.loading.set(false); + this.loadSubscriptions(channelId); + this.loadMessages(channelId); + }, + error: () => { + this.loading.set(false); + } + }); + } else { + this.loading.set(false); + this.loadMessages(channelId); } - this.loadMessages(channelId); }, error: () => { this.loading.set(false); @@ -148,14 +162,13 @@ export class ChannelDetailComponent implements OnInit { } loadMessages(channelId: string, nextPageToken?: string): void { - const userId = this.authService.getUserId(); - if (!userId) return; - this.loadingMessages.set(true); - this.apiService.getChannelMessages(userId, channelId, { + this.apiService.getMessages({ + channel_id: [channelId], page_size: this.messagesPageSize, next_page_token: nextPageToken, - trimmed: true + trimmed: true, + subscription_status: 'all' }).subscribe({ next: (response) => { this.messages.set(response.messages); @@ -210,6 +223,12 @@ export class ChannelDetailComponent implements OnInit { } } + private resolveOwner(ownerId: string): void { + this.userCacheService.resolveUser(ownerId).subscribe(resolved => { + this.resolvedOwner.set(resolved); + }); + } + private resolveUserNames(subscriptions: Subscription[]): void { const userIds = new Set(); for (const sub of subscriptions) { @@ -232,9 +251,16 @@ export class ChannelDetailComponent implements OnInit { } isOwner(): boolean { - const channel = this.channel(); const userId = this.authService.getUserId(); - return channel?.owner_user_id === userId; + const channel = this.channel(); + if (channel) return channel.owner_user_id === userId; + const preview = this.channelPreview(); + if (preview) return preview.owner_user_id === userId; + return false; + } + + channelData() { + return this.channel() ?? this.channelPreview(); } // Edit methods @@ -290,18 +316,20 @@ export class ChannelDetailComponent implements OnInit { } getSubscriptionStatus(): { label: string; color: string } { - const channel = this.channel(); - if (!channel) return { label: 'Unknown', color: 'default' }; + const data = this.channelData(); + if (!data) return { label: 'Unknown', color: 'default' }; + + const subscription = 'subscribe_key' in data ? data.subscription : data.subscription; if (this.isOwner()) { - if (channel.subscription) { + if (subscription) { return { label: 'Owned & Subscribed', color: 'green' }; } return { label: 'Owned', color: 'blue' }; } - if (channel.subscription) { - if (channel.subscription.confirmed) { + if (subscription) { + if (subscription.confirmed) { return { label: 'Subscribed', color: 'green' }; } return { label: 'Pending', color: 'orange' }; @@ -377,7 +405,7 @@ export class ChannelDetailComponent implements OnInit { } isUserSubscribed(): boolean { - return this.channel()?.subscription !== null; + return this.channelData()?.subscription !== null && this.channelData()?.subscription !== undefined; } toggleSelfSubscription(): void { diff --git a/webapp/src/app/features/clients/client-detail/client-detail.component.html b/webapp/src/app/features/clients/client-detail/client-detail.component.html index 454816c..6a6e7a7 100644 --- a/webapp/src/app/features/clients/client-detail/client-detail.component.html +++ b/webapp/src/app/features/clients/client-detail/client-detail.component.html @@ -3,13 +3,13 @@
- } @else if (client()) { + } @else if (clientData()) {
- @if (expertMode()) { + @if (isOwner() && expertMode()) {
- {{ client()!.client_id }} + {{ clientData()!.client_id }} - {{ getClientTypeLabel(client()!.type) }} + {{ getClientTypeLabel(clientData()!.type) }}
- {{ client()!.agent_model }} - v{{ client()!.agent_version }} + {{ clientData()!.agent_model }} + v{{ clientData()!.agent_version }}
-
{{ client()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
-
{{ client()!.timestamp_created | relativeTime }}
-
- - - {{ client()!.fcm_token }} - +
{{ clientData()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
+
{{ clientData()!.timestamp_created | relativeTime }}
+ @if (client()) { + + + {{ client()!.fcm_token }} + + + }
} @else { diff --git a/webapp/src/app/features/clients/client-detail/client-detail.component.ts b/webapp/src/app/features/clients/client-detail/client-detail.component.ts index fc55a2a..e160fb5 100644 --- a/webapp/src/app/features/clients/client-detail/client-detail.component.ts +++ b/webapp/src/app/features/clients/client-detail/client-detail.component.ts @@ -12,7 +12,7 @@ import { ApiService } from '../../../core/services/api.service'; import { AuthService } from '../../../core/services/auth.service'; import { NotificationService } from '../../../core/services/notification.service'; import { SettingsService } from '../../../core/services/settings.service'; -import { Client, ClientType, getClientTypeIcon } from '../../../core/models'; +import { Client, ClientPreview, ClientType, getClientTypeIcon } from '../../../core/models'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid'; @@ -45,6 +45,7 @@ export class ClientDetailComponent implements OnInit { private settingsService = inject(SettingsService); client = signal(null); + clientPreview = signal(null); loading = signal(true); expertMode = this.settingsService.expertMode; @@ -60,10 +61,22 @@ export class ClientDetailComponent implements OnInit { if (!userId) return; this.loading.set(true); - this.apiService.getClient(userId, clientId).subscribe({ - next: (client) => { - this.client.set(client); - this.loading.set(false); + this.apiService.getClientPreview(clientId).subscribe({ + next: (response) => { + this.clientPreview.set(response.client); + if (response.client.user_id === userId) { + this.apiService.getClient(userId, clientId).subscribe({ + next: (client) => { + this.client.set(client); + this.loading.set(false); + }, + error: () => { + this.loading.set(false); + } + }); + } else { + this.loading.set(false); + } }, error: () => { this.loading.set(false); @@ -71,6 +84,19 @@ export class ClientDetailComponent implements OnInit { }); } + clientData() { + return this.client() ?? this.clientPreview(); + } + + isOwner(): boolean { + const userId = this.authService.getUserId(); + const client = this.client(); + if (client) return client.user_id === userId; + const preview = this.clientPreview(); + if (preview) return preview.user_id === userId; + return false; + } + goBack(): void { this.router.navigate(['/clients']); } diff --git a/webapp/src/app/features/keys/key-detail/key-detail.component.html b/webapp/src/app/features/keys/key-detail/key-detail.component.html index fd503e3..aa87299 100644 --- a/webapp/src/app/features/keys/key-detail/key-detail.component.html +++ b/webapp/src/app/features/keys/key-detail/key-detail.component.html @@ -3,35 +3,37 @@
- } @else if (key()) { + } @else if (keyData()) {
-
- - @if (!isCurrentKey()) { - - } -
+ @if (!isCurrentKey()) { + + } +
+ }
-

{{ key()!.name }}

+

{{ keyData()!.name }}

@if (isCurrentKey()) { Current } @@ -39,7 +41,7 @@ - {{ key()!.keytoken_id }} + {{ keyData()!.keytoken_id }}
@@ -55,11 +57,11 @@
- @if (key()!.all_channels) { + @if (keyData()!.all_channels) { All Channels - } @else if (key()!.channels && key()!.channels.length > 0) { + } @else if (keyData()!.channels && keyData()!.channels.length > 0) {
- @for (channelId of key()!.channels; track channelId) { + @for (channelId of keyData()!.channels; track channelId) { {{ getChannelDisplayName(channelId) }} @@ -69,27 +71,29 @@ No channels } - - {{ key()!.messages_sent }} - - -
{{ key()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
-
{{ key()!.timestamp_created | relativeTime }}
-
- - @if (key()!.timestamp_lastused) { -
{{ key()!.timestamp_lastused | date:'yyyy-MM-dd HH:mm:ss' }}
-
{{ key()!.timestamp_lastused | relativeTime }}
- } @else { - Never - } -
+ @if (key()) { + + {{ key()!.messages_sent }} + + +
{{ key()!.timestamp_created | date:'yyyy-MM-dd HH:mm:ss' }}
+
{{ key()!.timestamp_created | relativeTime }}
+
+ + @if (key()!.timestamp_lastused) { +
{{ key()!.timestamp_lastused | date:'yyyy-MM-dd HH:mm:ss' }}
+
{{ key()!.timestamp_lastused | relativeTime }}
+ } @else { + Never + } +
+ } @if (resolvedOwner()) {
{{ resolvedOwner()!.displayName }}
-
{{ key()!.owner_user_id }}
+
{{ keyData()!.owner_user_id }}
} @else { - {{ key()!.owner_user_id }} + {{ keyData()!.owner_user_id }} }
diff --git a/webapp/src/app/features/keys/key-detail/key-detail.component.ts b/webapp/src/app/features/keys/key-detail/key-detail.component.ts index 66680ce..1b90a4b 100644 --- a/webapp/src/app/features/keys/key-detail/key-detail.component.ts +++ b/webapp/src/app/features/keys/key-detail/key-detail.component.ts @@ -22,7 +22,7 @@ import { AuthService } from '../../../core/services/auth.service'; import { NotificationService } from '../../../core/services/notification.service'; import { ChannelCacheService, ResolvedChannel } from '../../../core/services/channel-cache.service'; import { UserCacheService, ResolvedUser } from '../../../core/services/user-cache.service'; -import { KeyToken, parsePermissions, TokenPermission, ChannelWithSubscription, Message } from '../../../core/models'; +import { KeyToken, KeyTokenPreview, parsePermissions, TokenPermission, ChannelWithSubscription, Message } from '../../../core/models'; import { RelativeTimePipe } from '../../../shared/pipes/relative-time.pipe'; import { MetadataGridComponent, MetadataValueComponent } from '../../../shared/components/metadata-grid'; @@ -72,6 +72,7 @@ export class KeyDetailComponent implements OnInit { private userCacheService = inject(UserCacheService); key = signal(null); + keyPreview = signal(null); currentKeyId = signal(null); loading = signal(true); channelNames = signal>(new Map()); @@ -105,8 +106,6 @@ export class KeyDetailComponent implements OnInit { const keyId = this.route.snapshot.paramMap.get('id'); if (keyId) { this.loadKey(keyId); - this.loadCurrentKey(); - this.loadAvailableChannels(); } } @@ -115,13 +114,29 @@ export class KeyDetailComponent implements OnInit { if (!userId) return; this.loading.set(true); - this.apiService.getKey(userId, keyId).subscribe({ - next: (key) => { - this.key.set(key); - this.loading.set(false); - this.resolveChannelNames(key); - this.resolveOwner(key.owner_user_id); - this.loadMessages(keyId); + this.apiService.getKeyPreview(keyId).subscribe({ + next: (preview) => { + this.keyPreview.set(preview); + this.resolveOwner(preview.owner_user_id); + this.resolveChannelNamesFromPreview(preview); + if (preview.owner_user_id === userId) { + this.loadCurrentKey(); + this.loadAvailableChannels(); + this.apiService.getKey(userId, keyId).subscribe({ + next: (key) => { + this.key.set(key); + this.loading.set(false); + this.resolveChannelNames(key); + this.loadMessages(keyId); + }, + error: () => { + this.loading.set(false); + } + }); + } else { + this.loading.set(false); + this.loadMessages(keyId); + } }, error: () => { this.loading.set(false); @@ -217,6 +232,27 @@ export class KeyDetailComponent implements OnInit { } } + private resolveChannelNamesFromPreview(preview: KeyTokenPreview): void { + if (!preview.all_channels && preview.channels && preview.channels.length > 0) { + this.channelCacheService.resolveChannels(preview.channels).subscribe(resolved => { + this.channelNames.set(resolved); + }); + } + } + + keyData() { + return this.key() ?? this.keyPreview(); + } + + isOwner(): boolean { + const userId = this.authService.getUserId(); + const key = this.key(); + if (key) return key.owner_user_id === userId; + const preview = this.keyPreview(); + if (preview) return preview.owner_user_id === userId; + return false; + } + goBack(): void { this.router.navigate(['/keys']); } @@ -227,8 +263,8 @@ export class KeyDetailComponent implements OnInit { } getPermissions(): TokenPermission[] { - const key = this.key(); - return key ? parsePermissions(key.permissions) : []; + const data = this.keyData(); + return data ? parsePermissions(data.permissions) : []; } getPermissionColor(perm: TokenPermission): string {