feat(dev_tools): add diagnostics hub with permissions audit tool

Evolve the platform-debug page into a diagnostics hub with sidebar
explorer layout. Add permissions audit API that introspects all
registered page routes and reports auth/permission enforcement status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 15:44:49 +01:00
parent efca9734d2
commit 618376aa39
9 changed files with 687 additions and 120 deletions

View File

@@ -1,137 +1,341 @@
{# app/modules/dev_tools/templates/dev_tools/admin/platform-debug.html #}
{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/tables.html' import table_wrapper, table_header, table_empty_state %}
{% block title %}Platform Resolution Debug{% endblock %}
{% block title %}Diagnostics{% endblock %}
{% block alpine_data %}platformDebug(){% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Platform Resolution Trace</h1>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Simulates the middleware pipeline for each URL pattern to trace how platform &amp; store context are resolved.
</p>
</div>
{{ page_header('Diagnostics', back_url='/admin/dashboard', back_label='Back to Dashboard') }}
<!-- Controls -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-xs">
<div class="flex items-center gap-4 flex-wrap">
<button @click="runAllTests()" :disabled="running"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50">
<span x-show="!running">Run All Tests</span>
<span x-show="running">Running...</span>
</button>
<div class="flex gap-2">
<button @click="filterGroup = 'all'" :class="filterGroup === 'all' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">All</button>
<button @click="filterGroup = 'dev'" :class="filterGroup === 'dev' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Dev Path-Based</button>
<button @click="filterGroup = 'prod-domain'" :class="filterGroup === 'prod-domain' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Prod Domain</button>
<button @click="filterGroup = 'prod-subdomain'" :class="filterGroup === 'prod-subdomain' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Prod Subdomain</button>
<button @click="filterGroup = 'prod-custom'" :class="filterGroup === 'prod-custom' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Prod Custom Domain</button>
<div class="flex gap-6">
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- Left sidebar — Diagnostic Tools explorer -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="w-72 flex-shrink-0">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider mb-3 flex items-center gap-1.5">
<span x-html="$icon('cog', 'w-4 h-4')"></span>
Diagnostic Tools
</div>
<template x-for="group in toolGroups" :key="group.category">
<div class="mb-1">
<button @click="toggleCategory(group.category)"
class="flex items-center justify-between w-full text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase px-2 py-1 rounded hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<span x-text="group.category"></span>
<span class="text-[10px] font-mono leading-none" x-text="isCategoryExpanded(group.category) ? '' : '+'"></span>
</button>
<ul x-show="isCategoryExpanded(group.category)" x-collapse class="space-y-0.5 mt-0.5">
<template x-for="tool in group.items" :key="tool.id">
<li @click="selectTool(tool.id)"
class="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm cursor-pointer transition-colors"
:class="activeTool === tool.id
? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-200'">
<span x-html="$icon(tool.icon, 'w-3.5 h-3.5 flex-shrink-0')"></span>
<span class="truncate" x-text="tool.label"></span>
</li>
</template>
</ul>
</div>
</template>
</div>
</div>
</div>
<!-- Custom test -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-xs">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Custom Test</h3>
<div class="flex gap-3 items-end flex-wrap">
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Host</label>
<input x-model="customHost" type="text" placeholder="localhost:8000"
class="px-3 py-2 border rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white w-56">
</div>
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Path</label>
<input x-model="customPath" type="text" placeholder="/platforms/loyalty/store/WIZATECH/login"
class="px-3 py-2 border rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white w-80">
</div>
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Body platform_code</label>
<input x-model="customPlatformCode" type="text" placeholder="(optional)"
class="px-3 py-2 border rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white w-40">
</div>
<button @click="runCustomTest()" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 text-sm">
Trace
</button>
</div>
<!-- Custom result -->
<template x-if="customResult">
<div class="mt-4">
<div x-html="renderTrace(customResult)"></div>
</div>
</template>
</div>
<!-- ═══════════════════════════════════════════════════════════════════ -->
<!-- Main area — tool content panels -->
<!-- ═══════════════════════════════════════════════════════════════════ -->
<div class="flex-1 min-w-0">
<!-- Test Results -->
<div class="space-y-4">
<template x-for="test in filteredTests" :key="test.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs overflow-hidden">
<!-- Header -->
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer flex items-center justify-between"
@click="test.expanded = !test.expanded">
<div class="flex items-center gap-3">
<!-- Status indicator -->
<div class="w-3 h-3 rounded-full"
:class="{
'bg-gray-300 dark:bg-gray-600': !test.result,
'bg-green-500': test.result && test.pass,
'bg-red-500': test.result && !test.pass,
'animate-pulse bg-yellow-400': test.running
}"></div>
<span class="text-xs px-2 py-0.5 rounded-full font-medium"
:class="{
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': test.group === 'dev',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': test.group === 'prod-domain',
'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300': test.group === 'prod-subdomain',
'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300': test.group === 'prod-custom'
}" x-text="test.groupLabel"></span>
<div>
<span class="font-mono text-sm text-gray-900 dark:text-white" x-text="test.label"></span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2" x-text="test.description"></span>
<!-- ─────────────────────────────────────────────────────────── -->
<!-- Tool: Platform Trace (existing functionality, verbatim) -->
<!-- ─────────────────────────────────────────────────────────── -->
<div x-show="activeTool === 'platform-trace'" x-cloak>
<div class="mb-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Platform Resolution Trace</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Simulates the middleware pipeline for each URL pattern to trace how platform &amp; store context are resolved.
</p>
</div>
<!-- Controls -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-xs">
<div class="flex items-center gap-4 flex-wrap">
<button @click="runAllTests()" :disabled="running"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50">
<span x-show="!running">Run All Tests</span>
<span x-show="running">Running...</span>
</button>
<div class="flex gap-2">
<button @click="filterGroup = 'all'" :class="filterGroup === 'all' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">All</button>
<button @click="filterGroup = 'dev'" :class="filterGroup === 'dev' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Dev Path-Based</button>
<button @click="filterGroup = 'prod-domain'" :class="filterGroup === 'prod-domain' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Prod Domain</button>
<button @click="filterGroup = 'prod-subdomain'" :class="filterGroup === 'prod-subdomain' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Prod Subdomain</button>
<button @click="filterGroup = 'prod-custom'" :class="filterGroup === 'prod-custom' ? 'bg-gray-200 dark:bg-gray-600' : ''"
class="px-3 py-1 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-gray-300">Prod Custom Domain</button>
</div>
</div>
<div class="flex items-center gap-3">
<template x-if="test.result">
<span class="text-xs font-mono px-2 py-1 rounded"
:class="test.pass ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'"
x-text="'platform=' + (test.result.login_platform_code || 'null')"></span>
</template>
<template x-if="test.result">
<button @click.stop="copyTrace(test)" title="Copy full trace"
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
Copy
</button>
</template>
<svg class="w-4 h-4 text-gray-400 transition-transform" :class="test.expanded ? 'rotate-180' : ''"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
<!-- Detail -->
<div x-show="test.expanded" x-cloak class="px-4 py-3">
<template x-if="test.result">
<div x-html="renderTrace(test.result)"></div>
<!-- Custom test -->
<div class="mb-6 p-4 bg-white dark:bg-gray-800 rounded-lg shadow-xs">
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Custom Test</h3>
<div class="flex gap-3 items-end flex-wrap">
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Host</label>
<input x-model="customHost" type="text" placeholder="localhost:8000"
class="px-3 py-2 border rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white w-56">
</div>
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Path</label>
<input x-model="customPath" type="text" placeholder="/platforms/loyalty/store/WIZATECH/login"
class="px-3 py-2 border rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white w-80">
</div>
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Body platform_code</label>
<input x-model="customPlatformCode" type="text" placeholder="(optional)"
class="px-3 py-2 border rounded-lg text-sm dark:bg-gray-700 dark:border-gray-600 dark:text-white w-40">
</div>
<button @click="runCustomTest()" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 text-sm">
Trace
</button>
</div>
<!-- Custom result -->
<template x-if="customResult">
<div class="mt-4">
<div x-html="renderTrace(customResult)"></div>
</div>
</template>
<template x-if="!test.result && !test.running">
<p class="text-sm text-gray-500 dark:text-gray-400">Click "Run All Tests" to trace this case.</p>
</template>
<template x-if="test.running">
<p class="text-sm text-yellow-600 dark:text-yellow-400">Running...</p>
</div>
<!-- Test Results -->
<div class="space-y-4">
<template x-for="test in filteredTests" :key="test.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs overflow-hidden">
<!-- Header -->
<div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer flex items-center justify-between"
@click="test.expanded = !test.expanded">
<div class="flex items-center gap-3">
<!-- Status indicator -->
<div class="w-3 h-3 rounded-full"
:class="{
'bg-gray-300 dark:bg-gray-600': !test.result,
'bg-green-500': test.result && test.pass,
'bg-red-500': test.result && !test.pass,
'animate-pulse bg-yellow-400': test.running
}"></div>
<span class="text-xs px-2 py-0.5 rounded-full font-medium"
:class="{
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': test.group === 'dev',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': test.group === 'prod-domain',
'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300': test.group === 'prod-subdomain',
'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300': test.group === 'prod-custom'
}" x-text="test.groupLabel"></span>
<div>
<span class="font-mono text-sm text-gray-900 dark:text-white" x-text="test.label"></span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2" x-text="test.description"></span>
</div>
</div>
<div class="flex items-center gap-3">
<template x-if="test.result">
<span class="text-xs font-mono px-2 py-1 rounded"
:class="test.pass ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300'"
x-text="'platform=' + (test.result.login_platform_code || 'null')"></span>
</template>
<template x-if="test.result">
<button @click.stop="copyTrace(test)" title="Copy full trace"
class="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded hover:bg-gray-200 dark:hover:bg-gray-600">
Copy
</button>
</template>
<svg class="w-4 h-4 text-gray-400 transition-transform" :class="test.expanded ? 'rotate-180' : ''"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>
</div>
</div>
<!-- Detail -->
<div x-show="test.expanded" x-cloak class="px-4 py-3">
<template x-if="test.result">
<div x-html="renderTrace(test.result)"></div>
</template>
<template x-if="!test.result && !test.running">
<p class="text-sm text-gray-500 dark:text-gray-400">Click "Run All Tests" to trace this case.</p>
</template>
<template x-if="test.running">
<p class="text-sm text-yellow-600 dark:text-yellow-400">Running...</p>
</template>
</div>
</div>
</template>
</div>
</div>
</template>
<!-- ─────────────────────────────────────────────────────────── -->
<!-- Tool: Permissions Audit -->
<!-- ─────────────────────────────────────────────────────────── -->
<div x-show="activeTool === 'permissions-audit'" x-cloak>
<div class="mb-6">
<h2 class="text-xl font-bold text-gray-900 dark:text-white">Permissions Audit</h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Introspects all registered page routes and reports which have proper auth/permission enforcement.
</p>
</div>
<!-- Run Audit button -->
<div class="mb-6">
<button @click="runPermissionsAudit()" :disabled="auditLoading"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 text-sm font-medium">
<span x-show="!auditLoading">Run Audit</span>
<span x-show="auditLoading">Running...</span>
</button>
<span x-show="auditError" class="ml-3 text-sm text-red-600 dark:text-red-400" x-text="auditError"></span>
</div>
<!-- Summary cards -->
<template x-if="auditSummary">
<div class="grid grid-cols-4 gap-4 mb-6">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs p-4 text-center">
<div class="text-2xl font-bold text-gray-900 dark:text-white" x-text="auditSummary.total"></div>
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mt-1">Total Routes</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs p-4 text-center border-t-2 border-green-500">
<div class="text-2xl font-bold text-green-600 dark:text-green-400" x-text="auditSummary.ok"></div>
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mt-1">OK</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs p-4 text-center border-t-2 border-amber-500">
<div class="text-2xl font-bold text-amber-600 dark:text-amber-400" x-text="auditSummary.warnings"></div>
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mt-1">Warnings</div>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xs p-4 text-center border-t-2 border-red-500">
<div class="text-2xl font-bold text-red-600 dark:text-red-400" x-text="auditSummary.errors"></div>
<div class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mt-1">Errors</div>
</div>
</div>
</template>
<!-- Filter bar -->
<template x-if="auditRoutes.length > 0">
<div class="mb-4 flex items-center gap-4">
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Frontend</label>
<select x-model="auditFilterFrontend"
class="px-3 py-1.5 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="all">All</option>
<option value="admin">Admin</option>
<option value="store">Store</option>
<option value="merchant">Merchant</option>
<option value="storefront">Storefront</option>
</select>
</div>
<div>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">Status</label>
<select x-model="auditFilterStatus"
class="px-3 py-1.5 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white">
<option value="all">All</option>
<option value="ok">OK</option>
<option value="warning">Warnings</option>
<option value="error">Errors</option>
</select>
</div>
<div class="ml-auto text-sm text-gray-500 dark:text-gray-400">
Showing <span class="font-medium text-gray-900 dark:text-white" x-text="filteredAuditRoutes.length"></span>
of <span x-text="auditRoutes.length"></span> routes
</div>
</div>
</template>
<!-- Results table -->
<template x-if="auditRoutes.length > 0">
{% call table_wrapper() %}
{{ table_header(['Status', 'Frontend', 'Path', 'Endpoint', 'Auth', 'Issue']) }}
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
{{ table_empty_state(6, title='No routes match your filters', icon='shield-check', show_condition='filteredAuditRoutes.length === 0') }}
<template x-for="route in filteredAuditRoutes" :key="route.path + route.methods.join()">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 text-sm">
<td class="px-4 py-2.5">
<span class="text-xs px-2 py-0.5 rounded-full font-medium"
:class="{
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300': route.status === 'ok',
'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-300': route.status === 'warning',
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300': route.status === 'error'
}" x-text="route.status"></span>
</td>
<td class="px-4 py-2.5">
<span class="text-xs px-2 py-0.5 rounded-full font-medium"
:class="{
'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300': route.frontend === 'admin',
'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300': route.frontend === 'store',
'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-300': route.frontend === 'merchant',
'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-300': route.frontend === 'storefront',
'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300': !route.frontend
}" x-text="route.frontend || 'other'"></span>
</td>
<td class="px-4 py-2.5 font-mono text-xs" x-text="route.path"></td>
<td class="px-4 py-2.5 text-xs text-gray-500 dark:text-gray-400" x-text="route.endpoint_name"></td>
<td class="px-4 py-2.5 text-xs">
<span x-text="route.auth_dependency || '—'" class="font-mono"></span>
<template x-if="route.auth_detail">
<span class="text-gray-400 dark:text-gray-500 ml-1" x-text="'(' + route.auth_detail + ')'"></span>
</template>
</td>
<td class="px-4 py-2.5 text-xs text-gray-500 dark:text-gray-400" x-text="route.issue || '—'"></td>
</tr>
</template>
</tbody>
{% endcall %}
</template>
</div>
</div>
</div>
<script>
function platformDebug() {
return {
// Inherit base layout functionality
...data(),
// Page identifier
currentPage: 'platform-debug',
// ── Sidebar / tool navigation ──
activeTool: 'platform-trace',
expandedCategories: ['Resolution', 'Security'],
toolGroups: [
{
category: 'Resolution',
items: [
{ id: 'platform-trace', label: 'Platform Trace', icon: 'search' },
],
},
{
category: 'Security',
items: [
{ id: 'permissions-audit', label: 'Permissions Audit', icon: 'shield-check' },
],
},
],
toggleCategory(cat) {
const idx = this.expandedCategories.indexOf(cat);
if (idx >= 0) this.expandedCategories.splice(idx, 1);
else this.expandedCategories.push(cat);
},
isCategoryExpanded(cat) {
return this.expandedCategories.includes(cat);
},
selectTool(toolId) {
this.activeTool = toolId;
},
// ── Platform Trace (existing) ──
running: false,
filterGroup: 'all',
customHost: 'localhost:8000',
@@ -308,9 +512,6 @@ function platformDebug() {
},
// ── Prod custom domain ──
// Uses real StoreDomain records:
// wizatech.shop → store_id=1 (WIZATECH), platform_id=1 (oms)
// fashionhub.store → store_id=4 (FASHIONHUB), platform_id=3 (loyalty)
{
id: 'prod-c-1', group: 'prod-custom', groupLabel: 'Prod Custom',
label: 'wizatech.shop /store/login',
@@ -460,6 +661,35 @@ function platformDebug() {
return html;
},
// ── Permissions Audit ──
auditRoutes: [],
auditSummary: null,
auditLoading: false,
auditError: '',
auditFilterFrontend: 'all',
auditFilterStatus: 'all',
get filteredAuditRoutes() {
return this.auditRoutes.filter(r => {
if (this.auditFilterFrontend !== 'all' && r.frontend !== this.auditFilterFrontend) return false;
if (this.auditFilterStatus !== 'all' && r.status !== this.auditFilterStatus) return false;
return true;
});
},
async runPermissionsAudit() {
this.auditLoading = true;
this.auditError = '';
try {
const resp = await apiClient.get('/admin/diagnostics/permissions-audit');
this.auditRoutes = resp.routes;
this.auditSummary = resp.summary;
} catch (e) {
this.auditError = e.message || 'Failed to run audit';
}
this.auditLoading = false;
},
};
}
</script>