Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 1 addition & 2 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
@modl-gg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
@modl-gg:registry=https://nexus.modl.gg/repository/npm-releases/
2 changes: 2 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MonitoringPage from '@/pages/MonitoringPage';
import LoadingPage from '@/pages/LoadingPage';
import AnalyticsPage from '@/pages/AnalyticsPage';
import SystemPromptsPage from '@/pages/SystemPromptsPage';
import AlertsPage from '@/pages/AlertsPage';

function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuth();
Expand Down Expand Up @@ -46,6 +47,7 @@ function AppRoutes() {
<Route path="/monitoring" component={MonitoringPage} />
<Route path="/analytics" component={AnalyticsPage} />
<Route path="/prompts" component={SystemPromptsPage} />
<Route path="/alerts" component={AlertsPage} />
<Route>
<Redirect to="/" />
</Route>
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Link, useLocation } from 'wouter';
import { LayoutDashboard, Server, Activity, BarChart3, Sparkles, LogOut } from 'lucide-react';
import { LayoutDashboard, Server, Activity, BarChart3, Sparkles, Bell, LogOut } from 'lucide-react';
import { Button } from '@modl-gg/shared-web/components/ui/button';
import { useAuth } from '@/hooks/useAuth';
import Logo from '@/components/Logo';
Expand All @@ -10,6 +10,7 @@ const navItems = [
{ href: '/monitoring', label: 'Monitoring', icon: Activity },
{ href: '/analytics', label: 'Analytics', icon: BarChart3 },
{ href: '/prompts', label: 'AI Prompts', icon: Sparkles },
{ href: '/alerts', label: 'Alerts', icon: Bell },
];

export default function Layout({ children }: { children: React.ReactNode }) {
Expand Down
2 changes: 1 addition & 1 deletion client/src/hooks/useAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function useAuth() {
throw caught;
}
},
staleTime: 10 * 60 * 1000,
staleTime: 60 * 1000,
retry: false,
});

Expand Down
20 changes: 20 additions & 0 deletions client/src/lib/api-contracts/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ export function normalizeDateValue(value: unknown): string | undefined {
return parsed.toISOString();
}

export function normalizeEpochMillisValue(value: unknown): string | undefined {
let millis: number | undefined;

if (typeof value === 'number' && Number.isFinite(value)) {
millis = value;
} else if (typeof value === 'string' && /^-?\d+$/.test(value)) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
millis = parsed;
}
}

if (millis === undefined || millis <= 0) {
return undefined;
}

const date = new Date(millis);
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
}

export function normalizeStringArray(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
Expand Down
6 changes: 0 additions & 6 deletions client/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,12 +497,6 @@ class ApiClient {
return this.request(`/security/events${query ? `?${query}` : ''}`);
}

async testSecurityConfig() {
return this.request('/security/test', {
method: 'POST',
});
}

async getRateLimitStatus() {
return this.request('/system/rate-limits');
}
Expand Down
96 changes: 96 additions & 0 deletions client/src/lib/services/alerts-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { requestJsonRaw } from '@/lib/api';
import { isRecord, normalizeEpochMillisValue, toEpochMillisString } from '@/lib/api-contracts/common';

export type SystemAlertSeverity = 'BASIC' | 'WARNING' | 'CRITICAL';
export type SystemAlertAudience = 'ALL_PANEL_USERS' | 'SUPER_ADMINS_ONLY';

export interface SystemAlert {
id: string;
message: string;
severity: SystemAlertSeverity;
audience: SystemAlertAudience;
expiresAt?: string;
createdAt?: string;
updatedAt?: string;
createdBy?: string;
updatedBy?: string;
}

interface RawSystemAlert {
id?: unknown;
_id?: unknown;
message?: unknown;
severity?: unknown;
audience?: unknown;
expiresAt?: unknown;
createdAt?: unknown;
updatedAt?: unknown;
createdBy?: unknown;
updatedBy?: unknown;
}

export interface AlertPayload {
message: string;
severity: SystemAlertSeverity;
audience: SystemAlertAudience;
expiresAt: string;
}

function toSeverity(value: unknown): SystemAlertSeverity {
const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
return normalized === 'WARNING' || normalized === 'CRITICAL' ? normalized : 'BASIC';
}

function toAudience(value: unknown): SystemAlertAudience {
const normalized = typeof value === 'string' ? value.trim().toUpperCase() : '';
return normalized === 'SUPER_ADMINS_ONLY' ? normalized : 'ALL_PANEL_USERS';
}

function mapAlert(raw: RawSystemAlert): SystemAlert {
const id = typeof raw.id === 'string' ? raw.id : (typeof raw._id === 'string' ? raw._id : '');

return {
id,
message: typeof raw.message === 'string' ? raw.message : '',
severity: toSeverity(raw.severity),
audience: toAudience(raw.audience),
expiresAt: normalizeEpochMillisValue(raw.expiresAt),
createdAt: normalizeEpochMillisValue(raw.createdAt),
updatedAt: normalizeEpochMillisValue(raw.updatedAt),
createdBy: typeof raw.createdBy === 'string' ? raw.createdBy : undefined,
updatedBy: typeof raw.updatedBy === 'string' ? raw.updatedBy : undefined,
};
}

function toRequestBody(payload: AlertPayload): Record<string, unknown> {
return {
message: payload.message,
severity: payload.severity,
audience: payload.audience,
expiresAt: toEpochMillisString(payload.expiresAt) ?? '0',
};
}

export const alertsService = {
async getAlerts(): Promise<SystemAlert[]> {
const raw = await requestJsonRaw<unknown>('/v1/admin/alerts');
const items = isRecord(raw) && Array.isArray(raw.items) ? (raw.items as RawSystemAlert[]) : [];
return items.map(mapAlert);
},
Comment on lines +75 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Unwrap alert responses

This reads items from the top-level response, but the other admin services unwrap the API envelope before mapping payloads. If /v1/admin/alerts returns the same shape as the rest of these endpoints, such as { success: true, data: { items: [...] } }, this returns an empty array and the Alerts page shows no alerts even when alerts exist.

Suggested change
async getAlerts(): Promise<SystemAlert[]> {
const raw = await requestJsonRaw<unknown>('/v1/admin/alerts');
const items = isRecord(raw) && Array.isArray(raw.items) ? (raw.items as RawSystemAlert[]) : [];
return items.map(mapAlert);
},
async getAlerts(): Promise<SystemAlert[]> {
const raw = await requestJsonRaw<unknown>('/v1/admin/alerts');
const payload = isRecord(raw) && 'data' in raw ? raw.data : raw;
const items = Array.isArray(payload)
? (payload as RawSystemAlert[])
: (isRecord(payload) && Array.isArray(payload.items) ? (payload.items as RawSystemAlert[]) : []);
return items.map(mapAlert);
},

Rule Used: This is a React frontend project on React 19 with ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: client/src/lib/services/alerts-service.ts
Line: 75-79

Comment:
**Unwrap alert responses**

This reads `items` from the top-level response, but the other admin services unwrap the API envelope before mapping payloads. If `/v1/admin/alerts` returns the same shape as the rest of these endpoints, such as `{ success: true, data: { items: [...] } }`, this returns an empty array and the Alerts page shows no alerts even when alerts exist.

```suggestion
  async getAlerts(): Promise<SystemAlert[]> {
    const raw = await requestJsonRaw<unknown>('/v1/admin/alerts');
    const payload = isRecord(raw) && 'data' in raw ? raw.data : raw;
    const items = Array.isArray(payload)
      ? (payload as RawSystemAlert[])
      : (isRecord(payload) && Array.isArray(payload.items) ? (payload.items as RawSystemAlert[]) : []);
    return items.map(mapAlert);
  },
```

**Rule Used:** This is a React frontend project on React 19 with ... ([source](https://app.greptile.com/modl-gg/-/custom-context?memory=b7532101-0c9e-4ab6-b168-353a105ba593))

How can I resolve this? If you propose a fix, please make it concise.


async createAlert(payload: AlertPayload): Promise<SystemAlert> {
const raw = await requestJsonRaw<RawSystemAlert>('/v1/admin/alerts', {
method: 'POST',
body: toRequestBody(payload),
});
return mapAlert(raw);
},

async updateAlert(id: string, payload: AlertPayload): Promise<SystemAlert> {
const raw = await requestJsonRaw<RawSystemAlert>(`/v1/admin/alerts/${id}`, {
method: 'PUT',
body: toRequestBody(payload),
});
return mapAlert(raw);
},
};
2 changes: 1 addition & 1 deletion client/src/lib/services/analytics-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { requestJsonRaw, requestText } from '@/lib/api';
import { parseNumber, isRecord, unwrapEnvelope } from '@/lib/api-contracts/common';
import { isRecord, unwrapEnvelope } from '@/lib/api-contracts/common';

export type AnalyticsRange = '7d' | '30d' | '90d' | '1y';
export type AnalyticsExportType = 'csv' | 'json';
Expand Down
3 changes: 2 additions & 1 deletion client/src/lib/services/auth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ interface SessionPayload {
interface LoginPayload {
email?: unknown;
lastActivityAt?: unknown;
isAuthenticated?: boolean;
}

function mapSessionPayload(payload: SessionPayload): AdminSession {
Expand Down Expand Up @@ -63,7 +64,7 @@ export const authService = {
email: typeof data.email === 'string' ? data.email : email,
lastActivityAt: normalizeDateValue(data.lastActivityAt),
loggedInIps: [],
isAuthenticated: true,
isAuthenticated: data.isAuthenticated ?? true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Validate login state
This returns data.isAuthenticated directly whenever the field is present. If the API returns a string value such as "false", the runtime value becomes a truthy string and callers like ProtectedRoute can treat the login as authenticated until the session refetch corrects it. Keep the mapper consistent with mapSessionPayload by only accepting a real boolean true as authenticated.

Suggested change
isAuthenticated: data.isAuthenticated ?? true,
isAuthenticated: data.isAuthenticated === true,
Prompt To Fix With AI
This is a comment left during a code review.
Path: client/src/lib/services/auth-service.ts
Line: 67

Comment:
**Validate login state**
This returns `data.isAuthenticated` directly whenever the field is present. If the API returns a string value such as `"false"`, the runtime value becomes a truthy string and callers like `ProtectedRoute` can treat the login as authenticated until the session refetch corrects it. Keep the mapper consistent with `mapSessionPayload` by only accepting a real boolean `true` as authenticated.

```suggestion
      isAuthenticated: data.isAuthenticated === true,
```

How can I resolve this? If you propose a fix, please make it concise.

};
},

Expand Down
19 changes: 17 additions & 2 deletions client/src/lib/services/servers-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ function normalizeSubscriptionStatus(value: unknown): SubscriptionStatus | undef
: undefined;
}

function toOptionalCount(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}

if (typeof value === 'string' && /^-?\d+$/.test(value)) {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}

return undefined;
}

function ensureServerId(value: RawServer): string {
if (typeof value.id === 'string') {
return value.id;
Expand All @@ -219,8 +234,8 @@ function mapServerListItem(raw: RawServer): AdminServerListItem {
provisioningStatus: normalizeProvisioningStatus(raw.provisioningStatus),
createdAt: normalizeDateValue(raw.createdAt),
updatedAt: normalizeDateValue(raw.updatedAt),
userCount: typeof raw.userCount === 'number' ? raw.userCount : undefined,
ticketCount: typeof raw.ticketCount === 'number' ? raw.ticketCount : undefined,
userCount: toOptionalCount(raw.userCount),
ticketCount: toOptionalCount(raw.ticketCount),
lastActivityAt: normalizeDateValue(raw.lastActivityAt),
};
}
Expand Down
38 changes: 7 additions & 31 deletions client/src/lib/services/system-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,8 @@ export interface MaintenanceStatus {
message: string;
}

export type PromptStrictnessLevel = 'lenient' | 'standard' | 'strict';

export interface SystemPrompt {
id: string;
strictnessLevel: PromptStrictnessLevel;
prompt: string;
isActive: boolean;
createdAt?: string;
Expand All @@ -70,43 +67,24 @@ export interface SystemPrompt {
interface RawSystemPrompt {
id?: unknown;
_id?: unknown;
strictnessLevel?: unknown;
prompt?: unknown;
isActive?: unknown;
createdAt?: unknown;
updatedAt?: unknown;
}

function toPromptStrictness(value: unknown): PromptStrictnessLevel {
if (typeof value !== 'string') {
return 'standard';
}

const normalized = value.trim().toLowerCase();
if (normalized === 'lenient' || normalized === 'strict') {
return normalized;
}

return 'standard';
}

function mapPrompt(raw: RawSystemPrompt): SystemPrompt {
const id = typeof raw.id === 'string' ? raw.id : (typeof raw._id === 'string' ? raw._id : '');

return {
id,
strictnessLevel: toPromptStrictness(raw.strictnessLevel),
prompt: typeof raw.prompt === 'string' ? raw.prompt : '',
isActive: raw.isActive !== false,
createdAt: normalizeDateValue(raw.createdAt),
updatedAt: normalizeDateValue(raw.updatedAt),
};
}

function toUpperStrictness(level: PromptStrictnessLevel): string {
return level.toUpperCase();
}

export const systemService = {
async getSystemConfig(): Promise<SystemConfig> {
const raw = await requestJsonRaw<unknown>('/v1/admin/system/config');
Expand Down Expand Up @@ -149,16 +127,14 @@ export const systemService = {
return message ?? 'Service restart requested';
},

async getSystemPrompts(): Promise<SystemPrompt[]> {
async getSystemPrompt(): Promise<SystemPrompt> {
const raw = await requestJsonRaw<unknown>('/v1/admin/system/prompts');
const { data } = unwrapEnvelope<unknown>(raw, 'admin system prompts');

const prompts = Array.isArray(data) ? (data as RawSystemPrompt[]) : [];
return prompts.map(mapPrompt);
const { data } = unwrapEnvelope<RawSystemPrompt>(raw, 'admin system prompt');
return mapPrompt(data);
},

async updateSystemPrompt(strictnessLevel: PromptStrictnessLevel, prompt: string): Promise<SystemPrompt> {
const raw = await requestJsonRaw<unknown>(`/v1/admin/system/prompts/${toUpperStrictness(strictnessLevel)}`, {
async updateSystemPrompt(prompt: string): Promise<SystemPrompt> {
const raw = await requestJsonRaw<unknown>('/v1/admin/system/prompts', {
method: 'PUT',
body: { prompt },
});
Expand All @@ -167,8 +143,8 @@ export const systemService = {
return mapPrompt(data);
},

async resetSystemPrompt(strictnessLevel: PromptStrictnessLevel): Promise<SystemPrompt> {
const raw = await requestJsonRaw<unknown>(`/v1/admin/system/prompts/${toUpperStrictness(strictnessLevel)}/reset`, {
async resetSystemPrompt(): Promise<SystemPrompt> {
const raw = await requestJsonRaw<unknown>('/v1/admin/system/prompts/reset', {
method: 'POST',
});

Expand Down
Loading