Backend (crates/ericrfb-proxy):
- Session cookie now persisted in AppState for device API calls
- New kvm.rs with three REST endpoints:
GET /api/kvm/ports — scrapes kvm.asp, returns port config as JSON
PUT /api/kvm/ports — saves port names, hotkeys, visibility, count
POST /api/kvm/switch — switches active KVM port via home2.asp
- HTML scraping extracts form values from predictable firmware HTML
Frontend (crates/ericrfb-frontend):
- New shell.ts: sidebar navigation with page routing pattern
(Console, Ports — extensible for Virtual Media, Users, etc.)
- Console refactored into pages/console.ts with mount/unmount lifecycle
- Port switcher dropdown in toolbar (fetches port list, switches on change)
- WebSocket auto-reconnects after port switch
- New pages/ports.ts: editable port configuration table
- Port count, key pause duration, per-port name/hotkey/show-in-console
- Save, reload, and per-port switch buttons
- Active port highlighted
- Dark theme sidebar with active state indicators
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The mapping was built assuming keynr followed physical keyboard order
with Escape=0. In reality, KeyTranslator.java maps Java VK_* codes to
keynr values with a different layout:
- keynr 0 = Backquote (not Escape)
- keynr 59 = Escape
- keynr 27 = Enter (not 40)
- keynr 40 = Backslash (not 42)
This caused the number row and QWERTY row to be off by 1 (Escape
was inserted at position 0, pushing everything). The home row
(CapsLock=28, A=29...L=37) happened to align by coincidence, which
is why 'g'(33) worked but 'r'(should be 18, was 19=T) didn't.
Also fixes: F1-F12 off by 1, PrintScreen/ScrollLock/Pause off by 1,
numpad operator keys swapped with numpad 7/8/9.
Corrected both Rust (input.rs) and TypeScript (input.ts) mappings
to match the authoritative KeyTranslator.java VK→keynr table.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When pal_selector=0 (no predefined palette), the Java code reads two
separate full bytes for the 2-color palette (line 421-422 of
ByteColorRFBRenderer.java). We were reading one packed byte and
splitting into nibbles, consuming 1 byte too few and misaligning all
subsequent reads including the zlib compressed data.
This caused "deflate decompression error" on stream 1 because the
zlib header (78 da) was offset by 1 byte.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
codec/iip.rs:
- TileCache: (fb_width/16 × fb_height/16) tiles, 8 versions × 256 bytes
each, matching t.java's (8, 16*16) allocation
- TileEntry: versioned read/write at byte offsets within tile data
- decode_iip() handles all 4 modes from the control byte:
- Mode 0/12 (cache-read): tile control bytes select which cached
version of each 16x16 tile to display, no new pixel data on wire
- Mode 4 (write-only): Tight-decoded pixel data written to cache
- Mode 8 (update+read): conditionally writes new data to cache
(bit 7 of control byte = 0 means update), then reads from cache
- Tile control bytes compressed via zlib (varint length) when >= 12
- Sub-types 1-4/8 map to bit-depths 1/2/4/4/8 bpp
- Cache resized on framebuffer resize (ModeChange msg 128)
Wired into session dispatch for encoding 9. Not advertised in default
encoding list — only active if explicitly requested.
codec/tight.rs: made get_or_init() pub for IIP's zlib access.
39 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Frontend reconnection:
- WebSocket auto-reconnects with exponential backoff (1s → 30s)
- Re-authenticates with OmniView to get fresh APPLET_ID on reconnect
- Credentials stored in sessionStorage for automatic re-login
- Status bar shows connection state and reconnect countdown
- Disconnect button returns to login screen
Encoding 10 (Raw with tile interleave):
- codec/raw_tile.rs: decodes encoding 10 per ByteColorRFBRenderer.for()
- Flag byte bit 0 selects plain raw vs 16x16 tile-interleaved data
- Deinterleave handles edge tiles smaller than 16x16
- Wired into session dispatch
- 2 unit tests
39 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Simplified deploy steps — rsync directly to final paths using
sudo rsync on the remote instead of temp-file staging.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
publish.yml — triggered on push to main, two parallel jobs:
frontend:
- Builds Vite frontend (fnm + npm ci + npm run build)
- Rsyncs dist/ to gitea_ci@UI_HOST:UI_PATH/
- Rsyncs nginx config to UI_HOST, creates sites-enabled symlink,
runs nginx -t && systemctl reload nginx
backend:
- Builds release binary (cargo build --release -p ericrfb-proxy)
- Stops blekin.service on WS_HOST
- Rsyncs binary to WS_HOST:/usr/local/bin/ericrfb-proxy via sudo rsync
- Rsyncs systemd unit to WS_HOST:/etc/systemd/system/blekin.service
- Enables and starts the service
asset/nginx/blekin.kosherinata.internal.conf:
- Serves static frontend from UI_PATH
- Reverse proxies /api/ to frootmig:3000 with WebSocket upgrade
- 24h read/send timeouts for long-lived KVM sessions
asset/systemd/blekin.service:
- Runs ericrfb-proxy with BLEKIN_HOST=10.3.0.130
- Restart on failure with 5s backoff
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
codec/hextile.rs:
- Full Hextile (encoding 5) decoder per ByteColorRFBRenderer.int()
- Handles: Raw tiles, BackgroundSpecified, ForegroundSpecified,
AnySubrects, SubrectsColoured flags
- Background/foreground colors persist across tiles
- 4 unit tests covering all subencoding paths
framebuffer.rs:
- Added fill_rect() for Hextile background/subrect fills
session.rs:
- Wired Hextile encoding 5 into the rect dispatch
examples/record.rs:
- 30-second (configurable) recording session
- Saves 1 PNG per second to out/ directory
- Requests encodings [5, 1, 0] (Hextile, CopyRect, Raw)
- Tested against real OmniView: 10 frames in 10s, no errors
27 tests passing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The OmniView defaults to 16bpp when no SetPixelFormat is sent.
The Java applet sends SetPixelFormat (msg type 0) at
ByteColorRFBRenderer.new() line 76 to request 8bpp RGB332
(bpp=8, depth=8, true_color, red_max=7, green_max=7, blue_max=3,
shifts 0/3/6). Without this, pixel data arrives as 16bpp pairs
that produce green stripe artifacts when interpreted as 8bpp.
Verified: snapshot now matches the reference screenshot from
http://10.3.0.130/screenshot.jpg (dark screen with "Free" label).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes CI fmt check failure — rustfmt wants multi-line assert_eq! for
long struct literals and the varint roundtrip assertion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three parallel jobs on the rust runner:
- cargo check --workspace
- rustfmt --check on all .rs files
- cargo clippy --workspace -- -D warnings
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cross-referenced every wire-format claim against rc-src and corrected
several errors that would have caused stream desync during implementation:
- rect header is 4×u16 + i32 (12 bytes), not varints + u8
- ping payload is i32 (4 bytes), not 1 byte
- PointerEvent is 8 bytes (includes trailing extra_u16)
- ServerInit is 19 bytes, not 16
- pixel-format struct is variable-length, not fixed 25 bytes
- string primitive uses modified-UTF-8, not ISO-8859-1
- bandwidth probe is read-only (no echo response)
- Phase 1 uses sync Read traits (async deferred to Phase 2)
- corrected all stale line-number references in ac.java/aw.java
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>