Files
turf_saas/map_visual.html
2026-04-25 17:18:43 +02:00

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>