forensic-pathways/tools-yaml-editor.html
2025-08-10 16:20:29 +02:00

2082 lines
86 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tools.yaml Editor</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/4.1.0/js-yaml.min.js"></script>
<style>
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #e9ecef;
--text-primary: #212529;
--text-secondary: #6c757d;
--border-color: #dee2e6;
--accent-color: #0d6efd;
--success-color: #198754;
--warning-color: #fd7e14;
--danger-color: #dc3545;
--shadow: 0 2px 4px rgba(0,0,0,0.1);
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #404040;
--text-primary: #ffffff;
--text-secondary: #b0b0b0;
--border-color: #404040;
--accent-color: #4a9eff;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--shadow: 0 2px 4px rgba(0,0,0,0.3);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
line-height: 1.5;
min-width: 1200px;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: var(--shadow);
}
.controls {
display: flex;
gap: 15px;
align-items: center;
}
.search-section {
display: grid;
grid-template-columns: 1fr auto;
gap: 20px;
margin-bottom: 20px;
padding: 20px;
background: var(--bg-secondary);
border-radius: 8px;
box-shadow: var(--shadow);
}
.search-filters {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: 15px;
align-items: end;
}
.bulk-actions {
display: flex;
gap: 10px;
}
.main-content {
display: grid;
grid-template-columns: 1fr 500px;
gap: 20px;
}
.tools-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
}
.edit-section {
background: var(--bg-secondary);
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
position: sticky;
top: 20px;
max-height: calc(100vh - 40px);
overflow-y: auto;
}
.tools-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.tools-list {
max-height: 70vh;
overflow-y: auto;
}
.tool-item {
padding: 15px;
border: 1px solid var(--border-color);
border-radius: 6px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.2s;
background: var(--bg-primary);
}
.tool-item:hover {
background: var(--bg-tertiary);
}
.tool-item.selected {
border-color: var(--accent-color);
background: var(--bg-tertiary);
}
.tool-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.tool-name {
font-weight: 600;
color: var(--accent-color);
margin-bottom: 4px;
}
.tool-meta {
display: flex;
gap: 10px;
font-size: 0.85em;
color: var(--text-secondary);
}
.tool-description {
font-size: 0.9em;
margin-top: 8px;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
}
.tag {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
}
.form-group {
margin-bottom: 15px;
}
.form-label {
display: block;
margin-bottom: 5px;
font-weight: 500;
}
.form-input, .form-select, .form-textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}
.form-textarea {
resize: vertical;
min-height: 80px;
}
.form-checkbox {
margin-right: 8px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.btn-primary {
background: var(--accent-color);
color: white;
}
.btn-secondary {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-success {
background: var(--success-color);
color: white;
}
.btn-warning {
background: var(--warning-color);
color: white;
}
.btn-danger {
background: var(--danger-color);
color: white;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.autocomplete-container {
position: relative;
}
.autocomplete-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-top: none;
border-radius: 0 0 4px 4px;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
}
.autocomplete-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--border-color);
}
.autocomplete-item:hover,
.autocomplete-item.selected {
background: var(--bg-tertiary);
}
.autocomplete-item:last-child {
border-bottom: none;
}
.status-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.9em;
}
.theme-toggle {
background: none;
border: 1px solid var(--border-color);
color: var(--text-primary);
border-radius: 4px;
cursor: pointer;
padding: 6px 10px;
}
.validation-errors {
background: var(--danger-color);
color: white;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
display: none;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.stat-item {
text-align: center;
padding: 10px;
background: var(--bg-tertiary);
border-radius: 4px;
}
.stat-number {
font-size: 1.5em;
font-weight: bold;
color: var(--accent-color);
}
.bulk-panel {
display: none;
background: var(--bg-tertiary);
padding: 15px;
border-radius: 6px;
margin-top: 15px;
}
.bulk-panel.active {
display: block;
}
.tag-input-container {
position: relative;
}
.tag-input {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 36px;
padding: 4px 8px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
cursor: text;
}
.tag-input-item {
background: var(--accent-color);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
display: flex;
align-items: center;
gap: 4px;
}
.tag-input-item .remove {
cursor: pointer;
font-weight: bold;
}
.tag-input input {
border: none;
background: none;
outline: none;
color: var(--text-primary);
min-width: 100px;
flex: 1;
}
.section-tabs {
display: flex;
margin-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
.section-tab {
padding: 10px 20px;
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.section-tab.active {
color: var(--accent-color);
border-bottom-color: var(--accent-color);
}
.section-content {
display: none;
}
.section-content.active {
display: block;
}
.categories-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 4px;
margin-bottom: 15px;
}
.category-item {
padding: 10px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
transition: background 0.2s;
}
.category-item:hover {
background: var(--bg-tertiary);
}
.category-item:last-child {
border-bottom: none;
}
.category-item.selected {
background: var(--bg-tertiary);
border-left: 3px solid var(--accent-color);
}
.category-name {
font-weight: 600;
margin-bottom: 4px;
}
.category-description {
font-size: 0.9em;
color: var(--text-secondary);
}
</style>
</head>
<body data-theme="dark">
<div class="container">
<header class="header">
<h1>🔧 Tools.yaml Editor</h1>
<div class="controls">
<button class="theme-toggle" onclick="toggleTheme()">☀️</button>
<input type="file" id="importFile" accept=".yaml,.yml" style="display: none;" onchange="importYAML(event)">
<button class="btn btn-secondary" onclick="document.getElementById('importFile').click()">Import YAML</button>
<button class="btn btn-primary" onclick="exportYAML()">Export YAML</button>
<button class="btn btn-success" onclick="addNewTool()">Add Tool</button>
</div>
</header>
<section class="search-section">
<div class="search-filters">
<div class="form-group">
<label class="form-label">Search Tools</label>
<div class="autocomplete-container">
<input type="text" id="searchInput" class="form-input" placeholder="Search by name, description, tags..." oninput="filterTools()">
<div class="autocomplete-dropdown" id="searchAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Type</label>
<select id="typeFilter" class="form-select" onchange="filterTools()">
<option value="">All Types</option>
<option value="software">Software</option>
<option value="method">Method</option>
<option value="concept">Concept</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Skill Level</label>
<select id="skillFilter" class="form-select" onchange="filterTools()">
<option value="">All Levels</option>
<option value="novice">Novice</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
<option value="expert">Expert</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Domain</label>
<select id="domainFilter" class="form-select" onchange="filterTools()">
<option value="">All Domains</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Knowledgebase</label>
<select id="knowledgebaseFilter" class="form-select" onchange="filterTools()">
<option value="">All</option>
<option value="true">Has KB</option>
<option value="false">No KB</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Validation</label>
<select id="validationFilter" class="form-select" onchange="filterTools()">
<option value="">All</option>
<option value="valid">Valid Only</option>
<option value="invalid">Has Issues</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Sort by</label>
<select id="sortBy" class="form-select" onchange="sortTools()">
<option value="name">Name</option>
<option value="type">Type</option>
<option value="skillLevel">Skill Level</option>
<option value="domains">Domain</option>
</select>
</div>
</div>
<div class="bulk-actions">
<button class="btn btn-warning btn-sm" onclick="validateAllTools()">Validate All</button>
<button class="btn btn-secondary btn-sm" onclick="clearAllFilters()">Clear Filters</button>
<button class="btn btn-secondary btn-sm" onclick="toggleBulkPanel()">Bulk Actions</button>
<button class="btn btn-secondary btn-sm" onclick="showStats()">Statistics</button>
</div>
</section>
<div class="bulk-panel" id="bulkPanel">
<h3>Bulk Operations</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 10px;">
<div>
<button class="btn btn-warning btn-sm" onclick="bulkRemoveTags()">Remove Tags</button>
<input type="text" id="bulkRemoveTagsInput" placeholder="Tags to remove (comma-separated)" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
<div>
<button class="btn btn-warning btn-sm" onclick="bulkRenameTags()">Rename Tags</button>
<input type="text" id="bulkRenameFrom" placeholder="From tag" class="form-input" style="margin-top: 5px; font-size: 12px;">
<input type="text" id="bulkRenameTo" placeholder="To tag" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
<div>
<button class="btn btn-success btn-sm" onclick="bulkAssignRelatedConcepts()">Assign Related Concepts</button>
<input type="text" id="bulkConceptsInput" placeholder="Concepts (comma-separated)" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
<div>
<button class="btn btn-success btn-sm" onclick="bulkAssignRelatedSoftware()">Assign Related Software</button>
<input type="text" id="bulkSoftwareInput" placeholder="Software (comma-separated)" class="form-input" style="margin-top: 5px; font-size: 12px;">
</div>
</div>
</div>
<div class="main-content">
<section class="tools-section">
<div class="tools-header">
<h2>Tools (<span id="toolCount">0</span>)</h2>
<div style="display: flex; gap: 10px; align-items: center;">
<div id="validationSummary" style="display: none; padding: 5px 10px; background: var(--warning-color); color: white; border-radius: 4px; font-size: 0.9em;">
<span id="validationCount">0</span> tools need fixing
</div>
<button class="btn btn-danger btn-sm" onclick="clearSelection()">Clear Selection</button>
</div>
</div>
<div class="stats" id="statsContainer" style="display: none;"></div>
<div class="validation-errors" id="validationErrors"></div>
<div class="tools-list" id="toolsList"></div>
</section>
<section class="edit-section">
<div class="section-tabs">
<button class="section-tab active" onclick="switchSection('tools')">Tools</button>
<button class="section-tab" onclick="switchSection('domains')">Domains</button>
<button class="section-tab" onclick="switchSection('phases')">Phases</button>
<button class="section-tab" onclick="switchSection('categories')">Domain-Agnostic</button>
<button class="section-tab" onclick="switchSection('scenarios')">Scenarios</button>
</div>
<!-- Tools Section -->
<div id="tools-section" class="section-content active">
<h2 id="editTitle">Tool Editor</h2>
<form id="toolForm">
<div class="form-group">
<label class="form-label">Name *</label>
<input type="text" id="name" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Type *</label>
<select id="type" class="form-select" required>
<option value="">Select type...</option>
<option value="software">Software</option>
<option value="method">Method</option>
<option value="concept">Concept</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Icon</label>
<input type="text" id="icon" class="form-input" placeholder="🔧">
</div>
<div class="form-group">
<label class="form-label">Description *</label>
<textarea id="description" class="form-textarea" required placeholder="Detailed description of the tool..."></textarea>
</div>
<div class="form-group">
<label class="form-label">URL *</label>
<input type="url" id="url" class="form-input" required placeholder="https://example.com">
</div>
<div class="form-group">
<label class="form-label">Project URL</label>
<input type="url" id="projectUrl" class="form-input" placeholder="https://project.example.com">
</div>
<div class="form-group">
<label class="form-label">Status URL</label>
<input type="url" id="statusUrl" class="form-input" placeholder="https://status.example.com">
</div>
<div class="form-group">
<label class="form-label">Skill Level *</label>
<select id="skillLevel" class="form-select" required>
<option value="">Select level...</option>
<option value="novice">Novice</option>
<option value="beginner">Beginner</option>
<option value="intermediate">Intermediate</option>
<option value="advanced">Advanced</option>
<option value="expert">Expert</option>
</select>
</div>
<div class="form-group">
<label class="form-label">License</label>
<input type="text" id="license" class="form-input" placeholder="MIT, GPL-3.0, Proprietary...">
</div>
<div class="form-group">
<label class="form-label">Access Type</label>
<select id="accessType" class="form-select">
<option value="">Select access type...</option>
<option value="download">Download</option>
<option value="web">Web</option>
<option value="api">API</option>
<option value="cli">CLI</option>
<option value="commercial">Commercial</option>
<option value="server-based">Server-based</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Domains</label>
<div class="tag-input-container">
<div class="tag-input" id="domainsInput" onclick="focusTagInput('domainsInput')">
<input type="text" placeholder="Type domain..." onkeydown="handleTagInput(event, 'domains')" oninput="showAutocomplete(event, 'domains')">
</div>
<div class="autocomplete-dropdown" id="domainsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Phases</label>
<div class="tag-input-container">
<div class="tag-input" id="phasesInput" onclick="focusTagInput('phasesInput')">
<input type="text" placeholder="Type phase..." onkeydown="handleTagInput(event, 'phases')" oninput="showAutocomplete(event, 'phases')">
</div>
<div class="autocomplete-dropdown" id="phasesAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Platforms</label>
<div class="tag-input-container">
<div class="tag-input" id="platformsInput" onclick="focusTagInput('platformsInput')">
<input type="text" placeholder="Type platform..." onkeydown="handleTagInput(event, 'platforms')" oninput="showAutocomplete(event, 'platforms')">
</div>
<div class="autocomplete-dropdown" id="platformsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Domain-Agnostic Categories</label>
<div class="tag-input-container">
<div class="tag-input" id="domain-agnostic-softwareInput" onclick="focusTagInput('domain-agnostic-softwareInput')">
<input type="text" placeholder="Type category..." onkeydown="handleTagInput(event, 'domain-agnostic-software')" oninput="showAutocomplete(event, 'domain-agnostic-software')">
</div>
<div class="autocomplete-dropdown" id="domain-agnostic-softwareAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Tags</label>
<div class="tag-input-container">
<div class="tag-input" id="tagsInput" onclick="focusTagInput('tagsInput')">
<input type="text" placeholder="Type tag..." onkeydown="handleTagInput(event, 'tags')" oninput="showAutocomplete(event, 'tags')">
</div>
<div class="autocomplete-dropdown" id="tagsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Related Concepts</label>
<div class="tag-input-container">
<div class="tag-input" id="related_conceptsInput" onclick="focusTagInput('related_conceptsInput')">
<input type="text" placeholder="Type concept..." onkeydown="handleTagInput(event, 'related_concepts')" oninput="showAutocomplete(event, 'related_concepts')">
</div>
<div class="autocomplete-dropdown" id="related_conceptsAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label class="form-label">Related Software</label>
<div class="tag-input-container">
<div class="tag-input" id="related_softwareInput" onclick="focusTagInput('related_softwareInput')">
<input type="text" placeholder="Type software..." onkeydown="handleTagInput(event, 'related_software')" oninput="showAutocomplete(event, 'related_software')">
</div>
<div class="autocomplete-dropdown" id="related_softwareAutocomplete"></div>
</div>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="knowledgebase" class="form-checkbox">
Has Knowledgebase
</label>
</div>
<div class="form-group" style="display: flex; gap: 10px; margin-top: 20px;">
<button type="button" class="btn btn-primary" onclick="saveTool()">Save Tool</button>
<button type="button" class="btn btn-secondary" onclick="clearForm()">Clear</button>
<button type="button" class="btn btn-danger" onclick="deleteTool()" id="deleteBtn" style="display: none;">Delete</button>
</div>
</form>
</div>
<!-- Domain-Agnostic Categories Section -->
<div id="categories-section" class="section-content">
<h2>Domain-Agnostic Software Categories</h2>
<div class="categories-list" id="categoriesList"></div>
<form id="categoryForm">
<div class="form-group">
<label class="form-label">Category ID *</label>
<input type="text" id="categoryId" class="form-input" required placeholder="category-id">
</div>
<div class="form-group">
<label class="form-label">Category Name *</label>
<input type="text" id="categoryName" class="form-input" required placeholder="Category Name">
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea id="categoryDescription" class="form-textarea" placeholder="Category description..."></textarea>
</div>
<div class="form-group">
<label class="form-label">Use Cases</label>
<div class="tag-input-container">
<div class="tag-input" id="useCasesInput" onclick="focusTagInput('useCasesInput')">
<input type="text" placeholder="Type use case..." onkeydown="handleTagInput(event, 'useCases')">
</div>
</div>
</div>
<div class="form-group" style="display: flex; gap: 10px;">
<button type="button" class="btn btn-primary" onclick="saveCategory()">Save Category</button>
<button type="button" class="btn btn-secondary" onclick="clearCategoryForm()">Clear</button>
<button type="button" class="btn btn-danger" onclick="deleteCategory()" id="deleteCategoryBtn" style="display: none;">Delete</button>
</div>
</form>
</div>
<!-- Other sections placeholder -->
<div id="domains-section" class="section-content">
<h2>Domains Management</h2>
<p>Domain management interface would go here...</p>
</div>
<div id="phases-section" class="section-content">
<h2>Phases Management</h2>
<p>Phases management interface would go here...</p>
</div>
<div id="scenarios-section" class="section-content">
<h2>Scenarios Management</h2>
<p>Scenarios management interface would go here...</p>
</div>
</section>
</div>
</div>
<div class="status-bar">
<div id="statusText">Ready</div>
<div style="font-size: 0.8em; color: var(--text-secondary);">
Tools.yaml Editor v1.1 |
Shortcuts: Ctrl+S (Save), Ctrl+N (New), Ctrl+E (Export), Alt+V (Validate), Ctrl+F (Search), Esc (Clear)
</div>
</div>
<script>
// Global data storage
let toolsData = {
tools: [],
domains: [],
phases: [],
'domain-agnostic-software': [],
scenarios: [],
skill_levels: {}
};
let filteredTools = [];
let selectedTool = null;
let editingIndex = -1;
let selectedCategory = null;
let editingCategoryIndex = -1;
let currentSection = 'tools';
// Initialize the application
function init() {
const savedTheme = localStorage.getItem('tools-editor-theme') || 'dark';
document.body.setAttribute('data-theme', savedTheme);
const themeToggle = document.querySelector('.theme-toggle');
themeToggle.textContent = savedTheme === 'light' ? '🌙' : '☀️';
// Load sample data if none exists
if (toolsData.domains.length === 0) {
toolsData.domains = [
{ id: 'incident-response', name: 'Incident Response' },
{ id: 'static-investigations', name: 'Static Investigations' },
{ id: 'malware-analysis', name: 'Malware Analysis' },
{ id: 'network-forensics', name: 'Network Forensics' },
{ id: 'mobile-forensics', name: 'Mobile Forensics' },
{ id: 'cloud-forensics', name: 'Cloud Forensics' }
];
}
if (toolsData.phases.length === 0) {
toolsData.phases = [
{ id: 'data-collection', name: 'Data Collection' },
{ id: 'examination', name: 'Examination' },
{ id: 'analysis', name: 'Analysis' },
{ id: 'reporting', name: 'Reporting' }
];
}
if (toolsData['domain-agnostic-software'].length === 0) {
toolsData['domain-agnostic-software'] = [
{ id: 'data-recovery', name: 'Data Recovery', description: 'Tools for recovering deleted or corrupted data' },
{ id: 'file-analysis', name: 'File Analysis', description: 'Tools for analyzing file contents and metadata' },
{ id: 'documentation', name: 'Documentation', description: 'Tools for creating reports and documentation' }
];
}
populateFilters();
// ✅ Initialize filteredTools to everything at startup
filteredTools = Array.isArray(toolsData.tools) ? toolsData.tools.slice() : [];
renderTools();
renderCategories();
updateStatus('Application initialized');
}
// Section switching
function switchSection(section) {
currentSection = section;
document.querySelectorAll('.section-tab').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.section-content').forEach(content => content.classList.remove('active'));
document.querySelector(`[onclick="switchSection('${section}')"]`).classList.add('active');
document.getElementById(`${section}-section`).classList.add('active');
if (section === 'categories') {
renderCategories();
}
}
// Theme management
function toggleTheme() {
const body = document.body;
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
body.setAttribute('data-theme', newTheme);
localStorage.setItem('tools-editor-theme', newTheme);
const themeToggle = document.querySelector('.theme-toggle');
themeToggle.textContent = newTheme === 'light' ? '🌙' : '☀️';
updateStatus(`Switched to ${newTheme} theme`);
}
// Status updates
function updateStatus(message) {
document.getElementById('statusText').textContent = message;
}
// Populate filter dropdowns
function populateFilters() {
const domainFilter = document.getElementById('domainFilter');
domainFilter.innerHTML = '<option value="">All Domains</option>';
toolsData.domains.forEach(domain => {
const option = document.createElement('option');
option.value = domain.id;
option.textContent = domain.name;
domainFilter.appendChild(option);
});
}
// Get all unique values for autocomplete
function getUniqueValues(field) {
const values = new Set();
toolsData.tools.forEach(tool => {
if (tool[field]) {
if (Array.isArray(tool[field])) {
tool[field].forEach(value => values.add(value));
} else {
values.add(tool[field]);
}
}
});
// Add predefined values
if (field === 'domains') {
toolsData.domains.forEach(domain => values.add(domain.id));
} else if (field === 'phases') {
toolsData.phases.forEach(phase => values.add(phase.id));
} else if (field === 'platforms') {
['Windows', 'macOS', 'Linux', 'Web', 'Mobile', 'Cross-platform'].forEach(p => values.add(p));
} else if (field === 'domain-agnostic-software') {
toolsData['domain-agnostic-software'].forEach(cat => values.add(cat.id));
} else if (field === 'related_concepts') {
toolsData.tools.filter(t => t.type === 'concept').forEach(t => values.add(t.name));
} else if (field === 'related_software') {
toolsData.tools.filter(t => t.type === 'software').forEach(t => values.add(t.name));
}
return Array.from(values).sort();
}
// Tag input handling - consolidated function
function focusTagInput(containerId) {
const input = document.querySelector(`#${containerId} input`);
input.focus();
}
function handleTagInput(event, field) {
const dropdown = document.getElementById(`${field}Autocomplete`);
const items = dropdown ? dropdown.querySelectorAll('.autocomplete-item') : [];
let selectedIndex = Array.from(items).findIndex(item => item.classList.contains('selected'));
if (event.key === 'ArrowDown') {
event.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
updateAutocompleteSelection(items, selectedIndex);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
updateAutocompleteSelection(items, selectedIndex);
} else if (event.key === 'Enter') {
event.preventDefault();
if (selectedIndex >= 0 && items[selectedIndex]) {
const value = items[selectedIndex].textContent;
addTag(field, value);
event.target.value = '';
hideAutocomplete(field);
} else {
const value = event.target.value.trim();
if (value) {
addTag(field, value);
event.target.value = '';
hideAutocomplete(field);
}
}
} else if (event.key === ',' || event.key === 'Tab') {
event.preventDefault();
const input = event.target;
const value = input.value.trim();
if (value) {
addTag(field, value);
input.value = '';
hideAutocomplete(field);
}
} else if (event.key === 'Backspace' && event.target.value === '') {
removeLastTag(field);
}
}
function updateAutocompleteSelection(items, selectedIndex) {
items.forEach((item, index) => {
item.classList.toggle('selected', index === selectedIndex);
});
}
function addTag(field, value) {
const container = document.getElementById(`${field}Input`);
const input = container.querySelector('input');
// Check if tag already exists
const existingTags = getTagValues(field);
if (existingTags.includes(value)) return;
const tagElement = document.createElement('div');
tagElement.className = 'tag-input-item';
tagElement.innerHTML = `${value} <span class="remove" onclick="removeTag('${field}', this)">×</span>`;
container.insertBefore(tagElement, input);
}
function removeTag(field, element) {
element.parentElement.remove();
}
function removeLastTag(field) {
const container = document.getElementById(`${field}Input`);
const tags = container.querySelectorAll('.tag-input-item');
if (tags.length > 0) {
tags[tags.length - 1].remove();
}
}
function getTagValues(field) {
const container = document.getElementById(`${field}Input`);
if (!container) return [];
return Array.from(container.querySelectorAll('.tag-input-item')).map(item =>
item.textContent.replace('×', '').trim()
);
}
function setTagValues(field, values) {
const container = document.getElementById(`${field}Input`);
if (!container) return;
// Clear existing tags
container.querySelectorAll('.tag-input-item').forEach(item => item.remove());
// Add new tags
if (values && Array.isArray(values)) {
values.forEach(value => addTag(field, value));
}
}
// Autocomplete functionality
function showAutocomplete(event, field) {
const input = event.target;
const value = input.value.toLowerCase();
const dropdown = document.getElementById(`${field}Autocomplete`);
if (!dropdown || value.length < 1) {
hideAutocomplete(field);
return;
}
const suggestions = getUniqueValues(field).filter(item =>
item.toLowerCase().includes(value)
);
if (suggestions.length === 0) {
hideAutocomplete(field);
return;
}
dropdown.innerHTML = '';
suggestions.slice(0, 10).forEach((suggestion, index) => {
const item = document.createElement('div');
item.className = 'autocomplete-item';
if (index === 0) item.classList.add('selected');
item.textContent = suggestion;
item.onclick = () => {
addTag(field, suggestion);
input.value = '';
hideAutocomplete(field);
};
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
}
function hideAutocomplete(field) {
const dropdown = document.getElementById(`${field}Autocomplete`);
if (dropdown) dropdown.style.display = 'none';
}
// Consolidated validation function
function validateTool(tool, index = null) {
const prefix = index !== null ? `Tool ${index + 1} (${tool.name || 'Unnamed'}): ` : '';
const errors = [];
// Required fields validation
if (!tool.name || typeof tool.name !== 'string' || tool.name.trim() === '') {
errors.push(prefix + 'Name is required and must be a non-empty string');
}
if (!tool.type || !['software', 'method', 'concept'].includes(tool.type)) {
errors.push(prefix + 'Type is required and must be one of: software, method, concept');
}
if (!tool.description || typeof tool.description !== 'string' || tool.description.trim() === '') {
errors.push(prefix + 'Description is required and must be a non-empty string');
}
if (!tool.url || typeof tool.url !== 'string' || tool.url.trim() === '') {
errors.push(prefix + 'URL is required and must be a non-empty string');
}
if (!tool.skillLevel || !['novice', 'beginner', 'intermediate', 'advanced', 'expert'].includes(tool.skillLevel)) {
errors.push(prefix + 'Skill level is required and must be one of: novice, beginner, intermediate, advanced, expert');
}
// URL format validation
if (tool.url && tool.url.trim() !== '') {
try {
new URL(tool.url);
} catch {
errors.push(prefix + 'URL must be a valid URL format');
}
}
// Project URL validation (if provided)
if (tool.projectUrl && tool.projectUrl.trim() !== '') {
try {
new URL(tool.projectUrl);
} catch {
errors.push(prefix + 'Project URL must be a valid URL format');
}
}
// Status URL validation (if provided)
if (tool.statusUrl && tool.statusUrl.trim() !== '') {
try {
new URL(tool.statusUrl);
} catch {
errors.push(prefix + 'Status URL must be a valid URL format');
}
}
// Array fields validation
const arrayFields = ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software', 'domain-agnostic-software'];
arrayFields.forEach(field => {
if (tool[field] !== undefined && tool[field] !== null && !Array.isArray(tool[field])) {
errors.push(prefix + `${field} must be an array if provided`);
}
});
// String fields validation (optional but must be strings if provided)
const stringFields = ['icon', 'license', 'accessType', 'statusUrl'];
stringFields.forEach(field => {
if (tool[field] !== undefined && tool[field] !== null && typeof tool[field] !== 'string') {
errors.push(prefix + `${field} must be a string if provided`);
}
});
// Boolean field validation
if (tool.knowledgebase !== undefined && tool.knowledgebase !== null && typeof tool.knowledgebase !== 'boolean') {
errors.push(prefix + 'knowledgebase must be a boolean if provided');
}
// Duplicate name check: only when we know which index we're validating
if (index !== null && tool.name) {
const clashIndex = toolsData.tools.findIndex((t, i) => i !== index && t.name === tool.name);
if (clashIndex >= 0) {
errors.push(prefix + `Duplicate name "${tool.name}" also found at index ${clashIndex}`);
}
}
return errors;
}
// Tool management
function addNewTool() {
switchSection('tools');
clearForm();
editingIndex = -1;
document.getElementById('editTitle').textContent = 'Add New Tool';
document.getElementById('deleteBtn').style.display = 'none';
updateStatus('Creating new tool');
}
function editTool(index) {
switchSection('tools');
const tool = filteredTools[index];
editingIndex = toolsData.tools.indexOf(tool);
// Populate form with all fields
document.getElementById('name').value = tool.name || '';
document.getElementById('type').value = tool.type || '';
document.getElementById('icon').value = tool.icon || '';
document.getElementById('description').value = tool.description || '';
document.getElementById('url').value = tool.url || '';
document.getElementById('projectUrl').value = tool.projectUrl || '';
document.getElementById('statusUrl').value = tool.statusUrl || '';
document.getElementById('skillLevel').value = tool.skillLevel || '';
document.getElementById('license').value = tool.license || '';
document.getElementById('accessType').value = tool.accessType || '';
document.getElementById('knowledgebase').checked = tool.knowledgebase || false;
// Set tag values for all tag fields
setTagValues('domains', tool.domains);
setTagValues('phases', tool.phases);
setTagValues('platforms', tool.platforms);
setTagValues('tags', tool.tags);
setTagValues('related_concepts', tool.related_concepts);
setTagValues('related_software', tool.related_software);
setTagValues('domain-agnostic-software', tool['domain-agnostic-software']);
document.getElementById('editTitle').textContent = `Edit: ${tool.name}`;
document.getElementById('deleteBtn').style.display = 'inline-block';
updateStatus(`Editing ${tool.name}`);
}
function saveTool() {
const form = document.getElementById('toolForm');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const tool = {
name: document.getElementById('name').value.trim(),
type: document.getElementById('type').value,
description: document.getElementById('description').value.trim(),
url: document.getElementById('url').value.trim(),
skillLevel: document.getElementById('skillLevel').value,
domains: getTagValues('domains'),
phases: getTagValues('phases'),
platforms: getTagValues('platforms'),
tags: getTagValues('tags'),
related_concepts: getTagValues('related_concepts'),
related_software: getTagValues('related_software')
};
// Add domain-agnostic-software field
const domainAgnosticCategories = getTagValues('domain-agnostic-software');
if (domainAgnosticCategories.length > 0) {
tool['domain-agnostic-software'] = domainAgnosticCategories;
}
// Add optional fields only if they have values
const optionalFields = ['icon', 'projectUrl', 'statusUrl', 'license', 'accessType'];
optionalFields.forEach(field => {
const value = document.getElementById(field).value.trim();
if (value) tool[field] = value;
});
if (document.getElementById('knowledgebase').checked) {
tool.knowledgebase = true;
}
// Remove empty arrays to keep YAML clean
Object.keys(tool).forEach(key => {
if (Array.isArray(tool[key]) && tool[key].length === 0) {
delete tool[key];
}
});
// Validate tool
const errors = validateTool(tool);
if (errors.length > 0) {
showValidationErrors(errors);
return;
}
hideValidationErrors();
const isNewTool = editingIndex < 0;
const toolName = tool.name;
if (editingIndex >= 0) {
toolsData.tools[editingIndex] = tool;
updateStatus(`✅ Updated ${toolName}`);
showSaveSuccess('Tool updated successfully!');
} else {
toolsData.tools.push(tool);
updateStatus(`✅ Added ${toolName}`);
showSaveSuccess('Tool added successfully!');
}
filterTools();
clearForm();
// Update validation summary if needed
setTimeout(() => {
const validationFilter = document.getElementById('validationFilter').value;
if (validationFilter === 'invalid') {
const remainingErrors = [];
toolsData.tools.forEach((t, index) => {
const toolErrors = validateTool(t, index);
remainingErrors.push(...toolErrors);
});
if (remainingErrors.length === 0) {
hideValidationErrors();
updateStatus('🎉 All validation errors fixed!');
document.getElementById('validationFilter').value = '';
filterTools();
} else {
const validationSummary = document.getElementById('validationSummary');
const validationCount = document.getElementById('validationCount');
if (validationSummary && validationCount) {
validationCount.textContent = toolsData.tools.filter(t => validateTool(t, toolsData.tools.indexOf(t)).length > 0).length;
validationSummary.style.display = validationCount.textContent === '0' ? 'none' : 'block';
}
}
}
}, 100);
}
function showSaveSuccess(message) {
let successDiv = document.getElementById('saveSuccess');
if (!successDiv) {
successDiv = document.createElement('div');
successDiv.id = 'saveSuccess';
successDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: var(--success-color);
color: white;
padding: 12px 20px;
border-radius: 6px;
box-shadow: var(--shadow);
z-index: 1000;
opacity: 0;
transform: translateY(-20px);
transition: all 0.3s ease;
`;
document.body.appendChild(successDiv);
}
successDiv.textContent = message;
requestAnimationFrame(() => {
successDiv.style.opacity = '1';
successDiv.style.transform = 'translateY(0)';
});
setTimeout(() => {
successDiv.style.opacity = '0';
successDiv.style.transform = 'translateY(-20px)';
setTimeout(() => {
if (successDiv.parentNode) {
successDiv.parentNode.removeChild(successDiv);
}
}, 300);
}, 3000);
}
function deleteTool() {
if (editingIndex >= 0 && confirm('Are you sure you want to delete this tool?')) {
const toolName = toolsData.tools[editingIndex].name;
toolsData.tools.splice(editingIndex, 1);
updateStatus(`Deleted ${toolName}`);
renderTools();
clearForm();
}
}
function clearForm() {
document.getElementById('toolForm').reset();
const tagFields = ['domains', 'phases', 'platforms', 'tags', 'related_concepts', 'related_software', 'domain-agnostic-software'];
tagFields.forEach(field => {
setTagValues(field, []);
});
editingIndex = -1;
document.getElementById('editTitle').textContent = 'Tool Editor';
document.getElementById('deleteBtn').style.display = 'none';
}
// Category management
function renderCategories() {
const container = document.getElementById('categoriesList');
if (!container) return;
if (!toolsData['domain-agnostic-software'] || toolsData['domain-agnostic-software'].length === 0) {
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No categories found</div>';
return;
}
container.innerHTML = toolsData['domain-agnostic-software'].map((category, index) => `
<div class="category-item ${selectedCategory === category ? 'selected' : ''}" onclick="selectCategory(${index})">
<div class="category-name">${category.name}</div>
<div class="category-description">${category.description || 'No description'}</div>
<div style="font-size: 0.8em; color: var(--text-secondary); margin-top: 4px;">
ID: ${category.id} ${category.use_cases ? `${category.use_cases.length} use cases` : ''}
</div>
</div>
`).join('');
}
function selectCategory(index) {
selectedCategory = toolsData['domain-agnostic-software'][index];
editingCategoryIndex = index;
// Populate form
document.getElementById('categoryId').value = selectedCategory.id || '';
document.getElementById('categoryName').value = selectedCategory.name || '';
document.getElementById('categoryDescription').value = selectedCategory.description || '';
setTagValues('useCases', selectedCategory.use_cases);
document.getElementById('deleteCategoryBtn').style.display = 'inline-block';
renderCategories();
}
function saveCategory() {
const category = {
id: document.getElementById('categoryId').value.trim(),
name: document.getElementById('categoryName').value.trim(),
description: document.getElementById('categoryDescription').value.trim(),
use_cases: getTagValues('useCases')
};
if (!category.id || !category.name) {
alert('Category ID and Name are required');
return;
}
// Remove empty description
if (!category.description) {
delete category.description;
}
// Remove empty use_cases array
if (category.use_cases.length === 0) {
delete category.use_cases;
}
if (editingCategoryIndex >= 0) {
toolsData['domain-agnostic-software'][editingCategoryIndex] = category;
updateStatus(`✅ Updated category ${category.name}`);
} else {
toolsData['domain-agnostic-software'].push(category);
updateStatus(`✅ Added category ${category.name}`);
}
renderCategories();
clearCategoryForm();
}
function deleteCategory() {
if (editingCategoryIndex >= 0 && confirm('Are you sure you want to delete this category?')) {
const categoryName = toolsData['domain-agnostic-software'][editingCategoryIndex].name;
toolsData['domain-agnostic-software'].splice(editingCategoryIndex, 1);
updateStatus(`Deleted category ${categoryName}`);
renderCategories();
clearCategoryForm();
}
}
function clearCategoryForm() {
document.getElementById('categoryForm').reset();
setTagValues('useCases', []);
selectedCategory = null;
editingCategoryIndex = -1;
document.getElementById('deleteCategoryBtn').style.display = 'none';
renderCategories();
}
function validateAllTools() {
const allErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
allErrors.push(...toolErrors);
});
// Check for duplicate names across all tools
const nameMap = new Map();
toolsData.tools.forEach((tool, index) => {
if (tool.name) {
if (nameMap.has(tool.name)) {
allErrors.push(`Duplicate name "${tool.name}" found at indices ${nameMap.get(tool.name)} and ${index}`);
} else {
nameMap.set(tool.name, index);
}
}
});
if (allErrors.length === 0) {
updateStatus('✅ All tools are valid');
hideValidationErrors();
} else {
updateStatus(`❌ Found ${allErrors.length} validation errors`);
showValidationErrors(allErrors);
document.getElementById('validationErrors').scrollIntoView({ behavior: 'smooth' });
}
return allErrors.length === 0;
}
function showValidationErrors(errors) {
const errorDiv = document.getElementById('validationErrors');
if (Array.isArray(errors) && errors.length > 10) {
const firstTen = errors.slice(0, 10);
const remaining = errors.length - 10;
errorDiv.innerHTML = `
<strong>Validation Errors (showing first 10 of ${errors.length}):</strong><br>
${firstTen.map(error => `${error}`).join('<br>')}
<br><strong>... and ${remaining} more errors</strong>
`;
} else {
errorDiv.innerHTML = `
<strong>Validation Errors:</strong><br>
${(Array.isArray(errors) ? errors : [errors]).map(error => `${error}`).join('<br>')}
`;
}
errorDiv.style.display = 'block';
}
function hideValidationErrors() {
document.getElementById('validationErrors').style.display = 'none';
}
// Filtering and sorting
function filterTools() {
const rawSearch = document.getElementById('searchInput').value || '';
const searchTerm = rawSearch.toLowerCase();
const typeFilter = document.getElementById('typeFilter').value;
const skillFilter = document.getElementById('skillFilter').value;
const domainFilter = document.getElementById('domainFilter').value;
const knowledgebaseFilter = document.getElementById('knowledgebaseFilter').value;
const validationFilter = document.getElementById('validationFilter').value;
filteredTools = toolsData.tools.filter(tool => {
// Safe access helpers
const name = (tool.name || '').toLowerCase();
const desc = (tool.description || '').toLowerCase();
const tagHit = Array.isArray(tool.tags) && tool.tags.some(tag => (tag || '').toLowerCase().includes(searchTerm));
const matchesSearch = !searchTerm || name.includes(searchTerm) || desc.includes(searchTerm) || tagHit;
const matchesType = !typeFilter || tool.type === typeFilter;
const matchesSkill = !skillFilter || tool.skillLevel === skillFilter;
const matchesDomain = !domainFilter || (Array.isArray(tool.domains) && tool.domains.includes(domainFilter));
const matchesKnowledgebase = !knowledgebaseFilter ||
(knowledgebaseFilter === 'true' && !!tool.knowledgebase) ||
(knowledgebaseFilter === 'false' && !tool.knowledgebase);
let matchesValidation = true;
if (validationFilter) {
const hasValidationIssues = validateTool(tool, toolsData.tools.indexOf(tool)).length > 0;
matchesValidation = validationFilter === 'invalid' ? hasValidationIssues : !hasValidationIssues;
}
return matchesSearch && matchesType && matchesSkill && matchesDomain && matchesKnowledgebase && matchesValidation;
});
sortTools();
}
function showInvalidToolsFilter(errorCount) {
document.getElementById('validationFilter').value = 'invalid';
filterTools();
showValidationErrors([
`⚠️ Import completed with ${errorCount} validation errors.`,
'',
'The filter has been set to show only tools with validation issues.',
'Please fix these errors before exporting to ensure compatibility with the web application.',
'',
'Click on each tool marked with ⚠️ to edit and resolve the issues.'
]);
updateStatus(`⚠️ Import completed - ${errorCount} tools need fixing`);
}
function clearAllFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('typeFilter').value = '';
document.getElementById('skillFilter').value = '';
document.getElementById('domainFilter').value = '';
document.getElementById('knowledgebaseFilter').value = '';
document.getElementById('validationFilter').value = '';
document.getElementById('sortBy').value = 'name';
filterTools();
updateStatus('All filters cleared');
}
function sortTools() {
const sortBy = document.getElementById('sortBy').value;
filteredTools.sort((a, b) => {
let aVal = a && a[sortBy];
let bVal = b && b[sortBy];
if (Array.isArray(aVal)) aVal = aVal.join(', ');
if (Array.isArray(bVal)) bVal = bVal.join(', ');
aVal = (aVal == null ? '' : String(aVal));
bVal = (bVal == null ? '' : String(bVal));
return aVal.localeCompare(bVal);
});
renderTools();
}
// Rendering
function renderTools() {
const container = document.getElementById('toolsList');
const countElement = document.getElementById('toolCount');
if (!Array.isArray(filteredTools) || filteredTools.length === 0) {
countElement.textContent = 0;
container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">No tools found</div>';
return;
}
countElement.textContent = filteredTools.length;
container.innerHTML = filteredTools.map((tool, index) => {
const hasValidationIssues = validateTool(tool, toolsData.tools.indexOf(tool)).length > 0;
return `
<div class="tool-item ${selectedTool === tool ? 'selected' : ''}" onclick="selectTool(${index})">
<div class="tool-header">
<div>
<div class="tool-name">
${hasValidationIssues ? '⚠️ ' : ''}
${tool.icon || '🔧'} ${tool.name || 'Unnamed Tool'}
${tool.knowledgebase ? ' 📖' : ''}
${tool.projectUrl ? ' 🌐' : ''}
</div>
<div class="tool-meta">
<span>${tool.type || 'Unknown'}</span>
<span>${tool.skillLevel || 'Unknown'}</span>
${tool.license ? `<span>${tool.license}</span>` : ''}
${tool.accessType ? `<span>${tool.accessType}</span>` : ''}
${hasValidationIssues ? '<span style="color: var(--danger-color);">⚠️ Issues</span>' : ''}
</div>
</div>
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); editTool(${index})">Edit</button>
</div>
<div class="tool-description">${tool.description || 'No description provided'}</div>
${tool.domains && tool.domains.length ? `
<div style="margin-top: 8px; font-size: 0.85em; color: var(--text-secondary);">
<strong>Domains:</strong> ${tool.domains.slice(0, 3).join(', ')}${tool.domains.length > 3 ? '...' : ''}
</div>
` : ''}
${tool['domain-agnostic-software'] && tool['domain-agnostic-software'].length ? `
<div style="margin-top: 4px; font-size: 0.85em; color: var(--text-secondary);">
<strong>Categories:</strong> ${tool['domain-agnostic-software'].slice(0, 2).join(', ')}${tool['domain-agnostic-software'].length > 2 ? '...' : ''}
</div>
` : ''}
${tool.tags ? `
<div class="tags">
${tool.tags.slice(0, 5).map(tag => `<span class="tag">${tag}</span>`).join('')}
${tool.tags.length > 5 ? `<span class="tag">+${tool.tags.length - 5}</span>` : ''}
</div>
` : ''}
</div>
`}).join('');
}
function selectTool(index) {
selectedTool = filteredTools[index];
renderTools();
}
function clearSelection() {
selectedTool = null;
renderTools();
}
// Statistics
function showStats() {
const stats = calculateStats();
const container = document.getElementById('statsContainer');
container.innerHTML = Object.entries(stats).map(([key, value]) => `
<div class="stat-item">
<div class="stat-number">${value}</div>
<div>${key}</div>
</div>
`).join('');
container.style.display = container.style.display === 'none' ? 'grid' : 'none';
}
function calculateStats() {
const stats = {
'Total Tools': toolsData.tools.length,
'Software': toolsData.tools.filter(t => t.type === 'software').length,
'Methods': toolsData.tools.filter(t => t.type === 'method').length,
'Concepts': toolsData.tools.filter(t => t.type === 'concept').length,
'With KB': toolsData.tools.filter(t => t.knowledgebase).length,
'Open Source': toolsData.tools.filter(t => t.license && t.license !== 'Proprietary').length,
'Categories': toolsData['domain-agnostic-software'].length
};
return stats;
}
// Bulk operations
function toggleBulkPanel() {
const panel = document.getElementById('bulkPanel');
panel.classList.toggle('active');
}
function bulkRemoveTags() {
const tags = document.getElementById('bulkRemoveTagsInput').value
.split(',').map(t => t.trim()).filter(t => t);
if (tags.length === 0) {
updateStatus('Please enter tags to remove');
return;
}
if (!confirm(`Remove tags "${tags.join(', ')}" from all tools?`)) return;
let count = 0;
toolsData.tools.forEach(tool => {
if (tool.tags) {
const before = tool.tags.length;
tool.tags = tool.tags.filter(tag => !tags.includes(tag));
if (tool.tags.length < before) count++;
}
});
updateStatus(`Removed tags from ${count} tools`);
document.getElementById('bulkRemoveTagsInput').value = '';
renderTools();
}
function bulkRenameTags() {
const fromTag = document.getElementById('bulkRenameFrom').value.trim();
const toTag = document.getElementById('bulkRenameTo').value.trim();
if (!fromTag || !toTag) {
updateStatus('Please enter both "from" and "to" tags');
return;
}
if (!confirm(`Rename tag "${fromTag}" to "${toTag}" in all tools?`)) return;
let count = 0;
toolsData.tools.forEach(tool => {
if (tool.tags && tool.tags.includes(fromTag)) {
const index = tool.tags.indexOf(fromTag);
tool.tags[index] = toTag;
count++;
}
});
updateStatus(`Renamed tag in ${count} tools`);
document.getElementById('bulkRenameFrom').value = '';
document.getElementById('bulkRenameTo').value = '';
renderTools();
}
function bulkAssignRelatedConcepts() {
const concepts = document.getElementById('bulkConceptsInput').value
.split(',').map(t => t.trim()).filter(t => t);
if (concepts.length === 0) {
updateStatus('Please enter concepts to assign');
return;
}
if (!confirm(`Assign concepts "${concepts.join(', ')}" to all tools?`)) return;
const selectedTools = getSelectedTools();
selectedTools.forEach(tool => {
if (!tool.related_concepts) tool.related_concepts = [];
concepts.forEach(concept => {
if (!tool.related_concepts.includes(concept)) {
tool.related_concepts.push(concept);
}
});
});
updateStatus(`Assigned concepts to ${selectedTools.length} tools`);
document.getElementById('bulkConceptsInput').value = '';
renderTools();
}
function bulkAssignRelatedSoftware() {
const software = document.getElementById('bulkSoftwareInput').value
.split(',').map(t => t.trim()).filter(t => t);
if (software.length === 0) {
updateStatus('Please enter software to assign');
return;
}
if (!confirm(`Assign software "${software.join(', ')}" to all tools?`)) return;
const selectedTools = getSelectedTools();
selectedTools.forEach(tool => {
if (!tool.related_software) tool.related_software = [];
software.forEach(sw => {
if (!tool.related_software.includes(sw)) {
tool.related_software.push(sw);
}
});
});
updateStatus(`Assigned software to ${selectedTools.length} tools`);
document.getElementById('bulkSoftwareInput').value = '';
renderTools();
}
function getSelectedTools() {
return toolsData.tools;
}
// YAML operations
function importYAML(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const yamlContent = e.target.result;
const parsed = jsyaml.load(yamlContent);
if (!parsed || !parsed.tools || !Array.isArray(parsed.tools)) {
alert('Invalid YAML format: missing or invalid tools array');
return;
}
const importSummary = `
Import Summary:
${parsed.tools.length} tools
${parsed.tools.filter(t => t.type === 'software').length} software
${parsed.tools.filter(t => t.type === 'method').length} methods
${parsed.tools.filter(t => t.type === 'concept').length} concepts
${parsed.tools.filter(t => t.knowledgebase).length} with knowledgebase
This will replace all current data. Continue?`;
if (!confirm(importSummary)) {
return;
}
toolsData = parsed;
// Ensure required structure exists
if (!toolsData.domains) toolsData.domains = [];
if (!toolsData.phases) toolsData.phases = [];
if (!toolsData['domain-agnostic-software']) toolsData['domain-agnostic-software'] = [];
if (!toolsData.scenarios) toolsData.scenarios = [];
if (!toolsData.skill_levels) toolsData.skill_levels = {};
populateFilters();
updateStatus('Validating imported data...');
const validationErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
validationErrors.push(...toolErrors);
});
// Check for duplicate names
const nameMap = new Map();
toolsData.tools.forEach((tool, index) => {
if (tool.name) {
if (nameMap.has(tool.name)) {
validationErrors.push(`Duplicate name "${tool.name}" found at indices ${nameMap.get(tool.name)} and ${index}`);
} else {
nameMap.set(tool.name, index);
}
}
});
if (validationErrors.length > 0) {
showInvalidToolsFilter(validationErrors.length); // this already calls filterTools()
} else {
hideValidationErrors();
// ✅ show everything by default
filteredTools = toolsData.tools.slice();
sortTools(); // will render afterwards
updateStatus(`✅ Successfully imported ${toolsData.tools.length} tools - all valid`);
}
renderCategories();
} catch (error) {
alert('Error parsing YAML: ' + error.message);
updateStatus('❌ YAML parsing failed');
}
};
reader.readAsText(file);
}
function exportYAML() {
const allErrors = [];
toolsData.tools.forEach((tool, index) => {
const toolErrors = validateTool(tool, index);
allErrors.push(...toolErrors);
});
if (allErrors.length > 0) {
const confirmExport = confirm(`⚠️ Warning: Found ${allErrors.length} validation errors in the database.\n\nExporting this data may cause issues when importing into the web application.\n\nWould you like to:\n• Click OK to export anyway\n• Click Cancel to fix errors first (recommended)`);
if (!confirmExport) {
showInvalidToolsFilter(allErrors.length);
return;
}
}
try {
const processedData = {
...toolsData,
tools: toolsData.tools.map(tool => {
const processedTool = { ...tool };
// Clean up empty arrays and null values
Object.keys(processedTool).forEach(key => {
if (Array.isArray(processedTool[key]) && processedTool[key].length === 0) {
delete processedTool[key];
}
if (processedTool[key] === null || processedTool[key] === undefined || processedTool[key] === '') {
delete processedTool[key];
}
});
return processedTool;
})
};
const yamlContent = jsyaml.dump(processedData, {
indent: 2,
lineWidth: 80,
noRefs: true,
sortKeys: false,
styles: {
'!!str': 'fold'
}
});
const lines = yamlContent.split('\n');
const processedLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim().startsWith('description:')) {
if (line.includes('"') || line.includes("'")) {
const indent = line.match(/^(\s*)/)[1];
processedLines.push(`${indent}description: >-`);
let descContent = line.substring(line.indexOf(':') + 1).trim();
if (descContent.startsWith('"') || descContent.startsWith("'")) {
descContent = descContent.slice(1, -1);
}
const words = descContent.split(' ');
let currentLine = '';
const descIndent = indent + ' ';
words.forEach(word => {
if (currentLine.length + word.length > 76) {
if (currentLine) {
processedLines.push(descIndent + currentLine.trim());
}
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
});
if (currentLine.trim()) {
processedLines.push(descIndent + currentLine.trim());
}
} else {
processedLines.push(line);
}
} else {
processedLines.push(line);
}
}
const finalYaml = processedLines.join('\n');
const blob = new Blob([finalYaml], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tools.yaml';
a.click();
URL.revokeObjectURL(url);
updateStatus(allErrors.length > 0 ?
`⚠️ YAML exported with ${allErrors.length} validation errors` :
'✅ YAML exported successfully'
);
} catch (error) {
alert('Error exporting YAML: ' + error.message);
}
}
// Initialize on load
document.addEventListener('DOMContentLoaded', init);
// Keyboard shortcuts
document.addEventListener('keydown', function(event) {
if ((event.ctrlKey || event.metaKey) && event.key === 's') {
event.preventDefault();
if (currentSection === 'tools') {
saveTool();
} else if (currentSection === 'categories') {
saveCategory();
}
}
if ((event.ctrlKey || event.metaKey) && event.key === 'n') {
event.preventDefault();
if (currentSection === 'tools') {
addNewTool();
}
}
if ((event.ctrlKey || event.metaKey) && event.key === 'e') {
event.preventDefault();
exportYAML();
}
if (event.altKey && event.key === 'v') {
event.preventDefault();
validateAllTools();
}
if (event.key === 'Escape') {
clearSelection();
if (currentSection === 'tools') {
clearForm();
} else if (currentSection === 'categories') {
clearCategoryForm();
}
}
if ((event.ctrlKey || event.metaKey) && event.key === 'f') {
event.preventDefault();
document.getElementById('searchInput').focus();
}
});
// Hide autocomplete when clicking outside
document.addEventListener('click', function(event) {
if (!event.target.closest('.autocomplete-container')) {
document.querySelectorAll('.autocomplete-dropdown').forEach(dropdown => {
dropdown.style.display = 'none';
});
}
});
</script>
</body>
</html>