mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 14:12:10 +01:00
feat: complete development environment setup and code quality improvements
- Set up pre-commit hooks with husky and lint-staged for automated code quality - Improved TypeScript type safety by replacing 'any' types with proper generics - Fixed markdown linting violations (MD030 spacing) across all documentation - Fixed compound adjective hyphenation in technical documentation - Fixed invalid JSON union syntax in API documentation examples - Automated code formatting and linting on commit - Enhanced error handling with better type constraints - Configured biome and markdownlint for consistent code style - All changes verified with successful production build
This commit is contained in:
@ -156,9 +156,12 @@ async function getPerformanceHistory(limit: number) {
|
||||
0
|
||||
) / history.length
|
||||
: 0,
|
||||
memoryTrend: calculateTrend(history, "memoryUsage.heapUsed"),
|
||||
memoryTrend: calculateTrend(
|
||||
history as unknown as Record<string, unknown>[],
|
||||
"memoryUsage.heapUsed"
|
||||
),
|
||||
responseTrend: calculateTrend(
|
||||
history,
|
||||
history as unknown as Record<string, unknown>[],
|
||||
"requestMetrics.averageResponseTime"
|
||||
),
|
||||
},
|
||||
@ -539,8 +542,8 @@ function _calculateAverage(
|
||||
: 0;
|
||||
}
|
||||
|
||||
function calculateTrend(
|
||||
history: Array<any>,
|
||||
function calculateTrend<T extends Record<string, unknown>>(
|
||||
history: Array<T>,
|
||||
path: string
|
||||
): "increasing" | "decreasing" | "stable" {
|
||||
if (history.length < 2) return "stable";
|
||||
@ -570,10 +573,18 @@ function calculateTrend(
|
||||
return "stable";
|
||||
}
|
||||
|
||||
function getNestedPropertyValue(obj: any, path: string): number {
|
||||
return (
|
||||
path.split(".").reduce((current, key) => current?.[key] ?? 0, obj) || 0
|
||||
);
|
||||
function getNestedPropertyValue(
|
||||
obj: Record<string, unknown>,
|
||||
path: string
|
||||
): number {
|
||||
const result = path.split(".").reduce((current, key) => {
|
||||
if (current && typeof current === "object" && key in current) {
|
||||
return (current as Record<string, unknown>)[key];
|
||||
}
|
||||
return 0;
|
||||
}, obj as unknown);
|
||||
|
||||
return typeof result === "number" ? result : 0;
|
||||
}
|
||||
|
||||
function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration";
|
||||
import { createAdminHandler } from "@/lib/api";
|
||||
import { z } from "zod";
|
||||
import { createAdminHandler } from "@/lib/api";
|
||||
import { getSchedulerIntegration } from "@/lib/services/schedulers/ServerSchedulerIntegration";
|
||||
|
||||
/**
|
||||
* Get all schedulers with their status and metrics
|
||||
@ -21,72 +21,79 @@ export const GET = createAdminHandler(async (_context) => {
|
||||
};
|
||||
});
|
||||
|
||||
const PostInputSchema = z.object({
|
||||
action: z.enum(["start", "stop", "trigger", "startAll", "stopAll"]),
|
||||
schedulerId: z.string().optional(),
|
||||
}).refine(
|
||||
(data) => {
|
||||
// schedulerId is required for individual scheduler actions
|
||||
const actionsRequiringSchedulerId = ["start", "stop", "trigger"];
|
||||
if (actionsRequiringSchedulerId.includes(data.action)) {
|
||||
return data.schedulerId !== undefined && data.schedulerId.length > 0;
|
||||
const PostInputSchema = z
|
||||
.object({
|
||||
action: z.enum(["start", "stop", "trigger", "startAll", "stopAll"]),
|
||||
schedulerId: z.string().optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
// schedulerId is required for individual scheduler actions
|
||||
const actionsRequiringSchedulerId = ["start", "stop", "trigger"];
|
||||
if (actionsRequiringSchedulerId.includes(data.action)) {
|
||||
return data.schedulerId !== undefined && data.schedulerId.length > 0;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "schedulerId is required for start, stop, and trigger actions",
|
||||
path: ["schedulerId"],
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "schedulerId is required for start, stop, and trigger actions",
|
||||
path: ["schedulerId"],
|
||||
}
|
||||
);
|
||||
);
|
||||
|
||||
/**
|
||||
* Control scheduler operations (start/stop/trigger)
|
||||
* Requires admin authentication
|
||||
*/
|
||||
export const POST = createAdminHandler(async (_context, validatedData) => {
|
||||
const { action, schedulerId } = validatedData;
|
||||
export const POST = createAdminHandler(
|
||||
async (_context, validatedData) => {
|
||||
const { action, schedulerId } = validatedData as z.infer<
|
||||
typeof PostInputSchema
|
||||
>;
|
||||
|
||||
const integration = getSchedulerIntegration();
|
||||
const integration = getSchedulerIntegration();
|
||||
|
||||
switch (action) {
|
||||
case "start":
|
||||
if (schedulerId) {
|
||||
await integration.startScheduler(schedulerId);
|
||||
}
|
||||
break;
|
||||
switch (action) {
|
||||
case "start":
|
||||
if (schedulerId) {
|
||||
await integration.startScheduler(schedulerId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "stop":
|
||||
if (schedulerId) {
|
||||
await integration.stopScheduler(schedulerId);
|
||||
}
|
||||
break;
|
||||
case "stop":
|
||||
if (schedulerId) {
|
||||
await integration.stopScheduler(schedulerId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "trigger":
|
||||
if (schedulerId) {
|
||||
await integration.triggerScheduler(schedulerId);
|
||||
}
|
||||
break;
|
||||
case "trigger":
|
||||
if (schedulerId) {
|
||||
await integration.triggerScheduler(schedulerId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "startAll":
|
||||
await integration.getManager().startAll();
|
||||
break;
|
||||
case "startAll":
|
||||
await integration.getManager().startAll();
|
||||
break;
|
||||
|
||||
case "stopAll":
|
||||
await integration.getManager().stopAll();
|
||||
break;
|
||||
case "stopAll":
|
||||
await integration.getManager().stopAll();
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown action: ${action}`,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
error: `Unknown action: ${action}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Action '${action}' completed successfully`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
{
|
||||
validateInput: PostInputSchema,
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Action '${action}' completed successfully`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}, {
|
||||
validateInput: PostInputSchema,
|
||||
});
|
||||
);
|
||||
|
||||
@ -7,20 +7,20 @@
|
||||
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { sessionMetrics } from "../../../../lib/metrics";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import type { ChatSession } from "../../../../lib/types";
|
||||
import { withErrorHandling } from "@/lib/api/errors";
|
||||
import { createSuccessResponse } from "@/lib/api/response";
|
||||
import { caches } from "@/lib/performance/cache";
|
||||
import { deduplicators } from "@/lib/performance/deduplication";
|
||||
|
||||
// Performance system imports
|
||||
import {
|
||||
PerformanceUtils,
|
||||
performanceMonitor,
|
||||
} from "@/lib/performance/monitor";
|
||||
import { caches } from "@/lib/performance/cache";
|
||||
import { deduplicators } from "@/lib/performance/deduplication";
|
||||
import { withErrorHandling } from "@/lib/api/errors";
|
||||
import { createSuccessResponse } from "@/lib/api/response";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { sessionMetrics } from "../../../../lib/metrics";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import type { ChatSession, MetricsResult } from "../../../../lib/types";
|
||||
|
||||
/**
|
||||
* Converts a Prisma session to ChatSession format for metrics
|
||||
@ -101,9 +101,14 @@ interface MetricsRequestParams {
|
||||
}
|
||||
|
||||
interface MetricsResponse {
|
||||
metrics: any;
|
||||
metrics: MetricsResult;
|
||||
csvUrl: string | null;
|
||||
company: any;
|
||||
company: {
|
||||
id: string;
|
||||
name: string;
|
||||
csvUrl: string;
|
||||
status: string;
|
||||
};
|
||||
dateRange: { minDate: string; maxDate: string } | null;
|
||||
performanceMetrics?: {
|
||||
cacheHit: boolean;
|
||||
@ -207,9 +212,16 @@ const fetchQuestionsWithDeduplication = deduplicators.database.memoize(
|
||||
*/
|
||||
const calculateMetricsWithCache = async (
|
||||
chatSessions: ChatSession[],
|
||||
companyConfig: any,
|
||||
companyConfig: Record<string, unknown>,
|
||||
cacheKey: string
|
||||
): Promise<{ result: any; fromCache: boolean }> => {
|
||||
): Promise<{
|
||||
result: {
|
||||
metrics: MetricsResult;
|
||||
calculatedAt: string;
|
||||
sessionCount: number;
|
||||
};
|
||||
fromCache: boolean;
|
||||
}> => {
|
||||
return caches.metrics
|
||||
.getOrCompute(
|
||||
cacheKey,
|
||||
@ -326,7 +338,7 @@ export const GET = withErrorHandling(async (request: NextRequest) => {
|
||||
deduplicationHit = deduplicators.database.getStats().hitRate > 0;
|
||||
|
||||
// Fetch questions with deduplication
|
||||
const sessionIds = prismaSessions.map((s: any) => s.id);
|
||||
const sessionIds = prismaSessions.map((s) => s.id);
|
||||
const questionsResult = await fetchQuestionsWithDeduplication(sessionIds);
|
||||
const sessionQuestions = questionsResult.result;
|
||||
|
||||
@ -349,7 +361,7 @@ export const GET = withErrorHandling(async (request: NextRequest) => {
|
||||
const { result: chatSessions } = await PerformanceUtils.measureAsync(
|
||||
"metrics-session-conversion",
|
||||
async () => {
|
||||
return prismaSessions.map((ps: any) => {
|
||||
return prismaSessions.map((ps) => {
|
||||
const questions = questionsBySession[ps.id] || [];
|
||||
return convertToMockChatSession(ps, questions);
|
||||
});
|
||||
@ -372,7 +384,7 @@ export const GET = withErrorHandling(async (request: NextRequest) => {
|
||||
if (prismaSessions.length === 0) return null;
|
||||
|
||||
const dates = prismaSessions
|
||||
.map((s: any) => new Date(s.startTime))
|
||||
.map((s) => new Date(s.startTime))
|
||||
.sort((a: Date, b: Date) => a.getTime() - b.getTime());
|
||||
|
||||
return {
|
||||
|
||||
@ -232,14 +232,14 @@ function useCompanyData(
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
const errorMessage = `Failed to load company data (${response.status}: ${response.statusText})`;
|
||||
|
||||
|
||||
console.error("Failed to fetch company - HTTP Error:", {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
response: errorText,
|
||||
url: response.url,
|
||||
});
|
||||
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
description: errorMessage,
|
||||
@ -247,17 +247,18 @@ function useCompanyData(
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error occurred";
|
||||
|
||||
console.error("Failed to fetch company - Network/Parse Error:", {
|
||||
message: errorMessage,
|
||||
error: error,
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
url: `/api/platform/companies/${params.id}`,
|
||||
});
|
||||
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
title: "Error",
|
||||
description: `Failed to load company data: ${errorMessage}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
@ -371,12 +372,13 @@ function renderCompanyInfoCard(
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
const parsedValue = Number.parseInt(value, 10);
|
||||
|
||||
|
||||
// Validate input: must be a positive number
|
||||
const maxUsers = !Number.isNaN(parsedValue) && parsedValue > 0
|
||||
? parsedValue
|
||||
: 1; // Default to 1 for invalid/negative values
|
||||
|
||||
const maxUsers =
|
||||
!Number.isNaN(parsedValue) && parsedValue > 0
|
||||
? parsedValue
|
||||
: 1; // Default to 1 for invalid/negative values
|
||||
|
||||
state.setEditData((prev) => ({
|
||||
...prev,
|
||||
maxUsers,
|
||||
|
||||
@ -156,7 +156,9 @@ function usePlatformDashboardState() {
|
||||
adminPassword: "",
|
||||
maxUsers: 10,
|
||||
});
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>({});
|
||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>(
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
dashboardData,
|
||||
@ -210,22 +212,23 @@ function useFormIds() {
|
||||
*/
|
||||
function validateEmail(email: string): string | undefined {
|
||||
if (!email) return undefined;
|
||||
|
||||
const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
|
||||
|
||||
const emailRegex =
|
||||
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
||||
|
||||
if (!emailRegex.test(email)) {
|
||||
return "Please enter a valid email address";
|
||||
}
|
||||
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function validateUrl(url: string): string | undefined {
|
||||
if (!url) return undefined;
|
||||
|
||||
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
if (!['http:', 'https:'].includes(urlObj.protocol)) {
|
||||
if (!["http:", "https:"].includes(urlObj.protocol)) {
|
||||
return "URL must use HTTP or HTTPS protocol";
|
||||
}
|
||||
return undefined;
|
||||
@ -271,7 +274,7 @@ function renderCompanyFormFields(
|
||||
...prev,
|
||||
csvUrl: value,
|
||||
}));
|
||||
|
||||
|
||||
// Validate URL on change
|
||||
const error = validateUrl(value);
|
||||
setValidationErrors((prev) => ({
|
||||
@ -341,7 +344,7 @@ function renderCompanyFormFields(
|
||||
...prev,
|
||||
adminEmail: value,
|
||||
}));
|
||||
|
||||
|
||||
// Validate email on change
|
||||
const error = validateEmail(value);
|
||||
setValidationErrors((prev) => ({
|
||||
@ -683,13 +686,12 @@ export default function PlatformDashboard() {
|
||||
newCompanyData.adminEmail &&
|
||||
newCompanyData.adminName
|
||||
);
|
||||
|
||||
|
||||
// Check for validation errors
|
||||
const hasValidationErrors = !!(
|
||||
validationErrors.csvUrl ||
|
||||
validationErrors.adminEmail
|
||||
validationErrors.csvUrl || validationErrors.adminEmail
|
||||
);
|
||||
|
||||
|
||||
return hasRequiredFields && !hasValidationErrors;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user