diff --git a/infrastructure/control-panel/src/lib/fragments/TableCardHeader/TableCardHeader.svelte b/infrastructure/control-panel/src/lib/fragments/TableCardHeader/TableCardHeader.svelte index 0f9ed80e..663d2eaa 100644 --- a/infrastructure/control-panel/src/lib/fragments/TableCardHeader/TableCardHeader.svelte +++ b/infrastructure/control-panel/src/lib/fragments/TableCardHeader/TableCardHeader.svelte @@ -6,13 +6,22 @@ title, searchValue = $bindable(''), rightTitle, - placeholder - }: { title: string; searchValue: string; rightTitle: string; placeholder: string } = $props(); + placeholder, + showClearSelection = false, + onClearSelection = () => {} + }: { + title: string; + searchValue: string; + rightTitle: string; + placeholder: string; + showClearSelection?: boolean; + onClearSelection?: () => void; + } = $props();
-

{title}

+

{title}

- +
+ {#if showClearSelection} + + {/if}

{rightTitle}

diff --git a/infrastructure/control-panel/src/lib/ui/Table/Table.svelte b/infrastructure/control-panel/src/lib/ui/Table/Table.svelte index 87957e6a..0c697e4d 100644 --- a/infrastructure/control-panel/src/lib/ui/Table/Table.svelte +++ b/infrastructure/control-panel/src/lib/ui/Table/Table.svelte @@ -68,6 +68,7 @@ sortBy?: string; sortOrder?: 'asc' | 'desc'; onSelectionChange?: (index: number, checked: boolean) => void; + onSelectAllChange?: (checked: boolean) => void; } let { @@ -90,6 +91,7 @@ sortBy, sortOrder, onSelectionChange, + onSelectAllChange, ...restProps }: ITableProps = $props(); @@ -100,7 +102,17 @@ function toggleCheckAll(checked: boolean) { checkAll = checked; - checkItems = checkItems.map(() => checked); + + // Call the onSelectAllChange callback if provided + if (onSelectAllChange) { + onSelectAllChange(checked); + } + + // Update all individual checkboxes without calling onSelectionChange for each + // since onSelectAllChange already handles the bulk selection + for (let i = 0; i < tableData.length; i++) { + checkItems[i] = checked; + } } function toggleCheckItem(i: number, checked: boolean) { diff --git a/infrastructure/control-panel/src/routes/+page.svelte b/infrastructure/control-panel/src/routes/+page.svelte index d4295749..dabccb88 100644 --- a/infrastructure/control-panel/src/routes/+page.svelte +++ b/infrastructure/control-panel/src/routes/+page.svelte @@ -88,6 +88,48 @@ sessionStorage.setItem('selectedPlatforms', JSON.stringify(selectedPlatformData)); } + // Handle select all eVaults + function handleSelectAllEVaults(checked: boolean) { + if (checked) { + // Select all eVaults + selectedEVaults = Array.from({ length: evaults.length }, (_, i) => i); + } else { + // Deselect all eVaults + selectedEVaults = []; + } + + // Store selections immediately in sessionStorage + const selectedEVaultData = selectedEVaults.map((i) => evaults[i]); + sessionStorage.setItem('selectedEVaults', JSON.stringify(selectedEVaultData)); + } + + // Handle select all platforms + function handleSelectAllPlatforms(checked: boolean) { + if (checked) { + // Select all platforms + selectedPlatforms = Array.from({ length: platforms.length }, (_, i) => i); + } else { + // Deselect all platforms + selectedPlatforms = []; + } + + // Store selections immediately in sessionStorage + const selectedPlatformData = selectedPlatforms.map((i) => platforms[i]); + sessionStorage.setItem('selectedPlatforms', JSON.stringify(selectedPlatformData)); + } + + // Clear eVault selection + function clearEVaultSelection() { + selectedEVaults = []; + sessionStorage.removeItem('selectedEVaults'); + } + + // Clear platform selection + function clearPlatformSelection() { + selectedPlatforms = []; + sessionStorage.removeItem('selectedPlatforms'); + } + // Navigate to monitoring with selected items function goToMonitoring() { const selectedEVaultData = selectedEVaults.map((i) => evaults[i]); @@ -185,7 +227,11 @@ title="eVaults" placeholder="Search eVaults" bind:searchValue={evaultsSearchValue} - rightTitle="Monitoring all eVault pods across Kubernetes clusters" + rightTitle={selectedEVaults.length > 0 + ? `${selectedEVaults.length} eVault${selectedEVaults.length === 1 ? '' : 's'} selected` + : 'Monitoring all eVault pods across Kubernetes clusters'} + showClearSelection={selectedEVaults.length > 0} + onClearSelection={clearEVaultSelection} /> {#if isLoading} @@ -215,6 +261,7 @@ {handleNextPage} handleSelectedRow={handleEVaultRowClick} onSelectionChange={handleEVaultSelectionChange} + onSelectAllChange={handleSelectAllEVaults} /> {/if} @@ -224,7 +271,11 @@ title="Platforms" placeholder="Search Platforms" bind:searchValue={platformsSearchQuery} - rightTitle="No platform selected. Select a platform to monitor logs" + rightTitle={selectedPlatforms.length > 0 + ? `${selectedPlatforms.length} platform${selectedPlatforms.length === 1 ? '' : 's'} selected` + : 'No platform selected. Select a platform to monitor logs'} + showClearSelection={selectedPlatforms.length > 0} + onClearSelection={clearPlatformSelection} /> {#if platformsLoading}
@@ -252,6 +303,7 @@ {handlePreviousPage} {handleNextPage} onSelectionChange={handlePlatformSelectionChange} + onSelectAllChange={handleSelectAllPlatforms} /> {/if} diff --git a/platforms/blabsy-w3ds-auth-api/src/controllers/WebhookController.ts b/platforms/blabsy-w3ds-auth-api/src/controllers/WebhookController.ts index 19d306ee..09f66916 100644 --- a/platforms/blabsy-w3ds-auth-api/src/controllers/WebhookController.ts +++ b/platforms/blabsy-w3ds-auth-api/src/controllers/WebhookController.ts @@ -92,8 +92,15 @@ export class WebhookController { axios.post(new URL("blabsy", process.env.ANCHR_URL).toString(), req.body) } - if (adapter.lockedIds.includes(id)) return; - console.log("processing -- not skipped"); + // Early duplicate check + if (adapter.lockedIds.includes(id)) { + console.log(`Webhook skipped - ID ${id} already locked`); + return res.status(200).json({ success: true, skipped: true }); + } + + console.log(`Processing webhook for ID: ${id}`); + + // Lock the global ID immediately to prevent duplicates adapter.addToLockedIds(id); const mapping = Object.values(adapter.mapping).find( @@ -105,16 +112,17 @@ export class WebhookController { const local = await adapter.fromGlobal({ data, mapping }); console.log(local); - // + // Get the local ID from the mapping database const localId = await adapter.mappingDb.getLocalId(id); if (localId) { - console.log("LOCAL, updating"); + console.log(`LOCAL, updating - ID: ${id}, LocalID: ${localId}`); + // Lock local ID early to prevent duplicate processing adapter.addToLockedIds(localId); await this.updateRecord(tableName, localId, local.data); } else { - console.log("NOT LOCAL, creating"); + console.log(`NOT LOCAL, creating - ID: ${id}`); await this.createRecord(tableName, local.data, req.body.id); } diff --git a/platforms/blabsy-w3ds-auth-api/src/index.ts b/platforms/blabsy-w3ds-auth-api/src/index.ts index 263c1f48..9e3606d3 100644 --- a/platforms/blabsy-w3ds-auth-api/src/index.ts +++ b/platforms/blabsy-w3ds-auth-api/src/index.ts @@ -6,7 +6,7 @@ import path from "path"; import { AuthController } from "./controllers/AuthController"; import { initializeApp, cert, applicationDefault } from "firebase-admin/app"; import { Web3Adapter } from "./web3adapter"; -import { WebhookController } from "./controllers/WebhookController"; +import { WebhookController, adapter } from "./controllers/WebhookController"; config({ path: path.resolve(__dirname, "../../../.env") }); @@ -47,6 +47,35 @@ app.post("/api/auth", authController.login); app.get("/api/auth/sessions/:id", authController.sseStream); app.post("/api/webhook", webhookController.handleWebhook); +// Debug endpoints for monitoring duplicate prevention +app.get("/api/debug/watcher-stats", (req, res) => { + try { + const stats = web3Adapter.getWatcherStats(); + res.json({ success: true, stats }); + } catch (error) { + res.status(500).json({ success: false, error: "Failed to get watcher stats" }); + } +}); + +app.post("/api/debug/clear-processed-ids", (req, res) => { + try { + web3Adapter.clearAllProcessedIds(); + res.json({ success: true, message: "All processed IDs cleared" }); + } catch (error) { + res.status(500).json({ success: false, error: "Failed to clear processed IDs" }); + } +}); + +app.get("/api/debug/locked-ids", (req, res) => { + try { + // Access locked IDs from the exported adapter + const lockedIds = adapter.lockedIds || []; + res.json({ success: true, lockedIds, count: lockedIds.length }); + } catch (error) { + res.status(500).json({ success: false, error: "Failed to get locked IDs" }); + } +}); + // Graceful shutdown process.on("SIGTERM", async () => { console.log("SIGTERM received. Shutting down..."); diff --git a/platforms/blabsy-w3ds-auth-api/src/web3adapter/index.ts b/platforms/blabsy-w3ds-auth-api/src/web3adapter/index.ts index 63e962c9..cd10ba28 100644 --- a/platforms/blabsy-w3ds-auth-api/src/web3adapter/index.ts +++ b/platforms/blabsy-w3ds-auth-api/src/web3adapter/index.ts @@ -104,4 +104,23 @@ export class Web3Adapter { console.log(`Successfully restarted watcher for ${collectionName}`); } } + + // Method to get stats from all watchers + getWatcherStats(): Record { + const stats: Record = {}; + for (const [name, watcher] of this.watchers.entries()) { + stats[name] = watcher.getStats(); + } + return stats; + } + + // Method to clear processed IDs from all watchers + clearAllProcessedIds(): void { + console.log("Clearing processed IDs from all watchers..."); + for (const [name, watcher] of this.watchers.entries()) { + console.log(`Clearing processed IDs for ${name}...`); + watcher.clearProcessedIds(); + } + console.log("All processed IDs cleared"); + } } diff --git a/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts b/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts index 1597efbc..6a05a6c8 100644 --- a/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts +++ b/platforms/blabsy-w3ds-auth-api/src/web3adapter/watchers/firestoreWatcher.ts @@ -17,6 +17,13 @@ export class FirestoreWatcher { private retryCount = 0; private readonly maxRetries: number = 3; private readonly retryDelay: number = 1000; // 1 second + + // Track processed document IDs to prevent duplicates + private processedIds = new Set(); + private processingIds = new Set(); + + // Clean up old processed IDs periodically to prevent memory leaks + private cleanupInterval: NodeJS.Timeout | null = null; constructor( private readonly collection: @@ -63,6 +70,9 @@ export class FirestoreWatcher { ); console.log(`Successfully started watcher for ${collectionPath}`); + + // Start cleanup interval to prevent memory leaks + this.startCleanupInterval(); } catch (error) { console.error( `Failed to start watcher for ${collectionPath}:`, @@ -84,6 +94,37 @@ export class FirestoreWatcher { this.unsubscribe = null; console.log(`Successfully stopped watcher for ${collectionPath}`); } + + // Stop cleanup interval + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = null; + } + } + + private startCleanupInterval(): void { + // Clean up processed IDs every 5 minutes to prevent memory leaks + this.cleanupInterval = setInterval(() => { + const beforeSize = this.processedIds.size; + this.processedIds.clear(); + const afterSize = this.processedIds.size; + console.log(`Cleaned up processed IDs: ${beforeSize} -> ${afterSize}`); + }, 5 * 60 * 1000); // 5 minutes + } + + // Method to manually clear processed IDs (useful for debugging) + clearProcessedIds(): void { + const beforeSize = this.processedIds.size; + this.processedIds.clear(); + console.log(`Manually cleared processed IDs: ${beforeSize} -> 0`); + } + + // Method to get current stats for debugging + getStats(): { processed: number; processing: number } { + return { + processed: this.processedIds.size, + processing: this.processingIds.size + }; } private async handleError(error: any): Promise { @@ -112,30 +153,51 @@ export class FirestoreWatcher { for (const change of changes) { const doc = change.doc; + const docId = doc.id; const data = doc.data(); try { switch (change.type) { case "added": case "modified": - setTimeout(() => { - console.log( - `${collectionPath} - processing - ${doc.id}` - ); - if (adapter.lockedIds.includes(doc.id)) return; - this.handleCreateOrUpdate(doc, data); - }, 2_000); + // Check if already processed or currently processing + if (this.processedIds.has(docId) || this.processingIds.has(docId)) { + console.log(`${collectionPath} - skipping duplicate/processing - ${docId}`); + continue; + } + + // Check if locked in adapter + if (adapter.lockedIds.includes(docId)) { + console.log(`${collectionPath} - skipping locked - ${docId}`); + continue; + } + + // Mark as currently processing + this.processingIds.add(docId); + + // Process immediately without setTimeout to prevent race conditions + console.log(`${collectionPath} - processing - ${docId}`); + await this.handleCreateOrUpdate(doc, data); + + // Mark as processed and remove from processing + this.processedIds.add(docId); + this.processingIds.delete(docId); break; + case "removed": - console.log(`Document removed: ${doc.id}`); - // Handle document removal if needed + console.log(`Document removed: ${docId}`); + // Remove from processed IDs when document is deleted + this.processedIds.delete(docId); + this.processingIds.delete(docId); break; } } catch (error) { console.error( - `Error processing ${change.type} for document ${doc.id}:`, + `Error processing ${change.type} for document ${docId}:`, error ); + // Remove from processing IDs on error + this.processingIds.delete(docId); // Continue processing other changes even if one fails } } diff --git a/platforms/cerberus/src/database/entities/VotingObservation.ts b/platforms/cerberus/src/database/entities/VotingObservation.ts index 669129aa..c5fa7d31 100644 --- a/platforms/cerberus/src/database/entities/VotingObservation.ts +++ b/platforms/cerberus/src/database/entities/VotingObservation.ts @@ -9,7 +9,7 @@ export class VotingObservation { @Column("uuid") groupId!: string; - @Column("uuid") + @Column("uuid", { nullable: true }) owner!: string; @ManyToOne(() => User) diff --git a/platforms/evoting-api/src/controllers/PollController.ts b/platforms/evoting-api/src/controllers/PollController.ts index 0f7f0a39..ec9bf0f5 100644 --- a/platforms/evoting-api/src/controllers/PollController.ts +++ b/platforms/evoting-api/src/controllers/PollController.ts @@ -51,8 +51,12 @@ export class PollController { createPoll = async (req: Request, res: Response) => { try { + console.log('🔍 Full request body:', req.body); const { title, mode, visibility, options, deadline, groupId } = req.body; const creatorId = (req as any).user.id; + + console.log('🔍 Extracted data:', { title, mode, visibility, options, deadline, groupId, creatorId }); + console.log('🔍 groupId type:', typeof groupId, 'value:', groupId); // groupId is optional - only required for system messages @@ -66,6 +70,7 @@ export class PollController { groupId }); + console.log('🔍 Created poll:', poll); res.status(201).json(poll); } catch (error) { console.error("Error creating poll:", error); diff --git a/platforms/evoting-api/src/controllers/poll.controller.ts b/platforms/evoting-api/src/controllers/poll.controller.ts index 560cac18..5a695cd2 100644 --- a/platforms/evoting-api/src/controllers/poll.controller.ts +++ b/platforms/evoting-api/src/controllers/poll.controller.ts @@ -14,7 +14,11 @@ export class PollController { constructor(private readonly pollService: PollService) {} async createPoll(req: Request, res: Response) { - const { title, mode, visibility, options, deadline } = req.body; + console.log('🔍 Full request body:', req.body); + const { title, mode, visibility, options, deadline, groupId } = req.body; + console.log('🔍 Extracted data:', { title, mode, visibility, options, deadline, groupId }); + console.log('🔍 groupId type:', typeof groupId, 'value:', groupId); + if (!title || !mode || !visibility || !options) { return res.status(400).json({ error: "Missing required fields" }); } @@ -36,9 +40,12 @@ export class PollController { visibility, options, deadline ? new Date(deadline) : undefined, + groupId, ); + console.log('🔍 Created poll:', poll); res.status(201).json(poll); } catch (err: unknown) { + console.error('❌ Error creating poll:', err); res.status(400).json({ error: (err as Error).message }); } } diff --git a/platforms/evoting-api/src/services/PollService.ts b/platforms/evoting-api/src/services/PollService.ts index 8911b567..b8ac5d0e 100644 --- a/platforms/evoting-api/src/services/PollService.ts +++ b/platforms/evoting-api/src/services/PollService.ts @@ -148,6 +148,8 @@ export class PollService { creatorId: string; groupId?: string; // Optional groupId for system messages }): Promise { + console.log('🔍 PollService.createPoll called with:', pollData); + const creator = await this.userRepository.findOne({ where: { id: pollData.creatorId } }); @@ -156,17 +158,23 @@ export class PollService { throw new Error("Creator not found"); } - const poll = this.pollRepository.create({ + const pollDataForEntity = { title: pollData.title, mode: pollData.mode as "normal" | "point" | "rank", visibility: pollData.visibility as "public" | "private", options: pollData.options, deadline: pollData.deadline ? new Date(pollData.deadline) : null, creator, - creatorId: pollData.creatorId - }); + creatorId: pollData.creatorId, + groupId: pollData.groupId || null + }; + console.log('🔍 Creating poll entity with data:', pollDataForEntity); + + const poll = this.pollRepository.create(pollDataForEntity); + console.log('🔍 Poll entity created:', poll); const savedPoll = await this.pollRepository.save(poll); + console.log('🔍 Poll saved to database:', savedPoll); // Create a system message about the new vote if (pollData.groupId) { diff --git a/platforms/evoting-api/src/services/poll.service.ts b/platforms/evoting-api/src/services/poll.service.ts index dcd43001..3629d192 100644 --- a/platforms/evoting-api/src/services/poll.service.ts +++ b/platforms/evoting-api/src/services/poll.service.ts @@ -61,20 +61,31 @@ export class PollService { visibility: "public" | "private", options: string[], deadline?: Date, + groupId?: string, ): Promise { + console.log('🔍 poll.service.createPoll called with:', { title, mode, visibility, options, deadline, groupId }); + if (options.length < 2) { throw new Error("At least two options are required"); } - const poll = this.pollRepository.create({ + const pollData = { title, mode, visibility, options, deadline: deadline ?? null, - }); + groupId: groupId ?? null, + }; + console.log('🔍 Creating poll entity with data:', pollData); - return await this.pollRepository.save(poll); + const poll = this.pollRepository.create(pollData); + console.log('🔍 Poll entity created:', poll); + + const savedPoll = await this.pollRepository.save(poll); + console.log('🔍 Poll saved to database:', savedPoll); + + return savedPoll; } // Get a poll by ID