Files
PG-admi-onefile/public/index.html
2026-03-20 16:48:45 +07:00

831 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PostgreSQL SensoLab Panel</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/app.css">
</head>
<body class="bg-slate-50 text-slate-800 overflow-hidden" data-theme="light">
<!-- Login Screen -->
<div id="loginScreen" class="fixed inset-0 z-50 flex items-center justify-center bg-gradient-to-br from-slate-900 via-blue-900 to-slate-900">
<div class="w-full max-w-md p-8 glass-panel rounded-2xl shadow-2xl">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 bg-blue-600 rounded-2xl mb-4 shadow-lg shadow-blue-600/30">
<i data-lucide="database" class="w-8 h-8 text-white"></i>
</div>
<h1 class="text-2xl font-bold text-slate-800">PostgreSQL SensoLab</h1>
<p class="text-slate-500 mt-2">Войдите для управления базой данных</p>
</div>
<form id="loginForm" class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Логин администратора</label>
<input type="text" id="adminUser" value="admin" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all" placeholder="admin">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Пароль</label>
<input type="password" id="adminPass" value="admin" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all" placeholder="••••••••">
</div>
<button type="submit" class="w-full py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-all transform hover:scale-[1.02] shadow-lg shadow-blue-600/30 flex items-center justify-center gap-2">
<span id="loginText">Войти</span>
<div id="loginLoader" class="loader hidden" style="width: 20px; height: 20px; border-width: 2px;"></div>
</button>
</form>
</div>
</div>
<!-- Main Application -->
<div id="mainApp" class="hidden h-screen flex flex-col">
<!-- Header -->
<div id="mobileBackdrop" class="hidden fixed inset-0 bg-slate-950/50 z-20" onclick="app.closeSidebar()"></div>
<header id="mainHeader" class="bg-white border-b border-slate-200 h-16 flex items-center justify-between px-6 shadow-sm z-10">
<div class="flex items-center gap-4">
<button onclick="app.toggleSidebar()" class="lg:hidden p-2 rounded-lg bg-slate-100 text-slate-700">
<i data-lucide="menu" class="w-4 h-4"></i>
</button>
<div class="flex items-center gap-2 text-blue-600">
<i data-lucide="database" class="w-6 h-6"></i>
<span class="font-bold text-lg">PostgreSQL SensoLab</span>
</div>
<div class="h-6 w-px bg-slate-200 mx-2"></div>
<div class="flex items-center gap-2 text-sm text-slate-600 bg-slate-100 px-3 py-1 rounded-full">
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span id="connectionStatus">localhost:5432/postgres</span>
</div>
</div>
<div class="flex items-center gap-3">
<select id="themeSelect" onchange="app.setTheme(this.value)" class="border border-slate-200 rounded-lg px-3 py-2 text-sm bg-white text-slate-700">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="system">System</option>
</select>
<button id="managementButton" onclick="app.showManagementPanel('settings')" class="hidden flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm font-medium">
<i data-lucide="panel-left-open" class="w-4 h-4"></i>
Menu
</button>
<button id="logsButton" onclick="app.showLogsPanel()" class="hidden flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm font-medium">
<i data-lucide="scroll-text" class="w-4 h-4"></i>
Logs
</button>
<button onclick="app.showSQLPanel()" class="flex items-center gap-2 px-4 py-2 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition-colors text-sm font-medium">
<i data-lucide="terminal" class="w-4 h-4"></i>
SQL Query
</button>
<div class="h-6 w-px bg-slate-200"></div>
<div id="roleBadge" class="px-3 py-1 rounded-full bg-slate-100 text-slate-600 text-xs font-semibold uppercase tracking-wide"></div>
<button onclick="app.logout()" class="flex items-center gap-2 px-4 py-2 text-slate-600 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors text-sm font-medium">
<i data-lucide="log-out" class="w-4 h-4"></i>
Выйти
</button>
</div>
</header>
<div class="flex flex-1 overflow-hidden">
<!-- Sidebar -->
<aside id="sidebar" class="w-64 bg-slate-900 text-slate-300 flex flex-col">
<div class="p-4 border-b border-slate-800">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider">Таблицы</span>
<button onclick="app.showCreateTableModal()" class="p-1 hover:bg-slate-800 rounded transition-colors" title="Создать таблицу">
<i data-lucide="plus" class="w-4 h-4"></i>
</button>
</div>
<input type="text" id="tableSearch" placeholder="Поиск..." class="w-full bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-sm focus:outline-none focus:border-blue-500 text-slate-300" oninput="app.filterTables(this.value)">
</div>
<div id="tableList" class="flex-1 overflow-y-auto py-2">
<!-- Tables will be rendered here -->
</div>
<div class="p-4 border-t border-slate-800">
<div class="flex items-center gap-2 text-xs text-slate-500">
<i data-lucide="hard-drive" class="w-4 h-4"></i>
<span id="dbSize">24.5 MB</span>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="flex-1 flex flex-col bg-slate-50 overflow-hidden">
<!-- Toolbar -->
<div id="toolbar" class="bg-white border-b border-slate-200 p-4 flex items-center justify-between">
<div class="flex items-center gap-4">
<h2 id="currentTableTitle" class="text-xl font-semibold text-slate-800">Выберите таблицу</h2>
<div id="tableActions" class="hidden flex items-center gap-2">
<button onclick="app.showAddRecordModal()" class="flex items-center gap-2 px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm">
<i data-lucide="plus" class="w-4 h-4"></i>
Добавить запись
</button>
<button onclick="app.showTableStructure()" class="flex items-center gap-2 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
<i data-lucide="settings" class="w-4 h-4"></i>
Структура
</button>
<button onclick="app.showIndexesModal()" class="flex items-center gap-2 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
<i data-lucide="list-tree" class="w-4 h-4"></i>
Индексы
</button>
<button onclick="app.showMoveTableModal()" class="flex items-center gap-2 px-3 py-1.5 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
<i data-lucide="folder-input" class="w-4 h-4"></i>
Move
</button>
<button onclick="app.deleteTable()" class="flex items-center gap-2 px-3 py-1.5 bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors text-sm">
<i data-lucide="trash-2" class="w-4 h-4"></i>
Удалить
</button>
</div>
</div>
<div class="flex items-center gap-3" id="recordControls">
<input id="recordSearch" type="text" placeholder="Поиск по записям..." class="w-56 border border-slate-200 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<button onclick="app.toggleFilters()" class="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors text-sm">
<i data-lucide="filter" class="w-4 h-4"></i>
Фильтры
</button>
<div class="text-sm text-slate-600" id="recordCount">
<!-- Record count will be shown here -->
</div>
</div>
</div>
<!-- Content Area -->
<div id="contentArea" class="flex-1 overflow-auto p-6">
<!-- Dynamic content: Table data, SQL editor, Structure, etc. -->
<div id="emptyState" class="flex flex-col items-center justify-center h-full text-slate-400">
<i data-lucide="database" class="w-16 h-16 mb-4 opacity-20"></i>
<p class="text-lg">Выберите таблицу из списка слева или выполните SQL-запрос</p>
</div>
<div id="dataGrid" class="hidden bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
<tr id="tableHeaders"></tr>
</thead>
<tbody id="filterRow" class="hidden bg-blue-50 border-b border-slate-200">
<tr id="filterInputs"></tr>
</tbody>
<tbody id="tableBody" class="divide-y divide-slate-200"></tbody>
</table>
</div>
<div class="p-4 border-t border-slate-200 flex items-center justify-between bg-slate-50">
<div class="flex items-center gap-2">
<button onclick="app.changePage(-1)" class="p-2 hover:bg-slate-200 rounded-lg disabled:opacity-50" id="prevPage">
<i data-lucide="chevron-left" class="w-4 h-4"></i>
</button>
<span class="text-sm text-slate-600">Страница <span id="currentPage">1</span> из <span id="totalPages">1</span></span>
<button onclick="app.changePage(1)" class="p-2 hover:bg-slate-200 rounded-lg disabled:opacity-50" id="nextPage">
<i data-lucide="chevron-right" class="w-4 h-4"></i>
</button>
</div>
<div class="flex items-center gap-2">
<select onchange="app.changeLimit(this.value)" class="bg-white border border-slate-300 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:border-blue-500">
<option value="10">10 строк</option>
<option value="25">25 строк</option>
<option value="50">50 строк</option>
<option value="100">100 строк</option>
</select>
</div>
</div>
</div>
<!-- SQL Editor Panel -->
<div id="sqlPanel" class="hidden h-full flex flex-col gap-4">
<div class="bg-slate-900 rounded-xl overflow-hidden flex flex-col flex-1 shadow-lg">
<div class="bg-slate-800 px-4 py-2 flex items-center justify-between border-b border-slate-700">
<div class="flex items-center gap-2 text-slate-300 text-sm">
<i data-lucide="terminal" class="w-4 h-4"></i>
<span>SQL Query Editor</span>
</div>
<div class="flex items-center gap-2">
<button onclick="app.formatSQL()" class="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 text-slate-300 rounded transition-colors">
Форматировать
</button>
<button onclick="app.clearSQL()" class="px-3 py-1 text-xs bg-slate-700 hover:bg-slate-600 text-slate-300 rounded transition-colors">
Очистить
</button>
</div>
</div>
<textarea id="sqlEditor" class="flex-1 bg-slate-900 text-slate-300 p-4 font-mono text-sm resize-none outline-none" placeholder="-- Введите SQL запрос здесь
SELECT * FROM users LIMIT 10;"></textarea>
</div>
<div class="flex flex-wrap gap-2">
<button onclick="app.applySQLTemplate('select')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">SELECT *</button>
<button onclick="app.applySQLTemplate('count')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">COUNT rows</button>
<button onclick="app.applySQLTemplate('insert')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">INSERT row</button>
<button onclick="app.applySQLTemplate('update')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">UPDATE row</button>
<button onclick="app.applySQLTemplate('delete')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">DELETE row</button>
<button onclick="app.applySQLTemplate('schema')" class="px-3 py-2 bg-slate-800 text-slate-200 rounded-lg text-xs">Describe table</button>
</div>
<div class="flex justify-end">
<button onclick="app.executeSQL()" class="flex items-center gap-2 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-all shadow-lg shadow-blue-600/20">
<i data-lucide="play" class="w-4 h-4"></i>
Выполнить (Ctrl+Enter)
</button>
</div>
<div id="sqlResults" class="hidden bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden flex-1">
<div class="p-4 border-b border-slate-200 bg-slate-50 flex items-center justify-between">
<span class="font-medium text-slate-700">Результаты</span>
<span id="sqlStats" class="text-sm text-slate-500"></span>
</div>
<div class="overflow-x-auto max-h-96" id="sqlResultsContent"></div>
</div>
</div>
<div id="logsPanel" class="hidden h-full flex flex-col gap-4">
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-4 flex flex-wrap items-center gap-3">
<select id="containerSelect" onchange="app.changeContainer(this.value)" class="border border-slate-300 rounded-lg px-3 py-2 text-sm min-w-56">
<option value="">Select container</option>
</select>
<button onclick="app.refreshLogs()" class="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm">Refresh</button>
<button onclick="app.toggleLogStream()" id="logStreamButton" class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm">Start live</button>
<button onclick="app.clearLogs()" class="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm">Clear</button>
<div id="logStatus" class="text-sm text-slate-500">Logs are available for admin roles.</div>
</div>
<div class="bg-slate-900 text-slate-200 rounded-xl border border-slate-800 p-4 font-mono text-sm log-terminal overflow-auto" id="logOutput">Select a container to load recent logs.</div>
</div>
<div id="managementPanel" class="hidden h-full">
<div class="grid lg:grid-cols-[240px,1fr] gap-6 h-full">
<aside class="bg-white rounded-2xl border border-slate-200 p-4 space-y-2">
<button id="managementTab-settings" onclick="app.showManagementPanel('settings')" class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left bg-slate-100 text-slate-700">
<i data-lucide="sliders-horizontal" class="w-4 h-4"></i>
<span>Settings</span>
</button>
<button id="managementTab-backups" onclick="app.showManagementPanel('backups')" class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left text-slate-700 hover:bg-slate-100">
<i data-lucide="archive" class="w-4 h-4"></i>
<span>Backups</span>
</button>
<button id="managementTab-users" onclick="app.showManagementPanel('users')" class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left text-slate-700 hover:bg-slate-100">
<i data-lucide="users" class="w-4 h-4"></i>
<span>Users</span>
</button>
<button id="managementTab-audit" onclick="app.showManagementPanel('audit')" class="w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left text-slate-700 hover:bg-slate-100">
<i data-lucide="history" class="w-4 h-4"></i>
<span>Audit</span>
</button>
</aside>
<section class="min-h-0">
<div id="managementSection-settings" class="hidden bg-white rounded-2xl border border-slate-200 h-full overflow-auto p-6 space-y-6">
<div>
<h3 class="text-2xl font-bold text-slate-800">System settings</h3>
<p class="text-sm text-slate-500 mt-1">Telegram, automatic backups and retention are configured here.</p>
</div>
<section class="space-y-4">
<div>
<h4 class="text-lg font-semibold text-slate-800">Automatic backups</h4>
<p class="text-sm text-slate-500">The panel creates a database archive and, optionally, a site snapshot every day.</p>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="managementSettingsBackupsEnabled" class="w-4 h-4">
Enable automatic backups
</label>
<div class="grid sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Time</label>
<input type="time" id="managementSettingsBackupTime" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Keep last archives</label>
<input type="number" id="managementSettingsKeepLast" min="1" max="90" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="managementSettingsIncludeAppSnapshot" class="w-4 h-4">
Include site settings and audit snapshot
</label>
</div>
</div>
</section>
<section class="space-y-4">
<div>
<h4 class="text-lg font-semibold text-slate-800">Telegram notifications</h4>
<p class="text-sm text-slate-500">Used for backup errors, restore events and scheduled notifications.</p>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="managementSettingsTelegramEnabled" class="w-4 h-4">
Enable Telegram notifications
</label>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Bot token</label>
<input type="text" id="managementSettingsTelegramToken" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="123456:ABC...">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Chat ID</label>
<input type="text" id="managementSettingsTelegramChatId" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="-1001234567890">
</div>
</div>
</section>
<div class="flex justify-end">
<button onclick="app.saveSettings()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">Save settings</button>
</div>
</div>
<div id="managementSection-backups" class="hidden bg-white rounded-2xl border border-slate-200 h-full overflow-auto p-6 space-y-4">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h3 class="text-2xl font-bold text-slate-800">Backups</h3>
<p class="text-sm text-slate-500 mt-1">Archives contain the SQL dump and, if enabled, the application snapshot.</p>
</div>
<button onclick="app.createBackup()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Create archive</button>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="managementRestoreAppSnapshot" class="w-4 h-4" checked>
Restore users, settings and audit snapshot together with the database
</label>
<div id="managementBackupsList" class="space-y-3"></div>
</div>
<div id="managementSection-users" class="hidden bg-white rounded-2xl border border-slate-200 h-full overflow-auto">
<div class="grid xl:grid-cols-[1.15fr,0.85fr] min-h-full">
<div class="border-r border-slate-200 overflow-auto">
<div class="p-6 border-b border-slate-200">
<h3 class="text-2xl font-bold text-slate-800">Users</h3>
<p class="text-sm text-slate-500 mt-1">Create accounts and tune access to folders, tables and destructive actions.</p>
</div>
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left p-3">Username</th>
<th class="text-left p-3">Role</th>
<th class="text-left p-3">Status</th>
<th class="text-left p-3">Actions</th>
</tr>
</thead>
<tbody id="managementUsersTableBody"></tbody>
</table>
</div>
<div class="p-6 overflow-auto space-y-3">
<input type="hidden" id="managementUserEditMode" value="">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Username</label>
<input type="text" id="managementUserUsername" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Password</label>
<input type="text" id="managementUserPassword" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="Leave empty to keep current">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Role</label>
<select id="managementUserRole" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
<option value="viewer">viewer</option>
<option value="moderator">moderator</option>
<option value="admin">admin</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Readable folders</label>
<details class="border border-slate-300 rounded-lg p-3">
<summary class="cursor-pointer text-sm text-slate-700">Choose folders</summary>
<div id="managementUserViewFoldersList" class="mt-3 grid gap-2"></div>
</details>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Editable tables</label>
<details class="border border-slate-300 rounded-lg p-3">
<summary class="cursor-pointer text-sm text-slate-700">Choose tables</summary>
<div id="managementUserEditTablesList" class="mt-3 grid gap-2 max-h-48 overflow-auto"></div>
</details>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Deletable folders</label>
<details class="border border-slate-300 rounded-lg p-3">
<summary class="cursor-pointer text-sm text-slate-700">Choose folders</summary>
<div id="managementUserDeleteFoldersList" class="mt-3 grid gap-2"></div>
</details>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="managementUserDisabled" class="w-4 h-4">
Disable login
</label>
<div class="flex justify-end gap-3 pt-2">
<button onclick="app.resetUserForm()" class="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg">Reset</button>
<button onclick="app.saveUser()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Save user</button>
</div>
</div>
</div>
</div>
<div id="managementSection-audit" class="hidden bg-white rounded-2xl border border-slate-200 h-full overflow-auto p-6">
<div class="flex flex-wrap items-start justify-between gap-4 mb-4">
<div>
<h3 class="text-2xl font-bold text-slate-800">Audit log</h3>
<p class="text-sm text-slate-500 mt-1">Human-readable actions with time, source and actor.</p>
</div>
<button onclick="app.loadAuditLog()" class="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg">Refresh</button>
</div>
<div id="managementAuditList" class="space-y-3"></div>
</div>
</section>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Modals -->
<!-- Create Table Modal -->
<div id="createTableModal" class="hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Создать новую таблицу</h3>
<button onclick="app.closeModal('createTableModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 mb-1">Папка</label>
<input type="text" id="newTableFolder" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" placeholder="frontend">
<p class="text-xs text-slate-500 mt-1">Если указано, имя таблицы будет создано как <code>папка__имя</code>. Оставьте пустым для создания в корне.</p>
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-slate-700 mb-1">Название таблицы</label>
<input type="text" id="newTableName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none" placeholder="users">
</div>
<div class="mb-4">
<div class="flex items-center justify-between mb-2">
<label class="block text-sm font-medium text-slate-700">Колонки</label>
<button onclick="app.addColumnField()" class="text-sm text-blue-600 hover:text-blue-700 font-medium">+ Добавить колонку</button>
</div>
<div id="columnsContainer" class="space-y-2">
<!-- Column fields will be added here -->
</div>
</div>
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button onclick="app.closeModal('createTableModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg transition-colors">Отмена</button>
<button onclick="app.createTable()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors shadow-lg shadow-blue-600/20">Создать таблицу</button>
</div>
</div>
</div>
<!-- Add/Edit Record Modal -->
<div id="recordModal" class="hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 id="recordModalTitle" class="text-xl font-bold text-slate-800">Добавить запись</h3>
<button onclick="app.closeModal('recordModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div id="recordForm" class="p-6 overflow-y-auto flex-1 space-y-4">
<!-- Form fields will be generated here -->
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button onclick="app.closeModal('recordModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg transition-colors">Отмена</button>
<button onclick="app.saveRecord()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors shadow-lg shadow-blue-600/20">Сохранить</button>
</div>
</div>
</div>
<!-- Table Structure Modal -->
<div id="structureModal" class="hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Структура таблицы: <span id="structureTableName"></span></h3>
<button onclick="app.closeModal('structureModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div class="mb-4 flex justify-end">
<button onclick="app.showCreateColumnModal()" class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm">
<i data-lucide="plus" class="w-4 h-4"></i>
Добавить колонку
</button>
</div>
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
<tr>
<th class="text-left p-3">Колонка</th>
<th class="text-left p-3">Тип</th>
<th class="text-left p-3">NULL</th>
<th class="text-left p-3">По умолчанию</th>
<th class="text-left p-3">Действия</th>
</tr>
</thead>
<tbody id="structureBody" class="divide-y divide-slate-200"></tbody>
</table>
</div>
</div>
</div>
<!-- Indexes Modal -->
<div id="indexesModal" class="hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Индексы таблицы: <span id="indexesTableName"></span></h3>
<button onclick="app.closeModal('indexesModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-y-auto flex-1">
<div class="mb-4 flex justify-end">
<button onclick="app.showCreateIndexModal()" class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm">
<i data-lucide="plus" class="w-4 h-4"></i>
Создать индекс
</button>
</div>
<table class="w-full text-sm">
<thead class="bg-slate-50 text-slate-600 font-medium border-b border-slate-200">
<tr>
<th class="text-left p-3">Название</th>
<th class="text-left p-3">Колонки</th>
<th class="text-left p-3">Тип</th>
<th class="text-left p-3">Уникальный</th>
<th class="text-left p-3">Действия</th>
</tr>
</thead>
<tbody id="indexesBody" class="divide-y divide-slate-200"></tbody>
</table>
</div>
</div>
</div>
<!-- Column Modal -->
<div id="columnModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 id="columnModalTitle" class="text-xl font-bold text-slate-800">Добавить колонку</h3>
<button onclick="app.closeModal('columnModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Название колонки</label>
<input type="text" id="columnName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Тип</label>
<select id="columnType" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
<option value="VARCHAR(255)">VARCHAR(255)</option>
<option value="TEXT">TEXT</option>
<option value="INTEGER">INTEGER</option>
<option value="BIGINT">BIGINT</option>
<option value="DECIMAL">DECIMAL</option>
<option value="BOOLEAN">BOOLEAN</option>
<option value="DATE">DATE</option>
<option value="TIMESTAMP">TIMESTAMP</option>
<option value="UUID">UUID</option>
<option value="JSON">JSON</option>
<option value="JSONB">JSONB</option>
</select>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2">
<input type="checkbox" id="columnNullable" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
<span class="text-sm text-slate-700">NULL</span>
</label>
<label class="flex items-center gap-2">
<input type="checkbox" id="columnPrimary" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
<span class="text-sm text-slate-700">Первичный ключ</span>
</label>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Значение по умолчанию</label>
<input type="text" id="columnDefault" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none">
</div>
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button onclick="app.closeModal('columnModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Отмена</button>
<button onclick="app.saveColumn()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Сохранить</button>
</div>
</div>
</div>
<!-- Create Index Modal -->
<div id="createIndexModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md fade-in">
<div class="p-6 border-b border-slate-200">
<h3 class="text-xl font-bold text-slate-800">Создать индекс</h3>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Название индекса</label>
<input type="text" id="indexName" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" placeholder="idx_users_email">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Колонки (через запятую)</label>
<input type="text" id="indexColumns" class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none" placeholder="email, created_at">
</div>
<div class="flex items-center gap-2">
<input type="checkbox" id="indexUnique" class="w-4 h-4 text-blue-600 rounded focus:ring-blue-500">
<label for="indexUnique" class="text-sm text-slate-700">Уникальный индекс</label>
</div>
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button onclick="app.closeModal('createIndexModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Отмена</button>
<button onclick="app.createIndex()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Создать</button>
</div>
</div>
</div>
<div id="moveTableModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md fade-in">
<div class="p-6 border-b border-slate-200">
<h3 class="text-xl font-bold text-slate-800">Move table</h3>
</div>
<div class="p-6 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Folder</label>
<select id="moveTableFolder" class="w-full px-4 py-2 border border-slate-300 rounded-lg"></select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Table name</label>
<input type="text" id="moveTableName" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="users">
</div>
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button onclick="app.closeModal('moveTableModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg">Cancel</button>
<button onclick="app.moveTable()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Save</button>
</div>
</div>
</div>
<div id="usersModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Users & access</h3>
<button onclick="app.closeModal('usersModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="grid lg:grid-cols-[1.2fr,0.8fr] gap-0 flex-1 min-h-0">
<div class="border-r border-slate-200 overflow-auto">
<table class="w-full text-sm">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="text-left p-3">Username</th>
<th class="text-left p-3">Role</th>
<th class="text-left p-3">Status</th>
<th class="text-left p-3">Actions</th>
</tr>
</thead>
<tbody id="usersTableBody"></tbody>
</table>
</div>
<div class="p-6 overflow-auto space-y-3">
<input type="hidden" id="userEditMode" value="">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Username</label>
<input type="text" id="userUsername" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Password</label>
<input type="text" id="userPassword" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="Leave empty to keep current">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Role</label>
<select id="userRole" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
<option value="viewer">viewer</option>
<option value="moderator">moderator</option>
<option value="admin">admin</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Readable folders</label>
<details class="border border-slate-300 rounded-lg p-3">
<summary class="cursor-pointer text-sm text-slate-700">Choose folders</summary>
<div id="userViewFoldersList" class="mt-3 grid gap-2"></div>
</details>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Editable tables</label>
<details class="border border-slate-300 rounded-lg p-3">
<summary class="cursor-pointer text-sm text-slate-700">Choose tables</summary>
<div id="userEditTablesList" class="mt-3 grid gap-2 max-h-48 overflow-auto"></div>
</details>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Deletable folders</label>
<details class="border border-slate-300 rounded-lg p-3">
<summary class="cursor-pointer text-sm text-slate-700">Choose folders</summary>
<div id="userDeleteFoldersList" class="mt-3 grid gap-2"></div>
</details>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="userDisabled" class="w-4 h-4">
Disable login
</label>
<div class="flex justify-end gap-3 pt-2">
<button onclick="app.resetUserForm()" class="px-4 py-2 text-slate-600 hover:bg-slate-100 rounded-lg">Reset</button>
<button onclick="app.saveUser()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Save user</button>
</div>
</div>
</div>
</div>
</div>
<div id="auditModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Audit log</h3>
<button onclick="app.closeModal('auditModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-auto">
<div class="flex justify-end mb-4">
<button onclick="app.loadAuditLog()" class="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg">Refresh</button>
</div>
<div id="auditList" class="space-y-3"></div>
</div>
</div>
</div>
<div id="backupsModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">Backups</h3>
<button onclick="app.closeModal('backupsModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-auto">
<div class="flex justify-between items-center mb-4">
<p class="text-sm text-slate-500">Create and download recovery snapshots.</p>
<button onclick="app.createBackup()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg">Create backup</button>
</div>
<div id="backupsList" class="space-y-3"></div>
</div>
</div>
</div>
<div id="settingsModal" class="hidden fixed inset-0 z-50 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col fade-in">
<div class="p-6 border-b border-slate-200 flex items-center justify-between">
<h3 class="text-xl font-bold text-slate-800">System settings</h3>
<button onclick="app.closeModal('settingsModal')" class="p-2 hover:bg-slate-100 rounded-lg transition-colors">
<i data-lucide="x" class="w-5 h-5 text-slate-500"></i>
</button>
</div>
<div class="p-6 overflow-auto space-y-6">
<section class="space-y-4">
<div>
<h4 class="text-lg font-semibold text-slate-800">Automatic backups</h4>
<p class="text-sm text-slate-500">Daily SQL dump of PostgreSQL plus optional application snapshot.</p>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="settingsBackupsEnabled" class="w-4 h-4">
Enable automatic backups
</label>
<div class="grid sm:grid-cols-3 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Time</label>
<input type="time" id="settingsBackupTime" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Keep last days</label>
<input type="number" id="settingsKeepLast" min="1" max="90" class="w-full px-4 py-2 border border-slate-300 rounded-lg">
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="settingsIncludeAppSnapshot" class="w-4 h-4">
Include site settings and audit snapshot
</label>
</div>
</div>
</section>
<section class="space-y-4">
<div>
<h4 class="text-lg font-semibold text-slate-800">Telegram notifications</h4>
<p class="text-sm text-slate-500">Used for backup errors and important service events.</p>
</div>
<label class="flex items-center gap-2 text-sm text-slate-700">
<input type="checkbox" id="settingsTelegramEnabled" class="w-4 h-4">
Enable Telegram notifications
</label>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Bot token</label>
<input type="text" id="settingsTelegramToken" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="123456:ABC...">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 mb-1">Chat ID</label>
<input type="text" id="settingsTelegramChatId" class="w-full px-4 py-2 border border-slate-300 rounded-lg" placeholder="-1001234567890">
</div>
</div>
</section>
</div>
<div class="p-6 border-t border-slate-200 bg-slate-50 flex justify-end gap-3">
<button onclick="app.closeModal('settingsModal')" class="px-4 py-2 text-slate-600 hover:bg-slate-200 rounded-lg transition-colors">Cancel</button>
<button onclick="app.saveSettings()" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">Save settings</button>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toastContainer" class="fixed bottom-6 right-6 z-50 flex flex-col gap-2"></div>
<script src="assets/app.js"></script>
</body>
</html>