progress
This commit is contained in:
parent
65f7685b01
commit
44349266bd
112
.eleventy.js
112
.eleventy.js
@ -0,0 +1,112 @@
|
|||||||
|
const yaml = require('js-yaml');
|
||||||
|
const fs = require('fs');
|
||||||
|
const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight');
|
||||||
|
const sass = require('eleventy-plugin-sass');
|
||||||
|
|
||||||
|
module.exports = function(eleventyConfig) {
|
||||||
|
// Plugins
|
||||||
|
eleventyConfig.addPlugin(syntaxHighlight);
|
||||||
|
eleventyConfig.addPlugin(sass, {
|
||||||
|
compileOptions: {
|
||||||
|
permalink: function(contents, inputPath) {
|
||||||
|
return (data) => data.page.filePathStem.replace(/^\/scss\//, "/css/") + ".css";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sass: {
|
||||||
|
style: "compressed",
|
||||||
|
sourceMap: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy static assets
|
||||||
|
eleventyConfig.addPassthroughCopy("src/js");
|
||||||
|
eleventyConfig.addPassthroughCopy("src/images");
|
||||||
|
eleventyConfig.addPassthroughCopy("src/icons");
|
||||||
|
|
||||||
|
// Watch for changes
|
||||||
|
eleventyConfig.addWatchTarget("src/scss/");
|
||||||
|
eleventyConfig.addWatchTarget("src/js/");
|
||||||
|
eleventyConfig.addWatchTarget("src/_data/");
|
||||||
|
|
||||||
|
// Custom YAML data loader
|
||||||
|
eleventyConfig.addDataExtension("yaml", contents => yaml.load(contents));
|
||||||
|
eleventyConfig.addDataExtension("yml", contents => yaml.load(contents));
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
eleventyConfig.addFilter("filterByDomain", function(tools, domain) {
|
||||||
|
if (!domain) return tools;
|
||||||
|
return tools.filter(tool => tool.domains && tool.domains.includes(domain));
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.addFilter("filterByPhase", function(tools, phase) {
|
||||||
|
if (!phase) return tools;
|
||||||
|
return tools.filter(tool => tool.phases && tool.phases.includes(phase));
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.addFilter("filterByType", function(tools, type) {
|
||||||
|
if (!type) return tools;
|
||||||
|
return tools.filter(tool => tool.type === type);
|
||||||
|
});
|
||||||
|
|
||||||
|
eleventyConfig.addFilter("searchTools", function(tools, searchTerm) {
|
||||||
|
if (!searchTerm) return tools;
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
return tools.filter(tool =>
|
||||||
|
tool.name.toLowerCase().includes(term) ||
|
||||||
|
tool.description.toLowerCase().includes(term) ||
|
||||||
|
(tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(term)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Collection for tools by domain and phase (for matrix view)
|
||||||
|
eleventyConfig.addCollection("toolMatrix", function(collectionApi) {
|
||||||
|
const tools = collectionApi.getAll()[0].data.tools || [];
|
||||||
|
const domains = ['Filesystem Forensics', 'Network Forensics', 'Memory Forensics', 'Live Forensics', 'Malware Analysis', 'Cryptocurrency'];
|
||||||
|
const phases = ['Data Collection', 'Examination', 'Analysis', 'Reporting'];
|
||||||
|
|
||||||
|
const matrix = {};
|
||||||
|
domains.forEach(domain => {
|
||||||
|
matrix[domain] = {};
|
||||||
|
phases.forEach(phase => {
|
||||||
|
matrix[domain][phase] = tools.filter(tool =>
|
||||||
|
tool.domains && tool.domains.includes(domain) &&
|
||||||
|
tool.phases && tool.phases.includes(phase) &&
|
||||||
|
tool.type === 'FOSS'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return matrix;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Global data
|
||||||
|
eleventyConfig.addGlobalData("domains", [
|
||||||
|
'Filesystem Forensics',
|
||||||
|
'Network Forensics',
|
||||||
|
'Memory Forensics',
|
||||||
|
'Live Forensics',
|
||||||
|
'Malware Analysis',
|
||||||
|
'Cryptocurrency'
|
||||||
|
]);
|
||||||
|
|
||||||
|
eleventyConfig.addGlobalData("phases", [
|
||||||
|
'Data Collection',
|
||||||
|
'Examination',
|
||||||
|
'Analysis',
|
||||||
|
'Reporting'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
return {
|
||||||
|
dir: {
|
||||||
|
input: "src",
|
||||||
|
output: "_site",
|
||||||
|
includes: "_includes",
|
||||||
|
layouts: "_layouts",
|
||||||
|
data: "_data"
|
||||||
|
},
|
||||||
|
templateFormats: ["md", "njk", "html"],
|
||||||
|
markdownTemplateEngine: "njk",
|
||||||
|
htmlTemplateEngine: "njk"
|
||||||
|
};
|
||||||
|
};
|
23
package.json
23
package.json
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "dfir-tools-hub",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Self-hosted DFIR tools directory and service status hub",
|
||||||
|
"scripts": {
|
||||||
|
"start": "eleventy --serve",
|
||||||
|
"build": "eleventy",
|
||||||
|
"debug": "DEBUG=Eleventy* eleventy",
|
||||||
|
"clean": "rm -rf _site"
|
||||||
|
},
|
||||||
|
"keywords": ["dfir", "digital-forensics", "incident-response", "tools"],
|
||||||
|
"author": "Your Lab",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@11ty/eleventy": "^2.0.1",
|
||||||
|
"@11ty/eleventy-plugin-syntaxhighlight": "^5.0.0",
|
||||||
|
"eleventy-plugin-sass": "^2.2.0",
|
||||||
|
"sass": "^1.69.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"js-yaml": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,128 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="theme-auto">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% if title %}{{ title }} - {% endif %}DFIR Tools Hub</title>
|
||||||
|
<meta name="description" content="{{ description or 'Comprehensive directory of FOSS tools for Digital Forensics and Incident Response' }}">
|
||||||
|
|
||||||
|
<!-- Preload critical assets -->
|
||||||
|
<link rel="preload" href="/css/main.css" as="style">
|
||||||
|
|
||||||
|
<!-- Styles -->
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
|
||||||
|
<!-- Theme detection script (inline to prevent FOUC) -->
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const theme = localStorage.getItem('theme') || 'auto';
|
||||||
|
document.documentElement.className = 'theme-' + theme;
|
||||||
|
|
||||||
|
if (theme === 'auto') {
|
||||||
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
document.documentElement.classList.toggle('dark', isDark);
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-200">
|
||||||
|
<!-- Navigation -->
|
||||||
|
<nav class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="max-w-7xl mx-auto px-4">
|
||||||
|
<div class="flex justify-between items-center h-16">
|
||||||
|
<!-- Left Navigation -->
|
||||||
|
<div class="flex items-center space-x-8">
|
||||||
|
<a href="/"
|
||||||
|
class="nav-link {% if page.url == '/' %}nav-link-active{% endif %}"
|
||||||
|
data-page="home">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
|
||||||
|
</svg>
|
||||||
|
Home
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/status/"
|
||||||
|
class="nav-link {% if '/status/' in page.url %}nav-link-active{% endif %}"
|
||||||
|
data-page="status">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
Status
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/about/"
|
||||||
|
class="nav-link {% if '/about/' in page.url %}nav-link-active{% endif %}"
|
||||||
|
data-page="about">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||||
|
</svg>
|
||||||
|
About
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/privacy/"
|
||||||
|
class="nav-link {% if '/privacy/' in page.url %}nav-link-active{% endif %}"
|
||||||
|
data-page="privacy">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||||
|
</svg>
|
||||||
|
Privacy
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Center Logo -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="bg-blue-600 dark:bg-blue-500 text-white px-4 py-2 rounded-lg font-bold text-lg">
|
||||||
|
DFIR
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Theme Selector -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1" id="theme-selector">
|
||||||
|
<button class="theme-btn p-2 rounded-md transition-colors" data-theme="light" title="Light theme">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="theme-btn p-2 rounded-md transition-colors" data-theme="auto" title="Auto theme">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="theme-btn p-2 rounded-md transition-colors" data-theme="dark" title="Dark theme">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="min-h-screen">
|
||||||
|
{{ content | safe }}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Tool Detail Modal (initially hidden) -->
|
||||||
|
<div id="tool-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden">
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<div class="p-6" id="tool-modal-content">
|
||||||
|
<!-- Content will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scripts -->
|
||||||
|
<script src="/js/theme.js"></script>
|
||||||
|
<script src="/js/search.js"></script>
|
||||||
|
<script src="/js/modal.js"></script>
|
||||||
|
{% if page.url == "/status/" %}
|
||||||
|
<script src="/js/status.js"></script>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
layout: base.njk
|
||||||
|
title: "About"
|
||||||
|
description: "Learn about the FOSS DFIR Tools Framework and our mission"
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">About</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
Learn more about the FOSS DFIR Tools Framework and our mission.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Our Mission</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||||
|
The FOSS DFIR Tools Framework serves as a comprehensive hub for digital forensics and incident response professionals.
|
||||||
|
We curate and organize open-source tools to make them easily discoverable and accessible to the security community.
|
||||||
|
Our platform bridges the gap between scattered tool documentation and practical implementation in academic and
|
||||||
|
professional environments.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Framework Methodology</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||||
|
Our tool categorization follows the standard DFIR methodology established by Kent, Chevalier, Grance, and Dang
|
||||||
|
in the NIST Special Publication 800-86. This systematic approach ensures that tools are organized according to
|
||||||
|
their primary function within the investigation process:
|
||||||
|
</p>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
<div class="bg-blue-50 dark:bg-blue-900 p-4 rounded-lg">
|
||||||
|
<h4 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">Data Collection</h4>
|
||||||
|
<p class="text-blue-800 dark:text-blue-200 text-sm">
|
||||||
|
Tools for acquiring and preserving digital evidence while maintaining chain of custody and ensuring data integrity.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
|
||||||
|
<h4 class="font-semibold text-green-900 dark:text-green-100 mb-2">Examination</h4>
|
||||||
|
<p class="text-green-800 dark:text-green-200 text-sm">
|
||||||
|
Tools for extracting, parsing, and organizing data from collected evidence in preparation for analysis.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-purple-50 dark:bg-purple-900 p-4 rounded-lg">
|
||||||
|
<h4 class="font-semibold text-purple-900 dark:text-purple-100 mb-2">Analysis</h4>
|
||||||
|
<p class="text-purple-800 dark:text-purple-200 text-sm">
|
||||||
|
Tools for correlating, analyzing, and interpreting evidence to draw conclusions and identify patterns.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-yellow-50 dark:bg-yellow-900 p-4 rounded-lg">
|
||||||
|
<h4 class="font-semibold text-yellow-900 dark:text-yellow-100 mb-2">Reporting</h4>
|
||||||
|
<p class="text-yellow-800 dark:text-yellow-200 text-sm">
|
||||||
|
Tools for documenting findings, creating timelines, and generating comprehensive investigation reports.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Academic Focus</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-6">
|
||||||
|
This framework was designed specifically for academic and laboratory environments where self-hosted solutions
|
||||||
|
are preferred over cloud-based services. We emphasize tools that can be deployed locally, ensuring data sovereignty
|
||||||
|
and compliance with institutional policies. Our "Self-Hosted Services" section provides access to powerful platforms
|
||||||
|
like Timesketch, TheHive, and MISP that can be deployed within your network perimeter.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Community Driven</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-4">
|
||||||
|
This platform is maintained by the DFIR community, for the DFIR community. We welcome contributions,
|
||||||
|
suggestions, and feedback to help improve the framework and keep tool information current. The entire
|
||||||
|
platform is built using static site generation with YAML-driven content management, making it easy for
|
||||||
|
contributors to add new tools or update existing information through simple file edits.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-700 p-4 rounded-lg">
|
||||||
|
<h4 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">How to Contribute</h4>
|
||||||
|
<ul class="text-sm text-gray-600 dark:text-gray-300 space-y-1">
|
||||||
|
<li>• Edit <code>src/_data/tools.yaml</code> to add or update tool information</li>
|
||||||
|
<li>• Modify <code>src/_data/services.yaml</code> to configure service monitoring</li>
|
||||||
|
<li>• Submit pull requests with tool suggestions or corrections</li>
|
||||||
|
<li>• Report issues or suggest improvements via GitHub</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Technical Implementation -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Technical Implementation</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Architecture</h3>
|
||||||
|
<ul class="text-gray-600 dark:text-gray-300 text-sm space-y-2">
|
||||||
|
<li>• <strong>Static Site Generator:</strong> 11ty (Eleventy)</li>
|
||||||
|
<li>• <strong>Content Management:</strong> YAML data files</li>
|
||||||
|
<li>• <strong>Styling:</strong> Sass with utility-first approach</li>
|
||||||
|
<li>• <strong>Interactivity:</strong> Vanilla JavaScript</li>
|
||||||
|
<li>• <strong>Monitoring:</strong> Uptime Kuma integration</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Features</h3>
|
||||||
|
<ul class="text-gray-600 dark:text-gray-300 text-sm space-y-2">
|
||||||
|
<li>• Sub-500ms search and filtering</li>
|
||||||
|
<li>• Dark/light/auto theme support</li>
|
||||||
|
<li>• Mobile-responsive design</li>
|
||||||
|
<li>• Zero external dependencies</li>
|
||||||
|
<li>• Real-time service status monitoring</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900 rounded-lg">
|
||||||
|
<h4 class="font-semibold text-blue-900 dark:text-blue-100 mb-2">Performance Targets</h4>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div class="text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>Initial Load:</strong><br>
|
||||||
|
< 2 seconds
|
||||||
|
</div>
|
||||||
|
<div class="text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>Search/Filter:</strong><br>
|
||||||
|
< 500ms
|
||||||
|
</div>
|
||||||
|
<div class="text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>Theme Switch:</strong><br>
|
||||||
|
< 100ms
|
||||||
|
</div>
|
||||||
|
<div class="text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>Modal Open:</strong><br>
|
||||||
|
< 200ms
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Credits and References -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-8">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Credits and References</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Methodology Reference</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
|
Kent, K., Chevalier, S., Grance, T., & Dang, H. (2006).
|
||||||
|
<em>Guide to integrating forensic techniques into incident response.</em>
|
||||||
|
NIST Special Publication 800-86. National Institute of Standards and Technology.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Technology Stack</h3>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<div>• <a href="https://11ty.dev" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener">Eleventy (11ty)</a></div>
|
||||||
|
<div>• <a href="https://sass-lang.com" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener">Sass</a></div>
|
||||||
|
<div>• <a href="https://uptime.kuma.pet" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener">Uptime Kuma</a></div>
|
||||||
|
<div>• <a href="https://yaml.org" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener">YAML</a></div>
|
||||||
|
<div>• <a href="https://mozilla.github.io/nunjucks/" class="text-blue-600 dark:text-blue-400 hover:underline" target="_blank" rel="noopener">Nunjucks</a></div>
|
||||||
|
<div>• JavaScript ES6+</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,84 @@
|
|||||||
|
# Service Status Configuration
|
||||||
|
# This file configures the services monitored on the status page
|
||||||
|
# Connect to Uptime Kuma API for real-time data
|
||||||
|
|
||||||
|
# Uptime Kuma Configuration
|
||||||
|
uptimeKuma:
|
||||||
|
enabled: false # Set to true when Uptime Kuma is configured
|
||||||
|
apiUrl: "https://status.lab.local/api"
|
||||||
|
apiKey: "" # Add your Uptime Kuma API key here
|
||||||
|
refreshInterval: 30000 # Refresh every 30 seconds
|
||||||
|
|
||||||
|
# Static service definitions (used when Uptime Kuma is not available)
|
||||||
|
services:
|
||||||
|
- id: timesketch
|
||||||
|
name: "Timesketch Instance"
|
||||||
|
description: "Collaborative forensic timeline analysis platform"
|
||||||
|
url: "https://timesketch.lab.local"
|
||||||
|
category: "Analysis Tools"
|
||||||
|
status: "operational" # operational|degraded|maintenance|down
|
||||||
|
uptime: "99.9%"
|
||||||
|
responseTime: "245ms"
|
||||||
|
lastChecked: "2025-01-15T10:30:00Z"
|
||||||
|
|
||||||
|
- id: thehive
|
||||||
|
name: "TheHive Platform"
|
||||||
|
description: "Incident response and case management"
|
||||||
|
url: "https://thehive.lab.local"
|
||||||
|
category: "Case Management"
|
||||||
|
status: "operational"
|
||||||
|
uptime: "99.7%"
|
||||||
|
responseTime: "180ms"
|
||||||
|
lastChecked: "2025-01-15T10:30:00Z"
|
||||||
|
|
||||||
|
- id: misp
|
||||||
|
name: "MISP Instance"
|
||||||
|
description: "Threat intelligence sharing platform"
|
||||||
|
url: "https://misp.lab.local"
|
||||||
|
category: "Threat Intelligence"
|
||||||
|
status: "degraded"
|
||||||
|
uptime: "98.2%"
|
||||||
|
responseTime: "890ms"
|
||||||
|
lastChecked: "2025-01-15T10:29:00Z"
|
||||||
|
issues: "High response times due to database optimization"
|
||||||
|
|
||||||
|
- id: elasticsearch
|
||||||
|
name: "Elasticsearch Cluster"
|
||||||
|
description: "Search and analytics engine"
|
||||||
|
url: "https://elastic.lab.local"
|
||||||
|
category: "Infrastructure"
|
||||||
|
status: "operational"
|
||||||
|
uptime: "99.8%"
|
||||||
|
responseTime: "120ms"
|
||||||
|
lastChecked: "2025-01-15T10:30:00Z"
|
||||||
|
|
||||||
|
- id: neo4j
|
||||||
|
name: "Neo4j Database"
|
||||||
|
description: "Graph database for relationship analysis"
|
||||||
|
url: "https://neo4j.lab.local"
|
||||||
|
category: "Infrastructure"
|
||||||
|
status: "maintenance"
|
||||||
|
uptime: "97.1%"
|
||||||
|
responseTime: "N/A"
|
||||||
|
lastChecked: "2025-01-15T09:00:00Z"
|
||||||
|
issues: "Scheduled maintenance window: 09:00-11:00 UTC"
|
||||||
|
|
||||||
|
# Service categories for organization
|
||||||
|
categories:
|
||||||
|
- name: "Analysis Tools"
|
||||||
|
description: "Forensic analysis and timeline tools"
|
||||||
|
- name: "Case Management"
|
||||||
|
description: "Incident response and case tracking"
|
||||||
|
- name: "Threat Intelligence"
|
||||||
|
description: "IOC sharing and threat analysis"
|
||||||
|
- name: "Infrastructure"
|
||||||
|
description: "Backend services and databases"
|
||||||
|
|
||||||
|
# Overall status calculation
|
||||||
|
overall:
|
||||||
|
status: "partial_outage" # operational|degraded|partial_outage|major_outage
|
||||||
|
message: "4 of 5 services operational • 1 service under maintenance"
|
||||||
|
operationalCount: 3
|
||||||
|
degradedCount: 1
|
||||||
|
maintenanceCount: 1
|
||||||
|
downCount: 0
|
@ -0,0 +1,195 @@
|
|||||||
|
# DFIR Tools Database
|
||||||
|
# Edit this file to add, remove, or modify tools
|
||||||
|
# Structure: Each tool should have required fields marked with *
|
||||||
|
|
||||||
|
tools:
|
||||||
|
- id: sleuthkit
|
||||||
|
name: "The Sleuth Kit" # * Display name
|
||||||
|
description: "Collection of command line tools for digital forensic analysis" # * Brief description
|
||||||
|
domains: # * Array of forensic domains
|
||||||
|
- "Filesystem Forensics"
|
||||||
|
phases: # * Array of DFIR phases
|
||||||
|
- "Examination"
|
||||||
|
- "Analysis"
|
||||||
|
platforms: # * Supported platforms
|
||||||
|
- "Linux"
|
||||||
|
- "Windows"
|
||||||
|
- "macOS"
|
||||||
|
skillLevel: "Intermediate" # * Beginner|Intermediate|Advanced
|
||||||
|
accessType: "CLI" # * CLI|GUI|Web|SaaS
|
||||||
|
url: "https://sleuthkit.org" # * Project homepage
|
||||||
|
tags: # Optional tags for search
|
||||||
|
- "timeline"
|
||||||
|
- "file-recovery"
|
||||||
|
- "metadata"
|
||||||
|
type: "FOSS" # * FOSS|SaaS
|
||||||
|
|
||||||
|
- id: volatility
|
||||||
|
name: "Volatility"
|
||||||
|
description: "Advanced memory forensics framework"
|
||||||
|
domains:
|
||||||
|
- "Memory Forensics"
|
||||||
|
- "Live Forensics"
|
||||||
|
phases:
|
||||||
|
- "Examination"
|
||||||
|
- "Analysis"
|
||||||
|
platforms:
|
||||||
|
- "Linux"
|
||||||
|
- "Windows"
|
||||||
|
- "macOS"
|
||||||
|
skillLevel: "Advanced"
|
||||||
|
accessType: "CLI"
|
||||||
|
url: "https://volatilityfoundation.org"
|
||||||
|
tags:
|
||||||
|
- "memory-analysis"
|
||||||
|
- "malware-detection"
|
||||||
|
- "process-analysis"
|
||||||
|
type: "FOSS"
|
||||||
|
|
||||||
|
- id: wireshark
|
||||||
|
name: "Wireshark"
|
||||||
|
description: "Network protocol analyzer and packet capture tool"
|
||||||
|
domains:
|
||||||
|
- "Network Forensics"
|
||||||
|
phases:
|
||||||
|
- "Examination"
|
||||||
|
- "Reporting"
|
||||||
|
platforms:
|
||||||
|
- "Linux"
|
||||||
|
- "Windows"
|
||||||
|
- "macOS"
|
||||||
|
skillLevel: "Intermediate"
|
||||||
|
accessType: "GUI"
|
||||||
|
url: "https://wireshark.org"
|
||||||
|
tags:
|
||||||
|
- "packet-analysis"
|
||||||
|
- "network-traffic"
|
||||||
|
- "protocol-dissection"
|
||||||
|
type: "FOSS"
|
||||||
|
|
||||||
|
- id: plaso
|
||||||
|
name: "Plaso"
|
||||||
|
description: "Super timeline all the things"
|
||||||
|
domains:
|
||||||
|
- "Filesystem Forensics"
|
||||||
|
phases:
|
||||||
|
- "Analysis"
|
||||||
|
- "Reporting"
|
||||||
|
platforms:
|
||||||
|
- "Linux"
|
||||||
|
- "Windows"
|
||||||
|
- "macOS"
|
||||||
|
skillLevel: "Advanced"
|
||||||
|
accessType: "CLI"
|
||||||
|
url: "https://plaso.readthedocs.io"
|
||||||
|
tags:
|
||||||
|
- "timeline"
|
||||||
|
- "log-analysis"
|
||||||
|
- "artifact-parsing"
|
||||||
|
type: "FOSS"
|
||||||
|
|
||||||
|
- id: yara
|
||||||
|
name: "YARA"
|
||||||
|
description: "Pattern matching engine for malware research"
|
||||||
|
domains:
|
||||||
|
- "Malware Analysis"
|
||||||
|
- "Live Forensics"
|
||||||
|
phases:
|
||||||
|
- "Data Collection"
|
||||||
|
- "Analysis"
|
||||||
|
platforms:
|
||||||
|
- "Linux"
|
||||||
|
- "Windows"
|
||||||
|
- "macOS"
|
||||||
|
skillLevel: "Advanced"
|
||||||
|
accessType: "CLI"
|
||||||
|
url: "https://virustotal.github.io/yara/"
|
||||||
|
tags:
|
||||||
|
- "pattern-matching"
|
||||||
|
- "malware-detection"
|
||||||
|
- "signatures"
|
||||||
|
type: "FOSS"
|
||||||
|
|
||||||
|
# Self-hosted services (what you call "SaaS Tools")
|
||||||
|
- id: timesketch
|
||||||
|
name: "Timesketch"
|
||||||
|
description: "Collaborative forensic timeline analysis"
|
||||||
|
domains:
|
||||||
|
- "Filesystem Forensics"
|
||||||
|
- "Network Forensics"
|
||||||
|
phases:
|
||||||
|
- "Analysis"
|
||||||
|
- "Reporting"
|
||||||
|
platforms:
|
||||||
|
- "Web"
|
||||||
|
skillLevel: "Intermediate"
|
||||||
|
accessType: "Web"
|
||||||
|
url: "https://timesketch.org"
|
||||||
|
tags:
|
||||||
|
- "timeline"
|
||||||
|
- "collaboration"
|
||||||
|
- "visualization"
|
||||||
|
type: "SaaS"
|
||||||
|
selfHosted: true
|
||||||
|
serviceUrl: "https://timesketch.lab.local" # Internal lab URL
|
||||||
|
|
||||||
|
- id: thehive
|
||||||
|
name: "TheHive"
|
||||||
|
description: "Scalable incident response platform"
|
||||||
|
domains:
|
||||||
|
- "Live Forensics"
|
||||||
|
phases:
|
||||||
|
- "Data Collection"
|
||||||
|
- "Analysis"
|
||||||
|
- "Reporting"
|
||||||
|
platforms:
|
||||||
|
- "Web"
|
||||||
|
skillLevel: "Intermediate"
|
||||||
|
accessType: "Web"
|
||||||
|
url: "https://thehive-project.org"
|
||||||
|
tags:
|
||||||
|
- "incident-response"
|
||||||
|
- "case-management"
|
||||||
|
- "collaboration"
|
||||||
|
type: "SaaS"
|
||||||
|
selfHosted: true
|
||||||
|
serviceUrl: "https://thehive.lab.local"
|
||||||
|
|
||||||
|
- id: misp
|
||||||
|
name: "MISP"
|
||||||
|
description: "Threat intelligence sharing platform"
|
||||||
|
domains:
|
||||||
|
- "Malware Analysis"
|
||||||
|
- "Live Forensics"
|
||||||
|
phases:
|
||||||
|
- "Analysis"
|
||||||
|
- "Reporting"
|
||||||
|
platforms:
|
||||||
|
- "Web"
|
||||||
|
skillLevel: "Advanced"
|
||||||
|
accessType: "Web"
|
||||||
|
url: "https://misp-project.org"
|
||||||
|
tags:
|
||||||
|
- "threat-intelligence"
|
||||||
|
- "ioc-sharing"
|
||||||
|
- "attribution"
|
||||||
|
type: "SaaS"
|
||||||
|
selfHosted: true
|
||||||
|
serviceUrl: "https://misp.lab.local"
|
||||||
|
|
||||||
|
# Additional metadata
|
||||||
|
metadata:
|
||||||
|
lastUpdated: "2025-01-15"
|
||||||
|
totalTools: 8
|
||||||
|
domains:
|
||||||
|
- "Filesystem Forensics"
|
||||||
|
- "Network Forensics"
|
||||||
|
- "Memory Forensics"
|
||||||
|
- "Live Forensics"
|
||||||
|
- "Malware Analysis"
|
||||||
|
- "Cryptocurrency"
|
||||||
|
phases:
|
||||||
|
- "Data Collection"
|
||||||
|
- "Examination"
|
||||||
|
- "Analysis"
|
||||||
|
- "Reporting"
|
231
src/index.njk
231
src/index.njk
@ -0,0 +1,231 @@
|
|||||||
|
---
|
||||||
|
layout: base.njk
|
||||||
|
title: "Home"
|
||||||
|
description: "Comprehensive directory of FOSS tools for Digital Forensics and Incident Response"
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Header Content -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||||
|
<!-- Introductory Text -->
|
||||||
|
<div class="prose prose-gray dark:prose-invert max-w-4xl mb-8">
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||||
|
Welcome to the comprehensive directory of Free and Open Source Software (FOSS) tools for Digital Forensics and Incident Response.
|
||||||
|
This framework organizes tools according to the standard DFIR methodology: Data Collection, Examination, Analysis, and Reporting.
|
||||||
|
Use the selectors below to discover tools that match your specific needs, or explore the complete matrix view to see the full ecosystem.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="relative max-w-md">
|
||||||
|
<svg class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-input"
|
||||||
|
placeholder="Search tools, descriptions, tags..."
|
||||||
|
class="pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 w-full text-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Links -->
|
||||||
|
<div class="flex flex-wrap gap-4 mb-6">
|
||||||
|
<button
|
||||||
|
id="mode-selector"
|
||||||
|
class="view-mode-btn view-mode-active"
|
||||||
|
data-mode="selector"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.207A1 1 0 013 6.5V4z"/>
|
||||||
|
</svg>
|
||||||
|
Tool Selector
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="mode-matrix"
|
||||||
|
class="view-mode-btn"
|
||||||
|
data-mode="matrix"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"/>
|
||||||
|
</svg>
|
||||||
|
Matrix View
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="mode-saas"
|
||||||
|
class="view-mode-btn"
|
||||||
|
data-mode="saas"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"/>
|
||||||
|
</svg>
|
||||||
|
Self-Hosted Services
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||||
|
<!-- Tool Selector Mode -->
|
||||||
|
<div id="view-selector" class="view-mode view-mode-active">
|
||||||
|
<!-- Dimension Selectors -->
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Select Tool Dimensions</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Forensics Domain</label>
|
||||||
|
<div class="relative">
|
||||||
|
<select
|
||||||
|
id="domain-select"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 appearance-none bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<option value="">Choose domain...</option>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option value="{{ domain }}">{{ domain }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<svg class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 w-5 h-5 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">DFIR Phase</label>
|
||||||
|
<div class="relative">
|
||||||
|
<select
|
||||||
|
id="phase-select"
|
||||||
|
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 appearance-none bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||||
|
>
|
||||||
|
<option value="">Choose phase...</option>
|
||||||
|
{% for phase in phases %}
|
||||||
|
<option value="{{ phase }}">{{ phase }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<svg class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 w-5 h-5 pointer-events-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-end">
|
||||||
|
<button
|
||||||
|
id="reset-filters"
|
||||||
|
class="w-full px-4 py-3 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Reset Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<div id="tool-results" class="hidden">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 id="results-title" class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
Matching Tools (<span id="results-count">0</span>)
|
||||||
|
</h2>
|
||||||
|
<p id="results-description" class="text-gray-600 dark:text-gray-300"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tools-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- Tools will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="no-results" class="text-center py-12 hidden">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-lg">No tools found matching your criteria.</p>
|
||||||
|
<button
|
||||||
|
id="reset-filters-empty"
|
||||||
|
class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Reset Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Default State -->
|
||||||
|
<div id="default-state" class="text-center py-16">
|
||||||
|
<div class="max-w-md mx-auto">
|
||||||
|
<svg class="w-16 h-16 text-gray-400 dark:text-gray-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.207A1 1 0 013 6.5V4z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Select Your Criteria</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
Choose a forensics domain and/or DFIR phase above to discover relevant tools,
|
||||||
|
or use the search bar to find specific tools.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Matrix View Mode -->
|
||||||
|
<div id="view-matrix" class="view-mode hidden">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">Complete Tool Matrix</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
Overview of all FOSS DFIR tools organized by domain and phase. Click any tool name for details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
||||||
|
<th class="p-4 text-left font-semibold text-gray-900 dark:text-gray-100 min-w-48">Domain / Phase</th>
|
||||||
|
{% for phase in phases %}
|
||||||
|
<th class="p-4 text-center font-semibold min-w-64 text-gray-900 dark:text-gray-100">
|
||||||
|
{{ phase }}
|
||||||
|
</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for domain in domains %}
|
||||||
|
<tr class="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||||
|
<td class="p-4 font-medium text-gray-900 dark:text-gray-100">{{ domain }}</td>
|
||||||
|
{% for phase in phases %}
|
||||||
|
<td class="p-4 align-top">
|
||||||
|
<div class="space-y-2" data-domain="{{ domain }}" data-phase="{{ phase }}">
|
||||||
|
<!-- Tools will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SaaS Tools Mode -->
|
||||||
|
<div id="view-saas" class="view-mode hidden">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-2">
|
||||||
|
Self-Hosted Services (<span id="saas-count">0</span>)
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
Self-hosted DFIR services available in your lab environment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="saas-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<!-- SaaS tools will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="no-saas-results" class="text-center py-12 hidden">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-lg">No self-hosted services found matching your search.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pass data to JavaScript -->
|
||||||
|
<script>
|
||||||
|
window.toolsData = {{ tools.tools | dump | safe }};
|
||||||
|
window.domains = {{ domains | dump | safe }};
|
||||||
|
window.phases = {{ phases | dump | safe }};
|
||||||
|
</script>
|
243
src/js/modal.js
243
src/js/modal.js
@ -0,0 +1,243 @@
|
|||||||
|
// Tool detail modal functionality
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let modal;
|
||||||
|
let modalContent;
|
||||||
|
let currentTool = null;
|
||||||
|
|
||||||
|
// Initialize modal system
|
||||||
|
function init() {
|
||||||
|
modal = document.getElementById('tool-modal');
|
||||||
|
modalContent = document.getElementById('tool-modal-content');
|
||||||
|
|
||||||
|
if (!modal || !modalContent) {
|
||||||
|
console.warn('Modal elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event listeners for modal
|
||||||
|
function setupEventListeners() {
|
||||||
|
// Click outside modal to close
|
||||||
|
modal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === modal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Escape key to close modal
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape' && !modal.classList.contains('hidden')) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent body scroll when modal is open
|
||||||
|
modal.addEventListener('scroll', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show tool detail modal
|
||||||
|
function showToolModal(tool) {
|
||||||
|
if (!tool || !modal || !modalContent) return;
|
||||||
|
|
||||||
|
currentTool = tool;
|
||||||
|
|
||||||
|
// Generate modal content
|
||||||
|
modalContent.innerHTML = generateModalContent(tool);
|
||||||
|
|
||||||
|
// Add close button handler
|
||||||
|
const closeButton = modalContent.querySelector('.modal-close');
|
||||||
|
if (closeButton) {
|
||||||
|
closeButton.addEventListener('click', closeModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add external link handler
|
||||||
|
const externalLink = modalContent.querySelector('.external-link');
|
||||||
|
if (externalLink) {
|
||||||
|
externalLink.addEventListener('click', (event) => {
|
||||||
|
// Let the link work normally, but track the click
|
||||||
|
trackToolClick(tool);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// Focus trap
|
||||||
|
trapFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal
|
||||||
|
function closeModal() {
|
||||||
|
if (!modal) return;
|
||||||
|
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
currentTool = null;
|
||||||
|
|
||||||
|
// Return focus to the tool card that was clicked
|
||||||
|
const activeToolCard = document.querySelector('.tool-card:focus');
|
||||||
|
if (activeToolCard) {
|
||||||
|
activeToolCard.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate modal HTML content
|
||||||
|
function generateModalContent(tool) {
|
||||||
|
const typeLabel = tool.type === 'SaaS' ?
|
||||||
|
'<span class="tag tag-purple">SaaS</span>' :
|
||||||
|
'<span class="tag tag-green">FOSS</span>';
|
||||||
|
|
||||||
|
const domains = (tool.domains || []).map(domain =>
|
||||||
|
`<span class="block tag tag-green mb-2">${domain}</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const phases = (tool.phases || []).map(phase =>
|
||||||
|
`<span class="block tag tag-blue mb-2">${phase}</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const tags = (tool.tags || []).map(tag =>
|
||||||
|
`<span class="tag tag-gray mr-2 mb-2">${tag}</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const platforms = (tool.platforms || []).join(', ');
|
||||||
|
|
||||||
|
// Handle service URL for self-hosted services
|
||||||
|
const serviceInfo = tool.selfHosted && tool.serviceUrl ?
|
||||||
|
`<div class="mb-4 p-3 bg-blue-50 dark:bg-blue-900 rounded-lg">
|
||||||
|
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||||
|
<strong>Lab Instance:</strong>
|
||||||
|
<a href="${tool.serviceUrl}" target="_blank" rel="noopener noreferrer"
|
||||||
|
class="underline hover:no-underline">${tool.serviceUrl}</a>
|
||||||
|
</p>
|
||||||
|
</div>` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="flex justify-between items-start mb-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">${tool.name}</h2>
|
||||||
|
<div class="flex items-center gap-2 mt-1">
|
||||||
|
${typeLabel}
|
||||||
|
<span class="text-sm text-gray-500 dark:text-gray-400">${tool.skillLevel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="modal-close text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 text-2xl"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-6">${tool.description}</p>
|
||||||
|
|
||||||
|
${serviceInfo}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-6 mb-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Domains</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
${domains || '<span class="text-gray-500 dark:text-gray-400 text-sm">None specified</span>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Phases</h3>
|
||||||
|
<div class="space-y-2">
|
||||||
|
${phases || '<span class="text-gray-500 dark:text-gray-400 text-sm">None specified</span>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-3">Tags</h3>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
${tags || '<span class="text-gray-500 dark:text-gray-400 text-sm">No tags</span>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div>Access: <span class="font-medium">${tool.accessType}</span></div>
|
||||||
|
<div>Platforms: <span class="font-medium">${platforms}</span></div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="${tool.url}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="external-link flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Visit Project
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple focus trap for modal
|
||||||
|
function trapFocus() {
|
||||||
|
const focusableElements = modal.querySelectorAll(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (focusableElements.length === 0) return;
|
||||||
|
|
||||||
|
const firstElement = focusableElements[0];
|
||||||
|
const lastElement = focusableElements[focusableElements.length - 1];
|
||||||
|
|
||||||
|
// Focus first element
|
||||||
|
firstElement.focus();
|
||||||
|
|
||||||
|
// Handle tab key
|
||||||
|
modal.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key !== 'Tab') return;
|
||||||
|
|
||||||
|
if (event.shiftKey) {
|
||||||
|
// Shift + Tab
|
||||||
|
if (document.activeElement === firstElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
lastElement.focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tab
|
||||||
|
if (document.activeElement === lastElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
firstElement.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track tool clicks for analytics (placeholder)
|
||||||
|
function trackToolClick(tool) {
|
||||||
|
// This is where you'd send analytics data
|
||||||
|
console.log('Tool clicked:', tool.name, tool.url);
|
||||||
|
|
||||||
|
// Could send to analytics service:
|
||||||
|
// analytics.track('tool_clicked', {
|
||||||
|
// tool_name: tool.name,
|
||||||
|
// tool_type: tool.type,
|
||||||
|
// tool_url: tool.url
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for use by other scripts
|
||||||
|
window.showToolModal = showToolModal;
|
||||||
|
window.closeToolModal = closeModal;
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
382
src/js/search.js
382
src/js/search.js
@ -0,0 +1,382 @@
|
|||||||
|
// Search and filtering functionality
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// State management
|
||||||
|
let currentFilters = {
|
||||||
|
search: '',
|
||||||
|
domain: '',
|
||||||
|
phase: '',
|
||||||
|
mode: 'selector'
|
||||||
|
};
|
||||||
|
|
||||||
|
let allTools = [];
|
||||||
|
let domains = [];
|
||||||
|
let phases = [];
|
||||||
|
|
||||||
|
// DOM elements
|
||||||
|
let searchInput;
|
||||||
|
let domainSelect;
|
||||||
|
let phaseSelect;
|
||||||
|
let resetButton;
|
||||||
|
let resetEmptyButton;
|
||||||
|
let toolResults;
|
||||||
|
let toolsGrid;
|
||||||
|
let defaultState;
|
||||||
|
let noResults;
|
||||||
|
let resultsTitle;
|
||||||
|
let resultsCount;
|
||||||
|
let resultsDescription;
|
||||||
|
let viewModes;
|
||||||
|
let modeButtons;
|
||||||
|
|
||||||
|
// Initialize when DOM is loaded
|
||||||
|
function init() {
|
||||||
|
// Get data from global variables (set by template)
|
||||||
|
if (typeof window.toolsData !== 'undefined') {
|
||||||
|
allTools = window.toolsData;
|
||||||
|
domains = window.domains || [];
|
||||||
|
phases = window.phases || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get DOM elements
|
||||||
|
searchInput = document.getElementById('search-input');
|
||||||
|
domainSelect = document.getElementById('domain-select');
|
||||||
|
phaseSelect = document.getElementById('phase-select');
|
||||||
|
resetButton = document.getElementById('reset-filters');
|
||||||
|
resetEmptyButton = document.getElementById('reset-filters-empty');
|
||||||
|
toolResults = document.getElementById('tool-results');
|
||||||
|
toolsGrid = document.getElementById('tools-grid');
|
||||||
|
defaultState = document.getElementById('default-state');
|
||||||
|
noResults = document.getElementById('no-results');
|
||||||
|
resultsTitle = document.getElementById('results-title');
|
||||||
|
resultsCount = document.getElementById('results-count');
|
||||||
|
resultsDescription = document.getElementById('results-description');
|
||||||
|
|
||||||
|
// View mode elements
|
||||||
|
viewModes = {
|
||||||
|
selector: document.getElementById('view-selector'),
|
||||||
|
matrix: document.getElementById('view-matrix'),
|
||||||
|
saas: document.getElementById('view-saas')
|
||||||
|
};
|
||||||
|
|
||||||
|
modeButtons = {
|
||||||
|
selector: document.getElementById('mode-selector'),
|
||||||
|
matrix: document.getElementById('mode-matrix'),
|
||||||
|
saas: document.getElementById('mode-saas')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', handleSearchChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainSelect) {
|
||||||
|
domainSelect.addEventListener('change', handleDomainChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phaseSelect) {
|
||||||
|
phaseSelect.addEventListener('change', handlePhaseChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetButton) {
|
||||||
|
resetButton.addEventListener('click', resetFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetEmptyButton) {
|
||||||
|
resetEmptyButton.addEventListener('click', resetFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode switching
|
||||||
|
Object.keys(modeButtons).forEach(mode => {
|
||||||
|
if (modeButtons[mode]) {
|
||||||
|
modeButtons[mode].addEventListener('click', () => switchMode(mode));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize views
|
||||||
|
updateDisplay();
|
||||||
|
populateMatrix();
|
||||||
|
populateSaasGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
function handleSearchChange(event) {
|
||||||
|
currentFilters.search = event.target.value;
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDomainChange(event) {
|
||||||
|
currentFilters.domain = event.target.value;
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePhaseChange(event) {
|
||||||
|
currentFilters.phase = event.target.value;
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
currentFilters.search = '';
|
||||||
|
currentFilters.domain = '';
|
||||||
|
currentFilters.phase = '';
|
||||||
|
|
||||||
|
if (searchInput) searchInput.value = '';
|
||||||
|
if (domainSelect) domainSelect.value = '';
|
||||||
|
if (phaseSelect) phaseSelect.value = '';
|
||||||
|
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchMode(mode) {
|
||||||
|
currentFilters.mode = mode;
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
Object.keys(modeButtons).forEach(m => {
|
||||||
|
if (modeButtons[m]) {
|
||||||
|
modeButtons[m].classList.toggle('view-mode-active', m === mode);
|
||||||
|
modeButtons[m].classList.toggle('view-mode-btn', m !== mode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update view visibility
|
||||||
|
Object.keys(viewModes).forEach(m => {
|
||||||
|
if (viewModes[m]) {
|
||||||
|
viewModes[m].classList.toggle('hidden', m !== mode);
|
||||||
|
viewModes[m].classList.toggle('view-mode-active', m === mode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update displays based on mode
|
||||||
|
if (mode === 'matrix') {
|
||||||
|
populateMatrix();
|
||||||
|
} else if (mode === 'saas') {
|
||||||
|
populateSaasGrid();
|
||||||
|
} else {
|
||||||
|
updateDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter tools based on current filters
|
||||||
|
function filterTools() {
|
||||||
|
let filtered = allTools;
|
||||||
|
|
||||||
|
// Filter by type based on mode
|
||||||
|
if (currentFilters.mode === 'saas') {
|
||||||
|
filtered = filtered.filter(tool => tool.type === 'SaaS');
|
||||||
|
} else if (currentFilters.mode === 'selector' || currentFilters.mode === 'matrix') {
|
||||||
|
filtered = filtered.filter(tool => tool.type === 'FOSS');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply search filter
|
||||||
|
if (currentFilters.search) {
|
||||||
|
const searchTerm = currentFilters.search.toLowerCase();
|
||||||
|
filtered = filtered.filter(tool =>
|
||||||
|
tool.name.toLowerCase().includes(searchTerm) ||
|
||||||
|
tool.description.toLowerCase().includes(searchTerm) ||
|
||||||
|
(tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(searchTerm)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply domain filter (only for selector mode)
|
||||||
|
if (currentFilters.mode === 'selector' && currentFilters.domain) {
|
||||||
|
filtered = filtered.filter(tool =>
|
||||||
|
tool.domains && tool.domains.includes(currentFilters.domain)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply phase filter (only for selector mode)
|
||||||
|
if (currentFilters.mode === 'selector' && currentFilters.phase) {
|
||||||
|
filtered = filtered.filter(tool =>
|
||||||
|
tool.phases && tool.phases.includes(currentFilters.phase)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the display based on current filters
|
||||||
|
function updateDisplay() {
|
||||||
|
if (currentFilters.mode !== 'selector') return;
|
||||||
|
|
||||||
|
const hasFilters = currentFilters.search || currentFilters.domain || currentFilters.phase;
|
||||||
|
|
||||||
|
if (!hasFilters) {
|
||||||
|
// Show default state
|
||||||
|
if (toolResults) toolResults.classList.add('hidden');
|
||||||
|
if (defaultState) defaultState.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = filterTools();
|
||||||
|
|
||||||
|
// Hide default state, show results
|
||||||
|
if (defaultState) defaultState.classList.add('hidden');
|
||||||
|
if (toolResults) toolResults.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Update results metadata
|
||||||
|
if (resultsCount) resultsCount.textContent = filtered.length;
|
||||||
|
updateResultsDescription();
|
||||||
|
|
||||||
|
// Show/hide no results message
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
if (toolsGrid) toolsGrid.classList.add('hidden');
|
||||||
|
if (noResults) noResults.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
if (noResults) noResults.classList.add('hidden');
|
||||||
|
if (toolsGrid) toolsGrid.classList.remove('hidden');
|
||||||
|
renderTools(filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update results description text
|
||||||
|
function updateResultsDescription() {
|
||||||
|
if (!resultsDescription) return;
|
||||||
|
|
||||||
|
const parts = [];
|
||||||
|
if (currentFilters.domain) parts.push(`Domain: ${currentFilters.domain}`);
|
||||||
|
if (currentFilters.phase) parts.push(`Phase: ${currentFilters.phase}`);
|
||||||
|
if (currentFilters.search) parts.push(`Search: "${currentFilters.search}"`);
|
||||||
|
|
||||||
|
resultsDescription.textContent = parts.join(' • ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render tools in the grid
|
||||||
|
function renderTools(tools) {
|
||||||
|
if (!toolsGrid) return;
|
||||||
|
|
||||||
|
toolsGrid.innerHTML = tools.map(tool => createToolCard(tool)).join('');
|
||||||
|
|
||||||
|
// Add click handlers for tool cards
|
||||||
|
const toolCards = toolsGrid.querySelectorAll('.tool-card');
|
||||||
|
toolCards.forEach((card, index) => {
|
||||||
|
card.addEventListener('click', () => showToolModal(tools[index]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTML for a tool card
|
||||||
|
function createToolCard(tool) {
|
||||||
|
const typeLabel = tool.type === 'SaaS' ?
|
||||||
|
'<span class="tag tag-purple">SaaS</span>' : '';
|
||||||
|
|
||||||
|
const tags = (tool.tags || []).slice(0, 3).map(tag =>
|
||||||
|
`<span class="tag tag-blue">${tag}</span>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const platforms = (tool.platforms || []).join(', ');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="tool-card" data-tool-id="${tool.id}">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 text-lg">
|
||||||
|
${tool.name}
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
${typeLabel}
|
||||||
|
<svg class="w-4 h-4 text-gray-400 hover:text-blue-600 dark:text-gray-500 dark:hover:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 text-sm mb-3">${tool.description}</p>
|
||||||
|
<div class="flex flex-wrap gap-1 mb-2">
|
||||||
|
${tags}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
${tool.skillLevel} • ${tool.accessType} • ${platforms}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate matrix view
|
||||||
|
function populateMatrix() {
|
||||||
|
if (currentFilters.mode !== 'matrix') return;
|
||||||
|
|
||||||
|
domains.forEach(domain => {
|
||||||
|
phases.forEach(phase => {
|
||||||
|
const cell = document.querySelector(`[data-domain="${domain}"][data-phase="${phase}"]`);
|
||||||
|
if (!cell) return;
|
||||||
|
|
||||||
|
const cellTools = allTools.filter(tool =>
|
||||||
|
tool.type === 'FOSS' &&
|
||||||
|
tool.domains && tool.domains.includes(domain) &&
|
||||||
|
tool.phases && tool.phases.includes(phase) &&
|
||||||
|
(!currentFilters.search ||
|
||||||
|
tool.name.toLowerCase().includes(currentFilters.search.toLowerCase()) ||
|
||||||
|
tool.description.toLowerCase().includes(currentFilters.search.toLowerCase()) ||
|
||||||
|
(tool.tags && tool.tags.some(tag => tag.toLowerCase().includes(currentFilters.search.toLowerCase()))))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cellTools.length === 0) {
|
||||||
|
cell.innerHTML = '<div class="text-gray-400 dark:text-gray-500 text-sm text-center py-2">-</div>';
|
||||||
|
} else {
|
||||||
|
cell.innerHTML = cellTools.map(tool =>
|
||||||
|
`<div class="matrix-cell-tool" data-tool-id="${tool.id}">${tool.name}</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
const toolElements = cell.querySelectorAll('.matrix-cell-tool');
|
||||||
|
toolElements.forEach((element, index) => {
|
||||||
|
element.addEventListener('click', () => showToolModal(cellTools[index]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate SaaS grid
|
||||||
|
function populateSaasGrid() {
|
||||||
|
if (currentFilters.mode !== 'saas') return;
|
||||||
|
|
||||||
|
const saasGrid = document.getElementById('saas-grid');
|
||||||
|
const saasCount = document.getElementById('saas-count');
|
||||||
|
const noSaasResults = document.getElementById('no-saas-results');
|
||||||
|
|
||||||
|
if (!saasGrid) return;
|
||||||
|
|
||||||
|
const saasTools = filterTools();
|
||||||
|
|
||||||
|
if (saasCount) saasCount.textContent = saasTools.length;
|
||||||
|
|
||||||
|
if (saasTools.length === 0) {
|
||||||
|
saasGrid.classList.add('hidden');
|
||||||
|
if (noSaasResults) noSaasResults.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
if (noSaasResults) noSaasResults.classList.add('hidden');
|
||||||
|
saasGrid.classList.remove('hidden');
|
||||||
|
|
||||||
|
saasGrid.innerHTML = saasTools.map(tool => createToolCard(tool)).join('');
|
||||||
|
|
||||||
|
// Add click handlers
|
||||||
|
const toolCards = saasGrid.querySelectorAll('.tool-card');
|
||||||
|
toolCards.forEach((card, index) => {
|
||||||
|
card.addEventListener('click', () => showToolModal(saasTools[index]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show tool modal (defined in modal.js)
|
||||||
|
function showToolModal(tool) {
|
||||||
|
if (typeof window.showToolModal === 'function') {
|
||||||
|
window.showToolModal(tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use by other scripts
|
||||||
|
window.searchModule = {
|
||||||
|
init,
|
||||||
|
filterTools,
|
||||||
|
updateDisplay,
|
||||||
|
populateMatrix,
|
||||||
|
populateSaasGrid,
|
||||||
|
resetFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
385
src/js/status.js
385
src/js/status.js
@ -0,0 +1,385 @@
|
|||||||
|
// Status page functionality for Uptime Kuma integration
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let servicesData = {};
|
||||||
|
let uptimeKumaConfig = {};
|
||||||
|
let refreshInterval;
|
||||||
|
|
||||||
|
// Initialize status page
|
||||||
|
function init() {
|
||||||
|
// Get data from global variables
|
||||||
|
if (typeof window.servicesData !== 'undefined') {
|
||||||
|
servicesData = window.servicesData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.uptimeKumaConfig !== 'undefined') {
|
||||||
|
uptimeKumaConfig = window.uptimeKumaConfig || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up refresh button
|
||||||
|
const refreshButton = document.getElementById('refresh-status');
|
||||||
|
if (refreshButton) {
|
||||||
|
refreshButton.addEventListener('click', refreshStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Kuma configuration display
|
||||||
|
updateKumaStatusDisplay();
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
loadServiceStatus();
|
||||||
|
|
||||||
|
// Set up auto-refresh if Uptime Kuma is enabled
|
||||||
|
if (uptimeKumaConfig.enabled && uptimeKumaConfig.refreshInterval) {
|
||||||
|
refreshInterval = setInterval(loadServiceStatus, uptimeKumaConfig.refreshInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load service status (from Uptime Kuma API or static data)
|
||||||
|
async function loadServiceStatus() {
|
||||||
|
try {
|
||||||
|
let services;
|
||||||
|
|
||||||
|
if (uptimeKumaConfig.enabled && uptimeKumaConfig.apiUrl) {
|
||||||
|
// Try to fetch from Uptime Kuma API
|
||||||
|
services = await fetchFromUptimeKuma();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!services) {
|
||||||
|
// Fall back to static data
|
||||||
|
services = servicesData.services || [];
|
||||||
|
updateKumaConnectionStatus('static');
|
||||||
|
} else {
|
||||||
|
updateKumaConnectionStatus('connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render services
|
||||||
|
renderServices(services);
|
||||||
|
renderOverallStatus(services);
|
||||||
|
updateLastUpdated();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load service status:', error);
|
||||||
|
|
||||||
|
// Fall back to static data
|
||||||
|
const services = servicesData.services || [];
|
||||||
|
renderServices(services);
|
||||||
|
renderOverallStatus(services);
|
||||||
|
updateKumaConnectionStatus('error');
|
||||||
|
updateLastUpdated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data from Uptime Kuma API
|
||||||
|
async function fetchFromUptimeKuma() {
|
||||||
|
if (!uptimeKumaConfig.apiUrl) {
|
||||||
|
throw new Error('Uptime Kuma API URL not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {};
|
||||||
|
if (uptimeKumaConfig.apiKey) {
|
||||||
|
headers['Authorization'] = `Bearer ${uptimeKumaConfig.apiKey}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${uptimeKumaConfig.apiUrl}/status-page/heartbeat`, {
|
||||||
|
headers,
|
||||||
|
timeout: 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Transform Uptime Kuma data to our format
|
||||||
|
return transformUptimeKumaData(data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to fetch from Uptime Kuma:', error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform Uptime Kuma API response to our service format
|
||||||
|
function transformUptimeKumaData(kumaData) {
|
||||||
|
// This is a simplified transformation - adjust based on actual Uptime Kuma API response
|
||||||
|
const services = [];
|
||||||
|
|
||||||
|
if (kumaData.heartbeatList) {
|
||||||
|
Object.entries(kumaData.heartbeatList).forEach(([monitorId, heartbeats]) => {
|
||||||
|
const monitor = kumaData.monitors?.[monitorId];
|
||||||
|
if (!monitor) return;
|
||||||
|
|
||||||
|
const latestHeartbeat = heartbeats[heartbeats.length - 1];
|
||||||
|
if (!latestHeartbeat) return;
|
||||||
|
|
||||||
|
services.push({
|
||||||
|
id: monitorId,
|
||||||
|
name: monitor.name,
|
||||||
|
description: monitor.description || '',
|
||||||
|
url: monitor.url,
|
||||||
|
category: monitor.tags?.[0] || 'Infrastructure',
|
||||||
|
status: latestHeartbeat.status === 1 ? 'operational' : 'down',
|
||||||
|
uptime: calculateUptime(heartbeats),
|
||||||
|
responseTime: `${latestHeartbeat.ping}ms`,
|
||||||
|
lastChecked: new Date(latestHeartbeat.time).toISOString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate uptime percentage from heartbeat data
|
||||||
|
function calculateUptime(heartbeats) {
|
||||||
|
if (!heartbeats || heartbeats.length === 0) return '0%';
|
||||||
|
|
||||||
|
const total = heartbeats.length;
|
||||||
|
const successful = heartbeats.filter(h => h.status === 1).length;
|
||||||
|
const percentage = (successful / total) * 100;
|
||||||
|
|
||||||
|
return `${percentage.toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render services list
|
||||||
|
function renderServices(services) {
|
||||||
|
const serviceList = document.getElementById('service-list');
|
||||||
|
if (!serviceList) return;
|
||||||
|
|
||||||
|
if (!services || services.length === 0) {
|
||||||
|
serviceList.innerHTML = `
|
||||||
|
<div class="text-center py-8">
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">No services configured</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceList.innerHTML = services.map(service => createServiceCard(service)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create HTML for a service card
|
||||||
|
function createServiceCard(service) {
|
||||||
|
const statusClass = getStatusClass(service.status);
|
||||||
|
const statusText = service.status.charAt(0).toUpperCase() + service.status.slice(1);
|
||||||
|
const lastChecked = service.lastChecked ?
|
||||||
|
new Date(service.lastChecked).toLocaleString() : 'Unknown';
|
||||||
|
|
||||||
|
const issuesSection = service.issues ?
|
||||||
|
`<div class="mt-3 p-3 bg-yellow-50 dark:bg-yellow-900 rounded border-l-4 border-yellow-400">
|
||||||
|
<p class="text-sm text-yellow-800 dark:text-yellow-200">${service.issues}</p>
|
||||||
|
</div>` : '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="status-indicator ${service.status}"></div>
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">${service.name}</h3>
|
||||||
|
${service.description ? `<p class="text-sm text-gray-600 dark:text-gray-400">${service.description}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="status-badge ${service.status}">${statusText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Uptime:</span>
|
||||||
|
<span class="ml-2 font-medium text-gray-900 dark:text-gray-100">${service.uptime || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Response:</span>
|
||||||
|
<span class="ml-2 font-medium text-gray-900 dark:text-gray-100">${service.responseTime || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Category:</span>
|
||||||
|
<span class="ml-2 font-medium text-gray-900 dark:text-gray-100">${service.category || 'Unknown'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Last Check:</span>
|
||||||
|
<span class="ml-2 font-medium text-gray-900 dark:text-gray-100" title="${lastChecked}">
|
||||||
|
${formatRelativeTime(service.lastChecked)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${issuesSection}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render overall status summary
|
||||||
|
function renderOverallStatus(services) {
|
||||||
|
if (!services || services.length === 0) return;
|
||||||
|
|
||||||
|
const statusCounts = {
|
||||||
|
operational: services.filter(s => s.status === 'operational').length,
|
||||||
|
degraded: services.filter(s => s.status === 'degraded').length,
|
||||||
|
maintenance: services.filter(s => s.status === 'maintenance').length,
|
||||||
|
down: services.filter(s => s.status === 'down').length
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = services.length;
|
||||||
|
|
||||||
|
// Determine overall status
|
||||||
|
let overallStatus, overallText;
|
||||||
|
if (statusCounts.down > 0) {
|
||||||
|
overallStatus = 'down';
|
||||||
|
overallText = 'Major Outage';
|
||||||
|
} else if (statusCounts.degraded > 0 || statusCounts.maintenance > 0) {
|
||||||
|
overallStatus = statusCounts.maintenance > 0 ? 'maintenance' : 'degraded';
|
||||||
|
overallText = statusCounts.maintenance > 0 ? 'Partial Service' : 'Degraded Performance';
|
||||||
|
} else {
|
||||||
|
overallStatus = 'operational';
|
||||||
|
overallText = 'All Systems Operational';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update overall status display
|
||||||
|
const indicator = document.getElementById('overall-indicator');
|
||||||
|
const textElement = document.getElementById('overall-text');
|
||||||
|
const summaryElement = document.getElementById('overall-summary');
|
||||||
|
|
||||||
|
if (indicator) {
|
||||||
|
const statusDot = indicator.querySelector('.status-indicator');
|
||||||
|
if (statusDot) {
|
||||||
|
statusDot.className = `status-indicator ${overallStatus}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textElement) {
|
||||||
|
textElement.textContent = overallText;
|
||||||
|
textElement.className = getStatusTextClass(overallStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryElement) {
|
||||||
|
const parts = [];
|
||||||
|
if (statusCounts.operational > 0) parts.push(`${statusCounts.operational} operational`);
|
||||||
|
if (statusCounts.degraded > 0) parts.push(`${statusCounts.degraded} degraded`);
|
||||||
|
if (statusCounts.maintenance > 0) parts.push(`${statusCounts.maintenance} maintenance`);
|
||||||
|
if (statusCounts.down > 0) parts.push(`${statusCounts.down} down`);
|
||||||
|
|
||||||
|
summaryElement.textContent = `${parts.join(' • ')} of ${total} services`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Uptime Kuma connection status display
|
||||||
|
function updateKumaConnectionStatus(status) {
|
||||||
|
const kumaStatus = document.getElementById('kuma-connection-status');
|
||||||
|
const kumaEndpoint = document.getElementById('kuma-endpoint');
|
||||||
|
|
||||||
|
if (kumaStatus) {
|
||||||
|
switch (status) {
|
||||||
|
case 'connected':
|
||||||
|
kumaStatus.textContent = 'Connected';
|
||||||
|
kumaStatus.className = 'tag tag-green';
|
||||||
|
break;
|
||||||
|
case 'static':
|
||||||
|
kumaStatus.textContent = 'Using Static Data';
|
||||||
|
kumaStatus.className = 'tag tag-blue';
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
kumaStatus.textContent = 'Connection Failed';
|
||||||
|
kumaStatus.className = 'tag tag-red';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
kumaStatus.textContent = 'Not Configured';
|
||||||
|
kumaStatus.className = 'tag tag-gray';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kumaEndpoint) {
|
||||||
|
kumaEndpoint.textContent = uptimeKumaConfig.apiUrl || 'Not configured';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Kuma status display
|
||||||
|
function updateKumaStatusDisplay() {
|
||||||
|
const kumaStatusEl = document.getElementById('uptime-kuma-status');
|
||||||
|
if (!kumaStatusEl) return;
|
||||||
|
|
||||||
|
if (uptimeKumaConfig.enabled) {
|
||||||
|
kumaStatusEl.classList.remove('border-dashed');
|
||||||
|
kumaStatusEl.classList.add('border-solid', 'border-blue-300', 'dark:border-blue-600');
|
||||||
|
kumaStatusEl.classList.remove('bg-gray-100', 'dark:bg-gray-700');
|
||||||
|
kumaStatusEl.classList.add('bg-blue-50', 'dark:bg-blue-900');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual refresh
|
||||||
|
function refreshStatus() {
|
||||||
|
const button = document.getElementById('refresh-status');
|
||||||
|
if (button) {
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Refreshing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
loadServiceStatus().finally(() => {
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Refresh Status';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last updated timestamp
|
||||||
|
function updateLastUpdated() {
|
||||||
|
const element = document.getElementById('last-updated');
|
||||||
|
if (element) {
|
||||||
|
element.textContent = new Date().toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
function getStatusClass(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'operational': return 'operational';
|
||||||
|
case 'degraded': return 'degraded';
|
||||||
|
case 'maintenance': return 'maintenance';
|
||||||
|
case 'down': return 'down';
|
||||||
|
default: return 'down';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusTextClass(status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'operational': return 'text-green-600 dark:text-green-400 font-medium';
|
||||||
|
case 'degraded': return 'text-yellow-600 dark:text-yellow-400 font-medium';
|
||||||
|
case 'maintenance': return 'text-blue-600 dark:text-blue-400 font-medium';
|
||||||
|
case 'down': return 'text-red-600 dark:text-red-400 font-medium';
|
||||||
|
default: return 'text-gray-600 dark:text-gray-400 font-medium';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRelativeTime(dateString) {
|
||||||
|
if (!dateString) return 'Unknown';
|
||||||
|
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - date;
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now';
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on page unload
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
if (refreshInterval) {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
140
src/js/theme.js
140
src/js/theme.js
@ -0,0 +1,140 @@
|
|||||||
|
// Theme management functionality
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let currentTheme = 'auto';
|
||||||
|
let mediaQuery;
|
||||||
|
|
||||||
|
// Initialize theme system
|
||||||
|
function init() {
|
||||||
|
// Get stored theme or default to auto
|
||||||
|
currentTheme = localStorage.getItem('theme') || 'auto';
|
||||||
|
|
||||||
|
// Set up media query listener for auto mode
|
||||||
|
mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||||
|
|
||||||
|
// Apply initial theme
|
||||||
|
applyTheme(currentTheme);
|
||||||
|
|
||||||
|
// Set up theme selector buttons
|
||||||
|
setupThemeSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up theme selector button event handlers
|
||||||
|
function setupThemeSelector() {
|
||||||
|
const themeButtons = document.querySelectorAll('.theme-btn');
|
||||||
|
|
||||||
|
themeButtons.forEach(button => {
|
||||||
|
const theme = button.getAttribute('data-theme');
|
||||||
|
|
||||||
|
// Set initial active state
|
||||||
|
button.classList.toggle('active', theme === currentTheme);
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
setTheme(theme);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set theme and update UI
|
||||||
|
function setTheme(theme) {
|
||||||
|
if (!['light', 'dark', 'auto'].includes(theme)) {
|
||||||
|
console.warn('Invalid theme:', theme);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTheme = theme;
|
||||||
|
|
||||||
|
// Store in localStorage
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
|
||||||
|
// Apply theme
|
||||||
|
applyTheme(theme);
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
updateThemeButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme to document
|
||||||
|
function applyTheme(theme) {
|
||||||
|
const html = document.documentElement;
|
||||||
|
|
||||||
|
// Update theme class
|
||||||
|
html.className = html.className.replace(/theme-\w+/g, '');
|
||||||
|
html.classList.add(`theme-${theme}`);
|
||||||
|
|
||||||
|
// Handle dark mode class
|
||||||
|
if (theme === 'auto') {
|
||||||
|
// Use system preference
|
||||||
|
const isDark = mediaQuery ? mediaQuery.matches :
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
html.classList.toggle('dark', isDark);
|
||||||
|
} else {
|
||||||
|
// Use explicit theme
|
||||||
|
html.classList.toggle('dark', theme === 'dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle system theme changes (for auto mode)
|
||||||
|
function handleSystemThemeChange(event) {
|
||||||
|
if (currentTheme === 'auto') {
|
||||||
|
document.documentElement.classList.toggle('dark', event.matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update theme button active states
|
||||||
|
function updateThemeButtons() {
|
||||||
|
const themeButtons = document.querySelectorAll('.theme-btn');
|
||||||
|
|
||||||
|
themeButtons.forEach(button => {
|
||||||
|
const theme = button.getAttribute('data-theme');
|
||||||
|
button.classList.toggle('active', theme === currentTheme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current effective theme (resolves 'auto' to actual theme)
|
||||||
|
function getEffectiveTheme() {
|
||||||
|
if (currentTheme === 'auto') {
|
||||||
|
return mediaQuery && mediaQuery.matches ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
return currentTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle between light and dark (skips auto)
|
||||||
|
function toggleTheme() {
|
||||||
|
const effectiveTheme = getEffectiveTheme();
|
||||||
|
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keyboard shortcut support
|
||||||
|
function setupKeyboardShortcuts() {
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
// Ctrl/Cmd + Shift + T to toggle theme
|
||||||
|
if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'T') {
|
||||||
|
event.preventDefault();
|
||||||
|
toggleTheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export functions for external use
|
||||||
|
window.themeManager = {
|
||||||
|
setTheme,
|
||||||
|
getTheme: () => currentTheme,
|
||||||
|
getEffectiveTheme,
|
||||||
|
toggleTheme
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize when DOM is ready
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
init();
|
||||||
|
setupKeyboardShortcuts();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
setupKeyboardShortcuts();
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
layout: base.njk
|
||||||
|
title: "Privacy Policy"
|
||||||
|
description: "Privacy policy and data handling practices for the DFIR Tools Hub"
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">Privatsphäre und Datenschutz</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
Ich stehe für Privatsphäre und Datenschutz und gehe damit transparent um.
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||||
|
Letztes Update: 13.07.2025
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-8">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Datensammlung</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-4">
|
||||||
|
Auf dieser Webseite werden nur minimale technisch nicht vermeidbare Dsten gesammelt. Diese Plattform agiert nach dem Prinzip privacy-first und existiert als statische Website ohne Datenaggregation durch den Server.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Daten, die unweigerlich gespeichert werden:</h3>
|
||||||
|
<ul class="text-gray-600 dark:text-gray-300 space-y-2 ml-4">
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 mt-1">•</span>
|
||||||
|
<span><strong>Lokaler Browserspeicher:</strong> Präferenzen zum ausgewählten Farbschema (hell/dunkel) werden in Ihrem Browser gespeichert.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 mt-1">•</span>
|
||||||
|
<span><strong>Server Logs:</strong> Es werden standardisierte Accesslogs von NGINX undd HAProxy infrastrukturbedingt aufgezeichnet (IP-Adresse, Zeitstempel, HTTP-Endpunkt). Diese dienen ausschließlich der operativen Sicherheit und werden auf gerichtlichen Beschluss lediglich autorisierten Behörden ausgehändigt.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Was NICHT gesammelt wird:</h2>
|
||||||
|
<div class="bg-green-50 dark:bg-green-900 p-4 rounded-lg">
|
||||||
|
<ul class="text-green-800 dark:text-green-200 space-y-2">
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-400 mt-1">✓</span>
|
||||||
|
<span>Persönliche Informationen (Namen, E-Mails, Adressen)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-400 mt-1">✓</span>
|
||||||
|
<span>Nutzeraccounts oder Authentifizierungsdaten</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-400 mt-1">✓</span>
|
||||||
|
<span>Jegliche Art von Tracking-Cookies oder sonstige Mechanismen zum Fingerprinting</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-400 mt-1">✓</span>
|
||||||
|
<span>Verhaltensanalytische oder biometrische Daten</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-green-600 dark:text-green-400 mt-1">✓</span>
|
||||||
|
<span>Keine Interaktion mit kommerziellen Netzwerken, außer dem ISP zur Bereitstellung der Seite
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Cookies und lokaler Speicher Ihres Browsers</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-4">
|
||||||
|
Der Browserspeicher wird minimalistisch und nur zu o.g. Zwecken genutzt. Wir speichern keine Cookies.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<th class="p-3 text-left font-semibold text-gray-900 dark:text-gray-100">Storage Type</th>
|
||||||
|
<th class="p-3 text-left font-semibold text-gray-900 dark:text-gray-100">Purpose</th>
|
||||||
|
<th class="p-3 text-left font-semibold text-gray-900 dark:text-gray-100">Retention</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-sm">
|
||||||
|
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<td class="p-3 text-gray-900 dark:text-gray-100 font-medium">Theme Preference</td>
|
||||||
|
<td class="p-3 text-gray-600 dark:text-gray-300">Remember your dark/light mode choice</td>
|
||||||
|
<td class="p-3 text-gray-600 dark:text-gray-300">Until manually cleared</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<td class="p-3 text-gray-900 dark:text-gray-100 font-medium">Search Filters</td>
|
||||||
|
<td class="p-3 text-gray-600 dark:text-gray-300">Maintain search state during session</td>
|
||||||
|
<td class="p-3 text-gray-600 dark:text-gray-300">Session only</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Drittparteien und Dienste</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">Externe Verlinkungen</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
|
Links to external tools (GitHub repositories, project websites) are provided for convenience.
|
||||||
|
These external sites have their own privacy policies and data handling practices.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
|
<h3 class="font-semibold text-gray-900 dark:text-gray-100 mb-2">Bereitgestellte Dienste</h3>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 text-sm">
|
||||||
|
The "Self-Hosted Services" section links to services deployed within your local network.
|
||||||
|
Data handling for these services is governed by your institution's policies and the
|
||||||
|
specific configuration of each service.
|
||||||
|
Die als "on-premise" getaggten oder sonst offensichtlich gemachten Dienste werden innerhalb des CC24-Clusters (Teil des Mikoshi Compute Clusters) gehostet.
|
||||||
|
Jegliche Dateneingabe oder -verarbeitung unterliegt der Verantwortung des Nutzers, der die Daten erstmalig einbringt. Werden Vorschriften bestimmter Organisationen, die mit Ihnen in Verbindung stehen, berührt, sind Sie allein dafür verantwortlich, was Sie hochladen und verarbeiten.
|
||||||
|
Daten, deren Besitz strafbar ist, werden nicht toleriert.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">Data Security</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 leading-relaxed mb-4">
|
||||||
|
Ich implementiere zeitgemäße technische Maßnahmen, um Ihre Daten zu schützen:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="text-gray-600 dark:text-gray-300 space-y-2 ml-4">
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 mt-1">•</span>
|
||||||
|
<span><strong>Transport Security:</strong> Alle Verbindungen werden TLS-verschlüsselt.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 mt-1">•</span>
|
||||||
|
<span><strong>Statische Architektur:</strong> Sofern es die Applikation nicht offensichtlich erfordert, werden serverseitig keine Daten gespeichert.</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 mt-1">•</span>
|
||||||
|
<span><strong>Lokale Verarbeitung:</strong> Suche und Filterung dieser Seite werden lokal auf Ihrem Gerät durchgeführt (Browser-Funktion)</span>
|
||||||
|
</li>
|
||||||
|
<li class="flex items-start gap-2">
|
||||||
|
<span class="text-blue-600 dark:text-blue-400 mt-1">•</span>
|
||||||
|
<span><strong>Reguläre Updates:</strong> Abhängigkeiten und Sicherheitspatches werden regelmäßig geprüft und integriert.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,457 @@
|
|||||||
|
// Variables
|
||||||
|
:root {
|
||||||
|
--color-blue-50: #eff6ff;
|
||||||
|
--color-blue-100: #dbeafe;
|
||||||
|
--color-blue-500: #3b82f6;
|
||||||
|
--color-blue-600: #2563eb;
|
||||||
|
--color-blue-700: #1d4ed8;
|
||||||
|
--color-blue-800: #1e40af;
|
||||||
|
--color-blue-900: #1e3a8a;
|
||||||
|
|
||||||
|
--color-gray-50: #f9fafb;
|
||||||
|
--color-gray-100: #f3f4f6;
|
||||||
|
--color-gray-200: #e5e7eb;
|
||||||
|
--color-gray-300: #d1d5db;
|
||||||
|
--color-gray-400: #9ca3af;
|
||||||
|
--color-gray-500: #6b7280;
|
||||||
|
--color-gray-600: #4b5563;
|
||||||
|
--color-gray-700: #374151;
|
||||||
|
--color-gray-800: #1f2937;
|
||||||
|
--color-gray-900: #111827;
|
||||||
|
|
||||||
|
--color-green-100: #dcfce7;
|
||||||
|
--color-green-600: #16a34a;
|
||||||
|
--color-green-800: #166534;
|
||||||
|
--color-green-900: #14532d;
|
||||||
|
|
||||||
|
--color-purple-100: #f3e8ff;
|
||||||
|
--color-purple-600: #9333ea;
|
||||||
|
--color-purple-800: #6b21a8;
|
||||||
|
--color-purple-900: #581c87;
|
||||||
|
|
||||||
|
--color-yellow-100: #fef3c7;
|
||||||
|
--color-yellow-600: #d97706;
|
||||||
|
--color-yellow-800: #92400e;
|
||||||
|
--color-yellow-900: #78350f;
|
||||||
|
|
||||||
|
--color-red-100: #fee2e2;
|
||||||
|
--color-red-600: #dc2626;
|
||||||
|
--color-red-800: #991b1b;
|
||||||
|
--color-red-900: #7f1d1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme overrides
|
||||||
|
.dark {
|
||||||
|
--color-gray-50: #111827;
|
||||||
|
--color-gray-100: #1f2937;
|
||||||
|
--color-gray-200: #374151;
|
||||||
|
--color-gray-300: #4b5563;
|
||||||
|
--color-gray-400: #6b7280;
|
||||||
|
--color-gray-500: #9ca3af;
|
||||||
|
--color-gray-600: #d1d5db;
|
||||||
|
--color-gray-700: #e5e7eb;
|
||||||
|
--color-gray-800: #f3f4f6;
|
||||||
|
--color-gray-900: #f9fafb;
|
||||||
|
|
||||||
|
--color-blue-100: #1e3a8a;
|
||||||
|
--color-blue-400: #60a5fa;
|
||||||
|
--color-blue-500: #3b82f6;
|
||||||
|
|
||||||
|
--color-green-100: #14532d;
|
||||||
|
--color-green-400: #4ade80;
|
||||||
|
|
||||||
|
--color-purple-100: #581c87;
|
||||||
|
--color-purple-200: #a855f7;
|
||||||
|
|
||||||
|
--color-yellow-100: #78350f;
|
||||||
|
--color-yellow-400: #fbbf24;
|
||||||
|
|
||||||
|
--color-red-100: #7f1d1d;
|
||||||
|
--color-red-400: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset and base styles
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Classes
|
||||||
|
.transition-colors { transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; }
|
||||||
|
.transition-shadow { transition: box-shadow 0.15s ease-in-out; }
|
||||||
|
.duration-200 { transition-duration: 200ms; }
|
||||||
|
|
||||||
|
// Background colors
|
||||||
|
.bg-gray-50 { background-color: var(--color-gray-50); }
|
||||||
|
.bg-gray-100 { background-color: var(--color-gray-100); }
|
||||||
|
.bg-gray-600 { background-color: var(--color-gray-600); }
|
||||||
|
.bg-gray-700 { background-color: var(--color-gray-700); }
|
||||||
|
.bg-gray-800 { background-color: var(--color-gray-800); }
|
||||||
|
.bg-gray-900 { background-color: var(--color-gray-900); }
|
||||||
|
.bg-white { background-color: white; }
|
||||||
|
.bg-blue-50 { background-color: var(--color-blue-50); }
|
||||||
|
.bg-blue-100 { background-color: var(--color-blue-100); }
|
||||||
|
.bg-blue-500 { background-color: var(--color-blue-500); }
|
||||||
|
.bg-blue-600 { background-color: var(--color-blue-600); }
|
||||||
|
.bg-blue-700 { background-color: var(--color-blue-700); }
|
||||||
|
.bg-blue-800 { background-color: var(--color-blue-800); }
|
||||||
|
.bg-blue-900 { background-color: var(--color-blue-900); }
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
.text-gray-100 { color: var(--color-gray-100); }
|
||||||
|
.text-gray-300 { color: var(--color-gray-300); }
|
||||||
|
.text-gray-400 { color: var(--color-gray-400); }
|
||||||
|
.text-gray-500 { color: var(--color-gray-500); }
|
||||||
|
.text-gray-600 { color: var(--color-gray-600); }
|
||||||
|
.text-gray-700 { color: var(--color-gray-700); }
|
||||||
|
.text-gray-900 { color: var(--color-gray-900); }
|
||||||
|
.text-white { color: white; }
|
||||||
|
.text-blue-600 { color: var(--color-blue-600); }
|
||||||
|
.text-blue-800 { color: var(--color-blue-800); }
|
||||||
|
.text-blue-900 { color: var(--color-blue-900); }
|
||||||
|
.text-green-600 { color: var(--color-green-600); }
|
||||||
|
.text-green-800 { color: var(--color-green-800); }
|
||||||
|
|
||||||
|
// Dark mode text colors
|
||||||
|
.dark .text-gray-100 { color: var(--color-gray-100); }
|
||||||
|
.dark .text-gray-300 { color: var(--color-gray-300); }
|
||||||
|
.dark .text-gray-400 { color: var(--color-gray-400); }
|
||||||
|
.dark .text-gray-500 { color: var(--color-gray-500); }
|
||||||
|
.dark .text-blue-400 { color: var(--color-blue-400); }
|
||||||
|
.dark .text-green-400 { color: var(--color-green-400); }
|
||||||
|
|
||||||
|
// Layout utilities
|
||||||
|
.min-h-screen { min-height: 100vh; }
|
||||||
|
.max-w-4xl { max-width: 56rem; }
|
||||||
|
.max-w-6xl { max-width: 72rem; }
|
||||||
|
.max-w-7xl { max-width: 80rem; }
|
||||||
|
.max-w-md { max-width: 28rem; }
|
||||||
|
.max-w-2xl { max-width: 42rem; }
|
||||||
|
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||||
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||||
|
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||||
|
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
||||||
|
.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
|
||||||
|
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||||
|
.py-12 { padding-top: 3rem; padding-bottom: 3rem; }
|
||||||
|
.py-16 { padding-top: 4rem; padding-bottom: 4rem; }
|
||||||
|
.p-4 { padding: 1rem; }
|
||||||
|
.p-6 { padding: 1.5rem; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.mb-3 { margin-bottom: 0.75rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.mb-6 { margin-bottom: 1.5rem; }
|
||||||
|
.mb-8 { margin-bottom: 2rem; }
|
||||||
|
.mt-4 { margin-top: 1rem; }
|
||||||
|
|
||||||
|
// Flexbox
|
||||||
|
.flex { display: flex; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.items-start { align-items: flex-start; }
|
||||||
|
.items-end { align-items: flex-end; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.justify-center { justify-content: center; }
|
||||||
|
.space-x-8 > * + * { margin-left: 2rem; }
|
||||||
|
.space-x-4 > * + * { margin-left: 1rem; }
|
||||||
|
.space-x-2 > * + * { margin-left: 0.5rem; }
|
||||||
|
.space-y-2 > * + * { margin-top: 0.5rem; }
|
||||||
|
.space-y-4 > * + * { margin-top: 1rem; }
|
||||||
|
.space-y-6 > * + * { margin-top: 1.5rem; }
|
||||||
|
.space-y-8 > * + * { margin-top: 2rem; }
|
||||||
|
.gap-2 { gap: 0.5rem; }
|
||||||
|
.gap-4 { gap: 1rem; }
|
||||||
|
.gap-6 { gap: 1.5rem; }
|
||||||
|
.flex-wrap { flex-wrap: wrap; }
|
||||||
|
|
||||||
|
// Grid
|
||||||
|
.grid { display: grid; }
|
||||||
|
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||||
|
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Typography
|
||||||
|
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||||
|
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||||||
|
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||||
|
.text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
|
||||||
|
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||||
|
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||||
|
.font-bold { font-weight: 700; }
|
||||||
|
.font-semibold { font-weight: 600; }
|
||||||
|
.font-medium { font-weight: 500; }
|
||||||
|
.leading-relaxed { line-height: 1.625; }
|
||||||
|
|
||||||
|
// Borders and shadows
|
||||||
|
.border { border-width: 1px; }
|
||||||
|
.border-b { border-bottom-width: 1px; }
|
||||||
|
.border-gray-200 { border-color: var(--color-gray-200); }
|
||||||
|
.border-gray-300 { border-color: var(--color-gray-300); }
|
||||||
|
.border-gray-600 { border-color: var(--color-gray-600); }
|
||||||
|
.border-gray-700 { border-color: var(--color-gray-700); }
|
||||||
|
.dark .border-gray-700 { border-color: var(--color-gray-700); }
|
||||||
|
.dark .border-gray-600 { border-color: var(--color-gray-600); }
|
||||||
|
.rounded-lg { border-radius: 0.5rem; }
|
||||||
|
.rounded-md { border-radius: 0.375rem; }
|
||||||
|
.rounded { border-radius: 0.25rem; }
|
||||||
|
.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); }
|
||||||
|
.hover\:shadow-md:hover { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); }
|
||||||
|
|
||||||
|
// Positioning
|
||||||
|
.relative { position: relative; }
|
||||||
|
.absolute { position: absolute; }
|
||||||
|
.fixed { position: fixed; }
|
||||||
|
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
|
||||||
|
.top-1\/2 { top: 50%; }
|
||||||
|
.left-3 { left: 0.75rem; }
|
||||||
|
.right-3 { right: 0.75rem; }
|
||||||
|
.transform { transform: translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
|
||||||
|
.-translate-y-1\/2 { --tw-translate-y: -50%; }
|
||||||
|
.z-50 { z-index: 50; }
|
||||||
|
|
||||||
|
// Display utilities
|
||||||
|
.hidden { display: none; }
|
||||||
|
.block { display: block; }
|
||||||
|
.inline-block { display: inline-block; }
|
||||||
|
|
||||||
|
// Focus styles
|
||||||
|
.focus\:ring-2:focus { box-shadow: 0 0 0 2px var(--color-blue-500); }
|
||||||
|
.focus\:ring-blue-500:focus { --tw-ring-color: var(--color-blue-500); }
|
||||||
|
.focus\:border-blue-500:focus { border-color: var(--color-blue-500); }
|
||||||
|
|
||||||
|
// Pointer events
|
||||||
|
.pointer-events-none { pointer-events: none; }
|
||||||
|
|
||||||
|
// Sizing
|
||||||
|
.w-4 { width: 1rem; }
|
||||||
|
.w-5 { width: 1.25rem; }
|
||||||
|
.w-16 { width: 4rem; }
|
||||||
|
.w-full { width: 100%; }
|
||||||
|
.h-4 { height: 1rem; }
|
||||||
|
.h-5 { height: 1.25rem; }
|
||||||
|
.h-16 { height: 4rem; }
|
||||||
|
.min-w-48 { min-width: 12rem; }
|
||||||
|
.min-w-64 { min-width: 16rem; }
|
||||||
|
.max-h-\[90vh\] { max-height: 90vh; }
|
||||||
|
|
||||||
|
// Overflow
|
||||||
|
.overflow-x-auto { overflow-x: auto; }
|
||||||
|
.overflow-y-auto { overflow-y: auto; }
|
||||||
|
|
||||||
|
// Text alignment
|
||||||
|
.text-left { text-align: left; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
|
||||||
|
// Cursor
|
||||||
|
.cursor-pointer { cursor: pointer; }
|
||||||
|
|
||||||
|
// Component Styles
|
||||||
|
.nav-link {
|
||||||
|
@apply flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
||||||
|
@apply text-gray-600 hover:text-gray-900;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply text-gray-300 hover:text-gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
@apply text-blue-600 bg-blue-50;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply text-blue-400 bg-blue-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn {
|
||||||
|
@apply flex items-center gap-2 px-4 py-2 rounded-lg transition-colors;
|
||||||
|
@apply bg-gray-200 text-gray-700 hover:bg-gray-300;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-gray-700 text-gray-300 hover:bg-gray-600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-active {
|
||||||
|
@apply bg-blue-600 text-white;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-blue-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-btn {
|
||||||
|
@apply text-gray-600 hover:text-gray-900;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply text-gray-400 hover:text-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply bg-white text-gray-900 shadow-sm;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-gray-600 text-gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
@apply bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-gray-800 border-gray-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
@apply text-xs px-2 py-1 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-blue {
|
||||||
|
@apply bg-blue-100 text-blue-800;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-blue-900 text-blue-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-green {
|
||||||
|
@apply bg-green-100 text-green-800;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-green-900 text-green-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-purple {
|
||||||
|
@apply bg-purple-100 text-purple-800;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-purple-900 text-purple-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-gray {
|
||||||
|
@apply bg-gray-100 text-gray-800;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-gray-700 text-gray-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
@apply w-3 h-3 rounded-full;
|
||||||
|
|
||||||
|
&.operational { @apply bg-green-500; }
|
||||||
|
&.degraded { @apply bg-yellow-500; }
|
||||||
|
&.maintenance { @apply bg-blue-500; }
|
||||||
|
&.down { @apply bg-red-500; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
@apply px-3 py-1 rounded-full text-sm font-medium;
|
||||||
|
|
||||||
|
&.operational {
|
||||||
|
@apply text-green-600 bg-green-100;
|
||||||
|
.dark & { @apply text-green-400 bg-green-900; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.degraded {
|
||||||
|
@apply text-yellow-600 bg-yellow-100;
|
||||||
|
.dark & { @apply text-yellow-400 bg-yellow-900; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.maintenance {
|
||||||
|
@apply text-blue-600 bg-blue-100;
|
||||||
|
.dark & { @apply text-blue-400 bg-blue-900; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.down {
|
||||||
|
@apply text-red-600 bg-red-100;
|
||||||
|
.dark & { @apply text-red-400 bg-red-900; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form styles
|
||||||
|
input, select, textarea {
|
||||||
|
@apply bg-white text-gray-900;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-gray-700 text-gray-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table styles
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-cell-tool {
|
||||||
|
@apply bg-blue-50 border border-blue-200 rounded px-2 py-1 text-sm cursor-pointer hover:bg-blue-100 transition-colors text-blue-900;
|
||||||
|
|
||||||
|
.dark & {
|
||||||
|
@apply bg-blue-900 border-blue-700 hover:bg-blue-800 text-blue-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prose styles for content
|
||||||
|
.prose {
|
||||||
|
max-width: none;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// View modes
|
||||||
|
.view-mode {
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.view-mode-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation for search results
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-card {
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive utilities
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.space-x-8 > * + * { margin-left: 1rem; }
|
||||||
|
.px-4 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||||
|
|
||||||
|
.grid-cols-1 {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-x-auto {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
layout: base.njk
|
||||||
|
title: "Service Status"
|
||||||
|
description: "Real-time status of deployed DFIR services and infrastructure"
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="max-w-6xl mx-auto px-4 py-8">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-4">Service Status</h1>
|
||||||
|
<p class="text-gray-600 dark:text-gray-300">
|
||||||
|
Real-time status of our deployed DFIR SaaS services and infrastructure components.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Overall Status -->
|
||||||
|
<div id="overall-status" class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Overall Status</h2>
|
||||||
|
<div id="overall-indicator" class="flex items-center gap-2">
|
||||||
|
<div class="status-indicator operational"></div>
|
||||||
|
<span id="overall-text" class="text-green-600 dark:text-green-400 font-medium">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="overall-summary" class="text-gray-600 dark:text-gray-300">
|
||||||
|
Checking service status...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime Kuma Connection Status -->
|
||||||
|
<div id="uptime-kuma-status" class="bg-gray-100 dark:bg-gray-700 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 p-6">
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<svg class="w-6 h-6 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="text-lg font-semibold text-gray-600 dark:text-gray-400">Uptime Kuma Integration</h3>
|
||||||
|
<span id="kuma-connection-status" class="tag tag-gray">Checking...</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 mb-4">
|
||||||
|
Connect your Uptime Kuma instance for real-time monitoring data.
|
||||||
|
</p>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<p><strong>Configuration:</strong> Edit <code>src/_data/services.yaml</code> to enable integration</p>
|
||||||
|
<p><strong>API Endpoint:</strong> <span id="kuma-endpoint">Not configured</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Service Categories -->
|
||||||
|
<div id="service-categories" class="space-y-6">
|
||||||
|
<!-- Categories will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Service List -->
|
||||||
|
<div id="service-list" class="space-y-4">
|
||||||
|
<!-- Services will be populated by JavaScript -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Last Updated -->
|
||||||
|
<div class="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
<p>Last updated: <span id="last-updated">Never</span></p>
|
||||||
|
<button id="refresh-status" class="mt-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
Refresh Status
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pass service data to JavaScript -->
|
||||||
|
<script>
|
||||||
|
window.servicesData = {{ services | dump | safe }};
|
||||||
|
window.uptimeKumaConfig = {{ services.uptimeKuma | dump | safe }};
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user