Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions platforms/blabsy/src/components/common/maintenance-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useEffect, useState } from 'react';
import axios from 'axios';

interface Motd {
status: 'up' | 'maintenance';
message: string;
}
Comment on lines +4 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract shared type definition to prevent drift.

The Motd interface is duplicated across three platforms (blabsy, eVoting, group-charter-manager). This creates a maintenance burden and risk of type inconsistencies.

Consider creating a shared types package (e.g., packages/types/src/motd.ts) and importing from there:

// packages/types/src/motd.ts
export interface Motd {
    status: 'up' | 'maintenance';
    message: string;
}

Then import in each platform:

-interface Motd {
-    status: 'up' | 'maintenance';
-    message: string;
-}
+import type { Motd } from '@repo/types';
🤖 Prompt for AI Agents
In platforms/blabsy/src/components/common/maintenance-banner.tsx around lines 4
to 7, the Motd interface is duplicated across multiple platforms; extract it to
a shared types package (e.g., packages/types/src/motd.ts) that exports the Motd
interface, update this file to import Motd from that package instead of
declaring it locally, and update the other platforms (eVoting,
group-charter-manager) to import the same shared type to prevent drift and
ensure a single source of truth.


export function MaintenanceBanner(): JSX.Element | null {
const [motd, setMotd] = useState<Motd | null>(null);

useEffect(() => {
const fetchMotd = async () => {
try {
const registryUrl = process.env.NEXT_PUBLIC_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get<Motd>(`${registryUrl}/motd`);
setMotd(response.data);
} catch (error) {
console.error('Failed to fetch motd:', error);
}
};

void fetchMotd();
}, []);
Comment on lines +12 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout and consider periodic refetching.

The axios request lacks a timeout configuration, which could cause the request to hang indefinitely if the registry is unresponsive. Additionally, the MOTD is fetched only once on mount, so users won't see status updates without a page refresh.

Apply these improvements:

 useEffect(() => {
     const fetchMotd = async () => {
         try {
             const registryUrl = process.env.NEXT_PUBLIC_REGISTRY_URL || 'http://localhost:4321';
-            const response = await axios.get<Motd>(`${registryUrl}/motd`);
+            const response = await axios.get<Motd>(`${registryUrl}/motd`, {
+                timeout: 5000, // 5 second timeout
+            });
             setMotd(response.data);
         } catch (error) {
             console.error('Failed to fetch motd:', error);
         }
     };
 
     void fetchMotd();
+    
+    // Optionally refetch every 5 minutes to catch status changes
+    const interval = setInterval(() => void fetchMotd(), 5 * 60 * 1000);
+    return () => clearInterval(interval);
 }, []);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
const fetchMotd = async () => {
try {
const registryUrl = process.env.NEXT_PUBLIC_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get<Motd>(`${registryUrl}/motd`);
setMotd(response.data);
} catch (error) {
console.error('Failed to fetch motd:', error);
}
};
void fetchMotd();
}, []);
useEffect(() => {
const fetchMotd = async () => {
try {
const registryUrl = process.env.NEXT_PUBLIC_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get<Motd>(`${registryUrl}/motd`, {
timeout: 5000, // 5 second timeout
});
setMotd(response.data);
} catch (error) {
console.error('Failed to fetch motd:', error);
}
};
void fetchMotd();
// Optionally refetch every 5 minutes to catch status changes
const interval = setInterval(() => void fetchMotd(), 5 * 60 * 1000);
return () => clearInterval(interval);
}, []);
🤖 Prompt for AI Agents
In platforms/blabsy/src/components/common/maintenance-banner.tsx around lines 12
to 24, the axios GET has no timeout and the MOTD is fetched only once; update
the fetch to include a request timeout (e.g., axios timeout or AbortController)
and implement periodic refetching using setInterval (choose a sensible interval
like 30–60s), ensure you handle request cancellation on unmount/interval reset
to avoid leaks, and keep error handling intact while updating state only when
the component is still mounted.


if (motd?.status !== 'maintenance') {
return null;
}

return (
<div className='bg-yellow-500 px-4 py-3 text-center text-sm font-medium text-black'>
⚠️ {motd.message}
</div>
);
}
Comment on lines +1 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Eliminate code duplication across platforms.

The MaintenanceBanner component is nearly identical across three platforms (blabsy, eVoting, group-charter-manager), with only minor styling and directive differences. This creates a significant maintenance burden.

Consider one of these approaches:

Option 1 (Recommended): Extract to a shared package:

// packages/ui/src/maintenance-banner.tsx
'use client';

export interface MaintenanceBannerProps {
    className?: string;
}

export function MaintenanceBanner({ className = 'bg-yellow-500 px-4 py-3 text-center text-sm font-medium text-black' }: MaintenanceBannerProps) {
    // ... shared logic
}

Then import in each platform with platform-specific styling if needed.

Option 2: If styling must differ significantly, extract just the hook logic:

// packages/hooks/src/use-motd.ts
export function useMotd() {
    const [motd, setMotd] = useState<Motd | null>(null);
    // ... fetch logic
    return motd;
}


2 changes: 2 additions & 0 deletions platforms/blabsy/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import '@styles/globals.scss';
import { AuthContextProvider } from '@lib/context/auth-context';
import { ThemeContextProvider } from '@lib/context/theme-context';
import { AppHead } from '@components/common/app-head';
import { MaintenanceBanner } from '@components/common/maintenance-banner';
import type { ReactElement, ReactNode } from 'react';
import type { NextPage } from 'next';
import type { AppProps } from 'next/app';
Expand All @@ -24,6 +25,7 @@ export default function App({
return (
<>
<AppHead />
<MaintenanceBanner />
<AuthContextProvider>
<ThemeContextProvider>
{getLayout(<Component {...pageProps} />)}
Expand Down
2 changes: 2 additions & 0 deletions platforms/eVoting/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from "@/lib/auth-context";
import { MaintenanceBanner } from "@/components/MaintenanceBanner";

export const metadata: Metadata = {
title: "eVoting Platform",
Expand All @@ -18,6 +19,7 @@ export default function RootLayout({
return (
<html lang="en">
<body>
<MaintenanceBanner />
<AuthProvider>
{children}
</AuthProvider>
Expand Down
38 changes: 38 additions & 0 deletions platforms/eVoting/src/components/MaintenanceBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { useEffect, useState } from 'react';
import axios from 'axios';

interface Motd {
status: 'up' | 'maintenance';
message: string;
}

export function MaintenanceBanner() {
const [motd, setMotd] = useState<Motd | null>(null);

useEffect(() => {
const fetchMotd = async () => {
try {
const registryUrl = process.env.NEXT_PUBLIC_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get<Motd>(`${registryUrl}/motd`);
setMotd(response.data);
} catch (error) {
console.error('Failed to fetch motd:', error);
}
};

fetchMotd();
}, []);

if (motd?.status !== 'maintenance') {
return null;
}

return (
<div className="bg-yellow-500 px-4 py-3 text-center text-sm font-medium text-black">
⚠️ {motd.message}
</div>
);
}

2 changes: 2 additions & 0 deletions platforms/group-charter-manager/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./globals.css";
import { Toaster } from "@/components/ui/toaster";
import { AuthProvider } from "@/components/auth/auth-provider";
import DisclaimerModal from "@/components/disclaimer-modal";
import { MaintenanceBanner } from "@/components/MaintenanceBanner";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -23,6 +24,7 @@ export default function RootLayout({
return (
<html lang="en">
<body className={inter.className}>
<MaintenanceBanner />
<AuthProvider>
{children}
<Toaster />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use client';

import { useEffect, useState } from 'react';
import axios from 'axios';

interface Motd {
status: 'up' | 'maintenance';
message: string;
}

export function MaintenanceBanner() {
const [motd, setMotd] = useState<Motd | null>(null);

useEffect(() => {
const fetchMotd = async () => {
try {
const registryUrl = process.env.NEXT_PUBLIC_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get<Motd>(`${registryUrl}/motd`);
setMotd(response.data);
} catch (error) {
console.error('Failed to fetch motd:', error);
}
};

fetchMotd();
}, []);

if (motd?.status !== 'maintenance') {
return null;
}

return (
<div className="bg-yellow-500 px-4 py-3 text-center text-sm font-medium text-black">
⚠️ {motd.message}
</div>
);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<script lang="ts">
import { onMount } from 'svelte';
import axios from 'axios';
let motd = $state<{ status: 'up' | 'maintenance'; message: string } | null>(null);
onMount(async () => {
try {
const registryUrl = import.meta.env.VITE_REGISTRY_URL || 'http://localhost:4321';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

❓ Verification inconclusive

Hardcoded localhost fallback may not work in all environments.

The fallback http://localhost:4321 will only work in local development. In production or staging environments where the registry runs on a different host, this fallback could cause silent failures.

Consider:

  • Documenting that VITE_REGISTRY_URL must be set for non-local environments
  • Or providing a more robust fallback strategy (e.g., relative URL or throwing an error if env var is missing in production)

Run the following script to check if VITE_REGISTRY_URL is documented:


🏁 Script executed:

#!/bin/bash
# Check for documentation of VITE_REGISTRY_URL
rg -n "VITE_REGISTRY_URL" --type=md -C3

Length of output: 0


Missing documentation and unsafe localhost fallback
No references to VITE_REGISTRY_URL were found in the docs; the hardcoded fallback (http://localhost:4321) will break in staging/production. Add documentation requiring this env var in non-local environments or enforce it at runtime (e.g., throw an error or use a relative URL when import.meta.env.VITE_REGISTRY_URL is undefined).

🤖 Prompt for AI Agents
In
platforms/pictique/src/lib/fragments/MaintenanceBanner/MaintenanceBanner.svelte
around line 9, the code falls back to a hardcoded 'http://localhost:4321' when
import.meta.env.VITE_REGISTRY_URL is missing, which is unsafe for
staging/production and undocumented; update the file to enforce presence of
VITE_REGISTRY_URL in non-local environments by: 1) removing or avoiding a
localhost fallback, 2) throwing a clear runtime error when the env var is
undefined in NODE_ENV !== 'development' (or use a relative URL if intended for
production), and 3) add/update project docs to require VITE_REGISTRY_URL for
non-local deployments with example values and where to set it (CI/env config).

const response = await axios.get(`${registryUrl}/motd`);
motd = response.data;
} catch (error) {
console.error('Failed to fetch motd:', error);
}
});
Comment on lines 8 to 16
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout and improve error handling for the MOTD fetch.

The axios request lacks a timeout configuration, which could cause the fetch to hang indefinitely if the registry is unresponsive. Additionally, the error handling only logs to console without providing any user feedback or fallback behavior.

Apply this diff to add timeout and improve reliability:

 	onMount(async () => {
 		try {
 			const registryUrl = import.meta.env.VITE_REGISTRY_URL || 'http://localhost:4321';
-			const response = await axios.get(`${registryUrl}/motd`);
+			const response = await axios.get(`${registryUrl}/motd`, {
+				timeout: 5000,
+			});
 			motd = response.data;
 		} catch (error) {
-			console.error('Failed to fetch motd:', error);
+			console.error('Failed to fetch MOTD:', error);
+			// Silently fail - no banner shown if fetch fails
 		}
 	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onMount(async () => {
try {
const registryUrl = import.meta.env.VITE_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get(`${registryUrl}/motd`);
motd = response.data;
} catch (error) {
console.error('Failed to fetch motd:', error);
}
});
onMount(async () => {
try {
const registryUrl = import.meta.env.VITE_REGISTRY_URL || 'http://localhost:4321';
const response = await axios.get(`${registryUrl}/motd`, {
timeout: 5000,
});
motd = response.data;
} catch (error) {
console.error('Failed to fetch MOTD:', error);
// Silently fail - no banner shown if fetch fails
}
});
🤖 Prompt for AI Agents
In
platforms/pictique/src/lib/fragments/MaintenanceBanner/MaintenanceBanner.svelte
around lines 7 to 15, the MOTD fetch has no timeout and only logs errors to
console; update the axios.get call to include a timeout (e.g. 2–5s) and check
the HTTP status, and in the catch branch set a safe fallback for motd (like a
default message or null) and a reactive error flag so the UI can show
user-visible feedback; also differentiate timeout/network errors vs non-2xx
responses in the error handling and ensure the error is logged with details for
debugging.

</script>

{#if motd?.status === 'maintenance'}
<div class="bg-yellow-500 px-4 py-3 text-center text-sm font-medium text-black">
⚠️ {motd.message}
</div>
{/if}

18 changes: 11 additions & 7 deletions platforms/pictique/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { onMount } from 'svelte';
import '../app.css';
import { page } from '$app/state';
import MaintenanceBanner from '$lib/fragments/MaintenanceBanner/MaintenanceBanner.svelte';
let { children } = $props();
Expand Down Expand Up @@ -46,11 +47,14 @@
<img src="/images/Logo.svg" alt="logo" />
</main>
{:else}
<main
class="h-[100dvh] overflow-hidden {page.url.pathname.includes('/profile')
? 'px-0'
: 'px-4'} md:px-0"
>
{@render children()}
</main>
<div class="flex h-[100dvh] flex-col overflow-hidden">
<MaintenanceBanner />
<main
class="flex-1 overflow-hidden {page.url.pathname.includes('/profile')
? 'px-0'
: 'px-4'} md:px-0"
>
{@render children()}
</main>
</div>
Comment on lines +50 to +59
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Review overflow handling to ensure content remains scrollable.

The wrapper div has overflow-hidden while the inner main also has overflow-hidden and flex-1. This combination may prevent the main content from scrolling when it exceeds the available viewport height (especially when the maintenance banner is displayed).

Consider:

  • Removing overflow-hidden from the wrapper div, or
  • Changing the inner main to use overflow-y-auto instead of overflow-hidden

Apply this diff to allow main content to scroll:

-	<div class="flex h-[100dvh] flex-col overflow-hidden">
+	<div class="flex h-[100dvh] flex-col">
 		<MaintenanceBanner />
 		<main
-			class="flex-1 overflow-hidden {page.url.pathname.includes('/profile')
+			class="flex-1 overflow-y-auto {page.url.pathname.includes('/profile')
 				? 'px-0'
 				: 'px-4'}  md:px-0"
 		>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="flex h-[100dvh] flex-col overflow-hidden">
<MaintenanceBanner />
<main
class="flex-1 overflow-hidden {page.url.pathname.includes('/profile')
? 'px-0'
: 'px-4'} md:px-0"
>
{@render children()}
</main>
</div>
<div class="flex h-[100dvh] flex-col">
<MaintenanceBanner />
<main
class="flex-1 overflow-y-auto {page.url.pathname.includes('/profile')
? 'px-0'
: 'px-4'} md:px-0"
>
{@render children()}
</main>
</div>
🤖 Prompt for AI Agents
In platforms/pictique/src/routes/+layout.svelte around lines 50 to 59, the outer
wrapper and inner main both use overflow-hidden which can prevent scrolling of
main content; remove overflow-hidden from the wrapper div (or at minimum change
the inner main's overflow to overflow-y-auto) so the main can scroll when its
content exceeds the viewport; update the class on the wrapper to drop
overflow-hidden and/or change main's class to use overflow-y-auto (and keep
flex-1) so the page remains scrollable when the maintenance banner or other
content reduces available height.

{/if}
4 changes: 4 additions & 0 deletions platforms/registry/motd.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"status": "maintenance",
"message": "Registry service is down"
}
22 changes: 21 additions & 1 deletion platforms/registry/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import fastify from "fastify";
import { generateEntropy, generatePlatformToken, getJWK } from "./jwt";
import dotenv from "dotenv";
import path from "path";
import path from "node:path";
import { AppDataSource } from "./config/database";
import { VaultService } from "./services/VaultService";
import { UriResolutionService } from "./services/UriResolutionService";
import { KubernetesService } from "./services/KubernetesService";
import cors from "@fastify/cors";

import fs from "node:fs";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Synchronous I/O blocks the event loop and lacks error handling.

The implementation has several serious issues:

  1. Blocking I/O: fs.readFileSync blocks the event loop during startup and file reloads, degrading server responsiveness.
  2. No error handling: Missing try-catch will crash the server if the file is missing or contains invalid JSON.
  3. Path resolution risk: __dirname may not resolve correctly after TypeScript compilation, depending on build configuration.

Apply this diff to use async I/O with proper error handling:

-import fs from "node:fs";
+import fs from "node:fs/promises";

-function loadMotdJSON() {
-    const motdJSON = fs.readFileSync(path.resolve(__dirname, "../motd.json"), "utf8");
-    return JSON.parse(motdJSON) as {
+async function loadMotdJSON() {
+    try {
+        const motdPath = path.resolve(__dirname, "../motd.json");
+        const motdJSON = await fs.readFile(motdPath, "utf8");
+        return JSON.parse(motdJSON) as {
+            status: "up" | "maintenance"
+            message: string
+        };
+    } catch (error) {
+        server.log.error({ message: "Failed to load MOTD", error });
+        return {
+            status: "up" as const,
+            message: "Registry service is running"
+        };
+    }
+}
+
+let motd = {
-        status: "up" | "maintenance"
-        message: string
-    };
+    status: "up" as const,
+    message: "Registry service is running"
-}
+};

-let motd = loadMotdJSON();
+(async () => {
+    motd = await loadMotdJSON();
+})();

Note: You'll also need to update the file watcher (see next comment).

Also applies to: 13-19, 21-21


function loadMotdJSON() {
const motdJSON = fs.readFileSync(path.resolve(__dirname, "../motd.json"), "utf8");
return JSON.parse(motdJSON) as {
status: "up" | "maintenance"
message: string
};
}

let motd = loadMotdJSON();

fs.watchFile(path.resolve(__dirname, "../motd.json"), (_curr, _prev) => {
motd = loadMotdJSON();
});
Comment on lines +23 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Use fs.watch instead of fs.watchFile and add cleanup.

fs.watchFile uses inefficient stat polling and creates a resource leak without cleanup.

Apply this diff to use event-based watching with proper cleanup:

-fs.watchFile(path.resolve(__dirname, "../motd.json"), (_curr, _prev) => {
-    motd = loadMotdJSON();
-});
+const motdWatcher = fs.watch(
+    path.resolve(__dirname, "../motd.json"),
+    async (eventType) => {
+        if (eventType === "change") {
+            motd = await loadMotdJSON();
+            server.log.info("MOTD reloaded");
+        }
+    }
+);
+
+// Cleanup on server close
+server.addHook("onClose", async () => {
+    motdWatcher.close();
+});

Note: This assumes you've converted loadMotdJSON() to async as suggested in the previous comment.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs.watchFile(path.resolve(__dirname, "../motd.json"), (_curr, _prev) => {
motd = loadMotdJSON();
});
const motdWatcher = fs.watch(
path.resolve(__dirname, "../motd.json"),
async (eventType) => {
if (eventType === "change") {
motd = await loadMotdJSON();
server.log.info("MOTD reloaded");
}
}
);
// Cleanup on server close
server.addHook("onClose", async () => {
motdWatcher.close();
});


dotenv.config({ path: path.resolve(__dirname, "../../../.env") });

const server = fastify({ logger: true });
Expand Down Expand Up @@ -55,6 +71,10 @@ const checkSharedSecret = async (request: any, reply: any) => {
}
};

server.get("/motd", async (request, reply) => {
return motd;
});

// Create a new vault entry
server.post(
"/register",
Expand Down
Loading