* Revise portfolio: warm brown theme, elegant typography, optimized analytics tracking (#55) * Initial plan * Update color theme to warm brown and off-white, add elegant fonts, fix analytics tracking Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix 404 page integration with warm theme, update admin console colors, fix font loading Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Address code review feedback: fix navigation, add utils, improve tracking Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Fix accessibility and memory leak issues from code review Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * chore: Code cleanup, add Sentry.io monitoring, and documentation (#56) * Initial plan * Remove unused code and clean up console statements Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Remove unused components and fix type issues Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Wrap console.warn in development check Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Integrate Sentry.io monitoring and add text editing documentation Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * Initial plan * feat: Add Sentry configuration files and example pages - Add sentry.server.config.ts and sentry.edge.config.ts - Update instrumentation.ts with onRequestError export - Update instrumentation-client.ts with onRouterTransitionStart export - Update global-error.tsx to capture exceptions with Sentry - Create Sentry example page at app/sentry-example-page/page.tsx - Create Sentry example API route at app/api/sentry-example-api/route.ts Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * feat: Update middleware to allow Sentry example page and fix deprecated API - Update middleware to exclude /sentry-example-page from locale routing - Remove deprecated startTransaction API from Sentry example page - Use consistent DSN configuration with fallback values Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> * refactor: Improve Sentry configuration with environment-based sampling - Add comments explaining DSN fallback values - Use environment-based tracesSampleRate (10% in production, 100% in dev) - Address code review feedback for production-safe configuration Co-authored-by: denshooter <44590296+denshooter@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
144 lines
4.9 KiB
TypeScript
144 lines
4.9 KiB
TypeScript
// app/api/n8n/status/route.ts
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
// Cache für 30 Sekunden, damit wir n8n nicht zuspammen
|
|
export const revalidate = 30;
|
|
|
|
export async function GET(request: NextRequest) {
|
|
// Rate limiting for n8n status endpoint
|
|
const ip =
|
|
request.headers.get("x-forwarded-for") ||
|
|
request.headers.get("x-real-ip") ||
|
|
"unknown";
|
|
const ua = request.headers.get("user-agent") || "unknown";
|
|
const { checkRateLimit } = await import('@/lib/auth');
|
|
|
|
// In dev, many requests can share ip=unknown; use UA to avoid a shared bucket.
|
|
const rateKey =
|
|
process.env.NODE_ENV === "development" && ip === "unknown"
|
|
? `ua:${ua.slice(0, 120)}`
|
|
: ip;
|
|
const maxPerMinute = process.env.NODE_ENV === "development" ? 300 : 30;
|
|
|
|
if (!checkRateLimit(rateKey, maxPerMinute, 60000)) { // requests per minute
|
|
return NextResponse.json(
|
|
{ error: 'Rate limit exceeded. Please try again later.' },
|
|
{ status: 429 }
|
|
);
|
|
}
|
|
try {
|
|
// Check if n8n webhook URL is configured
|
|
const n8nWebhookUrl = process.env.N8N_WEBHOOK_URL;
|
|
|
|
if (!n8nWebhookUrl) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.warn("N8N_WEBHOOK_URL not configured for status endpoint");
|
|
}
|
|
// Return fallback if n8n is not configured
|
|
return NextResponse.json({
|
|
status: { text: "offline", color: "gray" },
|
|
music: null,
|
|
gaming: null,
|
|
coding: null,
|
|
});
|
|
}
|
|
|
|
// Rufe den n8n Webhook auf
|
|
// Add timestamp to query to bypass Cloudflare cache
|
|
const statusUrl = `${n8nWebhookUrl}/webhook/denshooter-71242/status?t=${Date.now()}`;
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log(`Fetching status from: ${statusUrl}`);
|
|
}
|
|
|
|
// Add timeout to prevent hanging requests
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
|
|
try {
|
|
const res = await fetch(statusUrl, {
|
|
method: "GET",
|
|
headers: {
|
|
// n8n sometimes responds with empty body; we'll parse defensively below.
|
|
Accept: "application/json",
|
|
...(process.env.N8N_SECRET_TOKEN && {
|
|
Authorization: `Bearer ${process.env.N8N_SECRET_TOKEN}`,
|
|
}),
|
|
},
|
|
next: { revalidate: 30 },
|
|
signal: controller.signal,
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text().catch(() => 'Unknown error');
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error(`n8n status webhook failed: ${res.status}`, errorText);
|
|
}
|
|
throw new Error(`n8n error: ${res.status} - ${errorText}`);
|
|
}
|
|
|
|
const raw = await res.text().catch(() => "");
|
|
if (!raw || !raw.trim()) {
|
|
throw new Error("Empty response body received from n8n");
|
|
}
|
|
|
|
let data: unknown;
|
|
try {
|
|
data = JSON.parse(raw);
|
|
} catch (_parseError) {
|
|
// Sometimes upstream sends HTML or a partial response; include a snippet for debugging.
|
|
const snippet = raw.slice(0, 240);
|
|
throw new Error(
|
|
`Invalid JSON from n8n (${res.status}): ${snippet}${raw.length > 240 ? "…" : ""}`,
|
|
);
|
|
}
|
|
|
|
// n8n gibt oft ein Array zurück: [{...}]. Wir wollen nur das Objekt.
|
|
const statusData = Array.isArray(data) ? data[0] : data;
|
|
|
|
// Safety check: if statusData is still undefined/null (e.g. empty array), use fallback
|
|
if (!statusData) {
|
|
throw new Error("Empty data received from n8n");
|
|
}
|
|
|
|
// Ensure coding object has proper structure
|
|
if (statusData.coding && typeof statusData.coding === "object") {
|
|
// Already properly formatted from n8n
|
|
} else if (statusData.coding === null || statusData.coding === undefined) {
|
|
// No coding data - keep as null
|
|
statusData.coding = null;
|
|
}
|
|
|
|
return NextResponse.json(statusData);
|
|
} catch (fetchError: unknown) {
|
|
clearTimeout(timeoutId);
|
|
|
|
if (process.env.NODE_ENV === 'development') {
|
|
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
console.error("n8n status webhook request timed out");
|
|
} else {
|
|
console.error("n8n status webhook fetch error:", fetchError);
|
|
}
|
|
}
|
|
throw fetchError;
|
|
}
|
|
} catch (error: unknown) {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.error("Error fetching n8n status:", error);
|
|
console.error("Error details:", {
|
|
message: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
n8nUrl: process.env.N8N_WEBHOOK_URL ? 'configured' : 'missing',
|
|
});
|
|
}
|
|
// Leeres Fallback-Objekt, damit die Seite nicht abstürzt
|
|
return NextResponse.json({
|
|
status: { text: "offline", color: "gray" },
|
|
music: null,
|
|
gaming: null,
|
|
coding: null,
|
|
});
|
|
}
|
|
}
|