Files
html/office-app.html

952 lines
36 KiB
HTML

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:;">
<title>OFFICE APP — Hub unifié Microsoft 365 · WEVAL</title>
<meta name="description" content="OFFICE APP — Hub unifié Microsoft Graph API : tenants, backdoors, scripts, intents chat. Zéro manuel, automation via client_credentials.">
<link rel="icon" href="data:,">
<style>
:root {
--bg: #0a0e1a;
--bg-2: #111829;
--bg-3: #1a2336;
--border: rgba(99, 102, 241, 0.22);
--border-hot: rgba(239, 68, 68, 0.45);
--ink: #e2e8f0;
--ink-dim: #94a3b8;
--ink-faint: #64748b;
--accent: #a5b4fc;
--accent-2: #5eead4;
--accent-ok: #34d399;
--accent-warn: #fbbf24;
--accent-fail: #f87171;
--font-display: 'JetBrains Mono', 'SF Mono', ui-monospace, monospace;
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
background-image:
radial-gradient(ellipse at top, rgba(99,102,241,0.08) 0%, transparent 50%),
radial-gradient(ellipse at bottom right, rgba(94,234,212,0.05) 0%, transparent 50%);
background-attachment: fixed;
color: var(--ink);
font-family: var(--font-body);
line-height: 1.5;
min-height: 100vh;
}
.wrap { max-width: 1400px; margin: 0 auto; padding: 24px; }
/* Header */
header.ofh {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 18px;
border-bottom: 1px solid var(--border);
margin-bottom: 28px;
}
header.ofh h1 {
font-family: var(--font-display);
font-size: 22px;
font-weight: 700;
letter-spacing: -0.02em;
margin: 0;
color: var(--ink);
}
header.ofh h1 .tag {
display: inline-block;
padding: 2px 8px;
margin-left: 10px;
font-size: 11px;
background: linear-gradient(90deg, #6366f1, #5eead4);
background-clip: text;
-webkit-background-clip: text;
color: transparent;
border: 1px solid var(--accent);
border-radius: 4px;
}
header.ofh .sub { color: var(--ink-dim); font-size: 13px; margin-top: 4px; }
header.ofh nav { display: flex; gap: 14px; font-size: 13px; }
header.ofh nav a {
color: var(--ink-dim);
text-decoration: none;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 6px;
transition: all .15s;
}
header.ofh nav a:hover { color: var(--accent); border-color: var(--accent); }
/* Stats grid */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 14px;
margin-bottom: 28px;
}
.stat-card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0;
width: 3px; height: 100%;
background: var(--accent);
}
.stat-card.ok::before { background: var(--accent-ok); }
.stat-card.warn::before { background: var(--accent-warn); }
.stat-card.fail::before { background: var(--accent-fail); }
.stat-card .lbl {
font-size: 11px;
color: var(--ink-dim);
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 8px;
font-family: var(--font-display);
}
.stat-card .val {
font-size: 28px;
font-weight: 700;
font-family: var(--font-display);
color: var(--ink);
line-height: 1;
}
.stat-card .sub {
font-size: 11px;
color: var(--ink-faint);
margin-top: 6px;
}
/* Tabs */
.tabs {
display: flex;
gap: 2px;
border-bottom: 1px solid var(--border);
margin-bottom: 20px;
overflow-x: auto;
}
.tab {
padding: 10px 18px;
font-size: 13px;
font-family: var(--font-display);
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--ink-dim);
cursor: pointer;
transition: all .15s;
white-space: nowrap;
}
.tab:hover { color: var(--ink); }
.tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.panel { display: none; }
.panel.active { display: block; }
/* Tables */
.tbl-wrap {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
.tbl-head {
padding: 14px 18px;
background: var(--bg-3);
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.tbl-head h3 {
margin: 0;
font-family: var(--font-display);
font-size: 13px;
color: var(--accent);
letter-spacing: 0.02em;
}
.tbl-head .info { font-size: 11px; color: var(--ink-faint); }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
table th {
text-align: left;
padding: 10px 14px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ink-faint);
background: var(--bg-3);
border-bottom: 1px solid var(--border);
font-weight: 600;
}
table td {
padding: 10px 14px;
border-bottom: 1px solid rgba(99,102,241,0.08);
color: var(--ink);
font-family: var(--font-display);
font-size: 12px;
}
table tbody tr:hover { background: rgba(99,102,241,0.05); }
table .td-center { text-align: center; }
.pill {
display: inline-block;
padding: 2px 8px;
font-size: 10px;
font-weight: 600;
border-radius: 10px;
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.pill.ok { background: rgba(52,211,153,0.15); color: var(--accent-ok); border: 1px solid rgba(52,211,153,0.3); }
.pill.no { background: rgba(148,163,184,0.1); color: var(--ink-faint); border: 1px solid rgba(148,163,184,0.2); }
.pill.warn { background: rgba(251,191,36,0.15); color: var(--accent-warn); border: 1px solid rgba(251,191,36,0.3); }
/* Buttons */
.btn {
display: inline-block;
padding: 6px 12px;
font-size: 11px;
font-family: var(--font-display);
font-weight: 600;
background: transparent;
border: 1px solid var(--border);
color: var(--ink);
border-radius: 5px;
cursor: pointer;
text-decoration: none;
transition: all .15s;
letter-spacing: 0.03em;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn.primary {
background: linear-gradient(135deg, rgba(99,102,241,0.15), rgba(94,234,212,0.12));
border-color: var(--accent);
color: var(--accent);
}
.btn.primary:hover {
background: linear-gradient(135deg, rgba(99,102,241,0.25), rgba(94,234,212,0.2));
box-shadow: 0 0 0 2px rgba(99,102,241,0.1);
}
.btn.danger { border-color: var(--border-hot); color: var(--accent-fail); }
.btn.danger:hover { background: rgba(239,68,68,0.1); }
.btn.sm { padding: 3px 8px; font-size: 10px; }
/* Console */
.console {
background: #030711;
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
font-family: var(--font-display);
font-size: 12px;
color: #9fef00;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
line-height: 1.6;
}
.console .ts { color: var(--ink-faint); }
.console .ok { color: #34d399; }
.console .fail { color: #f87171; }
.console .info { color: #60a5fa; }
/* Card lists */
.card-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
}
.card h4 {
margin: 0 0 6px 0;
font-family: var(--font-display);
font-size: 13px;
color: var(--accent);
}
.card .desc { color: var(--ink-dim); font-size: 12px; margin-bottom: 10px; }
.card .meta { display: flex; gap: 10px; font-size: 10px; color: var(--ink-faint); font-family: var(--font-display); }
.loading { color: var(--ink-faint); padding: 40px; text-align: center; }
.loading::after {
content: '...';
display: inline-block;
animation: dots 1.5s infinite steps(3);
}
@keyframes dots {
0%, 20% { content: ''; }
40% { content: '.'; }
60% { content: '..'; }
80%, 100% { content: '...'; }
}
/* Doctrine banner */
.doc-banner {
background: linear-gradient(90deg, rgba(99,102,241,0.08), rgba(94,234,212,0.05));
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 8px;
padding: 14px 18px;
margin-bottom: 24px;
font-size: 13px;
color: var(--ink-dim);
line-height: 1.6;
}
.doc-banner strong { color: var(--accent); font-family: var(--font-display); }
/* Action bar */
.action-bar {
display: flex;
gap: 10px;
margin-bottom: 16px;
flex-wrap: wrap;
}
/* Responsive */
@media (max-width: 768px) {
.wrap { padding: 14px; }
header.ofh { flex-direction: column; align-items: flex-start; gap: 12px; }
.stats { grid-template-columns: repeat(2, 1fr); }
.stat-card .val { font-size: 22px; }
table { font-size: 11px; }
table th, table td { padding: 8px 10px; }
}
</style>
<!-- DOCTRINE-60-UX-ENRICH direct-inject-20260424-143847 -->
<style id="doctrine60-ux-direct">
/* DOCTRINE-60-UX-ENRICH injected-direct */
body::before {
content: '';
position: fixed;
top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(circle at 50% 50%, rgba(100,180,255,0.08), transparent 60%);
pointer-events: none;
z-index: -1;
}
.card, .kpi, .panel, .btn {
transition: all 0.3s cubic-bezier(0.2,0,0.1,1);
}
.card:hover, .kpi:hover, .panel:hover {
box-shadow: 0 4px 20px rgba(100,180,255,0.2);
border-color: rgba(100,180,255,0.5);
}
@keyframes pulseD60 {
0%,100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.7; transform: scale(1.05); }
}
.pulse, .live-indicator, .active, .online {
animation: pulseD60 3s ease-in-out infinite;
}
.modal, .chat, .speech, .overlay {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.enter-stagger {
animation: enterStagD60 0.5s cubic-bezier(0.2,0,0.1,1) forwards;
}
@keyframes enterStagD60 {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div class="wrap">
<header class="ofh">
<div>
<h1>OFFICE APP <span class="tag">V33 · UNIFIED HUB</span></h1>
<div class="sub">Microsoft Graph API · 967 tenants · Automation via <code>client_credentials</code></div>
</div>
<nav>
<a href="/weval-technology-platform.html">← WTP</a>
<a href="/office-admins.html">Admins</a>
<a href="/office-senders-diag.html">Senders Diag</a>
<a href="/email-hub.html">Email Hub</a>
</nav>
</header>
<div class="doc-banner">
<strong>DOCTRINE OFFICE APP :</strong> Microsoft Graph API donne le contrôle enterprise complet (users, roles, mail, OneDrive, Teams, SharePoint, Intune, DLP, Conditional Access).
<strong>JAMAIS dire "can't with Office"</strong> — si Graph API le supporte, nous le faisons. Pattern : <code>client_credentials → Bearer → Graph v1.0 POST/PATCH</code>.
<a href="/wiki/DOCTRINE-OFFICE-APP-FULL-ENTERPRISE.md" style="color:var(--accent-2);text-decoration:underline;margin-left:8px">Doctrine complète →</a>
</div>
<!-- STATS -->
<div class="stats" id="stats">
<div class="stat-card"><div class="lbl">Accounts</div><div class="val"></div><div class="sub">loading</div></div>
<div class="stat-card"><div class="lbl">Tenants</div><div class="val"></div><div class="sub">loading</div></div>
<div class="stat-card ok"><div class="lbl">Automatables</div><div class="val"></div><div class="sub">Graph creds OK</div></div>
<div class="stat-card warn"><div class="lbl">Backdoors</div><div class="val"></div><div class="sub">recovery admins</div></div>
<div class="stat-card"><div class="lbl">Graph senders</div><div class="val"></div><div class="sub">verified</div></div>
<div class="stat-card"><div class="lbl">Sent 24h</div><div class="val"></div><div class="sub">Graph sends</div></div>
</div>
<!-- TABS -->
<div class="tabs">
<button class="tab active" data-tab="tenants">TENANTS</button>
<button class="tab" data-tab="actions">ACTIONS</button>
<button class="tab" data-tab="scripts">SCRIPTS & APIs</button>
<button class="tab" data-tab="intents">INTENTS CHAT</button>
<button class="tab" data-tab="workflow">WORKFLOW</button>
<button class="tab" data-tab="console">CONSOLE</button>
</div>
<!-- TENANTS PANEL -->
<div class="panel active" id="panel-tenants">
<div class="action-bar">
<button class="btn primary" onclick="bulkTestAuth()">▶ Test Graph auth (25 automatables)</button>
<button class="btn" onclick="loadTenants()">↻ Refresh</button>
<input id="filter-tenants" placeholder="filter tenant..." style="flex:1;min-width:200px;padding:6px 12px;background:var(--bg-2);border:1px solid var(--border);border-radius:5px;color:var(--ink);font-family:var(--font-display);font-size:12px" oninput="filterTenants(this.value)">
</div>
<div class="tbl-wrap">
<div class="tbl-head">
<h3>TENANTS — office_accounts</h3>
<span class="info" id="tenants-info"></span>
</div>
<div style="max-height:500px;overflow-y:auto">
<table id="tenants-table">
<thead>
<tr>
<th>TENANT DOMAIN</th>
<th>APP_ID</th>
<th class="td-center">SECRET</th>
<th class="td-center">USERS</th>
<th class="td-center">STATUS</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody id="tenants-body">
<tr><td colspan="6" class="loading">Loading tenants</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ACTIONS PANEL -->
<div class="panel" id="panel-actions">
<div class="card-list">
<div class="card">
<h4>🔐 Test Graph auth (bulk)</h4>
<div class="desc">Teste l'authentification OAuth2 client_credentials sur les 25 tenants automatables. Durée ~30s.</div>
<button class="btn primary" onclick="bulkTestAuth()">▶ Launch</button>
</div>
<div class="card">
<h4>👥 List users (single tenant)</h4>
<div class="desc">Liste 50 users d'un tenant via Graph API <code>/users</code>.</div>
<input id="listuser-tenant" placeholder="tenant.onmicrosoft.com" style="width:100%;padding:6px;background:var(--bg-3);border:1px solid var(--border);border-radius:5px;color:var(--ink);font-family:var(--font-display);font-size:11px;margin-bottom:8px">
<button class="btn" onclick="listUsers()">▶ List</button>
</div>
<div class="card">
<h4>🔑 Check permissions</h4>
<div class="desc">Vérifie User.Read.All + RoleManagement.Read.Directory sur un tenant.</div>
<input id="checkperm-tenant" placeholder="tenant.onmicrosoft.com" style="width:100%;padding:6px;background:var(--bg-3);border:1px solid var(--border);border-radius:5px;color:var(--ink);font-family:var(--font-display);font-size:11px;margin-bottom:8px">
<button class="btn" onclick="checkPerms()">▶ Check</button>
</div>
<div class="card">
<h4>🛡️ Backdoor plan</h4>
<div class="desc">Plan V96.23 : UPN + role + license pour 34 tenants. API <code>/office-recovery.php?action=plan</code></div>
<a class="btn" href="/api/office-recovery.php?action=plan" target="_blank">▶ View plan JSON</a>
</div>
<div class="card">
<h4>📧 Graph senders status</h4>
<div class="desc">Verified senders + recent send log (table <code>admin.graph_send_log</code>).</div>
<a class="btn" href="/office-senders-diag.html">▶ Open diag</a>
</div>
<div class="card" id="blade-remote-card" style="border-color:rgba(52,211,153,0.35)">
<h4 style="color:#5eead4">⚡ Blade Remote · Chrome session</h4>
<div class="desc">Yacine a Chrome ouvert sur Razer Blade avec tous portails loggé-in. Pilote via MCP + CDP. <strong>ZÉRO manuel, ZÉRO relogin.</strong></div>
<div style="display:flex;gap:6px;flex-wrap:wrap">
<a class="btn" href="/api/blade-status.php" target="_blank">▶ Status</a>
<a class="btn" href="/blade-hub.html" target="_blank">▶ Hub</a>
<a class="btn" href="/wiki/DOCTRINE-BLADE-IA-REMOTE.md" target="_blank">▶ Doctrine</a>
</div>
<div class="meta" style="margin-top:8px"><span>17 MCP tools · port 8765</span></div>
</div>
<div class="card">
<h4>♻️ Azure AD re-register</h4>
<div class="desc">Re-register app pour tenants avec secrets expirés. API <code>/azure-reregister-api.php</code></div>
<a class="btn" href="/api/azure-reregister-api.php" target="_blank">▶ View API</a>
</div>
</div>
</div>
<!-- SCRIPTS PANEL -->
<div class="panel" id="panel-scripts">
<div class="tbl-wrap" style="margin-bottom:18px">
<div class="tbl-head"><h3>APIs PHP — /var/www/html/api/office-*.php</h3></div>
<div id="scripts-apis" class="card-list" style="padding:18px"></div>
</div>
<div class="tbl-wrap">
<div class="tbl-head"><h3>DB tables — 20 tables Office/Graph</h3></div>
<div id="scripts-tables" style="padding:18px">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:8px;font-family:var(--font-display);font-size:11px">
<span class="pill no">admin.office_accounts</span>
<span class="pill no">admin.office_backdoors</span>
<span class="pill no">admin.office_connectors</span>
<span class="pill no">admin.office_credentials</span>
<span class="pill no">admin.office_domains</span>
<span class="pill no">admin.office_providers</span>
<span class="pill no">admin.office_scripts</span>
<span class="pill no">admin.office_users</span>
<span class="pill no">admin.office_workflow_steps</span>
<span class="pill no">admin.office365_accounts</span>
<span class="pill no">admin.o365_accounts</span>
<span class="pill no">admin.o365_accounts_local</span>
<span class="pill no">admin.o365_domains</span>
<span class="pill no">admin.o365_tenants</span>
<span class="pill no">admin.graph_accounts</span>
<span class="pill no">admin.graph_inbox_results</span>
<span class="pill no">admin.graph_mail_accounts</span>
<span class="pill no">admin.graph_send_log</span>
<span class="pill no">admin.graph_tenants</span>
<span class="pill no">admin.graph_verified_senders</span>
</div>
</div>
</div>
</div>
<!-- INTENTS CHAT PANEL -->
<div class="panel" id="panel-intents">
<div class="doc-banner">
<strong>CHAT WEVIA :</strong> tape un trigger dans le chat WEVIA Master pour activer l'intent. Les intents ci-dessous sont <code>wired-pending</code> — prêts à activer dans le registry.
</div>
<div class="tbl-wrap">
<div class="tbl-head">
<h3>INTENTS Office/Graph/Azure — wired-pending</h3>
<span class="info" id="intents-info"></span>
</div>
<div id="intents-body" style="padding:18px" class="card-list"></div>
</div>
</div>
<!-- WORKFLOW PANEL V34 -->
<div class="panel" id="panel-workflow">
<div class="action-bar">
<button class="btn primary" onclick="loadWorkflow()">▶ Load workflow</button>
<button class="btn" onclick="loadWorkflow()">↻ Refresh</button>
</div>
<div class="stats" id="wf-stats" style="margin-bottom:18px">
<div class="stat-card"><div class="lbl">Total</div><div class="val" id="wf-total"></div><div class="sub">loading</div></div>
<div class="stat-card ok"><div class="lbl">Active</div><div class="val" id="wf-active"></div><div class="sub">status active</div></div>
<div class="stat-card warn"><div class="lbl">Warming</div><div class="val" id="wf-warming"></div><div class="sub">warmup</div></div>
<div class="stat-card"><div class="lbl">Pending</div><div class="val" id="wf-pending"></div><div class="sub">pending</div></div>
<div class="stat-card fail"><div class="lbl">Blocked</div><div class="val" id="wf-blocked"></div><div class="sub">blocked</div></div>
</div>
<div class="tbl-wrap" style="margin-bottom:18px">
<div class="tbl-head">
<h3>WORKFLOW STEPS — 8 steps Register → Live</h3>
<span class="info" id="wf-info"></span>
</div>
<table id="wf-steps-table">
<thead>
<tr>
<th>STEP #</th>
<th>NAME</th>
<th class="td-center">TOTAL</th>
<th class="td-center">ACTIVE</th>
<th class="td-center">WARMING</th>
<th class="td-center">PENDING</th>
<th class="td-center">BLOCKED</th>
<th class="td-center">%</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody id="wf-steps-body">
<tr><td colspan="9" class="loading">Click Load workflow</td></tr>
</tbody>
</table>
</div>
<div class="tbl-wrap">
<div class="tbl-head">
<h3>ACCOUNTS (drill-down par step)</h3>
<span class="info" id="wf-accounts-info">Click un step pour voir accounts</span>
</div>
<table id="wf-accounts-table" style="display:none">
<thead>
<tr>
<th>#ID</th>
<th>TENANT DOMAIN</th>
<th>ADMIN EMAIL</th>
<th>STEP</th>
<th>STATUS</th>
<th class="td-center">LICENSE</th>
<th class="td-center">MFA</th>
<th>ACTIONS</th>
</tr>
</thead>
<tbody id="wf-accounts-body"></tbody>
</table>
</div>
</div>
<!-- /WORKFLOW PANEL V34 -->
<!-- CONSOLE PANEL -->
<div class="panel" id="panel-console">
<div class="tbl-head" style="background:var(--bg-2);border:1px solid var(--border);border-radius:10px 10px 0 0;margin-bottom:0">
<h3>LIVE CONSOLE</h3>
<button class="btn sm" onclick="clearConsole()">clear</button>
</div>
<div class="console" id="console"><span class="ts">[ready]</span> OFFICE APP hub initialized. Graph API ready.\n</div>
</div>
</div>
<script>
'use strict';
// ==========================================================================
// OFFICE APP — V33 Hub unifié
// API base: /api/office-app.php
// ==========================================================================
const API = '/api/office-app.php';
let _tenants = []; // cache
function log(msg, cls='') {
const c = document.getElementById('console');
const ts = new Date().toISOString().split('T')[1].slice(0,8);
const line = document.createElement('div');
line.innerHTML = `<span class="ts">[${ts}]</span> <span class="${cls}">${msg}</span>`;
c.appendChild(line);
c.scrollTop = c.scrollHeight;
}
function clearConsole() {
document.getElementById('console').innerHTML = '<span class="ts">[cleared]</span>\n';
}
async function loadOverview() {
try {
const r = await fetch(API + '?action=overview');
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'overview fail');
const s = d.stats;
const stats = document.getElementById('stats');
stats.innerHTML = `
<div class="stat-card"><div class="lbl">Accounts</div><div class="val">${s.total_accounts}</div><div class="sub">total DB</div></div>
<div class="stat-card"><div class="lbl">Tenants</div><div class="val">${s.total_tenants}</div><div class="sub">unique domains</div></div>
<div class="stat-card ok"><div class="lbl">Automatables</div><div class="val">${s.automatable_tenants}</div><div class="sub">Graph creds OK</div></div>
<div class="stat-card ${s.backdoors_registered > 0 ? 'ok' : 'fail'}"><div class="lbl">Backdoors</div><div class="val">${s.backdoors_registered}</div><div class="sub">coverage ${s.coverage_backdoor_pct}%</div></div>
<div class="stat-card"><div class="lbl">Graph senders</div><div class="val">${s.graph_verified_senders}</div><div class="sub">verified</div></div>
<div class="stat-card"><div class="lbl">Sent 24h</div><div class="val">${s.graph_sent_last_24h}</div><div class="sub">Graph sends</div></div>
`;
log(`overview OK — ${s.total_accounts} accounts, ${s.automatable_tenants} automatables, ${s.backdoors_registered} backdoors`, 'ok');
} catch (e) {
log('overview FAIL: ' + e.message, 'fail');
}
}
async function loadTenants() {
const body = document.getElementById('tenants-body');
body.innerHTML = '<tr><td colspan="6" class="loading">Loading</td></tr>';
try {
const r = await fetch(API + '?action=tenants_list');
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'tenants_list fail');
_tenants = d.tenants;
document.getElementById('tenants-info').textContent = `${d.count} tenants`;
renderTenants(_tenants);
log(`tenants loaded: ${d.count}`, 'ok');
} catch (e) {
body.innerHTML = `<tr><td colspan="6" class="loading">ERR: ${e.message}</td></tr>`;
log('tenants FAIL: ' + e.message, 'fail');
}
}
function renderTenants(rows) {
const body = document.getElementById('tenants-body');
if (!rows.length) { body.innerHTML = '<tr><td colspan="6" class="loading">no tenants</td></tr>'; return; }
const first = rows.slice(0, 200); // render first 200 for perf
body.innerHTML = first.map(t => {
const app = t.app_id ? t.app_id.slice(0, 16) + '…' : '<span style="color:var(--ink-faint)">—</span>';
const secret = t.has_secret == 1 ? '<span class="pill ok">YES</span>' : '<span class="pill no">NO</span>';
const auto = (t.app_id && t.has_secret == 1 && t.tenant_id) ? '<span class="pill ok">AUTO</span>' : '<span class="pill no">manual</span>';
const actions = (t.app_id && t.has_secret == 1 && t.tenant_id)
? `<button class="btn sm" onclick="testAuth('${t.tenant_domain}')">auth</button> <button class="btn sm" onclick="listUsersFor('${t.tenant_domain}')">users</button> <button class="btn sm" onclick="checkPermsFor('${t.tenant_domain}')">perms</button>`
: '<span style="color:var(--ink-faint);font-size:11px">manual-only</span>';
return `<tr data-tenant="${t.tenant_domain}">
<td style="color:var(--accent-2)">${t.tenant_domain}</td>
<td>${app}</td>
<td class="td-center">${secret}</td>
<td class="td-center">${t.users}</td>
<td class="td-center">${auto}</td>
<td>${actions}</td>
</tr>`;
}).join('');
if (rows.length > 200) {
body.innerHTML += `<tr><td colspan="6" class="loading" style="padding:16px;color:var(--ink-faint)">+ ${rows.length - 200} autres (filtrer pour voir)</td></tr>`;
}
}
function filterTenants(q) {
q = q.toLowerCase();
if (!q) return renderTenants(_tenants);
renderTenants(_tenants.filter(t => (t.tenant_domain || '').toLowerCase().includes(q)));
}
async function testAuth(tenant) {
log(`→ testAuth(${tenant})...`);
try {
const r = await fetch(`${API}?action=test_auth&tenant=${encodeURIComponent(tenant)}`);
const d = await r.json();
if (d.ok) {
log(`${tenant} → HTTP ${d.http}, users_readable=${d.users_readable}, users_sample=${d.users_count_sample}`, 'ok');
} else {
log(`${tenant}${d.error || 'fail'} ${d.error_description || ''}`, 'fail');
}
} catch (e) { log(`${tenant} ERR: ${e.message}`, 'fail'); }
}
async function listUsersFor(tenant) {
log(`→ listUsers(${tenant})...`);
try {
const r = await fetch(`${API}?action=list_users&tenant=${encodeURIComponent(tenant)}`);
const d = await r.json();
if (d.ok) {
log(`${tenant}${d.count} users`, 'ok');
(d.users || []).slice(0, 5).forEach(u => log(` · ${u.userPrincipalName} [${u.displayName}] enabled=${u.accountEnabled}`, 'info'));
if ((d.users || []).length > 5) log(` + ${(d.users || []).length - 5} autres users...`, 'info');
} else {
log(`${tenant}${d.error}`, 'fail');
}
} catch (e) { log(`${tenant} ERR: ${e.message}`, 'fail'); }
}
async function listUsers() {
const t = document.getElementById('listuser-tenant').value.trim();
if (t) { switchTab('console'); listUsersFor(t); }
}
async function checkPermsFor(tenant) {
log(`→ checkPerms(${tenant})...`);
try {
const r = await fetch(`${API}?action=check_perms&tenant=${encodeURIComponent(tenant)}`);
const d = await r.json();
if (d.ok) {
log(`${tenant} perms:`, 'ok');
Object.entries(d.perms).forEach(([k, v]) => log(` ${k}: ${v}`, v === true || (typeof v === 'number' && v > 0) ? 'ok' : 'fail'));
} else {
log(`${tenant}${d.error}`, 'fail');
}
} catch (e) { log(`${tenant} ERR: ${e.message}`, 'fail'); }
}
async function checkPerms() {
const t = document.getElementById('checkperm-tenant').value.trim();
if (t) { switchTab('console'); checkPermsFor(t); }
}
async function bulkTestAuth() {
switchTab('console');
log('▶ BULK TEST AUTH — 25 automatable tenants', 'info');
try {
const r = await fetch(`${API}?action=test_all_auth`);
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'fail');
log(`tested=${d.tested} auth_ok=${d.graph_auth_ok} auth_fail=${d.graph_auth_fail}`, 'ok');
d.results.forEach(res => {
const cls = res.auth_ok ? 'ok' : 'fail';
const icon = res.auth_ok ? '✓' : '✗';
log(` ${icon} ${res.tenant} → HTTP ${res.http}${res.error ? ' · ' + res.error : ''}`, cls);
});
log(`${d.graph_auth_ok}/${d.tested} tenants prêts pour backdoor automation`, 'info');
await loadOverview();
} catch (e) { log('bulk FAIL: ' + e.message, 'fail'); }
}
async function loadIntents() {
const body = document.getElementById('intents-body');
body.innerHTML = '<div class="loading">Loading</div>';
try {
const r = await fetch(API + '?action=intents_list');
const d = await r.json();
if (!d.ok) throw new Error(d.error);
document.getElementById('intents-info').textContent = `${d.count} intents`;
body.innerHTML = d.intents.map(i => {
const trigs = (i.triggers_sample || []).map(t => `<code style="background:var(--bg-3);padding:1px 5px;border-radius:3px;font-size:10px">${t.slice(0, 40)}${t.length > 40 ? '…' : ''}</code>`).join(' ');
return `<div class="card">
<h4>${i.name}</h4>
<div class="desc">Triggers: ${trigs || '<i style="color:var(--ink-faint)">—</i>'}</div>
<div class="meta">
<span>${i.file}</span>
<span>${(i.size/1024).toFixed(1)}KB</span>
</div>
</div>`;
}).join('');
} catch (e) {
body.innerHTML = `<div class="loading">ERR: ${e.message}</div>`;
}
}
async function loadApis() {
const body = document.getElementById('scripts-apis');
try {
const r = await fetch(API + '?action=overview');
const d = await r.json();
const apis = d.capabilities.apis_php || [];
body.innerHTML = apis.map(a => {
const name = a.replace('.php','');
return `<div class="card">
<h4>${name}</h4>
<div class="desc" style="font-size:11px">API PHP · /api/${a}</div>
<a class="btn sm" href="/api/${a}" target="_blank">open</a>
</div>`;
}).join('');
} catch (e) {
body.innerHTML = `<div class="loading">ERR: ${e.message}</div>`;
}
}
function switchTab(name) {
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
document.querySelectorAll('.panel').forEach(p => p.classList.toggle('active', p.id === 'panel-' + name));
if (name === 'intents' && !document.getElementById('intents-body').children.length) loadIntents();
if (name === 'scripts' && !document.getElementById('scripts-apis').children.length) loadApis();
if (name === 'workflow' && !document.getElementById('wf-steps-body').children.length) loadWorkflow();
if (name === 'workflow' && document.getElementById('wf-steps-body').children.length === 1) loadWorkflow();
}
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => switchTab(t.dataset.tab)));
// V34 — WORKFLOW ACTIONS
async function loadWorkflow() {
try {
const r = await fetch(API + '?action=workflow_overview');
const d = await r.json();
if (!d.ok) throw new Error(d.error || 'fail');
// Aggregate status totals
const bs = {};
(d.by_status || []).forEach(s => bs[s.status] = parseInt(s.n));
document.getElementById('wf-total').textContent = d.total_accounts;
document.getElementById('wf-active').textContent = bs.active || 0;
document.getElementById('wf-warming').textContent = bs.warming || 0;
document.getElementById('wf-pending').textContent = bs.pending || 0;
document.getElementById('wf-blocked').textContent = bs.blocked || 0;
const body = document.getElementById('wf-steps-body');
document.getElementById('wf-info').textContent = (d.by_step||[]).length + ' steps';
body.innerHTML = (d.by_step || []).map(s => {
const pct = d.total_accounts ? ((s.total / d.total_accounts) * 100).toFixed(1) : 0;
return `<tr>
<td style="color:var(--accent-2)"><strong>${s.current_step}</strong></td>
<td>${s.step_name}</td>
<td class="td-center">${s.total}</td>
<td class="td-center"><span class="pill ok">${s.active}</span></td>
<td class="td-center">${s.warming || 0}</td>
<td class="td-center">${s.pending || 0}</td>
<td class="td-center">${s.blocked > 0 ? '<span class="pill warn">' + s.blocked + '</span>' : 0}</td>
<td class="td-center">${pct}%</td>
<td><button class="btn sm" onclick="loadWfAccounts(${s.current_step})">drill</button></td>
</tr>`;
}).join('');
log('workflow loaded: ' + d.total_accounts + ' accounts', 'ok');
} catch (e) {
log('workflow FAIL: ' + e.message, 'fail');
}
}
async function loadWfAccounts(step) {
log('drill step=' + step + '...');
try {
const r = await fetch(API + '?action=workflow_accounts&step=' + step + '&limit=100');
const d = await r.json();
if (!d.ok) throw new Error(d.error);
document.getElementById('wf-accounts-info').textContent = 'step=' + step + ' · ' + d.count + ' accounts';
document.getElementById('wf-accounts-table').style.display = '';
const body = document.getElementById('wf-accounts-body');
body.innerHTML = (d.accounts || []).map(a => {
const stPill = a.status === 'active' ? 'ok' : (a.status === 'pending' ? 'no' : (a.status === 'blocked' ? 'warn' : 'no'));
return `<tr data-aid="${a.id}">
<td>${a.id}</td>
<td style="color:var(--accent-2)">${a.tenant_domain || ''}</td>
<td>${a.admin_email || '<span style=color:var(--ink-faint)>—</span>'}</td>
<td>${a.current_step} ${a.step_name}</td>
<td><span class="pill ${stPill}">${a.status || 'null'}</span></td>
<td class="td-center">${a.has_license ? '✓' : '—'}</td>
<td class="td-center">${a.has_mfa ? '✓' : '—'}</td>
<td>
<button class="btn sm" onclick="wfAdv(${a.id})" title="advance step">▶</button>
<button class="btn sm" onclick="wfRet(${a.id})" title="retreat step">◀</button>
</td>
</tr>`;
}).join('');
log(' ' + d.count + ' accounts for step ' + step, 'ok');
} catch (e) {
log('drill FAIL: ' + e.message, 'fail');
}
}
async function wfAdv(aid) {
try {
const fd = new FormData();
fd.append('aid', aid);
const r = await fetch(API + '?action=workflow_advance', { method: 'POST', body: fd });
const d = await r.json();
if (d.ok) log('advance #' + aid + ': step ' + d.from_step + ' -> ' + d.to_step + ' (' + d.new_step_name + ')', 'ok');
else log('advance #' + aid + ' FAIL: ' + d.error, 'fail');
} catch (e) { log('advance ERR: ' + e.message, 'fail'); }
}
async function wfRet(aid) {
try {
const fd = new FormData();
fd.append('aid', aid);
const r = await fetch(API + '?action=workflow_retreat', { method: 'POST', body: fd });
const d = await r.json();
if (d.ok) log('retreat #' + aid + ': step ' + d.from_step + ' -> ' + d.to_step + ' (' + d.new_step_name + ')', 'ok');
else log('retreat #' + aid + ' FAIL: ' + d.error, 'fail');
} catch (e) { log('retreat ERR: ' + e.message, 'fail'); }
}
// init
loadOverview();
loadTenants();
</script>
<script src="/api/a11y-auto-enhancer.js" defer></script>
<!-- WTP_UDOCK_V1 (Opus 21-avr t32b4) --><script src="/wtp-unified-dock.js" defer></script>
<script src="/opus-antioverlap-doctrine.js?v=1776776094" defer></script>
<!-- DOCTRINE-60-UX-JS --><script id="doctrine60-ux-js-direct">
// DOCTRINE-60-UX-JS staggered entrance
(function(){
if (!('IntersectionObserver' in window)) return;
const obs = new IntersectionObserver((entries) => {
entries.forEach((e, i) => {
if (e.isIntersecting) {
setTimeout(() => e.target.classList.add('enter-stagger'), i * 80);
obs.unobserve(e.target);
}
});
});
document.querySelectorAll('.card, .kpi, .panel').forEach(el => obs.observe(el));
})();
</script>
</body>
</html>