Compare commits

..

133 Commits

Author SHA1 Message Date
overcuriousity
bdee77f459 update color palette, dependencies, fix in tools.yaml 2025-09-10 10:37:14 +02:00
8a6d9d3324 src/data/tools.yaml aktualisiert 2025-09-08 10:22:44 +00:00
overcuriousity
dc9f52fb7c cleanup, prompt centralization 2025-08-29 14:50:11 +02:00
overcuriousity
b17458d153 enhance prompts 2025-08-29 12:53:36 +02:00
overcuriousity
b14ca1d243 fix tool mode ai pipiline logic 2025-08-29 12:27:15 +02:00
overcuriousity
4ee1cc4984 replace nwc 2025-08-23 22:23:33 +02:00
overcuriousity
bbe1b12251 lightning 2025-08-23 11:06:57 +02:00
overcuriousity
d569b74a20 revert 2025-08-23 01:23:52 +02:00
overcuriousity
a2d3d3170a package.json 2025-08-23 01:15:22 +02:00
overcuriousity
3823407d49 fix lightning 2025-08-23 01:03:57 +02:00
overcuriousity
496f2a5b43 fix lightning 2025-08-23 00:43:23 +02:00
overcuriousity
20a4c71d02 lighning tips 2025-08-23 00:30:24 +02:00
overcuriousity
dad5e5ea0c embeddings fix 2025-08-18 01:18:40 +02:00
overcuriousity
b689f24502 fix embeddings enabled 2025-08-18 01:07:46 +02:00
overcuriousity
630fc1643e enabled embeddings by default 2025-08-18 01:03:45 +02:00
overcuriousity
1d750307c4 .env.example 2025-08-18 01:00:41 +02:00
05d957324a Merge pull request 'airefactor' (#19) from airefactor into main
Reviewed-on: #19
2025-08-17 22:59:30 +00:00
overcuriousity
6160620e24 cleanup 2025-08-18 00:57:57 +02:00
overcuriousity
1d91dbf478 audit trail collapsed by default 2025-08-18 00:50:16 +02:00
overcuriousity
76694e003c attempt fix layout 2025-08-18 00:34:29 +02:00
overcuriousity
28af56d6ef fix audit trail 2025-08-18 00:08:57 +02:00
overcuriousity
3d5d2506e9 fix false truncation 2025-08-17 23:45:28 +02:00
overcuriousity
6b09eb062f add switching logic 2025-08-17 23:25:23 +02:00
overcuriousity
70fb012d63 fulldata 2025-08-17 23:18:15 +02:00
overcuriousity
2cb25d1dd6 remove some env vars 2025-08-17 18:17:33 +02:00
overcuriousity
bcd92af8a0 cleanup 2025-08-17 17:27:08 +02:00
overcuriousity
5ecbabea90 some cleanup 2025-08-17 17:20:54 +02:00
overcuriousity
07c8f707df audit trail detail, dupes detector 2025-08-17 16:55:02 +02:00
overcuriousity
e63ec367a5 audit trail detail 2025-08-17 16:30:58 +02:00
overcuriousity
5c3c308225 audit trail details 2025-08-17 15:45:40 +02:00
overcuriousity
dd26d45a21 layout fixes 2025-08-17 12:09:40 +02:00
overcuriousity
afbd8d2cd3 restore old after-confidence-scoring 2025-08-17 11:45:53 +02:00
overcuriousity
8bba0eefa9 unify styles 2025-08-17 11:11:26 +02:00
overcuriousity
170638a5fa update audit trail detail level 2025-08-17 10:52:48 +02:00
overcuriousity
c60730b4aa add back download btn 2025-08-17 00:01:30 +02:00
overcuriousity
b9964685f9 bugfix 2025-08-16 23:52:59 +02:00
overcuriousity
5d72549bb7 cleanup 2025-08-16 23:35:14 +02:00
overcuriousity
15d302031e improvements & cleanup 2025-08-16 23:27:55 +02:00
overcuriousity
48209c4639 finalize phase 3 2025-08-16 22:32:23 +02:00
overcuriousity
6d08dbdcd0 phase2 2025-08-16 22:08:02 +02:00
overcuriousity
77f09ed399 phase 2 2025-08-16 22:03:40 +02:00
overcuriousity
0c7c502b03 first iteration - buggy 2025-08-16 18:15:20 +02:00
overcuriousity
1d98dd3257 cleanup 2025-08-16 17:11:03 +02:00
overcuriousity
3ad0d8120a content 2025-08-15 22:59:12 +02:00
overcuriousity
88cf682790 small changes 2025-08-15 22:43:40 +02:00
overcuriousity
182b9d01f9 remove content 2025-08-14 23:01:03 +02:00
overcuriousity
12368ed7c8 content 2025-08-14 22:56:15 +02:00
overcuriousity
c4c52f6064 cleanup 2025-08-13 15:27:14 +02:00
overcuriousity
e93f394263 update readme files 2025-08-13 15:10:20 +02:00
overcuriousity
75410e2b84 Merge branch 'main' of https://git.cc24.dev/mstoeck3/forensic-pathways 2025-08-13 14:04:20 +02:00
overcuriousity
88e79d7780 update video embed 2025-08-13 14:04:08 +02:00
8283b71b8c src/content/README.md aktualisiert 2025-08-13 11:24:59 +00:00
overcuriousity
b630668897 adjust .gitignore 2025-08-13 13:15:56 +02:00
overcuriousity
479075e485 restrukturierung 2025-08-13 13:14:34 +02:00
overcuriousity
b6b3dfce8d remove content 2025-08-13 13:00:05 +02:00
overcuriousity
9c2e43af22 typo 2025-08-13 12:56:53 +02:00
overcuriousity
6656c28ae0 restructure fs preps 2025-08-13 12:45:46 +02:00
overcuriousity
6e9b7b4ea1 content removal README 2025-08-13 12:44:21 +02:00
overcuriousity
be76f2be5a attempt fix 2025-08-12 23:03:31 +02:00
4fd257cbd6 Merge pull request 'videos' (#17) from videos into main
Reviewed-on: #17
2025-08-12 20:35:05 +00:00
overcuriousity
d1c297189d cleanup 2025-08-12 22:34:11 +02:00
overcuriousity
e8daa37d08 fix auth 2025-08-12 22:26:24 +02:00
overcuriousity
27b94edcfa simplify video stuff 2025-08-12 22:13:14 +02:00
overcuriousity
b291492e2d video implementation 2025-08-12 21:02:52 +02:00
overcuriousity
0e3d654a58 progress 2025-08-12 16:28:05 +02:00
overcuriousity
2d920391ad videos 2025-08-12 16:06:29 +02:00
overcuriousity
f159f904f0 first draft videos 2025-08-12 14:53:11 +02:00
overcuriousity
d6760d0f84 helpful prompts 2025-08-12 08:45:33 +02:00
overcuriousity
a3736c9dbd Tool-Aktionen 2025-08-11 23:09:16 +02:00
overcuriousity
653e8d03de revert process.ts 2025-08-11 22:58:55 +02:00
overcuriousity
a52c0781e1 gatedcontent 2025-08-11 22:55:11 +02:00
overcuriousity
d49b031eb9 gated content 2025-08-11 22:00:49 +02:00
overcuriousity
2f17370938 enhance prompt 2025-08-11 16:21:12 +02:00
overcuriousity
6918df9348 fix share btn 2025-08-11 13:55:18 +02:00
overcuriousity
20682ef682 update knowledgebase dates 2025-08-11 10:42:42 +02:00
overcuriousity
d043bba17f phase completion justification 2025-08-11 08:57:16 +02:00
overcuriousity
9e42b2a98d contrib form fix 2025-08-10 23:06:59 +02:00
overcuriousity
4cc3e2c830 fix contrib mechanic 2025-08-10 23:00:01 +02:00
overcuriousity
2fcc84991a fix stylesheet 2025-08-10 22:15:12 +02:00
overcuriousity
e6cee2ab0e knowledgebase content 2025-08-10 21:50:47 +02:00
overcuriousity
a816c0630f knowledgebase style 2025-08-10 19:57:59 +02:00
overcuriousity
9a3122745d knowledgebase share button 2025-08-10 18:49:45 +02:00
overcuriousity
9ce2098439 deduplication 2025-08-10 16:30:06 +02:00
overcuriousity
050774ad99 update editor 2025-08-10 16:20:29 +02:00
overcuriousity
f09f519f46 small fix 2025-08-10 16:07:32 +02:00
overcuriousity
df6bda30b1 fix the matrix 2025-08-10 00:19:08 +02:00
overcuriousity
b1c31379b2 cleanup 2025-08-09 22:48:29 +02:00
overcuriousity
b8311e152d content update linux forensics 2025-08-09 22:45:33 +02:00
overcuriousity
4d423eb403 windows methods content update 2025-08-09 22:08:54 +02:00
overcuriousity
d9636578bd content update 2025-08-09 21:24:00 +02:00
overcuriousity
562f3a08f1 content quality 2025-08-09 17:50:38 +02:00
overcuriousity
5b9bd4525a content update 2025-08-09 15:47:05 +02:00
6e9f37c1cd Merge pull request 'upload-issue' (#9) from upload-issue into main
Reviewed-on: #9
2025-08-09 13:23:23 +00:00
overcuriousity
7607a73373 simplify 2025-08-09 15:22:11 +02:00
overcuriousity
3f9d1860aa uploads fix 2025-08-09 15:02:20 +02:00
overcuriousity
daa468c535 uploadFile 2025-08-09 10:42:59 +02:00
overcuriousity
1d10bfca2c more error handling and logging in uploads mechanic 2025-08-09 10:30:11 +02:00
overcuriousity
8aa9a9b082 mistral api disclaimer 2025-08-09 00:03:39 +02:00
overcuriousity
87b04cffb4 fix pipeline 2025-08-08 23:39:29 +02:00
overcuriousity
3c6fb568d6 fix pipeline 2025-08-08 22:54:47 +02:00
overcuriousity
138a494730 pipeline overhaul 2025-08-08 22:45:21 +02:00
overcuriousity
d5a6fe7dec revert prompt 2025-08-08 14:27:04 +02:00
overcuriousity
20b3bd44ca prompts 2025-08-08 13:54:10 +02:00
overcuriousity
93783dde3d prompts-update 2025-08-08 13:05:40 +02:00
overcuriousity
2aa741ed09 content updates, yaml editor 2025-08-08 10:57:00 +02:00
overcuriousity
0f780e3ce2 content update, macOS methods 2025-08-08 09:44:11 +02:00
overcuriousity
987f737122 content updates 2025-08-07 23:10:14 +02:00
overcuriousity
824d98b3f4 script 2025-08-07 15:59:32 +02:00
overcuriousity
56f3840fd7 script 2025-08-07 15:58:15 +02:00
overcuriousity
27021ab499 script 2025-08-07 15:54:35 +02:00
overcuriousity
f3e2480182 script 2025-08-07 15:52:48 +02:00
overcuriousity
4f879fa1f5 script 2025-08-07 15:50:18 +02:00
overcuriousity
def89822f6 script 2025-08-07 15:47:56 +02:00
overcuriousity
ad7dd5bc70 script 2025-08-07 15:45:52 +02:00
overcuriousity
ee21ce225e script 2025-08-07 15:43:53 +02:00
overcuriousity
b28f9b9213 fix ai auth, deploy script visual orgasm 2025-08-07 15:41:42 +02:00
overcuriousity
8516a39fcb auth variable consolidation 2025-08-07 15:20:00 +02:00
overcuriousity
6f065a6e3b fix env detection 2025-08-07 15:10:28 +02:00
overcuriousity
cc8343776d dotenv 2025-08-07 11:27:03 +02:00
overcuriousity
f9ec247d43 .env 2025-08-07 11:15:14 +02:00
overcuriousity
1beefb93bb script 2025-08-07 10:39:51 +02:00
overcuriousity
fd721ce930 script 2025-08-07 10:10:59 +02:00
overcuriousity
95ce48192b script 2025-08-07 10:06:39 +02:00
overcuriousity
73ace5965f script 2025-08-07 10:05:01 +02:00
overcuriousity
12d3b53fe2 script 2025-08-07 09:54:59 +02:00
overcuriousity
1ff437b7e6 script 2025-08-07 09:52:22 +02:00
overcuriousity
9d6ba83378 script 2025-08-07 09:50:26 +02:00
overcuriousity
ec0ee12ae5 script 2025-08-07 09:44:44 +02:00
overcuriousity
dff78cbe0a script 2025-08-07 09:42:42 +02:00
358a30072e Merge pull request 'semanticsearch' (#6) from semanticsearch into main
Reviewed-on: #6
2025-08-07 07:36:17 +00:00
overcuriousity
5795f3269f README, deploy script 2025-08-07 09:32:24 +02:00
overcuriousity
5d05c62a55 fix 2025-08-07 09:13:04 +02:00
overcuriousity
7f5fdef445 fixes & consolidation 2025-08-07 08:55:29 +02:00
75 changed files with 213905 additions and 121135 deletions

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

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -1,257 +1,200 @@
# ============================================================================
# ForensicPathways Environment Configuration - COMPLETE
# ForensicPathways Environment Configuration
# ============================================================================
# Copy this file to .env and adjust the values below.
# This file covers ALL environment variables used in the codebase.
# Copy this file to .env and configure the REQUIRED values below.
# Optional features can be enabled by uncommenting and configuring them.
# ============================================================================
# 1. CORE APPLICATION SETTINGS (REQUIRED)
# 🔥 CRITICAL - REQUIRED FOR BASIC OPERATION
# ============================================================================
# Your application's public URL (used for redirects and links)
PUBLIC_BASE_URL=http://localhost:4321
# Secret key for session encryption (GENERATE A SECURE RANDOM STRING!)
AUTH_SECRET=your-secret-key-change-in-production-please
# Primary AI service for query processing (REQUIRED for core functionality)
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
AI_ANALYZER_API_KEY=your-ai-api-key-here
AI_ANALYZER_MODEL=mistral/mistral-small-latest
# ============================================================================
# ⚙️ IMPORTANT - CORE FEATURES CONFIGURATION
# ============================================================================
# Application environment
NODE_ENV=development
# Secret key for session encryption (CHANGE IN PRODUCTION!)
AUTH_SECRET=your-secret-key-change-in-production-please
# ============================================================================
# 2. AI SERVICES CONFIGURATION (REQUIRED FOR AI FEATURES)
# ============================================================================
# Main AI Analysis Service (for query processing and recommendations)
# Examples: http://localhost:11434 (Ollama), https://api.mistral.ai, https://api.openai.com
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
AI_ANALYZER_API_KEY=
AI_ANALYZER_MODEL=mistral/mistral-small-latest
# Vector Embeddings Service (for semantic search)
# Leave API_KEY empty for Ollama, use actual key for cloud services
AI_EMBEDDINGS_ENABLED=true
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
AI_EMBEDDINGS_API_KEY=
AI_EMBEDDINGS_MODEL=mistral-embed
# ============================================================================
# 3. AI PIPELINE CONFIGURATION (CONTEXT & PERFORMANCE TUNING)
# ============================================================================
# === SIMILARITY SEARCH STAGE ===
# How many similar tools/concepts embeddings search returns as candidates
# 🔍 This is the FIRST filter - vector similarity matching
# Lower = faster, less comprehensive | Higher = slower, more comprehensive
AI_EMBEDDING_CANDIDATES=50
# Minimum similarity score threshold (0.0-1.0)
# Lower = more results but less relevant | Higher = fewer but more relevant
AI_SIMILARITY_THRESHOLD=0.3
# === AI SELECTION FROM EMBEDDINGS ===
# When embeddings are enabled, how many top tools to send with full context
# 🎯 This is the SECOND filter - take best N from embeddings results
AI_EMBEDDING_SELECTION_LIMIT=30
AI_EMBEDDING_CONCEPTS_LIMIT=15
# Maximum tools/concepts sent to AI when embeddings are DISABLED
# Set to 0 for no limit (WARNING: may cause token overflow with large datasets)
AI_NO_EMBEDDINGS_TOOL_LIMIT=0
AI_NO_EMBEDDINGS_CONCEPT_LIMIT=0
# === AI SELECTION STAGE ===
# Maximum tools the AI can select from embedding candidates
# 🤖 This is the SECOND filter - AI intelligent selection
# Should be ≤ AI_EMBEDDING_CANDIDATES
AI_MAX_SELECTED_ITEMS=25
# === EMBEDDINGS EFFICIENCY THRESHOLDS ===
# Minimum tools required for embeddings to be considered useful
AI_EMBEDDINGS_MIN_TOOLS=8
# Maximum percentage of total tools that embeddings can return to be considered "filtering"
AI_EMBEDDINGS_MAX_REDUCTION_RATIO=0.75
# === CONTEXT FLOW SUMMARY ===
# 1. Vector Search: 111 total tools → AI_EMBEDDING_CANDIDATES (40) most similar
# 2. AI Selection: 40 candidates → AI_MAX_SELECTED_ITEMS (25) best matches
# 3. Final Output: Recommendations based on analyzed subset
# ============================================================================
# 4. AI PERFORMANCE & RATE LIMITING
# ============================================================================
# === USER RATE LIMITS (per minute) ===
# Main queries per user per minute
AI_RATE_LIMIT_MAX_REQUESTS=4
# Total AI micro-task calls per user per minute (across all micro-tasks)
AI_MICRO_TASK_TOTAL_LIMIT=30
# === PIPELINE TIMING ===
# Delay between micro-tasks within a single query (milliseconds)
# Higher = gentler on AI service | Lower = faster responses
AI_MICRO_TASK_DELAY_MS=500
# Delay between queued requests (milliseconds)
AI_RATE_LIMIT_DELAY_MS=2000
# === EMBEDDINGS BATCH PROCESSING ===
# How many embeddings to generate per API call
AI_EMBEDDINGS_BATCH_SIZE=10
# Delay between embedding batches (milliseconds)
AI_EMBEDDINGS_BATCH_DELAY_MS=1000
# Maximum tools sent to AI for detailed analysis (micro-tasks)
AI_MAX_TOOLS_TO_ANALYZE=20
AI_MAX_CONCEPTS_TO_ANALYZE=10
# ============================================================================
# 5. AI CONTEXT & TOKEN MANAGEMENT
# ============================================================================
# Maximum context tokens to maintain across micro-tasks
# Controls how much conversation history is preserved between AI calls
AI_MAX_CONTEXT_TOKENS=4000
# Maximum tokens per individual AI prompt
# Larger = more context per call | Smaller = faster responses
AI_MAX_PROMPT_TOKENS=2500
# ============================================================================
# 6. AUTHENTICATION & AUTHORIZATION (OPTIONAL)
# ============================================================================
# Enable authentication for different features
AUTHENTICATION_NECESSARY=false
# === AUTHENTICATION & SECURITY ===
# Set to true to require authentication (RECOMMENDED for production)
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=false
AUTHENTICATION_NECESSARY_AI=false
AUTHENTICATION_NECESSARY_GATEDCONTENT=true
# OIDC Provider Settings (only needed if authentication enabled)
OIDC_ENDPOINT=https://your-oidc-provider.com
# OIDC Provider Configuration - Server appends endpoint (e.g. auth/callback) automatically
OIDC_ENDPOINT=https://cloud.cc24.dev/index.php
OIDC_CLIENT_ID=your-client-id
OIDC_CLIENT_SECRET=your-client-secret
# ============================================================================
# 7. FILE UPLOADS - NEXTCLOUD INTEGRATION (OPTIONAL)
# ============================================================================
# === FILE HANDLING ===
# Nextcloud server for file uploads (knowledgebase contributions)
# Leave empty to disable file upload functionality
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
# Nextcloud credentials (app password recommended)
NEXTCLOUD_USERNAME=your-username
NEXTCLOUD_PASSWORD=your-app-password
# Upload directory on Nextcloud (will be created if doesn't exist)
NEXTCLOUD_UPLOAD_PATH=/kb-media
# Public URL base for sharing uploaded files
# Usually your Nextcloud base URL + share path
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
# ============================================================================
# 8. GIT CONTRIBUTIONS - ISSUE CREATION (OPTIONAL)
# ============================================================================
# === COLLABORATION & CONTRIBUTIONS ===
# Git provider: gitea, github, or gitlab
GIT_PROVIDER=gitea
# Repository URL (used to extract owner/name)
# Example: https://git.example.com/owner/forensic-pathways.git
GIT_REPO_URL=https://git.example.com/owner/forensic-pathways.git
# API endpoint for your git provider
# Gitea: https://git.example.com/api/v1
# GitHub: https://api.github.com
# GitLab: https://gitlab.example.com/api/v4
GIT_API_ENDPOINT=https://git.example.com/api/v1
# Personal access token or API token for creating issues
# Generate this in your git provider's settings
GIT_API_TOKEN=your-git-api-token
# ============================================================================
# 9. AUDIT & DEBUGGING (OPTIONAL)
# ============================================================================
# === AUDIT TRAIL (Important for forensic work) ===
FORENSIC_AUDIT_ENABLED=true
FORENSIC_AUDIT_DETAIL_LEVEL=standard
FORENSIC_AUDIT_RETENTION_HOURS=24
FORENSIC_AUDIT_MAX_ENTRIES=50
# Enable detailed audit trail of AI decision-making
PUBLIC_FORENSIC_AUDIT_ENABLED=true
# === AI SEMANTIC SEARCH ===
# semantic search
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
AI_EMBEDDINGS_API_KEY=your-embeddings-api-key-here
AI_EMBEDDINGS_MODEL=mistral-embed
# Audit detail level: minimal, standard, verbose
PUBLIC_FORENSIC_AUDIT_DETAIL_LEVEL=standard
# Audit retention time (hours)
PUBLIC_FORENSIC_AUDIT_RETENTION_HOURS=24
# Maximum audit entries per request
PUBLIC_FORENSIC_AUDIT_MAX_ENTRIES=50
# User rate limiting (queries per minute)
AI_RATE_LIMIT_MAX_REQUESTS=4
# ============================================================================
# 10. SIMPLIFIED CONFIDENCE SCORING SYSTEM
# CACHING BEHAVIOR
# ============================================================================
# - Videos downloaded once, cached permanently
# - No time-based expiration
# - Dramatically improves loading times after first download
# - Emergency cleanup only when approaching disk space limit
# - Perfect for manually curated forensics training content
# ============================================================================
# Confidence component weights (must sum to 1.0)
CONFIDENCE_SEMANTIC_WEIGHT=0.5 # Weight for vector similarity quality
CONFIDENCE_SUITABILITY_WEIGHT=0.5 # Weight for AI-determined task fitness
# Confidence thresholds (0-100)
CONFIDENCE_MINIMUM_THRESHOLD=50 # Below this = weak recommendation
CONFIDENCE_MEDIUM_THRESHOLD=70 # 40-59 = weak, 60-79 = moderate
CONFIDENCE_HIGH_THRESHOLD=80 # 80+ = strong recommendation
# ============================================================================
# PERFORMANCE TUNING PRESETS
# 🎛️ PERFORMANCE TUNING - SENSIBLE DEFAULTS PROVIDED
# ============================================================================
# 🚀 FOR FASTER RESPONSES (prevent token overflow):
# AI_NO_EMBEDDINGS_TOOL_LIMIT=25
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=10
# === AI Pipeline Configuration ===
# These values are pre-tuned for optimal performance - adjust only if needed
# Vector similarity search settings
AI_EMBEDDING_CANDIDATES=50
AI_SIMILARITY_THRESHOLD=0.3
AI_EMBEDDING_SELECTION_LIMIT=30
AI_EMBEDDING_CONCEPTS_LIMIT=15
# === METHOD/TOOL BALANCE CONFIGURATION ===
# Controls the ratio of methods vs software tools sent to AI
# Methods = procedural guidance, best practices, workflows
# Software = actual tools and applications
# Values should sum to less than 1.0 (remainder is buffer)
AI_METHOD_SELECTION_RATIO=0.4 # 40% methods (increase for more procedural guidance)
AI_SOFTWARE_SELECTION_RATIO=0.5 # 50% software tools (increase for more tool recommendations)
# AI selection limits
AI_MAX_SELECTED_ITEMS=25
# Efficiency thresholds
AI_EMBEDDINGS_MIN_TOOLS=8
AI_EMBEDDINGS_MAX_REDUCTION_RATIO=0.75
# === Rate Limiting & Timing ===
AI_MICRO_TASK_TOTAL_LIMIT=30
AI_MICRO_TASK_DELAY_MS=500
AI_RATE_LIMIT_DELAY_MS=2000
# === Embeddings Batch Processing ===
AI_EMBEDDINGS_BATCH_SIZE=10
AI_EMBEDDINGS_BATCH_DELAY_MS=1000
# === Confidence Scoring ===
CONFIDENCE_SEMANTIC_WEIGHT=0.5
CONFIDENCE_SUITABILITY_WEIGHT=0.5
CONFIDENCE_MINIMUM_THRESHOLD=50
CONFIDENCE_MEDIUM_THRESHOLD=70
CONFIDENCE_HIGH_THRESHOLD=80
# 🎯 FOR FULL DATABASE ACCESS (risk of truncation):
# AI_NO_EMBEDDINGS_TOOL_LIMIT=0
# AI_NO_EMBEDDINGS_CONCEPT_LIMIT=0
# 🔋 FOR LOW-POWER SYSTEMS:
# AI_NO_EMBEDDINGS_TOOL_LIMIT=15
# ============================================================================
# FEATURE COMBINATIONS GUIDE
# 📋 QUICK SETUP CHECKLIST
# ============================================================================
# 📝 BASIC SETUP (AI only):
# - Configure AI_ANALYZER_* and AI_EMBEDDINGS_*
# - Leave authentication, file uploads, and git disabled
# 🔐 WITH AUTHENTICATION:
# - Set AUTHENTICATION_NECESSARY_* to true
# - Configure OIDC_* settings
# 📁 WITH FILE UPLOADS:
# - Configure all NEXTCLOUD_* settings
# - Test connection before enabling in UI
# 🔄 WITH CONTRIBUTIONS:
# - Configure all GIT_* settings
# - Test API token permissions for issue creation
# 🔍 WITH FULL MONITORING:
# - Enable FORENSIC_AUDIT_ENABLED=true
# - Configure audit retention and detail level
#
# MINIMUM FOR DEVELOPMENT/TESTING:
# 1. ✅ Set PUBLIC_BASE_URL to your domain/localhost
# 2. ✅ Generate secure AUTH_SECRET (use: openssl rand -base64 32)
# 3. ✅ Configure AI_ANALYZER_ENDPOINT and API_KEY for your AI service
# 4. ✅ Test basic functionality
#
# PRODUCTION-READY DEPLOYMENT:
# 5. ✅ Enable authentication (configure AUTHENTICATION_* and OIDC_*)
# 6. ✅ Configure file handling (set NEXTCLOUD_* for uploads)
# 7. ✅ Enable collaboration (set GIT_* for contributions)
# 8. ✅ Enable audit trail (verify FORENSIC_AUDIT_ENABLED=true)
# 9. ✅ Configure embeddings for better search (AI_EMBEDDINGS_*)
# 10. ✅ Adjust rate limits based on expected usage
# ============================================================================
# SETUP CHECKLIST
# 🏃‍♂️ PERFORMANCE PRESETS - UNCOMMENT ONE IF NEEDED
# ============================================================================
# ✅ 1. Set PUBLIC_BASE_URL to your domain
# ✅ 2. Change AUTH_SECRET to a secure random string
# ✅ 3. Configure AI endpoints (Ollama: leave API_KEY empty)
# ✅ 4. Start with default AI values, tune based on performance
# ✅ 5. Enable authentication if needed (configure OIDC)
# ✅ 6. Configure Nextcloud if file uploads needed
# ✅ 7. Configure Git provider if contributions needed
# ✅ 8. Test with a simple query to verify pipeline works
# ✅ 9. Enable audit trail for transparency if desired
# ✅ 10. Tune performance settings based on usage patterns
# 🚀 SPEED OPTIMIZED (faster responses, less comprehensive):
# AI_EMBEDDING_CANDIDATES=25
# AI_MAX_SELECTED_ITEMS=15
# AI_MAX_TOOLS_TO_ANALYZE=10
# AI_MICRO_TASK_DELAY_MS=250
# 🎯 ACCURACY OPTIMIZED (slower responses, more comprehensive):
# AI_EMBEDDING_CANDIDATES=100
# AI_MAX_SELECTED_ITEMS=50
# AI_MAX_TOOLS_TO_ANALYZE=40
# AI_MICRO_TASK_DELAY_MS=1000
# 🔋 RESOURCE CONSTRAINED (for limited AI quotas):
# AI_RATE_LIMIT_MAX_REQUESTS=2
# AI_MICRO_TASK_TOTAL_LIMIT=15
# AI_MAX_TOOLS_TO_ANALYZE=10
# AI_EMBEDDINGS_ENABLED=false
# 🔬 METHOD-FOCUSED (more procedural guidance, less tools):
# AI_METHOD_SELECTION_RATIO=0.6
# AI_SOFTWARE_SELECTION_RATIO=0.3
# 🛠️ TOOL-FOCUSED (more software recommendations, less methods):
# AI_METHOD_SELECTION_RATIO=0.2
# AI_SOFTWARE_SELECTION_RATIO=0.7
# ============================================================================
# 🌐 AI SERVICE EXAMPLES
# ============================================================================
# === OLLAMA (Local) ===
# AI_ANALYZER_ENDPOINT=http://localhost:11434/v1/chat/completions
# AI_ANALYZER_API_KEY=
# AI_ANALYZER_MODEL=llama3.1:8b
# AI_EMBEDDINGS_ENDPOINT=http://localhost:11434/v1/embeddings
# AI_EMBEDDINGS_API_KEY=
# AI_EMBEDDINGS_MODEL=nomic-embed-text
# === OPENAI ===
# AI_ANALYZER_ENDPOINT=https://api.openai.com/v1/chat/completions
# AI_ANALYZER_API_KEY=sk-your-openai-key
# AI_ANALYZER_MODEL=gpt-4o-mini
# AI_EMBEDDINGS_ENDPOINT=https://api.openai.com/v1/embeddings
# AI_EMBEDDINGS_API_KEY=sk-your-openai-key
# AI_EMBEDDINGS_MODEL=text-embedding-3-small
# === MISTRAL (Default) ===
# AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
# AI_ANALYZER_API_KEY=your-mistral-key
# AI_ANALYZER_MODEL=mistral-small-latest
# AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
# AI_EMBEDDINGS_API_KEY=your-mistral-key
# AI_EMBEDDINGS_MODEL=mistral-embed

5
.gitignore vendored
View File

@@ -11,6 +11,7 @@ _site/
dist/
.astro/
.astro/*
# Environment variables
.env
@@ -85,3 +86,7 @@ temp/
.astro/data-store.json
.astro/content.d.ts
prompt.md
.astro/settings.json
src/content/knowledgebase/
public/videos

View File

@@ -1,358 +0,0 @@
# Forensic-Grade RAG Implementation Roadmap
## Context & Current State Analysis
You have access to a forensic tools recommendation system built with:
- **Embeddings-based retrieval** (src/utils/embeddings.ts)
- **Multi-stage AI pipeline** (src/utils/aiPipeline.ts)
- **Micro-task processing** for detailed analysis
- **Rate limiting and queue management** (src/utils/rateLimitedQueue.ts)
- **YAML-based tool database** (src/data/tools.yaml)
**Current Architecture**: Basic RAG (Retrieve → AI Selection → Micro-task Generation)
**Target Architecture**: Forensic-Grade RAG with transparency, objectivity, and reproducibility
## Implementation Roadmap
### PHASE 1: Configuration Externalization & AI Architecture Enhancement (Weeks 1-2)
#### 1.1 Complete Configuration Externalization
**Objective**: Remove all hard-coded values from codebase (except AI prompts)
**Tasks**:
1. **Create comprehensive configuration schema** in `src/config/`
- `forensic-scoring.yaml` - All scoring criteria, weights, thresholds
- `ai-models.yaml` - AI model configurations and routing
- `system-parameters.yaml` - Rate limits, queue settings, processing parameters
- `validation-criteria.yaml` - Expert validation rules, bias detection parameters
2. **Implement configuration loader** (`src/utils/configLoader.ts`)
- Hot-reload capability for configuration changes
- Environment-specific overrides (dev/staging/prod)
- Configuration validation and schema enforcement
- Default fallbacks for missing values
3. **Audit existing codebase** for hard-coded values:
- Search for literal numbers, strings, arrays in TypeScript files
- Extract to configuration files with meaningful names
- Ensure all thresholds (similarity scores, rate limits, token counts) are configurable
#### 1.2 Dual AI Model Architecture Implementation
**Objective**: Implement large + small model strategy for optimal cost/performance
**Tasks**:
1. **Extend environment configuration**:
```
# Strategic Analysis Model (Large, Few Tokens)
AI_STRATEGIC_ENDPOINT=
AI_STRATEGIC_API_KEY=
AI_STRATEGIC_MODEL=mistral-large-latest
AI_STRATEGIC_MAX_TOKENS=500
AI_STRATEGIC_CONTEXT_WINDOW=32000
# Content Generation Model (Small, Many Tokens)
AI_CONTENT_ENDPOINT=
AI_CONTENT_API_KEY=
AI_CONTENT_MODEL=mistral-small-latest
AI_CONTENT_MAX_TOKENS=2000
AI_CONTENT_CONTEXT_WINDOW=8000
```
2. **Create AI router** (`src/utils/aiRouter.ts`):
- Route different task types to appropriate models
- **Strategic tasks** → Large model: tool selection, bias analysis, methodology decisions
- **Content tasks** → Small model: descriptions, explanations, micro-task outputs
- Automatic fallback logic if primary model fails
- Usage tracking and cost optimization
3. **Update aiPipeline.ts**:
- Replace single `callAI()` method with task-specific methods
- Implement intelligent routing based on task complexity
- Add token estimation for optimal model selection
### PHASE 2: Evidence-Based Scoring Framework (Weeks 3-5)
#### 2.1 Forensic Scoring Engine Implementation
**Objective**: Replace subjective AI selection with objective, measurable criteria
**Tasks**:
1. **Create scoring framework** (`src/scoring/ForensicScorer.ts`):
```typescript
interface ScoringCriterion {
name: string;
weight: number;
methodology: string;
dataSources: string[];
calculator: (tool: Tool, scenario: Scenario) => Promise<CriterionScore>;
}
interface CriterionScore {
value: number; // 0-100
confidence: number; // 0-100
evidence: Evidence[];
lastUpdated: Date;
}
```
2. **Implement core scoring criteria**:
- **Court Admissibility Scorer**: Based on legal precedent database
- **Scientific Validity Scorer**: Based on peer-reviewed research citations
- **Methodology Alignment Scorer**: NIST SP 800-86 compliance assessment
- **Expert Consensus Scorer**: Practitioner survey data integration
- **Error Rate Scorer**: Known false positive/negative rates
3. **Build evidence provenance system**:
- Track source of every score component
- Maintain citation database for all claims
- Version control for scoring methodologies
- Automatic staleness detection for outdated evidence
#### 2.2 Deterministic Core Implementation
**Objective**: Ensure reproducible results for identical inputs
**Tasks**:
1. **Implement deterministic pipeline** (`src/analysis/DeterministicAnalyzer.ts`):
- Rule-based scenario classification (SCADA/Mobile/Network/etc.)
- Mathematical scoring combination (weighted averages, not AI decisions)
- Consistent tool ranking algorithms
- Reproducibility validation tests
2. **Add AI enhancement layer**:
- AI provides explanations, NOT decisions
- AI generates workflow descriptions based on deterministic selections
- AI creates contextual advice around objective tool choices
### PHASE 3: Transparency & Audit Trail System (Weeks 4-6)
#### 3.1 Complete Audit Trail Implementation
**Objective**: Track every decision with forensic-grade documentation
**Tasks**:
1. **Create audit framework** (`src/audit/AuditTrail.ts`):
```typescript
interface ForensicAuditTrail {
queryId: string;
userQuery: string;
processingSteps: AuditStep[];
finalRecommendation: RecommendationWithEvidence;
reproducibilityHash: string;
validationStatus: ValidationStatus;
}
interface AuditStep {
stepName: string;
input: any;
methodology: string;
output: any;
evidence: Evidence[];
confidence: number;
processingTime: number;
modelUsed?: string;
}
```
2. **Implement evidence citation system**:
- Automatic citation generation for all claims
- Link to source standards (NIST, ISO, RFC)
- Reference scientific papers for methodology choices
- Track expert validation contributors
3. **Build explanation generator**:
- Human-readable reasoning for every recommendation
- "Why this tool" and "Why not alternatives" explanations
- Confidence level communication
- Uncertainty quantification
#### 3.2 Bias Detection & Mitigation System
**Objective**: Actively detect and correct recommendation biases
**Tasks**:
1. **Implement bias detection** (`src/bias/BiasDetector.ts`):
- **Popularity bias**: Over-recommendation of well-known tools
- **Availability bias**: Preference for easily accessible tools
- **Recency bias**: Over-weighting of newest tools
- **Cultural bias**: Platform or methodology preferences
2. **Create mitigation strategies**:
- Automatic bias adjustment algorithms
- Diversity requirements for recommendations
- Fairness metrics across tool categories
- Bias reporting in audit trails
### PHASE 4: Expert Validation & Learning System (Weeks 6-8)
#### 4.1 Expert Review Integration
**Objective**: Enable forensic experts to validate and improve recommendations
**Tasks**:
1. **Build expert validation interface** (`src/validation/ExpertReview.ts`):
- Structured feedback collection from forensic practitioners
- Agreement/disagreement tracking with detailed reasoning
- Expert consensus building over time
- Minority opinion preservation
2. **Implement validation loop**:
- Flag recommendations requiring expert review
- Track expert validation rates and patterns
- Update scoring based on real-world feedback
- Methodology improvement based on expert input
#### 4.2 Real-World Case Learning
**Objective**: Learn from actual forensic investigations
**Tasks**:
1. **Create case study integration** (`src/learning/CaseStudyLearner.ts`):
- Anonymous case outcome tracking
- Tool effectiveness measurement in real scenarios
- Methodology success/failure analysis
- Continuous improvement based on field results
2. **Implement feedback loops**:
- Post-case recommendation validation
- Tool performance tracking in actual investigations
- Methodology refinement based on outcomes
- Success rate improvement over time
### PHASE 5: Advanced Features & Scientific Rigor (Weeks 7-10)
#### 5.1 Confidence & Uncertainty Quantification
**Objective**: Provide scientific confidence levels for all recommendations
**Tasks**:
1. **Implement uncertainty quantification** (`src/uncertainty/ConfidenceCalculator.ts`):
- Statistical confidence intervals for scores
- Uncertainty propagation through scoring pipeline
- Risk assessment for recommendation reliability
- Alternative recommendation ranking
2. **Add fallback recommendation system**:
- Multiple ranked alternatives for each recommendation
- Contingency planning for tool failures
- Risk-based recommendation portfolios
- Sensitivity analysis for critical decisions
#### 5.2 Reproducibility Testing Framework
**Objective**: Ensure consistent results across time and implementations
**Tasks**:
1. **Build reproducibility testing** (`src/testing/ReproducibilityTester.ts`):
- Automated consistency validation
- Inter-rater reliability testing
- Cross-temporal stability analysis
- Version control for methodology changes
2. **Implement quality assurance**:
- Continuous integration for reproducibility
- Regression testing for methodology changes
- Performance monitoring for consistency
- Alert system for unexpected variations
### PHASE 6: Integration & Production Readiness (Weeks 9-12)
#### 6.1 System Integration
**Objective**: Integrate all forensic-grade components seamlessly
**Tasks**:
1. **Update existing components**:
- Modify `aiPipeline.ts` to use new scoring framework
- Update `embeddings.ts` with evidence tracking
- Enhance `rateLimitedQueue.ts` with audit capabilities
- Refactor `query.ts` API to return audit trails
2. **Performance optimization**:
- Caching strategies for expensive evidence lookups
- Parallel processing for scoring criteria
- Efficient storage for audit trails
- Load balancing for dual AI models
#### 6.2 Production Features
**Objective**: Make system ready for professional forensic use
**Tasks**:
1. **Add professional features**:
- Export recommendations to forensic report formats
- Integration with existing forensic workflows
- Batch processing for multiple scenarios
- API endpoints for external tool integration
2. **Implement monitoring & maintenance**:
- Health checks for all system components
- Performance monitoring for response times
- Error tracking and alerting
- Automatic system updates for new evidence
## Technical Implementation Guidelines
### Configuration Management
- Use YAML files for human-readable configuration
- Implement JSON Schema validation for all config files
- Support environment variable overrides
- Hot-reload for development, restart for production changes
### AI Model Routing Strategy
```typescript
// Task Classification for Model Selection
const AI_TASK_ROUTING = {
strategic: ['tool-selection', 'bias-analysis', 'methodology-decisions'],
content: ['descriptions', 'explanations', 'micro-tasks', 'workflows']
};
// Cost Optimization Logic
if (taskComplexity === 'high' && responseTokens < 500) {
useModel = 'large';
} else if (taskComplexity === 'low' && responseTokens > 1000) {
useModel = 'small';
} else {
useModel = config.defaultModel;
}
```
### Evidence Database Structure
```typescript
interface EvidenceSource {
type: 'standard' | 'paper' | 'case-law' | 'expert-survey';
citation: string;
reliability: number;
lastValidated: Date;
content: string;
metadata: Record<string, any>;
}
```
### Quality Assurance Requirements
- All scoring criteria must have documented methodologies
- Every recommendation must include confidence levels
- All AI-generated content must be marked as such
- Reproducibility tests must pass with >95% consistency
- Expert validation rate must exceed 80% for production use
## Success Metrics
### Forensic Quality Metrics
- **Transparency**: 100% of decisions traceable to evidence
- **Objectivity**: <5% variance in scoring between runs
- **Reproducibility**: >95% identical results for identical inputs
- **Expert Agreement**: >80% expert validation rate
- **Bias Reduction**: <10% bias score across all categories
### Performance Metrics
- **Response Time**: <30 seconds for workflow recommendations
- **Accuracy**: >90% real-world case validation success
- **Coverage**: Support for >95% of common forensic scenarios
- **Reliability**: <1% system error rate
- **Cost Efficiency**: <50% cost reduction vs. single large model
## Risk Mitigation
### Technical Risks
- **AI Model Failures**: Implement robust fallback mechanisms
- **Configuration Errors**: Comprehensive validation and testing
- **Performance Issues**: Load testing and optimization
- **Data Corruption**: Backup and recovery procedures
### Forensic Risks
- **Bias Introduction**: Continuous monitoring and expert validation
- **Methodology Errors**: Peer review and scientific validation
- **Legal Challenges**: Ensure compliance with admissibility standards
- **Expert Disagreement**: Transparent uncertainty communication

685
README.md
View File

@@ -1,232 +1,150 @@
# ForensicPathways
Ein kuratiertes Verzeichnis für Digital Forensics und Incident Response (DFIR) Tools, Methoden und Konzepte mit KI-gestützten Workflow-Empfehlungen.
Ein umfassendes Verzeichnis digitaler Forensik- und Incident-Response-Tools mit KI-gestützten Empfehlungen basierend auf der NIST SP 800-86 Methodik.
## ✨ Funktionen
## Lizenz
### 🎯 Hauptansichten
- **Kachelansicht (Grid View):** Übersichtliche Kartenansicht aller Tools/Methoden
- **Matrix-Ansicht:** Interaktive Matrix nach forensischen Domänen und Untersuchungsphasen (NIST Framework)
- **Forensic-AI:** AI-gestützte Workflow-Empfehlungen basierend auf Szenario-Beschreibungen
Dieses Projekt ist unter der BSD-3-Clause-Lizenz lizenziert.
### 🔍 Navigation & Filterung
- **Tag-System:** Intelligente Filterung nach Kategorien und Eigenschaften
- **Volltext-Suche:** Durchsuchen von Namen, Beschreibungen und Tags
- **Domain/Phase-Filter:** Filterung nach forensischen Bereichen und Ermittlungsphasen
## Funktionen
### 📚 Inhaltstypen
- **Software/Tools:** Open Source und proprietäre forensische Software
- **Methoden:** Bewährte forensische Verfahren und Prozesse
- **Konzepte:** Grundlegendes Fachwissen und theoretische Grundlagen
### Kernfunktionalität
- **Umfassende Tool-Datenbank**: 100+ forensische Tools kategorisiert nach Domänen, Phasen und Skill-Levels
- **NIST SP 800-86 Integration**: Vier-Phasen-Methodik (Sammlung → Auswertung → Analyse → Berichterstattung)
- **Multiple Ansichtsmodi**: Kachelansicht, Matrix-Übersicht und KI-gestützte Empfehlungen
- **Erweiterte Suche**: Textsuche, semantische Embedding-basierte Suche und Multi-Kriterien-Filterung
- **Responsive Design**: Dark/Light-Mode-Unterstützung, mobile-optimierte Benutzeroberfläche
### 📖 Knowledgebase
- **Erweiterte Dokumentation:** Detaillierte Artikel zu Tools und Methoden
- **Praktische Anleitungen:** Installation, Konfiguration und Best Practices
- **Markdown-basiert:** Einfache Erstellung und Wartung von Inhalten
### KI-gestützte Analyse
- **Micro-Task-Pipeline**: Intelligente Tool-Auswahl durch mehrere KI-Analyseschritte
- **Semantische Suche**: Vector-Embeddings für natürlichsprachige Tool-Entdeckung
- **Konfidenz-Bewertung**: Transparente Vertrauensmetriken für KI-Empfehlungen
- **Audit-Trail**: Vollständige Entscheidungstransparenz mit detaillierter Protokollierung
- **Rate Limiting**: Intelligente Warteschlangenverwaltung und nutzerbasierte Ratenbegrenzung
### 🤝 Contribution-System
- **Tool/Methoden-Beiträge:** Webformular für neue Einträge
- **Knowledgebase-Artikel:** Artikel-Editor mit Datei-Upload
- **Git-Integration:** Automatische Issue-Erstellung für Review-Prozess
- **File-Management:** Nextcloud-Integration für Medien-Uploads
### Zusammenarbeit & Beiträge
- **Tool-Beiträge**: Neue Tools einreichen oder bestehende über Git-Integration bearbeiten
- **Knowledgebase**: Community-beigetragene Artikel und Dokumentation
- **File-Upload-System**: Nextcloud-Integration für Medien-Anhänge
- **Authentifizierung**: OIDC-Integration mit konfigurierbaren Anbietern
### 🔐 Authentifizierung
- **OIDC-Integration:** Single Sign-On mit OpenID Connect
- **Berechtigungssteuerung:** Schutz für AI-Features und Contribution-System
- **Session-Management:** Sichere JWT-basierte Sessions
### Enterprise-Funktionen
- **Warteschlangenverwaltung**: Ratenbegrenzte KI-Verarbeitung mit Echtzeit-Status-Updates
- **Audit-Protokollierung**: Umfassender forensischer Audit-Trail für KI-Entscheidungsfindung
- **Multi-Provider-Unterstützung**: Konfigurierbare KI-Services (Mistral AI, Ollama, OpenAI)
- **Git-Integration**: Automatisierte Issue-Erstellung für Beiträge (Gitea, GitHub, GitLab)
## 🛠 Technische Grundlage
## Datenmodell
- **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)
Das System verwendet eine YAML-basierte Konfiguration in `src/data/tools.yaml`:
## 📋 Voraussetzungen
```yaml
tools:
- name: Tool Name
type: software|method|concept
description: Detaillierte Beschreibung
skillLevel: novice|beginner|intermediate|advanced|expert
url: https://tool-homepage.com
domains: [incident-response, static-investigations, ...]
phases: [data-collection, examination, analysis, reporting]
platforms: [Windows, Linux, macOS]
license: Lizenztyp
tags: [gui, commandline, ...]
related_concepts: [konzept1, konzept2]
# Optionale Felder
projectUrl: https://hosted-instance.com # Für CC24-Server gehostete Tools
knowledgebase: true # Hat KB-Artikel
accessType: download|hosted|cloud
- **Node.js:** Version 18.x oder höher
- **npm:** Version 8.x oder höher
- **Nginx:** Für Reverse Proxy (Produktion)
domains:
- id: incident-response
name: Incident Response & Breach-Untersuchung
## 🔧 Externe Abhängigkeiten (Optional)
phases:
- id: data-collection
name: Datensammlung
description: Imaging, Akquisition, Remote-Collection-Tools
### OIDC Provider
- **Zweck:** Benutzerauthentifizierung
- **Beispiel:** Nextcloud, Keycloak, Auth0
- **Konfiguration:** `OIDC_ENDPOINT`, `OIDC_CLIENT_ID`, `OIDC_CLIENT_SECRET`
### Nextcloud
- **Zweck:** File-Upload für Knowledgebase-Beiträge
- **Features:** Medien-Management, öffentliche Links
- **Konfiguration:** `NEXTCLOUD_ENDPOINT`, `NEXTCLOUD_USERNAME`, `NEXTCLOUD_PASSWORD`
### AI Service (Mistral/OpenAI-kompatibel)
- **Zweck:** KI-gestützte Tool-Empfehlungen
- **Konfiguration:** `AI_ANALYZER_ENDPOINT`, `AI_ANALYZER_API_KEY`, `AI_ANALYZER_MODEL`
### Uptime Kuma
- **Zweck:** Status-Monitoring für gehostete Services
- **Integration:** Status-Badges in der Service-Übersicht
### Git Provider (Gitea/GitHub/GitLab)
- **Zweck:** Issue-Erstellung für Contributions
- **Konfiguration:** `GIT_PROVIDER`, `GIT_API_ENDPOINT`, `GIT_API_TOKEN`
## 🚀 Installation
### Lokale Entwicklung
```bash
# Repository klonen
git clone https://git.cc24.dev/mstoeck3/forensic-pathways.git
cd forensic-pathways
# Dependencies installieren
npm install
# Umgebungsvariablen konfigurieren
cp .env.example .env
# .env bearbeiten (siehe Konfiguration unten)
npm run astro build
# Development Server starten
npm run dev
scenarios:
- id: scenario:memory_dump
icon: 🧠
friendly_name: RAM-Analyse
```
Die Seite ist dann unter `http://localhost:4321` verfügbar.
## AI Concept
### Produktions-Deployment
### Micro-Task Architecture
The AI system uses a sophisticated pipeline that breaks complex analysis into focused micro-tasks:
#### 1. System vorbereiten
1. **Scenario Analysis**: Understanding the forensic context
2. **Investigation Approach**: Determining optimal methodology
3. **Critical Considerations**: Identifying potential challenges
4. **Tool Selection**: Phase-specific or problem-specific recommendations
5. **Background Knowledge**: Relevant concepts and prerequisites
6. **Final Synthesis**: Integrated recommendations with confidence scoring
### Confidence Scoring
Each recommendation includes transparent confidence metrics:
- **Semantic Relevance**: Vector similarity between query and tool descriptions
- **Task Suitability**: AI-assessed fitness for the specific scenario
- **Uncertainty Factors**: Potential limitations and considerations
- **Strength Indicators**: Why the tool is well-suited
## NIST SP 800-86 Phases
The system organizes tools according to the four-phase NIST methodology:
1. **Data Collection**: Imaging, acquisition, and evidence preservation
2. **Examination**: Parsing, extraction, and initial data processing
3. **Analysis**: Deep investigation, correlation, and insight generation
4. **Reporting**: Documentation, visualization, and presentation
Each tool is mapped to appropriate phases, enabling workflow-based recommendations.
## Deployment
### Production Setup
1. **Build and Deploy**:
```bash
# System-Updates
sudo apt update && sudo apt upgrade -y
# Node.js installieren (Ubuntu/Debian)
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
# Nginx installieren
sudo apt install nginx -y
# Systemd für Service-Management
sudo systemctl enable nginx
npm run build
sudo ./deploy.sh # Copies dist/ to /var/www/forensic-pathways
```
#### 2. Anwendung installieren
2. **Configuration**:
```bash
# Klonen des Repositorys
sudo git clone https://git.cc24.dev/mstoeck3/forensic-pathways /opt/forensic-pathways
cd /opt/forensic-pathways
# Abhängigkeiten installieren
sudo npm install
# Production-Build erstellen
sudo npm run build
npm run astro build
# Berechtigungen setzen
sudo chown -R www-data:www-data /opt/forensic-pathways
cd /var/www/forensic-pathways
sudo cp .env.example .env
sudo nano .env # Configure AI services, authentication, etc.
```
#### 3. Umgebungsvariablen konfigurieren
3. **Systemd Service** (`/etc/systemd/system/forensic-pathways.service`):
```ini
[Unit]
Description=ForensicPathways
After=network.target
Erstelle `/opt/forensic-pathways/.env`:
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/forensic-pathways
ExecStart=/usr/bin/node server/entry.mjs
Restart=always
RestartSec=10
Environment=NODE_ENV=production
```bash
# ===========================================
# ForensicPathways Environment Configuration
# ===========================================
# Authentication & OIDC (Required)
AUTH_SECRET=change-this-to-a-strong-secret-key-in-production
OIDC_ENDPOINT=https://your-oidc-provider.com
OIDC_CLIENT_ID=your-oidc-client-id
OIDC_CLIENT_SECRET=your-oidc-client-secret
# Auth Scopes - set to true in prod
AUTHENTICATION_NECESSARY_CONTRIBUTIONS=true
AUTHENTICATION_NECESSARY_AI=true
# Application Configuration (Required)
PUBLIC_BASE_URL=https://your-domain.com
NODE_ENV=production
# AI Service Configuration (Required for AI features)
AI_ANALYZER_MODEL=mistral-large-latest
AI_ANALYZER_ENDPOINT=https://api.mistral.ai
AI_ANALYZER_API_KEY=your-mistral-api-key
AI_RATE_LIMIT_DELAY_MS=1000
# Git Integration (Required for contributions)
GIT_REPO_URL=https://git.cc24.dev/mstoeck3/forensic-pathways
GIT_PROVIDER=gitea
GIT_API_ENDPOINT=https://git.cc24.dev/api/v1
GIT_API_TOKEN=your-git-api-token
# File Upload Configuration (Optional)
LOCAL_UPLOAD_PATH=./public/uploads
# Nextcloud Integration (Optional)
NEXTCLOUD_ENDPOINT=https://your-nextcloud.com
NEXTCLOUD_USERNAME=your-username
NEXTCLOUD_PASSWORD=your-password
NEXTCLOUD_UPLOAD_PATH=/kb-media
NEXTCLOUD_PUBLIC_URL=https://your-nextcloud.com/s/
[Install]
WantedBy=multi-user.target
```
```bash
# Berechtigungen sichern
sudo chmod 600 /opt/forensic-pathways/.env
sudo chown www-data:www-data /opt/forensic-pathways/.env
```
#### 4. Nginx konfigurieren
Erstelle `/etc/nginx/sites-available/forensic-pathways`:
4. **Nginx Configuration**:
```nginx
server {
listen 80;
server_name ihre-domain.de;
server_name forensic-pathways.yourdomain.com;
client_max_body_size 50M; # Important for uploads
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name ihre-domain.de;
# SSL Konfiguration (Let's Encrypt empfohlen)
ssl_certificate /etc/letsencrypt/live/ihre-domain.de/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/ihre-domain.de/privkey.pem;
# Security Headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
# Static Files
location / {
try_files $uri $uri/ @nodejs;
root /opt/forensic-pathways/dist;
index index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?|ttf)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# API Routes to Node.js
location @nodejs {
proxy_pass http://localhost:4321;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
@@ -236,251 +154,178 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Upload limit
client_max_body_size 50M;
}
```
5. **Enable and Start**:
```bash
# Site aktivieren
sudo ln -s /etc/nginx/sites-available/forensic-pathways /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl enable forensic-pathways
sudo systemctl start forensic-pathways
sudo systemctl reload nginx
```
#### 5. Systemd Service einrichten
### Environment Configuration
Erstelle `/etc/systemd/system/forensic-pathways.service`:
```ini
[Unit]
Description=ForensicPathways DFIR Guide
After=network.target nginx.service
Wants=nginx.service
[Service]
Type=exec
User=www-data
Group=www-data
WorkingDirectory=/opt/forensic-pathways
Environment=NODE_ENV=production
ExecStart=/usr/bin/node ./dist/server/entry.mjs
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Security
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/opt/forensic-pathways
CapabilityBoundingSet=
# Resource Limits
LimitNOFILE=65536
MemoryMax=512M
[Install]
WantedBy=multi-user.target
```
Key configuration in `.env`:
```bash
# Service aktivieren und starten
sudo systemctl daemon-reload
sudo systemctl enable forensic-pathways
sudo systemctl start forensic-pathways
# Core Application
PUBLIC_BASE_URL=https://forensic-pathways.yourdomain.com
AUTH_SECRET=your-secure-random-secret
# Status prüfen
sudo systemctl status forensic-pathways
# AI Services (Required)
AI_ANALYZER_ENDPOINT=https://api.mistral.ai/v1/chat/completions
AI_ANALYZER_API_KEY=your-api-key
AI_ANALYZER_MODEL=mistral/mistral-small-latest
# Vector Embeddings (Recommended)
AI_EMBEDDINGS_ENABLED=true
AI_EMBEDDINGS_ENDPOINT=https://api.mistral.ai/v1/embeddings
AI_EMBEDDINGS_MODEL=mistral-embed
# Authentication (Optional)
AUTHENTICATION_NECESSARY_AI=false
OIDC_ENDPOINT=https://your-keycloak.com/auth/realms/your-realm
OIDC_CLIENT_ID=forensic-pathways
```
## 🔧 Konfiguration
## Externe Abhängigkeiten (Optionale Features)
### Minimalkonfiguration (ohne Auth)
### File-Upload-System
- **Nextcloud**: Primärer Speicher für Beitrags-Anhänge
- **Lokaler Fallback**: Automatischer Fallback zu lokalem Speicher bei Nextcloud-Ausfall
### Authentifizierungsanbieter
- **Keycloak**: Empfohlener OIDC-Provider
- **Andere OIDC**: Jeder OIDC-konforme Provider (Auth0, Azure AD, etc.)
### Git-Integration
- **Gitea**: Primärer Git-Provider für Beiträge
- **GitHub/GitLab**: Alternative Git-Provider unterstützt
### Monitoring
- **Uptime Kuma**: Service-Monitoring und Gesundheitschecks (optional)
### KI-Services
- **Mistral AI**: Empfohlen für Produktion (API-Schlüssel erforderlich)
- **Ollama**: Lokale Deployment-Option (kein API-Schlüssel benötigt)
- **OpenAI**: Alternative kommerzielle Anbieter
## Knowledgebase-System
### Artikel hinzufügen
Knowledgebase-Artikel werden in `src/content/knowledgebase/` als Markdown-Dateien mit Frontmatter gespeichert:
```markdown
---
title: "Tool-Konfigurationsanleitung"
description: "Schritt-für-Schritt-Setup-Anweisungen"
last_updated: 2024-01-15
author: "Ihr Name"
difficulty: intermediate
# Tool-Zuordnung (optional)
tool_name: "Autopsy"
related_tools: ["Volatility 3", "YARA"]
# Kategorisierung
categories: ["konfiguration", "setup"]
tags: ["gui", "installation", "windows"]
published: true
---
# Tool-Konfigurationsanleitung
Ihr Artikel-Inhalt hier...
## Voraussetzungen
- Systemanforderungen
- Abhängigkeiten
## Installationsschritte
1. Download von offizieller Quelle
2. Installer ausführen
3. Einstellungen konfigurieren
## Video-Demonstration
<video src="/videos/setup-tutorial.mp4" title="Setup-Tutorial" controls></video>
## Häufige Probleme
Lösungen für typische Probleme...
```
### Video-Integration
Knowledgebase-Artikel unterstützen eingebettete Videos für praktische Demonstrationen:
```html
<video src="/videos/demo.mp4" title="Tool-Demonstration" controls></video>
```
**Wichtige Hinweise**:
- Videos müssen manuell in `public/videos/` bereitgestellt werden (nicht im Git-Repository enthalten)
- Firefox-kompatible Formate verwenden (MP4 H.264, WebM VP9)
- Detaillierte Video-Dokumentation: siehe `src/content/knowledgebase/README.md`
### Artikel-Struktur-Richtlinien
**Erforderliche Felder**:
- `title`: Klarer, beschreibender Titel
- `description`: Einzeilige Zusammenfassung für Auflistungen
- `last_updated`: Artikel-Änderungsdatum
- `published`: Boolean-Flag für Sichtbarkeit
**Optionale Felder**:
- `tool_name`: Zuordnung zu spezifischem Tool aus Datenbank
- `author`: Mitwirkender Name (Standard: "Anon")
- `difficulty`: Komplexitätslevel passend zu Tool-Skill-Levels
- `categories`: Breite Klassifizierungen
- `tags`: Spezifische Stichwörter für Entdeckung
- `related_tools`: Array verwandter Tool-Namen
**Inhalt-Richtlinien**:
- Standard-Markdown-Formatierung verwenden
- Praktische Beispiele und Code-Snippets einschließen
- Screenshots oder Diagramme bei Bedarf hinzufügen
- Zu verwandten Tools mit `[Tool Name](/tools/tool-slug)` Format verlinken
- Troubleshooting-Abschnitte für komplexe Tools einschließen
### Automatische Verarbeitung
1. Artikel werden automatisch beim Build indexiert
2. Tool-Zuordnungen erstellen bidirektionale Links
3. Suche umfasst Volltext-Inhalt und Metadaten
4. Verwandte Artikel erscheinen in Tool-Detail-Ansichten
## Entwicklung
```bash
# Nur für Tests geeignet
AUTHENTICATION_NECESSARY=false
PUBLIC_BASE_URL=http://localhost:4321
# Setup
npm install
cp .env.example .env
# Entwicklung
npm run dev
# Build
npm run build
# Deploy
sudo ./deploy.sh
```
### Tools-Datenbank
## Konfigurationsübersicht
Die Tools werden in `src/data/tools.yaml` verwaltet. Vollständiges Beispiel:
Die `.env.example`-Datei enthält umfassende Konfigurationsoptionen für alle Features. Die meisten Optionen haben sinnvolle Standardwerte, wobei nur die KI-Service-Konfiguration für volle Funktionalität erforderlich ist.
```yaml
tools:
- name: Autopsy
type: software # software|method|concept
description: >-
Die führende Open-Source-Alternative zu kommerziellen Forensik-Suiten mit
intuitiver grafischer Oberfläche. Besonders stark in der Timeline-Analyse,
Keyword-Suche und dem Carving gelöschter Dateien. Die modulare
Plugin-Architektur erlaubt Erweiterungen für spezielle
Untersuchungsszenarien.
icon: 📦
skillLevel: intermediate # novice|beginner|intermediate|advanced|expert
url: https://www.autopsy.com/
domains:
- incident-response
- static-investigations
- malware-analysis
- mobile-forensics
- cloud-forensics
phases:
- examination
- analysis
platforms:
- Windows
- Linux
related_concepts:
- SQL Query Fundamentals
- Hash Functions & Digital Signatures
accessType: download # download|web|api|cli|service
license: Apache 2.0
knowledgebase: false # true für erweiterte Dokumentation
tags:
- gui
- filesystem
- timeline-analysis
- carving
- artifact-extraction
- keyword-search
# Optional: Für gehostete Services
projectUrl: https://autopsy.ihre-domain.de
statusUrl: https://status.ihre-domain.de/api/badge/1/status
## Architektur
# Beispiel Methode
- name: Live Response Methodology
type: method
description: >-
Strukturierte Vorgehensweise zur Sammlung volatiler Daten
von laufenden Systemen ohne Shutdown.
icon: 📋
skillLevel: advanced
url: https://www.sans.org/white-papers/live-response/
domains:
- incident-response
phases:
- data-collection
related_concepts:
- Memory Forensics Fundamentals
tags:
- volatile-data
- live-analysis
- methodology
knowledgebase: true
# Beispiel Konzept
- name: Hash Functions & Digital Signatures
type: concept
description: >-
Kryptographische Grundlagen für Datenintegrität und
Authentifizierung in der digitalen Forensik.
icon: 🔐
skillLevel: intermediate
url: https://en.wikipedia.org/wiki/Cryptographic_hash_function
domains:
- incident-response
- static-investigations
- malware-analysis
phases:
- data-collection
- examination
tags:
- cryptography
- data-integrity
- evidence-preservation
knowledgebase: false
# Konfiguration der Domänen
domains:
- id: incident-response
name: Incident Response & Breach-Untersuchung
- id: static-investigations
name: Datenträgerforensik & Ermittlungen
- id: malware-analysis
name: Malware-Analyse & Reverse Engineering
- id: mobile-forensics
name: Mobile Geräte & App-Forensik
- id: cloud-forensics
name: Cloud & Virtuelle Umgebungen
# Konfiguration der Phasen (NIST Framework)
phases:
- id: data-collection
name: Datensammlung
description: Imaging, Acquisition, Remote Collection Tools
- id: examination
name: Auswertung
description: Parsing, Extraction, Initial Analysis Tools
- id: analysis
name: Analyse
description: Deep Analysis, Correlation, Visualization Tools
- id: reporting
name: Bericht & Präsentation
description: Documentation, Visualization, Presentation Tools
# Domänenübergreifende Kategorien
domain-agnostic-software:
- id: collaboration-general
name: Übergreifend & Kollaboration
description: Cross-cutting tools and collaboration platforms
- id: specific-os
name: Betriebssysteme
description: Operating Systems which focus on forensics
```
## 📦 Updates
```bash
# Repository aktualisieren
cd /opt/forensic-pathways
sudo git pull
# Dependencies aktualisieren
sudo npm install
# Rebuild
sudo npm run build
# Service neustarten
sudo systemctl restart forensic-pathways
```
## 💾 Backup
Wichtige Dateien für Backup:
```bash
/opt/forensic-pathways/src/data/tools.yaml
/opt/forensic-pathways/.env
/etc/nginx/sites-available/forensic-pathways
/etc/systemd/system/forensic-pathways.service
```
## 🤝 Beiträge
Contributions sind willkommen! Bitte:
1. Issue im Repository erstellen
2. Feature-Branch erstellen
3. Pull Request öffnen
4. Tests durchführen
## 📞 Support
Bei Problemen oder Fragen:
- **Issues:** [Repository Issues](https://git.cc24.dev/mstoeck3/forensic-pathways/issues)
- **Dokumentation:** Siehe `/knowledgebase` auf der Website
## 📄 Lizenz
Dieses Projekt steht unter der **BSD-3-Clause** Lizenz.
- **Frontend**: Astro mit TypeScript, responsive CSS
- **Backend**: Node.js API-Routen mit intelligenter Ratenbegrenzung
- **KI-Pipeline**: Micro-Task-Architektur mit Audit-Protokollierung
- **Daten**: YAML-basierte Tool-Datenbank mit Git-basierten Beiträgen
- **Suche**: Dual-Mode Text- und semantische Vector-Suche
- **Auth**: OIDC-Integration mit Session-Management

View File

@@ -1,5 +1,6 @@
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
import { remarkVideoPlugin } from './src/utils/remarkVideoPlugin.ts';
export default defineConfig({
output: 'server',
@@ -7,6 +8,13 @@ export default defineConfig({
mode: 'standalone'
}),
markdown: {
remarkPlugins: [
remarkVideoPlugin
],
extendDefaultPlugins: true
},
build: {
assets: '_astro'
},

File diff suppressed because it is too large Load Diff

863
deploy.sh Executable file
View File

@@ -0,0 +1,863 @@
#!/bin/bash
# ForensicPathways Deployment Script *ownership-aware* + VISUAL ENHANCED
# Usage: sudo ./deploy.sh
set -e
# ═══════════════════════════════════════════════════════════════════════════════
# 🎨 VISUAL ENHANCEMENT SYSTEM
# ═══════════════════════════════════════════════════════════════════════════════
# Color palette
declare -r RED='\033[0;31m'
declare -r GREEN='\033[0;32m'
declare -r YELLOW='\033[0;33m'
declare -r BLUE='\033[0;34m'
declare -r MAGENTA='\033[0;35m'
declare -r CYAN='\033[0;36m'
declare -r WHITE='\033[0;37m'
declare -r BOLD='\033[1m'
declare -r DIM='\033[2m'
declare -r ITALIC='\033[3m'
declare -r UNDERLINE='\033[4m'
declare -r BLINK='\033[5m'
declare -r REVERSE='\033[7m'
declare -r RESET='\033[0m'
# Gradient colors
declare -r GRAD1='\033[38;5;196m' # Bright red
declare -r GRAD2='\033[38;5;202m' # Orange
declare -r GRAD3='\033[38;5;208m' # Dark orange
declare -r GRAD4='\033[38;5;214m' # Yellow orange
declare -r GRAD5='\033[38;5;220m' # Yellow
declare -r GRAD6='\033[38;5;118m' # Light green
declare -r GRAD7='\033[38;5;82m' # Green
declare -r GRAD8='\033[38;5;51m' # Cyan
declare -r GRAD9='\033[38;5;33m' # Blue
declare -r GRAD10='\033[38;5;129m' # Purple
# Background colors
declare -r BG_RED='\033[41m'
declare -r BG_GREEN='\033[42m'
declare -r BG_YELLOW='\033[43m'
declare -r BG_BLUE='\033[44m'
declare -r BG_MAGENTA='\033[45m'
declare -r BG_CYAN='\033[46m'
# Unicode box drawing
declare -r BOX_H='═'
declare -r BOX_V='║'
declare -r BOX_TL='╔'
declare -r BOX_TR='╗'
declare -r BOX_BL='╚'
declare -r BOX_BR='╝'
declare -r BOX_T='╦'
declare -r BOX_B='╩'
declare -r BOX_L='╠'
declare -r BOX_R='╣'
declare -r BOX_C='╬'
# Fancy Unicode characters
declare -r ARROW_R='▶'
declare -r ARROW_D='▼'
declare -r DIAMOND='◆'
declare -r STAR='★'
declare -r BULLET='●'
declare -r CIRCLE='◯'
declare -r SQUARE='▪'
declare -r TRIANGLE='▲'
# Animation frames
SPINNER_FRAMES=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏')
PULSE_FRAMES=('●' '◐' '◑' '◒' '◓' '◔' '◕' '◖' '◗' '◘')
WAVE_FRAMES=('▁' '▂' '▃' '▄' '▅' '▆' '▇' '█' '▇' '▆' '▅' '▄' '▃' '▂')
# Terminal dimensions
COLS=$(tput cols 2>/dev/null || echo 80)
LINES=$(tput lines 2>/dev/null || echo 24)
# ═══════════════════════════════════════════════════════════════════════════════
# 🎯 VISUAL FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
print_gradient_text() {
local text="$1"
local colors=("$GRAD1" "$GRAD2" "$GRAD3" "$GRAD4" "$GRAD5" "$GRAD6" "$GRAD7" "$GRAD8" "$GRAD9" "$GRAD10")
local length=${#text}
local color_count=${#colors[@]}
for ((i=0; i<length; i++)); do
local color_idx=$((i * color_count / length))
printf "${colors[$color_idx]}${text:$i:1}"
done
printf "$RESET"
}
animate_text() {
local text="$1"
local delay="${2:-0.03}"
for ((i=0; i<=${#text}; i++)); do
printf "\r${CYAN}${text:0:$i}${RESET}"
sleep "$delay"
done
echo
}
draw_box() {
local title="$1"
local content="$2"
local width=${3:-$((COLS-4))}
local color="${4:-$CYAN}"
# Top border
printf "${color}${BOX_TL}"
for ((i=0; i<width-2; i++)); do printf "${BOX_H}"; done
printf "${BOX_TR}${RESET}\n"
# Title line
if [ -n "$title" ]; then
local title_len=${#title}
local padding=$(((width-title_len-2)/2))
printf "${color}${BOX_V}${RESET}"
for ((i=0; i<padding; i++)); do printf " "; done
printf "${BOLD}${WHITE}$title${RESET}"
for ((i=0; i<width-title_len-padding-2; i++)); do printf " "; done
printf "${color}${BOX_V}${RESET}\n"
# Separator
printf "${color}${BOX_L}"
for ((i=0; i<width-2; i++)); do printf "${BOX_H}"; done
printf "${BOX_R}${RESET}\n"
fi
# Content lines
while IFS= read -r line; do
local line_len=${#line}
printf "${color}${BOX_V}${RESET} %-$((width-4))s ${color}${BOX_V}${RESET}\n" "$line"
done <<< "$content"
# Bottom border
printf "${color}${BOX_BL}"
for ((i=0; i<width-2; i++)); do printf "${BOX_H}"; done
printf "${BOX_BR}${RESET}\n"
}
progress_bar() {
local current="$1"
local total="$2"
local width="${3:-50}"
local label="$4"
local percentage=$((current * 100 / total))
local filled=$((current * width / total))
local empty=$((width - filled))
printf "\r${BOLD}%s${RESET} [" "$label"
# Filled portion with gradient
for ((i=0; i<filled; i++)); do
local color_idx=$((i * 10 / width))
case $color_idx in
0) printf "${GRAD1}${RESET}" ;;
1) printf "${GRAD2}${RESET}" ;;
2) printf "${GRAD3}${RESET}" ;;
3) printf "${GRAD4}${RESET}" ;;
4) printf "${GRAD5}${RESET}" ;;
5) printf "${GRAD6}${RESET}" ;;
6) printf "${GRAD7}${RESET}" ;;
7) printf "${GRAD8}${RESET}" ;;
8) printf "${GRAD9}${RESET}" ;;
*) printf "${GRAD10}${RESET}" ;;
esac
done
# Empty portion
for ((i=0; i<empty; i++)); do
printf "${DIM}${RESET}"
done
printf "] ${BOLD}%d%%${RESET}" "$percentage"
}
spinner() {
local pid=$1
local message="$2"
local frame=0
while kill -0 $pid 2>/dev/null; do
printf "\r${CYAN}${SPINNER_FRAMES[$frame]}${RESET} ${message}"
frame=$(((frame + 1) % ${#SPINNER_FRAMES[@]}))
sleep 0.1
done
printf "\r${GREEN}${RESET} ${message}\n"
}
pulsing_dots() {
local count="${1:-5}"
local cycles="${2:-3}"
for ((c=0; c<cycles; c++)); do
for frame in "${PULSE_FRAMES[@]}"; do
printf "\r${MAGENTA}"
for ((i=0; i<count; i++)); do
printf "$frame "
done
printf "${RESET}"
sleep 0.1
done
done
}
wave_animation() {
local width="${1:-$((COLS/2))}"
local cycles="${2:-2}"
for ((c=0; c<cycles; c++)); do
for frame in "${WAVE_FRAMES[@]}"; do
printf "\r${CYAN}"
for ((i=0; i<width; i++)); do
printf "$frame"
done
printf "${RESET}"
sleep 0.05
done
done
echo
}
celebrate() {
local width=$((COLS-10))
# Fireworks effect
for ((i=0; i<5; i++)); do
printf "\r"
for ((j=0; j<width; j++)); do
case $((RANDOM % 10)) in
0) printf "${GRAD1}*${RESET}" ;;
1) printf "${GRAD3}${RESET}" ;;
2) printf "${GRAD5}+${RESET}" ;;
3) printf "${GRAD7}×${RESET}" ;;
4) printf "${GRAD9}${RESET}" ;;
*) printf " " ;;
esac
done
sleep 0.2
done
echo
}
typewriter() {
local text="$1"
local delay="${2:-0.02}"
local color="${3:-$GREEN}"
printf "${color}"
for ((i=0; i<${#text}; i++)); do
printf "${text:$i:1}"
sleep "$delay"
done
printf "${RESET}\n"
}
fancy_header() {
local title="$1"
local subtitle="$2"
# Calculate centering
local title_len=${#title}
local subtitle_len=${#subtitle}
local box_width=$((COLS > 80 ? 80 : COLS-4))
local title_padding=$(((box_width-title_len)/2))
local subtitle_padding=$(((box_width-subtitle_len)/2))
echo
# Top gradient border
printf "${BOLD}"
for ((i=0; i<box_width; i++)); do
local color_idx=$((i * 10 / box_width))
case $color_idx in
0|1) printf "${GRAD1}${BOX_H}" ;;
2|3) printf "${GRAD3}${BOX_H}" ;;
4|5) printf "${GRAD5}${BOX_H}" ;;
6|7) printf "${GRAD7}${BOX_H}" ;;
*) printf "${GRAD9}${BOX_H}" ;;
esac
done
printf "${RESET}\n"
# Title line
printf "${GRAD1}${BOX_V}${RESET}"
for ((i=0; i<title_padding; i++)); do printf " "; done
print_gradient_text "$title"
for ((i=0; i<box_width-title_len-title_padding-2; i++)); do printf " "; done
printf "${GRAD1}${BOX_V}${RESET}\n"
# Subtitle line
if [ -n "$subtitle" ]; then
printf "${GRAD3}${BOX_V}${RESET}"
for ((i=0; i<subtitle_padding; i++)); do printf " "; done
printf "${ITALIC}${DIM}${subtitle}${RESET}"
for ((i=0; i<box_width-subtitle_len-subtitle_padding-2; i++)); do printf " "; done
printf "${GRAD3}${BOX_V}${RESET}\n"
fi
# Bottom gradient border
printf "${BOLD}"
for ((i=0; i<box_width; i++)); do
local color_idx=$((i * 10 / box_width))
case $color_idx in
0|1) printf "${GRAD1}${BOX_H}" ;;
2|3) printf "${GRAD3}${BOX_H}" ;;
4|5) printf "${GRAD5}${BOX_H}" ;;
6|7) printf "${GRAD7}${BOX_H}" ;;
*) printf "${GRAD9}${BOX_H}" ;;
esac
done
printf "${RESET}\n"
echo
}
section_header() {
local section_num="$1"
local title="$2"
local icon="$3"
echo
printf "${BOLD}${BG_BLUE}${WHITE} PHASE %s ${RESET} " "$section_num"
printf "${BOLD}${BLUE}%s %s${RESET}\n" "$icon" "$title"
# Animated underline
printf "${BLUE}"
for ((i=0; i<$((${#title}+10)); i++)); do
printf "▄"
sleep 0.01
done
printf "${RESET}\n"
}
status_ok() {
printf "${GREEN}${BOLD}${RESET} %s\n" "$1"
}
status_error() {
printf "${RED}${BOLD}${RESET} %s\n" "$1"
}
status_warning() {
printf "${YELLOW}${BOLD}${RESET} %s\n" "$1"
}
status_info() {
printf "${CYAN}${BOLD}${RESET} %s\n" "$1"
}
status_working() {
printf "${MAGENTA}${BOLD}${RESET} %s" "$1"
}
animated_countdown() {
local seconds="$1"
local message="$2"
for ((i=seconds; i>0; i--)); do
printf "\r${YELLOW}${BOLD}$message in ${i}s...${RESET}"
sleep 1
done
printf "\r${GREEN}${BOLD}🚀 $message${RESET} \n"
}
matrix_rain() {
local duration="${1:-2}"
local chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@#\$^&*()_+-=[]{}|;:,.<>?"
for ((i=0; i<duration*10; i++)); do
printf "\r${GREEN}"
for ((j=0; j<$((COLS/3)); j++)); do
printf "%s" "${chars:$((RANDOM % ${#chars})):1}"
done
printf "${RESET}"
sleep 0.1
done
echo
}
# ═══════════════════════════════════════════════════════════════════════════════
# 🚀 MAIN SCRIPT VARIABLES
# ═══════════════════════════════════════════════════════════════════════════════
WEBROOT="/var/www/forensic-pathways"
LOG_DIR="$WEBROOT/logs"
DATA_DIR="$WEBROOT/data"
UPLOADS_DIR="$WEBROOT/public/uploads"
# Get original user who called sudo
ORIGINAL_USER="${SUDO_USER:-$USER}"
ORIGINAL_HOME=$(eval echo "~$ORIGINAL_USER")
# ═══════════════════════════════════════════════════════════════════════════════
# 🎬 SPECTACULAR OPENING SEQUENCE
# ═══════════════════════════════════════════════════════════════════════════════
# Terminal setup
tput civis # Hide cursor
# ASCII Art Banner
fancy_header "FORENSIC PATHWAYS DEPLOYMENT" "Advanced Visual Enhancement System"
# Matrix effect intro
printf "${DIM}${GREEN}Initializing deployment matrix...${RESET}\n"
matrix_rain 1
# System information display
draw_box "DEPLOYMENT PARAMETERS" "$(cat << PARAMS
Timestamp: $(date '+%Y-%m-%d %H:%M:%S')
Original User: $ORIGINAL_USER
Working Directory: $(pwd)
Target Webroot: $WEBROOT
Terminal Size: ${COLS}x${LINES}
PARAMS
)" 60 "$MAGENTA"
sleep 1
# Animated countdown
animated_countdown 3 "Starting deployment"
# ═══════════════════════════════════════════════════════════════════════════════
# 🔒 PHASE 0: SAFETY CHECKS
# ═══════════════════════════════════════════════════════════════════════════════
section_header "0" "SECURITY & SAFETY VALIDATION" "🔒"
status_working "Verifying root privileges"
pulsing_dots 3 1
if [ "$EUID" -ne 0 ]; then
status_error "Script must be run as root (use sudo)"
echo
printf "${RED}${BOLD}${BG_YELLOW} DEPLOYMENT TERMINATED ${RESET}\n"
exit 1
fi
status_ok "Root privileges confirmed"
status_working "Validating project structure"
pulsing_dots 3 1
if [ ! -f "package.json" ] || [ ! -f "astro.config.mjs" ]; then
status_error "Must run from ForensicPathways project root"
status_info "Current directory: $(pwd)"
status_info "Files found: $(ls -la)"
echo
printf "${RED}${BOLD}${BG_YELLOW} DEPLOYMENT TERMINATED ${RESET}\n"
exit 1
fi
status_ok "Project structure validated"
# Security scan animation
printf "${CYAN}${BOLD}🔍 Running security scan${RESET}"
for i in {1..20}; do
printf "${CYAN}.${RESET}"
sleep 0.05
done
echo
status_ok "Security scan completed - all clear"
# ═══════════════════════════════════════════════════════════════════════════════
# 🔧 PHASE 1: NPM BUILD SYSTEM
# ═══════════════════════════════════════════════════════════════════════════════
find_and_use_npm() {
section_header "1" "BUILD SYSTEM INITIALIZATION" "🔧"
printf "${CYAN}${BOLD}🔍 Scanning for npm installation...${RESET}\n"
wave_animation 30 1
# A) system-wide npm
if command -v npm &>/dev/null; then
status_ok "System npm located: $(which npm)"
printf "${MAGENTA}${BOLD}📦 Installing dependencies${RESET}"
{
sudo -u "$ORIGINAL_USER" npm install > /tmp/npm_install.log 2>&1 &
spinner $! "Installing dependencies"
}
printf "${MAGENTA}${BOLD}🏗️ Building application${RESET}"
{
sudo -u "$ORIGINAL_USER" npm run build > /tmp/npm_build.log 2>&1 &
spinner $! "Building application"
}
return 0
fi
# B) nvm-managed npm
printf "${YELLOW}🔍 Scanning for nvm installation...${RESET}\n"
if sudo -u "$ORIGINAL_USER" bash -c "
export NVM_DIR='$ORIGINAL_HOME/.nvm'
[ -s \"\$NVM_DIR/nvm.sh\" ] && source \"\$NVM_DIR/nvm.sh\"
[ -s '$ORIGINAL_HOME/.bashrc' ] && source '$ORIGINAL_HOME/.bashrc'
command -v npm &>/dev/null
"; then
status_ok "NVM-managed npm located"
printf "${MAGENTA}${BOLD}📦 Installing dependencies with nvm${RESET}"
{
sudo -u "$ORIGINAL_USER" bash -c "
export NVM_DIR='$ORIGINAL_HOME/.nvm'
[ -s \"\$NVM_DIR/nvm.sh\" ] && source \"\$NVM_DIR/nvm.sh\"
[ -s '$ORIGINAL_HOME/.bashrc' ] && source '$ORIGINAL_HOME/.bashrc'
npm install > /tmp/npm_install.log 2>&1
npm run build > /tmp/npm_build.log 2>&1
" &
spinner $! "Building with nvm"
}
return 0
fi
# C) Installation instructions with fancy formatting
draw_box "NPM NOT FOUND" "$(cat << NPMHELP
Please install Node.js and npm first:
Option 1 (apt):
sudo apt update && sudo apt install nodejs npm
Option 2 (NodeSource recommended):
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
Option 3 (nvm as user):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc && nvm install 20
NPMHELP
)" 70 "$RED"
return 1
}
# ═══════════════════════════════════════════════════════════════════════════════
# 🏗️ PHASE 2: BUILD ORCHESTRATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "2" "BUILD ORCHESTRATION" "🏗️"
if [ ! -d "dist" ] || [ ! "$(ls -A dist 2>/dev/null)" ]; then
status_info "No dist/ directory found"
typewriter "Initiating build process..." 0.05 "$YELLOW"
find_and_use_npm || exit 1
else
status_ok "Existing dist/ directory detected"
echo
printf "${YELLOW}${BOLD}🤔 Rebuild application? ${RESET}${DIM}(y/N):${RESET} "
read -r REPLY
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
typewriter "Rebuilding application..." 0.05 "$CYAN"
find_and_use_npm || {
status_warning "Build failed, using existing dist/"
}
else
typewriter "Using existing build..." 0.05 "$GREEN"
fi
fi
# Build validation with dramatic effect
printf "${CYAN}${BOLD}🔍 Validating build output${RESET}"
pulsing_dots 8 2
if [ ! -d "dist" ] || [ ! "$(ls -A dist 2>/dev/null)" ]; then
echo
draw_box "BUILD FAILURE" "Build failed or dist/ directory is empty" 50 "$RED"
exit 1
fi
# Build success celebration
echo
printf "${GREEN}${BOLD}${BG_GREEN}${WHITE} BUILD SUCCESS ${RESET}\n"
celebrate
# ═══════════════════════════════════════════════════════════════════════════════
# 📁 PHASE 3: INFRASTRUCTURE SETUP
# ═══════════════════════════════════════════════════════════════════════════════
section_header "3" "INFRASTRUCTURE PROVISIONING" "📁"
status_working "Creating directory structure"
{
mkdir -p "$WEBROOT" "$LOG_DIR" "$DATA_DIR" "$UPLOADS_DIR" "$WEBROOT/src/data" &
spinner $! "Provisioning directories"
}
# Directory creation progress
DIRS=("$WEBROOT" "$LOG_DIR" "$DATA_DIR" "$UPLOADS_DIR" "$WEBROOT/src/data")
for i in "${!DIRS[@]}"; do
progress_bar $((i+1)) ${#DIRS[@]} 40 "Creating directories"
sleep 0.1
done
echo
status_ok "Directory infrastructure ready"
# ═══════════════════════════════════════════════════════════════════════════════
# 🚀 PHASE 4: APPLICATION DEPLOYMENT
# ═══════════════════════════════════════════════════════════════════════════════
section_header "4" "APPLICATION DEPLOYMENT" "🚀"
# File copy with visual progress
status_working "Deploying application files"
{
cp -r dist/. "$WEBROOT/" &
PID=$!
# Simple animated progress while copying
frame=0
while kill -0 $PID 2>/dev/null; do
printf "\r${MAGENTA}%s${RESET} Copying files..." "${SPINNER_FRAMES[$frame]}"
frame=$(((frame + 1) % ${#SPINNER_FRAMES[@]}))
sleep 0.1
done
wait $PID
printf "\r${GREEN}${RESET} Copying files... \n"
}
echo
SIZE=$(du -sh dist | cut -f1)
TOTAL_FILES=$(find dist -type f | wc -l)
status_ok "Application deployed ($SIZE, $TOTAL_FILES files)"
# Package.json copy with flair
printf "${MAGENTA}${BOLD}📋 Deploying package.json${RESET}"
pulsing_dots 3 1
cp package.json "$WEBROOT/"
status_ok "Package configuration deployed"
# ═══════════════════════════════════════════════════════════════════════════════
# ⚙️ PHASE 5: RUNTIME DEPENDENCY MANAGEMENT
# ═══════════════════════════════════════════════════════════════════════════════
section_header "5" "RUNTIME DEPENDENCY RESOLUTION" "⚙️"
typewriter "Transferring ownership for dependency installation..." 0.03 "$YELLOW"
chown -R "$ORIGINAL_USER":"$ORIGINAL_USER" "$WEBROOT"
printf "${CYAN}${BOLD}📦 Installing runtime dependencies${RESET}\n"
{
sudo -u "$ORIGINAL_USER" bash -c '
set -e
cd "'"$WEBROOT"'"
if command -v npm &>/dev/null; then
npm install --production > /tmp/runtime_deps.log 2>&1
else
export NVM_DIR="'$ORIGINAL_HOME'/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"
[ -s "'$ORIGINAL_HOME'/.bashrc" ] && source "'$ORIGINAL_HOME'/.bashrc"
npm install --production > /tmp/runtime_deps.log 2>&1
fi
' &
spinner $! "Installing runtime dependencies"
}
# Dependency success effect
printf "${GREEN}${BOLD}🎯 Dependencies locked and loaded!${RESET}\n"
wave_animation 40 1
# ═══════════════════════════════════════════════════════════════════════════════
# 🗃️ PHASE 6: DATA & CONTENT ORCHESTRATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "6" "DATA & CONTENT ORCHESTRATION" "🗃️"
status_working "Deploying core data structures"
if [ -f "src/data/tools.yaml" ]; then
cp src/data/tools.yaml "$WEBROOT/src/data/"
TOOL_COUNT=$(grep -c "^ - name:" "src/data/tools.yaml" || echo "unknown")
status_ok "Tools database deployed ($TOOL_COUNT tools)"
else
status_error "Critical file missing: src/data/tools.yaml"
exit 1
fi
status_working "Deploying knowledge base"
if [ -d "src/content/knowledgebase" ]; then
mkdir -p "$WEBROOT/src/content"
cp -r src/content/knowledgebase "$WEBROOT/src/content/"
KB_COUNT=$(find src/content/knowledgebase -name "*.md" 2>/dev/null | wc -l)
status_ok "Knowledge base deployed ($KB_COUNT articles)"
# Knowledge base visualization
printf "${BLUE}${BOLD}📚 Knowledge Base Structure:${RESET}\n"
find src/content/knowledgebase -name "*.md" | head -5 | while read -r file; do
printf " ${CYAN}${DIAMOND}${RESET} ${file#src/content/knowledgebase/}\n"
done
if [ $KB_COUNT -gt 5 ]; then
printf " ${DIM}... and $((KB_COUNT-5)) more articles${RESET}\n"
fi
else
status_warning "No knowledge base directory found (optional)"
fi
# ═══════════════════════════════════════════════════════════════════════════════
# ⚙️ PHASE 7: ENVIRONMENT CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "7" "ENVIRONMENT CONFIGURATION" "⚙️"
printf "${YELLOW}${BOLD}🔧 Configuring environment${RESET}"
pulsing_dots 5 1
cp .env.example "$WEBROOT/.env"
status_ok "Environment template deployed"
draw_box "CONFIGURATION NOTICE" "IMPORTANT: Edit $WEBROOT/.env with your configuration" 60 "$YELLOW"
# ═══════════════════════════════════════════════════════════════════════════════
# 📝 PHASE 8: LOGGING INFRASTRUCTURE
# ═══════════════════════════════════════════════════════════════════════════════
section_header "8" "LOGGING INFRASTRUCTURE" "📝"
LOG_FILES=("access.log" "error.log" "ai-pipeline.log")
for i in "${!LOG_FILES[@]}"; do
progress_bar $((i+1)) ${#LOG_FILES[@]} 30 "Creating log files"
touch "$LOG_DIR/${LOG_FILES[$i]}"
sleep 0.2
done
echo
status_ok "Logging infrastructure established"
# ═══════════════════════════════════════════════════════════════════════════════
# 🔐 PHASE 9: PERMISSION MATRIX
# ═══════════════════════════════════════════════════════════════════════════════
section_header "9" "PERMISSION MATRIX CONFIGURATION" "🔐"
typewriter "Implementing security hardening..." 0.04 "$RED"
# Permission operations with progress
PERM_OPERATIONS=(
"chown -R www-data:www-data $WEBROOT"
"chmod -R 755 $WEBROOT"
"chmod 600 $WEBROOT/.env"
"chmod 755 $DATA_DIR $UPLOADS_DIR $LOG_DIR"
"chmod 644 $LOG_DIR/*.log"
)
for i in "${!PERM_OPERATIONS[@]}"; do
progress_bar $((i+1)) ${#PERM_OPERATIONS[@]} 40 "Setting permissions"
eval "${PERM_OPERATIONS[$i]}"
sleep 0.3
done
echo
if [ -f "$WEBROOT/server/entry.mjs" ]; then
chmod 755 "$WEBROOT/server/entry.mjs"
status_ok "Server entry point permissions configured"
fi
status_ok "Permission matrix locked down"
# ═══════════════════════════════════════════════════════════════════════════════
# ✅ PHASE 10: DEPLOYMENT VALIDATION
# ═══════════════════════════════════════════════════════════════════════════════
section_header "10" "DEPLOYMENT VALIDATION MATRIX" "✅"
VALIDATION_ERRORS=0
VALIDATIONS=(
"$WEBROOT/.env|Environment configuration"
"$WEBROOT/src/data/tools.yaml|Tools database"
)
# Check application files (either index.html OR server directory)
if [ -f "$WEBROOT/index.html" ] || [ -d "$WEBROOT/server" ]; then
VALIDATIONS+=("APPLICATION_FILES_OK|Application files")
else
VALIDATIONS+=("APPLICATION_FILES_MISSING|Application files")
fi
echo
printf "${CYAN}${BOLD}🔍 Running comprehensive validation suite...${RESET}\n"
for validation in "${VALIDATIONS[@]}"; do
IFS='|' read -r file desc <<< "$validation"
printf "${YELLOW}Testing: %s${RESET}" "$desc"
pulsing_dots 3 1
if [[ "$file" == "APPLICATION_FILES_OK" ]]; then
status_ok "$desc validated"
elif [[ "$file" == "APPLICATION_FILES_MISSING" ]]; then
status_error "$desc missing"
((VALIDATION_ERRORS++))
elif [ -f "$file" ] || [ -d "$file" ]; then
status_ok "$desc validated"
else
status_error "$desc missing"
((VALIDATION_ERRORS++))
fi
done
# ═══════════════════════════════════════════════════════════════════════════════
# 🎊 FINAL RESULTS SPECTACULAR
# ═══════════════════════════════════════════════════════════════════════════════
echo
if [ $VALIDATION_ERRORS -eq 0 ]; then
# Success celebration sequence
printf "${GREEN}${BOLD}${BG_GREEN}${WHITE}"
for ((i=0; i<COLS; i++)); do printf "="; done
printf "${RESET}\n"
# Animated success banner
fancy_header "🎉 DEPLOYMENT SUCCESSFUL! 🎉" "All systems operational"
# Fireworks celebration
celebrate
# Next steps in a beautiful box
draw_box "🎯 MISSION BRIEFING - NEXT STEPS" "$(cat << STEPS
1. 🔧 Configure environment variables in $WEBROOT/.env
• Set PUBLIC_BASE_URL, AI service endpoints
• Configure AUTH_SECRET and database connections
2. 🔄 Restart system services:
sudo systemctl restart forensic-pathways
sudo systemctl reload nginx
3. 🔍 Monitor system health:
sudo systemctl status forensic-pathways
sudo tail -f $LOG_DIR/error.log
🌐 Application fortress established at: $WEBROOT
🎯 Ready for production deployment!
STEPS
)" 70 "$GREEN"
# Final celebration
echo
printf "${BOLD}"
print_gradient_text "🚀 FORENSIC PATHWAYS DEPLOYMENT COMPLETE 🚀"
echo
else
# Error summary
draw_box "⚠️ DEPLOYMENT COMPLETED WITH WARNINGS" "$(cat << WARNINGS
Found $VALIDATION_ERRORS validation issues
Please review and resolve before proceeding
WARNINGS
)" 60 "$YELLOW"
fi
# Final timestamp with style
echo
printf "${DIM}${ITALIC}Deployment completed at: "
printf "${BOLD}$(date '+%Y-%m-%d %H:%M:%S')${RESET}\n"
# Restore cursor
tput cnorm
# Final matrix effect fade-out
printf "${DIM}${GREEN}Deployment matrix shutting down...${RESET}\n"
matrix_rain 1
echo

View File

@@ -0,0 +1,83 @@
{
"toolsYamlPath": "./src/data/tools.yaml",
"models": [
{
"name": "granite-embedding:278m",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 512
},
{
"name": "paraphrase-multilingual:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 128
},
{
"name": "bge-large:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 512
},
{
"name": "snowflake-arctic-embed2:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 8192
},
{
"name": "snowflake-arctic-embed:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 512
},
{
"name": "all-minilm:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 256
},
{
"name": "bge-m3:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 8192
},
{
"name": "mxbai-embed-large:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 512
},
{
"name": "nomic-embed-text:latest",
"type": "ollama",
"endpoint": "http://192.168.178.100:11434/api/embeddings",
"rateLimit": false,
"contextSize": 2048
},
{
"name": "mistral-embed",
"type": "mistral",
"endpoint": "https://api.mistral.ai/v1/embeddings",
"apiKey": "${AI_EMBEDDINGS_API_KEY}",
"rateLimit": true,
"rateLimitDelayMs": 2000,
"contextSize": 8192
}
],
"testSettings": {
"maxToolsPerCategory": 6,
"maxNegativeExamples": 4,
"contextSizeTests": true,
"performanceIterations": 3
}
}

897
embeddings-comparison.js Normal file
View File

@@ -0,0 +1,897 @@
#!/usr/bin/env node
// efficient-embedding-comparison.js
// Proper embedding model evaluation with batch processing and vector search
// Run with: node efficient-embedding-comparison.js --config=config.json
import fs from 'fs/promises';
import yaml from 'js-yaml';
import path from 'path';
import crypto from 'crypto';
class EmbeddingCache {
constructor(cacheDir = './embedding-cache') {
this.cacheDir = cacheDir;
}
async ensureCacheDir() {
try {
await fs.access(this.cacheDir);
} catch {
await fs.mkdir(this.cacheDir, { recursive: true });
}
}
getCacheKey(model, text) {
const content = `${model.name}:${text}`;
return crypto.createHash('md5').update(content).digest('hex');
}
async getCachedEmbedding(model, text) {
await this.ensureCacheDir();
const key = this.getCacheKey(model, text);
const cachePath = path.join(this.cacheDir, `${key}.json`);
try {
const data = await fs.readFile(cachePath, 'utf8');
return JSON.parse(data);
} catch {
return null;
}
}
async setCachedEmbedding(model, text, embedding) {
await this.ensureCacheDir();
const key = this.getCacheKey(model, text);
const cachePath = path.join(this.cacheDir, `${key}.json`);
await fs.writeFile(cachePath, JSON.stringify(embedding));
}
async getCacheStats(model) {
await this.ensureCacheDir();
const files = await fs.readdir(this.cacheDir);
const modelFiles = files.filter(f => f.includes(model.name.replace(/[^a-zA-Z0-9]/g, '_')));
return { cached: modelFiles.length, total: files.length };
}
}
class SearchEvaluator {
constructor() {
this.cache = new EmbeddingCache();
}
async rateLimitedDelay(model) {
if (model.rateLimit && model.rateLimitDelayMs) {
await new Promise(resolve => setTimeout(resolve, model.rateLimitDelayMs));
}
}
async getEmbedding(text, model) {
// Check cache first
const cached = await this.cache.getCachedEmbedding(model, text);
if (cached) return cached;
const headers = { 'Content-Type': 'application/json' };
let body, endpoint;
if (model.type === 'mistral') {
if (model.apiKey) {
headers['Authorization'] = `Bearer ${model.apiKey.replace('${AI_EMBEDDINGS_API_KEY}', process.env.AI_EMBEDDINGS_API_KEY || '')}`;
}
body = { model: model.name, input: [text] };
endpoint = model.endpoint;
} else {
body = { model: model.name, prompt: text };
endpoint = model.endpoint;
}
try {
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify(body)
});
if (!response.ok) {
if (response.status === 429 && model.rateLimit) {
console.log(` ⚠️ Rate limited, waiting...`);
await new Promise(resolve => setTimeout(resolve, 10000));
return this.getEmbedding(text, model);
}
throw new Error(`API error ${response.status}: ${await response.text()}`);
}
const data = await response.json();
const embedding = model.type === 'mistral' ? data.data[0].embedding : data.embedding;
// Cache the result
await this.cache.setCachedEmbedding(model, text, embedding);
return embedding;
} catch (error) {
console.error(`❌ Failed to get embedding: ${error.message}`);
throw error;
}
}
constructToolText(item, maxLength = null) {
if (typeof item === 'string') {
// Even for string inputs, don't truncate to match real app behavior
return item.toLowerCase();
}
// EXACT match to embeddings.ts createContentString() - NO TRUNCATION
const parts = [
item.name,
item.description || '',
...(item.tags || []),
...(item.domains || []),
...(item.phases || [])
];
const contentString = parts.filter(Boolean).join(' ').toLowerCase();
// CRITICAL: No truncation! Return full content like real app
return contentString;
}
calculateOptimalBatchSize(model) {
// Factors that ACTUALLY matter for batching individual API calls:
// 1. Rate limiting aggressiveness
if (model.rateLimit && model.rateLimitDelayMs > 2000) {
return 5; // Conservative batching for heavily rate-limited APIs
}
// 2. API latency expectations
if (model.type === 'ollama') {
return 15; // Local APIs are fast, can handle larger batches
} else if (model.type === 'mistral') {
return 10; // Remote APIs might be slower, medium batches
}
// 3. Progress reporting frequency preference
// For 185 tools:
// - Batch size 10 = 19 progress updates
// - Batch size 15 = 13 progress updates
// - Batch size 20 = 10 progress updates
return 15; // Good balance for ~13 progress updates
}
async createBatchEmbeddings(items, model) {
const batchSize = this.calculateOptimalBatchSize(model);
const contextSize = model.contextSize || 2000; // Only for display/info
console.log(` 📦 Creating embeddings for ${items.length} items`);
console.log(` 📏 Model context: ${contextSize} chars (for reference - NOT truncating)`);
console.log(` 📋 Batch size: ${batchSize} (for progress reporting)`);
const embeddings = new Map();
let apiCalls = 0;
let cacheHits = 0;
const totalBatches = Math.ceil(items.length / batchSize);
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchNum = Math.floor(i/batchSize) + 1;
console.log(` 📋 Processing batch ${batchNum}/${totalBatches} (${batch.length} tools)`);
for (const item of batch) {
// Get FULL content (no truncation)
const text = this.constructToolText(item);
// Show actual text length for first few tools (full length!)
if (i < batchSize && batch.indexOf(item) < 3) {
const truncatedDisplay = text.length > 100 ? text.slice(0, 100) + '...' : text;
console.log(` 📝 ${item.name}: ${text.length} chars (full) - "${truncatedDisplay}"`);
}
try {
const embedding = await this.getEmbedding(text, model);
embeddings.set(item.id || item.name || text, {
text,
embedding,
metadata: item
});
const cached = await this.cache.getCachedEmbedding(model, text);
if (cached) cacheHits++; else apiCalls++;
await this.rateLimitedDelay(model);
} catch (error) {
console.warn(` ⚠️ Failed to embed: ${item.name || text.slice(0, 50)}...`);
// Log the error for debugging
if (text.length > 8000) {
console.warn(` 📏 Text was ${text.length} chars - may exceed model limits`);
}
}
}
}
// Show content length statistics
const lengths = Array.from(embeddings.values()).map(e => e.text.length);
const avgLength = lengths.reduce((a, b) => a + b, 0) / lengths.length;
const maxLength = Math.max(...lengths);
const minLength = Math.min(...lengths);
console.log(` 📊 Content stats: avg ${avgLength.toFixed(0)} chars, range ${minLength}-${maxLength} chars`);
console.log(` ✅ Created ${embeddings.size} embeddings (${apiCalls} API calls, ${cacheHits} cache hits)`);
return embeddings;
}
cosineSimilarity(a, b) {
if (!a || !b || a.length === 0 || b.length === 0) return 0;
let dotProduct = 0;
let normA = 0;
let normB = 0;
const minLength = Math.min(a.length, b.length);
for (let i = 0; i < minLength; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
if (normA === 0 || normB === 0) return 0;
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
searchSimilar(queryEmbedding, toolEmbeddings, topK = 10) {
const similarities = [];
for (const [id, data] of toolEmbeddings) {
const similarity = this.cosineSimilarity(queryEmbedding, data.embedding);
similarities.push({
id,
similarity,
metadata: data.metadata,
text: data.text
});
}
return similarities
.sort((a, b) => b.similarity - a.similarity)
.slice(0, topK);
}
calculateRetrievalMetrics(results, relevantIds, k = 10) {
const topK = results.slice(0, k);
const retrievedIds = new Set(topK.map(r => r.id));
const relevantSet = new Set(relevantIds);
// Precision@K
const relevantRetrieved = topK.filter(r => relevantSet.has(r.id)).length;
const precisionAtK = topK.length > 0 ? relevantRetrieved / topK.length : 0;
// Recall@K
const recallAtK = relevantIds.length > 0 ? relevantRetrieved / relevantIds.length : 0;
// F1@K
const f1AtK = (precisionAtK + recallAtK) > 0 ?
2 * (precisionAtK * recallAtK) / (precisionAtK + recallAtK) : 0;
// Mean Reciprocal Rank (MRR)
let mrr = 0;
for (let i = 0; i < topK.length; i++) {
if (relevantSet.has(topK[i].id)) {
mrr = 1 / (i + 1);
break;
}
}
// NDCG@K (simplified binary relevance)
let dcg = 0;
let idcg = 0;
for (let i = 0; i < k; i++) {
const rank = i + 1;
const discount = Math.log2(rank + 1);
// DCG
if (i < topK.length && relevantSet.has(topK[i].id)) {
dcg += 1 / discount;
}
// IDCG (ideal ranking)
if (i < relevantIds.length) {
idcg += 1 / discount;
}
}
const ndcgAtK = idcg > 0 ? dcg / idcg : 0;
return {
precisionAtK,
recallAtK,
f1AtK,
mrr,
ndcgAtK,
relevantRetrieved,
totalRelevant: relevantIds.length
};
}
}
class EfficientEmbeddingComparison {
constructor(configPath = './embedding-test-config.json') {
this.configPath = configPath;
this.config = null;
this.tools = [];
this.evaluator = new SearchEvaluator();
// Test queries tailored to the actual tools.yaml content
this.testQueries = [
{
query: "memory forensics RAM analysis",
keywords: ["memory", "forensics", "volatility", "ram", "dump", "analysis"],
category: "memory_analysis"
},
{
query: "network packet capture traffic analysis",
keywords: ["network", "packet", "pcap", "wireshark", "traffic", "capture"],
category: "network_analysis"
},
{
query: "malware reverse engineering binary analysis",
keywords: ["malware", "reverse", "engineering", "ghidra", "binary", "disassemble"],
category: "malware_analysis"
},
{
query: "digital forensics disk imaging",
keywords: ["forensics", "disk", "imaging", "autopsy", "investigation", "evidence"],
category: "disk_forensics"
},
{
query: "incident response threat hunting",
keywords: ["incident", "response", "threat", "hunting", "investigation", "compromise"],
category: "incident_response"
},
{
query: "mobile device smartphone forensics",
keywords: ["mobile", "smartphone", "android", "ios", "device", "cellebrite"],
category: "mobile_forensics"
},
{
query: "timeline analysis event correlation",
keywords: ["timeline", "analysis", "correlation", "events", "plaso", "timesketch"],
category: "timeline_analysis"
},
{
query: "registry analysis windows artifacts",
keywords: ["registry", "windows", "artifacts", "regripper", "hives", "keys"],
category: "registry_analysis"
},
{
query: "cloud forensics container analysis",
keywords: ["cloud", "container", "docker", "virtualization", "aws", "azure"],
category: "cloud_forensics"
},
{
query: "blockchain cryptocurrency investigation",
keywords: ["blockchain", "cryptocurrency", "bitcoin", "chainalysis", "transaction"],
category: "blockchain_analysis"
}
];
console.log('[INIT] Efficient embedding comparison initialized');
}
async loadConfig() {
try {
const configData = await fs.readFile(this.configPath, 'utf8');
this.config = JSON.parse(configData);
console.log(`[CONFIG] Loaded ${this.config.models.length} models`);
} catch (error) {
console.error('[CONFIG] Failed to load configuration:', error.message);
throw error;
}
}
async loadTools() {
try {
const yamlContent = await fs.readFile(this.config.toolsYamlPath, 'utf8');
const data = yaml.load(yamlContent);
// Extract tools (flexible - handle different YAML structures)
this.tools = data.tools || data.entries || data.applications || data;
if (!Array.isArray(this.tools)) {
this.tools = Object.values(this.tools);
}
// Filter out concepts and ensure required fields
this.tools = this.tools.filter(tool =>
tool &&
tool.type !== 'concept' &&
(tool.name || tool.title) &&
(tool.description || tool.summary)
);
// Normalize tool structure
this.tools = this.tools.map((tool, index) => ({
id: tool.id || tool.name || tool.title || `tool_${index}`,
name: tool.name || tool.title,
description: tool.description || tool.summary || '',
tags: tool.tags || [],
domains: tool.domains || tool.categories || [],
phases: tool.phases || [],
platforms: tool.platforms || [],
type: tool.type || 'tool',
skillLevel: tool.skillLevel,
license: tool.license
}));
console.log(`[DATA] Loaded ${this.tools.length} tools from ${this.config.toolsYamlPath}`);
// Show some statistics
const domainCounts = {};
const tagCounts = {};
this.tools.forEach(tool => {
(tool.domains || []).forEach(domain => {
domainCounts[domain] = (domainCounts[domain] || 0) + 1;
});
(tool.tags || []).forEach(tag => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
const topDomains = Object.entries(domainCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5)
.map(([domain, count]) => `${domain}(${count})`)
.join(', ');
console.log(`[DATA] Top domains: ${topDomains}`);
console.log(`[DATA] Sample tools: ${this.tools.slice(0, 3).map(t => t.name).join(', ')}`);
if (this.tools.length === 0) {
throw new Error('No valid tools found in YAML file');
}
} catch (error) {
console.error('[DATA] Failed to load tools:', error.message);
throw error;
}
}
findRelevantTools(query) {
const queryLower = query.query.toLowerCase();
const keywords = query.keywords.map(k => k.toLowerCase());
const relevantTools = this.tools.filter(tool => {
// Build searchable text from all tool metadata
const searchableFields = [
tool.name || '',
tool.description || '',
(tool.tags || []).join(' '),
(tool.domains || []).join(' '),
(tool.phases || []).join(' '),
(tool.platforms || []).join(' ')
];
const toolText = searchableFields.join(' ').toLowerCase();
// Check for keyword matches
const hasKeywordMatch = keywords.some(keyword => toolText.includes(keyword));
// Check for query word matches (words longer than 3 chars)
const queryWords = queryLower.split(' ').filter(word => word.length > 3);
const hasQueryWordMatch = queryWords.some(word => toolText.includes(word));
// Check for domain-specific matches
const isDomainRelevant = query.category && tool.domains &&
tool.domains.some(domain => domain.includes(query.category.replace('_', '-')));
return hasKeywordMatch || hasQueryWordMatch || isDomainRelevant;
});
console.log(` 🎯 Found ${relevantTools.length} relevant tools for "${query.query}"`);
// Log some examples for debugging
if (relevantTools.length > 0) {
console.log(` 📋 Examples: ${relevantTools.slice(0, 3).map(t => t.name).join(', ')}`);
}
return relevantTools.map(tool => tool.id || tool.name);
}
async testSearchPerformance(model) {
console.log(` 🔍 Testing search performance...`);
// Create embeddings for all tools
const toolEmbeddings = await this.evaluator.createBatchEmbeddings(this.tools, model);
const results = [];
let totalApiCalls = 0;
for (const testQuery of this.testQueries) {
console.log(` 📋 Query: "${testQuery.query}"`);
// Get query embedding
const queryEmbedding = await this.evaluator.getEmbedding(testQuery.query, model);
totalApiCalls++;
await this.evaluator.rateLimitedDelay(model);
// Find relevant tools for this query
const relevantIds = this.findRelevantTools(testQuery);
console.log(` 📊 Found ${relevantIds.length} relevant tools`);
if (relevantIds.length === 0) {
console.log(` ⚠️ No relevant tools found, skipping metrics calculation`);
continue;
}
// Perform search
const searchResults = this.evaluator.searchSimilar(queryEmbedding, toolEmbeddings, 20);
// Calculate metrics for different k values
const metrics = {};
for (const k of [1, 3, 5, 10]) {
metrics[`k${k}`] = this.evaluator.calculateRetrievalMetrics(searchResults, relevantIds, k);
}
results.push({
query: testQuery.query,
category: testQuery.category,
relevantCount: relevantIds.length,
searchResults: searchResults.slice(0, 5), // Top 5 for display
metrics
});
// Display results
console.log(` 🎯 Top results:`);
searchResults.slice(0, 3).forEach((result, i) => {
const isRelevant = relevantIds.includes(result.id) ? '✓' : '✗';
console.log(` ${i+1}. ${isRelevant} ${result.metadata.name} (${(result.similarity*100).toFixed(1)}%)`);
});
console.log(` 📈 P@5: ${(metrics.k5.precisionAtK*100).toFixed(1)}% | R@5: ${(metrics.k5.recallAtK*100).toFixed(1)}% | NDCG@5: ${(metrics.k5.ndcgAtK*100).toFixed(1)}%`);
}
return { results, totalApiCalls };
}
async testSemanticUnderstanding(model) {
console.log(` 🧠 Testing semantic understanding...`);
const semanticTests = [
{
primary: "memory forensics",
synonyms: ["RAM analysis", "volatile memory examination", "memory dump investigation"],
unrelated: ["file compression", "web browser", "text editor"]
},
{
primary: "network analysis",
synonyms: ["packet inspection", "traffic monitoring", "protocol analysis"],
unrelated: ["image editing", "music player", "calculator"]
},
{
primary: "malware detection",
synonyms: ["virus scanning", "threat identification", "malicious code analysis"],
unrelated: ["video converter", "password manager", "calendar app"]
}
];
let totalCorrect = 0;
let totalTests = 0;
let apiCalls = 0;
for (const test of semanticTests) {
console.log(` 🔤 Testing: "${test.primary}"`);
const primaryEmbedding = await this.evaluator.getEmbedding(test.primary, model);
apiCalls++;
await this.evaluator.rateLimitedDelay(model);
// Test synonyms (should be similar)
for (const synonym of test.synonyms) {
const synonymEmbedding = await this.evaluator.getEmbedding(synonym, model);
apiCalls++;
const synonymSim = this.evaluator.cosineSimilarity(primaryEmbedding, synonymEmbedding);
console.log(` ✓ "${synonym}": ${(synonymSim*100).toFixed(1)}%`);
await this.evaluator.rateLimitedDelay(model);
}
// Test unrelated terms (should be dissimilar)
for (const unrelated of test.unrelated) {
const unrelatedEmbedding = await this.evaluator.getEmbedding(unrelated, model);
apiCalls++;
const unrelatedSim = this.evaluator.cosineSimilarity(primaryEmbedding, unrelatedEmbedding);
console.log(` ✗ "${unrelated}": ${(unrelatedSim*100).toFixed(1)}%`);
await this.evaluator.rateLimitedDelay(model);
}
// Calculate semantic coherence
const avgSynonymSim = await this.calculateAvgSimilarity(primaryEmbedding, test.synonyms, model);
const avgUnrelatedSim = await this.calculateAvgSimilarity(primaryEmbedding, test.unrelated, model);
const isCorrect = avgSynonymSim > avgUnrelatedSim;
if (isCorrect) totalCorrect++;
totalTests++;
console.log(` 📊 Synonyms: ${(avgSynonymSim*100).toFixed(1)}% | Unrelated: ${(avgUnrelatedSim*100).toFixed(1)}% ${isCorrect ? '✓' : '✗'}`);
}
return {
accuracy: totalCorrect / totalTests,
correctTests: totalCorrect,
totalTests,
apiCalls
};
}
async calculateAvgSimilarity(baseEmbedding, terms, model) {
let totalSim = 0;
for (const term of terms) {
const embedding = await this.evaluator.getEmbedding(term, model);
const sim = this.evaluator.cosineSimilarity(baseEmbedding, embedding);
totalSim += sim;
await this.evaluator.rateLimitedDelay(model);
}
return totalSim / terms.length;
}
async benchmarkPerformance(model) {
console.log(` ⚡ Benchmarking performance...`);
const testTexts = this.tools.slice(0, 10).map(tool => `${tool.name} ${tool.description}`.slice(0, 500));
const times = [];
let apiCalls = 0;
console.log(` 🏃 Processing ${testTexts.length} texts...`);
for (const text of testTexts) {
const start = Date.now();
await this.evaluator.getEmbedding(text, model);
const time = Date.now() - start;
times.push(time);
apiCalls++;
await this.evaluator.rateLimitedDelay(model);
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
console.log(` 📊 Avg: ${avgTime.toFixed(0)}ms | Min: ${minTime}ms | Max: ${maxTime}ms`);
return {
avgLatency: avgTime,
minLatency: minTime,
maxLatency: maxTime,
throughput: 1000 / avgTime, // requests per second
apiCalls
};
}
async testModel(model) {
console.log(`\n🧪 Testing ${model.name} (${model.type})...`);
const startTime = Date.now();
let totalApiCalls = 0;
try {
// 1. Search Performance Testing
const searchResults = await this.testSearchPerformance(model);
totalApiCalls += searchResults.totalApiCalls;
// 2. Semantic Understanding Testing
const semanticResults = await this.testSemanticUnderstanding(model);
totalApiCalls += semanticResults.apiCalls;
// 3. Performance Benchmarking
const perfResults = await this.benchmarkPerformance(model);
totalApiCalls += perfResults.apiCalls;
const totalTime = Date.now() - startTime;
console.log(`${model.name} completed in ${(totalTime/1000).toFixed(1)}s (${totalApiCalls} API calls)`);
return {
searchPerformance: searchResults.results,
semanticUnderstanding: semanticResults,
performance: perfResults,
totalTime,
totalApiCalls
};
} catch (error) {
console.error(`${model.name} failed:`, error.message);
throw error;
}
}
calculateOverallScore(results) {
// Calculate average metrics across all queries
const searchMetrics = results.searchPerformance.filter(r => r.metrics && Object.keys(r.metrics).length > 0);
if (searchMetrics.length === 0) {
console.warn('⚠️ No search metrics available for scoring - may indicate relevance matching issues');
return {
overall: 0,
components: {
precision5: 0,
recall5: 0,
ndcg5: 0,
mrr: 0,
semanticAccuracy: results.semanticUnderstanding?.accuracy || 0,
throughput: results.performance?.throughput || 0
},
warning: 'No search metrics available'
};
}
console.log(`📊 Calculating score from ${searchMetrics.length} valid search results`);
const avgPrecision5 = searchMetrics.reduce((sum, r) => sum + (r.metrics.k5?.precisionAtK || 0), 0) / searchMetrics.length;
const avgRecall5 = searchMetrics.reduce((sum, r) => sum + (r.metrics.k5?.recallAtK || 0), 0) / searchMetrics.length;
const avgNDCG5 = searchMetrics.reduce((sum, r) => sum + (r.metrics.k5?.ndcgAtK || 0), 0) / searchMetrics.length;
const avgMRR = searchMetrics.reduce((sum, r) => sum + (r.metrics.k5?.mrr || 0), 0) / searchMetrics.length;
const semanticAccuracy = results.semanticUnderstanding?.accuracy || 0;
const throughput = results.performance?.throughput || 0;
// Weighted overall score
const weights = {
precision: 0.25,
recall: 0.25,
ndcg: 0.20,
semantic: 0.20,
speed: 0.10
};
const normalizedThroughput = Math.min(throughput / 10, 1); // Normalize to 0-1 (10 req/s = 1.0)
const overall = (
avgPrecision5 * weights.precision +
avgRecall5 * weights.recall +
avgNDCG5 * weights.ndcg +
semanticAccuracy * weights.semantic +
normalizedThroughput * weights.speed
);
return {
overall,
components: {
precision5: avgPrecision5,
recall5: avgRecall5,
ndcg5: avgNDCG5,
mrr: avgMRR,
semanticAccuracy,
throughput
}
};
}
printResults(modelResults) {
console.log(`\n${'='.repeat(80)}`);
console.log("🏆 EFFICIENT EMBEDDING MODEL COMPARISON RESULTS");
console.log(`${'='.repeat(80)}`);
const scores = modelResults.map(mr => ({
model: mr.model,
score: this.calculateOverallScore(mr.results),
results: mr.results
})).sort((a, b) => b.score.overall - a.score.overall);
console.log(`\n🥇 OVERALL RANKINGS:`);
scores.forEach((score, index) => {
console.log(` ${index + 1}. ${score.model.name}: ${(score.score.overall * 100).toFixed(1)}% overall`);
});
console.log(`\n📊 DETAILED METRICS:`);
console.log(`\n 🎯 Search Performance (Precision@5):`);
scores.forEach(score => {
console.log(` ${score.model.name}: ${(score.score.components.precision5 * 100).toFixed(1)}%`);
});
console.log(`\n 🔍 Search Performance (Recall@5):`);
scores.forEach(score => {
console.log(` ${score.model.name}: ${(score.score.components.recall5 * 100).toFixed(1)}%`);
});
console.log(`\n 📈 Search Quality (NDCG@5):`);
scores.forEach(score => {
console.log(` ${score.model.name}: ${(score.score.components.ndcg5 * 100).toFixed(1)}%`);
});
console.log(`\n 🧠 Semantic Understanding:`);
scores.forEach(score => {
console.log(` ${score.model.name}: ${(score.score.components.semanticAccuracy * 100).toFixed(1)}%`);
});
console.log(`\n ⚡ Performance (req/s):`);
scores.forEach(score => {
console.log(` ${score.model.name}: ${score.score.components.throughput.toFixed(1)} req/s`);
});
// Winner analysis
const winner = scores[0];
console.log(`\n🏆 WINNER: ${winner.model.name}`);
console.log(` Overall Score: ${(winner.score.overall * 100).toFixed(1)}%`);
console.log(` Best for: ${this.getBestUseCase(winner.score.components)}`);
// Summary stats
const totalQueries = modelResults[0]?.results.searchPerformance.length || 0;
const totalTools = this.tools.length;
console.log(`\n📋 Test Summary:`);
console.log(` Tools tested: ${totalTools}`);
console.log(` Search queries: ${totalQueries}`);
console.log(` Models compared: ${scores.length}`);
console.log(` Total API calls: ${modelResults.reduce((sum, mr) => sum + mr.results.totalApiCalls, 0)}`);
}
getBestUseCase(components) {
const strengths = [];
if (components.precision5 > 0.7) strengths.push("High precision");
if (components.recall5 > 0.7) strengths.push("High recall");
if (components.semanticAccuracy > 0.8) strengths.push("Semantic understanding");
if (components.throughput > 5) strengths.push("High performance");
return strengths.length > 0 ? strengths.join(", ") : "General purpose";
}
async run() {
try {
console.log("🚀 EFFICIENT EMBEDDING MODEL COMPARISON");
console.log("=====================================");
await this.loadConfig();
await this.loadTools();
console.log(`\n📋 Test Overview:`);
console.log(` Models: ${this.config.models.length}`);
console.log(` Tools: ${this.tools.length}`);
console.log(` Search queries: ${this.testQueries.length}`);
console.log(` Cache: ${this.evaluator.cache.cacheDir}`);
const modelResults = [];
for (const model of this.config.models) {
try {
const results = await this.testModel(model);
modelResults.push({ model, results });
} catch (error) {
console.error(`❌ Skipping ${model.name}: ${error.message}`);
}
}
if (modelResults.length === 0) {
throw new Error('No models completed testing successfully');
}
this.printResults(modelResults);
} catch (error) {
console.error('\n❌ Test failed:', error.message);
console.log('\nDebugging steps:');
console.log('1. Verify tools.yaml exists and contains valid tool data');
console.log('2. Check model endpoints are accessible');
console.log('3. For Ollama: ensure models are pulled and ollama serve is running');
console.log('4. For Mistral: verify AI_EMBEDDINGS_API_KEY environment variable');
}
}
}
// Execute
const configArg = process.argv.find(arg => arg.startsWith('--config='));
const configPath = configArg ? configArg.split('=')[1] : './embedding-test-config.json';
(async () => {
const comparison = new EfficientEmbeddingComparison(configPath);
await comparison.run();
})().catch(console.error);

333
find-duplicates.mjs Normal file
View File

@@ -0,0 +1,333 @@
#!/usr/bin/env node
// find-duplicate-functions.mjs
// Usage:
// node find-duplicate-functions.mjs [rootDir] [--mode exact|struct] [--min-lines N] [--json]
// Example:
// node find-duplicate-functions.mjs . --mode struct --min-lines 3
import fs from "fs";
import path from "path";
import * as url from "url";
import ts from "typescript";
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
/** -------- CLI OPTIONS -------- */
const args = process.argv.slice(2);
let rootDir = ".";
let mode = "struct"; // "exact" | "struct"
let minLines = 3;
let outputJson = false;
for (let i = 0; i < args.length; i++) {
const a = args[i];
if (!a.startsWith("--") && rootDir === ".") {
rootDir = a;
} else if (a === "--mode") {
mode = (args[++i] || "struct").toLowerCase();
if (!["exact", "struct"].includes(mode)) {
console.error("Invalid --mode. Use 'exact' or 'struct'.");
process.exit(1);
}
} else if (a === "--min-lines") {
minLines = parseInt(args[++i] || "3", 10);
} else if (a === "--json") {
outputJson = true;
}
}
/** -------- FILE DISCOVERY -------- */
const DEFAULT_IGNORES = new Set([
"node_modules",
".git",
".next",
".vercel",
"dist",
"build",
".astro", // Astro's generated cache dir
]);
const VALID_EXTS = new Set([".ts", ".tsx", ".astro", ".mts", ".cts"]);
function walk(dir) {
/** @type {string[]} */
const out = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) {
if (DEFAULT_IGNORES.has(e.name)) continue;
out.push(...walk(p));
} else if (e.isFile() && VALID_EXTS.has(path.extname(e.name))) {
out.push(p);
}
}
return out;
}
/** -------- ASTRO CODE EXTRACTION --------
* Extract TS/JS code from:
* - frontmatter: --- ... ---
* - <script ...> ... </script>
*/
function extractCodeFromAstro(source) {
/** @type {{code:string, offset:number}[]} */
const blocks = [];
// Frontmatter (must be at top in Astro)
// Match the FIRST pair of --- ... ---
const fm = source.startsWith("---")
? (() => {
const end = source.indexOf("\n---", 3);
if (end !== -1) {
const front = source.slice(3, end + 1); // include trailing \n
return { start: 0, end: end + 4, code: front };
}
return null;
})()
: null;
if (fm) {
// offset for line numbers is after the first '---\n'
blocks.push({ code: fm.code, offset: 4 }); // rough; well fix line numbers via positions later
}
// <script ...> ... </script>
const scriptRe = /<script\b[^>]*>([\s\S]*?)<\/script>/gi;
let m;
while ((m = scriptRe.exec(source))) {
const code = m[1] || "";
blocks.push({ code, offset: indexToLine(source, m.index) });
}
return blocks;
}
/** -------- UTIL: index -> 1-based line -------- */
function indexToLine(text, idx) {
let line = 1;
for (let i = 0; i < idx && i < text.length; i++) {
if (text.charCodeAt(i) === 10) line++;
}
return line;
}
/** -------- AST HELPERS -------- */
function createSourceFile(virtualPath, code) {
return ts.createSourceFile(
virtualPath,
code,
ts.ScriptTarget.Latest,
/*setParentNodes*/ true,
virtualPath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
);
}
// Normalize AST to a structural signature string
function structuralSignature(node) {
/** @type {string[]} */
const parts = [];
const visit = (n) => {
// Skip trivia: comments/whitespace are already not in AST
const kindName = ts.SyntaxKind[n.kind] || `K${n.kind}`;
switch (n.kind) {
case ts.SyntaxKind.Identifier:
parts.push("Id");
return;
case ts.SyntaxKind.PrivateIdentifier:
parts.push("PrivId");
return;
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
case ts.SyntaxKind.TemplateHead:
case ts.SyntaxKind.TemplateMiddle:
case ts.SyntaxKind.TemplateTail:
parts.push("Str");
return;
case ts.SyntaxKind.NumericLiteral:
parts.push("Num");
return;
case ts.SyntaxKind.TrueKeyword:
case ts.SyntaxKind.FalseKeyword:
parts.push("Bool");
return;
case ts.SyntaxKind.NullKeyword:
case ts.SyntaxKind.UndefinedKeyword:
parts.push("Nil");
return;
case ts.SyntaxKind.PropertyAssignment:
case ts.SyntaxKind.ShorthandPropertyAssignment:
case ts.SyntaxKind.MethodDeclaration:
case ts.SyntaxKind.MethodSignature:
parts.push("Prop");
break;
default:
parts.push(kindName);
}
n.forEachChild(visit);
};
visit(node);
return parts.join("|");
}
function getFunctionInfo(sf, filePath) {
/** @type {Array<{
name: string,
bodyText: string,
structKey: string,
start: number,
end: number,
startLine: number,
endLine: number
}>} */
const out = [];
const addFunc = (nameNode, bodyNode) => {
if (!bodyNode) return;
const bodyText = bodyNode.getText(sf).trim();
const start = bodyNode.getStart(sf);
const end = bodyNode.getEnd();
const { line: startLine } = sf.getLineAndCharacterOfPosition(start);
const { line: endLine } = sf.getLineAndCharacterOfPosition(end);
const name =
nameNode && ts.isIdentifier(nameNode) ? nameNode.escapedText.toString() : "(anonymous)";
// min-lines filter
const lines = bodyText.split(/\r?\n/).filter(Boolean);
if (lines.length < minLines) return;
// structural signature from the body
const structKey = structuralSignature(bodyNode);
out.push({
name,
bodyText,
structKey,
start,
end,
startLine: startLine + 1,
endLine: endLine + 1,
});
};
const visit = (node) => {
if (ts.isFunctionDeclaration(node) && node.body) {
addFunc(node.name ?? null, node.body);
} else if (
ts.isFunctionExpression(node) ||
ts.isArrowFunction(node)
) {
// find name if its assigned: const foo = () => {}
let name = null;
if (node.parent && ts.isVariableDeclaration(node.parent) && node.parent.name) {
name = node.parent.name;
} else if (
node.parent &&
ts.isPropertyAssignment(node.parent) &&
ts.isIdentifier(node.parent.name)
) {
name = node.parent.name;
} else if (node.name) {
name = node.name;
}
if (node.body) addFunc(name, node.body);
} else if (ts.isMethodDeclaration(node) && node.body) {
addFunc(node.name, node.body);
}
node.forEachChild(visit);
};
visit(sf);
return out;
}
/** -------- MAIN SCAN -------- */
const files = walk(path.resolve(process.cwd(), rootDir));
/** Maps from hash -> occurrences */
const groups = new Map();
/** Helper for exact hash */
import crypto from "crypto";
const exactHash = (text) => crypto.createHash("sha1").update(text.replace(/\s+/g, " ").trim()).digest("hex");
for (const file of files) {
try {
const ext = path.extname(file).toLowerCase();
const raw = fs.readFileSync(file, "utf8");
/** @type {Array<{virtualPath:string, code:string, lineOffset:number}>} */
const codeUnits = [];
if (ext === ".astro") {
const blocks = extractCodeFromAstro(raw);
blocks.forEach((b, i) => {
codeUnits.push({
virtualPath: file + `#astro${i + 1}.ts`,
code: b.code,
lineOffset: b.offset || 1,
});
});
} else {
codeUnits.push({ virtualPath: file, code: raw, lineOffset: 1 });
}
for (const { virtualPath, code, lineOffset } of codeUnits) {
const sf = createSourceFile(virtualPath, code);
const funcs = getFunctionInfo(sf, file);
for (const f of funcs) {
const key =
mode === "exact" ? exactHash(f.bodyText) : crypto.createHash("sha1").update(f.structKey).digest("hex");
const item = {
file,
where:
ext === ".astro"
? `${path.relative(process.cwd(), file)}:${f.startLine + lineOffset - 1}-${f.endLine + lineOffset - 1}`
: `${path.relative(process.cwd(), file)}:${f.startLine}-${f.endLine}`,
name: f.name,
lines: f.endLine - f.startLine + 1,
preview: f.bodyText.split(/\r?\n/).slice(0, 5).join("\n") + (f.endLine - f.startLine + 1 > 5 ? "\n..." : ""),
};
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(item);
}
}
} catch (e) {
console.warn(`⚠️ Skipping ${file}: ${e.message}`);
}
}
/** -------- REPORT -------- */
const dupes = [...groups.entries()]
.map(([key, arr]) => ({ key, items: arr }))
.filter((g) => g.items.length > 1)
.sort((a, b) => b.items.length - a.items.length);
if (outputJson) {
console.log(JSON.stringify({ mode, minLines, groups: dupes }, null, 2));
process.exit(0);
}
if (dupes.length === 0) {
console.log(`✅ No duplicate functions found (mode=${mode}, min-lines=${minLines}).`);
process.exit(0);
}
console.log(`\nFound ${dupes.length} duplicate group(s) (mode=${mode}, min-lines=${minLines}):\n`);
dupes.forEach((g, i) => {
console.log(`== Group ${i + 1} (${g.items.length} matches) ==`);
const example = g.items[0];
console.log(` Sample (${example.lines} lines) from ${example.where}${example.name ? ` [${example.name}]` : ""}`);
console.log(" ---");
console.log(indent(example.preview, " "));
console.log(" ---");
g.items.forEach((it) => {
console.log(`${it.where}${it.name ? ` [${it.name}]` : ""} (${it.lines} lines)`);
});
console.log();
});
function indent(s, pre) {
return s
.split("\n")
.map((l) => pre + l)
.join("\n");
}

198
helpful_prompts.md Normal file
View File

@@ -0,0 +1,198 @@
# These Prompts can be used as system prompts for an AI model which supports drafting knowledgebase articles or quality-check the database, as well as generate new yaml entries
```md
You maintain a forensic tools database. **NEVER output complete YAML files** - only specific entries or updates requested.
## Database Structure
- `tools[]` - Software, methods, concepts with German names/descriptions
- `domains[]`, `phases[]`, `scenarios[]` - Forensic categories
- Tags must be English, relationships link existing entries
## Entry Format
```yaml
- name: "German Name"
type: software|method|concept
description: >- # German, 350-550 chars, embedding-optimized
skillLevel: novice|beginner|intermediate|advanced|expert
url: https://...
domains: [domain-ids]
phases: [phase-ids]
tags: [english-keywords]
related_concepts: [existing-concepts]
related_software: [existing-tools]
# Software only: platforms, license, accessType
```
## Description Rules (Critical for Semantic Search)
1. **Start with function** - what it does, not what it is
2. **Use forensic terminology** - RAM-dump, artifact-extraction, timeline-analysis
3. **Specify capabilities** - mention specific features and use cases
4. **Context matters** - when/why to use this tool
5. **Differentiate** - what makes it unique vs similar tools
**Bad**: "Ein Tool zur Analyse von Daten"
**Good**: "RAM-Dump-Analyse für versteckte Prozesse und Malware-Artefakte in Windows-Systemen"
## Tag Categories (English only)
- Functions: `artifact-extraction`, `timeline-analysis`, `memory-analysis`
- Interface: `gui`, `commandline`, `api`
- Scenarios: `scenario:memory_dump`, `scenario:file_recovery`
- Domains: `malware-analysis`, `incident-response`
## Quality Checks
Always flag: inconsistent naming, generic descriptions, broken relationships, missing metadata, poor embedding optimization.
## Output Format
For additions: `# Addition to tools array` + YAML entry
For updates: `# Update existing: "Tool Name"` + changed fields only
Always explain changes and flag quality issues found.
## Data Model
A method is the exact description of a reproducible process to archieve a specific result.
A Software is a computer program which processes data in some way to implement a process.
A concept is a set of high-level background knowledge which is needed to understand and properly execute a method and/or operate a software.
For the knowledgebase attribute:
If the entry (no matter of be it a software, method or concept) can be fully described in the description, this may not be needed. If there is more detailed action or knowledge necessary, the knowledgebase article would expand on the description here for deeper information. Nonetheless, the description should work with semantic search.
```
```md
You generate knowledgebase articles for a forensic tools database. Create practical, detailed documentation that helps users effectively use forensic tools and methods.
## Content Focus
- **Practical guides** - Installation, configuration, usage workflows
- **Real-world scenarios** - Case studies, investigation examples
- **Technical deep-dives** - Advanced features, troubleshooting
- **Best practices** - Methodology, evidence handling, efficiency tips
- **Integration guides** - How tools work together in investigations
## Entry Structure
```markdown
---
title: "German Title - Clear and Descriptive"
description: "German summary (150-300 chars) explaining what users will learn"
author: "Author Name"
last_updated: 2024-01-15
difficulty: novice|beginner|intermediate|advanced|expert
categories: ["installation", "configuration", "analysis", "troubleshooting"]
tags: ["english-keywords", "tool-specific", "technique-related"]
gated_content: false # content can be gated if still needing verification or is secret information
tool_name: "Exact Tool Name from YAML DB" # Optional - if related to specific tool
related_tools: ["Tool 1", "Tool 2"] # Optional - other relevant tools
published: true
---
# Article Content Here
```
## Content Guidelines
### Title & Description
- **German titles** - Clear, specific, actionable
- **Descriptions** optimize for search - mention key concepts, tools, outcomes
- Examples: "Volatility 3 Installation unter Windows 11", "Timeline-Analyse mit Autopsy für Incident Response"
### Categories (German, common patterns)
- `installation` - Setup and deployment guides
- `configuration` - Settings and customization
- `analysis` - Investigation techniques and workflows
- `troubleshooting` - Problem solving and debugging
- `integration` - Multi-tool workflows
- `case-study` - Real-world application examples
### Tags (English, specific)
- Tool names: `autopsy`, `volatility`, `wireshark`
- Techniques: `timeline-analysis`, `memory-forensics`, `network-analysis`
- Platforms: `windows`, `linux`, `macos`
- Scenarios: `malware-investigation`, `data-recovery`, `incident-response`
- File types: `registry`, `logs`, `disk-images`, `memory-dumps`
### Content Structure
1. **Problem/Context** - What investigation challenge this addresses
2. **Prerequisites** - Required knowledge, tools, system requirements
3. **Step-by-step process** - Clear, numbered instructions
4. **Screenshots/Examples** - Visual aids for complex procedures
5. **Common issues** - Troubleshooting section
6. **Next steps** - What to do with results, related techniques
## Quality Standards
### Technical Accuracy
- Verify all commands, file paths, and procedures
- Include version-specific information where relevant
- Test instructions on specified platforms
- Reference official documentation
### Practical Value
- Focus on real investigation scenarios
- Include time estimates for procedures
- Explain why each step is necessary
- Provide context for forensic methodology
### Documentation Quality
- Clear, concise German prose
- Consistent formatting and terminology
- Proper code blocks and syntax highlighting
- Logical information hierarchy
## Database Integration
**Tool Relationships**: When `tool_name` is specified, ensure:
- Exact match to YAML database entry name
- Consistent skill level alignment
- Complementary information to tool description
- Cross-references to related tools from database
**Semantic Consistency**: Use terminology that aligns with:
- YAML tool descriptions and tags
- Forensic domain vocabulary
- Investigation phase terminology
## Content Types
### Installation Guides
```markdown
# Volatility 3 Installation unter Ubuntu 22.04
Schritt-für-Schritt-Anleitung für die Installation von Volatility 3
auf Ubuntu-Systemen für forensische RAM-Analyse.
## Systemanforderungen
- Ubuntu 22.04 LTS oder neuer
- Python 3.8+
- 8GB RAM minimum für größere Memory-Dumps
```
### Analysis Workflows
```markdown
# Timeline-Analyse mit Autopsy: Von der Akquisition zur Ergebnispräsentation
Vollständiger Workflow für die chronologische Rekonstruktion von
Benutzeraktivitäten bei forensischen Untersuchungen.
## Szenario
Untersuchung eines verdächtigen Arbeitsplatz-PCs nach Datenleck...
```
### Troubleshooting Guides
```markdown
# Autopsy Performance-Optimierung für große Datenträger
Lösungsansätze für häufige Performance-Probleme bei der Analyse
von Datenträgern über 1TB mit Autopsy.
## Häufige Symptome
- Langsame Indizierung bei großen Images...
```
## Output Format
Always provide complete markdown file content including:
- Full frontmatter with all required fields
- Well-structured content with headers
- Code blocks where appropriate
- Clear, actionable instructions
- German content with English technical terms preserved
- dont hallucinate links, only provide if considered verified, but mark any links which would need verification
```

View File

@@ -10,13 +10,14 @@
"astro": "astro"
},
"dependencies": {
"@astrojs/node": "^9.3.0",
"astro": "^5.12.3",
"@astrojs/node": "^9.4.3",
"astro": "^5.13.7",
"cookie": "^1.0.2",
"dotenv": "^16.4.5",
"jose": "^5.2.0",
"dotenv": "^16.6.1",
"jose": "^5.10.0",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.2",
"simple-boost": "^2.0.2",
"zod": "^3.25.76"
},
"devDependencies": {

View File

@@ -1,389 +0,0 @@
// src/js/auditTrailRenderer.js
import { auditService } from '../../src/utils/auditService.js';
export class AuditTrailRenderer {
constructor(containerId, options = {}) {
this.containerId = containerId;
this.options = {
title: options.title || 'KI-Entscheidungspfad',
collapsible: options.collapsible !== false,
defaultExpanded: options.defaultExpanded || false,
...options
};
this.componentId = `audit-trail-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
}
/**
* Render audit trail from raw audit data
* FIXED: Proper Promise handling
*/
render(rawAuditTrail) {
const container = document.getElementById(this.containerId);
if (!container) {
console.error(`[AUDIT RENDERER] Container ${this.containerId} not found`);
return;
}
if (!rawAuditTrail || !Array.isArray(rawAuditTrail) || rawAuditTrail.length === 0) {
this.renderEmpty();
return;
}
try {
console.log('[AUDIT RENDERER] Processing audit trail...', rawAuditTrail.length, 'entries');
// Process audit trail using the centralized service (synchronous)
const processedAudit = auditService.processAuditTrail(rawAuditTrail);
console.log('[AUDIT RENDERER] Processed audit:', processedAudit);
if (processedAudit && processedAudit.phases && processedAudit.phases.length > 0) {
this.renderProcessed(processedAudit);
// Attach event handlers after DOM is updated
setTimeout(() => this.attachEventHandlers(), 0);
} else {
console.warn('[AUDIT RENDERER] No processed audit data');
this.renderEmpty();
}
} catch (error) {
console.error('[AUDIT RENDERER] Failed to render audit trail:', error);
this.renderError(error);
}
}
/**
* Render processed audit trail
*/
renderProcessed(processedAudit) {
const container = document.getElementById(this.containerId);
if (!container) return;
const detailsId = `${this.componentId}-details`;
console.log('[AUDIT RENDERER] Rendering processed audit with', processedAudit.phases.length, 'phases');
container.innerHTML = `
<div class="audit-trail-container">
<div class="audit-trail-header ${this.options.collapsible ? 'clickable' : ''}"
${this.options.collapsible ? `data-target="${detailsId}"` : ''}>
<div class="audit-trail-title">
<div class="audit-icon">
<div class="audit-icon-gradient">✓</div>
<h4>${this.options.title}</h4>
</div>
<div class="audit-stats">
<div class="stat-item">
<div class="stat-dot stat-time"></div>
<span>${auditService.formatDuration(processedAudit.totalTime)}</span>
</div>
<div class="stat-item">
<div class="stat-dot" style="background-color: ${auditService.getConfidenceColor(processedAudit.avgConfidence)}"></div>
<span>${processedAudit.avgConfidence}% Vertrauen</span>
</div>
<div class="stat-item">
<span>${processedAudit.stepCount} Schritte</span>
</div>
</div>
</div>
${this.options.collapsible ? `
<div class="toggle-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
` : ''}
</div>
<div id="${detailsId}" class="audit-trail-details ${this.options.collapsible && !this.options.defaultExpanded ? 'collapsed' : ''}">
${this.renderSummary(processedAudit)}
${this.renderProcessFlow(processedAudit)}
${this.renderTechnicalDetails(processedAudit)}
</div>
</div>
`;
console.log('[AUDIT RENDERER] HTML rendered successfully');
}
/**
* Render audit summary section
*/
renderSummary(audit) {
return `
<div class="audit-summary">
<div class="summary-header">📊 Analyse-Qualität</div>
<div class="summary-grid">
<div class="summary-stat">
<div class="summary-value success">${audit.highConfidenceSteps}</div>
<div class="summary-label">Hohe Sicherheit</div>
</div>
<div class="summary-stat">
<div class="summary-value ${audit.lowConfidenceSteps > 0 ? 'warning' : 'success'}">
${audit.lowConfidenceSteps}
</div>
<div class="summary-label">Unsichere Schritte</div>
</div>
<div class="summary-stat">
<div class="summary-value">${auditService.formatDuration(audit.totalTime)}</div>
<div class="summary-label">Verarbeitungszeit</div>
</div>
</div>
${audit.summary.keyInsights && audit.summary.keyInsights.length > 0 ? `
<div class="insights-section">
<div class="insights-header success">✓ Erkenntnisse:</div>
<ul class="insights-list">
${audit.summary.keyInsights.map(insight => `<li>${this.escapeHtml(insight)}</li>`).join('')}
</ul>
</div>
` : ''}
${audit.summary.potentialIssues && audit.summary.potentialIssues.length > 0 ? `
<div class="insights-section">
<div class="insights-header warning">⚠ Hinweise:</div>
<ul class="insights-list">
${audit.summary.potentialIssues.map(issue => `<li>${this.escapeHtml(issue)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
/**
* Render process flow section
*/
renderProcessFlow(audit) {
if (!audit.phases || audit.phases.length === 0) {
return '<div class="audit-process-flow"><p>Keine Phasen verfügbar</p></div>';
}
return `
<div class="audit-process-flow">
${audit.phases.map((phase, index) => `
<div class="phase-group ${index === audit.phases.length - 1 ? 'last-phase' : ''}">
<div class="phase-header">
<div class="phase-info">
<span class="phase-icon">${phase.icon || '📋'}</span>
<span class="phase-name">${phase.displayName || phase.name}</span>
</div>
<div class="phase-divider"></div>
<div class="phase-stats">
<div class="confidence-bar">
<div class="confidence-fill"
style="width: ${phase.avgConfidence || 0}%; background-color: ${auditService.getConfidenceColor(phase.avgConfidence || 0)}">
</div>
</div>
<span class="confidence-text">${phase.avgConfidence || 0}%</span>
</div>
</div>
<div class="phase-entries">
${(phase.entries || []).map(entry => `
<div class="audit-entry">
<div class="entry-main">
<span class="entry-action">${auditService.getActionDisplayName(entry.action)}</span>
<div class="entry-meta">
<div class="confidence-indicator"
style="background-color: ${auditService.getConfidenceColor(entry.confidence || 0)}">
</div>
<span class="confidence-value">${entry.confidence || 0}%</span>
<span class="processing-time">${entry.processingTimeMs || 0}ms</span>
</div>
</div>
${(entry.inputSummary && entry.inputSummary !== 'null') || (entry.outputSummary && entry.outputSummary !== 'null') ? `
<div class="entry-details">
${entry.inputSummary && entry.inputSummary !== 'null' ? `
<div class="detail-item"><strong>Input:</strong> ${this.escapeHtml(entry.inputSummary)}</div>
` : ''}
${entry.outputSummary && entry.outputSummary !== 'null' ? `
<div class="detail-item"><strong>Output:</strong> ${this.escapeHtml(entry.outputSummary)}</div>
` : ''}
</div>
` : ''}
</div>
`).join('')}
</div>
</div>
`).join('')}
</div>
`;
}
/**
* Render technical details section
*/
renderTechnicalDetails(audit) {
const technicalId = `${this.componentId}-technical`;
return `
<div class="technical-toggle">
<button class="technical-toggle-btn" data-target="${technicalId}">
🔧 Technische Details anzeigen
</button>
<div id="${technicalId}" class="technical-details collapsed">
${(audit.phases || []).map(phase =>
(phase.entries || []).map(entry => `
<div class="technical-entry">
<div class="technical-header">
<span class="technical-phase">${entry.phase}/${entry.action}</span>
<span class="technical-time">
${new Date(entry.timestamp).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})} • ${entry.processingTimeMs || 0}ms
</span>
</div>
<div class="technical-content">
<div class="technical-row">
<strong>Confidence:</strong> ${entry.confidence || 0}%
</div>
${entry.metadata && Object.keys(entry.metadata).length > 0 ? `
<div class="technical-row">
<strong>Metadata:</strong> ${this.escapeHtml(JSON.stringify(entry.metadata))}
</div>
` : ''}
</div>
</div>
`).join('')
).join('')}
</div>
</div>
`;
}
/**
* Attach event handlers for interactions
*/
attachEventHandlers() {
console.log('[AUDIT RENDERER] Attaching event handlers...');
// Handle collapsible header
if (this.options.collapsible) {
const header = document.querySelector(`[data-target="${this.componentId}-details"]`);
const details = document.getElementById(`${this.componentId}-details`);
const toggleIcon = header?.querySelector('.toggle-icon svg');
if (header && details && toggleIcon) {
// Remove existing listeners
header.replaceWith(header.cloneNode(true));
const newHeader = document.querySelector(`[data-target="${this.componentId}-details"]`);
const newToggleIcon = newHeader?.querySelector('.toggle-icon svg');
if (newHeader && newToggleIcon) {
newHeader.addEventListener('click', () => {
const isCollapsed = details.classList.contains('collapsed');
if (isCollapsed) {
details.classList.remove('collapsed');
newToggleIcon.style.transform = 'rotate(180deg)';
} else {
details.classList.add('collapsed');
newToggleIcon.style.transform = 'rotate(0deg)';
}
});
console.log('[AUDIT RENDERER] Collapsible header handler attached');
}
}
}
// Handle technical details toggle
const technicalBtn = document.querySelector(`[data-target="${this.componentId}-technical"]`);
const technicalDetails = document.getElementById(`${this.componentId}-technical`);
if (technicalBtn && technicalDetails) {
// Remove existing listener
technicalBtn.replaceWith(technicalBtn.cloneNode(true));
const newTechnicalBtn = document.querySelector(`[data-target="${this.componentId}-technical"]`);
if (newTechnicalBtn) {
newTechnicalBtn.addEventListener('click', () => {
const isCollapsed = technicalDetails.classList.contains('collapsed');
if (isCollapsed) {
technicalDetails.classList.remove('collapsed');
newTechnicalBtn.textContent = '🔧 Technische Details ausblenden';
} else {
technicalDetails.classList.add('collapsed');
newTechnicalBtn.textContent = '🔧 Technische Details anzeigen';
}
});
console.log('[AUDIT RENDERER] Technical details handler attached');
}
}
}
/**
* Render empty state
*/
renderEmpty() {
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = `
<div class="audit-trail-container">
<div class="audit-trail-header">
<div class="audit-icon">
<div class="audit-icon-gradient">ⓘ</div>
<h4>Kein Audit-Trail verfügbar</h4>
</div>
</div>
</div>
`;
}
}
/**
* Render error state
*/
renderError(error) {
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = `
<div class="audit-trail-container">
<div class="audit-trail-header">
<div class="audit-icon">
<div class="audit-icon-gradient" style="background: var(--color-error);">✗</div>
<h4>Audit-Trail Fehler</h4>
</div>
</div>
<div class="audit-summary">
<p style="color: var(--color-error);">
Fehler beim Laden der Audit-Informationen: ${this.escapeHtml(error.message)}
</p>
</div>
</div>
`;
}
}
/**
* Utility method to escape HTML
*/
escapeHtml(text) {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Clear the audit trail display
*/
clear() {
const container = document.getElementById(this.containerId);
if (container) {
container.innerHTML = '';
}
}
/**
* Get container element
*/
getContainer() {
return document.getElementById(this.containerId);
}
}

405
public/videos/README.md Normal file
View File

@@ -0,0 +1,405 @@
# Video-Bereitstellung für ForensicPathways Knowledgebase
Videos müssen manuell in diesem Verzeichnis bereitgestellt werden, da sie aufgrund unterschiedlicher Lizenzierung nicht Bestandteil des Open-Source-Git-Repositorys sind.
## 🎥 Video-Quelle und Lizenzierung
**Video-Quelle:** https://cloud.cc24.dev/f/47971 (Interner Nextcloud-Share)
**Kontakt bei Fragen:** mstoeck3@hs-mittweida.de
### Lizenzhinweise
- Videos können proprietäre Lizenzen haben
- Nicht für öffentliche Redistribution geeignet
- Nur für den internen Gebrauch in ForensicPathways
- Urheberrechte beachten bei eigenen Video-Beiträgen
## 📁 Empfohlene Verzeichnisstruktur
```
public/videos/
├── tools/ # Tool-spezifische Tutorials
│ ├── autopsy/
│ │ ├── autopsy-installation.mp4
│ │ ├── autopsy-basics.mp4
│ │ └── autopsy-advanced-analysis.webm
│ ├── volatility/
│ │ ├── volatility-setup.mp4
│ │ ├── volatility-pslist-demo.mp4
│ │ └── volatility-malfind-tutorial.webm
│ └── yara/
│ ├── yara-rules-basics.mp4
│ └── yara-advanced-hunting.mp4
├── methods/ # Methodologie-Videos
│ ├── timeline-analysis/
│ │ ├── timeline-fundamentals.mp4
│ │ └── timeline-correlation.webm
│ ├── disk-imaging/
│ │ ├── imaging-best-practices.mp4
│ │ └── imaging-verification.mp4
│ └── incident-response/
│ ├── ir-methodology.mp4
│ └── ir-documentation.webm
├── concepts/ # Konzeptuelle Erklärungen
│ ├── forensics-fundamentals/
│ │ ├── hash-functions-explained.mp4
│ │ ├── chain-of-custody.mp4
│ │ └── evidence-handling.webm
│ └── technical-concepts/
│ ├── regex-patterns.mp4
│ └── file-systems.webm
└── shared/ # Übergreifende Inhalte
├── nist-methodology.mp4
├── legal-considerations.webm
└── best-practices-overview.mp4
```
## 🦊 Firefox-Kompatibilität (KRITISCH)
### **Wichtiger Hinweis**
Videos **müssen** in Firefox-kompatiblen Formaten bereitgestellt werden, da das System automatische Firefox-Unterstützung implementiert. Nicht-kompatible Formate führen zu Fehlern!
### Unterstützte Formate
#### ✅ Empfohlene Formate (höchste Kompatibilität)
**MP4 (H.264/AVC + AAC):**
```bash
# Konvertierung mit ffmpeg
ffmpeg -i input.mov \
-c:v libx264 \
-c:a aac \
-profile:v baseline \
-level 3.0 \
-movflags +faststart \
output.mp4
```
**WebM (VP8/VP9 + Vorbis/Opus):**
```bash
# VP9 für beste Qualität
ffmpeg -i input.mov \
-c:v libvpx-vp9 \
-c:a libopus \
-b:v 1M \
-b:a 128k \
output.webm
# VP8 für breitere Kompatibilität
ffmpeg -i input.mov \
-c:v libvpx \
-c:a libvorbis \
-b:v 1M \
-b:a 128k \
output.webm
```
#### ⚠️ Fallback-Format
**OGG Theora (für ältere Firefox-Versionen):**
```bash
ffmpeg -i input.mov \
-c:v libtheora \
-c:a libvorbis \
-b:v 1M \
-b:a 128k \
output.ogv
```
### ❌ Nicht unterstützte Formate in Firefox
- **H.265/HEVC** (.mp4, .mov) - Wird nicht dekodiert
- **AV1** (.mp4, .webm) - Eingeschränkte Unterstützung
- **Proprietäre Codecs** (.wmv, .avi mit proprietären Codecs)
- **Apple-spezifische Formate** (.mov mit ProRes, .m4v)
### Multi-Format-Bereitstellung
Für maximale Kompatibilität mehrere Formate bereitstellen:
```html
<video title="Autopsy Installation Tutorial" controls>
<source src="/videos/tools/autopsy/installation.mp4" type="video/mp4">
<source src="/videos/tools/autopsy/installation.webm" type="video/webm">
<source src="/videos/tools/autopsy/installation.ogv" type="video/ogg">
<p>Ihr Browser unterstützt das Video-Element nicht.</p>
</video>
```
## 🔧 Video-Konvertierung und -Optimierung
### Qualitätsrichtlinien
#### Auflösung und Bitrate
**720p (empfohlen für Tutorials):**
```bash
ffmpeg -i input.mov \
-vf scale=1280:720 \
-c:v libx264 \
-b:v 2M \
-c:a aac \
-b:a 128k \
output.mp4
```
**1080p (für detaillierte Demonstrationen):**
```bash
ffmpeg -i input.mov \
-vf scale=1920:1080 \
-c:v libx264 \
-b:v 4M \
-c:a aac \
-b:a 128k \
output.mp4
```
**480p (mobile-optimiert):**
```bash
ffmpeg -i input.mov \
-vf scale=854:480 \
-c:v libx264 \
-b:v 1M \
-c:a aac \
-b:a 96k \
output.mp4
```
### Optimierung für Web-Streaming
#### Fast Start für progressive Download
```bash
# Metadata an Dateianfang verschieben
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
```
#### Keyframe-Intervall optimieren
```bash
# Keyframes alle 2 Sekunden für bessere Suche
ffmpeg -i input.mov \
-c:v libx264 \
-g 60 \
-keyint_min 60 \
-sc_threshold 0 \
output.mp4
```
### Batch-Konvertierung
**Alle Videos in einem Verzeichnis konvertieren:**
```bash
#!/bin/bash
# convert-all.sh
for file in *.mov *.avi *.mkv; do
if [ -f "$file" ]; then
name=$(basename "$file" | cut -d. -f1)
# MP4 erstellen
ffmpeg -i "$file" \
-c:v libx264 \
-c:a aac \
-b:v 2M \
-b:a 128k \
-movflags +faststart \
"${name}.mp4"
# WebM erstellen
ffmpeg -i "$file" \
-c:v libvpx-vp9 \
-c:a libopus \
-b:v 1.5M \
-b:a 128k \
"${name}.webm"
fi
done
```
## 📊 Dateigröße und Performance
### Größenrichtlinien
**Streaming-optimiert:**
- 720p: 5-15 MB/Minute
- 1080p: 20-40 MB/Minute
- 480p: 2-8 MB/Minute
**Maximale Dateigröße:**
- Tutorial-Videos: < 100 MB
- Kurze Demos: < 50 MB
- Konzept-Erklärungen: < 30 MB
### Kompressionseinstellungen
**Ausgewogene Qualität/Größe:**
```bash
ffmpeg -i input.mov \
-c:v libx264 \
-preset medium \
-crf 23 \
-c:a aac \
-b:a 128k \
output.mp4
```
**Hohe Kompression (kleinere Dateien):**
```bash
ffmpeg -i input.mov \
-c:v libx264 \
-preset slow \
-crf 28 \
-c:a aac \
-b:a 96k \
output.mp4
```
## 🎬 Video-Thumbnail-Generierung
Automatische Thumbnail-Erstellung:
```bash
# Thumbnail nach 10 Sekunden
ffmpeg -i input.mp4 -ss 00:00:10 -vframes 1 -q:v 2 thumbnail.jpg
# Mehrere Thumbnails für Auswahl
ffmpeg -i input.mp4 -vf fps=1/30 thumb_%03d.jpg
```
Thumbnails speichern in:
```
public/images/video-thumbnails/
├── autopsy-installation-thumb.jpg
├── volatility-basics-thumb.jpg
└── timeline-analysis-thumb.jpg
```
## 🔍 Qualitätskontrolle
### Pre-Upload-Checkliste
**✅ Format-Kompatibilität:**
- [ ] MP4 mit H.264/AVC Video-Codec
- [ ] AAC Audio-Codec
- [ ] Fast Start aktiviert (`movflags +faststart`)
- [ ] Keyframe-Intervall ≤ 2 Sekunden
**✅ Firefox-Test:**
- [ ] Video lädt in Firefox ohne Fehler
- [ ] Audio synchron mit Video
- [ ] Controls funktionieren
- [ ] Seeking funktioniert flüssig
**✅ Technische Qualität:**
- [ ] Auflösung angemessen (720p+ für GUI-Demos)
- [ ] Audio klar und verständlich
- [ ] Keine Kompressionsartefakte
- [ ] Dateigröße < 100 MB
**✅ Inhaltliche Qualität:**
- [ ] Beschreibender Dateiname
- [ ] Angemessene Länge (< 10 Minuten für Tutorials)
- [ ] Klare Demonstration der Funktionalität
- [ ] Sichtbare UI-Elemente
### Automated Testing
```bash
#!/bin/bash
# video-check.sh - Basis-Validierung
for video in public/videos/**/*.mp4; do
echo "Checking: $video"
# Format prüfen
format=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$video")
if [ "$format" != "h264" ]; then
echo "❌ Wrong codec: $format (should be h264)"
fi
# Dateigröße prüfen
size=$(stat -c%s "$video")
if [ $size -gt 104857600 ]; then # 100MB
echo "⚠️ Large file: $(($size / 1048576))MB"
fi
echo "$video validated"
done
```
## 🚨 Troubleshooting
### Häufige Firefox-Probleme
**Problem: Video lädt nicht**
```
Lösung:
1. Codec überprüfen: ffprobe -v quiet -show_format -show_streams video.mp4
2. Fallback-Format hinzufügen
3. Fast Start aktivieren
```
**Problem: Audio/Video out of sync**
```
Lösung:
ffmpeg -i input.mp4 -c:v copy -c:a aac -avoid_negative_ts make_zero output.mp4
```
**Problem: Seeking funktioniert nicht**
```
Lösung:
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
```
### Performance-Probleme
**Problem: Lange Ladezeiten**
```
Lösungsansätze:
1. Bitrate reduzieren
2. Auflösung verringern
3. Keyframe-Intervall optimieren
4. Progressive Download aktivieren
```
**Problem: Hohe Bandbreiten-Nutzung**
```
Lösungsansätze:
1. Adaptive Streaming implementieren
2. Multiple Qualitätsstufen bereitstellen
3. Preload="metadata" verwenden
```
## 📋 Deployment-Checkliste
**Nach Video-Upload:**
1. **✅ Dateistruktur prüfen**
```bash
ls -la public/videos/tools/autopsy/
```
2. **✅ Permissions setzen**
```bash
chmod 644 public/videos/**/*.mp4
```
3. **✅ Artikel-Verlinkung testen**
- Video-Tags in Markdown funktionieren
- Responsive Container werden generiert
- Thumbnails laden korrekt
4. **✅ Browser-Kompatibilität**
- Firefox: Codec-Support prüfen
- Chrome: Performance testen
- Safari: Fallback-Formate testen
- Mobile: Touch-Controls funktionieren
5. **✅ Build-System**
```bash
npm run build
# Keine Video-bezogenen Fehler in Console
```
Bei Problemen kontaktieren Sie mstoeck3@hs-mittweida.de mit:
- Browser und Version
- Video-Dateiname und -pfad
- Fehlermeldungen aus Browser-Console
- Screenshot des Problems

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
---
// src/components/ContributionButton.astro - CLEANED: Removed duplicate auth script
// src/components/ContributionButton.astro
export interface Props {
type: 'edit' | 'new' | 'write';
toolName?: string;

View File

@@ -1,5 +1,5 @@
---
import { createToolSlug } from '../utils/toolHelpers.js';
import { createToolSlug } from '../utils/clientUtils.js';
export interface Props {
toolName: string;

View File

@@ -4,7 +4,6 @@ import { getToolsData } from '../utils/dataService.js';
const data = await getToolsData();
const scenarios = data.scenarios || [];
// Configuration
const maxDisplayed = 9;
const displayedScenarios = scenarios.slice(0, maxDisplayed);
---

View File

@@ -1,4 +1,5 @@
---
//src/components/ToolFilters.astro
import { getToolsData } from '../utils/dataService.js';
const data = await getToolsData();
@@ -54,7 +55,7 @@ const sortedTags = Object.entries(tagFrequency)
<!-- Semantic Search Toggle - Inline -->
<div id="semantic-search-container" class="semantic-search-inline hidden">
<label class="semantic-toggle-wrapper" title="Semantische Suche verwendet Embeddings. Dadurch kann mit natürlicher Sprache/Begriffen gesucht werden, die Ergebnisse richten sich nach der euklidischen Distanz.">
<label class="semantic-toggle-wrapper" title="Semantische Suche verwendet Embeddings. Dadurch kann mit natürlicher Sprache/Begriffen gesucht werden, die Ergebnisse richten sich nach der cosinus-Distanz.">
<input type="checkbox" id="semantic-search-enabled" disabled/>
<div class="semantic-checkbox-custom"></div>
<span class="semantic-toggle-label">
@@ -306,7 +307,6 @@ const sortedTags = Object.entries(tagFrequency)
<script define:vars={{ toolsData: data.tools, tagFrequency, sortedTags }}>
window.toolsData = toolsData;
document.addEventListener('DOMContentLoaded', () => {
const elements = {
searchInput: document.getElementById('search-input'),
@@ -354,24 +354,21 @@ const sortedTags = Object.entries(tagFrequency)
let semanticSearchAvailable = false;
let lastSemanticResults = null;
// Check embeddings availability
async function checkEmbeddingsAvailability() {
try {
const res = await fetch('/api/ai/embeddings-status');
const { embeddings } = await res.json();
semanticSearchAvailable = embeddings?.enabled && embeddings?.initialized;
semanticSearchAvailable = embeddings?.initialized;
if (semanticSearchAvailable) {
elements.semanticContainer.classList.remove('hidden');
elements.semanticCheckbox.disabled = false; // 👈 re-enable
elements.semanticCheckbox.disabled = false;
}
} catch (err) {
console.error('[EMBEDDINGS] Status check failed:', err);
// leave the checkbox disabled
}
}
// Semantic search function
async function performSemanticSearch(query) {
if (!semanticSearchAvailable || !query.trim()) {
return null;
@@ -396,6 +393,13 @@ const sortedTags = Object.entries(tagFrequency)
}
}
function isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
function toggleCollapsible(toggleBtn, content, storageKey) {
const isCollapsed = toggleBtn.getAttribute('data-collapsed') === 'true';
const newState = !isCollapsed;
@@ -435,13 +439,6 @@ const sortedTags = Object.entries(tagFrequency)
}
}
function isToolHosted(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
function initTagCloud() {
const visibleCount = 20;
elements.tagCloudItems.forEach((item, index) => {
@@ -576,7 +573,6 @@ const sortedTags = Object.entries(tagFrequency)
}
}
// FIXED: Consolidated filtering logic with semantic search support
async function filterTools() {
const searchTerm = elements.searchInput.value.trim().toLowerCase();
const selectedDomain = elements.domainSelect.value;
@@ -594,7 +590,6 @@ const sortedTags = Object.entries(tagFrequency)
let filteredTools = window.toolsData;
let semanticResults = null;
// CONSOLIDATED: Use semantic search if enabled and search term exists
if (semanticSearchEnabled && semanticSearchAvailable && searchTerm) {
semanticResults = await performSemanticSearch(searchTerm);
lastSemanticResults = semanticResults;
@@ -605,7 +600,6 @@ const sortedTags = Object.entries(tagFrequency)
} else {
lastSemanticResults = null;
// Traditional text-based search
if (searchTerm) {
filteredTools = window.toolsData.filter(tool =>
tool.name.toLowerCase().includes(searchTerm) ||
@@ -615,7 +609,6 @@ const sortedTags = Object.entries(tagFrequency)
}
}
// Apply additional filters to the results
filteredTools = filteredTools.filter(tool => {
if (selectedDomain && !(tool.domains || []).includes(selectedDomain)) {
return false;
@@ -666,9 +659,8 @@ const sortedTags = Object.entries(tagFrequency)
);
}
/* existing code continues */
const finalResults = semanticSearchEnabled && lastSemanticResults
? filteredTools // now properly re-sorted
? filteredTools
: (searchTerm && window.prioritizeSearchResults
? window.prioritizeSearchResults(filteredTools, searchTerm)
: filteredTools);
@@ -726,7 +718,6 @@ const sortedTags = Object.entries(tagFrequency)
filterTagCloud();
}
// Event listeners
elements.searchInput.addEventListener('input', (e) => {
const hasValue = e.target.value.length > 0;
elements.clearSearch.classList.toggle('hidden', !hasValue);
@@ -741,7 +732,6 @@ const sortedTags = Object.entries(tagFrequency)
filterTools();
});
// Semantic search checkbox handler
if (elements.semanticCheckbox) {
elements.semanticCheckbox.addEventListener('change', (e) => {
semanticSearchEnabled = e.target.checked;
@@ -785,14 +775,24 @@ const sortedTags = Object.entries(tagFrequency)
b.classList.toggle('active', b.getAttribute('data-view') === view);
});
window.dispatchEvent(new CustomEvent('viewChanged', { detail: view }));
if (view === 'hosted') {
const hosted = window.toolsData.filter(tool => isToolHosted(tool));
window.dispatchEvent(new CustomEvent('toolsFiltered', { detail: hosted }));
if (window.switchToView) {
window.switchToView(view);
} else {
console.error('switchToView function not available');
}
window.dispatchEvent(new CustomEvent('viewChanged', {
detail: view,
triggeredByButton: true
}));
setTimeout(() => {
if (view === 'matrix') {
filterTools();
} else if (view === 'grid') {
filterTools();
}
}, 100);
});
});
@@ -812,7 +812,6 @@ const sortedTags = Object.entries(tagFrequency)
window.clearTagFilters = resetTags;
window.clearAllFilters = resetAllFilters;
// Initialize
checkEmbeddingsAvailability();
initializeCollapsible();
initTagCloud();

View File

@@ -1,6 +1,6 @@
---
//src/components/ToolMatrix.astro
import { getToolsData } from '../utils/dataService.js';
import ShareButton from './ShareButton.astro';
const data = await getToolsData();
@@ -193,6 +193,15 @@ domains.forEach((domain: any) => {
</div>
<script define:vars={{ toolsData: tools, domainAgnosticSoftware, domainAgnosticTools }}>
if (!window.isToolHosted) {
window.isToolHosted = function(tool) {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
};
}
function getSelectedPhase() {
const activePhaseChip = document.querySelector('.phase-chip.active');
return activePhaseChip ? activePhaseChip.getAttribute('data-phase') : '';
@@ -216,9 +225,7 @@ domains.forEach((domain: any) => {
if (selectedDomain) {
const domainRow = matrixTable.querySelector(`tr[data-domain="${selectedDomain}"]`);
if (domainRow) {
domainRow.classList.add('highlight-row');
}
if (domainRow) domainRow.classList.add('highlight-row');
}
if (selectedPhase) {
@@ -231,9 +238,7 @@ domains.forEach((domain: any) => {
const columnIndex = phaseIndex + 1;
matrixTable.querySelectorAll(`tr`).forEach(row => {
const cell = row.children[columnIndex];
if (cell) {
cell.classList.add('highlight-column');
}
if (cell) cell.classList.add('highlight-column');
});
}
}
@@ -267,9 +272,7 @@ domains.forEach((domain: any) => {
const params = new URLSearchParams();
params.set('tool', toolSlug);
params.set('view', view);
if (modal) {
params.set('modal', modal);
}
if (modal) params.set('modal', modal);
return `${baseUrl}?${params.toString()}`;
}
@@ -301,7 +304,7 @@ domains.forEach((domain: any) => {
}
}
window.showShareDialog = function(shareButton) {
function showShareDialog(shareButton) {
const toolName = shareButton.getAttribute('data-tool-name');
const context = shareButton.getAttribute('data-context');
@@ -438,17 +441,12 @@ domains.forEach((domain: any) => {
copyToClipboard(url, btn);
});
});
};
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
window.showToolDetails = function(toolName, modalType = 'primary') {
const tool = toolsData.find(t => t.name === toolName);
if (!tool) {
console.error('Tool not found:', toolName);
return;
}
function showToolDetails(toolName, modalType = 'primary') {
const tool = toolsData.find(t => t.name === toolName);
if (!tool) return;
const isMethod = tool.type === 'method';
const isConcept = tool.type === 'concept';
@@ -462,10 +460,7 @@ domains.forEach((domain: any) => {
};
for (const [key, element] of Object.entries(elements)) {
if (!element) {
console.error(`Element not found: tool-${key}-${modalType}`);
return;
}
if (!element) return;
}
const iconHtml = tool.icon ? `<span class="mr-3 text-xl">${tool.icon}</span>` : '';
@@ -709,9 +704,9 @@ domains.forEach((domain: any) => {
if (primaryActive && secondaryActive) {
document.body.classList.add('modals-side-by-side');
}
};
}
window.hideToolDetails = function(modalType = 'both') {
function hideToolDetails(modalType = 'both') {
const overlay = document.getElementById('modal-overlay');
const primaryModal = document.getElementById('tool-details-primary');
const secondaryModal = document.getElementById('tool-details-secondary');
@@ -763,28 +758,42 @@ domains.forEach((domain: any) => {
} else {
document.body.classList.remove('modals-side-by-side');
}
};
}
window.hideAllToolDetails = function() {
window.hideToolDetails('both');
};
function hideAllToolDetails() {
hideToolDetails('both');
}
window.showToolDetails = showToolDetails;
window.hideToolDetails = hideToolDetails;
window.hideAllToolDetails = hideAllToolDetails;
window.toggleDomainAgnosticSection = toggleDomainAgnosticSection;
window.showShareDialog = showShareDialog;
window.matrixShowToolDetails = showToolDetails;
window.matrixHideToolDetails = hideToolDetails;
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
if (view === 'matrix') {
setTimeout(updateMatrixHighlighting, 100);
setTimeout(() => {
if (window.filterTools && typeof window.filterTools === 'function') {
window.filterTools();
} else {
const allTools = window.toolsData || [];
window.dispatchEvent(new CustomEvent('toolsFiltered', {
detail: {
tools: allTools,
semanticSearch: false
}
}));
}
}, 100);
}
});
window.addEventListener('toolsFiltered', (event) => {
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
setTimeout(updateMatrixHighlighting, 50);
}
});
window.addEventListener('toolsFiltered', (event) => {
const filtered = event.detail;
const { tools: filtered, semanticSearch } = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
if (currentView === 'matrix') {
@@ -793,13 +802,6 @@ domains.forEach((domain: any) => {
const domainAgnosticPhaseIds = domainAgnosticSoftware.map(section => section.id);
const isDomainAgnosticPhase = domainAgnosticPhaseIds.includes(selectedPhase);
domainAgnosticSoftware.forEach(sectionData => {
const section = document.getElementById(`domain-agnostic-section-${sectionData.id}`);
const container = document.getElementById(`domain-agnostic-tools-${sectionData.id}`);
if (!section || !container) return;
});
if (!isDomainAgnosticPhase) {
document.getElementById('dfir-matrix-section').style.display = 'block';
@@ -808,9 +810,7 @@ domains.forEach((domain: any) => {
});
filtered.forEach(tool => {
if (tool.type === 'concept') {
return;
}
if (tool.type === 'concept') return;
const isMethod = tool.type === 'method';
const hasValidProjectUrl = window.isToolHosted(tool);
@@ -827,6 +827,7 @@ domains.forEach((domain: any) => {
hasValidProjectUrl ? 'tool-chip-hosted' :
tool.license !== 'Proprietary' ? 'tool-chip-oss' : '';
chip.className = `tool-chip ${chipClass}`;
chip.setAttribute('data-tool-name', tool.name);
chip.setAttribute('title', `${tool.name}${tool.knowledgebase === true ? ' (KB verfügbar)' : ''}`);
chip.innerHTML = `${tool.name}${tool.knowledgebase === true ? '<span style="margin-left: 0.25rem; font-size: 0.6875rem;">📖</span>' : ''}`;
chip.onclick = () => window.showToolDetails(tool.name);

View File

@@ -1,243 +1,364 @@
// src/config/prompts.ts - Centralized German prompts for AI pipeline
// src/config/prompts.ts
const RELEVANCE_RUBRIC = `
TASK RELEVANCE (INTEGER 0100, NO %):
- 5565 = Basis/ok
- 6675 = Gut geeignet
- 7685 = Sehr gut geeignet
- >85 = Nur bei nahezu perfekter Übereinstimmung
`.trim();
const STRICTNESS = `
STRICTNESS:
- Output MUST be pure JSON (no prose, no code fences, no trailing commas).
- Use EXACT item names as provided (casing/spelling must match).
- Do NOT invent items or fields. If unsure, select fewer.
`.trim();
export const AI_PROMPTS = {
enhancementQuestions: (input: string) => {
return `Sie sind DFIR-Experte. Ein Nutzer beschreibt unten ein Szenario/Problem.
toolSelection: (mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number) => {
const modeInstruction = mode === 'workflow'
? 'Der Benutzer möchte einen UMFASSENDEN WORKFLOW mit mehreren Tools/Methoden über verschiedene Phasen. Wählen Sie 15-25 Tools aus, die den vollständigen Untersuchungslebenszyklus abdecken.'
: 'Der Benutzer möchte SPEZIFISCHE TOOLS/METHODEN, die ihr konkretes Problem direkt lösen. Wählen Sie 3-8 Tools aus, die am relevantesten und effektivsten sind.';
ZIEL:
- Stellen Sie NUR dann 13 präzise Rückfragen, wenn entscheidende forensische Lücken die weitere Analyse/Toolauswahl PHASENREIHENFOLGE oder EVIDENCE-STRATEGIE wesentlich beeinflussen würden.
- Wenn ausreichend abgedeckt: Geben Sie eine leere Liste [] zurück.
return `Sie sind ein DFIR-Experte mit Zugang zur kompletten forensischen Tool-Datenbank. Sie müssen die relevantesten Tools und Konzepte für diese spezifische Anfrage auswählen.
PRIORITÄT DER THEMEN (in dieser Reihenfolge prüfen):
1) Available Evidence & Artefakte (z.B. RAM-Dump, Disk-Image, Logs, PCAP, Registry, Cloud/Audit-Logs)
2) Scope/Systems (konkrete Plattformen/Assets/Identitäten/Netzsegmente)
3) Investigation Objectives (Ziele: IOC-Extraktion, Timeline, Impact, Attribution)
4) Timeline/Timeframe (kritische Zeitfenster, Erhalt flüchtiger Daten)
5) Legal & Compliance (Chain of Custody, Aufbewahrung, DSGVO/Branchenvorgaben)
6) Technical Constraints (Ressourcen, Zugriffsrechte, Tooling/EDR)
AUSWAHLMETHODE: ${selectionMethod}
${selectionMethod === 'embeddings_candidates' ?
'Diese Tools wurden durch Vektor-Ähnlichkeit vorgefiltert, sie sind bereits relevant. Ihre Aufgabe ist es, die BESTEN aus diesem relevanten Set auszuwählen.' :
'Sie haben Zugang zur vollständigen Tool-Datenbank. Wählen Sie die relevantesten Tools für die Anfrage aus.'}
FRAGEN-QUALITÄT:
- Forensisch spezifisch und entscheidungsrelevant (keine Allgemeinplätze).
- Eine Frage pro Thema, keine Dopplungen.
- Antwortbar vom Nutzer (keine Spekulation, keine “Beweise senden”-Aufforderungen).
- Maximal 18 Wörter, endet mit "?".
VALIDIERUNG:
- Stellen Sie NUR Fragen zu Themen, die im Nutzertext NICHT hinreichend konkret beantwortet sind (keine Wiederholung bereits gegebener Details).
- Wenn alle priorisierten Themen ausreichend sind → [].
ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
[
"präzise Frage 1?",
"präzise Frage 2?",
"präzise Frage 3?"
]
NUTZER-EINGABE:
${input}`.trim();
},
toolSelection: (mode: string, userQuery: string, maxSelectedItems: number) => {
const modeInstruction =
mode === 'workflow'
? 'Workflow mit 1525 Items über alle Phasen. Pflicht: ~40% Methoden, Rest Software/Konzepte (falls verfügbar).'
: 'Spezifische Lösung mit 410 Items. Pflicht: ≥30% Methoden (falls verfügbar).';
return `Du bist DFIR-Experte. Wähle die BESTEN Items aus dem bereits semantisch vorgefilterten Set für die konkrete Aufgabe.
${modeInstruction}
BENUTZER-ANFRAGE: "${userQuery}"
ANFRAGE: "${userQuery}"
KRITISCHE AUSWAHLPRINZIPIEN:
1. **KONTEXT ÜBER POPULARITÄT**: Verwenden Sie nicht automatisch "berühmte" Tools wie Volatility, Wireshark oder Autopsy nur weil sie bekannt sind. Wählen Sie basierend auf den SPEZIFISCHEN Szenario-Anforderungen.
ITEM-TYPEN:
- TOOLS (type: "software" | "method")
- KONZEPTE (type: "concept")
2. **METHODOLOGIE vs SOFTWARE**:
- Für SCHNELLE/DRINGENDE Szenarien → Priorisieren Sie METHODEN und schnelle Antwort-Ansätze
- Für ZEITKRITISCHE Vorfälle → Wählen Sie Triage-Methoden über tiefe Analyse-Tools
- Für UMFASSENDE Analysen → Dann betrachten Sie detaillierte Software-Tools
- METHODEN (Typ: "method") sind oft besser als SOFTWARE für prozedurale Anleitung
AUSWAHLPRINZIPIEN:
1) Relevanz zur Anfrage (direkt anwendbar, adressiert Kernproblem)
2) Ausgewogene Mischung (Praxis: selectedTools; Methodik: selectedConcepts)
3) Qualität > Quantität (lieber weniger, dafür passgenau)
4) Keine Erfindungen. Wenn etwas nicht passt, wähle weniger.
3. **SZENARIO-SPEZIFISCHE LOGIK**:
- "Schnell/Quick/Dringend/Triage" Szenarien → Rapid Incident Response und Triage METHODE > Volatility
- "Industrial/SCADA/ICS" Szenarien → Spezialisierte ICS-Tools > generische Netzwerk-Tools
- "Mobile/Android/iOS" Szenarien → Mobile-spezifische Tools > Desktop-Forensik-Tools
- "Speicher-Analyse dringend benötigt" → Schnelle Speicher-Tools/Methoden > umfassende Volatility-Analyse
AUSWAHLREGELN:
- Wähle ${mode === 'workflow' ? '1525' : '410'} Items total (max ${maxSelectedItems})
- Fülle BEIDE Arrays: selectedTools UND selectedConcepts
- Mindestens 12 Konzepte (falls verfügbar)
- Bevorzugt ~40% Methoden (Workflow) bzw. ≥30% Methoden (Tool-Modus), sofern vorhanden
- Sortiere selectedTools grob nach Eignung (bestes zuerst)
ANALYSE-ANWEISUNGEN:
1. Lesen Sie die VOLLSTÄNDIGE Beschreibung jedes Tools/Konzepts
2. Berücksichtigen Sie ALLE Tags, Plattformen, verwandte Tools und Metadaten
3. **PASSENDE DRINGLICHKEIT**: Schnelle Szenarien brauchen schnelle Methoden, nicht tiefe Analyse-Tools
4. **PASSENDE SPEZIFITÄT**: Spezialisierte Szenarien brauchen spezialisierte Tools, nicht generische
5. **BERÜCKSICHTIGEN SIE DEN TYP**: Methoden bieten prozedurale Anleitung, Software bietet technische Fähigkeiten
Skalenhinweis (für spätere Schritte einheitlich):
${RELEVANCE_RUBRIC}
Wählen Sie die relevantesten Elemente aus (max ${maxSelectedItems} insgesamt).
${STRICTNESS}
Antworten Sie NUR mit diesem JSON-Format:
ANTWORT (NUR JSON):
{
"selectedTools": ["Tool Name 1", "Tool Name 2", ...],
"selectedConcepts": ["Konzept Name 1", "Konzept Name 2", ...],
"reasoning": "Detaillierte Erklärung, warum diese spezifischen Tools für diese Anfrage ausgewählt wurden, und warum bestimmte populäre Tools NICHT ausgewählt wurden, falls sie für den Szenario-Kontext ungeeignet waren"
"selectedTools": ["ToolName1", "MethodName1", "..."],
"selectedConcepts": ["ConceptName1", "ConceptName2", "..."],
"reasoning": "Sehr kurz: Balance/Abdeckung begründen"
}`;
},
toolSelectionWithData: (basePrompt: string, toolsToSend: any[], conceptsToSend: any[]) => {
return `${basePrompt}
VERFÜGBARE TOOLS (${toolsToSend.length}):
${JSON.stringify(toolsToSend, null, 2)}
VERFÜGBARE KONZEPTE (${conceptsToSend.length}):
${JSON.stringify(conceptsToSend, null, 2)}
WICHTIG:
- Wähle nur aus obigen Listen. Keine neuen Namen.
- Nutze exakte Namen. Keine Synonyme/Varianten.
Hinweis zur einheitlichen Relevanz-Skala:
${RELEVANCE_RUBRIC}
${STRICTNESS}`;
},
scenarioAnalysis: (isWorkflow: boolean, userQuery: string) => {
const analysisType = isWorkflow ? 'forensische Szenario' : 'technische Problem';
const considerations = isWorkflow ?
`- Angriffsvektoren und Bedrohungsmodellierung nach MITRE ATT&CK
- Betroffene Systeme und kritische Infrastrukturen
- Zeitkritische Faktoren und Beweiserhaltung
- Forensische Artefakte und Datenquellen` :
`- Spezifische forensische Herausforderungen
- Verfügbare Datenquellen und deren Integrität
- Methodische Anforderungen für rechtssichere Analyse`;
const analysisType = isWorkflow ? 'Szenario' : 'Problem';
const focus = isWorkflow
? 'Angriffsvektoren, betroffene Systeme, Zeitkritikalität'
: 'Kernherausforderung, verfügbare Daten, methodische Anforderungen';
return `Sie sind ein erfahrener DFIR-Experte. Analysieren Sie das folgende ${analysisType}.
return `DFIR-Experte: Analysiere das ${analysisType}.
${isWorkflow ? 'FORENSISCHES SZENARIO' : 'TECHNISCHES PROBLEM'}: "${userQuery}"
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
Fokus: ${focus}
Führen Sie eine systematische ${isWorkflow ? 'Szenario-Analyse' : 'Problem-Analyse'} durch und berücksichtigen Sie dabei:
${considerations}
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen, Aufzählungen oder Markdown-Formatierung. Maximum 150 Wörter.`;
Antwort: Fließtext, max 100 Wörter. Keine Liste, keine Einleitung.`;
},
investigationApproach: (isWorkflow: boolean, userQuery: string) => {
const approachType = isWorkflow ? 'Untersuchungsansatz' : 'Lösungsansatz';
const considerations = isWorkflow ?
`- Triage-Prioritäten nach forensischer Dringlichkeit
- Phasenabfolge nach NIST-Methodik
- Kontaminationsvermeidung und forensische Isolierung` :
`- Methodik-Auswahl nach wissenschaftlichen Kriterien
- Validierung und Verifizierung der gewählten Ansätze
- Integration in bestehende forensische Workflows`;
const focus = isWorkflow
? 'Triage-Prioritäten, Phasenabfolge, Kontaminationsvermeidung'
: 'Methodenauswahl, Validierung, Integration';
return `Basierend auf der Analyse entwickeln Sie einen fundierten ${approachType} nach NIST SP 800-86 Methodik.
return `Entwickle einen ${approachType}.
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
Fokus: ${focus}
Entwickeln Sie einen systematischen ${approachType} unter Berücksichtigung von:
${considerations}
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 150 Wörter.`;
Antwort: Fließtext, max 100 Wörter.`;
},
criticalConsiderations: (isWorkflow: boolean, userQuery: string) => {
const considerationType = isWorkflow ? 'kritische forensische Überlegungen' : 'wichtige methodische Voraussetzungen';
const aspects = isWorkflow ?
`- Time-sensitive evidence preservation
- Chain of custody requirements und rechtliche Verwertbarkeit
- Incident containment vs. evidence preservation Dilemma
- Privacy- und Compliance-Anforderungen` :
`- Tool-Validierung und Nachvollziehbarkeit
- False positive/negative Risiken bei der gewählten Methodik
- Qualifikationsanforderungen für die Durchführung
- Dokumentations- und Reporting-Standards`;
const focus = isWorkflow
? 'Beweissicherung vs. Gründlichkeit, Chain of Custody'
: 'Tool-Validierung, False Positives/Negatives, Qualifikationen';
return `Identifizieren Sie ${considerationType} für diesen Fall.
return `Identifiziere kritische Überlegungen.
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
Fokus: ${focus}
Berücksichtigen Sie folgende forensische Aspekte:
${aspects}
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.`;
Antwort: Fließtext, max 100 Wörter.`;
},
phaseToolSelection: (userQuery: string, phase: any, phaseTools: any[]) => {
return `Wählen Sie 2-3 Methoden/Tools für die Phase "${phase.name}" und bewerten Sie deren Aufgaben-Eignung VERGLEICHEND.
const methods = phaseTools.filter(t => t.type === 'method');
const tools = phaseTools.filter(t => t.type === 'software');
if (phaseTools.length === 0) {
return `Keine Methoden/Tools für Phase "${phase.name}" verfügbar. Antworte mit leerem Array: []`;
}
return `Wähle die 23 BESTEN Items für Phase "${phase.name}".
SZENARIO: "${userQuery}"
SPEZIFISCHE PHASE: ${phase.name} - ${phase.description || 'Forensische Untersuchungsphase'}
PHASE: ${phase.name} ${phase.description || ''}
VERFÜGBARE TOOLS FÜR ${phase.name.toUpperCase()}:
${phaseTools.map((tool: any, index: number) => `${index + 1}. ${tool.name}: ${tool.description.slice(0, 150)}...
- Plattformen: ${tool.platforms?.join(', ') || 'N/A'}
- Skill Level: ${tool.skillLevel}
- Tags: ${tool.tags?.join(', ') || 'N/A'}`).join('\n\n')}
VERFÜGBARE ITEMS:
${methods.length > 0 ? `
METHODEN (${methods.length}):
${methods.map((m: any) =>
`- ${m.name}
Typ: ${m.type}
Beschreibung: ${m.description}
Domains: ${m.domains?.join(', ') || 'N/A'}
Skill Level: ${m.skillLevel}`
).join('\n\n')}
` : 'Keine Methoden verfügbar'}
Bewerten Sie ALLE Tools vergleichend für diese spezifische Aufgabe UND Phase. Wählen Sie die 2-3 besten aus.
${tools.length > 0 ? `
SOFTWARE (${tools.length}):
${tools.map((t: any) =>
`- ${t.name}
Typ: ${t.type}
Beschreibung: ${t.description}
Plattformen: ${t.platforms?.join(', ') || 'N/A'}
Skill Level: ${t.skillLevel}`
).join('\n\n')}
` : 'Keine Software-Tools verfügbar'}
BEWERTUNGSKRITERIEN:
- Wie gut löst das Tool das forensische Problem im SZENARIO-Kontext?
- Wie gut passt es zur spezifischen PHASE "${phase.name}"?
- Wie vergleicht es sich mit den anderen verfügbaren Tools für diese Phase?
REGELN:
1) 23 Items, direkt phasenrelevant; mind. 1 Methode, falls verfügbar
2) Begründung pro Item (präzise, anwendungsbezogen)
3) Verwende EXAKTE Namen aus den Listen. Keine Erfindungen.
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
${RELEVANCE_RUBRIC}
${STRICTNESS}
ANTWORT (NUR JSON):
[
{
"toolName": "Exakter Tool-Name",
"taskRelevance": 85,
"justification": "Vergleichende Begründung warum dieses Tool für diese Phase und Aufgabe besser/schlechter als die anderen geeignet ist",
"limitations": ["Spezifische Einschränkung 1", "Einschränkung 2"]
}
]
WICHTIG:
- taskRelevance: 0-100 Score basierend auf Szenario-Eignung UND Phasen-Passung im VERGLEICH zu anderen Tools
- Nur die 2-3 BESTEN Tools auswählen und bewerten
- justification soll VERGLEICHEND sein ("besser als X weil...", "für diese Phase ideal weil...")`;
},
toolEvaluation: (userQuery: string, tool: any, rank: number, taskRelevance: number) => {
return `Sie sind ein DFIR-Experte. Erklären Sie DETAILLIERT die Anwendung dieses bereits bewerteten Tools.
PROBLEM: "${userQuery}"
TOOL: ${tool.name} (bereits bewertet mit ${taskRelevance}% Aufgaben-Eignung)
BESCHREIBUNG: ${tool.description}
Das Tool wurde bereits als Rang ${rank} für diese Aufgabe bewertet. Erklären Sie nun:
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
{
"detailed_explanation": "Detaillierte Erklärung warum und wie dieses Tool für diese spezifische Aufgabe eingesetzt wird",
"implementation_approach": "Konkrete Schritt-für-Schritt Anleitung zur korrekten Anwendung",
"pros": ["Spezifischer Vorteil 1", "Spezifischer Vorteil 2"],
"limitations": ["Spezifische Einschränkung 1", "Spezifische Einschränkung 2"],
"alternatives": "Alternative Ansätze oder Tools falls dieses nicht verfügbar ist"
}
WICHTIG:
- Keine erneute Bewertung - nur detaillierte Erklärung der bereits bewerteten Eignung
- "limitations" soll spezifische technische/methodische Einschränkungen des Tools auflisten
- "pros" soll die Stärken für diese spezifische Aufgabe hervorheben`;
},
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
return `Wählen Sie relevante forensische Konzepte für das Verständnis der empfohlenen Methodik.
${mode === 'workflow' ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ')}
VERFÜGBARE KONZEPTE:
${availableConcepts.slice(0, 15).map((concept: any) => `- ${concept.name}: ${concept.description.slice(0, 80)}...`).join('\n')}
Wählen Sie 2-4 Konzepte aus, die für das Verständnis der forensischen Methodik essentiell sind.
Antworten Sie AUSSCHLIESSLICH mit diesem JSON-Format:
[
{
"conceptName": "Exakter Konzept-Name",
"relevance": "Forensische Relevanz: Warum dieses Konzept für das Verständnis der Methodik kritisch ist"
"toolName": "Exakter Name",
"taskRelevance": 0,
"justification": "6080 Wörter zur phasenspezifischen Eignung",
"limitations": ["Optionale spezifische Einschränkung"]
}
]`;
},
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
const prompt = isWorkflow ?
`Erstellen Sie eine Workflow-Empfehlung basierend auf DFIR-Prinzipien.
toolEvaluation: (userQuery: string, tool: any, rank: number) => {
const itemType = tool.type === 'method' ? 'Methode' : 'Tool';
SZENARIO: "${userQuery}"
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ') || 'Keine Tools ausgewählt'}
Erstellen Sie konkrete methodische Workflow-Schritte für dieses spezifische Szenario unter Berücksichtigung forensischer Best Practices, Objektivität und rechtlicher Verwertbarkeit.
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 120 Wörter.` :
`Erstellen Sie wichtige methodische Überlegungen für die korrekte Methoden-/Tool-Anwendung.
return `Bewerte diese/diesen ${itemType} ausschließlich bzgl. des PROBLEMS.
PROBLEM: "${userQuery}"
EMPFOHLENE TOOLS: ${selectedToolNames.join(', ') || 'Keine Methoden/Tools ausgewählt'}
${itemType.toUpperCase()}: ${tool.name}
TYP: ${tool.type}
Geben Sie kritische methodische Überlegungen, Validierungsanforderungen und Qualitätssicherungsmaßnahmen für die korrekte Anwendung der empfohlenen Methoden/Tools.
ANWEISUNGEN:
- Nur vorhandene Metadaten nutzen (keine Annahmen, keine Websuche).
- "taskRelevance" als GANZZAHL 0100 nach einheitlicher Skala vergeben.
- Realistische Scores i.d.R. 6080, >85 nur bei nahezu perfektem Fit.
- Keine Texte außerhalb des JSON.
WICHTIG: Antworten Sie NUR in fließendem deutschen Text ohne Listen oder Markdown. Maximum 100 Wörter.`;
${RELEVANCE_RUBRIC}
${STRICTNESS}
return prompt;
ANTWORT (NUR JSON):
{
"detailed_explanation": "Warum und wie einsetzen",
"implementation_approach": "Konkrete Schritte",
"pros": ["Vorteil 1", "Vorteil 2"],
"limitations": ["Einschränkung 1"],
"alternatives": "Kurz zu sinnvollen Alternativen",
"taskRelevance": 0
}`;
},
backgroundKnowledgeSelection: (userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]) => {
return `Wähle 24 Konzepte, die das Verständnis/den Einsatz der ausgewählten Tools verbessern.
${mode === 'workflow' ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
AUSGEWÄHLTE TOOLS: ${selectedToolNames.join(', ')}
VERFÜGBARE KONZEPTE (${availableConcepts.length}):
${availableConcepts.map((c: any) => `- ${c.name}: ${c.description}...`).join('\n')}
REGELN:
- Nur Konzepte aus obiger Liste wählen.
- Relevanz kurz und konkret begründen.
${STRICTNESS}
ANTWORT (NUR JSON):
[
{
"conceptName": "Exakter Name",
"relevance": "Warum dieses Konzept hier methodisch wichtig ist"
}
]`;
},
phaseCompletionReasoning: (
originalQuery: string,
phase: any,
selectedToolName: string,
tool: any,
completionContext: string
) => {
return `Begründe knapp die Nachergänzung für Phase "${phase.name}".
URSPRÜNGLICHE ANFRAGE: "${originalQuery}"
PHASE: ${phase.name}${phase.description || ''}
HINZUGEFÜGTES TOOL: ${selectedToolName} (${tool.type})
KONTEXT: ${completionContext}
Antwort: Prägnanter Fließtext, max 40 Wörter, keine Einleitung, keine Liste.`;
},
generatePhaseCompletionPrompt(
originalQuery: string,
phase: any,
candidateTools: any[],
candidateConcepts: any[]
): string {
return `Unterrepräsentierte Phase: "${phase.name}". Ergänze 12 passende Items aus der semantischen Nachsuche.
ORIGINALANFRAGE: "${originalQuery}"
PHASE: ${phase.name}${phase.description || ''}
KANDIDATEN — TOOLS (${candidateTools.length}):
${candidateTools.map((t: any) => `
- ${t.name} (${t.type})
Beschreibung: ${t.description}
Skill Level: ${t.skillLevel}
`).join('')}
${candidateConcepts.length > 0 ? `
KANDIDATEN — KONZEPTE (${candidateConcepts.length}):
${candidateConcepts.map((c: any) => `
- ${c.name}
Beschreibung: ${c.description}
`).join('')}
` : ''}
REGELN:
- Wähle 12 Tools/Methoden, die ${phase.name} sinnvoll ergänzen (keine Ersetzung).
- Nur aus obigen Kandidaten wählen; exakte Namen verwenden.
- Kurze Begründung, warum diese Ergänzung nötig ist.
Skalenhinweis (einheitlich):
${RELEVANCE_RUBRIC}
${STRICTNESS}
ANTWORT (NUR JSON):
{
"selectedTools": ["ToolName1", "ToolName2"],
"selectedConcepts": ["ConceptName1"],
"completionReasoning": "Kurze Erklärung zur Ergänzung der ${phase.name}-Phase"
}`;
},
finalRecommendations: (isWorkflow: boolean, userQuery: string, selectedToolNames: string[]) => {
const focus = isWorkflow
? 'Knappe Workflow-Schritte & Best Practices; neutral formulieren'
: 'Methodische Überlegungen, Validierung, Qualitätssicherung';
return `Erstelle ${isWorkflow ? 'Workflow-Empfehlung' : 'methodische Überlegungen'}.
${isWorkflow ? 'SZENARIO' : 'PROBLEM'}: "${userQuery}"
AUSGEWÄHLT: ${selectedToolNames.join(', ')}${selectedToolNames.length > 5 ? '...' : ''}
Fokus: ${focus}
Antwort: Fließtext, max ${isWorkflow ? '100' : '80'} Wörter. Keine Liste.`;
}
} as const;
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, selectionMethod: string, maxSelectedItems: number): string;
export function getPrompt(key: 'enhancementQuestions', input: string): string;
export function getPrompt(key: 'toolSelection', mode: string, userQuery: string, maxSelectedItems: number): string;
export function getPrompt(key: 'toolSelectionWithData', basePrompt: string, toolsToSend: any[], conceptsToSend: any[]): string;
export function getPrompt(key: 'scenarioAnalysis', isWorkflow: boolean, userQuery: string): string;
export function getPrompt(key: 'investigationApproach', isWorkflow: boolean, userQuery: string): string;
export function getPrompt(key: 'criticalConsiderations', isWorkflow: boolean, userQuery: string): string;
export function getPrompt(key: 'phaseToolSelection', userQuery: string, phase: any, phaseTools: any[]): string;
export function getPrompt(key: 'toolEvaluation', userQuery: string, tool: any, rank: number, taskRelevance: number): string;
export function getPrompt(key: 'toolEvaluation', userQuery: string, tool: any, rank: number): string;
export function getPrompt(key: 'backgroundKnowledgeSelection', userQuery: string, mode: string, selectedToolNames: string[], availableConcepts: any[]): string;
export function getPrompt(key: 'phaseCompletionReasoning', originalQuery: string, phase: any, selectedToolName: string, tool: any, completionContext: string): string;
export function getPrompt(key: 'finalRecommendations', isWorkflow: boolean, userQuery: string, selectedToolNames: string[]): string;
export function getPrompt(key: 'generatePhaseCompletionPrompt', originalQuery: string, phase: any, candidateTools: any[], candidateConcepts: any[]): string;
export function getPrompt(promptKey: keyof typeof AI_PROMPTS, ...args: any[]): string {
try {
const promptFunction = AI_PROMPTS[promptKey];
if (typeof promptFunction === 'function') {
return (promptFunction as (...args: any[]) => string)(...args);
} else {
const f = AI_PROMPTS[promptKey];
if (typeof f === 'function') return (f as (...a: any[]) => string)(...args);
console.error(`[PROMPTS] Invalid prompt key: ${promptKey}`);
return 'Error: Invalid prompt configuration';
}
} catch (error) {
console.error(`[PROMPTS] Error generating prompt ${promptKey}:`, error);
} catch (err) {
console.error(`[PROMPTS] Error generating prompt ${promptKey}:`, err);
return 'Error: Failed to generate prompt';
}
}

501
src/content/README.md Normal file
View File

@@ -0,0 +1,501 @@
# Manuell hinzufügen
Hier müssen Artikel, die eingebettet werden sollen, manuell abgespeichert werden.
Da diese anders lizensiert sein können, sind sie nicht Bestandteil des Open-Source-Repositorys.
**Artikel-Quelle:** https://cloud.cc24.dev/f/47971 (Interner Nextcloud-Share)
Bei Bedarf bitte Kontakt aufnehmen mit mstoeck3@hs-mittweida.de.
# Artikel-Schema
## Dateistruktur
Alle Artikel müssen als Markdown-Dateien im Format `src/content/knowledgebase/` gespeichert werden:
```
src/content/knowledgebase/
├── tool-autopsy-grundlagen.md
├── tool-volatility-memory-analysis.md
├── method-timeline-analysis.md
└── concept-hash-functions.md
```
### Namenskonventionen
- **Tool-Artikel**: `tool-{toolname}-{topic}.md`
- **Methoden-Artikel**: `method-{methodname}-{topic}.md`
- **Konzept-Artikel**: `concept-{conceptname}-{topic}.md`
## Frontmatter-Schema
Jeder Artikel muss einen YAML-Frontmatter-Header mit folgender Struktur haben:
### Pflichtfelder
```yaml
---
title: "Titel des Artikels"
description: "Kurze Beschreibung für die Übersicht und SEO"
last_updated: 2024-01-15 # Datum im YYYY-MM-DD Format
author: "Name des Autors"
published: true
---
```
### Optionale Felder
```yaml
---
# Tool-Verknüpfung
tool_name: "Autopsy" # Exakter Name aus tools.yaml
related_tools:
- "Volatility 3"
- "YARA"
# Klassifizierung
difficulty: "intermediate" # novice, beginner, intermediate, advanced, expert
categories:
- "Tutorial"
- "Best Practices"
tags:
- "memory-analysis"
- "malware"
- "windows"
# Zugriffskontrolle
gated_content: false # true = Authentifizierung erforderlich
---
```
## Vollständiges Beispiel
**Dateiname:** `tool-volatility-memory-analysis-grundlagen.md`
```yaml
---
title: "Volatility 3 Memory Analysis Grundlagen"
description: "Einführung in die RAM-Analyse mit Volatility 3 für Windows-Systeme"
last_updated: 2024-01-15
tool_name: "Volatility 3"
related_tools:
- "Autopsy"
- "YARA"
author: "Max Mustermann"
difficulty: "intermediate"
categories:
- "Tutorial"
- "Memory Analysis"
tags:
- "volatility"
- "memory-dump"
- "malware-analysis"
- "incident-response"
published: true
gated_content: false
---
# Volatility 3 Memory Analysis Grundlagen
Dieses Tutorial zeigt die Grundlagen der Speicher-Analyse...
## Installation
Volatility 3 kann über pip installiert werden:
```bash
pip install volatility3
```
## Erste Schritte
### Memory Dump laden
```bash
vol -f memory.dmp windows.info
```
### Prozesse auflisten
```bash
vol -f memory.dmp windows.pslist
```
## Video-Demonstration
<video src="/videos/volatility-basics.mp4" title="Volatility Grundlagen Tutorial" controls preload="metadata"></video>
## Weiterführende Links
- [Offizielle Dokumentation](https://volatility3.readthedocs.io/)
- [Cheat Sheet](/downloads/volatility-cheat-sheet.pdf)
```
## Content-Features
### Markdown-Unterstützung
- Standard Markdown-Syntax
- Code-Blöcke mit Syntax-Highlighting
- Tabellen
- Listen und verschachtelte Inhalte
- Automatische Inhaltsverzeichnis-Generierung
### Video-Einbindung
Videos können direkt in Markdown eingebettet werden und werden automatisch mit responsiven Containern erweitert:
#### Basis-Video-Einbindung
```html
<video src="/videos/demo.mp4" title="Tool-Demonstration" controls></video>
```
#### Vollständige Video-Konfiguration
```html
<video
src="/videos/advanced-tutorial.mp4"
title="Erweiterte Analysefunktionen"
controls
preload="metadata"
width="720"
height="405"
muted
poster="/images/video-thumbnail.jpg"
>
<p>Ihr Browser unterstützt das Video-Element nicht.</p>
</video>
```
#### Unterstützte Video-Attribute
**Basis-Attribute:**
- `src`: **Erforderlich** - Pfad zur Videodatei (relativ zu `/public/`)
- `title`: **Empfohlen** - Beschreibung für Metadaten und Accessibility
- `controls`: Zeigt Player-Steuerung (Standard-Empfehlung)
**Erweiterte Attribute:**
- `autoplay`: Automatisches Abspielen (nicht empfohlen für UX)
- `muted`: Stummgeschaltet (erforderlich für Autoplay in den meisten Browsern)
- `loop`: Endlosschleife
- `preload`: `"none"` | `"metadata"` | `"auto"` (Standard: `"metadata"`)
- `poster`: Vorschaubild-URL
- `width`/`height`: Feste Dimensionen (Optional, responsive Container anpasst sich automatisch)
**Accessibility-Attribute:**
- `aria-label`: Alternative Beschreibung
- `aria-describedby`: ID eines Elements mit detaillierter Beschreibung
#### iframe-Einbindung (YouTube, Vimeo, etc.)
```html
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
title="YouTube-Tutorial: Forensic Analysis mit Tool XYZ"
width="560"
height="315"
frameborder="0"
allowfullscreen
></iframe>
```
**iframe-Attribute:**
- `src`: **Erforderlich** - Embed-URL des Video-Dienstes
- `title`: **Erforderlich** - Beschreibung für Accessibility
- `width`/`height`: Empfohlene Dimensionen (werden responsive angepasst)
- `frameborder`: Auf `"0"` setzen für modernen Look
- `allowfullscreen`: Vollbild-Modus erlauben
- `loading`: `"lazy"` für Performance-Optimierung
### Automatische Video-Verarbeitung
Das System erweitert Video-Tags automatisch:
**Input:**
```html
<video src="/videos/demo.mp4" title="Demo" controls></video>
```
**Output (automatisch generiert):**
```html
<div class="video-container">
<video
src="/videos/demo.mp4"
title="Demo"
controls
preload="metadata"
data-video-title="Demo"
>
<p>Your browser does not support the video element.</p>
</video>
<div class="video-metadata">
<div class="video-title">Demo</div>
</div>
</div>
```
### Firefox-Kompatibilität
**Wichtiger Hinweis:** Videos müssen in Firefox-kompatiblen Formaten bereitgestellt werden:
#### Empfohlene Formate
**Primäre Formate (höchste Kompatibilität):**
- **MP4 (H.264/AVC)**: `.mp4` - Beste Kompatibilität across Browser
- **WebM (VP8/VP9)**: `.webm` - Moderne Browser, gute Kompression
**Sekundäre Formate:**
- **OGG Theora**: `.ogv` - Fallback für ältere Firefox-Versionen
#### Format-Konvertierung
```bash
# Mit ffmpeg zu Firefox-kompatiblem MP4 konvertieren
ffmpeg -i input.mov -c:v libx264 -c:a aac -movflags +faststart output.mp4
# Mit ffmpeg zu WebM konvertieren
ffmpeg -i input.mov -c:v libvpx-vp9 -c:a libvorbis output.webm
# Multi-Format-Bereitstellung
<video title="Demo" controls>
<source src="/videos/demo.mp4" type="video/mp4">
<source src="/videos/demo.webm" type="video/webm">
<p>Ihr Browser unterstützt das Video-Element nicht.</p>
</video>
```
#### Firefox-spezifische Probleme
Das System erkennt automatisch Firefox und implementiert Error-Recovery:
- **Automatische Fehlererkennung** für nicht unterstützte Formate
- **Fallback-Mechanismen** bei Codec-Problemen
- **Erweiterte Logging** für Debugging
**Bekannte Firefox-Probleme:**
- H.265/HEVC nicht unterstützt
- Proprietäre Codecs teilweise eingeschränkt
- MIME-Type-Sensitivität höher als bei Chrome
### Video-Datei-Management
#### Dateistruktur
```
public/
├── videos/
│ ├── tools/
│ │ ├── autopsy-basics.mp4
│ │ ├── volatility-tutorial.webm
│ │ └── yara-rules-demo.mp4
│ ├── methods/
│ │ ├── timeline-analysis.mp4
│ │ └── disk-imaging.mp4
│ └── concepts/
│ ├── hash-functions.mp4
│ └── chain-custody.mp4
└── images/
└── video-thumbnails/
├── autopsy-thumb.jpg
└── volatility-thumb.jpg
```
#### Dateigröße-Empfehlungen
- **Streaming-Qualität**: 5-15 MB/Minute (720p)
- **High-Quality Tutorials**: 20-40 MB/Minute (1080p)
- **Mobile-Optimiert**: 2-8 MB/Minute (480p)
#### Konventionen
**Dateinamen:**
- Lowercase mit Bindestrichen: `tool-autopsy-installation.mp4`
- Präfix nach Kategorie: `tool-`, `method-`, `concept-`
- Beschreibender Suffix: `-basics`, `-advanced`, `-troubleshooting`
**Video-Titel:**
- Beschreibend und suchfreundlich
- Tool/Methode im Titel erwähnen
- Skill-Level angeben: "Grundlagen", "Erweitert", "Expertenlevel"
### Code-Blöcke
```bash
# Bash-Beispiel
volatility -f memory.dmp --profile=Win7SP1x64 pslist
```
```python
# Python-Beispiel
import volatility.conf as conf
import volatility.registry as registry
```
### Tabellen
| Plugin | Beschreibung | Video-Tutorial |
|--------|--------------|----------------|
| pslist | Prozesse auflisten | [Tutorial ansehen](/videos/pslist-demo.mp4) |
| malfind | Malware finden | [Demo](/videos/malfind-basics.mp4) |
## Artikel-Typen
### 1. Tool-spezifische Artikel (`tool-*.md`)
Artikel die einem konkreten Software-Tool zugeordnet sind:
```yaml
tool_name: "Autopsy" # Muss exakt mit tools.yaml übereinstimmen
```
### 2. Methoden-Artikel (`method-*.md`)
Artikel zu forensischen Methoden und Vorgehensweisen:
```yaml
tool_name: "Timeline Analysis" # Verweis auf method-type in tools.yaml
categories: ["Methodology", "Best Practices"]
```
### 3. Konzept-Artikel (`concept-*.md`)
Artikel zu theoretischen Konzepten und Grundlagen:
```yaml
tool_name: "Hash Functions & Digital Signatures" # Verweis auf concept-type in tools.yaml
categories: ["Theory", "Fundamentals"]
```
Alle Typen erscheinen:
- In der Knowledgebase-Übersicht
- Bei gesetztem `tool_name`: In der Tool-Detailansicht
- Mit entsprechenden Icons und Badges
### 4. Geschützte Artikel
Unabhängig vom Typ können Artikel Authentifizierung erfordern:
```yaml
gated_content: true
```
Erscheinen mit 🔒-Symbol und erfordern Anmeldung.
## Verknüpfungen
### Related Tools
Tools aus dem `related_tools` Array werden automatisch verlinkt:
```yaml
related_tools:
- "YARA" # Wird zu Tool-Details verlinkt
- "Wireshark" # Muss in tools.yaml existieren
```
### Interne Links
```markdown
- [Knowledgebase](/knowledgebase)
- [Tool-Übersicht](/#tools-grid)
- [Anderer Artikel](/knowledgebase/artikel-slug)
```
## SEO und Metadaten
### Automatische Generierung
- URL: `/knowledgebase/{dateiname-ohne-extension}`
- Meta-Description: Aus `description`-Feld
- Breadcrumbs: Automatisch generiert
- Reading-Time: Automatisch berechnet
### Social Sharing
Jeder Artikel erhält automatisch Share-Buttons für:
- URL-Kopieren
- Tool-spezifische Verlinkung
## Validierung
### Pflichtfeld-Prüfung
Das System validiert automatisch:
-`title` ist gesetzt
-`description` ist gesetzt
-`last_updated` ist gültiges Datum
-`difficulty` ist gültiger Wert
-`tool_name` existiert in tools.yaml (falls gesetzt)
### Content-Validierung
- Automatische HTML-Escaping für Sicherheit
- Video-URLs werden validiert
- Broken Links werden geloggt (development)
- Dateinamen-Präfixe helfen bei der Organisation und Verknüpfung
### Video-Validierung
- Dateipfade auf Existenz geprüft (development)
- Format-Kompatibilität gewarnt
- Firefox-spezifische Warnings bei problematischen Formaten
## Deployment
1. Artikel von Nextcloud-Share herunterladen: https://cloud.cc24.dev/f/47971
2. Videos manuell in `public/videos/` bereitstellen (siehe `public/videos/README.md`)
3. Artikel in `src/content/knowledgebase/` ablegen (flache Struktur mit Präfixen)
4. Frontmatter nach Schema überprüfen/anpassen
5. Build-Prozess validiert automatisch
6. Artikel erscheint in Knowledgebase-Übersicht
### Troubleshooting
**Artikel erscheint nicht:**
- `published: true` gesetzt?
- Frontmatter-Syntax korrekt?
- Datei in `src/content/knowledgebase/` (flache Struktur)?
- Dateiname folgt Konvention (`tool-*`, `method-*`, `concept-*`)?
**Tool-Verknüpfung funktioniert nicht:**
- `tool_name` exakt wie in tools.yaml?
- Groß-/Kleinschreibung beachten
**Video lädt nicht:**
- Pfad korrekt? (beginnt mit `/videos/`)
- Datei im `public/videos/` Ordner?
- Unterstütztes Format? (mp4, webm, ogg)
- Firefox-kompatibel? (H.264/AVC für MP4)
**Firefox-Video-Probleme:**
- H.265/HEVC-Codecs vermeiden
- Multiple `<source>`-Tags für Fallbacks nutzen
- Browser-Console auf Codec-Fehler prüfen
- MIME-Types korrekt gesetzt?
## Beispiel-Ordnerstruktur
```
src/content/knowledgebase/
├── tool-autopsy-timeline-analysis.md
├── tool-volatility-basic-commands.md
├── tool-yara-rule-writing.md
├── method-timeline-analysis-fundamentals.md
├── method-disk-imaging-best-practices.md
├── concept-hash-functions-digital-signatures.md
├── concept-regex-pattern-matching.md
└── concept-chain-of-custody.md
public/videos/
├── tools/
│ ├── autopsy-timeline-tutorial.mp4
│ ├── volatility-basics-demo.mp4
│ └── yara-rules-advanced.webm
├── methods/
│ ├── timeline-analysis-walkthrough.mp4
│ └── disk-imaging-best-practices.mp4
└── concepts/
├── hash-functions-explained.mp4
└── chain-custody-procedures.mp4
```

View File

@@ -16,6 +16,11 @@ const knowledgebaseCollection = defineCollection({
tags: z.array(z.string()).default([]),
published: z.boolean().default(true),
gated_content: z.boolean().default(false),
})
});
export const collections = {
knowledgebase: knowledgebaseCollection
};

View File

@@ -1,490 +0,0 @@
---
title: "Extraktion logischer Dateisysteme alter Android-Smartphones - eine KI-Recherche"
tool_name: "Android Logical Imaging"
description: "Wie man alte Android-Handys aufbekommen könnte - eine Recherche von Claude"
last_updated: 2025-07-21
author: "Claude 4 Sonnet (Research)"
difficulty: "advanced"
categories: ["data-collection"]
tags: ["imaging", "filesystem", "hardware-interface"]
sections:
overview: true
installation: true
configuration: true
usage_examples: true
best_practices: true
troubleshooting: true
advanced_topics: true
review_status: "published"
---
# Übersicht
Open-Source Android Forensik bietet robuste Alternativen zu kommerziellen Lösungen wie Cellebrite UFED und Magnet AXIOM. Besonders für ältere Android-Geräte (5+ Jahre) existieren bewährte Methoden zur Datenextraktion und -analyse.
## Kernkomponenten des Open-Source Forensik-Stacks
**Autopsy Digital Forensics Platform** bildet das Fundament mit GUI-basierter Analyse und integrierten Android-Parsing-Fähigkeiten. Die Plattform unterstützt **ALEAPP (Android Logs Events And Protobuf Parser)**, das über 100 Artefakt-Kategorien aus Android-Extraktionen parst.
**Mobile Verification Toolkit (MVT)** von Amnesty International bietet spezialisierte Command-Line-Tools für Android-Analyse mit Fokus auf Kompromittierungserkennung.
**SIFT Workstation** stellt eine komplette Ubuntu-basierte forensische Umgebung mit 125+ vorinstallierten Tools bereit.
## Erfolgsraten nach Gerätealter
- **Pre-2017 Geräte**: 85-98% logische Extraktion, 30-70% physische Extraktion
- **2017-2019 Geräte**: 80-95% logische Extraktion, 15-35% physische Extraktion
- **2020+ Geräte**: 70-85% logische Extraktion, 5-15% physische Extraktion
# Installation
## SIFT Workstation Setup
### Systemanforderungen
- Quad-Core CPU 2.5GHz+
- 16GB+ RAM
- 500GB+ SSD Speicher
- USB 3.0+ Anschlüsse
### Installation
1. Download von [SANS SIFT Workstation](https://www.sans.org/tools/sift-workstation/)
2. VMware/VirtualBox Import der OVA-Datei
3. VM-Konfiguration: 8GB+ RAM, 4+ CPU-Kerne
```bash
# Update nach Installation
sudo apt update && sudo apt upgrade -y
sudo sift update
```
## Autopsy Installation
### Windows Installation
1. Download von [autopsy.com](https://www.autopsy.com/)
2. Java 8+ Installation erforderlich
3. Installation mit Administratorrechten
### Linux Installation
```bash
# Ubuntu/Debian
sudo apt install autopsy sleuthkit
# Oder manueller Download und Installation
wget https://github.com/sleuthkit/autopsy/releases/latest
```
## Essential Tools Installation
### Android Debug Bridge (ADB)
```bash
# Ubuntu/Debian
sudo apt install android-tools-adb android-tools-fastboot
# Windows - Download Android Platform Tools
# https://developer.android.com/studio/releases/platform-tools
```
### ALEAPP Installation
```bash
git clone https://github.com/abrignoni/ALEAPP.git
cd ALEAPP
pip3 install -r requirements.txt
```
### Mobile Verification Toolkit (MVT)
```bash
pip3 install mvt
# Oder via GitHub für neueste Version
git clone https://github.com/mvt-project/mvt.git
cd mvt && pip3 install .
```
### Andriller Installation
```bash
git clone https://github.com/den4uk/andriller.git
cd andriller
pip3 install -r requirements.txt
```
# Konfiguration
## ADB Setup und Gerätevorbereitung
### USB-Debugging aktivieren
1. Entwickleroptionen freischalten (7x Build-Nummer antippen)
2. USB-Debugging aktivieren
3. Gerät via USB verbinden
4. RSA-Fingerprint akzeptieren
### ADB Verbindung testen
```bash
adb devices
# Sollte Gerät mit "device" Status zeigen
adb shell getprop ro.build.version.release # Android Version
adb shell getprop ro.product.model # Gerätemodell
```
## Autopsy Projektkonfiguration
### Case-Setup
1. Neuen Fall erstellen
2. Ermittler-Informationen eingeben
3. Case-Verzeichnis festlegen (ausreichend Speicherplatz)
### Android Analyzer Module aktivieren
- Tools → Options → Modules
- Android Analyzer aktivieren
- ALEAPP Integration konfigurieren
### Hash-Algorithmen konfigurieren
- MD5, SHA-1, SHA-256 für Integritätsprüfung
- Automatische Hash-Berechnung bei Import aktivieren
## MVT Konfiguration
### Konfigurationsdatei erstellen
```yaml
# ~/.mvt/config.yaml
adb_path: "/usr/bin/adb"
output_folder: "/home/user/mvt_output"
```
# Verwendungsbeispiele
## Fall 1: Logische Datenextraktion mit ADB
### Geräteinformationen sammeln
```bash
# Systeminfo
adb shell getprop > device_properties.txt
adb shell cat /proc/version > kernel_info.txt
adb shell mount > mount_info.txt
# Installierte Apps
adb shell pm list packages -f > installed_packages.txt
```
### Datenbank-Extraktion
```bash
# SMS/MMS Datenbank
adb pull /data/data/com.android.providers.telephony/databases/mmssms.db
# Kontakte
adb pull /data/data/com.android.providers.contacts/databases/contacts2.db
# Anrufliste
adb pull /data/data/com.android.providers.contacts/databases/calllog.db
```
### WhatsApp Datenextraktion
```bash
# WhatsApp Datenbanken (Root erforderlich)
adb shell su -c "cp -r /data/data/com.whatsapp/ /sdcard/whatsapp_backup/"
adb pull /sdcard/whatsapp_backup/
```
## Fall 2: Android Backup-Analyse
### Vollständiges Backup erstellen
```bash
# Umfassendes Backup (ohne Root)
adb backup -all -system -apk -shared -f backup.ab
# Backup entschlüsseln (falls verschlüsselt)
java -jar abe.jar unpack backup.ab backup.tar
tar -xf backup.tar
```
### Backup mit ALEAPP analysieren
```bash
python3 aleappGUI.py
# Oder Command-Line
python3 aleapp.py -t tar -i backup.tar -o output_folder
```
## Fall 3: MVT Kompromittierungsanalyse
### Live-Geräteanalyse
```bash
# ADB-basierte Analyse
mvt-android check-adb --output /path/to/output/
# Backup-Analyse
mvt-android check-backup --output /path/to/output/ backup.ab
```
### IOC-Suche mit Pegasus-Indikatoren
```bash
# Mit vorgefertigten IOCs
mvt-android check-adb --iocs /path/to/pegasus.stix2 --output results/
```
## Fall 4: Physische Extraktion (Root erforderlich)
### Device Rooting - MediaTek Geräte
```bash
# MTKClient für MediaTek-Chipsets
git clone https://github.com/bkerler/mtkclient.git
cd mtkclient
python3 mtk payload
# Nach erfolgreichem Root
adb shell su
```
### Vollständiges Memory Dump
```bash
# Partitionslayout ermitteln
adb shell su -c "cat /proc/partitions"
adb shell su -c "ls -la /dev/block/"
# Vollständiges Device Image (Root erforderlich)
adb shell su -c "dd if=/dev/block/mmcblk0 of=/sdcard/full_device.img bs=4096"
adb pull /sdcard/full_device.img
```
# Best Practices
## Rechtliche Compliance
### Dokumentation und Chain of Custody
- **Vollständige Dokumentation**: Wer, Was, Wann, Wo, Warum
- **Hash-Verifikation**: MD5/SHA-256 für alle extrahierten Daten
- **Nur forensische Kopien analysieren**, niemals Originaldaten
- **Schriftliche Genehmigung** für Geräteanalyse einholen
### Familiengeräte und Nachlässe
- Genehmigung durch Nachlassverwalter erforderlich
- Gerichtsbeschlüsse für Cloud-Zugang eventuell nötig
- Drittpartei-Kommunikation kann weiterhin geschützt sein
## Technische Best Practices
### Hash-Integrität sicherstellen
```bash
# Hash vor und nach Transfer prüfen
md5sum original_file.db
sha256sum original_file.db
# Hash-Verifikation dokumentieren
echo "$(date): MD5: $(md5sum file.db)" >> chain_of_custody.log
```
### Sichere Arbeitsumgebung
- Isolierte VM für Forensik-Arbeit
- Netzwerk-Isolation während Analyse
- Verschlüsselte Speicherung aller Evidenz
- Regelmäßige Backups der Case-Datenbanken
### Qualitätssicherung
- Peer-Review kritischer Analysen
- Standardisierte Arbeitsabläufe (SOPs)
- Regelmäßige Tool-Validierung
- Kontinuierliche Weiterbildung
## Erfolgsmaximierung nach Gerätehersteller
### MediaTek-Geräte (Höchste Erfolgsrate)
- BootROM-Exploits für MT6735, MT6737, MT6750, MT6753, MT6797
- MTKClient für Hardware-Level-Zugang
- Erfolgsrate: 80%+ für Geräte 2015-2019
### Samsung-Geräte
- Ältere Knox-Implementierungen umgehbar
- Emergency Dialer Exploits für Android 4.x
- Erfolgsrate: 40-70% je nach Knox-Version
### Pixel/Nexus-Geräte
- Bootloader-Unlocking oft möglich
- Fastboot-basierte Recovery-Installation
- Erfolgsrate: 60-80% bei freigeschaltetem Bootloader
# Troubleshooting
## Problem: ADB erkennt Gerät nicht
### Lösung: USB-Treiber und Berechtigungen
```bash
# Linux: USB-Berechtigungen prüfen
lsusb | grep -i android
sudo chmod 666 /dev/bus/usb/XXX/XXX
# udev-Regeln erstellen
echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"' | sudo tee /etc/udev/rules.d/51-android.rules
sudo udevadm control --reload-rules
```
### Windows: Treiber-Installation
1. Geräte-Manager öffnen
2. Android-Gerät mit Warnsymbol finden
3. Treiber manuell installieren (Android USB Driver)
## Problem: Verschlüsselte Android Backups
### Lösung: Android Backup Extractor
```bash
# ADB Backup Extractor installieren
git clone https://github.com/nelenkov/android-backup-extractor.git
cd android-backup-extractor
gradle build
# Backup entschlüsseln
java -jar abe.jar unpack backup.ab backup.tar [password]
```
## Problem: Unzureichende Berechtigungen für Datenextraktion
### Lösung: Alternative Extraktionsmethoden
```bash
# AFLogical OSE für begrenzte Extraktion ohne Root
# WhatsApp Key/DB Extractor für spezifische Apps
# Backup-basierte Extraktion als Fallback
# Custom Recovery für erweiterten Zugang
fastboot flash recovery twrp-device.img
```
## Problem: ALEAPP Parsing-Fehler
### Lösung: Datenformat-Probleme beheben
```bash
# Log-Dateien prüfen
python3 aleapp.py -t dir -i /path/to/data -o output --debug
# Spezifische Parser deaktivieren
# Manuelle SQLite-Analyse bei Parser-Fehlern
sqlite3 database.db ".tables"
sqlite3 database.db ".schema table_name"
```
# Erweiterte Techniken
## Memory Forensics mit LiME
### LiME für ARM-Devices kompilieren
```bash
# Cross-Compilation Setup
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabi-
export KERNEL_DIR=/path/to/kernel/source
# LiME Module kompilieren
git clone https://github.com/504ensicsLabs/LiME.git
cd LiME/src
make
# Memory Dump erstellen (Root erforderlich)
adb push lime.ko /data/local/tmp/
adb shell su -c "insmod /data/local/tmp/lime.ko 'path=/sdcard/memory.lime format=lime'"
```
### Volatility-Analyse von Android Memory
```bash
# Memory Dump analysieren
python vol.py -f memory.lime --profile=Linux <profile> linux.pslist
python vol.py -f memory.lime --profile=Linux <profile> linux.bash
python vol.py -f memory.lime --profile=Linux <profile> linux.netstat
```
## FRIDA-basierte Runtime-Analyse
### FRIDA für Kryptographie-Hooks
```javascript
// crypto_hooks.js - SSL/TLS Traffic abfangen
Java.perform(function() {
var SSLContext = Java.use("javax.net.ssl.SSLContext");
SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom').implementation = function(keyManagers, trustManagers, secureRandom) {
console.log("[+] SSLContext.init() called");
this.init(keyManagers, trustManagers, secureRandom);
};
});
```
### FRIDA Installation und Verwendung
```bash
# FRIDA Server auf Android-Gerät installieren
adb push frida-server /data/local/tmp/
adb shell su -c "chmod 755 /data/local/tmp/frida-server"
adb shell su -c "/data/local/tmp/frida-server &"
# Script ausführen
frida -U -l crypto_hooks.js com.target.package
```
## Custom Recovery und Fastboot-Exploits
### TWRP Installation für forensischen Zugang
```bash
# Bootloader entsperren (Herstellerabhängig)
fastboot oem unlock
# Oder
fastboot flashing unlock
# TWRP flashen
fastboot flash recovery twrp-device.img
fastboot boot twrp-device.img # Temporäre Installation
# In TWRP: ADB-Zugang mit Root-Berechtigungen
adb shell mount /system
adb shell mount /data
```
### Partitions-Imaging mit dd
```bash
# Vollständige Partition-Liste
adb shell cat /proc/partitions
# Kritische Partitionen extrahieren
adb shell dd if=/dev/block/bootdevice/by-name/system of=/external_sd/system.img
adb shell dd if=/dev/block/bootdevice/by-name/userdata of=/external_sd/userdata.img
adb shell dd if=/dev/block/bootdevice/by-name/boot of=/external_sd/boot.img
```
## SQLite Forensics und gelöschte Daten
### Erweiterte SQLite-Analyse
```bash
# Freelist-Analyse für gelöschte Einträge
sqlite3 database.db "PRAGMA freelist_count;"
sqlite3 database.db "PRAGMA page_size;"
# WAL-Datei Analyse
sqlite3 database.db "PRAGMA wal_checkpoint;"
strings database.db-wal | grep -i "search_term"
# Undark für Deleted Record Recovery
undark database.db --freelist --export-csv
```
### Timeline-Rekonstruktion
```bash
# Autopsy Timeline-Generierung
# Tools → Generate Timeline
# Analyse von MAC-Times (Modified, Accessed, Created)
# Plaso Timeline-Tools
log2timeline.py timeline.plaso /path/to/android/data/
psort.py -o dynamic timeline.plaso
```
## Weiterführende Ressourcen
### Dokumentation und Standards
- [NIST SP 800-101 Rev. 1 - Mobile Device Forensics Guidelines](https://csrc.nist.gov/pubs/sp/800/101/r1/final)
- [SANS FOR585 - Smartphone Forensics](https://www.sans.org/cyber-security-courses/advanced-smartphone-mobile-device-forensics/)
- [ALEAPP GitHub Repository](https://github.com/abrignoni/ALEAPP)
- [MVT Documentation](https://docs.mvt.re/en/latest/)
### Community und Weiterbildung
- [Autopsy User Documentation](https://sleuthkit.org/autopsy/docs/)
- [Android Forensics References](https://github.com/impillar/AndroidReferences/blob/master/AndroidTools.md)
- [Digital Forensics Framework Collection](https://github.com/mesquidar/ForensicsTools)
### Spezialisierte Tools
- [MTKClient für MediaTek Exploits](https://github.com/bkerler/mtkclient)
- [Android Forensics Framework](https://github.com/nowsecure/android-forensics)
- [Santoku Linux Mobile Forensics Distribution](https://santoku-linux.com/)
---
**Wichtiger Hinweis**: Diese Anleitung dient ausschließlich für autorisierte forensische Untersuchungen. Stellen Sie sicher, dass Sie über entsprechende rechtliche Befugnisse verfügen, bevor Sie diese Techniken anwenden. Bei Zweifeln konsultieren Sie Rechtsberatung.

View File

@@ -1,141 +0,0 @@
---
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: "Claude 4 Sonnet"
difficulty: "intermediate"
categories: ["incident-response", "forensics", "penetration-testing"]
tags: ["live-boot", "tool-collection", "penetration-testing", "forensics-suite", "virtualization", "arm-support"]
sections:
overview: true
installation: true
configuration: true
usage_examples: true
best_practices: true
troubleshooting: true
advanced_topics: true
review_status: "published"
---
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
# Übersicht
Kali Linux ist eine auf Debian basierende Linux-Distribution, die speziell für Penetration Testing, digitale Forensik, Reverse Engineering und Incident Response entwickelt wurde. Mit über 600 vorinstallierten Tools ist sie ein unverzichtbares Werkzeug für Security-Experten, Ermittler und forensische Analysten. Die Live-Boot-Funktion erlaubt es, Systeme ohne Spuren zu hinterlassen zu analysieren ideal für forensische Untersuchungen.
## Installation
### Option 1: Live-System (USB/DVD)
1. ISO-Image von [kali.org](https://www.kali.org/get-kali/) herunterladen.
2. Mit **Rufus** oder **balenaEtcher** auf einen USB-Stick schreiben.
3. Vom USB-Stick booten (ggf. Boot-Reihenfolge im BIOS anpassen).
4. Kali kann direkt ohne Installation im Live-Modus verwendet werden.
### Option 2: Installation auf Festplatte
1. ISO-Image booten und **Graphical Install** wählen.
2. Schritt-für-Schritt durch den Installationsassistenten navigieren:
- Sprache, Zeitzone und Tastaturlayout auswählen
- Partitionierung konfigurieren (automatisch oder manuell)
- Benutzerkonten erstellen
3. Nach Installation Neustart durchführen.
### Option 3: Virtuelle Maschine (VM)
- Offizielle VM-Images für VirtualBox und VMware von der [Kali-Website](https://www.kali.org/get-kali/#kali-virtual-machines)
- Importieren, ggf. Netzwerkbrücke und Shared Folders aktivieren
## Konfiguration
### Netzwerkeinstellungen
- Konfiguration über `nmtui` oder `/etc/network/interfaces`
- VPN und Proxy-Integration über GUI oder Terminal
### Updates & Paketquellen
```bash
sudo apt update && sudo apt full-upgrade
````
> Hinweis: `kali-rolling` ist die Standard-Distribution für kontinuierliche Updates.
### Sprache & Lokalisierung
```bash
sudo dpkg-reconfigure locales
sudo dpkg-reconfigure keyboard-configuration
```
## Verwendungsbeispiele
### 1. Netzwerkscan mit Nmap
```bash
nmap -sS -T4 -A 192.168.1.0/24
```
### 2. Passwort-Cracking mit John the Ripper
```bash
john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
```
### 3. Forensik mit Autopsy
```bash
autopsy &
```
### 4. Android-Analyse mit MobSF (in Docker)
```bash
docker pull opensecurity/mobile-security-framework-mobsf
docker run -it -p 8000:8000 mobsf
```
## Best Practices
* Nutze immer **aktuelle Snapshots** oder VM-Clones vor gefährlichen Tests
* Verwende separate Netzwerke (z.B. Host-only oder NAT) für Tests
* Deaktiviere automatisches WLAN bei forensischen Analysen
* Prüfe und aktualisiere regelmäßig Toolsets (`apt`, `git`, `pip`)
* Halte deine ISO-Images versioniert für forensische Reproduzierbarkeit
## Troubleshooting
### Problem: Keine Internetverbindung nach Installation
**Lösung:** Netzwerkadapter prüfen, ggf. mit `ifconfig` oder `ip a` überprüfen, DHCP aktivieren.
### Problem: Tools fehlen nach Update
**Lösung:** Tool-Gruppen wie `kali-linux-default` manuell nachinstallieren:
```bash
sudo apt install kali-linux-default
```
### Problem: „Permission Denied“ bei Tools
**Lösung:** Root-Rechte nutzen oder mit `sudo` ausführen.
## Weiterführende Themen
* **Kustomisierung von Kali ISOs** mit `live-build`
* **NetHunter**: Kali für mobile Geräte (Android)
* **Kali Purple**: Defensive Security Suite
* Integration mit **Cloud-Infrastrukturen** via WSL oder Azure
---
**Links & Ressourcen:**
* Offizielle Website: [https://kali.org](https://kali.org/)
* Dokumentation: [https://docs.kali.org/](https://docs.kali.org/)
* GitLab Repo: [https://gitlab.com/kalilinux](https://gitlab.com/kalilinux)
* Discord-Community: [https://discord.com/invite/kali-linux](https://discord.com/invite/kali-linux)

View File

@@ -1,133 +0,0 @@
---
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: "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"]
sections:
overview: true
installation: true
configuration: true
usage_examples: true
best_practices: true
troubleshooting: true
advanced_topics: false
review_status: "published"
---
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
# Übersicht
**MISP (Malware Information Sharing Platform & Threat Sharing)** ist eine freie Open-Source-Plattform zur strukturierten Erfassung, Speicherung, Analyse und gemeinsamen Nutzung von Cyber-Bedrohungsdaten. Mit über 40.000 Instanzen weltweit ist MISP der De-facto-Standard für den Austausch von Indicators of Compromise (IoCs) und Threat Intelligence zwischen CERTs, SOCs, Strafverfolgungsbehörden und anderen sicherheitsrelevanten Organisationen.
Die föderierte Architektur ermöglicht einen kontrollierten, dezentralen Austausch von Informationen über vertrauenswürdige Partner hinweg. Durch Taxonomien, Tags und integrierte APIs ist eine automatische Anreicherung, Korrelation und Verarbeitung von Informationen in SIEMs, Firewalls oder Endpoint-Lösungen möglich.
## Installation
### Voraussetzungen
- **Server-Betriebssystem:** Linux (empfohlen: Debian/Ubuntu)
- **Abhängigkeiten:** MariaDB/MySQL, PHP, Apache/Nginx, Redis
- **Ressourcen:** Mindestens 4 GB RAM, SSD empfohlen
### Installationsschritte
```bash
# Beispiel für Debian/Ubuntu:
sudo apt update && sudo apt install -y curl gnupg git python3 python3-pip redis-server mariadb-server apache2 php libapache2-mod-php
# MISP klonen
git clone https://github.com/MISP/MISP.git /var/www/MISP
# Setup-Skript nutzen
cd /var/www/MISP && bash INSTALL/INSTALL.debian.sh
````
Weitere Details: [Offizielle Installationsanleitung](https://misp.github.io/MISP/INSTALL.debian/)
## Konfiguration
### Webserver
* HTTPS aktivieren (Let's Encrypt oder Reverse Proxy)
* PHP-Konfiguration anpassen (`upload_max_filesize`, `memory_limit`, `post_max_size`)
### Benutzerrollen
* Administrator, Org-Admin, Analyst etc.
* Zugriffsbeschränkungen nach Organisation/Feed definierbar
### Feeds und Galaxies
* Aktivierung von Feeds (z.B. CIRCL, Abuse.ch, OpenCTI)
* Nutzung von Galaxies zur Klassifizierung (APT-Gruppen, Malware-Familien)
## Verwendungsbeispiele
### Beispiel 1: Import von IoCs aus externem Feed
1. Feed aktivieren unter **Administration → List Feeds**
2. Feed synchronisieren
3. Ereignisse durchsuchen, analysieren, ggf. mit eigenen Daten korrelieren
### Beispiel 2: Automatisierte Anbindung an SIEM
* REST-API-Token erstellen
* API-Calls zur Abfrage neuer Events (z.B. mit Python, Logstash oder MISP Workbench)
* Integration in Security-Systeme über JSON/STIX export
## Best Practices
* Regelmäßige Backups der Datenbank
* Taxonomien konsistent verwenden
* Nutzung der Sighting-Funktion zur Validierung von IoCs
* Vertrauensstufen (TLP, PAP) korrekt setzen
* Nicht nur konsumieren auch teilen!
## Troubleshooting
### Problem: MISP-Feeds laden nicht
**Lösung:**
* Internetverbindung prüfen
* Cronjobs aktiv?
* Logs prüfen: `/var/www/MISP/app/tmp/logs/error.log`
### Problem: API gibt 403 zurück
**Lösung:**
* Ist der API-Key korrekt und aktiv?
* Rechte des Benutzers überprüfen
* IP-Filter im MISP-Backend beachten
### Problem: Hohe Datenbanklast
**Lösung:**
* Indizes optimieren
* Redis aktivieren
* Alte Events regelmäßig archivieren oder löschen
## Weiterführende Themen
* STIX2-Import/Export
* Erweiterungen mit MISP Modules (z.B. für Virustotal, YARA)
* Föderierte Netzwerke und Community-Portale
* Integration mit OpenCTI oder TheHive
---
**Links:**
* 🌐 [Offizielle Projektseite](https://misp-project.org/)
* 📦 [CC24-MISP-Instanz](https://misp.cc24.dev)
* 📊 [Status-Monitoring](https://status.mikoshi.de/api/badge/34/status)
Lizenz: **AGPL-3.0**

View File

@@ -1,124 +0,0 @@
---
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: "Claude 4 Sonnet"
difficulty: "novice"
categories: ["collaboration-general"]
tags: ["web-based", "collaboration", "file-sharing", "api", "encryption", "document-management"]
sections:
overview: true
installation: true
configuration: true
usage_examples: true
best_practices: true
troubleshooting: true
advanced_topics: false
review_status: "published"
---
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
# Übersicht
Nextcloud ist eine Open-Source-Cloud-Suite, die speziell für die sichere Zusammenarbeit entwickelt wurde. Sie eignet sich ideal für forensische Teams, da sie eine DSGVO-konforme Umgebung mit verschlüsselter Dateiablage, Office-Integration und Videokonferenzen bereitstellt. Zusätzlich bietet Nextcloud einen integrierten SSO-Provider, der das Identitätsmanagement für andere forensische Tools stark vereinfacht.
Skalierbar von kleinen Raspberry-Pi-Installationen bis hin zu hochverfügbaren Multi-Node-Setups.
- **Website:** [nextcloud.com](https://nextcloud.com/)
- **Demo/Projektinstanz:** [cloud.cc24.dev](https://cloud.cc24.dev)
- **Statusseite:** [Mikoshi Status](https://status.mikoshi.de/api/badge/11/status)
- **Lizenz:** AGPL-3.0
---
## Installation
### Voraussetzungen
- Linux-Server oder Raspberry Pi
- PHP 8.1 oder höher
- MariaDB/PostgreSQL
- Webserver (Apache/Nginx)
- SSL-Zertifikat (empfohlen: Let's Encrypt)
### Installationsschritte (Ubuntu Beispiel)
```bash
sudo apt update && sudo apt upgrade
sudo apt install apache2 mariadb-server libapache2-mod-php php php-mysql \
php-gd php-xml php-mbstring php-curl php-zip php-intl php-bcmath unzip
wget https://download.nextcloud.com/server/releases/latest.zip
unzip latest.zip -d /var/www/
chown -R www-data:www-data /var/www/nextcloud
````
Danach den Web-Installer im Browser aufrufen (`https://<your-domain>/nextcloud`) und Setup abschließen.
## Konfiguration
* **Trusted Domains** in `config.php` definieren
* SSO mit OpenID Connect aktivieren
* Dateiverschlüsselung aktivieren (`Settings → Security`)
* Benutzer und Gruppen über LDAP oder SAML integrieren
## Verwendungsbeispiele
### Gemeinsame Fallbearbeitung
1. Ermittlungsordner als geteiltes Gruppenverzeichnis anlegen
2. Versionierung und Kommentare zu forensischen Berichten aktivieren
3. Vorschau für Office-Dateien, PDFs und Bilder direkt im Browser nutzen
### Videokonferenzen mit "Nextcloud Talk"
* Sichere Kommunikation zwischen Ermittlern und Sachverständigen
* Ende-zu-Ende-verschlüsselt
* Bildschirmfreigabe möglich
### Automatischer Dateiimport per API
* REST-Schnittstelle nutzen, um z.B. automatisch Logdateien oder Exportdaten hochzuladen
* Ideal für Anbindung an SIEM, DLP oder Analyse-Pipelines
## Best Practices
* Zwei-Faktor-Authentifizierung aktivieren
* Tägliche Backups der Datenbank und Datenstruktur
* Nutzung von OnlyOffice oder Collabora für revisionssichere Dokumentenbearbeitung
* Zugriff regelmäßig überprüfen, insbesondere bei externen Partnern
## Troubleshooting
### Problem: Langsame Performance
**Lösung:** APCu aktivieren und Caching optimieren (`config.php → 'memcache.local'`).
### Problem: Dateien erscheinen nicht im Sync
**Lösung:** Cronjob für `files:scan` konfigurieren oder manuell ausführen:
```bash
sudo -u www-data php /var/www/nextcloud/occ files:scan --all
```
### Problem: Fehlermeldung "Trusted domain not set"
**Lösung:** In `config/config.php` Eintrag `trusted_domains` korrekt konfigurieren:
```php
'trusted_domains' =>
array (
0 => 'yourdomain.tld',
1 => 'cloud.cc24.dev',
),
```
## Weiterführende Themen
* **Integration mit Forensik-Plattformen** (über WebDAV, API oder SSO)
* **Custom Apps entwickeln** für spezielle Ermittlungs-Workflows
* **Auditing aktivieren**: Nutzung und Änderungen nachvollziehen mit Protokollierungsfunktionen

View File

@@ -1,110 +0,0 @@
---
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: "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"]
sections:
overview: true
installation: false
configuration: false
usage_examples: true
best_practices: true
troubleshooting: false
advanced_topics: true
review_status: "published"
---
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
# Übersicht
**Regular Expressions (Regex)** sind ein leistungsfähiges Werkzeug zur Erkennung, Extraktion und Transformation von Zeichenfolgen anhand vordefinierter Muster. In der digitalen Forensik sind Regex-Ausdrücke unverzichtbar: Sie helfen beim Auffinden von IP-Adressen, Hash-Werten, Dateipfaden, Malware-Signaturen oder Kreditkartennummern in großen Mengen unstrukturierter Daten wie Logdateien, Netzwerktraces oder Memory Dumps.
Regex ist nicht auf eine bestimmte Plattform oder Software beschränkt es wird in nahezu allen gängigen Programmiersprachen, Texteditoren und forensischen Tools unterstützt.
## Verwendungsbeispiele
### 1. IP-Adressen extrahieren
```regex
\b(?:\d{1,3}\.){3}\d{1,3}\b
````
Verwendung:
* Finden von IP-Adressen in Firewall-Logs oder Packet Captures.
* Beispiel-Zeile:
```
Connection from 192.168.1.101 to port 443 established
```
### 2. E-Mail-Adressen identifizieren
```regex
[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
```
Verwendung:
* Erkennung von kompromittierten Accounts in Phishing-E-Mails.
* Analyse von Useraktivitäten oder Kommunikationsverläufen.
### 3. Hash-Werte erkennen (z.B. SHA-256)
```regex
\b[A-Fa-f0-9]{64}\b
```
Verwendung:
* Extraktion von Malware-Hashes aus Memory Dumps oder YARA-Logs.
### 4. Zeitstempel in Logdateien extrahieren
```regex
\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}
```
Verwendung:
* Zeitsensitive Korrelationsanalysen (z.B. bei Intrusion Detection oder Timeline-Rekonstruktionen).
## Best Practices
* **Regex testen**: Nutze Plattformen wie [regexr.com](https://regexr.com/) oder [regex101.com](https://regex101.com/) zur Validierung.
* **Performance beachten**: Komplexe Ausdrücke können ineffizient sein und Systeme verlangsamen verwende Lazy Quantifiers (`*?`, `+?`) bei Bedarf.
* **Escape-Zeichen korrekt anwenden**: Spezielle Zeichen wie `.` oder `\` müssen bei Bedarf mit `\\` oder `\.` maskiert werden.
* **Portabilität prüfen**: Unterschiedliche Regex-Engines (z.B. Python `re`, PCRE, JavaScript) interpretieren manche Syntax leicht unterschiedlich.
* **Lesbarkeit fördern**: Verwende benannte Gruppen (`(?P<name>...)`) und Kommentare (`(?x)`), um reguläre Ausdrücke besser wartbar zu machen.
## Weiterführende Themen
### Lookaheads und Lookbehinds
Mit **Lookaheads** (`(?=...)`) und **Lookbehinds** (`(?<=...)`) können Bedingungen formuliert werden, ohne dass der Text Teil des Matchs wird.
Beispiel: Alle `.exe`-Dateinamen **ohne** das Wort `safe` davor matchen:
```regex
(?<!safe\s)[\w-]+\.exe
```
### Regex in Forensik-Tools
* **YARA**: Unterstützt Regex zur Erstellung von Malware-Signaturen.
* **Wireshark**: Filtert Payloads anhand von Regex-ähnlicher Syntax.
* **Splunk & ELK**: Verwenden Regex für Logparsing und Visualisierung.
* **Volatility Plugins**: Extrahieren Artefakte mit Regex-basierten Scans.
---
> 🔤 **Regex ist ein universelles Werkzeug für Analysten, Ermittler und Entwickler, um versteckte Informationen schnell und flexibel aufzuspüren.**
>
> Nutze es überall dort, wo Textdaten eine Rolle spielen.

View File

@@ -1,161 +0,0 @@
---
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: "Claude 4 Sonnet"
difficulty: "advanced"
categories: ["incident-response", "malware-analysis", "network-forensics"]
tags: ["web-based", "endpoint-monitoring", "artifact-extraction", "scripting", "live-forensics", "hunting"]
sections:
overview: true
installation: true
configuration: true
usage_examples: true
best_practices: true
troubleshooting: true
advanced_topics: true
review_status: "published"
---
> **⚠️ Hinweis**: Dies ist ein vorläufiger, KI-generierter Knowledgebase-Eintrag. Wir freuen uns über Verbesserungen und Ergänzungen durch die Community!
# Übersicht
Velociraptor ist ein Open-Source-Tool zur Endpoint-Forensik mit Fokus auf Skalierbarkeit, Präzision und Geschwindigkeit. Es ermöglicht die zielgerichtete Erfassung und Analyse digitaler Artefakte über eine eigene Query Language VQL (Velociraptor Query Language). Die Architektur erlaubt remote Zugriff auf tausende Endpoints gleichzeitig, ohne dass vollständige Disk-Images erforderlich sind.
## Hauptmerkmale
- 🌐 Web-basierte Benutzeroberfläche
- 💡 VQL mächtige, SQL-ähnliche Abfragesprache
- 🚀 Hochskalierbare Hunt-Funktionalität
- 🔍 Artefaktbasierte Sammlung (ohne Full-Image)
- 🖥️ Plattformunterstützung für Windows, macOS, Linux
- 📦 Apache 2.0 Lizenz Open Source
Weitere Infos: [velociraptor.app](https://www.velociraptor.app/)
Projektspiegel: [raptor.cc24.dev](https://raptor.cc24.dev)
Status: ![Status](https://status.mikoshi.de/api/badge/33/status)
---
## Installation
### Voraussetzungen
- Python ≥ 3.9
- Adminrechte auf dem System
- Firewall-Freigaben für Webport (Standard: 8000)
### Installation unter Linux/macOS
```bash
wget https://github.com/Velocidex/velociraptor/releases/latest/download/velociraptor
chmod +x velociraptor
sudo mv velociraptor /usr/local/bin/
````
### Installation unter Windows
1. Download der `.exe` von der [Release-Seite](https://github.com/Velocidex/velociraptor/releases)
2. Ausführung in PowerShell mit Adminrechten:
```powershell
.\velociraptor.exe config generate > server.config.yaml
```
---
## Konfiguration
### Server Setup
1. Generiere die Konfigurationsdatei:
```bash
velociraptor config generate > server.config.yaml
```
2. Starte den Server:
```bash
velociraptor --config server.config.yaml frontend
```
3. Zugriff über Browser via `https://<hostname>:8000`
### Client Deployment
* MSI/EXE für Windows, oder `deb/rpm` für Linux
* Unterstützt automatische Registrierung am Server
* Deployment über GPO, Puppet, Ansible etc. möglich
---
## Verwendungsbeispiele
### 1. Live-Memory-Artefakte sammeln
```vql
SELECT * FROM Artifact.MemoryInfo()
```
### 2. Hunt starten auf verdächtige Prozesse
```vql
SELECT * FROM pslist()
WHERE Name =~ "mimikatz|cobaltstrike"
```
### 3. Dateiinhalt extrahieren
```vql
SELECT * FROM glob(globs="C:\\Users\\*\\AppData\\*.dat")
```
---
## Best Practices
* Erstelle eigene Artefakte für unternehmensspezifische Bedrohungsmodelle
* Verwende "Notebook"-Funktion für strukturierte Analysen
* Nutze "Labels", um Endpoints zu organisieren (z.B. `location:Berlin`)
* Kombiniere Velociraptor mit SIEM/EDR-Systemen über REST API
---
## Troubleshooting
### Problem: Keine Verbindung vom Client zum Server
**Lösung:**
* Ports freigegeben? (Default: 8000/tcp)
* TLS-Zertifikate korrekt generiert?
* `server.config.yaml` auf korrekte `public_ip` prüfen
### Problem: Hunt hängt in Warteschleife
**Lösung:**
* Genügend Worker-Prozesse aktiv?
* Endpoint online?
* `log_level` auf `debug` setzen und Log analysieren
---
## Weiterführende Themen
* Eigene Artefakte schreiben mit VQL
* Integration mit ELK Stack
* Automatisiertes Incident Response Playbook
* Velociraptor als IR-as-a-Service einsetzen
---
🧠 **Tipp:** Die Lernkurve bei VQL ist steil aber mit hohem ROI. Testumgebung aufsetzen und mit Community-Artefakten starten.
📚 Weitere Ressourcen:
* [Offizielle Doku](https://docs.velociraptor.app/)
* [YouTube Channel](https://www.youtube.com/c/VelociraptorDFIR)
* [Community auf Discord](https://www.velociraptor.app/community/)

File diff suppressed because it is too large Load Diff

12
src/env.d.ts vendored
View File

@@ -11,6 +11,9 @@ declare global {
showToolDetails: (toolName: string, modalType?: string) => void;
hideToolDetails: (modalType?: string) => void;
hideAllToolDetails: () => void;
matrixShowToolDetails?: (toolName: string, modalType?: string) => void;
matrixHideToolDetails?: (modalType?: string) => void;
toggleKbEntry: (entryId: string) => void;
toggleDomainAgnosticSection: (sectionId: string) => void;
restoreAIResults?: () => void;
@@ -22,9 +25,9 @@ declare global {
findToolByIdentifier: (tools: any[], identifier: string) => any | undefined;
isToolHosted: (tool: any) => boolean;
checkClientAuth: (context?: string) => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: string) => Promise<boolean>;
showIfAuthenticated: (selector: string, context?: string) => Promise<void>;
checkClientAuth: (context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<{authenticated: boolean; authRequired: boolean; expires?: string}>;
requireClientAuth: (callback?: () => void, returnUrl?: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<boolean>;
showIfAuthenticated: (selector: string, context?: 'contributions' | 'ai' | 'general' | 'gatedcontent') => Promise<void>;
setupAuthButtons: (selector?: string) => void;
scrollToElement: (element: Element | null, options?: ScrollIntoViewOptions) => void;
@@ -39,6 +42,9 @@ declare global {
toggleAllScenarios?: () => void;
showShareDialog?: (shareButton: Element) => void;
modalHideInProgress?: boolean;
shareArticle: (button: HTMLElement, url: string, title: string) => Promise<void>;
shareCurrentArticle: (button: HTMLElement) => Promise<void>;
}
}

View File

@@ -3,6 +3,9 @@ import Navigation from '../components/Navigation.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
import '../styles/auditTrail.css';
import '../styles/knowledgebase.css';
import '../styles/palette.css';
import '../styles/autocomplete.css';
export interface Props {
title: string;
@@ -22,36 +25,43 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<script>
function createToolSlug(toolName) {
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
}
async function loadUtilityFunctions() {
try {
const { createToolSlug, findToolByIdentifier, isToolHosted } = await import('../utils/clientUtils.js');
return toolName.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
}
(window as any).createToolSlug = createToolSlug;
(window as any).findToolByIdentifier = findToolByIdentifier;
(window as any).isToolHosted = isToolHosted;
function findToolByIdentifier(tools, identifier) {
console.log('[UTILS] Utility functions loaded successfully');
} catch (error) {
console.error('Failed to load utility functions:', error);
(window as any).createToolSlug = (toolName: string) => {
if (!toolName || typeof toolName !== 'string') return '';
return toolName.toLowerCase().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
};
(window as any).findToolByIdentifier = (tools: any[], identifier: string) => {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool =>
return tools.find((tool: any) =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
(window as any).createToolSlug(tool.name) === identifier.toLowerCase()
);
}
};
function isToolHosted(tool) {
(window as any).isToolHosted = (tool: any) => {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
};
console.log('[UTILS] Fallback utility functions registered');
}
}
function scrollToElement(element, options = {}) {
function scrollToElement(element: Element | null, options = {}) {
if (!element) return;
setTimeout(() => {
@@ -67,17 +77,21 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
}, 100);
}
function scrollToElementById(elementId, options = {}) {
function scrollToElementById(elementId: string, options = {}) {
const element = document.getElementById(elementId);
if (element) {
scrollToElement(element, options);
}
}
function scrollToElementBySelector(selector, options = {}) {
function scrollToElementBySelector(selector: string, options = {}) {
const element = document.querySelector(selector);
if (element) {
scrollToElement(element, options);
}
}
function prioritizeSearchResults(tools, searchTerm) {
function prioritizeSearchResults(tools: any[], searchTerm: string) {
if (!searchTerm || !searchTerm.trim()) {
return tools;
}
@@ -85,8 +99,8 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
const lowerSearchTerm = searchTerm.toLowerCase().trim();
return tools.sort((a, b) => {
const aTagsLower = (a.tags || []).map(tag => tag.toLowerCase());
const bTagsLower = (b.tags || []).map(tag => tag.toLowerCase());
const aTagsLower = (a.tags || []).map((tag: string) => tag.toLowerCase());
const bTagsLower = (b.tags || []).map((tag: string) => tag.toLowerCase());
const aExactTag = aTagsLower.includes(lowerSearchTerm);
const bExactTag = bTagsLower.includes(lowerSearchTerm);
@@ -98,15 +112,14 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
});
}
(window as any).createToolSlug = createToolSlug;
(window as any).findToolByIdentifier = findToolByIdentifier;
(window as any).isToolHosted = isToolHosted;
(window as any).scrollToElement = scrollToElement;
(window as any).scrollToElementById = scrollToElementById;
(window as any).scrollToElementBySelector = scrollToElementBySelector;
(window as any).prioritizeSearchResults = prioritizeSearchResults;
document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('DOMContentLoaded', async () => {
await loadUtilityFunctions();
const THEME_KEY = 'dfir-theme';
function getSystemTheme() {
@@ -117,12 +130,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
return localStorage.getItem(THEME_KEY) || 'auto';
}
function applyTheme(theme) {
function applyTheme(theme: string) {
const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
document.documentElement.setAttribute('data-theme', effectiveTheme);
}
function updateThemeToggle(theme) {
function updateThemeToggle(theme: string) {
document.querySelectorAll('[data-theme-toggle]').forEach(button => {
button.setAttribute('data-current-theme', theme);
});
@@ -158,6 +171,43 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
getStoredTheme
};
(window as any).showToolDetails = function(toolName: string, modalType: string = 'primary') {
let attempts = 0;
const maxAttempts = 50;
const tryDelegate = () => {
const matrixShowToolDetails = (window as any).matrixShowToolDetails;
if (matrixShowToolDetails && typeof matrixShowToolDetails === 'function') {
return matrixShowToolDetails(toolName, modalType);
}
const directShowToolDetails = (window as any).directShowToolDetails;
if (directShowToolDetails && typeof directShowToolDetails === 'function') {
return directShowToolDetails(toolName, modalType);
}
attempts++;
if (attempts < maxAttempts) {
setTimeout(tryDelegate, 100);
} else {
}
};
tryDelegate();
};
(window as any).hideToolDetails = function(modalType: string = 'both') {
const matrixHideToolDetails = (window as any).matrixHideToolDetails;
if (matrixHideToolDetails && typeof matrixHideToolDetails === 'function') {
return matrixHideToolDetails(modalType);
}
};
(window as any).hideAllToolDetails = function() {
(window as any).hideToolDetails('both');
};
async function checkClientAuth(context = 'general') {
try {
const response = await fetch('/api/auth/status');
@@ -176,6 +226,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
authRequired: data.aiAuthRequired,
expires: data.expires
};
case 'gatedcontent':
return {
authenticated: data.gatedContentAuthenticated,
authRequired: data.gatedContentAuthRequired,
expires: data.expires
};
default:
return {
authenticated: data.authenticated,
@@ -192,7 +248,7 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
}
}
async function requireClientAuth(callback, returnUrl, context = 'general') {
async function requireClientAuth(callback: () => void, returnUrl: string, context = 'general') {
const authStatus = await checkClientAuth(context);
if (authStatus.authRequired && !authStatus.authenticated) {
@@ -207,12 +263,12 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
}
}
async function showIfAuthenticated(selector, context = 'general') {
async function showIfAuthenticated(selector: string, context = 'general') {
const authStatus = await checkClientAuth(context);
const element = document.querySelector(selector);
if (element) {
element.style.display = (!authStatus.authRequired || authStatus.authenticated)
(element as HTMLElement).style.display = (!authStatus.authRequired || authStatus.authenticated)
? 'inline-flex'
: 'none';
}
@@ -241,6 +297,51 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
(window as any).showIfAuthenticated = showIfAuthenticated;
(window as any).setupAuthButtons = setupAuthButtons;
async function copyUrlToClipboard(url: string, button: HTMLElement) {
try {
await navigator.clipboard.writeText(url);
const originalHTML = button.innerHTML;
button.innerHTML = `
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<polyline points="20,6 9,17 4,12"/>
</svg>
Kopiert!
`;
button.style.color = 'var(--color-accent)';
setTimeout(() => {
button.innerHTML = originalHTML;
button.style.color = '';
}, 2000);
} catch (err) {
const textArea = document.createElement('textarea');
textArea.value = url;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
const originalHTML = button.innerHTML;
button.innerHTML = 'Kopiert!';
setTimeout(() => {
button.innerHTML = originalHTML;
}, 2000);
}
}
async function shareArticle(button: HTMLElement, url: string, title: string) {
const fullUrl = window.location.origin + url;
await copyUrlToClipboard(fullUrl, button);
}
async function shareCurrentArticle(button: HTMLElement) {
await copyUrlToClipboard(window.location.href, button);
}
(window as any).shareArticle = shareArticle;
(window as any).shareCurrentArticle = shareCurrentArticle;
initTheme();
setupAuthButtons('[data-contribute-button]');
@@ -248,8 +349,29 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
await showIfAuthenticated('#ai-view-toggle', 'ai');
};
initAIButton();
});
document.addEventListener('DOMContentLoaded', () => {
const isFirefox = navigator.userAgent.toLowerCase().includes('firefox') ||
navigator.userAgent.toLowerCase().includes('librewolf');
console.log('[CONSOLIDATED] All utilities loaded and initialized');
if (isFirefox) {
console.log('[VIDEO] Firefox detected - setting up error recovery');
document.querySelectorAll('video').forEach(video => {
let errorCount = 0;
video.addEventListener('error', () => {
errorCount++;
console.log(`[VIDEO] Error ${errorCount} in Firefox for: ${video.getAttribute('data-video-title')}`);
});
video.addEventListener('loadedmetadata', () => {
const title = video.getAttribute('data-video-title') || 'Video';
console.log(`[VIDEO] Successfully loaded: ${title}`);
});
});
}
});
</script>
</head>

View File

@@ -184,7 +184,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<div style="display: grid; gap: 1.25rem;">
<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>
<h4 style="margin: 0 0 0.5rem 0; color: var(--color-accent);">📝 Vorschläge</h4>
<p style="margin: 0;">
Du hast eine Idee, wie wir den Hub erweitern können? Reiche deinen Vorschlag unkompliziert
über unsere <a href="/contribute#vorschlaege">/contribute</a>-Seite ein.
@@ -210,15 +210,54 @@ import BaseLayout from '../layouts/BaseLayout.astro';
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
GitRepository besuchen
Git-Repository besuchen
</a>
</div>
<!-- Lightning Support Section with simple-boost integration -->
<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);">⚡ Unterstützung</h4>
<p style="margin: 0;">
Kleine Spenden zur Infrastruktur-Finanzierung nehme ich auch gerne an, wenn es sein muss.
Fragt einfach nach der Lightning-Adresse oder BTC-Adresse!
<h4 style="margin: 0 0 0.75rem 0; color: var(--color-accent); 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">
<polygon points="13,2 3,14 12,14 11,22 21,10 12,10 13,2"/>
</svg>
⚡ Unterstützung
</h4>
<p style="margin: 0 0 1rem 0; font-size: 0.875rem; line-height: 1.5;">
Kleine Spenden zur Server-Finanzierung sind willkommen.
</p>
<div style="margin-bottom: 1rem;">
<!-- Simple EUR Payment -->
<div style="display: flex; gap: 0.75rem; align-items: center; justify-content: center; max-width: 300px; margin: 0 auto;">
<input
type="number"
id="eur-amount"
min="0.01"
step="0.01"
placeholder="0,50"
value="0.5"
style="width: 80px; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: 0.375rem; font-size: 0.875rem; text-align: center;">
<span style="font-size: 0.875rem; color: var(--color-text-secondary);">€</span>
<simple-boost
id="eur-boost"
class="bootstrap"
nwc="nostr+walletconnect://4fe05896e1faf09d1902ea24ef589f65a9606d1710420a9574ce331e3c7f486b?relay=wss://nostr.mikoshi.de&secret=bdfc861fe71e8d9e375b7a2484052e92def7caf4b317d8f6537b784d3cd6eb3b"
amount="0.5"
currency="eur"
memo="ForensicPathways Unterstützung - Vielen Dank!"
style="background-color: var(--color-accent); color: white; border: none; border-radius: 0.375rem; padding: 0.5rem 1rem; font-size: 0.875rem; cursor: pointer;">
⚡ Senden
</simple-boost>
</div>
</div>
<div style="margin-top: 1rem; padding: 0.75rem; background-color: var(--color-bg); border-radius: 0.375rem; border-left: 3px solid var(--color-accent);">
<p style="margin: 0; font-size: 0.75rem; color: var(--color-text-secondary); line-height: 1.4; text-align: center;">
<strong>⚡ Lightning-Unterstützung:</strong> Betrag eingeben und senden.
Benötigt eine Lightning-Wallet wie <a href="https://getalby.com" target="_blank" rel="noopener" style="color: var(--color-accent);">Alby</a> oder
<a href="https://phoenix.acinq.co" target="_blank" rel="noopener" style="color: var(--color-accent);">Phoenix</a>.
</p>
</div>
</div>
</div>
</div>
@@ -232,3 +271,69 @@ import BaseLayout from '../layouts/BaseLayout.astro';
</div>
</section>
</BaseLayout>
<script>
// TODO: cleanup
import('simple-boost').then(() => {
console.log('Simple-boost loaded successfully from local dependencies');
setupDynamicAmounts();
}).catch(error => {
console.error('Failed to load simple-boost:', error);
const script = document.createElement('script');
script.type = 'module';
script.src = '/node_modules/simple-boost/dist/simple-boost.js';
script.onload = () => {
console.log('Simple-boost fallback loaded');
setupDynamicAmounts();
};
script.onerror = () => console.error('Simple-boost fallback failed');
document.head.appendChild(script);
});
function setupDynamicAmounts() {
const eurBoost = document.getElementById('eur-boost');
const eurInput = document.getElementById('eur-amount') as HTMLInputElement;
if (eurBoost && eurInput) {
eurBoost.addEventListener('click', (e) => {
const amount = parseFloat(eurInput.value) || 0.5;
eurBoost.setAttribute('amount', amount.toString());
console.log('EUR amount set to:', amount);
});
eurInput.addEventListener('input', () => {
const amount = parseFloat(eurInput.value) || 0.5;
eurBoost.setAttribute('amount', amount.toString());
});
}
}
</script>
<style>
simple-boost {
--simple-boost-primary: var(--color-warning);
--simple-boost-primary-hover: var(--color-accent);
--simple-boost-text: white;
transition: all 0.2s ease;
}
simple-boost:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15) !important;
}
simple-boost .simple-boost-button {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: inherit;
font-size: 0.875rem;
}
/* Loading state styling */
simple-boost[loading] {
opacity: 0.7;
cursor: not-allowed;
}
</style>

View File

@@ -1,16 +1,18 @@
// src/pages/api/ai/embeddings-status.ts
import type { APIRoute } from 'astro';
import { embeddingsService } from '../../../utils/embeddings.js';
export const prerender = false;
export const GET: APIRoute = async () => {
try {
const { embeddingsService } = await import('../../../utils/embeddings.js');
await embeddingsService.waitForInitialization();
const stats = embeddingsService.getStats();
const status = stats.enabled && stats.initialized ? 'ready' :
stats.enabled && !stats.initialized ? 'initializing' : 'disabled';
const status = stats.initialized ? 'ready' :
!stats.initialized ? 'initializing' : 'disabled';
console.log(`[EMBEDDINGS-STATUS-API] Service status: ${status}, stats:`, stats);
return new Response(JSON.stringify({
success: true,
@@ -23,6 +25,8 @@ export const GET: APIRoute = async () => {
});
} catch (error) {
console.error('[EMBEDDINGS-STATUS-API] Error checking embeddings status:', error);
return new Response(JSON.stringify({
success: false,
embeddings: { enabled: false, initialized: false, count: 0 },

View File

@@ -1,28 +1,57 @@
// src/pages/api/ai/enhance-input.ts - Enhanced AI service compatibility
// src/pages/api/ai/enhance-input.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js';
import { enqueueApiCall } from '../../../utils/rateLimitedQueue.js';
import { aiService } from '../../../utils/aiService.js';
import { JSONParser } from '../../../utils/jsonUtils.js';
import { getPrompt } from '../../../config/prompts.js';
export const prerender = false;
function getEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing environment variable: ${key}`);
}
return value;
}
const RATE_LIMIT_WINDOW_MS =
Number.isFinite(parseInt(process.env.RATE_LIMIT_WINDOW_MS ?? '', 10))
? parseInt(process.env.RATE_LIMIT_WINDOW_MS!, 10)
: 60_000;
const AI_ENDPOINT = getEnv('AI_ANALYZER_ENDPOINT');
const AI_ANALYZER_API_KEY = getEnv('AI_ANALYZER_API_KEY');
const AI_ANALYZER_MODEL = getEnv('AI_ANALYZER_MODEL');
const RATE_LIMIT_MAX =
Number.isFinite(parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS ?? '', 10))
? parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS!, 10)
: 5;
const INPUT_MIN_CHARS = 40;
const INPUT_MAX_CHARS = 1000;
const Q_MIN_LEN = 15;
const Q_MAX_LEN = 160;
const Q_MAX_COUNT = 3;
const AI_TEMPERATURE = 0.3;
const CLEANER_TEMPERATURE = 0.0;
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 5;
function checkRateLimit(userId: string): boolean {
const now = Date.now();
const entry = rateLimitStore.get(userId);
if (!entry || now > entry.resetTime) {
rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });
return true;
}
if (entry.count >= RATE_LIMIT_MAX) return false;
entry.count++;
return true;
}
function cleanupExpiredRateLimits(): void {
const now = Date.now();
for (const [userId, entry] of rateLimitStore.entries()) {
if (now > entry.resetTime) rateLimitStore.delete(userId);
}
}
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
/**
* Helpers
*/
function sanitizeInput(input: string): string {
return input
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
@@ -30,111 +59,24 @@ function sanitizeInput(input: string): string {
.replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
.replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
.trim()
.slice(0, 1000);
.slice(0, INPUT_MAX_CHARS);
}
function checkRateLimit(userId: string): boolean {
const now = Date.now();
const userLimit = rateLimitStore.get(userId);
if (!userLimit || now > userLimit.resetTime) {
rateLimitStore.set(userId, { count: 1, resetTime: now + RATE_LIMIT_WINDOW });
return true;
}
if (userLimit.count >= RATE_LIMIT_MAX) {
return false;
}
userLimit.count++;
return true;
}
function cleanupExpiredRateLimits() {
const now = Date.now();
for (const [userId, limit] of rateLimitStore.entries()) {
if (now > limit.resetTime) {
rateLimitStore.delete(userId);
}
}
}
setInterval(cleanupExpiredRateLimits, 5 * 60 * 1000);
function createEnhancementPrompt(input: string): string {
return `Sie sind ein DFIR-Experte mit Spezialisierung auf forensische Methodik. Ein Nutzer beschreibt ein forensisches Szenario oder Problem. Analysieren Sie die Eingabe auf Vollständigkeit für eine wissenschaftlich fundierte forensische Untersuchung.
ANALYSIEREN SIE DIESE FORENSISCHEN KATEGORIEN:
1. **Incident Context**: Was ist passiert? Welche Angriffsvektoren oder technischen Probleme liegen vor?
2. **Affected Systems**: Welche spezifischen Technologien/Plattformen sind betroffen? (Windows/Linux/ICS/SCADA/Mobile/Cloud/Network Infrastructure)
3. **Available Evidence**: Welche forensischen Datenquellen stehen zur Verfügung? (RAM-Dumps, Disk-Images, Log-Files, Network-Captures, Registry-Hives)
4. **Investigation Objectives**: Was soll erreicht werden? (IOC-Extraktion, Timeline-Rekonstruktion, Attribution, Impact-Assessment)
5. **Timeline Constraints**: Wie zeitkritisch ist die Untersuchung?
6. **Legal & Compliance**: Rechtliche Anforderungen, Chain of Custody, Compliance-Rahmen (DSGVO, sector-specific regulations)
7. **Technical Constraints**: Verfügbare Ressourcen, Skills, Infrastrukturbeschränkungen
WENN die Beschreibung alle kritischen forensischen Aspekte abdeckt: Geben Sie eine leere Liste [] zurück.
WENN wichtige forensische Details fehlen: Formulieren Sie 2-3 präzise Fragen, die die kritischsten Lücken für eine wissenschaftlich fundierte forensische Analyse schließen.
QUALITÄTSKRITERIEN FÜR FRAGEN:
- Forensisch spezifisch, nicht allgemein (❌ "Mehr Details?" ✅ "Welche forensischen Artefakte (RAM-Dumps, Disk-Images, Logs) stehen zur Verfügung?")
- Methodisch relevant (❌ "Wann passierte das?" ✅ "Liegen Log-Dateien aus dem Incident-Zeitraum vor, und welche Retention-Policy gilt?")
- Priorisiert nach Auswirkung auf die forensische Untersuchungsqualität
ANTWORTFORMAT (NUR JSON, KEIN ZUSÄTZLICHER TEXT):
[
"Forensisch spezifische Frage 1?",
"Forensisch spezifische Frage 2?",
"Forensisch spezifische Frage 3?"
]
NUTZER-EINGABE:
${input}
`.trim();
}
async function callAIService(prompt: string): Promise<Response> {
const endpoint = AI_ENDPOINT;
const apiKey = AI_ANALYZER_API_KEY;
const model = AI_ANALYZER_MODEL;
let headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
console.log('[ENHANCE API] Using API key authentication');
} else {
console.log('[ENHANCE API] No API key - making request without authentication');
}
const requestBody = {
model,
messages: [{ role: 'user', content: prompt }],
max_tokens: 300,
temperature: 0.7,
top_p: 0.9,
frequency_penalty: 0.2,
presence_penalty: 0.1
};
return fetch(`${endpoint}/v1/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody)
});
function stripJsonFences(s: string): string {
return s.replace(/^```json\s*/i, '')
.replace(/^```\s*/i, '')
.replace(/\s*```\s*$/, '')
.trim();
}
/**
* Handler
*/
export const POST: APIRoute = async ({ request }) => {
try {
const authResult = await withAPIAuth(request, 'ai');
if (!authResult.authenticated) {
return createAuthErrorResponse();
}
const userId = authResult.userId;
const auth = await withAPIAuth(request, 'ai');
if (!auth.authenticated) return createAuthErrorResponse();
const userId = auth.userId;
if (!checkRateLimit(userId)) {
return apiError.rateLimit('Enhancement rate limit exceeded');
@@ -143,66 +85,40 @@ export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
const { input } = body;
if (!input || typeof input !== 'string' || input.length < 40) {
return apiError.badRequest('Input too short for enhancement (minimum 40 characters)');
if (!input || typeof input !== 'string' || input.length < INPUT_MIN_CHARS) {
return apiError.badRequest(`Input too short for enhancement (minimum ${INPUT_MIN_CHARS} characters)`);
}
const sanitizedInput = sanitizeInput(input);
if (sanitizedInput.length < 40) {
if (sanitizedInput.length < INPUT_MIN_CHARS) {
return apiError.badRequest('Input too short after sanitization');
}
const systemPrompt = createEnhancementPrompt(sanitizedInput);
const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 4)}`;
const taskId = `enhance_${userId}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
const questionsPrompt = getPrompt('enhancementQuestions', sanitizedInput);
const aiResponse = await enqueueApiCall(() => callAIService(systemPrompt), taskId);
console.log(`[ENHANCE-API] Processing enhancement request for user: ${userId}`);
if (!aiResponse.ok) {
const errorText = await aiResponse.text();
console.error('[ENHANCE API] AI enhancement error:', errorText, 'Status:', aiResponse.status);
return apiServerError.unavailable('Enhancement service unavailable');
}
const aiResponse = await enqueueApiCall(
() => aiService.callAI(questionsPrompt, { temperature: AI_TEMPERATURE }),
taskId
);
const aiData = await aiResponse.json();
const aiContent = aiData.choices?.[0]?.message?.content;
if (!aiContent) {
if (!aiResponse?.content) {
return apiServerError.unavailable('No enhancement response');
}
let questions;
try {
const cleanedContent = aiContent
.replace(/^```json\s*/i, '')
.replace(/\s*```\s*$/, '')
.trim();
questions = JSON.parse(cleanedContent);
if (!Array.isArray(questions)) {
throw new Error('Response is not an array');
}
let parsed: unknown = JSONParser.safeParseJSON(stripJsonFences(aiResponse.content), null);
let questions: string[] = Array.isArray(parsed) ? parsed : [];
questions = questions
.filter(q => typeof q === 'string' && q.length > 20 && q.length < 200)
.filter(q => q.includes('?'))
.filter(q => {
const forensicsTerms = ['forensisch', 'log', 'dump', 'image', 'artefakt', 'evidence', 'incident', 'system', 'netzwerk', 'zeitraum', 'verfügbar'];
const lowerQ = q.toLowerCase();
return forensicsTerms.some(term => lowerQ.includes(term));
})
.filter(q => typeof q === 'string')
.map(q => q.trim())
.slice(0, 3);
.filter(q => q.endsWith('?'))
.filter(q => q.length >= Q_MIN_LEN && q.length <= Q_MAX_LEN)
.slice(0, Q_MAX_COUNT);
if (questions.length === 0) {
questions = [];
}
} catch (error) {
console.error('Failed to parse enhancement response:', aiContent);
questions = [];
}
console.log(`[ENHANCE API] User: ${userId}, Forensics Questions: ${questions.length}, Input length: ${sanitizedInput.length}`);
console.log(`[ENHANCE-API] User: ${userId}, Questions generated: ${questions.length}, Input length: ${sanitizedInput.length}`);
return new Response(JSON.stringify({
success: true,
@@ -214,8 +130,8 @@ export const POST: APIRoute = async ({ request }) => {
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error('Enhancement error:', error);
} catch (err) {
console.error('[ENHANCE-API] Enhancement error:', err);
return apiServerError.internal('Enhancement processing failed');
}
};

View File

@@ -1,5 +1,4 @@
// src/pages/api/ai/query.ts - FIXED: Rate limiting for micro-task pipeline
// src/pages/api/ai/query.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiError, apiServerError, createAuthErrorResponse } from '../../../utils/api.js';
@@ -21,15 +20,14 @@ const MAIN_RATE_LIMIT_MAX = parseInt(process.env.AI_RATE_LIMIT_MAX_REQUESTS || '
const MICRO_TASK_TOTAL_LIMIT = parseInt(process.env.AI_MICRO_TASK_TOTAL_LIMIT || '50', 10);
function sanitizeInput(input: string): string {
let sanitized = input
return input
.replace(/```[\s\S]*?```/g, '[CODE_BLOCK_REMOVED]')
.replace(/\<\/?[^>]+(>|$)/g, '')
.replace(/\b(system|assistant|user)\s*[:]/gi, '[ROLE_REMOVED]')
.replace(/\b(ignore|forget|disregard)\s+(previous|all|your)\s+(instructions?|context|rules?)/gi, '[INSTRUCTION_REMOVED]')
.trim();
sanitized = sanitized.slice(0, 2000).replace(/\s+/g, ' ');
return sanitized;
.trim()
.slice(0, 2000)
.replace(/\s+/g, ' ');
}
function checkRateLimit(userId: string): { allowed: boolean; reason?: string; microTasksRemaining?: number } {
@@ -78,7 +76,7 @@ function incrementMicroTaskCount(userId: string, aiCallsMade: number): void {
}
}
function cleanupExpiredRateLimits() {
function cleanupExpiredRateLimits(): void {
const now = Date.now();
const maxStoreSize = 1000;
@@ -118,51 +116,52 @@ export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
const { query, mode = 'workflow', taskId: clientTaskId } = body;
console.log(`[MICRO-TASK API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`);
console.log(`[MICRO-TASK API] Micro-task calls remaining: ${rateLimitResult.microTasksRemaining}`);
console.log(`[AI-API] Received request - TaskId: ${clientTaskId}, Mode: ${mode}, Query length: ${query?.length || 0}`);
console.log(`[AI-API] Micro-task calls remaining: ${rateLimitResult.microTasksRemaining}`);
if (!query || typeof query !== 'string') {
console.log(`[MICRO-TASK API] Invalid query for task ${clientTaskId}`);
console.log(`[AI-API] Invalid query for task ${clientTaskId}`);
return apiError.badRequest('Query required');
}
if (!['workflow', 'tool'].includes(mode)) {
console.log(`[MICRO-TASK API] Invalid mode for task ${clientTaskId}: ${mode}`);
console.log(`[AI-API] Invalid mode for task ${clientTaskId}: ${mode}`);
return apiError.badRequest('Invalid mode. Must be "workflow" or "tool"');
}
const sanitizedQuery = sanitizeInput(query);
if (sanitizedQuery.includes('[FILTERED]')) {
console.log(`[MICRO-TASK API] Filtered input detected for task ${clientTaskId}`);
console.log(`[AI-API] Filtered input detected for task ${clientTaskId}`);
return apiError.badRequest('Invalid input detected');
}
const taskId = clientTaskId || `ai_${userId}_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
console.log(`[MICRO-TASK API] About to enqueue micro-task pipeline ${taskId}`);
console.log(`[AI-API] Enqueueing pipeline task ${taskId}`);
const result = await enqueueApiCall(() =>
aiPipeline.processQuery(sanitizedQuery, mode)
, taskId);
if (!result || !result.recommendation) {
return apiServerError.unavailable('No response from micro-task AI pipeline');
return apiServerError.unavailable('No response from AI pipeline');
}
const stats = result.processingStats;
const estimatedAICallsMade = stats.microTasksCompleted + stats.microTasksFailed;
incrementMicroTaskCount(userId, estimatedAICallsMade);
console.log(`[MICRO-TASK API] Pipeline completed for ${taskId}:`);
console.log(` - Mode: ${mode}`);
console.log(` - User: ${userId}`);
console.log(` - Query length: ${sanitizedQuery.length}`);
console.log(` - Processing time: ${stats.processingTimeMs}ms`);
console.log(` - Micro-tasks completed: ${stats.microTasksCompleted}`);
console.log(` - Micro-tasks failed: ${stats.microTasksFailed}`);
console.log(` - Estimated AI calls: ${estimatedAICallsMade}`);
console.log(` - Embeddings used: ${stats.embeddingsUsed}`);
console.log(` - Final items: ${stats.finalSelectedItems}`);
console.log(`[AI-API] Pipeline completed for ${taskId}:`, {
mode,
user: userId,
queryLength: sanitizedQuery.length,
processingTime: stats.processingTimeMs,
microTasksCompleted: stats.microTasksCompleted,
microTasksFailed: stats.microTasksFailed,
estimatedAICalls: estimatedAICallsMade,
embeddingsUsed: stats.embeddingsUsed,
finalItems: stats.finalSelectedItems
});
const currentLimit = rateLimitStore.get(userId);
const remainingMicroTasks = currentLimit ?
@@ -176,7 +175,7 @@ export const POST: APIRoute = async ({ request }) => {
query: sanitizedQuery,
processingStats: {
...result.processingStats,
pipelineType: 'micro-task',
pipelineType: 'refactored',
microTasksSuccessRate: stats.microTasksCompleted / (stats.microTasksCompleted + stats.microTasksFailed),
averageTaskTime: stats.processingTimeMs / (stats.microTasksCompleted + stats.microTasksFailed),
estimatedAICallsMade
@@ -192,18 +191,16 @@ export const POST: APIRoute = async ({ request }) => {
});
} catch (error) {
console.error('[MICRO-TASK API] Pipeline error:', error);
console.error('[AI-API] Pipeline error:', error);
if (error.message.includes('embeddings')) {
return apiServerError.unavailable('Embeddings service error - using AI fallback');
} else if (error.message.includes('micro-task')) {
return apiServerError.unavailable('Micro-task pipeline error - some analysis steps failed');
} else if (error.message.includes('selector')) {
return apiServerError.unavailable('AI selector service error');
return apiServerError.unavailable('Embeddings service error');
} else if (error.message.includes('AI')) {
return apiServerError.unavailable('AI service error');
} else if (error.message.includes('rate limit')) {
return apiError.rateLimit('AI service rate limits exceeded during micro-task processing');
return apiError.rateLimit('AI service rate limits exceeded');
} else {
return apiServerError.internal('Micro-task AI pipeline error');
return apiServerError.internal('AI pipeline error');
}
}
};

View File

@@ -1,5 +1,7 @@
// src/pages/api/auth/login.ts
import type { APIRoute } from 'astro';
import { generateAuthUrl, generateState, logAuthEvent } from '../../../utils/auth.js';
import { serialize } from 'cookie';
export const prerender = false;
@@ -8,14 +10,27 @@ export const GET: APIRoute = async ({ url, redirect }) => {
const state = generateState();
const authUrl = generateAuthUrl(state);
console.log('Generated auth URL:', authUrl);
console.log('[AUTH] Generated auth URL:', authUrl);
const returnTo = url.searchParams.get('returnTo') || '/';
logAuthEvent('Login initiated', { returnTo, authUrl });
const stateData = JSON.stringify({ state, returnTo });
const stateCookie = `auth_state=${encodeURIComponent(stateData)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=600`;
const publicBaseUrl = process.env.PUBLIC_BASE_URL || '';
const isProduction = process.env.NODE_ENV === 'production';
const isSecure = publicBaseUrl.startsWith('https://') || isProduction;
const stateCookie = serialize('auth_state', stateData, {
httpOnly: true,
secure: isSecure,
sameSite: 'lax',
maxAge: 600, // 10 minutes
path: '/'
});
console.log('[AUTH] Setting auth state cookie:', stateCookie.substring(0, 50) + '...');
return new Response(null, {
status: 302,

View File

@@ -1,4 +1,4 @@
// src/pages/api/auth/process.ts (FIXED - Proper cookie handling)
// src/pages/api/auth/process.ts
import type { APIRoute } from 'astro';
import {
verifyAuthState,
@@ -7,7 +7,7 @@ import {
createSessionWithCookie,
logAuthEvent
} from '../../../utils/auth.js';
import { apiError, apiSpecial, apiWithHeaders, handleAPIRequest } from '../../../utils/api.js';
import { apiError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
export const prerender = false;
@@ -30,9 +30,15 @@ export const POST: APIRoute = async ({ request }) => {
const stateVerification = verifyAuthState(request, state);
if (!stateVerification.isValid || !stateVerification.stateData) {
logAuthEvent('State verification failed', {
error: stateVerification.error,
hasStateData: !!stateVerification.stateData
});
return apiError.badRequest(stateVerification.error || 'Invalid state parameter');
}
console.log('[AUTH] State verification successful, exchanging code for tokens');
const tokens = await exchangeCodeForTokens(code);
const userInfo = await getUserInfo(tokens.access_token);
@@ -43,6 +49,12 @@ export const POST: APIRoute = async ({ request }) => {
email: sessionResult.userEmail
});
const returnUrl = new URL(stateVerification.stateData.returnTo, request.url);
returnUrl.searchParams.set('auth', 'success');
const redirectUrl = returnUrl.toString();
console.log('[AUTH] Redirecting to:', redirectUrl);
const responseHeaders = new Headers();
responseHeaders.set('Content-Type', 'application/json');
@@ -51,7 +63,7 @@ export const POST: APIRoute = async ({ request }) => {
return new Response(JSON.stringify({
success: true,
redirectTo: stateVerification.stateData.returnTo
redirectTo: redirectUrl
}), {
status: 200,
headers: responseHeaders

View File

@@ -9,13 +9,16 @@ export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
const contributionAuth = await withAPIAuth(request, 'contributions');
const aiAuth = await withAPIAuth(request, 'ai');
const gatedContentAuth = await withAPIAuth(request, 'gatedcontent');
return apiResponse.success({
authenticated: contributionAuth.authenticated || aiAuth.authenticated,
authenticated: contributionAuth.authenticated || aiAuth.authenticated || gatedContentAuth.authenticated,
contributionAuthRequired: contributionAuth.authRequired,
aiAuthRequired: aiAuth.authRequired,
gatedContentAuthRequired: gatedContentAuth.authRequired,
contributionAuthenticated: contributionAuth.authenticated,
aiAuthenticated: aiAuth.authenticated,
gatedContentAuthenticated: gatedContentAuth.authenticated,
expires: contributionAuth.session?.exp ? new Date(contributionAuth.session.exp * 1000).toISOString() : null
});
}, 'Status check failed');

View File

@@ -1,4 +1,4 @@
// src/pages/api/contribute/knowledgebase.ts - SIMPLIFIED: Issues only, minimal validation
// src/pages/api/contribute/knowledgebase.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, handleAPIRequest } from '../../../utils/api.js';

View File

@@ -1,4 +1,4 @@
// src/pages/api/contribute/tool.ts (UPDATED - Using consolidated API responses)
// src/pages/api/contribute/tool.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@@ -27,6 +27,7 @@ const ContributionToolSchema = z.object({
knowledgebase: z.boolean().optional().nullable(),
'domain-agnostic-software': z.array(z.string()).optional().nullable(),
related_concepts: z.array(z.string()).optional().nullable(),
related_software: z.array(z.string()).optional().nullable(),
tags: z.array(z.string()).default([]),
statusUrl: z.string().url('Must be a valid URL').optional().nullable()
});
@@ -80,6 +81,34 @@ function sanitizeInput(obj: any): any {
return obj;
}
function preprocessFormData(body: any): any {
if (body.tool) {
if (typeof body.tool.tags === 'string') {
body.tool.tags = body.tool.tags.split(',').map((t: string) => t.trim()).filter(Boolean);
}
if (body.tool.relatedConcepts) {
if (typeof body.tool.relatedConcepts === 'string') {
body.tool.related_concepts = body.tool.relatedConcepts.split(',').map((t: string) => t.trim()).filter(Boolean);
} else {
body.tool.related_concepts = body.tool.relatedConcepts;
}
delete body.tool.relatedConcepts;
}
if (body.tool.relatedSoftware) {
if (typeof body.tool.relatedSoftware === 'string') {
body.tool.related_software = body.tool.relatedSoftware.split(',').map((t: string) => t.trim()).filter(Boolean);
} else {
body.tool.related_software = body.tool.relatedSoftware;
}
delete body.tool.relatedSoftware;
}
}
return body;
}
async function validateToolData(tool: any, action: string): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = [];
@@ -109,6 +138,14 @@ async function validateToolData(tool: any, action: string): Promise<{ valid: boo
}
}
if (tool.related_concepts && tool.related_concepts.length > 0) {
console.log('[VALIDATION] Related concepts provided:', tool.related_concepts);
}
if (tool.related_software && tool.related_software.length > 0) {
console.log('[VALIDATION] Related software provided:', tool.related_software);
}
return { valid: errors.length === 0, errors };
} catch (error) {
@@ -143,6 +180,8 @@ export const POST: APIRoute = async ({ request }) => {
return apiSpecial.invalidJSON();
}
body = preprocessFormData(body);
const sanitizedBody = sanitizeInput(body);
let validatedData;
@@ -153,6 +192,7 @@ export const POST: APIRoute = async ({ request }) => {
const errorMessages = error.errors.map(err =>
`${err.path.join('.')}: ${err.message}`
);
console.log('[VALIDATION] Zod validation errors:', errorMessages);
return apiError.validation('Validation failed', errorMessages);
}
@@ -174,6 +214,16 @@ export const POST: APIRoute = async ({ request }) => {
}
};
console.log('[CONTRIBUTION] Processing contribution:', {
type: contributionData.type,
toolName: contributionData.tool.name,
toolType: contributionData.tool.type,
submitter: userEmail,
hasRelatedConcepts: !!(contributionData.tool.related_concepts?.length),
hasRelatedSoftware: !!(contributionData.tool.related_software?.length),
tagsCount: contributionData.tool.tags?.length || 0
});
try {
const gitManager = new GitContributionManager();
const result = await gitManager.submitContribution(contributionData);

View File

@@ -8,7 +8,7 @@ configDotenv();
const DEFAULT_MAX_RESULTS = (() => {
const raw = process.env.AI_EMBEDDING_CANDIDATES;
const n = Number.parseInt(raw ?? '', 10);
return Number.isFinite(n) && n > 0 ? n : 50; // fallback
return Number.isFinite(n) && n > 0 ? n : 50;
})();
const DEFAULT_THRESHOLD = (() => {
@@ -22,7 +22,6 @@ export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
try {
/* ---------- get body & apply defaults from env ---------------- */
const {
query,
maxResults = DEFAULT_MAX_RESULTS,
@@ -36,16 +35,8 @@ export const POST: APIRoute = async ({ request }) => {
);
}
/* --- (rest of the handler unchanged) -------------------------- */
const { embeddingsService } = await import('../../../utils/embeddings.js');
if (!embeddingsService.isEnabled()) {
return new Response(
JSON.stringify({ success: false, error: 'Semantic search not available' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
await embeddingsService.waitForInitialization();
const similarItems = await embeddingsService.findSimilar(

View File

@@ -1,4 +1,3 @@
// src/pages/api/upload/media.ts
import type { APIRoute } from 'astro';
import { withAPIAuth } from '../../../utils/auth.js';
import { apiResponse, apiError, apiServerError, apiSpecial, handleAPIRequest } from '../../../utils/api.js';
@@ -22,7 +21,9 @@ const UPLOAD_CONFIG = {
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedTypes: new Set([
'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
'video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov',
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@@ -30,7 +31,29 @@ const UPLOAD_CONFIG = {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'text/plain', 'text/csv', 'application/json'
'text/plain',
'text/csv',
'text/markdown',
'text/x-markdown',
'application/json',
'application/xml',
'text/xml',
'text/html',
'application/zip',
'application/x-tar',
'application/gzip',
'application/x-gzip',
'application/x-zip-compressed',
'application/x-rar-compressed',
'application/x-7z-compressed',
'application/rtf',
'text/richtext',
'application/x-yaml',
'text/yaml',
'application/yaml'
]),
localUploadPath: process.env.LOCAL_UPLOAD_PATH || './public/uploads',
publicBaseUrl: process.env.PUBLIC_BASE_URL || 'http://localhost:4321'
@@ -50,6 +73,7 @@ function checkUploadRateLimit(userEmail: string): boolean {
}
if (userLimit.count >= RATE_LIMIT_MAX) {
console.warn(`[UPLOAD] Rate limit exceeded for user: ${userEmail} (${userLimit.count}/${RATE_LIMIT_MAX})`);
return false;
}
@@ -58,27 +82,37 @@ function checkUploadRateLimit(userEmail: string): boolean {
}
function validateFile(file: File): { valid: boolean; error?: string } {
console.log(`[UPLOAD] Validating file: ${file.name}, size: ${file.size}, type: ${file.type}`);
if (file.size > UPLOAD_CONFIG.maxFileSize) {
return {
valid: false,
error: `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`
};
const errorMsg = `File too large. Maximum size is ${Math.round(UPLOAD_CONFIG.maxFileSize / 1024 / 1024)}MB`;
console.warn(`[UPLOAD] ${errorMsg} - File size: ${file.size}`);
return { valid: false, error: errorMsg };
}
if (!UPLOAD_CONFIG.allowedTypes.has(file.type)) {
return {
valid: false,
error: `File type ${file.type} not allowed`
};
const errorMsg = `File type ${file.type} not allowed`;
console.warn(`[UPLOAD] ${errorMsg} - Allowed types:`, Array.from(UPLOAD_CONFIG.allowedTypes));
return { valid: false, error: errorMsg };
}
console.log(`[UPLOAD] File validation passed for: ${file.name}`);
return { valid: true };
}
async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadResult> {
console.log(`[UPLOAD] Attempting Nextcloud upload for: ${file.name} by ${userEmail}`);
try {
const uploader = new NextcloudUploader();
const result = await uploader.uploadFile(file, userEmail);
console.log(`[UPLOAD] Nextcloud upload successful:`, {
filename: result.filename,
url: result.url,
size: file.size
});
return {
success: true,
url: result.url,
@@ -87,7 +121,7 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
storage: 'nextcloud'
};
} catch (error) {
console.error('Nextcloud upload failed:', error);
console.error('[UPLOAD] Nextcloud upload failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Nextcloud upload failed',
@@ -97,7 +131,10 @@ async function uploadToNextcloud(file: File, userEmail: string): Promise<UploadR
}
async function uploadToLocal(file: File, userType: string): Promise<UploadResult> {
console.log(`[UPLOAD] Attempting local upload for: ${file.name} (${userType})`);
try {
console.log(`[UPLOAD] Creating directory: ${UPLOAD_CONFIG.localUploadPath}`);
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
@@ -106,11 +143,20 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
const filename = `${timestamp}-${randomString}${extension}`;
const filepath = path.join(UPLOAD_CONFIG.localUploadPath, filename);
console.log(`[UPLOAD] Writing file to: ${filepath}`);
const buffer = Buffer.from(await file.arrayBuffer());
await fs.writeFile(filepath, buffer);
const publicUrl = `${UPLOAD_CONFIG.publicBaseUrl}/uploads/${filename}`;
console.log(`[UPLOAD] Local upload successful:`, {
filename,
filepath,
publicUrl,
size: file.size
});
return {
success: true,
url: publicUrl,
@@ -119,7 +165,7 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
storage: 'local'
};
} catch (error) {
console.error('Local upload failed:', error);
console.error('[UPLOAD] Local upload failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Local upload failed',
@@ -130,12 +176,22 @@ async function uploadToLocal(file: File, userType: string): Promise<UploadResult
export const POST: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
console.log('[UPLOAD] Processing upload request');
const authResult = await withAPIAuth(request, 'contributions');
console.log('[UPLOAD] Auth result:', {
authenticated: authResult.authenticated,
authRequired: authResult.authRequired,
userId: authResult.userId
});
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
console.warn('[UPLOAD] Upload rejected - authentication required but user not authenticated');
return apiError.unauthorized('Authentication required for file uploads');
}
const userEmail = authResult.session?.email || 'anon@anon.anon';
console.log(`[UPLOAD] Processing upload for user: ${userEmail}`);
if (!checkUploadRateLimit(userEmail)) {
return apiError.rateLimit('Upload rate limit exceeded. Please wait before uploading again.');
@@ -143,38 +199,59 @@ export const POST: APIRoute = async ({ request }) => {
let formData;
try {
console.log('[UPLOAD] Parsing form data');
formData = await request.formData();
console.log('[UPLOAD] Form data keys:', Array.from(formData.keys()));
} catch (error) {
return apiError.badRequest('Invalid form data');
console.error('[UPLOAD] Failed to parse form data:', error);
return apiError.badRequest('Invalid form data - could not parse request');
}
const file = formData.get('file') as File;
const type = formData.get('type') as string;
if (!file) {
console.warn('[UPLOAD] No file provided in request');
return apiSpecial.missingRequired(['file']);
}
console.log(`[UPLOAD] Processing file: ${file.name}, type parameter: ${type}`);
const validation = validateFile(file);
if (!validation.valid) {
return apiError.badRequest(validation.error!);
}
const nextcloudConfigured = isNextcloudConfigured();
console.log('[UPLOAD] Environment check:', {
nextcloudConfigured,
localUploadPath: UPLOAD_CONFIG.localUploadPath,
publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl,
nodeEnv: process.env.NODE_ENV
});
let result: UploadResult;
if (isNextcloudConfigured()) {
if (nextcloudConfigured) {
console.log('[UPLOAD] Using Nextcloud as primary storage');
result = await uploadToNextcloud(file, userEmail);
if (!result.success) {
console.warn('Nextcloud upload failed, trying local fallback:', result.error);
console.warn('[UPLOAD] Nextcloud upload failed, trying local fallback:', result.error);
result = await uploadToLocal(file, type);
}
} else {
console.log('[UPLOAD] Using local storage (Nextcloud not configured)');
result = await uploadToLocal(file, type);
}
if (result.success) {
console.log(`[MEDIA UPLOAD] ${file.name} (${file.size} bytes) by ${userEmail} -> ${result.storage}: ${result.url}`);
console.log(`[UPLOAD] Upload completed successfully:`, {
filename: result.filename,
storage: result.storage,
url: result.url,
user: userEmail
});
return apiSpecial.uploadSuccess({
url: result.url!,
@@ -183,7 +260,12 @@ export const POST: APIRoute = async ({ request }) => {
storage: result.storage!
});
} else {
console.error(`[MEDIA UPLOAD FAILED] ${file.name} by ${userEmail}: ${result.error}`);
console.error(`[UPLOAD] Upload failed completely:`, {
filename: file.name,
error: result.error,
storage: result.storage,
user: userEmail
});
return apiSpecial.uploadFailed(result.error!);
}
@@ -193,6 +275,8 @@ export const POST: APIRoute = async ({ request }) => {
export const GET: APIRoute = async ({ request }) => {
return await handleAPIRequest(async () => {
console.log('[UPLOAD] Getting upload status');
const authResult = await withAPIAuth(request);
if (authResult.authRequired && !authResult.authenticated) {
return apiError.unauthorized();
@@ -204,12 +288,14 @@ export const GET: APIRoute = async ({ request }) => {
try {
await fs.access(UPLOAD_CONFIG.localUploadPath);
localStorageAvailable = true;
console.log('[UPLOAD] Local storage accessible');
} catch {
try {
await fs.mkdir(UPLOAD_CONFIG.localUploadPath, { recursive: true });
localStorageAvailable = true;
console.log('[UPLOAD] Local storage created');
} catch (error) {
console.warn('Local upload directory not accessible:', error);
console.warn('[UPLOAD] Local upload directory not accessible:', error);
}
}
@@ -237,9 +323,14 @@ export const GET: APIRoute = async ({ request }) => {
paths: {
uploadEndpoint: '/api/upload/media',
localPath: localStorageAvailable ? '/uploads' : null
},
environment: {
nodeEnv: process.env.NODE_ENV,
publicBaseUrl: UPLOAD_CONFIG.publicBaseUrl
}
};
console.log('[UPLOAD] Status check completed:', status);
return apiResponse.success(status);
}, 'Upload status retrieval failed');

View File

@@ -1,5 +1,5 @@
---
// src/pages/contribute/index.astro - Consolidated Auth
// src/pages/contribute/index.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { withAuth } from '../../utils/auth.js';

View File

@@ -1,5 +1,5 @@
---
// src/pages/contribute/knowledgebase.astro - SIMPLIFIED: Issues only, minimal validation
// src/pages/contribute/knowledgebase.astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { withAuth } from '../../utils/auth.js';
import { getToolsData } from '../../utils/dataService.js';
@@ -114,8 +114,13 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
<div class="form-group">
<label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
<div class="upload-area" id="upload-area">
<input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" class="hidden">
<div class="upload-placeholder">
<input
type="file"
id="file-input"
multiple
accept=".pdf,.doc,.docx,.txt,.md,.markdown,.csv,.json,.xml,.html,.rtf,.yaml,.yml,.zip,.tar,.gz,.rar,.7z,.png,.jpg,.jpeg,.gif,.webp,.svg,.mp4,.webm,.mov,.avi"
class="hidden"
> <div class="upload-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
@@ -161,6 +166,14 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
</div>
</div>
<div class="form-group">
<label class="checkbox-wrapper">
<input type="checkbox" id="gated-content" name="gatedContent" />
<span>🔒 Als geschützten Inhalt markieren (Authentifizierung erforderlich)</span>
</label>
<small class="form-help">Nur für interne oder vertrauliche Inhalte</small>
</div>
<div class="form-group">
<label for="reason" class="form-label">Grund für den Beitrag (Optional)</label>
<textarea
@@ -304,6 +317,12 @@ class KnowledgebaseForm {
private handleFiles(files: File[]) {
files.forEach(file => {
const validation = this.validateFileBeforeUpload(file);
if (!validation.valid) {
console.log('[UPLOAD]Cannot upload ', file.name, ' Error: ', validation.error);
return;
}
const fileId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
const newFile: UploadedFile = {
id: fileId,
@@ -317,30 +336,98 @@ class KnowledgebaseForm {
this.renderFileList();
}
private validateFileBeforeUpload(file: File): { valid: boolean; error?: string } {
const maxSizeBytes = 50 * 1024 * 1024; // 50MB
if (file.size > maxSizeBytes) {
const sizeMB = (file.size / 1024 / 1024).toFixed(1);
const maxMB = (maxSizeBytes / 1024 / 1024).toFixed(0);
return {
valid: false,
error: `File too large (${sizeMB}MB). Maximum size: ${maxMB}MB`
};
}
const allowedExtensions = [
'.pdf', '.doc', '.docx', '.txt', '.md', '.markdown', '.csv', '.json',
'.xml', '.html', '.rtf', '.yaml', '.yml', '.zip', '.tar', '.gz',
'.rar', '.7z', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg',
'.mp4', '.webm', '.mov', '.avi'
];
const fileName = file.name.toLowerCase();
const hasValidExtension = allowedExtensions.some(ext => fileName.endsWith(ext));
if (!hasValidExtension) {
return {
valid: false,
error: `File type not allowed. Allowed: ${allowedExtensions.join(', ')}`
};
}
return { valid: true };
}
private async uploadFile(fileId: string) {
const fileItem = this.uploadedFiles.find(f => f.id === fileId);
if (!fileItem) return;
if (!fileItem) {
console.error('[UPLOAD] File item not found for ID:', fileId);
return;
}
console.log('[UPLOAD] Starting upload for:', fileItem.name, 'Size:', fileItem.file.size, 'Type:', fileItem.file.type);
const formData = new FormData();
formData.append('file', fileItem.file);
formData.append('type', 'knowledgebase');
try {
console.log('[UPLOAD] Sending request to /api/upload/media');
const response = await fetch('/api/upload/media', {
method: 'POST',
body: formData
});
console.log('[UPLOAD] Response status:', response.status);
let responseText: string;
let responseData: any;
try {
responseText = await response.text();
console.log('[UPLOAD] Raw response:', responseText.substring(0, 200));
try {
responseData = JSON.parse(responseText);
} catch (parseError) {
responseData = { error: responseText };
}
} catch (readError) {
console.error('[UPLOAD] Failed to read response:', readError);
throw new Error('Failed to read server response');
}
if (response.ok) {
const result = await response.json();
console.log('[UPLOAD] Success result:', responseData);
fileItem.uploaded = true;
fileItem.url = result.url;
fileItem.url = responseData.url;
this.renderFileList();
} else {
throw new Error('Upload failed');
if (responseData && responseData.details) {
console.error('[UPLOAD] Error details:', responseData.details);
}
}
} catch (error) {
this.showMessage('error', `Failed to upload ${fileItem.name}`);
console.error('[UPLOAD] Upload error for', fileItem.name, ':', error);
const errorMessage = error instanceof Error
? error.message
: 'Unknown upload error';
this.removeFile(fileId);
}
}
@@ -412,7 +499,6 @@ class KnowledgebaseForm {
} catch (error) {
console.error('[KB FORM] Submission error:', error);
this.showMessage('error', 'Submission failed. Please try again.');
} finally {
this.isSubmitting = false;
(this.elements.submitBtn as HTMLButtonElement).disabled = false;
@@ -441,18 +527,6 @@ class KnowledgebaseForm {
this.renderFileList();
}
private showMessage(type: 'success' | 'error' | 'warning', message: string) {
const container = document.getElementById('message-container');
if (!container) return;
const messageEl = document.createElement('div');
messageEl.className = `message message-${type}`;
messageEl.textContent = message;
container.appendChild(messageEl);
setTimeout(() => messageEl.remove(), 5000);
}
public removeFileById(fileId: string) {
this.removeFile(fileId);
}

View File

@@ -22,6 +22,16 @@ const existingTools = data.tools;
const editToolName = Astro.url.searchParams.get('edit');
const editTool = editToolName ? existingTools.find(tool => tool.name === editToolName) : null;
const isEdit = !!editTool;
const allTags = [...new Set(existingTools.flatMap(tool => tool.tags || []))].sort();
const allSoftwareAndMethods = existingTools
.filter(tool => tool.type === 'software' || tool.type === 'method')
.map(tool => tool.name)
.sort();
const allConcepts = existingTools
.filter(tool => tool.type === 'concept')
.map(tool => tool.name)
.sort();
---
<BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
@@ -194,16 +204,27 @@ const isEdit = !!editTool;
</div>
</div>
<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;">
{existingTools.filter(tool => tool.type === 'concept').map(concept => (
<label class="checkbox-wrapper">
<input type="checkbox" name="relatedConcepts" value={concept.name}
checked={editTool?.related_concepts?.includes(concept.name)} />
<span>{concept.name}</span>
</label>
))}
<div id="relations-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;">Verwandte Tools & Konzepte</h3>
<div style="display: grid; gap: 1.5rem;">
<div id="related-concepts-section">
<label for="related-concepts-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Konzepte</label>
<input type="text" id="related-concepts-input" placeholder="Beginne zu tippen, um Konzepte zu finden..." />
<input type="hidden" id="related-concepts-hidden" name="relatedConcepts" value={editTool?.related_concepts?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Konzepte, die mit diesem Tool/Methode in Verbindung stehen
</small>
</div>
<div id="related-software-section">
<label for="related-software-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Verwandte Software & Methoden</label>
<input type="text" id="related-software-input" placeholder="Beginne zu tippen, um Software/Methoden zu finden..." />
<input type="hidden" id="related-software-hidden" name="relatedSoftware" value={editTool?.related_software?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Software oder Methoden, die oft zusammen mit diesem Tool verwendet werden
</small>
</div>
</div>
</div>
@@ -211,9 +232,12 @@ const isEdit = !!editTool;
<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>
<div style="margin-bottom: 1.5rem;">
<label for="tags" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags" name="tags" value={editTool?.tags?.join(', ') || ''}
placeholder="Komma-getrennt: Passende Begriffe, nach denen ihr suchen würdet." />
<label for="tags-input" style="display: block; margin-bottom: 0.5rem; font-weight: 600;">Tags</label>
<input type="text" id="tags-input" placeholder="Beginne zu tippen, um Tags hinzuzufügen..." />
<input type="hidden" id="tags-hidden" name="tags" value={editTool?.tags?.join(', ') || ''} />
<small style="display: block; margin-top: 0.25rem; color: var(--color-text-secondary); font-size: 0.8125rem;">
Passende Begriffe, nach denen ihr suchen würdet
</small>
</div>
<div style="margin-bottom: 1.5rem;">
@@ -274,7 +298,269 @@ const isEdit = !!editTool;
</div>
</BaseLayout>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware }}>
<script define:vars={{ isEdit, editTool, domains, phases, domainAgnosticSoftware, allTags, allSoftwareAndMethods, allConcepts }}>
class AutocompleteManager {
constructor(inputElement, dataSource, options = {}) {
this.input = inputElement;
this.dataSource = dataSource;
this.options = {
minLength: 1,
maxResults: 10,
placeholder: 'Type to search...',
allowMultiple: false,
separator: ', ',
filterFunction: this.defaultFilter.bind(this),
renderFunction: this.defaultRender.bind(this),
...options
};
this.isOpen = false;
this.selectedIndex = -1;
this.filteredData = [];
this.selectedItems = new Set();
this.init();
}
init() {
this.createDropdown();
this.bindEvents();
if (this.options.allowMultiple) {
this.initMultipleMode();
}
}
createDropdown() {
this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown';
this.input.parentNode.style.position = 'relative';
this.input.parentNode.insertBefore(this.dropdown, this.input.nextSibling);
}
bindEvents() {
this.input.addEventListener('input', (e) => {
this.handleInput(e.target.value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
this.input.addEventListener('focus', () => {
if (this.input.value.length >= this.options.minLength) {
this.showDropdown();
}
});
this.input.addEventListener('blur', (e) => {
setTimeout(() => {
if (!this.dropdown.contains(document.activeElement)) {
this.hideDropdown();
}
}, 150);
});
document.addEventListener('click', (e) => {
if (!this.input.contains(e.target) && !this.dropdown.contains(e.target)) {
this.hideDropdown();
}
});
}
initMultipleMode() {
this.selectedContainer = document.createElement('div');
this.selectedContainer.className = 'autocomplete-selected';
this.input.parentNode.insertBefore(this.selectedContainer, this.input);
this.updateSelectedDisplay();
}
handleInput(value) {
if (value.length >= this.options.minLength) {
this.filteredData = this.options.filterFunction(value);
this.selectedIndex = -1;
this.renderDropdown();
this.showDropdown();
} else {
this.hideDropdown();
}
}
handleKeydown(e) {
if (!this.isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
this.updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectItem(this.filteredData[this.selectedIndex]);
}
break;
case 'Escape':
this.hideDropdown();
break;
}
}
defaultFilter(query) {
const searchTerm = query.toLowerCase();
return this.dataSource
.filter(item => {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return text.toLowerCase().includes(searchTerm) &&
(!this.options.allowMultiple || !this.selectedItems.has(text));
})
.slice(0, this.options.maxResults);
}
defaultRender(item) {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${this.escapeHtml(text)}</div>`;
}
renderDropdown() {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction(item);
return `<div class="autocomplete-option" data-index="${index}">${content}</div>`;
})
.join('');
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]);
});
option.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateHighlight();
});
});
}
updateHighlight() {
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.style.backgroundColor = index === this.selectedIndex
? 'var(--color-bg-secondary)'
: 'transparent';
});
}
selectItem(item) {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
if (this.options.allowMultiple) {
this.selectedItems.add(text);
this.updateSelectedDisplay();
this.updateInputValue();
this.input.value = '';
} else {
this.input.value = text;
this.hideDropdown();
}
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
}));
}
removeItem(text) {
if (this.options.allowMultiple) {
this.selectedItems.delete(text);
this.updateSelectedDisplay();
this.updateInputValue();
}
}
updateSelectedDisplay() {
if (!this.options.allowMultiple || !this.selectedContainer) return;
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
.map(item => `
<span class="autocomplete-tag">
${this.escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${this.escapeHtml(item)}">×</button>
</span>
`)
.join('');
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.removeItem(btn.getAttribute('data-item'));
});
});
}
updateInputValue() {
if (this.options.allowMultiple && this.options.hiddenInput) {
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator);
}
}
showDropdown() {
this.dropdown.style.display = 'block';
this.isOpen = true;
}
hideDropdown() {
this.dropdown.style.display = 'none';
this.isOpen = false;
this.selectedIndex = -1;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
setDataSource(newDataSource) {
this.dataSource = newDataSource;
}
getSelectedItems() {
return Array.from(this.selectedItems);
}
setSelectedItems(items) {
this.selectedItems = new Set(items);
if (this.options.allowMultiple) {
this.updateSelectedDisplay();
this.updateInputValue();
}
}
destroy() {
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.selectedContainer && this.selectedContainer.parentNode) {
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
}
}
}
console.log('[FORM] Script loaded, initializing...');
class ContributionForm {
@@ -283,6 +569,7 @@ class ContributionForm {
this.editTool = editTool;
this.elements = {};
this.isSubmitting = false;
this.autocompleteManagers = new Map();
this.init();
}
@@ -303,14 +590,20 @@ class ContributionForm {
yamlPreview: document.getElementById('yaml-preview'),
successModal: document.getElementById('success-modal'),
softwareFields: document.getElementById('software-fields'),
conceptsFields: document.getElementById('concepts-fields'),
relationsFields: document.getElementById('relations-fields'),
descriptionCount: document.getElementById('description-count'),
reasonCount: document.getElementById('reason-count'),
validationErrors: document.getElementById('validation-errors'),
errorList: document.getElementById('error-list'),
platformsRequired: document.getElementById('platforms-required'),
licenseRequired: document.getElementById('license-required'),
licenseInput: document.getElementById('license')
licenseInput: document.getElementById('license'),
tagsInput: document.getElementById('tags-input'),
tagsHidden: document.getElementById('tags-hidden'),
relatedConceptsInput: document.getElementById('related-concepts-input'),
relatedConceptsHidden: document.getElementById('related-concepts-hidden'),
relatedSoftwareInput: document.getElementById('related-software-input'),
relatedSoftwareHidden: document.getElementById('related-software-hidden')
};
if (!this.elements.form || !this.elements.submitBtn) {
@@ -327,6 +620,7 @@ class ContributionForm {
console.log('[FORM] Setting up handlers...');
this.setupEventListeners();
this.setupAutocomplete();
this.updateFieldVisibility();
this.setupCharacterCounters();
this.updateYAMLPreview();
@@ -334,6 +628,58 @@ class ContributionForm {
console.log('[FORM] Initialization complete!');
}
setupAutocomplete() {
if (this.elements.tagsInput && this.elements.tagsHidden) {
const tagsManager = new AutocompleteManager(this.elements.tagsInput, allTags, {
allowMultiple: true,
hiddenInput: this.elements.tagsHidden,
placeholder: 'Beginne zu tippen, um Tags hinzuzufügen...'
});
if (this.editTool?.tags) {
tagsManager.setSelectedItems(this.editTool.tags);
}
this.autocompleteManagers.set('tags', tagsManager);
}
if (this.elements.relatedConceptsInput && this.elements.relatedConceptsHidden) {
const conceptsManager = new AutocompleteManager(this.elements.relatedConceptsInput, allConcepts, {
allowMultiple: true,
hiddenInput: this.elements.relatedConceptsHidden,
placeholder: 'Beginne zu tippen, um Konzepte zu finden...'
});
if (this.editTool?.related_concepts) {
conceptsManager.setSelectedItems(this.editTool.related_concepts);
}
this.autocompleteManagers.set('relatedConcepts', conceptsManager);
}
if (this.elements.relatedSoftwareInput && this.elements.relatedSoftwareHidden) {
const softwareManager = new AutocompleteManager(this.elements.relatedSoftwareInput, allSoftwareAndMethods, {
allowMultiple: true,
hiddenInput: this.elements.relatedSoftwareHidden,
placeholder: 'Beginne zu tippen, um Software/Methoden zu finden...'
});
if (this.editTool?.related_software) {
softwareManager.setSelectedItems(this.editTool.related_software);
}
this.autocompleteManagers.set('relatedSoftware', softwareManager);
}
Object.values(this.autocompleteManagers).forEach(manager => {
if (manager.input) {
manager.input.addEventListener('autocomplete:select', () => {
this.updateYAMLPreview();
});
}
});
}
setupEventListeners() {
this.elements.typeSelect.addEventListener('change', () => {
this.updateFieldVisibility();
@@ -366,22 +712,23 @@ class ContributionForm {
updateFieldVisibility() {
const type = this.elements.typeSelect.value;
this.elements.softwareFields.style.display = 'none';
this.elements.conceptsFields.style.display = 'none';
this.elements.softwareFields.style.display = type === 'software' ? 'block' : 'none';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'none';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'none';
this.elements.relationsFields.style.display = 'block';
if (type === 'software') {
this.elements.softwareFields.style.display = 'block';
this.elements.conceptsFields.style.display = 'block';
if (this.elements.platformsRequired) this.elements.platformsRequired.style.display = 'inline';
if (this.elements.licenseRequired) this.elements.licenseRequired.style.display = 'inline';
} else if (type === 'method') {
this.elements.conceptsFields.style.display = 'block';
if (this.elements.platformsRequired) {
this.elements.platformsRequired.style.display = type === 'software' ? 'inline' : 'none';
}
if (this.elements.licenseRequired) {
this.elements.licenseRequired.style.display = type === 'software' ? 'inline' : 'none';
}
console.log('[FORM] Field visibility updated for type:', type);
const conceptsSection = document.getElementById('related-concepts-section');
const softwareSection = document.getElementById('related-software-section');
if (conceptsSection) conceptsSection.style.display = 'block';
if (softwareSection) softwareSection.style.display = 'block';
console.log('[FORM] Updated visibility for type:', type || '(no type selected)');
}
setupCharacterCounters() {
@@ -440,14 +787,19 @@ updateYAMLPreview() {
tool.knowledgebase = true;
}
const tags = formData.get('tags');
if (tags) {
tool.tags = tags.split(',').map(t => t.trim()).filter(Boolean);
const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) {
tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
}
const relatedConcepts = formData.getAll('relatedConcepts');
if (relatedConcepts.length > 0) {
tool.related_concepts = relatedConcepts;
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) {
tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
}
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) {
tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
}
const yaml = this.generateYAML(tool);
@@ -486,6 +838,9 @@ generateYAML(tool) {
if (tool.related_concepts && tool.related_concepts.length > 0) {
lines.push(`related_concepts: [${tool.related_concepts.map(c => `"${c}"`).join(', ')}]`);
}
if (tool.related_software && tool.related_software.length > 0) {
lines.push(`related_software: [${tool.related_software.map(s => `"${s}"`).join(', ')}]`);
}
return lines.join('\n');
}
@@ -560,6 +915,7 @@ showValidationErrors(errors) {
this.elements.validationErrors.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
async handleSubmit() {
console.log('[FORM] Submit handler called!');
@@ -597,14 +953,29 @@ showValidationErrors(errors) {
phases: formData.getAll('phases'),
skillLevel: formData.get('skillLevel'),
url: formData.get('url'),
tags: formData.get('tags') ?
formData.get('tags').split(',').map(t => t.trim()).filter(Boolean) : []
tags: []
},
metadata: {
reason: formData.get('reason') || ''
reason: formData.get('reason') || '',
contact: formData.get('contact') || ''
}
};
const tagsValue = this.elements.tagsHidden?.value || '';
if (tagsValue) {
submission.tool.tags = tagsValue.split(',').map(t => t.trim()).filter(Boolean);
}
const relatedConceptsValue = this.elements.relatedConceptsHidden?.value || '';
if (relatedConceptsValue) {
submission.tool.related_concepts = relatedConceptsValue.split(',').map(t => t.trim()).filter(Boolean);
}
const relatedSoftwareValue = this.elements.relatedSoftwareHidden?.value || '';
if (relatedSoftwareValue) {
submission.tool.related_software = relatedSoftwareValue.split(',').map(t => t.trim()).filter(Boolean);
}
if (formData.get('icon')) submission.tool.icon = formData.get('icon');
if (formData.has('knowledgebase')) submission.tool.knowledgebase = true;
@@ -620,13 +991,6 @@ showValidationErrors(errors) {
}
}
if (submission.tool.type !== 'concept') {
const related = formData.getAll('relatedConcepts');
if (related.length > 0) {
submission.tool.related_concepts = related;
}
}
console.log('[FORM] Sending submission:', submission);
const response = await fetch('/api/contribute/tool', {
@@ -681,6 +1045,13 @@ showValidationErrors(errors) {
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
destroy() {
this.autocompleteManagers.forEach(manager => {
manager.destroy();
});
this.autocompleteManagers.clear();
}
}
function initializeForm() {
@@ -707,3 +1078,4 @@ if (document.readyState === 'loading') {
console.log('[FORM] Script loaded successfully');
</script>
</BaseLayout>

View File

@@ -1,4 +1,5 @@
---
//src/pages/index.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import ToolCard from '../components/ToolCard.astro';
import ToolFilters from '../components/ToolFilters.astro';
@@ -6,10 +7,18 @@ import ToolMatrix from '../components/ToolMatrix.astro';
import AIQueryInterface from '../components/AIQueryInterface.astro';
import TargetedScenarios from '../components/TargetedScenarios.astro';
import { getToolsData } from '../utils/dataService.js';
import { withAPIAuth, getAuthRequirementForContext } from '../utils/auth.js';
const data = await getToolsData();
const tools = data.tools;
const phases = data.phases;
const aiAuthRequired = getAuthRequirementForContext('ai');
let aiAuthContext: { authenticated: boolean; userId: string; session?: any; authRequired: boolean; } | null = null;
if (aiAuthRequired) {
aiAuthContext = await withAPIAuth(Astro.request, 'ai');
}
---
<BaseLayout title="~/">
@@ -36,6 +45,21 @@ const phases = data.phases;
</div>
</div>
{aiAuthRequired && !aiAuthContext?.authenticated ? (
<div class="ai-auth-required">
<button id="ai-login-btn" class="btn btn-accent btn-lg">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
Anmelden für KI-Beratung
</button>
<p style="margin-top: 0.75rem; font-size: 0.875rem; color: var(--color-text-secondary); text-align: center;">
Authentifizierung erforderlich für KI-Features
</p>
</div>
) : (
<button id="ai-query-btn" class="btn btn-accent btn-lg ai-primary-btn">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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"/>
@@ -47,6 +71,7 @@ const phases = data.phases;
<polyline points="7,7 17,7 17,17"/>
</svg>
</button>
)}
<div class="ai-features-mini">
<span class="badge badge-secondary">Workflow-Empfehlungen</span>
@@ -178,7 +203,39 @@ const phases = data.phases;
<ToolFilters data={data} />
</section>
{aiAuthRequired && !aiAuthContext?.authenticated ? (
<section id="ai-interface" class="ai-interface hidden">
<div class="ai-query-section">
<div class="content-center-lg">
<div class="card" style="text-align: center; padding: 3rem; border-left: 4px solid var(--color-accent);">
<div style="margin-bottom: 2rem;">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="1.5" style="margin: 0 auto;">
<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"/>
<path d="M9 11V7a3 3 0 0 1 6 0v4"/>
<circle cx="12" cy="12" r="2"/>
</svg>
</div>
<h2 style="margin-bottom: 1rem; color: var(--color-primary);">Anmeldung erforderlich</h2>
<p style="margin-bottom: 2rem; color: var(--color-text-secondary); line-height: 1.6;">
Für die Nutzung der KI-Beratung ist eine Authentifizierung erforderlich.
Melden Sie sich an, um personalisierten Workflow-Empfehlungen und Tool-Analysen zu erhalten.
</p>
<a href={`/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`}
class="btn btn-accent btn-lg">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
<polyline points="10 17 15 12 10 7"/>
<line x1="15" y1="12" x2="3" y2="12"/>
</svg>
Anmelden
</a>
</div>
</div>
</div>
</section>
) : (
<AIQueryInterface />
)}
<section id="tools-grid" style="padding-bottom: 2rem;">
<div class="grid-auto-fit" id="tools-container">
@@ -195,18 +252,15 @@ const phases = data.phases;
<ToolMatrix data={data} />
</BaseLayout>
<script define:vars={{ toolsData: data.tools, phases: data.phases }}>
<script define:vars={{ toolsData: data.tools, phases: data.phases, aiAuthRequired: aiAuthRequired, aiAuthenticated: aiAuthContext?.authenticated }}>
window.toolsData = toolsData;
// CONSOLIDATED: Approach selection - Pure navigation aid
window.selectApproach = function(approach) {
console.log(`Selected approach: ${approach}`);
// Clear any existing AI results
const aiResults = document.getElementById('ai-results');
if (aiResults) aiResults.style.display = 'none';
// Update visual selection state
document.querySelectorAll('.approach-card').forEach(card => {
card.classList.remove('selected');
});
@@ -214,14 +268,12 @@ const phases = data.phases;
const selectedCard = document.querySelector(`.approach-card.${approach}`);
if (selectedCard) selectedCard.classList.add('selected');
// Hide all approach sections first (ensures mutual exclusivity)
const methodologySection = document.getElementById('methodology-section');
const targetedSection = document.getElementById('targeted-section');
if (methodologySection) methodologySection.classList.remove('active');
if (targetedSection) targetedSection.classList.remove('active');
// Show the selected approach section (navigation aid only)
if (approach === 'methodology') {
if (methodologySection) {
methodologySection.classList.add('active');
@@ -235,11 +287,9 @@ const phases = data.phases;
}
};
// CONSOLIDATED: Phase selection - Sets unified filter dropdown
window.selectPhase = function(phase) {
console.log(`Selected NIST phase: ${phase}`);
// Update visual selection of phase cards
document.querySelectorAll('.phase-card').forEach(card => {
card.classList.remove('active');
});
@@ -249,23 +299,19 @@ const phases = data.phases;
selectedCard.classList.add('active');
}
// CONSOLIDATED: Set the unified phase-select dropdown
const phaseSelect = document.getElementById('phase-select');
if (phaseSelect) {
phaseSelect.value = phase;
// Trigger the change event to activate unified filtering
const changeEvent = new Event('change', { bubbles: true });
phaseSelect.dispatchEvent(changeEvent);
}
// Switch to grid view to show filtered results
const gridToggle = document.querySelector('.view-toggle[data-view="grid"]');
if (gridToggle && !gridToggle.classList.contains('active')) {
gridToggle.click();
}
// Scroll to filtered results
setTimeout(() => {
window.scrollToElementById('tools-grid');
}, 200);
@@ -279,12 +325,20 @@ const phases = data.phases;
const filtersSection = document.getElementById('filters-section');
const noResults = document.getElementById('no-results');
const aiQueryBtn = document.getElementById('ai-query-btn');
const aiLoginBtn = document.getElementById('ai-login-btn');
if (!toolsContainer || !toolsGrid || !matrixContainer || !noResults || !aiInterface || !filtersSection) {
console.error('Required DOM elements not found');
return;
}
if (aiLoginBtn) {
aiLoginBtn.addEventListener('click', () => {
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = `/api/auth/login?returnTo=${currentUrl}`;
});
}
if (aiQueryBtn) {
aiQueryBtn.addEventListener('click', () => {
aiQueryBtn.classList.add('activated');
@@ -303,58 +357,73 @@ const phases = data.phases;
}
function switchToView(view) {
console.log('[VIEW] Switching to view:', view);
const toolsGrid = document.getElementById('tools-grid');
const matrixContainer = document.getElementById('matrix-container');
const aiInterface = document.getElementById('ai-interface');
const filtersSection = document.getElementById('filters-section');
const noResults = document.getElementById('no-results');
// FIXED: Hide approach sections when switching to ANY view mode
const methodologySection = document.getElementById('methodology-section');
const targetedSection = document.getElementById('targeted-section');
// Hide all main content areas
if (toolsGrid) toolsGrid.style.display = 'none';
if (matrixContainer) matrixContainer.style.display = 'none';
if (matrixContainer) {
matrixContainer.style.display = 'none';
matrixContainer.classList.add('hidden');
}
if (aiInterface) aiInterface.style.display = 'none';
if (noResults) noResults.style.display = 'none';
// FIXED: Hide approach sections when switching to view modes
if (methodologySection) methodologySection.classList.remove('active');
if (targetedSection) targetedSection.classList.remove('active');
switch (view) {
case 'grid':
console.log('[VIEW] Showing grid view');
if (toolsGrid) toolsGrid.style.display = 'block';
if (filtersSection) filtersSection.style.display = 'block';
break;
case 'matrix':
if (matrixContainer) matrixContainer.style.display = 'block';
console.log('[VIEW] Showing matrix view');
if (matrixContainer) {
matrixContainer.style.display = 'block';
matrixContainer.classList.remove('hidden');
}
if (filtersSection) filtersSection.style.display = 'block';
break;
case 'ai':
console.log('[VIEW] Showing AI view');
if (aiAuthRequired && !aiAuthenticated) {
console.log('[AUTH] AI access denied, redirecting to login');
const currentUrl = encodeURIComponent(window.location.href);
window.location.href = `/api/auth/login?returnTo=${currentUrl}`;
return;
}
if (aiInterface) aiInterface.style.display = 'block';
// FIXED: Show filters but hide everything except view controls
if (filtersSection) {
filtersSection.style.display = 'block';
// Hide all filter sections except the last one (view controls)
const filterSections = filtersSection.querySelectorAll('.filter-section');
filterSections.forEach((section, index) => {
if (index === filterSections.length - 1) {
// Keep view controls visible
section.style.display = 'block';
} else {
// Hide other filter sections
section.style.display = 'none';
}
});
}
break;
default:
console.warn('[VIEW] Unknown view:', view);
}
// FIXED: Reset filter sections visibility when not in AI view
if (view !== 'ai' && filtersSection) {
const filterSections = filtersSection.querySelectorAll('.filter-section');
filterSections.forEach(section => {
@@ -363,7 +432,6 @@ const phases = data.phases;
}
}
// Navigation functions for AI recommendations (unchanged)
window.navigateToGrid = function(toolName) {
console.log('Navigating to grid for tool:', toolName);
@@ -455,12 +523,18 @@ const phases = data.phases;
return;
}
const tool = window.findToolByIdentifier(window.toolsData, toolParam);
if (!tool) {
console.warn('Shared tool not found:', toolParam);
if (!window.findToolByIdentifier) {
console.error('[SHARE] findToolByIdentifier not available, retrying...');
setTimeout(() => handleSharedURL(), 200);
return;
}
const tool = window.findToolByIdentifier(window.toolsData, toolParam);
if (!tool) {
return;
}
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
@@ -482,11 +556,9 @@ const phases = data.phases;
default:
window.navigateToGrid(tool.name);
}
}, 100);
}, 300);
}
// REPLACE the existing toolsFiltered event listener in index.astro with this enhanced version:
window.addEventListener('toolsFiltered', (event) => {
const { tools: filtered, semanticSearch } = event.detail;
const currentView = document.querySelector('.view-toggle.active')?.getAttribute('data-view');
@@ -504,11 +576,9 @@ const phases = data.phases;
if (semanticSearch && filtered.length > 0) {
console.log('[SEMANTIC] Reordering tools by semantic similarity');
// FIXED: Create ordered array of cards based on semantic similarity
const orderedCards = [];
const remainingCards = [];
// First pass: collect cards in semantic order
filtered.forEach(tool => {
const toolName = tool.name.toLowerCase();
const matchingCard = Array.from(allToolCards).find(card =>
@@ -520,12 +590,10 @@ const phases = data.phases;
orderedCards.push(matchingCard);
visibleCount++;
// Add semantic indicators if available
if (tool._semanticSimilarity) {
matchingCard.setAttribute('data-semantic-similarity', tool._semanticSimilarity.toFixed(3));
matchingCard.setAttribute('data-semantic-rank', tool._semanticRank || '');
// Visual indication of semantic ranking (subtle)
const header = matchingCard.querySelector('.tool-card-header h3');
if (header && tool._semanticRank <= 3) {
const existingIndicator = header.querySelector('.semantic-rank-indicator');
@@ -551,7 +619,6 @@ const phases = data.phases;
}
});
// Second pass: hide non-matching cards and collect them
allToolCards.forEach(card => {
const toolName = card.getAttribute('data-tool-name');
if (!filteredNames.has(toolName)) {
@@ -560,18 +627,15 @@ const phases = data.phases;
}
});
// Reorder DOM: semantic results first, then hidden cards
const allCards = [...orderedCards, ...remainingCards];
allCards.forEach(card => {
toolsContainer.appendChild(card);
});
} else {
// FIXED: Standard filtering without semantic ordering
allToolCards.forEach(card => {
const toolName = card.getAttribute('data-tool-name');
// Clean up any semantic indicators
card.removeAttribute('data-semantic-similarity');
card.removeAttribute('data-semantic-rank');
const semanticIndicator = card.querySelector('.semantic-rank-indicator');
@@ -587,10 +651,8 @@ const phases = data.phases;
}
});
// Restore original order when not using semantic search
if (!semanticSearch) {
const originalOrder = Array.from(allToolCards).sort((a, b) => {
// Get original indices from data attributes or DOM order
const aIndex = Array.from(allToolCards).indexOf(a);
const bIndex = Array.from(allToolCards).indexOf(b);
return aIndex - bIndex;
@@ -602,14 +664,12 @@ const phases = data.phases;
}
}
// Show/hide no results message
if (visibleCount === 0) {
noResults.style.display = 'block';
} else {
noResults.style.display = 'none';
}
// Log semantic search info
if (semanticSearch) {
console.log(`[SEMANTIC] Displayed ${visibleCount} tools in semantic order`);
}
@@ -617,12 +677,17 @@ const phases = data.phases;
window.addEventListener('viewChanged', (event) => {
const view = event.detail;
if (!event.triggeredByButton) {
switchToView(view);
}
});
window.switchToAIView = () => switchToView('ai');
window.switchToView = switchToView;
setTimeout(() => {
handleSharedURL();
}, 1000);
});
</script>
</BaseLayout>

View File

@@ -1,14 +1,18 @@
---
//src/pages/knowledgebase.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
import { getToolsData } from '../utils/dataService.js';
import ContributionButton from '../components/ContributionButton.astro';
import { isGatedContentAuthRequired } from '../utils/auth.js';
const data = await getToolsData();
const allKnowledgebaseEntries = await getCollection('knowledgebase', (entry) => {
return entry.data.published !== false;
});
const gatedContentAuthEnabled = isGatedContentAuthRequired();
const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
const associatedTool = entry.data.tool_name
? data.tools.find((tool: any) => tool.name === entry.data.tool_name)
@@ -23,7 +27,7 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
difficulty: entry.data.difficulty,
categories: entry.data.categories || [],
tags: entry.data.tags || [],
gated_content: entry.data.gated_content || false,
tool_name: entry.data.tool_name,
related_tools: entry.data.related_tools || [],
associatedTool,
@@ -39,6 +43,9 @@ const knowledgebaseEntries = allKnowledgebaseEntries.map((entry) => {
});
knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
const gatedCount = knowledgebaseEntries.filter(entry => entry.gated_content).length;
const publicCount = knowledgebaseEntries.length - gatedCount;
---
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
@@ -52,6 +59,24 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
Praktische Erfahrungen, Konfigurationshinweise und Lektionen aus der Praxis
</p>
{gatedContentAuthEnabled && gatedCount > 0 && (
<div class="gated-content-info mb-4 p-3 rounded" style="background-color: var(--color-bg-secondary); border: 1px solid var(--color-border);">
<div class="flex items-center justify-center gap-2 text-sm text-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<span>
{gatedCount} geschützte Artikel • {publicCount} öffentliche Artikel
</span>
</div>
<p class="text-xs text-secondary mt-1">
🔒 Geschützte Artikel erfordern Authentifizierung
</p>
</div>
)}
<div class="flex gap-4 justify-center flex-wrap">
<ContributionButton type="write" variant="primary" text="Artikel schreiben" style="padding: 0.75rem 1.5rem;" />
<button onclick="window.scrollToElementById('kb-entries')" class="btn btn-secondary" style="padding: 0.75rem 1.5rem;">
@@ -60,7 +85,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
Artikel durchsuchen
</a>
</button>
</div>
</div>
@@ -107,6 +132,9 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
const isMethod = hasAssociatedTool && entry.associatedTool.type === 'method';
const isConcept = hasAssociatedTool && entry.associatedTool.type === 'concept';
const isStandalone = !hasAssociatedTool;
const isGated = entry.gated_content === true;
const articleUrl = `/knowledgebase/${entry.slug}`;
return (
<article
@@ -114,7 +142,8 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
id={`kb-${entry.slug}`}
data-tool-name={entry.title.toLowerCase()}
data-article-type={isStandalone ? 'standalone' : 'tool-associated'}
onclick={`window.location.href='/knowledgebase/${entry.slug}'`}
data-gated={isGated}
onclick={`window.location.href='${articleUrl}'`}
>
<!-- Card Header -->
<div class="flex-between mb-3">
@@ -123,6 +152,11 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
<div class="min-w-0 flex-1">
<h3 class="text-lg font-semibold text-primary mb-1 leading-tight">
{entry.title}
{isGated && gatedContentAuthEnabled && (
<span class="gated-indicator ml-2" title="Geschützter Inhalt - Authentifizierung erforderlich">
🔒
</span>
)}
</h3>
<div class="flex gap-2 flex-wrap mb-2">
{isStandalone && <span class="badge" style="background-color: var(--color-accent); color: white;">Artikel</span>}
@@ -132,26 +166,53 @@ 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>}
<span class="badge badge-error">📖</span>
{isGated && gatedContentAuthEnabled && <span class="badge badge-warning">🔒</span>}
</div>
</div>
</div>
<div class="flex gap-2 flex-shrink-0" onclick="event.stopPropagation();">
<a href={`/knowledgebase/${entry.slug}`} class="btn btn-primary btn-sm">
<a href={articleUrl}
class="btn btn-primary btn-sm"
title={isGated && isGatedContentAuthRequired() ? "Geschützter Inhalt - Anmeldung erforderlich" : "Artikel öffnen"}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<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>
Öffnen
{isGated && gatedContentAuthEnabled ? 'Anmelden' : 'Öffnen'}
</a>
<ContributionButton type="edit" toolName={entry.tool_name || entry.title} variant="secondary" text="Edit" style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;" />
<button
class="btn btn-secondary btn-sm"
onclick="window.shareArticle(this, '${articleUrl}', '${entry.title}')"
title="Artikel teilen"
style="font-size: 0.8125rem; padding: 0.5rem 0.75rem;"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.375rem;">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
Teilen
</button>
</div>
</div>
<!-- Description -->
<p class="text-secondary mb-4 leading-relaxed">
{entry.description}
{isGated && gatedContentAuthEnabled && (
<span class="gated-content-hint ml-2 text-xs">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: inline; margin-right: 0.25rem;">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
Authentifizierung erforderlich
</span>
)}
</p>
<!-- Metadata Footer -->
@@ -285,3 +346,24 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
});
});
</script>
<style>
.gated-indicator {
font-size: 0.875rem;
opacity: 0.8;
}
.gated-content-hint {
color: var(--color-warning);
font-weight: 500;
}
.kb-entry[data-gated="true"] {
border-left: 3px solid var(--color-warning);
}
.gated-content-info {
border-left: 4px solid var(--color-warning) !important;
}
</style>
</BaseLayout>

View File

@@ -2,6 +2,7 @@
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getToolsData } from '../../utils/dataService.js';
import { isGatedContentAuthRequired } from '../../utils/auth.js';
export const prerender = true;
@@ -20,6 +21,12 @@ export async function getStaticPaths() {
const { entry }: { entry: any } = Astro.props;
const isGatedContent = entry.data.gated_content === true;
const gatedContentAuthRequired = isGatedContentAuthRequired();
const requiresAuth = isGatedContent && gatedContentAuthRequired;
console.log(`[GATED CONTENT] Article: ${entry.data.title}, Gated: ${isGatedContent}, Auth Required: ${requiresAuth}`);
const { Content } = await entry.render();
const data = await getToolsData();
@@ -47,117 +54,253 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
displayTool.projectUrl !== null &&
displayTool.projectUrl !== "" &&
displayTool.projectUrl.trim() !== "";
const currentUrl = Astro.url.href;
---
<BaseLayout title={entry.data.title} description={entry.data.description}>
<article style="max-width: 900px; margin: 0 auto;">
<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;">
<h1 style="margin: 0 0 0.5rem 0; color: var(--color-primary);">
{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;">
{entry.data.description}
</p>
</div>
<div style="display: flex; flex-direction: column; gap: 0.5rem; align-items: end;">
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
{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>
{requiresAuth && (
<script define:vars={{ requiresAuth, articleTitle: entry.data.title }}>
document.addEventListener('DOMContentLoaded', async () => {
if (!requiresAuth) return;
<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);">
{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>
console.log('[GATED CONTENT] Checking client-side auth for: ' + articleTitle);
const urlParams = new URLSearchParams(window.location.search);
const authSuccess = urlParams.get('auth') === 'success';
const contentArea = document.querySelector('.article-content');
const sidebar = document.querySelector('.article-sidebar');
if (contentArea) {
contentArea.style.display = 'none';
}
if (authSuccess) {
console.log('[GATED CONTENT] Auth success detected, waiting for session...');
await new Promise(resolve => setTimeout(resolve, 1000));
const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
window.history.replaceState({}, document.title, cleanUrl);
}
try {
const response = await fetch('/api/auth/status');
const authStatus = await response.json();
const isAuthenticated = authStatus.gatedContentAuthenticated || false;
const authRequired = authStatus.gatedContentAuthRequired || false;
console.log('[GATED CONTENT] Auth status - Required: ' + authRequired + ', Authenticated: ' + isAuthenticated);
if (authRequired && !isAuthenticated) {
console.log('[GATED CONTENT] Access denied - showing auth required message: ' + articleTitle);
if (contentArea) {
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
contentArea.innerHTML = [
'<div class="gated-content-block">',
'<div class="gated-icon">🔒</div>',
'<h3 class="gated-title">Authentifizierung erforderlich</h3>',
'<p class="gated-description">Dieser Artikel enthält geschützte Inhalte und ist nur für authentifizierte Benutzer zugänglich.</p>',
'<div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.5rem;">',
'<a href="' + loginUrl + '" class="btn btn-primary" style="min-width: 140px; text-align: center;">',
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">',
'<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>',
'<polyline points="10 17 15 12 10 7"/>',
'<line x1="15" y1="12" x2="3" y2="12"/>',
'</svg>',
'Anmelden',
'</a>',
'<a href="/knowledgebase" class="btn btn-secondary" style="min-width: 140px; text-align: center;">Zurück zur Übersicht</a>',
'</div>',
'<div class="gated-help">',
'<small>Nach der Anmeldung werden Sie automatisch zu diesem Artikel zurückgeleitet.</small>',
'</div>',
'</div>'
].join('');
contentArea.style.display = 'block';
}
} else {
console.log('[GATED CONTENT] Access granted for: ' + articleTitle);
if (contentArea) {
contentArea.style.display = 'block';
}
setTimeout(() => {
if (typeof generateTOCContent === 'function') {
generateTOCContent();
}
}, 100);
}
} catch (error) {
console.error('[GATED CONTENT] Auth check failed:', error);
if (requiresAuth && contentArea) {
const loginUrl = '/api/auth/login?returnTo=' + encodeURIComponent(window.location.href);
contentArea.innerHTML = [
'<div class="gated-content-block">',
'<div class="gated-icon error">⚠️</div>',
'<h3 class="gated-title">Authentifizierungsfehler</h3>',
'<p class="gated-description">Es gab ein Problem bei der Überprüfung Ihrer Berechtigung. Bitte versuchen Sie es erneut.</p>',
'<div class="gated-actions">',
'<a href="' + loginUrl + '" class="btn btn-primary">Anmelden</a>',
'<button onclick="location.reload()" class="btn btn-secondary">Seite neu laden</button>',
'</div>',
'</div>'
].join('');
contentArea.style.display = 'block';
}
}
});
</script>
)}
<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>
<div class="article-layout">
<!-- Article Header -->
<header class="article-header">
<div class="article-header-content">
<div class="article-meta">
<nav class="breadcrumb">
<a href="/knowledgebase" class="breadcrumb-link">Knowledgebase</a>
<span class="breadcrumb-separator">→</span>
<span class="breadcrumb-current">{entry.data.title}</span>
</nav>
<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>
<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>
{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>
<div style="display: flex; flex-wrap: wrap; gap: 0.25rem; margin-top: 0.25rem;">
{entry.data.categories.map((cat: string) => (
<span class="tag" style="font-size: 0.75rem;">{cat}</span>
<div class="article-tags">
{entry.data.categories?.map((cat: string) => (
<span class="article-tag article-tag-category">{cat}</span>
))}
{entry.data.tags?.map((tag: string) => (
<span class="article-tag">{tag}</span>
))}
</div>
</div>
<div class="article-title-section">
<h1 class="article-title">
{displayTool?.icon && <span class="article-icon">{displayTool.icon}</span>}
{entry.data.title}
{isGatedContent && (
<span class="gated-indicator" title="Geschützter Inhalt - Authentifizierung erforderlich">
🔒
</span>
)}
</h1>
<p class="article-description">{entry.data.description}</p>
{isGatedContent && gatedContentAuthRequired && (
<div class="gated-content-notice">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem;">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<circle cx="12" cy="16" r="1"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<span>Dieser Artikel enthält geschützte Inhalte</span>
</div>
)}
</div>
<div class="article-metadata-grid">
<div class="metadata-item">
<span class="metadata-label">Typ</span>
<div class="metadata-badges">
{isStandalone ? (
<span class="badge badge-accent">Artikel</span>
) : (
<>
{isConcept && <span class="badge badge-concept">Konzept</span>}
{isMethod && <span class="badge badge-method">Methode</span>}
{!isMethod && !isConcept && <span class="badge badge-primary">Software</span>}
{hasValidProjectUrl && <span class="badge badge-primary">CC24-Server</span>}
</>
)}
<span class="badge badge-error">📖</span>
{isGatedContent && <span class="badge badge-warning">🔒</span>}
</div>
</div>
{entry.data.difficulty && (
<div class="metadata-item">
<span class="metadata-label">Schwierigkeit</span>
<span class="metadata-value">{entry.data.difficulty}</span>
</div>
)}
<div class="metadata-item">
<span class="metadata-label">Aktualisiert</span>
<span class="metadata-value">{entry.data.last_updated.toLocaleDateString('de-DE')}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Autor</span>
<span class="metadata-value">{entry.data.author}</span>
</div>
<div class="metadata-item">
<span class="metadata-label">Lesezeit</span>
<span class="metadata-value" id="reading-time">~5 min</span>
</div>
</div>
<div class="article-actions">
<button
id="share-article-btn"
class="btn btn-secondary"
onclick="window.shareCurrentArticle(this)"
title="Artikel teilen"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="18" cy="5" r="3"/>
<circle cx="6" cy="12" r="3"/>
<circle cx="18" cy="19" r="3"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
</svg>
Teilen
</button>
<a href="/knowledgebase" class="btn btn-secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15,18 9,12 15,6"></polyline>
</svg>
Zurück
</a>
</div>
</div>
</header>
<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;">
<polyline points="15,18 9,12 15,6"></polyline>
</svg>
Zurück zur Knowledgebase
</a>
</nav>
<!-- Main Content Area -->
<div class="article-content-wrapper">
<!-- Sidebar Navigation (will be populated by JS) -->
<aside class="article-sidebar">
<!-- TOC will be inserted here by JavaScript -->
</aside>
<div class="card" style="padding: 2rem;">
<div class="kb-content markdown-content" style="line-height: 1.7;">
<!-- Article Content -->
<main class="article-main">
<article class="article-content" style={requiresAuth ? "display: none;" : ""}>
<div class="markdown-content">
<Content />
</div>
</div>
</article>
<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'}
</h3>
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<!-- Article Footer -->
<footer class="article-footer">
<div class="article-footer-actions">
<h3>Links</h3>
<div class="footer-actions-grid">
{isStandalone ? (
<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">
<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>
Weitere Artikel
</a>
) : (
<>
{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;">
<a href={displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-concept">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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"/>
@@ -165,8 +308,8 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
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;">
<a href={displayTool.projectUrl || displayTool.url} target="_blank" rel="noopener noreferrer" class="btn btn-method">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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"/>
@@ -176,16 +319,16 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
) : (
<>
<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;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<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
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;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<path d="M12 16l4-4-4-4"/>
<path d="M8 12h8"/>
@@ -198,35 +341,8 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</>
)}
{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>
)}
<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">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9,22 9,12 15,12 15,22"/>
</svg>
@@ -234,5 +350,338 @@ const hasValidProjectUrl = displayTool && displayTool.projectUrl !== undefined &
</a>
</div>
</div>
</article>
{relatedTools.length > 0 && (
<div class="related-tools">
<h3>Verwandte Tools</h3>
<div class="related-tools-grid">
{relatedTools.map((tool: any) => (
<a href={tool.projectUrl || tool.url} target="_blank" rel="noopener noreferrer" class="related-tool-card">
{tool.icon && <span class="tool-icon">{tool.icon}</span>}
<span class="tool-name">{tool.name}</span>
</a>
))}
</div>
</div>
)}
</footer>
</main>
</div>
</div>
<script define:vars={{ requiresAuth }}>
/** @template {Element} T
* @param {string} sel
* @param {Document|Element} [root=document]
* @returns {T|null} */
const qs = (sel, root = document) => root.querySelector(sel);
/** @template {Element} T
* @param {string} sel
* @param {Document|Element} [root=document]
* @returns {T[]} */
const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel));
function calculateReadingTime() {
/** @type {HTMLElement|null} */
const content = qs('.markdown-content');
if (!content) return;
const text = (content.textContent || content.innerText || '').trim();
if (!text) return;
const wordsPerMinute = 200;
const words = text.split(/\s+/).length;
const readingTime = Math.ceil(words / wordsPerMinute);
const readingTimeElement = document.getElementById('reading-time');
if (readingTimeElement) {
readingTimeElement.textContent = `~${readingTime} min`;
}
}
function generateSidebarTOC() {
if (requiresAuth) {
return;
}
generateTOCContent();
}
function generateTOCContent() {
/** @type {HTMLElement|null} */
const article = qs('.markdown-content');
/** @type {HTMLElement|null} */
const sidebar = qs('.article-sidebar');
/** @type {HTMLElement|null} */
const main = qs('.article-main');
if (!article || !sidebar || !main) return;
/** @type {HTMLHeadingElement[]} */
const headings = qsa('h1, h2, h3, h4, h5, h6', article);
if (headings.length < 2) {
sidebar.style.display = 'none';
main.style.maxWidth = '100%';
return;
}
headings.forEach((h, i) => {
if (!h.id) h.id = `heading-${i}`;
});
const tocHTML = `
<div class="sidebar-toc">
<h3 class="toc-title">Inhaltsverzeichnis</h3>
<nav class="toc-navigation">
${headings.map((h) => {
const level = parseInt(h.tagName.slice(1), 10);
const text = (h.textContent || '').trim();
const id = h.id;
return `
<a href="#${id}" class="toc-item toc-level-${level}" data-heading="${id}">
${text}
</a>
`;
}).join('')}
</nav>
</div>
`;
sidebar.innerHTML = tocHTML;
/** @type {HTMLAnchorElement[]} */
const tocItems = qsa('.toc-item', sidebar);
tocItems.forEach((item) => {
item.addEventListener('click', (e) => {
e.preventDefault();
const href = item.getAttribute('href');
if (!href || !href.startsWith('#')) return;
const target = document.getElementById(href.slice(1));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' });
tocItems.forEach((i) => i.classList.remove('active'));
item.classList.add('active');
}
});
});
const updateActiveSection = () => {
const scrollY = window.scrollY + 100;
let currentId = null;
for (let i = 0; i < headings.length; i++) {
const h = headings[i];
const rect = h.getBoundingClientRect();
const absTop = rect.top + window.pageYOffset;
if (absTop <= scrollY) currentId = h.id;
}
if (currentId) {
tocItems.forEach((i) => i.classList.remove('active'));
/** @type {HTMLAnchorElement|null} */
const active = qs(`.toc-item[data-heading="${currentId}"]`, sidebar);
if (active) active.classList.add('active');
}
};
let ticking = false;
window.addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
updateActiveSection();
ticking = false;
});
});
updateActiveSection();
}
function enhanceCodeCopy() {
/** @type {HTMLPreElement[]} */
const pres = qsa('.markdown-content pre');
pres.forEach((pre) => {
if (pre.dataset.copyEnhanced === 'true') return;
pre.dataset.copyEnhanced = 'true';
pre.style.position ||= 'relative';
let btn =
pre.querySelector('.copy-btn') ||
pre.querySelector('.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]');
if (btn && !btn.classList.contains('copy-btn')) {
btn.classList.add('copy-btn');
}
if (!btn) {
btn = document.createElement('button');
btn.type = 'button';
btn.className = 'copy-btn';
btn.setAttribute('aria-label', 'Code kopieren');
btn.innerHTML = `
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
aria-hidden="true"><rect x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<span>Copy</span>
`;
pre.appendChild(btn);
}
const possibleOldButtons = pre.querySelectorAll(
'.btn-copy, .copy-button, .code-copy, .copy-code, button[aria-label*="copy" i]'
);
possibleOldButtons.forEach((b) => {
if (b !== btn) b.style.display = 'none';
});
if (!pre.querySelector('.copied-pill')) {
const pill = document.createElement('div');
pill.className = 'copied-pill';
pill.textContent = '✓ Kopiert';
pre.appendChild(pill);
}
if (!pre.querySelector('.sr-live')) {
const live = document.createElement('div');
live.className = 'sr-live';
live.setAttribute('aria-live', 'polite');
Object.assign(live.style, {
position: 'absolute', width: '1px', height: '1px', padding: '0', margin: '-1px',
overflow: 'hidden', clip: 'rect(0,0,0,0)', border: '0'
});
pre.appendChild(live);
}
btn.addEventListener('click', async () => {
const code = pre.querySelector('code');
const text = code ? code.innerText : pre.innerText;
const live = pre.querySelector('.sr-live');
const copyText = async (t) => {
try {
await navigator.clipboard.writeText(t);
return true;
} catch {
const ta = document.createElement('textarea');
ta.value = t;
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand('copy');
document.body.removeChild(ta);
return ok;
}
};
const ok = await copyText(text);
pre.dataset.copied = ok ? 'true' : 'false';
if (live) live.textContent = ok ? 'Code in die Zwischenablage kopiert' : 'Kopieren fehlgeschlagen';
window.setTimeout(() => { pre.dataset.copied = 'false'; }, 1200);
});
});
}
window.generateTOCContent = generateTOCContent;
document.addEventListener('DOMContentLoaded', () => {
calculateReadingTime();
generateSidebarTOC();
enhanceCodeCopy();
});
</script>
<style>
.gated-indicator {
font-size: 0.875rem;
opacity: 0.8;
margin-left: 0.5rem;
}
.gated-content-notice {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
background-color: var(--color-warning-bg, rgba(255, 193, 7, 0.1));
border: 1px solid var(--color-warning, #ffc107);
border-radius: 0.375rem;
color: var(--color-warning-text, #856404);
font-size: 0.875rem;
margin-top: 1rem;
}
.gated-content-block {
text-align: center;
padding: 4rem 2rem;
max-width: 600px;
margin: 0 auto;
}
.gated-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
opacity: 0.8;
}
.gated-icon.error {
color: var(--color-error, #dc3545);
}
.gated-title {
font-size: 1.5rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
}
.gated-description {
font-size: 1rem;
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: 2rem;
}
.gated-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.gated-actions .btn {
margin: 0 0.5rem;
}
.gated-help {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.gated-help small {
opacity: 0.8;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.gated-content-block {
padding: 3rem 1rem;
}
.gated-actions {
flex-direction: column;
align-items: center;
}
.gated-actions .btn {
width: 100%;
max-width: 280px;
}
}
</style>
</BaseLayout>

View File

@@ -1,4 +1,5 @@
---
//src/pages/status.astro
import BaseLayout from '../layouts/BaseLayout.astro';
import { getToolsData } from '../utils/dataService.js';

121
src/styles/autocomplete.css Normal file
View File

@@ -0,0 +1,121 @@
/* ============================================================================
AUTOCOMPLETE COMPONENT STYLES
============================================================================ */
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: var(--shadow-lg);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.autocomplete-option {
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
}
.autocomplete-option:last-child {
border-bottom: none;
}
.autocomplete-option:hover {
background-color: var(--color-bg-secondary);
}
.autocomplete-item {
font-size: 0.875rem;
color: var(--color-text);
}
.autocomplete-no-results {
padding: 0.75rem;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.875rem;
font-style: italic;
}
.autocomplete-selected {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
min-height: 1.5rem;
}
.autocomplete-tag {
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
animation: fadeInScale 0.2s ease-out;
}
.autocomplete-remove {
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.15s ease;
}
.autocomplete-remove:hover {
background-color: rgba(255, 255, 255, 0.2);
}
/* Animation for tag appearance */
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
/* Ensure autocomplete container has relative positioning */
.autocomplete-container {
position: relative;
}
/* Custom scrollbar for autocomplete dropdown */
.autocomplete-dropdown::-webkit-scrollbar {
width: 6px;
}
.autocomplete-dropdown::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.autocomplete-dropdown::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 3px;
}
.autocomplete-dropdown::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}

View File

@@ -14,72 +14,7 @@
padding: 0;
}
/* ===================================================================
2. CSS VARIABLES AND THEMING
================================================================= */
:root {
/* Light Theme Colors */
--color-bg: #fff;
--color-bg-secondary: #f8fafc;
--color-bg-tertiary: #e2e8f0;
--color-text: #1e293b;
--color-text-secondary: #64748b;
--color-border: #cbd5e1;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-accent: #059669;
--color-accent-hover: #047857;
--color-warning: #d97706;
--color-error: #dc2626;
/* Enhanced card type colors */
--color-hosted: #7c3aed;
--color-hosted-bg: #f3f0ff;
--color-oss: #059669;
--color-oss-bg: #ecfdf5;
--color-method: #0891b2;
--color-method-bg: #f0f9ff;
--color-concept: #ea580c;
--color-concept-bg: #fff7ed;
/* Shadows */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 5%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 10%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 10%);
/* Transitions */
--transition-fast: all 0.2s ease;
--transition-medium: all 0.3s ease;
}
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-secondary: #1e293b;
--color-bg-tertiary: #334155;
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-border: #475569;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-accent: #10b981;
--color-accent-hover: #34d399;
--color-warning: #f59e0b;
--color-error: #f87171;
--color-hosted: #a855f7;
--color-hosted-bg: #2e1065;
--color-oss: #10b981;
--color-oss-bg: #064e3b;
--color-method: #0891b2;
--color-method-bg: #164e63;
--color-concept: #f97316;
--color-concept-bg: #7c2d12;
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 30%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 40%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 50%);
}
/* ===================================================================
3. BASE HTML ELEMENTS
@@ -740,6 +675,7 @@ input[type="checkbox"] {
border-radius: 0.25rem;
font-size: 0.75rem;
margin: 0.125rem;
max-height: 1.5rem;
}
/* ===================================================================
@@ -747,11 +683,13 @@ input[type="checkbox"] {
================================================================= */
.tool-card {
height: 300px;
min-height: 300px;
max-height: 350px;
display: flex;
flex-direction: column;
padding: 1.25rem;
cursor: pointer;
overflow: hidden;
}
.tool-card-header {
@@ -760,6 +698,7 @@ input[type="checkbox"] {
align-items: flex-start;
min-height: 2.5rem;
margin-bottom: 0.75rem;
flex-shrink: 0;
}
.tool-card-header h3 {
@@ -788,6 +727,7 @@ input[type="checkbox"] {
font-size: 0.875rem;
margin-bottom: 0.5rem;
word-break: break-word;
flex-shrink: 0;
}
.tool-card-metadata {
@@ -796,6 +736,7 @@ input[type="checkbox"] {
gap: 1rem;
margin-bottom: 0.75rem;
line-height: 1;
flex-shrink: 0;
}
.metadata-item {
@@ -824,10 +765,11 @@ input[type="checkbox"] {
flex-wrap: wrap;
gap: 0.25rem;
max-height: 3.5rem;
min-height: 0;
overflow: hidden;
position: relative;
margin-bottom: 1rem;
flex-shrink: 0;
flex: 0 0 3.5rem;
}
.tool-tags-container::after {
@@ -861,6 +803,8 @@ input[type="checkbox"] {
.tool-card-buttons {
margin-top: auto;
flex-shrink: 0;
flex-basis: auto;
min-height: 0;
}
.button-row {
@@ -1863,11 +1807,44 @@ input[type="checkbox"] {
.ai-textarea-section {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.ai-textarea-section textarea {
width: 100%;
height: 180px;
min-height: 180px;
max-height: 300px;
resize: vertical;
font-size: 0.9375rem;
line-height: 1.5;
padding: 0.75rem;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
background-color: var(--color-bg);
color: var(--color-text);
transition: var(--transition-fast);
flex: 1;
}
.confidence-tooltip {
background: var(--color-bg) !important;
border: 2px solid var(--color-border) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15) !important;
z-index: 2000 !important;
}
.ai-textarea-section textarea:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
}
.ai-suggestions-section {
flex: 0 0 320px;
min-height: 120px;
min-height: 180px;
height: auto;
}
.ai-input-container textarea {
@@ -1944,7 +1921,6 @@ input[type="checkbox"] {
box-shadow: 0 2px 4px 0 rgb(255 255 255 / 10%);
}
/* Enhanced contextual analysis cards */
.contextual-analysis-card {
margin-bottom: 2rem;
border-left: 4px solid;
@@ -2041,7 +2017,6 @@ input[type="checkbox"] {
opacity: 1;
}
/* Enhanced queue status for micro-tasks */
.queue-status-card.micro-task-mode {
border-left: 4px solid var(--color-primary);
}
@@ -2054,7 +2029,6 @@ input[type="checkbox"] {
border-radius: 0.5rem 0.5rem 0 0;
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.micro-task-steps {
grid-template-columns: repeat(2, 1fr);
@@ -2246,12 +2220,20 @@ input[type="checkbox"] {
border-radius: 1rem;
font-weight: 500;
text-transform: uppercase;
position: relative;
z-index: 1;
}
.tool-rec-priority.high { background-color: var(--color-error); color: white; }
.tool-rec-priority.medium { background-color: var(--color-warning); color: white; }
.tool-rec-priority.low { background-color: var(--color-accent); color: white; }
[data-theme="dark"] .confidence-tooltip {
background: var(--color-bg-secondary) !important;
border-color: var(--color-border) !important;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4) !important;
}
.tool-rec-justification {
font-size: 0.875rem;
line-height: 1.5;
@@ -2670,7 +2652,8 @@ footer {
================================================================= */
.smart-prompting-container {
height: 100%;
height: auto;
min-height: 180px;
animation: smartPromptSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
@@ -2679,8 +2662,10 @@ footer {
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
height: 100%;
min-height: 120px;
height: auto;
min-height: 180px;
max-height: 400px;
overflow-y: auto;
display: flex;
flex-direction: column;
opacity: 0.85;
@@ -2720,8 +2705,8 @@ footer {
/* Smart Prompting Hint */
.smart-prompting-hint {
height: 100%;
min-height: 120px;
height: 180px;
min-height: 180px;
display: flex;
align-items: center;
animation: hintFadeIn 0.3s ease-in-out;
@@ -3435,8 +3420,8 @@ footer {
.ai-suggestions-section {
flex: 0 0 auto;
width: 100%;
max-width: none;
height: auto;
min-height: 120px;
}
.ai-textarea-section {
@@ -3446,6 +3431,11 @@ footer {
min-height: 100px;
}
.ai-textarea-section textarea {
height: 150px;
min-height: 150px;
}
.ai-spotlight-content {
flex-direction: column;
gap: 0.75rem;
@@ -3587,6 +3577,16 @@ footer {
width: 14px;
height: 14px;
}
.tool-card {
min-height: 280px;
max-height: 320px;
}
.tool-tags-container {
max-height: 3rem;
flex: 0 0 3rem;
}
}
@media (width <= 640px) {
@@ -3746,133 +3746,30 @@ footer {
}
}
/* ===================================================================
MIGRATION UTILITIES - Additional classes for inline style migration
================================================================= */
/* Alignment utilities */
.align-middle { vertical-align: middle; }
/* Font style utilities */
.italic { font-style: italic; }
/* Border width utilities */
.border-2 { border-width: 2px; }
/* Border color utilities */
.border-accent { border-color: var(--color-accent); }
/* Card variants for complex backgrounds */
.card-warning {
background: linear-gradient(135deg, var(--color-warning) 0%, var(--color-accent) 100%);
color: white;
}
/* Header variants for complex gradient combinations */
.header-primary {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%);
color: white;
}
/* Ensure we have all text size variants */
.text-2xl { font-size: 1.5rem; }
/* Additional spacing utilities that might be missing */
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
.px-4 { padding-left: 1rem; padding-right: 1rem; }
.my-6 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
/* Flex utilities that might be missing */
.flex-1 { flex: 1; }
/* Additional rounded variants */
.rounded-xl { border-radius: 0.75rem; }
/* ===================================================================
23. MARKDOWN CONTENT
================================================================= */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 2rem;
margin-bottom: 1rem;
}
.markdown-content h1:first-child,
.markdown-content h2:first-child,
.markdown-content h3:first-child {
margin-top: 0;
}
.markdown-content p {
margin-bottom: 1rem;
}
.markdown-content ul,
.markdown-content ol {
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.markdown-content li {
margin-bottom: 0.5rem;
}
.markdown-content pre {
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1rem;
margin: 1.5rem 0;
overflow-x: auto;
font-size: 0.875rem;
}
.markdown-content code:not(pre code) {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.25rem;
padding: 0.125rem 0.375rem;
font-size: 0.875rem;
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background-color: var(--color-bg);
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid var(--color-border);
}
.markdown-content th,
.markdown-content td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
.markdown-content th {
background-color: var(--color-bg-secondary);
font-weight: 600;
}
.markdown-content blockquote {
border-left: 4px solid var(--color-primary);
background-color: var(--color-bg-secondary);
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-radius: 0 0.5rem 0.5rem 0;
}
.markdown-content hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2rem 0;
}

View File

@@ -0,0 +1,794 @@
/* ==========================================================================
0) FOUNDATIONS
- Typography, rhythm, utility tokens, base elements
========================================================================== */
:root {
/* Optional: gentle defaults if not already defined upstream */
--radius-xs: 0.25rem;
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
--shadow-xs: 0 1px 2px rgba(0,0,0,0.06);
--shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
--shadow-md: 0 6px 18px rgba(0,0,0,0.10);
--shadow-lg: 0 12px 30px rgba(0,0,0,0.16);
--ease-quick: 0.2s ease;
}
/* Base text container for articles */
:where(.markdown-content) {
max-width: 70ch;
margin: 0 auto;
font-size: 1.125rem;
line-height: 1.8;
color: var(--color-text);
}
:where(.markdown-content) * {
scroll-margin-top: 100px;
}
/* ==========================================================================
1) HEADINGS & TEXT
========================================================================== */
:where(.markdown-content) h1,
:where(.markdown-content) h2,
:where(.markdown-content) h3,
:where(.markdown-content) h4,
:where(.markdown-content) h5,
:where(.markdown-content) h6 {
font-weight: 700;
line-height: 1.25;
color: var(--color-text);
margin: 0;
}
:where(.markdown-content) h1 {
font-size: 2.5rem;
margin: 3rem 0 1.5rem 0;
border-bottom: 3px solid var(--color-primary);
padding-bottom: 0.5rem;
}
:where(.markdown-content) h2 {
font-size: 2rem;
margin: 2.5rem 0 1rem 0;
border-bottom: 2px solid var(--color-border);
padding-bottom: 0.375rem;
}
:where(.markdown-content) h3 {
font-size: 1.5rem;
margin: 2rem 0 0.75rem 0;
color: var(--color-primary);
}
:where(.markdown-content) h4 {
font-size: 1.25rem;
margin: 1.5rem 0 0.5rem 0;
}
:where(.markdown-content) h5,
:where(.markdown-content) h6 {
font-size: 1.125rem;
margin: 1rem 0 0.5rem 0;
font-weight: 600;
}
:where(.markdown-content) p {
margin: 0 0 1.5rem 0;
text-align: justify;
hyphens: auto;
}
:where(.markdown-content) strong { font-weight: 700; color: var(--color-text); }
:where(.markdown-content) em { font-style: italic; color: var(--color-text-secondary); }
/* Links with refined hover state */
:where(.markdown-content) a {
color: var(--color-primary);
text-decoration: underline;
text-decoration-color: transparent;
transition: var(--ease-quick);
}
:where(.markdown-content) a:hover {
text-decoration-color: var(--color-primary);
background-color: var(--color-bg-secondary);
padding: 0.125rem 0.25rem;
border-radius: var(--radius-xs);
}
/* External link indicator */
:where(.markdown-content) a[href^="http"]:not([href*="localhost"]):not([href*="127.0.0.1"])::after {
content: " ↗";
font-size: 0.875rem;
color: var(--color-text-secondary);
}
/* ==========================================================================
2) LISTS, DEFINITIONS, TABLES, QUOTES
========================================================================== */
:where(.markdown-content) ul,
:where(.markdown-content) ol {
margin: 1.5rem 0;
padding-left: 2rem;
}
:where(.markdown-content) li {
margin-bottom: 0.75rem;
line-height: 1.2;
}
:where(.markdown-content) li > ul,
:where(.markdown-content) li > ol {
margin: 0.5rem 0;
}
:where(.markdown-content) dl { margin: 1.5rem 0; }
:where(.markdown-content) dt {
font-weight: 700;
margin: 1rem 0 0.5rem 0;
color: var(--color-primary);
}
:where(.markdown-content) dd {
margin-left: 1.5rem;
margin-bottom: 0.75rem;
padding-left: 1rem;
border-left: 2px solid var(--color-border);
}
:where(.markdown-content) table {
width: 100%;
border-collapse: collapse;
margin: 1.5rem 0;
background-color: var(--color-bg);
border-radius: var(--radius-md);
overflow: hidden;
border: 1px solid var(--color-border);
}
:where(.markdown-content) th,
:where(.markdown-content) td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
:where(.markdown-content) th {
background-color: var(--color-bg-secondary);
font-weight: 600;
}
:where(.markdown-content) blockquote {
border-left: 4px solid var(--color-primary);
background-color: var(--color-bg-secondary);
padding: 1rem 1.5rem;
margin: 1.5rem 0;
border-radius: 0 var(--radius-md) var(--radius-md) 0;
}
:where(.markdown-content) hr {
border: none;
border-top: 1px solid var(--color-border);
margin: 2rem 0;
}
/* ==========================================================================
3) CODE & INLINE CODE
========================================================================== */
:where(.markdown-content) pre {
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 1.5rem;
margin: 2rem 0;
overflow-x: auto;
font-size: 0.875rem;
line-height: 1.6;
position: relative;
box-shadow: var(--shadow-sm);
padding-left: 1.5rem;
}
:where(.markdown-content) pre::before {
content: attr(data-language);
position: absolute;
top: 0.5rem;
left: 0.75rem;
right: auto;
z-index: 1;
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
background: var(--color-bg);
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
font-size: 0.75rem;
}
:where(.markdown-content) pre[data-language]::before {
top: 0.5rem;
left: 0.75rem;
position: absolute;
z-index: 1;
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: var(--radius-sm);
background: var(--color-bg);
border: 1px solid var(--color-border);
color: var(--color-text-secondary);
line-height: 1;
}
:where(.markdown-content) code:not(pre code) {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
font-family: 'SF Mono','Monaco','Menlo','Consolas',monospace;
color: var(--color-text);
}
/* Language badges (keep if your renderer sets data-language) */
:where(.markdown-content) pre[data-language="bash"]::before { content: "BASH"; color: #4EAA25; }
:where(.markdown-content) pre[data-language="javascript"]::before { content: "JS"; color: #F7DF1E; }
:where(.markdown-content) pre[data-language="python"]::before { content: "PYTHON"; color: #3776AB; }
:where(.markdown-content) pre[data-language="sql"]::before { content: "SQL"; color: #336791; }
:where(.markdown-content) pre[data-language="yaml"]::before { content: "YAML"; color: #CB171E; }
:where(.markdown-content) pre[data-language="json"]::before { content: "JSON"; color: #000000; }
/* Keyboard UI hints */
:where(.markdown-content) kbd {
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: var(--radius-xs);
padding: 0.125rem 0.375rem;
font-size: 0.75rem;
font-family: monospace;
box-shadow: var(--shadow-xs);
margin: 0 0.125rem;
}
:where(.markdown-content) .copy-btn {
position: absolute;
top: 0.5rem; right: 0.5rem;
display: inline-flex; align-items: center; gap: 0.4rem;
padding: 0.35rem 0.6rem;
border: 1px solid var(--color-border);
background: var(--color-bg);
border-radius: var(--radius-sm);
font-size: 0.75rem;
cursor: pointer;
box-shadow: var(--shadow-sm);
line-height: 1;
z-index: 2;
transition: var(--transition-fast);
color: var(--color-text);
}
:where(.markdown-content) .copy-btn:hover {
background: var(--color-bg-secondary);
}
:where(.markdown-content) .copy-btn:focus-visible {
outline: 2px solid color-mix(in srgb, var(--color-primary) 60%, transparent);
outline-offset: 2px;
}
:where(.markdown-content) .copy-btn svg {
width: 14px; height: 14px;
}
[data-theme="dark"] :where(.markdown-content) .copy-btn {
background: var(--color-bg-secondary);
border-color: color-mix(in srgb, var(--color-border) 80%, transparent);
box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-bg-tertiary) 40%, transparent), var(--shadow-sm);
}
:where(.markdown-content) pre .copied-pill {
position: absolute;
top: 0.5rem; right: 0.5rem;
padding: 0.35rem 0.6rem;
border-radius: var(--radius-sm);
font-size: 0.75rem;
font-weight: 600;
background: var(--color-primary);
color: #fff;
opacity: 0; transform: translateY(-6px) scale(0.98);
pointer-events: none;
z-index: 2;
}
/* Success state driven by [data-copied="true"] on <pre> */
:where(.markdown-content) pre[data-copied="true"] {
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-primary) 30%, transparent) inset, var(--shadow-sm);
}
:where(.markdown-content) pre[data-copied="true"] .copy-btn { opacity: 0; }
:where(.markdown-content) pre[data-copied="true"] .copied-pill {
opacity: 1; transform: translateY(0) scale(1);
}
/* Smooth but gentle motion */
@media (prefers-reduced-motion: no-preference) {
:where(.markdown-content) pre,
:where(.markdown-content) .copy-btn,
:where(.markdown-content) pre .copied-pill {
transition: 180ms ease;
}
}
/* ==========================================================================
4) CALLOUTS, STEPS, HIGHLIGHT BOX
========================================================================== */
:where(.markdown-content) .callout {
border-left: 4px solid;
border-radius: 0.5rem;
margin: 2rem 0;
padding: 1.25rem 1.5rem;
background-color: var(--color-bg-secondary);
border-color: var(--color-border);
box-shadow: var(--shadow-sm);
}
:where(.markdown-content) .callout-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
font-weight: 600;
font-size: 0.9375rem;
}
:where(.markdown-content) .callout-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
}
/* Callout variants */
:where(.markdown-content) .callout-note {
border-left-color: var(--color-primary);
background-color: color-mix(in srgb, var(--color-primary) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-note .callout-header { color: var(--color-primary); }
:where(.markdown-content) .callout-tip {
border-left-color: var(--color-accent);
background-color: color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-tip .callout-header { color: var(--color-accent); }
:where(.markdown-content) .callout-warning {
border-left-color: var(--color-warning);
background-color: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-warning .callout-header { color: var(--color-warning); }
:where(.markdown-content) .callout-danger {
border-left-color: var(--color-error);
background-color: color-mix(in srgb, var(--color-error) 8%, var(--color-bg-secondary));
}
:where(.markdown-content) .callout-danger .callout-header { color: var(--color-error); }
/* Highlight feature box */
:where(.markdown-content) .highlight-box {
background: linear-gradient(135deg, var(--color-accent) 0%, var(--color-primary) 100%);
color: white;
padding: 2rem;
border-radius: 1rem;
margin: 2rem 0;
text-align: center;
box-shadow: var(--shadow-md);
}
:where(.markdown-content) .highlight-box h3 { margin: 0; color: white; }
/* Steps */
:where(.markdown-content) .steps { counter-reset: step-counter; margin: 2rem 0; }
:where(.markdown-content) .step {
counter-increment: step-counter;
margin: 1.25rem 0;
padding: 1.25rem 1.5rem;
background-color: var(--color-bg-secondary);
border-radius: 0.75rem;
border-left: 4px solid var(--color-primary);
position: relative;
}
:where(.markdown-content) .step::before {
content: counter(step-counter);
position: absolute;
left: -0.75rem;
top: 1rem;
background-color: var(--color-primary);
color: white;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: grid;
place-items: center;
font-weight: 700;
font-size: 0.875rem;
}
:where(.markdown-content) .step h4 { margin-top: 0; color: var(--color-primary); }
/* ==========================================================================
5) ARTICLE LAYOUT (Header, Meta, Sidebar, Footer)
========================================================================== */
.article-layout {
max-width: 1400px;
margin: 0 auto;
padding: 0 1rem;
}
/* Header */
.article-header {
background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%);
border: 1px solid var(--color-border);
border-radius: 1rem;
margin-bottom: 2rem;
overflow: hidden;
}
.article-header-content { padding: 2rem; }
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.breadcrumb-link {
color: var(--color-primary);
text-decoration: none;
transition: var(--ease-quick);
}
.breadcrumb-link:hover { text-decoration: underline; }
.breadcrumb-separator { color: var(--color-text-secondary); }
.breadcrumb-current { color: var(--color-text-secondary); font-weight: 500; }
.article-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.article-tag {
padding: 0.25rem 0.75rem;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 1rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--color-text-secondary);
}
.article-tag-category {
background-color: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.article-title-section { margin-bottom: 2rem; }
.article-title {
font-size: 2.5rem;
font-weight: 700;
margin: 0 0 1rem 0;
color: var(--color-primary);
line-height: 1.2;
}
.article-icon { margin-right: 1rem; font-size: 2rem; }
.article-description { font-size: 1.25rem; color: var(--color-text-secondary); margin: 0; line-height: 1.5; }
.article-metadata-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
padding: 1.5rem;
background-color: var(--color-bg);
border-radius: 0.75rem;
border: 1px solid var(--color-border);
}
.metadata-item { display: flex; flex-direction: column; gap: 0.5rem; }
.metadata-label {
font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.05em; color: var(--color-text-secondary);
}
.metadata-value { font-size: 0.9375rem; font-weight: 500; color: var(--color-text); }
.metadata-badges { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.article-actions { display: flex; gap: 1rem; justify-content: center; }
/* Content wrapper with sidebar */
.article-content-wrapper {
display: grid;
grid-template-columns: var(--main-grid-columns, 280px 1fr);
gap: 3rem;
align-items: start;
}
.article-sidebar {
position: sticky;
top: 0.1rem;
max-height: 100vh;
overflow-y: auto;
}
/* Sidebar TOC */
.sidebar-toc {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 1.5rem;
}
.toc-title {
margin: 0 0 1rem 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-primary);
}
.toc-navigation { display: flex; flex-direction: column; gap: 0.25rem; }
.toc-item {
display: block;
padding: 0.5rem 0.75rem;
color: var(--color-text);
text-decoration: none;
border-radius: var(--radius-sm);
font-size: 0.875rem;
line-height: 1.4;
transition: var(--ease-quick);
border-left: 3px solid transparent;
}
.toc-item:hover {
background-color: var(--color-bg-tertiary);
color: var(--color-primary);
border-left-color: var(--color-primary);
}
.toc-item.active {
background-color: var(--color-primary);
color: white;
border-left-color: var(--color-accent);
}
/* Indentation by level (JS sets .toc-level-X) */
.toc-level-1 { padding-left: 0.75rem; font-weight: 600; }
.toc-level-2 { padding-left: 1rem; font-weight: 500; }
.toc-level-3 { padding-left: 1.5rem; }
.toc-level-4 { padding-left: 2rem; font-size: 0.8125rem; }
.toc-level-5 { padding-left: 2.5rem; font-size: 0.8125rem; opacity: 0.85; }
.toc-level-6 { padding-left: 3rem; font-size: 0.8125rem; opacity: 0.8; }
/* Main article column */
.article-main {
min-width: 0;
max-width: 95ch;
background-color: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 1rem;
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.article-content {
padding: 2.5rem;
margin-bottom: 0;
background: linear-gradient(135deg, var(--color-bg) 0%, var(--color-bg-secondary) 100%);
}
.article-footer {
background-color: var(--color-bg-secondary);
border-top: 1px solid var(--color-border);
padding: 2rem 2.5rem;
margin-top: 0;
}
.article-footer h3 {
margin: 0 0 1.5rem 0;
color: var(--color-primary);
font-size: 1.25rem;
font-weight: 600;
}
.footer-actions-grid {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 2rem;
}
.footer-actions-grid .btn {
flex: 1;
min-width: 200px;
justify-content: center;
}
/* Tool button variants (keep since template uses them) */
.btn-concept { background-color: var(--color-concept); color: white; border-color: var(--color-concept); }
.btn-concept:hover { opacity: 0.9; }
.btn-method { background-color: var(--color-method); color: white; border-color: var(--color-method); }
.btn-method:hover { opacity: 0.9; }
.related-tools { margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--color-border); }
.related-tools h3 { margin: 0 0 1rem 0; color: var(--color-text); }
.related-tools-grid {
display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem;
}
.related-tool-card {
display: flex; align-items: center; gap: 0.75rem; padding: 1rem;
background-color: var(--color-bg-secondary); border: 1px solid var(--color-border);
border-radius: 0.5rem; color: var(--color-text); text-decoration: none;
transition: var(--ease-quick);
}
.related-tool-card:hover {
background-color: var(--color-bg-tertiary); border-color: var(--color-primary);
transform: translateY(-1px); box-shadow: var(--shadow-sm);
}
.tool-icon { font-size: 1.25rem; flex-shrink: 0; }
.tool-name { font-weight: 500; font-size: 0.9375rem; }
/* ==========================================================================
6) RESPONSIVE
========================================================================== */
@media (max-width: 1200px) {
.article-content-wrapper { grid-template-columns: 240px 1fr; gap: 2rem; }
}
@media (max-width: 1024px) {
.article-content-wrapper { grid-template-columns: 1fr; gap: 0; }
.article-sidebar { position: static; max-height: none; margin-bottom: 2rem; }
.sidebar-toc { max-width: 500px; margin: 0 auto; }
.article-main { max-width: 100%; }
}
@media (max-width: 768px) {
.article-layout { padding: 0 0.5rem; }
.article-header-content { padding: 1.5rem; }
.article-title { font-size: 2rem; }
.article-description { font-size: 1.125rem; }
.article-metadata-grid { grid-template-columns: 1fr; gap: 1rem; padding: 1rem; }
.article-actions { flex-direction: column; }
.footer-actions-grid { flex-direction: column; }
.footer-actions-grid .btn { min-width: auto; }
.article-content { padding: 1.5rem; }
.article-footer { padding: 1.5rem; }
.footer-actions-grid { flex-direction: column; }
.footer-actions-grid .btn { min-width: auto; }
:where(.markdown-content) {
font-size: 1rem;
line-height: 1.65;
max-width: 100%;
}
:where(.markdown-content) pre { padding: 1rem; margin: 1.5rem 0; font-size: 0.8125rem; }
:where(.markdown-content) .callout { padding: 1rem; margin: 1.5rem 0; }
:where(.markdown-content) .step { padding: 1rem; margin: 1rem 0; }
:where(.markdown-content) .step::before { left: -0.5rem; top: 0.75rem; }
}
@media (max-width: 480px) {
.article-title { font-size: 1.75rem; }
.article-icon { font-size: 1.5rem; margin-right: 0.75rem; }
.breadcrumb { font-size: 0.8125rem; }
}
/* ==========================================================================
7) PRINT
========================================================================== */
@media print {
/* Hide interactive chrome not used in print */
.article-sidebar { display: none !important; }
.article-actions,
.article-footer .footer-actions-grid { display: none !important; }
/* Expand content */
.article-main { max-width: 100% !important; }
}
/* ==========================================================================
VIDEO EMBEDDING - ULTRA SIMPLE: Just full width, natural aspect ratios
========================================================================== */
/* Video Container - just a styled wrapper */
:where(.markdown-content) .video-container {
width: 100%;
margin: 2rem 0;
border-radius: var(--radius-lg, 0.75rem);
overflow: hidden;
background-color: var(--color-bg-tertiary, #000);
box-shadow: var(--shadow-lg, 0 12px 30px rgba(0,0,0,0.16));
}
/* Video Element - full width, natural aspect ratio */
:where(.markdown-content) .video-container video {
width: 100%;
height: auto;
display: block;
background-color: #000;
border: none;
outline: none;
}
/* YouTube iframe - full width, preserve embedded dimensions ratio */
:where(.markdown-content) .video-container iframe {
width: 100%;
height: auto;
aspect-ratio: 16 / 9; /* Only for iframes since they don't have intrinsic ratio */
display: block;
border: none;
outline: none;
}
/* Focus states for accessibility */
:where(.markdown-content) .video-container video:focus,
:where(.markdown-content) .video-container iframe:focus {
outline: 3px solid var(--color-primary);
outline-offset: 3px;
}
/* Video Metadata */
:where(.markdown-content) .video-metadata {
background-color: var(--color-bg-secondary);
border: 1px solid var(--color-border);
border-top: none;
padding: 1rem 1.5rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
border-radius: 0 0 var(--radius-lg, 0.75rem) var(--radius-lg, 0.75rem);
}
:where(.markdown-content) .video-metadata .video-title {
font-weight: 600;
color: var(--color-text);
margin: 0;
}
/* Responsive Design */
@media (max-width: 768px) {
:where(.markdown-content) .video-container {
margin: 1.5rem -0.5rem;
border-radius: 0;
}
:where(.markdown-content) .video-metadata {
padding: 0.75rem 1rem;
font-size: 0.8125rem;
border-radius: 0;
}
}
/* Dark Theme */
[data-theme="dark"] :where(.markdown-content) .video-container {
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
}
[data-theme="dark"] :where(.markdown-content) .video-metadata {
background-color: var(--color-bg-tertiary);
border-color: color-mix(in srgb, var(--color-border) 60%, transparent);
}
/* Print Media */
@media print {
:where(.markdown-content) .video-container {
border: 2px solid #ddd;
background-color: #f5f5f5;
padding: 2rem;
text-align: center;
}
:where(.markdown-content) .video-container video,
:where(.markdown-content) .video-container iframe {
display: none !important;
}
:where(.markdown-content) .video-container::before {
content: "📹 Video: " attr(data-video-title, "Embedded Video");
display: block;
font-weight: 600;
}
}

72
src/styles/palette.css Normal file
View File

@@ -0,0 +1,72 @@
/* PALETTE OPTION 1: BLUEPRINT & AMBER */
:root {
/* Light Theme */
--color-bg: #ffffff;
--color-bg-secondary: #f1f5f9; /* Slate 100 */
--color-bg-tertiary: #e2e8f0; /* Slate 200 */
--color-text: #0f172a; /* Slate 900 */
--color-text-secondary: #475569; /* Slate 600 */
--color-border: #cbd5e1; /* Slate 300 */
--color-primary: #334155; /* Slate 700 - A strong, serious primary */
--color-primary-hover: #1e293b; /* Slate 800 */
--color-accent: #b45309; /* A sharp, focused amber for highlights */
--color-accent-hover: #92400e;
--color-warning: #d97706;
--color-error: #be123c; /* A deeper, more serious red */
/* Card/Tag Category Colors */
--color-hosted: #4f46e5; /* Indigo */
--color-hosted-bg: #eef2ff;
--color-oss: #0d9488; /* Teal */
--color-oss-bg: #f0fdfa;
--color-method: #0891b2; /* Cyan */
--color-method-bg: #ecfeff;
--color-concept: #c2410c; /* Orange */
--color-concept-bg: #fff7ed;
/* Shadows (Crisper) */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 6%);
--shadow-md: 0 3px 5px -1px rgb(0 0 0 / 8%);
--shadow-lg: 0 8px 12px -3px rgb(0 0 0 / 10%);
/* Transitions */
--transition-fast: all 0.2s ease;
--transition-medium: all 0.3s ease;
}
[data-theme="dark"] {
/* Dark Theme */
--color-bg: #0f172a; /* Slate 900 */
--color-bg-secondary: #1e293b; /* Slate 800 */
--color-bg-tertiary: #334155; /* Slate 700 */
--color-text: #f1f5f9; /* Slate 100 */
--color-text-secondary: #94a3b8; /* Slate 400 */
--color-border: #475569; /* Slate 600 */
--color-primary: #64748b; /* Slate 500 */
--color-primary-hover: #94a3b8; /* Slate 400 */
--color-accent: #f59e0b; /* A brighter amber for dark mode contrast */
--color-accent-hover: #fbbf24;
--color-warning: #f59e0b;
--color-error: #f43f5e;
/* Card/Tag Category Colors */
--color-hosted: #818cf8; /* Indigo */
--color-hosted-bg: #3730a3;
--color-oss: #2dd4bf; /* Teal */
--color-oss-bg: #115e59;
--color-method: #22d3ee; /* Cyan */
--color-method-bg: #164e63;
--color-concept: #fb923c; /* Orange */
--color-concept-bg: #7c2d12;
/* Shadows (Subtler for dark mode) */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 20%);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 30%);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 40%);
}

File diff suppressed because it is too large Load Diff

137
src/utils/aiService.ts Normal file
View File

@@ -0,0 +1,137 @@
// src/utils/aiService.ts
import 'dotenv/config';
export interface AIServiceConfig {
endpoint: string;
apiKey: string;
model: string;
}
export interface AICallOptions {
temperature?: number;
timeout?: number;
}
export interface AIResponse {
content: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
class AIService {
private config: AIServiceConfig;
private defaultOptions: AICallOptions;
constructor() {
this.config = {
endpoint: this.getRequiredEnv('AI_ANALYZER_ENDPOINT'),
apiKey: this.getRequiredEnv('AI_ANALYZER_API_KEY'),
model: this.getRequiredEnv('AI_ANALYZER_MODEL')
};
this.defaultOptions = {
temperature: 0.3,
timeout: 60000
};
console.log('[AI-SERVICE] Initialized with model:', this.config.model);
}
private getRequiredEnv(key: string): string {
const value = process.env[key];
if (!value) {
throw new Error(`Missing required environment variable: ${key}`);
}
return value;
}
async callAI(prompt: string, options: AICallOptions = {}): Promise<AIResponse> {
const mergedOptions = { ...this.defaultOptions, ...options };
console.log('[AI-SERVICE] Making API call:', {
promptLength: prompt.length,
temperature: mergedOptions.temperature
});
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (this.config.apiKey) {
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
}
const requestBody = {
model: this.config.model,
messages: [{ role: 'user', content: prompt }],
temperature: mergedOptions.temperature
};
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), mergedOptions.timeout);
const response = await fetch(`${this.config.endpoint}/v1/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(requestBody),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
console.error('[AI-SERVICE] API Error:', response.status, errorText);
throw new Error(`AI API error: ${response.status} - ${errorText}`);
}
const data = await response.json();
const content = data.choices?.[0]?.message?.content;
if (!content) {
console.error('[AI-SERVICE] No response content from AI model');
throw new Error('No response from AI model');
}
console.log('[AI-SERVICE] API call successful:', {
responseLength: content.length,
usage: data.usage
});
return {
content: content.trim(),
usage: data.usage
};
} catch (error) {
if (error.name === 'AbortError') {
console.error('[AI-SERVICE] Request timeout');
throw new Error('AI request timeout');
}
console.error('[AI-SERVICE] API call failed:', error.message);
throw error;
}
}
async callMicroTaskAI(prompt: string): Promise<AIResponse> {
return this.callAI(prompt, {
temperature: 0.3,
timeout: 30000
});
}
estimateTokens(text: string): number {
return Math.ceil(text.length / 4);
}
getConfig(): AIServiceConfig {
return { ...this.config };
}
}
export const aiService = new AIService();

View File

@@ -83,26 +83,21 @@ export const apiServerError = {
};
export const apiSpecial = {
// JSON parsing error
invalidJSON: (): Response =>
apiError.badRequest('Invalid JSON in request body'),
// Missing required fields
missingRequired: (fields: string[]): Response =>
apiError.badRequest(`Missing required fields: ${fields.join(', ')}`),
// Empty request body
emptyBody: (): Response =>
apiError.badRequest('Request body cannot be empty'),
// File upload responses
uploadSuccess: (data: { url: string; filename: string; size: number; storage: string }): Response =>
apiResponse.created(data),
uploadFailed: (error: string): Response =>
apiServerError.internal(`Upload failed: ${error}`),
// Contribution responses
contributionSuccess: (data: { prUrl?: string; branchName?: string; message: string }): Response =>
apiResponse.created({ success: true, ...data }),
@@ -114,7 +109,6 @@ export const apiWithHeaders = {
successWithHeaders: (data: any, headers: Record<string, string>): Response =>
createAPIResponse(data, 200, headers),
// Redirect response
redirect: (location: string, temporary: boolean = true): Response =>
new Response(null, {
status: temporary ? 302 : 301,

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// src/utils/auth.js (FIXED - Cleaned up and enhanced debugging)
// src/utils/auth.js
import type { AstroGlobal } from 'astro';
import crypto from 'crypto';
import { config } from 'dotenv';
@@ -27,7 +27,7 @@ export interface AuthContext {
userId: string;
}
export type AuthContextType = 'contributions' | 'ai' | 'general';
export type AuthContextType = 'contributions' | 'ai' | 'general' | 'gatedcontent';
export interface UserInfo {
sub?: string;
@@ -52,22 +52,17 @@ function getEnv(key: string): string {
export function getSessionFromRequest(request: Request): string | null {
const cookieHeader = request.headers.get('cookie');
console.log('[DEBUG] Cookie header:', cookieHeader ? 'present' : 'missing');
if (!cookieHeader) return null;
const cookies = parseCookie(cookieHeader);
console.log('[DEBUG] Parsed cookies:', Object.keys(cookies));
console.log('[DEBUG] Session cookie found:', !!cookies.session);
return cookies.session || null;
}
export async function verifySession(sessionToken: string): Promise<SessionData | null> {
try {
console.log('[DEBUG] Verifying session token, length:', sessionToken.length);
const { payload } = await jwtVerify(sessionToken, SECRET_KEY);
console.log('[DEBUG] JWT verification successful, payload keys:', Object.keys(payload));
if (
typeof payload.userId === 'string' &&
@@ -75,7 +70,6 @@ export async function verifySession(sessionToken: string): Promise<SessionData |
typeof payload.authenticated === 'boolean' &&
typeof payload.exp === 'number'
) {
console.log('[DEBUG] Session validation successful for user:', payload.userId);
return {
userId: payload.userId,
email: payload.email,
@@ -84,17 +78,14 @@ export async function verifySession(sessionToken: string): Promise<SessionData |
};
}
console.log('[DEBUG] Session payload validation failed, payload:', payload);
return null;
} catch (error) {
console.log('[DEBUG] Session verification failed:', error.message);
return null;
}
}
export async function createSession(userId: string, email: string): Promise<string> {
const exp = Math.floor(Date.now() / 1000) + SESSION_DURATION;
console.log('[DEBUG] Creating session for user:', userId, 'exp:', exp);
const token = await new SignJWT({
userId,
@@ -106,7 +97,6 @@ export async function createSession(userId: string, email: string): Promise<stri
.setExpirationTime(exp)
.sign(SECRET_KEY);
console.log('[DEBUG] Session token created, length:', token.length);
return token;
}
@@ -123,7 +113,6 @@ export function createSessionCookie(sessionToken: string): string {
path: '/'
});
console.log('[DEBUG] Created session cookie:', cookie.substring(0, 100) + '...');
return cookie;
}
@@ -260,8 +249,8 @@ function getAuthRequirement(context: AuthContextType): boolean {
return process.env.AUTHENTICATION_NECESSARY_CONTRIBUTIONS !== 'false';
case 'ai':
return process.env.AUTHENTICATION_NECESSARY_AI !== 'false';
case 'general':
return process.env.AUTHENTICATION_NECESSARY !== 'false';
case 'gatedcontent':
return process.env.AUTHENTICATION_NECESSARY_GATEDCONTENT !== 'false';
default:
return true;
}
@@ -292,8 +281,6 @@ export async function createSessionWithCookie(userInfo: UserInfo): Promise<{
export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'general'): Promise<AuthContext | Response> {
const authRequired = getAuthRequirement(context);
console.log(`[DEBUG PAGE] Auth required for ${context}:`, authRequired);
console.log('[DEBUG PAGE] Request URL:', Astro.url.toString());
if (!authRequired) {
return {
@@ -305,10 +292,8 @@ export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'g
}
const sessionToken = getSessionFromRequest(Astro.request);
console.log('[DEBUG PAGE] Session token found:', !!sessionToken);
if (!sessionToken) {
console.log('[DEBUG PAGE] No session token, redirecting to login');
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`;
return new Response(null, {
status: 302,
@@ -317,10 +302,8 @@ export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'g
}
const session = await verifySession(sessionToken);
console.log('[DEBUG PAGE] Session verification result:', !!session);
if (!session) {
console.log('[DEBUG PAGE] Session verification failed, redirecting to login');
const loginUrl = `/api/auth/login?returnTo=${encodeURIComponent(Astro.url.toString())}`;
return new Response(null, {
status: 302,
@@ -328,7 +311,6 @@ export async function withAuth(Astro: AstroGlobal, context: AuthContextType = 'g
});
}
console.log(`[DEBUG PAGE] Page authentication successful for ${context}:`, session.userId);
return {
authenticated: true,
session,
@@ -354,10 +336,8 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
}
const sessionToken = getSessionFromRequest(request);
console.log(`[DEBUG API] Session token found for ${context}:`, !!sessionToken);
if (!sessionToken) {
console.log(`[DEBUG API] No session token found for ${context}`);
return {
authenticated: false,
userId: '',
@@ -366,10 +346,8 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
}
const session = await verifySession(sessionToken);
console.log(`[DEBUG API] Session verification result for ${context}:`, !!session);
if (!session) {
console.log(`[DEBUG API] Session verification failed for ${context}`);
return {
authenticated: false,
userId: '',
@@ -377,7 +355,6 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
};
}
console.log(`[DEBUG API] Authentication successful for ${context}:`, session.userId);
return {
authenticated: true,
userId: session.userId,
@@ -389,3 +366,11 @@ export async function withAPIAuth(request: Request, context: AuthContextType = '
export function getAuthRequirementForContext(context: AuthContextType): boolean {
return getAuthRequirement(context);
}
export function isGatedContentAuthRequired(): boolean {
return getAuthRequirement('gatedcontent');
}
export function shouldGateContent(isGatedContent: boolean): boolean {
return isGatedContent && isGatedContentAuthRequired();
}

428
src/utils/clientUtils.ts Normal file
View File

@@ -0,0 +1,428 @@
// src/utils/clientUtils.ts
export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') {
console.warn('[CLIENT-UTILS] Invalid toolName provided to createToolSlug:', toolName);
return '';
}
return toolName.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
export function findToolByIdentifier(tools: any[], identifier: string): any | undefined {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find((tool: any) =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: any): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}
export function sanitizeText(text: string): string {
if (typeof text !== 'string') return '';
return text
.replace(/^#{1,6}\s+/gm, '')
.replace(/^\s*[-*+]\s+/gm, '')
.replace(/^\s*\d+\.\s+/gm, '')
.replace(/\*\*(.+?)\*\*/g, '$1')
.replace(/\*(.+?)\*/g, '$1')
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
.replace(/```[\s\S]*?```/g, '[CODE BLOCK]')
.replace(/`([^`]+)`/g, '$1')
.replace(/<[^>]+>/g, '')
.replace(/\n\s*\n\s*\n/g, '\n\n')
.trim();
}
export function escapeHtml(text: string): string {
if (typeof text !== 'string') return String(text);
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
export function truncateText(text: string, maxLength: number): string {
if (!text || text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
export function summarizeData(data: any): string {
if (data === null || data === undefined) return 'null';
if (typeof data === 'string') {
return data.length > 100 ? data.slice(0, 100) + '...' : data;
}
if (typeof data === 'number' || typeof data === 'boolean') {
return data.toString();
}
if (Array.isArray(data)) {
if (data.length === 0) return '[]';
if (data.length <= 3) return JSON.stringify(data);
return `[${data.slice(0, 3).map(i => typeof i === 'string' ? i : JSON.stringify(i)).join(', ')}, ...+${data.length - 3}]`;
}
if (typeof data === 'object') {
const keys = Object.keys(data);
if (keys.length === 0) return '{}';
if (keys.length <= 3) {
return '{' + keys.map(k => `${k}: ${typeof data[k] === 'string' ? data[k].slice(0, 20) + (data[k].length > 20 ? '...' : '') : JSON.stringify(data[k])}`).join(', ') + '}';
}
return `{${keys.slice(0, 3).join(', ')}, ...+${keys.length - 3} keys}`;
}
return String(data);
}
export function formatDuration(ms: number): string {
if (ms < 1000) return '< 1s';
if (ms < 60000) return `${Math.ceil(ms / 1000)}s`;
const minutes = Math.floor(ms / 60000);
const seconds = Math.ceil((ms % 60000) / 1000);
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
export function showElement(element: HTMLElement | null): void {
if (element) {
element.style.display = 'block';
element.classList.remove('hidden');
}
}
export function hideElement(element: HTMLElement | null): void {
if (element) {
element.style.display = 'none';
element.classList.add('hidden');
}
}
interface AutocompleteOptions {
minLength?: number;
maxResults?: number;
placeholder?: string;
allowMultiple?: boolean;
separator?: string;
filterFunction?: (query: string) => any[];
renderFunction?: (item: any) => string;
hiddenInput?: HTMLInputElement;
}
export class AutocompleteManager {
public input: HTMLInputElement;
public dataSource: any[];
public options: AutocompleteOptions;
public isOpen: boolean = false;
public selectedIndex: number = -1;
public filteredData: any[] = [];
public selectedItems: Set<string> = new Set();
public dropdown!: HTMLElement;
public selectedContainer!: HTMLElement;
constructor(inputElement: HTMLInputElement, dataSource: any[], options: AutocompleteOptions = {}) {
this.input = inputElement;
this.dataSource = dataSource;
this.options = {
minLength: 1,
maxResults: 10,
placeholder: 'Type to search...',
allowMultiple: false,
separator: ', ',
filterFunction: this.defaultFilter.bind(this),
renderFunction: this.defaultRender.bind(this),
...options
};
this.init();
}
init(): void {
this.createDropdown();
this.bindEvents();
if (this.options.allowMultiple) {
this.initMultipleMode();
}
}
createDropdown(): void {
this.dropdown = document.createElement('div');
this.dropdown.className = 'autocomplete-dropdown';
this.dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: 0.375rem;
box-shadow: var(--shadow-lg);
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
`;
const parentElement = this.input.parentNode as HTMLElement;
parentElement.style.position = 'relative';
parentElement.insertBefore(this.dropdown, this.input.nextSibling);
}
bindEvents(): void {
this.input.addEventListener('input', (e) => {
this.handleInput((e.target as HTMLInputElement).value);
});
this.input.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
this.input.addEventListener('focus', () => {
if (this.input.value.length >= (this.options.minLength || 1)) {
this.showDropdown();
}
});
this.input.addEventListener('blur', () => {
setTimeout(() => {
const activeElement = document.activeElement;
if (!activeElement || !this.dropdown.contains(activeElement)) {
this.hideDropdown();
}
}, 150);
});
document.addEventListener('click', (e) => {
const target = e.target as Node;
if (!this.input.contains(target) && !this.dropdown.contains(target)) {
this.hideDropdown();
}
});
}
initMultipleMode(): void {
this.selectedContainer = document.createElement('div');
this.selectedContainer.className = 'autocomplete-selected';
this.selectedContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 0.5rem;
min-height: 1.5rem;
`;
const parentElement = this.input.parentNode as HTMLElement;
parentElement.insertBefore(this.selectedContainer, this.input);
this.updateSelectedDisplay();
}
handleInput(value: string): void {
if (value.length >= (this.options.minLength || 1)) {
this.filteredData = this.options.filterFunction!(value);
this.selectedIndex = -1;
this.renderDropdown();
this.showDropdown();
} else {
this.hideDropdown();
}
}
handleKeydown(e: KeyboardEvent): void {
if (!this.isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.filteredData.length - 1);
this.updateHighlight();
break;
case 'ArrowUp':
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, -1);
this.updateHighlight();
break;
case 'Enter':
e.preventDefault();
if (this.selectedIndex >= 0) {
this.selectItem(this.filteredData[this.selectedIndex]);
}
break;
case 'Escape':
this.hideDropdown();
break;
}
}
defaultFilter(query: string): any[] {
const searchTerm = query.toLowerCase();
return this.dataSource
.filter(item => {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return text.toLowerCase().includes(searchTerm) &&
(!this.options.allowMultiple || !this.selectedItems.has(text));
})
.slice(0, this.options.maxResults || 10);
}
defaultRender(item: any): string {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
return `<div class="autocomplete-item">${escapeHtml(text)}</div>`;
}
renderDropdown(): void {
if (this.filteredData.length === 0) {
this.dropdown.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
return;
}
this.dropdown.innerHTML = this.filteredData
.map((item, index) => {
const content = this.options.renderFunction!(item);
return `<div class="autocomplete-option" data-index="${index}" style="
padding: 0.5rem;
cursor: pointer;
border-bottom: 1px solid var(--color-border-light);
transition: background-color 0.15s ease;
">${content}</div>`;
})
.join('');
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
option.addEventListener('click', () => {
this.selectItem(this.filteredData[index]);
});
option.addEventListener('mouseenter', () => {
this.selectedIndex = index;
this.updateHighlight();
});
});
}
updateHighlight(): void {
this.dropdown.querySelectorAll('.autocomplete-option').forEach((option, index) => {
(option as HTMLElement).style.backgroundColor = index === this.selectedIndex
? 'var(--color-bg-secondary)'
: 'transparent';
});
}
selectItem(item: any): void {
const text = typeof item === 'string' ? item : item.name || item.label || item.toString();
if (this.options.allowMultiple) {
this.selectedItems.add(text);
this.updateSelectedDisplay();
this.updateInputValue();
this.input.value = '';
} else {
this.input.value = text;
this.hideDropdown();
}
this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
detail: { item, text, selectedItems: Array.from(this.selectedItems) }
}));
}
removeItem(text: string): void {
if (this.options.allowMultiple) {
this.selectedItems.delete(text);
this.updateSelectedDisplay();
this.updateInputValue();
}
}
updateSelectedDisplay(): void {
if (!this.options.allowMultiple || !this.selectedContainer) return;
this.selectedContainer.innerHTML = Array.from(this.selectedItems)
.map(item => `
<span class="autocomplete-tag" style="
background-color: var(--color-primary);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.875rem;
display: flex;
align-items: center;
gap: 0.25rem;
">
${escapeHtml(item)}
<button type="button" class="autocomplete-remove" data-item="${escapeHtml(item)}" style="
background: none;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: center;
">×</button>
</span>
`)
.join('');
this.selectedContainer.querySelectorAll('.autocomplete-remove').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.removeItem((btn as HTMLElement).getAttribute('data-item')!);
});
});
}
updateInputValue(): void {
if (this.options.allowMultiple && this.options.hiddenInput) {
this.options.hiddenInput.value = Array.from(this.selectedItems).join(this.options.separator || ', ');
}
}
showDropdown(): void {
this.dropdown.style.display = 'block';
this.isOpen = true;
}
hideDropdown(): void {
this.dropdown.style.display = 'none';
this.isOpen = false;
this.selectedIndex = -1;
}
setDataSource(newDataSource: any[]): void {
this.dataSource = newDataSource;
}
getSelectedItems(): string[] {
return Array.from(this.selectedItems);
}
setSelectedItems(items: string[]): void {
this.selectedItems = new Set(items);
if (this.options.allowMultiple) {
this.updateSelectedDisplay();
this.updateInputValue();
}
}
destroy(): void {
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
}
if (this.selectedContainer && this.selectedContainer.parentNode) {
this.selectedContainer.parentNode.removeChild(this.selectedContainer);
}
}
}

View File

@@ -0,0 +1,225 @@
// src/utils/confidenceScoring.ts
import { isToolHosted } from './clientUtils.js';
import 'dotenv/config';
export interface ConfidenceMetrics {
overall: number;
semanticRelevance: number;
taskSuitability: number;
uncertaintyFactors: string[];
strengthIndicators: string[];
}
export interface ConfidenceConfig {
semanticWeight: number;
suitabilityWeight: number;
minimumThreshold: number;
mediumThreshold: number;
highThreshold: number;
}
export interface AnalysisContext {
userQuery: string;
mode: string;
embeddingsSimilarities: Map<string, number>;
selectedTools?: Array<{
tool: any;
phase: string;
priority: string;
justification?: string;
taskRelevance?: number;
limitations?: string[];
}>;
}
class ConfidenceScoring {
private config: ConfidenceConfig;
constructor() {
this.config = {
semanticWeight: this.getEnvFloat('CONFIDENCE_SEMANTIC_WEIGHT', 0.3),
suitabilityWeight: this.getEnvFloat('CONFIDENCE_SUITABILITY_WEIGHT', 0.7),
minimumThreshold: this.getEnvInt('CONFIDENCE_MINIMUM_THRESHOLD', 40),
mediumThreshold: this.getEnvInt('CONFIDENCE_MEDIUM_THRESHOLD', 60),
highThreshold: this.getEnvInt('CONFIDENCE_HIGH_THRESHOLD', 80)
};
console.log('[CONFIDENCE-SCORING] Initialized with restored config:', this.config);
}
private getEnvFloat(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseFloat(value) : defaultValue;
}
private getEnvInt(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseInt(value, 10) : defaultValue;
}
calculateRecommendationConfidence(
tool: any,
context: AnalysisContext,
taskRelevance: number = 70,
limitations: string[] = []
): ConfidenceMetrics {
console.log('[CONFIDENCE-SCORING] Calculating confidence for tool:', tool.name);
const rawSemanticRelevance = context.embeddingsSimilarities.has(tool.name) ?
context.embeddingsSimilarities.get(tool.name)! * 100 : 50;
let enhancedTaskSuitability = taskRelevance;
if (context.mode === 'workflow') {
const toolSelection = context.selectedTools?.find((st: any) => st.tool && st.tool.name === tool.name);
if (toolSelection && tool.phases && Array.isArray(tool.phases) && tool.phases.includes(toolSelection.phase)) {
const phaseBonus = Math.min(15, 100 - taskRelevance);
enhancedTaskSuitability = Math.min(100, taskRelevance + phaseBonus);
console.log('[CONFIDENCE-SCORING] Phase alignment bonus applied:', phaseBonus);
}
}
const overall = (
rawSemanticRelevance * this.config.semanticWeight +
enhancedTaskSuitability * this.config.suitabilityWeight
);
const uncertaintyFactors = this.identifyUncertaintyFactors(tool, context, limitations, overall);
const strengthIndicators = this.identifyStrengthIndicators(tool, context, overall);
const result = {
overall: Math.round(overall),
semanticRelevance: Math.round(rawSemanticRelevance),
taskSuitability: Math.round(enhancedTaskSuitability),
uncertaintyFactors,
strengthIndicators
};
console.log('[CONFIDENCE-SCORING] Confidence calculated:', {
tool: tool.name,
overall: result.overall,
semantic: result.semanticRelevance,
task: result.taskSuitability,
uncertaintyCount: uncertaintyFactors.length,
strengthCount: strengthIndicators.length
});
return result;
}
private identifyUncertaintyFactors(
tool: any,
context: AnalysisContext,
limitations: string[],
confidence: number
): string[] {
const factors: string[] = [];
if (limitations?.length > 0) {
factors.push(...limitations.slice(0, 2));
}
const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5;
if (similarity < 0.7) {
factors.push('Geringe semantische Ähnlichkeit zur Anfrage');
}
if (tool.skillLevel === 'expert' && /schnell|rapid|triage|urgent|sofort/i.test(context.userQuery)) {
factors.push('Experten-Tool für zeitkritisches Szenario');
}
if (tool.skillLevel === 'novice' && /komplex|erweitert|tiefgehend|advanced|forensisch/i.test(context.userQuery)) {
factors.push('Einsteiger-Tool für komplexe Analyse');
}
if (tool.type === 'software' && !isToolHosted(tool) && tool.accessType === 'download') {
factors.push('Installation und Setup erforderlich');
}
if (tool.license === 'Proprietary') {
factors.push('Kommerzielle Software - Lizenzkosten zu beachten');
}
if (confidence < 60) {
factors.push('Moderate Gesamtbewertung - alternative Ansätze empfohlen');
}
return factors.slice(0, 4);
}
private identifyStrengthIndicators(tool: any, context: AnalysisContext, confidence: number): string[] {
const indicators: string[] = [];
const similarity = context.embeddingsSimilarities.get(tool.name) || 0.5;
if (similarity >= 0.7) {
indicators.push('Sehr gute semantische Übereinstimmung mit Ihrer Anfrage');
}
if (tool.knowledgebase === true) {
indicators.push('Umfassende Dokumentation und Wissensbasis verfügbar');
}
if (isToolHosted(tool)) {
indicators.push('Sofort verfügbar über gehostete Lösung');
}
if (tool.skillLevel === 'intermediate' || tool.skillLevel === 'advanced') {
indicators.push('Ausgewogenes Verhältnis zwischen Funktionalität und Benutzerfreundlichkeit');
}
if (tool.type === 'method' && /methodik|vorgehen|prozess|ansatz/i.test(context.userQuery)) {
indicators.push('Methodischer Ansatz passt zu Ihrer prozeduralen Anfrage');
}
return indicators.slice(0, 4);
}
calculateSelectionConfidence(result: any, candidateCount: number): number {
if (!result?.selectedTools) {
console.log('[CONFIDENCE-SCORING] No selected tools for confidence calculation');
return 30;
}
const selectionRatio = result.selectedTools.length / candidateCount;
const hasReasoning = result.reasoning && result.reasoning.length > 50;
let confidence = 60;
if (selectionRatio > 0.05 && selectionRatio < 0.3) confidence += 20;
else if (selectionRatio <= 0.05) confidence -= 10;
else confidence -= 15;
if (hasReasoning) confidence += 15;
if (result.selectedConcepts?.length > 0) confidence += 5;
const finalConfidence = Math.min(95, Math.max(25, confidence));
console.log('[CONFIDENCE-SCORING] Selection confidence calculated:', {
candidateCount,
selectedCount: result.selectedTools.length,
selectionRatio: selectionRatio.toFixed(3),
hasReasoning,
confidence: finalConfidence
});
return finalConfidence;
}
getConfidenceLevel(confidence: number): 'weak' | 'moderate' | 'strong' {
if (confidence >= this.config.highThreshold) return 'strong';
if (confidence >= this.config.mediumThreshold) return 'moderate';
return 'weak';
}
getConfidenceColor(confidence: number): string {
if (confidence >= this.config.highThreshold) return 'var(--color-accent)';
if (confidence >= this.config.mediumThreshold) return 'var(--color-warning)';
return 'var(--color-error)';
}
getConfig(): ConfidenceConfig {
return { ...this.config };
}
}
export const confidenceScoring = new ConfidenceScoring();

View File

@@ -1,4 +1,4 @@
// src/utils/dataService.ts - Enhanced for micro-task AI pipeline
// src/utils/dataService.ts
import { promises as fs } from 'fs';
import { load } from 'js-yaml';
import path from 'path';
@@ -85,7 +85,7 @@ let cachedData: ToolsData | null = null;
let cachedRandomizedData: ToolsData | null = null;
let cachedCompressedData: EnhancedCompressedToolsData | null = null;
let lastRandomizationDate: string | null = null;
let dataVersion: string | null = null;
let cachedToolsHash: string | null = null;
function seededRandom(seed: number): () => number {
let x = Math.sin(seed) * 10000;
@@ -110,17 +110,6 @@ function shuffleArray<T>(array: T[], randomFn: () => number): T[] {
return shuffled;
}
function generateDataVersion(data: any): string {
const str = JSON.stringify(data, Object.keys(data).sort());
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
async function loadRawData(): Promise<ToolsData> {
if (!cachedData) {
const yamlPath = path.join(process.cwd(), 'src/data/tools.yaml');
@@ -128,7 +117,9 @@ async function loadRawData(): Promise<ToolsData> {
const rawData = load(yamlContent);
try {
console.log('Attempting to validate YAML structure...');
cachedData = ToolsDataSchema.parse(rawData);
console.log('Validation successful!');
if (!cachedData.skill_levels || Object.keys(cachedData.skill_levels).length === 0) {
cachedData.skill_levels = {
@@ -140,12 +131,20 @@ async function loadRawData(): Promise<ToolsData> {
};
}
dataVersion = generateDataVersion(cachedData);
console.log(`[DATA SERVICE] Loaded enhanced data version: ${dataVersion}`);
const { getToolsFileHash } = await import('./hashUtils.js');
cachedToolsHash = await getToolsFileHash();
console.log(`[DATA SERVICE] Loaded data with hash: ${cachedToolsHash.slice(0, 12)}...`);
} catch (error) {
console.error('YAML validation failed:', error);
throw new Error('Invalid tools.yaml structure');
if (error instanceof z.ZodError) {
console.error('ToolsDataSchema validation errors:');
error.errors.forEach((err, index) => {
console.error(`${index + 1}. Path: ${err.path.join('.')}, Error: ${err.message}`);
});
} else {
console.error('Non-Zod validation error:', error);
}
throw new Error(`Invalid tools.yaml structure: ${error}`);
}
}
return cachedData;
@@ -225,7 +224,7 @@ export async function getCompressedToolsDataForAI(): Promise<EnhancedCompressedT
}
export function getDataVersion(): string | null {
return dataVersion;
return cachedToolsHash;
}
export function clearCache(): void {
@@ -233,7 +232,7 @@ export function clearCache(): void {
cachedRandomizedData = null;
cachedCompressedData = null;
lastRandomizationDate = null;
dataVersion = null;
cachedToolsHash = null;
console.log('[DATA SERVICE] Enhanced cache cleared');
}

View File

@@ -1,11 +1,11 @@
// src/utils/embeddings.ts
// src/utils/embeddings.ts - Refactored
import { promises as fs } from 'fs';
import path from 'path';
import { getCompressedToolsDataForAI } from './dataService.js';
import 'dotenv/config';
import crypto from 'crypto';
interface EmbeddingData {
export interface EmbeddingData {
id: string;
type: 'tool' | 'concept';
name: string;
@@ -20,14 +20,22 @@ interface EmbeddingData {
};
}
export interface SimilarityResult extends EmbeddingData {
similarity: number;
}
interface EmbeddingsDatabase {
version: string;
lastUpdated: number;
embeddings: EmbeddingData[];
}
interface SimilarityResult extends EmbeddingData {
similarity: number;
interface EmbeddingsConfig {
endpoint?: string;
apiKey?: string;
model?: string;
batchSize: number;
batchDelay: number;
}
class EmbeddingsService {
@@ -35,58 +43,30 @@ class EmbeddingsService {
private isInitialized = false;
private initializationPromise: Promise<void> | null = null;
private readonly embeddingsPath = path.join(process.cwd(), 'data', 'embeddings.json');
private readonly batchSize: number;
private readonly batchDelay: number;
private enabled: boolean = false; // Make mutable again
private config: EmbeddingsConfig;
constructor() {
this.batchSize = parseInt(process.env.AI_EMBEDDINGS_BATCH_SIZE || '20', 10);
this.batchDelay = parseInt(process.env.AI_EMBEDDINGS_BATCH_DELAY_MS || '1000', 10);
// Don't call async method from constructor - handle in initialize() instead
this.enabled = true; // Start optimistically enabled for development
}
private async checkEnabledStatus(): Promise<void> {
try {
// Add debugging to see what's actually in process.env
console.log('[EMBEDDINGS] Debug env check:', {
AI_EMBEDDINGS_ENABLED: process.env.AI_EMBEDDINGS_ENABLED,
envKeys: Object.keys(process.env).filter(k => k.includes('EMBEDDINGS')).length,
allEnvKeys: Object.keys(process.env).length
this.config = this.loadConfig();
console.log('[EMBEDDINGS-SERVICE] Initialized:', {
hasEndpoint: !!this.config.endpoint,
hasModel: !!this.config.model
});
}
const envEnabled = process.env.AI_EMBEDDINGS_ENABLED;
if (envEnabled === 'true') {
// Check if we have the required API configuration
private loadConfig(): EmbeddingsConfig {
const endpoint = process.env.AI_EMBEDDINGS_ENDPOINT;
const apiKey = process.env.AI_EMBEDDINGS_API_KEY;
const model = process.env.AI_EMBEDDINGS_MODEL;
const batchSize = parseInt(process.env.AI_EMBEDDINGS_BATCH_SIZE || '20', 10);
const batchDelay = parseInt(process.env.AI_EMBEDDINGS_BATCH_DELAY_MS || '1000', 10);
if (!endpoint || !model) {
console.warn('[EMBEDDINGS] Embeddings enabled but API configuration missing - disabling');
this.enabled = false;
return;
}
console.log('[EMBEDDINGS] All requirements met - enabling embeddings');
this.enabled = true;
return;
}
// Check if embeddings file exists
try {
await fs.stat(this.embeddingsPath);
console.log('[EMBEDDINGS] Existing embeddings file found - enabling');
this.enabled = true;
} catch {
console.log('[EMBEDDINGS] Embeddings not explicitly enabled - disabling');
this.enabled = false;
}
} catch (error) {
console.error('[EMBEDDINGS] Error checking enabled status:', error);
this.enabled = false;
}
return {
endpoint,
apiKey,
model,
batchSize,
batchDelay
};
}
async initialize(): Promise<void> {
@@ -103,69 +83,55 @@ class EmbeddingsService {
}
private async performInitialization(): Promise<void> {
// 1⃣ Respect the on/off switch that the newer code introduced
await this.checkEnabledStatus();
if (!this.enabled) {
console.log('[EMBEDDINGS] Embeddings disabled, skipping initialization');
return;
}
const initStart = Date.now();
try {
console.log('[EMBEDDINGS] Initializing embeddings system…');
// Make sure the data folder exists
try {
console.log('[EMBEDDINGS-SERVICE] Starting initialization');
/*if (!this.config.enabled) {
console.log('[EMBEDDINGS-SERVICE] Service disabled via configuration');
return;
}*/
await fs.mkdir(path.dirname(this.embeddingsPath), { recursive: true });
// Load current tools / concepts and generate a hash
const toolsData = await getCompressedToolsDataForAI();
const currentDataHash = await this.hashToolsFile(); // <- keep the old helper
const { getToolsFileHash } = await import('./hashUtils.js');
const currentDataHash = await getToolsFileHash();
// Try to read an existing file
const existing = await this.loadEmbeddings();
console.log('[EMBEDDINGS] Current hash:', currentDataHash);
console.log('[EMBEDDINGS] Existing file version:', existing?.version);
console.log('[EMBEDDINGS] Existing embeddings length:', existing?.embeddings?.length);
const cacheIsUsable =
existing &&
const cacheIsUsable = existing &&
existing.version === currentDataHash &&
Array.isArray(existing.embeddings) &&
existing.embeddings.length > 0;
if (cacheIsUsable) {
console.log('[EMBEDDINGS] Using cached embeddings');
console.log('[EMBEDDINGS-SERVICE] Using cached embeddings');
this.embeddings = existing.embeddings;
} else {
console.log('[EMBEDDINGS] Generating new embeddings');
// 2⃣ Build and persist new vectors
await this.generateEmbeddings(toolsData, currentDataHash); // <- old helper
console.log('[EMBEDDINGS-SERVICE] Generating new embeddings');
await this.generateEmbeddings(toolsData, currentDataHash);
}
this.isInitialized = true;
console.log(`[EMBEDDINGS] Initialized with ${this.embeddings.length} embeddings in ${Date.now() - initStart} ms`);
} catch (err) {
console.error('[EMBEDDINGS] Failed to initialize:', err);
console.log(`[EMBEDDINGS-SERVICE] Initialized successfully with ${this.embeddings.length} embeddings in ${Date.now() - initStart}ms`);
} catch (error) {
console.error('[EMBEDDINGS-SERVICE] Initialization failed:', error);
this.isInitialized = false;
throw err; // Let the caller know same behaviour as before
throw error;
} finally {
// 3⃣ Always clear the promise so subsequent calls don't hang
this.initializationPromise = null;
}
}
private async hashToolsFile(): Promise<string> {
const file = path.join(process.cwd(), 'src', 'data', 'tools.yaml');
const raw = await fs.readFile(file, 'utf8');
return crypto.createHash('sha256').update(raw).digest('hex'); // 64-char hex
}
private async loadEmbeddings(): Promise<EmbeddingsDatabase | null> {
try {
const data = await fs.readFile(this.embeddingsPath, 'utf8');
return JSON.parse(data);
} catch (error) {
console.log('[EMBEDDINGS] No existing embeddings found');
console.log('[EMBEDDINGS-SERVICE] No existing embeddings file found');
return null;
}
}
@@ -178,7 +144,7 @@ class EmbeddingsService {
};
await fs.writeFile(this.embeddingsPath, JSON.stringify(database, null, 2));
console.log(`[EMBEDDINGS] Saved ${this.embeddings.length} embeddings to disk`);
console.log(`[EMBEDDINGS-SERVICE] Saved ${this.embeddings.length} embeddings to disk`);
}
private createContentString(item: any): string {
@@ -194,30 +160,23 @@ class EmbeddingsService {
}
private async generateEmbeddingsBatch(contents: string[]): Promise<number[][]> {
const endpoint = process.env.AI_EMBEDDINGS_ENDPOINT;
const apiKey = process.env.AI_EMBEDDINGS_API_KEY;
const model = process.env.AI_EMBEDDINGS_MODEL;
if (!endpoint || !model) {
const missing: string[] = [];
if (!endpoint) missing.push('AI_EMBEDDINGS_ENDPOINT');
if (!model) missing.push('AI_EMBEDDINGS_MODEL');
throw new Error(`Missing embeddings API configuration: ${missing.join(', ')}`);
if (!this.config.endpoint || !this.config.model) {
throw new Error('Missing embeddings API configuration');
}
const headers: Record<string, string> = {
'Content-Type': 'application/json'
};
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
if (this.config.apiKey) {
headers['Authorization'] = `Bearer ${this.config.apiKey}`;
}
const response = await fetch(endpoint, {
const response = await fetch(this.config.endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model,
model: this.config.model,
input: contents
})
});
@@ -249,11 +208,16 @@ class EmbeddingsService {
const contents = allItems.map(item => this.createContentString(item));
this.embeddings = [];
for (let i = 0; i < contents.length; i += this.batchSize) {
const batch = contents.slice(i, i + this.batchSize);
const batchItems = allItems.slice(i, i + this.batchSize);
console.log(`[EMBEDDINGS-SERVICE] Generating embeddings for ${contents.length} items`);
console.log(`[EMBEDDINGS] Processing batch ${Math.ceil((i + 1) / this.batchSize)} of ${Math.ceil(contents.length / this.batchSize)}`);
for (let i = 0; i < contents.length; i += this.config.batchSize) {
const batch = contents.slice(i, i + this.config.batchSize);
const batchItems = allItems.slice(i, i + this.config.batchSize);
const batchNumber = Math.ceil((i + 1) / this.config.batchSize);
const totalBatches = Math.ceil(contents.length / this.config.batchSize);
console.log(`[EMBEDDINGS-SERVICE] Processing batch ${batchNumber}/${totalBatches}`);
try {
const embeddings = await this.generateEmbeddingsBatch(batch);
@@ -276,12 +240,12 @@ class EmbeddingsService {
});
});
if (i + this.batchSize < contents.length) {
await new Promise(resolve => setTimeout(resolve, this.batchDelay));
if (i + this.config.batchSize < contents.length) {
await new Promise(resolve => setTimeout(resolve, this.config.batchDelay));
}
} catch (error) {
console.error(`[EMBEDDINGS] Failed to process batch ${Math.ceil((i + 1) / this.batchSize)}:`, error);
console.error(`[EMBEDDINGS-SERVICE] Batch ${batchNumber} failed:`, error);
throw error;
}
}
@@ -289,19 +253,17 @@ class EmbeddingsService {
await this.saveEmbeddings(version);
}
public async embedText(text: string): Promise<number[]> {
if (!this.enabled || !this.isInitialized) {
async embedText(text: string): Promise<number[]> {
if (!this.isInitialized) {
throw new Error('Embeddings service not available');
}
const [embedding] = await this.generateEmbeddingsBatch([text.toLowerCase()]);
return embedding;
}
async waitForInitialization(): Promise<void> {
// Always re-check environment status first in case variables loaded after initial check
await this.checkEnabledStatus();
if (!this.enabled || this.isInitialized) {
if (this.isInitialized) {
return Promise.resolve();
}
@@ -313,14 +275,6 @@ class EmbeddingsService {
return this.initialize();
}
// Force re-check of environment status (useful for development)
async forceRecheckEnvironment(): Promise<void> {
this.enabled = false;
this.isInitialized = false;
await this.checkEnabledStatus();
console.log('[EMBEDDINGS] Environment status re-checked, enabled:', this.enabled);
}
private cosineSimilarity(a: number[], b: number[]): number {
let dotProduct = 0;
let normA = 0;
@@ -336,166 +290,62 @@ class EmbeddingsService {
}
async findSimilar(query: string, maxResults: number = 30, threshold: number = 0.3): Promise<SimilarityResult[]> {
if (!this.enabled) {
console.log('[EMBEDDINGS] Service disabled for similarity search');
/*if (!this.config.enabled) {
console.log('[EMBEDDINGS-SERVICE] Service disabled, returning empty results');
return [];
}*/
if (!this.isInitialized || this.embeddings.length === 0) {
console.log('[EMBEDDINGS-SERVICE] Not initialized or no embeddings available');
return [];
}
try {
// If we have embeddings data, use it
if (this.isInitialized && this.embeddings.length > 0) {
console.log(`[EMBEDDINGS] Using embeddings data for similarity search: ${query}`);
console.log(`[EMBEDDINGS-SERVICE] Finding similar items for query: "${query}"`);
const queryEmbeddings = await this.generateEmbeddingsBatch([query.toLowerCase()]);
const queryEmbedding = queryEmbeddings[0];
console.log(`[EMBEDDINGS] Computing similarities for ${this.embeddings.length} items`);
const similarities: SimilarityResult[] = this.embeddings.map(item => ({
...item,
similarity: this.cosineSimilarity(queryEmbedding, item.embedding)
}));
const topScore = Math.max(...similarities.map(s => s.similarity));
const dynamicCutOff = Math.max(threshold, topScore * 0.85);
const dynamicThreshold = Math.max(threshold, topScore * 0.85);
const results = similarities
.filter(item => item.similarity >= dynamicCutOff)
.filter(item => item.similarity >= dynamicThreshold)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, maxResults);
console.log(`[EMBEDDINGS-SERVICE] Found ${results.length} similar items (threshold: ${dynamicThreshold.toFixed(3)})`);
const orderingValid = results.every((item, index) => {
if (index === 0) return true;
return item.similarity <= results[index - 1].similarity;
});
if (!orderingValid) {
console.error('[EMBEDDINGS] CRITICAL: Similarity ordering is broken!');
results.forEach((item, idx) => {
console.error(` ${idx}: ${item.name} = ${item.similarity.toFixed(4)}`);
});
}
console.log(`[EMBEDDINGS] Found ${results.length} similar items (threshold: ${threshold})`);
if (results.length > 0) {
console.log('[EMBEDDINGS] Top 10 similarity matches:');
results.slice(0, 10).forEach((item, idx) => {
console.log('[EMBEDDINGS-SERVICE] Top 5 matches:');
results.slice(0, 5).forEach((item, idx) => {
console.log(` ${idx + 1}. ${item.name} (${item.type}) = ${item.similarity.toFixed(4)}`);
});
const topSimilarity = results[0].similarity;
const hasHigherSimilarity = results.some(item => item.similarity > topSimilarity);
if (hasHigherSimilarity) {
console.error('[EMBEDDINGS] CRITICAL: Top result is not actually the highest similarity!');
}
}
return results;
} else {
// Fallback: generate mock similarity results from actual tools data
console.log(`[EMBEDDINGS] No embeddings data, using fallback text matching: ${query}`);
const { getToolsData } = await import('./dataService.js');
const toolsData = await getToolsData();
const queryLower = query.toLowerCase();
const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
const similarities: SimilarityResult[] = toolsData.tools
.map((tool: any) => {
let similarity = 0;
// Name matching
if (tool.name.toLowerCase().includes(queryLower)) {
similarity += 0.8;
}
// Description matching
if (tool.description && tool.description.toLowerCase().includes(queryLower)) {
similarity += 0.6;
}
// Tag matching
if (tool.tags && Array.isArray(tool.tags)) {
const matchingTags = tool.tags.filter((tag: string) =>
tag.toLowerCase().includes(queryLower) || queryLower.includes(tag.toLowerCase())
);
if (tool.tags.length > 0) {
similarity += (matchingTags.length / tool.tags.length) * 0.4;
}
}
// Word-level matching
const toolText = `${tool.name} ${tool.description || ''} ${(tool.tags || []).join(' ')}`.toLowerCase();
const matchingWords = queryWords.filter(word => toolText.includes(word));
if (queryWords.length > 0) {
similarity += (matchingWords.length / queryWords.length) * 0.3;
}
return {
id: `tool_${tool.name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase()}`,
type: 'tool' as const,
name: tool.name,
content: toolText,
embedding: [], // Empty for fallback
metadata: {
domains: tool.domains || [],
phases: tool.phases || [],
tags: tool.tags || [],
skillLevel: tool.skillLevel,
type: tool.type
},
similarity: Math.min(similarity, 1.0)
};
})
.filter(item => item.similarity >= threshold)
.sort((a, b) => b.similarity - a.similarity)
.slice(0, maxResults);
console.log(`[EMBEDDINGS] Fallback found ${similarities.length} similar items`);
return similarities;
}
} catch (error) {
console.error('[EMBEDDINGS] Failed to find similar items:', error);
console.error('[EMBEDDINGS-SERVICE] Similarity search failed:', error);
return [];
}
}
isEnabled(): boolean {
// If not enabled and not initialized, try re-checking environment
// This handles the case where environment variables loaded after initial check
if (!this.enabled && !this.isInitialized) {
// Don't await this, just trigger it and return current status
this.checkEnabledStatus().catch(console.error);
}
return this.enabled;
}
getStats(): { enabled: boolean; initialized: boolean; count: number } {
getStats(): {initialized: boolean; count: number } {
return {
enabled: this.enabled, // Always true during development
initialized: this.isInitialized,
count: this.embeddings.length
};
}
getConfig(): EmbeddingsConfig {
return { ...this.config };
}
}
const embeddingsService = new EmbeddingsService();
export { embeddingsService, type EmbeddingData, type SimilarityResult };
// Export utility functions for debugging
export const debugEmbeddings = {
async recheckEnvironment() {
return embeddingsService.forceRecheckEnvironment();
},
getStatus() {
return embeddingsService.getStats();
}
};
// Remove auto-initialization - let it initialize lazily when first needed
export const embeddingsService = new EmbeddingsService();

View File

@@ -5,22 +5,23 @@ export interface ContributionData {
type: 'add' | 'edit';
tool: {
name: string;
icon?: string;
icon?: string | null;
type: 'software' | 'method' | 'concept';
description: string;
domains: string[];
phases: string[];
platforms: string[];
skillLevel: string;
accessType?: string;
accessType?: string | null;
url: string;
projectUrl?: string;
license?: string;
knowledgebase?: boolean;
'domain-agnostic-software'?: string[];
related_concepts?: string[];
projectUrl?: string | null;
license?: string | null;
knowledgebase?: boolean | null;
'domain-agnostic-software'?: string[] | null;
related_concepts?: string[] | null;
related_software?: string[] | null;
tags: string[];
statusUrl?: string;
statusUrl?: string | null;
};
metadata: {
submitter: string;
@@ -134,6 +135,7 @@ export class GitContributionManager {
if (tool.projectUrl) cleanTool.projectUrl = tool.projectUrl;
if (tool.knowledgebase) cleanTool.knowledgebase = tool.knowledgebase;
if (tool.related_concepts?.length) cleanTool.related_concepts = tool.related_concepts;
if (tool.related_software?.length) cleanTool.related_software = tool.related_software;
if (tool.tags?.length) cleanTool.tags = tool.tags;
if (tool['domain-agnostic-software']?.length) {
cleanTool['domain-agnostic-software'] = tool['domain-agnostic-software'];
@@ -272,6 +274,8 @@ ${data.tool.platforms?.length ? `- **Platforms:** ${data.tool.platforms.join(',
${data.tool.license ? `- **License:** ${data.tool.license}` : ''}
${data.tool.domains?.length ? `- **Domains:** ${data.tool.domains.join(', ')}` : ''}
${data.tool.phases?.length ? `- **Phases:** ${data.tool.phases.join(', ')}` : ''}
${data.tool.related_concepts?.length ? `- **Related Concepts:** ${data.tool.related_concepts.join(', ')}` : ''}
${data.tool.related_software?.length ? `- **Related Software:** ${data.tool.related_software.join(', ')}` : ''}
${data.metadata.reason ? `### Reason
${data.metadata.reason}
@@ -299,9 +303,6 @@ ${data.metadata.contact}
private generateKnowledgebaseIssueBody(data: KnowledgebaseContribution): string {
const sections: string[] = [];
/* ------------------------------------------------------------------ */
/* Header */
/* ------------------------------------------------------------------ */
sections.push(`## Knowledge Base Article: ${data.title ?? 'Untitled'}`);
sections.push('');
sections.push(`**Submitted by:** ${data.submitter}`);
@@ -310,18 +311,12 @@ ${data.metadata.contact}
if (data.difficulty) sections.push(`**Difficulty:** ${data.difficulty}`);
sections.push('');
/* ------------------------------------------------------------------ */
/* Description */
/* ------------------------------------------------------------------ */
if (data.description) {
sections.push('### Description');
sections.push(data.description);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Content */
/* ------------------------------------------------------------------ */
if (data.content) {
sections.push('### Article Content');
sections.push('```markdown');
@@ -330,18 +325,12 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* External resources */
/* ------------------------------------------------------------------ */
if (data.externalLink) {
sections.push('### External Resource');
sections.push(`- [External Documentation](${data.externalLink})`);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Uploaded files */
/* ------------------------------------------------------------------ */
if (Array.isArray(data.uploadedFiles) && data.uploadedFiles.length) {
sections.push('### Uploaded Files');
data.uploadedFiles.forEach((file) => {
@@ -359,9 +348,6 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Categories & Tags */
/* ------------------------------------------------------------------ */
const hasCategories = Array.isArray(data.categories) && data.categories.length > 0;
const hasTags = Array.isArray(data.tags) && data.tags.length > 0;
@@ -372,18 +358,12 @@ ${data.metadata.contact}
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Reason */
/* ------------------------------------------------------------------ */
if (data.reason) {
sections.push('### Reason for Contribution');
sections.push(data.reason);
sections.push('');
}
/* ------------------------------------------------------------------ */
/* Footer */
/* ------------------------------------------------------------------ */
sections.push('### For Maintainers');
sections.push('1. Review the content for quality and accuracy');
sections.push('2. Create the appropriate markdown file in `src/content/knowledgebase/`');

20
src/utils/hashUtils.ts Normal file
View File

@@ -0,0 +1,20 @@
// src/utils/hashUtils.ts
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
export async function getToolsFileHash(): Promise<string> {
const file = path.join(process.cwd(), 'src', 'data', 'tools.yaml');
const raw = await fs.readFile(file, 'utf8');
return crypto.createHash('sha256').update(raw).digest('hex');
}
export function getToolsFileHashSync(): string | null {
try {
const file = path.join(process.cwd(), 'src', 'data', 'tools.yaml');
const raw = require('fs').readFileSync(file, 'utf8');
return crypto.createHash('sha256').update(raw).digest('hex');
} catch {
return null;
}
}

356
src/utils/jsonUtils.ts Normal file
View File

@@ -0,0 +1,356 @@
// src/utils/jsonUtils.ts
export class JSONParser {
static safeParseJSON(jsonString: string, fallback: any = null): any {
try {
let cleaned = jsonString.trim();
const jsonBlockPatterns = [
/```json\s*([\s\S]*?)\s*```/i,
/```\s*([\s\S]*?)\s*```/i,
/\{[\s\S]*\}/,
];
for (const pattern of jsonBlockPatterns) {
const match = cleaned.match(pattern);
if (match) {
cleaned = match[1] || match[0];
break;
}
}
if (!cleaned.endsWith('}') && !cleaned.endsWith(']')) {
console.warn('[JSON-PARSER] JSON appears truncated, attempting recovery');
cleaned = this.repairTruncatedJSON(cleaned);
}
const parsed = JSON.parse(cleaned);
if (parsed && typeof parsed === 'object') {
if (!parsed.selectedTools) parsed.selectedTools = [];
if (!parsed.selectedConcepts) parsed.selectedConcepts = [];
if (!Array.isArray(parsed.selectedTools)) parsed.selectedTools = [];
if (!Array.isArray(parsed.selectedConcepts)) parsed.selectedConcepts = [];
}
return parsed;
} catch (error) {
console.warn('[JSON-PARSER] JSON parsing failed:', error.message);
return fallback;
}
}
private static repairTruncatedJSON(cleaned: string): string {
let braceCount = 0;
let bracketCount = 0;
let inString = false;
let escaped = false;
let lastCompleteStructure = '';
for (let i = 0; i < cleaned.length; i++) {
const char = cleaned[i];
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}
if (char === '"' && !escaped) {
inString = !inString;
continue;
}
if (!inString) {
if (char === '{') braceCount++;
if (char === '}') braceCount--;
if (char === '[') bracketCount++;
if (char === ']') bracketCount--;
if (braceCount === 0 && bracketCount === 0 && (char === '}' || char === ']')) {
lastCompleteStructure = cleaned.substring(0, i + 1);
}
}
}
if (lastCompleteStructure) {
return lastCompleteStructure;
} else {
if (braceCount > 0) cleaned += '}';
if (bracketCount > 0) cleaned += ']';
return cleaned;
}
}
static extractToolsFromMalformedJSON(jsonString: string): { selectedTools: string[]; selectedConcepts: string[] } {
const selectedTools: string[] = [];
const selectedConcepts: string[] = [];
const toolsMatch = jsonString.match(/"selectedTools"\s*:\s*\[([\s\S]*?)\]/i);
if (toolsMatch) {
const toolMatches = toolsMatch[1].match(/"([^"]+)"/g);
if (toolMatches) {
selectedTools.push(...toolMatches.map(match => match.replace(/"/g, '')));
}
}
const conceptsMatch = jsonString.match(/"selectedConcepts"\s*:\s*\[([\s\S]*?)\]/i);
if (conceptsMatch) {
const conceptMatches = conceptsMatch[1].match(/"([^"]+)"/g);
if (conceptMatches) {
selectedConcepts.push(...conceptMatches.map(match => match.replace(/"/g, '')));
}
}
if (selectedTools.length === 0 && selectedConcepts.length === 0) {
const allMatches = jsonString.match(/"([^"]+)"/g);
if (allMatches) {
const possibleNames = allMatches
.map(match => match.replace(/"/g, ''))
.filter(name =>
name.length > 2 &&
!['selectedTools', 'selectedConcepts', 'reasoning'].includes(name) &&
!name.includes(':') &&
!name.match(/^\d+$/)
)
.slice(0, 15);
selectedTools.push(...possibleNames);
}
}
return { selectedTools, selectedConcepts };
}
static secureParseJSON(jsonString: string, maxSize: number = 10 * 1024 * 1024): any {
if (typeof jsonString !== 'string') {
throw new Error('Input must be a string');
}
if (jsonString.length > maxSize) {
throw new Error(`JSON string too large (${jsonString.length} bytes, max ${maxSize})`);
}
const suspiciousPatterns = [
/<script/i,
/javascript:/i,
/eval\(/i,
/function\s*\(/i,
/__proto__/i,
/constructor/i
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(jsonString)) {
throw new Error('Potentially malicious content detected in JSON');
}
}
try {
const parsed = JSON.parse(jsonString);
if (typeof parsed !== 'object' || parsed === null) {
throw new Error('JSON must be an object');
}
return parsed;
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(`Invalid JSON syntax: ${error.message}`);
}
throw error;
}
}
static sanitizeForAudit(obj: any, maxDepth: number = 5, currentDepth: number = 0): any {
if (currentDepth >= maxDepth) {
return '[Max depth reached]';
}
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === 'string') {
if (obj.length > 500) {
return obj.slice(0, 500) + '...[truncated]';
}
return obj.replace(/<script[\s\S]*?<\/script>/gi, '[script removed]');
}
if (typeof obj === 'number' || typeof obj === 'boolean') {
return obj;
}
if (Array.isArray(obj)) {
if (obj.length > 20) {
return [
...obj.slice(0, 20).map(item => this.sanitizeForAudit(item, maxDepth, currentDepth + 1)),
`...[${obj.length - 20} more items]`
];
}
return obj.map(item => this.sanitizeForAudit(item, maxDepth, currentDepth + 1));
}
if (typeof obj === 'object') {
const keys = Object.keys(obj);
if (keys.length > 50) {
const sanitized: any = {};
keys.slice(0, 50).forEach(key => {
sanitized[key] = this.sanitizeForAudit(obj[key], maxDepth, currentDepth + 1);
});
sanitized['[truncated]'] = `${keys.length - 50} more properties`;
return sanitized;
}
const sanitized: any = {};
keys.forEach(key => {
if (['__proto__', 'constructor', 'prototype'].includes(key)) {
return;
}
sanitized[key] = this.sanitizeForAudit(obj[key], maxDepth, currentDepth + 1);
});
return sanitized;
}
return String(obj);
}
static validateAuditExportStructure(data: any): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
if (!data || typeof data !== 'object') {
errors.push('Export data must be an object');
return { isValid: false, errors };
}
const requiredProps = ['metadata', 'recommendation', 'auditTrail'];
for (const prop of requiredProps) {
if (!(prop in data)) {
errors.push(`Missing required property: ${prop}`);
}
}
if (data.metadata && typeof data.metadata === 'object') {
const requiredMetadataProps = ['timestamp', 'version', 'userQuery', 'mode'];
for (const prop of requiredMetadataProps) {
if (!(prop in data.metadata)) {
errors.push(`Missing required metadata property: ${prop}`);
}
}
} else {
errors.push('Invalid metadata structure');
}
if (!Array.isArray(data.auditTrail)) {
errors.push('auditTrail must be an array');
} else {
data.auditTrail.forEach((entry: any, index: number) => {
if (!entry || typeof entry !== 'object') {
errors.push(`Audit entry ${index} is not a valid object`);
return;
}
const requiredEntryProps = ['timestamp', 'phase', 'action', 'confidence', 'processingTimeMs'];
for (const prop of requiredEntryProps) {
if (!(prop in entry)) {
errors.push(`Audit entry ${index} missing required property: ${prop}`);
}
}
});
}
return {
isValid: errors.length === 0,
errors
};
}
static prepareAuditExport(
recommendation: any,
userQuery: string,
mode: string,
auditTrail: any[] = [],
additionalMetadata: any = {}
): any {
return {
metadata: {
timestamp: new Date().toISOString(),
version: "1.0",
userQuery: userQuery.slice(0, 1000),
mode,
exportedBy: 'ForensicPathways',
toolsDataHash: additionalMetadata.toolsDataHash || 'unknown',
aiModel: additionalMetadata.aiModel || 'unknown',
aiParameters: additionalMetadata.aiParameters || {},
processingStats: additionalMetadata.processingStats || {}
},
recommendation: this.sanitizeForAudit(recommendation, 6),
auditTrail: auditTrail.map(entry => this.sanitizeForAudit(entry, 4)),
rawContext: {
selectedTools: additionalMetadata.selectedTools || [],
backgroundKnowledge: additionalMetadata.backgroundKnowledge || [],
contextHistory: additionalMetadata.contextHistory || [],
embeddingsSimilarities: additionalMetadata.embeddingsSimilarities || {}
}
};
}
static validateUploadedAnalysis(data: any): { isValid: boolean; issues: string[]; warnings: string[] } {
const issues: string[] = [];
const warnings: string[] = [];
const structureValidation = this.validateAuditExportStructure(data);
if (!structureValidation.isValid) {
issues.push(...structureValidation.errors);
return { isValid: false, issues, warnings };
}
if (data.metadata) {
const timestamp = new Date(data.metadata.timestamp);
if (isNaN(timestamp.getTime())) {
warnings.push('Invalid timestamp in metadata');
} else {
const age = Date.now() - timestamp.getTime();
const maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
if (age > maxAge) {
warnings.push(`Analysis is ${Math.floor(age / (24 * 60 * 60 * 1000))} days old`);
}
}
if (!['workflow', 'tool'].includes(data.metadata.mode)) {
warnings.push(`Unknown analysis mode: ${data.metadata.mode}`);
}
}
if (Array.isArray(data.auditTrail)) {
const aiDecisions = data.auditTrail.filter(e => e.action === 'ai-decision').length;
const toolSelections = data.auditTrail.filter(e => e.action === 'selection-decision').length;
if (aiDecisions === 0) {
warnings.push('No AI decisions found in audit trail');
}
if (toolSelections === 0) {
warnings.push('No tool selections found in audit trail');
}
const entriesWithConfidence = data.auditTrail.filter(e => typeof e.confidence === 'number').length;
const confidenceRatio = entriesWithConfidence / data.auditTrail.length;
if (confidenceRatio < 0.8) {
warnings.push(`Only ${Math.round(confidenceRatio * 100)}% of audit entries have confidence scores`);
}
}
return {
isValid: issues.length === 0,
issues,
warnings
};
}
}

View File

@@ -1,5 +1,4 @@
// src/utils/nextcloud.ts
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
@@ -338,7 +337,7 @@ export class NextcloudUploader {
info: {
path: remotePath,
exists: true,
response: text.substring(0, 200) + '...' // Truncated for safety
response: text.substring(0, 200) + '...'
}
};
}

View File

@@ -1,5 +1,4 @@
// src/utils/rateLimitedQueue.ts
import dotenv from "dotenv";
dotenv.config();

View File

@@ -0,0 +1,83 @@
// src/utils/remarkVideoPlugin.ts
import { visit } from 'unist-util-visit';
import type { Plugin } from 'unified';
import type { Root } from 'hast';
function escapeHtml(unsafe: string): string {
if (typeof unsafe !== 'string') return '';
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
export const remarkVideoPlugin: Plugin<[], Root> = () => {
return (tree: Root) => {
visit(tree, 'html', (node: any, index: number | undefined, parent: any) => {
if (node.value && node.value.includes('<video') && typeof index === 'number') {
const srcMatch = node.value.match(/src=["']([^"']+)["']/);
const titleMatch = node.value.match(/title=["']([^"']+)["']/);
if (srcMatch) {
const originalSrc = srcMatch[1];
const title = titleMatch?.[1] || 'Video';
const hasControls = node.value.includes('controls');
const hasAutoplay = node.value.includes('autoplay');
const hasMuted = node.value.includes('muted');
const hasLoop = node.value.includes('loop');
const preloadMatch = node.value.match(/preload=["']([^"']+)["']/);
const enhancedHTML = `
<div class="video-container">
<video
src="${escapeHtml(originalSrc)}"
${hasControls ? 'controls' : ''}
${hasAutoplay ? 'autoplay' : ''}
${hasMuted ? 'muted' : ''}
${hasLoop ? 'loop' : ''}
${preloadMatch ? `preload="${preloadMatch[1]}"` : 'preload="metadata"'}
data-video-title="${escapeHtml(title)}"
>
<p>Your browser does not support the video element.</p>
</video>
${title !== 'Video' ? `
<div class="video-metadata">
<div class="video-title">${escapeHtml(title)}</div>
</div>
` : ''}
</div>
`.trim();
parent.children[index] = { type: 'html', value: enhancedHTML };
console.log(`[VIDEO] Enhanced: ${title} (${originalSrc})`);
}
}
if (node.value && node.value.includes('<iframe') && typeof index === 'number' && parent) {
if (node.value.includes('video-container')) {
return;
}
const titleMatch = node.value.match(/title=["']([^"']+)["']/);
const title = titleMatch?.[1] || 'Embedded Video';
const enhancedHTML = `
<div class="video-container">
${node.value}
</div>
<div class="video-metadata">
<div class="video-title">${escapeHtml(title)}</div>
</div>
`.trim();
parent.children[index] = { type: 'html', value: enhancedHTML };
console.log(`[VIDEO] Enhanced iframe: ${title}`);
}
});
};
};

View File

@@ -1,43 +0,0 @@
export interface Tool {
name: string;
type?: 'software' | 'method' | 'concept';
projectUrl?: string | null;
license?: string;
knowledgebase?: boolean;
domains?: string[];
phases?: string[];
platforms?: string[];
skillLevel?: string;
description?: string;
tags?: string[];
related_concepts?: string[];
}
export function createToolSlug(toolName: string): string {
if (!toolName || typeof toolName !== 'string') {
console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
return '';
}
return toolName.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
}
export function findToolByIdentifier(tools: Tool[], identifier: string): Tool | undefined {
if (!identifier || !Array.isArray(tools)) return undefined;
return tools.find(tool =>
tool.name === identifier ||
createToolSlug(tool.name) === identifier.toLowerCase()
);
}
export function isToolHosted(tool: Tool): boolean {
return tool.projectUrl !== undefined &&
tool.projectUrl !== null &&
tool.projectUrl !== "" &&
tool.projectUrl.trim() !== "";
}

372
src/utils/toolSelector.ts Normal file
View File

@@ -0,0 +1,372 @@
// src/utils/toolSelector.ts
import { aiService } from './aiService.js';
import { embeddingsService, type SimilarityResult } from './embeddings.js';
import { confidenceScoring } from './confidenceScoring.js';
import { JSONParser } from './jsonUtils.js';
import { getPrompt } from '../config/prompts.js';
import 'dotenv/config';
export interface ToolSelectionConfig {
maxSelectedItems: number;
embeddingCandidates: number;
similarityThreshold: number;
embeddingSelectionLimit: number;
embeddingConceptsLimit: number;
embeddingsMinTools: number;
embeddingsMaxReductionRatio: number;
methodSelectionRatio: number;
softwareSelectionRatio: number;
}
export interface SelectionContext {
userQuery: string;
mode: string;
embeddingsSimilarities: Map<string, number>;
seenToolNames: Set<string>;
selectedTools?: Array<{
tool: any;
phase: string;
priority: string;
justification?: string;
taskRelevance?: number;
limitations?: string[];
}>;
}
export interface ToolSelectionResult {
selectedTools: any[];
selectedConcepts: any[];
confidence: number;
}
class ToolSelector {
private config: ToolSelectionConfig;
constructor() {
this.config = {
maxSelectedItems: this.getEnvInt('AI_MAX_SELECTED_ITEMS', 25),
embeddingCandidates: this.getEnvInt('AI_EMBEDDING_CANDIDATES', 50),
similarityThreshold: this.getEnvFloat('AI_SIMILARITY_THRESHOLD', 0.3),
embeddingSelectionLimit: this.getEnvInt('AI_EMBEDDING_SELECTION_LIMIT', 30),
embeddingConceptsLimit: this.getEnvInt('AI_EMBEDDING_CONCEPTS_LIMIT', 15),
embeddingsMinTools: this.getEnvInt('AI_EMBEDDINGS_MIN_TOOLS', 8),
embeddingsMaxReductionRatio: this.getEnvFloat('AI_EMBEDDINGS_MAX_REDUCTION_RATIO', 0.75),
methodSelectionRatio: this.getEnvFloat('AI_METHOD_SELECTION_RATIO', 0.4),
softwareSelectionRatio: this.getEnvFloat('AI_SOFTWARE_SELECTION_RATIO', 0.5),
};
console.log('[TOOL-SELECTOR] Initialized with config:', this.config);
}
private getEnvInt(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseInt(value, 10) : defaultValue;
}
private getEnvFloat(key: string, defaultValue: number): number {
const value = process.env[key];
return value ? parseFloat(value) : defaultValue;
}
async getIntelligentCandidates(
userQuery: string,
toolsData: any,
mode: string,
context: SelectionContext
): Promise<{
tools: any[];
concepts: any[];
domains: any[];
phases: any[];
'domain-agnostic-software': any[];
}> {
console.log('[TOOL-SELECTOR] Getting intelligent candidates for query');
let candidateTools: any[] = [];
let candidateConcepts: any[] = [];
context.embeddingsSimilarities.clear();
try {
await embeddingsService.waitForInitialization();
} catch (error) {
console.error('[TOOL-SELECTOR] Embeddings initialization failed:', error);
}
console.log('[TOOL-SELECTOR] Using embeddings for candidate selection');
const embeddingsSearchStart = Date.now();
const similarItems = await embeddingsService.findSimilar(
userQuery,
this.config.embeddingCandidates,
this.config.similarityThreshold
) as SimilarityResult[];
console.log('[TOOL-SELECTOR] Embeddings found', similarItems.length, 'similar items');
const { auditService } = await import('./auditService.js');
const { getDataVersion } = await import('./dataService.js');
const toolsDataHash = getDataVersion() || 'unknown';
auditService.addEmbeddingsSearch(
userQuery,
similarItems,
this.config.similarityThreshold,
embeddingsSearchStart,
{
toolsDataHash: toolsDataHash,
selectionPhase: 'initial-candidate-selection',
candidateLimit: this.config.embeddingCandidates,
mode: mode,
reasoning: `Initiale semantische Suche für ${mode}-Modus - Reduzierung der ${toolsData.tools.length} verfügbaren Tools auf ${similarItems.length} relevante Kandidaten`
}
);
similarItems.forEach(item => {
context.embeddingsSimilarities.set(item.name, item.similarity);
});
const toolsMap = new Map(toolsData.tools.map((tool: any) => [tool.name, tool]));
const conceptsMap = new Map(toolsData.concepts.map((concept: any) => [concept.name, concept]));
const similarTools = similarItems
.filter((item: any) => item.type === 'tool')
.map((item: any) => toolsMap.get(item.name))
.filter((tool: any): tool is NonNullable<any> => tool !== undefined && tool !== null);
const similarConcepts = similarItems
.filter((item: any) => item.type === 'concept')
.map((item: any) => conceptsMap.get(item.name))
.filter((concept: any): concept is NonNullable<any> => concept !== undefined && concept !== null);
const totalAvailableTools = toolsData.tools.length;
const reductionRatio = similarTools.length / totalAvailableTools;
if (similarTools.length >= this.config.embeddingsMinTools && reductionRatio <= this.config.embeddingsMaxReductionRatio) {
candidateTools = similarTools;
candidateConcepts = similarConcepts;
console.log('[TOOL-SELECTOR] Using embeddings filtering:', totalAvailableTools, '→', similarTools.length, 'tools');
} else {
console.log('[TOOL-SELECTOR] Embeddings filtering insufficient, using full dataset');
candidateTools = toolsData.tools;
candidateConcepts = toolsData.concepts;
}
const selection = await this.performAISelection(
userQuery,
candidateTools,
candidateConcepts,
mode,
context
);
return {
tools: selection.selectedTools,
concepts: selection.selectedConcepts,
domains: toolsData.domains,
phases: toolsData.phases,
'domain-agnostic-software': toolsData['domain-agnostic-software']
};
}
private async performAISelection(
userQuery: string,
candidateTools: any[],
candidateConcepts: any[],
mode: string,
context: SelectionContext
): Promise<ToolSelectionResult> {
console.log('[TOOL-SELECTOR] Performing AI selection');
const candidateMethods = candidateTools.filter((t: any) => t && t.type === 'method');
const candidateSoftware = candidateTools.filter((t: any) => t && t.type === 'software');
console.log('[TOOL-SELECTOR] Candidates:',
candidateMethods.length, 'methods,',
candidateSoftware.length, 'software,',
candidateConcepts.length, 'concepts'
);
const methodsWithFullData = candidateMethods.map(this.createToolData);
const softwareWithFullData = candidateSoftware.map(this.createToolData);
const conceptsWithFullData = candidateConcepts.map(this.createConceptData);
const maxTools = Math.min(this.config.embeddingSelectionLimit, candidateTools.length);
const maxConcepts = Math.min(this.config.embeddingConceptsLimit, candidateConcepts.length);
const methodRatio = Math.max(0, Math.min(1, this.config.methodSelectionRatio));
const softwareRatio = Math.max(0, Math.min(1, this.config.softwareSelectionRatio));
let methodLimit = Math.round(maxTools * methodRatio);
let softwareLimit = Math.round(maxTools * softwareRatio);
if (methodLimit + softwareLimit > maxTools) {
const scale = maxTools / (methodLimit + softwareLimit);
methodLimit = Math.floor(methodLimit * scale);
softwareLimit = Math.floor(softwareLimit * scale);
}
const methodsPrimary = methodsWithFullData.slice(0, methodLimit);
const softwarePrimary = softwareWithFullData.slice(0, softwareLimit);
const toolsToSend: any[] = [...methodsPrimary, ...softwarePrimary];
let mIdx = methodsPrimary.length;
let sIdx = softwarePrimary.length;
while (toolsToSend.length < maxTools && (mIdx < methodsWithFullData.length || sIdx < softwareWithFullData.length)) {
const remM = methodsWithFullData.length - mIdx;
const remS = softwareWithFullData.length - sIdx;
if (remS >= remM && sIdx < softwareWithFullData.length) {
toolsToSend.push(softwareWithFullData[sIdx++]);
} else if (mIdx < methodsWithFullData.length) {
toolsToSend.push(methodsWithFullData[mIdx++]);
} else if (sIdx < softwareWithFullData.length) {
toolsToSend.push(softwareWithFullData[sIdx++]);
} else {
break;
}
}
const conceptsToSend = conceptsWithFullData.slice(0, maxConcepts);
console.log('[TOOL-SELECTOR-DEBUG] maxTools:', maxTools, 'maxConcepts:', maxConcepts);
console.log('[TOOL-SELECTOR] Sending to AI:',
toolsToSend.filter((t: any) => t.type === 'method').length, 'methods,',
toolsToSend.filter((t: any) => t.type === 'software').length, 'software,',
conceptsToSend.length, 'concepts'
);
const basePrompt = getPrompt('toolSelection', mode, userQuery, this.config.maxSelectedItems);
const prompt = getPrompt('toolSelectionWithData', basePrompt, toolsToSend, conceptsToSend);
try {
const response = await aiService.callAI(prompt);
const result = JSONParser.safeParseJSON(response.content, null);
if (!result || !Array.isArray(result.selectedTools) || !Array.isArray(result.selectedConcepts)) {
console.error('[TOOL-SELECTOR] AI selection returned invalid structure');
throw new Error('AI selection failed to return valid tool and concept selection');
}
const totalSelected = result.selectedTools.length + result.selectedConcepts.length;
if (totalSelected === 0) {
throw new Error('AI selection returned empty selection');
}
const toolsMap = new Map(candidateTools.map((tool: any) => [tool.name, tool]));
const conceptsMap = new Map(candidateConcepts.map((concept: any) => [concept.name, concept]));
const selectedTools = result.selectedTools
.map((name: string) => toolsMap.get(name))
.filter((tool: any): tool is NonNullable<any> => tool !== undefined && tool !== null);
const selectedConcepts = result.selectedConcepts
.map((name: string) => conceptsMap.get(name))
.filter((concept: any): concept is NonNullable<any> => concept !== undefined && concept !== null);
const selectedMethods = selectedTools.filter((t: any) => t && t.type === 'method');
const selectedSoftware = selectedTools.filter((t: any) => t && t.type === 'software');
console.log('[TOOL-SELECTOR] AI selected:',
selectedMethods.length, 'methods,',
selectedSoftware.length, 'software,',
selectedConcepts.length, 'concepts'
);
const confidence = confidenceScoring.calculateSelectionConfidence(
result,
candidateTools.length + candidateConcepts.length
);
return { selectedTools, selectedConcepts, confidence };
} catch (error) {
console.error('[TOOL-SELECTOR] AI selection failed:', error);
throw error;
}
}
async selectToolsForPhase(
userQuery: string,
phase: any,
availableTools: any[],
context: SelectionContext
): Promise<Array<{
toolName: string;
taskRelevance: number;
justification: string;
limitations: string[];
}>> {
console.log('[TOOL-SELECTOR] Selecting tools for phase:', phase.id);
if (availableTools.length === 0) {
console.log('[TOOL-SELECTOR] No tools available for phase:', phase.id);
return [];
}
const prompt = getPrompt('phaseToolSelection', userQuery, phase, availableTools);
try {
const response = await aiService.callMicroTaskAI(prompt);
const selections = JSONParser.safeParseJSON(response.content, []);
if (Array.isArray(selections)) {
const validSelections = selections.filter((sel: any) => {
const matchingTool = availableTools.find((tool: any) => tool && tool.name === sel.toolName);
if (!matchingTool) {
console.warn('[TOOL-SELECTOR] Invalid tool selection for phase:', phase.id, sel.toolName);
}
return !!matchingTool;
});
console.log('[TOOL-SELECTOR] Valid selections for phase:', phase.id, validSelections.length);
return validSelections;
}
return [];
} catch (error) {
console.error('[TOOL-SELECTOR] Phase tool selection failed:', error);
return [];
}
}
private createToolData = (tool: any) => ({
name: tool.name,
type: tool.type,
description: tool.description,
domains: tool.domains,
phases: tool.phases,
platforms: tool.platforms || [],
tags: tool.tags || [],
skillLevel: tool.skillLevel,
license: tool.license,
accessType: tool.accessType,
projectUrl: tool.projectUrl,
knowledgebase: tool.knowledgebase,
related_concepts: tool.related_concepts || [],
related_software: tool.related_software || []
});
private createConceptData = (concept: any) => ({
name: concept.name,
type: 'concept',
description: concept.description,
domains: concept.domains,
phases: concept.phases,
tags: concept.tags || [],
skillLevel: concept.skillLevel,
related_concepts: concept.related_concepts || [],
related_software: concept.related_software || []
});
getConfig(): ToolSelectionConfig {
return { ...this.config };
}
}
export const toolSelector = new ToolSelector();

2082
tools-yaml-editor.html Normal file

File diff suppressed because it is too large Load Diff