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
Some checks failed
WEVAL NonReg / nonreg (push) Has been cancelled
This commit is contained in:
721
office-app.html
Normal file
721
office-app.html
Normal 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>
|
||||
Reference in New Issue
Block a user