Skip to content

Commit 282893a

Browse files
committed
feat: add service cards, tabbed output, and markdown table
Parse Docker Compose YAML into structured ServiceInfo[] with normalized ports, volumes, networks, environment, and extras. Render per-service cards in a responsive two-column grid. Add tabbed YAML/Cards output switching and a "Copy as Markdown Table" button for sharing in Discord or GitHub support channels. New modules: dom.ts, services.ts, markdown.ts, cards.ts Tests: 150 passing (46 new across 3 test files) Rebrand: Docker Compose Sanitizer → Compose Debugger
1 parent cace7c9 commit 282893a

File tree

12 files changed

+1058
-59
lines changed

12 files changed

+1058
-59
lines changed

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Compose Sanitizer
1+
# Compose Debugger
22

33
## Build
44
npm install && npm run build

README.md

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
# Docker Compose Sanitizer
1+
# Compose Debugger
22

3-
Browser-based tool that redacts sensitive values from Docker Compose YAML while preserving debugging-relevant structure. Paste output from `docker-autocompose`, `docker compose config`, or raw `docker-compose.yml` and get back a version safe to share in support channels.
3+
Browser-based tool for parsing Docker Compose output into structured debugging views. Paste output from `docker-autocompose`, `docker compose config`, or raw `docker-compose.yml` get sanitized YAML, per-service cards, and a markdown table ready for Discord or GitHub support channels.
44

55
**Live:** [bakerboy448.github.io/compose-sanitizer](https://bakerboy448.github.io/compose-sanitizer/)
66

77
## Features
88

9+
### Service Cards
10+
11+
Parsed per-service view showing image, ports, volumes, networks, environment, and extras (restart policy, hostname, depends_on, resource limits). Empty sections are omitted. Switch between YAML and Cards views with the tab bar.
12+
13+
### Markdown Table
14+
15+
One-click "Copy as Markdown Table" generates a table with columns for Service, Image, Ports, Volumes, and Networks — paste directly into Discord or GitHub issues.
16+
917
### Redaction
1018

1119
| What | Example | Result |
1220
|------|---------|--------|
13-
| Sensitive env values | `MYSQL_PASSWORD: supersecret` | `MYSQL_PASSWORD: **REDACTED**` |
21+
| Sensitive env values | `RADARR__POSTGRES__HOST: db.example.com` | `RADARR__POSTGRES__HOST: **REDACTED**` |
1422
| Email addresses | `NOTIFY: [email protected]` | `NOTIFY: **REDACTED**` |
1523
| Home directory paths | `/home/john/media:/tv` | `~/media:/tv` |
1624

@@ -26,7 +34,7 @@ Removes auto-generated fields that clutter compose output:
2634
- S6-overlay env vars (`S6_*`)
2735
- Default runtime values (`ipc: private`, `entrypoint: /init`)
2836
- Locale/path env vars (`PATH`, `LANG`, `XDG_*`)
29-
- Empty maps and arrays
37+
- Empty env values and empty maps/arrays
3038

3139
### Advisories
3240

@@ -44,7 +52,7 @@ Accepts multiple input formats:
4452

4553
### Customizable Patterns
4654

47-
The Settings panel allows custom sensitive patterns (regex) and safe key lists. Configuration persists in `localStorage`.
55+
The Advanced Settings panel allows custom sensitive patterns (regex) and safe key lists. Configuration persists in `localStorage`.
4856

4957
## Self-Hosting
5058

@@ -65,21 +73,23 @@ Single-page app built with Vite + vanilla TypeScript. The build produces one sel
6573

6674
```
6775
src/
68-
patterns.ts # Shared type guards, regex patterns, utility functions
76+
dom.ts # Shared el() DOM helper (no innerHTML)
77+
patterns.ts # Type guards, regex patterns, utility functions
6978
extract.ts # Extracts YAML from mixed console output
7079
redact.ts # Redacts sensitive values, anonymizes paths
7180
noise.ts # Strips auto-generated noise fields
7281
advisories.ts # Detects misconfigurations (hardlinks, etc.)
82+
services.ts # Parses compose object into ServiceInfo[]
83+
markdown.ts # Generates markdown table from ServiceInfo[]
84+
cards.ts # Renders per-service card DOM
7385
config.ts # Customizable patterns, localStorage persistence
7486
clipboard.ts # Copy, PrivateBin, and Gist sharing
7587
disclaimer.ts # PII warnings and legal disclaimers
76-
main.ts # UI assembly and event wiring
88+
main.ts # UI assembly, tabs, and event wiring
7789
```
7890

7991
### Testing
8092

81-
104 tests across 7 test files with >93% statement coverage:
82-
8393
```bash
8494
npm test # Run tests
8595
npx vitest run --coverage # Run with coverage report

index.html

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<meta http-equiv="Content-Security-Policy"
77
content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; base-uri 'none'">
8-
<title>Docker Compose Sanitizer</title>
8+
<title>Compose Debugger</title>
99
<style>
1010
:root {
1111
--bg: #1a1a2e;
@@ -273,6 +273,83 @@
273273

274274
.footer a { color: var(--text); }
275275

276+
/* Tab bar */
277+
.tab-bar {
278+
display: flex;
279+
gap: 0;
280+
margin: 0.75rem 0 0;
281+
border-bottom: 2px solid var(--border);
282+
}
283+
284+
.tab-btn {
285+
padding: 0.5rem 1.2rem;
286+
font-size: 0.9rem;
287+
font-weight: 500;
288+
border: none;
289+
background: transparent;
290+
color: var(--text-muted);
291+
cursor: pointer;
292+
border-bottom: 2px solid transparent;
293+
margin-bottom: -2px;
294+
transition: color 0.15s, border-color 0.15s;
295+
}
296+
297+
.tab-btn:hover { color: var(--text); }
298+
299+
.tab-btn.active {
300+
color: var(--primary);
301+
border-bottom-color: var(--primary);
302+
}
303+
304+
/* Cards */
305+
.cards-container {
306+
display: grid;
307+
grid-template-columns: repeat(auto-fit, minmax(380px, 1fr));
308+
gap: 0.75rem;
309+
max-height: 600px;
310+
overflow-y: auto;
311+
padding: 0.5rem 0;
312+
}
313+
314+
.card {
315+
background: var(--surface);
316+
border: 1px solid var(--border);
317+
border-radius: 6px;
318+
padding: 0.75rem;
319+
}
320+
321+
.card-header {
322+
font-size: 1rem;
323+
font-weight: 700;
324+
margin-bottom: 0.5rem;
325+
padding-bottom: 0.4rem;
326+
border-bottom: 1px solid var(--border);
327+
color: var(--primary);
328+
}
329+
330+
.card-section {
331+
margin-top: 0.4rem;
332+
display: flex;
333+
gap: 0.75rem;
334+
align-items: baseline;
335+
}
336+
337+
.card-label {
338+
font-size: 0.8rem;
339+
font-weight: 600;
340+
color: var(--text-muted);
341+
min-width: 100px;
342+
flex-shrink: 0;
343+
}
344+
345+
.card-value {
346+
font-family: var(--mono);
347+
font-size: 0.8rem;
348+
white-space: pre-wrap;
349+
word-break: break-all;
350+
color: var(--text);
351+
}
352+
276353
.hidden { display: none !important; }
277354
</style>
278355
</head>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "compose-sanitizer",
33
"version": "0.1.0",
4-
"description": "Browser-based Docker Compose sanitizer — redacts secrets, keeps debugging info",
4+
"description": "Browser-based Docker Compose debugger — redacts secrets, shows service cards, and generates markdown tables for support channels",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src/cards.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { el } from './dom'
2+
import type { ServiceInfo } from './services'
3+
4+
function renderListSection(label: string, items: readonly string[]): HTMLElement {
5+
const section = el('div', { className: 'card-section' })
6+
const labelEl = el('div', { className: 'card-label' })
7+
labelEl.textContent = label
8+
section.appendChild(labelEl)
9+
10+
const valueEl = el('div', { className: 'card-value' })
11+
valueEl.textContent = items.join('\n')
12+
section.appendChild(valueEl)
13+
14+
return section
15+
}
16+
17+
function renderMapSection(label: string, entries: ReadonlyMap<string, string>): HTMLElement {
18+
const section = el('div', { className: 'card-section' })
19+
const labelEl = el('div', { className: 'card-label' })
20+
labelEl.textContent = label
21+
section.appendChild(labelEl)
22+
23+
const valueEl = el('div', { className: 'card-value' })
24+
const lines = Array.from(entries).map(([k, v]) => `${k}=${v}`)
25+
valueEl.textContent = lines.join('\n')
26+
section.appendChild(valueEl)
27+
28+
return section
29+
}
30+
31+
function renderCard(service: ServiceInfo): HTMLElement {
32+
const card = el('div', { className: 'card' })
33+
34+
const header = el('div', { className: 'card-header' })
35+
header.textContent = service.name
36+
card.appendChild(header)
37+
38+
// Image (always shown if present)
39+
if (service.image) {
40+
const section = el('div', { className: 'card-section' })
41+
const label = el('div', { className: 'card-label' })
42+
label.textContent = 'Image'
43+
section.appendChild(label)
44+
const value = el('div', { className: 'card-value' })
45+
value.textContent = service.image
46+
section.appendChild(value)
47+
card.appendChild(section)
48+
}
49+
50+
if (service.ports.length > 0) {
51+
card.appendChild(renderListSection('Ports', service.ports))
52+
}
53+
54+
if (service.volumes.length > 0) {
55+
card.appendChild(renderListSection('Volumes', service.volumes))
56+
}
57+
58+
if (service.networks.length > 0) {
59+
card.appendChild(renderListSection('Networks', service.networks))
60+
}
61+
62+
if (service.environment.size > 0) {
63+
card.appendChild(renderMapSection('Environment', service.environment))
64+
}
65+
66+
// Extras rendered as individual labeled fields
67+
for (const [key, value] of service.extras) {
68+
const section = el('div', { className: 'card-section' })
69+
const label = el('div', { className: 'card-label' })
70+
label.textContent = key
71+
section.appendChild(label)
72+
const valueEl = el('div', { className: 'card-value' })
73+
valueEl.textContent = value
74+
section.appendChild(valueEl)
75+
card.appendChild(section)
76+
}
77+
78+
return card
79+
}
80+
81+
export function renderCards(services: readonly ServiceInfo[]): HTMLElement {
82+
const container = el('div', { className: 'cards-container' })
83+
for (const service of services) {
84+
container.appendChild(renderCard(service))
85+
}
86+
return container
87+
}

src/dom.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
export function el<K extends keyof HTMLElementTagNameMap>(
2+
tag: K,
3+
attrs?: Record<string, string>,
4+
children?: (HTMLElement | string)[],
5+
): HTMLElementTagNameMap[K] {
6+
const element = document.createElement(tag)
7+
if (attrs) {
8+
for (const [key, value] of Object.entries(attrs)) {
9+
if (key === 'className') {
10+
element.className = value
11+
} else {
12+
element.setAttribute(key, value)
13+
}
14+
}
15+
}
16+
if (children) {
17+
for (const child of children) {
18+
if (typeof child === 'string') {
19+
element.appendChild(document.createTextNode(child))
20+
} else {
21+
element.appendChild(child)
22+
}
23+
}
24+
}
25+
return element
26+
}

0 commit comments

Comments
 (0)