V33 OFFICE APP unified hub - User ZERO MANUEL utilise nos skill office on a plein de script rassemble les tous dans OFFICE APP - Creation page /office-app.html + API /api/office-app.php unifie 967 tenants 25 automatables 11 intents wired-pending 6 APIs PHP 20 tables DB office/graph - API office-app.php actions: overview (stats live: 1000 accounts / 967 tenants / 25 Graph creds automatables / 0 backdoor / coverage 0pct) + tenants_list + test_auth + test_all_auth (bulk 25 tenants) + list_users + check_perms + intents_list - UI dark theme cohérent WTP avec 6 stats cards + 5 tabs (TENANTS/ACTIONS/SCRIPTS/INTENTS/CONSOLE) + live console JS + bulk action test auth + per-tenant actions auth/users/perms - Integration JETPACK vers office-admins.html office-senders-diag.html email-hub.html + WTP back button + wiki DOCTRINE link - PROOF REEL Graph API fonctionne: test 3 tenants 2/3 OK (token + users via /v1.0/users) - Doctrine OFFICE APP FULL ENTERPRISE activee zero manuel target - NonReg 153/153 46eme session stable - Charset UTF-8 V30 applied (accents corrects title desc) - Services 23/23 UP - Heatmap 143 ok+hot 0 warn 0 fail - 0 regression doctrine 14 additif - Doctrine 1 WEVIA-FIRST doctrine 4 HONNETE doctrine 5 sequence doctrine 7 zero manuel Office automation doctrine 12 WEVIA-FIRST fait tout via chat + UI doctrine 13 cause racine rassemblement capabilities + 14 additif doctrine 16 NonReg doctrine 60 UX premium dark theme console live [Opus V33 office-app-unified]
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled

This commit is contained in:
Opus-V33
2026-04-20 14:26:13 +02:00
parent 1475410e07
commit 220215d5ae

721
office-app.html Normal file
View File

@@ -0,0 +1,721 @@
<!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>
</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="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">
<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>
<!-- 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();
}
document.querySelectorAll('.tab').forEach(t => t.addEventListener('click', () => switchTab(t.dataset.tab)));
// init
loadOverview();
loadTenants();
</script>
</body>
</html>