515 lines
17 KiB
HTML
515 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>H3R7Tech - Architecture Visuelle</title>
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
.home-btn{position:fixed;top:10px;left:10px;z-index:9999;background:linear-gradient(135deg,#00d9ff,#7b2cbf);color:#fff;border:none;border-radius:8px;padding:10px 15px;font-size:14px;cursor:pointer;text-decoration:none;display:flex;align-items:center;gap:8px;box-shadow:0 2px 10px rgba(0,217,255,0.3);transition:all 0.3s;font-family:-apple-system,BlinkMacSystemFont,sans-serif}.home-btn:hover{transform:translateY(-2px);box-shadow:0 4px 15px rgba(0,217,255,0.5)}
|
|
:root {
|
|
--bg-primary: #0d1117;
|
|
--bg-secondary: #161b22;
|
|
--bg-tertiary: #21262d;
|
|
--text-primary: #e6edf3;
|
|
--text-secondary: #8b949e;
|
|
--border: #30363d;
|
|
--accent: #58a6ff;
|
|
--api: #22c55e;
|
|
--db: #3b82f6;
|
|
--scraper: #f59e0b;
|
|
--core: #a855f7;
|
|
--utils: #6b7280;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
}
|
|
.toolbar {
|
|
height: 60px;
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 0 20px;
|
|
gap: 16px;
|
|
}
|
|
.logo {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-weight: 600;
|
|
font-size: 18px;
|
|
color: var(--accent);
|
|
}
|
|
.logo span { color: var(--text-secondary); font-weight: 400; }
|
|
.view-tabs {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-left: 20px;
|
|
}
|
|
.view-tab {
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: all 0.2s;
|
|
}
|
|
.view-tab:hover { border-color: var(--accent); color: var(--text-primary); }
|
|
.view-tab.active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: #fff;
|
|
}
|
|
.filters {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
}
|
|
.filter-btn {
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
transition: all 0.2s;
|
|
}
|
|
.filter-btn:hover { border-color: var(--accent); }
|
|
.filter-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
#graph-container {
|
|
width: 100%;
|
|
height: calc(100vh - 60px);
|
|
position: relative;
|
|
}
|
|
#graph-svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.node {
|
|
cursor: pointer;
|
|
}
|
|
.node circle {
|
|
stroke-width: 2px;
|
|
transition: all 0.3s;
|
|
}
|
|
.node:hover circle {
|
|
stroke-width: 4px;
|
|
filter: brightness(1.2);
|
|
}
|
|
.node text {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 11px;
|
|
fill: var(--text-primary);
|
|
pointer-events: none;
|
|
}
|
|
.link {
|
|
stroke: var(--border);
|
|
stroke-opacity: 0.6;
|
|
stroke-width: 1px;
|
|
}
|
|
.link:hover {
|
|
stroke: var(--accent);
|
|
stroke-opacity: 1;
|
|
}
|
|
.sidebar {
|
|
position: fixed;
|
|
top: 60px;
|
|
right: 0;
|
|
width: 350px;
|
|
height: calc(100vh - 60px);
|
|
background: var(--bg-secondary);
|
|
border-left: 1px solid var(--border);
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
transform: translateX(100%);
|
|
transition: transform 0.3s;
|
|
}
|
|
.sidebar.open {
|
|
transform: translateX(0);
|
|
}
|
|
.sidebar h2 {
|
|
font-size: 18px;
|
|
margin-bottom: 15px;
|
|
color: var(--accent);
|
|
}
|
|
.sidebar .meta {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 15px;
|
|
}
|
|
.sidebar .section {
|
|
margin-bottom: 20px;
|
|
}
|
|
.sidebar .section-title {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 8px;
|
|
text-transform: uppercase;
|
|
}
|
|
.sidebar .tag {
|
|
display: inline-block;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border);
|
|
padding: 4px 8px;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
margin: 2px;
|
|
}
|
|
.sidebar .route {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border-color: var(--api);
|
|
color: var(--api);
|
|
}
|
|
.stats-panel {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 20px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
font-size: 12px;
|
|
}
|
|
.stats-panel .stat-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin: 5px 0;
|
|
}
|
|
.stats-panel .stat-value {
|
|
font-weight: 600;
|
|
}
|
|
.legend {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
right: 20px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
.legend-title {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 10px;
|
|
text-transform: uppercase;
|
|
}
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin: 5px 0;
|
|
font-size: 12px;
|
|
}
|
|
.legend-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
}
|
|
.tooltip {
|
|
position: absolute;
|
|
background: var(--bg-tertiary);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 10px;
|
|
font-size: 12px;
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
max-width: 300px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<a href="https://portal-kolifee.duckdns.org/" class="home-btn" title="Retour au portail"><span style="font-size:18px">🏠</span><span>Accueil</span></a>
|
|
|
|
<div class="toolbar">
|
|
<div class="logo">H3R7<span>Tech</span></div>
|
|
<div class="view-tabs">
|
|
<button class="view-tab active" data-view="graph">Graphe</button>
|
|
<button class="view-tab" data-view="tree">Arborescence</button>
|
|
</div>
|
|
<div class="filters">
|
|
<button class="filter-btn active" data-filter="all">Tout</button>
|
|
<button class="filter-btn" data-filter="api">API</button>
|
|
<button class="filter-btn" data-filter="db">DB</button>
|
|
<button class="filter-btn" data-filter="scraper">Scrapers</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="graph-container">
|
|
<svg id="graph-svg"></svg>
|
|
</div>
|
|
|
|
<div class="sidebar" id="sidebar">
|
|
<h2 id="node-title">Selectionnez un fichier</h2>
|
|
<div class="meta" id="node-meta"></div>
|
|
<div class="section" id="routes-section" style="display:none;">
|
|
<div class="section-title">Routes</div>
|
|
<div id="node-routes"></div>
|
|
</div>
|
|
<div class="section" id="functions-section" style="display:none;">
|
|
<div class="section-title">Fonctions</div>
|
|
<div id="node-functions"></div>
|
|
</div>
|
|
<div class="section" id="imports-section" style="display:none;">
|
|
<div class="section-title">Imports</div>
|
|
<div id="node-imports"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stats-panel" id="stats-panel">
|
|
<div class="stat-row"><span>Fichiers:</span><span class="stat-value" id="stat-files">0</span></div>
|
|
<div class="stat-row"><span>Liens:</span><span class="stat-value" id="stat-links">0</span></div>
|
|
<div class="stat-row"><span>API:</span><span class="stat-value" id="stat-api" style="color:var(--api)">0</span></div>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<div class="legend-title">GROUPES</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--api)"></div>API</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--db)"></div>Database</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--scraper)"></div>Scraper</div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--core)"></div>Core</div>
|
|
</div>
|
|
|
|
<div class="tooltip" id="tooltip"></div>
|
|
|
|
<script>
|
|
let architectureData = null;
|
|
let simulation = null;
|
|
let currentFilter = 'all';
|
|
|
|
const colorMap = {
|
|
'api': '#22c55e',
|
|
'db': '#3b82f6',
|
|
'scraper': '#f59e0b',
|
|
'core': '#a855f7',
|
|
'utils': '#6b7280'
|
|
};
|
|
|
|
async function loadData() {
|
|
try {
|
|
const response = await fetch('/architecture.json');
|
|
architectureData = await response.json();
|
|
initGraph();
|
|
updateStats();
|
|
} catch (error) {
|
|
console.error('Erreur chargement donnees:', error);
|
|
document.getElementById('graph-svg').innerHTML = '<text x="50%" y="50%" text-anchor="middle" fill="#8b949e">Erreur de chargement des donnees</text>';
|
|
}
|
|
}
|
|
|
|
function initGraph() {
|
|
const svg = d3.select('#graph-svg');
|
|
const width = window.innerWidth;
|
|
const height = window.innerHeight - 60;
|
|
|
|
svg.attr('viewBox', [0, 0, width, height]);
|
|
|
|
// Filtrer les donnees
|
|
let nodes = architectureData.nodes;
|
|
let links = architectureData.links;
|
|
|
|
if (currentFilter !== 'all') {
|
|
const filteredIds = nodes.filter(n => n.group === currentFilter).map(n => n.id);
|
|
nodes = nodes.filter(n => n.group === currentFilter ||
|
|
links.some(l => (l.source === n.id && filteredIds.includes(l.target)) ||
|
|
(l.target === n.id && filteredIds.includes(l.source))));
|
|
links = links.filter(l =>
|
|
nodes.map(n => n.id).includes(l.source) &&
|
|
nodes.map(n => n.id).includes(l.target));
|
|
}
|
|
|
|
// Creer les noeuds pour D3
|
|
const nodesData = nodes.map(n => ({
|
|
...n,
|
|
x: width / 2 + (Math.random() - 0.5) * 400,
|
|
y: height / 2 + (Math.random() - 0.5) * 400
|
|
}));
|
|
|
|
const linksData = links.map(l => ({
|
|
source: nodesData.find(n => n.id === l.source),
|
|
target: nodesData.find(n => n.id === l.target)
|
|
})).filter(l => l.source && l.target);
|
|
|
|
// Simulation force
|
|
simulation = d3.forceSimulation(nodesData)
|
|
.force('link', d3.forceLink(linksData).id(d => d.id).distance(150))
|
|
.force('charge', d3.forceManyBody().strength(-300))
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
.force('collision', d3.forceCollide().radius(40));
|
|
|
|
// Dessiner les liens
|
|
const link = svg.append('g')
|
|
.selectAll('line')
|
|
.data(linksData)
|
|
.enter()
|
|
.append('line')
|
|
.attr('class', 'link')
|
|
.attr('stroke-width', d => Math.sqrt(d.source.dep_count + 1));
|
|
|
|
// Dessiner les noeuds
|
|
const node = svg.append('g')
|
|
.selectAll('g')
|
|
.data(nodesData)
|
|
.enter()
|
|
.append('g')
|
|
.attr('class', 'node')
|
|
.call(d3.drag()
|
|
.on('start', dragstarted)
|
|
.on('drag', dragged)
|
|
.on('end', dragended))
|
|
.on('click', (event, d) => showNodeDetails(d))
|
|
.on('mouseover', showTooltip)
|
|
.on('mouseout', hideTooltip);
|
|
|
|
// Cercles
|
|
node.append('circle')
|
|
.attr('r', d => Math.max(10, Math.min(30, 10 + d.dep_count * 2)))
|
|
.attr('fill', d => colorMap[d.group] || colorMap.core)
|
|
.attr('stroke', d => d3.color(colorMap[d.group] || colorMap.core).darker());
|
|
|
|
// Labels
|
|
node.append('text')
|
|
.attr('dy', 4)
|
|
.attr('dx', d => Math.max(10, Math.min(30, 10 + d.dep_count * 2)) + 5)
|
|
.text(d => d.id.replace('.py', ''));
|
|
|
|
// Mise a jour positions
|
|
simulation.on('tick', () => {
|
|
link
|
|
.attr('x1', d => d.source.x)
|
|
.attr('y1', d => d.source.y)
|
|
.attr('x2', d => d.target.x)
|
|
.attr('y2', d => d.target.y);
|
|
|
|
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
});
|
|
|
|
function dragstarted(event) {
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
event.subject.fx = event.subject.x;
|
|
event.subject.fy = event.subject.y;
|
|
}
|
|
|
|
function dragged(event) {
|
|
event.subject.fx = event.x;
|
|
event.subject.fy = event.y;
|
|
}
|
|
|
|
function dragended(event) {
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
event.subject.fx = null;
|
|
event.subject.fy = null;
|
|
}
|
|
}
|
|
|
|
function showNodeDetails(node) {
|
|
document.getElementById('sidebar').classList.add('open');
|
|
document.getElementById('node-title').textContent = node.id;
|
|
document.getElementById('node-meta').innerHTML = `
|
|
Groupe: <strong>${node.group}</strong><br>
|
|
Taille: <strong>${(node.size / 1024).toFixed(1)} KB</strong><br>
|
|
Dependances: <strong>${node.dep_count}</strong>
|
|
`;
|
|
|
|
// Routes
|
|
if (node.routes && node.routes.length > 0) {
|
|
document.getElementById('routes-section').style.display = 'block';
|
|
document.getElementById('node-routes').innerHTML = node.routes
|
|
.map(r => `<span class="tag route">${r}</span>`)
|
|
.join('');
|
|
} else {
|
|
document.getElementById('routes-section').style.display = 'none';
|
|
}
|
|
|
|
// Fonctions
|
|
if (node.functions && node.functions.length > 0) {
|
|
document.getElementById('functions-section').style.display = 'block';
|
|
document.getElementById('node-functions').innerHTML = node.functions
|
|
.map(f => `<span class="tag">${f}</span>`)
|
|
.join('');
|
|
} else {
|
|
document.getElementById('functions-section').style.display = 'none';
|
|
}
|
|
|
|
// Imports
|
|
if (node.imports && node.imports.length > 0) {
|
|
document.getElementById('imports-section').style.display = 'block';
|
|
document.getElementById('node-imports').innerHTML = node.imports
|
|
.map(i => `<span class="tag">${i}</span>`)
|
|
.join('');
|
|
} else {
|
|
document.getElementById('imports-section').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function showTooltip(event, d) {
|
|
const tooltip = document.getElementById('tooltip');
|
|
tooltip.innerHTML = `<strong>${d.id}</strong><br>${d.group} - ${(d.size/1024).toFixed(1)} KB`;
|
|
tooltip.style.left = (event.pageX + 10) + 'px';
|
|
tooltip.style.top = (event.pageY - 30) + 'px';
|
|
tooltip.style.opacity = 1;
|
|
}
|
|
|
|
function hideTooltip() {
|
|
document.getElementById('tooltip').style.opacity = 0;
|
|
}
|
|
|
|
function updateStats() {
|
|
if (architectureData) {
|
|
document.getElementById('stat-files').textContent = architectureData.stats.total_files;
|
|
document.getElementById('stat-links').textContent = architectureData.stats.total_links;
|
|
document.getElementById('stat-api').textContent = architectureData.stats.groups.api;
|
|
}
|
|
}
|
|
|
|
// Filtres
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
currentFilter = btn.dataset.filter;
|
|
d3.select('#graph-svg').selectAll('*').remove();
|
|
if (simulation) simulation.stop();
|
|
initGraph();
|
|
});
|
|
});
|
|
|
|
// Vues
|
|
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
document.querySelectorAll('.view-tab').forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
// TODO: Implementer vue arborescence
|
|
});
|
|
});
|
|
|
|
// Fermer sidebar
|
|
document.getElementById('sidebar').addEventListener('click', (e) => {
|
|
if (e.target.id === 'sidebar') {
|
|
e.target.classList.remove('open');
|
|
}
|
|
});
|
|
|
|
// Resize
|
|
window.addEventListener('resize', () => {
|
|
if (simulation) {
|
|
simulation.force('center', d3.forceCenter(window.innerWidth / 2, (window.innerHeight - 60) / 2));
|
|
simulation.alpha(0.3).restart();
|
|
}
|
|
});
|
|
|
|
// Init
|
|
loadData();
|
|
</script>
|
|
</body>
|
|
</html>
|