simplify knowledgebase articles

This commit is contained in:
overcuriousity 2025-07-26 13:20:01 +02:00
parent a9c15eb9c6
commit 4cc28bceb7
14 changed files with 1092 additions and 846 deletions

6
.astro/content.d.ts vendored
View File

@ -164,11 +164,9 @@ declare module 'astro:content' {
type DataEntryMap = {
"knowledgebase": Record<string, {
id: string;
render(): Render[".md"];
slug: string;
body: string;
body?: string;
collection: "knowledgebase";
data: InferEntrySchema<"knowledgebase">;
data: any;
rendered?: RenderedContent;
filePath?: string;
}>;

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1752478949435
"lastUpdateCheck": 1753528124767
}
}

3
.gitignore vendored
View File

@ -81,3 +81,6 @@ src/_data/config.local.yaml
tmp/
temp/
.astro/data-store.json
.astro/settings.json
.astro/data-store.json
.astro/content.d.ts

View File

@ -37,7 +37,7 @@ Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR)
## 🛠 Technische Grundlage
- **Framework:** Astro 5.x mit TypeScript
- **Framework:** Astro 4.x mit TypeScript
- **Styling:** CSS Custom Properties mit Dark/Light Mode
- **API:** Node.js Backend mit Astro API Routes
- **Datenbank:** YAML-basierte Konfiguration (tools.yaml)
@ -117,8 +117,8 @@ sudo systemctl enable nginx
```bash
# Klonen des Repositorys
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /var/www/cc24-hub
cd /var/www/cc24-hub
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /opt/cc24-hub
cd /opt/cc24-hub
# Abhängigkeiten installieren
sudo npm install
@ -127,12 +127,12 @@ sudo npm install
sudo npm run build
# Berechtigungen setzen
sudo chown -R www-data:www-data /var/www/cc24-hub
sudo chown -R www-data:www-data /opt/cc24-hub
```
#### 3. Umgebungsvariablen konfigurieren
Erstelle `/var/www/cc24-hub/.env`:
Erstelle `/opt/cc24-hub/.env`:
```bash
# === GRUNDKONFIGURATION ===
@ -166,13 +166,13 @@ GIT_API_TOKEN=your_git_api_token
GIT_REPO_URL=https://ihr-git-server.de/user/cc24-hub
# === UPLOAD-KONFIGURATION ===
LOCAL_UPLOAD_PATH=/var/www/cc24-hub/public/uploads
LOCAL_UPLOAD_PATH=/opt/cc24-hub/public/uploads
```
```bash
# Berechtigungen sichern
sudo chmod 600 /var/www/cc24-hub/.env
sudo chown www-data:www-data /var/www/cc24-hub/.env
sudo chmod 600 /opt/cc24-hub/.env
sudo chown www-data:www-data /opt/cc24-hub/.env
```
#### 4. Nginx konfigurieren
@ -205,7 +205,7 @@ server {
# Static Files
location / {
try_files $uri $uri/ @nodejs;
root /var/www/cc24-hub/dist;
root /opt/cc24-hub/dist;
index index.html;
# Cache static assets
@ -256,7 +256,7 @@ Wants=nginx.service
Type=exec
User=www-data
Group=www-data
WorkingDirectory=/var/www/cc24-hub
WorkingDirectory=/opt/cc24-hub
Environment=NODE_ENV=production
ExecStart=/usr/bin/node ./dist/server/entry.mjs
Restart=always
@ -269,7 +269,7 @@ NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/www/cc24-hub
ReadWritePaths=/opt/cc24-hub
CapabilityBoundingSet=
# Resource Limits
@ -431,7 +431,7 @@ domain-agnostic-software:
```bash
# Repository aktualisieren
cd /var/www/cc24-hub
cd /opt/cc24-hub
sudo git pull
# Dependencies aktualisieren
@ -449,8 +449,8 @@ sudo systemctl restart cc24-hub
Wichtige Dateien für Backup:
```bash
/var/www/cc24-hub/src/data/tools.yaml
/var/www/cc24-hub/.env
/opt/cc24-hub/src/data/tools.yaml
/opt/cc24-hub/.env
/etc/nginx/sites-available/cc24-hub
/etc/systemd/system/cc24-hub.service
```
@ -472,4 +472,5 @@ Bei Problemen oder Fragen:
- **Dokumentation:** Siehe `/knowledgebase` auf der Website
## 📄 Lizenz
Dieses Projekt steht unter der BSD-3-Clause Lizenz.
Dieses Projekt steht unter der **BSD-3-Clause** Lizenz.

File diff suppressed because it is too large Load Diff

View File

@ -4,26 +4,18 @@ const knowledgebaseCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
tool_name: z.string(),
description: z.string(),
last_updated: z.date(),
author: z.string().default('CC24-Team'),
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']),
tool_name: z.string().optional(),
related_tools: z.array(z.string()).default([]),
author: z.string().default('Anon'),
difficulty: z.enum(['novice', 'beginner', 'intermediate', 'advanced', 'expert']).optional(),
categories: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
sections: z.object({
overview: z.boolean().default(true),
installation: z.boolean().default(false),
configuration: z.boolean().default(false),
usage_examples: z.boolean().default(true),
best_practices: z.boolean().default(true),
troubleshooting: z.boolean().default(false),
advanced_topics: z.boolean().default(false),
}).default({}),
review_status: z.enum(['draft', 'review', 'published']).default('published'),
published: z.boolean().default(true),
})
});
export const collections = {
knowledgebase: knowledgebaseCollection,
};
});

View File

@ -3,7 +3,7 @@ title: "Kali Linux - Die Hacker-Distribution für Forensik & Penetration Testing
tool_name: "Kali Linux"
description: "Leitfaden zur Installation, Nutzung und Best Practices für Kali Linux die All-in-One-Plattform für Security-Profis."
last_updated: 2025-07-20
author: "CC24-Team"
author: "Claude 4 Sonnet"
difficulty: "intermediate"
categories: ["incident-response", "forensics", "penetration-testing"]
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]

View File

@ -3,7 +3,7 @@ title: "MISP - Plattform für Threat Intelligence Sharing"
tool_name: "MISP"
description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit."
last_updated: 2025-07-20
author: "CC24-Team"
author: "Claude 4 Sonnet"
difficulty: "intermediate"
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]

View File

@ -3,7 +3,7 @@ title: "Nextcloud - Sichere Kollaborationsplattform"
tool_name: "Nextcloud"
description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien"
last_updated: 2025-07-20
author: "CC24-Team"
author: "Claude 4 Sonnet"
difficulty: "novice"
categories: ["collaboration-general"]
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]

View File

@ -3,7 +3,7 @@ title: "Regular Expressions (Regex) Musterbasierte Textanalyse"
tool_name: "Regular Expressions (Regex)"
description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen."
last_updated: 2025-07-20
author: "CC24-Team"
author: "Claude 4 Sonnet"
difficulty: "intermediate"
categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"]
tags: ["pattern-matching", "text-processing", "log-analysis", "string-manipulation", "search-algorithms"]

View File

@ -3,7 +3,7 @@ title: "Velociraptor Skalierbare Endpoint-Forensik mit VQL"
tool_name: "Velociraptor"
description: "Detaillierte Anleitung und Best Practices für Velociraptor Remote-Forensik der nächsten Generation"
last_updated: 2025-07-20
author: "CC24-Team"
author: "Claude 4 Sonnet"
difficulty: "advanced"
categories: ["incident-response", "malware-analysis", "network-forensics"]
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]

View File

@ -1,16 +1,51 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
import { getToolsData } from '../utils/dataService.js';
import ContributionButton from '../components/ContributionButton.astro';
// Load tools data
// 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;
});
// Filter tools that have knowledgebase flag set to true
const knowledgebaseTools = data.tools.filter((tool: any) => tool.knowledgebase === true);
// 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,
author: entry.data.author,
last_updated: entry.data.last_updated,
difficulty: entry.data.difficulty,
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
type: associatedTool?.type || 'article',
icon: associatedTool?.icon || '📖',
platforms: associatedTool?.platforms || [],
skillLevel: entry.data.difficulty || associatedTool?.skillLevel || 'intermediate',
phases: associatedTool?.phases || [],
license: associatedTool?.license
};
});
// Sort alphabetically by name
knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
// 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">
@ -48,16 +83,16 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
/>
</div>
<!-- Tools Count -->
<!-- Articles Count -->
<div style="text-align: center; margin-bottom: 2rem;">
<p class="text-muted" style="font-size: 0.875rem;">
<span id="visible-count">{knowledgebaseTools.length}</span> von {knowledgebaseTools.length} Einträgen
<span id="visible-count">{knowledgebaseEntries.length}</span> von {knowledgebaseEntries.length} Einträgen
</p>
</div>
<!-- Knowledgebase Entries -->
<div style="max-width: 1000px; margin: 0 auto;">
{knowledgebaseTools.length === 0 ? (
{knowledgebaseEntries.length === 0 ? (
<div class="card" style="text-align: center; padding: 3rem;">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" style="margin: 0 auto 1rem;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
@ -68,54 +103,61 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
</svg>
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3>
<p class="text-muted">
Knowledgebase-Einträge werden automatisch angezeigt, sobald Datenbankeinträge das Attribut "knowledgebase: true" haben.
Knowledgebase-Einträge werden automatisch angezeigt, sobald Artikel veröffentlicht werden.
</p>
</div>
) : (
<div id="kb-entries">
{knowledgebaseTools.map((tool: any, index: number) => {
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
{knowledgebaseEntries.map((entry: any, index: number) => {
const hasAssociatedTool = !!entry.associatedTool;
const hasValidProjectUrl = hasAssociatedTool &&
entry.associatedTool.projectUrl !== undefined &&
entry.associatedTool.projectUrl !== null &&
entry.associatedTool.projectUrl !== "" &&
entry.associatedTool.projectUrl.trim() !== "";
const toolSlug = tool.name.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
const isStandalone = !hasAssociatedTool;
return (
<article
class="kb-entry card"
id={`kb-${toolSlug}`}
data-tool-name={tool.name.toLowerCase()}
id={`kb-${entry.slug}`}
data-tool-name={entry.title.toLowerCase()}
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
>
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 1rem;">
<h3 style="margin: 0; color: var(--color-primary);">
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
{tool.name}
<span style="margin-right: 0.5rem;">{entry.icon}</span>
{entry.title}
</h3>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<!-- Type indicator badges -->
{tool.type === 'concept' && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
{tool.type === 'method' && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>}
{tool.type === 'software' && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
{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>}
{hasAssociatedTool && !isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{tool.license !== 'Proprietary' && tool.type !== 'concept' && tool.type !== 'method' && <span class="badge badge-success">Open Source</span>}
{hasAssociatedTool && entry.associatedTool.license !== 'Proprietary' && !isMethod && !isConcept && <span class="badge badge-success">Open Source</span>}
<!-- Difficulty indicator -->
<span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;">
{tool.skillLevel || 'intermediate'}
</span>
{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/${toolSlug}`} class="btn btn-primary" style="font-size: 0.8125rem;">
<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;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
@ -124,38 +166,58 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
Artikel öffnen
</a>
<!-- NEW: Edit button for existing knowledgebase articles -->
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
<!-- 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>
<!-- Rest of existing article content remains unchanged -->
<!-- Description -->
<p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;">
{tool.description}
{entry.description}
</p>
<!-- Tags and Metadata -->
<!-- Metadata and Tags -->
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;">
{tool.tags && tool.tags.length > 0 && (
<!-- Tags -->
{entry.tags && entry.tags.length > 0 && (
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">
{tool.tags.map((tag: string) => (
{entry.tags.map((tag: string) => (
<span class="tag" style="font-size: 0.75rem;">{tag}</span>
))}
</div>
)}
{tool.phases && tool.phases.length > 0 && (
<!-- Categories -->
{entry.categories && entry.categories.length > 0 && (
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Phasen:</strong> {tool.phases.join(', ')}
<strong>Kategorien:</strong> {entry.categories.join(', ')}
</div>
)}
{tool.platforms && tool.platforms.length > 0 && tool.type !== 'concept' && tool.type !== 'method' && (
<!-- 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>Plattformen:</strong> {tool.platforms.join(', ')}
<strong>Phasen:</strong> {entry.phases.join(', ')}
</div>
)}
{hasAssociatedTool && entry.platforms && entry.platforms.length > 0 && !isMethod && !isConcept && (
<div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Plattformen:</strong> {entry.platforms.join(', ')}
</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>
</div>
</article>
);
@ -170,18 +232,19 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
<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"
variant="primary"
text="✍️"
style="border-radius: 50%; width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-lg); font-size: 1.5rem; padding: 0;"
className="fab-button"
/>
</div>
<ContributionButton
type="write"
variant="primary"
text="✍️"
style="border-radius: 50%; width: 56px; height: 56px; display: flex; align-items: center; justify-content: center; box-shadow: var(--shadow-lg); font-size: 1.5rem; padding: 0;"
className="fab-button"
/>
</div>
</BaseLayout>
<script>
// Enhanced knowledgebase functionality with search
document.addEventListener('DOMContentLoaded', () => {
@ -228,5 +291,22 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
filterEntries(target.value);
});
}
// 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;
}
});
});
</script>

View File

@ -26,20 +26,35 @@ const { Content } = await entry.render();
// Load tools data to get the tool details
const data = await getToolsData();
const tool = data.tools.find((t: any) => t.name === entry.data.tool_name);
if (!tool) {
console.warn(`Tool not found for knowledgebase entry: ${entry.data.tool_name}`);
return Astro.redirect('/knowledgebase');
// 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 tool type for styling
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
const hasValidProjectUrl = tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
// Determine styling based on tool type or fallback to generic
const isMethod = displayTool?.type === 'method';
const isConcept = displayTool?.type === 'concept';
const isStandalone = !displayTool;
const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &&
displayTool.projectUrl !== null &&
displayTool.projectUrl !== "" &&
displayTool.projectUrl.trim() !== "";
---
<BaseLayout title={entry.data.title} description={entry.data.description}>
@ -49,7 +64,7 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div style="flex: 1;">
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
{tool.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{tool.icon}</span>}
{displayTool?.icon && <span style="margin-right: 0.75rem; font-size: 1.5rem;">{displayTool.icon}</span>}
{entry.data.title}
</h1>
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
@ -58,32 +73,59 @@ const hasValidProjectUrl = tool.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;">
{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>}
{!isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{!isMethod && !isConcept && tool.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
<!-- UPGRADED: Conditional badges based on tool type or standalone -->
{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>}
{!isMethod && !isConcept && !isStandalone && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>}
{!isMethod && !isConcept && hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
{!isMethod && !isConcept && !isStandalone && displayTool?.license !== 'Proprietary' && <span class="badge badge-success">Open Source</span>}
</>
)}
<span class="badge badge-error">📖</span>
</div>
</div>
</div>
<!-- Metadata -->
<!-- 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);">
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
</div>
<!-- Difficulty (always shown if present) -->
{entry.data.difficulty && (
<div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong>
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p>
</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;">
{isStandalone ? 'Allgemeiner Artikel' :
isConcept ? 'Konzept-Artikel' :
isMethod ? 'Methoden-Artikel' :
'Software-Artikel'}
</p>
</div>
<!-- UPGRADED: Categories (if present) -->
{entry.data.categories && entry.data.categories.length > 0 && (
<div>
<div style="grid-column: 1 / -1;">
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Kategorien</strong>
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
{entry.data.categories.map((cat: string) => (
@ -105,57 +147,107 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
</a>
</nav>
<!-- Content -->
<div class="card" style="padding: 2rem;">
<div class="kb-content markdown-content" style="line-height: 1.7;">
<Content />
<!-- Content -->
<div class="card" style="padding: 2rem;">
<div class="kb-content markdown-content" style="line-height: 1.7;">
<Content />
</div>
</div>
</div>
<!-- Tool Actions -->
<!-- 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);">Tool-Aktionen</h3>
<h3 style="margin: 0 0 1rem 0; color: var(--color-text);">
{isStandalone ? 'Verwandte Aktionen' : 'Tool-Aktionen'}
</h3>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
{isConcept ? (
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
{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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>
Mehr erfahren
</a>
) : isMethod ? (
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Zur Methode
Weitere Artikel
</a>
) : (
<!-- UPGRADED: Tool-specific actions (existing logic) -->
<>
<a href={tool.url} target="_blank" rel="noopener noreferrer" 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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Software-Homepage
</a>
{hasValidProjectUrl && (
<a href={tool.projectUrl} target="_blank" rel="noopener noreferrer" class="btn btn-primary">
{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);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16l4-4-4-4"/>
<path d="M8 12h8"/>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Zugreifen
Mehr erfahren
</a>
) : isMethod ? (
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-method); border-color: var(--color-method);">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Zur Methode
</a>
) : (
<>
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" 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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<polyline points="15 3 21 3 21 9"/>
<line x1="10" y1="14" x2="21" y2="3"/>
</svg>
Software-Homepage
</a>
{hasValidProjectUrl && (
<a href={displayTool.projectUrl} target="_blank" rel="noopener noreferrer" 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;">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16l4-4-4-4"/>
<path d="M8 12h8"/>
</svg>
Zugreifen
</a>
)}
</>
)}
</>
)}
<!-- UPGRADED: Show related tools if present -->
{relatedTools.length > 0 && relatedTools.length > (primaryTool ? 1 : 0) && (
<div style="margin-left: auto;">
<details style="position: relative;">
<summary class="btn btn-secondary" style="cursor: pointer; list-style: none;">
<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"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
Verwandte Tools ({relatedTools.length})
</summary>
<div style="position: absolute; top: 100%; left: 0; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1rem; min-width: 200px; z-index: 100; box-shadow: var(--shadow-lg);">
{relatedTools.map((tool: any) => (
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer"
style="display: block; padding: 0.5rem; border-radius: 0.25rem; text-decoration: none; color: var(--color-text); margin-bottom: 0.25rem;"
onmouseover="this.style.backgroundColor='var(--color-bg-secondary)'"
onmouseout="this.style.backgroundColor='transparent'">
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>}
{tool.name}
</a>
))}
</div>
</details>
</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"/>
@ -166,4 +258,4 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
</div>
</div>
</article>
</BaseLayout>
</BaseLayout>