Bump version to 1.7.8

This commit is contained in:
rxf
2026-05-31 15:34:34 +00:00
parent d94de334d7
commit 27f2d438e2
12 changed files with 514 additions and 11 deletions
+21 -8
View File
@@ -22,6 +22,8 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
const [refreshKey, setRefreshKey] = useState(0);
const [editEntry, setEditEntry] = useState<LogbuchEintrag | null>(null);
const grafikSrc = '/api/statistik/grafik';
const version = packageJson.version;
const buildDate =
process.env.NEXT_PUBLIC_BUILD_DATE ||
@@ -98,7 +100,10 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
.map((tab) => (
<button
key={tab}
onClick={() => { setActiveTab(tab); if (tab === 'eingabe') setEditEntry(null); }}
onClick={() => {
setActiveTab(tab);
if (tab === 'eingabe') setEditEntry(null);
}}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeTab === tab
? 'border-[#85B7D7] text-gray-900'
@@ -173,14 +178,22 @@ export default function MainClient({ kuerzel, beoId, beoName, role }: Props) {
{/* Statistik-Tab */}
{activeTab === 'statistik' && (
<div className="border-2 border-gray-400 rounded-xl bg-white p-3 print:border-0 print:rounded-none print:p-0">
<div className="flex justify-between items-center mb-2 print:hidden">
<div className="flex justify-between items-center mb-2 print:hidden gap-2">
<span className="text-sm font-semibold text-gray-600">Statistik (alle Kuppeln)</span>
<button
onClick={() => window.print()}
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg"
>
🖨 Drucken
</button>
<div className="flex items-center gap-2">
<button
onClick={() => window.print()}
className="text-sm px-3 py-1.5 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg"
>
🖨 Drucken
</button>
<button
onClick={() => window.open(grafikSrc, '_blank')}
className="text-sm px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg"
>
📊 Grafik
</button>
</div>
</div>
<Statistik />
</div>
@@ -0,0 +1,18 @@
import { grafikProxy } from '../proxy';
function getSlugFromRequest(req: Request) {
const url = new URL(req.url);
const prefix = '/api/statistik/grafik';
const path = url.pathname;
if (!path.startsWith(prefix)) return undefined;
const suffix = path.slice(prefix.length);
return suffix.split('/').filter(Boolean);
}
export async function GET(req: Request) {
return grafikProxy(req, getSlugFromRequest(req));
}
export async function POST(req: Request) {
return grafikProxy(req, getSlugFromRequest(req));
}
+110
View File
@@ -0,0 +1,110 @@
import { NextResponse } from 'next/server';
const blockedHeaders = new Set([
'x-frame-options',
'content-security-policy',
'content-security-policy-report-only',
'frame-options',
'content-encoding',
'content-length',
'transfer-encoding',
]);
function buildProxyTarget(req: Request, slug?: string[]) {
const upstreamUrl = process.env.STATISTIK_GRAFIK_URL;
if (!upstreamUrl) {
throw new Error('STATISTIK_GRAFIK_URL is not configured');
}
const target = new URL(upstreamUrl);
const pathSuffix = slug?.filter(Boolean).join('/') || '';
if (pathSuffix) {
target.pathname = `${target.pathname.replace(/\/$/, '')}/${pathSuffix}`;
}
target.search = new URL(req.url).search;
return target.toString();
}
function buildAuthHeaders() {
const headers: Record<string, string> = {};
const user = process.env.STATISTIK_GRAFIK_USER;
const pass = process.env.STATISTIK_GRAFIK_PASS;
if (user && pass) {
const token = Buffer.from(`${user}:${pass}`).toString('base64');
headers.Authorization = `Basic ${token}`;
}
return headers;
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function rewriteHtml(body: string) {
const proxyBase = '/api/statistik/grafik';
const upstreamOrigin = new URL(process.env.STATISTIK_GRAFIK_URL!).origin;
const escapedProxyBase = escapeRegExp(proxyBase);
const proxyBasePattern = escapedProxyBase.replace(/\//g, '\\/');
return body
.replace(/<meta[^>]*?(?:http-equiv|httpEquiv)\s*=\s*['"]?[^'">\s]+['"]?[^>]*>/ig, (m) => {
if (/x-frame-options/i.test(m)) return '';
if (/content-security-policy/i.test(m)) return '';
if (/frame-ancestors/i.test(m)) return '';
return m;
})
.replace(/<head([^>]*)>/i, `<head$1><base href="${proxyBase}/">`)
.replace(new RegExp(`(href|src|action)=(["'])(?!${proxyBasePattern}\\/)\/`, 'gi'), `$1=$2${proxyBase}/`)
.replace(new RegExp(`url\\((['"]?)(?!${proxyBasePattern}\\/)\/`, 'gi'), `url($1${proxyBase}/`)
.replace(new RegExp(escapeRegExp(upstreamOrigin), 'g'), proxyBase);
}
export async function grafikProxy(req: Request, slug?: string[]) {
let targetUrl: string;
try {
targetUrl = buildProxyTarget(req, slug);
} catch (error) {
return new NextResponse((error as Error).message, { status: 500 });
}
try {
const requestHeaders: Record<string, string> = {};
req.headers.forEach((value, key) => {
const lower = key.toLowerCase();
if (lower === 'host' || lower === 'content-length') return;
requestHeaders[key] = value;
});
Object.assign(requestHeaders, buildAuthHeaders());
const body = ['GET', 'HEAD'].includes(req.method) ? undefined : await req.arrayBuffer();
const upstream = await fetch(targetUrl, {
method: req.method,
headers: requestHeaders,
body,
cache: 'no-store',
});
const bodyBuf = await upstream.arrayBuffer();
const outHeaders: Record<string, string> = {};
upstream.headers.forEach((value, key) => {
if (!blockedHeaders.has(key.toLowerCase())) {
outHeaders[key] = value;
}
});
const contentType = upstream.headers.get('content-type') || outHeaders['content-type'] || '';
if (contentType.toLowerCase().includes('text/html')) {
const text = Buffer.from(bodyBuf).toString('utf8');
const cleaned = rewriteHtml(text);
outHeaders['content-type'] = 'text/html; charset=utf-8';
outHeaders['x-frame-options'] = 'ALLOWALL';
return new NextResponse(cleaned, { status: upstream.status, headers: outHeaders });
}
outHeaders['x-frame-options'] = 'ALLOWALL';
return new NextResponse(Buffer.from(bodyBuf), { status: upstream.status, headers: outHeaders });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return new NextResponse(`grafikProxy error: ${message}`, { status: 500 });
}
}
+18
View File
@@ -0,0 +1,18 @@
import { grafikProxy } from './proxy';
function getSlugFromRequest(req: Request) {
const url = new URL(req.url);
const prefix = '/api/statistik/grafik';
const path = url.pathname;
if (!path.startsWith(prefix)) return undefined;
const suffix = path.slice(prefix.length);
return suffix.split('/').filter(Boolean);
}
export async function GET(req: Request) {
return grafikProxy(req, getSlugFromRequest(req));
}
export async function POST(req: Request) {
return grafikProxy(req, getSlugFromRequest(req));
}