ken_nogi/NodeSrv/apps/app-portal/src/dashboard.js
Kenichiro NOGI ce58cb4be4 初回コミット: dev配下(NodeSrv/Pleasanter等)をGitea管理下に統合
GitHub(nextgroup2706/ken_nogi)は今後使わず自社Gitea運用に切替え。
NodeSrvは旧リポジトリの履歴を破棄しファイルのみ統合(Dokploy用サービスアカウントは
別途mygit-admin/NodeSrv.gitに履歴あり)。notepmエクスポート(12GB)とPleasanter
インストーラzip(208MB)はサイズが大きいため.gitignoreで除外。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 15:37:06 +09:00

85 lines
2.9 KiB
JavaScript

const { listComposes, getComposeEnv, getContainers } = require('./dokployClient');
const { extractPortalMeta } = require('./portalMeta');
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
async function buildCards(environmentId) {
const composes = await listComposes(environmentId);
const settled = await Promise.allSettled(
composes.map(async (c) => {
const { env, appName } = await getComposeEnv(c.composeId);
const meta = extractPortalMeta(env, c.name);
if (!meta) return null;
let containerState = 'unknown';
try {
const containers = await getContainers(appName);
containerState = containers[0]?.state || 'stopped';
} catch {
containerState = 'unknown';
}
return { composeId: c.composeId, appName, composeStatus: c.composeStatus, containerState, meta };
})
);
const cards = [];
settled.forEach((result, i) => {
if (result.status === 'fulfilled') {
if (result.value) cards.push(result.value);
} else {
cards.push({ error: true, composeId: composes[i].composeId, name: composes[i].name });
}
});
return cards;
}
function renderCard(card) {
if (card.error) {
return `<div class="card card-error" data-compose-id="${escapeHtml(card.composeId)}">
<h3>${escapeHtml(card.name || card.composeId)}</h3>
<p>状態取得失敗</p>
</div>`;
}
const { composeId, composeStatus, containerState, meta } = card;
const openLink =
meta.appType === 'web' && meta.url
? `<a href="${escapeHtml(meta.url)}" target="_blank" rel="noopener">開く</a>`
: '';
const actionButtons =
meta.appType === 'web'
? `<button data-action="start" data-compose-id="${escapeHtml(composeId)}">Start</button>
<button data-action="stop" data-compose-id="${escapeHtml(composeId)}">Stop</button>
<button data-action="restart" data-compose-id="${escapeHtml(composeId)}">Restart</button>`
: `<button data-action="trigger" data-compose-id="${escapeHtml(composeId)}">今すぐ実行</button>`;
return `<div class="card" data-compose-id="${escapeHtml(composeId)}">
<h3>${escapeHtml(meta.label)}</h3>
<p>compose: ${escapeHtml(composeStatus)} / container: ${escapeHtml(containerState)}</p>
${openLink}
${actionButtons}
<button data-action="logs" data-compose-id="${escapeHtml(composeId)}">ログ表示</button>
</div>`;
}
async function renderDashboard(environmentId) {
const cards = await buildCards(environmentId);
const cardsHtml = cards.map(renderCard).join('\n');
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>App Portal</title>
</head>
<body>
<h1>App Portal</h1>
<div id="cards">${cardsHtml}</div>
<script src="/portal.js"></script>
</body>
</html>`;
}
module.exports = { buildCards, renderCard, renderDashboard };