แดชบอร์ด Docker ในแผงข้าง Claude Code: container ที่รัน/ตาย CPU แรม พอร์ต ดู log สั่ง start/stop/restart และดู Pod ของ Kubernetes ที่มีปัญหา

แดชบอร์ด Docker ในแผงข้าง Claude Code: container ที่รันอยู่และที่พัง (health, CPU, แรม, พอร์ต, กลุ่ม compose) รีเฟรชทุก 10 วินาทีและเด้งเตือนเมื่อ container ตายหรือ unhealthy ระหว่างเปิดแผง ดู log สั่ง start/stop/restart ให้ Claude อ่าน log แล้วบอกสาเหตุ และดู Pod ของ Kubernetes ที่พัง (CrashLoopBackOff, restart บ่อย)
| คำสั่ง | ทำอะไร | ||
|---|---|---|---|
/docker | แดชบอร์ด (เฉพาะตัวที่รัน/มีปัญหา) | ||
/docker all | ทุกตัว รวมที่หยุดปกติ | ||
/docker logs <ชื่อ> [บรรทัด] | log ล่าสุด | ||
/docker why <ชื่อ> | ให้ Claude อ่าน log แล้วบอกสาเหตุ | ||
| `/docker start\ | stop\ | restart <ชื่อ>` | สั่ง container (stop ถามก่อน) |
/docker pods | Pod ของ Kubernetes ที่มีปัญหา |
/plugin install docker-pilot-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ต้องมี Docker (และ kubectl สำหรับ /docker pods) ชื่อ container พิมพ์บางส่วนได้
hooks/register.tsx 272 lines1/**
2 * ============================================================================
3 * MOD 55: Docker Pilot TH - กัปตันเรือ Container (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: Docker Desktop และ k9s แต่ย่อให้อยู่ข้างแชต
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. /docker - แดชบอร์ด container ในแผง: สถานะ, health, CPU, แรม, พอร์ต (รีเฟรชทุก 10 วินาที)
9 * 2. แจ้งเตือนเมื่อ container ที่เคยรันอยู่ตาย หรือกลายเป็น unhealthy ระหว่างเปิดแผง
10 * 3. /docker logs <ชื่อ> - log ล่าสุด, /docker start|stop|restart <ชื่อ> (stop ถามก่อน)
11 * 4. /docker pods - Pod ของ Kubernetes ที่พัง (CrashLoopBackOff, Error, restart บ่อย)
12 * 5. /docker why <ชื่อ> - ให้ Claude อ่าน log แล้วบอกว่าทำไมพัง
13 *
14 * เรียกคำสั่ง docker / kubectl ผ่าน $.process.run (argv ล้วน ชื่อ container ส่งเป็น argument)
15 */
16
17import type { EngineInterface, Register, Timer } from 'claude-code'
18
19interface Container {
20 id: string
21 name: string
22 image: string
23 state: string // running | exited | restarting | paused | created | dead
24 status: string // "Up 2 hours (healthy)", "Exited (1) 3 minutes ago"
25 ports: string
26 project: string
27 cpu?: string
28 mem?: string
29}
30
31const PANE = 'docker-pilot-th'
32let containers: Container[] = []
33let error = ''
34let updatedAt = 0
35let ticker: Timer | undefined
36let showAll = false
37
38// ==========================================================================
39// บทที่ 1: อ่านสถานะ - docker ps + docker stats แบบ JSON ทีละบรรทัด
40// ==========================================================================
41function jsonLines<T>(text: string): T[] {
42 return text
43 .split('\n')
44 .map(l => {
45 try {
46 return JSON.parse(l) as T
47 } catch {
48 return undefined
49 }
50 })
51 .filter((x): x is T => !!x)
52}
53
54async function refresh($: EngineInterface, withStats: boolean): Promise<void> {
55 const ps = await $.process.run(['docker', 'ps', '-a', '--format', '{{json .}}'], { timeoutMs: 20_000 }).catch(() => undefined)
56 if (!ps || ps.exitCode !== 0) {
57 error = !ps ? 'ไม่พบคำสั่ง docker บนเครื่องนี้' : /daemon|pipe|connect/i.test(ps.stderr) ? 'Docker ยังไม่ได้เปิด (เปิด Docker Desktop ก่อน)' : ps.stderr.trim().slice(0, 200)
58 containers = []
59 return
60 }
61 error = ''
62 const rows = jsonLines<Record<string, string>>(ps.stdout)
63 const next: Container[] = rows.map(r => ({
64 id: r.ID ?? '',
65 name: r.Names ?? '',
66 image: r.Image ?? '',
67 state: r.State ?? '',
68 status: r.Status ?? '',
69 ports: (r.Ports ?? '').replace(/0\.0\.0\.0:|\[::\]:|:::/g, '').split(', ').filter((p, i, a) => p && a.indexOf(p) === i).join(', '),
70 project: /com\.docker\.compose\.project=([^,]+)/.exec(r.Labels ?? '')?.[1] ?? '',
71 }))
72 if (withStats && next.some(c => c.state === 'running')) {
73 const st = await $.process.run(['docker', 'stats', '--no-stream', '--format', '{{json .}}'], { timeoutMs: 20_000 }).catch(() => undefined)
74 for (const s of jsonLines<Record<string, string>>(st?.stdout ?? '')) {
75 const c = next.find(x => x.id === s.ID)
76 if (c) Object.assign(c, { cpu: s.CPUPerc, mem: (s.MemUsage ?? '').split(' / ')[0] })
77 }
78 }
79 // เทียบกับรอบก่อน: ตัวที่เพิ่งตาย / เพิ่ง unhealthy
80 for (const c of next) {
81 const before = containers.find(x => x.id === c.id)
82 if (!before) continue
83 if (before.state === 'running' && c.state !== 'running') $.ui.toast(`💥 ${c.name} หยุดทำงาน: ${c.status}`, { timeoutMs: 15_000 })
84 else if (!/unhealthy/.test(before.status) && /unhealthy/.test(c.status)) $.ui.toast(`🤒 ${c.name} unhealthy`, { timeoutMs: 15_000 })
85 }
86 containers = next
87 updatedAt = await $.clock.now()
88}
89
90function icon(c: Container): string {
91 if (/unhealthy/.test(c.status)) return '🤒'
92 if (c.state === 'running') return /healthy/.test(c.status) ? '💚' : '🟢'
93 if (c.state === 'restarting') return '🔄'
94 if (c.state === 'paused') return '⏸️'
95 return /Exited \(0\)/.test(c.status) ? '⚪' : '🔴'
96}
97
98function problem(c: Container): boolean {
99 return /unhealthy/.test(c.status) || c.state === 'restarting' || c.state === 'dead' || (c.state === 'exited' && !/Exited \(0\)/.test(c.status))
100}
101
102function visible(): Container[] {
103 return showAll ? containers : containers.filter(c => c.state === 'running' || c.state === 'restarting' || problem(c))
104}
105
106function table(list: Container[]): string {
107 if (list.length === 0) return 'ไม่มี container'
108 return list.map(c => `${icon(c)} ${c.name.slice(0, 32).padEnd(32)} ${c.status.slice(0, 26).padEnd(26)} ${(c.cpu ?? '').padStart(6)} ${(c.mem ?? '').padStart(9)} ${c.ports}`).join('\n')
109}
110
111function startTicker($: EngineInterface): void {
112 ticker ??= $.clock.every(10_000, () => {
113 void refresh($, true).then(() => $.ui.invalidate('ui.render'))
114 })
115}
116
117// ==========================================================================
118// บทที่ 2: Kubernetes - หา Pod ที่มีปัญหา
119// ==========================================================================
120interface PodList {
121 items: { metadata: { name: string; namespace: string }; status: { phase: string; containerStatuses?: { restartCount: number; state: { waiting?: { reason?: string }; terminated?: { reason?: string } } }[] } }[]
122}
123
124async function pods($: EngineInterface): Promise<string> {
125 const r = await $.process.run(['kubectl', 'get', 'pods', '-A', '-o', 'json', '--request-timeout=10s'], { timeoutMs: 20_000 }).catch(() => undefined)
126 if (!r) return '❌ ไม่พบคำสั่ง kubectl'
127 if (r.exitCode !== 0) {
128 if (/connect|refused|no such host|localhost:8080/i.test(r.stderr)) return '☸️ ยังไม่มี Kubernetes cluster ที่ต่อได้ (เปิด Kubernetes ใน Docker Desktop หรือตั้งค่า kubeconfig)'
129 return `❌ ต่อ cluster ไม่ได้: ${r.stderr.trim().split('\n').pop()?.slice(0, 200)}`
130 }
131 const list = (JSON.parse(r.stdout) as PodList).items
132 const rows = list.map(p => {
133 const cs = p.status.containerStatuses ?? []
134 const reason = cs.map(c => c.state.waiting?.reason ?? c.state.terminated?.reason).find(Boolean) ?? p.status.phase
135 const restarts = cs.reduce((s, c) => s + c.restartCount, 0)
136 return { name: `${p.metadata.namespace}/${p.metadata.name}`, reason, restarts, bad: !['Running', 'Succeeded', 'Completed'].includes(reason) || restarts >= 5 }
137 })
138 const bad = rows.filter(p => p.bad)
139 return [`☸️ Pod ทั้งหมด ${rows.length} · มีปัญหา ${bad.length}`, ...bad.slice(0, 30).map(p => ` 🔴 ${p.name.padEnd(50)} ${p.reason.padEnd(18)} restart ${p.restarts}`), bad.length ? '\nดู log: kubectl logs -n <ns> <pod> --previous' : ' ✅ ไม่มี Pod ที่มีปัญหา'].join('\n')
140}
141
142function find(name: string): Container | undefined {
143 return containers.find(c => c.name === name) ?? containers.find(c => c.id.startsWith(name)) ?? containers.find(c => c.name.includes(name))
144}
145
146async function logs($: EngineInterface, c: Container, lines: number): Promise<string> {
147 const r = await $.process.run(['docker', 'logs', '--tail', String(lines), '--timestamps', c.id], { timeoutMs: 20_000 }).catch(() => undefined)
148 return `${r?.stdout ?? ''}${r?.stderr ?? ''}`.trim()
149}
150
151const HELP = `🐳 Docker Pilot TH (Mod 55)
152========================================
153/docker แดชบอร์ด (แสดงเฉพาะตัวที่รัน/มีปัญหา)
154/docker all แสดงทุกตัว รวมที่หยุดปกติ
155/docker logs <ชื่อ> [บรรทัด] log ล่าสุด (ค่าเริ่มต้น 40)
156/docker why <ชื่อ> ให้ Claude อ่าน log แล้วบอกสาเหตุ
157/docker start|stop|restart <ชื่อ>
158/docker pods Pod ของ Kubernetes ที่มีปัญหา
159ชื่อพิมพ์บางส่วนได้ เช่น /docker logs db`
160
161export const register: Register = on => {
162 on('session.start', async ($, e, next) => {
163 await $.command.register({ name: 'docker', description: 'แดชบอร์ด Docker: container, CPU, แรม, log, start/stop และ Pod ที่พัง (/docker help)', argumentHint: 'all|logs|why|start|stop|restart <ชื่อ>|pods' })
164 return next(e)
165 })
166
167 // ==========================================================================
168 // บทที่ 3: แผงแดชบอร์ด
169 // ==========================================================================
170 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
171 const { Box, Text } = $.ui.resolve(e)
172 const list = visible()
173 const running = containers.filter(c => c.state === 'running').length
174 const bad = containers.filter(problem).length
175 return (
176 <Box flexDirection="column">
177 {error ? (
178 <Text color="#e53935">❌ {error}</Text>
179 ) : (
180 <Text>
181 🐳 รัน <Text color="#43a047">{running}</Text> · มีปัญหา <Text color={bad ? '#e53935' : undefined}>{bad}</Text> · ทั้งหมด {containers.length}
182 <Text dimColor> · อัปเดต {updatedAt ? new Date(updatedAt).toLocaleTimeString('th-TH') : '-'}</Text>
183 </Text>
184 )}
185 {list.map(c => (
186 <Box flexDirection="column" marginTop={1}>
187 <Text bold color={problem(c) ? '#e53935' : undefined}>
188 {icon(c)} {c.name}
189 {c.project ? <Text dimColor> ({c.project})</Text> : null}
190 </Text>
191 <Text dimColor>
192 {' '}
193 {c.status}
194 {c.cpu ? ` · CPU ${c.cpu} · RAM ${c.mem}` : ''}
195 {c.ports ? ` · ${c.ports}` : ''}
196 </Text>
197 </Box>
198 ))}
199 {!showAll && containers.length > list.length && <Text dimColor>ซ่อน {containers.length - list.length} ตัวที่หยุดปกติ (/docker all)</Text>}
200 </Box>
201 )
202 })
203
204 on('ui.close', async ($, e, next) => {
205 if (e.id === PANE) {
206 ticker?.cancel()
207 ticker = undefined
208 }
209 return next(e)
210 })
211
212 on('command.run', { command: 'docker' }, async ($, e) => {
213 const [sub = '', name = '', extra = ''] = e.args.trim().split(/\s+/).filter(Boolean)
214 switch (sub) {
215 case '':
216 case 'all': {
217 showAll = sub === 'all'
218 await refresh($, true)
219 await $.ui.open({ id: PANE, title: '🐳 Docker' })
220 startTicker($)
221 $.ui.invalidate('ui.render')
222 return { text: error ? `❌ ${error}` : table(visible()) }
223 }
224 case 'pods':
225 return { text: await pods($) }
226 case 'logs':
227 case 'why':
228 case 'start':
229 case 'stop':
230 case 'restart': {
231 if (!name) return { text: `วิธีใช้: /docker ${sub} <ชื่อ container>` }
232 await refresh($, false)
233 if (error) return { text: `❌ ${error}` }
234 const c = find(name)
235 if (!c) return { text: `❌ ไม่พบ container "${name}"` }
236 if (sub === 'logs') {
237 const n = Math.min(500, Math.max(1, Number(extra) || 40))
238 return { text: `📜 ${c.name} (${c.status})\n${(await logs($, c, n)) || '(ไม่มี log)'}` }
239 }
240 if (sub === 'why') {
241 const text = await logs($, c, 120)
242 const system = 'คุณเป็นวิศวกร DevOps อ่าน log ของ Docker container แล้วอธิบายเป็นภาษาไทยไม่เกิน 6 บรรทัด: สถานะตอนนี้, สาเหตุที่น่าจะเป็น (อ้างบรรทัด log), สิ่งที่ควรลองทำ ห้ามเดาเกินข้อมูล'
243 for (const model of ['haiku', await $.session.model()]) {
244 const r = await $.model.complete({ model, system, prompt: `container: ${c.name}\nimage: ${c.image}\nstatus: ${c.status}\n\nlog:\n${text.slice(-8000)}`, maxTokens: 500 }).catch(() => undefined)
245 if (r?.isAnswered) return { text: `🩺 ${c.name}\n\n${r.text.trim()}` }
246 }
247 return { text: '❌ ถาม Claude ไม่สำเร็จ' }
248 }
249 if (sub === 'stop') {
250 const a = await $.ui.ask(`หยุด container ${c.name} ไหม?`, ['หยุดเลย', 'ยกเลิก']).catch(() => '')
251 if (a !== 'หยุดเลย') return { text: 'ยกเลิก' }
252 }
253 const r = await $.process.run(['docker', sub, c.id], { timeoutMs: 60_000 }).catch(() => undefined)
254 await refresh($, false)
255 $.ui.invalidate('ui.render')
256 return { text: r?.exitCode === 0 ? `✅ ${sub} ${c.name} แล้ว → ${find(c.name)?.status ?? ''}` : `❌ ${sub} ไม่สำเร็จ: ${r?.stderr.trim() ?? ''}` }
257 }
258 default:
259 return { text: HELP }
260 }
261 })
262}
263
264/**
265 * ========================================================================
266 * การบ้าน / ต่อยอด
267 * ========================================================================
268 * 1. จัดกลุ่มตาม docker compose project แล้วสั่ง up/down ทั้งกลุ่ม
269 * 2. วาดกราฟ CPU ย้อนหลังด้วยตัวอักษร ▁▂▃▅▇
270 * 3. ส่งแจ้งเตือนไปมือถือเมื่อ container ตาย (ต่อกับ Mod 22)
271 */
272