Skip to content

feat: debug HTTP server on port 8085, LAN clients only (#96) - #97

Merged
compot-dev merged 3 commits into
v2from
feat/debug-http
Oct 13, 2026
Merged

compot-dev merged 3 commits into
v2from
feat/debug-http

Conversation

@compot-dev

@compot-dev compot-dev commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #96.

Always-on read-only HTTP server on port 8085:

  • / page refreshing itself every 1.5 s, /stats.json the same as JSON, /log the last 500 lines of the app's log;
  • answers only local-network clients (loopback, 10/8, 172.16/12, 192.168/16, link-local, IPv6 fc00::/7), others get 403;
  • logins and passwords are redacted in URLs, log lines and Authorization headers;
  • the state is read on the UI thread with a 1 s timeout, so a stuck UI thread shows up as "ui_thread": "not responding" (ANR on AG-216: RenderThread blocked in eglSwapBuffers with 8 multi-view SurfaceViews #76).

NetInfo.addresses() (IPv4, no 127/8, no 169.254/16) is shared with #87.

Not in this PR: the http://<ip>:8085/ line in the on-screen debug text. The D button comes with #86; the line goes there once #86 is merged.

Device check (AG-216)

  • curl http://<box>:8085/stats.json from the LAN: cells with text and fps while 8 cameras play
  • / refreshes in a browser; /log shows the app's lines, no passwords
  • single view, multi-view, U view, app in background (mode: paused)
  • no effect on decoding (fps in the corner text unchanged with the page open)

Checked on AG-216 with build 1565dce (CI): 8 cameras in multi-view, all cells in /stats.json with text and fps; / 200, POST 405, unknown path 404; /log 500 lines with no login, password or Authorization value. Found and fixed on the device: user=admin in an XM URL was shown (only the password was masked). App in background → mode: paused, back → multi-view. Two device addresses are listed (172.17.32.144 and 172.17.32.115). Build.HARDWARE reports sun50iw9p1 (Allwinner H616) on this "AG-216".
The decoding impact was not measured separately; the corner fps looked the same with the page open.

- DebugServer: a plain ServerSocket on its own thread, always on for the process,
  started by the activity. GET only (405 otherwise), one request per connection,
  3 s socket timeouts, two workers and a short queue; extra connections are closed.
  A port that cannot be bound is retried every 10 s and reported in the JSON.
- Only clients on the local network are answered: loopback, 10/8, 172.16/12,
  192.168/16, link-local (169.254/16, fe80::/10), IPv6 fc00::/7; others get 403.
- / is a static page that fetches /stats.json every 1.5 s and renders it.
  /stats.json: app version and git hash, device, SoC, Android, screen, the device
  addresses as http://<ip>:8085/ (no 127/8, no 169.254/16), mode, the single-view
  status or every cell with its corner text, in/lost fps, paused and audio.
  The state is read on the UI thread; if it does not answer within 1 s the JSON says
  "ui_thread": "not responding" (useful for the ANR in #76).
  /log: the last 500 lines of the app's own log (logcat without READ_LOGS).
- Logins and passwords never leave the device: URLs and log lines go through the same
  redaction as the log (#55), plus Authorization headers.
- NetInfo: the device addresses and the local-client test; #87 will use it for the
  U cell hint.
- README (EN/RU/ZH): the debug page and a security note.
Seen on the box: an XM slot showed user=admin, only the password was masked.
@compot-dev
compot-dev marked this pull request as ready for review October 13, 2026 05:36
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Site-local IPv6 clients get diagnostics ✓ Resolved
Description
NetInfo.isLocalClient() accepts any address for which isSiteLocalAddress() returns true,
including deprecated IPv6 site-local addresses such as fec0::/10. Those clients pass the access
check in DebugServer.handle() and can retrieve the debug endpoints despite not belonging to the
allowed IPv6 ranges.
Code

android/app/src/main/java/com/openipc/decoder/NetInfo.java[R54-57]

+    static boolean isLocalClient(InetAddress a) {
+        if (a == null) return false;
+        if (a.isLoopbackAddress() || a.isSiteLocalAddress() || a.isLinkLocalAddress()) return true;
+        return a instanceof Inet6Address && (a.getAddress()[0] & 0xfe) == 0xfc;
Evidence
The access predicate directly treats isSiteLocalAddress() as allowed, while the checklist permits
only IPv4 private ranges plus IPv6 unique-local and link-local ranges. IPv6 site-local addresses
therefore bypass the 403 path.

Restrict debug HTTP access to local networks
android/app/src/main/java/com/openipc/decoder/NetInfo.java[54-57]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[114-121]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The local-client check uses `isSiteLocalAddress()`, which accepts IPv6 site-local addresses outside the permitted loopback, link-local, and unique-local ranges.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/NetInfo.java[54-57]
## Recommended Fix
Restrict `isSiteLocalAddress()` to IPv4 private ranges, and explicitly allow only IPv6 loopback, link-local, and `fc00::/7` unique-local addresses. Add a regression test for an IPv6 site-local address such as `fec0::1` and assert that it is rejected.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Web logs are not kept in memory ✓ Resolved
Description
readLog() launches logcat -d for each /log request and redacts that command's output instead
of reading a bounded in-memory application-log ring buffer. The endpoint therefore depends on
platform logcat retention and cannot guarantee that it returns the application's last 500 lines as
required.
Code

android/app/src/main/java/com/openipc/decoder/DebugServer.java[R220-235]

+    static String readLog(int lines) {
+        Process p = null;
+        try {
+            p = new ProcessBuilder("logcat", "-d", "-v", "threadtime", "-t", String.valueOf(lines))
+                    .redirectErrorStream(true).start();
+            ArrayDeque<String> tail = new ArrayDeque<>(lines);
+            try (BufferedReader r = new BufferedReader(
+                    new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) {
+                String line;
+                while ((line = r.readLine()) != null) {
+                    if (tail.size() == lines) tail.removeFirst();
+                    tail.addLast(line);
+                }
+            }
+            StringBuilder sb = new StringBuilder();
+            for (String line : tail) sb.append(redact(line)).append('\n');
Evidence
The implementation creates a ProcessBuilder for logcat -d and then serves its tail, whereas the
checklist requires /log to be backed by a bounded in-memory app-log buffer.

Provide complete debug endpoints
android/app/src/main/java/com/openipc/decoder/DebugServer.java[216-242]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `/log` endpoint reads from `logcat` on demand rather than from a bounded in-memory buffer owned by the application, so the required application log history is not guaranteed.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/DebugServer.java[216-242]
## Recommended Fix
Add a thread-safe bounded ring buffer for application log lines, append redacted lines when the app logs them, and have `/log` return the newest 500 buffered lines without invoking `logcat`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Decoder health details are missing 📎 Requirement gap ◔ Observability
Description
debugState() exposes each multi-view cell's text, input and lost fps, paused state, and audio
flag, but it does not provide full-rate or key-frame status or decoder-overload data. Both the HTML
page and /stats.json consume this same cell data, so those required diagnostics are absent for
every multi-view response.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R1018-1025]

+                    c.put("url", DebugServer.redact(mHosts[i]));
+                    c.put("transport", mTransports[i] != null ? mTransports[i].name() : null);
+                    TextView v = quadStats[i];
+                    c.put("text", v != null ? v.getText().toString() : null);
+                    c.put("in_fps", cell.inFps());
+                    c.put("lost_fps", cell.lostFps());
+                    c.put("paused", cell.isPaused());
+                    c.put("audio", i == quadAudio);
Evidence
The changed snapshot code adds only text, in_fps, lost_fps, paused, and audio for each
cell. The checklist requires additional full-rate or key-frame and decoder-overload diagnostics in
the root page and JSON endpoint.

Provide complete debug endpoints
android/app/src/main/java/com/openipc/decoder/Decoder.java[1007-1029]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[359-368]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The debug snapshot omits the required full-rate or key-frame status and decoder-overload information for multi-view cells.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1007-1029]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[255-293]
## Recommended Fix
Add explicit per-cell fields for output/full-rate or key-frame status and decoder overload, sourcing them from the decoder state and frame-processing metrics. Include the fields in the JSON snapshot so the existing HTML renderer displays them automatically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. UDP view shows the previous camera's state ✓ Resolved
Description
debugState() emits the single-camera object whenever quadEnabled is false, even when
udpViewShown is true. Entering the UDP view stops the RTSP client but leaves the previous URL,
status, codec and resolution fields intact, so /stats.json presents those fields as the state of a
view with no active camera stream.
Code

android/app/src/main/java/com/openipc/decoder/Decoder.java[R997-1000]

+        if (!quadEnabled) {
+            Map<String, Object> single = new LinkedHashMap<>();
+            single.put("url", DebugServer.redact(mHost));
+            single.put("transport", mTransports[mActive] != null ? mTransports[mActive].name() : null);
Evidence
The new mode expression identifies the UDP view, but the following branch still fills single from
camera fields. onShowUdp() stops RTSP and shows the UDP hint without resetting those fields; the
UDP view has no receiver yet.

android/app/src/main/java/com/openipc/decoder/Decoder.java[994-1005]
android/app/src/main/java/com/openipc/decoder/Decoder.java[329-355]
android/app/src/main/java/com/openipc/decoder/Decoder.java[703-706]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The UDP view reports stale single-camera details after stopping RTSP playback.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[994-1005]
- android/app/src/main/java/com/openipc/decoder/Decoder.java[329-355]
## Recommended Fix
Only populate the single-camera object when the single-camera view is active. Give the UDP view its own status payload, or omit camera playback fields while that view is shown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Slow readers can exhaust debug requests ✓ Resolved
Description
reply() writes and flushes responses without a write deadline; the socket's three-second timeout
applies to reads, not these output operations. If local clients request sufficiently large /log
responses and stop reading, the two workers can remain blocked, the four queued requests fill the
queue, and later connections are closed.
Code

android/app/src/main/java/com/openipc/decoder/DebugServer.java[R200-202]

+        out.write(head.getBytes(StandardCharsets.ISO_8859_1));
+        out.write(data);
+        out.flush();
Evidence
The server has only two workers and four queue slots. Its socket timeout protects input reads, while
/log can produce up to 500 lines and reply() has no deadline on output writes; the accept loop
closes connections once the queue is full.

android/app/src/main/java/com/openipc/decoder/DebugServer.java[58-63]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[95-100]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[114-117]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[191-202]
android/app/src/main/java/com/openipc/decoder/DebugServer.java[220-236]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Clients that stop reading a large response can hold every debug HTTP worker indefinitely.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/DebugServer.java[114-117]
- android/app/src/main/java/com/openipc/decoder/DebugServer.java[191-203]
- android/app/src/main/java/com/openipc/decoder/DebugServer.java[220-236]
## Recommended Fix
Enforce a deadline for response writes, for example through nonblocking channels with timed writes, and close connections that exceed it. Keep the concurrency limit, but make sure a non-reading client releases its worker.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/NetInfo.java
Comment thread android/app/src/main/java/com/openipc/decoder/DebugServer.java Outdated
Comment on lines +1018 to +1025
c.put("url", DebugServer.redact(mHosts[i]));
c.put("transport", mTransports[i] != null ? mTransports[i].name() : null);
TextView v = quadStats[i];
c.put("text", v != null ? v.getText().toString() : null);
c.put("in_fps", cell.inFps());
c.put("lost_fps", cell.lostFps());
c.put("paused", cell.isPaused());
c.put("audio", i == quadAudio);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Decoder health details are missing 📎 Requirement gap ◔ Observability

debugState() exposes each multi-view cell's text, input and lost fps, paused state, and audio
flag, but it does not provide full-rate or key-frame status or decoder-overload data. Both the HTML
page and /stats.json consume this same cell data, so those required diagnostics are absent for
every multi-view response.
Agent Prompt
## Issue description
The debug snapshot omits the required full-rate or key-frame status and decoder-overload information for multi-view cells.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/Decoder.java[1007-1029]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[255-293]

## Recommended Fix
Add explicit per-cell fields for output/full-rate or key-frame status and decoder overload, sourcing them from the decoder state and frame-processing metrics. Include the fields in the JSON snapshot so the existing HTML renderer displays them automatically.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread android/app/src/main/java/com/openipc/decoder/Decoder.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/DebugServer.java
…data, U view, write deadline

- IPv6 site-local (fec0::/10) is no longer a local client: isSiteLocalAddress() counts
  for IPv4 only.
- /log is served from LogBuffer, a ring of the last 500 redacted lines fed by a logcat
  reader that follows the app's own log for the life of the process.
- Each cell reports behind_s (seconds in a row the decoder made it lose frames), with
  overload_after_s for the threshold (#83). Full rate / key frames comes with #86.
- In the U view the JSON has a "udp" object instead of the last single-view state.
- A request has a 5 s deadline, writing included: a client that stops reading has its
  socket closed and cannot hold a worker.
@compot-dev

Copy link
Copy Markdown
Collaborator Author

All five Qodo findings are fixed in f853086:

  1. IPv6 site-local fec0::/10 is rejected (isSiteLocalAddress() counts for IPv4 only), regression test added.
  2. /log is served from LogBuffer, a ring of the last 500 redacted lines fed by a logcat reader that follows the app's own log.
  3. Each cell reports behind_s (seconds in a row the decoder made it lose frames) and the JSON has overload_after_s. Full rate / key frames per cell comes with feat: adaptive multi-view decoding (full rate or key frames per cell) and D for the debug text (#85) #86 and will be added there.
  4. In the U view the JSON has a udp object instead of the stale single-view state.
  5. Every request has a 5 s deadline, writing included: the socket is closed by a watchdog.

Device check of f853086 on the AG-216, 8 cameras: /stats.json with behind_s, /log 500 lines with no login, password or Basic token, one logcat reader process. 8 connections that request /log and never read: 7 s later /stats.json answers 200 in 0.29 s.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo re-review of f853086: 1, 4 and 5 resolved. The two remaining items are decided as follows:

  • 2 (logs in memory): /log is served from an in-memory ring (LogBuffer, 500 redacted lines). It is fed by a logcat reader that follows the app's own log rather than by wrapping every Log call: that catches every line the app writes (including platform classes logging for it) without touching ~100 call sites. Kept as is.
  • 3 (full rate / key frames): that mode does not exist on v2; it comes with feat: adaptive multi-view decoding (full rate or key frames per cell) and D for the debug text (#85) #86, which will add it to the JSON. Overload data (behind_s, overload_after_s) is in this PR.

@compot-dev
compot-dev merged commit 236e6f4 into v2 Oct 13, 2026
4 checks passed
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Qodo re-review of f853086: 1, 4 and 5 resolved. The two remaining items are decided as follows:

  • 2 (logs in memory): /log is served from an in-memory ring (LogBuffer, 500 redacted lines). It is fed by a logcat reader that follows the app's own log rather than by wrapping every Log call: that catches every line the app writes (including platform classes logging for it) without touching ~100 call sites. Kept as is.
  • 3 (full rate / key frames): that mode does not exist on v2; it comes with feat: adaptive multi-view decoding (full rate or key frames per cell) and D for the debug text (#85) #86, which will add it to the JSON. Overload data (behind_s, overload_after_s) is in this PR.

Understood. The current review status is:

  • finding 1, finding 4, and finding 5: resolved in commit f853086.
  • finding 2: retained as an intentional implementation choice; /log now uses the bounded, redacted 500-line LogBuffer, populated by the continuous logcat reader.
  • finding 3: deferred because full-rate/key-frame status is not available in v2; issue #86 will add those fields. The existing behind_s and overload_after_s metrics remain available.

Because the PR is already merged, no further change is required here.

@compot-dev
compot-dev deleted the feat/debug-http branch October 13, 2026 17:15
compot-dev added a commit that referenced this pull request Oct 15, 2026
…data, U view, write deadline

- IPv6 site-local (fec0::/10) is no longer a local client: isSiteLocalAddress() counts
  for IPv4 only.
- /log is served from LogBuffer, a ring of the last 500 redacted lines fed by a logcat
  reader that follows the app's own log for the life of the process.
- Each cell reports behind_s (seconds in a row the decoder made it lose frames), with
  overload_after_s for the threshold (#83). Full rate / key frames comes with #86.
- In the U view the JSON has a "udp" object instead of the last single-view state.
- A request has a 5 s deadline, writing included: a client that stops reading has its
  socket closed and cannot hold a worker.
compot-dev added a commit that referenced this pull request Oct 15, 2026
feat: debug HTTP server on port 8085, LAN clients only (#96)
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.

Built-in HTTP server on port 8085 with the debug information (LAN only)

1 participant