remove dev comments

This commit is contained in:
overcuriousity
2025-07-26 15:14:02 +02:00
parent 86d2370976
commit f24531d86d
34 changed files with 71 additions and 693 deletions

View File

@@ -4,7 +4,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<BaseLayout title="Über das Projekt" description="ForensicPathways - Ein Projekt für die Seminargruppe CC24-w1">
<section style="padding: 2rem 0; max-width: 900px; margin: 0 auto;">
<!-- Hero Section -->
<div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 2.5rem; color: var(--color-primary);">ForensicPathways</h1>
<p style="font-size: 1.25rem; color: var(--color-text-secondary); margin-bottom: 0.5rem;">
@@ -15,7 +14,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</p>
</div>
<!-- Project Goal -->
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
@@ -41,7 +39,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</div>
</div>
<!-- Methodology -->
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-accent);">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
@@ -77,7 +74,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</p>
</div>
<!-- Open Source Focus -->
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-warning);">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-warning)" stroke-width="2">
@@ -87,7 +83,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<h2 style="margin: 0; color: var(--color-warning);">Weshalb der Fokus auf Open Source?</h2>
</div>
<!-- Hypothesis Box -->
<div style="background: linear-gradient(135deg, var(--color-warning) 0%, var(--color-accent) 100%); color: white; padding: 1.5rem; border-radius: 0.75rem; margin-bottom: 1.5rem;">
<h3 style="margin: 0 0 1rem 0; font-size: 1.125rem;">💡 Zentrale Hypothese</h3>
<p style="margin: 0; line-height: 1.6; font-style: italic;">
@@ -131,7 +126,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</div>
</div>
<!-- Support Section -->
<div id= "support" class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
@@ -144,7 +138,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
oder sonstige Probleme auftreten: <strong>Schreibt mir einfach auf Signal</strong> oder an <a href="mailto:mstoeck3@hs-mittweida.de">mstoeck3@hs-mittweida.de</a>.
</p>
<!-- Special Note Box -->
<div style="background-color: var(--color-warning); color: white; padding: 1.25rem; border-radius: 0.5rem; margin-top: 1.5rem;">
<h4 style="margin: 0 0 0.75rem 0; display: flex; align-items: center; gap: 0.5rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -178,8 +171,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</div>
</div>
<!-- Contributing Section -->
<!-- Contribution Section -->
<div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-accent);">
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
@@ -192,7 +183,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</div>
<div style="display: grid; gap: 1.25rem;">
<!-- Suggestions -->
<div style="background-color: var(--color-bg-secondary); padding: 1.25rem; border-radius: 0.5rem;">
<h4 style="margin: 0 0 0.5rem 0; color: var(--color-accent);">🔍 Vorschläge</h4>
<p style="margin: 0;">
@@ -201,7 +191,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</p>
</div>
<!-- Corrections & Updates -->
<div style="background-color: var(--color-bg-secondary); padding: 1.25rem; border-radius: 0.5rem;">
<h4 style="margin: 0 0 0.5rem 0; color: var(--color-accent);">🔧 Korrekturen &amp; Updates</h4>
<p style="margin: 0;">
@@ -210,7 +199,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</p>
</div>
<!-- Code Contributions -->
<div style="background-color: var(--color-bg-secondary); padding: 1.25rem; border-radius: 0.5rem;">
<h4 style="margin: 0 0 0.5rem 0; color: var(--color-accent);">💻 CodeBeiträge</h4>
<p style="margin-bottom: 0.75rem;">
@@ -235,7 +223,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</div>
</div>
<!-- Footer Note -->
<div style="text-align: center; padding: 2rem; background-color: var(--color-bg-secondary); border-radius: 0.75rem; border: 1px solid var(--color-border);">
<p style="margin: 0; color: var(--color-text-secondary); font-size: 0.875rem;">
<strong>Inoffizielles Studienprojekt</strong> | Mario Stöckl |

View File

@@ -17,12 +17,10 @@ function getEnv(key: string): string {
const AI_MODEL = getEnv('AI_MODEL');
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 10; // 10 requests per minute per user
const RATE_LIMIT_WINDOW = 60 * 1000;
const RATE_LIMIT_MAX = 10;
// Input validation and sanitization
function sanitizeInput(input: string): string {
// Remove any content that looks like system instructions
let sanitized = input
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]') // Remove code blocks
.replace(/\<\/?[^>]+(>|$)/g, '') // Remove HTML tags
@@ -30,22 +28,18 @@ function sanitizeInput(input: string): string {
.replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
.trim();
// Limit length and remove excessive whitespace
sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' ');
return sanitized;
}
// Strip markdown code blocks from AI response
function stripMarkdownJson(content: string): string {
// Remove ```json and ``` wrappers
return content
.replace(/^```json\s*/i, '')
.replace(/\s*```\s*$/, '')
.trim();
}
// Rate limiting check
function checkRateLimit(userId: string): boolean {
const now = Date.now();
const userLimit = rateLimitStore.get(userId);
@@ -74,7 +68,6 @@ function cleanupExpiredRateLimits() {
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
// Load tools database
async function loadToolsDatabase() {
try {
return await getCompressedToolsDataForAI();
@@ -84,7 +77,6 @@ async function loadToolsDatabase() {
}
}
// Create system prompt for workflow mode
function createWorkflowSystemPrompt(toolsData: any): string {
const toolsList = toolsData.tools.map((tool: any) => ({
name: tool.name,
@@ -99,7 +91,6 @@ function createWorkflowSystemPrompt(toolsData: any): string {
related_concepts: tool.related_concepts || []
}));
// Include concepts for background knowledge
const conceptsList = toolsData.concepts.map((concept: any) => ({
name: concept.name,
description: concept.description,
@@ -109,13 +100,10 @@ function createWorkflowSystemPrompt(toolsData: any): string {
tags: concept.tags
}));
// Get regular phases
const regularPhases = toolsData.phases || [];
// Get domain-agnostic software phases
const domainAgnosticSoftware = toolsData['domain-agnostic-software'] || [];
// Combine all phases for the description
const allPhaseItems = [
...regularPhases,
...domainAgnosticSoftware
@@ -125,22 +113,18 @@ function createWorkflowSystemPrompt(toolsData: any): string {
`- ${phase.id}: ${phase.name}`
).join('\n');
// Dynamically build domains list from configuration
const domainsDescription = toolsData.domains.map((domain: any) =>
`- ${domain.id}: ${domain.name}`
).join('\n');
// Build dynamic phase descriptions for tool selection
const phaseDescriptions = regularPhases.map((phase: any) =>
`- ${phase.name}: ${phase.description || 'Tools/Methods for this phase'}`
).join('\n');
// Add domain-agnostic software descriptions
const domainAgnosticDescriptions = domainAgnosticSoftware.map((section: any) =>
`- ${section.name}: ${section.description || 'Cross-cutting software and platforms'}`
).join('\n');
// Create valid phase values for JSON schema
const validPhases = [
...regularPhases.map((p: any) => p.id),
...domainAgnosticSoftware.map((s: any) => s.id)
@@ -201,7 +185,6 @@ ANTWORT-FORMAT (strict JSON):
Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des JSON.`;
}
// Create system prompt for tool-specific mode
function createToolSystemPrompt(toolsData: any): string {
const toolsList = toolsData.tools.map((tool: any) => ({
name: tool.name,
@@ -217,7 +200,6 @@ function createToolSystemPrompt(toolsData: any): string {
related_concepts: tool.related_concepts || []
}));
// Include concepts for background knowledge
const conceptsList = toolsData.concepts.map((concept: any) => ({
name: concept.name,
description: concept.description,
@@ -277,7 +259,6 @@ Antworte NUR mit validen JSON. Keine zusätzlichen Erklärungen außerhalb des J
export const POST: APIRoute = async ({ request }) => {
try {
// Authentication check
const authResult = await withAPIAuth(request, 'ai');
if (!authResult.authenticated) {
return createAuthErrorResponse();
@@ -285,16 +266,13 @@ export const POST: APIRoute = async ({ request }) => {
const userId = authResult.userId;
// Rate limiting
if (!checkRateLimit(userId)) {
return apiError.rateLimit('Rate limit exceeded');
}
// Parse request body
const body = await request.json();
const { query, mode = 'workflow', taskId: clientTaskId } = body;
// Validation
if (!query || typeof query !== 'string') {
return apiError.badRequest('Query required');
}
@@ -303,24 +281,19 @@ export const POST: APIRoute = async ({ request }) => {
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
}
// Sanitize input
const sanitizedQuery = sanitizeInput(query);
if (sanitizedQuery.includes('[FILTERED]')) {
return apiError.badRequest('Invalid input detected');
}
// Load tools database
const toolsData = await loadToolsDatabase();
// Create appropriate system prompt based on mode
const systemPrompt = mode === 'workflow'
? createWorkflowSystemPrompt(toolsData)
: createToolSystemPrompt(toolsData);
// Generate task ID for queue tracking (use client-provided ID if available)
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
// Make AI API call through rate-limited queue
const aiResponse = await enqueueApiCall(() =>
fetch(process.env.AI_API_ENDPOINT + '/v1/chat/completions', {
method: 'POST',
@@ -346,7 +319,6 @@ export const POST: APIRoute = async ({ request }) => {
})
, taskId);
// AI response handling
if (!aiResponse.ok) {
console.error('AI API error:', await aiResponse.text());
return apiServerError.unavailable('AI service unavailable');
@@ -359,7 +331,6 @@ export const POST: APIRoute = async ({ request }) => {
return apiServerError.unavailable('No response from AI');
}
// Parse AI JSON response
let recommendation;
try {
const cleanedContent = stripMarkdownJson(aiContent);
@@ -369,7 +340,6 @@ export const POST: APIRoute = async ({ request }) => {
return apiServerError.unavailable('Invalid AI response format');
}
// Validate tool names and concept names against database
const validToolNames = new Set(toolsData.tools.map((t: any) => t.name));
const validConceptNames = new Set(toolsData.concepts.map((c: any) => c.name));
@@ -404,8 +374,8 @@ export const POST: APIRoute = async ({ request }) => {
return true;
}).map((tool: any, index: number) => ({
...tool,
rank: tool.rank || (index + 1), // Ensure rank is set
suitability_score: tool.suitability_score || 'medium', // Default suitability
rank: tool.rank || (index + 1),
suitability_score: tool.suitability_score || 'medium',
pros: Array.isArray(tool.pros) ? tool.pros : [],
cons: Array.isArray(tool.cons) ? tool.cons : []
})) || [],
@@ -419,10 +389,8 @@ export const POST: APIRoute = async ({ request }) => {
};
}
// Log successful query
console.log(`[AI Query] Mode: ${mode}, User: ${userId}, Query length: ${sanitizedQuery.length}, Tools: ${validatedRecommendation.recommended_tools.length}, Concepts: ${validatedRecommendation.background_knowledge?.length || 0}`);
// Success response with task ID
return new Response(JSON.stringify({
success: true,
mode,

View File

@@ -8,17 +8,14 @@ export const GET: APIRoute = async ({ url, redirect }) => {
const state = generateState();
const authUrl = generateAuthUrl(state);
// Debug: log the generated URL
console.log('Generated auth URL:', authUrl);
// Get the intended destination after login (if any)
const returnTo = url.searchParams.get('returnTo') || '/';
logAuthEvent('Login initiated', { returnTo, authUrl });
// Store state and returnTo in a cookie for the callback
const stateData = JSON.stringify({ state, returnTo });
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`; // 10 minutes
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`;
return new Response(null, {
status: 302,

View File

@@ -13,7 +13,6 @@ export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Parse request body
let body;
try {
body = await request.json();
@@ -29,17 +28,14 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.missingRequired(['code', 'state']);
}
// CONSOLIDATED: Single function call replaces 15+ lines of duplicated state verification
const stateVerification = verifyAuthState(request, state);
if (!stateVerification.isValid || !stateVerification.stateData) {
return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
}
// Exchange code for tokens and get user info
const tokens = await exchangeCodeForTokens(code);
const userInfo = await getUserInfo(tokens.access_token);
// CONSOLIDATED: Single function call replaces 10+ lines of session creation
const sessionResult = await createSessionWithCookie(userInfo);
logAuthEvent('Authentication successful', {
@@ -47,11 +43,9 @@ export const POST: APIRoute = async ({ request }) => {
email: sessionResult.userEmail
});
// FIXED: Create response with multiple Set-Cookie headers
const responseHeaders = new Headers();
responseHeaders.set('Content-Type', 'application/json');
// Each cookie needs its own Set-Cookie header
responseHeaders.append('Set-Cookie', sessionResult.sessionCookie);
responseHeaders.append('Set-Cookie', sessionResult.clearStateCookie);

View File

@@ -7,7 +7,6 @@ import { z } from 'zod';
export const prerender = false;
// Simple schema - all fields optional except for having some content
const KnowledgebaseContributionSchema = z.object({
toolName: z.string().optional().nullable().transform(val => val || undefined),
title: z.string().optional().nullable().transform(val => val || undefined),
@@ -40,7 +39,6 @@ interface KnowledgebaseContributionData {
reason?: string;
}
// Rate limiting
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX = 3; // Max 3 submissions per hour per user
@@ -63,8 +61,6 @@ function checkRateLimit(userEmail: string): boolean {
}
function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid: boolean; errors?: string[] } {
// Very minimal validation - just check that SOMETHING was provided
// Use nullish coalescing to avoid “possibly undefined” errors in strict mode
const hasContent = (data.content ?? '').trim().length > 0;
const hasLink = (data.externalLink ?? '').trim().length > 0;
const hasFiles = Array.isArray(data.uploadedFiles) && data.uploadedFiles.length > 0;
@@ -83,7 +79,6 @@ function validateKnowledgebaseData(data: KnowledgebaseContributionData): { valid
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Check authentication
const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
@@ -91,12 +86,10 @@ export const POST: APIRoute = async ({ request }) => {
const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting
if (!checkRateLimit(userEmail)) {
return apiError.rateLimit('Rate limit exceeded. Please wait before submitting again.');
}
// Parse form data
let formData;
try {
formData = await request.formData();
@@ -106,7 +99,6 @@ export const POST: APIRoute = async ({ request }) => {
const rawData = Object.fromEntries(formData);
// Validate and sanitize data
let validatedData;
try {
validatedData = KnowledgebaseContributionSchema.parse(rawData);
@@ -121,13 +113,11 @@ export const POST: APIRoute = async ({ request }) => {
return apiError.badRequest('Invalid request data');
}
// Basic content validation
const contentValidation = validateKnowledgebaseData(validatedData);
if (!contentValidation.valid) {
return apiError.validation('Content validation failed', contentValidation.errors);
}
// Submit as issue via Git
try {
const gitManager = new GitContributionManager();
const result = await gitManager.submitKnowledgebaseContribution({

View File

@@ -7,7 +7,6 @@ import { z } from 'zod';
export const prerender = false;
// Enhanced tool schema for contributions (stricter validation)
const ContributionToolSchema = z.object({
name: z.string().min(1, 'Tool name is required').max(100, 'Tool name too long'),
icon: z.string().optional().nullable(),
@@ -42,7 +41,6 @@ const ContributionRequestSchema = z.object({
}).optional()
});
// Rate limiting
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX = 15; // 15 contributions per hour per user
@@ -64,7 +62,6 @@ function checkRateLimit(userId: string): boolean {
return true;
}
// Input sanitization
function sanitizeInput(obj: any): any {
if (typeof obj === 'string') {
return obj.trim().slice(0, 1000);
@@ -82,15 +79,12 @@ function sanitizeInput(obj: any): any {
return obj;
}
// Tool validation function
async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
try {
// Load existing data for validation
const existingData = { tools: [] }; // Replace with actual data loading
const existingData = { tools: [] };
// Check for duplicate names (on add)
if (action === 'add') {
const existingNames = new Set(existingData.tools.map((t: any) => t.name.toLowerCase()));
if (existingNames.has(tool.name.toLowerCase())) {
@@ -98,7 +92,6 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
}
}
// Type-specific validation
if (tool.type === 'method') {
if (tool.platforms && tool.platforms.length > 0) {
errors.push('Methods should not have platform information');
@@ -126,7 +119,6 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Authentication check
const authResult = await withAPIAuth(request, 'contributions');
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
@@ -135,12 +127,10 @@ export const POST: APIRoute = async ({ request }) => {
const userId = authResult.session?.userId || 'anonymous';
const userEmail = authResult.session?.email || 'anon@anon.anon';
// Rate limiting
if (!checkRateLimit(userId)) {
return apiError.rateLimit('Rate limit exceeded. Please wait before submitting another contribution.');
}
// Parse and sanitize request body
let body;
try {
const rawBody = await request.text();
@@ -152,10 +142,8 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.invalidJSON();
}
// Sanitize input
const sanitizedBody = sanitizeInput(body);
// Validate request structure
let validatedData;
try {
validatedData = ContributionRequestSchema.parse(sanitizedBody);
@@ -170,13 +158,11 @@ export const POST: APIRoute = async ({ request }) => {
return apiError.badRequest('Invalid request data');
}
// Additional tool-specific validation
const toolValidation = await validateToolData(validatedData.tool, validatedData.action);
if (!toolValidation.valid) {
return apiError.validation('Tool validation failed', toolValidation.errors);
}
// Prepare contribution data
const contributionData: ContributionData = {
type: validatedData.action,
tool: validatedData.tool,
@@ -186,9 +172,7 @@ export const POST: APIRoute = async ({ request }) => {
}
};
// CRITICAL FIX: Enhanced error handling for Git operations
try {
// Submit contribution via Git (now creates issue instead of PR)
const gitManager = new GitContributionManager();
const result = await gitManager.submitContribution(contributionData);
@@ -207,10 +191,8 @@ export const POST: APIRoute = async ({ request }) => {
return apiServerError.internal(`Contribution failed: ${result.message}`);
}
} catch (gitError) {
// CRITICAL: Handle Git operation errors properly
console.error(`[GIT ERROR] ${validatedData.action} "${validatedData.tool.name}" by ${userEmail}:`, gitError);
// Return proper error response
const errorMessage = gitError instanceof Error ? gitError.message : 'Git operation failed';
return apiServerError.internal(`Git operation failed: ${errorMessage}`);
}

View File

@@ -18,15 +18,11 @@ interface UploadResult {
storage?: 'nextcloud' | 'local';
}
// Configuration
const UPLOAD_CONFIG = {
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedTypes: new Set([
// Images
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
// Videos
'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
// Documents
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@@ -40,10 +36,9 @@ const UPLOAD_CONFIG = {
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
};
// Rate limiting for uploads
const uploadRateLimit = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour
const RATE_LIMIT_MAX = 100; // Max 100 uploads per hour per user
const RATE_LIMIT_MAX = 10; // Max 10 uploads per hour per user
function checkUploadRateLimit(userEmail: string): boolean {
const now = Date.now();
@@ -63,7 +58,6 @@ function checkUploadRateLimit(userEmail: string): boolean {
}
function validateFile(file: File): { valid: boolean; error?: string } {
// File size check
if (file.size > UPLOAD_CONFIG.maxFileSize) {
return {
valid: false,
@@ -71,7 +65,6 @@ function validateFile(file: File): { valid: boolean; error?: string } {
};
}
// File type check
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
return {
valid: false,
@@ -105,21 +98,17 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
try {
// Ensure upload directory exists
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
// Generate unique filename
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const randomString = crypto.randomBytes(8).toString('hex');
const extension = path.extname(file.name);
const filename = `${timestamp}-${randomString}${extension}`;
// Save file
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
const buffer = Buffer.from(await file.arrayBuffer());
await fs.writeFile(filepath, buffer);
// Generate public URL
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
return {
@@ -166,19 +155,16 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.missingRequired(['file']);
}
// Validate file
const validation = validateFile(file);
if (!validation.valid) {
return apiError.badRequest(validation.error!);
}
// Attempt upload (Nextcloud first, then local fallback)
let result: UploadResult;
if (isNextcloudConfigured()) {
result = await uploadToNextcloud(file, userEmail);
// If Nextcloud fails, try local fallback
if (!result.success) {
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
result = await uploadToLocal(file, type);
@@ -188,16 +174,8 @@ export const POST: APIRoute = async ({ request }) => {
}
if (result.success) {
// Log successful upload
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
// BEFORE: Manual success response (5 lines)
// return new Response(JSON.stringify(result), {
// status: 200,
// headers: { 'Content-Type': 'application/json' }
// });
// AFTER: Single line with specialized helper
return apiSpecial.uploadSuccess({
url: result.url!,
filename: result.filename!,
@@ -205,35 +183,23 @@ export const POST: APIRoute = async ({ request }) => {
storage: result.storage!
});
} else {
// Log failed upload
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
// BEFORE: Manual error response (5 lines)
// return new Response(JSON.stringify(result), {
// status: 500,
// headers: { 'Content-Type': 'application/json' }
// });
// AFTER: Single line with specialized helper
return apiSpecial.uploadFailed(result.error!);
}
}, 'Media upload processing failed');
};
// GET endpoint for upload status/info
export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
// Authentication check
const authResult = await withAPIAuth(request);
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
}
// Return upload configuration and status
const nextcloudConfigured = isNextcloudConfigured();
// Check local upload directory
let localStorageAvailable = false;
try {
await fs.access(UPLOAD_CONFIG.localUploadPath);

View File

@@ -1,7 +1,5 @@
---
// src/pages/auth/callback.astro - Fixed with Email
// Since server-side URL parameters aren't working,
// we'll handle this client-side and POST to the API
// src/pages/auth/callback.astro
---
<html>
@@ -58,7 +56,6 @@
<script>
(function() {
// Get URL parameters from client-side
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
@@ -77,7 +74,6 @@
window.location.href = '/?auth=error';
}, 3000);
} else if (code && state) {
// Send the parameters to our API endpoint
fetch('/api/auth/process', {
method: 'POST',
headers: {

View File

@@ -1,14 +1,13 @@
---
// src/pages/contribute/index.astro - Consolidated Auth
import BaseLayout from '../../layouts/BaseLayout.astro';
import { withAuth } from '../../utils/auth.js'; // Note: .js extension!
import { withAuth } from '../../utils/auth.js';
export const prerender = false;
// CONSOLIDATED: Replace 15+ lines with single function call
const authResult = await withAuth(Astro, 'contributions');
if (authResult instanceof Response) {
return authResult; // Redirect to login
return authResult;
}
const { authenticated, userEmail, userId } = authResult;
@@ -16,7 +15,6 @@ const { authenticated, userEmail, userId } = authResult;
<BaseLayout title="Contribute" description="Inhalte zum ForensicPathways beitragen">
<section style="padding: 2rem 0;">
<!-- Header -->
<div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 2.5rem;">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
@@ -38,8 +36,6 @@ const { authenticated, userEmail, userId } = authResult;
)}
</div>
<!-- Contribution Options -->
<!-- WRAPPER -->
<div
style="
display:grid;
@@ -50,9 +46,6 @@ const { authenticated, userEmail, userId } = authResult;
"
>
<!-- src/pages/contribute/index.astro - Replace the Tools/Methods/Concepts card -->
<!-- Tools, Methods & Concepts - IMPROVED UX -->
<div class="card"
style="padding: 2rem; border-left: 4px solid var(--color-primary); transition: var(--transition-fast);
display:flex; flex-direction:column;">
@@ -85,7 +78,6 @@ const { authenticated, userEmail, userId } = authResult;
Neuer Eintrag
</a>
<!-- IMPROVED: Clear guidance instead of confusing button -->
<div style="background-color: var(--color-bg-secondary); padding: 1.25rem; border-radius: 0.5rem; border-left: 3px solid var(--color-accent);">
<div style="display: flex; align-items: start; gap: 0.75rem;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2" style="flex-shrink: 0; margin-top: 0.125rem;">
@@ -112,7 +104,6 @@ const { authenticated, userEmail, userId } = authResult;
</div>
</div>
</div>
<!-- Knowledgebase Articles -->
<div class="card"
style="padding: 2rem; border-left: 4px solid var(--color-accent); cursor: pointer; transition: var(--transition-fast);
display:flex; flex-direction:column;"
@@ -148,7 +139,6 @@ const { authenticated, userEmail, userId } = authResult;
</div>
</div>
<!-- Issues & Improvements -->
<div class="card"
style="padding: 2rem; border-left: 4px solid var(--color-accent); cursor: pointer; transition: var(--transition-fast);
display:flex; flex-direction:column;">
@@ -187,14 +177,12 @@ const { authenticated, userEmail, userId } = authResult;
</div>
</div>
<!-- Push this actions block down if you add more later -->
<div style="margin-top:auto;"></div>
</div>
</div>
<!-- Guidelines -->
<div class="card" style="margin-bottom: 2rem;">
<h3 style="margin-bottom: 1.5rem; color: var(--color-text);">Richtlinien</h3>
@@ -236,7 +224,6 @@ const { authenticated, userEmail, userId } = authResult;
</div>
<script>
// Add hover effects for cards
document.querySelectorAll('.card[onclick]').forEach((card) => {
const cardEl = card as HTMLElement;
cardEl.addEventListener('mouseenter', function() {

View File

@@ -6,7 +6,6 @@ import { getToolsData } from '../../utils/dataService.js';
export const prerender = false;
// Check authentication
const authResult = await withAuth(Astro, 'contributions');
if (authResult instanceof Response) {
return authResult;
@@ -14,7 +13,6 @@ if (authResult instanceof Response) {
const { authenticated, userEmail, userId } = authResult;
// Load tools for reference (optional dropdown)
const data = await getToolsData();
const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.name));
---
@@ -22,18 +20,15 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
<BaseLayout title="Contribute Knowledge Base Article">
<div class="container" style="max-width: 900px; margin: 0 auto; padding: 2rem 1rem;">
<!-- Header -->
<div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem;">
<h1 style="margin-bottom: 1rem; font-size: 2rem;">Knowledgebase-Artikel</h1>
<p style="margin: 0.5rem 0; opacity: 0.9;">Danke für deinen Beitrag!</p>
{userEmail && <p style="margin: 0.5rem 0; opacity: 0.8;"><strong>Eingeloggt als:</strong> {userEmail}</p>}
</div>
<!-- Main Form -->
<div class="card">
<form id="kb-form" novalidate>
<!-- Basic Information -->
<div class="form-section">
<h3 class="section-title">Grundinformationen</h3>
@@ -87,7 +82,6 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<!-- Content -->
<div class="form-section">
<h3 class="section-title">Inhalt</h3>
@@ -114,7 +108,6 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<!-- File Upload -->
<div class="form-section">
<h3 class="section-title">Dateien hochladen</h3>
@@ -139,7 +132,6 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<!-- Additional Information -->
<div class="form-section">
<h3 class="section-title">Zusatzinformation</h3>
@@ -181,7 +173,6 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<!-- Submit Button -->
<div class="form-actions">
<a href="/" class="btn btn-secondary">Abbruch</a>
<button type="submit" id="submit-btn" class="btn btn-accent">
@@ -192,7 +183,6 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</form>
</div>
<!-- Success Modal -->
<div id="success-modal"
style="display:none; position:fixed; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,.5); z-index:1000; align-items:center; justify-content:center;">
@@ -209,7 +199,6 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<!-- Message Container -->
<div id="message-container" class="message-container"></div>
</div>
</BaseLayout>
@@ -239,7 +228,6 @@ class KnowledgebaseForm {
}
private init() {
// Get elements
this.elements = {
form: document.getElementById('kb-form'),
submitBtn: document.getElementById('submit-btn'),
@@ -262,7 +250,6 @@ class KnowledgebaseForm {
}
private setupEventListeners() {
// Form submission
this.elements.form?.addEventListener('submit', (e) => {
e.preventDefault();
if (!this.isSubmitting) {
@@ -381,7 +368,6 @@ class KnowledgebaseForm {
this.isSubmitting = true;
// Update UI
(this.elements.submitBtn as HTMLButtonElement).disabled = true;
(this.elements.submitText as HTMLElement).textContent = 'Submitting...';
(this.elements.submitSpinner as HTMLElement).style.display = 'inline';
@@ -389,7 +375,6 @@ class KnowledgebaseForm {
try {
const formData = new FormData(this.elements.form as HTMLFormElement);
// Process categories and tags
const categoriesValue = (formData.get('categories') as string) || '';
const tagsValue = (formData.get('tags') as string) || '';
@@ -398,7 +383,6 @@ class KnowledgebaseForm {
formData.set('categories', JSON.stringify(categories));
formData.set('tags', JSON.stringify(tags));
// Add uploaded files
formData.set('uploadedFiles', JSON.stringify(this.uploadedFiles.filter(f => f.uploaded)));
const response = await fetch('/api/contribute/knowledgebase', {
@@ -440,7 +424,6 @@ class KnowledgebaseForm {
(this.elements.successModal as HTMLElement).style.display = 'flex';
// Reset form
(this.elements.form as HTMLFormElement).reset();
this.uploadedFiles = [];
this.renderFileList();
@@ -458,23 +441,19 @@ class KnowledgebaseForm {
setTimeout(() => messageEl.remove(), 5000);
}
// Public method for file removal
public removeFileById(fileId: string) {
this.removeFile(fileId);
}
}
// Global instance
let formInstance: KnowledgebaseForm;
// Global function for file removal
window.removeFile = (fileId: string) => {
if (formInstance) {
formInstance.removeFileById(fileId);
}
};
// Initialize form
document.addEventListener('DOMContentLoaded', () => {
formInstance = new KnowledgebaseForm();
});

View File

@@ -1,12 +1,11 @@
---
// src/pages/contribute/tool.astro - COMPLETE REWRITE
// src/pages/contribute/tool.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { withAuth } from '../../utils/auth.js';
import { getToolsData } from '../../utils/dataService.js';
export const prerender = false;
// Check authentication
const authResult = await withAuth(Astro, 'contributions');
if (authResult instanceof Response) {
return authResult;
@@ -14,14 +13,12 @@ if (authResult instanceof Response) {
const { authenticated, userEmail, userId } = authResult;
// Load existing data
const data = await getToolsData();
const domains = data.domains;
const phases = data.phases;
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
const existingTools = data.tools;
// Check if this is an edit operation
const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool;
@@ -30,7 +27,6 @@ const isEdit = !!editTool;
<BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
<div class="container" style="max-width: 900px; margin: 0 auto; padding: 2rem 1rem;">
<!-- Header -->
<div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem;">
<h1 style="margin-bottom: 1rem; font-size: 2rem;">{isEdit ? `Edit: ${editTool?.name}` : 'Tool / Methode / Konzept beitragen'}</h1>
<p style="margin: 0.5rem 0; opacity: 0.9;">
@@ -42,17 +38,14 @@ const isEdit = !!editTool;
{userEmail && <p style="margin: 0.5rem 0; opacity: 0.8;"><strong>Eingeloggt als:</strong> {userEmail}</p>}
</div>
<!-- Validation Error Display -->
<div id="validation-errors" class="card" style="display: none; background-color: var(--color-error); color: white; margin-bottom: 2rem;">
<h3 style="margin: 0 0 1rem 0;">⚠️ Please fix the following issues:</h3>
<ul id="error-list" style="margin: 0; padding-left: 1.5rem;"></ul>
</div>
<!-- Main Form -->
<div class="card">
<form id="contribution-form" novalidate style="padding: 2rem;">
<!-- Basic Information -->
<div style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Grundlegende Infos</h3>
@@ -109,7 +102,6 @@ const isEdit = !!editTool;
</div>
</div>
<!-- Categories -->
<div style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Kategorien</h3>
@@ -142,7 +134,6 @@ const isEdit = !!editTool;
</div>
</div>
<!-- Software-Specific Fields -->
<div id="software-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Software Details</h3>
@@ -203,7 +194,6 @@ const isEdit = !!editTool;
</div>
</div>
<!-- Related Concepts -->
<div id="concepts-fields" style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem; display: none;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Konzepte im Zusammenhang</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.5rem;">
@@ -217,7 +207,6 @@ const isEdit = !!editTool;
</div>
</div>
<!-- Additional Information -->
<div style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Zusatzinfos</h3>
@@ -245,7 +234,6 @@ const isEdit = !!editTool;
</div>
</div>
<!-- YAML Preview -->
<div style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem;">
<h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Preview</h3>
<div style="border: 1px solid var(--color-border); border-radius: 0.375rem; overflow: hidden;">
@@ -253,7 +241,6 @@ const isEdit = !!editTool;
</div>
</div>
<!-- Submit -->
<div style="display: flex; gap: 1rem; justify-content: flex-end; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--color-border);">
<a href="/" class="btn btn-secondary">Cancel</a>
<button type="submit" id="submit-btn" class="btn btn-primary">
@@ -264,7 +251,6 @@ const isEdit = !!editTool;
</form>
</div>
<!-- Success Modal -->
<div id="success-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 1000; align-items: center; justify-content: center;">
<div class="card" style="max-width: 500px; width: 90%; margin: 2rem; text-align: center;">
<div style="font-size: 3rem; margin-bottom: 1rem;">✅</div>
@@ -280,7 +266,6 @@ const isEdit = !!editTool;
</BaseLayout>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware }}>
// FIXED: Prevent duplicate form submissions
console.log('[FORM] Script loaded, initializing...');
class ContributionForm {
@@ -288,14 +273,13 @@ class ContributionForm {
this.isEdit = isEdit;
this.editTool = editTool;
this.elements = {};
this.isSubmitting = false; // NEW: Prevent concurrent submissions
this.isSubmitting = false;
this.init();
}
init() {
console.log('[FORM] Starting initialization...');
// Get all form elements
this.elements = {
form: document.getElementById('contribution-form'),
submitBtn: document.getElementById('submit-btn'),
@@ -320,19 +304,16 @@ class ContributionForm {
licenseInput: document.getElementById('license')
};
// Verify critical elements
if (!this.elements.form || !this.elements.submitBtn) {
console.error('[FORM] Critical elements missing!');
return;
}
// FIXED: Check if already initialized
if (this.elements.form.hasAttribute('data-form-initialized')) {
console.log('[FORM] Form already initialized, skipping...');
return;
}
// Mark as initialized
this.elements.form.setAttribute('data-form-initialized', 'true');
console.log('[FORM] Setting up handlers...');
@@ -345,13 +326,11 @@ class ContributionForm {
}
setupEventListeners() {
// Type change handler
this.elements.typeSelect.addEventListener('change', () => {
this.updateFieldVisibility();
this.updateYAMLPreview();
});
// Form input handlers
this.elements.form.addEventListener('input', () => {
this.debounce(() => this.updateYAMLPreview(), 300);
});
@@ -360,12 +339,10 @@ class ContributionForm {
this.updateYAMLPreview();
});
// FIXED: Single submit handler with double-submission prevention
this.elements.form.addEventListener('submit', (e) => {
e.preventDefault();
e.stopPropagation();
// Prevent double submission
if (this.isSubmitting) {
console.log('[FORM] Submission already in progress, ignoring...');
return;
@@ -380,15 +357,12 @@ class ContributionForm {
updateFieldVisibility() {
const type = this.elements.typeSelect.value;
// Hide all conditional fields
this.elements.softwareFields.style.display = 'none';
this.elements.conceptsFields.style.display = 'none';
// Hide required indicators
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
// Show relevant fields based on type
if (type === 'software') {
this.elements.softwareFields.style.display = 'block';
this.elements.conceptsFields.style.display = 'block';
@@ -437,12 +411,10 @@ updateYAMLPreview() {
url: formData.get('url') || 'https://example.com'
};
// Add icon if provided
if (formData.get('icon')) {
tool.icon = formData.get('icon');
}
// Add software-specific fields
if (tool.type === 'software') {
tool.platforms = formData.getAll('platforms');
tool.license = formData.get('license') || 'Unknown';
@@ -455,7 +427,6 @@ updateYAMLPreview() {
}
}
// Add optional fields
if (formData.has('knowledgebase')) {
tool.knowledgebase = true;
}
@@ -470,7 +441,6 @@ updateYAMLPreview() {
tool.related_concepts = relatedConcepts;
}
// Generate YAML
const yaml = this.generateYAML(tool);
this.elements.yamlPreview.textContent = yaml;
@@ -515,7 +485,6 @@ validateForm() {
const errors = [];
const formData = new FormData(this.elements.form);
// Required field validation
const name = formData.get('name')?.trim();
if (!name) {
errors.push('Tool name is required');
@@ -549,7 +518,6 @@ validateForm() {
}
}
// Software-specific validation
if (type === 'software') {
const platforms = formData.getAll('platforms');
if (platforms.length === 0) {
@@ -571,26 +539,21 @@ showValidationErrors(errors) {
return;
}
// Clear previous errors
this.elements.errorList.innerHTML = '';
// Add each error as list item
errors.forEach(error => {
const li = document.createElement('li');
li.textContent = error;
this.elements.errorList.appendChild(li);
});
// Show error container
this.elements.validationErrors.style.display = 'block';
// Scroll to top to show errors
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async handleSubmit() {
console.log('[FORM] Submit handler called!');
// FIXED: Immediate submission lock
if (this.isSubmitting) {
console.log('[FORM] Already submitting, aborting...');
return;
@@ -598,19 +561,16 @@ showValidationErrors(errors) {
this.isSubmitting = true;
// Validate before submitting
const validationErrors = this.validateForm();
if (validationErrors.length > 0) {
console.log('[FORM] Validation failed:', validationErrors);
this.showValidationErrors(validationErrors);
this.isSubmitting = false; // Reset lock
this.isSubmitting = false;
return;
}
// Hide validation errors
this.elements.validationErrors.style.display = 'none';
// Immediate UI feedback
this.elements.submitBtn.disabled = true;
this.elements.submitText.textContent = this.isEdit ? 'Updating...' : 'Submitting...';
this.elements.submitSpinner.style.display = 'inline';
@@ -618,7 +578,6 @@ showValidationErrors(errors) {
try {
const formData = new FormData(this.elements.form);
// Build submission object
const submission = {
action: this.isEdit ? 'edit' : 'add',
tool: {
@@ -637,11 +596,9 @@ showValidationErrors(errors) {
}
};
// Add optional fields
if (formData.get('icon')) submission.tool.icon = formData.get('icon');
if (formData.has('knowledgebase')) submission.tool.knowledgebase = true;
// Add software-specific fields
if (submission.tool.type === 'software') {
submission.tool.platforms = formData.getAll('platforms');
submission.tool.license = formData.get('license');
@@ -654,7 +611,6 @@ showValidationErrors(errors) {
}
}
// Add related concepts
if (submission.tool.type !== 'concept') {
const related = formData.getAll('relatedConcepts');
if (related.length > 0) {
@@ -664,7 +620,6 @@ showValidationErrors(errors) {
console.log('[FORM] Sending submission:', submission);
// Submit to API
const response = await fetch('/api/contribute/tool', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -685,7 +640,6 @@ showValidationErrors(errors) {
console.error('[FORM] Submission error:', error);
alert(`Submission failed: ${error.message}\n\nPlease try again or contact support if the problem persists.`);
} finally {
// FIXED: Always reset submission state
this.isSubmitting = false;
this.elements.submitBtn.disabled = false;
this.elements.submitText.textContent = this.isEdit ? 'Update Tool' : 'Submit Contribution';
@@ -694,13 +648,11 @@ showValidationErrors(errors) {
}
showSuccess(result) {
// Update success message
const successMessage = document.getElementById('success-message');
if (successMessage) {
successMessage.textContent = `Your ${this.isEdit ? 'update' : 'contribution'} has been submitted as an issue and will be reviewed by maintainers.`;
}
// Show issue link if available
if (result.issueUrl) {
const prLink = document.getElementById('pr-link');
if (prLink) {
@@ -710,7 +662,6 @@ showValidationErrors(errors) {
}
}
// Show modal
this.elements.successModal.style.display = 'flex';
}
@@ -723,7 +674,6 @@ showValidationErrors(errors) {
}
}
// FIXED: Single initialization only
function initializeForm() {
const form = document.getElementById('contribution-form');
if (!form) {
@@ -740,7 +690,6 @@ function initializeForm() {
new ContributionForm();
}
// FIXED: Simple initialization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeForm);
} else {

View File

@@ -4,7 +4,6 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<BaseLayout title="Impressum" description="ForensicPathways - Impressum">
<section style="padding: 2rem 0; max-width: 900px; margin: 0 auto;">
<!-- Hero Section -->
<div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<p>Bei dieser Webseite handelt es sich um ein privates Werk. Keine Bildungseinrichtung, kein Unternehmen sind finanziell oder moderierend Teil dieses Projekts.</p>
<h1>Impressum</h1>

View File

@@ -6,13 +6,11 @@ import ToolMatrix from '../components/ToolMatrix.astro';
import AIQueryInterface from '../components/AIQueryInterface.astro';
import { getToolsData } from '../utils/dataService.js';
// Load tools data
const data = await getToolsData();
const tools = data.tools;
---
<BaseLayout title="~/">
<!-- Hero Section -->
<section style="padding: 2rem 0 1rem; border-bottom: 1px solid var(--color-border);">
<div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 1.5rem; color: var(--color-primary);">ForensicPathways</h1>
@@ -43,7 +41,6 @@ const tools = data.tools;
Infos zu SSO & Zugang
</a>
<!-- AI Query Button -->
<button id="ai-query-btn" class="btn btn-accent" style="padding: 0.75rem 1.5rem; background-color: var(--color-accent); color: white;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
@@ -52,7 +49,6 @@ const tools = data.tools;
KI befragen
</button>
<!-- Contribution Button -->
<a href="/contribute" class="btn" style="padding: 0.75rem 1.5rem; background-color: var(--color-warning); color: white; border-color: var(--color-warning);" data-contribute-button="new">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
@@ -75,15 +71,12 @@ const tools = data.tools;
</div>
</section>
<!-- Filters Section -->
<section id="filters-section" style="padding: 2rem 0;">
<ToolFilters data={data} />
</section>
<!-- AI Query Interface -->
<AIQueryInterface />
<!-- Tools Grid -->
<section id="tools-grid" style="padding-bottom: 2rem;">
<div class="grid-auto-fit" id="tools-container">
{tools.map((tool: any) => (
@@ -91,21 +84,17 @@ const tools = data.tools;
))}
</div>
<!-- No results message -->
<div id="no-results" style="display: none; text-align: center; padding: 4rem 0;">
<p class="text-muted" style="font-size: 1.125rem;">No tools found matching your criteria.</p>
</div>
</section>
<!-- Matrix View -->
<ToolMatrix data={data} />
</BaseLayout>
<script define:vars={{ toolsData: data.tools }}>
// Store tools data globally
window.toolsData = toolsData;
// Handle view changes and filtering
document.addEventListener('DOMContentLoaded', () => {
const toolsContainer = document.getElementById('tools-container');
const toolsGrid = document.getElementById('tools-grid');
@@ -115,7 +104,6 @@ const tools = data.tools;
const noResults = document.getElementById('no-results');
const aiQueryBtn = document.getElementById('ai-query-btn');
// Guard against null elements
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
console.error('Required DOM elements not found');
return;
@@ -124,7 +112,6 @@ const tools = data.tools;
if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', async () => {
if (typeof window.requireClientAuth === 'function') {
// ENHANCED: Use AI-specific authentication
await window.requireClientAuth(() => switchToView('ai'), `${window.location.pathname}?view=ai`, 'ai');
} else {
console.warn('[AUTH] requireClientAuth not available');
@@ -133,21 +120,17 @@ const tools = data.tools;
});
}
// Function to switch between different views
function switchToView(view) {
// Hide all views first
toolsGrid.style.display = 'none';
matrixContainer.style.display = 'none';
aiInterface.style.display = 'none';
filtersSection.style.display = 'none';
// Update view toggle buttons
const viewToggles = document.querySelectorAll('.view-toggle');
viewToggles.forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-view') === view);
});
// Show appropriate view and manage filter visibility
switch (view) {
case 'ai':
aiInterface.style.display = 'block';
@@ -166,20 +149,18 @@ const tools = data.tools;
filtersSection.style.display = 'block';
showFilterControls();
break;
default: // grid
default:
toolsGrid.style.display = 'block';
filtersSection.style.display = 'block';
showFilterControls();
break;
}
// Clear URL parameters after switching
if (window.location.search) {
window.history.replaceState({}, '', window.location.pathname);
}
}
// Helper functions for filter control visibility
function hideFilterControls() {
const elements = [
'.domain-phase-container',
@@ -215,24 +196,16 @@ const tools = data.tools;
checkboxWrappers.forEach(wrapper => wrapper.style.display = 'flex');
}
// REMOVED: createToolSlug function - now using window.createToolSlug
// REMOVED: findTool function - now using window.findToolByIdentifier
// Navigation functions for sharing
window.navigateToGrid = function(toolName) {
console.log('Navigating to grid for tool:', toolName);
// Switch to grid view first
switchToView('grid');
// Wait for view switch, then find and scroll to tool
setTimeout(() => {
// Clear any filters first
if (window.clearAllFilters) {
window.clearAllFilters();
}
// Wait for filters to clear and re-render
setTimeout(() => {
const toolCards = document.querySelectorAll('.tool-card');
let targetCard = null;
@@ -240,7 +213,6 @@ const tools = data.tools;
toolCards.forEach(card => {
const cardTitle = card.querySelector('h3');
if (cardTitle) {
// Clean title text (remove icons and extra spaces)
const titleText = cardTitle.textContent?.replace(/[^\w\s\-\.]/g, '').trim();
if (titleText === toolName) {
targetCard = card;
@@ -268,29 +240,23 @@ const tools = data.tools;
window.navigateToMatrix = function(toolName) {
console.log('Navigating to matrix for tool:', toolName);
// Switch to matrix view
switchToView('matrix');
// Wait for view switch and matrix to render
setTimeout(() => {
const toolChips = document.querySelectorAll('.tool-chip');
let firstMatch = null;
let matchCount = 0;
toolChips.forEach(chip => {
// Clean the chip text (remove emoji and extra spaces)
const chipText = chip.textContent?.replace(/📖/g, '').replace(/[^\w\s\-\.]/g, '').trim();
if (chipText === toolName) {
// Highlight this occurrence
chip.style.animation = 'highlight-flash 2s ease-out';
matchCount++;
// Remember the first match for scrolling
if (!firstMatch) {
firstMatch = chip;
}
// Clean up animation after it completes
setTimeout(() => {
chip.style.animation = '';
}, 8000);
@@ -306,7 +272,6 @@ const tools = data.tools;
}, 500);
};
// Handle URL parameters on page load
function handleSharedURL() {
const urlParams = new URLSearchParams(window.location.search);
const toolParam = urlParams.get('tool');
@@ -314,25 +279,21 @@ const tools = data.tools;
const modalParam = urlParams.get('modal');
if (!toolParam) {
// Check for AI view parameter
if (viewParam === 'ai') {
switchToView('ai');
}
return;
}
// Find the tool by name or slug using global function
const tool = window.findToolByIdentifier(window.toolsData, toolParam);
if (!tool) {
console.warn('Shared tool not found:', toolParam);
return;
}
// Clear URL parameters to avoid re-triggering
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
// Handle different view types
setTimeout(() => {
switch (viewParam) {
case 'grid':
@@ -354,7 +315,6 @@ const tools = data.tools;
}, 100);
}
// ENHANCED: New filtering logic using show/hide pattern
window.addEventListener('toolsFiltered', (event) => {
const filtered = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
@@ -363,7 +323,6 @@ const tools = data.tools;
return;
}
// Get all existing tool cards
const allToolCards = document.querySelectorAll('.tool-card');
const filteredNames = new Set(filtered.map(tool => tool.name.toLowerCase()));
@@ -379,7 +338,6 @@ const tools = data.tools;
}
});
// Show/hide no results message
if (visibleCount === 0) {
noResults.style.display = 'block';
} else {
@@ -387,16 +345,13 @@ const tools = data.tools;
}
});
// Handle view changes
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
switchToView(view);
});
// Make switchToView available globally
window.switchToAIView = () => switchToView('ai');
// Initialize URL handling
handleSharedURL();
});
</script>

View File

@@ -4,21 +4,17 @@ import { getCollection } from 'astro:content';
import { getToolsData } from '../utils/dataService.js';
import ContributionButton from '../components/ContributionButton.astro';
// Load tools data and knowledgebase articles
const data = await getToolsData();
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
// Only include published articles
return entry.data.published !== false;
});
// Create unified knowledgebase entries with optional tool association
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
const associatedTool = entry.data.tool_name
? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
: null;
return {
// Article metadata
slug: entry.slug,
title: entry.data.title,
description: entry.data.description,
@@ -28,13 +24,11 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
categories: entry.data.categories || [],
tags: entry.data.tags || [],
// Tool association (optional)
tool_name: entry.data.tool_name,
related_tools: entry.data.related_tools || [],
associatedTool,
// Derived properties for consistency with existing UI
name: entry.data.title, // For search compatibility
name: entry.data.title,
type: associatedTool?.type || 'article',
icon: associatedTool?.icon || '📖',
platforms: associatedTool?.platforms || [],
@@ -44,13 +38,11 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
};
});
// Sort alphabetically by title
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
---
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
<section style="padding: 2rem 0;">
<!-- Header -->
<div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<h1 style="margin-bottom: 1rem; font-size: 2.5rem; color: var(--color-primary);">Knowledgebase</h1>
<p style="font-size: 1.25rem; color: var(--color-text-secondary); margin-bottom: 1.125rem;">
@@ -60,7 +52,6 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
Praktische Erfahrungen, Konfigurationshinweise und Lektionen aus der Praxis
</p>
<!--contribution button -->
<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
<ContributionButton type="write" variant="primary" text="Artikel schreiben" style="padding: 0.75rem 1.5rem;" />
<a href="#kb-entries" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
@@ -73,7 +64,6 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
</div>
</div>
<!-- Search -->
<div style="margin-bottom: 2rem;">
<input
type="text"
@@ -83,14 +73,12 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
/>
</div>
<!-- Articles Count -->
<div style="text-align: center; margin-bottom: 2rem;">
<p class="text-muted" style="font-size: 0.875rem;">
<span id="visible-count">{knowledgebaseEntries.length}</span> von {knowledgebaseEntries.length} Einträgen
</p>
</div>
<!-- Knowledgebase Entries -->
<div style="max-width: 1000px; margin: 0 auto;">
{knowledgebaseEntries.length === 0 ? (
<div class="card" style="text-align: center; padding: 3rem;">
@@ -134,7 +122,6 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
{entry.title}
</h3>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<!-- Type indicator badges -->
{isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
@@ -143,19 +130,16 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
<!-- Difficulty indicator -->
{entry.difficulty && (
<span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;">
{entry.difficulty}
</span>
)}
<!-- Knowledge Base indicator -->
<span class="badge badge-error">📖</span>
</div>
</div>
<!-- Action buttons -->
<div style="display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0;">
<a href={`/knowledgebase/${entry.slug}`} class="btn btn-primary" style="font-size: 0.8125rem;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
@@ -166,19 +150,15 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
Artikel öffnen
</a>
<!-- Edit button for knowledgebase articles -->
<ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
</div>
</div>
<!-- Description -->
<p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;">
{entry.description}
</p>
<!-- Metadata and Tags -->
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;">
<!-- Tags -->
{entry.tags && entry.tags.length > 0 && (
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
{entry.tags.map((tag: string) => (
@@ -187,14 +167,12 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
</div>
)}
<!-- Categories -->
{entry.categories && entry.categories.length > 0 && (
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Kategorien:</strong> {entry.categories.join(', ')}
</div>
)}
<!-- Tool-specific metadata (only if associated with tool) -->
{hasAssociatedTool && entry.phases && entry.phases.length > 0 && (
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Phasen:</strong> {entry.phases.join(', ')}
@@ -207,14 +185,12 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
</div>
)}
<!-- Related tools for standalone articles -->
{isStandalone && entry.related_tools && entry.related_tools.length > 0 && (
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Verwandte Tools:</strong> {entry.related_tools.join(', ')}
</div>
)}
<!-- Author and date -->
<div style="font-size: 0.8125rem; color: var(--color-text-secondary); margin-left: auto;">
<strong>Autor:</strong> {entry.author} • <strong>Aktualisiert:</strong> {entry.last_updated.toLocaleDateString('de-DE')}
</div>
@@ -226,14 +202,12 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
)}
</div>
<!-- No Results -->
<div id="no-kb-results" class="card" style="text-align: center; padding: 3rem; display: none;">
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Keine Ergebnisse gefunden</h3>
<p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p>
</div>
</section>
<!-- Floating Action Button -->
<div id="fab-container" style="position: fixed; bottom: 2rem; right: 2rem; z-index: 100; display: none;">
<ContributionButton
type="write"
@@ -246,7 +220,6 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
</BaseLayout>
<script>
// Enhanced knowledgebase functionality with search
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('kb-search') as HTMLInputElement | null;
const entries = document.querySelectorAll('.kb-entry') as NodeListOf<HTMLElement>;
@@ -278,13 +251,11 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
updateVisibleCount(visibleEntries);
// Show/hide no results message
if (noResults) {
noResults.style.display = visibleEntries === 0 && searchTerm.length > 0 ? 'block' : 'none';
}
}
// Search functionality
if (searchInput) {
searchInput.addEventListener('input', (e) => {
const target = e.target as HTMLInputElement;
@@ -292,17 +263,14 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
});
}
// Show floating action button on scroll (optional enhancement)
let lastScrollY = window.scrollY;
const fabContainer = document.getElementById('fab-container');
window.addEventListener('scroll', () => {
if (fabContainer) {
if (window.scrollY > 200 && window.scrollY < lastScrollY) {
// Scrolling up and past threshold
fabContainer.style.display = 'block';
} else {
// Scrolling down or at top
fabContainer.style.display = 'none';
}
lastScrollY = window.scrollY;

View File

@@ -3,7 +3,6 @@ import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getToolsData } from '../../utils/dataService.js';
// Prerender these pages at build time
export const prerender = true;
export async function getStaticPaths() {
@@ -21,33 +20,26 @@ export async function getStaticPaths() {
const { entry }: { entry: any } = Astro.props;
// Render the content
const { Content } = await entry.render();
// Load tools data to get the tool details
const data = await getToolsData();
// UPGRADED: Handle optional tool association
const primaryTool = entry.data.tool_name
? data.tools.find((t: any) => t.name === entry.data.tool_name)
: null;
// UPGRADED: Handle multiple related tools
const relatedTools = entry.data.related_tools
? entry.data.related_tools.map((toolName: string) =>
data.tools.find((t: any) => t.name === toolName)
).filter(Boolean)
: [];
// UPGRADED: Use primary tool or first related tool for styling, fallback to generic
const displayTool = primaryTool || relatedTools[0];
// UPGRADED: Don't redirect - show article even without tool association
if (!displayTool && !entry.data.tool_name && relatedTools.length === 0) {
console.log(`Standalone knowledgebase article: ${entry.slug}`);
}
// Determine styling based on tool type or fallback to generic
const isMethod = displayTool?.type === 'method';
const isConcept = displayTool?.type === 'concept';
const isStandalone = !displayTool;
@@ -59,7 +51,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
<BaseLayout title={entry.data.title} description={entry.data.description}>
<article style="max-width: 900px; margin: 0 auto;">
<!-- Header -->
<header style="margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div style="flex: 1;">
@@ -73,7 +64,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</div>
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<!-- UPGRADED: Conditional badges based on tool type or standalone -->
{isStandalone ? (
<span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>
) : (
@@ -90,9 +80,7 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</div>
</div>
<!-- UPGRADED: Flexible metadata section -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-border);">
<!-- Difficulty (always shown if present) -->
{entry.data.difficulty && (
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
@@ -100,19 +88,16 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</div>
)}
<!-- Last Updated (always shown) -->
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
</div>
<!-- Author (always shown) -->
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
</div>
<!-- UPGRADED: Show article type -->
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Typ</strong>
<p style="margin: 0; font-size: 0.9375rem;">
@@ -123,7 +108,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</p>
</div>
<!-- UPGRADED: Categories (if present) -->
{entry.data.categories && entry.data.categories.length > 0 && (
<div style="grid-column: 1 / -1;">
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
@@ -137,7 +121,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</div>
</header>
<!-- Navigation -->
<nav style="margin-bottom: 2rem; position: relative; z-index: 50;">
<a href="/knowledgebase" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
@@ -147,14 +130,12 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</a>
</nav>
<!-- Content -->
<div class="card" style="padding: 2rem;">
<div class="kb-content markdown-content" style="line-height: 1.7;">
<Content />
</div>
</div>
<!-- UPGRADED: Flexible Tool Actions Section -->
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);">
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">
{isStandalone ? 'Verwandte Aktionen' : 'Tool-Aktionen'}
@@ -162,7 +143,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
{isStandalone ? (
<!-- UPGRADED: Standalone article actions -->
<a href="/knowledgebase" class="btn btn-primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
@@ -174,7 +154,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
Weitere Artikel
</a>
) : (
<!-- UPGRADED: Tool-specific actions (existing logic) -->
<>
{isConcept ? (
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
@@ -219,7 +198,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</>
)}
<!-- UPGRADED: Show related tools if present -->
{relatedTools.length > 0 && relatedTools.length > (primaryTool ? 1 : 0) && (
<div style="margin-left: auto;">
<details style="position: relative;">
@@ -247,7 +225,6 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</div>
)}
<!-- Always show return to main page -->
<a href="/" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>

View File

@@ -3,10 +3,8 @@ import BaseLayout from '../layouts/BaseLayout.astro';
import { getToolsData } from '../utils/dataService.js';
// Load tools data to get server-based services
const data = await getToolsData();
// Filter for hosted services based on projectUrl presence
const hostedServices = data.tools.filter((tool: any) => {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
@@ -22,14 +20,12 @@ const hostedServices = data.tools.filter((tool: any) => {
Live-Monitoring zum Onlinestatus der Dienste.
</p>
<!-- Service Status Grid -->
<div class="grid grid-cols-2 gap-4" style="margin-bottom: 3rem;">
{hostedServices.map((service: any) => (
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3 style="margin: 0;">{service.name}</h3>
<div id={`status-${service.name.toLowerCase().replace(/\s+/g, '-')}`}>
<!-- Status badge will be inserted here -->
<span class="badge badge-warning">Loading...</span>
</div>
</div>
@@ -43,41 +39,28 @@ const hostedServices = data.tools.filter((tool: any) => {
))}
</div>
<!-- Uptime Kuma Embed
<div class="card" style="padding: 0; overflow: hidden;">
<iframe
src="https://status.mikoshi.de/status/cc24-hub?embed=true"
style="width: 100%; height: 600px; border: none;"
title="Uptime Kuma Status Page"
></iframe>
</div>-->
</section>
</BaseLayout>
<script define:vars={{ hostedServices }}>
// Load status badges for each service
document.addEventListener('DOMContentLoaded', () => {
hostedServices.forEach(service => {
const statusElement = document.getElementById(`status-${service.name.toLowerCase().replace(/\s+/g, '-')}`);
if (statusElement && service.statusUrl) {
// Create image element
const img = document.createElement('img');
img.src = service.statusUrl;
img.alt = `Status for ${service.name}`;
img.style.height = '20px';
img.style.verticalAlign = 'middle';
// Handle load error
img.onerror = function() {
statusElement.innerHTML = '<span class="badge badge-warning">Unknown</span>';
};
// Replace loading badge with image
statusElement.innerHTML = '';
statusElement.appendChild(img);
} else if (statusElement) {
// No status URL available
statusElement.innerHTML = '<span class="badge badge-warning">No Status</span>';
}
});