SLOPSHOPPER

docker-pilot-th

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

newpanecommandtoastmodelprocess
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · docker-pilot-th
│ ┃ 🐳 Docker ✕ › fix the failing auth test and add an audit log call │ ┃ 🐳 รัน 0 · มีปัญหา 0 · ทั้งหมด 0 · อัปเดต │ ┃ 08:53:20 ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /docker │ ⎿ docker-pilot-th: ไม่มี container │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 🐳 Docker
🐳 รัน 0 · มีปัญหา 0 · ทั้งหมด 0 · อัปเดต 08:53:20
README

Mod 55: Docker Pilot TH

แดชบอร์ด 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 podsPod ของ Kubernetes ที่มีปัญหา

ติดตั้ง

/plugin install docker-pilot-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. อ่าน docker ps / docker stats แบบ JSON แล้วเทียบกับรอบก่อน
  2. หา Pod ที่มีปัญหาจาก kubectl
  3. แผงแดชบอร์ดที่รีเฟรชเฉพาะตอนเปิด
  4. คำสั่ง /docker

หมายเหตุ

ต้องมี Docker (และ kubectl สำหรับ /docker pods) ชื่อ container พิมพ์บางส่วนได้

Source 1 files
hooks/register.tsx 272 lines
1/**
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