Skip to content

fix: adversarial review findings for environment/raw endpoints#167

Merged
ng merged 10 commits intodevfrom
feat/environment-raw-management
Mar 15, 2026
Merged

fix: adversarial review findings for environment/raw endpoints#167
ng merged 10 commits intodevfrom
feat/environment-raw-management

Conversation

@ng
Copy link
Copy Markdown
Contributor

@ng ng commented Mar 15, 2026

Summary

Follow-up to #162 — addresses findings from dual-model adversarial code review.

  • Delete orphaned migration 0002_mushy_loki.sql (not in drizzle journal)
  • Re-throw TRPCError in deleteFile catch block (was swallowed as 500 instead of 400)
  • Fix limit schema: default(1440) exceeded max(1000) — raised max to 1440
  • Parallelize getSummary queries with Promise.all
  • Add dev fallback for df -B1 in diskUsage (GNU-only, fails on macOS)
  • Use SAFE_FILENAME filter in listRawFiles for consistency with download/delete
  • Add 18 unit tests for tempUtils (centiDegrees conversions, toF/toC, ensureF, mapToEightSleepScale)

Test plan

  • 947 tests pass (18 new)
  • TypeScript compiles clean

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Environmental temperature monitoring now supports bed and freezer sensors with configurable unit conversion (Celsius/Fahrenheit).
    • New API endpoints to fetch, retrieve latest, and summarize temperature/humidity data with date range filtering.
    • Added raw file management endpoints: list files, delete with safety validation, and retrieve disk usage statistics.
    • Health status reporting now persists to system database.
  • Tests

    • Added comprehensive unit tests for temperature conversion utilities covering precision, edge cases, and accuracy.

ng and others added 8 commits March 15, 2026 11:36
Parse bedTemp/frzTemp CBOR records from RAW files into new biometrics DB
tables, expose via tRPC with unit conversion. Add WiFi signal strength
endpoint and RAW file list/download/delete API for disk management.

- New DB tables: bed_temp (zone temps, humidity), freezer_temp (water temps)
- Python module: environment-monitor (60s downsampled writes, 32MB/10% CPU)
- Environment router: date-range queries, latest, summary with F/C conversion
- System router: wifiStatus via nmcli with dev fallback
- Raw router: file listing, safe delete (protects active file), disk usage
- Next.js route: streaming binary download with path traversal protection
- Deploy: install script updated for environment-monitor module
- nmcli parser: handle escaped colons in SSIDs
- raw download: reject symlinks, verify canonical path
- raw delete: mark as protected endpoint
- Python module: INSERT OR IGNORE + seed cursors from DB on restart
- Schema: unique indexes on timestamp (dedup guarantee)
- Regenerate clean 0002 migration
- CI build workflow: trigger on all branches, not just main
- Service: run as dac user instead of root
- UNIQUE indexes on bed_temp.timestamp and freezer_temp.timestamp
  (migration SQL + drizzle schema) to prevent duplicate rows
- Sync biometrics modules in sp-update flow so OTA updates
  install/restart piezo-processor, sleep-detector, environment-monitor
- (Already in PR) symlink check + realpath validation in RAW route
- (Already in PR) ENOENT vs 500 error differentiation
- (Already in PR) INSERT OR IGNORE + DB-seeded cursors for restart safety
- (Already in PR) deleteFile protect:true, nmcli escape-aware parsing
- (Already in PR) User=dac in environment-monitor service

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- raw delete: canonical path + symlink checks (matches download route)
- raw diskUsage: 5s timeout on df subprocess
- wifiStatus: NaN guard on signal parsing
- Python module: narrow except to sqlite3.Error, skip malformed records
- Delete orphaned migration 0002_mushy_loki.sql (not in journal)
- Re-throw TRPCError in deleteFile catch block (was swallowed as 500)
- Fix limit default(1440) > max(1000) schema bug → max(1440)
- Parallelize getSummary queries with Promise.all
- Add dev fallback for df -B1 in diskUsage (GNU-only flag)
- Use SAFE_FILENAME filter in listRawFiles for consistency
- Add 18 unit tests for tempUtils (centiDegrees, toF/toC, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 15, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a1f772e-68a3-41f3-ac3a-138ff3a078fb

📥 Commits

Reviewing files that changed from the base of the PR and between 2932287 and 97fd22d.

📒 Files selected for processing (1)
  • modules/environment-monitor/main.py

📝 Walkthrough

Walkthrough

Python backend introduces sensor data processing with centi-unit converters and database persistence for bed and freezer temperatures. TypeScript layer adds comprehensive unit tests for temperature conversions and new TRPC routers for environmental data retrieval and RAW file management with security controls.

Changes

Cohort / File(s) Summary
Backend Sensor Processing
modules/environment-monitor/main.py
Added helper converters (_to_centidegrees, _to_centipercent, _safe_temp) for sensor data normalization. Implemented write_bed_temp and write_freezer_temp functions to persist temperature readings with NO_SENSOR guards. Added report_health to write system health status to database. Enhanced main loop to handle bedTemp/bedTemp2/frzTemp record types with downsampling logic.
Temperature Utility Tests
src/lib/tests/tempUtils.test.ts
Comprehensive test suite for temperature conversion utilities covering Celsius↔Fahrenheit conversions, centi-degree/centi-percent conversions, Eight Sleep scale mapping, and edge cases including fractional precision and round-trip accuracy.
Environment Data API
src/server/routers/environment.ts
New TRPC router with procedures for fetching bed/freezer temperatures (getBedTemp, getFreezerTemp), retrieving latest readings (getLatestBedTemp, getLatestFreezerTemp), and computing aggregated statistics (getSummary) with unit conversion and date range filtering.
RAW File Management
src/server/routers/raw.ts
New TRPC router for RAW file operations: listing files with metadata, deleting files with path traversal/symlink protections and active file prevention, and computing disk usage. Includes SAFE_FILENAME validation and comprehensive error handling.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰✨ Sensors now sing to the database deep,
While temps and humidity gently leap,
From centi-units to scales so clean,
The finest environment monitor I've seen!
Raw files managed with locks held tight—
Eight Sleep's health shining ever bright! 🌡️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the primary change: addressing adversarial review findings for environment and raw endpoints through specific fixes and improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/environment-raw-management
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Hardware emits bedTemp2 (not bedTemp) with nested left/right dicts
containing floats in °C, and frzTemp with short field names (amb/hs/
left/right) as raw centidegrees. Convert bedTemp2 floats to
centidegrees for consistent storage. Handle -327.68 sentinel as null.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/lib/tempUtils.ts (1)

39-39: Prefer composing through centiDegreesToC to avoid duplicated scale logic.

♻️ Suggested refactor
-export const centiDegreesToF = (cd: number) => toF(cd / 100)
+export const centiDegreesToF = (cd: number) => toF(centiDegreesToC(cd))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/tempUtils.ts` at line 39, The centiDegreesToF function duplicates
scaling logic; change centiDegreesToF to reuse centiDegreesToC by converting cd
-> centiDegreesToC(cd) and then passing that result to toF (i.e.,
centiDegreesToF should call centiDegreesToC(cd) and then toF on the Celsius
result) so scaling logic lives only in centiDegreesToC and toF is used for the
unit conversion.
app/api/raw/[filename]/route.ts (1)

7-10: Extract shared constants to avoid drift.

RAW_DIR and SAFE_FILENAME are duplicated between this file and src/server/routers/raw.ts. If either changes independently, security behavior could diverge.

♻️ Suggested refactor

Extract these constants to a shared module (e.g., src/lib/rawConfig.ts):

// src/lib/rawConfig.ts
export const RAW_DIR = process.env.RAW_DATA_DIR ?? '/persistent'
export const SAFE_FILENAME = /^[\w.-]+\.RAW$/i

Then import in both files to ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/raw/`[filename]/route.ts around lines 7 - 10, RAW_DIR and
SAFE_FILENAME are duplicated and may drift; extract them into a single shared
module (e.g., export const RAW_DIR and export const SAFE_FILENAME from a new
module) and replace the local declarations in both route handlers (the file
containing RAW_DIR and SAFE_FILENAME and the server/router that also declares
them) with imports from that module so both use the same canonical symbols
RAW_DIR and SAFE_FILENAME.
src/server/routers/environment.ts (1)

34-34: Consider defining typed output schemas.

All procedures use z.any() for output, which bypasses runtime validation and loses TypeScript inference for API consumers. While this simplifies initial development, typed schemas would improve API contracts and catch serialization issues.

Also applies to: 74-74, 109-109, 146-146, 177-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/environment.ts` at line 34, The procedures currently call
.output(z.any()) which disables runtime validation and loses TypeScript
inference; replace each .output(z.any()) in the environment router with a proper
Zod schema (e.g., EnvironmentSchema, EnvironmentListSchema,
CreateEnvironmentResponseSchema, UpdateEnvironmentResponseSchema) that matches
the expected response shape, import or define those schemas near the top of
src/server/routers/environment.ts, use them in the .output(...) calls that
currently use z.any(), and export corresponding TypeScript types (z.infer<typeof
...>) so API consumers get correct typing and runtime validation for the
procedures that currently contain .output(z.any()).
src/db/biometrics-schema.ts (1)

53-78: LGTM! Schema correctly aligns with both the Python writer and tRPC API consumer.

The table definitions match the column names used in write_bed_temp and write_freezer_temp (Python), and the field selections in getBedTemp, getFreezerTemp, and getSummary (TypeScript). The unique indexes on timestamp properly support the INSERT OR IGNORE deduplication strategy.

Minor naming inconsistency (optional): The unique indexes use an idx_ prefix (e.g., idx_bed_temp_timestamp), while the existing vitals table uses uq_ for its unique index (uq_vitals_side_timestamp). Consider renaming to uq_bed_temp_timestamp and uq_freezer_temp_timestamp for consistency, though this is purely stylistic.

,

♻️ Optional: Rename unique indexes for consistency
 }, t => [
-  uniqueIndex('idx_bed_temp_timestamp').on(t.timestamp),
+  uniqueIndex('uq_bed_temp_timestamp').on(t.timestamp),
 ])

 export const freezerTemp = sqliteTable('freezer_temp', {
   ...
 }, t => [
-  uniqueIndex('idx_freezer_temp_timestamp').on(t.timestamp),
+  uniqueIndex('uq_freezer_temp_timestamp').on(t.timestamp),
 ])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/biometrics-schema.ts` around lines 53 - 78, Rename the unique index
names to match the existing `vitals` convention by changing
`idx_bed_temp_timestamp` to `uq_bed_temp_timestamp` and
`idx_freezer_temp_timestamp` to `uq_freezer_temp_timestamp` in the table
definitions for `bedTemp` and `freezerTemp` (the uniqueIndex calls that call
.on(t.timestamp)); keep the same unique constraint logic and placement inside
the index arrays so only the index identifier strings are updated while
retaining the same .on(t.timestamp) targets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/api/raw/`[filename]/route.ts:
- Around line 35-39: The startsWith check on canonicalFile vs canonicalDir can
be fooled by prefix matches (e.g., /persistent vs /persistent-backup); modify
the validation in the route handler that uses realpath, canonicalFile and
canonicalDir so you append a path separator to canonicalDir before calling
startsWith (use path.sep or ensure a trailing slash) and compare
canonicalFile.startsWith(canonicalDirWithSep) to guarantee the resolved file is
inside RAW_DIR; update the error branch that returns NextResponse.json({ error:
'Path traversal detected' }, { status: 400 }) to remain the same.

In `@modules/environment-monitor/main.py`:
- Around line 160-171: The DB connection and cursor seeding (open_biometrics_db,
last_bed_write, last_frz_write) are done before the guarded/protected startup
path so failures can abort the process without consistent health/fatal handling;
move that work into the existing protected startup/failure handling: wrap the
open_biometrics_db call, the two SELECTs that compute last_bed_write and
last_frz_write, and the RawFileFollower instantiation in a try/except block
inside the guarded startup sequence (the same area that uses _shutdown and
report_health), and on exception call report_health("unhealthy", "<contextual
error message>") and trigger the same shutdown/fatal handling used elsewhere
(invoke _shutdown or the existing exit path); ensure follower is only created
after DB seed succeeds and close/cleanup db_conn on failure.

In `@scripts/install`:
- Around line 680-708: The script currently swallows failures while syncing
biometrics modules; update the loop that processes MODULES_DEST and each module
(piezo-processor, sleep-detector, environment-monitor) so that failures creating
virtualenvs (python3 -m venv), installing requirements
("$MODULES_DEST/$mod/venv/bin/pip install ..."), copying the service file (cp
"$MODULES_DEST/$mod/$svc" ...), and systemctl enable/restart are checked for
non-zero exit status: on error, write a clear message to stderr (including $mod
and the failing command), set a failure flag (e.g. MODULE_SYNC_FAILED=1) and
continue to attempt other modules, and after the for loop exit non-zero (or
return a non-zero status) if MODULE_SYNC_FAILED is set so the installer does not
report success when module setup failed. Ensure messages reference the module
name and service variable svc to aid debugging.

In `@src/server/routers/raw.ts`:
- Around line 75-79: The startsWith check on canonicalFile vs canonicalDir in
raw.ts can produce false positives for directories with similar prefixes; update
the check that currently uses canonicalFile.startsWith(canonicalDir) to compare
canonicalFile against canonicalDir with a trailing path separator (e.g., ensure
canonicalDir ends with path.sep or '/' before calling startsWith) so only true
directory children pass; locate the code using realpath, canonicalFile,
canonicalDir and RAW_DIR and change the comparison to append the separator to
canonicalDir (or normalize both and compare with path utilities) to prevent
prefix-matching edge cases.

---

Nitpick comments:
In `@app/api/raw/`[filename]/route.ts:
- Around line 7-10: RAW_DIR and SAFE_FILENAME are duplicated and may drift;
extract them into a single shared module (e.g., export const RAW_DIR and export
const SAFE_FILENAME from a new module) and replace the local declarations in
both route handlers (the file containing RAW_DIR and SAFE_FILENAME and the
server/router that also declares them) with imports from that module so both use
the same canonical symbols RAW_DIR and SAFE_FILENAME.

In `@src/db/biometrics-schema.ts`:
- Around line 53-78: Rename the unique index names to match the existing
`vitals` convention by changing `idx_bed_temp_timestamp` to
`uq_bed_temp_timestamp` and `idx_freezer_temp_timestamp` to
`uq_freezer_temp_timestamp` in the table definitions for `bedTemp` and
`freezerTemp` (the uniqueIndex calls that call .on(t.timestamp)); keep the same
unique constraint logic and placement inside the index arrays so only the index
identifier strings are updated while retaining the same .on(t.timestamp)
targets.

In `@src/lib/tempUtils.ts`:
- Line 39: The centiDegreesToF function duplicates scaling logic; change
centiDegreesToF to reuse centiDegreesToC by converting cd -> centiDegreesToC(cd)
and then passing that result to toF (i.e., centiDegreesToF should call
centiDegreesToC(cd) and then toF on the Celsius result) so scaling logic lives
only in centiDegreesToC and toF is used for the unit conversion.

In `@src/server/routers/environment.ts`:
- Line 34: The procedures currently call .output(z.any()) which disables runtime
validation and loses TypeScript inference; replace each .output(z.any()) in the
environment router with a proper Zod schema (e.g., EnvironmentSchema,
EnvironmentListSchema, CreateEnvironmentResponseSchema,
UpdateEnvironmentResponseSchema) that matches the expected response shape,
import or define those schemas near the top of
src/server/routers/environment.ts, use them in the .output(...) calls that
currently use z.any(), and export corresponding TypeScript types (z.infer<typeof
...>) so API consumers get correct typing and runtime validation for the
procedures that currently contain .output(z.any()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1aab7cf7-337f-4fdd-a7fd-0839d94ac917

📥 Commits

Reviewing files that changed from the base of the PR and between 5b92689 and 2932287.

📒 Files selected for processing (17)
  • app/api/raw/[filename]/route.ts
  • modules/environment-monitor/main.py
  • modules/environment-monitor/manifest.json
  • modules/environment-monitor/requirements.txt
  • modules/environment-monitor/sleepypod-environment-monitor.service
  • scripts/install
  • src/db/biometrics-migrations/0002_fuzzy_strong_guy.sql
  • src/db/biometrics-migrations/meta/0002_snapshot.json
  • src/db/biometrics-migrations/meta/_journal.json
  • src/db/biometrics-schema.ts
  • src/lib/tempUtils.ts
  • src/lib/tests/tempUtils.test.ts
  • src/server/openapi.ts
  • src/server/routers/app.ts
  • src/server/routers/environment.ts
  • src/server/routers/raw.ts
  • src/server/routers/system.ts

Comment on lines +35 to +39
const canonicalFile = await realpath(resolved)
const canonicalDir = await realpath(RAW_DIR)
if (!canonicalFile.startsWith(canonicalDir)) {
return NextResponse.json({ error: 'Path traversal detected' }, { status: 400 })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Append path separator to prevent prefix-matching edge case.

If RAW_DIR is /persistent and a path resolves to /persistent-backup/file.RAW, the startsWith check would incorrectly pass. Append a path separator to ensure the file is within the directory, not just sharing a prefix.

🛡️ Proposed fix
     const canonicalFile = await realpath(resolved)
     const canonicalDir = await realpath(RAW_DIR)
-    if (!canonicalFile.startsWith(canonicalDir)) {
+    if (!canonicalFile.startsWith(canonicalDir + path.sep)) {
       return NextResponse.json({ error: 'Path traversal detected' }, { status: 400 })
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const canonicalFile = await realpath(resolved)
const canonicalDir = await realpath(RAW_DIR)
if (!canonicalFile.startsWith(canonicalDir)) {
return NextResponse.json({ error: 'Path traversal detected' }, { status: 400 })
}
const canonicalFile = await realpath(resolved)
const canonicalDir = await realpath(RAW_DIR)
if (!canonicalFile.startsWith(canonicalDir + path.sep)) {
return NextResponse.json({ error: 'Path traversal detected' }, { status: 400 })
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/raw/`[filename]/route.ts around lines 35 - 39, The startsWith check
on canonicalFile vs canonicalDir can be fooled by prefix matches (e.g.,
/persistent vs /persistent-backup); modify the validation in the route handler
that uses realpath, canonicalFile and canonicalDir so you append a path
separator to canonicalDir before calling startsWith (use path.sep or ensure a
trailing slash) and compare canonicalFile.startsWith(canonicalDirWithSep) to
guarantee the resolved file is inside RAW_DIR; update the error branch that
returns NextResponse.json({ error: 'Path traversal detected' }, { status: 400 })
to remain the same.

Comment on lines +160 to +171
db_conn = open_biometrics_db()
follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.5)

# Seed cursors from DB so restarts don't replay already-ingested samples
last_bed_write = float(
db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM bed_temp").fetchone()[0] or 0
)
last_frz_write = float(
db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM freezer_temp").fetchone()[0] or 0
)

report_health("healthy", "environment-monitor started")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move startup DB initialization into a guarded failure path.

DB connection + cursor seeding currently happen before the protected loop; startup failures can terminate the process without consistent fatal handling/health status updates.

💡 Suggested fix
-    db_conn = open_biometrics_db()
-    follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.5)
-
-    # Seed cursors from DB so restarts don't replay already-ingested samples
-    last_bed_write = float(
-        db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM bed_temp").fetchone()[0] or 0
-    )
-    last_frz_write = float(
-        db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM freezer_temp").fetchone()[0] or 0
-    )
-
-    report_health("healthy", "environment-monitor started")
+    try:
+        db_conn = open_biometrics_db()
+        follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.5)
+
+        # Seed cursors from DB so restarts don't replay already-ingested samples
+        last_bed_write = float(
+            db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM bed_temp").fetchone()[0] or 0
+        )
+        last_frz_write = float(
+            db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM freezer_temp").fetchone()[0] or 0
+        )
+
+        report_health("healthy", "environment-monitor started")
+    except Exception as e:
+        log.exception("Startup failure: %s", e)
+        report_health("down", f"startup failure: {e}")
+        sys.exit(1)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
db_conn = open_biometrics_db()
follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.5)
# Seed cursors from DB so restarts don't replay already-ingested samples
last_bed_write = float(
db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM bed_temp").fetchone()[0] or 0
)
last_frz_write = float(
db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM freezer_temp").fetchone()[0] or 0
)
report_health("healthy", "environment-monitor started")
try:
db_conn = open_biometrics_db()
follower = RawFileFollower(RAW_DATA_DIR, _shutdown, poll_interval=0.5)
# Seed cursors from DB so restarts don't replay already-ingested samples
last_bed_write = float(
db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM bed_temp").fetchone()[0] or 0
)
last_frz_write = float(
db_conn.execute("SELECT COALESCE(MAX(timestamp), 0) FROM freezer_temp").fetchone()[0] or 0
)
report_health("healthy", "environment-monitor started")
except Exception as e:
log.exception("Startup failure: %s", e)
report_health("down", f"startup failure: {e}")
sys.exit(1)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modules/environment-monitor/main.py` around lines 160 - 171, The DB
connection and cursor seeding (open_biometrics_db, last_bed_write,
last_frz_write) are done before the guarded/protected startup path so failures
can abort the process without consistent health/fatal handling; move that work
into the existing protected startup/failure handling: wrap the
open_biometrics_db call, the two SELECTs that compute last_bed_write and
last_frz_write, and the RawFileFollower instantiation in a try/except block
inside the guarded startup sequence (the same area that uses _shutdown and
report_health), and on exception call report_health("unhealthy", "<contextual
error message>") and trigger the same shutdown/fatal handling used elsewhere
(invoke _shutdown or the existing exit path); ensure follower is only created
after DB seed succeeds and close/cleanup db_conn on failure.

Comment thread scripts/install
Comment on lines +680 to +708
# Sync biometrics modules
if [ -d "$INSTALL_DIR/modules" ]; then
MODULES_DEST="/opt/sleepypod/modules"
mkdir -p "$MODULES_DEST"
if [ -d "$INSTALL_DIR/modules/common" ]; then
mkdir -p "$MODULES_DEST/common"
cp -r "$INSTALL_DIR/modules/common/." "$MODULES_DEST/common/"
fi
for mod in piezo-processor sleep-detector environment-monitor; do
if [ -d "$INSTALL_DIR/modules/$mod" ]; then
mkdir -p "$MODULES_DEST/$mod"
cp -r "$INSTALL_DIR/modules/$mod/." "$MODULES_DEST/$mod/"
if [ ! -d "$MODULES_DEST/$mod/venv" ] && command -v python3 &>/dev/null; then
python3 -m venv "$MODULES_DEST/$mod/venv" 2>/dev/null || true
fi
if [ -d "$MODULES_DEST/$mod/venv" ] && [ -f "$MODULES_DEST/$mod/requirements.txt" ]; then
"$MODULES_DEST/$mod/venv/bin/pip" install --quiet -r "$MODULES_DEST/$mod/requirements.txt" 2>/dev/null || true
fi
svc="sleepypod-$mod.service"
if [ -f "$MODULES_DEST/$mod/$svc" ]; then
cp "$MODULES_DEST/$mod/$svc" "/etc/systemd/system/$svc"
systemctl daemon-reload
systemctl enable "$svc" 2>/dev/null || true
systemctl restart "$svc" 2>/dev/null || true
fi
fi
done
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t silently ignore biometrics module sync failures.

Dependency install/service enable-restart failures are currently swallowed, so updates can report success while modules are non-functional.

💡 Suggested fix
-    for mod in piezo-processor sleep-detector environment-monitor; do
+    module_sync_failed=false
+    for mod in piezo-processor sleep-detector environment-monitor; do
       if [ -d "$INSTALL_DIR/modules/$mod" ]; then
@@
-        if [ -d "$MODULES_DEST/$mod/venv" ] && [ -f "$MODULES_DEST/$mod/requirements.txt" ]; then
-          "$MODULES_DEST/$mod/venv/bin/pip" install --quiet -r "$MODULES_DEST/$mod/requirements.txt" 2>/dev/null || true
+        if [ -d "$MODULES_DEST/$mod/venv" ] && [ -f "$MODULES_DEST/$mod/requirements.txt" ]; then
+          if ! "$MODULES_DEST/$mod/venv/bin/pip" install --quiet -r "$MODULES_DEST/$mod/requirements.txt"; then
+            echo "Warning: failed to install requirements for $mod" >&2
+            module_sync_failed=true
+          fi
         fi
@@
-          systemctl enable "$svc" 2>/dev/null || true
-          systemctl restart "$svc" 2>/dev/null || true
+          if ! systemctl enable "$svc"; then
+            echo "Warning: failed to enable $svc" >&2
+            module_sync_failed=true
+          fi
+          if ! systemctl restart "$svc"; then
+            echo "Warning: failed to restart $svc" >&2
+            module_sync_failed=true
+          fi
         fi
       fi
     done
+    if [ "$module_sync_failed" = true ]; then
+      echo "Warning: one or more biometrics modules failed to sync; check module service logs." >&2
+    fi
   fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install` around lines 680 - 708, The script currently swallows
failures while syncing biometrics modules; update the loop that processes
MODULES_DEST and each module (piezo-processor, sleep-detector,
environment-monitor) so that failures creating virtualenvs (python3 -m venv),
installing requirements ("$MODULES_DEST/$mod/venv/bin/pip install ..."), copying
the service file (cp "$MODULES_DEST/$mod/$svc" ...), and systemctl
enable/restart are checked for non-zero exit status: on error, write a clear
message to stderr (including $mod and the failing command), set a failure flag
(e.g. MODULE_SYNC_FAILED=1) and continue to attempt other modules, and after the
for loop exit non-zero (or return a non-zero status) if MODULE_SYNC_FAILED is
set so the installer does not report success when module setup failed. Ensure
messages reference the module name and service variable svc to aid debugging.

Comment thread src/server/routers/raw.ts
Comment on lines +75 to +79
const canonicalFile = await realpath(resolved)
const canonicalDir = await realpath(RAW_DIR)
if (!canonicalFile.startsWith(canonicalDir)) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Path traversal detected' })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same path separator edge case as route.ts.

The startsWith check on canonical paths should append a path separator to prevent prefix-matching edge cases (e.g., /persistent vs /persistent-backup).

🛡️ Proposed fix
         const canonicalFile = await realpath(resolved)
         const canonicalDir = await realpath(RAW_DIR)
-        if (!canonicalFile.startsWith(canonicalDir)) {
+        if (!canonicalFile.startsWith(canonicalDir + path.sep)) {
           throw new TRPCError({ code: 'BAD_REQUEST', message: 'Path traversal detected' })
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/routers/raw.ts` around lines 75 - 79, The startsWith check on
canonicalFile vs canonicalDir in raw.ts can produce false positives for
directories with similar prefixes; update the check that currently uses
canonicalFile.startsWith(canonicalDir) to compare canonicalFile against
canonicalDir with a trailing path separator (e.g., ensure canonicalDir ends with
path.sep or '/' before calling startsWith) so only true directory children pass;
locate the code using realpath, canonicalFile, canonicalDir and RAW_DIR and
change the comparison to append the separator to canonicalDir (or normalize both
and compare with path utilities) to prevent prefix-matching edge cases.

…anagement

# Conflicts:
#	modules/environment-monitor/main.py
#	src/server/routers/environment.ts
#	src/server/routers/raw.ts
@ng ng enabled auto-merge March 15, 2026 19:58
@ng ng merged commit 189abe9 into dev Mar 15, 2026
5 of 6 checks passed
@ng ng deleted the feat/environment-raw-management branch March 15, 2026 19:58
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 1.1.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant