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 = { type DataEntryMap = {
"knowledgebase": Record<string, { "knowledgebase": Record<string, {
id: string; id: string;
render(): Render[".md"]; body?: string;
slug: string;
body: string;
collection: "knowledgebase"; collection: "knowledgebase";
data: InferEntrySchema<"knowledgebase">; data: any;
rendered?: RenderedContent; rendered?: RenderedContent;
filePath?: string; filePath?: string;
}>; }>;

File diff suppressed because one or more lines are too long

View File

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

3
.gitignore vendored
View File

@ -81,3 +81,6 @@ src/_data/config.local.yaml
tmp/ tmp/
temp/ temp/
.astro/data-store.json .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 ## 🛠 Technische Grundlage
- **Framework:** Astro 5.x mit TypeScript - **Framework:** Astro 4.x mit TypeScript
- **Styling:** CSS Custom Properties mit Dark/Light Mode - **Styling:** CSS Custom Properties mit Dark/Light Mode
- **API:** Node.js Backend mit Astro API Routes - **API:** Node.js Backend mit Astro API Routes
- **Datenbank:** YAML-basierte Konfiguration (tools.yaml) - **Datenbank:** YAML-basierte Konfiguration (tools.yaml)
@ -117,8 +117,8 @@ sudo systemctl enable nginx
```bash ```bash
# Klonen des Repositorys # Klonen des Repositorys
sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /var/www/cc24-hub sudo git clone https://git.cc24.dev/mstoeck3/cc24-hub /opt/cc24-hub
cd /var/www/cc24-hub cd /opt/cc24-hub
# Abhängigkeiten installieren # Abhängigkeiten installieren
sudo npm install sudo npm install
@ -127,12 +127,12 @@ sudo npm install
sudo npm run build sudo npm run build
# Berechtigungen setzen # 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 #### 3. Umgebungsvariablen konfigurieren
Erstelle `/var/www/cc24-hub/.env`: Erstelle `/opt/cc24-hub/.env`:
```bash ```bash
# === GRUNDKONFIGURATION === # === GRUNDKONFIGURATION ===
@ -166,13 +166,13 @@ GIT_API_TOKEN=your_git_api_token
GIT_REPO_URL=https://ihr-git-server.de/user/cc24-hub GIT_REPO_URL=https://ihr-git-server.de/user/cc24-hub
# === UPLOAD-KONFIGURATION === # === UPLOAD-KONFIGURATION ===
LOCAL_UPLOAD_PATH=/var/www/cc24-hub/public/uploads LOCAL_UPLOAD_PATH=/opt/cc24-hub/public/uploads
``` ```
```bash ```bash
# Berechtigungen sichern # Berechtigungen sichern
sudo chmod 600 /var/www/cc24-hub/.env sudo chmod 600 /opt/cc24-hub/.env
sudo chown www-data:www-data /var/www/cc24-hub/.env sudo chown www-data:www-data /opt/cc24-hub/.env
``` ```
#### 4. Nginx konfigurieren #### 4. Nginx konfigurieren
@ -205,7 +205,7 @@ server {
# Static Files # Static Files
location / { location / {
try_files $uri $uri/ @nodejs; try_files $uri $uri/ @nodejs;
root /var/www/cc24-hub/dist; root /opt/cc24-hub/dist;
index index.html; index index.html;
# Cache static assets # Cache static assets
@ -256,7 +256,7 @@ Wants=nginx.service
Type=exec Type=exec
User=www-data User=www-data
Group=www-data Group=www-data
WorkingDirectory=/var/www/cc24-hub WorkingDirectory=/opt/cc24-hub
Environment=NODE_ENV=production Environment=NODE_ENV=production
ExecStart=/usr/bin/node ./dist/server/entry.mjs ExecStart=/usr/bin/node ./dist/server/entry.mjs
Restart=always Restart=always
@ -269,7 +269,7 @@ NoNewPrivileges=yes
PrivateTmp=yes PrivateTmp=yes
ProtectSystem=strict ProtectSystem=strict
ProtectHome=yes ProtectHome=yes
ReadWritePaths=/var/www/cc24-hub ReadWritePaths=/opt/cc24-hub
CapabilityBoundingSet= CapabilityBoundingSet=
# Resource Limits # Resource Limits
@ -431,7 +431,7 @@ domain-agnostic-software:
```bash ```bash
# Repository aktualisieren # Repository aktualisieren
cd /var/www/cc24-hub cd /opt/cc24-hub
sudo git pull sudo git pull
# Dependencies aktualisieren # Dependencies aktualisieren
@ -449,8 +449,8 @@ sudo systemctl restart cc24-hub
Wichtige Dateien für Backup: Wichtige Dateien für Backup:
```bash ```bash
/var/www/cc24-hub/src/data/tools.yaml /opt/cc24-hub/src/data/tools.yaml
/var/www/cc24-hub/.env /opt/cc24-hub/.env
/etc/nginx/sites-available/cc24-hub /etc/nginx/sites-available/cc24-hub
/etc/systemd/system/cc24-hub.service /etc/systemd/system/cc24-hub.service
``` ```
@ -472,4 +472,5 @@ Bei Problemen oder Fragen:
- **Dokumentation:** Siehe `/knowledgebase` auf der Website - **Dokumentation:** Siehe `/knowledgebase` auf der Website
## 📄 Lizenz ## 📄 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', type: 'content',
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
tool_name: z.string(),
description: z.string(), description: z.string(),
last_updated: z.date(), 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([]), categories: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]), tags: z.array(z.string()).default([]),
sections: z.object({
overview: z.boolean().default(true), published: 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'),
}) })
}); });
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" 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." 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 last_updated: 2025-07-20
author: "CC24-Team" author: "Claude 4 Sonnet"
difficulty: "intermediate" difficulty: "intermediate"
categories: ["incident-response", "forensics", "penetration-testing"] categories: ["incident-response", "forensics", "penetration-testing"]
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"] 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" tool_name: "MISP"
description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit." description: "Das Rückgrat des modernen Threat-Intelligence-Sharings mit über 40.000 aktiven Instanzen weltweit."
last_updated: 2025-07-20 last_updated: 2025-07-20
author: "CC24-Team" author: "Claude 4 Sonnet"
difficulty: "intermediate" difficulty: "intermediate"
categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"] categories: ["incident-response", "static-investigations", "malware-analysis", "network-forensics", "cloud-forensics"]
tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"] tags: ["web-based", "threat-intelligence", "api", "correlation", "ioc-sharing", "automation"]

View File

@ -3,7 +3,7 @@ title: "Nextcloud - Sichere Kollaborationsplattform"
tool_name: "Nextcloud" tool_name: "Nextcloud"
description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien" description: "Detaillierte Anleitung und Best Practices für Nextcloud in forensischen Einsatzszenarien"
last_updated: 2025-07-20 last_updated: 2025-07-20
author: "CC24-Team" author: "Claude 4 Sonnet"
difficulty: "novice" difficulty: "novice"
categories: ["collaboration-general"] categories: ["collaboration-general"]
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"] 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)" tool_name: "Regular Expressions (Regex)"
description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen." description: "Pattern matching language für Suche, Extraktion und Manipulation von Text in forensischen Analysen."
last_updated: 2025-07-20 last_updated: 2025-07-20
author: "CC24-Team" author: "Claude 4 Sonnet"
difficulty: "intermediate" difficulty: "intermediate"
categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"] categories: ["incident-response", "malware-analysis", "network-forensics", "fraud-investigation"]
tags: ["pattern-matching", "text-processing", "log-analysis", "string-manipulation", "search-algorithms"] 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" tool_name: "Velociraptor"
description: "Detaillierte Anleitung und Best Practices für Velociraptor Remote-Forensik der nächsten Generation" description: "Detaillierte Anleitung und Best Practices für Velociraptor Remote-Forensik der nächsten Generation"
last_updated: 2025-07-20 last_updated: 2025-07-20
author: "CC24-Team" author: "Claude 4 Sonnet"
difficulty: "advanced" difficulty: "advanced"
categories: ["incident-response", "malware-analysis", "network-forensics"] categories: ["incident-response", "malware-analysis", "network-forensics"]
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"] tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]

View File

@ -1,16 +1,51 @@
--- ---
import BaseLayout from '../layouts/BaseLayout.astro'; import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
import { getToolsData } from '../utils/dataService.js'; import { getToolsData } from '../utils/dataService.js';
import ContributionButton from '../components/ContributionButton.astro'; import ContributionButton from '../components/ContributionButton.astro';
// Load tools data // Load tools data and knowledgebase articles
const data = await getToolsData(); 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 // Create unified knowledgebase entries with optional tool association
const knowledgebaseTools = data.tools.filter((tool: any) => tool.knowledgebase === true); const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
const associatedTool = entry.data.tool_name
? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
: null;
// Sort alphabetically by name return {
knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name)); // 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 title
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
--- ---
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools"> <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> </div>
<!-- Tools Count --> <!-- Articles Count -->
<div style="text-align: center; margin-bottom: 2rem;"> <div style="text-align: center; margin-bottom: 2rem;">
<p class="text-muted" style="font-size: 0.875rem;"> <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> </p>
</div> </div>
<!-- Knowledgebase Entries --> <!-- Knowledgebase Entries -->
<div style="max-width: 1000px; margin: 0 auto;"> <div style="max-width: 1000px; margin: 0 auto;">
{knowledgebaseTools.length === 0 ? ( {knowledgebaseEntries.length === 0 ? (
<div class="card" style="text-align: center; padding: 3rem;"> <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;"> <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"/> <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> </svg>
<h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3> <h3 style="color: var(--color-text-secondary); margin-bottom: 0.5rem;">Noch keine Knowledgebase-Einträge</h3>
<p class="text-muted"> <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> </p>
</div> </div>
) : ( ) : (
<div id="kb-entries"> <div id="kb-entries">
{knowledgebaseTools.map((tool: any, index: number) => { {knowledgebaseEntries.map((entry: any, index: number) => {
const hasValidProjectUrl = tool.projectUrl !== undefined && const hasAssociatedTool = !!entry.associatedTool;
tool.projectUrl !== null && const hasValidProjectUrl = hasAssociatedTool &&
tool.projectUrl !== "" && entry.associatedTool.projectUrl !== undefined &&
tool.projectUrl.trim() !== ""; entry.associatedTool.projectUrl !== null &&
entry.associatedTool.projectUrl !== "" &&
entry.associatedTool.projectUrl.trim() !== "";
const toolSlug = tool.name.toLowerCase() const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
.replace(/\s+/g, '-') // Replace spaces with hyphens const isStandalone = !hasAssociatedTool;
.replace(/-+/g, '-') // Remove duplicate hyphens
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
return ( return (
<article <article
class="kb-entry card" class="kb-entry card"
id={`kb-${toolSlug}`} id={`kb-${entry.slug}`}
data-tool-name={tool.name.toLowerCase()} 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; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 1rem;"> <div style="display: flex; align-items: center; gap: 1rem;">
<h3 style="margin: 0; color: var(--color-primary);"> <h3 style="margin: 0; color: var(--color-primary);">
{tool.icon && <span style="margin-right: 0.5rem;">{tool.icon}</span>} <span style="margin-right: 0.5rem;">{entry.icon}</span>
{tool.name} {entry.title}
</h3> </h3>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<!-- Type indicator badges --> <!-- Type indicator badges -->
{tool.type === 'concept' && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>} {isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
{tool.type === 'method' && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>} {isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>}
{tool.type === 'software' && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</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>} {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 --> <!-- Difficulty indicator -->
<span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;"> {entry.difficulty && (
{tool.skillLevel || 'intermediate'} <span class="badge" style="background-color: var(--color-text-secondary); color: white; font-size: 0.75rem;">
</span> {entry.difficulty}
</span>
)}
<!-- Knowledge Base indicator -->
<span class="badge badge-error">📖</span>
</div> </div>
</div> </div>
<!-- Action buttons --> <!-- Action buttons -->
<div style="display: flex; gap: 0.5rem; align-items: center; flex-shrink: 0;"> <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;"> <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"/> <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"/> <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 Artikel öffnen
</a> </a>
<!-- NEW: Edit button for existing knowledgebase articles --> <!-- Edit button for knowledgebase articles -->
<ContributionButton type="edit" toolName={tool.name} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" /> <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>
</div> </div>
<!-- Rest of existing article content remains unchanged -->
<!-- Description --> <!-- Description -->
<p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;"> <p style="margin: 1rem 0; color: var(--color-text-secondary); line-height: 1.5;">
{tool.description} {entry.description}
</p> </p>
<!-- Tags and Metadata --> <!-- Metadata and Tags -->
<div style="display: flex; gap: 1rem; flex-wrap: wrap; align-items: center; margin-top: 1rem;"> <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;"> <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> <span class="tag" style="font-size: 0.75rem;">{tag}</span>
))} ))}
</div> </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);"> <div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Phasen:</strong> {tool.phases.join(', ')} <strong>Kategorien:</strong> {entry.categories.join(', ')}
</div> </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);"> <div style="font-size: 0.8125rem; color: var(--color-text-secondary);">
<strong>Plattformen:</strong> {tool.platforms.join(', ')} <strong>Phasen:</strong> {entry.phases.join(', ')}
</div> </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> </div>
</article> </article>
); );
@ -170,17 +232,18 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
<p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p> <p class="text-muted">Versuchen Sie es mit anderen Suchbegriffen.</p>
</div> </div>
</section> </section>
<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>
</BaseLayout>
<!-- 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>
</BaseLayout>
<script> <script>
// Enhanced knowledgebase functionality with search // Enhanced knowledgebase functionality with search
@ -228,5 +291,22 @@ knowledgebaseTools.sort((a: any, b: any) => a.name.localeCompare(b.name));
filterEntries(target.value); 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> </script>

View File

@ -26,20 +26,35 @@ const { Content } = await entry.render();
// Load tools data to get the tool details // Load tools data to get the tool details
const data = await getToolsData(); const data = await getToolsData();
const tool = data.tools.find((t: any) => t.name === entry.data.tool_name);
if (!tool) { // UPGRADED: Handle optional tool association
console.warn(`Tool not found for knowledgebase entry: ${entry.data.tool_name}`); const primaryTool = entry.data.tool_name
return Astro.redirect('/knowledgebase'); ? 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 // Determine styling based on tool type or fallback to generic
const isMethod = tool.type === 'method'; const isMethod = displayTool?.type === 'method';
const isConcept = tool.type === 'concept'; const isConcept = displayTool?.type === 'concept';
const hasValidProjectUrl = tool.projectUrl !== undefined && const isStandalone = !displayTool;
tool.projectUrl !== null && const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &&
tool.projectUrl !== "" && displayTool.projectUrl !== null &&
tool.projectUrl.trim() !== ""; displayTool.projectUrl !== "" &&
displayTool.projectUrl.trim() !== "";
--- ---
<BaseLayout title={entry.data.title} description={entry.data.description}> <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="display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem;">
<div style="flex: 1;"> <div style="flex: 1;">
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);"> <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} {entry.data.title}
</h1> </h1>
<p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;"> <p style="margin: 0; color: var(--color-text-secondary); font-size: 1.125rem;">
@ -58,32 +73,59 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
</div> </div>
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;"> <div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;"> <div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
{isConcept && <span class="badge" style="background-color: var(--color-concept); color: white;">Konzept</span>} <!-- UPGRADED: Conditional badges based on tool type or standalone -->
{isMethod && <span class="badge" style="background-color: var(--color-method); color: white;">Methode</span>} {isStandalone ? (
{!isMethod && !isConcept && <span class="badge" style="background-color: var(--color-primary); color: white;">Software</span>} <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</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>} <>
{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> <span class="badge badge-error">📖</span>
</div> </div>
</div> </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 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> <!-- Difficulty (always shown if present) -->
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Schwierigkeit</strong> {entry.data.difficulty && (
<p style="margin: 0; font-size: 0.9375rem;">{entry.data.difficulty}</p> <div>
</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> <div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Letztes Update</strong> <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> <p style="margin: 0; font-size: 0.9375rem;">{entry.data.last_updated.toLocaleDateString('de-DE')}</p>
</div> </div>
<!-- Author (always shown) -->
<div> <div>
<strong style="font-size: 0.875rem; color: var(--color-text-secondary);">Autor</strong> <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> <p style="margin: 0; font-size: 0.9375rem;">{entry.data.author}</p>
</div> </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 && ( {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> <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;"> <div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
{entry.data.categories.map((cat: string) => ( {entry.data.categories.map((cat: string) => (
@ -105,57 +147,107 @@ const hasValidProjectUrl = tool.projectUrl !== undefined &&
</a> </a>
</nav> </nav>
<!-- Content --> <!-- Content -->
<div class="card" style="padding: 2rem;"> <div class="card" style="padding: 2rem;">
<div class="kb-content markdown-content" style="line-height: 1.7;"> <div class="kb-content markdown-content" style="line-height: 1.7;">
<Content /> <Content />
</div>
</div> </div>
</div>
<!-- Tool Actions --> <!-- UPGRADED: Flexible Tool Actions Section -->
<div class="card" style="margin-top: 2rem; background-color: var(--color-bg-secondary);"> <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;"> <div style="display: flex; gap: 1rem; flex-wrap: wrap;">
{isConcept ? ( {isStandalone ? (
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);"> <!-- 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;"> <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"/> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="15 3 21 3 21 9"/> <polyline points="14 2 14 8 20 8"/>
<line x1="10" y1="14" x2="21" y2="3"/> <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> </svg>
Mehr erfahren Weitere Artikel
</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
</a> </a>
) : ( ) : (
<!-- UPGRADED: Tool-specific actions (existing logic) -->
<> <>
<a href={tool.url} target="_blank" rel="noopener noreferrer" class="btn btn-secondary"> {isConcept ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;"> <a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-primary" style="background-color: var(--color-concept); border-color: var(--color-concept);">
<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">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;"> <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="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
<path d="M12 16l4-4-4-4"/> <polyline points="15 3 21 3 21 9"/>
<path d="M8 12h8"/> <line x1="10" y1="14" x2="21" y2="3"/>
</svg> </svg>
Zugreifen Mehr erfahren
</a> </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"> <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;"> <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"/> <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>