Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cli/src/main/java/com/labwatcher/config/ConfigLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
/** Loads {@link AppConfig} from a TOML file with {@code ${VAR}} interpolation. */
public final class ConfigLoader {

private static final Pattern VAR = Pattern.compile("\\$\\{([A-Z0-9_]+)}");
// Match POSIX-style env var names: letters (either case), digits and
// underscore, not starting with a digit. The previous uppercase-only
// pattern silently skipped lowercase vars like ${home} or ${my_token}.
private static final Pattern VAR =
Pattern.compile("\\$\\{([A-Za-z_][A-Za-z0-9_]*)}");

private final Function<String, String> envLookup;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public boolean dispatch(FileSummary summary) {
}
LOG.error("slack: HTTP {} body={}", resp.statusCode(), resp.body());
return false;
} catch (InterruptedException e) {
// Preserve interrupt status so the surrounding shutdown flow
// (FileProcessor's executor drain) can observe it and stop.
Thread.currentThread().interrupt();
LOG.warn("slack: dispatch interrupted");
return false;
} catch (Exception e) {
LOG.error("slack: dispatch failed: {}", e.getMessage());
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,13 @@ public Optional<ProcessedFile> findByHash(String sha256) {

@Override
public List<ProcessedFile> recent(int limit) {
// processed_at is stored as second-precision ISO strings, so two
// files ingested in the same second would sort non-deterministically
// and could even be swapped between calls. Break ties on the
// monotonic primary key so recent() matches the dashboard's
// id-ordered `recent` query.
final String sql = "SELECT * FROM processed_files "
+ "ORDER BY processed_at DESC LIMIT ?";
+ "ORDER BY processed_at DESC, id DESC LIMIT ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setInt(1, limit);
try (ResultSet rs = ps.executeQuery()) {
Expand Down
8 changes: 6 additions & 2 deletions dashboard/src/routes/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ export function apiRouter(db: StateDb): Router {

r.get("/files/:id", (req, res) => {
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: "id must be a number" });
// The SQLite primary key is a positive integer; reject fractional,
// negative, zero and non-numeric inputs up front instead of handing
// them to better-sqlite3 (which would coerce 1.5 -> 1 silently or
// throw for NaN).
if (!Number.isInteger(id) || id <= 0) {
res.status(400).json({ error: "id must be a positive integer" });
return;
}
const row = db.byId(id);
Expand Down
12 changes: 12 additions & 0 deletions engine/src/validator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,18 @@ ValidationReport validate(const ParseResult& parsed, const SchemaSpec& schema) {
", got: " + cell));
continue;
}
if (col.type == ColumnType::INTEGER) {
// strtod happily accepts "1.5" / "1e-3" / NaN / Inf for
// an INTEGER column; reject any value that isn't a
// finite integral number.
double int_part;
if (!std::isfinite(v) || std::modf(v, &int_part) != 0.0) {
report.errors.push_back(make_err(line, col.name,
ErrorCode::TYPE_MISMATCH, Severity::ERROR_,
"expected integer, got: " + cell));
continue;
}
}
if (col.min_value && v < *col.min_value) {
std::ostringstream m;
m << "value " << v << " below min " << *col.min_value;
Expand Down
Loading