A focused Docker Compose management web application.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: basic logs terminal

Brooke 17ac2c3e 8c24d967

+113 -33
+3 -2
packages/node/src/api/realtime/logs.rs
··· 4 4 5 5 use std::convert::Infallible; 6 6 7 - use base64::prelude::*; 7 + use base64::Engine; 8 + use base64::engine::general_purpose::STANDARD; 8 9 use eyre::Context; 9 10 use futures_util::StreamExt; 10 11 use salvo::{ ··· 46 47 47 48 /// Creates a Server-Sent Event from a chunk of log bytes. 48 49 fn create_event(bytes: &[u8]) -> Result<SseEvent, Infallible> { 49 - let encoded = BASE64_STANDARD.encode(bytes); 50 + let encoded = STANDARD.encode(bytes); 50 51 return Ok(SseEvent::default().text(encoded)); 51 52 }
+1
packages/node/src/core/engine.rs
··· 82 82 let mut child = Command::new("docker") 83 83 .current_dir(path) 84 84 .arg("compose") 85 + .args(["--ansi", "always"]) 85 86 .args(args) 86 87 .stdout(Stdio::piped()) 87 88 .stderr(Stdio::piped())
+20
packages/node/src/core/logs.rs
··· 68 68 match result.wrap_err("Error streaming logs for project") { 69 69 Err(err) => error!("{}", eyre_fmt!(err)), 70 70 Ok(bytes) => { 71 + let bytes = normalise_line_endings(&bytes); 72 + 73 + // Update buffer with logs and send to subscribers 71 74 buffer.write().await.extend_from_slice(&bytes); 72 75 if channel.send(bytes).is_err() { 73 76 // There are no subscribers, so clean up and stop the worker ··· 102 105 return entry; 103 106 } 104 107 } 108 + 109 + /// Normalises line endings in the given bytes to be CRLF. 110 + fn normalise_line_endings(bytes: &[u8]) -> Bytes { 111 + let mut out = BytesMut::with_capacity(bytes.len()); 112 + 113 + let mut prev = 0u8; 114 + for &b in bytes.iter() { 115 + if b == b'\n' && prev != b'\r' { 116 + out.extend_from_slice(b"\r\n"); 117 + } else { 118 + out.extend_from_slice(&[b]); 119 + } 120 + prev = b; 121 + } 122 + 123 + return out.freeze(); 124 + }
+3
packages/panel/package.json
··· 24 24 "@sveltejs/kit": "^2.16.0", 25 25 "@sveltejs/vite-plugin-svelte": "^5.0.0", 26 26 "@types/node": "^25.3.5", 27 + "@xterm/addon-fit": "^0.11.0", 28 + "@xterm/addon-web-links": "^0.12.0", 29 + "@xterm/xterm": "^6.0.0", 27 30 "melt": "^0.44.0", 28 31 "openapi-fetch": "^0.17.0", 29 32 "openapi-typescript": "^7.13.0",
+1 -1
packages/panel/src/lib/api/index.ts
··· 4 4 import { goto } from "$app/navigation"; 5 5 import { error } from "$lib"; 6 6 7 - export * from "./state.svelte"; 7 + export * from "./realtime.svelte"; 8 8 9 9 const TOKEN_KEY = "luminary-token"; 10 10
packages/panel/src/lib/api/state.svelte.ts packages/panel/src/lib/api/realtime.svelte.ts
+44
packages/panel/src/lib/component/LogTerminal.svelte
··· 1 + <script lang="ts"> 2 + import { parseServerSentEvents, type ServerSentEvent } from "parse-sse"; 3 + import { WebLinksAddon } from "@xterm/addon-web-links"; 4 + import type { Attachment } from "svelte/attachments"; 5 + import { FitAddon } from "@xterm/addon-fit"; 6 + import { Terminal } from "@xterm/xterm"; 7 + import "@xterm/xterm/css/xterm.css"; 8 + import { client } from "$lib/api"; 9 + 10 + let { project }: { project: string } = $props(); 11 + 12 + const terminal: Attachment<HTMLElement> = (el) => { 13 + const terminal = new Terminal(); 14 + const fitAddon = new FitAddon(); 15 + 16 + terminal.loadAddon(new WebLinksAddon()); 17 + terminal.loadAddon(fitAddon); 18 + terminal.open(el); 19 + 20 + fitAddon.fit(); 21 + const observer = new ResizeObserver(() => fitAddon.fit()); 22 + observer.observe(el); 23 + 24 + stream(terminal); 25 + 26 + return () => { 27 + observer.disconnect(); 28 + terminal.dispose(); 29 + }; 30 + }; 31 + 32 + async function stream(terminal: Terminal) { 33 + const { response } = await client.GET("/api/project/{project}/logs", { 34 + params: { path: { project } }, 35 + parseAs: "stream", 36 + }); 37 + 38 + for await (const event of parseServerSentEvents(response) as unknown as AsyncIterable<ServerSentEvent>) { 39 + terminal.write(Uint8Array.from(atob(event.data), (c) => c.charCodeAt(0))); 40 + } 41 + } 42 + </script> 43 + 44 + <div {@attach terminal}></div>
+16 -1
packages/panel/src/routes/(authenticated)/projects/[project]/+page.svelte
··· 2 2 import { faCircleInfo, faGears, faLayerGroup, faPencil } from "@fortawesome/free-solid-svg-icons"; 3 3 import StatusIcon from "$lib/component/StatusIcon.svelte"; 4 4 import Tabs from "$lib/component/Tabs.svelte"; 5 - import StatusTab from "./StatusTab.svelte"; 5 + import StatusTab from "./ProjectStatus.svelte"; 6 6 import { getList } from "$lib/api"; 7 7 import { page } from "$app/state"; 8 8 import Fa from "svelte-fa"; 9 + import LogTerminal from "$lib/component/LogTerminal.svelte"; 9 10 10 11 let project = $derived(getList()[page.params.project!]); 11 12 let { data } = $props(); ··· 35 36 36 37 {#snippet status()} 37 38 <StatusTab {project} /> 39 + <h2>Logs</h2> 40 + <LogTerminal project={project.name} /> 38 41 {/snippet} 39 42 40 43 {#snippet compose()} ··· 44 47 {#snippet variables()} 45 48 <h2>Variables Tab</h2> 46 49 {/snippet} 50 + 51 + <style lang="scss"> 52 + // Modify h2 of all child components 53 + * :global(h2) { 54 + margin-bottom: 5px; 55 + font-size: 16px; 56 + 57 + &:not(:first-child) { 58 + margin-top: 15px; 59 + } 60 + } 61 + </style>
+1 -10
packages/panel/src/routes/(authenticated)/projects/[project]/StatusTab.svelte packages/panel/src/routes/(authenticated)/projects/[project]/ProjectStatus.svelte
··· 56 56 > 57 57 <div class="flexr center gap-10"> 58 58 <Fa icon={faStop} /> 59 - Stop 59 + Stop All 60 60 </div> 61 61 </PromiseButton> 62 62 </div> ··· 148 148 </div> 149 149 150 150 <style lang="scss"> 151 - h2 { 152 - margin-bottom: 5px; 153 - font-size: 16px; 154 - 155 - &:not(:first-child) { 156 - margin-top: 15px; 157 - } 158 - } 159 - 160 151 h3 { 161 152 font-size: 14px; 162 153 }
+24 -19
pnpm-lock.yaml
··· 43 43 '@types/node': 44 44 specifier: ^25.3.5 45 45 version: 25.3.5 46 + '@xterm/addon-fit': 47 + specifier: ^0.11.0 48 + version: 0.11.0 49 + '@xterm/addon-web-links': 50 + specifier: ^0.12.0 51 + version: 0.12.0 52 + '@xterm/xterm': 53 + specifier: ^6.0.0 54 + version: 6.0.0 46 55 melt: 47 56 specifier: ^0.44.0 48 57 version: 0.44.0(@floating-ui/dom@1.7.6)(svelte@5.53.7) ··· 335 344 engines: {node: '>= 10.0.0'} 336 345 cpu: [arm] 337 346 os: [linux] 338 - libc: [glibc] 339 347 340 348 '@parcel/watcher-linux-arm-musl@2.5.6': 341 349 resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} 342 350 engines: {node: '>= 10.0.0'} 343 351 cpu: [arm] 344 352 os: [linux] 345 - libc: [musl] 346 353 347 354 '@parcel/watcher-linux-arm64-glibc@2.5.6': 348 355 resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} 349 356 engines: {node: '>= 10.0.0'} 350 357 cpu: [arm64] 351 358 os: [linux] 352 - libc: [glibc] 353 359 354 360 '@parcel/watcher-linux-arm64-musl@2.5.6': 355 361 resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} 356 362 engines: {node: '>= 10.0.0'} 357 363 cpu: [arm64] 358 364 os: [linux] 359 - libc: [musl] 360 365 361 366 '@parcel/watcher-linux-x64-glibc@2.5.6': 362 367 resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} 363 368 engines: {node: '>= 10.0.0'} 364 369 cpu: [x64] 365 370 os: [linux] 366 - libc: [glibc] 367 371 368 372 '@parcel/watcher-linux-x64-musl@2.5.6': 369 373 resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} 370 374 engines: {node: '>= 10.0.0'} 371 375 cpu: [x64] 372 376 os: [linux] 373 - libc: [musl] 374 377 375 378 '@parcel/watcher-win32-arm64@2.5.6': 376 379 resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} ··· 441 444 resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} 442 445 cpu: [arm] 443 446 os: [linux] 444 - libc: [glibc] 445 447 446 448 '@rollup/rollup-linux-arm-musleabihf@4.59.0': 447 449 resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} 448 450 cpu: [arm] 449 451 os: [linux] 450 - libc: [musl] 451 452 452 453 '@rollup/rollup-linux-arm64-gnu@4.59.0': 453 454 resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} 454 455 cpu: [arm64] 455 456 os: [linux] 456 - libc: [glibc] 457 457 458 458 '@rollup/rollup-linux-arm64-musl@4.59.0': 459 459 resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} 460 460 cpu: [arm64] 461 461 os: [linux] 462 - libc: [musl] 463 462 464 463 '@rollup/rollup-linux-loong64-gnu@4.59.0': 465 464 resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} 466 465 cpu: [loong64] 467 466 os: [linux] 468 - libc: [glibc] 469 467 470 468 '@rollup/rollup-linux-loong64-musl@4.59.0': 471 469 resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} 472 470 cpu: [loong64] 473 471 os: [linux] 474 - libc: [musl] 475 472 476 473 '@rollup/rollup-linux-ppc64-gnu@4.59.0': 477 474 resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} 478 475 cpu: [ppc64] 479 476 os: [linux] 480 - libc: [glibc] 481 477 482 478 '@rollup/rollup-linux-ppc64-musl@4.59.0': 483 479 resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} 484 480 cpu: [ppc64] 485 481 os: [linux] 486 - libc: [musl] 487 482 488 483 '@rollup/rollup-linux-riscv64-gnu@4.59.0': 489 484 resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} 490 485 cpu: [riscv64] 491 486 os: [linux] 492 - libc: [glibc] 493 487 494 488 '@rollup/rollup-linux-riscv64-musl@4.59.0': 495 489 resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} 496 490 cpu: [riscv64] 497 491 os: [linux] 498 - libc: [musl] 499 492 500 493 '@rollup/rollup-linux-s390x-gnu@4.59.0': 501 494 resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} 502 495 cpu: [s390x] 503 496 os: [linux] 504 - libc: [glibc] 505 497 506 498 '@rollup/rollup-linux-x64-gnu@4.59.0': 507 499 resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} 508 500 cpu: [x64] 509 501 os: [linux] 510 - libc: [glibc] 511 502 512 503 '@rollup/rollup-linux-x64-musl@4.59.0': 513 504 resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} 514 505 cpu: [x64] 515 506 os: [linux] 516 - libc: [musl] 517 507 518 508 '@rollup/rollup-openbsd-x64@4.59.0': 519 509 resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} ··· 603 593 604 594 '@types/trusted-types@2.0.7': 605 595 resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} 596 + 597 + '@xterm/addon-fit@0.11.0': 598 + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} 599 + 600 + '@xterm/addon-web-links@0.12.0': 601 + resolution: {integrity: sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==} 602 + 603 + '@xterm/xterm@6.0.0': 604 + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} 606 605 607 606 acorn@8.16.0: 608 607 resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} ··· 1419 1418 undici-types: 7.18.2 1420 1419 1421 1420 '@types/trusted-types@2.0.7': {} 1421 + 1422 + '@xterm/addon-fit@0.11.0': {} 1423 + 1424 + '@xterm/addon-web-links@0.12.0': {} 1425 + 1426 + '@xterm/xterm@6.0.0': {} 1422 1427 1423 1428 acorn@8.16.0: {} 1424 1429