<!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>