Skip to content

Log images to server tty - #100

Merged
jayvdb merged 3 commits into
mainfrom
image-logging
Jul 22, 2026
Merged

Log images to server tty#100
jayvdb merged 3 commits into
mainfrom
image-logging

Conversation

@jayvdb

@jayvdb jayvdb commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added optional upload consent controls for workflow data and eye captures.
    • Added periodic and detection-triggered eye captures when consent is granted.
    • Added terminal image thumbnails for uploaded image files.
    • Improved capture visibility with clearer overlays and upload error events.
  • Bug Fixes
    • Reduced stale module loading by forcing fresh wheel downloads.
    • Preserved successful file uploads even when thumbnail rendering fails.
  • Style
    • Improved workflow controls, consent-note layout, and GPU meter positioning.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds dependency-limited terminal image rendering to storage uploads, introduces consent-gated test and eye-capture uploads, adds periodic capture scheduling and failure events, updates browser consent controls, and repositions the GPU meter overlay.

Changes

Storage image rendering

Layer / File(s) Summary
Image renderer and dependency wiring
Cargo.toml, services/storage/Cargo.toml, services/storage/src/lib.rs, services/storage/src/tty_image.rs, CLAUDE.md
Adds the feature-limited image dependency and a hand-written ANSI half-block renderer, with repository guidance on avoiding GPL/LGPL dependencies.
Image upload integration and validation
services/storage/src/routes.rs, services/storage/tests/*
Tracks uploaded bytes, detects image extensions, renders images on the terminal, logs rendering failures, and tests filename detection and upload persistence.

Consented module uploads

Layer / File(s) Summary
Pydata consent upload contract
services/ws-modules/pydata1/*
Passes browser consent into the pydata workflow, conditionally uploads consent_test.txt, logs module versions, and updates package versions.
Eye capture platform bridge
services/ws-modules/pyeye1/pkg/et_ws_pyeye1.js
Adds consent and canvas-capture primitives, uploads PNG captures to storage, refreshes wheel downloads, and outlines overlay text.
Consent-gated capture scheduling
services/ws-modules/pyeye1/pyeye1/eye_detection.py
Adds periodic and indicator-triggered captures, consent checks, non-aborting upload failures, and client-visible error events.
Capture workflow validation
services/ws-modules/pyeye1/tests/*, services/ws-modules/pyeye1/pyproject.toml
Tests consent timing, capture scheduling, retries, combined triggers, and failure reporting while updating the module version.
Browser consent controls
services/ws-server/static/app.js, services/ws-server/static/index.html, services/ws-server/static/style.css
Controls checkbox availability around connection and module execution, emits consent/build events, and updates workflow control layout.

GPU meter positioning

Layer / File(s) Summary
GPU meter overlay placement
services/ws-server/static/meters.js
Positions the GPU meter overlay at the fixed bottom-right of the page and updates its documentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser UI
  participant Adapter as pyeye1 JS adapter
  participant EyeLoop as eye_detection sample_loop
  participant Storage as Storage PUT route
  participant Renderer as tty_image renderer

  Browser->>Adapter: Read upload_consent
  Adapter->>EyeLoop: Provide consent and capture callbacks
  EyeLoop->>EyeLoop: Trigger periodic or indicator capture
  EyeLoop->>Adapter: Call save_eye_capture
  Adapter->>Storage: PUT PNG capture
  Storage->>Renderer: Render recognized image filename
  Renderer-->>Storage: Terminal output or render error
  Storage-->>Adapter: Upload response
  Adapter-->>EyeLoop: Capture success or failure
Loading

Possibly related PRs

  • edge-toolkit/core#91: Introduced the pyeye1 workflow extended here with consent-gated periodic eye capture and uploads.
  • edge-toolkit/core#99: Modified the pyeye1 JavaScript and Python platform bridge extended by these consent and capture changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: uploaded images are rendered to the server TTY.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch image-logging

Comment @coderabbitai help to get the list of available commands.

@deepsource-io

deepsource-io Bot commented Jul 22, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 8d69f15...f037c15 on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

Important

Some issues found as part of this review are outside of the diff in this pull request and aren't shown in the inline review comments due to GitHub's API limitations. You can see those issues on the DeepSource dashboard.

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Coverage  

Code Review Summary

Analyzer Status Updated (UTC) Details
C# Jul 22, 2026 7:39a.m. Review ↗
C & C++ Jul 22, 2026 7:39a.m. Review ↗
Docker Jul 22, 2026 7:39a.m. Review ↗
Java Jul 22, 2026 7:39a.m. Review ↗
JavaScript Jul 22, 2026 7:39a.m. Review ↗
Python Jul 22, 2026 7:39a.m. Review ↗
Rust Jul 22, 2026 7:39a.m. Review ↗
Secrets Jul 22, 2026 7:39a.m. Review ↗
Code coverage Jul 22, 2026 8:05a.m. Review ↗

Code Coverage Summary

Language Line Coverage (New Code) Line Coverage (Overall)
Aggregate
60.9%
58.4%
[▲ up 0.2% from main]
Python
79.5%
84.3%
[▲ up 1.3% from main]
Rust
45.8%
56.6%

➟ Additional coverage metrics may have been reported. See full coverage report ↗


Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@codacy-production

codacy-production Bot commented Jul 22, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 high · 4 minor

Alerts:
⚠ 6 issues (≤ 0 issues of at least minor severity)

Results:
6 new issues

Category Results
Documentation 4 minor
ErrorProne 2 high

View in Codacy

🟢 Metrics 73 complexity · 7 duplication

Metric Results
Complexity 73
Duplication 7

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

platform.send_event(eye_capture_error_event_json(str(exc)))


async def sample_loop(platform) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`sample_loop` has a cyclomatic complexity of 20 with "high" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

.resize_exact(TARGET_COLUMNS, sample_height, image::imageops::FilterType::Triangle)
.to_rgba8();

let mut out = String::new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty call to `new()`


The new() function is used to initialise an object with specific data.
If no arguments are passed, the behaviour is identical to default().

@jayvdb
jayvdb marked this pull request as ready for review July 22, 2026 08:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (1)
services/ws-modules/pydata1/pkg/et_ws_pydata1.js (1)

65-74: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Diagnostic logs the version, but doesn't fix the underlying stale-cache gap.

The new comment explicitly calls out that this is the version fetched from package.json + injected as the wheel filename, so seeing the right number here after a version bump proves the module reloaded fresh (no stale-cache reuse of the package.json/wheel fetches below, neither of which is cache-busted). That's a good diagnostic, but et_ws_pyeye1.js in this same PR fixed the identical stale-cache risk by adding { cache: "no-cache" } to its package.json/wheel fetches, while et_ws_pydata1.js's equivalent fetches (line 66's wheel fetch, line 70's package.json fetch) were left uncached. Consider applying the same fix here instead of only detecting the symptom after the fact.

♻️ Apply the same no-cache fix used in et_ws_pyeye1.js
   const injectWheel = async (wheelName) => {
-    const bytes = new Uint8Array(await fetch(new URL(wheelName, import.meta.url)).then((r) => r.arrayBuffer()));
+    const bytes = new Uint8Array(
+      await fetch(new URL(wheelName, import.meta.url), { cache: "no-cache" }).then((r) => r.arrayBuffer()),
+    );
     pyodide.FS.writeFile(`/tmp/${wheelName}`, bytes);
     pyodide.runPython(`import sys\nsys.path.insert(0, "/tmp/${wheelName}")`);
   };
-  const pkg = await fetch(new URL("package.json", import.meta.url)).then((r) => r.json());
+  const pkg = await fetch(new URL("package.json", import.meta.url), { cache: "no-cache" }).then((r) => r.json());

Also applies to: 120-123

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/ws-modules/pydata1/pkg/et_ws_pydata1.js` around lines 65 - 74,
Update the fetch calls in injectWheel and the package.json loading flow to pass
{ cache: "no-cache" }, matching the existing fix in et_ws_pyeye1.js. Apply this
to both wheel retrieval and package metadata retrieval so version and wheel
updates cannot reuse stale cached responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CLAUDE.md`:
- Around line 1078-1085: The licensing rationale is duplicated across the
repository policy and module documentation. Keep the detailed viuer/ansi_colours
precedent in CLAUDE.md lines 1078-1085 unchanged, and remove the duplicated
dependency-license explanation from services/storage/src/tty_image.rs lines 3-8
while retaining only documentation about the renderer’s behavior.

In `@services/storage/src/routes.rs`:
- Around line 119-126: Update the first-line Rust doc summaries at
services/storage/src/routes.rs:119-126 and
services/storage/tests/image_logging.rs:1-2 so each is a self-contained,
complete sentence ending with a period; remove the mid-sentence wording and
cross-references to tty_image and put_file, while leaving the remaining
documentation unchanged.
- Around line 111-113: Update the image-handling branch around show_image_on_tty
to run terminal rendering in a bounded blocking/background task after
persistence, rather than synchronously on the Actix request worker. Preserve the
existing image detection and info! logging, and propagate render failures to
warn! from the spawned task.

In `@services/storage/src/tty_image.rs`:
- Around line 1-13: Update
config/ast-grep/rules/doc-summary-ends-with-period.yaml by adding every modified
Rust file to its sorted files list: services/storage/src/tty_image.rs (anchor),
services/storage/src/lib.rs, services/storage/src/routes.rs,
services/storage/tests/image_logging.rs, and services/storage/tests/put.rs.
Retain any entries already present and do not modify the Rust implementation
files directly.
- Around line 30-31: Update tty_image::render to check whether stdout is a
terminal before calling image::open; return successfully immediately when
std::io::stdout().is_terminal() is false, preserving the existing image
rendering path for terminal output.
- Around line 31-57: Bound thumbnail resource usage in the image-rendering
function: configure decoder limits before image::open to reject excessively
large dimensions or decoded pixels, and clamp the aspect-derived rows value
before sample_height and resize_exact are used. Preserve the existing minimum
one-row behavior while enforcing a finite maximum that also limits the generated
ANSI output.

In `@services/storage/tests/put.rs`:
- Around line 110-112: Update the first doc-summary line for the image PUT test
to explain that rendering fails because line 126 provides malformed PNG bytes,
not because of terminal availability or viuer behavior. Make the summary a
complete sentence ending with a period, while preserving the remaining
documentation.

In `@services/ws-server/static/meters.js`:
- Around line 254-257: Update the overlay initialization near makeDraggable so
the opposing left/top anchors are cleared when pointer dragging begins, before
pixel left/top values are written. Preserve the fixed bottom-right placement
before the first drag, then remove right/bottom when applying the first drag
position to prevent the overlay from stretching.

---

Nitpick comments:
In `@services/ws-modules/pydata1/pkg/et_ws_pydata1.js`:
- Around line 65-74: Update the fetch calls in injectWheel and the package.json
loading flow to pass { cache: "no-cache" }, matching the existing fix in
et_ws_pyeye1.js. Apply this to both wheel retrieval and package metadata
retrieval so version and wheel updates cannot reuse stale cached responses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72622927-0f9a-4a27-a592-f28d96bd0573

📥 Commits

Reviewing files that changed from the base of the PR and between 8d69f15 and f037c15.

⛔ Files ignored due to path filters (2)
  • Cargo.lock is excluded by !**/*.lock
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (20)
  • CLAUDE.md
  • Cargo.toml
  • services/storage/Cargo.toml
  • services/storage/src/lib.rs
  • services/storage/src/routes.rs
  • services/storage/src/tty_image.rs
  • services/storage/tests/image_logging.rs
  • services/storage/tests/put.rs
  • services/ws-modules/pydata1/pkg/et_ws_pydata1.js
  • services/ws-modules/pydata1/pkg/package.json
  • services/ws-modules/pydata1/pydata1/__init__.py
  • services/ws-modules/pydata1/pyproject.toml
  • services/ws-modules/pyeye1/pkg/et_ws_pyeye1.js
  • services/ws-modules/pyeye1/pyeye1/eye_detection.py
  • services/ws-modules/pyeye1/pyproject.toml
  • services/ws-modules/pyeye1/tests/test_run_workflow.py
  • services/ws-server/static/app.js
  • services/ws-server/static/index.html
  • services/ws-server/static/meters.js
  • services/ws-server/static/style.css

Comment thread CLAUDE.md
Comment on lines +1078 to +1085
Concrete precedent: `viuer` (added for the storage service's tty image-preview feature) pulled in
`ansi_colours` under LGPL-3.0-or-later as an unconditional dependency for its RGB-to-256-color quantization
path. Rather than allow-listing LGPL, the feature was reimplemented as a small hand-rolled ANSI truecolor
renderer in `services/storage/src/tty_image.rs`, built on the `image` crate the workspace already depended on
via `deno_image`/`ws-web-runner` -- truecolor output needs no palette quantization at all, so there was
nothing left for the LGPL dependency to do. If a future dependency's only viable path involves a GPL/LGPL
component, treat that the same way: look for a permissively-licensed alternative or a narrower reimplementation
first, and only bring the question to the user if neither is workable -- don't add the exception yourself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the licensing rationale in one location. The detailed viuer/ansi_colours precedent is duplicated between repository policy and module documentation; retain the policy rationale in CLAUDE.md and keep the module docs focused on renderer behavior.

  • CLAUDE.md#L1078-L1085: retain the repository policy and precedent here.
  • services/storage/src/tty_image.rs#L3-L8: remove the duplicated dependency-license explanation.

As per coding guidelines, "Document each fact exactly once in its most relevant location; do not duplicate explanations or add documentation cross-references."

📍 Affects 2 files
  • CLAUDE.md#L1078-L1085 (this comment)
  • services/storage/src/tty_image.rs#L3-L8
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 1078 - 1085, The licensing rationale is duplicated
across the repository policy and module documentation. Keep the detailed
viuer/ansi_colours precedent in CLAUDE.md lines 1078-1085 unchanged, and remove
the duplicated dependency-license explanation from
services/storage/src/tty_image.rs lines 3-8 while retaining only documentation
about the renderer’s behavior.

Source: Coding guidelines

Comment on lines +111 to +113
if is_image_filename(&filename) {
info!("Agent {} stored image {:?} ({} bytes)", agent_id, path, bytes_written);
show_image_on_tty(&path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -e
rg -n "show_image_on_tty|is_image_filename|stored image" services/storage/src -S

Repository: edge-toolkit/core

Length of output: 847


🏁 Script executed:

set -e
sed -n '1,220p' services/storage/src/routes.rs

Repository: edge-toolkit/core

Length of output: 6988


🏁 Script executed:

set -e
python3 - <<'PY'
from pathlib import Path
p = Path('services/storage/src/routes.rs')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 90 <= i <= 150:
        print(f"{i:4}: {line}")
PY

Repository: edge-toolkit/core

Length of output: 3296


🏁 Script executed:

set -e
printf '%s\n' '--- routes.rs ---'
python3 - <<'PY'
from pathlib import Path
p = Path('services/storage/src/routes.rs')
text = p.read_text().splitlines()
for i, line in enumerate(text, 1):
    if 1 <= i <= 220:
        print(f"{i:4}: {line}")
PY

printf '\n%s\n' '--- show_image_on_tty references ---'
rg -n "fn show_image_on_tty|show_image_on_tty\(" -S services/storage/src

Repository: edge-toolkit/core

Length of output: 8227


🏁 Script executed:

set -e
python3 - <<'PY'
from pathlib import Path
p = Path('services/storage/src/routes.rs')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 103 <= i <= 118:
        print(f"{i:4}: {line}")
PY

Repository: edge-toolkit/core

Length of output: 919


🏁 Script executed:

set -e
wc -l services/storage/src/tty_image.rs
sed -n '1,220p' services/storage/src/tty_image.rs

Repository: edge-toolkit/core

Length of output: 4483


Offload terminal rendering from the request worker. show_image_on_tty is synchronous here and does image decode, resize, string generation, and stdout I/O on the Actix worker. Move it to a bounded blocking/background task after persistence, while keeping the warn! on render failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/storage/src/routes.rs` around lines 111 - 113, Update the
image-handling branch around show_image_on_tty to run terminal rendering in a
bounded blocking/background task after persistence, rather than synchronously on
the Actix request worker. Preserve the existing image detection and info!
logging, and propagate render failures to warn! from the spawned task.

Comment on lines +119 to +126
/// Render a thumbnail of the image at `path` directly to stdout, so the operator actually *sees* what was
/// stored rather than just its filename and byte count.
///
/// This bypasses `tracing` entirely and writes straight to the terminal: the escape sequences (ANSI
/// truecolor half-block art -- see the `tty_image` module) are tty-only presentation, not structured log
/// data, and would otherwise get shipped to the OTLP log exporter as log-record noise. Decode/render
/// failures (a corrupt upload, a non-terminal stdout) only cost tty visibility, not the request, so they're
/// reported via `warn!` rather than via `?`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use self-contained first-line Rust doc summaries. Both new doc blocks begin mid-sentence and point readers at another source file instead of stating a complete local summary.

  • services/storage/src/routes.rs#L119-L126: make Line 119 a complete period-terminated summary and remove the tty_image cross-reference.
  • services/storage/tests/image_logging.rs#L1-L2: make Line 1 a complete period-terminated summary and remove the put_file cross-reference.

As per coding guidelines, "fix first-line doc-summary punctuation" and "do not duplicate explanations or add documentation cross-references."

📍 Affects 2 files
  • services/storage/src/routes.rs#L119-L126 (this comment)
  • services/storage/tests/image_logging.rs#L1-L2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/storage/src/routes.rs` around lines 119 - 126, Update the first-line
Rust doc summaries at services/storage/src/routes.rs:119-126 and
services/storage/tests/image_logging.rs:1-2 so each is a self-contained,
complete sentence ending with a period; remove the mid-sentence wording and
cross-references to tty_image and put_file, while leaving the remaining
documentation unchanged.

Source: Coding guidelines

Comment on lines +1 to +13
//! Hand-rolled ANSI truecolor half-block renderer for images on a real terminal.
//!
//! Kept dependency-free beyond the `image` crate (already resolved in the workspace via
//! deno_image/ws-web-runner, so this adds no new crate to review) rather than pulling in a terminal-image
//! crate: `viuer`, the obvious off-the-shelf choice, drags in `ansi_colours` (LGPL-3.0-or-later) as an
//! unconditional dependency for its RGB-to-256-color quantization path -- copyleft, and the first such
//! license this workspace would have had to accept. Emitting 24-bit truecolor SGR codes directly needs no
//! palette quantization at all, so there is nothing for that dependency to do here.
//!
//! Uses the "half-block" technique standard terminal-image tools (viu, chafa, catimg, viuer's own fallback)
//! use: each terminal row encodes two source-pixel rows via the upper-half-block character `\u{2580}`, whose
//! foreground and background colors are set independently, doubling the effective vertical resolution for
//! the same character-cell budget.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Register every modified Rust file in the doc-summary rule. config/ast-grep/rules/doc-summary-ends-with-period.yaml is not updated in this change set.

  • services/storage/src/tty_image.rs#L1-L13: add this new Rust file to the sorted files: list.
  • services/storage/src/lib.rs#L12-L12: retain or add this modified Rust file in the sorted files: list.
  • services/storage/src/routes.rs#L100-L149: retain or add this modified Rust file in the sorted files: list.
  • services/storage/tests/image_logging.rs#L1-L29: add this modified Rust test file to the sorted files: list.
  • services/storage/tests/put.rs#L110-L136: retain or add this modified Rust test file in the sorted files: list.

As per coding guidelines, "Add every modified Rust file to config/ast-grep/rules/doc-summary-ends-with-period.yaml's sorted files: list."

📍 Affects 5 files
  • services/storage/src/tty_image.rs#L1-L13 (this comment)
  • services/storage/src/lib.rs#L12-L12
  • services/storage/src/routes.rs#L100-L149
  • services/storage/tests/image_logging.rs#L1-L29
  • services/storage/tests/put.rs#L110-L136
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/storage/src/tty_image.rs` around lines 1 - 13, Update
config/ast-grep/rules/doc-summary-ends-with-period.yaml by adding every modified
Rust file to its sorted files list: services/storage/src/tty_image.rs (anchor),
services/storage/src/lib.rs, services/storage/src/routes.rs,
services/storage/tests/image_logging.rs, and services/storage/tests/put.rs.
Retain any entries already present and do not modify the Rust implementation
files directly.

Source: Coding guidelines

Comment on lines +30 to +31
pub fn render(path: &Path) -> image::ImageResult<()> {
let source = image::open(path)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '## tty_image.rs\n'
sed -n '1,220p' services/storage/src/tty_image.rs

printf '\n## render usages\n'
rg -n "tty_image::render|render\(.*tty_image|render\(" services/storage -g'*.rs'

printf '\n## terminal/stdout checks in storage service\n'
rg -n "is_terminal|IsTerminal|stdout\(\)\.is_terminal|stderr\(\)\.is_terminal|tty" services/storage -g'*.rs'

Repository: edge-toolkit/core

Length of output: 6223


🏁 Script executed:

sed -n '90,150p' services/storage/src/routes.rs
printf '\n## tests mentioning tty render\n'
sed -n '90,150p' services/storage/tests/put.rs

Repository: edge-toolkit/core

Length of output: 5527


Gate tty rendering on stdout
render() still runs when stdout is redirected, so valid image uploads can emit ANSI escapes into logs and pay the decode/resize cost unnecessarily. Return early when std::io::stdout().is_terminal() is false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/storage/src/tty_image.rs` around lines 30 - 31, Update
tty_image::render to check whether stdout is a terminal before calling
image::open; return successfully immediately when
std::io::stdout().is_terminal() is false, preserving the existing image
rendering path for terminal output.

Comment on lines +31 to +57
let source = image::open(path)?;
if source.width() == 0 || source.height() == 0 {
return Ok(());
}

// Each output row consumes two source-pixel rows (the half-block trick above), so the resize target's
// height is twice the column-derived row count; `max(2)` keeps a hairline-thin source image visible as
// at least one row instead of rounding away to nothing. The float math and truncating cast are both
// genuinely needed to turn a source aspect ratio into a terminal row count; TARGET_COLUMNS being a small
// constant keeps the result comfortably within u32 for any real image.
#[expect(
clippy::float_arithmetic,
reason = "aspect-ratio scaling to a row count needs float math"
)]
let aspect = f64::from(source.height()) / f64::from(source.width());
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::float_arithmetic,
reason = "converting the aspect-scaled row count (always small and non-negative) back to u32"
)]
let rows = ((aspect * f64::from(TARGET_COLUMNS) / 2.0).round().max(1.0)) as u32;
let sample_height = rows.saturating_mul(2).max(2);
let resized = source
.resize_exact(TARGET_COLUMNS, sample_height, image::imageops::FilterType::Triangle)
.to_rgba8();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and any related constants/usages.
git ls-files services/storage/src/tty_image.rs
wc -l services/storage/src/tty_image.rs
cat -n services/storage/src/tty_image.rs | sed -n '1,220p'

printf '\n-- nearby references --\n'
rg -n "TARGET_COLUMNS|rows|min\(MAX_ROWS\)|resize_exact|image::open|decode" services/storage/src -S

Repository: edge-toolkit/core

Length of output: 6331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe the image crate API usage in the repo to see whether decode limits are already applied elsewhere.
rg -n "limits\(|with_guessed_format|ImageReader|max_dimensions|max_alloc" . -S

Repository: edge-toolkit/core

Length of output: 448


Bound the rendered thumbnail size. rows scales directly with the decoded aspect ratio, so a very tall, narrow image can make resize_exact allocate a huge RGBA buffer and then build an equally large ANSI string. Clamp the rendered rows and add decoder limits before image::open so a single upload cannot exhaust memory or CPU.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/storage/src/tty_image.rs` around lines 31 - 57, Bound thumbnail
resource usage in the image-rendering function: configure decoder limits before
image::open to reject excessively large dimensions or decoded pixels, and clamp
the aspect-derived rows value before sample_height and resize_exact are used.
Preserve the existing minimum one-row behavior while enforcing a finite maximum
that also limits the generated ANSI output.

Comment on lines +110 to +112
/// An image PUT under `cargo test` has no real terminal on stdout, so `viuer`'s decode-and-render step
/// (triggered by the `.png` extension) is expected to fail internally. The route must still store the file
/// and return 200 regardless -- tty display is a best-effort side effect, never a reason to fail the upload.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the test's failure explanation.

This test fails rendering because Line 126 supplies malformed PNG bytes, not because cargo test lacks a terminal; viuer is no longer used, and stdout writes are intentionally ignored. Rewrite the first doc-summary line as a complete sentence ending in a period. As per coding guidelines, "fix first-line doc-summary punctuation."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/storage/tests/put.rs` around lines 110 - 112, Update the first
doc-summary line for the image PUT test to explain that rendering fails because
line 126 provides malformed PNG bytes, not because of terminal availability or
viuer behavior. Make the summary a complete sentence ending with a period, while
preserving the remaining documentation.

Source: Coding guidelines

Comment on lines +254 to +257
// stats-gl's own dom defaults to the top-left corner; place it bottom-right instead, clear of the
// page content. `makeDraggable`'s pointermove handler always writes pixel left/top, so switching
// away from right/bottom on the first drag is expected and fine.
Object.assign(overlay.style, { position: "fixed", top: "auto", left: "auto", right: "24px", bottom: "32px" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and surrounding drag logic.
grep -n -C 4 -E 'makeDraggable|pointerdown|pointermove|pointerup|overlay.style|right|bottom|left|top' services/ws-server/static/meters.js

# Map the file structure to locate the draggable helper definition.
python3 - <<'PY'
from pathlib import Path
p = Path('services/ws-server/static/meters.js')
for i, line in enumerate(p.read_text().splitlines(), 1):
    if 'makeDraggable' in line or 'pointerdown' in line or 'pointermove' in line:
        print(f'{i}: {line}')
PY

# Try to inspect the published stats-gl artifact referenced in the review comment if reachable from the sandbox.
python3 - <<'PY'
import urllib.request, ssl
url = 'https://app.unpkg.com/stats-gl%403.8.0/files/dist/main.cjs'
ctx = ssl.create_default_context()
try:
    with urllib.request.urlopen(url, context=ctx, timeout=15) as r:
        data = r.read(4000).decode('utf-8', 'replace')
    print(data)
except Exception as e:
    print(f'FETCH_ERROR: {e}')
PY

Repository: edge-toolkit/core

Length of output: 5801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ssl, urllib.request, re

url = 'https://app.unpkg.com/stats-gl%403.8.0/files/dist/main.cjs'
ctx = ssl._create_unverified_context()
with urllib.request.urlopen(url, context=ctx, timeout=20) as r:
    text = r.read().decode('utf-8', 'replace')

# Show the parts that mention the overlay element's sizing/positioning.
for pat in [
    r'position\s*[:=]\s*["\']fixed["\']',
    r'right\s*[:=]\s*["\']0["\']',
    r'bottom\s*[:=]\s*["\']0["\']',
    r'width\s*[:=]\s*["\']auto["\']',
    r'height\s*[:=]\s*["\']auto["\']',
    r'offsetWidth',
    r'offsetHeight',
    r'left\s*[:=]\s*["\']',
    r'top\s*[:=]\s*["\']',
]:
    m = re.search(pat, text)
    print(f'{pat}: {"yes" if m else "no"}')

# Print a small window around the first "position:fixed" or "right:0" occurrence if present.
for needle in ['position:"fixed"', "position:'fixed'", 'right:"0"', "right:'0'", 'bottom:"0"', "bottom:'0'"]:
    idx = text.find(needle)
    if idx != -1:
        start = max(0, idx - 300)
        end = min(len(text), idx + 900)
        print('\n--- excerpt ---')
        print(text[start:end])
        break
PY

Repository: edge-toolkit/core

Length of output: 405


🌐 Web query:

CSS absolutely positioned element left right width auto stretch shrink-to-fit specification

💡 Result:

For an absolutely positioned element, the interaction between left, right, and width properties is defined by a specific set of rules in the CSS specification [1][2]. When width is auto, the element does not automatically stretch unless both left and right are also specified [3]. Instead, it typically defaults to a shrink-to-fit behavior [1][4]. The layout behavior depends on which properties are set to auto: 1. Stretching behavior: An absolutely positioned element fills the available horizontal space (stretches) when both left and right are explicitly defined (i.e., not auto) and the width property is left as auto [2][3]. In this state, the browser solves for width based on the distance between the left and right offsets [2][4]. 2. Shrink-to-fit behavior: If width is auto and one of the horizontal offsets (left or right) is also auto, the element adopts a shrink-to-fit width [1][2]. Shrink-to-fit is a sizing algorithm similar to that used for table cells, which calculates the width based on the content's preferred minimum and maximum widths, constrained by the available space [4]. Specifically: - If left and width are auto, and right is specified, the element uses shrink-to-fit and then solves for left [1][5]. - If right and width are auto, and left is specified, the element uses shrink-to-fit and then solves for right [1][5]. 3. All auto: If left, right, and width are all auto, the browser sets margin-left and margin-right to 0, sets the element to its static position, and applies the shrink-to-fit behavior for the width [1][2]. Note that this behavior is distinct from the stretch keyword found in CSS Alignment, which can be applied to align-self or justify-self properties [6]. While specifying left and right offsets forces a stretch-like result for absolutely positioned elements with width: auto, it is a consequence of the layout equation solving for the width variable rather than an alignment-based stretch [2][3].

Citations:


Clear the opposing anchors before the first drag

makeDraggable writes left/top while right/bottom stay set, which over-constrains this auto-width overlay and can stretch it across the viewport on the first drag.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/ws-server/static/meters.js` around lines 254 - 257, Update the
overlay initialization near makeDraggable so the opposing left/top anchors are
cleared when pointer dragging begins, before pixel left/top values are written.
Preserve the fixed bottom-right placement before the first drag, then remove
right/bottom when applying the first drag position to prevent the overlay from
stretching.

@jayvdb
jayvdb merged commit cce209c into main Jul 22, 2026
27 of 34 checks passed
@jayvdb
jayvdb deleted the image-logging branch July 22, 2026 08:20
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.32184% with 38 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
services/storage/src/tty_image.rs 10.00% 27 Missing ⚠️
services/ws-modules/pydata1/pydata1/__init__.py 30.76% 8 Missing and 1 partial ⚠️
services/storage/src/routes.rs 88.88% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants