<!DOCTYPE html>
<?php
/* Data Studio — full-screen, phpMyAdmin-class database workspace.
   Vars from DatabaseController::studio(): $baseUrl, $assetsUrl, $user, $isOwner,
   $csrf, $databases, $shareUsers, $planSlug, $planName, $planCapMb, $quota, $targets.
   Interaction code is lifted, near-verbatim, from the sibling Views/index.php so the
   proven table builder / row editor / SQL console / snapshots / structure behave identically. */
$databases = is_array($databases ?? null) ? $databases : [];
$quota     = is_array($quota ?? null) ? $quota : ['default' => 3, 'mine' => -1, 'used' => 0];
$targets   = is_array($targets ?? null) ? $targets : [];
$csrf      = $csrf ?? \PpPhp\Core\Security::CSRF()->Token();
$planName  = (string) ($planName ?? 'Standard');
$planCapMb = $planCapMb ?? null;
$shareUsers = !empty($shareUsers);
$mineMax   = (int) ($quota['mine'] ?? -1);
$usedCnt   = (int) ($quota['used'] ?? count($databases));
$u         = static fn (string $p): string => rtrim((string) $baseUrl, '/') . $p;
?>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title><?= htmlspecialchars(\PpPhp\Core\Brand::name()) ?> · Data Studio</title>
    <?php include constant('BASE_PATH') . '/core/views/favicon.php'; ?>
    <link href="<?= $assetsUrl ?>/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <link href="<?= $assetsUrl ?>/icons/bootstrap-icons.css" rel="stylesheet">
    <?php include constant('BASE_PATH') . '/core/views/theme-head.php'; ?>
    <link href="<?= $assetsUrl ?>/sweetalert2/sweetalert2.min.css" rel="stylesheet">
    <script src="<?= $assetsUrl ?>/sweetalert2/sweetalert2.min.js"></script>
    <script src="<?= $assetsUrl ?>/bootstrap/js/bootstrap.bundle.min.js" defer></script>
    <style>
        :root {
            --st-accent: #7c3aed; --st-accent-2: #6d28d9;
            --st-rail-w: 300px;
        }
        html, body { height: 100%; margin: 0; }
        body { background: var(--pp-app-bg, #0f1117); color: var(--pp-text, #e5e9f2); overflow: hidden;
            font-size: 14px; -webkit-font-smoothing: antialiased; }
        * { box-sizing: border-box; }
        a { text-decoration: none; }

        /* ── shell ── */
        .studio { display: flex; flex-direction: column; height: calc(100vh - 56px); margin-top: 56px; }
        .studio-top { flex: 0 0 auto; display: flex; align-items: center; gap: .9rem;
            padding: .5rem .95rem; background: var(--pp-surface, #fff); color: var(--pp-text, #172033);
            border-bottom: 1px solid var(--pp-border, #dbe3ee); }
        .studio-top .brand { font-weight: 800; letter-spacing: -.02em; font-size: 1rem; white-space: nowrap; }
        .studio-top a { color: var(--pp-text-muted, #64748b); font-size: .82rem; }
        .studio-top a:hover { color: var(--pp-text, #172033); }
        .studio-crumb { font-size: .84rem; color: var(--pp-text-muted, #9aa4b6); display: flex; align-items: center; gap: .35rem;
            min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
        .studio-crumb b { color: var(--pp-text, #e5e9f2); font-weight: 700; }
        .studio-crumb .sep { opacity: .5; }
        .st-badge { font-size: .72rem; background: var(--pp-surface-muted, #21262f); border: 1px solid var(--pp-border, #2c333f);
            border-radius: 999px; padding: .22rem .6rem; white-space: nowrap; }
        .studio-body { flex: 1 1 auto; display: flex; min-height: 0; }

        /* ── left rail ── */
        .studio-rail { flex: 0 0 var(--st-rail-w); width: var(--st-rail-w); display: flex; flex-direction: column;
            background: var(--pp-surface, #12151c); border-right: 1px solid var(--pp-border, #262b36); min-height: 0; }
        .rail-head { padding: .7rem .75rem .55rem; border-bottom: 1px solid var(--pp-border, #262b36); }
        .rail-tree { flex: 1 1 auto; overflow-y: auto; padding: .4rem .45rem; }
        .rail-foot { flex: 0 0 auto; padding: .55rem .75rem; border-top: 1px solid var(--pp-border, #262b36);
            font-size: .74rem; color: var(--pp-text-muted, #8a94a6); display: flex; align-items: center; justify-content: space-between; gap: .4rem; }
        .st-input { width: 100%; border: 1px solid var(--pp-border, #cdd6e4); border-radius: 8px; padding: .34rem .55rem;
            font-size: .82rem; background: var(--pp-input-bg, #fff); color: var(--pp-text, inherit); }
        .st-input:focus { outline: none; border-color: var(--st-accent); }

        .db-node { margin-bottom: .12rem; }
        .db-head { display: flex; align-items: center; gap: .4rem; width: 100%; text-align: left; cursor: pointer;
            border: 0; background: none; color: inherit; padding: .4rem .5rem; border-radius: 8px; font-size: .85rem; }
        .db-head:hover { background: var(--pp-surface-muted, #1a1e26); }
        .db-head.active { background: color-mix(in srgb, var(--st-accent) 16%, transparent); color: var(--st-accent); }
        .db-head .chev { transition: transform .15s; font-size: .7rem; opacity: .7; }
        .db-head.open .chev { transform: rotate(90deg); }
        .db-head .nm { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 650; }
        .db-head .cnt { font-size: .68rem; color: var(--pp-text-muted, #8a94a6); }
        .db-lock { font-size: .66rem; color: #f59e0b; }
        .db-meter { height: 3px; border-radius: 3px; background: var(--pp-surface-muted, #262b36); margin: 0 .5rem .3rem 1.7rem; overflow: hidden; }
        .db-meter > i { display: block; height: 100%; border-radius: 3px; }
        .db-children { padding: .1rem 0 .3rem 1.35rem; }
        .obj-row { display: flex; align-items: center; gap: .4rem; width: 100%; text-align: left; cursor: pointer;
            border: 0; background: none; color: inherit; padding: .3rem .5rem; border-radius: 7px; font-size: .8rem; }
        .obj-row:hover { background: var(--pp-surface-muted, #1a1e26); }
        .obj-row.active { background: color-mix(in srgb, var(--st-accent) 18%, transparent); color: var(--st-accent); font-weight: 650; }
        .obj-row .nm { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .obj-row .cnt { font-size: .68rem; color: var(--pp-text-muted, #8a94a6); }

        .lvl-ok { color: #10b981; } .lvl-warn { color: #f59e0b; } .lvl-critical, .lvl-over { color: #ef4444; }
        .bg-ok { background: #10b981; } .bg-warn { background: #f59e0b; } .bg-critical, .bg-over { background: #ef4444; }

        /* ── main workspace ── */
        .studio-main { flex: 1 1 auto; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
        .main-tabs { flex: 0 0 auto; display: flex; gap: .25rem; padding: .5rem .8rem 0; border-bottom: 1px solid var(--pp-border, #262b36);
            background: var(--pp-surface, #12151c); overflow-x: auto; }
        .main-tab { border: 1px solid transparent; border-bottom: 0; background: none; color: var(--pp-text-muted, #9aa4b6);
            border-radius: 8px 8px 0 0; padding: .4rem .85rem; font-size: .82rem; font-weight: 600; cursor: pointer; white-space: nowrap; }
        .main-tab:hover { color: var(--pp-text, #e5e9f2); }
        .main-tab.active { color: var(--st-accent); border-color: var(--pp-border, #262b36); border-bottom-color: var(--pp-surface, #12151c);
            background: var(--pp-app-bg, #0f1117); position: relative; top: 1px; }
        .main-scroll { flex: 1 1 auto; overflow: auto; padding: 1rem 1.1rem 3rem; }
        .pane { display: none; } .pane.active { display: block; }
        .st-empty { text-align: center; color: var(--pp-text-muted, #8a94a6); padding: 3rem 1rem; }
        .st-empty i { font-size: 2.2rem; display: block; margin-bottom: .5rem; opacity: .5; }

        /* meter banner */
        .st-meterbar { background: var(--pp-surface, #171b23); border: 1px solid var(--pp-border, #262b36); border-radius: 12px;
            padding: .7rem .85rem; margin-bottom: 1rem; }
        .st-meterbar .track { height: 8px; border-radius: 6px; background: var(--pp-surface-muted, #262b36); overflow: hidden; margin: .45rem 0 .3rem; }
        .st-meterbar .track > i { display: block; height: 100%; border-radius: 6px; }
        .st-ro-banner { background: color-mix(in srgb, #f59e0b 14%, transparent); border: 1px solid color-mix(in srgb, #f59e0b 40%, transparent);
            color: #f59e0b; border-radius: 10px; padding: .5rem .75rem; margin-bottom: 1rem; font-size: .82rem; }
        .st-pause { color: #ef4444; font-weight: 600; }

        /* buttons */
        .st-btn { border: 1px solid var(--pp-border, #2c333f); background: var(--pp-surface-muted, #1a1e26); color: var(--pp-text, #d7deea);
            border-radius: 8px; padding: .34rem .62rem; font-size: .8rem; cursor: pointer; display: inline-flex; align-items: center; gap: .35rem; }
        .st-btn:hover { border-color: var(--st-accent); color: var(--st-accent); }
        .st-btn.primary { background: var(--st-accent); border-color: var(--st-accent); color: #fff; font-weight: 700; }
        .st-btn.primary:hover { background: var(--st-accent-2); color: #fff; }
        .st-btn.danger:hover { border-color: #ef4444; color: #ef4444; }
        .st-flex { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
        .st-toolbar { display: flex; align-items: center; gap: .45rem; flex-wrap: wrap; margin-bottom: .85rem; }
        .st-note { font-size: .76rem; color: var(--pp-text-muted, #8a94a6); }
        .st-actions { display: flex; gap: .28rem; flex-wrap: nowrap; white-space: nowrap; }

        /* listing / grid */
        .st-tablewrap { overflow-x: auto; }
        .st-table { width: 100%; border-collapse: collapse; font-size: .84rem; }
        .st-table th { text-align: left; font-size: .68rem; text-transform: uppercase; letter-spacing: .04em;
            color: var(--pp-text-muted, #8a94a6); padding: .45rem .6rem; border-bottom: 1px solid var(--pp-border, #262b36);
            position: sticky; top: 0; background: var(--pp-app-bg, #0f1117); }
        .st-table td { padding: .5rem .6rem; border-bottom: 1px solid var(--pp-border, #1c2129); vertical-align: middle; }
        .st-table tr:hover td { background: color-mix(in srgb, var(--st-accent) 6%, transparent); }
        .st-grid { width: 100%; border-collapse: collapse; font-size: .82rem; white-space: nowrap; }
        .st-grid th { position: sticky; top: 0; background: var(--pp-surface-muted, #1a1e26); color: var(--pp-text-muted, #8a94a6);
            padding: .42rem .55rem; text-align: left; font-size: .68rem; text-transform: uppercase; letter-spacing: .04em;
            cursor: pointer; user-select: none; border-bottom: 1px solid var(--pp-border, #262b36); }
        .st-grid td { padding: .4rem .55rem; border-bottom: 1px solid var(--pp-border, #1c2129);
            max-width: 340px; overflow: hidden; text-overflow: ellipsis; }
        .st-grid tr:hover td { background: color-mix(in srgb, var(--st-accent) 6%, transparent); }
        .st-null { color: var(--pp-text-muted, #6b7488); font-style: italic; }
        .st-blob { color: #f59e0b; font-size: .72rem; }
        .st-pager { display: flex; align-items: center; gap: .6rem; margin-top: .7rem; font-size: .8rem; }

        /* sql */
        .st-sql { width: 100%; min-height: 160px; font-family: ui-monospace, Menlo, monospace; font-size: .84rem;
            border: 1px solid var(--pp-border, #2c333f); border-radius: 10px; padding: .65rem .75rem;
            background: var(--pp-input-bg, #fff); color: var(--pp-text, inherit); }
        .st-report { font-size: .8rem; margin-top: .8rem; }
        .st-report .ok { color: #10b981; } .st-report .err { color: #ef4444; }
        .st-report pre { background: var(--pp-surface-muted, #1a1e26); border-radius: 8px; padding: .55rem .65rem;
            margin: .25rem 0 .6rem; white-space: pre-wrap; }
        .card-box { background: var(--pp-surface, #171b23); border: 1px solid var(--pp-border, #262b36); border-radius: 12px;
            padding: 1rem 1.05rem; margin-bottom: 1rem; }
        .card-box h6 { font-weight: 800; font-size: .9rem; display: flex; align-items: center; gap: .45rem; margin: 0 0 .7rem; }

        /* modal (table / view builder) */
        .st-modal-back { position: fixed; inset: 0; background: rgba(2,6,23,.6); backdrop-filter: blur(2px); z-index: 1080;
            display: none; align-items: flex-start; justify-content: center; overflow: auto; padding: 3vh 1rem; }
        .st-modal-back.show { display: flex; }
        .st-modal { background: var(--pp-surface, #171b23); color: var(--pp-text, #e5e9f2); border: 1px solid var(--pp-border, #2c333f);
            border-radius: 14px; width: min(920px, 100%); box-shadow: 0 24px 60px rgba(2,6,23,.5); }
        .st-modal-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; padding: .8rem 1rem;
            border-bottom: 1px solid var(--pp-border, #2c333f); font-weight: 800; }
        .st-modal-head .x { background: none; border: 0; font-size: 1.1rem; color: var(--pp-text-muted, #8a94a6); cursor: pointer; }
        .st-modal-body { padding: 1rem; max-height: 68vh; overflow: auto; }
        .st-modal-foot { display: flex; align-items: center; justify-content: flex-end; gap: .5rem; padding: .8rem 1rem;
            border-top: 1px solid var(--pp-border, #2c333f); }
        .st-cols { width: 100%; border-collapse: collapse; font-size: .8rem; }
        .st-cols th { text-align: left; font-size: .66rem; text-transform: uppercase; letter-spacing: .04em;
            color: var(--pp-text-muted, #8a94a6); padding: .3rem .35rem; border-bottom: 1px solid var(--pp-border, #2c333f); }
        .st-cols td { padding: .25rem .35rem; vertical-align: middle; border-bottom: 1px solid var(--pp-border, #1c2129); }
        .st-cols input[type=text], .st-cols input[type=number], .st-cols select {
            width: 100%; border: 1px solid var(--pp-border, #2c333f); border-radius: 7px; padding: .28rem .4rem;
            font-size: .8rem; background: var(--pp-input-bg, #fff); color: var(--pp-text, inherit); }
        .st-cols input[type=number] { width: 74px; }
        .st-cols td.mid { text-align: center; }
        .st-cols .drag { cursor: grab; color: var(--pp-text-muted, #8a94a6); }
        .st-cols tr.dragging { opacity: .45; }
        .st-name-in { font-weight: 600; }
        .search-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .7rem; margin-bottom: 1rem; }
        .search-grid label { display: block; font-size: .74rem; font-weight: 700; margin-bottom: .2rem; }
        .search-grid .ty { color: var(--pp-text-muted, #8a94a6); font-weight: 400; }
    </style>
</head>
<body>
<?php
// Page-specific right-cluster items for the shared platform navbar.
ob_start(); ?>
    <span class="badge rounded-pill text-bg-dark border border-secondary d-none d-md-inline-flex align-items-center" style="font-weight:600;font-size:.72rem" title="Your plan and the per-database size limit it grants"><i class="bi bi-gem me-1 text-warning"></i><?= htmlspecialchars($planName) ?> · <?= $planCapMb === null ? 'unlimited' : (int) $planCapMb . ' MB / db' ?></span>
    <button class="btn btn-sm btn-outline-light" data-guide-open="studioGuide" title="Open the Data Studio guide"><i class="bi bi-question-circle"></i><span class="d-none d-lg-inline ms-1">Guide</span></button>
<?php if ($isOwner): ?><button class="btn btn-sm btn-outline-light" id="stSizeCaps" title="Set the per-plan database size caps"><i class="bi bi-sliders"></i><span class="d-none d-lg-inline ms-1">Size caps</span></button><?php endif;
$navExtra = ob_get_clean();
$navBrandLabel = 'Data Studio'; $navBrandIcon = 'window-sidebar';
$navBack = ['url' => $u('/database-manager'), 'label' => 'Back to Database Engine', 'icon' => 'hdd-stack'];
include constant('BASE_PATH') . '/core/views/console-navbar.php';
?>
<div class="studio">
    <!-- ── CONTEXT STRIP (breadcrumb) ── -->
    <div class="studio-top">
        <div class="studio-crumb" id="stCrumb"><i class="bi bi-hdd-network"></i> Server</div>
        <a class="ms-auto d-md-none" href="<?= $u('/database-manager') ?>" style="font-size:.8rem"><i class="bi bi-arrow-left"></i> Engine</a>
    </div>

    <div class="studio-body">
        <!-- ── LEFT RAIL ── -->
        <aside class="studio-rail">
            <div class="rail-head">
                <button class="st-btn primary w-100 mb-2" id="stNewDb" data-tip="Create a private database instantly"><i class="bi bi-plus-lg"></i> New database</button>
                <input type="text" class="st-input" id="stDbFilter" placeholder="Filter databases…" autocomplete="off">
            </div>
            <div class="rail-tree" id="stTree"></div>
            <div class="rail-foot">
                <span id="stSeat"></span>
                <span class="st-badge"><?= htmlspecialchars($planName) ?></span>
            </div>
        </aside>

        <!-- ── MAIN WORKSPACE ── -->
        <main class="studio-main">
            <div class="main-tabs" id="stTabs"></div>
            <div class="main-scroll" id="stPanes">
                <div class="st-empty" id="stWelcome">
                    <i class="bi bi-database"></i>
                    Select a database on the left to browse its tables, run SQL, and manage it.
                </div>

                <!-- BROWSE (table) -->
                <div class="pane" data-pane="browse">
                    <div class="st-toolbar">
                        <input type="text" id="stRowSearch" class="st-input" placeholder="Search all columns…" style="flex:1;max-width:340px">
                        <button class="st-btn wr" id="stAddRow"><i class="bi bi-plus-lg"></i> Add row</button>
                        <span class="st-note" id="stBrowseMeta"></span>
                    </div>
                    <div class="st-tablewrap"><table class="st-grid" id="stRows"><thead></thead><tbody></tbody></table></div>
                    <div class="st-pager">
                        <button class="st-btn" id="stPrev">‹ Prev</button>
                        <span id="stPageInfo"></span>
                        <button class="st-btn" id="stNext">Next ›</button>
                        <span class="st-note" id="stEditableNote"></span>
                    </div>
                </div>

                <!-- STRUCTURE (db listing OR table columns) -->
                <div class="pane" data-pane="structure">
                    <div id="stStructDb"></div>
                    <div id="stStructTable" style="display:none">
                        <div class="st-toolbar">
                            <button class="st-btn primary wr" id="stEditStruct"><i class="bi bi-pencil-square"></i> Edit structure</button>
                            <span class="st-note" id="stStructMeta"></span>
                        </div>
                        <div class="st-tablewrap"><table class="st-grid" id="stStructure"><thead></thead><tbody></tbody></table></div>
                        <div class="mt-2 st-note" id="stStructureExtra"></div>
                    </div>
                </div>

                <!-- SQL -->
                <div class="pane" data-pane="sql">
                    <div class="st-ro-banner" id="stSqlRo" style="display:none"><i class="bi bi-lock-fill"></i> Read-only access — SELECT works, but writes are blocked by the engine.</div>
                    <textarea class="st-sql" id="stSqlText" placeholder="SELECT * FROM …  — any SQL works: CREATE TABLE, INSERT, UPDATE, PRAGMA…"></textarea>
                    <div class="st-flex mt-2">
                        <button class="st-btn primary" id="stSqlRun"><i class="bi bi-play-fill"></i> Run</button>
                        <label class="st-note"><input type="checkbox" id="stSqlTx"> wrap in transaction</label>
                        <label class="st-note"><input type="checkbox" id="stSqlDry"> dry run (roll back)</label>
                    </div>
                    <div class="st-report" id="stSqlReport"></div>
                </div>

                <!-- SEARCH -->
                <div class="pane" data-pane="search">
                    <div id="stSearchBody"></div>
                </div>

                <!-- EXPORT -->
                <div class="pane" data-pane="export">
                    <div class="card-box" style="max-width:520px">
                        <h6><i class="bi bi-filetype-sql"></i> Export SQL script</h6>
                        <label class="st-note d-block mb-1">Write the script for:</label>
                        <select id="stExpTarget" class="st-input mb-2"></select>
                        <label class="st-note d-block mb-1">Contents:</label>
                        <select id="stExpMode" class="st-input mb-3">
                            <option value="both">Structure and data</option>
                            <option value="structure">Structure only</option>
                            <option value="data">Data only</option>
                        </select>
                        <button class="st-btn primary" id="stExpGo"><i class="bi bi-download"></i> Download</button>
                    </div>
                </div>

                <!-- IMPORT -->
                <div class="pane" data-pane="import">
                    <div class="card-box" style="max-width:560px">
                        <h6><i class="bi bi-upload"></i> Import data</h6>
                        <label class="st-note d-block mb-1">File (.sql script or .csv rows):</label>
                        <input type="file" id="stImpFile" class="st-input mb-2" accept=".sql,.csv">
                        <label class="st-note d-block mb-1">Target table (CSV — header row must match column names):</label>
                        <select id="stImpTable" class="st-input mb-3"></select>
                        <button class="st-btn primary" id="stImpGo"><i class="bi bi-box-arrow-in-down"></i> Import</button>
                        <div class="st-note mt-2" id="stImpMsg"></div>
                    </div>
                </div>

                <!-- OPERATIONS -->
                <div class="pane" data-pane="operations">
                    <div id="stOpsDb"></div>
                    <div id="stOpsTable" style="display:none"></div>
                </div>
            </div>
        </main>
    </div>
</div>

<!-- Visual table builder modal (lifted from index.php) -->
<div class="st-modal-back" id="stTblModal">
    <div class="st-modal">
        <div class="st-modal-head"><span id="stTblModalTitle"><i class="bi bi-table me-1"></i> New table</span><button class="x" id="stTblModalX">&times;</button></div>
        <div class="st-modal-body">
            <label style="display:block;font-size:.75rem;font-weight:700;margin-bottom:.15rem">Table name</label>
            <input type="text" id="stTblName" class="st-input" style="width:100%;margin-bottom:.8rem" placeholder="e.g. customers" autocomplete="off">
            <table class="st-cols">
                <thead><tr>
                    <th style="width:1%"></th><th>Column</th><th style="width:130px">Type</th><th style="width:84px">Length</th>
                    <th style="width:1%" class="mid" title="Nullable">Null</th><th style="width:1%" class="mid" title="Primary key">PK</th>
                    <th style="width:1%" class="mid" title="Auto-increment identity">AI</th><th style="width:150px">Default</th><th style="width:1%"></th>
                </tr></thead>
                <tbody id="stColRows"></tbody>
            </table>
            <button class="st-btn mt-2" id="stAddColBtn"><i class="bi bi-plus-lg"></i> Add column</button>
            <div class="st-note mt-2">Tip: mark one INTEGER column as <b>AI</b> for an auto-incrementing primary key. Reorder with the <i class="bi bi-grip-vertical"></i> handle.</div>
        </div>
        <div class="st-modal-foot">
            <span class="st-note me-auto" id="stTblErr" style="color:#ef4444"></span>
            <button class="st-btn" id="stTblCancel">Cancel</button>
            <button class="st-btn primary" id="stTblSave"><i class="bi bi-check-lg"></i> Save table</button>
        </div>
    </div>
</div>

<script>
(() => {
    'use strict';
    const BASE = <?= json_encode($u(''), JSON_UNESCAPED_SLASHES) ?>;
    const CSRF = <?= json_encode($csrf) ?>;
    const IS_OWNER = <?= $isOwner ? 'true' : 'false' ?>;
    const SHARE_USERS = <?= $shareUsers ? 'true' : 'false' ?>;
    const TARGETS = <?= json_encode($targets, JSON_UNESCAPED_SLASHES) ?>;
    const SEED = <?= json_encode($databases, JSON_UNESCAPED_SLASHES) ?>;
    const SEAT_MAX = <?= (int) $mineMax ?>;
    const MY_PREFIX = <?= json_encode($myPrefix ?? 'user') ?>;
    const ASSIGN_USERS = <?= json_encode($assignUsers ?? [], JSON_UNESCAPED_SLASHES) ?>;
    let DBS = Array.isArray(SEED) ? SEED : [];
    // Mirror of VirtualDb::userPrefix — the cPanel-style account prefix for a name.
    const prefixOf = (u) => (String(u || '').toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 24)) || 'user';
    const slugPart = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');

    /* ── helpers (lifted from index.php) ── */
    const $ = (id) => document.getElementById(id);
    const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
    const post = async (path, body) => {
        const r = await fetch(BASE + path, { method: 'POST', credentials: 'same-origin',
            headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...body, _token: CSRF }) });
        return r.json();
    };
    const getJson = async (path) => (await fetch(BASE + path, { credentials: 'same-origin' })).json();
    const toast = (ok, msg) => Swal.fire({ toast: true, position: 'top-end', timer: 2300, showConfirmButton: false,
        icon: ok ? 'success' : 'error', title: msg });
    const targetId = (t) => (t && (t.value ?? t.id)) ?? '';

    /* ── studio state ── */
    const S = {
        db: null, mode: 'none', objType: 'TABLE',
        objects: [], objStats: null,
        table: '', tab: '',
        page: 1, per: 25, q: '', sort: '', dir: 'asc', filters: {},
        editable: true, columns: [],
    };
    const canWrite = () => S.db && ['owner', 'manage', 'write'].includes(S.db.access);

    const TABS_DB    = ['structure', 'sql', 'search', 'export', 'import', 'operations'];
    const TABS_TABLE = ['browse', 'structure', 'sql', 'search', 'insert', 'export', 'operations'];
    const TAB_LABEL = { browse: 'Browse', structure: 'Structure', sql: 'SQL', search: 'Search',
        insert: 'Insert', export: 'Export', import: 'Import', operations: 'Operations' };

    /* ─────────────── meters ─────────────── */
    function meterBanner(usage) {
        if (!usage) return '';
        const lvl = usage.level || 'ok';
        const pct = Math.min(100, usage.percent || 0);
        const paused = lvl === 'over' ? ' <span class="st-pause">— writes paused</span>' : '';
        return `<div class="st-meterbar">
            <div class="st-flex" style="justify-content:space-between">
                <b class="lvl-${lvl}"><i class="bi bi-hdd"></i> ${esc(usage.label)}</b>
                <span class="st-note">${usage.unlimited ? 'no limit' : (usage.percent + '%')}${paused}</span>
            </div>
            <div class="track"><i class="bg-${lvl}" style="width:${usage.unlimited ? 0 : pct}%"></i></div>
            <div class="st-note">${esc(usage.message || '')}</div>
        </div>`;
    }

    /* ─────────────── left rail tree ─────────────── */
    function seatText() {
        const m = SEAT_MAX < 0 ? '∞' : SEAT_MAX;
        return `${DBS.length} of ${m} databases`;
    }
    function renderTree() {
        const box = $('stTree');
        const f = ($('stDbFilter').value || '').toLowerCase();
        box.innerHTML = '';
        const list = DBS.filter((d) => !f || (d.label || d.name || '').toLowerCase().includes(f) || (d.name || '').toLowerCase().includes(f));
        if (!list.length) { box.innerHTML = '<div class="st-note" style="padding:.8rem .6rem">No databases.</div>'; }
        list.forEach((d) => {
            const sel = S.db && S.db.id === d.id;
            const node = document.createElement('div');
            node.className = 'db-node';
            const lock = d.access === 'read' ? '<span class="db-lock" title="Read-only"><i class="bi bi-lock-fill"></i></span>' : '';
            const usage = d.usage || {};
            const lvl = usage.level || 'ok';
            const pct = usage.unlimited ? 0 : Math.min(100, usage.percent || 0);
            node.innerHTML = `
                <button class="db-head ${sel ? 'active open' : ''}" data-db="${d.id}">
                    <i class="bi bi-caret-right-fill chev"></i>
                    <i class="bi bi-database"></i>
                    <span class="nm" title="${esc(d.label || '')}">${esc(d.name || d.label)}</span>
                    ${lock}
                    <span class="cnt">${d.tables ?? 0}</span>
                </button>
                <div class="db-meter" title="${esc(usage.label || '')}"><i class="bg-${lvl}" style="width:${pct}%"></i></div>
                <div class="db-children" data-children="${d.id}" style="display:${sel ? 'block' : 'none'}"></div>`;
            box.appendChild(node);
            if (sel) renderObjectsInto(node.querySelector(`[data-children="${d.id}"]`));
        });
        $('stSeat').textContent = seatText();
    }
    function renderObjectsInto(host) {
        if (!host) return;
        if (!S.objects.length) { host.innerHTML = '<div class="st-note" style="padding:.2rem .5rem">No tables yet.</div>'; return; }
        host.innerHTML = '';
        S.objects.forEach((o) => {
            const b = document.createElement('button');
            b.className = 'obj-row' + (o.name === S.table ? ' active' : '');
            b.dataset.table = o.name;
            b.dataset.type = o.type;
            b.innerHTML = `<i class="bi bi-${o.type === 'VIEW' ? 'eye' : 'table'}"></i><span class="nm">${esc(o.name)}</span><span class="cnt">${o.type === 'VIEW' ? 'view' : o.rows}</span>`;
            host.appendChild(b);
        });
    }
    $('stDbFilter').addEventListener('input', renderTree);
    $('stTree').addEventListener('click', (e) => {
        const dbBtn = e.target.closest('[data-db]');
        const objBtn = e.target.closest('[data-table]');
        if (objBtn) { selectTable(objBtn.dataset.table, objBtn.dataset.type); return; }
        if (dbBtn) {
            const id = parseInt(dbBtn.dataset.db, 10);
            const db = DBS.find((x) => x.id === id);
            if (db) selectDb(db);
        }
    });

    /* ─────────────── crumb + tabs ─────────────── */
    function renderCrumb() {
        let h = '<i class="bi bi-hdd-network"></i> Server';
        if (S.db) h += ` <span class="sep">›</span> <b>${esc(S.db.name || S.db.label)}</b>`;
        if (S.table) h += ` <span class="sep">›</span> <b>${esc(S.table)}</b>`;
        if (S.db && S.db.access === 'read') h += ' <span class="db-lock"><i class="bi bi-lock-fill"></i> read-only</span>';
        $('stCrumb').innerHTML = h;
    }
    function renderTabs() {
        const bar = $('stTabs');
        bar.innerHTML = '';
        if (S.mode === 'none') return;
        const tabs = (S.mode === 'table' ? TABS_TABLE : TABS_DB).slice();
        // hide write-only tabs for read-only DBs
        const ro = S.db && S.db.access === 'read';
        tabs.forEach((t) => {
            if (ro && (t === 'import' || t === 'insert')) return;
            const b = document.createElement('button');
            b.className = 'main-tab' + (t === S.tab ? ' active' : '');
            b.dataset.tab = t;
            b.textContent = TAB_LABEL[t];
            bar.appendChild(b);
        });
    }
    $('stTabs').addEventListener('click', (e) => {
        const b = e.target.closest('[data-tab]');
        if (!b) return;
        if (b.dataset.tab === 'insert') { addRow(); return; }
        switchTab(b.dataset.tab);
    });
    function switchTab(name) {
        S.tab = name;
        $('stWelcome').style.display = 'none';
        document.querySelectorAll('.main-tab').forEach((t) => t.classList.toggle('active', t.dataset.tab === name));
        document.querySelectorAll('.pane').forEach((p) => p.classList.toggle('active', p.dataset.pane === name));
        renderPane(name);
    }
    function renderPane(name) {
        switch (name) {
            case 'browse': loadRows(); break;
            case 'structure': renderStructurePane(); break;
            case 'sql': $('stSqlRo').style.display = (S.db && S.db.access === 'read') ? '' : 'none'; break;
            case 'search': renderSearchPane(); break;
            case 'export': renderExportPane(); break;
            case 'import': renderImportPane(); break;
            case 'operations': renderOperationsPane(); break;
        }
    }

    /* ─────────────── selection ─────────────── */
    async function selectDb(db) {
        S.db = db; S.table = ''; S.objType = 'TABLE'; S.mode = 'db';
        S.page = 1; S.q = ''; S.sort = ''; S.dir = 'asc'; S.filters = {};
        renderCrumb();
        await loadObjects();
        S.tab = 'structure';
        renderTabs();
        switchTab('structure');
    }
    async function selectTable(name, type) {
        if (!S.db) return;
        S.table = name; S.objType = (type || 'TABLE').toUpperCase(); S.mode = 'table';
        S.page = 1; S.q = ''; S.sort = ''; S.dir = 'asc'; S.filters = {};
        $('stRowSearch').value = '';
        renderCrumb();
        renderTree();
        S.tab = 'browse';
        renderTabs();
        // pull structure so Search/Insert have columns even before Browse
        await loadRowsMeta();
        switchTab('browse');
    }

    async function loadObjects() {
        const d = await getJson('/database-manager/api/tables?db=' + S.db.id);
        if (!d.success) { toast(false, d.message || 'Could not read the database'); S.objects = []; return; }
        S.objects = d.objects || [];
        S.objStats = d.stats || null;
        // keep the rail row count fresh
        const idx = DBS.findIndex((x) => x.id === S.db.id);
        if (idx >= 0 && d.stats) { DBS[idx].tables = d.stats.tables; }
        renderTree();
    }
    /* fetch columns only (page 1) so Search/Insert panels are populated */
    async function loadRowsMeta() {
        if (!S.db || !S.table) return;
        const p = new URLSearchParams({ db: S.db.id, table: S.table, page: 1, per: 1 });
        const d = await getJson('/database-manager/api/rows?' + p.toString());
        if (d.success) { S.columns = d.columns || []; S.editable = d.editable; }
    }

    /* ─────────────── browse grid (lifted) ─────────────── */
    async function loadRows() {
        if (!S.db || !S.table) return;
        const p = new URLSearchParams({ db: S.db.id, table: S.table, page: S.page, per: S.per, q: S.q, sort: S.sort, dir: S.dir });
        if (Object.keys(S.filters).length) p.set('filters', JSON.stringify(S.filters));
        const d = await getJson('/database-manager/api/rows?' + p.toString());
        if (!d.success) { toast(false, d.message || 'Could not load rows'); return; }
        S.editable = d.editable; S.columns = d.columns;
        const wr = canWrite() && d.editable;
        $('stAddRow').style.display = wr ? '' : 'none';
        const thead = document.querySelector('#stRows thead');
        const tbody = document.querySelector('#stRows tbody');
        thead.innerHTML = '<tr>' + d.columns.map((c) =>
            `<th data-sort="${esc(c.name)}">${esc(c.name)}${c.pk ? ' <i class="bi bi-key-fill" style="color:#f59e0b"></i>' : ''}${S.sort === c.name ? (S.dir === 'asc' ? ' ▲' : ' ▼') : ''}</th>`).join('')
            + (wr ? '<th></th>' : '') + '</tr>';
        tbody.innerHTML = '';
        d.rows.forEach((r) => {
            const tr = document.createElement('tr');
            let cells = '';
            d.columns.forEach((c) => {
                const v = r[c.name];
                if (v === null || v === undefined) cells += '<td><span class="st-null">NULL</span></td>';
                else if (typeof v === 'object' && v.__blob) cells += `<td><span class="st-blob">BLOB ${v.bytes} B · ${esc(v.hex)}…</span></td>`;
                else cells += `<td title="${esc(v)}">${esc(String(v).length > 120 ? String(v).slice(0, 120) + '…' : v)}</td>`;
            });
            if (wr) {
                cells += `<td class="st-actions">
                    <button class="st-btn" data-edit-rid="${r.__rid}" title="Edit"><i class="bi bi-pencil"></i></button>
                    <button class="st-btn danger" data-del-rid="${r.__rid}" title="Delete"><i class="bi bi-trash3"></i></button></td>`;
            }
            tr.innerHTML = cells;
            tr.dataset.row = JSON.stringify(r);
            tbody.appendChild(tr);
        });
        const pages = Math.max(1, Math.ceil(d.total / S.per));
        $('stPageInfo').textContent = `Page ${S.page} / ${pages} · ${d.total} rows`;
        $('stPrev').disabled = S.page <= 1;
        $('stNext').disabled = S.page >= pages;
        $('stBrowseMeta').textContent = S.table + (Object.keys(S.filters).length ? ' · filtered' : '');
        $('stEditableNote').textContent = d.editable ? '' : 'read-only (WITHOUT ROWID or view)';
    }
    document.querySelector('#stRows thead').addEventListener('click', (e) => {
        const th = e.target.closest('[data-sort]');
        if (!th) return;
        const col = th.dataset.sort;
        if (S.sort === col) S.dir = S.dir === 'asc' ? 'desc' : 'asc';
        else { S.sort = col; S.dir = 'asc'; }
        loadRows();
    });
    $('stPrev').addEventListener('click', () => { if (S.page > 1) { S.page--; loadRows(); } });
    $('stNext').addEventListener('click', () => { S.page++; loadRows(); });
    let searchTimer = null;
    $('stRowSearch').addEventListener('input', () => {
        clearTimeout(searchTimer);
        searchTimer = setTimeout(() => { S.q = $('stRowSearch').value.trim(); S.page = 1; loadRows(); }, 320);
    });

    /* row editor (lifted) */
    async function rowEditor(existing) {
        const fields = S.columns.map((c) => {
            const raw = existing ? existing[c.name] : null;
            const isBlob = raw && typeof raw === 'object' && raw.__blob;
            const val = raw === null || raw === undefined || isBlob ? '' : String(raw);
            return `<div style="text-align:left;margin-bottom:.5rem">
                <label style="font-size:.75rem;font-weight:700">${esc(c.name)} <span style="color:#8a94a6">${esc(c.type)}${c.pk ? ' · PK' : ''}${c.notnull ? ' · NOT NULL' : ''}</span></label>
                ${isBlob ? `<div class="st-blob">BLOB kept as-is (edit via SQL)</div>`
                    : `<textarea data-field="${esc(c.name)}" class="form-control" rows="1" style="font-size:.85rem" ${existing === null && c.pk ? 'placeholder="auto"' : ''}>${esc(val)}</textarea>
                       <label style="font-size:.7rem;color:#8a94a6"><input type="checkbox" data-null="${esc(c.name)}" ${raw === null && existing ? 'checked' : ''}> NULL</label>`}
            </div>`;
        }).join('');
        const r = await Swal.fire({
            title: existing ? 'Edit row' : 'Add row', html: `<div style="max-height:55vh;overflow:auto">${fields}</div>`,
            width: 640, showCancelButton: true, confirmButtonText: existing ? 'Save' : 'Insert',
            preConfirm: () => {
                const data = {};
                S.columns.forEach((c) => {
                    const t = document.querySelector(`[data-field="${CSS.escape(c.name)}"]`);
                    const n = document.querySelector(`[data-null="${CSS.escape(c.name)}"]`);
                    if (!t) return;
                    if (n && n.checked) { data[c.name] = null; return; }
                    if (!existing && c.pk && t.value === '') return;
                    data[c.name] = t.value;
                });
                return data;
            }
        });
        return r.isConfirmed ? r.value : null;
    }
    async function addRow() {
        if (!S.table) { toast(false, 'Pick a table first.'); return; }
        if (!canWrite() || !S.editable) { toast(false, 'This table is read-only here.'); return; }
        if (!S.columns.length) await loadRowsMeta();
        const data = await rowEditor(null);
        if (!data) return;
        const out = await post('/database-manager/api/row-save', { db: S.db.id, table: S.table, rid: null, data });
        toast(out.success, out.message);
        if (out.success) { if (S.tab !== 'browse') switchTab('browse'); else loadRows(); loadObjects(); }
    }
    $('stAddRow').addEventListener('click', addRow);
    document.querySelector('#stRows tbody').addEventListener('click', async (e) => {
        const edit = e.target.closest('[data-edit-rid]');
        const del = e.target.closest('[data-del-rid]');
        if (edit) {
            const row = JSON.parse(edit.closest('tr').dataset.row);
            const data = await rowEditor(row);
            if (!data) return;
            const out = await post('/database-manager/api/row-save', { db: S.db.id, table: S.table, rid: edit.dataset.editRid, data });
            toast(out.success, out.message);
            if (out.success) loadRows();
        } else if (del) {
            const r = await Swal.fire({ title: 'Delete this row?', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/api/row-delete', { db: S.db.id, table: S.table, rid: del.dataset.delRid });
            toast(out.success, out.message);
            loadRows(); loadObjects();
        }
    });

    /* ─────────────── structure pane ─────────────── */
    function renderStructurePane() {
        const dbBox = $('stStructDb'), tblBox = $('stStructTable');
        if (S.mode === 'table') { dbBox.style.display = 'none'; tblBox.style.display = ''; loadStructure(); }
        else { dbBox.style.display = ''; tblBox.style.display = 'none'; renderStructDb(); }
    }
    function renderStructDb() {
        const box = $('stStructDb');
        const ro = S.db && S.db.access === 'read';
        const tools = ro ? '' : `<div class="st-toolbar">
            <button class="st-btn primary" id="stNewTableBtn"><i class="bi bi-plus-lg"></i> New table</button>
            <button class="st-btn" id="stNewViewBtn"><i class="bi bi-eye"></i> New view</button>
            <button class="st-btn" id="stImportBtn"><i class="bi bi-upload"></i> Import</button>
        </div>`;
        let rows = '';
        S.objects.forEach((o) => {
            const isView = o.type === 'VIEW';
            const acts = `<div class="st-actions">
                <button class="st-btn" data-act="browse" data-t="${esc(o.name)}" data-ty="${o.type}" title="Browse"><i class="bi bi-table"></i></button>
                <button class="st-btn" data-act="structure" data-t="${esc(o.name)}" data-ty="${o.type}" title="Structure"><i class="bi bi-diagram-3"></i></button>
                <button class="st-btn" data-act="search" data-t="${esc(o.name)}" data-ty="${o.type}" title="Search"><i class="bi bi-search"></i></button>
                ${ro || isView ? '' : `<button class="st-btn" data-act="insert" data-t="${esc(o.name)}" data-ty="${o.type}" title="Insert"><i class="bi bi-plus-square"></i></button>
                <button class="st-btn danger" data-act="empty" data-t="${esc(o.name)}" data-ty="${o.type}" title="Empty"><i class="bi bi-eraser"></i></button>`}
                ${ro ? '' : `<button class="st-btn danger" data-act="drop" data-t="${esc(o.name)}" data-ty="${o.type}" title="Drop"><i class="bi bi-trash3"></i></button>`}
            </div>`;
            rows += `<tr>
                <td><b>${esc(o.name)}</b></td>
                <td>${acts}</td>
                <td>${isView ? '—' : o.rows}</td>
                <td>${isView ? 'view' : 'table'}</td>
                <td class="st-note">${esc(o.size || '')}</td>
            </tr>`;
        });
        box.innerHTML = meterBanner(S.db.usage)
            + tools
            + (S.objects.length
                ? `<div class="st-tablewrap"><table class="st-table">
                    <thead><tr><th>Table</th><th>Actions</th><th>Rows</th><th>Type</th><th>Size</th></tr></thead>
                    <tbody>${rows}</tbody></table></div>`
                : '<div class="st-empty"><i class="bi bi-table"></i>No tables yet. Use <b>New table</b> to create one.</div>');
        if (!ro) {
            $('stNewTableBtn')?.addEventListener('click', () => openTableBuilder(null));
            $('stNewViewBtn')?.addEventListener('click', newView);
            $('stImportBtn')?.addEventListener('click', () => switchTab('import'));
        }
        box.querySelectorAll('[data-act]').forEach((b) => b.addEventListener('click', () => structAction(b.dataset.act, b.dataset.t, b.dataset.ty)));
    }
    async function structAction(act, name, type) {
        if (act === 'empty') {
            const r = await Swal.fire({ title: `Empty "${name}"?`, text: 'All rows will be deleted; the table structure stays.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Empty' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/api/table-empty', { db: S.db.id, table: name });
            toast(out.success, out.message); loadObjects().then(renderStructDb); return;
        }
        if (act === 'drop') {
            const r = await Swal.fire({ title: `Drop ${type === 'VIEW' ? 'view' : 'table'} "${name}"?`, text: 'This permanently removes it and all its data.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Drop' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/api/table-drop', { db: S.db.id, table: name, type: type || 'TABLE' });
            toast(out.success, out.message);
            if (S.table === name) S.table = '';
            loadObjects().then(renderStructDb); return;
        }
        // browse / structure / search / insert → select the table then switch tab
        await selectTable(name, type);
        if (act === 'insert') addRow();
        else switchTab(act);
    }
    async function loadStructure() {
        if (!S.db || !S.table) return;
        const d = await getJson('/database-manager/api/structure?db=' + S.db.id + '&table=' + encodeURIComponent(S.table));
        if (!d.success) return;
        const cols = (d.details && d.details.columns) || [];
        const thead = document.querySelector('#stStructure thead');
        const tbody = document.querySelector('#stStructure tbody');
        thead.innerHTML = '<tr><th>Column</th><th>Type</th><th>Null</th><th>Default</th><th>Key</th></tr>';
        tbody.innerHTML = cols.map((c) => `<tr>
            <td><b>${esc(c.name)}</b></td>
            <td>${esc(c.type)}${c.size ? '(' + c.size + (c.scale ? ',' + c.scale : '') + ')' : ''}</td>
            <td>${c.nullable ? 'YES' : 'NO'}</td>
            <td>${c.default === null || c.default === undefined ? '<span class="st-null">—</span>' : esc(c.default)}</td>
            <td>${c.primary_key ? '<i class="bi bi-key-fill" style="color:#f59e0b"></i> PK' : ''}${c.auto_increment ? ' AI' : ''}</td>
        </tr>`).join('');
        const idx = (d.details && d.details.indexes) || [];
        const fks = (d.details && (d.details.foreign_keys || d.details.foreignKeys)) || [];
        $('stStructureExtra').innerHTML =
            (idx.length ? '<b>Indexes:</b> ' + idx.map((i) => esc(i.name || JSON.stringify(i))).join(', ') + '<br>' : '')
            + (fks.length ? '<b>Foreign keys:</b> ' + fks.map((f) => esc(JSON.stringify(f))).join('<br>') : '');
        const isView = S.objType === 'VIEW';
        $('stEditStruct').style.display = (canWrite() && !isView) ? '' : 'none';
        $('stStructMeta').textContent = S.table + (isView ? ' · view' : '');
    }
    $('stEditStruct').addEventListener('click', async () => {
        if (!S.table) return;
        if (S.objType === 'VIEW') { toast(false, 'Views are edited from New view (they have no columns to alter).'); return; }
        const d = await getJson('/database-manager/api/structure?db=' + S.db.id + '&table=' + encodeURIComponent(S.table));
        if (!d.success) { toast(false, 'Could not read the table structure.'); return; }
        openTableBuilder({ name: S.table, columns: (d.details && d.details.columns) || [] });
    });

    /* ─────────────── SQL console (lifted) ─────────────── */
    $('stSqlRun').addEventListener('click', async () => {
        if (!S.db) { toast(false, 'Select a database first.'); return; }
        const out = await post('/database-manager/api/sql', {
            db: S.db.id, sql: $('stSqlText').value,
            transaction: $('stSqlTx').checked, dryRun: $('stSqlDry').checked,
        });
        const box = $('stSqlReport');
        if (!out.success) { box.innerHTML = `<div class="err">${esc(out.message)}</div>`; return; }
        const rep = out.result.statements || out.result.report || out.result;
        let html = '';
        (Array.isArray(rep) ? rep : []).forEach((r) => {
            html += `<div class="${r.status === 'ok' ? 'ok' : 'err'}"><i class="bi bi-${r.status === 'ok' ? 'check-circle' : 'x-circle'}"></i>
                #${r.n ?? ''} ${esc(r.status)} ${r.kind ? '· ' + esc(r.kind) : ''} ${r.affected ? '· ' + r.affected + ' affected' : ''} ${r.ms ? '· ' + r.ms + ' ms' : ''} ${r.message ? '— ' + esc(r.message) : ''}</div>`;
            if (r.columns && r.rows) {
                html += '<pre>' + esc(r.columns.join(' | ')) + '\n' + r.rows.map((row) => row.map((v) => v === null ? 'NULL' : v).join(' | ')).join('\n')
                    + (r.truncated ? '\n… (truncated)' : '') + '</pre>';
            }
        });
        if (out.result.transaction && out.result.transaction !== 'none') html += `<div class="st-note">transaction: ${esc(out.result.transaction)}${out.result.dryRun ? ' (dry run — nothing saved)' : ''}</div>`;
        box.innerHTML = html || '<div class="st-note">No statements.</div>';
        loadObjects();
    });

    /* ─────────────── search pane ─────────────── */
    async function renderSearchPane() {
        const box = $('stSearchBody');
        if (!S.table) {
            box.innerHTML = '<div class="st-empty"><i class="bi bi-search"></i>Pick a table in the tree to search its columns.</div>';
            return;
        }
        if (!S.columns.length) await loadRowsMeta();
        const inputs = S.columns.map((c) =>
            `<div><label>${esc(c.name)} <span class="ty">${esc(c.type)}</span></label>
                <input type="text" class="st-input" data-search="${esc(c.name)}" value="${esc(S.filters[c.name] ?? '')}" placeholder="contains…"></div>`).join('');
        box.innerHTML = `<div class="card-box">
            <h6><i class="bi bi-search"></i> Search ${esc(S.table)}</h6>
            <div class="search-grid">${inputs}</div>
            <div class="st-flex">
                <button class="st-btn primary" id="stSearchGo"><i class="bi bi-search"></i> Search</button>
                <button class="st-btn" id="stSearchClear">Clear</button>
            </div>
        </div>`;
        $('stSearchGo').addEventListener('click', () => {
            const f = {};
            box.querySelectorAll('[data-search]').forEach((i) => { const v = i.value.trim(); if (v !== '') f[i.dataset.search] = v; });
            S.filters = f; S.page = 1;
            switchTab('browse');
        });
        $('stSearchClear').addEventListener('click', () => { S.filters = {}; renderSearchPane(); });
    }

    /* ─────────────── export pane ─────────────── */
    function renderExportPane() {
        const sel = $('stExpTarget');
        if (!sel.dataset.built) {
            const opts = ['<option value="">SQLite (native)</option>'].concat(
                TARGETS.filter((t) => targetId(t) !== 'sqlite').map((t) => `<option value="${esc(targetId(t))}">${esc(t.label)} (converted)</option>`));
            sel.innerHTML = opts.join('');
            sel.dataset.built = '1';
        }
    }
    $('stExpGo').addEventListener('click', () => {
        if (!S.db) { toast(false, 'Select a database first.'); return; }
        const target = $('stExpTarget').value;
        const mode = $('stExpMode').value;
        location.href = BASE + '/database-manager/export?db=' + S.db.id + '&mode=' + encodeURIComponent(mode) + '&targetType=' + encodeURIComponent(target);
    });

    /* ─────────────── import pane ─────────────── */
    function renderImportPane() {
        const sel = $('stImpTable');
        const opts = ['<option value="">— auto (SQL script) —</option>'].concat(
            S.objects.filter((o) => o.type !== 'VIEW').map((o) => `<option value="${esc(o.name)}"${o.name === S.table ? ' selected' : ''}>${esc(o.name)}</option>`));
        sel.innerHTML = opts.join('');
        $('stImpMsg').textContent = '';
    }
    $('stImpGo').addEventListener('click', async () => {
        if (!S.db) { toast(false, 'Select a database first.'); return; }
        const file = $('stImpFile').files[0];
        if (!file) { toast(false, 'Choose a .sql or .csv file first.'); return; }
        const fd = new FormData();
        fd.append('db', String(S.db.id));
        fd.append('file', file);
        const t = $('stImpTable').value;
        if (t) fd.append('table', t);
        fd.append('_token', CSRF);
        $('stImpMsg').textContent = 'Importing…';
        const out = await (await fetch(BASE + '/database-manager/api/import', { method: 'POST', credentials: 'same-origin', body: fd })).json();
        $('stImpMsg').textContent = out.message || (out.success ? 'Done.' : 'Import failed.');
        toast(out.success, out.message || (out.success ? 'Imported.' : 'Import failed.'));
        if (out.success) { loadObjects(); }
    });

    /* ─────────────── operations pane ─────────────── */
    function renderOperationsPane() {
        if (S.mode === 'table') { $('stOpsDb').style.display = 'none'; $('stOpsTable').style.display = ''; renderOpsTable(); }
        else { $('stOpsDb').style.display = ''; $('stOpsTable').style.display = 'none'; renderOpsDb(); }
    }
    function renderOpsDb() {
        const box = $('stOpsDb');
        const manage = S.db && ['owner', 'manage'].includes(S.db.access);
        const ro = S.db && S.db.access === 'read';
        const share = SHARE_USERS && manage;
        box.innerHTML = `<div class="card-box">
            <h6><i class="bi bi-gear"></i> Database operations</h6>
            <div class="st-flex" style="gap:.45rem">
                ${ro ? '' : '<button class="st-btn" data-op="rename"><i class="bi bi-pencil"></i> Rename</button>'}
                <button class="st-btn" data-op="clone"><i class="bi bi-files"></i> Clone</button>
                ${ro ? '' : '<button class="st-btn" data-op="vacuum"><i class="bi bi-magic"></i> Vacuum</button>'}
                <button class="st-btn" data-op="integrity"><i class="bi bi-shield-check"></i> Integrity check</button>
                ${ro ? '' : '<button class="st-btn" data-op="maintain"><i class="bi bi-heart-pulse"></i> Maintain</button>'}
                <button class="st-btn" data-op="snapshots"><i class="bi bi-clock-history"></i> Snapshots</button>
                <a class="st-btn" href="${BASE}/database-manager/backup?db=${S.db.id}"><i class="bi bi-download"></i> Backup</a>
                ${ro ? '' : '<button class="st-btn" data-op="restore"><i class="bi bi-upload"></i> Restore</button>'}
                ${share ? '<button class="st-btn" data-op="share"><i class="bi bi-people"></i> Share…</button>' : ''}
                ${IS_OWNER ? '<button class="st-btn" data-op="size"><i class="bi bi-arrows-collapse"></i> Size limit…</button>' : ''}
                ${ro ? '' : '<button class="st-btn danger" data-op="drop"><i class="bi bi-trash3"></i> Drop database</button>'}
            </div>
            ${ro ? '<div class="st-ro-banner mt-3"><i class="bi bi-lock-fill"></i> You have read-only access to this database.</div>' : ''}
        </div>`;
        box.querySelectorAll('[data-op]').forEach((b) => b.addEventListener('click', () => dbOperation(b.dataset.op)));
    }
    function renderOpsTable() {
        const box = $('stOpsTable');
        const ro = !canWrite();
        box.innerHTML = `<div class="card-box">
            <h6><i class="bi bi-gear"></i> Table operations · ${esc(S.table)}</h6>
            <div class="st-flex" style="gap:.45rem">
                ${ro ? '' : '<button class="st-btn" data-top="rename"><i class="bi bi-pencil"></i> Rename table</button>'}
                ${ro ? '' : '<button class="st-btn danger" data-top="empty"><i class="bi bi-eraser"></i> Empty table</button>'}
                ${ro ? '' : '<button class="st-btn danger" data-top="drop"><i class="bi bi-trash3"></i> Drop table</button>'}
            </div>
            ${ro ? '<div class="st-ro-banner mt-3"><i class="bi bi-lock-fill"></i> This object is read-only here.</div>' : ''}
        </div>`;
        box.querySelectorAll('[data-top]').forEach((b) => b.addEventListener('click', () => tableOperation(b.dataset.top)));
    }
    async function tableOperation(op) {
        if (op === 'empty') {
            const r = await Swal.fire({ title: `Empty "${S.table}"?`, text: 'All rows deleted; structure kept.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Empty' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/api/table-empty', { db: S.db.id, table: S.table });
            toast(out.success, out.message); loadObjects(); if (S.tab === 'browse') loadRows(); return;
        }
        if (op === 'drop') {
            const r = await Swal.fire({ title: `Drop table "${S.table}"?`, text: 'This permanently removes it and all its data.', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Drop' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/api/table-drop', { db: S.db.id, table: S.table, type: S.objType || 'TABLE' });
            toast(out.success, out.message);
            if (out.success) { S.table = ''; await selectDb(S.db); }
            return;
        }
        if (op === 'rename') {
            const r = await Swal.fire({ title: 'Rename table', input: 'text', inputValue: S.table, showCancelButton: true, confirmButtonText: 'Rename' });
            if (!r.isConfirmed) return;
            const nn = (r.value || '').trim();
            if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(nn)) { toast(false, 'Invalid table name.'); return; }
            const d = await getJson('/database-manager/api/structure?db=' + S.db.id + '&table=' + encodeURIComponent(S.table));
            if (!d.success) { toast(false, 'Could not read the table structure.'); return; }
            const fields = ((d.details && d.details.columns) || []).map((c) => {
                const f = { name: c.name, type: (c.type || 'TEXT').toUpperCase(), nullable: !!c.nullable,
                    primary_key: !!c.primary_key, auto_increment: !!c.auto_increment, original_name: c.name };
                if (c.size) f.size = c.size;
                if (c.scale) f.scale = c.scale;
                if (c.default !== null && c.default !== undefined) f.default = c.default;
                return f;
            });
            const out = await post('/database-manager/api/table-update', { db: S.db.id, old_table: S.table, new_table: nn, fields });
            toast(out.success, out.message);
            if (out.success) { S.table = nn; await loadObjects(); renderCrumb(); renderTree(); }
        }
    }
    async function dbOperation(op) {
        const db = S.db;
        if (op === 'rename') {
            const r = await Swal.fire({ title: 'Rename database', input: 'text', inputValue: db.label || '', showCancelButton: true });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/rename', { id: db.id, label: r.value });
            toast(out.success, out.message); await refresh(); return;
        }
        if (op === 'vacuum') { const out = await post('/database-manager/vacuum', { id: db.id }); toast(out.success, out.message); refresh(); return; }
        if (op === 'integrity') { const out = await post('/database-manager/integrity', { id: db.id }); Swal.fire(out.success ? 'Healthy' : 'Problems found', out.message, out.success ? 'success' : 'warning'); return; }
        if (op === 'maintain') { const out = await post('/database-manager/maintain', { id: db.id }); toast(out.success, out.message); refresh(); return; }
        if (op === 'snapshots') { await openSnapshots(db); return; }
        if (op === 'clone') {
            const r = await Swal.fire({ title: 'Clone database', input: 'text', inputPlaceholder: 'New database name (optional)',
                inputValue: (db.label || db.name) + ' (copy)', showCancelButton: true, confirmButtonText: 'Clone',
                text: 'Creates a brand-new virtual database with a full copy of this one’s structure and data.' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/duplicate', { id: db.id, label: (r.value || '').trim() });
            toast(out.success, out.message); if (out.success) refresh(); return;
        }
        if (op === 'drop') {
            const r = await Swal.fire({ title: 'Delete this database?', text: `"${db.label || db.name}" and ALL its data will be permanently removed. Download a backup first if you need it.`,
                icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Delete forever' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/drop', { id: db.id });
            toast(out.success, out.message);
            if (out.success) { S.db = null; S.mode = 'none'; S.table = ''; renderCrumb(); renderTabs(); document.querySelectorAll('.pane').forEach((p) => p.classList.remove('active')); $('stWelcome').style.display = ''; }
            await refresh(); return;
        }
        if (op === 'restore') {
            const r = await Swal.fire({ title: 'Restore from backup', html: '<input type="file" id="stRf" class="form-control" accept=".sqlite,.db,.sqlite3">',
                text: 'The current contents will be replaced.', showCancelButton: true, confirmButtonText: 'Restore',
                preConfirm: () => document.getElementById('stRf').files[0] || Swal.showValidationMessage('Choose a .sqlite backup file') });
            if (!r.isConfirmed || !r.value) return;
            const fd = new FormData();
            fd.append('id', String(db.id)); fd.append('file', r.value); fd.append('_token', CSRF);
            const out = await (await fetch(BASE + '/database-manager/restore', { method: 'POST', credentials: 'same-origin', body: fd })).json();
            toast(out.success, out.message); if (out.success) { await refresh(); loadObjects(); } return;
        }
        if (op === 'share') { await openShares(db); return; }
        if (op === 'size') {
            const r = await Swal.fire({ title: 'Database size limit', input: 'text',
                inputPlaceholder: 'MB — blank = inherit plan, -1 = unlimited', showCancelButton: true, confirmButtonText: 'Save',
                text: 'Overrides the plan cap for THIS database only.' });
            if (!r.isConfirmed) return;
            const out = await post('/database-manager/size', { id: db.id, mb: (r.value || '').trim() });
            toast(out.success, out.message); await refresh(); return;
        }
    }

    /* ─────────────── snapshots (lifted) ─────────────── */
    async function openSnapshots(db) {
        const load = async () => (await getJson('/database-manager/api/snapshots?db=' + db.id)).snapshots || [];
        const rowHtml = (s) => `
            <div style="display:flex;align-items:center;justify-content:space-between;gap:.6rem;padding:.45rem .1rem;border-bottom:1px solid var(--pp-border,#262b36)">
                <div style="text-align:left;min-width:0">
                    <div style="font-weight:650">${esc(s.when)}</div>
                    <div class="st-note">${esc(s.size)}${s.encrypted ? ' · <i class="bi bi-lock-fill"></i> encrypted at rest' : ''}</div>
                </div>
                <button class="st-btn" data-snap="${esc(s.name)}" style="flex:0 0 auto"><i class="bi bi-arrow-counterclockwise"></i> Restore</button>
            </div>`;
        const render = (snaps) => snaps.length
            ? `<div style="max-height:46vh;overflow:auto">${snaps.map(rowHtml).join('')}</div>`
            : '<div class="st-note" style="padding:.7rem 0">No snapshots yet — take one below. The newest 7 are kept.</div>';
        await Swal.fire({
            title: 'Snapshots · ' + esc(db.label || db.name),
            html: render(await load()),
            width: 560, showConfirmButton: false, showCloseButton: true,
            footer: '<div style="display:flex;gap:.5rem;flex-wrap:wrap;justify-content:center">'
                + '<button id="stSnapNow" class="swal2-confirm swal2-styled" style="margin:0">Snapshot now</button>'
                + '<button id="stSnapEnc" class="swal2-deny swal2-styled" style="margin:0"><i class="bi bi-lock-fill"></i> Encrypted snapshot</button>'
                + '</div>',
            didOpen: () => {
                const box = Swal.getHtmlContainer();
                const bind = () => box.querySelectorAll('[data-snap]').forEach((b) => b.addEventListener('click', async () => {
                    const c = await Swal.fire({ title: 'Roll back to this snapshot?', text: 'The live database will be replaced with this snapshot (integrity-checked first).', icon: 'warning', showCancelButton: true, confirmButtonColor: '#ef4444', confirmButtonText: 'Restore' });
                    if (!c.isConfirmed) return;
                    const out = await post('/database-manager/snapshot-restore', { id: db.id, snapshot: b.dataset.snap });
                    toast(out.success, out.message); if (out.success) { refresh(); loadObjects(); }
                }));
                const rebind = async () => { box.innerHTML = render(await load()); bind(); };
                bind();
                document.getElementById('stSnapNow')?.addEventListener('click', async () => { const o = await post('/database-manager/snapshot', { id: db.id, keep: 7, encrypt: false }); toast(o.success, o.message); rebind(); });
                document.getElementById('stSnapEnc')?.addEventListener('click', async () => { const o = await post('/database-manager/snapshot', { id: db.id, keep: 7, encrypt: true }); toast(o.success, o.message); rebind(); });
            }
        });
    }

    /* ─────────────── sharing ─────────────── */
    async function openShares(db) {
        const load = async () => await getJson('/database-manager/api/shares?db=' + db.id);
        const render = (data) => {
            const shares = data.shares || [];
            const cands = data.candidates || [];
            const shareRows = shares.length ? shares.map((s) => `
                <div style="display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem 0;border-bottom:1px solid var(--pp-border,#262b36)">
                    <span>${esc(s.username)}</span>
                    <span style="display:flex;gap:.5rem;align-items:center">
                        <label class="st-note"><input type="checkbox" data-tog="${s.user_id}" ${s.can_write ? 'checked' : ''}> can write</label>
                        <button class="st-btn danger" data-rm="${s.user_id}">Remove</button>
                    </span>
                </div>`).join('') : '<div class="st-note" style="padding:.4rem 0">Not shared with anyone yet.</div>';
            const opts = cands.map((c) => `<option value="${c.id}">${esc(c.username)}</option>`).join('');
            const adder = cands.length ? `
                <div style="display:flex;gap:.4rem;margin-top:.7rem;align-items:center">
                    <select id="stShareUser" class="st-input" style="flex:1">${opts}</select>
                    <label class="st-note"><input type="checkbox" id="stShareWrite"> write</label>
                    <button class="st-btn primary" id="stShareAdd">Add</button>
                </div>` : '<div class="st-note" style="margin-top:.6rem">No teammates available to share with.</div>';
            return `<div style="text-align:left">${shareRows}${adder}</div>`;
        };
        await Swal.fire({
            title: 'Share · ' + esc(db.label || db.name),
            html: render(await load()), width: 560, showConfirmButton: false, showCloseButton: true,
            didOpen: () => {
                const box = Swal.getHtmlContainer();
                const rebind = async () => { box.innerHTML = render(await load()); bind(); };
                const bind = () => {
                    box.querySelectorAll('[data-tog]').forEach((c) => c.addEventListener('change', async () => {
                        const out = await post('/database-manager/share', { db: db.id, user_id: parseInt(c.dataset.tog, 10), can_write: c.checked });
                        toast(out.success, out.message);
                    }));
                    box.querySelectorAll('[data-rm]').forEach((b) => b.addEventListener('click', async () => {
                        const out = await post('/database-manager/unshare', { db: db.id, user_id: parseInt(b.dataset.rm, 10) });
                        toast(out.success, out.message); rebind();
                    }));
                    const add = document.getElementById('stShareAdd');
                    add?.addEventListener('click', async () => {
                        const uid = parseInt(document.getElementById('stShareUser').value, 10);
                        const w = document.getElementById('stShareWrite').checked;
                        const out = await post('/database-manager/share', { db: db.id, user_id: uid, can_write: w });
                        toast(out.success, out.message); rebind();
                    });
                };
                bind();
            }
        });
    }

    /* ─────────────── owner size caps ─────────────── */
    async function openSizeCaps() {
        const data = await getJson('/database-manager/api/size-policy');
        if (!data.success) { toast(false, data.message || 'Owner only.'); return; }
        const plans = data.plans || [];
        const rows = plans.map((p) => `
            <div style="display:flex;align-items:center;justify-content:space-between;gap:.6rem;padding:.4rem 0;border-bottom:1px solid var(--pp-border,#262b36)">
                <span><i class="bi bi-${esc(p.icon || 'gem')}" style="color:${esc(p.color || '#7c3aed')}"></i> ${esc(p.name)}</span>
                <span style="display:flex;gap:.4rem;align-items:center">
                    <input type="number" min="0" class="st-input" style="width:120px" data-cap="${esc(p.slug)}" value="${p.mb === null || p.mb === 0 ? '' : p.mb}" placeholder="MB (blank=∞)">
                </span>
            </div>`).join('');
        const planOpts = plans.map((p) => `<option value="${esc(p.slug)}"${p.slug === data.current_plan ? ' selected' : ''}>${esc(p.name)}</option>`).join('');
        const r = await Swal.fire({
            title: 'Per-plan size caps', width: 560,
            html: `<div style="text-align:left">${rows}
                <div style="margin-top:.8rem"><label class="st-note d-block mb-1">Active plan for this install:</label>
                <select id="stCurPlan" class="st-input">${planOpts}</select></div>
                <div class="st-note" style="margin-top:.5rem">Blank or 0 MB = unlimited.</div></div>`,
            showCancelButton: true, confirmButtonText: 'Save',
            preConfirm: () => {
                const caps = {};
                Swal.getHtmlContainer().querySelectorAll('[data-cap]').forEach((i) => { caps[i.dataset.cap] = i.value.trim(); });
                return { caps, current_plan: document.getElementById('stCurPlan').value };
            }
        });
        if (!r.isConfirmed) return;
        const out = await post('/database-manager/size-policy', r.value);
        toast(out.success, out.message); refresh();
    }
    $('stSizeCaps')?.addEventListener('click', openSizeCaps);

    /* ─────────────── visual table builder (lifted) ─────────────── */
    const FIELD_TYPES = ['INTEGER','BIGINT','REAL','NUMERIC','DECIMAL','TEXT','VARCHAR','CHAR','BOOLEAN','DATE','TIME','DATETIME','JSON','BLOB'];
    const SIZED = ['VARCHAR','CHAR','NUMERIC','DECIMAL'];
    let TB = { edit: null };

    function colRow(col) {
        col = col || {};
        const tr = document.createElement('tr');
        tr.dataset.orig = col.name || '';
        const type = (col.type || 'INTEGER').toUpperCase();
        const opts = FIELD_TYPES.map((t) => `<option value="${t}"${t === type ? ' selected' : ''}>${t}</option>`).join('');
        const len = col.size ? (col.scale ? col.size + ',' + col.scale : String(col.size)) : '';
        const dflt = (col.default === null || col.default === undefined) ? '' : String(col.default);
        tr.innerHTML =
            `<td class="mid"><i class="bi bi-grip-vertical drag" draggable="true" title="Drag to reorder"></i></td>
             <td><input type="text" class="st-name-in c-name" value="${esc(col.name || '')}" placeholder="column_name" autocomplete="off"></td>
             <td><select class="c-type">${opts}</select></td>
             <td><input type="text" class="c-len" value="${esc(len)}" placeholder="—"></td>
             <td class="mid"><input type="checkbox" class="c-null"${col.nullable ? ' checked' : ''}></td>
             <td class="mid"><input type="checkbox" class="c-pk"${col.primary_key ? ' checked' : ''}></td>
             <td class="mid"><input type="checkbox" class="c-ai"${col.auto_increment ? ' checked' : ''}></td>
             <td><input type="text" class="c-def" value="${esc(dflt)}" placeholder="—"></td>
             <td class="mid"><button class="st-btn danger c-del" title="Remove column"><i class="bi bi-x-lg"></i></button></td>`;
        const typeSel = tr.querySelector('.c-type'), lenIn = tr.querySelector('.c-len'),
              pk = tr.querySelector('.c-pk'), ai = tr.querySelector('.c-ai'), nul = tr.querySelector('.c-null');
        const syncLen = () => { lenIn.disabled = !SIZED.includes(typeSel.value.toUpperCase()) || ai.checked; if (lenIn.disabled) lenIn.value = ''; };
        typeSel.addEventListener('change', syncLen);
        ai.addEventListener('change', () => { if (ai.checked) { typeSel.value = 'INTEGER'; pk.checked = true; nul.checked = false; } syncLen(); });
        pk.addEventListener('change', () => { if (pk.checked) nul.checked = false; });
        tr.querySelector('.c-del').addEventListener('click', () => tr.remove());
        const grip = tr.querySelector('.drag');
        grip.addEventListener('dragstart', () => tr.classList.add('dragging'));
        grip.addEventListener('dragend', () => tr.classList.remove('dragging'));
        syncLen();
        return tr;
    }
    function wireDrag(tbody) {
        tbody.addEventListener('dragover', (e) => {
            e.preventDefault();
            const dragging = tbody.querySelector('tr.dragging'); if (!dragging) return;
            const after = [...tbody.querySelectorAll('tr:not(.dragging)')].find((r) => e.clientY <= r.getBoundingClientRect().top + r.offsetHeight / 2);
            if (after) tbody.insertBefore(dragging, after); else tbody.appendChild(dragging);
        });
    }
    function openTableBuilder(existing) {
        TB.edit = existing ? existing.name : null;
        $('stTblModalTitle').innerHTML = existing ? `<i class="bi bi-pencil-square me-1"></i> Edit table · ${esc(existing.name)}` : '<i class="bi bi-table me-1"></i> New table';
        $('stTblName').value = existing ? existing.name : '';
        $('stTblErr').textContent = '';
        const tbody = $('stColRows');
        tbody.innerHTML = '';
        const cols = (existing && existing.columns && existing.columns.length) ? existing.columns
            : [{ name: 'id', type: 'INTEGER', primary_key: true, auto_increment: true, nullable: false }];
        cols.forEach((c) => tbody.appendChild(colRow(c)));
        $('stTblModal').classList.add('show');
        setTimeout(() => $('stTblName').focus(), 50);
    }
    function collectFields() {
        const rows = [...$('stColRows').querySelectorAll('tr')];
        return rows.map((tr) => {
            const g = (s) => tr.querySelector(s);
            const name = g('.c-name').value.trim();
            if (!name) return null;
            const type = g('.c-type').value.toUpperCase();
            let size = 0, scale = 0;
            const lv = g('.c-len').value.trim();
            if (lv) { const p = lv.split(','); size = parseInt(p[0], 10) || 0; scale = p[1] ? (parseInt(p[1], 10) || 0) : 0; }
            const defRaw = g('.c-def').value;
            const f = { name, type, nullable: g('.c-null').checked, primary_key: g('.c-pk').checked, auto_increment: g('.c-ai').checked };
            if (size) f.size = size;
            if (scale) f.scale = scale;
            if (defRaw !== '') f.default = defRaw;
            if (tr.dataset.orig) f.original_name = tr.dataset.orig;
            return f;
        }).filter(Boolean);
    }
    $('stAddColBtn').addEventListener('click', () => $('stColRows').appendChild(colRow({ type: 'VARCHAR', size: 255, nullable: true })));
    $('stTblModalX').addEventListener('click', () => $('stTblModal').classList.remove('show'));
    $('stTblCancel').addEventListener('click', () => $('stTblModal').classList.remove('show'));
    $('stTblModal').addEventListener('click', (e) => { if (e.target === $('stTblModal')) $('stTblModal').classList.remove('show'); });
    wireDrag($('stColRows'));
    $('stTblSave').addEventListener('click', async () => {
        const table = $('stTblName').value.trim();
        const fields = collectFields();
        const err = $('stTblErr');
        if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) { err.textContent = 'Enter a valid table name (letters, numbers, underscore).'; return; }
        if (!fields.length) { err.textContent = 'Add at least one column.'; return; }
        err.textContent = '';
        const btn = $('stTblSave'); btn.disabled = true;
        let out;
        if (TB.edit) out = await post('/database-manager/api/table-update', { db: S.db.id, old_table: TB.edit, new_table: table, fields });
        else out = await post('/database-manager/api/table-create', { db: S.db.id, table, fields });
        btn.disabled = false;
        if (!out.success) { err.textContent = out.message || 'Could not save.'; return; }
        $('stTblModal').classList.remove('show');
        toast(true, out.message);
        S.table = table; S.objType = 'TABLE';
        await loadObjects();
        renderCrumb();
        if (S.tab === 'structure') renderStructurePane();
    });

    /* ─────────────── view builder (lifted) ─────────────── */
    async function newView() {
        const r = await Swal.fire({
            title: 'New view', width: 640,
            html: `<input id="stVwName" class="swal2-input" placeholder="view name" style="margin-bottom:.4rem">
                   <textarea id="stVwSel" class="swal2-textarea" placeholder="SELECT … FROM …" style="height:150px;font-family:ui-monospace,monospace;font-size:.82rem"></textarea>
                   <div style="text-align:left"><button id="stVwPrev" type="button" class="swal2-confirm swal2-styled" style="font-size:.8rem;padding:.3rem .7rem;background:#64748b">Preview</button></div>
                   <pre id="stVwOut" style="text-align:left;max-height:22vh;overflow:auto;font-size:.75rem;margin-top:.4rem"></pre>`,
            showCancelButton: true, confirmButtonText: 'Create view',
            didOpen: () => {
                document.getElementById('stVwPrev').addEventListener('click', async () => {
                    const sel = document.getElementById('stVwSel').value.trim();
                    const out = await post('/database-manager/api/view-preview', { db: S.db.id, select: sel });
                    const box = document.getElementById('stVwOut');
                    if (!out.success) { box.textContent = out.message || 'Preview failed.'; return; }
                    const p = out.preview || {};
                    box.textContent = (p.columns || []).join(' | ') + '\n' + (p.rows || []).map((row) => (Array.isArray(row) ? row : Object.values(row)).map((v) => v === null ? 'NULL' : v).join(' | ')).join('\n');
                });
            },
            preConfirm: () => ({ name: document.getElementById('stVwName').value.trim(), select: document.getElementById('stVwSel').value.trim() })
        });
        if (!r.isConfirmed) return;
        const out = await post('/database-manager/api/view-save', { db: S.db.id, name: r.value.name, select: r.value.select, replace: true });
        toast(out.success, out.message);
        if (out.success) { S.table = r.value.name; S.objType = 'VIEW'; await loadObjects(); if (S.tab === 'structure') renderStructurePane(); }
    }

    /* ─────────────── new database ─────────────── */
    $('stNewDb').addEventListener('click', async () => {
        if (!IS_OWNER && SEAT_MAX >= 0 && DBS.length >= SEAT_MAX) {
            toast(false, `You have used ${DBS.length} of ${SEAT_MAX} databases. Ask the owner to raise your quota.`);
            return;
        }
        // cPanel-style: the database name is prefixed with the owning account's handle.
        const ownerSel = IS_OWNER
            ? `<div style="text-align:left;margin-bottom:.5rem"><label style="font-size:.72rem;font-weight:700;color:#8a94a6">Belongs to</label>
                 <select id="npOwner" class="swal2-select" style="margin:.2rem 0;width:100%">
                   <option value="">— me (${esc(MY_PREFIX)}) —</option>
                   ${(ASSIGN_USERS || []).map((u) => `<option value="${u.id}" data-u="${esc(u.username)}">${esc(u.username)}</option>`).join('')}
                 </select></div>`
            : '';
        const res = await Swal.fire({
            title: 'New database', width: 480,
            html: `${ownerSel}
                <div style="text-align:left"><label style="font-size:.72rem;font-weight:700;color:#8a94a6">Database name</label>
                <div style="display:flex;align-items:stretch;margin-top:.2rem">
                  <span id="npPrefix" style="display:inline-flex;align-items:center;padding:0 .6rem;background:var(--pp-surface-muted,#1a1e26);border:1px solid var(--pp-border,#2b3040);border-right:0;border-radius:8px 0 0 8px;font-family:ui-monospace,monospace;font-size:.85rem;color:#8a94a6">${esc(MY_PREFIX)}_</span>
                  <input id="npName" class="swal2-input" placeholder="cms" style="margin:0;border-radius:0 8px 8px 0;flex:1" autocomplete="off">
                </div>
                <div id="npFull" style="font-family:ui-monospace,monospace;font-size:.78rem;color:var(--st-accent,#7c6cf0);margin-top:.4rem"></div>
                <div style="font-size:.72rem;color:#8a94a6;margin-top:.2rem">Like cPanel — every database is filed under its owner's name so it's easy to track and stays private to them.</div></div>`,
            showCancelButton: true, confirmButtonText: 'Create',
            didOpen: () => {
                const nameEl = document.getElementById('npName');
                const ownerEl = document.getElementById('npOwner');
                const prefEl = document.getElementById('npPrefix');
                const fullEl = document.getElementById('npFull');
                const curPrefix = () => {
                    if (!IS_OWNER || !ownerEl || ownerEl.value === '') return MY_PREFIX;
                    const opt = ownerEl.selectedOptions[0];
                    return prefixOf(opt ? opt.getAttribute('data-u') : '');
                };
                const sync = () => { const p = curPrefix(); prefEl.textContent = p + '_'; const part = slugPart(nameEl.value) || 'db'; fullEl.textContent = 'Will be created as:  ' + p + '_' + part; };
                nameEl.addEventListener('input', sync); if (ownerEl) ownerEl.addEventListener('change', sync);
                sync(); nameEl.focus();
            },
            preConfirm: () => ({
                label: (document.getElementById('npName').value || '').trim(),
                assigned_to: IS_OWNER ? parseInt((document.getElementById('npOwner') || {}).value || '0', 10) : 0
            })
        });
        if (!res.isConfirmed || !res.value.label) return;
        const out = await post('/database-manager/create', { label: res.value.label, assigned_to: res.value.assigned_to || undefined });
        toast(out.success, out.message || 'Done');
        if (out.success) refresh();
    });
    function updateSeatButton() {
        const btn = $('stNewDb');
        if (!IS_OWNER && SEAT_MAX >= 0 && DBS.length >= SEAT_MAX) {
            btn.disabled = true;
            btn.innerHTML = `<i class="bi bi-lock-fill"></i> ${DBS.length} of ${SEAT_MAX} used`;
        } else {
            btn.disabled = false;
            btn.innerHTML = '<i class="bi bi-plus-lg"></i> New database';
        }
    }

    /* ─────────────── refresh ─────────────── */
    async function refresh() {
        const d = await getJson('/database-manager/api/databases');
        if (d.success) {
            DBS = d.databases || [];
            if (S.db) { const again = DBS.find((x) => x.id === S.db.id); S.db = again || null; if (!again) { S.mode = 'none'; S.table = ''; } }
            renderTree(); updateSeatButton(); renderCrumb();
        }
    }

    /* boot: rail renders instantly from SEED, then refresh from the API */
    renderTree();
    updateSeatButton();
    refresh();
})();
</script>
<?php
/* ── Data Studio Guide (owner-ux modal, opened by the navbar “Guide” button) ── */
$__brand = htmlspecialchars(\PpPhp\Core\Brand::name());
$ownerGuide = [
    'id'       => 'studioGuide',
    'eyebrow'  => 'Data Studio',
    'title'    => 'Your visual database workbench',
    'subtitle' => 'Create, design, query and manage databases — no server, no SQL required.',
    'icon'     => 'window-sidebar',
    'topics'   => [
        ['id' => 'start', 'label' => 'Getting started', 'icon' => 'signpost-2', 'keywords' => 'overview rail tabs workspace begin intro',
         'body' => '<p><b>Data Studio</b> is a full-screen workbench for your <b>Virtual Databases</b> — the zero-setup databases that live inside ' . $__brand . ', with no external server.</p>'
                 . '<ul><li>The <b>left rail</b> lists every database you can access. Click one to open it; it expands to show its tables and views, each with a live <b>storage meter</b>.</li>'
                 . '<li>The <b>main area</b> changes with what you pick. With a <b>database</b> selected you get <em>Structure, SQL, Search, Export, Import, Operations</em>; pick a <b>table</b> and you get <em>Browse, Structure, SQL, Search, Insert, Export, Operations</em>.</li>'
                 . '<li>The footer shows how many database <b>seats</b> you have used, and your plan.</li></ul>'],
        ['id' => 'create', 'label' => 'Creating databases & naming', 'icon' => 'plus-square', 'keywords' => 'new database create prefix cpanel name quota seats username',
         'body' => '<p><b>New database</b> creates one instantly — no host, no password. Type a short name (e.g. <code>cms</code>) and it is filed under your account, cPanel-style: <code>yourname_cms</code>.</p>'
                 . '<ul><li>The <b>prefix is your account handle</b>, so every database is easy to trace to its owner and names never clash between users.</li>'
                 . '<li>The <b>owner</b> can create a database <em>for</em> a specific user or client — then it is prefixed with <b>that user\'s</b> name and assigned to them.</li>'
                 . '<li>You can create up to your <b>seat quota</b> (shown in the rail footer). Ask the owner to raise it if you need more.</li>'
                 . '<li>Existing databases keep their original names — the prefix applies to new ones, just like cPanel.</li></ul>'],
        ['id' => 'tables', 'label' => 'Tables & views', 'icon' => 'table', 'keywords' => 'table view create edit structure column rows browse insert builder',
         'body' => '<p>Everything is visual — no SQL needed.</p>'
                 . '<ul><li><b>New table</b> opens a builder: add columns with their type, length, primary key, auto-increment and default.</li>'
                 . '<li><b>Structure → Edit structure</b> reopens the builder to rename, retype, add, drop or reorder columns — <b>your data is preserved</b> across the change.</li>'
                 . '<li><b>Browse</b> shows rows with search, sort and paging; edit or add rows inline. <b>Insert</b> opens a quick add-row form.</li>'
                 . '<li><b>New view</b> saves a query as a view (with a live preview). <b>Empty</b> truncates a table; <b>Drop</b> removes it.</li></ul>'],
        ['id' => 'sql', 'label' => 'SQL, Search, Import & Export', 'icon' => 'terminal', 'keywords' => 'sql console query search filter import csv export convert mysql postgres',
         'body' => '<ul><li><b>SQL</b> runs any query with an optional <b>transaction</b> or <b>dry-run</b> (a dry run rolls everything back), and reports each statement.</li>'
                 . '<li><b>Search</b> filters a table by any column.</li>'
                 . '<li><b>Import</b> accepts a <b>.sql</b> script (run as-is) or a <b>.csv</b> file (appended to a table) — bounded by your size limit.</li>'
                 . '<li><b>Export</b> downloads a SQL script — native SQLite, or <b>converted</b> to MySQL, PostgreSQL, SQL Server, Oracle and more, ready to run on live hosting.</li></ul>'],
        ['id' => 'share', 'label' => 'Sharing with your team', 'icon' => 'people', 'keywords' => 'share teammate group read write permission collaborate private',
         'body' => '<p>Your databases are <b>private to you</b> — no one else can see or reach them. To collaborate, open a database\'s <b>Operations → Share</b>.</p>'
                 . '<ul><li>Pick a teammate and grant <b>read-only</b> or <b>read/write</b> access.</li>'
                 . '<li>Read-only members can browse, export and run <em>SELECT</em> queries, but every change is blocked.</li>'
                 . '<li>Only a database\'s owner (or the platform owner) can manage its sharing.</li></ul>'],
        ['id' => 'storage', 'label' => 'Storage & plan limits', 'icon' => 'hdd', 'keywords' => 'size limit plan standard pro premium quota full storage cap',
         'body' => '<p>Each database has a <b>size limit</b> set by your plan (for example Standard 5&nbsp;MB, Pro 15&nbsp;MB, Premium 25&nbsp;MB). The meter in the rail and on the Structure tab shows how full it is.</p>'
                 . '<ul><li>The limit is <b>hard-enforced</b>: once a database is full, new writes are paused, but you can still read and delete to free space.</li>'
                 . '<li>Raising the plan (or the owner lifting the limit) restores headroom instantly.</li></ul>'],
        ['id' => 'ops', 'label' => 'Backups, snapshots & health', 'icon' => 'shield-check', 'keywords' => 'snapshot backup restore clone maintain vacuum integrity encrypt safety',
         'body' => '<p>Under a database\'s <b>Operations</b> tab:</p>'
                 . '<ul><li><b>Snapshots</b> — rotating point-in-time copies you can roll back to; take one before a risky change, optionally <b>encrypted at rest</b>.</li>'
                 . '<li><b>Backup / Restore</b> — download a <code>.sqlite</code> file or restore from one.</li>'
                 . '<li><b>Maintain / Vacuum / Integrity</b> — keep the database fast and healthy. <b>Clone</b> forks it into a new one.</li></ul>'],
        ['id' => 'owner', 'label' => 'For owners & admins', 'icon' => 'gear', 'keywords' => 'owner admin size caps quota assign plan manage',
         'body' => '<p>As the platform owner you also control:</p>'
                 . '<ul><li><b>Size caps</b> (top bar) — the per-plan maximum database size, and per-database overrides.</li>'
                 . '<li><b>Seats / quotas</b> — how many databases each user may create (in the Virtual Database console).</li>'
                 . '<li><b>Assigning</b> databases to users or client apps when you create them, so each one is filed under the right account.</li></ul>'],
    ],
];
include constant('BASE_PATH') . '/core/views/owner-ux.php';
?>
</body>
</html>
