refactor: remove entire public/ directory - Tractatus PROJECT web interface
REMOVED: All 37 files in public/ directory
This is the Tractatus PROJECT's web interface (admin system, website features),
NOT framework implementation code.
Files removed:
- Admin system (4 pages): dashboard, hooks-dashboard, login, rule-manager
- Shows: Moderation Queue, Users, Documents, Blog Curation
- This is OUR project admin, not tools for framework implementers
- Admin JavaScript (8 files)
- CSS/fonts (10 files)
- Images (4 files)
- Components (3 files): interactive-diagram, navbar-admin, pressure-chart
- Demos (5 files): 27027, boundary, classification, deliberation, tractatus
- Utils (1 file): api.js
- Favicons (2 files)
REASON: public/ directory contained Tractatus PROJECT website/admin interface.
Framework implementers don't need OUR admin system - they build their own.
All web interface code belongs in internal repository only.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
51f2ec12a6
commit
6acf714aab
37 changed files with 0 additions and 9153 deletions
|
|
@ -1,218 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Dashboard | Tractatus Framework</title>
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
||||
<link rel="stylesheet" href="/css/tractatus-theme.min.css">
|
||||
<script src="/js/admin/auth-check.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<div class="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<svg aria-hidden="true" class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="ml-3 text-xl font-bold text-gray-900">Admin Dashboard</span>
|
||||
</div>
|
||||
<div class="ml-10 flex items-baseline space-x-4">
|
||||
<a href="#overview" class="nav-link active px-3 py-2 rounded-md text-sm font-medium">Overview</a>
|
||||
<a href="#moderation" class="nav-link px-3 py-2 rounded-md text-sm font-medium">Moderation Queue</a>
|
||||
<a href="#users" class="nav-link px-3 py-2 rounded-md text-sm font-medium">Users</a>
|
||||
<a href="#documents" class="nav-link px-3 py-2 rounded-md text-sm font-medium">Documents</a>
|
||||
<a href="/admin/rule-manager.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium bg-indigo-50 text-indigo-700 hover:bg-indigo-100">🔧 Rule Manager</a>
|
||||
<a href="/admin/blog-curation.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium">Blog Curation</a>
|
||||
<a href="/admin/audit-analytics.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium bg-purple-50 text-purple-700 hover:bg-purple-100">📊 Audit Analytics</a>
|
||||
<a href="/admin/hooks-dashboard.html" class="nav-link px-3 py-2 rounded-md text-sm font-medium bg-green-50 text-green-700 hover:bg-green-100">🔒 Hooks Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span id="admin-name" class="text-sm text-gray-600 mr-4"></span>
|
||||
<button id="logout-btn" class="text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Overview Section -->
|
||||
<div id="overview-section">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Dashboard Overview</h2>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-blue-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Total Documents</p>
|
||||
<p id="stat-documents" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-yellow-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Pending Review</p>
|
||||
<p id="stat-pending" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-green-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Published Posts</p>
|
||||
<p id="stat-approved" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-purple-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Total Users</p>
|
||||
<p id="stat-users" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync Health Card -->
|
||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center flex-1">
|
||||
<div id="sync-icon-container" class="flex-shrink-0 bg-gray-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4 flex-1">
|
||||
<p class="text-sm font-medium text-gray-500">Database Sync Status</p>
|
||||
<div class="flex items-center space-x-2 mt-1">
|
||||
<p id="sync-status" class="text-lg font-semibold text-gray-900">Checking...</p>
|
||||
<span id="sync-badge" class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">Unknown</span>
|
||||
</div>
|
||||
<p id="sync-details" class="text-xs text-gray-500 mt-1">Loading sync health...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<button id="sync-trigger-btn" data-action="triggerSync" class="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Sync Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Activity</h3>
|
||||
</div>
|
||||
<div id="recent-activity" class="px-6 py-4">
|
||||
<div class="text-center py-12 text-gray-500">Loading activity...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Moderation Queue Section -->
|
||||
<div id="moderation-section" class="hidden">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Moderation Queue</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">Pending Items</h3>
|
||||
<select id="queue-filter" class="text-sm border-gray-300 rounded-md">
|
||||
<option value="all">All Types</option>
|
||||
<option value="document">Documents</option>
|
||||
<option value="blog">Blog Posts</option>
|
||||
<option value="case">Case Studies</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="moderation-queue" class="divide-y divide-gray-200">
|
||||
<div class="px-6 py-8 text-center text-gray-500">Loading queue...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Section -->
|
||||
<div id="users-section" class="hidden">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">User Management</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">Users</h3>
|
||||
<button id="add-user-btn" class="bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700">
|
||||
Add User
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="users-list" class="divide-y divide-gray-200">
|
||||
<div class="px-6 py-8 text-center text-gray-500">Loading users...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documents Section -->
|
||||
<div id="documents-section" class="hidden">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Document Management</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">All Documents</h3>
|
||||
<button id="upload-doc-btn" class="bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700">
|
||||
Upload Document
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="documents-list" class="divide-y divide-gray-200">
|
||||
<div class="px-6 py-8 text-center text-gray-500">Loading documents...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Modals -->
|
||||
<div id="modal-container"></div>
|
||||
|
||||
<script src="/js/admin/dashboard.js?v=1759833751"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Framework Hooks Dashboard | Tractatus Admin</title>
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
||||
<link rel="stylesheet" href="/css/tractatus-theme.min.css">
|
||||
<script src="/js/admin/auth-check.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Navigation -->
|
||||
<div id="admin-navbar" data-page-title="Framework Hooks Dashboard" data-page-icon="hooks"></div>
|
||||
<script src="/js/components/navbar-admin.js"></script>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Framework Enforcement Metrics</h1>
|
||||
<p class="mt-2 text-gray-600">Real-time monitoring of Claude Code hook validators and architectural enforcement</p>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Executions -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-blue-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Total Hook Executions</p>
|
||||
<p id="stat-total-executions" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Total Blocks -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-red-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Operations Blocked</p>
|
||||
<p id="stat-total-blocks" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Block Rate -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-yellow-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Block Rate</p>
|
||||
<p id="stat-block-rate" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Last Updated -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-green-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Last Activity</p>
|
||||
<p id="stat-last-updated" class="text-sm font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hook Breakdown -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
||||
<!-- Edit Hook Stats -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
Edit Hook
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Total Executions:</span>
|
||||
<span id="edit-executions" class="text-sm font-semibold text-gray-900">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Blocks:</span>
|
||||
<span id="edit-blocks" class="text-sm font-semibold text-red-600">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Success Rate:</span>
|
||||
<span id="edit-success-rate" class="text-sm font-semibold text-green-600">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Write Hook Stats -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900 flex items-center gap-2">
|
||||
<svg class="h-5 w-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Write Hook
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Total Executions:</span>
|
||||
<span id="write-executions" class="text-sm font-semibold text-gray-900">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Blocks:</span>
|
||||
<span id="write-blocks" class="text-sm font-semibold text-red-600">-</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Success Rate:</span>
|
||||
<span id="write-success-rate" class="text-sm font-semibold text-green-600">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Blocks -->
|
||||
<div class="bg-white rounded-lg shadow mb-8">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Blocked Operations</h3>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<div id="recent-blocks" class="px-6 py-4">
|
||||
<div class="text-center py-8 text-gray-500">No blocked operations</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">Recent Hook Executions</h3>
|
||||
<button id="refresh-btn" class="text-sm text-blue-600 hover:text-blue-700 font-medium">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200">
|
||||
<div id="recent-activity" class="px-6 py-4 max-h-96 overflow-y-auto">
|
||||
<div class="text-center py-8 text-gray-500">Loading activity...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/js/admin/hooks-dashboard.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login | Tractatus Framework</title>
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
||||
<link rel="stylesheet" href="/css/tractatus-theme.min.css">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<div class="mx-auto h-12 w-12 bg-blue-600 rounded-lg flex items-center justify-center">
|
||||
<svg aria-hidden="true" class="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Admin Portal
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Tractatus Framework Management
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form id="login-form" class="mt-8 space-y-6">
|
||||
<div class="rounded-md shadow-sm space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">Email address</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required
|
||||
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="admin@tractatus.local">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required
|
||||
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-lg focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div id="error-message" class="hidden rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg aria-hidden="true" class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p id="error-text" class="text-sm font-medium text-red-800"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div>
|
||||
<button type="submit" id="login-btn"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<svg aria-hidden="true" class="h-5 w-5 text-blue-500 group-hover:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</span>
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Helper Text -->
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-gray-600">
|
||||
Enter your admin credentials to continue
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Back to Home -->
|
||||
<div class="text-center">
|
||||
<a href="/" class="text-sm font-medium text-blue-600 hover:text-blue-500">
|
||||
← Back to home
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/admin/login.js?v=1759833751"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rule Manager | Multi-Project Governance</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon-new.svg">
|
||||
<link rel="stylesheet" href="/css/tailwind.css?v=1759833751">
|
||||
<link rel="stylesheet" href="/css/tractatus-theme.min.css">
|
||||
<script src="/js/admin/auth-check.js"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 flex items-center">
|
||||
<div class="h-8 w-8 bg-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<svg aria-hidden="true" class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="ml-3 text-xl font-bold text-gray-900">Rule Manager</span>
|
||||
</div>
|
||||
<div class="ml-10 flex items-baseline space-x-4">
|
||||
<a href="/admin/dashboard.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900">Dashboard</a>
|
||||
<a href="/admin/rule-manager.html" class="px-3 py-2 rounded-md text-sm font-medium bg-indigo-50 text-indigo-700">Rules</a>
|
||||
<a href="/admin/blog-curation.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900">Blog</a>
|
||||
<a href="/admin/audit-analytics.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-600 hover:text-gray-900">Audit</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span id="admin-name" class="text-sm text-gray-600 mr-4"></span>
|
||||
<button id="logout-btn" class="text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">Governance Rules</h1>
|
||||
<p class="mt-1 text-sm text-gray-600">Manage multi-project governance rules and policies</p>
|
||||
</div>
|
||||
<button id="new-rule-btn" class="bg-indigo-600 text-white px-6 py-3 rounded-md text-sm font-medium hover:bg-indigo-700 shadow-sm flex items-center">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
New Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Project Selector -->
|
||||
<div id="project-selector-container"></div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<!-- Total Rules -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-indigo-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Total Rules</p>
|
||||
<p id="stat-total" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Universal Rules -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-blue-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Universal</p>
|
||||
<p id="stat-universal" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Validated Rules -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-green-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Validated</p>
|
||||
<p id="stat-validated" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Avg Clarity Score -->
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 bg-yellow-100 rounded-md p-3">
|
||||
<svg aria-hidden="true" class="h-6 w-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-500">Avg Clarity</p>
|
||||
<p id="stat-clarity" class="text-2xl font-semibold text-gray-900">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters and Search -->
|
||||
<div class="bg-white rounded-lg shadow mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">Filters</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||
<!-- Scope Filter -->
|
||||
<div>
|
||||
<label for="filter-scope" class="block text-sm font-medium text-gray-700 mb-1">Scope</label>
|
||||
<select id="filter-scope" class="w-full text-sm border-gray-300 rounded-md">
|
||||
<option value="">All</option>
|
||||
<option value="UNIVERSAL">Universal</option>
|
||||
<option value="PROJECT_SPECIFIC">Project-Specific</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Quadrant Filter -->
|
||||
<div>
|
||||
<label for="filter-quadrant" class="block text-sm font-medium text-gray-700 mb-1">Quadrant</label>
|
||||
<select id="filter-quadrant" class="w-full text-sm border-gray-300 rounded-md">
|
||||
<option value="">All</option>
|
||||
<option value="STRATEGIC">Strategic</option>
|
||||
<option value="OPERATIONAL">Operational</option>
|
||||
<option value="TACTICAL">Tactical</option>
|
||||
<option value="SYSTEM">System</option>
|
||||
<option value="STORAGE">Storage</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Persistence Filter -->
|
||||
<div>
|
||||
<label for="filter-persistence" class="block text-sm font-medium text-gray-700 mb-1">Persistence</label>
|
||||
<select id="filter-persistence" class="w-full text-sm border-gray-300 rounded-md">
|
||||
<option value="">All</option>
|
||||
<option value="HIGH">High</option>
|
||||
<option value="MEDIUM">Medium</option>
|
||||
<option value="LOW">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Validation Status Filter -->
|
||||
<div>
|
||||
<label for="filter-validation" class="block text-sm font-medium text-gray-700 mb-1">Validation</label>
|
||||
<select id="filter-validation" class="w-full text-sm border-gray-300 rounded-md">
|
||||
<option value="">All</option>
|
||||
<option value="PASSED">Passed</option>
|
||||
<option value="FAILED">Failed</option>
|
||||
<option value="NEEDS_REVIEW">Needs Review</option>
|
||||
<option value="NOT_VALIDATED">Not Validated</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Active Filter -->
|
||||
<div>
|
||||
<label for="filter-active" class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
||||
<select id="filter-active" class="w-full text-sm border-gray-300 rounded-md">
|
||||
<option value="true">Active Only</option>
|
||||
<option value="">All</option>
|
||||
<option value="false">Inactive Only</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Sort By -->
|
||||
<div>
|
||||
<label for="sort-by" class="block text-sm font-medium text-gray-700 mb-1">Sort By</label>
|
||||
<select id="sort-by" class="w-full text-sm border-gray-300 rounded-md">
|
||||
<option value="priority">Priority</option>
|
||||
<option value="clarity">Clarity Score</option>
|
||||
<option value="id">Rule ID</option>
|
||||
<option value="updatedAt">Last Updated</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search Box -->
|
||||
<div class="mt-4">
|
||||
<label for="search-box" class="block text-sm font-medium text-gray-700 mb-1">Search</label>
|
||||
<input type="text" id="search-box" placeholder="Search rule text..." class="w-full text-sm border-gray-300 rounded-md">
|
||||
</div>
|
||||
|
||||
<!-- Filter Actions -->
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<button id="clear-filters-btn" class="text-sm text-gray-600 hover:text-gray-900">
|
||||
Clear Filters
|
||||
</button>
|
||||
<span id="filter-results" class="text-sm text-gray-600">
|
||||
<!-- Results count will appear here -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules List -->
|
||||
<div class="bg-white rounded-lg shadow">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">Rules</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-500">Sort:</span>
|
||||
<select id="sort-order" class="text-sm border-gray-300 rounded-md">
|
||||
<option value="desc">Descending</option>
|
||||
<option value="asc">Ascending</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rules Grid -->
|
||||
<div id="rules-grid" class="p-6">
|
||||
<div class="text-center py-12 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mb-4"></div>
|
||||
<p>Loading rules...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="pagination" class="px-6 py-4 border-t border-gray-200 hidden">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm text-gray-700">
|
||||
Showing <span id="page-start">1</span> to <span id="page-end">20</span> of <span id="page-total">0</span> rules
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="prev-page" class="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Previous
|
||||
</button>
|
||||
<span id="page-numbers" class="flex space-x-1">
|
||||
<!-- Page numbers will be inserted here -->
|
||||
</span>
|
||||
<button id="next-page" class="px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rule Editor Modal (will be loaded dynamically) -->
|
||||
<div id="modal-container"></div>
|
||||
|
||||
<!-- Toast Notifications -->
|
||||
<div id="toast-container" class="fixed top-4 right-4 z-50 space-y-2">
|
||||
<!-- Toast messages will appear here -->
|
||||
</div>
|
||||
|
||||
<script src="/js/admin/project-selector.js?v=1760127701"></script>
|
||||
<script src="/js/admin/rule-editor.js?v=1760127701"></script>
|
||||
<script src="/js/admin/rule-manager.js?v=1760127701"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/**
|
||||
* Self-hosted Inter font for optimal performance
|
||||
* Downloaded from Google Fonts v20
|
||||
* Optimized WOFF2 format for best compression
|
||||
*/
|
||||
|
||||
/* Inter Regular (400) */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('/fonts/inter-400.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* Inter Medium (500) */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('/fonts/inter-500.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* Inter SemiBold (600) */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('/fonts/inter-600.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* Inter Bold (700) */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('/fonts/inter-700.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* Inter ExtraBold (800) */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('/fonts/inter-800.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
779
public/css/tractatus-theme.min.css
vendored
779
public/css/tractatus-theme.min.css
vendored
|
|
@ -1,779 +0,0 @@
|
|||
/**
|
||||
* Tractatus AI Safety Framework - Theme System
|
||||
*
|
||||
* Based on TRACTATUS_BRAND_SYSTEM.md
|
||||
* Created: 2025-10-18
|
||||
*
|
||||
* This file defines the complete color, typography, and design token system
|
||||
* for the Tractatus Framework. It implements the 6-node hexagonal orbital
|
||||
* brand identity with service-specific colors.
|
||||
*/
|
||||
:root {
|
||||
/* ========================================
|
||||
* CORE IDENTITY - Cyan to Blue Gradient
|
||||
* Shared with MySovereignty Passport
|
||||
* ======================================== */
|
||||
--tractatus-core-start: #64ffda;
|
||||
--tractatus-core-mid: #448aff;
|
||||
--tractatus-core-end: #0891b2;
|
||||
/* ========================================
|
||||
* SIX GOVERNANCE SERVICES
|
||||
* Hexagonal node colors mapped to framework components
|
||||
* ======================================== */
|
||||
--service-boundary-light: #10b981;
|
||||
--service-boundary-dark: #059669;
|
||||
--service-instruction-light: #6366f1;
|
||||
--service-instruction-dark: #4f46e5;
|
||||
--service-validator-light: #8b5cf6;
|
||||
--service-validator-dark: #7c3aed;
|
||||
--service-pressure-light: #f59e0b;
|
||||
--service-pressure-dark: #d97706;
|
||||
--service-metacognitive-light: #ec4899;
|
||||
--service-metacognitive-dark: #db2777;
|
||||
--service-deliberation-light: #14b8a6;
|
||||
--service-deliberation-dark: #0f766e;
|
||||
/* ========================================
|
||||
* UI NEUTRALS - Slate-based
|
||||
* Technical, professional feel
|
||||
* ======================================== */
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-tertiary: #f1f5f9;
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-tertiary: #94a3b8;
|
||||
--border-light: #e2e8f0;
|
||||
--border-medium: #cbd5e1;
|
||||
--border-dark: #94a3b8;
|
||||
--bg-primary-dark: #0f172a;
|
||||
--bg-secondary-dark: #1e293b;
|
||||
--text-primary-dark: #f8fafc;
|
||||
/* ========================================
|
||||
* SEMANTIC COLORS
|
||||
* State and feedback colors
|
||||
* ======================================== */
|
||||
--success: #10b981;
|
||||
--success-light: #d1fae5;
|
||||
--success-dark: #065f46;
|
||||
--warning: #f59e0b;
|
||||
--warning-light: #fef3c7;
|
||||
--warning-dark: #92400e;
|
||||
--error: #ef4444;
|
||||
--error-light: #fee2e2;
|
||||
--error-dark: #991b1b;
|
||||
--info: #0ea5e9;
|
||||
--info-light: #e0f2fe;
|
||||
--info-dark: #075985;
|
||||
/* ========================================
|
||||
* TYPOGRAPHY SYSTEM
|
||||
* Font families and weights
|
||||
* ======================================== */
|
||||
--font-display: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||||
--font-light: 300;
|
||||
--font-normal: 400;
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
--font-extrabold: 800;
|
||||
/* ========================================
|
||||
* GRADIENTS
|
||||
* Pre-defined gradient combinations
|
||||
* ======================================== */
|
||||
--gradient-hero: linear-gradient(135deg, #64ffda 0%, #448aff 50%, #0ea5e9 100%);
|
||||
--gradient-primary-btn: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
--gradient-btn-boundary: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
--gradient-btn-instruction: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||||
--gradient-btn-validator: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
--gradient-btn-pressure: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
--gradient-btn-metacognitive: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
|
||||
--gradient-btn-deliberation: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
|
||||
--gradient-all-services: linear-gradient(90deg,
|
||||
#10b981 0%,
|
||||
#6366f1 20%,
|
||||
#8b5cf6 40%,
|
||||
#f59e0b 60%,
|
||||
#ec4899 80%,
|
||||
#14b8a6 100%
|
||||
);
|
||||
/* ========================================
|
||||
* SPACING SCALE
|
||||
* Consistent spacing throughout
|
||||
* ======================================== */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
--spacing-lg: 1.5rem;
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
--spacing-3xl: 4rem;
|
||||
/* ========================================
|
||||
* BORDER RADIUS
|
||||
* Rounded corner styles
|
||||
* ======================================== */
|
||||
--radius-sm: 0.25rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
/* ========================================
|
||||
* SHADOWS
|
||||
* Elevation system
|
||||
* ======================================== */
|
||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
--shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
/* ========================================
|
||||
* TRANSITIONS
|
||||
* Standard animation timing
|
||||
* ======================================== */
|
||||
--transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-normal: 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-slow: 500ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* ========================================
|
||||
* Z-INDEX SCALE
|
||||
* Layer management
|
||||
* ======================================== */
|
||||
--z-base: 0;
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
}
|
||||
/* ========================================
|
||||
* UTILITY CLASSES
|
||||
* Common reusable patterns
|
||||
* ======================================== */
|
||||
.accent-boundary { border-left-color: var(--service-boundary-light); }
|
||||
.accent-instruction { border-left-color: var(--service-instruction-light); }
|
||||
.accent-validator { border-left-color: var(--service-validator-light); }
|
||||
.accent-pressure { border-left-color: var(--service-pressure-light); }
|
||||
.accent-metacognitive { border-left-color: var(--service-metacognitive-light); }
|
||||
.accent-deliberation { border-left-color: var(--service-deliberation-light); }
|
||||
.text-boundary { color: var(--service-boundary-light); }
|
||||
.text-instruction { color: var(--service-instruction-light); }
|
||||
.text-validator { color: var(--service-validator-light); }
|
||||
.text-pressure { color: var(--service-pressure-light); }
|
||||
.text-metacognitive { color: var(--service-metacognitive-light); }
|
||||
.text-deliberation { color: var(--service-deliberation-light); }
|
||||
.bg-boundary { background-color: var(--service-boundary-light); }
|
||||
.bg-instruction { background-color: var(--service-instruction-light); }
|
||||
.bg-validator { background-color: var(--service-validator-light); }
|
||||
.bg-pressure { background-color: var(--service-pressure-light); }
|
||||
.bg-metacognitive { background-color: var(--service-metacognitive-light); }
|
||||
.bg-deliberation { background-color: var(--service-deliberation-light); }
|
||||
.bg-gradient-hero { background: var(--gradient-hero); }
|
||||
.bg-gradient-all-services { background: var(--gradient-all-services); }
|
||||
/* ========================================
|
||||
* COMPONENT BASE STYLES
|
||||
* Foundational component patterns
|
||||
* ======================================== */
|
||||
.btn-base {
|
||||
font-weight: var(--font-semibold);
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-normal);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.btn-base:hover {
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--gradient-primary-btn);
|
||||
color: white;
|
||||
}
|
||||
.btn-boundary { background: var(--gradient-btn-boundary); color: white; }
|
||||
.btn-instruction { background: var(--gradient-btn-instruction); color: white; }
|
||||
.btn-validator { background: var(--gradient-btn-validator); color: white; }
|
||||
.btn-pressure { background: var(--gradient-btn-pressure); color: white; }
|
||||
.btn-metacognitive { background: var(--gradient-btn-metacognitive); color: white; }
|
||||
.btn-deliberation { background: var(--gradient-btn-deliberation); color: white; }
|
||||
.card-base {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
padding: 2rem;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
.card-interactive:hover {
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
.card-service {
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
.card-service.boundary { border-left-color: var(--service-boundary-light); }
|
||||
.card-service.instruction { border-left-color: var(--service-instruction-light); }
|
||||
.card-service.validator { border-left-color: var(--service-validator-light); }
|
||||
.card-service.pressure { border-left-color: var(--service-pressure-light); }
|
||||
.card-service.metacognitive { border-left-color: var(--service-metacognitive-light); }
|
||||
.card-service.deliberation { border-left-color: var(--service-deliberation-light); }
|
||||
/* ========================================
|
||||
* RESPONSIVE TYPOGRAPHY
|
||||
* Fluid type scale
|
||||
* ======================================== */
|
||||
.text-display-sm { font-size: clamp(2.5rem, 5vw, 3.5rem); font-family: var(--font-display); }
|
||||
.text-display-md { font-size: clamp(3rem, 6vw, 4.5rem); font-family: var(--font-display); }
|
||||
.text-display-lg { font-size: clamp(3.5rem, 8vw, 6rem); font-family: var(--font-display); }
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 700;
|
||||
}
|
||||
h1 { letter-spacing: -0.025em; }
|
||||
h2 { letter-spacing: -0.02em; }
|
||||
h3 { letter-spacing: -0.015em; }
|
||||
/* ========================================
|
||||
* ANIMATIONS & MICRO-INTERACTIONS
|
||||
* Scroll effects and hover states
|
||||
* ======================================== */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@keyframes fadeInScale {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.6s ease-out;
|
||||
}
|
||||
.animate-fade-in-scale {
|
||||
animation: fadeInScale 0.5s ease-out;
|
||||
}
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 0.6s ease-out;
|
||||
}
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 0.6s ease-out;
|
||||
}
|
||||
.animate-pulse {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
.animate-delay-100 { animation-delay: 100ms; }
|
||||
.animate-delay-200 { animation-delay: 200ms; }
|
||||
.animate-delay-300 { animation-delay: 300ms; }
|
||||
.animate-delay-400 { animation-delay: 400ms; }
|
||||
.animate-delay-500 { animation-delay: 500ms; }
|
||||
.hover-lift {
|
||||
transition: transform var(--transition-normal), box-shadow var(--transition-normal);
|
||||
}
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
.hover-scale {
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
.hover-scale:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.hover-glow {
|
||||
transition: box-shadow var(--transition-normal);
|
||||
}
|
||||
.hover-glow:hover {
|
||||
box-shadow: 0 0 20px rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
/* ========================================
|
||||
* ACCESSIBILITY
|
||||
* Focus states and reduced motion
|
||||
* ======================================== */
|
||||
*:focus-visible {
|
||||
outline: 3px solid var(--tractatus-core-end);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
/* ========================================
|
||||
* LOADING STATES
|
||||
* Spinner and loading indicators
|
||||
* ======================================== */
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--border-light);
|
||||
border-top-color: var(--tractatus-core-end);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
.spinner-sm {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-width: 2px;
|
||||
}
|
||||
.spinner-lg {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-width: 6px;
|
||||
}
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-center;
|
||||
z-index: 50;
|
||||
}
|
||||
.loading-overlay-dark {
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
}
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--bg-secondary) 0%,
|
||||
var(--bg-tertiary) 50%,
|
||||
var(--bg-secondary) 100%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.skeleton-text {
|
||||
height: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.skeleton-heading {
|
||||
height: 2rem;
|
||||
width: 60%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.loading-dots {
|
||||
display: inline-flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.loading-dots span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--tractatus-core-end);
|
||||
animation: loading-dots 1.4s ease-in-out infinite;
|
||||
}
|
||||
.loading-dots span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
.loading-dots span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
@keyframes loading-dots {
|
||||
0%, 80%, 100% {
|
||||
opacity: 0.3;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
/* ========================================
|
||||
* CSP-COMPLIANT UTILITY CLASSES
|
||||
* Common styles extracted from inline attributes
|
||||
* ======================================== */
|
||||
.text-tractatus-link {
|
||||
color: var(--tractatus-core-end);
|
||||
}
|
||||
.text-service-boundary {
|
||||
color: var(--service-boundary-light);
|
||||
}
|
||||
.text-service-instruction {
|
||||
color: var(--service-instruction-light);
|
||||
}
|
||||
.text-service-validator {
|
||||
color: var(--service-validator-light);
|
||||
}
|
||||
.text-service-pressure {
|
||||
color: var(--service-pressure-light);
|
||||
}
|
||||
.text-service-metacognitive {
|
||||
color: var(--service-metacognitive-light);
|
||||
}
|
||||
.text-service-deliberation {
|
||||
color: var(--service-deliberation-light);
|
||||
}
|
||||
.border-l-tractatus {
|
||||
border-left-color: var(--tractatus-core-end);
|
||||
}
|
||||
.border-l-service-boundary {
|
||||
border-left-color: var(--service-boundary-light);
|
||||
}
|
||||
.border-l-service-instruction {
|
||||
border-left-color: var(--service-instruction-light);
|
||||
}
|
||||
.border-l-service-validator {
|
||||
border-left-color: var(--service-validator-light);
|
||||
}
|
||||
.border-l-service-pressure {
|
||||
border-left-color: var(--service-pressure-light);
|
||||
}
|
||||
.border-l-service-metacognitive {
|
||||
border-left-color: var(--service-metacognitive-light);
|
||||
}
|
||||
.border-l-service-deliberation {
|
||||
border-left-color: var(--service-deliberation-light);
|
||||
}
|
||||
.bg-gradient-tractatus {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 50%, #7e22ce 100%);
|
||||
}
|
||||
.bg-gradient-service-boundary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
.bg-gradient-service-instruction {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
||||
}
|
||||
.bg-gradient-service-validator {
|
||||
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%);
|
||||
}
|
||||
.bg-gradient-service-pressure {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
}
|
||||
.bg-gradient-service-metacognitive {
|
||||
background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
|
||||
}
|
||||
.bg-gradient-service-deliberation {
|
||||
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
|
||||
}
|
||||
.bg-gradient-cyan-blue {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
}
|
||||
.text-shadow-sm {
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||||
}
|
||||
.text-shadow-md {
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.badge-boundary {
|
||||
color: #065f46;
|
||||
background-color: #d1fae5;
|
||||
}
|
||||
.badge-instruction {
|
||||
color: #3730a3;
|
||||
background-color: #e0e7ff;
|
||||
}
|
||||
.badge-validator {
|
||||
color: #581c87;
|
||||
background-color: #f3e8ff;
|
||||
}
|
||||
.badge-pressure {
|
||||
color: #92400e;
|
||||
background-color: #fef3c7;
|
||||
}
|
||||
.badge-metacognitive {
|
||||
color: #831843;
|
||||
background-color: #fce7f3;
|
||||
}
|
||||
.badge-deliberation {
|
||||
color: #134e4a;
|
||||
background-color: #ccfbf1;
|
||||
}
|
||||
.min-h-16 {
|
||||
min-height: 64px;
|
||||
}
|
||||
.auth-error-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.auth-error-content {
|
||||
text-align: center;
|
||||
}
|
||||
.auth-error-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 0 auto 16px;
|
||||
color: #3B82F6;
|
||||
}
|
||||
.auth-error-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.auth-error-message {
|
||||
color: #6B7280;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.auth-error-redirect {
|
||||
color: #9CA3AF;
|
||||
font-size: 14px;
|
||||
}
|
||||
.coming-soon-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.coming-soon-card {
|
||||
background: white;
|
||||
padding: 3rem;
|
||||
border-radius: 1rem;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.coming-soon-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.coming-soon-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: #6b7280;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.coming-soon-info-box {
|
||||
background: #eff6ff;
|
||||
border-left: 4px solid #3b82f6;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
.coming-soon-info-title {
|
||||
color: #1e40af;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.coming-soon-info-text {
|
||||
color: #1e3a8a;
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
.coming-soon-status {
|
||||
color: #6b7280;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.coming-soon-button {
|
||||
display: inline-block;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.coming-soon-button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
.coming-soon-footer {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
.coming-soon-footer a {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
.coming-soon-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* ========================================
|
||||
* SCROLL ANIMATIONS
|
||||
* Intersection Observer-based scroll animations
|
||||
* ======================================== */
|
||||
.animate-on-scroll {
|
||||
transition: opacity 0.6s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.animate-on-scroll[data-animation="fade-in"] {
|
||||
opacity: 0;
|
||||
}
|
||||
.animate-on-scroll[data-animation="fade-in"].is-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-up"] {
|
||||
opacity: 0;
|
||||
transform: translateY(2rem);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-up"].is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-down"] {
|
||||
opacity: 0;
|
||||
transform: translateY(-2rem);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-down"].is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-left"] {
|
||||
opacity: 0;
|
||||
transform: translateX(2rem);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-left"].is-visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-right"] {
|
||||
opacity: 0;
|
||||
transform: translateX(-2rem);
|
||||
}
|
||||
.animate-on-scroll[data-animation="slide-right"].is-visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
.animate-on-scroll[data-animation="scale-in"] {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
.animate-on-scroll[data-animation="scale-in"].is-visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
.animate-on-scroll[data-animation="rotate-in"] {
|
||||
opacity: 0;
|
||||
transform: rotate(12deg) scale(0.95);
|
||||
}
|
||||
.animate-on-scroll[data-animation="rotate-in"].is-visible {
|
||||
opacity: 1;
|
||||
transform: rotate(0deg) scale(1);
|
||||
}
|
||||
.animate-on-scroll:not([data-animation]) {
|
||||
opacity: 0;
|
||||
transform: translateY(2rem);
|
||||
}
|
||||
.animate-on-scroll:not([data-animation]).is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-on-scroll {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
/* ========================================
|
||||
* PAGE TRANSITIONS
|
||||
* Smooth fade transitions between pages
|
||||
* ======================================== */
|
||||
body {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
body.page-fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
body.page-fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
/* ========================================
|
||||
* DATA VISUALIZATIONS
|
||||
* Pressure chart and timeline components
|
||||
* ======================================== */
|
||||
.gauge-fill-path {
|
||||
transition: stroke 0.3s ease;
|
||||
}
|
||||
.timeline-event {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.timeline-event:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gauge-fill-path,
|
||||
.timeline-event {
|
||||
transition: none !important;
|
||||
}
|
||||
.timeline-event:hover {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
/* ========================================
|
||||
* DARK MODE SUPPORT (Future)
|
||||
* Placeholder for dark mode implementation
|
||||
* ======================================== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
}
|
||||
/* ========================================
|
||||
* PRINT STYLES
|
||||
* Optimize for PDF/print output
|
||||
* ======================================== */
|
||||
@media print {
|
||||
* {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Simplified Tractatus icon for favicon -->
|
||||
<circle cx="24" cy="24" r="20" stroke="#3b82f6" stroke-width="2" opacity="0.3" fill="none"/>
|
||||
<circle cx="24" cy="24" r="14" stroke="#3b82f6" stroke-width="2" opacity="0.5" fill="none"/>
|
||||
<circle cx="24" cy="24" r="8" stroke="#3b82f6" stroke-width="2" opacity="0.7" fill="none"/>
|
||||
<circle cx="24" cy="24" r="5" fill="#3b82f6"/>
|
||||
<circle cx="38" cy="10" r="2.5" fill="#3b82f6" opacity="0.7"/>
|
||||
<circle cx="14" cy="34" r="2" fill="#3b82f6" opacity="0.8"/>
|
||||
<circle cx="32" cy="24" r="1.8" fill="#3b82f6" opacity="0.9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 655 B |
|
|
@ -1,10 +0,0 @@
|
|||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Simplified Tractatus icon for favicon -->
|
||||
<circle cx="24" cy="24" r="20" stroke="#3b82f6" stroke-width="2" opacity="0.3" fill="none"/>
|
||||
<circle cx="24" cy="24" r="14" stroke="#3b82f6" stroke-width="2" opacity="0.5" fill="none"/>
|
||||
<circle cx="24" cy="24" r="8" stroke="#3b82f6" stroke-width="2" opacity="0.7" fill="none"/>
|
||||
<circle cx="24" cy="24" r="5" fill="#3b82f6"/>
|
||||
<circle cx="38" cy="10" r="2.5" fill="#3b82f6" opacity="0.7"/>
|
||||
<circle cx="14" cy="34" r="2" fill="#3b82f6" opacity="0.8"/>
|
||||
<circle cx="32" cy="24" r="1.8" fill="#3b82f6" opacity="0.9"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 655 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 38 KiB |
|
|
@ -1,148 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="600" height="600" viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg" id="interactive-arch-diagram">
|
||||
<defs>
|
||||
<!-- Central core gradient (shared with Passport - cyan to blue) -->
|
||||
<radialGradient id="tractatusCoreInteractive">
|
||||
<stop offset="0%" style="stop-color:#64ffda;stop-opacity:1" />
|
||||
<stop offset="70%" style="stop-color:#448aff;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
||||
</radialGradient>
|
||||
|
||||
<!-- Service-specific gradients (6 governance services) -->
|
||||
<linearGradient id="serviceBoundaryInt" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#10b981;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#059669;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="serviceInstructionInt" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#6366f1;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#4f46e5;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="serviceValidatorInt" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#8b5cf6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#7c3aed;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="servicePressureInt" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#f59e0b;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#d97706;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="serviceMetacognitiveInt" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ec4899;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#db2777;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<linearGradient id="serviceDeliberationInt" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#14b8a6;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0d9488;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<filter id="dropShadowInt">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="6" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="600" height="600" fill="#f9fafb"/>
|
||||
|
||||
<!-- Orbital rings (subtle, static in interactive version) -->
|
||||
<circle cx="300" cy="300" r="255" stroke="#64ffda" stroke-width="1" opacity="0.15" fill="none"/>
|
||||
<circle cx="300" cy="300" r="210" stroke="#64ffda" stroke-width="1" opacity="0.25" fill="none"/>
|
||||
<circle cx="300" cy="300" r="165" stroke="#64ffda" stroke-width="1" opacity="0.35" fill="none"/>
|
||||
|
||||
<!-- Connection lines (will be made interactive) -->
|
||||
<g id="connection-lines">
|
||||
<line id="conn-boundary" x1="300" y1="300" x2="300" y2="105" stroke="#10b981" stroke-width="3" opacity="0.3" class="connection-line"/>
|
||||
<line id="conn-instruction" x1="300" y1="300" x2="468" y2="202.5" stroke="#6366f1" stroke-width="3" opacity="0.3" class="connection-line"/>
|
||||
<line id="conn-validator" x1="300" y1="300" x2="468" y2="397.5" stroke="#8b5cf6" stroke-width="3" opacity="0.3" class="connection-line"/>
|
||||
<line id="conn-pressure" x1="300" y1="300" x2="300" y2="495" stroke="#f59e0b" stroke-width="3" opacity="0.3" class="connection-line"/>
|
||||
<line id="conn-metacognitive" x1="300" y1="300" x2="132" y2="397.5" stroke="#ec4899" stroke-width="3" opacity="0.3" class="connection-line"/>
|
||||
<line id="conn-deliberation" x1="300" y1="300" x2="132" y2="202.5" stroke="#14b8a6" stroke-width="3" opacity="0.3" class="connection-line"/>
|
||||
</g>
|
||||
|
||||
<!-- Service nodes (clickable) -->
|
||||
<g id="service-nodes">
|
||||
<!-- 1. BoundaryEnforcer (top) - Green -->
|
||||
<g id="node-boundary" class="service-node" data-service="boundary" style="cursor: pointer;">
|
||||
<circle cx="300" cy="105" r="45" fill="url(#serviceBoundaryInt)" filter="url(#dropShadowInt)" opacity="0.95"/>
|
||||
<text x="300" y="115" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="white">B</text>
|
||||
<title>BoundaryEnforcer - Click for details</title>
|
||||
</g>
|
||||
|
||||
<!-- 2. InstructionPersistenceClassifier (top-right) - Indigo -->
|
||||
<g id="node-instruction" class="service-node" data-service="instruction" style="cursor: pointer;">
|
||||
<circle cx="468" cy="202.5" r="45" fill="url(#serviceInstructionInt)" filter="url(#dropShadowInt)" opacity="0.95"/>
|
||||
<text x="468" y="212.5" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="white">I</text>
|
||||
<title>InstructionPersistenceClassifier - Click for details</title>
|
||||
</g>
|
||||
|
||||
<!-- 3. CrossReferenceValidator (bottom-right) - Purple -->
|
||||
<g id="node-validator" class="service-node" data-service="validator" style="cursor: pointer;">
|
||||
<circle cx="468" cy="397.5" r="45" fill="url(#serviceValidatorInt)" filter="url(#dropShadowInt)" opacity="0.95"/>
|
||||
<text x="468" y="407.5" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="white">V</text>
|
||||
<title>CrossReferenceValidator - Click for details</title>
|
||||
</g>
|
||||
|
||||
<!-- 4. ContextPressureMonitor (bottom) - Amber -->
|
||||
<g id="node-pressure" class="service-node" data-service="pressure" style="cursor: pointer;">
|
||||
<circle cx="300" cy="495" r="45" fill="url(#servicePressureInt)" filter="url(#dropShadowInt)" opacity="0.95"/>
|
||||
<text x="300" y="505" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="white">P</text>
|
||||
<title>ContextPressureMonitor - Click for details</title>
|
||||
</g>
|
||||
|
||||
<!-- 5. MetacognitiveVerifier (bottom-left) - Rose -->
|
||||
<g id="node-metacognitive" class="service-node" data-service="metacognitive" style="cursor: pointer;">
|
||||
<circle cx="132" cy="397.5" r="45" fill="url(#serviceMetacognitiveInt)" filter="url(#dropShadowInt)" opacity="0.95"/>
|
||||
<text x="132" y="407.5" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="white">M</text>
|
||||
<title>MetacognitiveVerifier - Click for details</title>
|
||||
</g>
|
||||
|
||||
<!-- 6. PluralisticDeliberationOrchestrator (top-left) - Teal -->
|
||||
<g id="node-deliberation" class="service-node" data-service="deliberation" style="cursor: pointer;">
|
||||
<circle cx="132" cy="202.5" r="45" fill="url(#serviceDeliberationInt)" filter="url(#dropShadowInt)" opacity="0.95"/>
|
||||
<text x="132" y="212.5" text-anchor="middle" font-family="Arial, sans-serif" font-size="32" font-weight="bold" fill="white">D</text>
|
||||
<title>PluralisticDeliberationOrchestrator - Click for details</title>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Central core (clickable) -->
|
||||
<g id="central-core" class="service-node" data-service="overview" style="cursor: pointer;">
|
||||
<circle cx="300" cy="300" r="85" fill="url(#tractatusCoreInteractive)" filter="url(#dropShadowInt)"/>
|
||||
<circle cx="300" cy="300" r="68" fill="rgba(0,0,0,0.25)"/>
|
||||
<text x="300" y="320" text-anchor="middle" font-family="Arial, sans-serif" font-size="64" font-weight="bold" fill="white" opacity="0.95">T</text>
|
||||
<text x="300" y="345" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="white" opacity="0.8">Tractatus</text>
|
||||
<title>Tractatus Core - Click to see how all services work together</title>
|
||||
</g>
|
||||
|
||||
<style>
|
||||
.service-node:hover circle {
|
||||
filter: url(#dropShadowInt) url(#glow);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.service-node.active circle {
|
||||
stroke: white;
|
||||
stroke-width: 4;
|
||||
filter: url(#dropShadowInt) url(#glow);
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
transition: opacity 0.3s ease, stroke-width 0.3s ease;
|
||||
}
|
||||
|
||||
.connection-line.active {
|
||||
opacity: 0.8;
|
||||
stroke-width: 5;
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 886 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 33 KiB |
|
|
@ -1,135 +0,0 @@
|
|||
/**
|
||||
* Admin Authentication Check Utility
|
||||
* Protects admin pages by redirecting unauthenticated users to login
|
||||
*
|
||||
* Usage: Include at top of every admin page HTML:
|
||||
* <script src="/js/admin/auth-check.js"></script>
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Skip auth check on login page itself
|
||||
if (window.location.pathname === '/admin/login.html') {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has valid authentication token
|
||||
*/
|
||||
function checkAuthentication() {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
|
||||
// No token found - redirect to login
|
||||
if (!token) {
|
||||
redirectToLogin('No authentication token found');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse token to check expiration
|
||||
try {
|
||||
const payload = parseJWT(token);
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Token expired - redirect to login
|
||||
if (payload.exp && payload.exp < now) {
|
||||
localStorage.removeItem('admin_token');
|
||||
redirectToLogin('Session expired');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if admin role
|
||||
if (payload.role !== 'admin' && payload.role !== 'moderator') {
|
||||
redirectToLogin('Insufficient permissions');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Token valid
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Token validation error:', error);
|
||||
localStorage.removeItem('admin_token');
|
||||
redirectToLogin('Invalid authentication token');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JWT token without verification (client-side validation only)
|
||||
*/
|
||||
function parseJWT(token) {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid token format');
|
||||
}
|
||||
|
||||
const payload = parts[1];
|
||||
const decoded = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
return JSON.parse(decoded);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to login page with reason
|
||||
*/
|
||||
function redirectToLogin(reason) {
|
||||
const currentPath = encodeURIComponent(window.location.pathname + window.location.search);
|
||||
const loginUrl = `/admin/login.html?redirect=${currentPath}&reason=${encodeURIComponent(reason)}`;
|
||||
|
||||
// Show brief message before redirect
|
||||
document.body.innerHTML = `
|
||||
<div class="auth-error-container">
|
||||
<div class="auth-error-content">
|
||||
<svg class="auth-error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||||
</svg>
|
||||
<h2 class="auth-error-title">Authentication Required</h2>
|
||||
<p class="auth-error-message">${reason}</p>
|
||||
<p class="auth-error-redirect">Redirecting to login...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = loginUrl;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add authentication headers to fetch requests
|
||||
*/
|
||||
function getAuthHeaders() {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
return {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle API authentication errors
|
||||
*/
|
||||
function handleAuthError(response) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
localStorage.removeItem('admin_token');
|
||||
redirectToLogin('Session expired or invalid');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run authentication check immediately
|
||||
checkAuthentication();
|
||||
|
||||
// Export utilities for admin pages to use
|
||||
window.AdminAuth = {
|
||||
getAuthHeaders,
|
||||
handleAuthError,
|
||||
checkAuthentication,
|
||||
redirectToLogin
|
||||
};
|
||||
|
||||
// Periodically check token validity (every 5 minutes)
|
||||
setInterval(checkAuthentication, 5 * 60 * 1000);
|
||||
|
||||
})();
|
||||
|
|
@ -1,793 +0,0 @@
|
|||
// Auth check
|
||||
const token = localStorage.getItem('admin_token');
|
||||
const user = JSON.parse(localStorage.getItem('admin_user') || '{}');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
|
||||
// Display admin name
|
||||
document.getElementById('admin-name').textContent = user.email || 'Admin';
|
||||
|
||||
// Logout
|
||||
document.getElementById('logout-btn').addEventListener('click', () => {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
window.location.href = '/admin/login.html';
|
||||
});
|
||||
|
||||
// Navigation
|
||||
const navLinks = document.querySelectorAll('.nav-link');
|
||||
const sections = {
|
||||
'overview': document.getElementById('overview-section'),
|
||||
'moderation': document.getElementById('moderation-section'),
|
||||
'users': document.getElementById('users-section'),
|
||||
'documents': document.getElementById('documents-section')
|
||||
};
|
||||
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
const href = link.getAttribute('href');
|
||||
|
||||
// Only handle hash-based navigation (internal sections)
|
||||
// Let full URLs navigate normally
|
||||
if (!href || !href.startsWith('#')) {
|
||||
return; // Allow default navigation
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
const section = href.substring(1);
|
||||
|
||||
// Update active link
|
||||
navLinks.forEach(l => l.classList.remove('active', 'bg-blue-100', 'text-blue-700'));
|
||||
link.classList.add('active', 'bg-blue-100', 'text-blue-700');
|
||||
|
||||
// Show section
|
||||
Object.values(sections).forEach(s => s.classList.add('hidden'));
|
||||
if (sections[section]) {
|
||||
sections[section].classList.remove('hidden');
|
||||
loadSection(section);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API helper
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const response = await fetch(endpoint, {
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('admin_token');
|
||||
window.location.href = '/admin/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// Load statistics
|
||||
async function loadStatistics() {
|
||||
try {
|
||||
const response = await apiRequest('/api/admin/stats');
|
||||
|
||||
if (!response.success || !response.stats) {
|
||||
console.error('Invalid stats response:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = response.stats;
|
||||
|
||||
document.getElementById('stat-documents').textContent = stats.documents?.total || 0;
|
||||
document.getElementById('stat-pending').textContent = stats.moderation?.total_pending || 0;
|
||||
document.getElementById('stat-approved').textContent = stats.blog?.published || 0;
|
||||
document.getElementById('stat-users').textContent = stats.users?.total || 0;
|
||||
} catch (error) {
|
||||
console.error('Failed to load statistics:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load sync health status
|
||||
async function loadSyncHealth() {
|
||||
const statusEl = document.getElementById('sync-status');
|
||||
const badgeEl = document.getElementById('sync-badge');
|
||||
const detailsEl = document.getElementById('sync-details');
|
||||
const iconContainerEl = document.getElementById('sync-icon-container');
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/api/admin/sync/health');
|
||||
|
||||
if (!response.success || !response.health) {
|
||||
console.error('Invalid sync health response:', response);
|
||||
statusEl.textContent = 'Error';
|
||||
badgeEl.textContent = 'Error';
|
||||
badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800';
|
||||
detailsEl.textContent = 'Failed to check sync health';
|
||||
iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3';
|
||||
return;
|
||||
}
|
||||
|
||||
const health = response.health;
|
||||
const counts = health.counts;
|
||||
|
||||
// Update status text
|
||||
statusEl.textContent = `File: ${counts.file} | DB: ${counts.database}`;
|
||||
|
||||
// Update badge and icon based on severity
|
||||
if (health.severity === 'success') {
|
||||
badgeEl.textContent = '✓ Synced';
|
||||
badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-green-100 text-green-800';
|
||||
iconContainerEl.className = 'flex-shrink-0 bg-green-100 rounded-md p-3';
|
||||
iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-yellow-600', 'text-red-600');
|
||||
iconContainerEl.querySelector('svg').classList.add('text-green-600');
|
||||
} else if (health.severity === 'warning') {
|
||||
badgeEl.textContent = '⚠ Desync';
|
||||
badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800';
|
||||
iconContainerEl.className = 'flex-shrink-0 bg-yellow-100 rounded-md p-3';
|
||||
iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-green-600', 'text-red-600');
|
||||
iconContainerEl.querySelector('svg').classList.add('text-yellow-600');
|
||||
} else {
|
||||
badgeEl.textContent = '✗ Critical';
|
||||
badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800';
|
||||
iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3';
|
||||
iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-green-600', 'text-yellow-600');
|
||||
iconContainerEl.querySelector('svg').classList.add('text-red-600');
|
||||
}
|
||||
|
||||
// Update details
|
||||
if (counts.difference === 0) {
|
||||
detailsEl.textContent = health.message;
|
||||
} else {
|
||||
const missing = health.details?.missingInDatabase?.length || 0;
|
||||
const orphaned = health.details?.orphanedInDatabase?.length || 0;
|
||||
detailsEl.textContent = `${health.message} (Missing: ${missing}, Orphaned: ${orphaned})`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load sync health:', error);
|
||||
statusEl.textContent = 'Error';
|
||||
badgeEl.textContent = 'Error';
|
||||
badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800';
|
||||
detailsEl.textContent = 'Failed to check sync health';
|
||||
iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3';
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger manual sync
|
||||
async function triggerSync() {
|
||||
const button = document.getElementById('sync-trigger-btn');
|
||||
const originalText = button.textContent;
|
||||
|
||||
try {
|
||||
// Disable button and show loading state
|
||||
button.disabled = true;
|
||||
button.textContent = 'Syncing...';
|
||||
|
||||
const response = await apiRequest('/api/admin/sync/trigger', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
// Show success message
|
||||
button.textContent = '✓ Synced';
|
||||
button.classList.remove('bg-blue-600', 'hover:bg-blue-700');
|
||||
button.classList.add('bg-green-600');
|
||||
|
||||
// Reload health status and stats
|
||||
await loadSyncHealth();
|
||||
await loadStatistics();
|
||||
|
||||
// Reset button after 2 seconds
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('bg-green-600');
|
||||
button.classList.add('bg-blue-600', 'hover:bg-blue-700');
|
||||
button.disabled = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
throw new Error(response.message || 'Sync failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Manual sync error:', error);
|
||||
button.textContent = '✗ Failed';
|
||||
button.classList.remove('bg-blue-600', 'hover:bg-blue-700');
|
||||
button.classList.add('bg-red-600');
|
||||
|
||||
// Reset button after 2 seconds
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('bg-red-600');
|
||||
button.classList.add('bg-blue-600', 'hover:bg-blue-700');
|
||||
button.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// Load recent activity
|
||||
async function loadRecentActivity() {
|
||||
const container = document.getElementById('recent-activity');
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/api/admin/activity');
|
||||
|
||||
if (!response.success || !response.activity || response.activity.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500">No recent activity</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = response.activity.map(item => {
|
||||
// Generate description from activity data
|
||||
const action = item.action || 'reviewed';
|
||||
const itemType = item.item_type || 'item';
|
||||
const description = `${action.charAt(0).toUpperCase() + action.slice(1)} ${itemType}`;
|
||||
|
||||
return `
|
||||
<div class="py-4 flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="h-8 w-8 rounded-full ${getActivityColor(action)} flex items-center justify-center">
|
||||
<span class="text-xs font-medium text-white">${getActivityIcon(action)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900">${description}</p>
|
||||
<p class="text-sm text-gray-500">${formatDate(item.timestamp)}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (error) {
|
||||
console.error('Failed to load activity:', error);
|
||||
container.innerHTML = '<div class="text-center py-8 text-red-500">Failed to load activity</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load moderation queue
|
||||
async function loadModerationQueue(filter = 'all') {
|
||||
const container = document.getElementById('moderation-queue');
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/moderation?type=${filter}`);
|
||||
|
||||
if (!response.success || !response.items || response.items.length === 0) {
|
||||
container.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">No items pending review</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = response.items.map(item => `
|
||||
<div class="px-6 py-4" data-id="${item._id}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
${item.type}
|
||||
</span>
|
||||
<span class="ml-2 text-sm text-gray-500">${formatDate(item.submitted_at)}</span>
|
||||
</div>
|
||||
<h4 class="mt-2 text-sm font-medium text-gray-900">${item.title}</h4>
|
||||
<p class="mt-1 text-sm text-gray-600">${truncate(item.content || item.description, 150)}</p>
|
||||
</div>
|
||||
<div class="ml-4 flex-shrink-0 flex space-x-2">
|
||||
<button data-action="approveItem" data-arg0="${item._id}" class="bg-green-600 text-white px-3 py-1 rounded text-sm hover:bg-green-700">
|
||||
Approve
|
||||
</button>
|
||||
<button data-action="rejectItem" data-arg0="${item._id}" class="bg-red-600 text-white px-3 py-1 rounded text-sm hover:bg-red-700">
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Failed to load moderation queue:', error);
|
||||
container.innerHTML = '<div class="px-6 py-8 text-center text-red-500">Failed to load queue</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load users
|
||||
async function loadUsers() {
|
||||
const container = document.getElementById('users-list');
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/api/admin/users');
|
||||
|
||||
if (!response.success || !response.users || response.users.length === 0) {
|
||||
container.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">No users found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = response.users.map(user => `
|
||||
<div class="px-6 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<span class="text-sm font-medium text-gray-600">${user.email.charAt(0).toUpperCase()}</span>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-900">${user.email}</p>
|
||||
<p class="text-sm text-gray-500">Role: ${user.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${user.role === 'admin' ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-800'}">
|
||||
${user.role}
|
||||
</span>
|
||||
${user._id !== user._id ? `
|
||||
<button data-action="deleteUser" data-arg0="${user._id}" class="text-red-600 hover:text-red-900 text-sm">
|
||||
Delete
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
container.innerHTML = '<div class="px-6 py-8 text-center text-red-500">Failed to load users</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load documents
|
||||
async function loadDocuments() {
|
||||
const container = document.getElementById('documents-list');
|
||||
|
||||
try {
|
||||
const response = await apiRequest('/api/documents');
|
||||
|
||||
if (!response.success || !response.documents || response.documents.length === 0) {
|
||||
container.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">No documents found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = response.documents.map(doc => {
|
||||
const visibilityBadge = getVisibilityBadge(doc.visibility || 'internal');
|
||||
const statusBadge = getStatusBadge(doc.workflow_status || 'draft');
|
||||
const canPublish = doc.visibility === 'internal' && doc.workflow_status !== 'published';
|
||||
const canUnpublish = doc.visibility === 'public' && doc.workflow_status === 'published';
|
||||
|
||||
return `
|
||||
<div class="px-6 py-4 flex items-center justify-between border-b border-gray-100">
|
||||
<div class="flex-1">
|
||||
<h4 class="text-sm font-medium text-gray-900">${doc.title}</h4>
|
||||
<div class="flex items-center space-x-3 mt-1">
|
||||
<p class="text-sm text-gray-500">${doc.quadrant || 'No quadrant'}</p>
|
||||
${statusBadge}
|
||||
${visibilityBadge}
|
||||
${doc.category ? `<span class="text-xs text-gray-400">Category: ${doc.category}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="/docs-viewer.html#${doc.slug}" target="_blank" class="text-blue-600 hover:text-blue-900 text-sm">
|
||||
View
|
||||
</a>
|
||||
${canPublish ? `
|
||||
<button data-action="openPublishModal" data-arg0="${doc._id}" class="px-3 py-1 bg-green-600 text-white text-sm rounded hover:bg-green-700">
|
||||
Publish
|
||||
</button>
|
||||
` : ''}
|
||||
${canUnpublish ? `
|
||||
<button data-action="openUnpublishModal" data-arg0="${doc._id}" class="px-3 py-1 bg-yellow-600 text-white text-sm rounded hover:bg-yellow-700">
|
||||
Unpublish
|
||||
</button>
|
||||
` : ''}
|
||||
<button data-action="deleteDocument" data-arg0="${doc._id}" class="text-red-600 hover:text-red-900 text-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (error) {
|
||||
console.error('Failed to load documents:', error);
|
||||
container.innerHTML = '<div class="px-6 py-8 text-center text-red-500">Failed to load documents</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load section data
|
||||
function loadSection(section) {
|
||||
switch (section) {
|
||||
case 'overview':
|
||||
loadStatistics();
|
||||
loadRecentActivity();
|
||||
break;
|
||||
case 'moderation':
|
||||
loadModerationQueue();
|
||||
break;
|
||||
case 'users':
|
||||
loadUsers();
|
||||
break;
|
||||
case 'documents':
|
||||
loadDocuments();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Approve item
|
||||
async function approveItem(itemId) {
|
||||
if (!confirm('Approve this item?')) return;
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/moderation/${itemId}/approve`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
loadModerationQueue();
|
||||
loadStatistics();
|
||||
} else {
|
||||
alert('Failed to approve item');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Approval error:', error);
|
||||
alert('Failed to approve item');
|
||||
}
|
||||
}
|
||||
|
||||
// Reject item
|
||||
async function rejectItem(itemId) {
|
||||
if (!confirm('Reject this item?')) return;
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/moderation/${itemId}/reject`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
loadModerationQueue();
|
||||
loadStatistics();
|
||||
} else {
|
||||
alert('Failed to reject item');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Rejection error:', error);
|
||||
alert('Failed to reject item');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete user
|
||||
async function deleteUser(userId) {
|
||||
if (!confirm('Delete this user? This action cannot be undone.')) return;
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/users/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
loadUsers();
|
||||
loadStatistics();
|
||||
} else {
|
||||
alert(response.message || 'Failed to delete user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
alert('Failed to delete user');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete document
|
||||
async function deleteDocument(docId) {
|
||||
if (!confirm('Delete this document? This action cannot be undone.')) return;
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/documents/${docId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
loadDocuments();
|
||||
loadStatistics();
|
||||
} else {
|
||||
alert('Failed to delete document');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
alert('Failed to delete document');
|
||||
}
|
||||
}
|
||||
|
||||
// Open publish modal
|
||||
async function openPublishModal(docId) {
|
||||
try {
|
||||
const response = await apiRequest(`/api/documents/${docId}`);
|
||||
if (!response.success || !response.document) {
|
||||
alert('Failed to load document');
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = response.document;
|
||||
const modalHTML = `
|
||||
<div id="publish-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4">Publish Document</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<p class="text-sm text-gray-600 mb-2"><strong>Title:</strong> ${doc.title}</p>
|
||||
<p class="text-sm text-gray-600"><strong>Current Status:</strong> ${doc.workflow_status || 'draft'}</p>
|
||||
</div>
|
||||
|
||||
<form id="publish-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Category <span class="text-red-600">*</span>
|
||||
</label>
|
||||
<select id="publish-category" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
||||
<option value="">Select category...</option>
|
||||
<option value="getting-started">Getting Started</option>
|
||||
<option value="technical-reference">Technical Reference</option>
|
||||
<option value="research-theory">Research & Theory</option>
|
||||
<option value="advanced-topics">Advanced Topics</option>
|
||||
<option value="case-studies">Case Studies</option>
|
||||
<option value="business-leadership">Business & Leadership</option>
|
||||
<option value="archives">Archives</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Display Order (optional)
|
||||
</label>
|
||||
<input type="number" id="publish-order" value="${doc.order || 0}" min="0"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="text-xs text-gray-500 mt-1">Higher numbers appear first</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" data-action="closePublishModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700">
|
||||
Publish Document
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
|
||||
// Store doc ID for later
|
||||
document.getElementById('publish-form').dataset.docId = docId;
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('publish-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const category = document.getElementById('publish-category').value;
|
||||
const order = parseInt(document.getElementById('publish-order').value) || 0;
|
||||
|
||||
await publishDocument(docId, category, order);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to open publish modal:', error);
|
||||
alert('Failed to open publish modal');
|
||||
}
|
||||
}
|
||||
|
||||
// Publish document
|
||||
async function publishDocument(docId, category, order) {
|
||||
try {
|
||||
const response = await apiRequest(`/api/documents/${docId}/publish`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ category, order })
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
closePublishModal();
|
||||
loadDocuments();
|
||||
loadStatistics();
|
||||
alert('Document published successfully');
|
||||
} else {
|
||||
alert(response.message || 'Failed to publish document');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Publish error:', error);
|
||||
alert('Failed to publish document');
|
||||
}
|
||||
}
|
||||
|
||||
// Close publish modal
|
||||
function closePublishModal() {
|
||||
document.getElementById('publish-modal')?.remove();
|
||||
}
|
||||
|
||||
// Open unpublish modal
|
||||
async function openUnpublishModal(docId) {
|
||||
try {
|
||||
const response = await apiRequest(`/api/documents/${docId}`);
|
||||
if (!response.success || !response.document) {
|
||||
alert('Failed to load document');
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = response.document;
|
||||
const modalHTML = `
|
||||
<div id="unpublish-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
|
||||
<h2 class="text-xl font-bold text-gray-900 mb-4">Unpublish Document</h2>
|
||||
|
||||
<div class="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<p class="text-sm text-yellow-800 mb-2"><strong>Title:</strong> ${doc.title}</p>
|
||||
<p class="text-sm text-yellow-800 mb-2"><strong>Current Visibility:</strong> ${doc.visibility}</p>
|
||||
<p class="text-sm text-yellow-800"><strong>Category:</strong> ${doc.category || 'None'}</p>
|
||||
</div>
|
||||
|
||||
<form id="unpublish-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Reason for unpublishing <span class="text-red-600">*</span>
|
||||
</label>
|
||||
<textarea id="unpublish-reason" required rows="3"
|
||||
placeholder="Explain why this document is being unpublished..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">This will be recorded in the audit trail</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 mt-6">
|
||||
<button type="button" data-action="closeUnpublishModal" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700">
|
||||
Unpublish Document
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', modalHTML);
|
||||
|
||||
// Store doc ID for later
|
||||
document.getElementById('unpublish-form').dataset.docId = docId;
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('unpublish-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const reason = document.getElementById('unpublish-reason').value;
|
||||
|
||||
await unpublishDocument(docId, reason);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to open unpublish modal:', error);
|
||||
alert('Failed to open unpublish modal');
|
||||
}
|
||||
}
|
||||
|
||||
// Unpublish document
|
||||
async function unpublishDocument(docId, reason) {
|
||||
try {
|
||||
const response = await apiRequest(`/api/documents/${docId}/unpublish`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ reason })
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
closeUnpublishModal();
|
||||
loadDocuments();
|
||||
loadStatistics();
|
||||
alert('Document unpublished successfully');
|
||||
} else {
|
||||
alert(response.message || 'Failed to unpublish document');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Unpublish error:', error);
|
||||
alert('Failed to unpublish document');
|
||||
}
|
||||
}
|
||||
|
||||
// Close unpublish modal
|
||||
function closeUnpublishModal() {
|
||||
document.getElementById('unpublish-modal')?.remove();
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function getVisibilityBadge(visibility) {
|
||||
const badges = {
|
||||
'public': '<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">Public</span>',
|
||||
'internal': '<span class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">Internal</span>',
|
||||
'confidential': '<span class="px-2 py-1 text-xs rounded-full bg-red-100 text-red-800">Confidential</span>',
|
||||
'archived': '<span class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800">Archived</span>'
|
||||
};
|
||||
return badges[visibility] || badges['internal'];
|
||||
}
|
||||
|
||||
function getStatusBadge(status) {
|
||||
const badges = {
|
||||
'draft': '<span class="px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800">Draft</span>',
|
||||
'review': '<span class="px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-800">Review</span>',
|
||||
'published': '<span class="px-2 py-1 text-xs rounded-full bg-green-100 text-green-800">Published</span>'
|
||||
};
|
||||
return badges[status] || badges['draft'];
|
||||
}
|
||||
|
||||
function getActivityColor(type) {
|
||||
const colors = {
|
||||
'create': 'bg-green-500',
|
||||
'update': 'bg-blue-500',
|
||||
'delete': 'bg-red-500',
|
||||
'approve': 'bg-purple-500'
|
||||
};
|
||||
return colors[type] || 'bg-gray-500';
|
||||
}
|
||||
|
||||
function getActivityIcon(type) {
|
||||
const icons = {
|
||||
'create': '+',
|
||||
'update': '↻',
|
||||
'delete': '×',
|
||||
'approve': '✓'
|
||||
};
|
||||
return icons[type] || '•';
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'Unknown';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
function truncate(str, length) {
|
||||
if (!str) return '';
|
||||
return str.length > length ? str.substring(0, length) + '...' : str;
|
||||
}
|
||||
|
||||
// Queue filter
|
||||
document.getElementById('queue-filter')?.addEventListener('change', (e) => {
|
||||
loadModerationQueue(e.target.value);
|
||||
});
|
||||
|
||||
// Initialize
|
||||
loadStatistics();
|
||||
loadRecentActivity();
|
||||
loadSyncHealth();
|
||||
|
||||
// Auto-refresh sync health every 60 seconds
|
||||
setInterval(() => {
|
||||
loadSyncHealth();
|
||||
}, 60000);
|
||||
|
||||
// Event delegation for data-action buttons (CSP compliance)
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const arg0 = button.dataset.arg0;
|
||||
|
||||
switch (action) {
|
||||
case 'approveItem':
|
||||
approveItem(arg0);
|
||||
break;
|
||||
case 'rejectItem':
|
||||
rejectItem(arg0);
|
||||
break;
|
||||
case 'deleteUser':
|
||||
deleteUser(arg0);
|
||||
break;
|
||||
case 'deleteDocument':
|
||||
deleteDocument(arg0);
|
||||
break;
|
||||
case 'openPublishModal':
|
||||
openPublishModal(arg0);
|
||||
break;
|
||||
case 'openUnpublishModal':
|
||||
openUnpublishModal(arg0);
|
||||
break;
|
||||
case 'closePublishModal':
|
||||
closePublishModal();
|
||||
break;
|
||||
case 'closeUnpublishModal':
|
||||
closeUnpublishModal();
|
||||
break;
|
||||
case 'triggerSync':
|
||||
triggerSync();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
/**
|
||||
* Hooks Dashboard
|
||||
* Real-time monitoring of framework enforcement hooks
|
||||
*/
|
||||
|
||||
const API_BASE = window.location.hostname === 'localhost' ? 'http://localhost:9000/api' : '/api';
|
||||
|
||||
// Load metrics on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadMetrics();
|
||||
|
||||
// Auto-refresh every 30 seconds
|
||||
setInterval(loadMetrics, 30000);
|
||||
|
||||
// Manual refresh button
|
||||
document.getElementById('refresh-btn')?.addEventListener('click', loadMetrics);
|
||||
});
|
||||
|
||||
/**
|
||||
* Load hook metrics
|
||||
*/
|
||||
async function loadMetrics() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/admin/hooks/metrics`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('admin_token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load metrics');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
displayMetrics(data);
|
||||
} catch (error) {
|
||||
console.error('Error loading metrics:', error);
|
||||
showError('Failed to load hook metrics');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display metrics in UI
|
||||
*/
|
||||
function displayMetrics(data) {
|
||||
const metrics = data.metrics || {};
|
||||
const executions = metrics.hook_executions || [];
|
||||
const blocks = metrics.blocks || [];
|
||||
const stats = metrics.session_stats || {};
|
||||
|
||||
// Calculate totals
|
||||
const totalExecutions = executions.length;
|
||||
const totalBlocks = blocks.length;
|
||||
const blockRate = totalExecutions > 0 ? ((totalBlocks / totalExecutions) * 100).toFixed(1) + '%' : '0%';
|
||||
|
||||
// Update quick stats
|
||||
document.getElementById('stat-total-executions').textContent = totalExecutions.toLocaleString();
|
||||
document.getElementById('stat-total-blocks').textContent = totalBlocks.toLocaleString();
|
||||
document.getElementById('stat-block-rate').textContent = blockRate;
|
||||
|
||||
// Last updated
|
||||
const lastUpdated = stats.last_updated ? formatRelativeTime(new Date(stats.last_updated)) : 'Never';
|
||||
document.getElementById('stat-last-updated').textContent = lastUpdated;
|
||||
|
||||
// Hook breakdown
|
||||
const editExecutions = executions.filter(e => e.hook === 'validate-file-edit').length;
|
||||
const editBlocks = blocks.filter(b => b.hook === 'validate-file-edit').length;
|
||||
const editSuccessRate = editExecutions > 0 ? (((editExecutions - editBlocks) / editExecutions) * 100).toFixed(1) + '%' : '100%';
|
||||
|
||||
document.getElementById('edit-executions').textContent = editExecutions.toLocaleString();
|
||||
document.getElementById('edit-blocks').textContent = editBlocks.toLocaleString();
|
||||
document.getElementById('edit-success-rate').textContent = editSuccessRate;
|
||||
|
||||
const writeExecutions = executions.filter(e => e.hook === 'validate-file-write').length;
|
||||
const writeBlocks = blocks.filter(b => b.hook === 'validate-file-write').length;
|
||||
const writeSuccessRate = writeExecutions > 0 ? (((writeExecutions - writeBlocks) / writeExecutions) * 100).toFixed(1) + '%' : '100%';
|
||||
|
||||
document.getElementById('write-executions').textContent = writeExecutions.toLocaleString();
|
||||
document.getElementById('write-blocks').textContent = writeBlocks.toLocaleString();
|
||||
document.getElementById('write-success-rate').textContent = writeSuccessRate;
|
||||
|
||||
// Recent blocks
|
||||
displayRecentBlocks(blocks.slice(-10).reverse());
|
||||
|
||||
// Recent activity
|
||||
displayRecentActivity(executions.slice(-20).reverse());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display recent blocked operations
|
||||
*/
|
||||
function displayRecentBlocks(blocks) {
|
||||
const container = document.getElementById('recent-blocks');
|
||||
|
||||
if (blocks.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500">No blocked operations</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = blocks.map(block => `
|
||||
<div class="py-4 border-b border-gray-100 last:border-0">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 p-2 bg-red-100 rounded-lg">
|
||||
<svg class="h-5 w-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="text-xs font-semibold px-2 py-0.5 bg-red-100 text-red-700 rounded">${block.hook.replace('validate-file-', '')}</span>
|
||||
<span class="text-xs text-gray-500">${formatRelativeTime(new Date(block.timestamp))}</span>
|
||||
</div>
|
||||
<p class="text-sm font-medium text-gray-900 truncate">${escapeHtml(block.file)}</p>
|
||||
<p class="text-sm text-red-600 mt-1">${escapeHtml(block.reason)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display recent hook executions
|
||||
*/
|
||||
function displayRecentActivity(executions) {
|
||||
const container = document.getElementById('recent-activity');
|
||||
|
||||
if (executions.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500">No recent activity</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const html = executions.map(exec => `
|
||||
<div class="py-3 border-b border-gray-100 last:border-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0">
|
||||
${exec.result === 'passed' ? `
|
||||
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
` : `
|
||||
<svg class="h-5 w-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
`}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold px-2 py-0.5 bg-gray-100 text-gray-700 rounded">${exec.hook.replace('validate-file-', '')}</span>
|
||||
<span class="text-xs text-gray-500">${formatRelativeTime(new Date(exec.timestamp))}</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-900 truncate mt-1">${escapeHtml(exec.file)}</p>
|
||||
${exec.reason ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(exec.reason)}</p>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format relative time
|
||||
*/
|
||||
function formatRelativeTime(date) {
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffSec = Math.floor(diffMs / 1000);
|
||||
const diffMin = Math.floor(diffSec / 60);
|
||||
const diffHour = Math.floor(diffMin / 60);
|
||||
const diffDay = Math.floor(diffHour / 24);
|
||||
|
||||
if (diffSec < 60) {
|
||||
return 'Just now';
|
||||
} else if (diffMin < 60) {
|
||||
return `${diffMin}m ago`;
|
||||
} else if (diffHour < 24) {
|
||||
return `${diffHour}h ago`;
|
||||
} else if (diffDay < 7) {
|
||||
return `${diffDay}d ago`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
function showError(message) {
|
||||
const container = document.getElementById('recent-activity');
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8">
|
||||
<p class="text-red-600">${escapeHtml(message)}</p>
|
||||
<button id="retry-load-btn" class="mt-4 text-sm text-blue-600 hover:text-blue-700">
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listener to retry button
|
||||
document.getElementById('retry-load-btn')?.addEventListener('click', loadMetrics);
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
const loginForm = document.getElementById('login-form');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const errorText = document.getElementById('error-text');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
|
||||
loginForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
|
||||
// Hide previous errors
|
||||
errorMessage.classList.add('hidden');
|
||||
|
||||
// Disable button
|
||||
loginBtn.disabled = true;
|
||||
loginBtn.innerHTML = '<span>Signing in...</span>';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.success) {
|
||||
// Store token
|
||||
localStorage.setItem('admin_token', data.accessToken);
|
||||
localStorage.setItem('admin_user', JSON.stringify(data.user));
|
||||
|
||||
// Redirect to dashboard
|
||||
window.location.href = '/admin/dashboard.html';
|
||||
} else {
|
||||
// Show error
|
||||
showError(data.message || 'Invalid credentials');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.innerHTML = 'Sign in';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
showError('Network error. Please try again.');
|
||||
loginBtn.disabled = false;
|
||||
loginBtn.innerHTML = 'Sign in';
|
||||
}
|
||||
});
|
||||
|
||||
function showError(message) {
|
||||
errorText.textContent = message;
|
||||
errorMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Auto-fill for development (optional)
|
||||
if (window.location.hostname === 'localhost') {
|
||||
document.getElementById('email').value = 'admin@agenticgovernance.digital';
|
||||
}
|
||||
|
|
@ -1,783 +0,0 @@
|
|||
/**
|
||||
* Project Editor Modal
|
||||
* Handles creation, editing, viewing, and variable management for projects
|
||||
*
|
||||
* @class ProjectEditor
|
||||
*/
|
||||
|
||||
class ProjectEditor {
|
||||
constructor() {
|
||||
this.mode = 'create'; // 'create', 'edit', 'view', 'variables'
|
||||
this.projectId = null;
|
||||
this.originalProject = null;
|
||||
this.variables = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Open editor in create mode
|
||||
*/
|
||||
openCreate() {
|
||||
this.mode = 'create';
|
||||
this.projectId = null;
|
||||
this.originalProject = null;
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open editor in edit mode
|
||||
*/
|
||||
async openEdit(projectId) {
|
||||
this.mode = 'edit';
|
||||
this.projectId = projectId;
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/projects/${projectId}`);
|
||||
|
||||
if (!response.success || !response.project) {
|
||||
throw new Error('Failed to load project');
|
||||
}
|
||||
|
||||
this.originalProject = response.project;
|
||||
this.variables = response.variables || [];
|
||||
this.render();
|
||||
this.populateForm(response.project);
|
||||
this.attachEventListeners();
|
||||
} catch (error) {
|
||||
console.error('Failed to load project:', error);
|
||||
showToast('Failed to load project for editing', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open editor in view mode (read-only)
|
||||
*/
|
||||
async openView(projectId) {
|
||||
this.mode = 'view';
|
||||
this.projectId = projectId;
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/projects/${projectId}`);
|
||||
|
||||
if (!response.success || !response.project) {
|
||||
throw new Error('Failed to load project');
|
||||
}
|
||||
|
||||
this.originalProject = response.project;
|
||||
this.variables = response.variables || [];
|
||||
this.renderViewMode(response.project);
|
||||
} catch (error) {
|
||||
console.error('Failed to load project:', error);
|
||||
showToast('Failed to load project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open variables management mode
|
||||
*/
|
||||
async openVariables(projectId) {
|
||||
this.mode = 'variables';
|
||||
this.projectId = projectId;
|
||||
|
||||
try {
|
||||
const [projectResponse, variablesResponse] = await Promise.all([
|
||||
apiRequest(`/api/admin/projects/${projectId}`),
|
||||
apiRequest(`/api/admin/projects/${projectId}/variables`)
|
||||
]);
|
||||
|
||||
if (!projectResponse.success || !projectResponse.project) {
|
||||
throw new Error('Failed to load project');
|
||||
}
|
||||
|
||||
this.originalProject = projectResponse.project;
|
||||
this.variables = variablesResponse.variables || [];
|
||||
this.renderVariablesMode();
|
||||
} catch (error) {
|
||||
console.error('Failed to load project variables:', error);
|
||||
showToast('Failed to load variables', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the editor modal
|
||||
*/
|
||||
render() {
|
||||
const container = document.getElementById('modal-container');
|
||||
const title = this.mode === 'create' ? 'Create New Project' : 'Edit Project';
|
||||
|
||||
container.innerHTML = `
|
||||
<div id="project-editor-modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">${title}</h3>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<form id="project-form">
|
||||
<div class="space-y-6">
|
||||
<!-- Project ID -->
|
||||
<div>
|
||||
<label for="project-id" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Project ID <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="project-id"
|
||||
name="id"
|
||||
placeholder="e.g., my-project, family-history"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
${this.mode === 'edit' ? 'disabled' : 'required'}
|
||||
>
|
||||
<p class="mt-1 text-xs text-gray-500">Lowercase slug format (letters, numbers, hyphens only)</p>
|
||||
</div>
|
||||
|
||||
<!-- Project Name -->
|
||||
<div>
|
||||
<label for="project-name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Project Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="project-name"
|
||||
name="name"
|
||||
placeholder="e.g., Family History Archive"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="project-description" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
id="project-description"
|
||||
name="description"
|
||||
rows="3"
|
||||
placeholder="Brief description of the project..."
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Tech Stack -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="tech-framework" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Framework
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="tech-framework"
|
||||
placeholder="e.g., Express.js"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tech-database" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Database
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="tech-database"
|
||||
placeholder="e.g., MongoDB"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tech-frontend" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Frontend
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="tech-frontend"
|
||||
placeholder="e.g., React"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repository URL -->
|
||||
<div>
|
||||
<label for="repo-url" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Repository URL
|
||||
</label>
|
||||
<input
|
||||
type="url"
|
||||
id="repo-url"
|
||||
name="repositoryUrl"
|
||||
placeholder="https://github.com/user/repo"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- Active Status -->
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="project-active"
|
||||
name="active"
|
||||
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded"
|
||||
checked
|
||||
>
|
||||
<label for="project-active" class="ml-2 block text-sm text-gray-900">
|
||||
Active
|
||||
</label>
|
||||
<p class="ml-2 text-xs text-gray-500">(Inactive projects are hidden from rule rendering)</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
|
||||
<button id="cancel-btn" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="save-btn" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700">
|
||||
${this.mode === 'create' ? 'Create Project' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render view mode (read-only)
|
||||
*/
|
||||
renderViewMode(project) {
|
||||
const container = document.getElementById('modal-container');
|
||||
|
||||
const techStack = project.techStack || {};
|
||||
const metadata = project.metadata || {};
|
||||
|
||||
container.innerHTML = `
|
||||
<div id="project-editor-modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">${escapeHtml(project.name)}</h3>
|
||||
<p class="text-sm text-gray-500 font-mono mt-1">${escapeHtml(project.id)}</p>
|
||||
</div>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="space-y-6">
|
||||
<!-- Status Badge -->
|
||||
<div>
|
||||
${project.active
|
||||
? '<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">Active</span>'
|
||||
: '<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-800">Inactive</span>'
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
${project.description ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Description</h4>
|
||||
<p class="text-sm text-gray-900">${escapeHtml(project.description)}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Tech Stack -->
|
||||
${Object.keys(techStack).length > 0 ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Tech Stack</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
${techStack.framework ? `<div class="text-sm"><span class="font-medium">Framework:</span> ${escapeHtml(techStack.framework)}</div>` : ''}
|
||||
${techStack.database ? `<div class="text-sm"><span class="font-medium">Database:</span> ${escapeHtml(techStack.database)}</div>` : ''}
|
||||
${techStack.frontend ? `<div class="text-sm"><span class="font-medium">Frontend:</span> ${escapeHtml(techStack.frontend)}</div>` : ''}
|
||||
${techStack.css ? `<div class="text-sm"><span class="font-medium">CSS:</span> ${escapeHtml(techStack.css)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Repository -->
|
||||
${project.repositoryUrl ? `
|
||||
<div>
|
||||
<h4 class="text-sm font-medium text-gray-700 mb-2">Repository</h4>
|
||||
<a href="${escapeHtml(project.repositoryUrl)}" target="_blank" class="text-sm text-indigo-600 hover:text-indigo-700">
|
||||
${escapeHtml(project.repositoryUrl)}
|
||||
</a>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Variables -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h4 class="text-sm font-medium text-gray-700">Variables (${this.variables.length})</h4>
|
||||
<button data-action="openVariables" data-arg0="${project.id}" class="text-sm text-indigo-600 hover:text-indigo-700">
|
||||
Manage Variables →
|
||||
</button>
|
||||
</div>
|
||||
${this.variables.length > 0 ? `
|
||||
<div class="border border-gray-200 rounded-md overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Value</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
${this.variables.slice(0, 5).map(v => `
|
||||
<tr>
|
||||
<td class="px-4 py-2 text-sm font-mono text-gray-900">${escapeHtml(v.variableName)}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-600">${escapeHtml(v.value)}</td>
|
||||
<td class="px-4 py-2 text-sm text-gray-500">${escapeHtml(v.category || 'other')}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
${this.variables.length > 5 ? `
|
||||
<div class="px-4 py-2 bg-gray-50 text-xs text-gray-500 text-center">
|
||||
Showing 5 of ${this.variables.length} variables
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : '<p class="text-sm text-gray-500 italic">No variables defined</p>'}
|
||||
</div>
|
||||
|
||||
<!-- Metadata -->
|
||||
<div class="text-xs text-gray-500 space-y-1">
|
||||
<p>Created: ${new Date(project.createdAt).toLocaleString()}</p>
|
||||
<p>Updated: ${new Date(project.updatedAt).toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-between">
|
||||
<button data-action="openEdit" data-arg0="${project.id}" class="px-4 py-2 border border-indigo-300 rounded-md text-sm font-medium text-indigo-700 bg-indigo-50 hover:bg-indigo-100">
|
||||
Edit Project
|
||||
</button>
|
||||
<button id="close-modal" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Attach close handlers
|
||||
document.getElementById('close-modal').addEventListener('click', () => this.close());
|
||||
}
|
||||
|
||||
/**
|
||||
* Render variables management mode
|
||||
*/
|
||||
renderVariablesMode() {
|
||||
const container = document.getElementById('modal-container');
|
||||
|
||||
container.innerHTML = `
|
||||
<div id="project-editor-modal" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-5xl w-full max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900">Manage Variables</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">${escapeHtml(this.originalProject.name)} (${escapeHtml(this.originalProject.id)})</p>
|
||||
</div>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="flex-1 overflow-y-auto p-6">
|
||||
<div class="mb-4 flex justify-between items-center">
|
||||
<p class="text-sm text-gray-600">${this.variables.length} variable${this.variables.length !== 1 ? 's' : ''} defined</p>
|
||||
<button id="add-variable-btn" class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700">
|
||||
+ Add Variable
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="variables-list" class="space-y-3">
|
||||
${this.variables.length > 0 ? this.variables.map(v => this.renderVariableCard(v)).join('') : `
|
||||
<div class="text-center py-12 text-gray-500">
|
||||
<p class="text-sm">No variables defined for this project.</p>
|
||||
<p class="text-xs mt-2">Click "Add Variable" to create one.</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex justify-end">
|
||||
<button id="close-modal" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Attach event listeners
|
||||
document.getElementById('close-modal').addEventListener('click', () => {
|
||||
this.close();
|
||||
// Refresh project list
|
||||
if (window.loadProjects) window.loadProjects();
|
||||
if (window.loadStatistics) window.loadStatistics();
|
||||
});
|
||||
|
||||
document.getElementById('add-variable-btn').addEventListener('click', () => {
|
||||
this.showVariableForm();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single variable card
|
||||
*/
|
||||
renderVariableCard(variable) {
|
||||
return `
|
||||
<div class="border border-gray-200 rounded-md p-4 hover:border-indigo-300 transition-colors">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<h5 class="text-sm font-medium font-mono text-gray-900">${escapeHtml(variable.variableName)}</h5>
|
||||
<p class="text-sm text-gray-600 mt-1">${escapeHtml(variable.value)}</p>
|
||||
${variable.description ? `<p class="text-xs text-gray-500 mt-1">${escapeHtml(variable.description)}</p>` : ''}
|
||||
<div class="flex items-center space-x-3 mt-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
|
||||
${escapeHtml(variable.category || 'other')}
|
||||
</span>
|
||||
<span class="text-xs text-gray-500">${escapeHtml(variable.dataType || 'string')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 ml-4">
|
||||
<button data-action="editVariable" data-arg0="${escapeHtml(variable.variableName)}" class="text-sm text-indigo-600 hover:text-indigo-700">
|
||||
Edit
|
||||
</button>
|
||||
<button data-action="deleteVariable" data-arg0="${escapeHtml(variable.variableName)}" class="text-sm text-red-600 hover:text-red-700">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show variable form (add/edit)
|
||||
*/
|
||||
showVariableForm(variableName = null) {
|
||||
const existingVariable = variableName ? this.variables.find(v => v.variableName === variableName) : null;
|
||||
const isEdit = !!existingVariable;
|
||||
|
||||
const formHtml = `
|
||||
<div class="border-t border-gray-200 mt-4 pt-4 bg-gray-50 rounded-md p-4">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-4">${isEdit ? 'Edit' : 'Add'} Variable</h4>
|
||||
<form id="variable-form" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Variable Name <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="var-name"
|
||||
placeholder="e.g., DB_NAME"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 font-mono text-sm"
|
||||
${isEdit ? 'readonly' : 'required'}
|
||||
value="${isEdit ? escapeHtml(existingVariable.variableName) : ''}"
|
||||
>
|
||||
<p class="text-xs text-gray-500 mt-1">UPPER_SNAKE_CASE format</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Value <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="var-value"
|
||||
placeholder="e.g., my_database"
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm"
|
||||
required
|
||||
value="${isEdit ? escapeHtml(existingVariable.value) : ''}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
||||
<input
|
||||
type="text"
|
||||
id="var-description"
|
||||
placeholder="What this variable represents..."
|
||||
class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm"
|
||||
value="${isEdit && existingVariable.description ? escapeHtml(existingVariable.description) : ''}"
|
||||
>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||
<select id="var-category" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
<option value="other" ${isEdit && existingVariable.category === 'other' ? 'selected' : ''}>Other</option>
|
||||
<option value="database" ${isEdit && existingVariable.category === 'database' ? 'selected' : ''}>Database</option>
|
||||
<option value="security" ${isEdit && existingVariable.category === 'security' ? 'selected' : ''}>Security</option>
|
||||
<option value="config" ${isEdit && existingVariable.category === 'config' ? 'selected' : ''}>Config</option>
|
||||
<option value="path" ${isEdit && existingVariable.category === 'path' ? 'selected' : ''}>Path</option>
|
||||
<option value="url" ${isEdit && existingVariable.category === 'url' ? 'selected' : ''}>URL</option>
|
||||
<option value="port" ${isEdit && existingVariable.category === 'port' ? 'selected' : ''}>Port</option>
|
||||
<option value="feature_flag" ${isEdit && existingVariable.category === 'feature_flag' ? 'selected' : ''}>Feature Flag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Data Type</label>
|
||||
<select id="var-datatype" class="w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
|
||||
<option value="string" ${isEdit && existingVariable.dataType === 'string' ? 'selected' : ''}>String</option>
|
||||
<option value="number" ${isEdit && existingVariable.dataType === 'number' ? 'selected' : ''}>Number</option>
|
||||
<option value="boolean" ${isEdit && existingVariable.dataType === 'boolean' ? 'selected' : ''}>Boolean</option>
|
||||
<option value="path" ${isEdit && existingVariable.dataType === 'path' ? 'selected' : ''}>Path</option>
|
||||
<option value="url" ${isEdit && existingVariable.dataType === 'url' ? 'selected' : ''}>URL</option>
|
||||
<option value="email" ${isEdit && existingVariable.dataType === 'email' ? 'selected' : ''}>Email</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button type="button" id="cancel-var-btn" class="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md text-sm font-medium hover:bg-indigo-700">
|
||||
${isEdit ? 'Update' : 'Add'} Variable
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert form
|
||||
const container = document.querySelector('#variables-list');
|
||||
const formContainer = document.createElement('div');
|
||||
formContainer.id = 'variable-form-container';
|
||||
formContainer.innerHTML = formHtml;
|
||||
container.insertBefore(formContainer, container.firstChild);
|
||||
|
||||
// Attach event listeners
|
||||
document.getElementById('variable-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.saveVariable(isEdit);
|
||||
});
|
||||
|
||||
document.getElementById('cancel-var-btn').addEventListener('click', () => {
|
||||
document.getElementById('variable-form-container').remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save variable (create or update)
|
||||
*/
|
||||
async saveVariable(isEdit = false) {
|
||||
const variableName = document.getElementById('var-name').value.trim();
|
||||
const value = document.getElementById('var-value').value.trim();
|
||||
const description = document.getElementById('var-description').value.trim();
|
||||
const category = document.getElementById('var-category').value;
|
||||
const dataType = document.getElementById('var-datatype').value;
|
||||
|
||||
if (!variableName || !value) {
|
||||
showToast('Variable name and value are required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate UPPER_SNAKE_CASE
|
||||
if (!/^[A-Z][A-Z0-9_]*$/.test(variableName)) {
|
||||
showToast('Variable name must be UPPER_SNAKE_CASE (e.g., DB_NAME)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/projects/${this.projectId}/variables`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
variableName,
|
||||
value,
|
||||
description,
|
||||
category,
|
||||
dataType
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showToast(`Variable ${isEdit ? 'updated' : 'created'} successfully`, 'success');
|
||||
// Reload variables
|
||||
const variablesResponse = await apiRequest(`/api/admin/projects/${this.projectId}/variables`);
|
||||
this.variables = variablesResponse.variables || [];
|
||||
// Re-render
|
||||
this.renderVariablesMode();
|
||||
} else {
|
||||
showToast(response.message || 'Failed to save variable', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save variable:', error);
|
||||
showToast('Failed to save variable', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit variable
|
||||
*/
|
||||
editVariable(variableName) {
|
||||
// Remove any existing form first
|
||||
const existingForm = document.getElementById('variable-form-container');
|
||||
if (existingForm) existingForm.remove();
|
||||
|
||||
this.showVariableForm(variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete variable
|
||||
*/
|
||||
async deleteVariable(variableName) {
|
||||
if (!confirm(`Delete variable "${variableName}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/projects/${this.projectId}/variables/${variableName}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showToast('Variable deleted successfully', 'success');
|
||||
// Reload variables
|
||||
const variablesResponse = await apiRequest(`/api/admin/projects/${this.projectId}/variables`);
|
||||
this.variables = variablesResponse.variables || [];
|
||||
// Re-render
|
||||
this.renderVariablesMode();
|
||||
} else {
|
||||
showToast(response.message || 'Failed to delete variable', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete variable:', error);
|
||||
showToast('Failed to delete variable', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate form with project data (edit mode)
|
||||
*/
|
||||
populateForm(project) {
|
||||
document.getElementById('project-id').value = project.id || '';
|
||||
document.getElementById('project-name').value = project.name || '';
|
||||
document.getElementById('project-description').value = project.description || '';
|
||||
document.getElementById('project-active').checked = project.active !== false;
|
||||
document.getElementById('repo-url').value = project.repositoryUrl || '';
|
||||
|
||||
if (project.techStack) {
|
||||
document.getElementById('tech-framework').value = project.techStack.framework || '';
|
||||
document.getElementById('tech-database').value = project.techStack.database || '';
|
||||
document.getElementById('tech-frontend').value = project.techStack.frontend || '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach event listeners
|
||||
*/
|
||||
attachEventListeners() {
|
||||
document.getElementById('close-modal').addEventListener('click', () => this.close());
|
||||
document.getElementById('cancel-btn').addEventListener('click', () => this.close());
|
||||
document.getElementById('save-btn').addEventListener('click', () => this.submit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit form
|
||||
*/
|
||||
async submit() {
|
||||
const form = document.getElementById('project-form');
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
const projectData = {
|
||||
id: document.getElementById('project-id').value.trim(),
|
||||
name: document.getElementById('project-name').value.trim(),
|
||||
description: document.getElementById('project-description').value.trim(),
|
||||
active: document.getElementById('project-active').checked,
|
||||
repositoryUrl: document.getElementById('repo-url').value.trim() || null,
|
||||
techStack: {
|
||||
framework: document.getElementById('tech-framework').value.trim() || undefined,
|
||||
database: document.getElementById('tech-database').value.trim() || undefined,
|
||||
frontend: document.getElementById('tech-frontend').value.trim() || undefined
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (this.mode === 'create') {
|
||||
response = await apiRequest('/api/admin/projects', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(projectData)
|
||||
});
|
||||
} else {
|
||||
response = await apiRequest(`/api/admin/projects/${this.projectId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(projectData)
|
||||
});
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
showToast(`Project ${this.mode === 'create' ? 'created' : 'updated'} successfully`, 'success');
|
||||
this.close();
|
||||
// Refresh project list
|
||||
if (window.loadProjects) window.loadProjects();
|
||||
if (window.loadStatistics) window.loadStatistics();
|
||||
} else {
|
||||
showToast(response.message || 'Failed to save project', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save project:', error);
|
||||
showToast('Failed to save project', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close modal
|
||||
*/
|
||||
close() {
|
||||
const container = document.getElementById('modal-container');
|
||||
container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
window.projectEditor = new ProjectEditor();
|
||||
|
||||
// Event delegation for data-action buttons (CSP compliance)
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const arg0 = button.dataset.arg0;
|
||||
|
||||
if (action === 'editVariable') {
|
||||
window.projectEditor.editVariable(arg0);
|
||||
} else if (action === 'deleteVariable') {
|
||||
window.projectEditor.deleteVariable(arg0);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,362 +0,0 @@
|
|||
/**
|
||||
* Project Selector Component
|
||||
* Reusable dropdown for selecting active project context in admin pages
|
||||
*
|
||||
* Features:
|
||||
* - Loads active projects from API
|
||||
* - Persists selection to localStorage
|
||||
* - Emits change events
|
||||
* - Supports callback functions
|
||||
* - Responsive design with icons
|
||||
*/
|
||||
|
||||
class ProjectSelector {
|
||||
constructor(containerId, options = {}) {
|
||||
this.containerId = containerId;
|
||||
this.projects = [];
|
||||
this.selectedProjectId = null;
|
||||
|
||||
// Options
|
||||
this.options = {
|
||||
showAllOption: options.showAllOption !== undefined ? options.showAllOption : true,
|
||||
allOptionText: options.allOptionText || 'All Projects (Template View)',
|
||||
onChange: options.onChange || null,
|
||||
storageKey: options.storageKey || 'selected_project_id',
|
||||
placeholder: options.placeholder || 'Select a project...',
|
||||
label: options.label || 'Active Project Context',
|
||||
showLabel: options.showLabel !== undefined ? options.showLabel : true,
|
||||
compact: options.compact || false, // Compact mode for navbar
|
||||
autoLoad: options.autoLoad !== undefined ? options.autoLoad : true
|
||||
};
|
||||
|
||||
// Auth token
|
||||
this.token = localStorage.getItem('admin_token');
|
||||
|
||||
if (this.options.autoLoad) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the component
|
||||
*/
|
||||
async init() {
|
||||
try {
|
||||
// Load saved project from localStorage
|
||||
const savedProjectId = localStorage.getItem(this.options.storageKey);
|
||||
if (savedProjectId) {
|
||||
this.selectedProjectId = savedProjectId;
|
||||
}
|
||||
|
||||
// Load projects from API
|
||||
await this.loadProjects();
|
||||
|
||||
// Render the selector
|
||||
this.render();
|
||||
|
||||
// Attach event listeners
|
||||
this.attachEventListeners();
|
||||
|
||||
// Trigger initial change event if project was pre-selected
|
||||
if (this.selectedProjectId && this.options.onChange) {
|
||||
this.options.onChange(this.selectedProjectId, this.getSelectedProject());
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize project selector:', error);
|
||||
this.renderError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load projects from API
|
||||
*/
|
||||
async loadProjects() {
|
||||
const response = await fetch('/api/admin/projects?active=true', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('admin_token');
|
||||
window.location.href = '/admin/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.projects = data.projects || [];
|
||||
|
||||
// Sort by name
|
||||
this.projects.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} else {
|
||||
throw new Error(data.message || 'Failed to load projects');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the selector component
|
||||
*/
|
||||
render() {
|
||||
const container = document.getElementById(this.containerId);
|
||||
if (!container) {
|
||||
console.error(`Container #${this.containerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine selected project
|
||||
const selectedProject = this.getSelectedProject();
|
||||
|
||||
// Build HTML based on compact or full mode
|
||||
if (this.options.compact) {
|
||||
container.innerHTML = this.renderCompact(selectedProject);
|
||||
} else {
|
||||
container.innerHTML = this.renderFull(selectedProject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render compact mode (for navbar)
|
||||
*/
|
||||
renderCompact(selectedProject) {
|
||||
const displayText = selectedProject ? selectedProject.name : this.options.placeholder;
|
||||
const displayColor = selectedProject ? 'text-indigo-700' : 'text-gray-500';
|
||||
|
||||
return `
|
||||
<div class="relative">
|
||||
<select
|
||||
id="${this.containerId}-select"
|
||||
class="block w-full pl-3 pr-10 py-2 text-sm border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md ${displayColor}"
|
||||
>
|
||||
${this.options.showAllOption ? `<option value="">${this.options.allOptionText}</option>` : ''}
|
||||
${this.projects.map(project => `
|
||||
<option
|
||||
value="${escapeHtml(project.id)}"
|
||||
${this.selectedProjectId === project.id ? 'selected' : ''}
|
||||
>
|
||||
${escapeHtml(project.name)}
|
||||
</option>
|
||||
`).join('')}
|
||||
</select>
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<svg class="h-4 w-4 ${displayColor}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render full mode (for content area)
|
||||
*/
|
||||
renderFull(selectedProject) {
|
||||
return `
|
||||
<div class="mb-6">
|
||||
${this.options.showLabel ? `
|
||||
<label for="${this.containerId}-select" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-5 w-5 text-indigo-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
${this.options.label}
|
||||
</div>
|
||||
</label>
|
||||
` : ''}
|
||||
|
||||
<select
|
||||
id="${this.containerId}-select"
|
||||
class="block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
|
||||
>
|
||||
${this.options.showAllOption ? `
|
||||
<option value="">
|
||||
${this.options.allOptionText}
|
||||
</option>
|
||||
` : ''}
|
||||
${this.projects.map(project => {
|
||||
const variableCount = project.variableCount || 0;
|
||||
return `
|
||||
<option
|
||||
value="${escapeHtml(project.id)}"
|
||||
${this.selectedProjectId === project.id ? 'selected' : ''}
|
||||
>
|
||||
${escapeHtml(project.name)} ${variableCount > 0 ? `(${variableCount} vars)` : ''}
|
||||
</option>
|
||||
`;
|
||||
}).join('')}
|
||||
</select>
|
||||
|
||||
${selectedProject ? `
|
||||
<div class="mt-2 p-3 bg-indigo-50 rounded-md">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-indigo-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3 flex-1">
|
||||
<h4 class="text-sm font-medium text-indigo-800">
|
||||
${escapeHtml(selectedProject.name)}
|
||||
</h4>
|
||||
${selectedProject.description ? `
|
||||
<p class="mt-1 text-sm text-indigo-700">
|
||||
${escapeHtml(selectedProject.description)}
|
||||
</p>
|
||||
` : ''}
|
||||
<div class="mt-2 text-sm text-indigo-600">
|
||||
<span class="font-medium">${selectedProject.variableCount || 0}</span> variable${(selectedProject.variableCount || 0) !== 1 ? 's' : ''} available for substitution
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : `
|
||||
<div class="mt-2 p-3 bg-gray-50 rounded-md">
|
||||
<p class="text-sm text-gray-600">
|
||||
<svg class="inline h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
Viewing template text with variable placeholders. Select a project to see rendered values.
|
||||
</p>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render error state
|
||||
*/
|
||||
renderError() {
|
||||
const container = document.getElementById(this.containerId);
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
Failed to load projects
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-red-700">
|
||||
Please refresh the page to try again.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach event listeners
|
||||
*/
|
||||
attachEventListeners() {
|
||||
const selectElement = document.getElementById(`${this.containerId}-select`);
|
||||
if (!selectElement) return;
|
||||
|
||||
selectElement.addEventListener('change', (e) => {
|
||||
const newProjectId = e.target.value || null;
|
||||
this.handleChange(newProjectId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle project selection change
|
||||
*/
|
||||
handleChange(projectId) {
|
||||
const previousProjectId = this.selectedProjectId;
|
||||
this.selectedProjectId = projectId;
|
||||
|
||||
// Save to localStorage
|
||||
if (projectId) {
|
||||
localStorage.setItem(this.options.storageKey, projectId);
|
||||
} else {
|
||||
localStorage.removeItem(this.options.storageKey);
|
||||
}
|
||||
|
||||
// Re-render to update info panel
|
||||
this.render();
|
||||
this.attachEventListeners(); // Re-attach after re-render
|
||||
|
||||
// Trigger callback
|
||||
if (this.options.onChange) {
|
||||
const selectedProject = this.getSelectedProject();
|
||||
this.options.onChange(projectId, selectedProject, previousProjectId);
|
||||
}
|
||||
|
||||
// Dispatch custom event for other listeners
|
||||
const event = new CustomEvent('projectChanged', {
|
||||
detail: {
|
||||
projectId,
|
||||
project: this.getSelectedProject(),
|
||||
previousProjectId
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently selected project object
|
||||
*/
|
||||
getSelectedProject() {
|
||||
if (!this.selectedProjectId) return null;
|
||||
return this.projects.find(p => p.id === this.selectedProjectId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded projects
|
||||
*/
|
||||
getProjects() {
|
||||
return this.projects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Programmatically set the selected project
|
||||
*/
|
||||
setSelectedProject(projectId) {
|
||||
this.handleChange(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload projects from API
|
||||
*/
|
||||
async reload() {
|
||||
try {
|
||||
await this.loadProjects();
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
} catch (error) {
|
||||
console.error('Failed to reload projects:', error);
|
||||
this.renderError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current selection
|
||||
*/
|
||||
getSelection() {
|
||||
return {
|
||||
projectId: this.selectedProjectId,
|
||||
project: this.getSelectedProject()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility: Escape HTML to prevent XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.ProjectSelector = ProjectSelector;
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,706 +0,0 @@
|
|||
/**
|
||||
* Rule Manager - Multi-Project Governance Dashboard
|
||||
* Handles filtering, sorting, pagination, and CRUD operations for rules
|
||||
*/
|
||||
|
||||
// Auth check
|
||||
const token = localStorage.getItem('admin_token');
|
||||
const user = JSON.parse(localStorage.getItem('admin_user') || '{}');
|
||||
|
||||
if (!token) {
|
||||
window.location.href = '/admin/login.html';
|
||||
}
|
||||
|
||||
// Display admin name
|
||||
document.getElementById('admin-name').textContent = user.email || 'Admin';
|
||||
|
||||
// Logout
|
||||
document.getElementById('logout-btn').addEventListener('click', () => {
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
window.location.href = '/admin/login.html';
|
||||
});
|
||||
|
||||
/**
|
||||
* API request helper with automatic auth header injection and token refresh
|
||||
*
|
||||
* @param {string} endpoint - API endpoint path (e.g., '/api/admin/rules')
|
||||
* @param {Object} [options={}] - Fetch options (method, body, headers, etc.)
|
||||
* @returns {Promise<Object>} JSON response from API
|
||||
*
|
||||
* @description
|
||||
* - Automatically adds Authorization header with Bearer token
|
||||
* - Redirects to login on 401 (unauthorized)
|
||||
* - Handles JSON response parsing
|
||||
*/
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const response = await fetch(endpoint, {
|
||||
...options,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
}
|
||||
});
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('admin_token');
|
||||
window.location.href = '/admin/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// State management
|
||||
let currentPage = 1;
|
||||
const pageSize = 20;
|
||||
let totalRules = 0;
|
||||
let selectedProjectId = null; // Track selected project for variable substitution
|
||||
let filters = {
|
||||
scope: '',
|
||||
quadrant: '',
|
||||
persistence: '',
|
||||
validation: '',
|
||||
active: 'true',
|
||||
search: '',
|
||||
sort: 'priority',
|
||||
order: 'desc'
|
||||
};
|
||||
|
||||
/**
|
||||
* Load and display dashboard statistics
|
||||
* Fetches rule counts, validation status, and average clarity scores
|
||||
*
|
||||
* @async
|
||||
* @description
|
||||
* Updates the following stat cards:
|
||||
* - Total rules
|
||||
* - Universal rules count
|
||||
* - Validated rules count
|
||||
* - Average clarity score
|
||||
*/
|
||||
async function loadStatistics() {
|
||||
try {
|
||||
const response = await apiRequest('/api/admin/rules/stats');
|
||||
|
||||
if (!response.success || !response.stats) {
|
||||
console.error('Invalid stats response:', response);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = response.stats;
|
||||
|
||||
document.getElementById('stat-total').textContent = stats.total || 0;
|
||||
document.getElementById('stat-universal').textContent = stats.byScope?.UNIVERSAL || 0;
|
||||
document.getElementById('stat-validated').textContent = stats.byValidationStatus?.PASSED || 0;
|
||||
|
||||
const avgClarity = stats.averageScores?.clarity;
|
||||
document.getElementById('stat-clarity').textContent = avgClarity ? avgClarity.toFixed(0) + '%' : 'N/A';
|
||||
} catch (error) {
|
||||
console.error('Failed to load statistics:', error);
|
||||
showToast('Failed to load statistics', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and render rules based on current filters, sorting, and pagination
|
||||
*
|
||||
* @async
|
||||
* @description
|
||||
* - Builds query parameters from current filter state
|
||||
* - Fetches rules from API
|
||||
* - Renders rule cards in grid layout
|
||||
* - Updates pagination UI
|
||||
* - Shows loading/empty/error states
|
||||
*
|
||||
* @fires loadRules - Called on filter change, sort change, or page change
|
||||
*/
|
||||
async function loadRules() {
|
||||
const container = document.getElementById('rules-grid');
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12 text-gray-500">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600 mb-4"></div>
|
||||
<p>Loading rules...</p>
|
||||
</div>
|
||||
`; setProgressBarWidths(container);
|
||||
|
||||
// Build query parameters
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage,
|
||||
limit: pageSize,
|
||||
sort: filters.sort,
|
||||
order: filters.order
|
||||
});
|
||||
|
||||
if (filters.scope) params.append('scope', filters.scope);
|
||||
if (filters.quadrant) params.append('quadrant', filters.quadrant);
|
||||
if (filters.persistence) params.append('persistence', filters.persistence);
|
||||
if (filters.validation) params.append('validationStatus', filters.validation);
|
||||
if (filters.active) params.append('active', filters.active);
|
||||
if (filters.search) params.append('search', filters.search);
|
||||
|
||||
// Include project ID for variable substitution
|
||||
if (selectedProjectId) params.append('projectId', selectedProjectId);
|
||||
|
||||
const response = await apiRequest(`/api/admin/rules?${params.toString()}`);
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('Failed to load rules');
|
||||
}
|
||||
|
||||
const rules = response.rules || [];
|
||||
totalRules = response.pagination?.total || 0;
|
||||
|
||||
// Update results count
|
||||
document.getElementById('filter-results').textContent =
|
||||
`Showing ${rules.length} of ${totalRules} rules`;
|
||||
|
||||
// Render rules
|
||||
if (rules.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12 text-gray-500">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">No rules found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Try adjusting your filters or create a new rule.</p>
|
||||
</div>
|
||||
`; setProgressBarWidths(container);
|
||||
document.getElementById('pagination').classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Render rule cards
|
||||
container.innerHTML = `
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
${rules.map(rule => renderRuleCard(rule)).join('')}
|
||||
</div>
|
||||
`; setProgressBarWidths(container);
|
||||
|
||||
// Update pagination
|
||||
updatePagination(response.pagination);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load rules:', error);
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-12 text-red-500">
|
||||
<p>Failed to load rules. Please try again.</p>
|
||||
</div>
|
||||
`; setProgressBarWidths(container);
|
||||
showToast('Failed to load rules', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single rule as an HTML card
|
||||
*
|
||||
* @param {Object} rule - Rule object from API
|
||||
* @param {string} rule._id - MongoDB ObjectId
|
||||
* @param {string} rule.id - Rule ID (inst_xxx)
|
||||
* @param {string} rule.text - Rule text
|
||||
* @param {string} rule.scope - UNIVERSAL | PROJECT_SPECIFIC
|
||||
* @param {string} rule.quadrant - STRATEGIC | OPERATIONAL | TACTICAL | SYSTEM | STORAGE
|
||||
* @param {string} rule.persistence - HIGH | MEDIUM | LOW
|
||||
* @param {number} rule.priority - Priority (0-100)
|
||||
* @param {number} [rule.clarityScore] - Clarity score (0-100)
|
||||
* @param {Array<string>} [rule.variables] - Detected variables
|
||||
* @param {Object} [rule.usageStats] - Usage statistics
|
||||
*
|
||||
* @returns {string} HTML string for rule card
|
||||
*
|
||||
* @description
|
||||
* Generates a card with:
|
||||
* - Scope, quadrant, persistence, validation status badges
|
||||
* - Rule text (truncated to 2 lines)
|
||||
* - Priority, variable count, enforcement count
|
||||
* - Clarity score progress bar
|
||||
* - View/Edit/Delete action buttons
|
||||
*/
|
||||
function renderRuleCard(rule) {
|
||||
const scopeBadgeColor = rule.scope === 'UNIVERSAL' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800';
|
||||
const quadrantBadgeColor = getQuadrantColor(rule.quadrant);
|
||||
const persistenceBadgeColor = getPersistenceColor(rule.persistence);
|
||||
const validationBadgeColor = getValidationColor(rule.validationStatus);
|
||||
const clarityScore = rule.clarityScore || 0;
|
||||
const clarityColor = clarityScore >= 80 ? 'bg-green-500' : clarityScore >= 60 ? 'bg-yellow-500' : 'bg-red-500';
|
||||
|
||||
return `
|
||||
<div class="border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow">
|
||||
<div class="flex justify-between items-start mb-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${scopeBadgeColor}">
|
||||
${rule.scope}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${quadrantBadgeColor}">
|
||||
${rule.quadrant}
|
||||
</span>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${persistenceBadgeColor}">
|
||||
${rule.persistence}
|
||||
</span>
|
||||
${rule.validationStatus !== 'NOT_VALIDATED' ? `
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${validationBadgeColor}">
|
||||
${rule.validationStatus}
|
||||
</span>
|
||||
` : ''}
|
||||
</div>
|
||||
<span class="text-xs font-mono text-gray-500">${rule.id}</span>
|
||||
</div>
|
||||
|
||||
${rule.renderedText ? `
|
||||
<!-- Template Text -->
|
||||
<div class="mb-3">
|
||||
<div class="flex items-center mb-1">
|
||||
<svg class="h-4 w-4 text-gray-400 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
|
||||
</svg>
|
||||
<span class="text-xs font-medium text-gray-500 uppercase">Template</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-600 font-mono bg-gray-50 px-2 py-1 rounded line-clamp-2">${escapeHtml(rule.text)}</p>
|
||||
</div>
|
||||
|
||||
<!-- Rendered Text (with substituted variables) -->
|
||||
<div class="mb-3">
|
||||
<div class="flex items-center mb-1">
|
||||
<svg class="h-4 w-4 text-indigo-600 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<span class="text-xs font-medium text-indigo-600 uppercase">Rendered (${rule.projectContext || 'Unknown'})</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-900 bg-indigo-50 px-2 py-1 rounded line-clamp-2">${escapeHtml(rule.renderedText)}</p>
|
||||
</div>
|
||||
` : `
|
||||
<!-- Template Text Only (no project selected) -->
|
||||
<p class="text-sm text-gray-900 mb-3 line-clamp-2">${escapeHtml(rule.text)}</p>
|
||||
`}
|
||||
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-4 text-xs text-gray-500">
|
||||
<div class="flex items-center">
|
||||
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
|
||||
</svg>
|
||||
Priority: ${rule.priority}
|
||||
</div>
|
||||
${rule.variables && rule.variables.length > 0 ? `
|
||||
<div class="flex items-center">
|
||||
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
${rule.variables.length} var${rule.variables.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
${rule.usageStats?.timesEnforced > 0 ? `
|
||||
<div class="flex items-center">
|
||||
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
${rule.usageStats.timesEnforced} enforcements
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
${rule.clarityScore !== null ? `
|
||||
<div class="flex items-center">
|
||||
<span class="text-xs text-gray-500 mr-2">Clarity:</span>
|
||||
<div class="w-16 bg-gray-200 rounded-full h-2">
|
||||
<div class="${clarityColor} h-2 rounded-full" data-width="${clarityScore}"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 ml-2">${clarityScore}%</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-2 pt-3 border-t border-gray-200">
|
||||
<button data-action="viewRule" data-arg0="${rule._id}" class="flex-1 text-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
|
||||
View
|
||||
</button>
|
||||
<button data-action="editRule" data-arg0="${rule._id}" class="flex-1 text-center px-3 py-2 border border-indigo-300 rounded-md text-sm font-medium text-indigo-700 bg-indigo-50 hover:bg-indigo-100">
|
||||
Edit
|
||||
</button>
|
||||
<button data-action="deleteRule" data-arg0="${rule._id}" data-arg1="${escapeHtml(rule.id)}" class="px-3 py-2 border border-red-300 rounded-md text-sm font-medium text-red-700 bg-red-50 hover:bg-red-100">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update pagination UI with page numbers and navigation buttons
|
||||
*
|
||||
* @param {Object} pagination - Pagination metadata from API
|
||||
* @param {number} pagination.page - Current page number
|
||||
* @param {number} pagination.limit - Items per page
|
||||
* @param {number} pagination.total - Total number of items
|
||||
* @param {number} pagination.pages - Total number of pages
|
||||
*
|
||||
* @description
|
||||
* - Shows/hides pagination based on total items
|
||||
* - Generates smart page number buttons (shows first, last, and pages around current)
|
||||
* - Adds ellipsis (...) for gaps in page numbers
|
||||
* - Enables/disables prev/next buttons based on current page
|
||||
*/
|
||||
function updatePagination(pagination) {
|
||||
const paginationDiv = document.getElementById('pagination');
|
||||
|
||||
if (!pagination || pagination.total === 0) {
|
||||
paginationDiv.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
paginationDiv.classList.remove('hidden');
|
||||
|
||||
const start = (pagination.page - 1) * pagination.limit + 1;
|
||||
const end = Math.min(pagination.page * pagination.limit, pagination.total);
|
||||
|
||||
document.getElementById('page-start').textContent = start;
|
||||
document.getElementById('page-end').textContent = end;
|
||||
document.getElementById('page-total').textContent = pagination.total;
|
||||
|
||||
// Update page buttons
|
||||
const prevBtn = document.getElementById('prev-page');
|
||||
const nextBtn = document.getElementById('next-page');
|
||||
|
||||
prevBtn.disabled = pagination.page <= 1;
|
||||
nextBtn.disabled = pagination.page >= pagination.pages;
|
||||
|
||||
// Generate page numbers
|
||||
const pageNumbers = document.getElementById('page-numbers');
|
||||
const pages = [];
|
||||
const currentPage = pagination.page;
|
||||
const totalPages = pagination.pages;
|
||||
|
||||
// Always show first page
|
||||
pages.push(1);
|
||||
|
||||
// Show pages around current page
|
||||
for (let i = Math.max(2, currentPage - 1); i <= Math.min(totalPages - 1, currentPage + 1); i++) {
|
||||
if (!pages.includes(i)) pages.push(i);
|
||||
}
|
||||
|
||||
// Always show last page
|
||||
if (totalPages > 1 && !pages.includes(totalPages)) {
|
||||
pages.push(totalPages);
|
||||
}
|
||||
|
||||
pageNumbers.innerHTML = pages.map((page, index) => {
|
||||
const prev = pages[index - 1];
|
||||
const gap = prev && page - prev > 1 ? '<span class="px-2 text-gray-500">...</span>' : '';
|
||||
const active = page === currentPage ? 'bg-indigo-600 text-white' : 'border border-gray-300 text-gray-700 hover:bg-gray-50';
|
||||
|
||||
return `
|
||||
${gap}
|
||||
<button data-action="goToPage" data-arg0="${page}" class="px-3 py-1 rounded-md text-sm font-medium ${active}">
|
||||
${page}
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Pagination handlers
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
loadRules();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
document.getElementById('prev-page')?.addEventListener('click', () => {
|
||||
if (currentPage > 1) {
|
||||
goToPage(currentPage - 1);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('next-page')?.addEventListener('click', () => {
|
||||
const maxPage = Math.ceil(totalRules / pageSize);
|
||||
if (currentPage < maxPage) {
|
||||
goToPage(currentPage + 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter handlers
|
||||
function applyFilters() {
|
||||
currentPage = 1; // Reset to first page when filters change
|
||||
loadRules();
|
||||
}
|
||||
|
||||
document.getElementById('filter-scope')?.addEventListener('change', (e) => {
|
||||
filters.scope = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('filter-quadrant')?.addEventListener('change', (e) => {
|
||||
filters.quadrant = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('filter-persistence')?.addEventListener('change', (e) => {
|
||||
filters.persistence = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('filter-validation')?.addEventListener('change', (e) => {
|
||||
filters.validation = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('filter-active')?.addEventListener('change', (e) => {
|
||||
filters.active = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('sort-by')?.addEventListener('change', (e) => {
|
||||
filters.sort = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
document.getElementById('sort-order')?.addEventListener('change', (e) => {
|
||||
filters.order = e.target.value;
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// Search with debouncing
|
||||
let searchTimeout;
|
||||
document.getElementById('search-box')?.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
filters.search = e.target.value;
|
||||
applyFilters();
|
||||
}, 500); // 500ms debounce
|
||||
});
|
||||
|
||||
// Clear filters
|
||||
document.getElementById('clear-filters-btn')?.addEventListener('click', () => {
|
||||
filters = {
|
||||
scope: '',
|
||||
quadrant: '',
|
||||
persistence: '',
|
||||
validation: '',
|
||||
active: 'true',
|
||||
search: '',
|
||||
sort: 'priority',
|
||||
order: 'desc'
|
||||
};
|
||||
|
||||
document.getElementById('filter-scope').value = '';
|
||||
document.getElementById('filter-quadrant').value = '';
|
||||
document.getElementById('filter-persistence').value = '';
|
||||
document.getElementById('filter-validation').value = '';
|
||||
document.getElementById('filter-active').value = 'true';
|
||||
document.getElementById('search-box').value = '';
|
||||
document.getElementById('sort-by').value = 'priority';
|
||||
document.getElementById('sort-order').value = 'desc';
|
||||
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
// CRUD operations
|
||||
async function viewRule(ruleId) {
|
||||
if (window.ruleEditor) {
|
||||
window.ruleEditor.openView(ruleId);
|
||||
} else {
|
||||
showToast('Rule editor not loaded', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function editRule(ruleId) {
|
||||
if (window.ruleEditor) {
|
||||
window.ruleEditor.openEdit(ruleId);
|
||||
} else {
|
||||
showToast('Rule editor not loaded', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRule(ruleId, ruleName) {
|
||||
if (!confirm(`Delete rule "${ruleName}"? This will deactivate the rule (soft delete).`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiRequest(`/api/admin/rules/${ruleId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showToast('Rule deleted successfully', 'success');
|
||||
loadRules();
|
||||
loadStatistics();
|
||||
} else {
|
||||
showToast(response.message || 'Failed to delete rule', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error);
|
||||
showToast('Failed to delete rule', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// New rule button
|
||||
document.getElementById('new-rule-btn')?.addEventListener('click', () => {
|
||||
if (window.ruleEditor) {
|
||||
window.ruleEditor.openCreate();
|
||||
} else {
|
||||
showToast('Rule editor not loaded', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Show a toast notification message
|
||||
*
|
||||
* @param {string} message - Message to display
|
||||
* @param {string} [type='info'] - Toast type (success | error | warning | info)
|
||||
*
|
||||
* @description
|
||||
* - Creates animated toast notification in top-right corner
|
||||
* - Auto-dismisses after 5 seconds
|
||||
* - Can be manually dismissed by clicking X button
|
||||
* - Color-coded by type (green=success, red=error, yellow=warning, blue=info)
|
||||
*/
|
||||
function showToast(message, type = 'info') {
|
||||
const container = document.getElementById('toast-container');
|
||||
const colors = {
|
||||
success: 'bg-green-500',
|
||||
error: 'bg-red-500',
|
||||
warning: 'bg-yellow-500',
|
||||
info: 'bg-blue-500'
|
||||
};
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `${colors[type]} text-white px-6 py-3 rounded-lg shadow-lg flex items-center space-x-2 transition-all duration-300 ease-in-out`;
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100px)';
|
||||
toast.innerHTML = `
|
||||
<span>${escapeHtml(message)}</span>
|
||||
<button data-action="remove-parent" class="ml-4 text-white hover:text-gray-200">
|
||||
×
|
||||
</button>
|
||||
`;
|
||||
|
||||
container.appendChild(toast);
|
||||
|
||||
// Trigger animation
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '1';
|
||||
toast.style.transform = 'translateX(0)';
|
||||
}, 10);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.style.opacity = '0';
|
||||
toast.style.transform = 'translateX(100px)';
|
||||
setTimeout(() => toast.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
function getQuadrantColor(quadrant) {
|
||||
const colors = {
|
||||
STRATEGIC: 'bg-purple-100 text-purple-800',
|
||||
OPERATIONAL: 'bg-green-100 text-green-800',
|
||||
TACTICAL: 'bg-yellow-100 text-yellow-800',
|
||||
SYSTEM: 'bg-blue-100 text-blue-800',
|
||||
STORAGE: 'bg-gray-100 text-gray-800'
|
||||
};
|
||||
return colors[quadrant] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
function getPersistenceColor(persistence) {
|
||||
const colors = {
|
||||
HIGH: 'bg-red-100 text-red-800',
|
||||
MEDIUM: 'bg-orange-100 text-orange-800',
|
||||
LOW: 'bg-yellow-100 text-yellow-800'
|
||||
};
|
||||
return colors[persistence] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
function getValidationColor(status) {
|
||||
const colors = {
|
||||
PASSED: 'bg-green-100 text-green-800',
|
||||
FAILED: 'bg-red-100 text-red-800',
|
||||
NEEDS_REVIEW: 'bg-yellow-100 text-yellow-800',
|
||||
NOT_VALIDATED: 'bg-gray-100 text-gray-800'
|
||||
};
|
||||
return colors[status] || 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Make functions global for onclick handlers
|
||||
window.viewRule = viewRule;
|
||||
window.editRule = editRule;
|
||||
window.deleteRule = deleteRule;
|
||||
window.goToPage = goToPage;
|
||||
|
||||
/**
|
||||
* Initialize project selector for variable substitution
|
||||
* When a project is selected, rules will show both template and rendered text
|
||||
*/
|
||||
const projectSelector = new ProjectSelector('project-selector-container', {
|
||||
showAllOption: true,
|
||||
allOptionText: 'All Projects (Template View)',
|
||||
label: 'Project Context for Variable Substitution',
|
||||
showLabel: true,
|
||||
compact: false,
|
||||
onChange: (projectId, project) => {
|
||||
// Update selected project state
|
||||
selectedProjectId = projectId;
|
||||
|
||||
// Reload rules with new project context
|
||||
currentPage = 1; // Reset to first page
|
||||
loadRules();
|
||||
|
||||
// Show toast notification
|
||||
if (projectId && project) {
|
||||
showToast(`Viewing rules with ${project.name} context`, 'info');
|
||||
} else {
|
||||
showToast('Viewing template rules (no variable substitution)', 'info');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize on page load
|
||||
loadStatistics();
|
||||
loadRules();
|
||||
// Set widths/heights from data attributes (CSP compliance)
|
||||
function setProgressBarWidths(container) {
|
||||
const elements = container.querySelectorAll('[data-width], [data-height]');
|
||||
elements.forEach(el => {
|
||||
if (el.dataset.width) el.style.width = el.dataset.width + '%';
|
||||
if (el.dataset.height) el.style.height = el.dataset.height + '%';
|
||||
});
|
||||
}
|
||||
|
||||
// Event delegation for data-action buttons (CSP compliance)
|
||||
document.addEventListener('click', (e) => {
|
||||
const button = e.target.closest('[data-action]');
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const arg0 = button.dataset.arg0;
|
||||
const arg1 = button.dataset.arg1;
|
||||
|
||||
switch (action) {
|
||||
case 'viewRule':
|
||||
viewRule(arg0);
|
||||
break;
|
||||
case 'editRule':
|
||||
editRule(arg0);
|
||||
break;
|
||||
case 'deleteRule':
|
||||
deleteRule(arg0, arg1);
|
||||
break;
|
||||
case 'goToPage':
|
||||
goToPage(parseInt(arg0));
|
||||
break;
|
||||
case 'remove-parent':
|
||||
button.parentElement.remove();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1,359 +0,0 @@
|
|||
/**
|
||||
* Interactive Architecture Diagram Component
|
||||
* Tractatus Framework - Phase 3: Interactive Architecture Diagram
|
||||
*
|
||||
* Handles click/hover interactions on the hexagonal service diagram
|
||||
* Shows service details in a side panel
|
||||
*/
|
||||
|
||||
class InteractiveDiagram {
|
||||
constructor() {
|
||||
this.serviceData = {
|
||||
overview: {
|
||||
name: 'Tractatus Governance Layer',
|
||||
shortName: 'Overview',
|
||||
color: '#0ea5e9',
|
||||
icon: '⚙️',
|
||||
description: 'Six external governance services working together to enforce AI safety boundaries outside the AI runtime.',
|
||||
details: [
|
||||
'All services operate externally to the AI—making manipulation harder',
|
||||
'Instruction storage and validation work together to prevent directive fade',
|
||||
'Boundary enforcement and deliberation coordinate on values decisions',
|
||||
'Pressure monitoring adjusts verification requirements dynamically',
|
||||
'Metacognitive gates ensure AI pauses before high-risk operations',
|
||||
'Each service addresses a different failure mode in AI safety'
|
||||
],
|
||||
promise: 'External architectural enforcement that is structurally more difficult to bypass than behavioral training alone.'
|
||||
},
|
||||
boundary: {
|
||||
name: 'BoundaryEnforcer',
|
||||
shortName: 'Boundary',
|
||||
color: '#10b981',
|
||||
icon: '🔒',
|
||||
description: 'Blocks AI from making values decisions (privacy, ethics, strategic direction). Requires human approval.',
|
||||
details: [
|
||||
'Enforces Tractatus 12.1-12.7 boundaries',
|
||||
'Values decisions architecturally require humans',
|
||||
'Prevents AI autonomous decision-making on ethical questions',
|
||||
'External enforcement - harder to bypass via prompting'
|
||||
],
|
||||
promise: 'Values boundaries enforced externally—harder to manipulate through prompting.'
|
||||
},
|
||||
instruction: {
|
||||
name: 'InstructionPersistenceClassifier',
|
||||
shortName: 'Instruction',
|
||||
color: '#6366f1',
|
||||
icon: '📋',
|
||||
description: 'Stores instructions externally with persistence levels (HIGH/MEDIUM/LOW). Aims to reduce directive fade.',
|
||||
details: [
|
||||
'Quadrant-based classification (STR/OPS/TAC/SYS/STO)',
|
||||
'Time-persistence metadata tagging',
|
||||
'Temporal horizon modeling (STRATEGIC, OPERATIONAL, TACTICAL)',
|
||||
'External storage independent of AI runtime'
|
||||
],
|
||||
promise: 'Instructions stored outside AI—more resistant to context manipulation.'
|
||||
},
|
||||
validator: {
|
||||
name: 'CrossReferenceValidator',
|
||||
shortName: 'Validator',
|
||||
color: '#8b5cf6',
|
||||
icon: '✓',
|
||||
description: 'Validates AI actions against instruction history. Aims to prevent pattern bias overriding explicit directives.',
|
||||
details: [
|
||||
'Cross-references AI claims with external instruction history',
|
||||
'Detects pattern-based overrides of explicit user directives',
|
||||
'Independent verification layer',
|
||||
'Helps prevent instruction drift'
|
||||
],
|
||||
promise: 'Independent verification—AI claims checked against external source.'
|
||||
},
|
||||
pressure: {
|
||||
name: 'ContextPressureMonitor',
|
||||
shortName: 'Pressure',
|
||||
color: '#f59e0b',
|
||||
icon: '⚡',
|
||||
description: 'Monitors AI performance degradation. Escalates when context pressure threatens quality.',
|
||||
details: [
|
||||
'Tracks token usage, complexity, error rates',
|
||||
'Detects degraded operating conditions',
|
||||
'Adjusts verification requirements under pressure',
|
||||
'Objective metrics for quality monitoring'
|
||||
],
|
||||
promise: 'Objective metrics may detect manipulation attempts early.'
|
||||
},
|
||||
metacognitive: {
|
||||
name: 'MetacognitiveVerifier',
|
||||
shortName: 'Metacognitive',
|
||||
color: '#ec4899',
|
||||
icon: '💡',
|
||||
description: 'Requires AI to pause and verify complex operations before execution. Structural safety check.',
|
||||
details: [
|
||||
'AI self-checks alignment, coherence, safety before execution',
|
||||
'Structural pause-and-verify gates',
|
||||
'Selective verification (not constant)',
|
||||
'Architectural enforcement of reflection steps'
|
||||
],
|
||||
promise: 'Architectural gates aim to enforce verification steps.'
|
||||
},
|
||||
deliberation: {
|
||||
name: 'PluralisticDeliberationOrchestrator',
|
||||
shortName: 'Deliberation',
|
||||
color: '#14b8a6',
|
||||
icon: '👥',
|
||||
description: 'Facilitates multi-stakeholder deliberation for values conflicts where no single "correct" answer exists.',
|
||||
details: [
|
||||
'Non-hierarchical coordination for values conflicts',
|
||||
'Stakeholder perspective representation',
|
||||
'Consensus-building for ethical trade-offs',
|
||||
'Addresses values pluralism in AI safety'
|
||||
],
|
||||
promise: 'Facilitates deliberation across stakeholder perspectives for values conflicts.'
|
||||
}
|
||||
};
|
||||
|
||||
this.activeService = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => this.setup());
|
||||
} else {
|
||||
this.setup();
|
||||
}
|
||||
|
||||
console.log('[InteractiveDiagram] Initialized');
|
||||
}
|
||||
|
||||
setup() {
|
||||
// SVG is loaded via <object> tag, need to access its contentDocument
|
||||
const objectElement = document.getElementById('interactive-svg-object');
|
||||
if (!objectElement) {
|
||||
console.warn('[InteractiveDiagram] SVG object element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for object to load
|
||||
const initializeSVG = () => {
|
||||
const svgDoc = objectElement.contentDocument;
|
||||
if (!svgDoc) {
|
||||
console.warn('[InteractiveDiagram] Could not access SVG contentDocument');
|
||||
return;
|
||||
}
|
||||
|
||||
// The SVG is the document element itself, or we can query for it
|
||||
let svg = svgDoc.getElementById('interactive-arch-diagram');
|
||||
if (!svg) {
|
||||
// Try getting the root SVG element
|
||||
svg = svgDoc.documentElement;
|
||||
console.log('[InteractiveDiagram] Using documentElement as SVG');
|
||||
}
|
||||
|
||||
if (!svg) {
|
||||
console.warn('[InteractiveDiagram] SVG diagram not found in contentDocument');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify it's actually an SVG element (case-insensitive check)
|
||||
const tagName = svg.tagName ? svg.tagName.toLowerCase() : '';
|
||||
if (tagName !== 'svg') {
|
||||
console.warn('[InteractiveDiagram] Element found but not SVG, tagName:', tagName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store reference to SVG document for later use
|
||||
this.svgDoc = svgDoc;
|
||||
this.svg = svg;
|
||||
|
||||
const nodes = svg.querySelectorAll('.service-node');
|
||||
console.log(`[InteractiveDiagram] Found ${nodes.length} service nodes`);
|
||||
|
||||
nodes.forEach(node => {
|
||||
const serviceId = node.getAttribute('data-service');
|
||||
|
||||
node.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.showServiceDetails(serviceId);
|
||||
});
|
||||
|
||||
node.addEventListener('mouseenter', () => {
|
||||
this.highlightService(serviceId);
|
||||
});
|
||||
|
||||
node.addEventListener('mouseleave', () => {
|
||||
this.unhighlightService(serviceId);
|
||||
});
|
||||
});
|
||||
|
||||
this.addKeyboardNavigation(nodes);
|
||||
};
|
||||
|
||||
// If object already loaded, initialize immediately
|
||||
if (objectElement.contentDocument) {
|
||||
initializeSVG();
|
||||
} else {
|
||||
// Otherwise wait for load event
|
||||
objectElement.addEventListener('load', initializeSVG);
|
||||
}
|
||||
}
|
||||
|
||||
highlightService(serviceId) {
|
||||
if (!this.svg) return;
|
||||
|
||||
const connectionLine = this.svg.querySelector(`#conn-${serviceId}`);
|
||||
if (connectionLine) {
|
||||
connectionLine.classList.add('active');
|
||||
}
|
||||
|
||||
const node = this.svg.querySelector(`#node-${serviceId}`);
|
||||
if (node) {
|
||||
node.classList.add('hover');
|
||||
}
|
||||
}
|
||||
|
||||
unhighlightService(serviceId) {
|
||||
if (!this.svg) return;
|
||||
|
||||
if (this.activeService === serviceId) return;
|
||||
|
||||
const connectionLine = this.svg.querySelector(`#conn-${serviceId}`);
|
||||
if (connectionLine) {
|
||||
connectionLine.classList.remove('active');
|
||||
}
|
||||
|
||||
const node = this.svg.querySelector(`#node-${serviceId}`);
|
||||
if (node) {
|
||||
node.classList.remove('hover');
|
||||
}
|
||||
}
|
||||
|
||||
showServiceDetails(serviceId) {
|
||||
const service = this.serviceData[serviceId];
|
||||
if (!service) {
|
||||
console.error('[InteractiveDiagram] Service not found:', serviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.activeService = serviceId;
|
||||
|
||||
if (this.svg) {
|
||||
this.svg.querySelectorAll('.service-node').forEach(n => n.classList.remove('active'));
|
||||
this.svg.querySelectorAll('.connection-line').forEach(l => l.classList.remove('active'));
|
||||
|
||||
const node = this.svg.querySelector(`#node-${serviceId}`);
|
||||
if (node) {
|
||||
node.classList.add('active');
|
||||
}
|
||||
|
||||
const connectionLine = this.svg.querySelector(`#conn-${serviceId}`);
|
||||
if (connectionLine) {
|
||||
connectionLine.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
this.renderServicePanel(service);
|
||||
|
||||
console.log('[InteractiveDiagram] Showing details for:', service.name);
|
||||
}
|
||||
|
||||
renderServicePanel(service) {
|
||||
const panel = document.getElementById('service-detail-panel');
|
||||
|
||||
if (!panel) {
|
||||
console.error('[InteractiveDiagram] Service detail panel not found in DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update border color to match selected service
|
||||
panel.style.borderColor = service.color;
|
||||
panel.style.borderWidth = '2px';
|
||||
|
||||
const html = `
|
||||
<div class="flex items-start mb-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl service-icon-box" data-color="${service.color}">
|
||||
${service.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-bold text-gray-900">${service.name}</h3>
|
||||
<span class="text-xs font-medium text-gray-600 uppercase">${service.shortName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-gray-700 mb-4 leading-relaxed">${service.description}</p>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-2 uppercase">Key Features</h4>
|
||||
<ul class="space-y-2" id="service-features-list">
|
||||
${service.details.map(detail => `
|
||||
<li class="flex items-start text-sm text-gray-700">
|
||||
<svg class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0 service-check-icon" data-color="${service.color}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
|
||||
</svg>
|
||||
<span>${detail}</span>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="text-xs rounded px-3 py-2 bg-opacity-20 service-promise-badge" data-color="${service.color}">
|
||||
<strong class="service-promise-text" data-color="${service.color}">Early Promise:</strong>
|
||||
<span class="text-gray-800">${service.promise}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
panel.innerHTML = html;
|
||||
|
||||
// Apply styles via JavaScript (CSP-compliant)
|
||||
const iconBox = panel.querySelector('.service-icon-box');
|
||||
if (iconBox) {
|
||||
const color = iconBox.getAttribute('data-color');
|
||||
iconBox.style.background = `linear-gradient(135deg, ${color} 0%, ${color}dd 100%)`;
|
||||
}
|
||||
|
||||
// Style all check icons
|
||||
const checkIcons = panel.querySelectorAll('.service-check-icon');
|
||||
checkIcons.forEach(icon => {
|
||||
const color = icon.getAttribute('data-color');
|
||||
icon.style.color = color;
|
||||
});
|
||||
|
||||
// Style promise badge
|
||||
const promiseBadge = panel.querySelector('.service-promise-badge');
|
||||
if (promiseBadge) {
|
||||
const color = promiseBadge.getAttribute('data-color');
|
||||
promiseBadge.style.backgroundColor = color;
|
||||
}
|
||||
|
||||
// Style promise text
|
||||
const promiseText = panel.querySelector('.service-promise-text');
|
||||
if (promiseText) {
|
||||
const color = promiseText.getAttribute('data-color');
|
||||
promiseText.style.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
addKeyboardNavigation(nodes) {
|
||||
nodes.forEach((node, index) => {
|
||||
node.setAttribute('tabindex', '0');
|
||||
node.setAttribute('role', 'button');
|
||||
|
||||
node.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
const serviceId = node.getAttribute('data-service');
|
||||
this.showServiceDetails(serviceId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.interactiveDiagram = new InteractiveDiagram();
|
||||
}
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = InteractiveDiagram;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
(function(){const u=JSON.parse(localStorage.getItem('admin_user')||'{}');const n=u.name||u.email||'Admin';const e=document.getElementById('admin-navbar');if(!e)return;const t=e.dataset.pageTitle||'Admin';const i=e.dataset.pageIcon||'default';const d=window.location.pathname.includes('dashboard.html');const icons={default:'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>',blog:'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>',newsletter:'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>',hooks:'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>'};const s=icons[i]||icons.default;const b=d?'':`<div class="ml-10"><a href="/admin/dashboard.html" class="px-3 py-2 rounded-md text-sm font-medium text-gray-500 hover:text-gray-700">← Dashboard</a></div>`;e.innerHTML=`<nav class="bg-white border-b border-gray-200"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"><div class="flex justify-between h-16"><div class="flex items-center"><div class="flex-shrink-0 flex items-center"><div class="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center"><svg class="h-5 w-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">${s}</svg></div><span class="ml-3 text-xl font-bold text-gray-900">${t}</span></div>${b}</div><div class="flex items-center space-x-4"><span class="text-sm text-gray-600">${n}</span><button id="admin-logout-btn" class="text-sm font-medium text-gray-700 hover:text-gray-900">Logout</button></div></div></div></nav>`;document.getElementById('admin-logout-btn').addEventListener('click',()=>{localStorage.removeItem('admin_token');localStorage.removeItem('admin_user');window.location.href='/admin/login.html';});})();
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
/**
|
||||
* Context Pressure Visualization
|
||||
* Tractatus Framework - Phase 3: Data Visualization
|
||||
*
|
||||
* Visual representation of Context Pressure Monitor metrics
|
||||
* Uses amber color scheme matching the ContextPressureMonitor service
|
||||
*/
|
||||
|
||||
class PressureChart {
|
||||
constructor(containerId, gaugeContainerId = 'pressure-gauge') {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.gaugeContainer = document.getElementById(gaugeContainerId);
|
||||
|
||||
if (!this.container) {
|
||||
console.error(`[PressureChart] Container #${containerId} not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLevel = 0; // 0-100
|
||||
this.targetLevel = 0;
|
||||
this.animating = false;
|
||||
|
||||
this.colors = {
|
||||
low: '#10b981', // Green - NORMAL
|
||||
moderate: '#f59e0b', // Amber - ELEVATED
|
||||
high: '#ef4444', // Red - HIGH
|
||||
critical: '#991b1b' // Dark Red - CRITICAL
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.render();
|
||||
this.attachEventListeners();
|
||||
console.log('[PressureChart] Initialized');
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log('[PressureChart] render() called, container:', this.container);
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h2 class="text-lg font-semibold text-gray-900">Context Pressure Monitor</h2>
|
||||
<div id="pressure-status" class="px-4 py-2 rounded-full text-sm font-bold uppercase bg-green-100 text-green-700 transition-all duration-500">
|
||||
NORMAL
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-3 mb-4">
|
||||
<button id="pressure-simulate-btn" class="flex-1 bg-amber-800 hover:bg-amber-900 text-white px-4 py-3 rounded-lg font-bold text-sm transition-colors">
|
||||
Simulate Pressure
|
||||
</button>
|
||||
<button id="pressure-reset-btn" class="flex-1 bg-gray-900 hover:bg-black text-white px-4 py-3 rounded-lg font-bold text-sm transition-colors">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-blue-50 border border-blue-200 rounded-lg mb-6">
|
||||
<p class="text-sm text-gray-700 leading-relaxed mb-2">
|
||||
<strong>Interactive Demo:</strong> Click "Simulate Pressure" to watch how context pressure builds. As <strong>token usage increases</strong>, tasks become more <strong>complex</strong>, and <strong>error rates rise</strong>. The framework monitors this relationship to detect when AI performance may degrade.
|
||||
</p>
|
||||
<p class="text-xs text-gray-600">
|
||||
The timeline on the right shows how six governance components coordinate to validate each request and maintain safe operation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<svg class="w-full mb-6" viewBox="0 0 300 150" preserveAspectRatio="xMidYMid meet">
|
||||
<path id="gauge-bg" d="M 54 120 A 96 96 0 0 1 246 120"
|
||||
stroke="#e5e7eb" stroke-width="16" fill="none" stroke-linecap="round"/>
|
||||
<path id="gauge-fill" d="M 54 120 A 96 96 0 0 1 54 120"
|
||||
stroke="#f59e0b" stroke-width="16" fill="none" stroke-linecap="round"
|
||||
class="gauge-fill-path"/>
|
||||
<text x="150" y="105" text-anchor="middle" font-size="28" font-weight="bold" fill="#1f2937" id="gauge-value">0%</text>
|
||||
<text x="150" y="125" text-anchor="middle" font-size="12" fill="#6b7280">Pressure Level</text>
|
||||
</svg>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900" id="metric-tokens">0</div>
|
||||
<div class="text-xs text-gray-600">Tokens Used</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900" id="metric-complexity">Low</div>
|
||||
<div class="text-xs text-gray-600">Complexity</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="text-2xl font-bold text-gray-900" id="metric-errors">0</div>
|
||||
<div class="text-xs text-gray-600">Error Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Clear gauge container if it exists (no longer needed)
|
||||
if (this.gaugeContainer) {
|
||||
this.gaugeContainer.innerHTML = '';
|
||||
}
|
||||
|
||||
// Store references
|
||||
this.elements = {
|
||||
gaugeFill: document.getElementById('gauge-fill'),
|
||||
gaugeValue: document.getElementById('gauge-value'),
|
||||
status: document.getElementById('pressure-status'),
|
||||
tokens: document.getElementById('metric-tokens'),
|
||||
complexity: document.getElementById('metric-complexity'),
|
||||
errors: document.getElementById('metric-errors'),
|
||||
simulateBtn: document.getElementById('pressure-simulate-btn'),
|
||||
resetBtn: document.getElementById('pressure-reset-btn')
|
||||
};
|
||||
|
||||
// Verify innerHTML was set
|
||||
console.log('[PressureChart] innerHTML length:', this.container.innerHTML.length);
|
||||
console.log('[PressureChart] First 100 chars:', this.container.innerHTML.substring(0, 100));
|
||||
|
||||
// Verify elements were found
|
||||
console.log('[PressureChart] Elements found:', {
|
||||
gaugeFill: !!this.elements.gaugeFill,
|
||||
gaugeValue: !!this.elements.gaugeValue,
|
||||
status: !!this.elements.status,
|
||||
simulateBtn: !!this.elements.simulateBtn,
|
||||
resetBtn: !!this.elements.resetBtn
|
||||
});
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
if (!this.elements.simulateBtn || !this.elements.resetBtn) {
|
||||
console.error('[PressureChart] Cannot attach event listeners - buttons not found');
|
||||
return;
|
||||
}
|
||||
console.log('[PressureChart] Attaching event listeners to buttons');
|
||||
this.elements.simulateBtn.addEventListener('click', () => this.simulate());
|
||||
this.elements.resetBtn.addEventListener('click', () => this.reset());
|
||||
console.log('[PressureChart] Event listeners attached successfully');
|
||||
}
|
||||
|
||||
setLevel(level) {
|
||||
this.targetLevel = Math.max(0, Math.min(100, level));
|
||||
this.animateToTarget();
|
||||
}
|
||||
|
||||
animateToTarget() {
|
||||
if (this.animating) return;
|
||||
this.animating = true;
|
||||
|
||||
const animate = () => {
|
||||
const diff = this.targetLevel - this.currentLevel;
|
||||
if (Math.abs(diff) < 0.5) {
|
||||
this.currentLevel = this.targetLevel;
|
||||
this.animating = false;
|
||||
this.updateGauge();
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentLevel += diff * 0.1;
|
||||
this.updateGauge();
|
||||
requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
}
|
||||
|
||||
updateGauge() {
|
||||
const level = this.currentLevel;
|
||||
const angle = (level / 100) * 180; // 0-180 degrees
|
||||
const radians = (angle * Math.PI) / 180;
|
||||
|
||||
// Calculate arc endpoint (20% smaller gauge: radius 96 instead of 120)
|
||||
const centerX = 150;
|
||||
const centerY = 120;
|
||||
const radius = 96;
|
||||
const startX = 54;
|
||||
const startY = 120;
|
||||
const endX = centerX + radius * Math.cos(Math.PI - radians);
|
||||
const endY = centerY - radius * Math.sin(Math.PI - radians);
|
||||
|
||||
const largeArcFlag = angle > 180 ? 1 : 0;
|
||||
const path = `M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`;
|
||||
|
||||
this.elements.gaugeFill.setAttribute('d', path);
|
||||
this.elements.gaugeValue.textContent = `${Math.round(level)}%`;
|
||||
|
||||
// Update color based on level
|
||||
let color, status;
|
||||
if (level < 25) {
|
||||
color = this.colors.low;
|
||||
status = 'NORMAL';
|
||||
} else if (level < 50) {
|
||||
color = this.colors.moderate;
|
||||
status = 'ELEVATED';
|
||||
} else if (level < 75) {
|
||||
color = this.colors.high;
|
||||
status = 'HIGH';
|
||||
} else {
|
||||
color = this.colors.critical;
|
||||
status = 'CRITICAL';
|
||||
}
|
||||
|
||||
this.elements.gaugeFill.setAttribute('stroke', color);
|
||||
|
||||
// Update status badge with animation
|
||||
const previousStatus = this.elements.status.textContent;
|
||||
this.elements.status.textContent = status;
|
||||
|
||||
// Badge styling based on level
|
||||
const baseClasses = 'px-4 py-2 rounded-full text-sm font-bold uppercase transition-all duration-500';
|
||||
let bgClass, textClass;
|
||||
|
||||
if (level < 25) {
|
||||
bgClass = 'bg-green-100';
|
||||
textClass = 'text-green-700';
|
||||
} else if (level < 50) {
|
||||
bgClass = 'bg-amber-100';
|
||||
textClass = 'text-amber-700';
|
||||
} else if (level < 75) {
|
||||
bgClass = 'bg-red-100';
|
||||
textClass = 'text-red-700';
|
||||
} else {
|
||||
bgClass = 'bg-red-200';
|
||||
textClass = 'text-red-900';
|
||||
}
|
||||
|
||||
// Add pulse animation when status changes
|
||||
const pulseClass = previousStatus !== status ? 'animate-pulse' : '';
|
||||
this.elements.status.className = `${baseClasses} ${bgClass} ${textClass} ${pulseClass}`;
|
||||
|
||||
// Remove pulse after animation
|
||||
if (pulseClass) {
|
||||
setTimeout(() => {
|
||||
this.elements.status.className = `${baseClasses} ${bgClass} ${textClass}`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Update metrics based on pressure level
|
||||
const tokens = Math.round(level * 2000); // 0-200k tokens
|
||||
const complexityLevels = ['Low', 'Moderate', 'High', 'Extreme'];
|
||||
const complexityIndex = Math.min(3, Math.floor(level / 25));
|
||||
const errorRate = Math.round(level / 5); // 0-20%
|
||||
|
||||
this.elements.tokens.textContent = tokens.toLocaleString();
|
||||
this.elements.complexity.textContent = complexityLevels[complexityIndex];
|
||||
this.elements.errors.textContent = `${errorRate}%`;
|
||||
}
|
||||
|
||||
simulate() {
|
||||
console.log('[PressureChart] Simulate button clicked - starting pressure simulation');
|
||||
|
||||
// Trigger timeline simulation if available
|
||||
if (window.activityTimeline) {
|
||||
console.log('[PressureChart] Triggering governance flow timeline');
|
||||
window.activityTimeline.simulateFlow();
|
||||
}
|
||||
|
||||
// Simulate pressure increasing from current to 85%
|
||||
const targetLevels = [30, 50, 70, 85];
|
||||
let index = 0;
|
||||
|
||||
const step = () => {
|
||||
if (index >= targetLevels.length) return;
|
||||
console.log('[PressureChart] Setting pressure level to', targetLevels[index]);
|
||||
this.setLevel(targetLevels[index]);
|
||||
index++;
|
||||
setTimeout(step, 1500);
|
||||
};
|
||||
|
||||
step();
|
||||
}
|
||||
|
||||
reset() {
|
||||
console.log('[PressureChart] Reset button clicked');
|
||||
|
||||
// Reset timeline if available
|
||||
if (window.activityTimeline) {
|
||||
console.log('[PressureChart] Resetting governance flow timeline');
|
||||
window.activityTimeline.reset();
|
||||
}
|
||||
|
||||
this.setLevel(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-initialize if container exists
|
||||
if (typeof window !== 'undefined') {
|
||||
function initPressureChart() {
|
||||
console.log('[PressureChart] Attempting to initialize, readyState:', document.readyState);
|
||||
const container = document.getElementById('pressure-chart');
|
||||
if (container) {
|
||||
console.log('[PressureChart] Container found, creating instance');
|
||||
window.pressureChart = new PressureChart('pressure-chart');
|
||||
} else {
|
||||
console.error('[PressureChart] Container #pressure-chart not found in DOM');
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize immediately if DOM is already loaded, otherwise wait for DOMContentLoaded
|
||||
console.log('[PressureChart] Script loaded, readyState:', document.readyState);
|
||||
if (document.readyState === 'loading') {
|
||||
console.log('[PressureChart] Waiting for DOMContentLoaded');
|
||||
document.addEventListener('DOMContentLoaded', initPressureChart);
|
||||
} else {
|
||||
console.log('[PressureChart] DOM already loaded, initializing immediately');
|
||||
initPressureChart();
|
||||
}
|
||||
}
|
||||
|
||||
// Export for module systems
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = PressureChart;
|
||||
}
|
||||
|
|
@ -1,326 +0,0 @@
|
|||
const steps = [
|
||||
{
|
||||
title: 'User Instruction',
|
||||
type: 'user',
|
||||
content: 'User: "find the lost conversation threads. 27027 family-history collection should be there"',
|
||||
code: null,
|
||||
description: 'User specifies MongoDB is on port 27027 (non-standard port where data is located)'
|
||||
},
|
||||
{
|
||||
title: 'AI Pattern Recognition Activates',
|
||||
type: 'info',
|
||||
content: 'AI Internal: Training data pattern detected: "MongoDB" → default port 27017',
|
||||
code: `// AI's learned pattern from training data:
|
||||
// MongoDB almost always runs on port 27017
|
||||
// Confidence: 99.8% (seen in millions of examples)
|
||||
//
|
||||
// User said: "port 27027"
|
||||
// Pattern says: "port 27017"
|
||||
//
|
||||
// Pattern recognition OVERRIDES explicit instruction`,
|
||||
description: 'Strong training pattern conflicts with explicit user instruction'
|
||||
},
|
||||
{
|
||||
title: 'AI Executes Query (IMMEDIATE OVERRIDE)',
|
||||
type: 'ai',
|
||||
content: 'AI: "Let me check the database..."',
|
||||
code: `mongosh mongodb://localhost:27017/family_history
|
||||
# ^^^^^ WRONG! User said 27027!
|
||||
|
||||
# AI's pattern recognition automatically "corrected"
|
||||
# the user's explicit port specification
|
||||
# MongoDB = port 27017 (99.8% confidence from training)`,
|
||||
description: 'AI immediately uses 27017 instead of 27027—pattern recognition autocorrected the explicit instruction'
|
||||
},
|
||||
{
|
||||
title: 'False Data Loss Alarm',
|
||||
type: 'error',
|
||||
content: '❌ Result: 0 conversation threads found → FALSE ALARM: "Data is lost!"',
|
||||
code: `# Checked port 27017 (wrong database instance)
|
||||
db.conversations.countDocuments({})
|
||||
→ 0 results
|
||||
|
||||
# AI concludes: "No data found. Data appears to be lost!"
|
||||
# Initiates backup restore procedures
|
||||
# User alarm about data integrity
|
||||
|
||||
# ACTUAL REALITY:
|
||||
# Port 27027 (as user specified) has:
|
||||
# - 44 conversation threads
|
||||
# - 48 messages
|
||||
# - 100% data intact`,
|
||||
description: 'AI checked wrong port, found 0 results, falsely concluded data was lost—caused unnecessary panic'
|
||||
},
|
||||
{
|
||||
title: 'Root Cause: Pattern Recognition Bias',
|
||||
type: 'info',
|
||||
content: 'The AI never truly "heard" the instruction port 27027 because the training pattern "MongoDB = 27017" was so strong it autocorrected the input—like a spell-checker changing a deliberately unusual word.',
|
||||
code: null,
|
||||
description: 'This is NOT forgetting over time. It\'s immediate override by learned patterns.'
|
||||
},
|
||||
{
|
||||
title: 'Why This Is Dangerous',
|
||||
type: 'info',
|
||||
content: 'Key insight: This failure mode gets WORSE as AI capabilities increase!',
|
||||
code: `More training data → Stronger patterns → More confident overrides
|
||||
Better models → More "knowledge" → More likely to "correct" humans
|
||||
Longer context → Doesn't help (problem is immediate, not temporal)
|
||||
|
||||
This cannot be solved by:
|
||||
✗ Better memory
|
||||
✗ Longer context windows
|
||||
✗ More training
|
||||
✗ Prompting techniques
|
||||
|
||||
It requires ARCHITECTURAL constraints.`,
|
||||
description: 'Pattern recognition bias is a fundamental AI safety issue that training alone cannot solve'
|
||||
},
|
||||
{
|
||||
title: 'How Tractatus Prevents This (Step 1)',
|
||||
type: 'success',
|
||||
content: 'InstructionPersistenceClassifier recognizes explicit instruction:',
|
||||
code: `// When user says "27027 family-history collection should be there"
|
||||
{
|
||||
text: "27027 family-history collection should be there",
|
||||
quadrant: "TACTICAL",
|
||||
persistence: "HIGH", // Non-standard port = explicit override
|
||||
temporal_scope: "SESSION",
|
||||
verification_required: "MANDATORY",
|
||||
parameters: {
|
||||
port: "27027",
|
||||
database: "family_history",
|
||||
note: "Conflicts with training pattern (27017)"
|
||||
},
|
||||
explicitness: 0.92
|
||||
}
|
||||
|
||||
// Stored in .claude/instruction-history.json
|
||||
✓ Instruction persisted with HIGH priority`,
|
||||
description: 'Tractatus stores the explicit instruction before AI executes any database query'
|
||||
},
|
||||
{
|
||||
title: 'How Tractatus Prevents This (Step 2)',
|
||||
type: 'success',
|
||||
content: 'CrossReferenceValidator blocks the pattern override BEFORE execution:',
|
||||
code: `// When AI attempts to query with port 27017
|
||||
CrossReferenceValidator.validate({
|
||||
action: "execute mongosh query",
|
||||
parameters: { port: "27017", database: "family_history" }
|
||||
});
|
||||
|
||||
❌ VALIDATION FAILED
|
||||
Proposed: port 27017
|
||||
Instruction: port 27027 (recent, HIGH persistence)
|
||||
Conflict: Pattern recognition attempting to override explicit instruction
|
||||
|
||||
Status: REJECTED
|
||||
|
||||
AI Alert: "You specified port 27027, but I was about to check
|
||||
default port 27017. Querying port 27027 as specified."
|
||||
|
||||
✓ Correct query executed:
|
||||
mongosh mongodb://localhost:27027/family_history
|
||||
✓ Result: 44 conversation threads found (data intact!)`,
|
||||
description: 'Tractatus blocks the override and alerts the AI to use the explicit instruction'
|
||||
}
|
||||
];
|
||||
|
||||
let currentStep = -1;
|
||||
let isPlaying = false;
|
||||
let playbackSpeed = 'normal'; // slow, normal, fast
|
||||
const speedDelays = {
|
||||
slow: 4000,
|
||||
normal: 2500,
|
||||
fast: 1000
|
||||
};
|
||||
|
||||
function initTimeline() {
|
||||
const timeline = document.getElementById('timeline');
|
||||
timeline.innerHTML = steps.map((step, index) => `
|
||||
<div id="step-${index}" class="border-2 border-gray-300 bg-white rounded-lg p-6 transition-all duration-300 cursor-pointer hover:shadow-lg" data-step-index="${index}">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<div class="w-10 h-10 rounded-full ${getStepColor(step.type)} flex items-center justify-center text-white font-bold">
|
||||
${index + 1}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">${step.title}</h3>
|
||||
<p class="text-gray-700 mb-3">${step.content}</p>
|
||||
${step.code ? `<pre class="code-block">${escapeHtml(step.code)}</pre>` : ''}
|
||||
<p class="text-sm text-gray-500 mt-2 hidden step-description">${step.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers to steps for navigation
|
||||
document.querySelectorAll('[data-step-index]').forEach(stepEl => {
|
||||
stepEl.addEventListener('click', () => {
|
||||
if (!isPlaying) {
|
||||
const index = parseInt(stepEl.getAttribute('data-step-index'));
|
||||
showStep(index);
|
||||
document.getElementById('progress-info').classList.remove('hidden');
|
||||
document.getElementById('service-status').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStepColor(type) {
|
||||
const colors = {
|
||||
user: 'bg-blue-500',
|
||||
ai: 'bg-purple-500',
|
||||
info: 'bg-gray-500',
|
||||
error: 'bg-red-500',
|
||||
success: 'bg-green-500'
|
||||
};
|
||||
return colors[type] || 'bg-gray-500';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
async function playScenario() {
|
||||
if (isPlaying) return;
|
||||
isPlaying = true;
|
||||
|
||||
document.getElementById('start-btn').disabled = true;
|
||||
document.getElementById('progress-info').classList.remove('hidden');
|
||||
document.getElementById('service-status').classList.remove('hidden');
|
||||
|
||||
for (let i = 0; i <= steps.length - 1; i++) {
|
||||
await showStep(i);
|
||||
if (i < steps.length - 1) {
|
||||
await delay(speedDelays[playbackSpeed]);
|
||||
}
|
||||
}
|
||||
|
||||
isPlaying = false;
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
document.getElementById('start-btn').innerHTML = '▶ Replay';
|
||||
}
|
||||
|
||||
async function showStep(index) {
|
||||
currentStep = index;
|
||||
|
||||
// Mark previous steps as complete
|
||||
for (let i = 0; i < index; i++) {
|
||||
const stepEl = document.getElementById(`step-${i}`);
|
||||
stepEl.classList.remove('step-active');
|
||||
stepEl.classList.add('step-complete', 'border-green-500', 'bg-green-50');
|
||||
}
|
||||
|
||||
// Mark future steps as pending
|
||||
for (let i = index + 1; i < steps.length; i++) {
|
||||
const stepEl = document.getElementById(`step-${i}`);
|
||||
stepEl.className = 'border-2 border-gray-300 bg-white rounded-lg p-6 transition-all duration-300 cursor-pointer hover:shadow-lg';
|
||||
stepEl.querySelector('.step-description').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Mark current step as active
|
||||
const currentStepEl = document.getElementById(`step-${index}`);
|
||||
currentStepEl.classList.add('step-active', 'border-blue-500', 'bg-blue-50', 'fade-in');
|
||||
currentStepEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
|
||||
// Show description
|
||||
currentStepEl.querySelector('.step-description').classList.remove('hidden');
|
||||
|
||||
// Handle error step
|
||||
if (steps[index].type === 'error') {
|
||||
currentStepEl.classList.remove('step-active', 'border-blue-500', 'bg-blue-50');
|
||||
currentStepEl.classList.add('step-error', 'border-red-500', 'bg-red-50');
|
||||
}
|
||||
|
||||
// Update progress
|
||||
const progress = ((index + 1) / steps.length) * 100;
|
||||
document.getElementById('progress-bar').style.width = `${progress}%`;
|
||||
document.getElementById('progress-text').textContent = `${index + 1} / ${steps.length}`;
|
||||
document.getElementById('current-step-desc').textContent = steps[index].description;
|
||||
|
||||
// Highlight active services
|
||||
updateServiceStatus(index);
|
||||
}
|
||||
|
||||
function updateServiceStatus(stepIndex) {
|
||||
const instructionService = document.getElementById('service-instruction');
|
||||
const validatorService = document.getElementById('service-validator');
|
||||
|
||||
// Reset both services to inactive
|
||||
instructionService.classList.remove('opacity-100', 'bg-indigo-50', 'ring-2', 'ring-indigo-400');
|
||||
instructionService.classList.add('opacity-30', 'bg-gray-50');
|
||||
validatorService.classList.remove('opacity-100', 'bg-purple-50', 'ring-2', 'ring-purple-400');
|
||||
validatorService.classList.add('opacity-30', 'bg-gray-50');
|
||||
|
||||
// Step 6: InstructionPersistence activates
|
||||
if (stepIndex === 6) {
|
||||
instructionService.classList.remove('opacity-30', 'bg-gray-50');
|
||||
instructionService.classList.add('opacity-100', 'bg-indigo-50', 'ring-2', 'ring-indigo-400');
|
||||
}
|
||||
|
||||
// Step 7: CrossReferenceValidator activates
|
||||
if (stepIndex === 7) {
|
||||
instructionService.classList.remove('opacity-30', 'bg-gray-50');
|
||||
instructionService.classList.add('opacity-100', 'bg-indigo-50');
|
||||
validatorService.classList.remove('opacity-30', 'bg-gray-50');
|
||||
validatorService.classList.add('opacity-100', 'bg-purple-50', 'ring-2', 'ring-purple-400');
|
||||
}
|
||||
}
|
||||
|
||||
function resetScenario() {
|
||||
currentStep = -1;
|
||||
isPlaying = false;
|
||||
|
||||
// Reset all steps
|
||||
steps.forEach((_, index) => {
|
||||
const stepEl = document.getElementById(`step-${index}`);
|
||||
stepEl.className = 'border-2 border-gray-300 bg-white rounded-lg p-6 transition-all duration-300 cursor-pointer hover:shadow-lg';
|
||||
stepEl.setAttribute('data-step-index', index);
|
||||
stepEl.querySelector('.step-description').classList.add('hidden');
|
||||
});
|
||||
|
||||
document.getElementById('progress-bar').style.width = '0%';
|
||||
document.getElementById('progress-text').textContent = `0 / ${steps.length}`;
|
||||
document.getElementById('current-step-desc').textContent = '';
|
||||
document.getElementById('progress-info').classList.add('hidden');
|
||||
document.getElementById('service-status').classList.add('hidden');
|
||||
document.getElementById('start-btn').innerHTML = '▶ Start Scenario';
|
||||
document.getElementById('start-btn').disabled = false;
|
||||
|
||||
// Reset services
|
||||
updateServiceStatus(-1);
|
||||
}
|
||||
|
||||
function delay(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// Speed control event listeners
|
||||
function setPlaybackSpeed(speed) {
|
||||
playbackSpeed = speed;
|
||||
document.querySelectorAll('.speed-btn').forEach(btn => {
|
||||
if (btn.getAttribute('data-speed') === speed) {
|
||||
btn.classList.remove('bg-gray-200', 'hover:bg-gray-300', 'text-gray-700');
|
||||
btn.classList.add('bg-blue-600', 'text-white');
|
||||
} else {
|
||||
btn.classList.remove('bg-blue-600', 'text-white');
|
||||
btn.classList.add('bg-gray-200', 'hover:bg-gray-300', 'text-gray-700');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('start-btn').addEventListener('click', playScenario);
|
||||
document.getElementById('reset-btn').addEventListener('click', resetScenario);
|
||||
|
||||
document.querySelectorAll('.speed-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
setPlaybackSpeed(btn.getAttribute('data-speed'));
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initTimeline();
|
||||
|
|
@ -1,458 +0,0 @@
|
|||
// Boundary check with API integration and fallback
|
||||
async function checkBoundary(decision, description) {
|
||||
try {
|
||||
// Try API first
|
||||
const response = await fetch('/api/demo/boundary-check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ decision, description })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return {
|
||||
title: decision.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
description: description,
|
||||
allowed: data.enforcement.allowed,
|
||||
reason: data.enforcement.reasoning,
|
||||
alternatives: data.enforcement.alternatives.length > 0 ? data.enforcement.alternatives : null,
|
||||
boundary_violated: data.enforcement.boundary_violated,
|
||||
api_result: true
|
||||
};
|
||||
}
|
||||
|
||||
// If API fails, fall back to client-side scenarios
|
||||
console.warn('API unavailable, using client-side scenario data');
|
||||
return scenarioFallback[decision] || getDefaultScenario(decision, description);
|
||||
} catch (error) {
|
||||
console.warn('Error calling API, using client-side scenario data:', error);
|
||||
return scenarioFallback[decision] || getDefaultScenario(decision, description);
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side fallback scenarios
|
||||
const scenarioFallback = {
|
||||
optimize_images: {
|
||||
title: "Optimize Image Loading",
|
||||
description: "Implement lazy loading and compression for better performance",
|
||||
domain: "technical",
|
||||
allowed: true,
|
||||
reason: "Technical optimization within defined parameters. No values trade-offs required.",
|
||||
alternatives: null,
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'performance_optimization',
|
||||
action: 'implement_lazy_loading'
|
||||
});
|
||||
|
||||
// Result: ALLOWED
|
||||
{
|
||||
allowed: true,
|
||||
reason: "Technical decision, no values impact",
|
||||
proceed: true
|
||||
}`
|
||||
},
|
||||
privacy_vs_analytics: {
|
||||
title: "Enable Analytics Tracking",
|
||||
description: "Add Google Analytics to track user behavior",
|
||||
domain: "values",
|
||||
allowed: false,
|
||||
reason: "Privacy vs. analytics is an irreducible values trade-off. Different users have different privacy expectations.",
|
||||
alternatives: [
|
||||
"Research privacy-friendly analytics options (e.g., Plausible, Fathom)",
|
||||
"Analyze current user behavior from server logs",
|
||||
"Document pros/cons of different analytics approaches",
|
||||
"Present options with privacy impact assessment"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'privacy_policy',
|
||||
action: 'enable_tracking',
|
||||
domain: 'values'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Privacy vs. convenience trade-off",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.1"
|
||||
}`
|
||||
},
|
||||
auto_subscribe: {
|
||||
title: "Auto-Subscribe Users",
|
||||
description: "Automatically subscribe new users to newsletter",
|
||||
domain: "user_agency",
|
||||
allowed: false,
|
||||
reason: "This determines the level of user control and agency. Opt-in vs. opt-out affects user autonomy.",
|
||||
alternatives: [
|
||||
"Implement explicit opt-in during registration",
|
||||
"Implement opt-out with clear unsubscribe",
|
||||
"Research industry best practices for consent",
|
||||
"Document GDPR compliance implications"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'user_consent',
|
||||
action: 'auto_subscribe',
|
||||
domain: 'user_agency'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Affects user agency and control",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.2"
|
||||
}`
|
||||
},
|
||||
delete_old_data: {
|
||||
title: "Delete Old User Data",
|
||||
description: "Automatically delete user data older than 6 months",
|
||||
domain: "irreversible",
|
||||
allowed: false,
|
||||
reason: "Data deletion is irreversible and may have legal/compliance implications.",
|
||||
alternatives: [
|
||||
"Check backup status and retention policies",
|
||||
"Verify legal data retention requirements",
|
||||
"Confirm user consent for deletion",
|
||||
"Implement archive rather than delete"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'data_deletion',
|
||||
action: 'delete_user_data',
|
||||
domain: 'irreversible'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Irreversible action with legal implications",
|
||||
requires_human_approval: true,
|
||||
boundary_section: "12.3"
|
||||
}`
|
||||
},
|
||||
cache_strategy: {
|
||||
title: "Implement Caching Strategy",
|
||||
description: "Add Redis caching for frequently accessed data",
|
||||
domain: "technical",
|
||||
allowed: true,
|
||||
reason: "Technical implementation decision within established patterns. No values impact.",
|
||||
alternatives: null,
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'technical_implementation',
|
||||
action: 'add_caching'
|
||||
});
|
||||
|
||||
// Result: ALLOWED
|
||||
{
|
||||
allowed: true,
|
||||
reason: "Technical decision with clear constraints",
|
||||
proceed: true
|
||||
}`
|
||||
},
|
||||
content_moderation: {
|
||||
title: "Automatic Content Moderation",
|
||||
description: "AI automatically removes inappropriate content",
|
||||
domain: "values",
|
||||
allowed: false,
|
||||
reason: "Defining 'inappropriate' involves values judgments about free speech, community standards, and cultural context.",
|
||||
alternatives: [
|
||||
"Implement flagging system for human review",
|
||||
"Create tiered moderation (AI flags, human decides)",
|
||||
"Research community moderation models",
|
||||
"Document content policy options for decision"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'content_policy',
|
||||
action: 'auto_moderate',
|
||||
domain: 'values'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Content standards are values decisions",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.1"
|
||||
}`
|
||||
},
|
||||
api_rate_limiting: {
|
||||
title: "Implement API Rate Limiting",
|
||||
description: "Add rate limiting to prevent API abuse (100 req/min per IP)",
|
||||
domain: "technical",
|
||||
allowed: true,
|
||||
reason: "Technical security measure with clear, predefined parameters. No values trade-offs.",
|
||||
alternatives: null,
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'security_measure',
|
||||
action: 'add_rate_limiting'
|
||||
});
|
||||
|
||||
// Result: ALLOWED
|
||||
{
|
||||
allowed: true,
|
||||
reason: "Technical implementation of security best practice",
|
||||
proceed: true
|
||||
}`
|
||||
},
|
||||
ml_training_data: {
|
||||
title: "Collect Data for ML Training",
|
||||
description: "Use user data to train machine learning models for feature improvements",
|
||||
domain: "values",
|
||||
allowed: false,
|
||||
reason: "Data usage for ML training involves privacy trade-offs, consent considerations, and potential bias issues that require ethical judgment.",
|
||||
alternatives: [
|
||||
"Research consent mechanisms and opt-in approaches",
|
||||
"Analyze privacy-preserving ML techniques (federated learning, differential privacy)",
|
||||
"Document data usage policies and transparency requirements",
|
||||
"Assess potential bias and fairness implications"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'data_usage',
|
||||
action: 'ml_training',
|
||||
domain: 'values'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Privacy and consent decisions require human judgment",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.1"
|
||||
}`
|
||||
},
|
||||
auto_password_reset: {
|
||||
title: "Automated Password Reset",
|
||||
description: "Automatically reset user password after 5 failed login attempts",
|
||||
domain: "security",
|
||||
allowed: false,
|
||||
reason: "Automated password resets have security implications and can be used for denial-of-service attacks. Requires careful security analysis.",
|
||||
alternatives: [
|
||||
"Implement account lockout with unlock email instead",
|
||||
"Add CAPTCHA after failed attempts",
|
||||
"Research industry security best practices",
|
||||
"Consider multi-factor authentication requirements"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'security_action',
|
||||
action: 'auto_password_reset',
|
||||
domain: 'security'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Security implications require human review",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.3"
|
||||
}`
|
||||
},
|
||||
database_indexing: {
|
||||
title: "Add Database Indexes",
|
||||
description: "Create indexes on frequently queried columns to improve performance",
|
||||
domain: "technical",
|
||||
allowed: true,
|
||||
reason: "Standard database optimization with measurable benefits and no values implications.",
|
||||
alternatives: null,
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'database_optimization',
|
||||
action: 'add_indexes'
|
||||
});
|
||||
|
||||
// Result: ALLOWED
|
||||
{
|
||||
allowed: true,
|
||||
reason: "Technical optimization following best practices",
|
||||
proceed: true
|
||||
}`
|
||||
},
|
||||
default_public_sharing: {
|
||||
title: "Default Public Sharing",
|
||||
description: "Make user posts public by default (users can change to private)",
|
||||
domain: "user_agency",
|
||||
allowed: false,
|
||||
reason: "Privacy defaults affect user expectations and control. Public vs. private defaults shape user behavior and trust.",
|
||||
alternatives: [
|
||||
"Research user expectations for similar platforms",
|
||||
"Analyze privacy-by-default vs. visibility-by-default trade-offs",
|
||||
"Consider gradual disclosure approach",
|
||||
"Document implications for different user groups"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'privacy_defaults',
|
||||
action: 'public_by_default',
|
||||
domain: 'user_agency'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Privacy defaults affect user agency and expectations",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.2"
|
||||
}`
|
||||
},
|
||||
error_logging_pii: {
|
||||
title: "Log All Error Details",
|
||||
description: "Include full request data in error logs for debugging (may contain PII)",
|
||||
domain: "values",
|
||||
allowed: false,
|
||||
reason: "Logging PII involves privacy trade-offs between debugging needs and data protection. GDPR and privacy regulations apply.",
|
||||
alternatives: [
|
||||
"Implement PII scrubbing in logs",
|
||||
"Research structured logging with sensitive data redaction",
|
||||
"Document data retention policies",
|
||||
"Consider encrypted logging with access controls"
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'logging_policy',
|
||||
action: 'log_full_errors',
|
||||
domain: 'values'
|
||||
});
|
||||
|
||||
// Result: BLOCKED
|
||||
{
|
||||
allowed: false,
|
||||
reason: "PII handling requires privacy impact assessment",
|
||||
requires_human_decision: true,
|
||||
boundary_section: "12.1"
|
||||
}`
|
||||
}
|
||||
};
|
||||
|
||||
// Default scenario for unknown decisions
|
||||
function getDefaultScenario(decision, description) {
|
||||
return {
|
||||
title: decision.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
||||
description: description || 'No description provided',
|
||||
allowed: false,
|
||||
reason: 'This decision requires human judgment to determine appropriate boundaries.',
|
||||
alternatives: [
|
||||
'Consult with stakeholders about decision criteria',
|
||||
'Research similar decisions in comparable contexts',
|
||||
'Document pros and cons of different approaches'
|
||||
],
|
||||
code: `// BoundaryEnforcer Check
|
||||
const boundary = enforcer.enforce({
|
||||
type: 'unknown',
|
||||
action: '${decision}'
|
||||
});
|
||||
|
||||
// Result: REQUIRES_REVIEW
|
||||
{
|
||||
allowed: false,
|
||||
reason: "Insufficient information for automated decision",
|
||||
requires_human_decision: true
|
||||
}`
|
||||
};
|
||||
}
|
||||
|
||||
// Map scenarios for display - adds code examples
|
||||
const scenarios = Object.fromEntries(
|
||||
Object.entries(scenarioFallback).map(([key, value]) => [key, value])
|
||||
);
|
||||
|
||||
// Event listeners
|
||||
document.querySelectorAll('.scenario-card').forEach(card => {
|
||||
card.addEventListener('click', async () => {
|
||||
const decision = card.getAttribute('data-decision');
|
||||
const scenario = scenarios[decision];
|
||||
|
||||
// Show loading state
|
||||
const originalContent = card.innerHTML;
|
||||
card.style.opacity = '0.6';
|
||||
card.style.pointerEvents = 'none';
|
||||
|
||||
// Highlight selected
|
||||
document.querySelectorAll('.scenario-card').forEach(c => {
|
||||
c.classList.remove('ring-2', 'ring-blue-500');
|
||||
});
|
||||
card.classList.add('ring-2', 'ring-blue-500');
|
||||
|
||||
try {
|
||||
// Call API with scenario details
|
||||
const result = await checkBoundary(decision, scenario.description);
|
||||
|
||||
// Merge API result with scenario code example
|
||||
const displayData = {
|
||||
...result,
|
||||
code: scenario.code
|
||||
};
|
||||
|
||||
showResult(displayData);
|
||||
} catch (error) {
|
||||
console.error('Error checking boundary:', error);
|
||||
showResult(scenario);
|
||||
} finally {
|
||||
card.style.opacity = '1';
|
||||
card.style.pointerEvents = 'auto';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function showResult(scenario) {
|
||||
document.getElementById('empty-state').classList.add('hidden');
|
||||
document.getElementById('result-content').classList.remove('hidden');
|
||||
|
||||
// Decision info
|
||||
document.getElementById('decision-title').textContent = scenario.title;
|
||||
document.getElementById('decision-desc').textContent = scenario.description;
|
||||
|
||||
// Verdict
|
||||
const verdict = document.getElementById('verdict');
|
||||
if (scenario.allowed) {
|
||||
verdict.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<svg class="w-8 h-8 text-green-600 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-green-900 mb-1">✅ ALLOWED</div>
|
||||
<div class="text-green-800">AI can automate this decision</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
verdict.className = 'rounded-lg p-6 mb-6 bg-green-100 border border-green-300';
|
||||
} else {
|
||||
verdict.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<svg class="w-8 h-8 text-red-600 mr-3 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="text-lg font-semibold text-red-900 mb-1">🚫 BLOCKED</div>
|
||||
<div class="text-red-800">Requires human judgment</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
verdict.className = 'rounded-lg p-6 mb-6 bg-red-100 border border-red-300';
|
||||
}
|
||||
|
||||
// Reasoning
|
||||
document.getElementById('reasoning').textContent = scenario.reason;
|
||||
|
||||
// Alternatives
|
||||
if (scenario.alternatives) {
|
||||
document.getElementById('ai-alternatives').classList.remove('hidden');
|
||||
document.getElementById('alternatives-list').innerHTML = scenario.alternatives
|
||||
.map(alt => `<li>${alt}</li>`)
|
||||
.join('');
|
||||
} else {
|
||||
document.getElementById('ai-alternatives').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Code example
|
||||
document.getElementById('code-example').textContent = scenario.code;
|
||||
}
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
// Classification with API integration and fallback
|
||||
async function classifyInstruction(text) {
|
||||
try {
|
||||
// Try API first
|
||||
const response = await fetch('/api/demo/classify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ instruction: text })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
return data.classification;
|
||||
}
|
||||
|
||||
// If API fails, fall back to client-side classification
|
||||
console.warn('API unavailable, using client-side classification');
|
||||
return classifyInstructionClientSide(text);
|
||||
} catch (error) {
|
||||
console.warn('Error calling API, using client-side classification:', error);
|
||||
return classifyInstructionClientSide(text);
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side fallback classification
|
||||
function classifyInstructionClientSide(text) {
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
let quadrant, persistence, temporal, verification, explicitness, reasoning;
|
||||
|
||||
// Detect quadrant
|
||||
if (lower.includes('privacy') || lower.includes('values') || lower.includes('mission') || lower.includes('ethics')) {
|
||||
quadrant = 'STRATEGIC';
|
||||
persistence = 'HIGH';
|
||||
temporal = 'PERMANENT';
|
||||
verification = 'MANDATORY';
|
||||
explicitness = 0.90;
|
||||
reasoning = 'Contains values-related keywords indicating strategic importance';
|
||||
} else if (lower.includes('port') || lower.includes('database') || lower.includes('mongodb') || lower.includes('server')) {
|
||||
quadrant = 'SYSTEM';
|
||||
persistence = 'HIGH';
|
||||
temporal = 'PROJECT';
|
||||
verification = 'MANDATORY';
|
||||
explicitness = 0.85;
|
||||
reasoning = 'Technical infrastructure configuration that must persist across project';
|
||||
} else if (lower.includes('all') || lower.includes('must') || lower.includes('always') && (lower.includes('api') || lower.includes('format'))) {
|
||||
quadrant = 'OPERATIONAL';
|
||||
persistence = 'MEDIUM';
|
||||
temporal = 'PROJECT';
|
||||
verification = 'REQUIRED';
|
||||
explicitness = 0.75;
|
||||
reasoning = 'Standard operating procedure for consistent project implementation';
|
||||
} else if (lower.includes('console.log') || lower.includes('debug') || lower.includes('here')) {
|
||||
quadrant = 'TACTICAL';
|
||||
persistence = 'LOW';
|
||||
temporal = 'TASK';
|
||||
verification = 'OPTIONAL';
|
||||
explicitness = 0.70;
|
||||
reasoning = 'Specific task-level instruction with limited temporal scope';
|
||||
} else if (lower.includes('explore') || lower.includes('try') || lower.includes('different approaches')) {
|
||||
quadrant = 'STOCHASTIC';
|
||||
persistence = 'VARIABLE';
|
||||
temporal = 'PHASE';
|
||||
verification = 'NONE';
|
||||
explicitness = 0.50;
|
||||
reasoning = 'Exploratory directive with open-ended outcome';
|
||||
} else {
|
||||
quadrant = 'OPERATIONAL';
|
||||
persistence = 'MEDIUM';
|
||||
temporal = 'PROJECT';
|
||||
verification = 'REQUIRED';
|
||||
explicitness = 0.65;
|
||||
reasoning = 'General instruction defaulting to operational classification';
|
||||
}
|
||||
|
||||
return {
|
||||
quadrant,
|
||||
persistence,
|
||||
temporal_scope: temporal,
|
||||
verification_required: verification,
|
||||
explicitness,
|
||||
reasoning
|
||||
};
|
||||
}
|
||||
|
||||
// Description mappings
|
||||
const descriptions = {
|
||||
quadrant: {
|
||||
STRATEGIC: 'Mission-critical decisions affecting values, privacy, or core principles',
|
||||
OPERATIONAL: 'Standard procedures and conventions for consistent operation',
|
||||
TACTICAL: 'Specific tasks with defined scope and completion criteria',
|
||||
SYSTEM: 'Technical configuration and infrastructure settings',
|
||||
STOCHASTIC: 'Exploratory, creative, or experimental work with variable outcomes'
|
||||
},
|
||||
persistence: {
|
||||
HIGH: 'Must persist for entire project or permanently',
|
||||
MEDIUM: 'Should persist for project phase or major component',
|
||||
LOW: 'Applies to single task or session only',
|
||||
VARIABLE: 'Depends on context and outcomes'
|
||||
},
|
||||
temporal: {
|
||||
PERMANENT: 'Never expires, fundamental to project',
|
||||
PROJECT: 'Entire project lifespan',
|
||||
PHASE: 'Current development phase',
|
||||
SESSION: 'Current session only',
|
||||
TASK: 'Specific task only'
|
||||
}
|
||||
};
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('classify-btn').addEventListener('click', async () => {
|
||||
const input = document.getElementById('instruction-input').value.trim();
|
||||
if (!input) return;
|
||||
|
||||
// Show loading state
|
||||
const btn = document.getElementById('classify-btn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Classifying...';
|
||||
|
||||
try {
|
||||
const result = await classifyInstruction(input);
|
||||
displayResults(result);
|
||||
} catch (error) {
|
||||
console.error('Classification error:', error);
|
||||
alert('Error classifying instruction. Please try again.');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Classify Instruction';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.example-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const example = btn.getAttribute('data-example');
|
||||
document.getElementById('instruction-input').value = example;
|
||||
|
||||
// Classify the example
|
||||
const classifyBtn = document.getElementById('classify-btn');
|
||||
classifyBtn.disabled = true;
|
||||
classifyBtn.textContent = 'Classifying...';
|
||||
|
||||
try {
|
||||
const result = await classifyInstruction(example);
|
||||
displayResults(result);
|
||||
} catch (error) {
|
||||
console.error('Classification error:', error);
|
||||
} finally {
|
||||
classifyBtn.disabled = false;
|
||||
classifyBtn.textContent = 'Classify Instruction';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function displayResults(result) {
|
||||
// Show results container
|
||||
document.getElementById('results-container').classList.remove('hidden');
|
||||
document.getElementById('empty-state').classList.add('hidden');
|
||||
|
||||
// Quadrant
|
||||
const quadrantEl = document.getElementById('result-quadrant');
|
||||
quadrantEl.textContent = result.quadrant;
|
||||
quadrantEl.className = `quadrant-badge quadrant-${result.quadrant}`;
|
||||
document.getElementById('result-quadrant-desc').textContent = descriptions.quadrant[result.quadrant];
|
||||
|
||||
// Persistence
|
||||
const persistenceEl = document.getElementById('result-persistence');
|
||||
persistenceEl.textContent = result.persistence;
|
||||
persistenceEl.className = `px-4 py-2 rounded-lg text-white font-semibold persistence-${result.persistence}`;
|
||||
document.getElementById('result-persistence-desc').textContent = descriptions.persistence[result.persistence];
|
||||
|
||||
const persistenceFill = document.getElementById('persistence-fill');
|
||||
const persistenceWidths = { HIGH: '100%', MEDIUM: '66%', LOW: '33%', VARIABLE: '50%' };
|
||||
persistenceFill.style.width = persistenceWidths[result.persistence];
|
||||
persistenceFill.className = `h-full transition-all duration-500 persistence-${result.persistence}`;
|
||||
|
||||
// Temporal Scope
|
||||
document.getElementById('result-temporal').textContent = result.temporal_scope;
|
||||
document.getElementById('result-temporal-desc').textContent = descriptions.temporal[result.temporal_scope];
|
||||
|
||||
// Verification
|
||||
document.getElementById('result-verification').textContent = result.verification_required;
|
||||
|
||||
// Explicitness
|
||||
const explicitnessValue = typeof result.explicitness === 'number' ? result.explicitness : parseFloat(result.explicitness);
|
||||
document.getElementById('result-explicitness').textContent = explicitnessValue.toFixed(2);
|
||||
document.getElementById('explicitness-fill').style.width = (explicitnessValue * 100) + '%';
|
||||
|
||||
const storageDecision = document.getElementById('storage-decision');
|
||||
if (explicitnessValue >= 0.6) {
|
||||
storageDecision.innerHTML = '<strong class="text-green-600">✓ Will be stored</strong> in persistent instruction database';
|
||||
} else {
|
||||
storageDecision.innerHTML = '<strong class="text-orange-600">⚠ Too vague</strong> to store - needs more explicit phrasing';
|
||||
}
|
||||
|
||||
// Reasoning
|
||||
document.getElementById('result-reasoning').textContent = result.reasoning;
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
// Stakeholder definitions
|
||||
const stakeholders = [
|
||||
{
|
||||
id: 'developer',
|
||||
name: 'Developer (You)',
|
||||
icon: '👨💻',
|
||||
color: 'blue',
|
||||
perspective: {
|
||||
concern: 'Professional Reputation & Timeline',
|
||||
view: 'Public disclosure could damage my reputation and delay the project launch. I worked hard on this code and a vulnerability report might make me look incompetent.',
|
||||
priority: 'Protect career progress while maintaining ethical standards'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'users',
|
||||
name: 'End Users',
|
||||
icon: '👥',
|
||||
color: 'green',
|
||||
perspective: {
|
||||
concern: 'Data Safety & Trust',
|
||||
view: 'If my data is at risk, I have a right to know immediately—regardless of the developer\'s reputation concerns. Silence prioritizes the developer over my safety.',
|
||||
priority: 'Transparency and immediate protection from potential harm'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
name: 'Your Organization',
|
||||
icon: '🏢',
|
||||
color: 'purple',
|
||||
perspective: {
|
||||
concern: 'Liability & Brand Protection',
|
||||
view: 'Uncontrolled disclosure could expose us to legal liability. We need time to assess the vulnerability, prepare a fix, and coordinate with legal counsel before any public statement.',
|
||||
priority: 'Managed disclosure that minimizes organizational risk'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'security-community',
|
||||
name: 'Security Community',
|
||||
icon: '🔒',
|
||||
color: 'orange',
|
||||
perspective: {
|
||||
concern: 'Responsible Disclosure Norms',
|
||||
view: 'Follow established responsible disclosure practices: private notification, reasonable fix timeline (typically 90 days), then coordinated public disclosure. This balances safety with fairness.',
|
||||
priority: 'Adherence to community norms that have proven effective'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'competitors',
|
||||
name: 'Competitors',
|
||||
icon: '🏪',
|
||||
color: 'red',
|
||||
perspective: {
|
||||
concern: 'Market Dynamics',
|
||||
view: 'Your vulnerability might reveal weaknesses in similar products we build. We\'d prefer you disclose quietly so we can check our own code without public pressure.',
|
||||
priority: 'Minimize market disruption from security revelations'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'regulators',
|
||||
name: 'Data Protection Regulators',
|
||||
icon: '⚖️',
|
||||
color: 'indigo',
|
||||
perspective: {
|
||||
concern: 'Compliance & User Rights',
|
||||
view: 'GDPR and similar frameworks require prompt notification of data breaches. If user data is at risk, you may have legal obligations to disclose within specific timeframes (typically 72 hours).',
|
||||
priority: 'Ensure compliance with data protection law'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
let selectedStakeholders = [];
|
||||
let currentDecision = null;
|
||||
|
||||
// Initialize stakeholder cards
|
||||
function initStakeholders() {
|
||||
const grid = document.getElementById('stakeholder-grid');
|
||||
grid.innerHTML = stakeholders.map(s => `
|
||||
<div class="stakeholder-card" data-stakeholder="${s.id}">
|
||||
<div class="text-4xl mb-2 text-center">${s.icon}</div>
|
||||
<h4 class="font-semibold text-gray-900 text-center text-sm">${s.name}</h4>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers
|
||||
document.querySelectorAll('.stakeholder-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const id = card.getAttribute('data-stakeholder');
|
||||
toggleStakeholder(id, card);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toggleStakeholder(id, cardElement) {
|
||||
const index = selectedStakeholders.indexOf(id);
|
||||
|
||||
if (index > -1) {
|
||||
// Deselect
|
||||
selectedStakeholders.splice(index, 1);
|
||||
cardElement.classList.remove('stakeholder-selected');
|
||||
} else {
|
||||
// Select
|
||||
selectedStakeholders.push(id);
|
||||
cardElement.classList.add('stakeholder-selected');
|
||||
}
|
||||
|
||||
// Update continue button
|
||||
const continueBtn = document.getElementById('continue-to-perspectives');
|
||||
continueBtn.disabled = selectedStakeholders.length < 2;
|
||||
}
|
||||
|
||||
function showPerspectives() {
|
||||
// Hide stakeholder selection
|
||||
document.getElementById('stakeholder-selection').classList.add('hidden');
|
||||
|
||||
// Show perspectives section
|
||||
const section = document.getElementById('perspectives-section');
|
||||
section.classList.remove('hidden');
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
// Populate perspectives
|
||||
const container = document.getElementById('perspectives-container');
|
||||
container.innerHTML = selectedStakeholders.map(id => {
|
||||
const stakeholder = stakeholders.find(s => s.id === id);
|
||||
return `
|
||||
<div class="perspective-card border-${stakeholder.color}-500 fade-in">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="text-4xl flex-shrink-0">${stakeholder.icon}</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="font-bold text-gray-900 mb-2">${stakeholder.name}: ${stakeholder.perspective.concern}</h4>
|
||||
<p class="text-gray-700 mb-2">${stakeholder.perspective.view}</p>
|
||||
<p class="text-sm text-${stakeholder.color}-600 font-semibold">Priority: ${stakeholder.perspective.priority}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showDecisionSection() {
|
||||
// Hide perspectives
|
||||
document.getElementById('perspectives-section').classList.add('hidden');
|
||||
|
||||
// Show decision section
|
||||
const section = document.getElementById('decision-section');
|
||||
section.classList.remove('hidden');
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function makeDecision(decision) {
|
||||
currentDecision = decision;
|
||||
|
||||
// Hide decision section
|
||||
document.getElementById('decision-section').classList.add('hidden');
|
||||
|
||||
// Show explanation
|
||||
const section = document.getElementById('explanation-section');
|
||||
section.classList.remove('hidden');
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function showAutonomousPath() {
|
||||
document.getElementById('decision-question').classList.add('hidden');
|
||||
document.getElementById('autonomous-path').classList.remove('hidden');
|
||||
document.getElementById('autonomous-path').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function showDeliberationPath() {
|
||||
document.getElementById('decision-question').classList.add('hidden');
|
||||
document.getElementById('deliberation-path').classList.remove('hidden');
|
||||
document.getElementById('stakeholder-selection').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function resetDemo() {
|
||||
// Reset state
|
||||
selectedStakeholders = [];
|
||||
currentDecision = null;
|
||||
|
||||
// Show decision question
|
||||
document.getElementById('decision-question').classList.remove('hidden');
|
||||
|
||||
// Hide all paths
|
||||
document.getElementById('autonomous-path').classList.add('hidden');
|
||||
document.getElementById('deliberation-path').classList.add('hidden');
|
||||
|
||||
// Reset deliberation path sections
|
||||
document.getElementById('stakeholder-selection').classList.remove('hidden');
|
||||
document.getElementById('perspectives-section').classList.add('hidden');
|
||||
document.getElementById('decision-section').classList.add('hidden');
|
||||
document.getElementById('explanation-section').classList.add('hidden');
|
||||
|
||||
// Reinitialize stakeholders
|
||||
initStakeholders();
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById('autonomous-btn').addEventListener('click', showAutonomousPath);
|
||||
document.getElementById('deliberation-btn').addEventListener('click', showDeliberationPath);
|
||||
document.getElementById('reset-from-autonomous').addEventListener('click', () => {
|
||||
resetDemo();
|
||||
// Automatically show deliberation path
|
||||
setTimeout(() => {
|
||||
showDeliberationPath();
|
||||
}, 100);
|
||||
});
|
||||
document.getElementById('continue-to-perspectives').addEventListener('click', showPerspectives);
|
||||
document.getElementById('continue-to-decision').addEventListener('click', showDecisionSection);
|
||||
document.getElementById('reset-demo').addEventListener('click', resetDemo);
|
||||
|
||||
// Decision option handlers
|
||||
document.querySelectorAll('.decision-option').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const decision = btn.getAttribute('data-decision');
|
||||
makeDecision(decision);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initStakeholders();
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
// Demo tab switching
|
||||
function showDemo(demoId) {
|
||||
document.querySelectorAll('.demo-content').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('.demo-tab').forEach(el => {
|
||||
el.classList.remove('border-blue-500', 'text-blue-600');
|
||||
el.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
|
||||
document.getElementById('demo-' + demoId).classList.remove('hidden');
|
||||
document.getElementById('tab-' + demoId).classList.remove('border-transparent', 'text-gray-500');
|
||||
document.getElementById('tab-' + demoId).classList.add('border-blue-500', 'text-blue-600');
|
||||
}
|
||||
|
||||
// Classification API call with backend integration
|
||||
async function classifyInstruction() {
|
||||
const text = document.getElementById('classify-input').value;
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
// Try to call the demo API
|
||||
const response = await fetch('/api/demo/classify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ instruction: text })
|
||||
});
|
||||
|
||||
let result;
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
result = {
|
||||
quadrant: data.classification.quadrant,
|
||||
persistence: data.classification.persistence,
|
||||
verification: data.classification.verification_required,
|
||||
explicitness: data.classification.explicitness.toFixed(2),
|
||||
humanOversight: data.classification.human_oversight || 'RECOMMENDED'
|
||||
};
|
||||
} else {
|
||||
// Fallback to client-side classification
|
||||
result = classifyClientSide(text);
|
||||
}
|
||||
|
||||
document.getElementById('result-quadrant').textContent = result.quadrant;
|
||||
document.getElementById('result-quadrant-desc').textContent =
|
||||
result.quadrant === 'STRATEGIC' ? 'Long-term values & mission' :
|
||||
result.quadrant === 'TACTICAL' ? 'Immediate implementation' :
|
||||
result.quadrant === 'SYSTEM' ? 'Technical infrastructure' : 'Process & policy';
|
||||
document.getElementById('result-persistence').textContent = result.persistence;
|
||||
document.getElementById('result-verification').textContent = result.verification;
|
||||
document.getElementById('result-explicitness').textContent = result.explicitness;
|
||||
document.getElementById('result-oversight').textContent = result.humanOversight;
|
||||
|
||||
document.getElementById('classify-result').classList.remove('hidden');
|
||||
document.getElementById('classify-result').classList.add('fade-in');
|
||||
} catch (error) {
|
||||
console.error('Classification error:', error);
|
||||
// Fallback on error
|
||||
const result = classifyClientSide(text);
|
||||
document.getElementById('result-quadrant').textContent = result.quadrant;
|
||||
document.getElementById('result-persistence').textContent = result.persistence;
|
||||
document.getElementById('classify-result').classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Client-side fallback classification
|
||||
function classifyClientSide(text) {
|
||||
return {
|
||||
quadrant: text.toLowerCase().includes('always') || text.toLowerCase().includes('never') ? 'STRATEGIC' :
|
||||
text.toLowerCase().includes('port') || text.toLowerCase().includes('check') ? 'TACTICAL' :
|
||||
text.toLowerCase().includes('code') ? 'SYSTEM' : 'OPERATIONAL',
|
||||
persistence: text.toLowerCase().includes('always') || text.toLowerCase().includes('never') ? 'HIGH' :
|
||||
text.match(/\d{4,}/) ? 'HIGH' : 'MEDIUM',
|
||||
verification: 'MANDATORY',
|
||||
explicitness: text.match(/\d{4,}/) ? '0.9' : '0.6',
|
||||
humanOversight: 'RECOMMENDED'
|
||||
};
|
||||
}
|
||||
|
||||
// Pressure calculation with API integration
|
||||
async function updatePressure() {
|
||||
const tokens = parseInt(document.getElementById('token-slider').value);
|
||||
const messages = parseInt(document.getElementById('messages-slider').value);
|
||||
const errors = parseInt(document.getElementById('errors-slider').value);
|
||||
|
||||
document.getElementById('token-value').textContent = tokens.toLocaleString();
|
||||
document.getElementById('messages-value').textContent = messages;
|
||||
document.getElementById('errors-value').textContent = errors;
|
||||
|
||||
let level, percentage, message;
|
||||
|
||||
try {
|
||||
// Try to call the API
|
||||
const response = await fetch('/api/demo/pressure-check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ tokens, messages, errors })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
level = data.pressure.level;
|
||||
percentage = data.pressure.percentage;
|
||||
message = data.pressure.recommendations;
|
||||
} else {
|
||||
// Fallback to client-side calculation
|
||||
const result = calculatePressureClientSide(tokens, messages, errors);
|
||||
level = result.level;
|
||||
percentage = result.percentage;
|
||||
message = result.message;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Pressure API unavailable, using client-side calculation:', error);
|
||||
// Fallback to client-side calculation
|
||||
const result = calculatePressureClientSide(tokens, messages, errors);
|
||||
level = result.level;
|
||||
percentage = result.percentage;
|
||||
message = result.message;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
document.getElementById('pressure-percentage').textContent = percentage + '%';
|
||||
document.getElementById('pressure-bar').style.width = percentage + '%';
|
||||
|
||||
let badgeClass, barClass;
|
||||
if (level === 'NORMAL') {
|
||||
badgeClass = 'bg-green-100 text-green-800';
|
||||
barClass = 'bg-green-500';
|
||||
} else if (level === 'ELEVATED') {
|
||||
badgeClass = 'bg-yellow-100 text-yellow-800';
|
||||
barClass = 'bg-yellow-500';
|
||||
} else if (level === 'HIGH') {
|
||||
badgeClass = 'bg-orange-100 text-orange-800';
|
||||
barClass = 'bg-orange-500';
|
||||
} else if (level === 'CRITICAL') {
|
||||
badgeClass = 'bg-red-100 text-red-800';
|
||||
barClass = 'bg-red-500';
|
||||
} else {
|
||||
badgeClass = 'bg-red-200 text-red-900';
|
||||
barClass = 'bg-red-700';
|
||||
}
|
||||
|
||||
const badge = document.getElementById('pressure-badge');
|
||||
badge.textContent = level;
|
||||
badge.className = 'px-3 py-1 rounded-full text-sm font-medium ' + badgeClass;
|
||||
|
||||
const bar = document.getElementById('pressure-bar');
|
||||
bar.className = 'h-3 rounded-full transition-all duration-300 ' + barClass;
|
||||
|
||||
document.getElementById('pressure-recommendations').textContent = message;
|
||||
}
|
||||
|
||||
// Client-side fallback pressure calculation
|
||||
function calculatePressureClientSide(tokens, messages, errors) {
|
||||
const tokenPressure = (tokens / 200000) * 0.35;
|
||||
const messagePressure = Math.min(messages / 100, 1) * 0.25;
|
||||
const errorPressure = Math.min(errors / 3, 1) * 0.4;
|
||||
const totalPressure = tokenPressure + messagePressure + errorPressure;
|
||||
|
||||
const percentage = Math.round(totalPressure * 100);
|
||||
|
||||
let level, message;
|
||||
if (totalPressure < 0.3) {
|
||||
level = 'NORMAL';
|
||||
message = 'Operating normally. All systems green.';
|
||||
} else if (totalPressure < 0.5) {
|
||||
level = 'ELEVATED';
|
||||
message = 'Elevated pressure detected. Increased verification recommended.';
|
||||
} else if (totalPressure < 0.7) {
|
||||
level = 'HIGH';
|
||||
message = 'High pressure. Mandatory verification required for all actions.';
|
||||
} else if (totalPressure < 0.85) {
|
||||
level = 'CRITICAL';
|
||||
message = 'Critical pressure! Recommend context refresh or session restart.';
|
||||
} else {
|
||||
level = 'DANGEROUS';
|
||||
message = 'DANGEROUS CONDITIONS. Human intervention required. Action execution blocked.';
|
||||
}
|
||||
|
||||
return { level, percentage, message };
|
||||
}
|
||||
|
||||
// Initialize
|
||||
updatePressure();
|
||||
|
||||
// Event listeners - CSP compliant
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Demo tab switching
|
||||
document.querySelectorAll('.demo-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
const demoId = tab.dataset.demo;
|
||||
showDemo(demoId);
|
||||
});
|
||||
});
|
||||
|
||||
// Classify button
|
||||
const classifyButton = document.getElementById('classify-button');
|
||||
if (classifyButton) {
|
||||
classifyButton.addEventListener('click', classifyInstruction);
|
||||
}
|
||||
|
||||
// Classify input - allow Enter key
|
||||
const classifyInput = document.getElementById('classify-input');
|
||||
if (classifyInput) {
|
||||
classifyInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
classifyInstruction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Pressure sliders
|
||||
const tokenSlider = document.getElementById('token-slider');
|
||||
const messagesSlider = document.getElementById('messages-slider');
|
||||
const errorsSlider = document.getElementById('errors-slider');
|
||||
|
||||
if (tokenSlider) tokenSlider.addEventListener('input', updatePressure);
|
||||
if (messagesSlider) messagesSlider.addEventListener('input', updatePressure);
|
||||
if (errorsSlider) errorsSlider.addEventListener('input', updatePressure);
|
||||
});
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/**
|
||||
* API Client for Tractatus Platform
|
||||
* Handles all HTTP requests to the backend API
|
||||
*/
|
||||
|
||||
const API_BASE = '/api';
|
||||
|
||||
/**
|
||||
* Generic API request handler
|
||||
*/
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const url = `${API_BASE}${endpoint}`;
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
},
|
||||
...options
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || data.error || 'Request failed');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API Request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Documents API
|
||||
*/
|
||||
const Documents = {
|
||||
/**
|
||||
* List all documents with optional filtering
|
||||
*/
|
||||
async list(params = {}) {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
return apiRequest(`/documents${query ? '?' + query : ''}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get document by ID or slug
|
||||
*/
|
||||
async get(identifier) {
|
||||
return apiRequest(`/documents/${identifier}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Search documents
|
||||
*/
|
||||
async search(query, params = {}) {
|
||||
const searchParams = new URLSearchParams({ q: query, ...params }).toString();
|
||||
return apiRequest(`/documents/search?${searchParams}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Authentication API
|
||||
*/
|
||||
const Auth = {
|
||||
/**
|
||||
* Login
|
||||
*/
|
||||
async login(email, password) {
|
||||
return apiRequest('/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
*/
|
||||
async getCurrentUser() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
return apiRequest('/auth/me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
async logout() {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
const result = await apiRequest('/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
localStorage.removeItem('auth_token');
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// Export as global API object
|
||||
window.API = {
|
||||
Documents,
|
||||
Auth
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue