"feat: Added debug server and related components for improved development experience"

This commit is contained in:
2025-05-23 07:24:51 +02:00
parent d457a8d86b
commit 9f6219832c
189 changed files with 35730 additions and 133 deletions

View File

@@ -0,0 +1,54 @@
import type { printJobs } from "@/server/db/schema";
import type { InferResultType } from "@/utils/drizzle";
import type { InferSelectModel } from "drizzle-orm";
export interface PrinterErrorRate {
printerId: string;
name: string;
errorRate: number; // Error rate as a percentage (0-100)
}
/**
* Calculates the error rate for print jobs aggregated by printer as a percentage.
*
* @param pJobs - Array of print job objects.
* @returns An array of PrinterErrorRate objects, each containing the printer ID and its error rate.
*/
export function calculatePrinterErrorRate(
pJobs: InferResultType<"printJobs", { printer: true }>[],
): PrinterErrorRate[] {
if (pJobs.length === 0) {
return []; // No jobs, no data.
}
const printers = pJobs.map((job) => job.printer);
// Group jobs by printer ID
const jobsByPrinter: Record<string, InferSelectModel<typeof printJobs>[]> = pJobs.reduce(
(acc, job) => {
if (!acc[job.printerId]) {
acc[job.printerId] = [];
}
acc[job.printerId].push(job);
return acc;
},
{} as Record<string, InferSelectModel<typeof printJobs>[]>,
);
// Calculate the error rate for each printer
const printerErrorRates: PrinterErrorRate[] = Object.entries(jobsByPrinter).map(([printerId, jobs]) => {
const totalJobs = jobs.length;
const abortedJobsCount = jobs.filter((job) => job.aborted).length;
const errorRate = (abortedJobsCount / totalJobs) * 100;
const printer = printers.find((printer) => printer.id === printerId);
const printerName = printer ? printer.name : "Unbekannter Drucker";
return {
printerId,
name: printerName,
errorRate: Number.parseFloat(errorRate.toFixed(2)), // Rounded to two decimal places
};
});
return printerErrorRates;
}

View File

@@ -0,0 +1,39 @@
import type { InferResultType } from "@/utils/drizzle";
export interface AbortReasonCount {
abortReason: string;
count: number;
}
/**
* Calculates the count of each unique abort reason for print jobs.
*
* @param pJobs - Array of print job objects.
* @returns An array of AbortReasonCount objects, each containing the abort reason and its count.
*/
export function calculateAbortReasonsCount(pJobs: InferResultType<"printJobs">[]): AbortReasonCount[] {
if (pJobs.length === 0) {
return []; // No jobs, no data.
}
// Filter aborted jobs and count each abort reason
const abortReasonsCount = pJobs
.filter((job) => job.aborted && job.abortReason) // Consider only aborted jobs with a reason
.reduce(
(acc, job) => {
const reason = job.abortReason || "Unbekannter Grund";
if (!acc[reason]) {
acc[reason] = 0;
}
acc[reason]++;
return acc;
},
{} as Record<string, number>,
);
// Convert the result to an array of AbortReasonCount objects
return Object.entries(abortReasonsCount).map(([abortReason, count]) => ({
abortReason,
count,
}));
}

View File

@@ -0,0 +1,97 @@
import type { InferResultType } from "@/utils/drizzle";
type UsagePerDay = {
day: number; // 0 (Sunday) to 6 (Saturday)
usageMinutes: number;
};
function aggregateUsageByDay(jobs: InferResultType<"printJobs">[]): {
usageData: UsagePerDay[];
earliestDate: Date;
latestDate: Date;
} {
const usagePerDayMap = new Map<number, number>();
let earliestDate: Date | null = null;
let latestDate: Date | null = null;
for (const job of jobs) {
let remainingDuration = job.durationInMinutes;
const currentStart = new Date(job.startAt);
// Update earliest and latest dates
if (!earliestDate || currentStart < earliestDate) {
earliestDate = new Date(currentStart);
}
const jobEnd = new Date(currentStart);
jobEnd.setMinutes(jobEnd.getMinutes() + job.durationInMinutes);
if (!latestDate || jobEnd > latestDate) {
latestDate = new Date(jobEnd);
}
while (remainingDuration > 0) {
const day = currentStart.getDay();
// Calculate minutes remaining in the current day
const minutesRemainingInDay = (24 - currentStart.getHours()) * 60 - currentStart.getMinutes();
const minutesToAdd = Math.min(remainingDuration, minutesRemainingInDay);
// Update the usage for the current day
const usageMinutes = usagePerDayMap.get(day) || 0;
usagePerDayMap.set(day, usageMinutes + minutesToAdd);
// Update remaining duration and move to the next day
remainingDuration -= minutesToAdd;
currentStart.setDate(currentStart.getDate() + 1);
currentStart.setHours(0, 0, 0, 0); // Start at the beginning of the next day
}
}
const usageData: UsagePerDay[] = Array.from({ length: 7 }, (_, day) => ({
day,
usageMinutes: usagePerDayMap.get(day) || 0,
}));
if (earliestDate === null) {
earliestDate = new Date();
}
if (latestDate === null) {
latestDate = new Date();
}
return { usageData, earliestDate: earliestDate, latestDate: latestDate };
}
function countWeekdays(startDate: Date, endDate: Date): number[] {
const countPerDay = Array(7).fill(0);
const currentDate = new Date(startDate);
currentDate.setHours(0, 0, 0, 0); // Ensure starting at midnight
endDate.setHours(0, 0, 0, 0); // Ensure ending at midnight
while (currentDate <= endDate) {
const day = currentDate.getDay();
countPerDay[day]++;
currentDate.setDate(currentDate.getDate() + 1);
}
return countPerDay;
}
export function forecastPrinterUsage(jobs: InferResultType<"printJobs">[]): number[] {
const { usageData, earliestDate, latestDate } = aggregateUsageByDay(jobs);
// Count the number of times each weekday occurs in the data period
const weekdaysCount = countWeekdays(earliestDate, latestDate);
const forecasts: number[] = [];
for (const data of usageData) {
const dayCount = weekdaysCount[data.day];
let usagePrediction = data.usageMinutes / dayCount;
if (Number.isNaN(usagePrediction)) {
usagePrediction = 0;
}
forecasts.push(Math.round(usagePrediction));
}
return forecasts;
}

View File

@@ -0,0 +1,32 @@
import type { InferResultType } from "@/utils/drizzle";
export function calculatePrinterUtilization(jobs: InferResultType<"printJobs", { printer: true }>[]) {
const printers = jobs.reduce<Record<string, string>>((acc, job) => {
acc[job.printerId] = job.printer.name;
return acc;
}, {});
const usedTimePerPrinter: Record<string, number> = jobs.reduce(
(acc, job) => {
acc[job.printer.id] = (acc[job.printer.id] || 0) + job.durationInMinutes;
return acc;
},
{} as Record<string, number>,
);
const totalTimeInMinutes = 60 * 35 * 3; // 60 Minutes * 35h * 3 Weeks
// 35h Woche, 3 mal in der Woche in TBA
const printerUtilizationPercentage = Object.keys(usedTimePerPrinter).map((printerId) => {
const usedTime = usedTimePerPrinter[printerId];
return {
printerId,
name: printers[printerId],
utilizationPercentage: usedTime / totalTimeInMinutes,
};
});
return printerUtilizationPercentage;
}

View File

@@ -0,0 +1,52 @@
import type { printJobs } from "@/server/db/schema";
import { endOfDay, endOfMonth, endOfWeek, startOfDay, startOfMonth, startOfWeek } from "date-fns";
import type { InferSelectModel } from "drizzle-orm";
interface PrintVolumes {
today: number;
thisWeek: number;
thisMonth: number;
}
/**
* Calculates the number of print jobs for today, this week, and this month.
*
* @param printJobs - Array of print job objects.
* @returns An object with counts of print jobs for today, this week, and this month.
*/
export function calculatePrintVolumes(pJobs: InferSelectModel<typeof printJobs>[]): PrintVolumes {
const now = new Date();
// Define time ranges with week starting on Monday
const timeRanges = {
today: { start: startOfDay(now), end: endOfDay(now) },
thisWeek: {
start: startOfWeek(now, { weekStartsOn: 1 }),
end: endOfWeek(now, { weekStartsOn: 1 }),
},
thisMonth: { start: startOfMonth(now), end: endOfMonth(now) },
};
// Initialize counts
const volumes: PrintVolumes = {
today: 0,
thisWeek: 0,
thisMonth: 0,
};
// Iterate over print jobs and count based on time ranges
for (const job of pJobs) {
const jobStart = new Date(job.startAt);
if (jobStart >= timeRanges.today.start && jobStart <= timeRanges.today.end) {
volumes.today += 1;
}
if (jobStart >= timeRanges.thisWeek.start && jobStart <= timeRanges.thisWeek.end) {
volumes.thisWeek += 1;
}
if (jobStart >= timeRanges.thisMonth.start && jobStart <= timeRanges.thisMonth.end) {
volumes.thisMonth += 1;
}
}
return volumes;
}