Browse Source

feat: add display limits and improve visualization reactivity

- Add display limits store and utilities
- Fix reactivity issues with Svelte 5
- Add max 30040/30041 event controls
- Implement fetch if not found functionality
- Reorganize settings panel with sections
- Add debug logging for troubleshooting
master
limina1 10 months ago
parent
commit
f2fec5eafc
  1. 763
      package-lock.json
  2. 4
      package.json
  3. 14
      src/lib/navigator/EventNetwork/Legend.svelte
  4. 130
      src/lib/navigator/EventNetwork/Settings.svelte
  5. 12
      src/lib/navigator/EventNetwork/index.svelte
  6. 74
      src/lib/navigator/EventNetwork/types.ts
  7. 19
      src/lib/stores/displayLimits.ts
  8. 2
      src/lib/stores/index.ts
  9. 137
      src/lib/utils/displayLimits.ts
  10. 137
      src/routes/visualize/+page.svelte
  11. 4
      vite.config.ts

763
package-lock.json generated

File diff suppressed because it is too large Load Diff

4
package.json

@ -37,15 +37,19 @@ @@ -37,15 +37,19 @@
"@sveltejs/adapter-static": "~3.0.8",
"@sveltejs/kit": "^2.21.0",
"@sveltejs/vite-plugin-svelte": "~4.0.4",
"@testing-library/dom": "^10.4.0",
"@testing-library/svelte": "^5.2.8",
"@types/d3": "^7.4.3",
"@types/he": "1.2.x",
"@types/node": "22.x",
"@types/qrcode": "^1.5.5",
"@vitest/ui": "^3.1.4",
"autoprefixer": "10.x",
"eslint-plugin-svelte": "2.x",
"flowbite": "2.x",
"flowbite-svelte": "0.x",
"flowbite-svelte-icons": "2.1.x",
"jsdom": "^26.1.0",
"playwright": "^1.50.1",
"postcss": "8.x",
"postcss-load-config": "6.x",

14
src/lib/navigator/EventNetwork/Legend.svelte

@ -1,5 +1,3 @@ @@ -1,5 +1,3 @@
<!-- Legend Component (Svelte 5, Runes Mode) -->
<script lang="ts">
import { Button } from "flowbite-svelte";
import { CaretDownOutline, CaretUpOutline } from "flowbite-svelte-icons";
@ -67,7 +65,8 @@ @@ -67,7 +65,8 @@
</span>
</div>
<span class="legend-text"
>{eventCounts[30040] || 0} Index events (kind 30040) - Star centers with unique colors</span
>{eventCounts[30040] || 0} Index events (kind 30040) - Star centers with
unique colors</span
>
</li>
@ -79,7 +78,8 @@ @@ -79,7 +78,8 @@
</span>
</div>
<span class="legend-text"
>{eventCounts[30041] || 0} Content nodes (kind 30041) - Arranged around star centers</span
>{eventCounts[30041] || 0} Content nodes (kind 30041) - Arranged around
star centers</span
>
</li>
@ -109,7 +109,8 @@ @@ -109,7 +109,8 @@
</span>
</div>
<span class="legend-text"
>{eventCounts[30040] || 0} Index events (kind 30040) - Each with a unique pastel color</span
>{eventCounts[30040] || 0} Index events (kind 30040) - Each with a unique
pastel color</span
>
</li>
@ -121,7 +122,8 @@ @@ -121,7 +122,8 @@
</span>
</div>
<span class="legend-text"
>{(eventCounts[30041] || 0) + (eventCounts[30818] || 0)} Content events (kinds 30041, 30818) - Publication sections</span
>{(eventCounts[30041] || 0) + (eventCounts[30818] || 0)} Content events
(kinds 30041, 30818) - Publication sections</span
>
</li>

130
src/lib/navigator/EventNetwork/Settings.svelte

@ -1,30 +1,33 @@ @@ -1,30 +1,33 @@
<!--
Settings Component
-->
<script lang="ts">
import { Button } from "flowbite-svelte";
import { Button, Label } from "flowbite-svelte";
import { CaretDownOutline, CaretUpOutline } from "flowbite-svelte-icons";
import { fly } from "svelte/transition";
import { quintOut } from "svelte/easing";
import EventLimitControl from "$lib/components/EventLimitControl.svelte";
import EventRenderLevelLimit from "$lib/components/EventRenderLevelLimit.svelte";
import { networkFetchLimit } from "$lib/state";
import { displayLimits } from "$lib/stores/displayLimits";
import { Toggle, Select } from "flowbite-svelte";
let {
count = 0,
totalCount = 0,
onupdate,
starVisualization = $bindable(true),
showTagAnchors = $bindable(false),
selectedTagType = $bindable("t"),
tagExpansionDepth = $bindable(0),
onFetchMissing = () => {},
} = $props<{
count: number;
totalCount: number;
onupdate: () => void;
starVisualization?: boolean;
showTagAnchors?: boolean;
selectedTagType?: string;
tagExpansionDepth?: number;
onFetchMissing?: (ids: string[]) => void;
}>();
let expanded = $state(false);
@ -49,6 +52,37 @@ @@ -49,6 +52,37 @@
tagExpansionDepth = 0;
}
}
function handleDisplayLimitInput(event: Event, limitType: 'max30040' | 'max30041') {
const input = event.target as HTMLInputElement;
const value = input.value.trim();
console.log('[Settings] Display limit input changed:', limitType, 'value:', value);
if (value === '' || value === '-1') {
displayLimits.update(limits => ({
...limits,
[limitType]: -1
}));
console.log('[Settings] Set', limitType, 'to unlimited (-1)');
} else {
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue >= 1) {
displayLimits.update(limits => ({
...limits,
[limitType]: numValue
}));
console.log('[Settings] Set', limitType, 'to', numValue);
}
}
}
function toggleFetchIfNotFound() {
displayLimits.update(limits => ({
...limits,
fetchIfNotFound: !limits.fetchIfNotFound
}));
}
</script>
<div class="leather-legend sm:!right-1 sm:!left-auto">
@ -72,22 +106,82 @@ @@ -72,22 +106,82 @@
{#if expanded}
<div class="space-y-4">
<span class="leather bg-transparent legend-text">
Showing {count} events from {$networkFetchLimit} headers
Showing {count} of {totalCount} events
</span>
<div class="space-y-2">
<label
class="leather bg-transparent legend-text flex items-center space-x-2"
>
<Toggle bind:checked={starVisualization} class="text-xs" />
<span>Star Network View</span>
</label>
<p class="text-xs text-gray-500 dark:text-gray-400">
Toggle between star clusters (on) and linear sequence (off)
visualization
</p>
<!-- Initial Load Settings Section -->
<div class="border-t border-gray-300 dark:border-gray-700 pt-3">
<h4 class="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Initial Load</h4>
<EventLimitControl on:update={handleLimitUpdate} />
<EventRenderLevelLimit on:update={handleLimitUpdate} />
</div>
<!-- Display Limits Section -->
<div class="border-t border-gray-300 dark:border-gray-700 pt-3">
<h4 class="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Display Limits</h4>
<div class="space-y-3">
<div>
<Label for="max-30040" class="text-xs text-gray-600 dark:text-gray-400">
Max Publication Indices (30040)
</Label>
<input
type="number"
id="max-30040"
min="-1"
value={$displayLimits.max30040}
oninput={(e) => handleDisplayLimitInput(e, 'max30040')}
placeholder="-1 for unlimited"
class="w-full text-xs bg-primary-0 dark:bg-primary-1000 border border-gray-300 dark:border-gray-700 rounded-md px-2 py-1 dark:text-white"
/>
</div>
<div>
<Label for="max-30041" class="text-xs text-gray-600 dark:text-gray-400">
Max Content Events (30041)
</Label>
<input
type="number"
id="max-30041"
min="-1"
value={$displayLimits.max30041}
oninput={(e) => handleDisplayLimitInput(e, 'max30041')}
placeholder="-1 for unlimited"
class="w-full text-xs bg-primary-0 dark:bg-primary-1000 border border-gray-300 dark:border-gray-700 rounded-md px-2 py-1 dark:text-white"
/>
</div>
<label class="flex items-center space-x-2">
<Toggle
checked={$displayLimits.fetchIfNotFound}
on:click={toggleFetchIfNotFound}
class="text-xs"
/>
<span class="text-xs text-gray-600 dark:text-gray-400">Fetch if not found</span>
</label>
<p class="text-xs text-gray-500 dark:text-gray-400 ml-6">
Automatically fetch missing referenced events
</p>
</div>
</div>
<!-- Visual Settings Section -->
<div class="border-t border-gray-300 dark:border-gray-700 pt-3">
<h4 class="text-sm font-semibold mb-2 text-gray-700 dark:text-gray-300">Visual Settings</h4>
<div class="space-y-2">
<label
class="leather bg-transparent legend-text flex items-center space-x-2"
>
<Toggle bind:checked={starVisualization} class="text-xs" />
<span>Star Network View</span>
</label>
<p class="text-xs text-gray-500 dark:text-gray-400">
Toggle between star clusters (on) and linear sequence (off)
visualization
</p>
</div>
<div class="space-y-2">
<label
class="leather bg-transparent legend-text flex items-center space-x-2"
@ -149,10 +243,8 @@ @@ -149,10 +243,8 @@
</div>
</div>
{/if}
</div>
</div>
<EventLimitControl on:update={handleLimitUpdate} />
<EventRenderLevelLimit on:update={handleLimitUpdate} />
</div>
{/if}
</div>

12
src/lib/navigator/EventNetwork/index.svelte

@ -58,10 +58,18 @@ @@ -58,10 +58,18 @@
}
// Component props
let { events = [], onupdate, onTagExpansionChange } = $props<{
let {
events = [],
totalCount = 0,
onupdate,
onTagExpansionChange,
onFetchMissing = () => {}
} = $props<{
events?: NDKEvent[];
totalCount?: number;
onupdate: () => void;
onTagExpansionChange?: (depth: number, tags: string[]) => void;
onFetchMissing?: (ids: string[]) => void;
}>();
// Error state
@ -848,7 +856,9 @@ @@ -848,7 +856,9 @@
<!-- Settings Panel (shown when settings button is clicked) -->
<Settings
count={events.length}
{totalCount}
{onupdate}
{onFetchMissing}
bind:starVisualization
bind:showTagAnchors
bind:selectedTagType

74
src/lib/navigator/EventNetwork/types.ts

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
/**
* Type definitions for the Event Network visualization
*
*
* This module defines the core data structures used in the D3 force-directed
* graph visualization of Nostr events.
*/
@ -12,13 +12,13 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk"; @@ -12,13 +12,13 @@ import type { NDKEvent } from "@nostr-dev-kit/ndk";
* Represents the physical properties of a node in the simulation
*/
export interface SimulationNodeDatum {
index?: number; // Node index in the simulation
x?: number; // X position
y?: number; // Y position
vx?: number; // X velocity
vy?: number; // Y velocity
fx?: number | null; // Fixed X position (when node is pinned)
fy?: number | null; // Fixed Y position (when node is pinned)
index?: number; // Node index in the simulation
x?: number; // X position
y?: number; // Y position
vx?: number; // X velocity
vy?: number; // Y velocity
fx?: number | null; // Fixed X position (when node is pinned)
fy?: number | null; // Fixed Y position (when node is pinned)
}
/**
@ -26,9 +26,9 @@ export interface SimulationNodeDatum { @@ -26,9 +26,9 @@ export interface SimulationNodeDatum {
* Represents connections between nodes
*/
export interface SimulationLinkDatum<NodeType> {
source: NodeType | string | number; // Source node or identifier
target: NodeType | string | number; // Target node or identifier
index?: number; // Link index in the simulation
source: NodeType | string | number; // Source node or identifier
target: NodeType | string | number; // Target node or identifier
index?: number; // Link index in the simulation
}
/**
@ -36,23 +36,23 @@ export interface SimulationLinkDatum<NodeType> { @@ -36,23 +36,23 @@ export interface SimulationLinkDatum<NodeType> {
* Extends the base simulation node with Nostr event-specific properties
*/
export interface NetworkNode extends SimulationNodeDatum {
id: string; // Unique identifier (event ID)
event?: NDKEvent; // Reference to the original NDK event
level: number; // Hierarchy level in the network
kind: number; // Nostr event kind (30040 for index, 30041/30818 for content)
title: string; // Event title
content: string; // Event content
author: string; // Author's public key
type: "Index" | "Content" | "TagAnchor"; // Node type classification
naddr?: string; // NIP-19 naddr identifier
nevent?: string; // NIP-19 nevent identifier
isContainer?: boolean; // Whether this node is a container (index)
// Tag anchor specific fields
isTagAnchor?: boolean; // Whether this is a tag anchor node
tagType?: string; // Type of tag (t, p, e, etc.)
tagValue?: string; // The tag value
connectedNodes?: string[]; // IDs of nodes that have this tag
id: string; // Unique identifier (event ID)
event?: NDKEvent; // Reference to the original NDK event
level: number; // Hierarchy level in the network
kind: number; // Nostr event kind (30040 for index, 30041/30818 for content)
title: string; // Event title
content: string; // Event content
author: string; // Author's public key
type: "Index" | "Content" | "TagAnchor"; // Node type classification
naddr?: string; // NIP-19 naddr identifier
nevent?: string; // NIP-19 nevent identifier
isContainer?: boolean; // Whether this node is a container (index)
// Tag anchor specific fields
isTagAnchor?: boolean; // Whether this is a tag anchor node
tagType?: string; // Type of tag (t, p, e, etc.)
tagValue?: string; // The tag value
connectedNodes?: string[]; // IDs of nodes that have this tag
}
/**
@ -60,17 +60,17 @@ export interface NetworkNode extends SimulationNodeDatum { @@ -60,17 +60,17 @@ export interface NetworkNode extends SimulationNodeDatum {
* Extends the base simulation link with event-specific properties
*/
export interface NetworkLink extends SimulationLinkDatum<NetworkNode> {
source: NetworkNode; // Source node (overridden to be more specific)
target: NetworkNode; // Target node (overridden to be more specific)
isSequential: boolean; // Whether this link represents a sequential relationship
source: NetworkNode; // Source node (overridden to be more specific)
target: NetworkNode; // Target node (overridden to be more specific)
isSequential: boolean; // Whether this link represents a sequential relationship
}
/**
* Represents the complete graph data for visualization
*/
export interface GraphData {
nodes: NetworkNode[]; // All nodes in the graph
links: NetworkLink[]; // All links in the graph
nodes: NetworkNode[]; // All nodes in the graph
links: NetworkLink[]; // All links in the graph
}
/**
@ -78,8 +78,8 @@ export interface GraphData { @@ -78,8 +78,8 @@ export interface GraphData {
* Used to track relationships and build the final graph
*/
export interface GraphState {
nodeMap: Map<string, NetworkNode>; // Maps event IDs to nodes
links: NetworkLink[]; // All links in the graph
eventMap: Map<string, NDKEvent>; // Maps event IDs to original events
referencedIds: Set<string>; // Set of event IDs referenced by other events
nodeMap: Map<string, NetworkNode>; // Maps event IDs to nodes
links: NetworkLink[]; // All links in the graph
eventMap: Map<string, NDKEvent>; // Maps event IDs to original events
referencedIds: Set<string>; // Set of event IDs referenced by other events
}

19
src/lib/stores/displayLimits.ts

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
import { writable } from 'svelte/store';
export interface DisplayLimits {
max30040: number; // -1 for unlimited
max30041: number; // -1 for unlimited
fetchIfNotFound: boolean;
}
// Create the store with default values
export const displayLimits = writable<DisplayLimits>({
max30040: -1, // Show all publication indices by default
max30041: -1, // Show all content by default
fetchIfNotFound: false // Don't fetch missing events by default
});
// Helper to check if limits are active
export function hasActiveLimits(limits: DisplayLimits): boolean {
return limits.max30040 !== -1 || limits.max30041 !== -1;
}

2
src/lib/stores/index.ts

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
export * from './relayStore';
export * from './displayLimits';

137
src/lib/utils/displayLimits.ts

@ -0,0 +1,137 @@ @@ -0,0 +1,137 @@
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import type { DisplayLimits } from '$lib/stores/displayLimits';
/**
* Filters events based on display limits
* @param events - All available events
* @param limits - Display limit settings
* @returns Filtered events that should be displayed
*/
export function filterByDisplayLimits(events: NDKEvent[], limits: DisplayLimits): NDKEvent[] {
if (limits.max30040 === -1 && limits.max30041 === -1) {
// No limits, return all events
return events;
}
const result: NDKEvent[] = [];
let count30040 = 0;
let count30041 = 0;
for (const event of events) {
if (event.kind === 30040) {
if (limits.max30040 === -1 || count30040 < limits.max30040) {
result.push(event);
count30040++;
}
} else if (event.kind === 30041) {
if (limits.max30041 === -1 || count30041 < limits.max30041) {
result.push(event);
count30041++;
}
} else {
// Other event kinds always pass through
result.push(event);
}
// Early exit optimization if both limits are reached
if (limits.max30040 !== -1 && count30040 >= limits.max30040 &&
limits.max30041 !== -1 && count30041 >= limits.max30041) {
// Add remaining non-limited events
const remaining = events.slice(events.indexOf(event) + 1);
for (const e of remaining) {
if (e.kind !== 30040 && e.kind !== 30041) {
result.push(e);
}
}
break;
}
}
return result;
}
/**
* Detects events that are referenced but not present in the current set
* @param events - Current events
* @param existingIds - Set of all known event IDs
* @returns Set of missing event identifiers
*/
export function detectMissingEvents(events: NDKEvent[], existingIds: Set<string>): Set<string> {
const missing = new Set<string>();
for (const event of events) {
// Check 'a' tags for NIP-33 references (kind:pubkey:d-tag)
const aTags = event.getMatchingTags('a');
for (const aTag of aTags) {
if (aTag.length < 2) continue;
const identifier = aTag[1];
const parts = identifier.split(':');
if (parts.length >= 3) {
const [kind, pubkey, dTag] = parts;
// Create a synthetic ID for checking
const syntheticId = `${kind}:${pubkey}:${dTag}`;
// Check if we have an event matching this reference
const hasEvent = Array.from(existingIds).some(id => {
// This is a simplified check - in practice, you'd need to
// check the actual event's d-tag value
return id === dTag || id === syntheticId;
});
if (!hasEvent) {
missing.add(dTag);
}
}
}
// Check 'e' tags for direct event references
const eTags = event.getMatchingTags('e');
for (const eTag of eTags) {
if (eTag.length < 2) continue;
const eventId = eTag[1];
if (!existingIds.has(eventId)) {
missing.add(eventId);
}
}
}
return missing;
}
/**
* Groups events by kind for easier counting and display
*/
export function groupEventsByKind(events: NDKEvent[]): Map<number, NDKEvent[]> {
const groups = new Map<number, NDKEvent[]>();
for (const event of events) {
const kind = event.kind;
if (kind !== undefined) {
if (!groups.has(kind)) {
groups.set(kind, []);
}
groups.get(kind)!.push(event);
}
}
return groups;
}
/**
* Counts events by kind
*/
export function countEventsByKind(events: NDKEvent[]): Map<number, number> {
const counts = new Map<number, number>();
for (const event of events) {
const kind = event.kind;
if (kind !== undefined) {
counts.set(kind, (counts.get(kind) || 0) + 1);
}
}
return counts;
}

137
src/routes/visualize/+page.svelte

@ -11,6 +11,8 @@ @@ -11,6 +11,8 @@
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { filterValidIndexEvents } from "$lib/utils";
import { networkFetchLimit } from "$lib/state";
import { displayLimits } from "$lib/stores/displayLimits";
import { filterByDisplayLimits, detectMissingEvents } from "$lib/utils/displayLimits";
import type { PageData } from './$types';
// Configuration
@ -19,7 +21,7 @@ @@ -19,7 +21,7 @@
const CONTENT_EVENT_KINDS = [30041, 30818];
// Props from load function
export let data: PageData;
let { data } = $props<{ data: PageData }>();
/**
* Debug logging function that only logs when DEBUG is true
@ -31,12 +33,14 @@ @@ -31,12 +33,14 @@
}
// State
let events: NDKEvent[] = [];
let loading = true;
let error: string | null = null;
let showSettings = false;
let tagExpansionDepth = 0;
let baseEvents: NDKEvent[] = []; // Store original events before expansion
let allEvents = $state<NDKEvent[]>([]); // All fetched events
let events = $state<NDKEvent[]>([]); // Events to display (filtered by limits)
let loading = $state(true);
let error = $state<string | null>(null);
let showSettings = $state(false);
let tagExpansionDepth = $state(0);
let baseEvents = $state<NDKEvent[]>([]); // Store original events before expansion
let missingEventIds = $state(new Set<string>()); // Track missing referenced events
/**
* Fetches events from the Nostr network
@ -119,14 +123,29 @@ @@ -119,14 +123,29 @@
debug("Fetched content events:", contentEvents.size);
// Step 5: Combine both sets of events
events = [...Array.from(validIndexEvents), ...Array.from(contentEvents)];
baseEvents = [...events]; // Store base events for tag expansion
debug("Total events for visualization:", events.length);
allEvents = [...Array.from(validIndexEvents), ...Array.from(contentEvents)];
baseEvents = [...allEvents]; // Store base events for tag expansion
// Step 6: Apply display limits
events = filterByDisplayLimits(allEvents, $displayLimits);
// Step 7: Detect missing events
const eventIds = new Set(allEvents.map(e => e.id));
missingEventIds = detectMissingEvents(events, eventIds);
debug("Total events fetched:", allEvents.length);
debug("Events displayed:", events.length);
debug("Missing event IDs:", missingEventIds.size);
debug("Display limits:", $displayLimits);
debug("About to set loading to false");
debug("Current loading state:", loading);
} catch (e) {
console.error("Error fetching events:", e);
error = e instanceof Error ? e.message : String(e);
} finally {
loading = false;
debug("Loading set to false in fetchEvents");
debug("Final state check - loading:", loading, "events.length:", events.length, "allEvents.length:", allEvents.length);
}
}
@ -139,7 +158,8 @@ @@ -139,7 +158,8 @@
if (depth === 0 || tags.length === 0) {
// Reset to base events only
events = [...baseEvents];
allEvents = [...baseEvents];
events = filterByDisplayLimits(allEvents, $displayLimits);
return;
}
@ -167,7 +187,7 @@ @@ -167,7 +187,7 @@
// Extract content event IDs from new publications
const contentEventIds = new Set<string>();
const existingContentIds = new Set(
baseEvents.filter(e => CONTENT_EVENT_KINDS.includes(e.kind)).map(e => e.id)
baseEvents.filter(e => e.kind !== undefined && CONTENT_EVENT_KINDS.includes(e.kind)).map(e => e.id)
);
newPublications.forEach((event) => {
@ -191,17 +211,26 @@ @@ -191,17 +211,26 @@
}
// Combine all events: base events + new publications + new content
events = [
allEvents = [
...baseEvents,
...newPublications,
...newContentEvents
];
// Apply display limits
events = filterByDisplayLimits(allEvents, $displayLimits);
// Update missing events detection
const eventIds = new Set(allEvents.map(e => e.id));
missingEventIds = detectMissingEvents(events, eventIds);
debug("Events after expansion:", {
base: baseEvents.length,
newPubs: newPublications.length,
newContent: newContentEvents.length,
total: events.length
totalFetched: allEvents.length,
displayed: events.length,
missing: missingEventIds.size
});
} catch (e) {
@ -210,6 +239,77 @@ @@ -210,6 +239,77 @@
}
}
/**
* Dynamically fetches missing events when "fetch if not found" is enabled
*/
async function fetchMissingEvents(missingIds: string[]) {
if (!$displayLimits.fetchIfNotFound || missingIds.length === 0) {
return;
}
debug("Fetching missing events:", missingIds);
debug("Current loading state:", loading);
try {
// Fetch by event IDs and d-tags
const fetchedEvents = await $ndkInstance.fetchEvents({
kinds: [...[INDEX_EVENT_KIND], ...CONTENT_EVENT_KINDS],
"#d": missingIds, // For parameterized replaceable events
});
if (fetchedEvents.size === 0) {
// Try fetching by IDs directly
const eventsByIds = await $ndkInstance.fetchEvents({
ids: missingIds
});
// Add events from the second fetch to the first set
eventsByIds.forEach(e => fetchedEvents.add(e));
}
if (fetchedEvents.size > 0) {
debug(`Fetched ${fetchedEvents.size} missing events`);
// Add to all events
allEvents = [...allEvents, ...Array.from(fetchedEvents)];
// Re-apply display limits
events = filterByDisplayLimits(allEvents, $displayLimits);
// Update missing events list
const eventIds = new Set(allEvents.map(e => e.id));
missingEventIds = detectMissingEvents(events, eventIds);
}
} catch (e) {
console.error("Error fetching missing events:", e);
}
}
// React to display limit changes
$effect(() => {
debug("Effect triggered: allEvents.length =", allEvents.length, "displayLimits =", $displayLimits);
if (allEvents.length > 0) {
const newEvents = filterByDisplayLimits(allEvents, $displayLimits);
// Only update if actually different to avoid infinite loops
if (newEvents.length !== events.length) {
debug("Updating events due to display limit change:", events.length, "->", newEvents.length);
events = newEvents;
// Check for missing events when limits change
const eventIds = new Set(allEvents.map(e => e.id));
missingEventIds = detectMissingEvents(events, eventIds);
debug("Effect: events filtered to", events.length, "missing:", missingEventIds.size);
}
// Auto-fetch if enabled (but be conservative to avoid infinite loops)
if ($displayLimits.fetchIfNotFound && missingEventIds.size > 0 && missingEventIds.size < 20) {
debug("Auto-fetching", missingEventIds.size, "missing events");
fetchMissingEvents(Array.from(missingEventIds));
}
}
});
// Fetch events when component mounts
onMount(() => {
debug("Component mounted");
@ -227,6 +327,7 @@ @@ -227,6 +327,7 @@
<!-- Loading spinner -->
{#if loading}
<div class="flex justify-center items-center h-64">
{debug("TEMPLATE: Loading is true, events.length =", events.length, "allEvents.length =", allEvents.length)}
<div role="status">
<svg
aria-hidden="true"
@ -266,6 +367,12 @@ @@ -266,6 +367,12 @@
<!-- Network visualization -->
{:else}
<!-- Event network visualization -->
<EventNetwork {events} onupdate={fetchEvents} onTagExpansionChange={handleTagExpansion} />
<EventNetwork
{events}
totalCount={allEvents.length}
onupdate={fetchEvents}
onTagExpansionChange={handleTagExpansion}
onFetchMissing={fetchMissingEvents}
/>
{/if}
</div>

4
vite.config.ts

@ -32,7 +32,9 @@ export default defineConfig({ @@ -32,7 +32,9 @@ export default defineConfig({
}
},
test: {
include: ['./tests/unit/**/*.test.ts', './tests/integration/**/*.test.ts']
include: ['./tests/unit/**/*.test.ts', './tests/integration/**/*.test.ts'],
environment: 'jsdom',
setupFiles: ['./tests/vitest-setup.ts']
},
define: {
// Expose the app version as a global variable

Loading…
Cancel
Save