Newer
Older
TelosDB / src-frontend / index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>TelosDB - Database Browser</title>
    <style>
        :root {
            --bg-color: #0f172a;
            --sidebar-bg: #1e293b;
            --card-bg: #1e293b;
            --text-primary: #f8fafc;
            --text-secondary: #94a3b8;
            --accent-color: #38bdf8;
            --accent-hover: #0ea5e9;
            --border-color: #334155;
            --success: #22c55e;
            --warning: #f59e0b;
            --error: #ef4444;
            --font-family: 'Inter', system-ui, -apple-system, sans-serif;
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: var(--font-family);
            background-color: var(--bg-color);
            color: var(--text-primary);
            height: 100vh;
            display: flex;
            overflow: hidden;
        }

        /* Sidebar */
        .sidebar {
            width: 260px;
            background-color: var(--sidebar-bg);
            border-right: 1px solid var(--border-color);
            display: flex;
            flex-direction: column;
            padding: 20px 0;
            flex-shrink: 0;
        }

        .sidebar-header {
            padding: 0 20px 20px 20px;
            border-bottom: 1px solid var(--border-color);
            margin-bottom: 20px;
        }

        .sidebar-header h1 {
            font-size: 1.25rem;
            font-weight: 700;
            color: var(--accent-color);
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .sidebar-section {
            margin-bottom: 24px;
            padding: 0 10px;
        }

        .sidebar-section-title {
            padding: 0 10px 8px 10px;
            font-size: 0.75rem;
            text-transform: uppercase;
            letter-spacing: 0.05em;
            color: var(--text-secondary);
            font-weight: 600;
        }

        .status-list {
            list-style: none;
        }

        .status-item {
            padding: 8px 10px;
            font-size: 0.875rem;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .status-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            display: inline-block;
            margin-right: 8px;
        }

        .table-list {
            list-style: none;
        }

        .table-item {
            padding: 10px;
            margin: 2px 0;
            border-radius: 6px;
            font-size: 0.875rem;
            cursor: pointer;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .table-item:hover {
            background-color: rgba(255, 255, 255, 0.05);
        }

        .table-item.active {
            background-color: var(--accent-color);
            color: white;
        }

        /* Main Content */
        .main-content {
            flex-grow: 1;
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }

        .main-header {
            padding: 16px 24px;
            background-color: rgba(15, 23, 42, 0.8);
            backdrop-filter: blur(8px);
            border-bottom: 1px solid var(--border-color);
            display: flex;
            justify-content: space-between;
            align-items: center;
            z-index: 10;
        }

        .main-header h2 {
            font-size: 1.125rem;
            font-weight: 600;
        }

        .tabs {
            display: flex;
            gap: 20px;
            padding: 0 24px;
            background-color: rgba(15, 23, 42, 0.5);
            border-bottom: 1px solid var(--border-color);
        }

        .tab {
            padding: 12px 4px;
            font-size: 0.875rem;
            font-weight: 500;
            color: var(--text-secondary);
            cursor: pointer;
            border-bottom: 2px solid transparent;
            transition: all 0.2s;
        }

        .tab:hover {
            color: var(--text-primary);
        }

        .tab.active {
            color: var(--accent-color);
            border-bottom-color: var(--accent-color);
        }

        .view-container {
            flex-grow: 1;
            overflow: auto;
            padding: 24px;
            position: relative;
        }

        .view {
            display: none;
        }

        .view.active {
            display: block;
        }

        /* Tables and Info */
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: 0.875rem;
            margin-bottom: 20px;
        }

        th {
            text-align: left;
            padding: 12px 16px;
            background-color: rgba(255, 255, 255, 0.02);
            border-bottom: 1px solid var(--border-color);
            color: var(--text-secondary);
            font-weight: 600;
        }

        td {
            padding: 12px 16px;
            border-bottom: 1px solid var(--border-color);
        }

        tr:hover td {
            background-color: rgba(255, 255, 255, 0.01);
        }

        .pagination {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px 0;
            gap: 10px;
        }

        .btn {
            padding: 8px 16px;
            background-color: var(--card-bg);
            border: 1px solid var(--border-color);
            border-radius: 6px;
            color: var(--text-primary);
            font-size: 0.875rem;
            cursor: pointer;
            transition: all 0.2s;
        }

        .btn:hover:not(:disabled) {
            border-color: var(--accent-color);
            background-color: rgba(56, 189, 248, 0.1);
        }

        .btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        .json-block {
            background: #0f172a;
            padding: 16px;
            border-radius: 8px;
            border: 1px solid var(--border-color);
            position: relative;
            margin-top: 10px;
        }

        .json-block pre {
            margin: 0;
            overflow-x: auto;
            font-family: 'Fira Code', monospace;
            font-size: 0.8125rem;
            line-height: 1.5;
            color: #e2e8f0;
        }

        .copy-btn {
            position: absolute;
            top: 10px;
            right: 10px;
            padding: 4px 8px;
            background: rgba(56, 189, 248, 0.2);
            color: var(--accent-color);
            border: 1px solid var(--accent-color);
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.75rem;
        }

        .copy-btn:hover {
            background: var(--accent-color);
            color: white;
        }

        .tag {
            padding: 2px 6px;
            border-radius: 4px;
            font-size: 0.75rem;
            font-weight: 500;
        }

        .tag-blue {
            background: rgba(56, 189, 248, 0.2);
            color: var(--accent-color);
        }

        .tag-green {
            background: rgba(34, 197, 94, 0.2);
            color: var(--success);
        }

        .tag-orange {
            background: rgba(245, 158, 11, 0.2);
            color: var(--warning);
        }

        /* Loader */
        .loader {
            width: 20px;
            height: 20px;
            border: 2px solid var(--border-color);
            border-top-color: var(--accent-color);
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }

        @keyframes spin {
            to {
                transform: rotate(360deg);
            }
        }

        .empty-state {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100%;
            color: var(--text-secondary);
            gap: 16px;
        }

        .empty-state i {
            font-size: 3rem;
            opacity: 0.3;
        }
    </style>
</head>

<body>
    <div class="sidebar">
        <div class="sidebar-header">
            <h1>TelosDB</h1>
        </div>

        <div class="sidebar-section">
            <div class="sidebar-section-title">System Status</div>
            <ul class="status-list">
                <li class="status-item">
                    <span>MCP Server</span>
                    <span id="mcp-status" class="tag tag-orange">Loading</span>
                </li>
                <li class="status-item">
                    <span>Sidecar (LLM)</span>
                    <span id="sidecar-status" class="tag tag-orange">Checking</span>
                </li>
                <li class="status-item">
                    <span>Clients</span>
                    <span id="conn-count" style="font-weight: 600;">0</span>
                </li>
            </ul>
        </div>

        <div class="sidebar-section">
            <div class="sidebar-section-title">Tables</div>
            <ul id="table-list" class="table-list">
                <!-- Tables will be loaded here -->
            </ul>
        </div>

        <div class="sidebar-section" style="margin-top: auto;">
            <button class="btn" style="width: 100%;" onclick="loadMcpConfig()">MCP Config</button>
        </div>
    </div>

    <div class="main-content">
        <div class="main-header">
            <h2 id="current-table-name">Select a table</h2>
            <div id="loading-indicator" style="display: none;">
                <div class="loader"></div>
            </div>
        </div>

        <div id="main-tabs" class="tabs" style="display: none;">
            <div class="tab active" onclick="switchTab('data')">Data</div>
            <div class="tab" onclick="switchTab('schema')">Schema</div>
        </div>

        <div class="view-container">
            <div id="empty-view" class="empty-state">
                <p>サイドバーからテーブルを選択して、データを確認してください。</p>
            </div>

            <div id="data-view" class="view">
                <div style="overflow-x: auto;">
                    <table id="data-table">
                        <thead>
                            <tr id="data-head"></tr>
                        </thead>
                        <tbody id="data-body"></tbody>
                    </table>
                </div>
                <div class="pagination">
                    <span id="pagination-info">Row 0-0 of 0</span>
                    <div style="display: flex; gap: 8px;">
                        <button id="prev-btn" class="btn" onclick="changePage(-1)">Previous</button>
                        <button id="next-btn" class="btn" onclick="changePage(1)">Next</button>
                    </div>
                </div>
            </div>

            <div id="schema-view" class="view">
                <table>
                    <thead>
                        <tr>
                            <th>CID</th>
                            <th>Name</th>
                            <th>Type</th>
                            <th>Not Null</th>
                            <th>Default</th>
                            <th>PK</th>
                        </tr>
                    </thead>
                    <tbody id="schema-body"></tbody>
                </table>
            </div>

            <div id="mcp-config-view" class="view">
                <h3>MCP Configuration</h3>
                <div class="json-block">
                    <button class="copy-btn" onclick="copyToClipboard()">Copy JSON</button>
                    <pre id="mcp-json"></pre>
                </div>
                <p style="font-size: 0.75rem; color: var(--text-secondary); margin-top: 12px;">
                    このJSONをMCPクライアント(Claude Desktopなど)の設定に使用してください。
                </p>
            </div>
        </div>
    </div>

    <script type="module">
        const { invoke } = window.__TAURI__.core;
        const { listen } = window.__TAURI__.event;

        let selectedTable = null;
        let currentPage = 0;
        const pageSize = 20;

        // --- Core Functions ---

        async function init() {
            updateStatus();
            loadTableList();
            setupListeners();

            // Initial poll
            setInterval(updateStatus, 5000);
        }

        async function updateStatus() {
            try {
                const sidecarStatus = await invoke('get_sidecar_status');
                const sidecarEl = document.getElementById('sidecar-status');
                if (sidecarStatus) {
                    sidecarEl.textContent = 'Running';
                    sidecarEl.className = 'tag tag-green';
                } else {
                    sidecarEl.textContent = 'Stopped';
                    sidecarEl.className = 'tag tag-orange';
                }

                document.getElementById('mcp-status').textContent = 'Running';
                document.getElementById('mcp-status').className = 'tag tag-green';
            } catch (e) {
                console.error(e);
            }
        }

        async function loadTableList() {
            try {
                const tables = await invoke('get_table_list');
                const list = document.getElementById('table-list');
                list.innerHTML = '';
                tables.forEach(table => {
                    const li = document.createElement('li');
                    li.className = 'table-item';
                    li.textContent = table;
                    li.onclick = () => selectTable(table);
                    list.appendChild(li);
                });
            } catch (e) {
                console.error('Failed to load table list', e);
            }
        }

        async function selectTable(name) {
            selectedTable = name;
            currentPage = 0;
            document.querySelectorAll('.table-item').forEach(el => {
                el.classList.toggle('active', el.textContent === name);
            });
            document.getElementById('current-table-name').textContent = name;
            document.getElementById('main-tabs').style.display = 'flex';
            document.getElementById('empty-view').style.display = 'none';

            switchTab('data');
            loadTableData();
            loadTableSchema();
        }

        async function loadTableData() {
            if (!selectedTable) return;
            showLoading(true);
            try {
                const result = await invoke('get_table_data', {
                    tableName: selectedTable,
                    limit: pageSize,
                    offset: currentPage * pageSize
                });

                renderData(result.data, result.total);
            } catch (e) {
                console.error(e);
            } finally {
                showLoading(false);
            }
        }

        function renderData(data, total) {
            const head = document.getElementById('data-head');
            const body = document.getElementById('data-body');
            head.innerHTML = '';
            body.innerHTML = '';

            if (data.length === 0) {
                body.innerHTML = '<tr><td colspan="100" style="text-align:center;">No data</td></tr>';
                return;
            }

            // Headers
            const keys = Object.keys(data[0]);
            keys.forEach(key => {
                const th = document.createElement('th');
                th.textContent = key;
                head.appendChild(th);
            });

            // Rows
            data.forEach(row => {
                const tr = document.createElement('tr');
                keys.forEach(key => {
                    const td = document.createElement('td');
                    let val = row[key];
                    if (val === null) val = '<span style="color:var(--text-secondary);font-style:italic;">NULL</span>';
                    else if (typeof val === 'string' && val.length > 100) val = val.substring(0, 100) + '...';
                    td.innerHTML = val;
                    tr.appendChild(td);
                });
                body.appendChild(tr);
            });

            // Pagination info
            const start = total === 0 ? 0 : currentPage * pageSize + 1;
            const end = Math.min((currentPage + 1) * pageSize, total);
            document.getElementById('pagination-info').textContent = `Row ${start}-${end} of ${total}`;
            document.getElementById('prev-btn').disabled = currentPage === 0;
            document.getElementById('next-btn').disabled = end >= total;
        }

        async function loadTableSchema() {
            try {
                const schema = await invoke('get_table_schema', { tableName: selectedTable });
                const body = document.getElementById('schema-body');
                body.innerHTML = '';
                schema.forEach(col => {
                    const tr = document.createElement('tr');
                    tr.innerHTML = `
                        <td>${col.cid}</td>
                        <td style="font-weight:600;">${col.name}</td>
                        <td><span class="tag tag-blue">${col.type}</span></td>
                        <td>${col.notnull ? '✅' : '-'}</td>
                        <td>${col.dflt_value || '-'}</td>
                        <td>${col.pk ? '🔑' : '-'}</td>
                    `;
                    body.appendChild(tr);
                });
            } catch (e) {
                console.error(e);
            }
        }

        window.switchTab = function (tabName) {
            document.querySelectorAll('.tab').forEach(el => {
                el.classList.toggle('active', el.textContent.toLowerCase() === tabName);
            });
            document.querySelectorAll('.view').forEach(el => {
                el.classList.remove('active');
            });
            document.getElementById(`${tabName}-view`).classList.add('active');
        }

        window.changePage = function (delta) {
            currentPage += delta;
            loadTableData();
        }

        window.loadMcpConfig = async function () {
            document.getElementById('current-table-name').textContent = 'MCP Configuration';
            document.getElementById('main-tabs').style.display = 'none';
            document.getElementById('empty-view').style.display = 'none';
            document.querySelectorAll('.view').forEach(el => el.classList.remove('active'));
            document.getElementById('mcp-config-view').classList.add('active');

            try {
                const mcpData = await invoke('get_mcp_info');
                document.getElementById('mcp-json').textContent = JSON.stringify({ mcpServers: mcpData.mcpServers }, null, 2);
            } catch (e) {
                console.error(e);
            }
        }

        function showLoading(show) {
            document.getElementById('loading-indicator').style.display = show ? 'block' : 'none';
        }

        function setupListeners() {
            listen('mcp-connection-update', (event) => {
                document.getElementById('conn-count').textContent = event.payload;
            });

            listen('mcp-db-update', () => {
                if (selectedTable === 'items') loadTableData();
            });
        }

        window.copyToClipboard = function () {
            const text = document.getElementById('mcp-json').textContent;
            navigator.clipboard.writeText(text).then(() => {
                alert('Copied to clipboard!');
            });
        }

        init();
    </script>
</body>

</html>