328 lines
12 KiB
HTML
328 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ja" data-app-id="posimai-dev">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<script>
|
|
(function () {
|
|
var t = localStorage.getItem('posimai-dev-theme') || 'dark';
|
|
document.documentElement.setAttribute('data-theme', t === 'light' ? 'light' : 'dark');
|
|
document.documentElement.setAttribute('data-theme-pref', t);
|
|
})();
|
|
</script>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<meta name="description" content="posimai-dev session history">
|
|
<meta name="color-scheme" content="dark light">
|
|
<meta name="theme-color" content="#0C1221">
|
|
<title>セッション履歴 — posimai-dev</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://posimai-ui.vercel.app/v1/base.css">
|
|
<script src="https://unpkg.com/lucide@0.344.0/dist/umd/lucide.min.js"></script>
|
|
<style>
|
|
:root {
|
|
--accent: #A78BFA;
|
|
--accent-dim: rgba(167, 139, 250, 0.15);
|
|
--dev-bg: #0C1221;
|
|
}
|
|
[data-theme="light"] { --accent: #7C3AED; }
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100dvh;
|
|
background:
|
|
radial-gradient(ellipse at 15% 60%, rgba(34,211,238,0.07) 0%, transparent 55%),
|
|
radial-gradient(ellipse at 85% 25%, rgba(167,139,250,0.07) 0%, transparent 55%),
|
|
var(--dev-bg);
|
|
font-family: Inter, sans-serif;
|
|
color: #F3F4F6;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 16px;
|
|
height: 48px;
|
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
flex-shrink: 0;
|
|
background: rgba(12, 18, 33, 0.8);
|
|
backdrop-filter: blur(12px);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
.header-left { display: flex; align-items: center; gap: 10px; }
|
|
.header-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); }
|
|
.header-title { font-size: 14px; font-weight: 600; color: #F3F4F6; letter-spacing: -0.01em; }
|
|
|
|
.back-btn {
|
|
display: flex; align-items: center; gap: 6px;
|
|
padding: 5px 12px; border-radius: 8px; border: none; cursor: pointer;
|
|
background: var(--accent-dim); color: var(--accent);
|
|
font-size: 12px; font-weight: 500; font-family: Inter, sans-serif;
|
|
text-decoration: none;
|
|
transition: background 0.15s;
|
|
}
|
|
.back-btn:hover { background: rgba(167,139,250,0.25); }
|
|
|
|
.content {
|
|
flex: 1;
|
|
padding: 24px 16px;
|
|
max-width: 800px;
|
|
width: 100%;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.page-title {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
margin-bottom: 4px;
|
|
letter-spacing: -0.02em;
|
|
}
|
|
.page-subtitle {
|
|
font-size: 13px;
|
|
color: rgba(255,255,255,0.35);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.session-list { display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
.session-card {
|
|
background: rgba(255,255,255,0.04);
|
|
border: 1px solid rgba(255,255,255,0.07);
|
|
border-radius: 12px;
|
|
padding: 14px 16px;
|
|
cursor: pointer;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.session-card:hover { background: rgba(167,139,250,0.08); border-color: rgba(167,139,250,0.2); }
|
|
.session-card.active { background: rgba(167,139,250,0.1); border-color: rgba(167,139,250,0.3); }
|
|
|
|
.session-icon {
|
|
width: 36px; height: 36px; border-radius: 8px;
|
|
background: var(--accent-dim);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.session-info { flex: 1; min-width: 0; }
|
|
.session-id {
|
|
font-size: 13px; font-weight: 500; font-family: monospace;
|
|
color: #F3F4F6; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
.session-meta {
|
|
font-size: 11px; color: rgba(255,255,255,0.35); margin-top: 2px;
|
|
}
|
|
|
|
.session-size {
|
|
font-size: 11px; color: rgba(255,255,255,0.3);
|
|
font-family: monospace; flex-shrink: 0;
|
|
}
|
|
|
|
/* ログビューアー */
|
|
.log-viewer {
|
|
display: none;
|
|
margin-top: 16px;
|
|
background: rgba(0,0,0,0.3);
|
|
border: 1px solid rgba(255,255,255,0.07);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.log-viewer.open { display: block; }
|
|
|
|
.log-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
background: rgba(255,255,255,0.03);
|
|
}
|
|
.log-header-title { font-size: 12px; color: rgba(255,255,255,0.5); font-family: monospace; }
|
|
|
|
.log-close-btn {
|
|
background: none; border: none; cursor: pointer;
|
|
color: rgba(255,255,255,0.4); padding: 2px;
|
|
display: flex; align-items: center;
|
|
transition: color 0.15s;
|
|
}
|
|
.log-close-btn:hover { color: #F3F4F6; }
|
|
|
|
.log-body {
|
|
padding: 14px;
|
|
font-family: "JetBrains Mono", "Fira Code", monospace;
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
color: #B0BAD3;
|
|
white-space: pre-wrap;
|
|
word-break: break-all;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: rgba(255,255,255,0.25);
|
|
}
|
|
.empty-state-icon { margin-bottom: 12px; }
|
|
.empty-state-text { font-size: 14px; }
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: rgba(255,255,255,0.3);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.icon-btn { color: rgba(255,255,255,0.5); }
|
|
.icon-btn:hover { color: #F3F4F6; background: rgba(255,255,255,0.06); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="header">
|
|
<div class="header-left">
|
|
<div class="header-dot" aria-hidden="true"></div>
|
|
<span class="header-title">セッション履歴</span>
|
|
</div>
|
|
<a href="/" class="back-btn">
|
|
<i data-lucide="arrow-left" style="width:13px;height:13px;stroke-width:1.75"></i>
|
|
ターミナルに戻る
|
|
</a>
|
|
</header>
|
|
|
|
<div class="content">
|
|
<div class="page-title">過去のセッション</div>
|
|
<div class="page-subtitle" id="sessionCount">読み込み中...</div>
|
|
|
|
<div class="session-list" id="sessionList">
|
|
<div class="loading">読み込み中...</div>
|
|
</div>
|
|
|
|
<div class="log-viewer" id="logViewer">
|
|
<div class="log-header">
|
|
<span class="log-header-title" id="logTitle"></span>
|
|
<button class="log-close-btn" id="logCloseBtn" aria-label="閉じる">
|
|
<i data-lucide="x" style="width:14px;height:14px;stroke-width:1.75"></i>
|
|
</button>
|
|
</div>
|
|
<div class="log-body" id="logBody"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://posimai-ui.vercel.app/v1/base.js" defer></script>
|
|
<script>
|
|
(function () {
|
|
const sessionList = document.getElementById('sessionList');
|
|
const sessionCount = document.getElementById('sessionCount');
|
|
const logViewer = document.getElementById('logViewer');
|
|
const logTitle = document.getElementById('logTitle');
|
|
const logBody = document.getElementById('logBody');
|
|
|
|
let activeCard = null;
|
|
|
|
function formatDate(iso) {
|
|
const d = new Date(iso);
|
|
return d.toLocaleString('ja-JP', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
function formatSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
return (bytes / 1024).toFixed(1) + ' KB';
|
|
}
|
|
|
|
// ANSIエスケープを除去して表示
|
|
function stripAnsi(str) {
|
|
return str.replace(/\x1b\[[0-9;]*[mGKHFJ]/g, '').replace(/\r/g, '');
|
|
}
|
|
|
|
async function loadLog(id) {
|
|
logBody.textContent = '読み込み中...';
|
|
logTitle.textContent = id + '.log';
|
|
logViewer.classList.add('open');
|
|
logViewer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
try {
|
|
const r = await fetch(`/api/sessions/${encodeURIComponent(id)}`);
|
|
const text = await r.text();
|
|
logBody.textContent = stripAnsi(text);
|
|
} catch (e) {
|
|
logBody.textContent = '読み込みに失敗しました。';
|
|
}
|
|
}
|
|
|
|
document.getElementById('logCloseBtn').addEventListener('click', () => {
|
|
logViewer.classList.remove('open');
|
|
if (activeCard) { activeCard.classList.remove('active'); activeCard = null; }
|
|
});
|
|
|
|
async function loadSessions() {
|
|
try {
|
|
const r = await fetch('/api/sessions');
|
|
const sessions = await r.json();
|
|
|
|
if (!sessions.length) {
|
|
sessionList.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">
|
|
<i data-lucide="inbox" style="width:32px;height:32px;stroke-width:1;color:rgba(255,255,255,0.15)"></i>
|
|
</div>
|
|
<div class="empty-state-text">セッションはまだありません</div>
|
|
</div>`;
|
|
sessionCount.textContent = 'セッションなし';
|
|
lucide.createIcons();
|
|
return;
|
|
}
|
|
|
|
sessionCount.textContent = `${sessions.length} セッション`;
|
|
sessionList.innerHTML = '';
|
|
|
|
sessions.forEach((s) => {
|
|
const card = document.createElement('div');
|
|
card.className = 'session-card';
|
|
card.innerHTML = `
|
|
<div class="session-icon">
|
|
<i data-lucide="terminal" style="width:16px;height:16px;stroke-width:1.75;color:var(--accent)"></i>
|
|
</div>
|
|
<div class="session-info">
|
|
<div class="session-id">${s.id}</div>
|
|
<div class="session-meta">${formatDate(s.mtime)}</div>
|
|
</div>
|
|
<div class="session-size">${formatSize(s.size)}</div>`;
|
|
|
|
card.addEventListener('click', () => {
|
|
if (activeCard === card) {
|
|
logViewer.classList.remove('open');
|
|
card.classList.remove('active');
|
|
activeCard = null;
|
|
return;
|
|
}
|
|
if (activeCard) activeCard.classList.remove('active');
|
|
activeCard = card;
|
|
card.classList.add('active');
|
|
loadLog(s.id);
|
|
});
|
|
|
|
sessionList.appendChild(card);
|
|
});
|
|
|
|
lucide.createIcons();
|
|
} catch (e) {
|
|
sessionList.innerHTML = `<div class="empty-state"><div class="empty-state-text">読み込みに失敗しました</div></div>`;
|
|
sessionCount.textContent = 'エラー';
|
|
}
|
|
}
|
|
|
|
loadSessions();
|
|
lucide.createIcons();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|