tillerhand 3.43.0 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/board-chair.md +20 -20
- package/.claude/agents/code-reviewer.md +20 -20
- package/.claude/agents/council-advisor.md +14 -14
- package/.claude/agents/security-auditor.md +30 -30
- package/.claude/agents/test-writer.md +22 -22
- package/.claude/board/board-ai.md +25 -25
- package/.claude/board/board-api.md +25 -25
- package/.claude/board/board-architect.md +28 -28
- package/.claude/board/board-devops.md +25 -25
- package/.claude/board/board-mobile.md +25 -25
- package/.claude/board/board-performance.md +24 -24
- package/.claude/board/board-privacy.md +25 -25
- package/.claude/board/board-product.md +26 -26
- package/.claude/board/board-qa.md +28 -28
- package/.claude/board/board-ux.md +26 -26
- package/.claude/board/council.md +76 -76
- package/.claude/board/rules.md +61 -61
- package/.claude/claude-security-guidance.md +10 -10
- package/.claude/commands/adopt.md +70 -69
- package/.claude/commands/ask-mode.md +9 -9
- package/.claude/commands/board.md +15 -15
- package/.claude/commands/build.md +47 -47
- package/.claude/commands/dashboard.md +17 -17
- package/.claude/commands/design.md +31 -31
- package/.claude/commands/findings-all.md +14 -14
- package/.claude/commands/findings-business.md +7 -7
- package/.claude/commands/findings-legal.md +7 -7
- package/.claude/commands/findings-perf.md +7 -7
- package/.claude/commands/findings-security.md +9 -9
- package/.claude/commands/findings-tech.md +7 -7
- package/.claude/commands/findings-ux.md +7 -7
- package/.claude/commands/fix-bug.md +15 -15
- package/.claude/commands/freeze.md +8 -8
- package/.claude/commands/handoff.md +38 -38
- package/.claude/commands/kit-update.md +17 -14
- package/.claude/commands/language.md +13 -0
- package/.claude/commands/maintain.md +19 -19
- package/.claude/commands/plan-task.md +32 -32
- package/.claude/commands/qa.md +17 -17
- package/.claude/commands/release.md +40 -40
- package/.claude/commands/research.md +31 -31
- package/.claude/commands/retro.md +32 -32
- package/.claude/commands/review-changes.md +15 -15
- package/.claude/commands/rollback.md +24 -24
- package/.claude/commands/start.md +116 -113
- package/.claude/commands/tools.md +11 -11
- package/.claude/commands/user-feedback.md +11 -11
- package/.claude/commands/wrap-up.md +18 -18
- package/.claude/findings/lenses/business.md +24 -24
- package/.claude/findings/lenses/legal.md +29 -29
- package/.claude/findings/lenses/perf.md +19 -19
- package/.claude/findings/lenses/security.md +30 -30
- package/.claude/findings/lenses/tech.md +23 -23
- package/.claude/findings/lenses/ux.md +19 -19
- package/.claude/findings/protocol.md +54 -54
- package/.claude/findings/report-template.md +30 -30
- package/.claude/hooks/budget.mjs +7 -7
- package/.claude/hooks/checkpoint.mjs +12 -12
- package/.claude/hooks/commit-gate.mjs +38 -37
- package/.claude/hooks/cost.mjs +2 -2
- package/.claude/hooks/council.mjs +3 -3
- package/.claude/hooks/dashboard-data.mjs +3 -2
- package/.claude/hooks/dashboard.mjs +164 -149
- package/.claude/hooks/destructive-guard.mjs +74 -64
- package/.claude/hooks/edit-rounds.mjs +3 -3
- package/.claude/hooks/flow.mjs +10 -10
- package/.claude/hooks/freeze-guard.mjs +27 -23
- package/.claude/hooks/gate.mjs +25 -25
- package/.claude/hooks/guard-secrets.mjs +11 -11
- package/.claude/hooks/lang.mjs +114 -0
- package/.claude/hooks/messages.mjs +495 -0
- package/.claude/hooks/models.mjs +14 -11
- package/.claude/hooks/notify.mjs +3 -2
- package/.claude/hooks/package-gate.mjs +17 -15
- package/.claude/hooks/plugins-sync.mjs +9 -9
- package/.claude/hooks/prefs.mjs +19 -19
- package/.claude/hooks/preview.mjs +11 -11
- package/.claude/hooks/progress.mjs +14 -14
- package/.claude/hooks/qa-report.mjs +11 -10
- package/.claude/hooks/reminder.mjs +6 -3
- package/.claude/hooks/session-start.mjs +23 -17
- package/.claude/hooks/shared.mjs +1 -1
- package/.claude/hooks/status-read.mjs +5 -4
- package/.claude/hooks/statusline.mjs +14 -11
- package/.claude/hooks/structure-guard.mjs +9 -9
- package/.claude/hooks/test-guard.mjs +12 -12
- package/.claude/hooks/tools.mjs +28 -29
- package/.claude/hooks/usage.mjs +18 -15
- package/.claude/output-styles/anlatici.md +28 -28
- package/.claude/security-patterns.json +3 -3
- package/.claude/settings.json +2 -0
- package/.claude/skills/logo-design/SKILL.md +42 -42
- package/.claude/skills/motion-video/SKILL.md +52 -52
- package/.claude/skills/source-research/SKILL.md +35 -35
- package/.claude/skills/ui-motion/SKILL.md +38 -38
- package/.github/workflows/kit-tests.yml +7 -7
- package/.github/workflows/npm-publish.yml +16 -16
- package/AGENTS.md +56 -56
- package/CHANGELOG.md +7 -7
- package/CLAUDE.md +5 -5
- package/README.md +199 -172
- package/README.tr.md +292 -0
- package/docs/architecture/overview.md +31 -31
- package/docs/architecture/stack.md +2 -2
- package/docs/archive/README.md +4 -4
- package/docs/decisions/adr-template.md +14 -14
- package/docs/decisions/log.md +10 -10
- package/docs/findings/README.md +13 -13
- package/docs/findings/config.md +13 -13
- package/docs/legal/privacy-kvkk.md +1 -1
- package/docs/operations/accounts.md +25 -25
- package/docs/operations/deploy-setup.md +64 -64
- package/docs/operations/feature-flags.md +17 -17
- package/docs/operations/maintenance.md +48 -48
- package/docs/operations/release-checklist.md +61 -61
- package/docs/operations/spend-alarms.md +28 -28
- package/docs/product/feedback.md +9 -8
- package/docs/product/glossary.md +16 -16
- package/docs/product/launch.md +28 -28
- package/docs/product/prd.md +43 -42
- package/docs/product/roadmap.md +31 -30
- package/docs/product/specs/_template.md +13 -12
- package/docs/product/unit-economics.md +23 -22
- package/docs/quality/metrics.md +14 -14
- package/docs/quality/retros.md +11 -11
- package/docs/quality/test-plan.md +48 -48
- package/docs/security/checklist.md +57 -57
- package/docs/workflow/board.md +7 -7
- package/docs/workflow/cost-log.md +4 -4
- package/docs/workflow/decision-protocol.md +70 -70
- package/docs/workflow/learning.md +4 -4
- package/docs/workflow/mcp-catalog.md +31 -31
- package/docs/workflow/models.md +35 -35
- package/docs/workflow/progress.md +7 -7
- package/docs/workflow/skills.md +108 -108
- package/docs/workflow/usage-limits.md +62 -62
- package/kit/CHANGELOG.md +876 -841
- package/kit/VERSION +1 -1
- package/kit/guide.html +713 -477
- package/kit/guide.tr.html +1328 -0
- package/kit/kit.mjs +56 -54
- package/kit/project/README.md +29 -29
- package/kit/project/gitignore +12 -12
- package/kit/templates/addons/ai/.env.example.append +5 -5
- package/kit/templates/addons/ai/ai/evals/README.md +11 -11
- package/kit/templates/addons/ai/ai/evals/cases.jsonl +2 -2
- package/kit/templates/addons/ai/ai/prompts/README.md +8 -8
- package/kit/templates/addons/ai/docs/ai/guide.md +56 -56
- package/kit/templates/backend/.claude/hooks/format.mjs +1 -1
- package/kit/templates/backend/.claude/structure.json +1 -1
- package/kit/templates/backend/.env.example +6 -6
- package/kit/templates/backend/.github/dependabot.yml +1 -1
- package/kit/templates/backend/.github/workflows/ci.yml +13 -13
- package/kit/templates/backend/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/backend/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/backend/.github/workflows/live-monitoring.yml +14 -14
- package/kit/templates/backend/.pre-commit-config.yaml +3 -3
- package/kit/templates/backend/docs/architecture/stack.md +98 -98
- package/kit/templates/backend/docs/architecture/structure.md +34 -34
- package/kit/templates/mobile/.claude/structure.json +1 -1
- package/kit/templates/mobile/.env.example +6 -6
- package/kit/templates/mobile/.github/dependabot.yml +2 -2
- package/kit/templates/mobile/.github/workflows/ci.yml +18 -18
- package/kit/templates/mobile/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/mobile/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/mobile/.gitignore.append +2 -2
- package/kit/templates/mobile/docs/architecture/stack.md +113 -113
- package/kit/templates/mobile/docs/architecture/structure.md +33 -33
- package/kit/templates/mobile/docs/design/design-guide.md +37 -37
- package/kit/templates/mobile/tests/security/rls.mjs +22 -22
- package/kit/templates/web-saas/.claude/structure.json +1 -1
- package/kit/templates/web-saas/.env.example +11 -11
- package/kit/templates/web-saas/.github/dependabot.yml +3 -3
- package/kit/templates/web-saas/.github/workflows/ci.yml +20 -20
- package/kit/templates/web-saas/.github/workflows/claude-ci-fix.yml +19 -19
- package/kit/templates/web-saas/.github/workflows/claude-maintenance.yml +18 -18
- package/kit/templates/web-saas/.github/workflows/live-monitoring.yml +12 -12
- package/kit/templates/web-saas/docs/architecture/stack.md +116 -116
- package/kit/templates/web-saas/docs/architecture/structure.md +44 -44
- package/kit/templates/web-saas/docs/design/design-guide.md +38 -38
- package/kit/templates/web-saas/tests/live/smoke.spec.ts +4 -4
- package/kit/templates/web-saas/tests/security/rls.mjs +22 -22
- package/kit/tools/codex/.codex/hooks.json +1 -1
- package/package.json +2 -2
- package/README.en.md +0 -267
|
@@ -9,7 +9,8 @@ import { execFileSync } from 'node:child_process';
|
|
|
9
9
|
import { readStatus, readText, sections, findSection, tally, tableRows } from './status-read.mjs';
|
|
10
10
|
import { readLimits, summary, level, THRESHOLDS } from './usage.mjs';
|
|
11
11
|
import { packageStatus, commandMode } from './tools.mjs';
|
|
12
|
-
import { readModels, modelsFile, modelsNote,
|
|
12
|
+
import { readModels, modelsFile, modelsNote, roleLabel } from './models.mjs';
|
|
13
|
+
import { readLang, t } from './lang.mjs';
|
|
13
14
|
import { acceptanceStatus, accounts, flags, unitEconomics, feedback, learning, qaSummary, metrics, githubStatus, readContext, findingReports, FINDING_KINDS, producedAssets, costSummary, councilMinutes, spendAlarms, envWarnings, repeatedBugClasses } from './dashboard-data.mjs';
|
|
14
15
|
|
|
15
16
|
const args = process.argv.slice(2);
|
|
@@ -18,6 +19,15 @@ const quiet = args.includes('--quiet');
|
|
|
18
19
|
if (quiet) process.on('uncaughtException', () => process.exit(0));
|
|
19
20
|
const root = path.resolve(args.find((a) => !a.startsWith('--')) ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
|
|
20
21
|
const file = (f) => path.join(root, f);
|
|
22
|
+
// Every visible text comes from messages.mjs in the project language (L = t with this project's root)
|
|
23
|
+
const L = (key, vars) => t(root, key, vars);
|
|
24
|
+
const dateKind = (k) => (k === 'bitiş' ? L('dashboard.dateExpiry') : L('dashboard.dateKeyRotation'));
|
|
25
|
+
const kindName = (k) => (Object.hasOwn(FINDING_KINDS, k) ? L(`dashboard.findingKind.${k}`) : k);
|
|
26
|
+
// The dashboard is made in the user's language: none before /start or /adopt asks it
|
|
27
|
+
const lang = readLang(root);
|
|
28
|
+
if (!lang) { if (!quiet) console.log('No language chosen yet: the dashboard is made after /start or /adopt asks it.'); process.exit(0); }
|
|
29
|
+
// Dates follow the conversation language (English when the runtime does not know the code)
|
|
30
|
+
const loc = (() => { try { return Intl.DateTimeFormat.supportedLocalesOf(lang)[0] ?? 'en'; } catch { return 'en'; } })();
|
|
21
31
|
|
|
22
32
|
// ---------- data ----------
|
|
23
33
|
const d = readStatus(root);
|
|
@@ -38,7 +48,7 @@ function gitInfo() {
|
|
|
38
48
|
info.commits = g('log', '-300', '--since=60.days', '--date=iso-strict', '--pretty=format:%h%x1f%ad%x1f%s')
|
|
39
49
|
.split('\n').filter(Boolean).map((s) => {
|
|
40
50
|
const [hash, date, message] = s.split('\x1f');
|
|
41
|
-
return { hash, date, message, kind: (message.match(/^(\w+)(\(.+?\))?!?:/)?.[1] ?? '
|
|
51
|
+
return { hash, date, message, kind: (message.match(/^(\w+)(\(.+?\))?!?:/)?.[1] ?? L('dashboard.commitKindOther')).toLowerCase() };
|
|
42
52
|
});
|
|
43
53
|
const latest = g('log', '-1', '--format=%H', '--', 'docs/workflow/progress.md');
|
|
44
54
|
if (latest) info.progressStale = Number(g('rev-list', '--count', `${latest}..HEAD`));
|
|
@@ -103,48 +113,48 @@ const findings = findingReports(readText(file('docs/findings/README.md')));
|
|
|
103
113
|
// Things that need attention
|
|
104
114
|
const alerts = [];
|
|
105
115
|
const addAlert = (sev, text, action = '') => alerts.push({ sev, text, action });
|
|
106
|
-
if (d.kitPending) addAlert('info', '
|
|
107
|
-
if (d.handoffPending) addAlert('warn', '
|
|
116
|
+
if (d.kitPending) addAlert('info', L('dashboard.alert.kitPending'), L('dashboard.act.startIdea'));
|
|
117
|
+
if (d.handoffPending) addAlert('warn', L('dashboard.alert.handoffPending'), '/handoff devral');
|
|
108
118
|
const lastReject = [...d.board.meetings].reverse().find((t) => /RET/i.test(t.result) && !/KOŞULLU/i.test(t.result));
|
|
109
|
-
if (lastReject && !lastReject.decision) addAlert('critical',
|
|
119
|
+
if (lastReject && !lastReject.decision) addAlert('critical', L('dashboard.alert.boardReject', { kind: lastReject.kind, scope: lastReject.scope, critical: lastReject.critical || L('dashboard.criticalFinding') }), L('dashboard.act.askClaudeFix'));
|
|
110
120
|
const limitLevel = level(limit);
|
|
111
|
-
const limitShort = limit ? [['
|
|
112
|
-
.map(([name, p]) => `${name} %${Math.round(p.percent)}${p.resetsAt ? ` (
|
|
113
|
-
if (limitLevel === 'red') addAlert('critical',
|
|
114
|
-
else if (limitLevel === 'yellow') addAlert('warn',
|
|
115
|
-
for (const b of d.pending) addAlert('warn',
|
|
116
|
-
for (const k of d.leftOff) addAlert('warn',
|
|
117
|
-
for (const s of d.issues.slice(0, 5)) addAlert('warn',
|
|
118
|
-
for (const r of repeatedBugClasses(root)) addAlert('info',
|
|
119
|
-
if (git?.changed.length > 15) addAlert('info',
|
|
120
|
-
if (git?.progressStale >= 3) addAlert('info',
|
|
121
|
+
const limitShort = limit ? [[L('dashboard.limit5h'), limit.five_hour], [L('dashboard.limitWeeklyLower'), limit.seven_day]].filter(([, p]) => p)
|
|
122
|
+
.map(([name, p]) => `${name} %${Math.round(p.percent)}${p.resetsAt ? ` ${L('dashboard.resetsParen', { when: new Date(p.resetsAt * 1000).toLocaleString(loc, { weekday: 'short', hour: '2-digit', minute: '2-digit' }) })}` : ''}`).join(', ') : '';
|
|
123
|
+
if (limitLevel === 'red') addAlert('critical', L('dashboard.alert.limitRed', { short: limitShort }), L('dashboard.act.limitRed'));
|
|
124
|
+
else if (limitLevel === 'yellow') addAlert('warn', L('dashboard.alert.limitYellow', { short: limitShort }), L('dashboard.act.limitYellow'));
|
|
125
|
+
for (const b of d.pending) addAlert('warn', L('dashboard.alert.pendingDecision', { text: b }), L('dashboard.act.claudeWaiting'));
|
|
126
|
+
for (const k of d.leftOff) addAlert('warn', L('dashboard.alert.leftOff', { text: k }), L('dashboard.act.buildContinue'));
|
|
127
|
+
for (const s of d.issues.slice(0, 5)) addAlert('warn', L('dashboard.alert.openIssue', { text: s }));
|
|
128
|
+
for (const r of repeatedBugClasses(root)) addAlert('info', L('dashboard.alert.repeatedBug', { count: r.specs.length, cls: r.cls, specs: r.specs.join(', ') }), '/retro');
|
|
129
|
+
if (git?.changed.length > 15) addAlert('info', L('dashboard.alert.uncommitted', { count: git.changed.length }), L('dashboard.act.commitOrWrapUp'));
|
|
130
|
+
if (git?.progressStale >= 3) addAlert('info', L('dashboard.alert.progressStale', { count: git.progressStale }), '/wrap-up');
|
|
121
131
|
const off = (a['Soru modu'] ?? '').match(/kapalı konular:\s*(.+)$/i)?.[1];
|
|
122
|
-
if (off && !/^yok$/i.test(off)) addAlert('info',
|
|
123
|
-
if (d.installed && d.board.state === 'kurulmadı' && !/başlangıç/i.test(a['Aktif faz'] ?? '')) addAlert('info', '
|
|
124
|
-
if (github?.liveMonitoring?.length) addAlert('critical',
|
|
125
|
-
else if (github?.liveRun?.conclusion === 'failure') addAlert('critical', '
|
|
132
|
+
if (off && !/^yok$/i.test(off)) addAlert('info', L('dashboard.alert.closedTopics', { topics: off }), L('dashboard.act.askModeOpen'));
|
|
133
|
+
if (d.installed && d.board.state === 'kurulmadı' && !/başlangıç/i.test(a['Aktif faz'] ?? '')) addAlert('info', L('dashboard.alert.boardNotSetUp'), '/board setup');
|
|
134
|
+
if (github?.liveMonitoring?.length) addAlert('critical', L('dashboard.alert.liveMonitoring', { title: github.liveMonitoring[0].title }), L('dashboard.act.fixLive', { number: github.liveMonitoring[0].number }));
|
|
135
|
+
else if (github?.liveRun?.conclusion === 'failure') addAlert('critical', L('dashboard.alert.liveRunFailed'), L('dashboard.act.fixLiveShort'));
|
|
126
136
|
const lastMain = github?.runs?.find((c) => /^(main|master)$/.test(c.headBranch) && c.workflowName === 'CI');
|
|
127
|
-
if (lastMain?.conclusion === 'failure') addAlert('warn', '
|
|
137
|
+
if (lastMain?.conclusion === 'failure') addAlert('warn', L('dashboard.alert.ciFailed'), L('dashboard.act.operationsTab'));
|
|
128
138
|
for (const h of accountList) for (const t of h.dates) {
|
|
129
|
-
if (t.left < 0) addAlert('critical',
|
|
130
|
-
else if (t.left <= 30) addAlert('warn',
|
|
139
|
+
if (t.left < 0) addAlert('critical', L('dashboard.alert.dateOverdue', { service: h.service, kind: dateKind(t.kind), date: t.date }), 'docs/operations/accounts.md');
|
|
140
|
+
else if (t.left <= 30) addAlert('warn', L('dashboard.alert.dateSoon', { service: h.service, kind: dateKind(t.kind), left: t.left, date: t.date }), L('dashboard.act.renew'));
|
|
131
141
|
}
|
|
132
|
-
for (const h of accountList.filter((x) => x.noTwoFactor)) addAlert('warn',
|
|
142
|
+
for (const h of accountList.filter((x) => x.noTwoFactor)) addAlert('warn', L('dashboard.alert.noTwoFactor', { service: h.service }), 'docs/operations/accounts.md');
|
|
133
143
|
for (const o of envWarnings(root)) addAlert(o.sev, o.text, o.action);
|
|
134
|
-
for (const alarm of alarmList.filter((x) => x.unset)) addAlert('warn',
|
|
135
|
-
if (economics && economics.margin < 0) addAlert('critical',
|
|
136
|
-
for (const k of acceptance.mismatched.slice(0, 3)) addAlert('warn',
|
|
144
|
+
for (const alarm of alarmList.filter((x) => x.unset)) addAlert('warn', L('dashboard.alert.spendUnset', { service: alarm.service }), 'docs/operations/spend-alarms.md');
|
|
145
|
+
if (economics && economics.margin < 0) addAlert('critical', L('dashboard.alert.negativeEconomics', { amount: Math.abs(economics.margin).toFixed(1), currency: economics.currency }), L('dashboard.act.priceDecision'));
|
|
146
|
+
for (const k of acceptance.mismatched.slice(0, 3)) addAlert('warn', L(k.result === 'failed' ? 'dashboard.alert.acceptRed' : k.test ? 'dashboard.alert.acceptNoResult' : 'dashboard.alert.acceptMissing', { id: k.id }), L('dashboard.act.acceptFix'));
|
|
137
147
|
const overdue = flagList.filter((b) => b.isOverdue);
|
|
138
|
-
if (overdue.length) addAlert('info',
|
|
139
|
-
if (fb.newCount) addAlert('info',
|
|
148
|
+
if (overdue.length) addAlert('info', L('dashboard.alert.flagsOverdue', { count: overdue.length, names: overdue.map((b) => b.name).join(', ') }), L('dashboard.act.removeFlag'));
|
|
149
|
+
if (fb.newCount) addAlert('info', L('dashboard.alert.newFeedback', { count: fb.newCount }), '/user-feedback');
|
|
140
150
|
const enabledPackages = (() => { try { return packageStatus(root).filter((p) => p.enabled).map((p) => p.name); } catch { return []; } })();
|
|
141
|
-
if (enabledPackages.length) addAlert('info',
|
|
151
|
+
if (enabledPackages.length) addAlert('info', L('dashboard.alert.packagesOn', { names: enabledPackages.join(', ') }), L('dashboard.act.packagesOff'));
|
|
142
152
|
const cmdMode = (() => { try { return commandMode(root).mode; } catch { return 'light'; } })();
|
|
143
|
-
if (cmdMode !== 'full' && /^Tam\b/.test(a['Profil'] ?? '')) addAlert('info', '
|
|
144
|
-
else if (cmdMode !== 'light' && !/^Tam\b/.test(a['Profil'] ?? '')) addAlert('info',
|
|
145
|
-
if (assets.codeMapDays > 30) addAlert('info',
|
|
146
|
-
if (context?.percent >= 70 && Date.now() - context.readAt < 3600e3) addAlert('warn',
|
|
147
|
-
for (const r of Object.values(findings.latest)) if (r.critical > 0) addAlert('warn',
|
|
153
|
+
if (cmdMode !== 'full' && /^Tam\b/.test(a['Profil'] ?? '')) addAlert('info', L('dashboard.alert.cmdListLight'), L('dashboard.act.cmdFull'));
|
|
154
|
+
else if (cmdMode !== 'light' && !/^Tam\b/.test(a['Profil'] ?? '')) addAlert('info', L('dashboard.alert.cmdListHeavy', { mode: cmdMode === 'full' ? L('dashboard.cmdModeFull') : L('dashboard.cmdModeMixed') }), L('dashboard.act.cmdLight'));
|
|
155
|
+
if (assets.codeMapDays > 30) addAlert('info', L('dashboard.alert.codeMapStale', { days: assets.codeMapDays }), L('dashboard.act.understand'));
|
|
156
|
+
if (context?.percent >= 70 && Date.now() - context.readAt < 3600e3) addAlert('warn', L('dashboard.alert.contextFull', { percent: context.percent }), L('dashboard.act.wrapUpClear'));
|
|
157
|
+
for (const r of Object.values(findings.latest)) if (r.critical > 0) addAlert('warn', L('dashboard.alert.findingsCritical', { kind: kindName(r.kind), critical: r.critical, health: r.health }), `docs/findings/${r.kind}/latest.md`);
|
|
148
158
|
// Severity order: critical > warn > info (same severity keeps insertion order)
|
|
149
159
|
const SEVERITY_ORDER = { critical: 0, warn: 1, info: 2 };
|
|
150
160
|
alerts.sort((x, y) => SEVERITY_ORDER[x.sev] - SEVERITY_ORDER[y.sev]);
|
|
@@ -184,170 +194,175 @@ const md = (s) => e(s).replace(/`([^`]+)`/g, '<code>$1</code>').replace(/\*\*([^
|
|
|
184
194
|
const percent = (b, t) => (t ? Math.round((b / t) * 100) : 0);
|
|
185
195
|
const progressBar = (b, t, cls = '') => `<div class="bar ${cls}" role="img" aria-label="${b}/${t}"><span style="width:${percent(b, t)}%"></span></div>`;
|
|
186
196
|
const empty = (text, action = '') => `<p class="empty">${md(text)}${action ? ` <code>${e(action)}</code>` : ''}</p>`;
|
|
187
|
-
const SEV = { critical: ['⛔', '
|
|
188
|
-
const
|
|
189
|
-
const
|
|
197
|
+
const SEV = { critical: ['⛔', 'dashboard.sevCritical'], warn: ['⚠', 'dashboard.sevWarn'], info: ['ℹ', 'dashboard.sevInfo'], good: ['✓', 'dashboard.sevGood'] };
|
|
198
|
+
const FEED_KEYS = { commit: 'dashboard.feedCommit', karar: 'dashboard.feedDecision', kurul: 'dashboard.feedBoard', 'bakım': 'dashboard.feedMaintenance' };
|
|
199
|
+
const feedLabel = (k) => (Object.hasOwn(FEED_KEYS, k) ? L(FEED_KEYS[k]) : k);
|
|
200
|
+
const onOff = (v) => (v === 'açık' ? L('dashboard.on') : v === 'kapalı' ? L('dashboard.off') : v);
|
|
201
|
+
// A text for the page's own script: a single-quoted JS string, no tag can end the script block
|
|
202
|
+
const jsStr = (key) => `'${L(key).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/</g, '\\u003c')}'`;
|
|
203
|
+
const badge = (sev, caption) => `<span class="badge ${sev}"><span class="ic" aria-hidden="true">${SEV[sev][0]}</span> ${e(caption ?? L(SEV[sev][1]))}</span>`;
|
|
204
|
+
const fmtDate = (t) => { const z = toDate(t); return Number.isNaN(z.getTime()) ? e(t) : z.toLocaleString(loc, { day: '2-digit', month: 'short', hour: t.length > 10 ? '2-digit' : undefined, minute: t.length > 10 ? '2-digit' : undefined }); };
|
|
190
205
|
const limitSeverity = (p, k) => (p == null ? 'info' : p >= THRESHOLDS[k].red ? 'critical' : p >= THRESHOLDS[k].yellow ? 'warn' : 'good');
|
|
191
206
|
const TOPICS = ['urun', 'teknik', 'tasarim', 'icerik', 'veri', 'guvenlik', 'maliyet'];
|
|
192
207
|
|
|
193
208
|
// The command to paste into Claude: "`/build G-013` …" or "/wrap-up ve /clear" → first command, without its <placeholder> or note
|
|
194
|
-
const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(/ ve | ile |\s*\(/)[0].replace(/\s*[[<].*$/, '').trim() : ''; };
|
|
195
|
-
const copyBtn = (s) => { const c = cmdOf(s); return c ? ` <button class="kopyala" type="button" data-cmd="${e(c)}" aria-label="${e(c)}
|
|
196
|
-
const alertRow = (x) => `<li class="${x.sev}"><span class="ic" aria-hidden="true">${SEV[x.sev][0]}</span><span><span class="sr-only">${SEV[x.sev][1]}: </span>${md(x.text)}</span>${x.action ? `<span class="eylem">→ ${md(x.action)}${copyBtn(x.action)}</span>` : ''}</li>`;
|
|
209
|
+
const cmdOf = (s = '') => { const m = String(s).match(/`(\/[^`\n]+)`/) ?? String(s).match(/^(\/.*)$/); return m ? m[1].split(/ ve | ile | and | to |\s*\(/)[0].replace(/\s*[[<].*$/, '').trim() : ''; };
|
|
210
|
+
const copyBtn = (s) => { const c = cmdOf(s); return c ? ` <button class="kopyala" type="button" data-cmd="${e(c)}" aria-label="${e(L('dashboard.copyAria', { command: c }))}">${e(L('dashboard.copy'))}</button>` : ''; };
|
|
211
|
+
const alertRow = (x) => `<li class="${x.sev}"><span class="ic" aria-hidden="true">${SEV[x.sev][0]}</span><span><span class="sr-only">${L(SEV[x.sev][1])}: </span>${md(x.text)}</span>${x.action ? `<span class="eylem">→ ${md(x.action)}${copyBtn(x.action)}</span>` : ''}</li>`;
|
|
197
212
|
const kpi = (label, value, sub = '', bar = '') => `<div class="kpi"><div class="kpi-l">${e(label)}</div><div class="kpi-v">${value}</div>${bar}<div class="kpi-s">${sub}</div></div>`;
|
|
198
213
|
|
|
199
214
|
// ---------- sections ----------
|
|
200
215
|
const activeIndex = Math.max(0, [{ name: 'Başlangıç' }, ...d.phases].findIndex((f) => (a['Aktif faz'] ?? '').toLowerCase().startsWith(f.name.toLowerCase().split(' ').slice(0, 2).join(' '))));
|
|
201
216
|
const start = (a['Başlangıç adımı'] ?? '0/9').split('/').map(Number);
|
|
202
217
|
const stageStrip = `<ol class="steps">${[{ name: 'Başlangıç', done: start[0] || 0, total: start[1] || 9 }, ...d.phases].map((f, i) =>
|
|
203
|
-
`<li class="step${i === activeIndex ? ' now' : ''}${f.total && f.done === f.total ? ' done' : ''}"><span class="step-n">${i === 0 ? '
|
|
218
|
+
`<li class="step${i === activeIndex ? ' now' : ''}${f.total && f.done === f.total ? ' done' : ''}"><span class="step-n">${i === 0 ? e(L('dashboard.stageStartLetter')) : i - 1}</span><div class="step-b"><b>${e(i === 0 ? L('dashboard.stageStart') : f.name.replace(/\s*\(.*\)$/, ''))}</b>${progressBar(f.done, f.total)}<span class="muted num">${f.done}/${f.total}</span></div></li>`).join('')}</ol>`;
|
|
204
219
|
|
|
205
220
|
const overview = `
|
|
206
221
|
<div class="grid2">
|
|
207
|
-
<section class="card"><h3
|
|
208
|
-
<h4
|
|
209
|
-
<section class="card"><h3
|
|
222
|
+
<section class="card"><h3>${L('dashboard.statusSummary')}</h3>${d.state ? `<p>${md(d.state)}</p>` : empty(L('dashboard.noSummary'))}
|
|
223
|
+
<h4>${L('dashboard.lastSession')}</h4>${d.lastSession.length ? `<ul>${d.lastSession.map((s) => `<li>${md(s)}</li>`).join('')}</ul>` : empty(L('dashboard.noRecords'))}</section>
|
|
224
|
+
<section class="card"><h3>${L('dashboard.recentActivity')}</h3>${feed.length ? `<ul class="feed compact">${feed.slice(0, 7).map(feedRow).join('')}</ul>` : empty(L('dashboard.noActivity'))}</section>
|
|
210
225
|
</div>
|
|
211
|
-
<section class="card"><h3
|
|
226
|
+
<section class="card"><h3>${L('dashboard.stages')}</h3>${stageStrip}</section>`;
|
|
212
227
|
|
|
213
228
|
function feedRow(x) {
|
|
214
|
-
return `<li class="feed-i t-${x.kind.replace('ı', 'i')}"><span class="ft">${e(x.kind)}</span><div><div>${md(x.title)}</div><div class="muted small">${fmtDate(x.time)}${x.extra ? ' · ' + e(x.extra) : ''}</div></div></li>`;
|
|
229
|
+
return `<li class="feed-i t-${x.kind.replace('ı', 'i')}"><span class="ft">${e(feedLabel(x.kind))}</span><div><div>${md(x.title)}</div><div class="muted small">${fmtDate(x.time)}${x.extra ? ' · ' + e(x.extra) : ''}</div></div></li>`;
|
|
215
230
|
}
|
|
216
231
|
|
|
217
232
|
const roadmapSection = d.phases.length ? d.phases.map((f) => `
|
|
218
233
|
<details class="card faz" ${f.name.toLowerCase().startsWith((a['Aktif faz'] ?? '#').toLowerCase().split(' ').slice(0, 2).join(' ')) ? 'open' : ''}>
|
|
219
234
|
<summary><b>${e(f.name)}</b><span class="faz-bar">${progressBar(f.done, f.total)}<span class="num muted">${f.done}/${f.total}</span></span></summary>
|
|
220
235
|
<ul class="tasks">${f.tasks.map((g) => `<li class="${g.done ? 'ok' : ''}${(a['Aktif görev'] && g.text.startsWith(a['Aktif görev'])) ? ' active' : ''}"><span class="chk" aria-hidden="true">${g.done ? '✓' : ''}</span><span>${md(g.text)}</span></li>`).join('')}</ul>
|
|
221
|
-
</details>`).join('') : empty('
|
|
236
|
+
</details>`).join('') : empty(L('dashboard.noRoadmap'));
|
|
222
237
|
|
|
223
238
|
const decisionTable = d.decisions.length ? `
|
|
224
|
-
<div class="filters" role="group" aria-label="
|
|
225
|
-
<div class="tw"><table id="kararlar"><thead><tr><th>#</th><th
|
|
226
|
-
${[...d.decisions].reverse().map((k) => `<tr data-k="${e(k.topic)}" data-v="${/otomatik/i.test(k.decidedBy) ? 'otomatik' : ''}" data-kapi="${/^tek/i.test(k.door) ? 'tek' : ''}"><td class="num">${e(k.no)}</td><td class="num nw">${e(k.date)}</td><td>${e(k.stage)}</td><td><span class="tag">${e(k.topic)}</span></td><td>${md(k.question)}</td><td><b>${md(k.chosen)}</b></td><td>${/^tek/i.test(k.door) ? badge('warn', '
|
|
227
|
-
</tbody></table></div>` : empty('
|
|
239
|
+
<div class="filters" role="group" aria-label="${e(L('dashboard.topicFilter'))}"><button class="chip on" data-k="hepsi">${L('dashboard.all')} (${d.decisions.length})</button>${TOPICS.filter((k) => d.decisions.some((x) => x.topic === k)).map((k) => `<button class="chip" data-k="${k}">${k} (${d.decisions.filter((x) => x.topic === k).length})</button>`).join('')}<button class="chip" data-k="otomatik">${L('dashboard.auto')} (${autoDecisions})</button><button class="chip" data-k="tek">${L('dashboard.oneWay')} (${d.decisions.filter((k) => /^tek/i.test(k.door)).length})</button></div>
|
|
240
|
+
<div class="tw"><table id="kararlar"><thead><tr><th>#</th><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.stage')}</th><th>${L('dashboard.th.topic')}</th><th>${L('dashboard.th.question')}</th><th>${L('dashboard.th.chosen')}</th><th>${L('dashboard.th.door')}</th><th>${L('dashboard.th.decidedBy')}</th></tr></thead><tbody>
|
|
241
|
+
${[...d.decisions].reverse().map((k) => `<tr data-k="${e(k.topic)}" data-v="${/otomatik/i.test(k.decidedBy) ? 'otomatik' : ''}" data-kapi="${/^tek/i.test(k.door) ? 'tek' : ''}"><td class="num">${e(k.no)}</td><td class="num nw">${e(k.date)}</td><td>${e(k.stage)}</td><td><span class="tag">${e(k.topic)}</span></td><td>${md(k.question)}</td><td><b>${md(k.chosen)}</b></td><td>${/^tek/i.test(k.door) ? badge('warn', L('dashboard.oneWay')) : k.door ? `<span class="muted small">${e(k.door)}</span>` : ''}</td><td>${/otomatik/i.test(k.decidedBy) ? badge('warn', L('dashboard.auto')) : badge('good', L('dashboard.user'))}</td></tr>`).join('')}
|
|
242
|
+
</tbody></table></div>` : empty(L('dashboard.noDecisions'));
|
|
228
243
|
|
|
229
244
|
const boardSection = `
|
|
230
245
|
<div class="grid2">
|
|
231
|
-
<section class="card"><h3
|
|
232
|
-
<p class="muted small"
|
|
233
|
-
<section class="card"><h3
|
|
246
|
+
<section class="card"><h3>${L('dashboard.members')} · ${e(d.board.state === 'kuruldu' ? L('dashboard.boardSetUp') : d.board.state === 'kurulmadı' ? L('dashboard.notSetUp') : d.board.state)}</h3>${d.board.members.length ? `<ul class="members">${d.board.members.map((u) => `<li><b>${e(u.member)}</b><span class="muted small">${e(u.model)} · ${e(u.focus)}</span></li>`).join('')}</ul>` : empty(L('dashboard.boardNotSetUp'), '/board setup')}
|
|
247
|
+
<p class="muted small">${L('dashboard.commitReview', { value: e(onOff(d.board.commitReview)) })}</p></section>
|
|
248
|
+
<section class="card"><h3>${L('dashboard.summary')}</h3><div class="kpis mini">${kpi(L('dashboard.kpiMeetings'), d.board.meetings.length)}${kpi('RET', rejectCount, rejectCount ? badge('critical', L('dashboard.criticalFinding')) : '')}${kpi(L('dashboard.kpiApproval'), d.board.meetings.length ? `%${percent(d.board.meetings.filter((t) => /^ONAY/i.test(t.result)).length, d.board.meetings.length)}` : '-')}</div></section>
|
|
234
249
|
</div>
|
|
235
|
-
<section class="card"><h3
|
|
250
|
+
<section class="card"><h3>${L('dashboard.councilHeading')}</h3>${councils.length ? (() => {
|
|
236
251
|
const latest = councils[0];
|
|
237
|
-
const decisionBadge = (k) => (!k ? badge('info', '
|
|
238
|
-
return `<h4
|
|
239
|
-
${latest.ranking.length ? `<h4
|
|
240
|
-
<div class="tw"><table><thead><tr><th
|
|
241
|
-
})() : empty('
|
|
242
|
-
<section class="card"><h3
|
|
252
|
+
const decisionBadge = (k) => (!k ? badge('info', L('dashboard.noDecision')) : /KOŞULLU/i.test(k) ? badge('warn', k) : /RET/i.test(k) ? badge('critical', k) : badge('good', k));
|
|
253
|
+
return `<h4>${L('dashboard.latestCouncil', { topic: md(latest.topic) })}</h4><div class="kpis mini">${kpi(L('dashboard.th.decision'), decisionBadge(latest.decision))}${kpi(L('dashboard.th.consensus'), latest.consensus == null ? '-' : `%${latest.consensus}`, latest.consensus != null && latest.consensus < 60 ? badge('warn', L('dashboard.low')) : '')}${kpi(L('dashboard.kpiDisputed'), latest.disputed)}${kpi(L('dashboard.kpiEliminated'), latest.eliminated, L('dashboard.byCrossVote'))}</div>
|
|
254
|
+
${latest.ranking.length ? `<h4>${L('dashboard.overallRanking')}</h4><div class="rows">${latest.ranking.map((r, i) => `<div class="row"><span><b>${i + 1}.</b> ${L('dashboard.opinion', { letter: e(r.opinion) })}${r.who ? ` <span class="muted small">${e(r.who)}</span>` : ''}</span><span class="num muted">${L('dashboard.avg', { average: e(r.average) })}</span></div>`).join('')}</div>` : ''}
|
|
255
|
+
<div class="tw"><table><thead><tr><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.topic')}</th><th>${L('dashboard.th.decision')}</th><th>${L('dashboard.th.consensus')}</th><th>${L('dashboard.th.minutes')}</th></tr></thead><tbody>${councils.slice(0, 10).map((k) => `<tr><td class="num nw">${e(k.date)}</td><td>${e(k.kind)}</td><td>${md(k.topic)}</td><td>${decisionBadge(k.decision)}</td><td class="num">${k.consensus == null ? '-' : `%${k.consensus}`}</td><td><a href="${k.file.split('/').map(encodeURIComponent).join('/')}" target="_blank" rel="noopener">${L('dashboard.open')}</a></td></tr>`).join('')}</tbody></table></div>`;
|
|
256
|
+
})() : empty(L('dashboard.noCouncil'), '/board council <soru>')}</section>
|
|
257
|
+
<section class="card"><h3>${L('dashboard.meetingLog')}</h3>${d.board.meetings.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.scope')}</th><th>${L('dashboard.th.attendees')}</th><th>${L('dashboard.th.result')}</th><th>${L('dashboard.th.critical')}</th><th>${L('dashboard.th.userDecision')}</th></tr></thead><tbody>
|
|
243
258
|
${[...d.board.meetings].reverse().map((t) => `<tr><td class="num">${e(t.no)}</td><td class="num nw">${e(t.date)}</td><td>${e(t.kind)}</td><td>${md(t.scope)}</td><td>${e(t.attendees)}</td><td>${/KOŞULLU/i.test(t.result) ? badge('warn', t.result) : /RET/i.test(t.result) ? badge('critical', t.result) : badge('good', t.result)}</td><td>${md(t.critical)}</td><td>${md(t.decision)}</td></tr>`).join('')}
|
|
244
|
-
</tbody></table></div>` : empty('
|
|
259
|
+
</tbody></table></div>` : empty(L('dashboard.noMeetings'))}</section>`;
|
|
245
260
|
|
|
246
261
|
const quality = `
|
|
247
262
|
<div class="grid2">
|
|
248
|
-
<section class="card"><h3
|
|
249
|
-
<section class="card"><h3
|
|
250
|
-
<div class="kpis mini">${kpi('
|
|
263
|
+
<section class="card"><h3>${L('dashboard.checklists')}</h3><div class="rows">${d.checklists.map((l) => `<div class="row"><span>${e(l.name)}</span>${progressBar(l.done, l.total)}<span class="num muted">${l.done}/${l.total}</span></div>`).join('')}</div></section>
|
|
264
|
+
<section class="card"><h3>${L('dashboard.codeHealth')}</h3>
|
|
265
|
+
<div class="kpis mini">${kpi(L('dashboard.th.commit'), last30.length)}${kpi(L('dashboard.fixRate'), fixRatio == null ? '-' : `%${fixRatio}`, 'fix / (feat + fix)')}${kpi(L('dashboard.uncommitted'), git ? git.changed.length : '-')}</div>
|
|
251
266
|
${Object.keys(kindCounts).length ? `<div class="rows">${Object.entries(kindCounts).sort((x, y) => y[1] - x[1]).map(([t, n]) => `<div class="row"><span><code>${e(t)}</code></span>${progressBar(n, last30.length)}<span class="num muted">${n}</span></div>`).join('')}</div>` : ''}
|
|
252
|
-
<p class="muted small"
|
|
267
|
+
<p class="muted small">${L('dashboard.fixRateNote')}</p></section>
|
|
253
268
|
</div>
|
|
254
|
-
${qa ? `<section class="card"><h3
|
|
255
|
-
<section class="card"><h3
|
|
256
|
-
<section class="card"><h3
|
|
257
|
-
<section class="card"><h3
|
|
258
|
-
<section class="card"><h3
|
|
259
|
-
${lastRetro ? `<p class="muted small"
|
|
269
|
+
${qa ? `<section class="card"><h3>${L('dashboard.lastQa', { name: e(qa.name) })}</h3><div class="kpis mini">${kpi(L('dashboard.passed'), qa.passed)}${kpi(L('dashboard.failed'), qa.failed, qa.failed ? badge('critical', L('dashboard.withEvidence')) : '')}${kpi(L('dashboard.notRun'), qa.notRun)}</div>${qa.failures.length ? `<ul>${qa.failures.slice(0, 5).map((x) => `<li>${md(x.criterion)}${x.note ? ` <span class="muted small">${e(x.note)}</span>` : ''}</li>`).join('')}</ul>` : ''}<p class="muted small"><a href="${e(qa.file)}" target="_blank" rel="noopener">${L('dashboard.openReport')}</a></p></section>` : ''}
|
|
270
|
+
<section class="card"><h3>${L('dashboard.acceptanceTests')}</h3>${acceptance.total ? `<div class="kpis mini">${kpi(L('dashboard.th.criterion'), acceptance.total)}${kpi(L('dashboard.kpiTested'), `${acceptance.tested}/${acceptance.total}`, '', progressBar(acceptance.tested, acceptance.total))}${kpi(L('dashboard.kpiPassing'), `${acceptance.passing}/${acceptance.total}`, acceptance.reportTime ? L('dashboard.reportTime', { when: new Date(acceptance.reportTime).toLocaleString(loc, { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) }) : L('dashboard.noJunit'), progressBar(acceptance.passing, acceptance.total, 'good'))}</div>` : empty(L('dashboard.noAcceptanceIds'))}</section>
|
|
271
|
+
<section class="card"><h3>${L('dashboard.metricsMonthly')}</h3>${metricRecords.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.period')}</th><th>${L('dashboard.th.task')}</th><th>${L('dashboard.th.avgMinutes')}</th><th>${L('dashboard.th.avgLimit')}</th><th>${L('dashboard.th.fixes')}</th><th>${L('dashboard.th.reverts')}</th><th>${L('dashboard.th.duplicateCode')}</th><th>${L('dashboard.th.openCritical')}</th><th>${L('dashboard.th.acceptance')}</th></tr></thead><tbody>${[...metricRecords].reverse().slice(0, 8).map((m) => `<tr><td class="nw">${e(m.period)}</td><td class="num">${e(m.taskCount)}</td><td class="num">${e(m.duration)}</td><td class="num">${e(m.limit)}</td><td class="num">${e(m.fixes)}</td><td class="num">${e(m.reverts)}</td><td class="num">${e(m.duplicates)}</td><td class="num">${e(m.critical)}</td><td class="num">${e(m.acceptance)}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noMetrics'))}</section>
|
|
272
|
+
<section class="card"><h3>${L('dashboard.criticalFlowTests')}</h3>${criticalFlows.length ? `<div class="tw"><table><thead><tr><th>#</th><th>${L('dashboard.th.flow')}</th><th>${L('dashboard.th.testKind')}</th><th>${L('dashboard.th.file')}</th><th>${L('dashboard.status')}</th></tr></thead><tbody>${criticalFlows.map((k) => `<tr><td class="num">${e(k.no)}</td><td>${md(k.feed)}</td><td>${e(k.kind)}</td><td>${md(k.fileName)}</td><td>${/✅|yeşil|var/i.test(k.state) ? badge('good', L('dashboard.present')) : badge('warn', L('dashboard.missing'))}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noTestPlan'))}</section>
|
|
273
|
+
<section class="card"><h3>${L('dashboard.maintenanceLog')}</h3>${maintenance.length ? `<ul class="feed compact">${[...maintenance].reverse().slice(0, 8).map((b) => feedRow({ time: b.date, kind: 'bakım', title: `${b.kind}: ${b.findings}`, extra: b.work })).join('')}</ul>` : empty(L('dashboard.noMaintenance'), '/maintain weekly')}
|
|
274
|
+
${lastRetro ? `<p class="muted small">${L('dashboard.lastRetro', { value: e(lastRetro) })}</p>` : ''}</section>`;
|
|
260
275
|
|
|
261
276
|
const maxDay = Math.max(1, ...Object.values(dayCounts));
|
|
262
277
|
const activity = `
|
|
263
|
-
<section class="card"><h3
|
|
264
|
-
<div class="spark" role="img" aria-label="
|
|
265
|
-
<div class="spark-x muted small"><span>${toDate(days[0]).toLocaleDateString(
|
|
266
|
-
<details class="tablo-gor"><summary class="muted small"
|
|
278
|
+
<section class="card"><h3>${L('dashboard.commitsLast14')} <span class="muted small">${L('dashboard.totalParen', { total: days.reduce((t, g) => t + dayCounts[g], 0) })}</span></h3>
|
|
279
|
+
<div class="spark" role="img" aria-label="${e(L('dashboard.sparkAria'))}">${days.map((g) => `<div class="sb" data-tip="${e(L('dashboard.commitTip', { date: toDate(g).toLocaleDateString(loc, { day: 'numeric', month: 'short', weekday: 'short' }), count: dayCounts[g] }))}"><span style="height:${dayCounts[g] ? Math.max(6, (dayCounts[g] / maxDay) * 100) : 0}%"></span></div>`).join('')}</div>
|
|
280
|
+
<div class="spark-x muted small"><span>${toDate(days[0]).toLocaleDateString(loc, { day: 'numeric', month: 'short' })}</span><span>${L('dashboard.today')}</span></div>
|
|
281
|
+
<details class="tablo-gor"><summary class="muted small">${L('dashboard.showTable')}</summary><div class="tw"><table><thead><tr><th>${L('dashboard.th.day')}</th><th>${L('dashboard.th.commit')}</th></tr></thead><tbody>${days.map((g) => `<tr><td class="num">${g}</td><td class="num">${dayCounts[g]}</td></tr>`).join('')}</tbody></table></div></details>
|
|
267
282
|
</section>
|
|
268
|
-
<section class="card"><h3
|
|
283
|
+
<section class="card"><h3>${L('dashboard.feed')}</h3>${feed.length ? `<ul class="feed">${feed.map(feedRow).join('')}</ul>` : empty(L('dashboard.noActivity'))}</section>`;
|
|
269
284
|
|
|
270
285
|
const product = `
|
|
271
286
|
<div class="grid2">
|
|
272
|
-
<section class="card"><h3
|
|
273
|
-
<h4
|
|
274
|
-
<h4
|
|
275
|
-
${outOfScope.length ? `<h4
|
|
276
|
-
<section class="card"><h3
|
|
277
|
-
${acceptance.total ? `<h4
|
|
287
|
+
<section class="card"><h3>${L('dashboard.product')}</h3>
|
|
288
|
+
<h4>${L('dashboard.problem')}</h4>${firstSentence(findSection(prd, '1. Problem')) ? `<p>${md(firstSentence(findSection(prd, '1. Problem')))}</p>` : empty(L('dashboard.noPrd'), '/start')}
|
|
289
|
+
<h4>${L('dashboard.valueProp')}</h4>${firstSentence(findSection(prd, '3. Değer')) ? `<p>${md(firstSentence(findSection(prd, '3. Değer')))}</p>` : empty('-')}
|
|
290
|
+
${outOfScope.length ? `<h4>${L('dashboard.outOfScope')}</h4><ul>${outOfScope.map((k) => `<li>${md(k)}</li>`).join('')}</ul>` : ''}</section>
|
|
291
|
+
<section class="card"><h3>${L('dashboard.mvpFeatures')}</h3>${mvp.length ? `<div class="rows">${mvp.map((o) => `<div class="row"><span>${md(o.title)} ${o.priority ? `<span class="tag">${e(o.priority)}</span>` : ''}</span>${progressBar(o.done, o.total)}<span class="num muted">${o.done}/${o.total}</span></div>`).join('')}</div>` : empty(L('dashboard.noMvp'))}
|
|
292
|
+
${acceptance.total ? `<h4>${L('dashboard.criteriaResults')}</h4><div class="tw"><table><thead><tr><th>${L('dashboard.th.criterion')}</th><th>PRD</th><th>${L('dashboard.th.test')}</th></tr></thead><tbody>${acceptance.list.map((k) => `<tr><td><code>${e(k.id)}</code> ${md(k.text)}</td><td>${k.checked ? '✓' : ''}</td><td>${k.result === 'passed' ? badge('good', L('dashboard.passedBadge')) : k.result === 'failed' ? badge('critical', L('dashboard.failedBadge')) : k.test ? badge('info', L('dashboard.noResult')) : badge('warn', L('dashboard.noTest'))}</td></tr>`).join('')}</tbody></table></div>` : ''}</section>
|
|
278
293
|
</div>
|
|
279
294
|
<div class="grid2">
|
|
280
|
-
<section class="card"><h3
|
|
281
|
-
<section class="card"><h3
|
|
282
|
-
${previews.length ? `<h4
|
|
295
|
+
<section class="card"><h3>${L('dashboard.technology')}</h3>${stack.length && !stack.every((s) => !s.choice) ? `<dl class="dl">${stack.map((s) => `<dt>${e(s.layer)}</dt><dd>${md(s.choice || '-')}</dd>`).join('')}</dl>` : empty(L('dashboard.noStack'))}</section>
|
|
296
|
+
<section class="card"><h3>${L('dashboard.design')}</h3>${designDirection.length ? `<dl class="dl">${designDirection.map((t) => `<dt>${e(t.area)}</dt><dd>${md(t.choice)}</dd>`).join('')}</dl>` : empty(L('dashboard.noDesign'), '/design genel')}
|
|
297
|
+
${previews.length ? `<h4>${L('dashboard.previews')}</h4><ul class="links">${previews.map((o) => `<li><b>${e(o.name)}</b> ${o.files.map((f) => `<a href=".previews/${encodeURIComponent(o.name)}/${encodeURIComponent(f)}" target="_blank" rel="noopener">${e(f.replace(/\.html$/, ''))}</a>`).join(' ')}</li>`).join('')}</ul>` : ''}</section>
|
|
283
298
|
</div>
|
|
284
299
|
<div class="grid2">
|
|
285
|
-
<section class="card"><h3
|
|
286
|
-
<section class="card"><h3
|
|
300
|
+
<section class="card"><h3>${L('dashboard.unitEconomics')}</h3>${economics ? `<div class="kpis mini">${kpi(L('dashboard.kpiMargin'), `${economics.margin.toFixed(1)} ${e(economics.currency)}`, L('dashboard.perPayingUserMonth'))}${kpi(L('dashboard.kpiBreakEven'), economics.breakEven ?? '—', economics.breakEven ? L('dashboard.payingUsers') : L('dashboard.negativeMargin'))}${kpi(L('dashboard.atTarget', { target: economics.target }), `${economics.profit >= 0 ? '+' : ''}${Math.round(economics.profit)} ${e(economics.currency)}`, L('dashboard.monthlyProfitLoss'))}${economics.variableRatio != null ? kpi(L('dashboard.variableOverPrice'), `%${Math.round(economics.variableRatio)}`, economics.variableRatio > 30 ? badge('warn', L('dashboard.over30')) : '') : ''}</div>` : empty(L('dashboard.noInputs'), 'docs/product/unit-economics.md')}</section>
|
|
301
|
+
<section class="card"><h3>${L('dashboard.userFeedback')}</h3>${fb.records.length ? `<div class="kpis mini">${kpi(L('dashboard.kpiTotal'), fb.records.length)}${kpi(L('dashboard.kpiNew'), fb.newCount)}${kpi(L('dashboard.kpiPlanned'), fb.records.filter((k) => /planland/i.test(k.state)).length)}</div>${fb.themes.length ? `<h4>${L('dashboard.topThemes')}</h4><div class="rows">${fb.themes.slice(0, 5).map((t) => `<div class="row"><span>${md(t.theme)}</span>${progressBar(t.count, fb.themes[0].count)}<span class="num muted">${t.count}</span></div>`).join('')}</div>` : ''}` : empty(L('dashboard.noFeedback'), '/user-feedback')}</section>
|
|
287
302
|
</div>
|
|
288
|
-
${learnt ? `<section class="card"><h3
|
|
303
|
+
${learnt ? `<section class="card"><h3>${L('dashboard.learning')}</h3><div class="kpis mini">${kpi(L('dashboard.kpiRight'), learnt.right)}${kpi(L('dashboard.kpiWrong'), learnt.wrong)}${kpi(L('dashboard.kpiSuccess'), `%${percent(learnt.right, learnt.right + learnt.wrong)}`)}</div>${learnt.missed.length ? `<h4>${L('dashboard.oftenMissed')}</h4><div class="rows">${learnt.missed.slice(0, 5).map((m) => `<div class="row"><span>${md(m.concept)}</span>${progressBar(m.count, learnt.missed[0].count)}<span class="num muted">${m.count}</span></div>`).join('')}</div>` : ''}</section>` : ''}
|
|
289
304
|
<div class="grid2">
|
|
290
|
-
<section class="card"><h3
|
|
291
|
-
<section class="card"><h3
|
|
292
|
-
<h4
|
|
305
|
+
<section class="card"><h3>${L('dashboard.brandPack')}</h3>${assets.brand.length ? `<p class="muted small"><code>${e(assets.brandDir)}</code> · ${L('dashboard.filesCount', { count: assets.brand.length })}${assets.brandGuide ? ` · <a href="docs/design/brand.md" target="_blank" rel="noopener">${L('dashboard.usageRules')}</a>` : ''}</p><ul class="links">${assets.brand.map((f) => `<li><a href="${e(assets.brandDir)}/${encodeURIComponent(f)}" target="_blank" rel="noopener">${e(f)}</a></li>`).join('')}</ul>` : empty(L('dashboard.noLogo'), 'logo-design')}</section>
|
|
306
|
+
<section class="card"><h3>${L('dashboard.promoResearch')}</h3><h4>${L('dashboard.videos')}</h4>${assets.videos.length ? `<ul class="links">${assets.videos.map((v) => `<li><b>${e(v.name)}</b> ${v.mp4 ? `<a href="${v.mp4.split('/').map(encodeURIComponent).join('/')}" target="_blank" rel="noopener">mp4</a>` : badge('info', L('dashboard.noRender'))}</li>`).join('')}</ul>` : empty(L('dashboard.noVideos'), 'motion-video')}
|
|
307
|
+
<h4>${L('dashboard.researchNotes')}</h4>${assets.research.length ? `<ul class="links">${assets.research.slice(0, 8).map((r) => `<li><a href="${r.file.split('/').map(encodeURIComponent).join('/')}" target="_blank" rel="noopener">${md(r.title)}</a></li>`).join('')}</ul>` : empty(L('dashboard.noNotes'), '/research')}</section>
|
|
293
308
|
</div>
|
|
294
|
-
<section class="card"><h3
|
|
309
|
+
<section class="card"><h3>${L('dashboard.unreleased')}</h3>${unreleased.length ? `<ul>${unreleased.map((s) => `<li>${md(s)}</li>`).join('')}</ul>` : empty(L('dashboard.noUnreleased'))}</section>`;
|
|
295
310
|
|
|
296
311
|
const limitCard = (name, k) => {
|
|
297
312
|
const p = limit?.[k];
|
|
298
313
|
const sev = limitSeverity(p?.percent, k);
|
|
299
|
-
return `<div class="lim"><div class="lim-h"><b>${name}</b>${badge(sev, p ? `%${Math.round(p.percent)}` : '
|
|
300
|
-
<div class="bar big ${sev}"><span style="width:${p ? Math.min(100, p.percent) : 0}%"></span><i style="left:${THRESHOLDS[k].yellow}%" title="
|
|
301
|
-
<div class="muted small">${p?.resetsAt ?
|
|
314
|
+
return `<div class="lim"><div class="lim-h"><b>${name}</b>${badge(sev, p ? `%${Math.round(p.percent)}` : L('dashboard.unknown'))}</div>
|
|
315
|
+
<div class="bar big ${sev}"><span style="width:${p ? Math.min(100, p.percent) : 0}%"></span><i style="left:${THRESHOLDS[k].yellow}%" title="${e(L('dashboard.yellowThreshold'))}"></i><i style="left:${THRESHOLDS[k].red}%" title="${e(L('dashboard.redThreshold'))}"></i></div>
|
|
316
|
+
<div class="muted small">${p?.resetsAt ? L('dashboard.resetsLabel', { when: new Date(p.resetsAt * 1000).toLocaleString(loc, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) }) : p?.renewed ? L('dashboard.renewed') : '-'} · ${L('dashboard.thresholdsNote', { yellow: THRESHOLDS[k].yellow, red: THRESHOLDS[k].red })}</div></div>`;
|
|
302
317
|
};
|
|
303
318
|
const limitSection = `
|
|
304
|
-
<section class="card"><h3
|
|
319
|
+
<section class="card"><h3>${L('dashboard.usageLimit')}</h3><div class="grid2 tight">${limitCard(L('dashboard.limit5h'), 'five_hour')}${limitCard(L('dashboard.limitWeekly'), 'seven_day')}</div>
|
|
305
320
|
<p class="muted small">${e(summary(limit))}</p>
|
|
306
|
-
<h4
|
|
307
|
-
<h4
|
|
308
|
-
<section class="card"><h3
|
|
321
|
+
<h4>${L('dashboard.contextWindow')}</h4>${context ? `<div class="lim"><div class="lim-h"><b>${L('dashboard.lastSessionShort')}</b>${badge(context.percent >= 70 ? 'critical' : context.percent >= 50 ? 'warn' : 'good', `%${context.percent}`)}</div><div class="bar big ${context.percent >= 70 ? 'critical' : context.percent >= 50 ? 'warn' : 'good'}"><span style="width:${Math.min(100, context.percent)}%"></span><i style="left:50%"></i><i style="left:70%"></i></div><div class="muted small">${L('dashboard.contextHint', { time: new Date(context.readAt).toLocaleString(loc, { hour: '2-digit', minute: '2-digit' }) })}</div></div>` : empty(L('dashboard.noContext'))}
|
|
322
|
+
<h4>${L('dashboard.modelEffort')}</h4><div class="tw"><table><tbody>${Object.entries(readModels(modelsFile(root))).map(([r, v]) => `<tr><td>${e(roleLabel(root, r))}</td><td class="nw">${e(v.replace(':', ' · '))}</td></tr>`).join('')}</tbody></table></div><p class="muted small">${modelsNote(modelsFile(root), root) ? `${e(modelsNote(modelsFile(root), root))}. ` : ''}${L('dashboard.modelsHint')}</p></section>
|
|
323
|
+
<section class="card"><h3>${L('dashboard.tokenCost')}</h3>${tokenCost?.days.length ? (() => {
|
|
309
324
|
const latest = tokenCost.days.slice(-14);
|
|
310
325
|
const maxTokens = Math.max(...latest.map((g) => g.token));
|
|
311
|
-
return `<div class="kpis mini">${kpi('
|
|
326
|
+
return `<div class="kpis mini">${kpi(L('dashboard.totalTokens'), `${(tokenCost.total.token / 1e6).toFixed(1)}M`)}${kpi(L('dashboard.apiEquivalent'), `$${tokenCost.total.cost.toFixed(2)}`, L('dashboard.notBilled'))}${kpi(L('dashboard.dailyAvg'), `$${(tokenCost.total.cost / tokenCost.days.length).toFixed(2)}`, L('dashboard.activeDays', { count: tokenCost.days.length }))}</div>
|
|
312
327
|
<div class="rows">${latest.map((g) => `<div class="row"><span class="num nw">${e(g.date.slice(5))}</span>${progressBar(g.token, maxTokens)}<span class="num muted">${(g.token / 1e6).toFixed(1)}M · $${g.cost.toFixed(2)}</span></div>`).join('')}</div>
|
|
313
|
-
<p class="muted small"
|
|
314
|
-
})() : empty('
|
|
315
|
-
<section class="card"><h3
|
|
316
|
-
<div class="tw"><table><thead><tr><th
|
|
328
|
+
<p class="muted small">${L('dashboard.ccusageNote', { when: fmtDate(tokenCost.readAt) })}</p>`;
|
|
329
|
+
})() : empty(L('dashboard.noData'), 'node .claude/hooks/cost.mjs')}</section>
|
|
330
|
+
<section class="card"><h3>${L('dashboard.costLog')}</h3>${avgCost.length ? `<div class="kpis mini">${avgCost.map((m) => kpi(L('dashboard.avgSize', { size: m.size }), `%${m.avg.toFixed(1)}`, L('dashboard.recordsCount', { count: m.n }))).join('')}</div>
|
|
331
|
+
<div class="tw"><table><thead><tr><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.job')}</th><th>${L('dashboard.th.size')}</th><th>${L('dashboard.th.usage5h')}</th></tr></thead><tbody>${[...cost].reverse().slice(0, 15).map((m) => `<tr><td class="num nw">${e(m.date)}</td><td>${md(m.job)}</td><td>${e(m.size)}</td><td class="num">${Number.isNaN(m.diff) ? '-' : `%${m.diff}`}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noCostRecords'))}</section>`;
|
|
317
332
|
|
|
318
|
-
const daysBadge = (t) => (t.left < 0 ? badge('critical',
|
|
333
|
+
const daysBadge = (t) => (t.left < 0 ? badge('critical', L('dashboard.daysOverdue', { days: -t.left })) : t.left <= 30 ? badge('warn', L('dashboard.daysLeft', { days: t.left })) : badge('good', L('dashboard.daysLeft', { days: t.left })));
|
|
319
334
|
const operations = `
|
|
320
|
-
<section class="card"><h3
|
|
321
|
-
<section class="card"><h3
|
|
322
|
-
<section class="card"><h3
|
|
335
|
+
<section class="card"><h3>${L('dashboard.accountsDeadlines')}</h3>${accountList.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.service')}</th><th>${L('dashboard.th.purpose')}</th><th>${L('dashboard.th.owner')}</th><th>2FA</th><th>Plan</th><th>${L('dashboard.th.dates')}</th></tr></thead><tbody>${accountList.map((h) => `<tr><td><b>${e(h.service)}</b></td><td>${e(h.purpose)}</td><td>${e(h.owner)}</td><td>${h.noTwoFactor ? badge('warn', L('dashboard.none')) : e(h.twoFactor)}</td><td>${e(h.plan)}</td><td>${h.dates.map((t) => `<div class="nw">${e(dateKind(t.kind))}: ${e(t.date)} ${daysBadge(t)}</div>`).join('') || '<span class="muted">-</span>'}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noAccounts'), 'docs/operations/accounts.md')}</section>
|
|
336
|
+
<section class="card"><h3>${L('dashboard.spendAlarms')}</h3>${alarmList.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.service')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.amount')}</th><th>${L('dashboard.th.alertThresholds')}</th><th>${L('dashboard.th.lastCheck')}</th></tr></thead><tbody>${alarmList.map((alarm) => `<tr><td><b>${e(alarm.service)}</b></td><td>${e(alarm.kind)}</td><td>${alarm.unset ? badge('warn', L('dashboard.notSet')) : e(alarm.amount)}</td><td>${e(alarm.thresholds)}</td><td class="nw">${e(alarm.lastCheck || '-')} ${alarm.stale ? badge('info', L('dashboard.checkIt')) : ''}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noSpendAlarms'), 'docs/operations/spend-alarms.md')}</section>
|
|
337
|
+
<section class="card"><h3>${L('dashboard.flagsHeading')}</h3>${flagList.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.flag')}</th><th>${L('dashboard.th.purpose')}</th><th>${L('dashboard.status')}</th><th>${L('dashboard.th.removal')}</th><th>${L('dashboard.th.killSwitch')}</th></tr></thead><tbody>${flagList.map((b) => `<tr><td><code>${e(b.name)}</code></td><td>${md(b.purpose)}</td><td>${e(b.state)}</td><td class="nw">${e(b.removal)} ${b.isOverdue ? badge('warn', L('dashboard.overdue')) : ''}</td><td>${b.untested ? badge('warn', L('dashboard.untested')) : badge('good', L('dashboard.tested'))}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noFlags'), 'docs/operations/feature-flags.md')}</section>
|
|
323
338
|
<section class="card"><h3>GitHub</h3>${github ? `
|
|
324
|
-
${github.liveRun ? `<p><b
|
|
325
|
-
${github.liveMonitoring?.length ? `<p>${badge('critical', '
|
|
326
|
-
<h4
|
|
327
|
-
<h4
|
|
328
|
-
<p class="muted small"
|
|
339
|
+
${github.liveRun ? `<p><b>${L('dashboard.liveMonitoring')}</b> · ${L('dashboard.liveLastRun')} ${fmtDate(github.liveRun.createdAt)} ${github.liveRun.conclusion === 'skipped' ? `${badge('info', L('dashboard.notSetUp'))} <span class="muted small">${L('dashboard.liveUrlHint')}</span>` : github.liveRun.conclusion === 'success' ? badge('good', L('dashboard.succeeded')) : github.liveRun.conclusion === 'failure' ? `${badge('critical', L('dashboard.failedRun'))} <span class="muted small">/fix-bug</span>` : badge('info', github.liveRun.conclusion || github.liveRun.status)} <a href="${e(github.liveRun.url)}" target="_blank" rel="noopener">${L('dashboard.logLink')}</a></p>` : ''}
|
|
340
|
+
${github.liveMonitoring?.length ? `<p>${badge('critical', L('dashboard.liveBroken'))} ${github.liveMonitoring.map((i) => `<a href="${e(i.url)}" target="_blank" rel="noopener">#${e(i.number)} ${e(i.title)}</a>`).join(', ')}</p>` : ''}
|
|
341
|
+
<h4>${L('dashboard.recentWorkflows')}</h4>${github.runs?.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.workflow')}</th><th>${L('dashboard.th.branch')}</th><th>${L('dashboard.th.result')}</th><th>${L('dashboard.th.time')}</th></tr></thead><tbody>${github.runs.map((c) => `<tr><td><a href="${e(c.url)}" target="_blank" rel="noopener">${e(c.workflowName)}</a></td><td><code>${e(c.headBranch)}</code></td><td>${c.conclusion === 'success' ? badge('good', L('dashboard.succeeded')) : c.conclusion === 'failure' ? badge('critical', L('dashboard.failedRun')) : badge('info', c.conclusion || c.status)}</td><td class="num nw">${fmtDate(c.createdAt)}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noRuns'))}
|
|
342
|
+
<h4>${L('dashboard.openPrs')}</h4>${github.prs?.length ? `<ul>${github.prs.map((r) => `<li><a href="${e(r.url)}" target="_blank" rel="noopener">#${e(r.number)} ${e(r.title)}</a>${r.isDraft ? ` <span class="tag">${L('dashboard.draft')}</span>` : ''}</li>`).join('')}</ul>` : empty(L('dashboard.noPrs'))}
|
|
343
|
+
<p class="muted small">${L('dashboard.ghNote')}</p>` : empty(L('dashboard.noGithub'))}</section>`;
|
|
329
344
|
|
|
330
345
|
const healthSeverity = (p) => (p >= 85 ? 'good' : p >= 70 ? 'info' : p >= 50 ? 'warn' : 'critical');
|
|
331
|
-
const healthLabel = (p) => (p >= 85 ? '
|
|
346
|
+
const healthLabel = (p) => L(p >= 85 ? 'dashboard.healthHealthy' : p >= 70 ? 'dashboard.healthGood' : p >= 50 ? 'dashboard.sevWarn' : 'dashboard.healthRisky');
|
|
332
347
|
const findingsSection = `
|
|
333
|
-
<section class="card"><h3
|
|
348
|
+
<section class="card"><h3>${L('dashboard.areaScores')}</h3>${Object.keys(findings.latest).length ? `<div class="kpis mini">${Object.keys(FINDING_KINDS).filter((t) => findings.latest[t]).map((t) => {
|
|
334
349
|
const r = findings.latest[t];
|
|
335
350
|
const diff = r.previous != null ? r.health - r.previous : null;
|
|
336
|
-
return kpi(
|
|
337
|
-
}).join('')}</div>${Object.keys(FINDING_KINDS).filter((t) => !findings.latest[t]).length ? `<p class="muted small"
|
|
338
|
-
<section class="card"><h3
|
|
339
|
-
<p class="muted small"
|
|
351
|
+
return kpi(kindName(t), `${r.health}/100`, `${badge(healthSeverity(r.health), healthLabel(r.health))} ${diff != null ? `<span class="num">${diff >= 0 ? '+' : ''}${diff}</span>` : ''}<br><a href="docs/findings/${e(t)}/latest.md" target="_blank" rel="noopener">${L('dashboard.findingsLine', { date: e(r.date), findings: r.findings ?? '-' })}${r.critical ? L('dashboard.findingsCritical', { critical: r.critical }) : ''}</a>`, progressBar(r.health, 100, healthSeverity(r.health)));
|
|
352
|
+
}).join('')}</div>${Object.keys(FINDING_KINDS).filter((t) => !findings.latest[t]).length ? `<p class="muted small">${L('dashboard.noReportYet')} ${Object.keys(FINDING_KINDS).filter((t) => !findings.latest[t]).map((t) => `<code>/findings-${t}</code>`).join(' ')}</p>` : ''}` : empty(L('dashboard.noFindingReports'), '/findings-tech')}</section>
|
|
353
|
+
<section class="card"><h3>${L('dashboard.reportHistory')}</h3>${findings.rows.length ? `<div class="tw"><table><thead><tr><th>${L('dashboard.th.date')}</th><th>${L('dashboard.th.kind')}</th><th>${L('dashboard.th.health')}</th><th>${L('dashboard.th.change')}</th><th>${L('dashboard.th.finding')}</th><th>${L('dashboard.th.critical')}</th><th>${L('dashboard.th.report')}</th></tr></thead><tbody>${[...findings.rows].reverse().slice(0, 30).map((r) => `<tr><td class="num nw">${e(r.date)}</td><td>${e(kindName(r.kind))}</td><td>${badge(healthSeverity(r.health), `${r.health}`)}</td><td class="num">${r.previous != null ? `${r.health - r.previous >= 0 ? '+' : ''}${r.health - r.previous}` : '-'}</td><td class="num">${r.findings ?? '-'}</td><td class="num">${r.critical || '-'}</td><td>${md(r.report)}</td></tr>`).join('')}</tbody></table></div>` : empty(L('dashboard.noReports'))}
|
|
354
|
+
<p class="muted small">${L('dashboard.scoringNote')}</p></section>`;
|
|
340
355
|
|
|
341
356
|
const TABS = [
|
|
342
|
-
['genel', '
|
|
343
|
-
['kurul', '
|
|
344
|
-
['urun', '
|
|
357
|
+
['genel', L('dashboard.tabOverview'), overview], ['yol', L('dashboard.tabRoadmap'), roadmapSection], ['kararlar', L('dashboard.tabDecisions'), decisionTable],
|
|
358
|
+
['kurul', L('dashboard.board'), boardSection], ['bulgular', L('dashboard.tabFindings'), findingsSection], ['kalite', L('dashboard.tabQuality'), quality], ['aktivite', L('dashboard.tabActivity'), activity],
|
|
359
|
+
['urun', L('dashboard.tabProduct'), product], ['operasyon', L('dashboard.tabOperations'), operations], ['limit', L('dashboard.tabLimit'), limitSection],
|
|
345
360
|
];
|
|
346
361
|
|
|
347
|
-
const next = d.next[0] ?? (d.kitPending ? '
|
|
362
|
+
const next = d.next[0] ?? (d.kitPending ? L('dashboard.startIdeaCode') : '-');
|
|
348
363
|
const html = `<!doctype html>
|
|
349
|
-
<html lang="
|
|
350
|
-
<title>${e(d.project)} ·
|
|
364
|
+
<html lang="${e(lang)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
365
|
+
<title>${e(d.project)} · ${e(L('dashboard.title'))}</title>
|
|
351
366
|
<style>
|
|
352
367
|
:root{--bg:#f4f6f9;--surface:#fff;--sunk:#eef1f5;--fg:#141b27;--muted:#5b6577;--line:#dde3eb;--accent:#2a78d6;--accent-soft:#e3eefb;
|
|
353
368
|
--good:#0ca30c;--warn:#fab219;--serious:#ec835a;--crit:#d03b3b;--good-bg:#e5f5e5;--warn-bg:#fdf3dc;--crit-bg:#fbe7e7;--info-bg:#e9eef6;
|
|
@@ -435,48 +450,48 @@ footer{color:var(--muted);font-size:.78rem;padding-top:8px}
|
|
|
435
450
|
<header class="top">
|
|
436
451
|
<div><h1>${e(d.project)}</h1></div>
|
|
437
452
|
<div class="meta">
|
|
438
|
-
${badge(health, health === 'good' ? '
|
|
439
|
-
${d.kit?.type ? `<span class="pill">${e(d.kit.type)}${d.kit.addons?.length ? ' + ' + e(d.kit.addons.join(', ')) : ''}</span><span class="pill"
|
|
453
|
+
${badge(health, health === 'good' ? L('dashboard.sevGood') : health === 'warn' ? L('dashboard.healthWarn') : L('dashboard.healthCritical')).replace('class="badge', 'class="badge health')}
|
|
454
|
+
${d.kit?.type ? `<span class="pill">${e(d.kit.type)}${d.kit.addons?.length ? ' + ' + e(d.kit.addons.join(', ')) : ''}</span><span class="pill">${e(L('dashboard.kitVersion', { version: d.kit.version }))}</span>${d.kit.tool?.some((t) => t !== 'claude') ? `<span class="pill">${e(d.kit.tool.join(' + '))}</span>` : ''}` : `<span class="pill">${e(L('dashboard.kitNotInstalled'))}</span>`}
|
|
440
455
|
${git?.branch ? `<span class="pill">⎇ ${e(git.branch)}</span>` : ''}
|
|
441
|
-
<label class="refresh"><input type="checkbox" id="oto" checked>
|
|
456
|
+
<label class="refresh"><input type="checkbox" id="oto" checked> ${e(L('dashboard.autoRefresh'))}</label>
|
|
442
457
|
</div>
|
|
443
458
|
</header>
|
|
444
459
|
|
|
445
460
|
<div class="hero">
|
|
446
461
|
<section class="card" aria-labelledby="simdi">
|
|
447
|
-
<div class="now-l" id="simdi"
|
|
462
|
+
<div class="now-l" id="simdi">${L('dashboard.now')}</div>
|
|
448
463
|
<div class="now-v">${e(a['Aktif faz'] ?? '-')} · ${md(a['Aktif görev'] ?? '-')}</div>
|
|
449
464
|
<div class="now-grid">
|
|
450
|
-
<div><div class="now-l"
|
|
451
|
-
<div><div class="now-l"
|
|
452
|
-
<div><div class="now-l"
|
|
453
|
-
${/başlangıç/i.test(a['Aktif faz'] ?? '') ? `<div><div class="now-l"
|
|
465
|
+
<div><div class="now-l">${L('dashboard.questionMode')}</div><div>${e(a['Soru modu'] ?? '-')}</div></div>
|
|
466
|
+
<div><div class="now-l">${L('dashboard.profile')}</div><div>${e(a['Profil'] ?? '-')}</div></div>
|
|
467
|
+
<div><div class="now-l">${L('dashboard.lastUpdate')}</div><div class="num">${e(a['Son güncelleme'] ?? '-')}</div></div>
|
|
468
|
+
${/başlangıç/i.test(a['Aktif faz'] ?? '') ? `<div><div class="now-l">${L('dashboard.startStep')}</div><div class="num">${e(a['Başlangıç adımı'] ?? '-')}</div></div>` : ''}
|
|
454
469
|
</div>
|
|
455
|
-
<div class="next"><div class="now-l"
|
|
456
|
-
${d.state ? `<div class="now-l" style="margin-top:14px"
|
|
457
|
-
${d.lastSession.length ? `<div class="now-l" style="margin-top:10px"
|
|
470
|
+
<div class="next"><div class="now-l">${L('dashboard.nextStep')}</div><div>${md(next)}${copyBtn(next)}</div>${cmdOf(next) ? `<div class="muted small">${e(L('dashboard.pasteHint'))}</div>` : ''}</div>
|
|
471
|
+
${d.state ? `<div class="now-l" style="margin-top:14px">${L('dashboard.status')}</div><p style="margin-top:4px">${md(d.state)}</p>` : ''}
|
|
472
|
+
${d.lastSession.length ? `<div class="now-l" style="margin-top:10px">${L('dashboard.lastSession')}</div><ul style="margin-top:4px">${d.lastSession.slice(0, 4).map((x) => `<li>${md(x)}</li>`).join('')}</ul>` : ''}
|
|
458
473
|
</section>
|
|
459
474
|
<section class="card" aria-labelledby="dikkat">
|
|
460
|
-
<h3 id="dikkat"
|
|
461
|
-
${alerts.length ? `<ul class="att">${alerts.slice(0, 6).map(alertRow).join('')}</ul>${alerts.length > 6 ? `<details class="daha"><summary>${alerts.length - 6}
|
|
475
|
+
<h3 id="dikkat">${L('dashboard.attention')} <span class="muted small">(${alerts.length})</span></h3>
|
|
476
|
+
${alerts.length ? `<ul class="att">${alerts.slice(0, 6).map(alertRow).join('')}</ul>${alerts.length > 6 ? `<details class="daha"><summary>${L('dashboard.moreItems', { count: alerts.length - 6 })}</summary><ul class="att">${alerts.slice(6).map(alertRow).join('')}</ul></details>` : ''}` : `<p>${badge('good', L('dashboard.allGood'))} ${L('dashboard.allGoodText')}</p>`}
|
|
462
477
|
</section>
|
|
463
478
|
</div>
|
|
464
479
|
|
|
465
480
|
<div class="kpis">
|
|
466
|
-
${kpi('
|
|
467
|
-
${acceptance.total ? kpi('
|
|
468
|
-
${kpi('
|
|
469
|
-
${kpi('
|
|
470
|
-
${kpi('
|
|
471
|
-
${kpi('
|
|
481
|
+
${kpi(L('dashboard.kpiTasks'), `${d.taskCount.done}/${d.taskCount.total}`, L('dashboard.donePct', { percent: percent(d.taskCount.done, d.taskCount.total) }), progressBar(d.taskCount.done, d.taskCount.total))}
|
|
482
|
+
${acceptance.total ? kpi(L('dashboard.acceptanceTests'), `${acceptance.passing}/${acceptance.total}`, L('dashboard.criteriaTested', { count: acceptance.tested }), progressBar(acceptance.passing, acceptance.total, 'good')) : kpi(L('dashboard.kpiMvp'), `${mvpCriteria.done}/${mvpCriteria.total}`, L('dashboard.featuresCount', { count: mvp.length }), progressBar(mvpCriteria.done, mvpCriteria.total))}
|
|
483
|
+
${kpi(L('dashboard.kpiDecisions'), d.decisions.length, L('dashboard.usersAuto', { user: d.decisions.length - autoDecisions, auto: autoDecisions }))}
|
|
484
|
+
${kpi(L('dashboard.board'), d.board.meetings.length, rejectCount ? L('dashboard.rejects', { count: rejectCount }) : L('dashboard.membersCount', { count: d.board.members.length }))}
|
|
485
|
+
${kpi(L('dashboard.kpiCommits7'), git ? last7 : '-', git ? L('dashboard.filesWaiting', { count: git.changed.length }) : L('dashboard.noGit'))}
|
|
486
|
+
${kpi(L('dashboard.kpiLimit'), limit?.five_hour ? `%${Math.round(limit.five_hour.percent)}` : '-', limit?.seven_day ? L('dashboard.weeklyLevel', { percent: Math.round(limit.seven_day.percent), level: limitLevel }) : L('dashboard.unknown'), limit?.five_hour ? progressBar(Math.round(limit.five_hour.percent), 100, limitSeverity(limit.five_hour.percent, 'five_hour')) : '')}
|
|
472
487
|
</div>
|
|
473
488
|
|
|
474
|
-
<nav class="tabs" role="tablist" aria-label="
|
|
489
|
+
<nav class="tabs" role="tablist" aria-label="${e(L('dashboard.tabsLabel'))}">
|
|
475
490
|
${TABS.map(([id, name], i) => `<button class="tab" role="tab" id="t-${id}" aria-controls="p-${id}" aria-selected="${i === 0}" data-id="${id}">${name}</button>`).join('')}
|
|
476
491
|
</nav>
|
|
477
492
|
${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${id}" aria-labelledby="t-${id}" ${i ? 'hidden' : ''}>${content}</div>`).join('\n')}
|
|
478
493
|
|
|
479
|
-
<footer
|
|
494
|
+
<footer>${L('dashboard.footer', { when: new Date().toLocaleString(loc) })}</footer>
|
|
480
495
|
</div>
|
|
481
496
|
<div id="tip" role="tooltip"></div>
|
|
482
497
|
<style>.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}</style>
|
|
@@ -516,7 +531,7 @@ ${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${i
|
|
|
516
531
|
document.querySelectorAll('.kopyala').forEach(function (b) {
|
|
517
532
|
b.addEventListener('click', function () {
|
|
518
533
|
var c = b.dataset.cmd;
|
|
519
|
-
function done(ok) { b.textContent = ok ? '
|
|
534
|
+
function done(ok) { b.textContent = ok ? ${jsStr('dashboard.copied')} : ${jsStr('dashboard.copyFailed')}; setTimeout(function () { b.textContent = ${jsStr('dashboard.copy')}; }, 1500); }
|
|
520
535
|
function fallback() { var t = document.createElement('textarea'); t.value = c; document.body.appendChild(t); t.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (er) {} t.remove(); done(ok); }
|
|
521
536
|
if (navigator.clipboard) navigator.clipboard.writeText(c).then(function () { done(true); }, fallback); else fallback();
|
|
522
537
|
});
|
|
@@ -541,4 +556,4 @@ ${TABS.map(([id, , content], i) => `<div class="panel" role="tabpanel" id="p-${i
|
|
|
541
556
|
// A committed dashboard.html link (to ~/.bashrc, say) must not be written through
|
|
542
557
|
try { if (fs.lstatSync(file('dashboard.html')).isSymbolicLink()) fs.unlinkSync(file('dashboard.html')); } catch { /* not there yet */ }
|
|
543
558
|
fs.writeFileSync(file('dashboard.html'), html);
|
|
544
|
-
if (!quiet) console.log(`dashboard.html
|
|
559
|
+
if (!quiet) console.log(`dashboard.html updated · ${d.taskCount.done}/${d.taskCount.total} tasks · ${d.decisions.length} decisions · ${alerts.length} alerts`);
|