Skip to content

Commit ee230d3

Browse files
committed
feat: enrich discovery names with network, add deep-linking and status tags
- Auto-generated discovery names now include network name ("Type — Network") in daemon registration, session completion, and stalled session cleanup - Homepage discovery cards show daemon name in description and are clickable to navigate to the history detail modal - History cards replace redundant Type field with Network, add phase status tag - Discovery history detail modal is deep-linkable via URL params
1 parent a36a73e commit ee230d3

File tree

7 files changed

+123
-24
lines changed

7 files changed

+123
-24
lines changed

backend/src/server/daemons/service.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,11 @@ impl DaemonService {
917917
"Creating default discovery jobs for daemon"
918918
);
919919

920+
let network_name = match self.network_service.get_by_id(&network_id).await {
921+
Ok(Some(network)) => network.base.name,
922+
_ => "Unknown Network".to_string(),
923+
};
924+
920925
// Free plans use AdHoc (run once immediately), paid plans use Scheduled
921926
let default_run_type = if is_free_plan {
922927
RunType::AdHoc { last_run: None }
@@ -938,7 +943,7 @@ impl DaemonService {
938943
Discovery::new(DiscoveryBase {
939944
run_type: default_run_type.clone(),
940945
discovery_type: self_report_discovery_type.clone(),
941-
name: self_report_discovery_type.to_string(),
946+
name: format!("{} \u{2014} {}", self_report_discovery_type, network_name),
942947
daemon_id,
943948
network_id,
944949
tags: Vec::new(),
@@ -964,7 +969,7 @@ impl DaemonService {
964969
Discovery::new(DiscoveryBase {
965970
run_type: default_run_type.clone(),
966971
discovery_type: docker_discovery_type.clone(),
967-
name: docker_discovery_type.to_string(),
972+
name: format!("{} \u{2014} {}", docker_discovery_type, network_name),
968973
daemon_id,
969974
network_id,
970975
tags: Vec::new(),
@@ -992,7 +997,7 @@ impl DaemonService {
992997
Discovery::new(DiscoveryBase {
993998
run_type: default_run_type.clone(),
994999
discovery_type: network_discovery_type.clone(),
995-
name: network_discovery_type.to_string(),
1000+
name: format!("{} \u{2014} {}", network_discovery_type, network_name),
9961001
daemon_id,
9971002
network_id,
9981003
tags: Vec::new(),

backend/src/server/discovery/service.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,14 +789,19 @@ impl DiscoveryService {
789789
self.pull_cancellation_for_daemon(&session.daemon_id).await;
790790

791791
// Create historical discovery record
792+
let network_name = match self.network_service.get_by_id(&session.network_id).await {
793+
Ok(Some(network)) => network.base.name,
794+
_ => "Unknown Network".to_string(),
795+
};
796+
792797
let historical_discovery = Discovery {
793798
id: Uuid::new_v4(),
794799
created_at: session.started_at.unwrap_or(Utc::now()),
795800
updated_at: Utc::now(),
796801
base: crate::server::discovery::r#impl::base::DiscoveryBase {
797802
daemon_id: session.daemon_id,
798803
network_id: session.network_id,
799-
name: session.discovery_type.to_string(),
804+
name: format!("{} \u{2014} {}", session.discovery_type, network_name),
800805
tags: Vec::new(),
801806
discovery_type: session.discovery_type.clone(),
802807
run_type: RunType::Historical {
@@ -1158,6 +1163,11 @@ impl DiscoveryService {
11581163
}
11591164

11601165
// Create historical discovery record for the stalled session
1166+
let network_name = match self.network_service.get_by_id(&session.network_id).await {
1167+
Ok(Some(network)) => network.base.name,
1168+
_ => "Unknown Network".to_string(),
1169+
};
1170+
11611171
let historical_discovery = Discovery {
11621172
id: Uuid::new_v4(),
11631173
created_at: session.started_at.unwrap_or(now),
@@ -1166,7 +1176,7 @@ impl DiscoveryService {
11661176
daemon_id: session.daemon_id,
11671177
network_id: session.network_id,
11681178
tags: Vec::new(),
1169-
name: "Discovery Run (Stalled)".to_string(),
1179+
name: format!("{} \u{2014} {}", session.discovery_type, network_name),
11701180
discovery_type: session.discovery_type.clone(),
11711181
run_type: RunType::Historical { results: session },
11721182
},

ui/src/lib/features/discovery/components/cards/DiscoveryHistoryCard.svelte

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
<script lang="ts">
22
import GenericCard from '$lib/shared/components/data/GenericCard.svelte';
33
import { entities } from '$lib/shared/stores/metadata';
4+
import { toColor } from '$lib/shared/utils/styling';
45
import { Info } from 'lucide-svelte';
56
import type { Discovery } from '../../types/base';
67
import { useDaemonsQuery } from '$lib/features/daemons/queries';
8+
import { useNetworksQuery } from '$lib/features/networks/queries';
79
import { formatDuration, formatTimestamp } from '$lib/shared/utils/formatting';
10+
import type { TagProps } from '$lib/shared/components/data/types';
811
912
// Queries
1013
const daemonsQuery = useDaemonsQuery();
14+
const networksQuery = useNetworksQuery();
1115
1216
// Derived data
1317
let daemonsData = $derived(daemonsQuery.data ?? []);
18+
let networksData = $derived(networksQuery.data ?? []);
1419
1520
let {
1621
viewMode,
@@ -30,18 +35,34 @@
3035
discovery.run_type.type == 'Historical' ? discovery.run_type.results : null
3136
);
3237
38+
let status = $derived.by((): TagProps | null => {
39+
const phase = results?.phase ?? null;
40+
if (!phase) return null;
41+
switch (phase) {
42+
case 'Complete':
43+
return { label: 'Complete', color: toColor('green') };
44+
case 'Failed':
45+
return { label: 'Failed', color: toColor('red') };
46+
case 'Cancelled':
47+
return { label: 'Cancelled', color: toColor('yellow') };
48+
default:
49+
return { label: phase, color: toColor('blue') };
50+
}
51+
});
52+
3353
let cardData = $derived({
3454
title: discovery.name,
3555
iconColor: entities.getColorHelper('Discovery').icon,
3656
Icon: entities.getIconComponent('Discovery'),
57+
status,
3758
fields: [
3859
{
39-
label: 'Daemon',
40-
value: daemonsData.find((d) => d.id == discovery.daemon_id)?.name || 'Unknown Daemon'
60+
label: 'Network',
61+
value: networksData.find((n) => n.id == discovery.network_id)?.name || 'Unknown Network'
4162
},
4263
{
43-
label: 'Type',
44-
value: discovery.discovery_type.type
64+
label: 'Daemon',
65+
value: daemonsData.find((d) => d.id == discovery.daemon_id)?.name || 'Unknown Daemon'
4566
},
4667
{
4768
label: 'Started',

ui/src/lib/features/discovery/components/tabs/DiscoveryHistoryTab.svelte

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import { useHostsQuery } from '$lib/features/hosts/queries';
2121
import type { TabProps } from '$lib/shared/types';
2222
import { downloadCsv } from '$lib/shared/utils/csvExport';
23+
import { modalState, openModal, closeModal } from '$lib/shared/stores/modal-registry';
2324
import {
2425
common_created,
2526
common_duration,
@@ -57,9 +58,23 @@
5758
let showDiscoveryModal = $state(false);
5859
let editingDiscovery: Discovery | null = $state(null);
5960
61+
// Deep-link: open detail modal from URL
62+
$effect(() => {
63+
if ($modalState.name === 'discovery-history-detail' && !showDiscoveryModal) {
64+
if ($modalState.id) {
65+
const disc = discoveriesData.find((d) => d.id === $modalState.id);
66+
if (disc) {
67+
editingDiscovery = disc;
68+
showDiscoveryModal = true;
69+
}
70+
}
71+
}
72+
});
73+
6074
function handleEditDiscovery(discovery: Discovery) {
6175
editingDiscovery = discovery;
6276
showDiscoveryModal = true;
77+
openModal('discovery-history-detail', { id: discovery.id });
6378
}
6479
6580
async function handleDiscoveryCreate(data: Discovery) {
@@ -77,6 +92,7 @@
7792
function handleCloseEditor() {
7893
showDiscoveryModal = false;
7994
editingDiscovery = null;
95+
closeModal();
8096
}
8197
8298
async function handleBulkDelete(ids: string[]) {
@@ -175,6 +191,7 @@
175191
</div>
176192

177193
<DiscoveryEditModal
194+
name="discovery-history-detail"
178195
isOpen={showDiscoveryModal}
179196
hosts={hostsData}
180197
daemons={daemonsData}

ui/src/lib/features/home/components/HomeDiscoveryDisplay.svelte

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,31 @@
33
import { toColor } from '$lib/shared/utils/styling';
44
import { formatTimestamp } from '$lib/shared/utils/formatting';
55
import type { Discovery } from '$lib/features/discovery/types/base';
6+
import type { Daemon } from '$lib/features/daemons/types/base';
7+
import type { components } from '$lib/api/schema';
68
7-
export const HomeDiscoveryDisplay: EntityDisplayComponent<Discovery, Record<string, never>> = {
9+
type NetworkSummary = components['schemas']['NetworkSummary'];
10+
11+
export interface HomeDiscoveryContext {
12+
daemons: Daemon[];
13+
networks: NetworkSummary[];
14+
}
15+
16+
export const HomeDiscoveryDisplay: EntityDisplayComponent<Discovery, HomeDiscoveryContext> = {
817
getId: (discovery) => discovery.id,
9-
getLabel: (discovery) => discovery.name,
10-
getDescription: (discovery) => formatTimestamp(discovery.created_at),
18+
getLabel: (discovery, context) => {
19+
// New records already have enriched names ("Type — Network").
20+
// Old records missing the separator get enriched client-side.
21+
if (discovery.name.includes(' \u2014 ')) return discovery.name;
22+
const network = context?.networks.find((n) => n.id === discovery.network_id);
23+
if (network) return `${discovery.name} \u2014 ${network.name}`;
24+
return discovery.name;
25+
},
26+
getDescription: (discovery, context) => {
27+
const daemon = context.daemons.find((d) => d.id === discovery.daemon_id);
28+
const daemonName = daemon?.name ?? 'Unknown Daemon';
29+
return `${daemonName} \u00b7 ${formatTimestamp(discovery.created_at)}`;
30+
},
1131
getIcon: () => entities.getIconComponent('Discovery'),
1232
getIconColor: () => entities.getColorHelper('Discovery').icon,
1333
getTags: (discovery) => {
@@ -36,8 +56,10 @@
3656
import type { EntityDisplayComponent } from '$lib/shared/components/forms/selection/types';
3757
import ListSelectItem from '$lib/shared/components/forms/selection/ListSelectItem.svelte';
3858
39-
export let item: Discovery;
40-
export let context: Record<string, never> = {} as Record<string, never>;
59+
let {
60+
item,
61+
context = { daemons: [], networks: [] }
62+
}: { item: Discovery; context?: HomeDiscoveryContext } = $props();
4163
</script>
4264

4365
<ListSelectItem {item} {context} displayComponent={HomeDiscoveryDisplay} />

ui/src/lib/features/home/components/HomeTab.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import type { TabProps } from '$lib/shared/types';
1313
import type { components } from '$lib/api/schema';
1414
import { onMount } from 'svelte';
15+
import { openModal } from '$lib/shared/stores/modal-registry';
1516
1617
type OnboardingOperation = components['schemas']['OnboardingOperation'];
1718
@@ -83,7 +84,15 @@
8384

8485
<!-- Recent Discoveries — shown when discoveries exist -->
8586
{#if dashboard.recent_discoveries.length > 0}
86-
<RecentDiscoveries discoveries={dashboard.recent_discoveries} />
87+
<RecentDiscoveries
88+
discoveries={dashboard.recent_discoveries}
89+
daemons={dashboard.daemons}
90+
networks={dashboard.networks}
91+
onNavigate={(discovery) => {
92+
openModal('discovery-history-detail', { id: discovery.id });
93+
navigateTo('discovery-history');
94+
}}
95+
/>
8796
{/if}
8897

8998
<!-- Plan Usage — always visible if limits are approaching -->
Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,38 @@
11
<script lang="ts">
22
import type { Discovery } from '$lib/features/discovery/types/base';
3+
import type { Daemon } from '$lib/features/daemons/types/base';
4+
import type { components } from '$lib/api/schema';
35
import HomeDiscoveryDisplay from './HomeDiscoveryDisplay.svelte';
46
5-
let { discoveries }: { discoveries: Discovery[] } = $props();
7+
type NetworkSummary = components['schemas']['NetworkSummary'];
8+
9+
let {
10+
discoveries,
11+
daemons = [],
12+
networks = [],
13+
onNavigate
14+
}: {
15+
discoveries: Discovery[];
16+
daemons?: Daemon[];
17+
networks?: NetworkSummary[];
18+
onNavigate?: (discovery: Discovery) => void;
19+
} = $props();
620
</script>
721

822
<section>
923
<h3 class="text-primary mb-3 text-base font-semibold">Recent Discoveries</h3>
1024
{#if discoveries.length === 0}
1125
<p class="text-tertiary text-sm">No discovery results yet.</p>
1226
{:else}
13-
<div class="sm:w-1/2">
14-
<div class="space-y-2">
15-
{#each discoveries as discovery (discovery.id)}
16-
<div class="card card-static">
17-
<HomeDiscoveryDisplay item={discovery} />
18-
</div>
19-
{/each}
20-
</div>
27+
<div class="grid grid-cols-[repeat(auto-fill,minmax(360px,1fr))] gap-3">
28+
{#each discoveries as discovery (discovery.id)}
29+
<button
30+
class="card card-static cursor-pointer text-left"
31+
onclick={() => onNavigate?.(discovery)}
32+
>
33+
<HomeDiscoveryDisplay item={discovery} context={{ daemons, networks }} />
34+
</button>
35+
{/each}
2136
</div>
2237
{/if}
2338
</section>

0 commit comments

Comments
 (0)