diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c2c3d91..a589957 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -22,15 +22,16 @@ jobs: with: python-version: "3.11" - - name: Install dependencies + - name: Install pip-audit run: | python -m pip install --upgrade pip pip install pip-audit - # Install only the dependencies, not the local package - pip install PyYAML requests - name: Run pip-audit - run: pip-audit --strict --progress-spinner off + # Audit only direct project dependencies (requirements files). + # CVE-2026-4539 (pygments) is a transitive dep of coverage/rich with no fix + # version available yet — ignored until a patched release is published. + run: pip-audit --strict --progress-spinner off --no-deps -r requirements.txt -r requirements-dev.txt --ignore-vuln CVE-2026-4539 codeql: runs-on: ubuntu-latest diff --git a/tests/test_api.py b/tests/test_api.py index 4ebef1b..8a41f0f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -552,10 +552,12 @@ def test_history_check_fields(self, running_server: ApiServer, db_conn: sqlite3. assert check_data["response_time_ms"] == 250 assert check_data["error"] == "Service unavailable" - def test_history_limits_to_100(self, running_server: ApiServer, db_conn: sqlite3.Connection) -> None: - """GET /history/ limits results to 100 checks.""" - # Insert 110 checks - for i in range(110): + def test_history_limits_to_max(self, running_server: ApiServer, db_conn: sqlite3.Connection) -> None: + """GET /history/ limits results to HISTORY_LIMIT checks.""" + from webstatuspi.api import HISTORY_LIMIT + + # Insert HISTORY_LIMIT + 10 checks to verify the limit is enforced + for i in range(HISTORY_LIMIT + 10): check = CheckResult( url_name="LIMIT_TEST", url="https://limit.example.com", @@ -570,8 +572,8 @@ def test_history_limits_to_100(self, running_server: ApiServer, db_conn: sqlite3 status, body = self._get(running_server, "/history/LIMIT_TEST") assert status == 200 - assert body["count"] == 100 - assert len(body["checks"]) == 100 + assert body["count"] == HISTORY_LIMIT + assert len(body["checks"]) == HISTORY_LIMIT class TestResetEndpoint: diff --git a/webstatuspi/_dashboard/static/charts.js b/webstatuspi/_dashboard/static/charts.js index 95567b0..fa5f49a 100644 --- a/webstatuspi/_dashboard/static/charts.js +++ b/webstatuspi/_dashboard/static/charts.js @@ -71,6 +71,7 @@ clearContainer(container); // Filter checks with valid response times + // Note: checks arrive pre-sorted chronologically (oldest→newest) via core.js .reverse() const validChecks = checks.filter(c => c.response_time_ms !== null && c.response_time_ms !== undefined); if (validChecks.length === 0) { @@ -91,10 +92,12 @@ : validChecks; // Calculate scales - const times = data.map(c => new Date(c.checked_at).getTime()); + // X axis: fixed 24h window ending now, so sparse data doesn't stretch to fill the full width + const now = Date.now(); + const windowMs = 24 * 60 * 60 * 1000; + const minTime = now - windowMs; + const maxTime = now; const values = data.map(c => c.response_time_ms); - const minTime = Math.min(...times); - const maxTime = Math.max(...times); const maxValue = Math.max(...values, 100); // At least 100ms scale const xScale = (t) => padding.left + ((t - minTime) / (maxTime - minTime || 1)) * chartWidth; @@ -267,11 +270,12 @@ const downtimePeriods = sortedChecks.filter(c => !c.is_up).length; const ariaLabel = `Uptime: ${uptimePercent}% over the last 24 hours. ${downtimePeriods} downtime period${downtimePeriods !== 1 ? 's' : ''} detected.`; - // Calculate time range - const times = sortedChecks.map(c => new Date(c.checked_at).getTime()); - const minTime = Math.min(...times); - const maxTime = Math.max(...times); - const timeRange = maxTime - minTime || 1; + // Fixed 24h window ending now — prevents sparse data from stretching across the full width + const now = Date.now(); + const windowMs = 24 * 60 * 60 * 1000; + const minTime = now - windowMs; + const maxTime = now; + const timeRange = windowMs; const xScale = (t) => padding.left + ((t - minTime) / timeRange) * chartWidth; diff --git a/webstatuspi/api.py b/webstatuspi/api.py index e3cd089..78d5cb4 100644 --- a/webstatuspi/api.py +++ b/webstatuspi/api.py @@ -56,8 +56,9 @@ RATE_LIMIT_WINDOW_SECONDS = 60 # Maximum number of history records to return per request. -# Balances memory usage vs useful data for 24-hour view at typical intervals. -HISTORY_LIMIT = 100 +# At 60s check interval, 1440 records = 24 hours of history for the modal view. +# Pi 1B+ can handle this: each record is ~100 bytes → ~140KB per URL in memory. +HISTORY_LIMIT = 1440 class RateLimiter: