Background rect elements were being inserted inside text elements,
causing invalid SVG structure. Now collect all background rects first,
then render them before the text element for each row.
- Test rect dimensions and positioning
- Test hex colors with/without # prefix
- Test multiple background spans in one row
- Test wide character background width
- Test same-as-terminal-bg optimization
- Test combined foreground and background colors
- Fix hex color conversion for pyte's 256-color/truecolor format (no # prefix)
- Track column count separately from text length for proper wide char alignment
- Add tests for rgb() color format, empty rows, unicode slugify
- Improve test coverage to 80%
Bump version to 0.3.1
Track column count separately from character count to properly
handle wide characters (CJK, emoji) that occupy 2 terminal columns
but have a single character + empty placeholder in pyte buffer.
- Created svg_exporter.py with direct pyte-to-SVG rendering
- Eliminates Rich's export_svg() quirks (clip path count mismatch)
- Added 63 comprehensive tests for SVG exporter
- Removed Rich imports from local_server.py, terminal_session.py,
app_session.py, and cli.py
- Replaced RichHandler with standard logging.basicConfig
- Replaced @rich.repr.auto with standard __repr__ methods
- Rich is no longer directly imported (still transitive via textual-serve)
Bump version to 0.3.0
- Skip empty placeholder cells for wide characters in SVG
- Single redraw on reconnect (integrated into set_terminal_size)
- Sync pyte to PTY size with redraw trigger for screenshots
- Fix extra line in SVG by not adding newline after last row
Bump version to 0.2.10
- Remove separate force_redraw on WebSocket connect
- Integrate size toggle into set_terminal_size for single redraw
- Update test to expect two executor calls (toggle pattern)
- Sync pyte screen to actual PTY size before screenshots
- Toggle terminal size to force full tmux redraw on reconnect
- Query PTY size with TIOCGWINSZ to detect external resizes
Bump version to 0.2.9
- Force terminal redraw on WebSocket reconnect (fixes tmux display)
- Simplify screenshot dimensions (use DEFAULT_TERMINAL_SIZE for new sessions)
- Track last known terminal size for reconnection
- Fix trailing whitespace in tests
Bump version to 0.2.8
Remove width/height query params from screenshot endpoint.
New sessions created by screenshot use DEFAULT_TERMINAL_SIZE.
Existing sessions keep their current size.
- Track last known terminal size in TerminalSession
- Add force_redraw() method that re-sends SIGWINCH to trigger redraw
- Call force_redraw() when WebSocket reconnects to existing session
- Helps tmux and similar apps restore proper display after disconnect
- Fix Ctrl-C to exit immediately by setting exit_event before cleanup
- Filter Docker containers by compose project name to match correct stack
- Derive compose project from manifest directory (matches docker-compose default)
- Improve Docker socket availability check to test actual connectivity
- Add DOCKER_HOST env var support for alternate socket paths
- Better error logging for socket permission issues
Bump version to 0.2.5
- Remove excessive debug logging
- Add single warning when no containers found (with hint about socket mount)
- Use HTTP/1.0 to avoid chunked encoding complexity
- Simplify response parsing
- Refactor into separate methods for cleaner code
- Better handling of chunked transfer encoding
- Add more debug logging to diagnose parsing failures
- Log body preview when JSON not found
Helps diagnose empty sparklines by logging:
- Container discovery results and matches
- Stats request failures
- CPU calculation results
- What containers/services were found vs expected
Server-side:
- Limit SSE notifications to max once per second per route
- Track last notification time per route
Client-side:
- Debounce screenshot refreshes with 2s minimum interval per tile
- Pending refreshes are scheduled if events arrive during debounce window
When terminal resizes, old screenshot content is stale until
the app (tmux etc) re-renders at new dimensions. Clear cache
to force re-capture after resize.
- Pass service names (not slugs) to DockerStatsCollector
- Create slug->name mapping for sparkline lookups
- Stats are stored by service name, looked up by slug
- Add debug logging when no containers found
New features:
- CPU sparklines on dashboard showing 30-minute container history
- Real-time screenshot updates via Server-Sent Events
- Auto-focus terminals on page load
- Tab reuse when clicking dashboard tiles
- pyte-based screenshot rendering with proper ANSI interpretation
- Dirty tracking for efficient screenshot caching
Documentation:
- Updated README with new features and API endpoints
- Added Dashboard Features section
- Documented compose mode CPU sparklines
- Added API endpoints table
Technical improvements:
- Docker stats collection via Unix socket (no new deps)
- SSE endpoint for activity notifications
- Proper pyte-to-Rich color mapping
- Terminal lifecycle race condition fixes
- New /events SSE endpoint pushes activity notifications to browsers
- Dashboard subscribes to SSE stream instead of polling
- Screenshots refresh instantly when terminal activity occurs
- Sparklines still poll every 30s (appropriate for 30min history)
- SSE includes keepalive every 30s and auto-reconnect on error
- Removes inefficient 5s polling; updates only on actual changes
Sparklines:
- Poll interval: 2s -> 10s
- History size: 30 -> 180 readings
- Now shows 30 minutes of CPU history
Screenshots:
- Dashboard refresh interval: 15s -> 5s
- Combined with dirty tracking, updates on activity with 5s cap
- New docker_stats.py module reads container stats from Docker socket
using only asyncio + stdlib (no new dependencies)
- Calculates CPU % from delta of cpu_usage and system_cpu_usage
- Maintains ring buffer of last 30 CPU readings per container
- render_sparkline_svg() generates mini SVG chart from history
- DockerStatsCollector polls containers every 2 seconds
- New /cpu-sparkline.svg endpoint serves sparkline for a container
- Dashboard shows sparkline in tile header next to container name
- Only active in compose mode (--compose-manifest flag)
- Graceful degradation if Docker socket unavailable
Bump version to 0.1.17
- get_screen_state() now returns has_changes flag indicating if screen changed
- pyte's dirty set tracks which rows have been modified since last read
- Screenshot handler returns cached SVG immediately when no changes detected
- Removed _screenshot_last_rendered_activity tracking (replaced by dirty flag)
- Added test for dirty flag behavior
Bump version to 0.1.16
Use tile slug as window name in window.open() so clicking the same
tile twice focuses the existing tab instead of opening a new one.
Changed: window.open(url, '_blank') -> window.open(url, 'webterm-{slug}')
Changes since 0.1.12:
- Fix pyte color name mapping for Rich compatibility
- Use session's actual screen state for screenshots
- Use requested dimensions when creating screenshot sessions
- Fix terminal lifecycle race conditions
1. Lock pyte screen initialization in open() to prevent races with
concurrent _update_screen() calls
2. Reorder session registration: call open() BEFORE adding to
sessions/routes dicts, so sessions are fully initialized before
other code can access them
3. Add clarifying comment that PTY resize completes before pyte resize
These fixes prevent dimension mismatches between PTY and pyte screen
that could cause content wrapping in screenshots.
When creating a new session for screenshot:
1. Use width/height query params instead of hardcoded DISCONNECT_RESIZE
2. Add a small delay (0.5s) after creating session to allow initial output
This ensures new sessions are created with the correct dimensions
matching what the screenshot expects.
The screenshot was creating a new pyte screen with arbitrary dimensions
from query params, but the replay buffer contains ANSI sequences meant
for the session's actual terminal size. This mismatch caused wrapping.
Now we use get_screen_state() which returns the actual screen buffer
from the terminal session's pyte screen, with the correct dimensions.
This ensures the screenshot matches exactly what the terminal rendered.
Pyte uses 'brightblack', 'brightred', etc. but Rich expects
'bright_black', 'bright_red' with underscores. Added PYTE_TO_RICH_COLOR
mapping to translate color names in screenshot rendering.
Test fixes:
- Fix app_session.py to use 'textual-webterm' package name (not 'textual-web')
- Fix CLI version test to not hardcode version number
- Fix static path test to not use removed Path._flavour attribute
Removed unused dependencies:
- xdg
- msgpack
- httpx
All 209 tests pass with 86% coverage.