diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddfc38f..6d4f2e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,18 +134,18 @@ jobs: run: | short_sha="${GITHUB_SHA:0:12}" mkdir -p release - bash scripts/package-release.sh v5.6.1 "$GITHUB_SHA" "release/Licora-5.6.1-ci-${short_sha}.zip" - cp "release/Licora-5.6.1-ci-${short_sha}.zip" "release/Licora-5.6.1.zip" - python3 scripts/build-update-manifest.py --version 5.6.1 --ref "$GITHUB_SHA" --package "release/Licora-5.6.1.zip" --output release/licora-update-manifest.json - rm "release/Licora-5.6.1.zip" + bash scripts/package-release.sh v5.7.1 "$GITHUB_SHA" "release/Licora-5.7.1-ci-${short_sha}.zip" + cp "release/Licora-5.7.1-ci-${short_sha}.zip" "release/Licora-5.7.1.zip" + python3 scripts/build-update-manifest.py --version 5.7.1 --ref "$GITHUB_SHA" --package "release/Licora-5.7.1.zip" --output release/licora-update-manifest.json + rm "release/Licora-5.7.1.zip" - name: Upload CI release-candidate artifact uses: actions/upload-artifact@v6 with: - name: Licora-5.6.1-${{ github.sha }} + name: Licora-5.7.1-${{ github.sha }} path: | - release/Licora-5.6.1-ci-*.zip - release/Licora-5.6.1-ci-*.zip.sha256 + release/Licora-5.7.1-ci-*.zip + release/Licora-5.7.1-ci-*.zip.sha256 release/licora-update-manifest.json if-no-files-found: error retention-days: 14 diff --git a/BASELINE_v5.7.0.md b/BASELINE_v5.7.0.md new file mode 100644 index 0000000..c8173a2 --- /dev/null +++ b/BASELINE_v5.7.0.md @@ -0,0 +1,53 @@ +# Licora v5.7.0 — Phase 2 Source Candidate Freeze + +## Parent Authority + +- Official parent baseline: `Licora-5.6.1.zip` +- Parent version/tag: `v5.6.1` +- Parent Git commit: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent release ZIP SHA-256: `0ca0ad76b5c0091912aa441fcac4c033a54bac630d6c1a7255ac5b2b75db5493` +- Parent release manifest source: `v5.6.1` + +The parent baseline is immutable. v5.7.0 is produced in an isolated work copy and does not rewrite the v5.6.1 freeze. + +## Candidate Identity + +- Target version: `5.7.0` +- Scope: Dashboard Phase 2 only +- Database migration: `NONE` +- Deleted files: `NONE` +- External API contract change: `NONE` +- Dashboard backend/read-model contract change: `NONE` +- License/device enforcement change: `NONE` +- Authentication/role change: `NONE` +- Cron mutation change: `NONE` +- Updater protocol/state-machine change: `NONE` +- Shared sidebar/topbar redesign: `NONE` + +## Phase 2 Runtime Scope + +1. compact Dashboard operations composition; +2. measured system-status strip; +3. four primary truthful KPI cards; +4. dedicated `admin/assets/js/dashboard.js` browser controller; +5. authenticated 30-second AJAX refresh through the existing Phase 1 endpoint; +6. manual refresh and last-updated feedback; +7. in-place Chart.js/KPI/activity/top-license updates; +8. request-overlap protection; +9. stale-data preservation/Retry state and auth-expiry polling shutdown; +10. responsive/accessibility behavior and dedicated Phase 2 tests. + +## Acceptance State + +Current source-candidate evidence: + +- full local verifier: `PASS`; +- targeted Phase 2 contract/browser-runtime tests: `PASS`; + +The candidate must not be treated as a published baseline until the remaining required gates have evidence: + +- required remote CI/MySQL matrix after an authorized push; +- manual desktop/tablet/mobile production/staging smoke; +- authorized merge/tag/release workflow. + +The exact v5.7.0 commit SHA does not exist until the user commits the reviewed delta. diff --git a/BASELINE_v5.7.1.md b/BASELINE_v5.7.1.md new file mode 100644 index 0000000..1a4ce6f --- /dev/null +++ b/BASELINE_v5.7.1.md @@ -0,0 +1,51 @@ +# Licora v5.7.1 — Dashboard Phase 2 Corrective Source Freeze + +## Parent Authority + +- Official parent baseline: `Licora_v5.7.0_Baseline.zip` +- Parent baseline SHA-256: `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +- Parent embedded Git HEAD: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent source version: `5.7.0` +- Parent GitHub tag status at audit time: `NOT PUBLISHED` +- Parent Phase 2 state: source implemented + locally verified; remote/live acceptance pending + +The uploaded v5.7.0 archive is immutable forensic authority. v5.7.1 is produced only in an isolated work copy. + +## Corrective Scope Lock + +Only the latest Phase 2 verification/fix scope is changed: + +1. preserve `Retry` after failed-refresh cleanup; +2. preserve the 401/session-expiry refresh lock and `Refresh paused` UI; +3. capture synchronous request transport failures in the normal stale/error lifecycle; +4. advance `lastSuccessAt` only after successful render completion; +5. extend Dashboard browser/runtime tests for those four cases; +6. align current source/release/docs identity to `5.7.1`. + +## Frozen / Unchanged + +- Dashboard read model and authenticated JSON endpoint +- database schema and migration set +- API v1/v2 request/response/auth/crypto contracts +- license/device state/enforcement +- authentication/roles/session policy +- Cron mutation behavior +- updater protocol/signing/rollback/state machine +- shared sidebar/topbar and non-Dashboard admin UI +- Phase 2 layout, KPI definitions, chart semantics, polling cadence and existing navigation routes + +## Chrome/Browser Finding + +No Chrome launcher/downloader implementation exists in the v5.7.0 baseline application runtime. No browser-specific downloader is introduced in v5.7.1. + +## Acceptance State + +The full `python3 scripts/verify-local.py` gate passed after corrective source/tests/version/documentation alignment. Remote CI/MySQL and manual live/staging UI smoke remain required before Phase 2 is marked final `COMPLETE + VERIFIED`. + +## Release Compatibility + +The v5.7.1 signed update specification accepts `5.6.1` and `5.7.0`, with no migrations and no delete list. This allows direct upgrade from the published v5.6.1 release while also supporting installations where the v5.7.0 source baseline was already applied. + +## Source Freeze Packaging + +The v5.7.1 source baseline freeze is packaged from the verified work tree with repository `.git` metadata excluded. This avoids treating an uncommitted local Git object database as product source. The external baseline ZIP SHA-256 is recorded alongside the delivered artifact; the authoritative Git commit SHA will be established only after the user performs the reviewed commit/push flow. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f5ef28..3d8b891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,36 @@ All notable public-release changes are recorded here. Historical project notes r ### Planned -- Continue the reviewed Dashboard production program with Phase 2: compact UI and reload-free refresh. +- No additional Dashboard scope is approved beyond the v5.7.1 Phase 2 corrective release candidate. + +## [5.7.1] - 2026-08-20 + +### Fixed +- Preserved the Dashboard `Retry` label after failed-refresh loading cleanup instead of resetting it to `Refresh`. +- Preserved the `401 AUTH_REQUIRED` refresh lock and `Refresh paused` state after request cleanup. +- Routed synchronous Dashboard request-transport exceptions through the normal stale/error lifecycle so the in-flight lock and loading state are always released. +- Advanced the Dashboard last-success timestamp only after a snapshot completes rendering successfully. +- Expanded Dashboard browser/runtime regression coverage for the corrected stale/auth/transport/render lifecycle. + +### Compatibility +- No database migration, file deletion, backend Dashboard contract change, external API change, license/device enforcement change, authentication/role change, Cron mutation change, updater protocol change, shared shell change or new browser dependency. +- Signed update compatibility accepts both published `v5.6.1` and an already-applied `v5.7.0` source baseline. + +## [5.7.0] - 2026-08-20 (source baseline; superseded by 5.7.1 corrective) + +### Changed +- Rebuilt the Dashboard as a compact operations view while preserving the existing Licora light shell, sidebar, topbar and other admin pages. +- Replaced the 30-second full-page Dashboard reload with authenticated 30-second AJAX polling against the existing read-only `admin/ajax/dashboard-data.php` contract. +- Added manual refresh, last-updated feedback, stale-data handling, session-expiry handling and request-overlap protection without changing backend business truth. +- Updated API and expiration charts in place, combined source-labelled API v1/v2 recent activity, and converted Quick Actions to compact links over existing routes. + +### Added +- Added `admin/assets/js/dashboard.js` as the dedicated Dashboard refresh/controller layer. +- Added Phase 2 source/DOM contract and browser-runtime tests covering polling, manual refresh, overlap prevention, stale behavior and auth expiry. + +### Compatibility +- No database migration, file deletion, external API contract change, license/device enforcement change, authentication/role change, Cron mutation change, updater protocol change, installer schema change, sidebar/topbar redesign or shared application architecture change. +- Signed update compatibility starts from the frozen official `v5.6.1` baseline. ## [5.6.1] - 2026-08-20 diff --git a/DASHBOARD_PRODUCTION_UPDATE_INDEX.md b/DASHBOARD_PRODUCTION_UPDATE_INDEX.md index 28b2ac8..e82eb87 100644 --- a/DASHBOARD_PRODUCTION_UPDATE_INDEX.md +++ b/DASHBOARD_PRODUCTION_UPDATE_INDEX.md @@ -2,18 +2,17 @@ ## Authority -এই documentation program v5.5.1 থেকে শুরু হয়েছিল; বর্তমান corrective source authority হলো uploaded **Licora v5.6.0 Official Baseline Freeze**। +এই documentation program v5.5.1 থেকে শুরু হয়েছিল; বর্তমান immutable source authority হলো uploaded **Licora v5.7.0 Official Baseline Freeze**। -- Current baseline ZIP SHA-256: `ba99c6e4fd74c2b59d392c3010b3aefe493390b3b0b0c94bd3c211218f14d597` -- Current baseline Git commit: `5c685636e955422bc70e3bf07694f55d9c7fb1dc` -- Current baseline Git tree: `848801c1785ebba0b2523a34afcf6af3ee05d5d6` -- Current baseline version: `5.6.0` +- Current baseline ZIP SHA-256: `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +- Current baseline embedded Git HEAD: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Current baseline version: `5.7.0` - Original Phase-program baseline: `v5.5.1 / 2f48ef569e6c532ab0de974a418c644e4ea8423f` -- Development implementation status: **v5.6.1 Phase 1 COMPLETE + VERIFIED — PR #8 Actions run 32423210356 PASS** +- Development implementation status: **v5.7.1 Phase 2 corrective source + local verification PASS — remote CI/live acceptance gates pending** - Planned update phases: **2** -- Current corrective target: **v5.6.1 — Phase 1 verified baseline; Phase 2 NOT STARTED** +- Current target: **v5.7.1 — Phase 2 verification corrective candidate** -এই document set Phase 1 runtime implementation এবং v5.6.1 corrective verification state record করে। v5.6.1 কোনো database migration, external API contract change, license/device enforcement change, Cron mutation change, updater protocol change বা Phase 2 polling/UI feature যোগ করে না। +এই document set Phase 1 verified foundation, v5.7.0 Phase 2 source baseline এবং v5.7.1 corrective verification candidate record করে। Phase 2 Dashboard presentation/browser behavior পরিবর্তন করে, কিন্তু database schema, external API contracts, license/device enforcement, authentication/roles, Cron mutation behavior, updater protocol এবং shared sidebar/topbar architecture অপরিবর্তিত রাখে। ## Document Map diff --git a/README.md b/README.md index 749738e..b2554e8 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Licora is maintained by **Vib Tools**. Vib Tools is a professional tools and dig - Device registration, activity tracking, revocation, blacklist handling, and risk indicators. - Role-aware admin panel for super administrators, managers, and viewers. - Audit trail, operational logs, CSV exports, SQL backup generation, and health checks. +- Reload-free operational Dashboard with truthful license/device/API/expiration metrics, manual refresh, stale-data feedback, and source-separated API activity. - CSRF tokens for admin mutations, prepared SQL statements, password hashing, rate limiting, and session hardening. - Scheduled cleanup and expiring-license reporting through CLI cron scripts. - Super-Admin-only Secure Update Center with signed GitHub release manifests, preflight, staged installation, persistent live logs, migration tracking, and rollback protection. @@ -124,7 +125,7 @@ The application accepts deployment-specific values through environment variables | Database password | `LICENSE_DB_PASS` | empty | | Application name | `APP_NAME` | `Licora` | | Application URL | `APP_URL` | `http://localhost` | -| Application version | `APP_VERSION` | `5.6.1` | +| Application version | `APP_VERSION` | `5.7.1` | | Environment | `APP_ENV` | `production` | | Encryption key | `LICENSE_ENCRYPTION_KEY` | empty fallback | | API limit | `API_RATE_LIMIT` | `1000` | @@ -155,6 +156,8 @@ The validation script checks PHP syntax, security behavior, compatibility invari - [Secure in-app updater](docs/UPDATER.md) - [UI design system](docs/UI_DESIGN_SYSTEM.md) - [Troubleshooting](docs/TROUBLESHOOTING.md) +- [v5.7.1 release notes](RELEASE_NOTES_v5.7.1.md) +- [v5.7.0 release notes](RELEASE_NOTES_v5.7.0.md) - [v5.6.1 release notes](RELEASE_NOTES_v5.6.1.md) - [v5.6.0 release notes](RELEASE_NOTES_v5.6.0.md) - [v5.5.1 release notes](RELEASE_NOTES_v5.5.1.md) @@ -173,9 +176,17 @@ The validation script checks PHP syntax, security behavior, compatibility invari - [Dependency review](audit/DEPENDENCY_REPORT.md) +## Dashboard Phase 2 verification corrective candidate (v5.7.1) + +Licora v5.7.1 corrects the client-side refresh lifecycle found during forensic review of the v5.7.0 Phase 2 source baseline: stale refresh keeps `Retry`, `401 AUTH_REQUIRED` keeps refresh paused/disabled, synchronous transport errors are captured without leaving the request lock stuck, and the last-success timestamp advances only after a successful render. Phase 2 layout/data semantics, backend contracts, database schema, APIs, licensing/device enforcement, authentication, Cron behavior, updater protocol and shared shell remain unchanged. + +## Dashboard Phase 2 source baseline (v5.7.0) + +Licora v5.7.0 builds on the frozen/published v5.6.1 Phase 1 baseline. The Dashboard keeps its server-rendered initial snapshot but replaces the former full-page 30-second reload with authenticated 30-second AJAX refresh, manual Refresh, last-updated/stale/session feedback, request-overlap protection and in-place chart/KPI/activity updates. Phase 1 backend data semantics, database schema, APIs, license/device enforcement, authentication/roles, Cron mutation behavior and updater protocol remain unchanged. + ## Phase 1 verification corrective update (v5.6.1) -Licora v5.6.1 is a no-migration corrective update over the applied v5.6.0 Phase 1 source baseline; v5.6.0 was not published as a GitHub tag/release and is superseded by v5.6.1. It fixes the MySQL integration-test foreign-key cleanup exposed by PR #8 CI, aligns the authenticated Dashboard JSON envelope with its documented top-level `recent_activity` field, and makes the Dashboard API v2 `Ready` status depend on a verified matching server signing key pair rather than the public key alone. Licora remains a browser-based PHP application and contains no Google Chrome installer/downloader dependency. Phase 2 reload-free polling/UI work remains unimplemented. +Licora v5.6.1 is a no-migration corrective update over the applied v5.6.0 Phase 1 source baseline; v5.6.0 was not published as a GitHub tag/release and is superseded by v5.6.1. It fixes the MySQL integration-test foreign-key cleanup exposed by PR #8 CI, aligns the authenticated Dashboard JSON envelope with its documented top-level `recent_activity` field, and makes the Dashboard API v2 `Ready` status depend on a verified matching server signing key pair rather than the public key alone. Licora remains a browser-based PHP application and contains no Google Chrome installer/downloader dependency. At the v5.6.1 baseline, Phase 2 had not yet been implemented; the separately scoped v5.7.0 source baseline added that browser refresh layer without changing the Phase 1 backend contract. ## Dashboard data truth and read model (v5.6.0) diff --git a/RELEASE_COMMANDS_v5.7.0.md b/RELEASE_COMMANDS_v5.7.0.md new file mode 100644 index 0000000..98e25ef --- /dev/null +++ b/RELEASE_COMMANDS_v5.7.0.md @@ -0,0 +1,68 @@ +# Licora v5.7.0 — Phase 2 Review / Publication Commands + +This file records the reviewed Windows/CMD sequence for the v5.7.0 Dashboard Phase 2 delta. It does not authorize GitHub writes by itself. + +## Preconditions + +- Official parent baseline: published `v5.6.1` +- Parent commit: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent release ZIP SHA-256: `0ca0ad76b5c0091912aa441fcac4c033a54bac630d6c1a7255ac5b2b75db5493` +- Apply the replace-ready delta only to a clean checkout of the reviewed parent baseline/main. +- Stop on any unexpected file, error, failed check or merge conflict. + +## Create the Phase 2 branch + +```cmd +git switch main +git pull --ff-only origin main +git status --short --branch +git switch -c feature/v5.7.0-dashboard-phase2 +``` + +After extracting the reviewed delta into the repository root: + +```cmd +git diff --check +git status --short +``` + +Only the reviewed v5.7.0 delta paths should appear. + +## Exact staging command + +Never use `git add -A`, `git add .` or `git add --all` for this release. + +```cmd +git add -- .github/workflows/ci.yml BASELINE_v5.7.0.md CHANGELOG.md DASHBOARD_PRODUCTION_UPDATE_INDEX.md README.md RELEASE_COMMANDS_v5.7.0.md RELEASE_NOTES_v5.7.0.md REPOSITORY_METADATA.md ROADMAP.md admin/assets/css/admin-ui.css admin/assets/js/dashboard.js admin/index.php audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md audit/V5.7.0_DELTA_MANIFEST.md audit/V5.7.0_DELTA_SHA256SUMS.txt config.sample.php docs/ACTUAL_IMPLEMENTATION_LEDGER.md docs/ARCHITECTURE.md docs/CONFIGURATION.md docs/DASHBOARD_CHANGE_CONTROL.md docs/DASHBOARD_DATA_CONTRACT.md docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md docs/DASHBOARD_UPDATE_PHASE_LOG.md docs/ERROR_HANDLING_BASELINE_AND_TARGET.md docs/FEATURE_MATRIX.md docs/INSTALLATION.md docs/RELEASE.md docs/UI_DESIGN_SYSTEM.md docs/UPGRADE_GUIDE.md includes/config.php includes/installation.php install.php scripts/verify-local.py tests/compatibility_regression.php tests/dashboard_browser_runtime.js tests/dashboard_data_contract.php tests/dashboard_phase2_contract.php tests/installer_smoke.php tests/release_readiness.php tests/updater_state_machine.php update/release-spec.json +``` + +Then inspect exactly what is staged: + +```cmd +git diff --cached --check +git diff --cached --name-status +git status +``` + +## Commit / push — only after explicit GitHub-write authorization + +```cmd +git commit -m "feat: add reload-free compact Dashboard in Licora v5.7.0" +git push -u origin feature/v5.7.0-dashboard-phase2 +``` + +Create/review a PR targeting `main`, then run the required CI once. Do not merge on a failing/pending gate. + +## Tag / release — only after Phase 2 final acceptance and explicit authorization + +Do not tag until the PR is merged, `main` is clean/synchronized, remote CI is green, and the required manual production/staging Dashboard smoke is accepted. + +```cmd +git switch main +git pull --ff-only origin main +git status --short --branch +git tag -a v5.7.0 -m "Licora v5.7.0 - Compact Dashboard and Reload-Free Refresh" +git push origin v5.7.0 +``` + +The tag-triggered release workflow must then build/verify the exact-tag ZIP, run the required database gates, build/sign the update manifest and publish the GitHub Release. diff --git a/RELEASE_COMMANDS_v5.7.1.md b/RELEASE_COMMANDS_v5.7.1.md new file mode 100644 index 0000000..1b3f5e3 --- /dev/null +++ b/RELEASE_COMMANDS_v5.7.1.md @@ -0,0 +1,111 @@ +# Licora v5.7.1 — Phase 2 Verification Fix Review / Publication Commands + +This file records the reviewed Windows/CMD sequence for the v5.7.1 corrective candidate. It does not authorize GitHub writes by itself. + +## Source authority + +- Official corrective parent: uploaded `Licora_v5.7.0_Baseline.zip` +- Parent ZIP SHA-256: `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +- Parent embedded Git HEAD: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent local branch: `feature/v5.7.0-dashboard-phase2` +- Parent GitHub v5.7.0 tag: not published at audit time +- Target: `v5.7.1` +- Replace-ready corrective delta: `37 paths` relative to the uploaded v5.7.0 baseline +- Combined Git publication scope: `47 paths` relative to Git HEAD `4b430b7` +- Delete list: none +- Database migrations: none + +## Why Git publication has 47 paths + +The uploaded v5.7.0 baseline contains the original Phase 2 implementation as an **uncommitted** 41-path work-tree delta over the published v5.6.1 commit. v5.7.1 then corrects that baseline and adds corrective/version/audit files. + +Therefore: + +- the delivered v5.7.1 replace-ready delta contains only the **37 paths that differ from the uploaded v5.7.0 baseline**; +- but the Git commit that publishes Phase 2 must include the **full 47-path combined work tree** relative to the current Git HEAD, otherwise original Phase 2 files would be omitted. + +Do not use `git add -A`, `git add .` or `git add --all`. + +## Apply v5.7.1 corrective delta + +Stay on the existing Phase 2 branch: + +```cmd +git status --short --branch +``` + +Expected branch: + +```text +feature/v5.7.0-dashboard-phase2 +``` + +Extract `Licora-v5.7.1-Dashboard-Phase2-Verification-Fix-Delta.zip` over the repository root and overwrite matching files. + +Then: + +```cmd +git diff --check +git status --short +``` + +The combined status should contain exactly 47 reviewed paths: 32 modified and 15 untracked. + +## Exact combined staging command + +```cmd +git add -- .github/workflows/ci.yml BASELINE_v5.7.0.md BASELINE_v5.7.1.md CHANGELOG.md DASHBOARD_PRODUCTION_UPDATE_INDEX.md README.md RELEASE_COMMANDS_v5.7.0.md RELEASE_COMMANDS_v5.7.1.md RELEASE_NOTES_v5.7.0.md RELEASE_NOTES_v5.7.1.md REPOSITORY_METADATA.md ROADMAP.md admin/assets/css/admin-ui.css admin/assets/js/dashboard.js admin/index.php audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md audit/V5.7.0_DELTA_MANIFEST.md audit/V5.7.0_DELTA_SHA256SUMS.txt audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md audit/V5.7.1_DELTA_MANIFEST.md audit/V5.7.1_DELTA_SHA256SUMS.txt config.sample.php docs/ACTUAL_IMPLEMENTATION_LEDGER.md docs/ARCHITECTURE.md docs/CONFIGURATION.md docs/DASHBOARD_CHANGE_CONTROL.md docs/DASHBOARD_DATA_CONTRACT.md docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md docs/DASHBOARD_UPDATE_PHASE_LOG.md docs/ERROR_HANDLING_BASELINE_AND_TARGET.md docs/FEATURE_MATRIX.md docs/INSTALLATION.md docs/RELEASE.md docs/UI_DESIGN_SYSTEM.md docs/UPGRADE_GUIDE.md includes/config.php includes/installation.php install.php scripts/verify-local.py tests/compatibility_regression.php tests/dashboard_browser_runtime.js tests/dashboard_data_contract.php tests/dashboard_phase2_contract.php tests/installer_smoke.php tests/release_readiness.php tests/updater_state_machine.php update/release-spec.json +``` + +Then inspect once: + +```cmd +git diff --cached --check +git diff --cached --name-status +git status +``` + +Expected: exactly 47 staged paths and no unstaged Phase 2/v5.7.1 paths. + +## Commit / push — only after explicit GitHub-write authorization + +```cmd +git commit -m "feat: complete Dashboard Phase 2 with v5.7.1 verification fixes" +git push -u origin feature/v5.7.0-dashboard-phase2 +``` + +Recommended PR title: + +```text +Licora v5.7.1 - Dashboard Phase 2 Complete with Verification Fixes +``` + +Recommended PR body summary: + +```text +Implements the approved Dashboard Phase 2 compact/reload-free UI over the published v5.6.1 base and includes the v5.7.1 corrective refresh-lifecycle fixes found during forensic verification. No database migration, file deletion, external API contract change, license/device enforcement change, auth/role change, Cron mutation change, updater protocol change, or shared sidebar/topbar redesign. +``` + +After push, run the PR CI once. Do not merge while any required gate is pending or failing. + +## Tag / release — only after final Phase 2 acceptance and explicit authorization + +Do not tag until: + +1. PR CI/MySQL/PHP/Windows gates are green; +2. required manual desktop/tablet/mobile staging or production smoke is accepted; +3. PR is merged; +4. local `main` is clean and synchronized. + +Then: + +```cmd +git switch main +git pull --ff-only origin main +git status --short --branch +git tag -a v5.7.1 -m "Licora v5.7.1 - Dashboard Phase 2 Complete and Verified" +git show --no-patch --decorate v5.7.1 +git push origin v5.7.1 +``` + +The tag-triggered release workflow must verify the exact tag, run the database gate, build the exact-tag ZIP/checksum, build/sign/verify the updater manifest, and publish the GitHub Release. diff --git a/RELEASE_NOTES_v5.7.0.md b/RELEASE_NOTES_v5.7.0.md new file mode 100644 index 0000000..0aaf6a1 --- /dev/null +++ b/RELEASE_NOTES_v5.7.0.md @@ -0,0 +1,51 @@ +# Licora v5.7.0 — Compact Dashboard & Reload-Free Refresh + +**Release type:** Dashboard Phase 2 UI/runtime release candidate +**Official parent baseline:** v5.6.1 / `4b430b77ccc303aebeadc2852bebd3f11f67452a` +**Database migration:** None +**Deleted files:** None +**External API v1/v2 contracts:** Unchanged +**License/device enforcement:** Unchanged +**Authentication/roles:** Unchanged +**Cron mutation behavior:** Unchanged +**Updater protocol/state machine:** Unchanged +**Shared sidebar/topbar:** Unchanged + +## Dashboard Phase 2 + +v5.7.0 keeps the verified v5.6.1 Dashboard read model/error contract and changes only the approved Dashboard presentation/browser interaction layer. + +### Changed + +- Rebuilt `admin/index.php` as a compact operations Dashboard while retaining server-rendered initial content. +- Removed the former 30-second `window.location.reload()` behavior. +- Added authenticated 30-second AJAX refresh through the existing GET-only `admin/ajax/dashboard-data.php` endpoint. +- Added manual Refresh and a last-successful-update indicator. +- Updates license/device KPIs, health facts, API/expiration charts, recent activity and top API v1 licenses in place. +- Combines recent API v1 calls and API v2 audit events in one source-labelled operational activity view without changing the backend source distinction. +- Replaces large Quick Action tiles with compact links to existing admin routes. + +### Error and session behavior + +- Prevents overlapping poll/manual requests with an in-flight lock. +- Refresh failures preserve the last successfully rendered data and show a stale-data/Retry state instead of replacing values with zero or blank content. +- An endpoint `401 AUTH_REQUIRED` pauses polling and surfaces the existing sign-in path. +- No backend exception/credential/key detail is exposed by the browser controller. + +### Accessibility and responsive behavior + +- Refresh is a real keyboard-accessible button. +- Refresh/stale/auth feedback uses an ARIA live status region. +- System facts include visible text in addition to color indicators. +- Dashboard layout collapses for tablet/mobile while the existing shared shell remains unchanged. +- Refresh animation respects `prefers-reduced-motion`. + +## Verification added + +- `tests/dashboard_phase2_contract.php` verifies Dashboard DOM/scope/runtime contract and the frozen Phase 1 endpoint boundary. +- `tests/dashboard_browser_runtime.js` verifies polling cadence, manual refresh, overlap prevention, successful rendering lifecycle, stale preservation and 401 polling shutdown. +- Existing Dashboard data/DB, API, installer, security, updater and UI regression gates remain required. + +## Compatibility + +v5.7.0 declares an empty migration list and empty delete list. The signed update source is the frozen official `v5.6.1` baseline. No new database table/column, external API response/request field, license/device state transition, auth policy, Cron write behavior or updater protocol is introduced. diff --git a/RELEASE_NOTES_v5.7.1.md b/RELEASE_NOTES_v5.7.1.md new file mode 100644 index 0000000..9d6307e --- /dev/null +++ b/RELEASE_NOTES_v5.7.1.md @@ -0,0 +1,55 @@ +# Licora v5.7.1 — Dashboard Phase 2 Verification Fix + +**Release type:** Corrective Dashboard Phase 2 verification release candidate +**Official source baseline:** `Licora_v5.7.0_Baseline.zip` +**Baseline ZIP SHA-256:** `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +**Embedded Git HEAD:** `4b430b77ccc303aebeadc2852bebd3f11f67452a` +**Database migration:** None +**Deleted files:** None +**External API v1/v2 contracts:** Unchanged +**License/device enforcement:** Unchanged +**Authentication/roles:** Unchanged +**Cron mutation behavior:** Unchanged +**Updater protocol/state machine:** Unchanged +**Shared sidebar/topbar:** Unchanged + +## Why v5.7.1 exists + +The v5.7.0 Phase 2 source candidate passed the complete local verifier, but a manual forensic review of the Dashboard refresh lifecycle found four client-side state-management defects that were not covered by the original browser-runtime test. + +### Fixed + +1. **Stale refresh label preservation** — a failed refresh now keeps the reviewed `Retry` label after loading cleanup instead of being overwritten back to `Refresh`. +2. **401/auth lock preservation** — `AUTH_REQUIRED` now keeps manual refresh disabled and preserves `Refresh paused` after the request `finally` path runs. +3. **Synchronous transport failure recovery** — a synchronous request/transport exception is now captured by the normal Promise error path, releases the in-flight lock, clears loading state and surfaces stale data rather than escaping and leaving the Dashboard stuck. +4. **Last-success timestamp correctness** — `lastSuccessAt` now advances only after the new snapshot finishes rendering successfully. A render failure retains the prior successful timestamp. + +The Dashboard browser-runtime regression test was expanded to exercise each corrective case. + +## Phase 2 behavior retained + +- compact operational Dashboard composition; +- server-rendered initial snapshot; +- authenticated 30-second AJAX polling through the unchanged Phase 1 endpoint; +- manual Refresh; +- in-place KPI, measured health, Chart.js, recent-activity and top-license updates; +- overlap protection; +- stale-data preservation; +- responsive/accessibility behavior. + +## Browser portability finding + +Licora is server-rendered PHP web software. The audited v5.7.0 baseline contains no Google Chrome executable launcher, Chrome installer/downloader, or Google Chrome download URL in the application runtime. The reported “Chrome unavailable → Chrome download failed” behavior therefore is not produced by this Licora source tree. No speculative browser downloader is added. The existing verifier guard against a Chrome-specific runtime dependency remains active. + +## Update compatibility + +`v5.7.0` is not present as a published GitHub tag on the audited repository at the time of this corrective preparation. The v5.7.1 release specification therefore accepts both: + +- published `v5.6.1`; and +- an already-applied `v5.7.0` source baseline. + +This corrective release has an empty migration list and empty delete list. + +## Remaining acceptance gates + +Source/local verification can be completed before publication. Phase 2 must not be marked final `COMPLETE + VERIFIED` until the authorized remote CI/MySQL matrix and the required desktop/tablet/mobile live or staging smoke are accepted. diff --git a/REPOSITORY_METADATA.md b/REPOSITORY_METADATA.md index 4ef8f2e..8951382 100644 --- a/REPOSITORY_METADATA.md +++ b/REPOSITORY_METADATA.md @@ -27,22 +27,47 @@ Licora v5.1.0 adds a ten-step first-run installer, delimiter-aware schema execution, secure administrator and secret generation, atomic private configuration, installation locking, optional demo data, database-port support, safe installer diagnostics, and expanded regression validation. The release preserves the existing license engine, API contracts, database schema, routes, admin UI, cron entry points, and encrypted-data compatibility. -## v5.6.1 release candidate +## v5.7.1 corrective release candidate -- **Intended tag:** `v5.6.1` -- **Publication status:** pending corrected PR #8 CI, merge, and tag publication +- **Intended tag:** `v5.7.1` +- **Publication status:** source candidate only; GitHub write/release not yet authorized +- **Title:** `Licora v5.7.1 — Dashboard Phase 2 Verification Fix` +- **Release notes:** `RELEASE_NOTES_v5.7.1.md` +- **Primary assets:** `Licora-5.7.1.zip`, `.zip.sha256`, `licora-update-manifest.json`, `licora-update-manifest.sig` +- **Database migration:** none +- **Delete list:** empty +- **Supported update sources:** `v5.6.1`, applied `v5.7.0` + +Licora v5.7.1 preserves the v5.7.0 compact/reload-free Dashboard scope and corrects four refresh-lifecycle defects: stale Retry label persistence, 401 refresh-lock persistence, synchronous transport-error capture, and last-success timestamp advancement only after successful render. + +## v5.7.0 source baseline + +- **Intended tag:** `v5.7.0` +- **Publication status:** not published as a GitHub tag; uploaded source baseline is superseded by the v5.7.1 corrective candidate +- **Title:** `Licora v5.7.0 — Compact Dashboard & Reload-Free Refresh` +- **Release notes:** `RELEASE_NOTES_v5.7.0.md` +- **Primary assets:** `Licora-5.7.0.zip`, `.zip.sha256`, `licora-update-manifest.json`, `licora-update-manifest.sig` +- **Database migration:** none +- **Delete list:** empty +- **Supported update source:** `v5.6.1` + +Licora v5.7.0 implements Dashboard Phase 2 over the frozen v5.6.1 release: compact operational composition, authenticated reload-free polling, manual refresh, last-updated/stale/auth feedback, in-place chart/activity updates and request-overlap protection. Backend data truth, APIs, licensing/device enforcement, schema, Cron mutation behavior and updater protocol remain unchanged. + +## v5.6.1 published baseline + +- **Tag:** `v5.6.1` +- **Commit:** `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- **Publication status:** published and frozen as the official Phase 2 parent baseline - **Title:** `Licora v5.6.1 — Dashboard Phase 1 Verification Fix` - **Release notes:** `RELEASE_NOTES_v5.6.1.md` - **Primary assets:** `Licora-5.6.1.zip`, `.zip.sha256`, `licora-update-manifest.json`, `licora-update-manifest.sig` - **Database migration:** none -- **Delete list:** empty -- **Supported update sources:** `v5.5.1`, `v5.6.0` -Licora v5.6.1 corrects the Phase 1 DB integration fixture, restores exact Dashboard JSON/data-contract parity, and makes API v2 Dashboard readiness require a valid matching signing key pair. Phase 2 remains pending. +Licora v5.6.1 is the verified Phase 1 baseline. Its release ZIP SHA-256 is `0ca0ad76b5c0091912aa441fcac4c033a54bac630d6c1a7255ac5b2b75db5493`. ## v5.6.0 source baseline (not published) -- **Tag status:** `v5.6.0` was not published on GitHub; this source baseline is superseded by the v5.6.1 corrective candidate +- **Tag status:** `v5.6.0` was not published on GitHub; this source baseline is superseded by the published v5.6.1 corrective release - **Title:** `Licora v5.6.0 — Dashboard Data Truth & Read Model` - **Release notes:** `RELEASE_NOTES_v5.6.0.md` - **Primary assets:** `Licora-5.6.0.zip`, `.zip.sha256`, `licora-update-manifest.json`, `licora-update-manifest.sig` diff --git a/ROADMAP.md b/ROADMAP.md index 931de86..0b5fdfa 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,9 +1,9 @@ # Roadmap -## Dashboard production program (v5.6.x) +## Dashboard production program (v5.6.1 → v5.7.1) -- **Phase 1 — Data Truth, Backend Read Model & Error Contract:** implemented in v5.6.0 and corrected in v5.6.1 after PR #8 exposed a MySQL fixture FK-order failure and contract/readiness mismatches; final remote DB/CI verification remains the release gate. -- **Phase 2 — Compact UI, Reload-Free Refresh & Production Gate:** pending; no Phase 2 polling/UI implementation is included in v5.6.1. +- **Phase 1 — Data Truth, Backend Read Model & Error Contract:** COMPLETE + VERIFIED in the published v5.6.1 baseline. +- **Phase 2 — Compact UI, Reload-Free Refresh & Production Gate:** implemented in the v5.7.0 source baseline and corrected in the v5.7.1 candidate with server-rendered fallback, 30-second authenticated AJAX polling, manual refresh, in-place KPI/chart/activity updates, overlap protection and stale/auth UX. v5.7.1 additionally verifies stale/auth/transport/render lifecycle correctness; remote CI and live production smoke remain release gates. Roadmap items are proposals and must be implemented through reviewed, backward-compatible changes. diff --git a/admin/assets/css/admin-ui.css b/admin/assets/css/admin-ui.css index 04fa54c..1355b0b 100644 --- a/admin/assets/css/admin-ui.css +++ b/admin/assets/css/admin-ui.css @@ -1,2 +1,80 @@ /* Compatibility entrypoint. All Licora admin visuals are sourced from the v5.4.0 component system. */ @import url('./licora/licora-ui.css'); + +/* Licora v5.7.0 Dashboard Phase 2 — scoped compact operations view. */ +.dashboard-page .dashboard-header { align-items: center; } +.dashboard-page .dashboard-refresh-group { display: flex; align-items: center; justify-content: flex-end; gap: 10px; } +.dashboard-page .dashboard-refresh-meta { display: grid; gap: 1px; text-align: right; color: var(--text-secondary); font-size: 10px; line-height: 1.25; } +.dashboard-page .dashboard-refresh-meta strong { color: var(--text-primary); font-size: 11px; font-weight: var(--font-weight-bold); } +.dashboard-page [data-dashboard-refresh].is-loading i { animation: dashboard-spin .8s linear infinite; } +.dashboard-page .dashboard-state { display: flex; align-items: center; gap: 7px; min-height: 34px; margin: -2px auto 10px; padding: 7px 10px; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--component-feedback-bg); color: var(--text-secondary); font-size: 11px; } +.dashboard-page .dashboard-state[hidden] { display: none!important; } +.dashboard-page .dashboard-state.is-warning { border-color: var(--state-warning-border); background: var(--state-warning-bg); color: var(--state-warning-text); } +.dashboard-page .dashboard-state.is-danger { border-color: var(--state-danger-border); background: var(--state-danger-bg); color: var(--state-danger-text); } +.dashboard-page .dashboard-state a { margin-left: auto; color: inherit; font-weight: var(--font-weight-bold); } + +.dashboard-page .dashboard-health-strip { display: grid; grid-template-columns: repeat(5, minmax(0,1fr)); gap: 1px; margin-bottom: 12px; overflow: hidden; border: 1px solid var(--border-card); border-radius: var(--radius-lg); background: var(--border-inner); } +.dashboard-page .dashboard-health-item { min-width: 0; display: flex; align-items: center; gap: 6px; padding: 8px 10px; background: var(--bg-surface-card); color: var(--text-secondary); font-size: 10.5px; } +.dashboard-page .dashboard-health-item strong { margin-left: auto; overflow: hidden; color: var(--text-primary); font-size: 10.5px; text-overflow: ellipsis; white-space: nowrap; } +.dashboard-page .dashboard-health-dot { width: 7px; height: 7px; flex: 0 0 7px; border-radius: 999px; background: var(--text-muted); } +.dashboard-page .dashboard-health-item.is-ok .dashboard-health-dot { background: var(--status-success); } +.dashboard-page .dashboard-health-item.is-warning .dashboard-health-dot { background: var(--status-warning); } +.dashboard-page .dashboard-health-item.is-danger .dashboard-health-dot { background: var(--status-danger); } + +.dashboard-page .dashboard-kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: 10px; margin-bottom: 12px; } +.dashboard-page .dashboard-kpi-card { min-width: 0; display: grid; grid-template-columns: 34px minmax(0,1fr); gap: 10px; align-items: start; padding: 12px; border: 1px solid var(--border-card); border-radius: var(--radius-lg); background: var(--bg-surface-card); } +.dashboard-page .dashboard-kpi-icon { width: 34px; height: 34px; display: grid; place-items: center; border: 1px solid var(--component-active-border); border-radius: var(--radius-md); background: var(--component-active-bg); color: var(--licora-primary); font-size: 15px; } +.dashboard-page .dashboard-kpi-copy { min-width: 0; display: grid; gap: 2px; } +.dashboard-page .dashboard-kpi-label { color: var(--text-secondary); font-size: 10.5px; font-weight: var(--font-weight-medium); } +.dashboard-page .dashboard-kpi-value { color: var(--text-primary); font-size: 24px; line-height: 1.05; letter-spacing: -.03em; font-weight: var(--font-weight-bold); } +.dashboard-page .dashboard-kpi-meta { overflow: hidden; color: var(--text-muted); font-size: 9.75px; line-height: 1.35; text-overflow: ellipsis; } + +.dashboard-page .dashboard-chart-grid { display: grid; grid-template-columns: minmax(0,1.35fr) minmax(320px,.85fr); gap: 10px; margin-bottom: 10px; } +.dashboard-page .dashboard-operations-grid { display: grid; grid-template-columns: minmax(0,1.65fr) minmax(260px,.65fr); gap: 10px; margin-bottom: 10px; } +.dashboard-page .dashboard-panel { min-width: 0; margin: 0; } +.dashboard-page .dashboard-panel .card-header > div { min-width: 0; } +.dashboard-page .dashboard-panel .card-header small { display: block; margin-top: 2px; color: var(--text-muted); font-size: 9.5px; font-weight: var(--font-weight-normal); } +.dashboard-page .dashboard-chart-body { position: relative; min-height: 250px; } +.dashboard-page .dashboard-chart-body canvas { width: 100%!important; height: 226px!important; } + +.dashboard-page .dashboard-quick-actions { display: grid; gap: 4px; } +.dashboard-page .dashboard-action-link { min-height: 45px; display: grid; grid-template-columns: 28px minmax(0,1fr) 14px; gap: 8px; align-items: center; padding: 7px 8px; border: 1px solid transparent; border-radius: var(--radius-md); color: var(--text-content); text-decoration: none; } +.dashboard-page .dashboard-action-link:hover { border-color: var(--border-subtle); background: var(--component-hover-bg); color: var(--text-primary); text-decoration: none; } +.dashboard-page .dashboard-action-link > i:first-child { width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid var(--border-subtle); border-radius: var(--radius-md); background: var(--bg-surface-soft); color: var(--licora-primary); } +.dashboard-page .dashboard-action-link > i:last-child { color: var(--text-muted); font-size: 10px; } +.dashboard-page .dashboard-action-link span { min-width: 0; display: grid; gap: 1px; } +.dashboard-page .dashboard-action-link strong { overflow: hidden; color: var(--text-primary); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; } +.dashboard-page .dashboard-action-link small { overflow: hidden; color: var(--text-muted); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; } + +.dashboard-page .dashboard-activity-table th, +.dashboard-page .dashboard-top-licenses-table th { white-space: nowrap; } +.dashboard-page .dashboard-activity-table td { vertical-align: middle; } +.dashboard-page .dashboard-source-badge { display: inline-flex; min-height: 20px; align-items: center; padding: 2px 6px; border: 1px solid var(--border-subtle); border-radius: 999px; background: var(--bg-surface-soft); color: var(--text-secondary); font-size: 9.5px; font-weight: var(--font-weight-medium); white-space: nowrap; } +.dashboard-page .dashboard-empty { padding: 26px 12px; color: var(--text-muted); text-align: center; font-size: 11px; } +.dashboard-page .dashboard-top-licenses-panel { margin-bottom: 0; } + +@keyframes dashboard-spin { to { transform: rotate(360deg); } } +@media (prefers-reduced-motion: reduce) { + .dashboard-page [data-dashboard-refresh].is-loading i { animation: none; } +} +@media (max-width: 1099.98px) { + .dashboard-page .dashboard-health-strip { grid-template-columns: repeat(3,minmax(0,1fr)); } + .dashboard-page .dashboard-kpi-grid { grid-template-columns: repeat(2,minmax(0,1fr)); } + .dashboard-page .dashboard-chart-grid { grid-template-columns: 1fr; } + .dashboard-page .dashboard-operations-grid { grid-template-columns: minmax(0,1.35fr) minmax(240px,.65fr); } +} +@media (max-width: 767.98px) { + .dashboard-page .dashboard-refresh-group { width: 100%; justify-content: space-between; } + .dashboard-page .dashboard-refresh-meta { text-align: left; } + .dashboard-page .dashboard-health-strip { grid-template-columns: 1fr 1fr; } + .dashboard-page .dashboard-operations-grid { grid-template-columns: 1fr; } + .dashboard-page .dashboard-activity-panel { order: 2; } + .dashboard-page .dashboard-actions-panel { order: 1; } +} +@media (max-width: 479.98px) { + .dashboard-page .dashboard-health-strip, + .dashboard-page .dashboard-kpi-grid { grid-template-columns: 1fr; } + .dashboard-page .dashboard-kpi-value { font-size: 22px; } + .dashboard-page .dashboard-chart-body { min-height: 220px; } + .dashboard-page .dashboard-chart-body canvas { height: 200px!important; } +} diff --git a/admin/assets/js/dashboard.js b/admin/assets/js/dashboard.js new file mode 100644 index 0000000..4220a43 --- /dev/null +++ b/admin/assets/js/dashboard.js @@ -0,0 +1,439 @@ +(function (root, factory) { + var api = factory(); + if (typeof module === 'object' && module.exports) module.exports = api; + if (root) root.LicoraDashboard = api; +})(typeof window !== 'undefined' ? window : globalThis, function () { + 'use strict'; + + var DEFAULT_POLL_MS = 30000; + + function asInt(value) { + var parsed = parseInt(value, 10); + return Number.isFinite(parsed) ? parsed : 0; + } + + function text(value, fallback) { + if (value === null || value === undefined || value === '') return fallback || '—'; + return String(value); + } + + function formatTimestamp(value) { + if (!value) return '—'; + var date = new Date(value); + if (Number.isNaN(date.getTime())) return '—'; + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + } + + function formatActivityTime(value) { + if (!value) return '—'; + var date = new Date(value.replace(' ', 'T')); + if (Number.isNaN(date.getTime())) return text(value, '—'); + return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); + } + + function indexSeries(series) { + var indexed = Object.create(null); + (Array.isArray(series) ? series : []).forEach(function (point) { + if (!point || !point.date) return; + indexed[String(point.date)] = asInt(point.count); + }); + return indexed; + } + + function alignSeries(left, right) { + var labels = Object.create(null); + (Array.isArray(left) ? left : []).concat(Array.isArray(right) ? right : []).forEach(function (point) { + if (point && point.date) labels[String(point.date)] = true; + }); + var ordered = Object.keys(labels).sort(); + var leftIndex = indexSeries(left); + var rightIndex = indexSeries(right); + return { + labels: ordered, + left: ordered.map(function (label) { return leftIndex[label] || 0; }), + right: ordered.map(function (label) { return rightIndex[label] || 0; }) + }; + } + + function combineRecentActivity(recent) { + var result = []; + var v1 = recent && Array.isArray(recent.v1_tracked) ? recent.v1_tracked : []; + var v2 = recent && Array.isArray(recent.v2_tracked) ? recent.v2_tracked : []; + v1.forEach(function (call) { + var code = asInt(call && call.response_code); + result.push({ + source: 'API v1', + action: text(call && call.endpoint, 'verify'), + context: call && call.license_key ? String(call.license_key).slice(0, 12) + '…' : 'No license', + result: String(code), + tone: code === 200 ? 'success' : (code >= 400 && code < 500 ? 'warning' : 'danger'), + created_at: text(call && call.created_at, '') + }); + }); + v2.forEach(function (event) { + var parts = []; + if (event && event.app_id) parts.push(String(event.app_id)); + if (event && event.license_id !== null && event.license_id !== undefined) parts.push('License #' + asInt(event.license_id)); + result.push({ + source: 'API v2', + action: text(event && event.event_type, 'audit_event'), + context: parts.length ? parts.join(' · ') : 'Audit event', + result: 'Recorded', + tone: 'primary', + created_at: text(event && event.created_at, '') + }); + }); + result.sort(function (a, b) { + var at = Date.parse(String(a.created_at).replace(' ', 'T')) || 0; + var bt = Date.parse(String(b.created_at).replace(' ', 'T')) || 0; + return bt - at; + }); + return result.slice(0, 12); + } + + function validatePayload(payload) { + if (!payload || payload.success !== true || !payload.data || typeof payload.data !== 'object') { + throw new Error('Dashboard response contract is invalid.'); + } + ['licenses', 'devices', 'api_activity', 'recent_activity', 'expiration', 'health'].forEach(function (key) { + if (!payload.data[key] || typeof payload.data[key] !== 'object') { + throw new Error('Dashboard response is missing ' + key + '.'); + } + }); + return payload; + } + + function createController(options) { + options = options || {}; + var request = options.request; + var view = options.view || {}; + var pollMs = asInt(options.pollMs) || DEFAULT_POLL_MS; + var setTimer = options.setTimer || function (fn, ms) { return setInterval(fn, ms); }; + var clearTimer = options.clearTimer || function (id) { clearInterval(id); }; + var inFlight = false; + var timer = null; + var stopped = false; + var lastSuccessAt = null; + + function notify(name) { + if (typeof view[name] === 'function') { + var args = Array.prototype.slice.call(arguments, 1); + view[name].apply(view, args); + } + } + + function refresh(reason) { + if (stopped || inFlight) return Promise.resolve({ skipped: true }); + if (typeof request !== 'function') return Promise.reject(new Error('Dashboard request transport is unavailable.')); + inFlight = true; + notify('setLoading', true, reason || 'poll'); + return Promise.resolve().then(function () { return request(); }).then(function (response) { + if (response && response.status === 401) { + stopped = true; + if (timer !== null) { clearTimer(timer); timer = null; } + notify('showAuthRequired'); + return { authRequired: true }; + } + if (!response || response.ok !== true) { + var error = new Error('Dashboard refresh failed.'); + error.status = response ? response.status : 0; + throw error; + } + return response.json(); + }).then(function (payload) { + if (payload && payload.authRequired) return payload; + validatePayload(payload); + var completedAt = payload.generated_at || new Date().toISOString(); + notify('render', payload); + lastSuccessAt = completedAt; + notify('showFresh', lastSuccessAt); + return payload; + }).catch(function (error) { + notify('showStale', lastSuccessAt, error); + return { error: error }; + }).finally(function () { + inFlight = false; + notify('setLoading', false, reason || 'poll'); + }); + } + + function start() { + if (stopped || timer !== null) return; + timer = setTimer(function () { refresh('poll'); }, pollMs); + } + + function stop() { + stopped = true; + if (timer !== null) { clearTimer(timer); timer = null; } + } + + function seed(payload) { + validatePayload(payload); + lastSuccessAt = payload.generated_at || null; + notify('render', payload); + notify('showFresh', lastSuccessAt); + } + + return { + refresh: refresh, + start: start, + stop: stop, + seed: seed, + isInFlight: function () { return inFlight; }, + isStopped: function () { return stopped; }, + getLastSuccessAt: function () { return lastSuccessAt; } + }; + } + + function createElement(documentRef, tag, className, value) { + var el = documentRef.createElement(tag); + if (className) el.className = className; + if (value !== undefined) el.textContent = String(value); + return el; + } + + function createDomView(documentRef, chartFactory) { + var apiChart = null; + var expirationChart = null; + var refreshButton = documentRef.querySelector('[data-dashboard-refresh]'); + var refreshText = documentRef.querySelector('[data-dashboard-refresh-text]'); + var updatedAt = documentRef.querySelector('[data-dashboard-updated-at]'); + var state = documentRef.querySelector('[data-dashboard-state]'); + var stateText = documentRef.querySelector('[data-dashboard-state-text]'); + var signin = documentRef.querySelector('[data-dashboard-signin]'); + var authLocked = false; + + function cssVar(name, fallback) { + if (!documentRef.defaultView || !documentRef.defaultView.getComputedStyle) return fallback; + var value = documentRef.defaultView.getComputedStyle(documentRef.documentElement).getPropertyValue(name).trim(); + return value || fallback; + } + + function setState(message, tone, showSignin) { + if (!state || !stateText) return; + state.hidden = !message; + state.classList.remove('is-warning', 'is-danger'); + if (tone) state.classList.add('is-' + tone); + stateText.textContent = message || ''; + if (signin) signin.hidden = !showSignin; + } + + function setText(selector, value) { + var node = documentRef.querySelector(selector); + if (node) node.textContent = String(value); + } + + function updateHealth(health) { + var v2Ready = !!(health.api_v2 && health.api_v2.schema_ready && health.api_v2.key_pair_ready); + var items = { + database: { value: text(health.database && health.database.label), tone: health.database && health.database.ok ? 'ok' : 'danger' }, + api_v2: { value: v2Ready ? 'Ready' : 'Needs setup', tone: v2Ready ? 'ok' : 'warning' }, + cron_scripts: { value: health.cron_scripts && health.cron_scripts.available ? 'Available' : 'Missing', tone: health.cron_scripts && health.cron_scripts.available ? 'ok' : 'danger' }, + php: { value: text(health.php && health.php.version), tone: health.php && health.php.ok ? 'ok' : 'danger' }, + environment: { value: text(health.environment && health.environment.value), tone: 'neutral' } + }; + Object.keys(items).forEach(function (key) { + var item = documentRef.querySelector('[data-dashboard-health="' + key + '"]'); + var value = documentRef.querySelector('[data-dashboard-health-value="' + key + '"]'); + if (value) value.textContent = key === 'environment' ? items[key].value.charAt(0).toUpperCase() + items[key].value.slice(1) : items[key].value; + if (item) { + item.classList.remove('is-ok', 'is-warning', 'is-danger', 'is-neutral'); + item.classList.add('is-' + items[key].tone); + } + }); + } + + function updateCharts(data) { + if (typeof chartFactory !== 'function') return; + var api = alignSeries(data.api_activity.v1_tracked.last_14_days, data.api_activity.v2_tracked.last_14_days); + var expiration = alignSeries(data.expiration.expired_last_30_days, data.expiration.expiring_next_30_days); + var primary = cssVar('--licora-primary', '#2563eb'); + var secondary = cssVar('--licora-secondary', '#7c3aed'); + var danger = cssVar('--status-danger', '#c9363e'); + var warning = cssVar('--status-warning', '#b76d00'); + var muted = cssVar('--text-muted', '#7b8798'); + var border = cssVar('--border-inner', '#e7ebf1'); + var chartOptions = { + responsive: true, + maintainAspectRatio: false, + animation: false, + interaction: { mode: 'index', intersect: false }, + plugins: { legend: { position: 'bottom', labels: { boxWidth: 10, boxHeight: 10, color: muted } } }, + scales: { x: { grid: { display: false }, ticks: { color: muted, maxTicksLimit: 7 } }, y: { beginAtZero: true, grid: { color: border }, ticks: { color: muted, precision: 0 } } } + }; + if (!apiChart) { + var apiCanvas = documentRef.getElementById('dailyApiChart'); + if (apiCanvas) apiChart = chartFactory(apiCanvas, { type: 'line', data: { labels: api.labels, datasets: [ + { label: 'API v1 Verify', data: api.left, borderColor: primary, backgroundColor: primary, tension: .25, pointRadius: 2 }, + { label: 'API v2 Audit Events', data: api.right, borderColor: secondary, backgroundColor: secondary, tension: .25, pointRadius: 2 } + ] }, options: chartOptions }); + } else { + apiChart.data.labels = api.labels; + apiChart.data.datasets[0].data = api.left; + apiChart.data.datasets[1].data = api.right; + apiChart.update('none'); + } + if (!expirationChart) { + var expirationCanvas = documentRef.getElementById('expiredTrendChart'); + if (expirationCanvas) expirationChart = chartFactory(expirationCanvas, { type: 'bar', data: { labels: expiration.labels, datasets: [ + { label: 'Expired — Last 30 Days', data: expiration.left, backgroundColor: danger }, + { label: 'Expiring — Next 30 Days', data: expiration.right, backgroundColor: warning } + ] }, options: chartOptions }); + } else { + expirationChart.data.labels = expiration.labels; + expirationChart.data.datasets[0].data = expiration.left; + expirationChart.data.datasets[1].data = expiration.right; + expirationChart.update('none'); + } + } + + function updateRecentActivity(recent) { + var tbody = documentRef.querySelector('[data-dashboard-recent-activity]'); + if (!tbody) return; + while (tbody.firstChild) tbody.removeChild(tbody.firstChild); + var rows = combineRecentActivity(recent); + if (!rows.length) { + var emptyRow = createElement(documentRef, 'tr'); + var emptyCell = createElement(documentRef, 'td'); + emptyCell.colSpan = 5; + emptyCell.appendChild(createElement(documentRef, 'div', 'dashboard-empty', 'No tracked activity yet.')); + emptyRow.appendChild(emptyCell); + tbody.appendChild(emptyRow); + return; + } + rows.forEach(function (item) { + var tr = createElement(documentRef, 'tr'); + tr.appendChild(createElement(documentRef, 'td', '', formatActivityTime(item.created_at))); + var sourceCell = createElement(documentRef, 'td'); + sourceCell.appendChild(createElement(documentRef, 'span', 'dashboard-source-badge', item.source)); + tr.appendChild(sourceCell); + var actionCell = createElement(documentRef, 'td'); + actionCell.appendChild(createElement(documentRef, 'code', '', item.action)); + tr.appendChild(actionCell); + tr.appendChild(createElement(documentRef, 'td', '', item.context)); + var resultCell = createElement(documentRef, 'td'); + resultCell.appendChild(createElement(documentRef, 'span', 'badge bg-' + item.tone, item.result)); + tr.appendChild(resultCell); + tbody.appendChild(tr); + }); + } + + function updateTopLicenses(apiActivity) { + var tbody = documentRef.querySelector('[data-dashboard-top-licenses]'); + if (!tbody) return; + while (tbody.firstChild) tbody.removeChild(tbody.firstChild); + var rows = apiActivity && apiActivity.v1_tracked && Array.isArray(apiActivity.v1_tracked.top_licenses) ? apiActivity.v1_tracked.top_licenses : []; + if (!rows.length) { + var emptyRow = createElement(documentRef, 'tr'); + var emptyCell = createElement(documentRef, 'td'); + emptyCell.colSpan = 2; + emptyCell.appendChild(createElement(documentRef, 'div', 'dashboard-empty', 'No tracked API v1 license activity yet.')); + emptyRow.appendChild(emptyCell); + tbody.appendChild(emptyRow); + return; + } + rows.forEach(function (license) { + var tr = createElement(documentRef, 'tr'); + var licenseCell = createElement(documentRef, 'td'); + licenseCell.appendChild(createElement(documentRef, 'code', '', license && license.license_key ? String(license.license_key).slice(0, 18) : 'Unknown')); + tr.appendChild(licenseCell); + var countCell = createElement(documentRef, 'td', 'text-end'); + countCell.appendChild(createElement(documentRef, 'span', 'badge bg-primary', asInt(license && license.count))); + tr.appendChild(countCell); + tbody.appendChild(tr); + }); + } + + return { + render: function (payload) { + var data = payload.data; + setText('[data-dashboard-kpi="total_licenses"]', asInt(data.licenses.total)); + setText('[data-dashboard-kpi="active_licenses"]', asInt(data.licenses.active)); + setText('[data-dashboard-kpi="recent_devices"]', asInt(data.devices.recently_seen)); + setText('[data-dashboard-kpi="expiring_soon"]', asInt(data.licenses.expiring_soon)); + setText('[data-dashboard-kpi-meta="expired_licenses"]', asInt(data.licenses.expired)); + setText('[data-dashboard-kpi-meta="suspended_licenses"]', asInt(data.licenses.suspended)); + setText('[data-dashboard-kpi-meta="active_devices"]', asInt(data.devices.active_flagged)); + setText('[data-dashboard-kpi-meta="total_devices"]', asInt(data.devices.total_records)); + updateHealth(data.health); + updateCharts(data); + updateRecentActivity(data.recent_activity); + updateTopLicenses(data.api_activity); + }, + setLoading: function (loading) { + if (refreshButton) { + refreshButton.disabled = authLocked || !!loading; + refreshButton.classList.toggle('is-loading', !!loading); + } + if (loading && refreshText) refreshText.textContent = 'Refreshing…'; + }, + showFresh: function (timestamp) { + authLocked = false; + if (updatedAt) updatedAt.textContent = formatTimestamp(timestamp); + setState('', '', false); + if (refreshButton) refreshButton.disabled = false; + if (refreshText) refreshText.textContent = 'Refresh'; + }, + showStale: function (lastSuccessAt) { + var suffix = lastSuccessAt ? ' Last successful update: ' + formatTimestamp(lastSuccessAt) + '.' : ''; + setState('Data may be stale.' + suffix, 'warning', false); + if (refreshText) refreshText.textContent = 'Retry'; + }, + showAuthRequired: function () { + authLocked = true; + setState('Session expired. Sign in again to continue dashboard refresh.', 'danger', true); + if (refreshText) refreshText.textContent = 'Refresh paused'; + if (refreshButton) refreshButton.disabled = true; + } + }; + } + + function initBrowser(documentRef, windowRef) { + var root = documentRef.getElementById('licora-dashboard'); + if (!root) return null; + var endpoint = root.getAttribute('data-dashboard-endpoint') || 'ajax/dashboard-data.php'; + var pollMs = asInt(root.getAttribute('data-dashboard-poll-ms')) || DEFAULT_POLL_MS; + var chartFactory = windowRef.Chart ? function (canvas, config) { return new windowRef.Chart(canvas, config); } : null; + var view = createDomView(documentRef, chartFactory); + var controller = createController({ + pollMs: pollMs, + view: view, + request: function () { + return windowRef.fetch(endpoint, { + method: 'GET', + credentials: 'same-origin', + cache: 'no-store', + headers: { 'Accept': 'application/json' } + }); + } + }); + var initialNode = documentRef.getElementById('dashboard-initial-data'); + if (initialNode) { + try { controller.seed(JSON.parse(initialNode.textContent || '{}')); } + catch (error) { view.showStale(null, error); } + } + var refresh = documentRef.querySelector('[data-dashboard-refresh]'); + if (refresh) refresh.addEventListener('click', function () { controller.refresh('manual'); }); + controller.start(); + return controller; + } + + if (typeof document !== 'undefined' && typeof window !== 'undefined') { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', function () { initBrowser(document, window); }, { once: true }); + } else { + initBrowser(document, window); + } + } + + return { + DEFAULT_POLL_MS: DEFAULT_POLL_MS, + alignSeries: alignSeries, + combineRecentActivity: combineRecentActivity, + validatePayload: validatePayload, + createController: createController, + createDomView: createDomView, + initBrowser: initBrowser, + formatTimestamp: formatTimestamp + }; +}); diff --git a/admin/index.php b/admin/index.php index 29d37b0..cdcec33 100644 --- a/admin/index.php +++ b/admin/index.php @@ -7,7 +7,7 @@ $auth = new Auth(); if (!$auth->isAdminLoggedIn()) { - header("Location: login.php"); + header('Location: login.php'); exit(); } @@ -15,46 +15,58 @@ $dashboardSnapshot = $dashboard->snapshot(); $licenseStats = $dashboardSnapshot['licenses']; $deviceStats = $dashboardSnapshot['devices']; -$apiStats = $dashboardSnapshot['api_keys']; $apiActivity = $dashboardSnapshot['api_activity']; $expiration = $dashboardSnapshot['expiration']; $health = $dashboardSnapshot['health']; - -$chartV1 = $apiActivity['v1_tracked']['last_14_days']; -$chartV2 = $apiActivity['v2_tracked']['last_14_days']; $topLicenses = $apiActivity['v1_tracked']['top_licenses']; -$recentCalls = $apiActivity['v1_tracked']['recent_calls']; -$apiDates = []; -foreach (array_merge($chartV1, $chartV2) as $point) { - if (!empty($point['date'])) { - $apiDates[(string)$point['date']] = true; - } +$recentActivity = []; +foreach ($dashboardSnapshot['recent_activity']['v1_tracked'] as $call) { + $recentActivity[] = [ + 'timestamp' => strtotime((string)($call['created_at'] ?? '')) ?: 0, + 'time' => (string)($call['created_at'] ?? ''), + 'source' => 'API v1', + 'action' => (string)($call['endpoint'] ?? 'verify'), + 'context' => !empty($call['license_key']) ? substr((string)$call['license_key'], 0, 12) . '…' : 'No license', + 'result' => (string)((int)($call['response_code'] ?? 0)), + 'result_class' => (int)($call['response_code'] ?? 0) === 200 ? 'success' : (((int)($call['response_code'] ?? 0) >= 400 && (int)($call['response_code'] ?? 0) < 500) ? 'warning' : 'danger'), + ]; } -$apiLabels = array_keys($apiDates); -sort($apiLabels); -$seriesByDate = static function (array $series, array $labels): array { - $indexed = []; - foreach ($series as $point) { - $indexed[(string)($point['date'] ?? '')] = (int)($point['count'] ?? 0); +foreach ($dashboardSnapshot['recent_activity']['v2_tracked'] as $event) { + $contextParts = []; + if (!empty($event['app_id'])) { + $contextParts[] = (string)$event['app_id']; } - return array_map(static fn(string $date): int => (int)($indexed[$date] ?? 0), $labels); -}; -$apiV1Data = $seriesByDate($chartV1, $apiLabels); -$apiV2Data = $seriesByDate($chartV2, $apiLabels); - -$expiredSeries = $expiration['expired_last_30_days']; -$expiringSeries = $expiration['expiring_next_30_days']; -$expirationDates = []; -foreach (array_merge($expiredSeries, $expiringSeries) as $point) { - if (!empty($point['date'])) { - $expirationDates[(string)$point['date']] = true; + if ($event['license_id'] !== null) { + $contextParts[] = 'License #' . (int)$event['license_id']; } + $recentActivity[] = [ + 'timestamp' => strtotime((string)($event['created_at'] ?? '')) ?: 0, + 'time' => (string)($event['created_at'] ?? ''), + 'source' => 'API v2', + 'action' => (string)($event['event_type'] ?? 'audit_event'), + 'context' => $contextParts !== [] ? implode(' · ', $contextParts) : 'Audit event', + 'result' => 'Recorded', + 'result_class' => 'primary', + ]; } -$expirationLabels = array_keys($expirationDates); -sort($expirationLabels); -$expiredData = $seriesByDate($expiredSeries, $expirationLabels); -$expiringData = $seriesByDate($expiringSeries, $expirationLabels); +usort($recentActivity, static fn(array $a, array $b): int => $b['timestamp'] <=> $a['timestamp']); +$recentActivity = array_slice($recentActivity, 0, 12); + +$v2Ready = !empty($health['api_v2']['schema_ready']) && !empty($health['api_v2']['key_pair_ready']); +$initialPayload = [ + 'success' => true, + 'generated_at' => $dashboardSnapshot['generated_at'], + 'data' => [ + 'licenses' => $dashboardSnapshot['licenses'], + 'devices' => $dashboardSnapshot['devices'], + 'api_keys' => $dashboardSnapshot['api_keys'], + 'api_activity' => $dashboardSnapshot['api_activity'], + 'recent_activity' => $dashboardSnapshot['recent_activity'], + 'expiration' => $dashboardSnapshot['expiration'], + 'health' => $dashboardSnapshot['health'], + ], +]; ?> @@ -67,265 +79,212 @@ - + -
-
-

Dashboard

- Create License -
- - -
-
-
-
-
-
-
Total Licenses
-

- Active: -
- -
-
-
+
+
+
+

Dashboard

+

License system overview

- -
-
-
-
-
-
Recently Seen Devices
-

- Active flagged: · Total: -
- -
-
+
+
+ Last updated +
+
+
+ -
-
-
-
-
-
Expired Licenses
-

- Suspended: -
- -
-
-
+
+
+ + Database +
-
-
-
-
-
-
API Keys
-

- Tracked v1 requests: -
- -
-
-
+
+ + API v2 +
- -
-
-
-
-
-
Environment
-

- Version: -
- -
-
-
+
+ + Cron Scripts +
-
- +
+ + PHP + +
+
+ + Environment + +
+
-
-
Tracked API Activity
-
Expiration Timeline
-
Top Licenses — API v1 Verify
-
+
+
+
+
+ Total Licenses + + All license records +
+
+
+
+
+ Active Licenses + + Expired: · Suspended: +
+
+
+
+
+ Recently Seen Devices + + 5 min · Active flagged: · Total: +
+
+
+
+
+ Expiring Soon + + Next days +
+
+
- -
-
-
-
-
Quick Actions
+
+ +
+
+
+
Expiration Timeline
+ Expired last 30 days · expiring next 30 days
-
+
+ License expiration timeline chart +
+ + -
-
-
-
Recent API v1 Verify Calls
- View All +
+
+
+
+
Recent Activity
+ Tracked API v1 calls and API v2 audit events
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - -
TimeEndpointLicenseStatus
No API calls yet
- - - - Test - - - - - -
-
+ View logs +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TimeSourceActivityContextResult
No tracked activity yet.
-
-
+ - -
-
-
-
-
System Status
-
-
-
-
-
-
-
Database
-
-
-
-
-
-
PHP Runtime
-
-
-
-
-
-
Cron Scripts
-
-
-
-
- -
-
API v2
-
-
-
+ + + +
+
+
+
Top Licenses — API v1 Verify
+ Highest tracked API v1 verification volume +
-
-
+
+
+ + + + + + + + + + + + + + +
LicenseTracked Requests
No tracked API v1 license activity yet.
+
+
+ +
+ - + - - + - \ No newline at end of file + diff --git a/audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md b/audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md new file mode 100644 index 0000000..54edb6f --- /dev/null +++ b/audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md @@ -0,0 +1,102 @@ +# Licora v5.7.0 — Dashboard Phase 2 Implementation Audit + +## Authority + +- Frozen parent: `v5.6.1` +- Parent commit: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent ZIP SHA-256: `0ca0ad76b5c0091912aa441fcac4c033a54bac630d6c1a7255ac5b2b75db5493` +- Approved target: `v5.7.0` +- Approved scope: compact Dashboard UI, reload-free AJAX refresh and production gate +- GitHub write authorization: `NO` + +## Baseline Finding + +v5.6.1 already supplied every data field required by Phase 2 through `DashboardReadModel` and authenticated `GET /admin/ajax/dashboard-data.php`, including `expiring_soon`, device recency, source-separated API v1/v2 activity, expiration datasets and measured health facts. Therefore Phase 2 required no backend response-contract expansion and no database change. + +The baseline Dashboard still performed `window.location.reload()` after 30 seconds and initialized Chart.js through inline script. Quick Actions used large tile buttons, recent activity showed only API v1 calls, and there was no manual refresh, last-updated indicator, stale state or overlap protection. + +## Implemented Runtime Changes + +### Dashboard composition + +`admin/index.php` now keeps the shared Licora light shell and provides a Dashboard-only compact hierarchy: + +1. header + last-updated/manual Refresh; +2. measured system-status strip; +3. Total Licenses / Active Licenses / Recently Seen Devices / Expiring Soon KPIs; +4. Tracked API Activity + Expiration Timeline charts; +5. source-labelled Recent Activity + compact Quick Actions; +6. Top Licenses — API v1 Verify. + +The initial snapshot remains server rendered so the page is usable before browser refresh logic runs. + +### Reload-free controller + +New `admin/assets/js/dashboard.js`: + +- consumes only the existing authenticated GET endpoint; +- keeps the reviewed 30-second cadence; +- performs no POST/mutation request; +- updates values and existing Chart.js instances in place; +- blocks overlapping requests; +- exposes manual Refresh; +- records the last successful `generated_at` time; +- preserves the last successful UI when refresh fails; +- surfaces stale/Retry feedback; +- stops polling on 401 and exposes the existing login route; +- uses DOM `textContent` for backend-derived text rather than `innerHTML`. + +### Presentation isolation + +Dashboard styling is appended under `.dashboard-page` selectors in the existing `admin/assets/css/admin-ui.css` compatibility entrypoint. The shared sidebar/topbar and non-Dashboard admin pages are not restyled. Reduced-motion preference disables refresh rotation. + +## Tests Added/Updated + +- `tests/dashboard_phase2_contract.php` +- `tests/dashboard_browser_runtime.js` +- existing `tests/dashboard_data_contract.php` updated so Phase 1 truth assertions coexist with the approved Phase 2 consumer behavior +- `scripts/verify-local.py` updated to require/run the new Phase 2 assets/tests + +Targeted Phase 2 contract and browser-runtime tests passed during implementation. The complete `python3 scripts/verify-local.py` run also passed after v5.7.0 source/tests/version/docs alignment. Local environment-dependent DB integration tests were skipped because a dedicated MySQL test database was not enabled; the unchanged remote MySQL gate remains mandatory. + +## Frozen Contracts + +No Phase 2 change is made to: + +- `includes/dashboard.php` Dashboard business/data semantics; +- `admin/ajax/dashboard-data.php` response/error contract; +- API v1 or Secure API v2 public contracts; +- license generation/verification/state rules; +- device authorization/revocation behavior; +- auth roles/session/CSRF policy; +- database schema/migrations; +- Cron mutation behavior; +- updater signing/manifest/state machine/rollback; +- shared sidebar/topbar architecture. + +## Remaining Acceptance Gates + +Before Phase 2 can be marked `COMPLETE + VERIFIED`: + +1. after a separately authorized GitHub push, required CI/MySQL/PHP/Windows gates must pass; +2. manual desktop/tablet/mobile smoke must confirm no layout/console/full-page-reload regression and correct stale/manual/polling behavior; +3. release/tag publication remains separately authorized. + + +## Local Verification Result + +`python3 scripts/verify-local.py`: **PASS** + +Included evidence: + +- all PHP syntax checks: PASS +- security/compatibility/installer/release readiness: PASS +- API v1 freeze + API v2 crypto/static: PASS +- updater static/manifest/state-machine/UI/DOM/builder: PASS +- existing UI route/form/component/updater/v5.5.0/v5.5.1 contracts: PASS +- Phase 1 Dashboard data contract: PASS +- Phase 2 Dashboard contract: PASS +- Dashboard browser/runtime: PASS +- updater/sidebar browser runtime: PASS +- DB integration tests: skipped locally because dedicated test DB was not enabled; remote CI remains required +- updater archive failure/recovery: skipped locally because ZipArchive is unavailable diff --git a/audit/V5.7.0_DELTA_MANIFEST.md b/audit/V5.7.0_DELTA_MANIFEST.md new file mode 100644 index 0000000..ee039cc --- /dev/null +++ b/audit/V5.7.0_DELTA_MANIFEST.md @@ -0,0 +1,93 @@ +# Licora v5.7.0 — Dashboard Phase 2 Replace-Ready Delta Manifest + +## Authority + +- Official parent baseline: `Licora-5.6.1.zip` +- Parent version/tag: `v5.6.1` +- Parent Git commit: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent release ZIP SHA-256: `0ca0ad76b5c0091912aa441fcac4c033a54bac630d6c1a7255ac5b2b75db5493` +- Target source version: `5.7.0` +- Patch type: `replace-ready Dashboard Phase 2 delta` +- Delete list: `NONE` +- Database migrations: `NONE` + +## Approved Scope + +1. compact Dashboard-only operations composition while preserving the shared Licora light shell; +2. dedicated `admin/assets/js/dashboard.js` controller; +3. authenticated 30-second reload-free AJAX refresh through the unchanged Phase 1 endpoint; +4. manual Refresh, last-updated, stale/Retry and session-expiry feedback; +5. in-place KPI, measured health, Chart.js, recent-activity and top-license updates; +6. request-overlap protection and responsive/accessibility behavior; +7. Phase 2 source/browser tests plus v5.7.0 version/release/documentation alignment. + +## Frozen / Unchanged + +- `includes/dashboard.php`: unchanged from v5.6.1 +- `admin/ajax/dashboard-data.php`: unchanged from v5.6.1 +- API v1 and API v2 runtime files: unchanged +- database/migration files: unchanged +- license/device/auth/Cron semantics: unchanged +- updater runtime/signing/state-machine files: unchanged +- shared sidebar/topbar implementation: unchanged + +## Verification State + +- `python3 scripts/verify-local.py`: **PASS** +- targeted Phase 2 contract: **PASS** +- targeted Dashboard browser/runtime: **PASS** +- targeted release readiness after final evidence-doc update: **PASS** +- local DB integration: **SKIPPED — dedicated test DB not enabled** +- updater archive failure/recovery: **SKIPPED — ZipArchive unavailable** +- remote CI/MySQL/PHP/Windows gate: **PENDING — no GitHub write performed** +- manual desktop/tablet/mobile live smoke: **PENDING** + +Phase 2 remains **INCOMPLETE** until remote CI and manual live/staging smoke are accepted. + +## Replace-Ready Rule + +Extract the delta ZIP at the root of a clean v5.6.1 source checkout/release tree and overwrite matching paths. No file is deleted and no SQL migration is run. Private deployment/runtime configuration and signing keys are not included. + +## Delta Paths + +- `.github/workflows/ci.yml` +- `BASELINE_v5.7.0.md` +- `CHANGELOG.md` +- `DASHBOARD_PRODUCTION_UPDATE_INDEX.md` +- `README.md` +- `RELEASE_COMMANDS_v5.7.0.md` +- `RELEASE_NOTES_v5.7.0.md` +- `REPOSITORY_METADATA.md` +- `ROADMAP.md` +- `admin/assets/css/admin-ui.css` +- `admin/assets/js/dashboard.js` +- `admin/index.php` +- `audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md` +- `audit/V5.7.0_DELTA_MANIFEST.md` +- `audit/V5.7.0_DELTA_SHA256SUMS.txt` +- `config.sample.php` +- `docs/ACTUAL_IMPLEMENTATION_LEDGER.md` +- `docs/ARCHITECTURE.md` +- `docs/CONFIGURATION.md` +- `docs/DASHBOARD_CHANGE_CONTROL.md` +- `docs/DASHBOARD_DATA_CONTRACT.md` +- `docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md` +- `docs/DASHBOARD_UPDATE_PHASE_LOG.md` +- `docs/ERROR_HANDLING_BASELINE_AND_TARGET.md` +- `docs/FEATURE_MATRIX.md` +- `docs/INSTALLATION.md` +- `docs/RELEASE.md` +- `docs/UI_DESIGN_SYSTEM.md` +- `docs/UPGRADE_GUIDE.md` +- `includes/config.php` +- `includes/installation.php` +- `install.php` +- `scripts/verify-local.py` +- `tests/compatibility_regression.php` +- `tests/dashboard_browser_runtime.js` +- `tests/dashboard_data_contract.php` +- `tests/dashboard_phase2_contract.php` +- `tests/installer_smoke.php` +- `tests/release_readiness.php` +- `tests/updater_state_machine.php` +- `update/release-spec.json` diff --git a/audit/V5.7.0_DELTA_SHA256SUMS.txt b/audit/V5.7.0_DELTA_SHA256SUMS.txt new file mode 100644 index 0000000..72562bb --- /dev/null +++ b/audit/V5.7.0_DELTA_SHA256SUMS.txt @@ -0,0 +1,40 @@ +033b39396ea1f7ab9af147c0c7ae9eb35c5fbc596e8947d2b7b86e82ba4dbaa5 .github/workflows/ci.yml +bce87307b33764517e4268972096d4aa1347c9cbdf8489ae76853171c2090a02 BASELINE_v5.7.0.md +1637d018dc8f6c985c3fdea4e814a9fa9f8344d6e63129aeaf6d6e22889aca8a CHANGELOG.md +e7f9f01771e58742f10d2b58478c5ab6629fcbfe51915145b1d6a67a258ceee0 DASHBOARD_PRODUCTION_UPDATE_INDEX.md +5c30ee703facf82412cc669d37d9588e3f756bfef04c40c373f5159c10cc07b6 README.md +dedf885da0f8e42e861b4e2d07ba9f5362493a847dd7140e588decfc38e8d9f3 RELEASE_COMMANDS_v5.7.0.md +0004ea4d79d6b02750db1d10f40b0f790a6e27652929f46557e67019798f5d60 RELEASE_NOTES_v5.7.0.md +9ba2f29c913210bda9c882f686297893f2be3aeddb840a80f35b28e7da6bf574 REPOSITORY_METADATA.md +8a9189c1ed01432dca252958ea18f9fe913bd8e692890c4feed5eb330f58feeb ROADMAP.md +a01013db7c6d29f7983937735c151ab65ac7f2bc2dd491ccae0905b18d1a5737 admin/assets/css/admin-ui.css +0c0c76fcd4b06ad1f173e7b34f43993ec1adca9f953a91b465d47cbb2cdd0188 admin/assets/js/dashboard.js +40d20d00cd00c26195146fc6a982ebb28161ac1e4c722806312b1b9752ff6635 admin/index.php +3854980925f45723c26f68a9448a46e15f5e204b1e4a391e08a0cf0be0e69207 audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md +1f44f5789bb32ec9912922a63d3ee0f17694bf670dcbe48127d1185b84834720 audit/V5.7.0_DELTA_MANIFEST.md +8b7a56a1ff442d00a175243f58c0459b7dfc9a868b15aec7ee9fe4278adf3a14 config.sample.php +18198abe0c425ca4afff1f89c8be19a71b8dd7f74937c5a116629eba98b3339b docs/ACTUAL_IMPLEMENTATION_LEDGER.md +2d4b1d93f7e76679e42533f14dc3c284a6d983942bd96d30976a70ff0749a6dd docs/ARCHITECTURE.md +af3e385401c472d837595746fae0f9343ea679f4c5a07363a3a8ac4ec1f0fd9d docs/CONFIGURATION.md +11ab6d7803cd67721a44824977af10c9741103e16ec838188984200bb7f1973c docs/DASHBOARD_CHANGE_CONTROL.md +9a67afde997b95c33941877fe93e0f7d75178fe3267898dcf4c32f21a1902f6c docs/DASHBOARD_DATA_CONTRACT.md +44ea1e264b9d925f815da065fd7959487d102783a71eb0893a52204bdf8544e5 docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md +bc2eb363d487a5e6f9b19524cc82b8fbfc9c639900e3ed4ec0ca396f3405fe4a docs/DASHBOARD_UPDATE_PHASE_LOG.md +121e57bee5bd62afd3ecf91757b2e2ffc2421ae202de221a9e6506250a079001 docs/ERROR_HANDLING_BASELINE_AND_TARGET.md +70295e0bb4b15fb969a9b586eba40f53922754c8b6dac8262ba7473dd36a42d7 docs/FEATURE_MATRIX.md +d1fe330607cfa6cf4faeffe2a4e316b51b738443257f4daccd7308057de00375 docs/INSTALLATION.md +0507f17781b7b42ec3318849b3264c631e9bcac32241c601a96a2f077b58144a docs/RELEASE.md +4b810ceb1d31cfd39844c9f8118e2fe63f0ba3308a2c7249c94bcb6baecd2b3d docs/UI_DESIGN_SYSTEM.md +9108a88989e190a3c2cceb2dbd08bfdeadac22e5480a714fc809fbe7e29114c8 docs/UPGRADE_GUIDE.md +e211a6580ca12894952e6f1be526339959613b4648390d6e4e4902ab2c78f5e4 includes/config.php +0ac5c957b2102dd338694435ded1621bd549b941c1e787ba5b3f2610a4781f13 includes/installation.php +41db483dd60b0da16a3350440ec046503ea85e53e3b13ab1304d3600ebdeb9fb install.php +5f38eb32880befb989bb8efd4a9f60b80fbad2df1f1df63dd53e139643999e9b scripts/verify-local.py +2876c40f73c0640bd4ad249a594080af500e3e5f044e4b376727ae31b2c68b15 tests/compatibility_regression.php +bc6a87b3e0353ad17cfda231364310af18cb0a093faf10b4b1b6e112640f47e5 tests/dashboard_browser_runtime.js +89bc2ff117b4e89554296180e9eb2ab8f376aa71cf15d01e19e5205dc947b0ba tests/dashboard_data_contract.php +f547ddd666e9631c70b78bf133f1d23dffbbb7e5c0109a41e7ee4614b33bfd42 tests/dashboard_phase2_contract.php +76b3290dfe419f445c1594a21d527f2853c4bce5cc72a3f705f00320acfdeafb tests/installer_smoke.php +c816fb47e9a932d790c866c88fa3a414d02092e4a1259aa2c7f2ec156b5cb612 tests/release_readiness.php +e6cb9a828b53cb9eb43eef7082283c7b7e3c7271384a58da48d531c672cbea30 tests/updater_state_machine.php +36c8a2a8f404bf8d7c88809952be2281e217dec36572228926b05dbd9b466454 update/release-spec.json diff --git a/audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md b/audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md new file mode 100644 index 0000000..11f40ff --- /dev/null +++ b/audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md @@ -0,0 +1,162 @@ +# Licora v5.7.1 — Dashboard Phase 2 Verification & Corrective Forensic Audit + +## Authority + +- Uploaded official baseline: `Licora_v5.7.0_Baseline.zip` +- Baseline ZIP SHA-256: `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +- Baseline archive entries: `450` +- Extracted files: `375` +- Runtime/source files outside `.git`: `301` +- Embedded Git branch: `feature/v5.7.0-dashboard-phase2` +- Embedded Git HEAD: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Embedded Phase 2 working-tree delta: `32 modified + 9 untracked = 41 paths` +- Deleted Phase 2 files: `NONE` +- GitHub v5.7.0 tag at audit time: `NOT PUBLISHED` +- Corrective target: `v5.7.1` + +The uploaded v5.7.0 archive is the immutable forensic parent. All v5.7.1 work is performed in an isolated copy. + +## Approved Corrective Scope + +The user authorized forensic verification of the latest Phase 2 implementation, correction of concrete broken/error/wrong/missing/fake/demo behavior, v5.7.1 version/release/documentation alignment, and creation of a replace-ready delta. No unrelated refactor or feature expansion is authorized. + +## Phase 2 Plan-to-Source Matrix + +| Plan item | Baseline finding | v5.7.1 result | +|---|---|---| +| Compact Dashboard header + refresh metadata | Implemented | Preserved | +| Measured status strip | Implemented from Phase 1 health facts | Preserved | +| Four primary KPI cards | Implemented | Preserved | +| API v1/v2 tracked activity chart | Implemented with source separation | Preserved | +| Expiration timeline | Implemented with past/future separation | Preserved | +| Combined recent activity | Implemented, source-labelled | Preserved | +| Compact Quick Actions | Existing routes only | Preserved | +| Top Licenses — API v1 Verify | Implemented | Preserved | +| Dedicated `dashboard.js` | Implemented | Corrected lifecycle defects | +| 30-second authenticated polling | Implemented | Preserved | +| Request overlap protection | Implemented | Preserved + sync-throw recovery verified | +| Manual refresh | Implemented | Corrected Retry/auth cleanup state | +| Last-updated | Implemented | Corrected to advance after successful render | +| Stale/error state | Implemented | Corrected Retry persistence | +| 401 auth handling | Implemented | Corrected paused/disabled persistence | +| Accessibility/responsive layout | Implemented | Preserved | +| Server-rendered fallback | Implemented | Preserved | +| Backend/data contract | Unchanged from Phase 1 | Preserved | +| DB/API/license/device/auth/Cron/updater/shared shell | Frozen | Preserved | + +No approved Phase 2 feature is missing from source. Final remote/live acceptance remains pending until GitHub CI and manual staging/production smoke are executed. + +## Concrete Findings + +### RC-001 — Stale Retry label overwritten + +**Baseline behavior:** `showStale()` set the refresh label to `Retry`, but the controller `finally` invoked `setLoading(false)`, which reset it to `Refresh`. + +**Impact:** stale data remained visible correctly, but the reviewed Retry UX did not persist. + +**Fix:** non-loading cleanup no longer owns the semantic refresh label. Fresh/stale/auth state methods own their final labels. + +### RC-002 — 401 auth lock undone by cleanup + +**Baseline behavior:** `showAuthRequired()` disabled the button and set `Refresh paused`, then `setLoading(false)` re-enabled the button and restored `Refresh`. + +**Impact:** polling was stopped internally, but the visible control contradicted the auth-paused state and could be clicked without effect. + +**Fix:** the DOM view now keeps an `authLocked` state. Loading cleanup cannot re-enable the refresh control after `AUTH_REQUIRED`. + +### RC-003 — synchronous request throw could escape lifecycle cleanup + +**Baseline behavior:** `Promise.resolve(request())` evaluates `request()` before the Promise is created. A synchronous throw could therefore bypass the normal `.catch()`/`.finally()` chain. + +**Impact:** `inFlight` could remain true and loading UI could remain stuck. + +**Fix:** transport invocation is entered via `Promise.resolve().then(() => request())`, so synchronous throws use the same stale/error/finally path as rejected requests. + +### RC-004 — last-success timestamp advanced before render completed + +**Baseline behavior:** `lastSuccessAt` was updated before `view.render(payload)`. + +**Impact:** if rendering threw, the failed snapshot could be reported as the last successful update. + +**Fix:** a candidate timestamp is held locally and committed to `lastSuccessAt` only after render completes successfully. + +## Regression Coverage Added + +`tests/dashboard_browser_runtime.js` now verifies: + +- stale state keeps `Retry` after loading cleanup; +- auth-required keeps `Refresh paused` and disabled after cleanup; +- synchronous transport exceptions surface stale state and release the request lock; +- a render failure does not advance `lastSuccessAt`. + +The targeted browser/runtime test passed after these changes. + +## Backend / Contract Audit + +Phase 2 requires no backend expansion. The v5.7.0 baseline already consumes the Phase 1 authenticated `GET /admin/ajax/dashboard-data.php` contract. The corrective patch does not change: + +- `includes/dashboard.php`; +- `admin/ajax/dashboard-data.php`; +- database schema or SQL migrations; +- API v1/v2 implementation; +- license/device enforcement; +- authentication/role/session policy; +- Cron mutation behavior; +- updater runtime/signing/state machine; +- sidebar/topbar or non-Dashboard admin workflows. + +## Fake / Demo Audit + +No fake, demo, mock or placeholder runtime implementation was found in the Phase 2 Dashboard runtime files. Test fixtures remain confined to tests. + +## Chrome / Browser Audit + +The application runtime was searched for `chrome.exe`, Google Chrome download URLs, Chrome installer/downloader logic and browser-launch shell/process calls. No Chrome launcher/downloader exists in Licora runtime. The only Chrome-specific markers are the verifier guard itself. Licora is server-rendered PHP software opened by a user-selected compatible browser. + +Therefore the reported “Chrome unavailable → Chrome download failed” behavior is not attributable to this repository. No speculative Chrome downloader was added. + +## Version / Updater Corrective Alignment + +v5.7.1 uses: + +```json +{ + "version": "5.7.1", + "minimum_updater": "5.3.0", + "upgrade_from": ["5.6.1", "5.7.0"], + "delete_files": [], + "migrations": [] +} +``` + +`v5.7.0` was not published as a GitHub tag during this audit, so direct compatibility with the published v5.6.1 release is retained while also accepting an already-applied v5.7.0 source baseline. + +## Verification State + +- Baseline full verifier before corrective changes: `PASS` +- Targeted v5.7.1 Dashboard browser/runtime corrective test: `PASS` +- Final full v5.7.1 verifier after source/version/docs alignment: `PASS` +- `git diff --check` equivalent whitespace gate: `PASS` +- Remote GitHub CI/MySQL/PHP/Windows gate: `PENDING` +- Manual desktop/tablet/mobile live/staging smoke: `PENDING` + + + +## Git Publication Scope + +The corrective delta is 37 paths relative to the uploaded v5.7.0 baseline. The embedded Git HEAD is still the published v5.6.1 merge commit and the v5.7.0 Phase 2 implementation was never committed. Therefore the eventual Phase 2 Git commit must stage the full combined 47-path work tree (32 modified + 15 untracked) relative to `4b430b7`, not only the 37 corrective-delta paths. The exact combined staging list is recorded in `RELEASE_COMMANDS_v5.7.1.md`. + +## Delta Integrity Evidence + +- Corrective delta paths: `37` +- Deleted files: `0` +- SQL migrations added: `0` +- Unsafe ZIP paths: `0` +- private/runtime configuration or signing-key paths in delta: `0` +- clean overlay parity against the v5.7.1 verified work tree: `0 mismatches` +- non-delta v5.7.0 baseline byte-parity mismatches: `0` +- `.git` metadata is excluded from the replace-ready delta and from the clean v5.7.1 source baseline package. + +## Phase 2 Decision + +`INCOMPLETE — v5.7.1 source + local verification PASS; remote CI and manual live/staging smoke are still required before Phase 2 can be marked COMPLETE + VERIFIED.` diff --git a/audit/V5.7.1_DELTA_MANIFEST.md b/audit/V5.7.1_DELTA_MANIFEST.md new file mode 100644 index 0000000..11a5b72 --- /dev/null +++ b/audit/V5.7.1_DELTA_MANIFEST.md @@ -0,0 +1,95 @@ +# Licora v5.7.1 — Dashboard Phase 2 Verification Fix Replace-Ready Delta Manifest + +## Authority + +- Official parent baseline: `Licora_v5.7.0_Baseline.zip` +- Parent baseline SHA-256: `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +- Parent embedded Git HEAD: `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Parent source version: `5.7.0` +- Parent GitHub tag at audit time: `NOT PUBLISHED` +- Target source version: `5.7.1` +- Patch type: `replace-ready Phase 2 verification corrective delta` +- Delta paths: `37` +- Deleted files: `NONE` +- Database migrations: `NONE` + +## Corrective Scope + +1. preserve stale `Retry` after loading cleanup; +2. preserve `401 AUTH_REQUIRED` paused/disabled refresh state; +3. normalize synchronous request transport exceptions into stale/error/finally cleanup; +4. advance `lastSuccessAt` only after successful render; +5. extend Dashboard browser/runtime regression coverage; +6. align v5.7.1 runtime/installer/verifier/release/updater/documentation identity. + +## Frozen / Unchanged + +The patch does not change: + +- `admin/index.php` Phase 2 layout; +- `admin/assets/css/admin-ui.css` Phase 2 styles; +- `includes/dashboard.php`; +- `admin/ajax/dashboard-data.php`; +- API v1/v2 runtime; +- database/migrations; +- license/device/auth/Cron behavior; +- updater runtime/signing/rollback/state machine; +- shared sidebar/topbar or other admin UI. + +## Verification + +- Baseline v5.7.0 full local verifier: `PASS` +- v5.7.1 targeted corrected Dashboard browser/runtime test: `PASS` +- v5.7.1 full local verifier: `PASS` +- targeted release readiness after evidence-doc update: `PASS` +- `git diff --check`: `PASS` +- local DB integration: `SKIPPED — dedicated test DB not enabled` +- updater archive failure/recovery: `SKIPPED — ZipArchive unavailable` +- remote CI/MySQL/PHP/Windows: `PENDING` +- manual desktop/tablet/mobile live/staging smoke: `PENDING` + +Phase 2 remains acceptance-pending until remote CI and manual live/staging smoke are accepted. + +## Replace-Ready Rule + +Extract this delta over the root of the exact uploaded v5.7.0 baseline source and overwrite matching files. Do not delete files and do not run SQL migrations. `.git` metadata and private/runtime configuration are not included in this corrective delta. + +## Delta Paths + +- `.github/workflows/ci.yml` +- `BASELINE_v5.7.1.md` +- `CHANGELOG.md` +- `DASHBOARD_PRODUCTION_UPDATE_INDEX.md` +- `README.md` +- `RELEASE_COMMANDS_v5.7.1.md` +- `RELEASE_NOTES_v5.7.1.md` +- `REPOSITORY_METADATA.md` +- `ROADMAP.md` +- `admin/assets/js/dashboard.js` +- `audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md` +- `audit/V5.7.1_DELTA_MANIFEST.md` +- `audit/V5.7.1_DELTA_SHA256SUMS.txt` +- `config.sample.php` +- `docs/ACTUAL_IMPLEMENTATION_LEDGER.md` +- `docs/ARCHITECTURE.md` +- `docs/CONFIGURATION.md` +- `docs/DASHBOARD_CHANGE_CONTROL.md` +- `docs/DASHBOARD_DATA_CONTRACT.md` +- `docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md` +- `docs/DASHBOARD_UPDATE_PHASE_LOG.md` +- `docs/ERROR_HANDLING_BASELINE_AND_TARGET.md` +- `docs/FEATURE_MATRIX.md` +- `docs/INSTALLATION.md` +- `docs/RELEASE.md` +- `docs/UI_DESIGN_SYSTEM.md` +- `docs/UPGRADE_GUIDE.md` +- `includes/config.php` +- `includes/installation.php` +- `install.php` +- `scripts/verify-local.py` +- `tests/compatibility_regression.php` +- `tests/dashboard_browser_runtime.js` +- `tests/installer_smoke.php` +- `tests/release_readiness.php` +- `tests/updater_state_machine.php` +- `update/release-spec.json` diff --git a/audit/V5.7.1_DELTA_SHA256SUMS.txt b/audit/V5.7.1_DELTA_SHA256SUMS.txt new file mode 100644 index 0000000..79970d2 --- /dev/null +++ b/audit/V5.7.1_DELTA_SHA256SUMS.txt @@ -0,0 +1,36 @@ +97119f056c41ccce2e62b0178ae289621fae87b2c478d17acfa73a8c73a06201 .github/workflows/ci.yml +8efa641932839740d953c2efeec345d6be28624d2e8f9000ad017629ab713e29 BASELINE_v5.7.1.md +79249edba149adbe7f21d014712067d7f4602a2d5571f1dc3aac2fca4e72ab6b CHANGELOG.md +817435388be0a38b907dc5c34d67f3c2daa7b55686d5f48252b35056b0d7fd7e DASHBOARD_PRODUCTION_UPDATE_INDEX.md +0e677854f490c972178bcbb401468d83d96ff993b9a3eef21607ee9fc502bee0 README.md +e02f960b560d4c4f0da2f964e3bc0240727a52d70699200cb766fa44b1fd0282 RELEASE_COMMANDS_v5.7.1.md +03e460bdf93b317a6cb7d139f6b7c37b36ad1ae69d11ed66c7526c44d0c57b14 RELEASE_NOTES_v5.7.1.md +0faeba32647827df5c17815ecfb09f85e2947ef731db1960632f3063223b4c6f REPOSITORY_METADATA.md +9128ccef1ece1f84c2a0e7f9babf08da077b1849f09eea0cb94585d23e5c1722 ROADMAP.md +e33cab082bb852acf52960ca58e7a2f2e83fdac22e64b61e3241922d8cd5d98e admin/assets/js/dashboard.js +193fd682688b527691ffabac072f10de504ddc56b3d42bcc1dc64c64acdb3b95 audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md +ad7f9487e54564f37c492eb782b806da4c83688ea93dd9b144cb26df6a072bb9 audit/V5.7.1_DELTA_MANIFEST.md +34d1988c8c4d40237bd268c34cb4a6d277656d0b419214fe9813785631c6e613 config.sample.php +cff165d1fb63e8c6731206daa904d28aaaa7b559161733c6d86102ac97ba0fe9 docs/ACTUAL_IMPLEMENTATION_LEDGER.md +64bfc89739d45fb43d33fe80dc059f7a4bebc3339ecb006685c8fd60869f2fc1 docs/ARCHITECTURE.md +e12aebee7af11e3aac93360bbcb49a790677c812b4c64e84f247bf98f7e4f89d docs/CONFIGURATION.md +39250e76c6c41c7cd78d18e9366bf2544d6f8f606eca79ef770b26279bafd2d0 docs/DASHBOARD_CHANGE_CONTROL.md +e5d348150148f3351963766f0ca77785ddf2fe3988a715e58e251b5d62474b21 docs/DASHBOARD_DATA_CONTRACT.md +732b6662ed42b74f1dc4fcded4a5ab48fca78b5e3ef16a128635ffcbd27e80af docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md +122183da1f8d3f9be63b353fbfd6631febed11e491e194b0b135f96a11b9f6a5 docs/DASHBOARD_UPDATE_PHASE_LOG.md +b18009f3c7bb364f100253fc15f29a80e26ee735217837b53eef06a14501dad5 docs/ERROR_HANDLING_BASELINE_AND_TARGET.md +887d8e7e7fe09b98cf2231b0fbe78df5aab8c7cda1fbbb8d41aefbdd326da059 docs/FEATURE_MATRIX.md +6b45cab92c9a32adf191675e127e6610380485562d4c17d0e4ce32ec518e9c49 docs/INSTALLATION.md +8491be2191822e82fd1ccdac9ca5ea5766e320ab1f5ad55e1cf361d18602e6c3 docs/RELEASE.md +ff91ff80ba0a3d1f7964e8ddaf78917454b93bda1be02fd178d8421599bfc0da docs/UI_DESIGN_SYSTEM.md +f8eaff1564818971951bac6c4d4ab45efcadb671204582bbfd0f7cfb4fa54b34 docs/UPGRADE_GUIDE.md +f0e1d237f06448bbecc1659fb55e2791fd9fe4452de6fc4748b4c6e71f74ab3f includes/config.php +3a79d4db48ca7fa8657444611a1a4076d018f92ba338fc54fae5be897bb2cd46 includes/installation.php +f318160fabc806baa4aaf3a7af76c9b52be8db9c6c465def21051b760af19a05 install.php +86e91e45fc4cb72e8aa705805c0ff341ebb7533c7312c6335e4ba7f369ed86c3 scripts/verify-local.py +24ac59c7aaf66f19791c7df80bae57c71261c1d7d3bcea69e731ac31afa69325 tests/compatibility_regression.php +fc7f739d71946204d42c53e05b356178a72dbb75e0ba8b9e3367ab342b9f506b tests/dashboard_browser_runtime.js +9267c7c843fa6579a768e43a08a28d56a3f5275883a5c00496919be0f07de6fa tests/installer_smoke.php +7d48609104a9e43774ae5384f289a4c24393fa382321b89292f3f5db8f73fcff tests/release_readiness.php +508c8ae913c8c61a88c25765461e767e4891b7b5ab56c9845b685893a08d7cd5 tests/updater_state_machine.php +398a591574298ada76280c41c2464e57e85d61b1a0e2f0642116e680bc502077 update/release-spec.json diff --git a/config.sample.php b/config.sample.php index e9ed4bd..2e6a149 100644 --- a/config.sample.php +++ b/config.sample.php @@ -6,7 +6,7 @@ if (!defined('DB_PASS')) define('DB_PASS', ''); if (!defined('APP_NAME')) define('APP_NAME', 'Licora'); if (!defined('APP_URL')) define('APP_URL', 'http://localhost/licora'); -if (!defined('APP_VERSION')) define('APP_VERSION', '5.6.1'); +if (!defined('APP_VERSION')) define('APP_VERSION', '5.7.1'); if (!defined('APP_TIMEZONE')) define('APP_TIMEZONE', 'Asia/Dhaka'); if (!defined('APP_LOCALE')) define('APP_LOCALE', 'en'); if (!defined('MAIL_FROM_NAME')) define('MAIL_FROM_NAME', 'Licora'); diff --git a/docs/ACTUAL_IMPLEMENTATION_LEDGER.md b/docs/ACTUAL_IMPLEMENTATION_LEDGER.md index a6d37ef..7f5c447 100644 --- a/docs/ACTUAL_IMPLEMENTATION_LEDGER.md +++ b/docs/ACTUAL_IMPLEMENTATION_LEDGER.md @@ -92,26 +92,40 @@ Source/static verification and corrected remote CI confirm the Phase 1 implement --- -# C. After Phase 2 — Expected Verified Working State +# C. Phase 2 — v5.7.1 Corrective Candidate (from v5.7.0 Source Baseline) | Feature | Current | Target after P2 | |---|---|---| -| Compact dashboard hierarchy | PARTIAL | PLANNED P2 | -| Compact Quick Actions | NOT IMPLEMENTED | PLANNED P2 | -| Reload-free Dashboard polling | NOT IMPLEMENTED | PLANNED P2 | -| In-place KPI updates | NOT IMPLEMENTED | PLANNED P2 | -| In-place Chart.js updates | NOT IMPLEMENTED | PLANNED P2 | -| Recent activity partial refresh | NOT IMPLEMENTED | PLANNED P2 | -| Last updated indicator | NOT IMPLEMENTED | PLANNED P2 | -| Manual Refresh | NOT IMPLEMENTED | PLANNED P2 | -| Stale data indicator | NOT IMPLEMENTED | PLANNED P2 | -| Retry/backoff | NOT IMPLEMENTED | PLANNED P2 | -| No request overlap | NOT IMPLEMENTED | PLANNED P2 | -| Dashboard browser runtime tests | NOT IMPLEMENTED | PLANNED P2 | +| Compact dashboard hierarchy | IMPLEMENTED / ACCEPTANCE PENDING | Compact operations layout implemented | +| Compact Quick Actions | IMPLEMENTED / ACCEPTANCE PENDING | Existing routes only | +| Reload-free Dashboard polling | IMPLEMENTED / ACCEPTANCE PENDING | Authenticated 30-second GET polling | +| In-place KPI updates | IMPLEMENTED / ACCEPTANCE PENDING | Phase 1 snapshot values only | +| In-place Chart.js updates | IMPLEMENTED / ACCEPTANCE PENDING | Existing chart instances reused | +| Recent activity partial refresh | IMPLEMENTED / ACCEPTANCE PENDING | Source-labelled v1/v2 rows | +| Last updated indicator | IMPLEMENTED / ACCEPTANCE PENDING | Uses successful snapshot timestamp | +| Manual Refresh | IMPLEMENTED / ACCEPTANCE PENDING | No navigation/reload | +| Stale data indicator | IMPLEMENTED / ACCEPTANCE PENDING | Preserves last successful snapshot | +| Retry/error UX | IMPLEMENTED / ACCEPTANCE PENDING | Manual Retry; fixed 30-second cadence retained | +| No request overlap | IMPLEMENTED / ACCEPTANCE PENDING | In-flight lock enforced | +| Dashboard browser runtime tests | TARGETED PASS | Full verifier/remote CI pending | ### Phase 2 actual additions -`[EMPTY — fill after verified implementation]` +### v5.7.1 corrective lifecycle fixes + +- stale refresh keeps the `Retry` action after loading cleanup +- `401 AUTH_REQUIRED` keeps refresh disabled and labelled `Refresh paused` +- synchronous transport exceptions enter the normal stale/error path and release the in-flight lock +- `lastSuccessAt` changes only after render completes successfully +- browser/runtime regression tests cover each corrective behavior + + +- `admin/index.php` compact Dashboard composition with server-rendered fallback +- `admin/assets/js/dashboard.js` AJAX/manual refresh controller +- scoped Dashboard CSS appended to `admin/assets/css/admin-ui.css` +- `tests/dashboard_phase2_contract.php` and `tests/dashboard_browser_runtime.js` + +Do not promote these Phase 2 rows to `ACTUAL WORKING` until the v5.7.1 full local verifier, remote CI and the required manual live smoke are accepted. --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cdc09ca..498c4df 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -68,9 +68,9 @@ Licenses use `active`, `expired`, and `suspended` states. Devices use `is_active Migrations are additive. The code includes fallbacks for older schemas, and repository changes should preserve existing endpoints and database columns unless a versioned migration and rollback are supplied. -## Dashboard read model (v5.6.1 current; introduced in v5.6.0) +## Dashboard read model and browser controller (v5.7.0) -Dashboard data is centralized in `includes/dashboard.php`. Both the server-rendered `admin/index.php` and the authenticated read-only `admin/ajax/dashboard-data.php` endpoint use `DashboardReadModel`, preventing the initial page and future AJAX consumer from drifting to different metric definitions. +Dashboard data is centralized in `includes/dashboard.php`. Both the server-rendered `admin/index.php` and the authenticated read-only `admin/ajax/dashboard-data.php` endpoint use `DashboardReadModel`, preventing the initial page and browser refresh controller from drifting to different metric definitions. ```text Admin Dashboard / dashboard-data.php @@ -84,9 +84,9 @@ core v1 tables optional v2 tables The model performs reads only. It does not expire licenses, touch devices, write logs, run cleanup, migrate schema or advance updater jobs. API v1 tracked activity remains sourced from `api_logs`; Secure API v2 tracked activity remains sourced from `v2_audit_logs`. Device presence reporting can read `v2_device_credentials.last_seen_at` where the additive v2 schema exists while falling back safely to the base `devices.last_active` data. -Phase 1 intentionally keeps the existing 30-second full-page Dashboard reload; the JSON endpoint is the backend foundation for the separately-scoped Phase 2 browser refresh controller. +v5.7.0 Phase 2 adds `admin/assets/js/dashboard.js`. The server-rendered snapshot remains the progressive-enhancement fallback; the browser controller polls the authenticated endpoint every 30 seconds, supports manual refresh, updates KPIs/charts/activity in place, prevents overlapping requests, and preserves the last successful snapshot on refresh errors. A 401 pauses polling and surfaces the existing sign-in path. -v5.6.1 corrects Phase 1 verification/runtime truthfulness without changing that boundary: the snapshot now exposes the documented top-level source-separated `recent_activity` view, API v2 `Ready` requires the complete v2 schema plus a cryptographically matching server signing key pair, and the MySQL integration fixture performs foreign-key-safe isolation. +v5.6.1 established the frozen backend truthfulness contract: the snapshot now exposes the documented top-level source-separated `recent_activity` view, API v2 `Ready` requires the complete v2 schema plus a cryptographically matching server signing key pair, and the MySQL integration fixture performs foreign-key-safe isolation. ## Secure API v2 architecture @@ -158,3 +158,7 @@ Supplied assets under `admin/assets/brand/` are presentation resources only and ## Settings and About presentation boundary (v5.5.1) v5.5.1 keeps the v5.4/v5.5 shared component architecture and changes only presentation composition: Settings uses a summary grid plus an API/secondary-stack detail grid, nested Settings navigation is controlled by the shared sidebar component, and About uses reusable product/feature/company metadata components. API, license/device, cron, authentication and updater execution paths are unchanged. + +## Dashboard refresh lifecycle correction (v5.7.1) + +v5.7.1 keeps the v5.7.0 Dashboard architecture unchanged and tightens only controller state transitions: request transport calls are entered through the Promise chain so synchronous throws are caught, `lastSuccessAt` advances only after successful render completion, stale `Retry` survives loading cleanup, and an auth-required state remains locked/disabled after `finally`. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e8c72be..c9c5ed6 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -22,7 +22,7 @@ Environment variables are preferred in managed hosting and containers. On shared | `DB_PASS` | `LICENSE_DB_PASS`, fallback `DB_PASS` | empty | Required when the database account has a password. | | `APP_NAME` | `APP_NAME` | `Licora` | Application label. | | `APP_URL` | `APP_URL` | `http://localhost` | Public application root, without a query string or fragment. | -| `APP_VERSION` | `APP_VERSION` | `5.6.1` | Source release identity returned by the verification API. | +| `APP_VERSION` | `APP_VERSION` | `5.7.1` | Source release identity returned by the verification API. | | `APP_TIMEZONE` | `APP_TIMEZONE` | `Asia/Dhaka` | PHP runtime timezone. | | `APP_LOCALE` | `APP_LOCALE` | `en` | Application locale metadata. | | `MAIL_FROM_NAME` | `MAIL_FROM_NAME` | `APP_NAME` | Mail display name. CR/LF characters are rejected by the installer. | @@ -59,7 +59,7 @@ LICENSE_ALLOWED_ORIGIN=https://app.example.com ## Installer-generated private configuration -A fresh v5.6.1 wizard installation writes `includes/config.local.php` atomically and then creates `includes/.licora-installed`. Preserve both files, together with `includes/.licora-encryption.key` when present, during backup or upgrade operations. +A fresh v5.7.1 wizard installation writes `includes/config.local.php` atomically and then creates `includes/.licora-installed`. Preserve both files, together with `includes/.licora-encryption.key` when present, during backup or upgrade operations. Never commit or publish: @@ -111,6 +111,6 @@ The updater has no arbitrary URL field. Release metadata is obtained only from t The database `settings` table stores `updater_auto_check`, `updater_check_interval_seconds`, `updater_channel`, cached release metadata and updater history pointers. `updater_auto_check=0` disables automatic outbound GitHub checks but does not disable the explicit Super Admin **Check for Updates** action. In updater protocol v1, `updater_channel` is reserved and fixed to `stable`; it is not a selectable beta/dev channel. Automatic checking is notification-only; installation always requires a Super Admin action. -## Dashboard reporting configuration (v5.6.1) +## Dashboard reporting configuration (v5.7.1) -The Phase 1 Dashboard read model adds no deployment secret or database setting. Device `recently_seen` reporting uses a fixed five-minute (`300` second) read-only window. Dashboard AJAX data is authenticated, non-cacheable and contains no private signing keys, API credentials or database credentials. API v2 readiness is a boolean-only check that requires both the full v2 schema and a readable matching private/public signing key pair; key contents and paths are never returned. Cron status is limited to script availability because v5.6.1 does not add a scheduler heartbeat. +The Phase 1 Dashboard read model adds no deployment secret or database setting. Device `recently_seen` reporting uses a fixed five-minute (`300` second) read-only window. Dashboard AJAX data is authenticated, non-cacheable and contains no private signing keys, API credentials or database credentials. API v2 readiness is a boolean-only check that requires both the full v2 schema and a readable matching private/public signing key pair; key contents and paths are never returned. Cron status is limited to script availability because v5.7.1 does not add a scheduler heartbeat. Phase 2 adds no deployment setting or secret; the browser controller consumes the same authenticated read-only snapshot every 30 seconds and on manual refresh. diff --git a/docs/DASHBOARD_CHANGE_CONTROL.md b/docs/DASHBOARD_CHANGE_CONTROL.md index 0b998f2..0d5fded 100644 --- a/docs/DASHBOARD_CHANGE_CONTROL.md +++ b/docs/DASHBOARD_CHANGE_CONTROL.md @@ -138,3 +138,22 @@ Stop implementation and record state if: - tests expose an undocumented existing defect Do not hide the conflict by changing unrelated architecture. + + +### Decision D-002 — v5.7.0 Phase 2 Implementation + +- Approved baseline: published/frozen `v5.6.1` release, commit `4b430b77ccc303aebeadc2852bebd3f11f67452a` +- Approved target: `v5.7.0` +- Scope: compact Dashboard-only composition, dedicated browser controller, authenticated reload-free 30-second polling, manual refresh, last-updated/stale/auth feedback, in-place chart/activity/KPI updates, overlap protection, responsive/accessibility behavior, tests/docs/release candidate delta +- Backend data contract: frozen; no field/semantics change required +- Database/API/license/device/auth/Cron/updater protocol/shared shell: frozen +- GitHub writes: not authorized by implementation approval +- Phase 2 completion: requires full local verifier, remote CI and manual live smoke evidence + +### Decision D-003 — v5.7.1 Phase 2 Verification Corrective + +- Approved source authority: uploaded `Licora_v5.7.0_Baseline.zip` +- Approved target: `v5.7.1` +- Scope: only verified Phase 2 browser lifecycle defects plus regression coverage/version/release/documentation alignment +- Backend/data-contract/schema/API/license/device/auth/Cron/updater/shared-shell scope: frozen +- Browser downloader/launcher implementation: prohibited; no such Licora runtime exists in the audited baseline diff --git a/docs/DASHBOARD_DATA_CONTRACT.md b/docs/DASHBOARD_DATA_CONTRACT.md index f4f0a98..0499505 100644 --- a/docs/DASHBOARD_DATA_CONTRACT.md +++ b/docs/DASHBOARD_DATA_CONTRACT.md @@ -4,7 +4,7 @@ এই document implementation-এর আগে metric meaning freeze করে, যাতে UI label এবং backend query পরে একে অপরের সঙ্গে conflict না করে। -This contract is implemented by the Phase 1 read model and corrected in **Licora v5.6.1**. Phase 2 will consume it for reload-free browser refresh. +This contract was implemented/corrected by Phase 1 and is frozen by the **Licora v5.6.1** baseline. **Licora v5.7.0 Phase 2 consumes it unchanged** for reload-free browser refresh; no backend response field or external API contract is added by Phase 2. ## Endpoint @@ -232,3 +232,7 @@ Data maintenance remains in existing API/admin/cron paths. This contract must not modify existing external API v1/v2 JSON contracts. It is an internal authenticated admin endpoint only. + +## v5.7.1 consumer note + +v5.7.1 consumes this Phase 1 Dashboard response contract unchanged. The corrective work is limited to browser refresh state management and does not add, remove, rename or reinterpret a response field. diff --git a/docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md b/docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md index e06a77e..92f75ae 100644 --- a/docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md +++ b/docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md @@ -231,3 +231,27 @@ Any skipped critical test keeps the phase `INCOMPLETE` unless explicitly accepte PR #8 workflow run `32420291770` passed PHP 8.0–8.4 validation and Windows Python portability but failed the MySQL integration job in `tests/dashboard_db_integration.php` with MySQL error 3730: `v2_device_credentials` could not be dropped while `v2_refresh_tokens.fk_v2_refresh_device` referenced it. v5.6.1 fixes the fixture cleanup using foreign-key-safe isolation. The corrected MySQL job must run green once after the v5.6.1 commit is pushed; an unchanged failing run must not be repeatedly retried. + + +## v5.7.0 Phase 2 Candidate Evidence + +Current source-level evidence before full acceptance: + +- `tests/dashboard_phase2_contract.php`: **PASS** (targeted) +- `tests/dashboard_browser_runtime.js`: **PASS** (targeted) +- full `scripts/verify-local.py`: **PASS** +- remote PHP 8.0–8.4 / MySQL / Windows builder CI: **PENDING — no push yet** +- manual desktop/tablet/mobile/live smoke: **PENDING** + +Phase 2 must not be marked `COMPLETE + VERIFIED` until the remaining gates pass. + +## v5.7.1 Corrective Candidate Evidence + +The v5.7.1 corrective browser-runtime gate must additionally prove: + +- a failed refresh preserves the `Retry` action after loading cleanup; +- `401 AUTH_REQUIRED` preserves the paused/disabled refresh state after loading cleanup; +- a synchronous request transport throw enters stale handling and releases the in-flight lock; +- a render failure does not advance the last-success timestamp. + +The targeted Dashboard browser/runtime test and the full v5.7.1 source verifier pass for these cases. The remote MySQL/CI matrix and manual desktop/tablet/mobile live or staging smoke remain the final acceptance sequence. diff --git a/docs/DASHBOARD_UPDATE_PHASE_LOG.md b/docs/DASHBOARD_UPDATE_PHASE_LOG.md index 37d6e45..997f477 100644 --- a/docs/DASHBOARD_UPDATE_PHASE_LOG.md +++ b/docs/DASHBOARD_UPDATE_PHASE_LOG.md @@ -4,14 +4,15 @@ | Field | Value | |---|---| -| Baseline | `v5.6.0 / 5c68563` (current corrective baseline; phase program originated at v5.5.1) | -| Target version | `v5.6.1` | +| Official baseline | `Licora_v5.7.0_Baseline.zip` / SHA-256 `e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` | +| Baseline embedded Git HEAD | `4b430b77ccc303aebeadc2852bebd3f11f67452a` | +| Target version | `v5.7.1` | | Total phases | `2` | | Completed phases | `1` | | Remaining phases | `1` | -| Current phase | `Phase 2 — NOT STARTED` | -| Runtime code changes | `Phase 1 complete and verified; Phase 2 unchanged/not started` | -| Last confirmed action | PR #8 Actions run `32423210356` passed all 8 required checks at head `ab085ae1738ef49be506cb10ae2353799108a969` | +| Current phase | `Phase 2 — v5.7.1 SOURCE + LOCAL VERIFIED; REMOTE/LIVE GATES PENDING` | +| Runtime code changes | `Dashboard-only compact UI + reload-free AJAX controller; Phase 1 backend contract unchanged` | +| Last confirmed baseline action | `Uploaded v5.7.0 Phase 2 source baseline frozen for corrective audit; GitHub v5.7.0 tag not published` | --- @@ -88,72 +89,84 @@ DB-backed correctness is verified by PR #8 Actions run `32423210356`; no Phase 1 # Phase 2 Log — Compact UI, Reload-Free Refresh & Production Gate -**Status:** `NOT STARTED` +**Status:** `v5.7.1 SOURCE + LOCAL VERIFIED — REMOTE CI / MANUAL LIVE GATES PENDING` -## Planned Features / Changes +## Approved Features / Changes -- compact dashboard composition -- compact Quick Actions -- truthful status strip -- dedicated dashboard JS controller -- AJAX polling -- in-place KPI/chart/activity updates +- compact Dashboard composition and status strip +- four primary KPI cards +- compact Quick Actions over existing routes +- dedicated `admin/assets/js/dashboard.js` controller +- authenticated 30-second AJAX polling +- in-place KPI/chart/activity/top-license updates - manual refresh + last-updated indicator -- stale/error UI -- accessibility behavior -- browser/runtime tests -- final regression + production smoke - -## Completion Record +- stale-data and session-expiry UI +- request-overlap protection +- responsive/accessibility behavior +- browser/runtime contract tests -Fill only after implementation and verification. - -- Started at: -- Completed at: -- Commit: -- Branch: -- Files changed: -- Automated tests: -- DB tests: -- Browser/runtime tests: -- Manual production smoke: -- Known deviations: -- Remaining known issues: - -## Verified Features Added +## Source Implementation Record -`[NONE — phase not started]` +- Started at: `2026-08-20` +- Parent baseline: `Licora_v5.7.0_Baseline.zip / e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022` +- Target: `v5.7.1` +- Database migration: `NO` +- Deleted files: `NONE` +- Backend Dashboard data contract change: `NONE` +- External API change: `NONE` +- Shared sidebar/topbar change: `NONE` +- New runtime controller: `admin/assets/js/dashboard.js` +- New tests: `tests/dashboard_phase2_contract.php`, `tests/dashboard_browser_runtime.js` +- Targeted Phase 2 contract test: `PASS` +- Targeted Dashboard browser/runtime test: `PASS` after v5.7.1 corrective cases were added +- Full v5.7.1 local verifier: `PASS — python3 scripts/verify-local.py` +- Remote CI/MySQL gate: `PENDING — not yet pushed` +- Manual production smoke: `PENDING` + +## Implemented Features + +- initial Dashboard remains server rendered for progressive enhancement +- former `window.location.reload()` 30-second full-page refresh removed +- existing authenticated GET-only Phase 1 snapshot endpoint is consumed unchanged +- manual Refresh updates without navigation/reload +- automatic polling retains the reviewed 30-second cadence +- one request at a time; overlapping poll/manual attempts are skipped while a request is in flight +- last successful values remain visible after network/server failure +- stale warning and Retry state surface without replacing truth with zero/fake data +- 401 pauses polling and surfaces the existing login path +- Chart.js instances are reused and updated in place +- API v1/API v2 source labels remain explicit +- Quick Actions preserve existing admin routes and permissions +- reduced-motion preference disables refresh-icon animation + +## v5.7.1 Corrective Verification Findings + +Manual forensic review of the v5.7.0 browser controller found four concrete client-side lifecycle defects: + +1. stale `Retry` text was overwritten to `Refresh` by final loading cleanup; +2. `401 AUTH_REQUIRED` refresh-disabled/`Refresh paused` state was undone by final loading cleanup; +3. a synchronous request transport throw could escape before the Promise chain and leave `inFlight`/loading stuck; +4. `lastSuccessAt` advanced before render completed, so a render failure could report a failed snapshot as the last successful update. + +The v5.7.1 corrective source fixes only those four behaviors and extends the browser/runtime regression test. Phase 2 layout, backend data contract, polling cadence, API/schema/license/device/auth/Cron/updater behavior and shared shell remain unchanged. ## Phase 2 Exit Decision -`[PENDING]` +`INCOMPLETE — v5.7.1 SOURCE + LOCAL VERIFIED; REMOTE CI AND MANUAL LIVE SMOKE STILL REQUIRED` --- # Program Continuation Pointer -After each development session, update this exact block: - -```text -LAST VERIFIED BASELINE/COMMIT: -COMPLETED PHASES: -CURRENT PHASE: -LAST COMPLETED STEP: -CURRENT WORKTREE STATE: -KNOWN FAILURES: -NEXT EXACT STEP: -DO NOT REPEAT: -``` - -## Current Pointer - ```text -LAST VERIFIED BASELINE/COMMIT: v5.6.1 / ab085ae1738ef49be506cb10ae2353799108a969 +LAST VERIFIED BASELINE: Licora_v5.7.0_Baseline.zip / e198fda3a90f38ef0d15faeab3f0b2797b92ba98b542cb7f22ac8f01b3bda022 +EMBEDDED GIT HEAD: 4b430b77ccc303aebeadc2852bebd3f11f67452a +TARGET SOURCE: v5.7.1 Phase 2 corrective candidate COMPLETED PHASES: 1/2 -CURRENT PHASE: Phase 2 — NOT STARTED -LAST COMPLETED STEP: PR #8 Actions run 32423210356 passed all 8 required checks, including MySQL integration and verified source artifact -CURRENT WORKTREE STATE: Phase 1 verified on remote feature branch; completion documentation update pending commit -KNOWN FAILURES: NONE remaining in Phase 1. Historical run 32420291770 failure is retained above as root-cause evidence. -NEXT EXACT STEP: commit/push this completion-documentation delta, confirm the resulting PR CI is green, then merge PR #8 into main -DO NOT REPEAT: Phase 1 implementation or already-passed verification unless a new concrete failure/code change requires it +CURRENT PHASE: Phase 2 — v5.7.1 SOURCE + LOCAL VERIFIED; REMOTE/LIVE GATES PENDING +LAST COMPLETED STEP: full v5.7.1 local verifier passed after corrective source/tests/version/docs alignment +CURRENT WORKTREE STATE: isolated v5.7.1 corrective work copy; no GitHub write performed +KNOWN FAILURES: NONE remaining in local v5.7.1 source verification; four v5.7.0 lifecycle defects are corrected and recorded +NEXT EXACT STEP: finalize replace-ready v5.7.1 delta integrity evidence; then push only after explicit GitHub-write authorization and run remote CI/MySQL +DO NOT REPEAT: completed Phase 1 gates or targeted corrective test unless new code/error/evidence requires it ``` diff --git a/docs/ERROR_HANDLING_BASELINE_AND_TARGET.md b/docs/ERROR_HANDLING_BASELINE_AND_TARGET.md index 0b4d4fd..6c06453 100644 --- a/docs/ERROR_HANDLING_BASELINE_AND_TARGET.md +++ b/docs/ERROR_HANDLING_BASELINE_AND_TARGET.md @@ -2,7 +2,7 @@ ## Authority -This matrix preserves the original v5.5.1 baseline inventory and records the implemented Phase 1 backend error contract through the v5.6.1 corrective candidate; Phase 2 client-side refresh handling remains planned. +This matrix preserves the original v5.5.1 baseline inventory and records the verified Phase 1 backend error contract through v5.6.1 and the v5.7.0 Phase 2 client-side refresh handling implementation. Status meanings: @@ -291,14 +291,14 @@ Never return: --- -# 11. Dashboard Client-Side Error Handling — `REQUIRED` +# 11. Dashboard Client-Side Error Handling — `IMPLEMENTED IN v5.7.0 SOURCE CANDIDATE` The JS controller must handle: - HTTP 401 → session-expired state / login path - HTTP 405/4xx → stop inappropriate retry loop - HTTP 500 → keep last good data + mark stale -- network timeout/failure → stale indicator + backoff +- network/server failure → preserve the last successful snapshot, show stale feedback, keep the reviewed 30-second cadence, and expose manual Retry - invalid JSON/schema → treat as refresh failure - aborted request → not reported as an application error - duplicate/in-flight refresh → skip/abort safely @@ -323,7 +323,7 @@ For Dashboard AJAX: - include a request/error reference where useful - do not log secrets - do not log complete license/API credentials -- avoid logging successful 15-second polling requests unless diagnostics justify it +- avoid logging successful 30-second polling requests unless diagnostics justify it --- @@ -357,3 +357,14 @@ Not automatically included in the Dashboard two-phase update: - legacy compatibility endpoint redesign These require separate explicit approval if they would enlarge scope. + +# 12. Dashboard Client-Side Corrective Handling — `v5.7.1` + +The v5.7.1 corrective candidate preserves the v5.7.0 stale/auth contract while fixing cleanup ordering: + +- stale state owns the `Retry` action until a new request starts or a successful refresh restores `Refresh`; +- auth-required state owns `Refresh paused` and keeps the refresh control disabled; +- synchronous request transport exceptions are normalized into the same stale path as rejected/failed requests; +- failed rendering does not advance the last-success timestamp. + +No server error envelope or backend exception behavior changes. diff --git a/docs/FEATURE_MATRIX.md b/docs/FEATURE_MATRIX.md index 1bd3369..bfb84d4 100644 --- a/docs/FEATURE_MATRIX.md +++ b/docs/FEATURE_MATRIX.md @@ -33,6 +33,6 @@ | VibTools Compact Light UI | Implemented in v5.5.0 | Compact tables/forms/toolbars/action menus/scrollbars and responsive License/Device recomposition refine the existing v5.4 component shell without changing backend contracts. | | Dashboard centralized read model | Implemented in v5.6.0; contract corrected in v5.6.1 | `DashboardReadModel` supplies the initial Dashboard and authenticated Dashboard JSON endpoint through read-only queries. | | Dashboard data truth | Implemented in v5.6.0; corrected in v5.6.1 | License/device/API/expiration labels are tied to explicit sources; v1/v2 tracked activity is separated and fake operational health claims are removed. | -| Dashboard reload-free refresh | Phase 2 pending | v5.6.1 intentionally retains the existing 30-second full-page reload; AJAX polling/manual refresh/stale-state UX are not yet implemented. | +| Dashboard reload-free refresh | Implemented in v5.7.0 source baseline; v5.7.1 corrective candidate | Dedicated Dashboard controller uses authenticated 30-second AJAX polling, manual refresh, in-place KPI/chart/activity updates, overlap prevention, last-updated feedback and stale/auth states while preserving server-rendered fallback. | | Browser runtime dependency | Browser-agnostic | Licora is server-rendered PHP and contains no Chrome installer/downloader or `chrome.exe` launcher dependency; browser selection belongs to the user/client environment. | | Frontend offline mode | Not implemented | UI depends on external CDNs. | diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index a07efef..ffffd9d 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -1,6 +1,6 @@ # Installation -Licora v5.6.1 provides a first-run installer for fresh deployments while preserving the existing manual installation and upgrade paths. +Licora v5.7.1 provides a first-run installer for fresh deployments while preserving the existing manual installation and upgrade paths. ## Requirements @@ -146,6 +146,15 @@ Fresh v5.5.1 installs include updater persistence in `database.sql`. Existing v5 The first-run UI presents the fixed product name **Licora** and uses the tracked Licora branding assets. The installer still writes the same private configuration/runtime data and does not introduce a database migration. `APP_NAME` remains a compatibility configuration value, but the product UI is not a tenant/site-name customization surface. + +## v5.7.1 Dashboard Phase 2 verification fix + +v5.7.1 adds no installation-time table, column, trigger or migration. It preserves the v5.7.0 Dashboard layout and backend contract while correcting only the client-side refresh lifecycle. Existing v5.6.1 deployments and installations where the v5.7.0 source baseline was already applied are accepted by the v5.7.1 release specification. + +## v5.7.0 Dashboard Phase 2 source baseline + +v5.7.0 adds no installation-time table, column, trigger or migration. Fresh installations receive the compact Dashboard and reload-free browser controller from source. The Dashboard still reads through the v5.6.1 `DashboardReadModel`/authenticated JSON contract; no new secret, scheduler, API or database configuration is introduced. + ## v5.6.1 Dashboard Phase 1 corrected foundation v5.6.1 adds no installation-time table, column, trigger or migration. Fresh installations receive the corrected Dashboard read model and authenticated Dashboard data endpoint from source. The signed source-compatibility contract accepts v5.5.1 and an already-applied v5.6.0 baseline without schema changes. diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 78cdeb1..461cf60 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -2,7 +2,7 @@ ## Current release contract -Licora uses semantic version tags. The current release candidate is `5.6.1`; runtime, installer, verifier, release notes, update release specification and GitHub workflow markers must agree and required CI must pass before a tag can publish. +Licora uses semantic version tags. The current release candidate is `5.7.1`; runtime, installer, verifier, release notes, update release specification and GitHub workflow markers must agree and required CI must pass before a tag can publish. Every v5.3.0+ official release intended for the in-app updater consists of four updater-facing assets: @@ -15,6 +15,36 @@ licora-update-manifest.sig The ZIP/checksum are generated from the exact Git ref by `scripts/package-release.sh`. `scripts/build-update-manifest.py` inventories the exact ZIP, records per-file SHA-256 values, package hash/size, commit identity, migration metadata, protected deletion intent and compatibility requirements. GitHub Actions signs the exact manifest bytes with the dedicated repository secret `LICORA_UPDATE_SIGNING_PRIVATE_KEY`; the matching public key is tracked at `includes/updater/update-signing-public.pem`. +## v5.7.1 release specification + +v5.7.1 is a no-migration Dashboard Phase 2 corrective release candidate over the uploaded v5.7.0 source baseline. It also accepts the published v5.6.1 source so live installations can move directly to the corrected Phase 2 release without requiring an unpublished v5.7.0 tag. + +```json +{ + "protocol_version": 1, + "application": "Licora", + "version": "5.7.1", + "channel": "stable", + "minimum_updater": "5.3.0", + "minimum_php": "8.0", + "upgrade_from": ["5.6.1", "5.7.0"], + "delete_files": [], + "migrations": [] +} +``` + +The reviewed packager command is: + +```bash +bash scripts/package-release.sh v5.7.1 v5.7.1 +``` + +Publication commands are recorded in `RELEASE_COMMANDS_v5.7.1.md`. The release keeps the existing API v2/Admin v2/updater/Dashboard MySQL gate and Phase 2 Dashboard source/runtime verification. + +### v5.7.0 source baseline + +The v5.7.0 Phase 2 source baseline remains historical evidence. It was not published as a GitHub tag during this audit and is superseded by the v5.7.1 corrective candidate. + ## v5.6.1 release specification v5.6.1 is a no-migration corrective release for the Dashboard Phase 1 verification scope: @@ -234,4 +264,4 @@ Release archives must exclude deployment-private/runtime material including `con Every future release intended for one-click installation must update `update/release-spec.json`, declare the exact direct source versions it supports in signed `upgrade_from`, ship every migration required for those supported direct paths, keep `minimum_updater` compatible, and publish the four assets above. A latest release that does not list the installed version in `upgrade_from` is deliberately blocked rather than silently skipping an intermediate migration. Never modify a published manifest/ZIP in place; create a new semantic version. -See [UPDATER.md](UPDATER.md) for the runtime trust/rollback model, [UI_DESIGN_SYSTEM.md](UI_DESIGN_SYSTEM.md) for the v5.4.0 presentation contract, and `RELEASE_COMMANDS_v5.6.1.md` for the current Windows-friendly command sequence. +See [UPDATER.md](UPDATER.md) for the runtime trust/rollback model, [UI_DESIGN_SYSTEM.md](UI_DESIGN_SYSTEM.md) for the v5.4.0 presentation contract, and `RELEASE_COMMANDS_v5.7.1.md` for the current Windows-friendly command sequence. diff --git a/docs/UI_DESIGN_SYSTEM.md b/docs/UI_DESIGN_SYSTEM.md index 07b4c26..234795d 100644 --- a/docs/UI_DESIGN_SYSTEM.md +++ b/docs/UI_DESIGN_SYSTEM.md @@ -201,3 +201,12 @@ Editable Settings controls must map to a current runtime consumer. Stored-only l - Tall/short Settings content must not share a stretching grid row when it creates blank card space; related compact cards may be stacked in a dedicated secondary column. - Sidebar child navigation is collapsed by default, uses a dedicated button with `aria-expanded`/`aria-controls`, and automatically expands when a child route is active. - Product/About pages use shared hero, feature-grid, company and metadata components. Page-specific CSS remains prohibited. + + +## v5.7.0 Dashboard compact operations view + +The Dashboard keeps the shared Licora light shell and uses page-scoped `.dashboard-page` selectors only. The approved hierarchy is: compact header/refresh status → measured health strip → four KPI cards → API/expiration charts → recent activity + Quick Actions → top API v1 licenses. Dashboard-specific CSS must not restyle the shared sidebar/topbar or other admin pages. Status meaning must not rely on color alone, refresh/error feedback uses an ARIA live region, and reduced-motion preference disables the refresh-icon animation. + +## v5.7.1 Dashboard feedback-state correction + +The v5.7.0 compact Dashboard composition is unchanged. v5.7.1 only corrects refresh-control state persistence so `Retry` remains visible after a failed refresh and `Refresh paused` remains disabled after session expiry. No global theme, component, sidebar, topbar or non-Dashboard UI style is changed. diff --git a/docs/UPGRADE_GUIDE.md b/docs/UPGRADE_GUIDE.md index 2ccbf16..43d26b2 100644 --- a/docs/UPGRADE_GUIDE.md +++ b/docs/UPGRADE_GUIDE.md @@ -3,7 +3,7 @@ ## Supported path ```text -v5.0.1 -> v5.0.1.1 -> v5.1.0 -> v5.2.0 -> v5.2.1 -> v5.2.2 -> v5.3.0 -> v5.4.0 -> v5.4.1 -> v5.5.0 -> v5.5.1 -> v5.6.0 -> v5.6.1 +v5.0.1 -> v5.0.1.1 -> v5.1.0 -> v5.2.0 -> v5.2.1 -> v5.2.2 -> v5.3.0 -> v5.4.0 -> v5.4.1 -> v5.5.0 -> v5.5.1 -> v5.6.0 -> v5.6.1 -> v5.7.0 -> v5.7.1 ``` The v5.1.0 installer is for fresh installations only. Existing deployments are never required to reinstall. @@ -154,6 +154,31 @@ v5.4.1 introduces **no database migration** and preserves the v5.4.0 sidebar/com The signed v5.4.1 release accepts reviewed direct source versions `5.3.0` and `5.4.0`, with an empty migration list. A pre-existing active job still targets the version recorded when that job was created and must be resumed/finalized before a later release can start. +## v5.6.1 / v5.7.0 to v5.7.1 Dashboard Phase 2 corrective + +v5.7.1 is a signed **no-migration** corrective candidate for the Phase 2 Dashboard browser lifecycle. The release specification accepts both the published `v5.6.1` source and an already-applied `v5.7.0` source baseline. It preserves the v5.7.0 layout, 30-second AJAX cadence, backend data contract and application behavior while correcting Retry/auth-lock/synchronous-transport/last-success state handling. + +1. Back up application files and the database using the existing procedure. +2. Run the v5.7.1 preflight only after an official signed release is published. +3. Install v5.7.1. +4. Confirm the Dashboard loads from the server-rendered snapshot, refreshes without full-page reload, preserves Retry on failure, and pauses refresh on session expiry. + +## v5.6.1 to v5.7.0 Dashboard Phase 2 source baseline + +v5.7.0 is a signed **no-migration** Dashboard interaction/UI update from the frozen `v5.6.1` baseline. It keeps the Phase 1 read model and authenticated JSON contract unchanged, replaces the 30-second full-page refresh with 30-second authenticated AJAX polling, and adds manual refresh, last-updated/stale/auth feedback, overlap protection and in-place KPI/chart/activity updates. + +1. Preserve normal deployment backups and private configuration/key material. +2. Run the v5.7.0 preflight only after an official signed release is published. +3. Install v5.7.0. +4. Verify initial server-rendered Dashboard content still appears before/without JavaScript refresh. +5. Verify manual Refresh updates data without a full-page reload. +6. Verify the automatic 30-second refresh updates Dashboard data in place and does not overlap a request already in flight. +7. Verify API v1/v2 tracked activity, expiration timeline, recently-seen devices and health facts remain truthful to Phase 1 semantics. +8. Simulate a refresh failure/session expiry in a safe environment and verify the last successful snapshot remains visible with stale/auth feedback. +9. Verify desktop/tablet/mobile layout and existing sidebar/topbar/other admin pages remain unchanged. + +No file is deleted and no database migration is executed. + ## v5.5.1/v5.6.0 to v5.6.1 Phase 1 verification corrective v5.6.1 is a signed **no-migration** corrective update. It accepts the official `v5.5.1` source as well as an already-applied `v5.6.0` source, fixes the Dashboard DB integration fixture, aligns the internal Dashboard JSON contract, and makes API v2 readiness require a valid matching signing key pair. @@ -171,7 +196,7 @@ No file is deleted and no migration is executed. v5.6.0 was the no-migration Phase 1 source baseline, but it was **not published as a GitHub tag/release**. PR #8 CI exposed a Dashboard DB-test fixture defect before release acceptance, and v5.6.1 supersedes it. -Do not wait for or publish a v5.6.0 updater release. Deployments on official v5.5.1 should use the eventual signed v5.6.1 release after its corrected CI/merge/tag gates pass. Deployments where the v5.6.0 source delta was already applied are also accepted by the v5.6.1 release specification. +Do not wait for or publish a v5.6.0 updater release. Deployments on official v5.5.1 should use the published signed v5.6.1 release before the v5.7.0 Phase 2 update. Deployments where the v5.6.0 source delta was already applied are also accepted by the v5.6.1 release specification. No file deletion or database migration is required for either source path. Reload-free Dashboard polling remains Phase 2. diff --git a/includes/config.php b/includes/config.php index c1a632a..07f1645 100644 --- a/includes/config.php +++ b/includes/config.php @@ -29,7 +29,7 @@ function env_value($key, $default = '') { // Resolve release identity before private local configuration is loaded. // This prevents an installer-generated local configuration from pinning // future source upgrades while retaining the APP_VERSION environment override. -if (!defined('APP_VERSION')) define('APP_VERSION', env_value('APP_VERSION', '5.6.1')); +if (!defined('APP_VERSION')) define('APP_VERSION', env_value('APP_VERSION', '5.7.1')); // Optional private local override. Keep this file outside public web root where possible. $localConfig = __DIR__ . '/config.local.php'; diff --git a/includes/installation.php b/includes/installation.php index ec0c7e9..9fc2e0b 100644 --- a/includes/installation.php +++ b/includes/installation.php @@ -233,7 +233,7 @@ function licora_installation_write_flag(?string $root = null, ?string $version = $payload = [ 'product' => 'Licora', - 'version' => $version ?? (defined('APP_VERSION') ? (string)APP_VERSION : '5.6.1'), + 'version' => $version ?? (defined('APP_VERSION') ? (string)APP_VERSION : '5.7.1'), 'installed_at' => gmdate('c'), ]; $json = json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); @@ -596,7 +596,7 @@ function licora_installer_build_config(array $data): string 'DB_PASS' => (string)$data['db']['pass'], 'APP_NAME' => (string)$data['app']['app_name'], 'APP_URL' => rtrim((string)$data['app']['base_url'], '/'), - 'APP_VERSION' => '5.6.1', + 'APP_VERSION' => '5.7.1', 'APP_TIMEZONE' => (string)$data['app']['timezone'], 'APP_LOCALE' => (string)$data['app']['locale'], 'MAIL_FROM_NAME' => (string)$data['app']['mail_from_name'], @@ -606,7 +606,7 @@ function licora_installer_build_config(array $data): string 'JWT_SECRET' => (string)$data['secrets']['jwt_secret'], ]; - $lines = [" $value) { $lines[] = "if (!defined('{$name}')) define('{$name}', " . var_export($value, true) . ');'; } @@ -803,7 +803,7 @@ function licora_installer_finalize(?string $root, array $data): array $flagPayload = json_encode([ 'product' => 'Licora', - 'version' => '5.6.1', + 'version' => '5.7.1', 'installed_at' => gmdate('c'), ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); if ($flagPayload === false || file_put_contents($flagTemporary, $flagPayload . PHP_EOL, LOCK_EX) === false) { @@ -836,7 +836,7 @@ function licora_installer_finalize(?string $root, array $data): array 'locale' => (string)$data['app']['locale'], 'mail_from_name' => (string)$data['app']['mail_from_name'], 'api_base_url' => rtrim((string)$data['app']['base_url'], '/') . '/api/verify.php', - 'installed_version' => '5.6.1', + 'installed_version' => '5.7.1', 'demo_data_installed' => '0', ]; $settingStmt = $pdo->prepare( @@ -882,7 +882,7 @@ function licora_installer_finalize(?string $root, array $data): array @chmod($flagPath, 0600); return [ - 'version' => '5.6.1', + 'version' => '5.7.1', 'username' => (string)$data['admin']['username'], 'application_url' => rtrim((string)$data['app']['base_url'], '/'), 'admin_url' => rtrim((string)$data['app']['base_url'], '/') . '/admin/login.php', diff --git a/install.php b/install.php index 7f9a73e..172c120 100644 --- a/install.php +++ b/install.php @@ -302,7 +302,7 @@ function installer_render_locked(): void
-
Product: Licora   Version: 5.6.1   License: MIT   Database: MySQL/MariaDB
+
Product: Licora   Version: 5.7.1   License: MIT   Database: MySQL/MariaDB
@@ -353,7 +353,7 @@ function installer_render_locked(): void
-

Licora will initialize database.sql, preserving the existing API v1 schema and adding only the Secure API v2 tables introduced in v5.2.0 and retained unchanged through v5.6.1.

+

Licora will initialize database.sql, preserving the existing API v1 schema and adding only the Secure API v2 tables introduced in v5.2.0 and retained unchanged through v5.7.1.

The target database must not already contain Licora tables. Unrelated existing tables are never removed. If installation fails, only installer-created objects are cleaned up.
Database
@@ -409,7 +409,7 @@ function installer_render_locked(): void Restart
- + diff --git a/scripts/verify-local.py b/scripts/verify-local.py index a49f8a8..09a00aa 100644 --- a/scripts/verify-local.py +++ b/scripts/verify-local.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Local source verifier for Licora v5.6.1. +"""Local source verifier for Licora v5.7.1. This verifier validates source and tests only. It never creates a Git tag, release, or GitHub artifact. Release packaging is intentionally owned by GitHub Actions and @@ -17,7 +17,7 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] -VERSION = "5.6.1" +VERSION = "5.7.1" V1_GIT_BLOBS = { "api/verify.php": "4dc549c2afea0772d3f2ffa8b330fd24b8b13ec2", @@ -28,6 +28,8 @@ REQUIRED = [ "README.md", "CHANGELOG.md", "SECURITY.md", "REPOSITORY_METADATA.md", + "RELEASE_NOTES_v5.7.1.md", "RELEASE_COMMANDS_v5.7.1.md", "BASELINE_v5.7.1.md", + "RELEASE_NOTES_v5.7.0.md", "RELEASE_COMMANDS_v5.7.0.md", "BASELINE_v5.7.0.md", "RELEASE_NOTES_v5.6.1.md", "RELEASE_COMMANDS_v5.6.1.md", "RELEASE_NOTES_v5.6.0.md", "RELEASE_COMMANDS_v5.6.0.md", "RELEASE_NOTES_v5.5.1.md", "RELEASE_COMMANDS_v5.5.1.md", @@ -40,13 +42,15 @@ "audit/V5.2.1_PHASE02_STEP001_FORENSIC_AUDIT.md", "audit/V5.2.1_DELTA_PATCH_MANIFEST.txt", "audit/V5.2.1_DELTA_FILE_SHA256SUMS.txt", "audit/V5.4.0_UI_MIGRATION_AUDIT.md", "audit/V5.4.1_SCOPE_INTEGRITY_AUDIT.md", "audit/V5.4.1_ROOT_CAUSE_MATRIX.md", + "audit/V5.7.1_DASHBOARD_PHASE2_VERIFICATION_AUDIT.md", + "audit/V5.7.0_DASHBOARD_PHASE2_AUDIT.md", "audit/V5.6.1_PHASE1_VERIFICATION_AUDIT.md", "audit/V5.6.0_DASHBOARD_PHASE1_AUDIT.md", "audit/V5.5.1_UI_HOTFIX_AUDIT.md", "audit/V5.5.0_UI_FORENSIC_AUDIT.md", "audit/V5.5.0_SCOPE_VERIFICATION.md", "migration-v5.2.0-api-v2.sql", "migration-v5.3.0-updater.sql", "database.sql", "includes/.htaccess", "api/verify.php", "api/check_license.php", - "includes/dashboard.php", "admin/ajax/dashboard-data.php", + "includes/dashboard.php", "admin/ajax/dashboard-data.php", "admin/assets/js/dashboard.js", "api/v2/activate.php", "api/v2/refresh.php", "api/v2/status.php", "api/v2/deactivate.php", "includes/v2/V2Exception.php", "includes/v2/V2KeyManager.php", "includes/v2/V2TokenService.php", "includes/v2/V2DeviceProof.php", "includes/v2/ApiV2.php", "includes/v2/V2Repository.php", "includes/v2/V2Provisioner.php", "includes/v2/bootstrap.php", @@ -57,7 +61,7 @@ "tests/api_v1_freeze.php", "tests/api_v2_crypto.php", "tests/api_v2_static.php", "tests/api_v2_db_integration.php", "tests/admin_v2_ui_db_integration.php", "tests/updater_static.php", "tests/updater_manifest.php", "tests/updater_state_machine.php", "tests/updater_failure_recovery.php", "tests/updater_ui_contract.php", "tests/updater_db_integration.php", "tests/updater_dom_contract.php", "tests/updater_builder_contract.py", "tests/updater_browser_runtime.js", "tests/sidebar_submenu_runtime.js", "tests/ui_route_contract.php", "tests/ui_form_contract.php", "tests/ui_component_contract.php", "tests/ui_updater_contract.php", "tests/ui_v550_contract.php", "tests/ui_v551_contract.php", - "tests/dashboard_data_contract.php", "tests/dashboard_db_integration.php", + "tests/dashboard_data_contract.php", "tests/dashboard_phase2_contract.php", "tests/dashboard_db_integration.php", "tests/dashboard_browser_runtime.js", "docs/API_V2.md", "docs/API_V2_SECURITY.md", "docs/API_V2_CLIENT_INTEGRATION.md", "docs/API_V2_MIGRATION.md", "docs/CONFIGURATION.md", "docs/ARCHITECTURE.md", "docs/RELEASE.md", "docs/INSTALLATION.md", "docs/UPGRADE_GUIDE.md", "docs/FEATURE_MATRIX.md", "docs/UPDATER.md", "docs/UI_DESIGN_SYSTEM.md", "DASHBOARD_PRODUCTION_UPDATE_INDEX.md", "docs/DASHBOARD_PRODUCTION_ROADMAP_2_PHASE.md", "docs/DASHBOARD_UPDATE_PHASE_LOG.md", "docs/ERROR_HANDLING_BASELINE_AND_TARGET.md", "docs/ACTUAL_IMPLEMENTATION_LEDGER.md", "docs/DASHBOARD_DATA_CONTRACT.md", "docs/DASHBOARD_PRODUCTION_VALIDATION_GATES.md", "docs/DASHBOARD_CHANGE_CONTROL.md", "audit/V5.5.1_DASHBOARD_PRODUCTION_READINESS_FORENSIC_REPORT.md", @@ -88,6 +92,7 @@ "tests/ui_v550_contract.php", "tests/ui_v551_contract.php", "tests/dashboard_data_contract.php", + "tests/dashboard_phase2_contract.php", "tests/dashboard_db_integration.php", ] @@ -148,10 +153,10 @@ def require_action_minimum(text: str, action: str, minimum: tuple[int, ...], lab print("[3/12] Release/version consistency") config = read("includes/config.php") if f"env_value('APP_VERSION', '{VERSION}')" not in config: - fail("runtime APP_VERSION is not 5.6.1") -for rel in ["config.sample.php", "install.php", "includes/installation.php", "RELEASE_NOTES_v5.6.1.md", "CHANGELOG.md", "REPOSITORY_METADATA.md"]: + fail("runtime APP_VERSION is not 5.7.1") +for rel in ["config.sample.php", "install.php", "includes/installation.php", "RELEASE_NOTES_v5.7.1.md", "CHANGELOG.md", "REPOSITORY_METADATA.md"]: if VERSION not in read(rel): - fail(f"5.6.1 release marker missing from {rel}") + fail(f"5.7.1 release marker missing from {rel}") print("[4/12] API v2 protocol/security contract") v2_endpoint_text = "\n".join(read(f"api/v2/{name}.php") for name in ("activate", "refresh", "status", "deactivate")) @@ -205,17 +210,17 @@ def require_action_minimum(text: str, action: str, minimum: tuple[int, ...], lab if "-- Licora v5.3.0 Secure In-App Updater additive migration." not in read("database.sql"): fail("fresh-install database.sql does not contain updater additive schema") release_spec = read("update/release-spec.json") -for marker in ['\"protocol_version\": 1', '\"version\": \"5.6.1\"', '\"minimum_updater\": \"5.3.0\"', '\"upgrade_from\"']: +for marker in ['\"protocol_version\": 1', '\"version\": \"5.7.1\"', '\"minimum_updater\": \"5.3.0\"', '\"upgrade_from\"']: if marker not in release_spec: fail(f"updater release-spec marker missing: {marker}") if 'migration-v5.3.0-updater.sql' in release_spec: - fail('v5.6.1 dashboard corrective release spec must not replay the v5.3.0 updater migration') + fail('v5.7.1 Dashboard Phase 2 corrective release spec must not replay the v5.3.0 updater migration') import json as _json _release_spec_data = _json.loads(release_spec) -if _release_spec_data.get("upgrade_from") != ["5.5.1", "5.6.0"]: - fail("v5.6.1 release spec must accept the official v5.5.1 source and applied v5.6.0 baseline") +if _release_spec_data.get("upgrade_from") != ["5.6.1", "5.7.0"]: + fail("v5.7.1 release spec must accept both the published v5.6.1 source and the applied v5.7.0 source baseline") if _release_spec_data.get("migrations") != []: - fail("v5.6.1 dashboard Phase 1 corrective release must not declare a database migration") + fail("v5.7.1 Dashboard Phase 2 corrective release must not declare a database migration") print("[6/12] Signing-key and secret hygiene") for rel in [ @@ -325,6 +330,7 @@ def require_action_minimum(text: str, action: str, minimum: tuple[int, ...], lab read(rel) for rel in [ "admin/index.php", "admin/assets/js/admin-ui.js", + "admin/assets/js/dashboard.js", "admin/assets/js/licora-updater.js", "admin/assets/js/update-notifier.js", "index.php", @@ -375,12 +381,13 @@ def require_action_minimum(text: str, action: str, minimum: tuple[int, ...], lab print("[11/12] JavaScript syntax/runtime") node = shutil.which("node") -js_files = [ROOT / "admin/assets/js/admin-ui.js", ROOT / "admin/assets/js/components/sidebar.js", ROOT / "admin/assets/js/licora-updater.js", ROOT / "admin/assets/js/update-notifier.js"] +js_files = [ROOT / "admin/assets/js/admin-ui.js", ROOT / "admin/assets/js/dashboard.js", ROOT / "admin/assets/js/components/sidebar.js", ROOT / "admin/assets/js/licora-updater.js", ROOT / "admin/assets/js/update-notifier.js"] if node and all(js.is_file() for js in js_files): for js in js_files: run([node, "--check", str(js)]) run([node, "tests/updater_browser_runtime.js"]) run([node, "tests/sidebar_submenu_runtime.js"]) + run([node, "tests/dashboard_browser_runtime.js"]) else: print("Node.js not installed; JavaScript syntax/runtime check skipped locally.") @@ -394,4 +401,4 @@ def require_action_minimum(text: str, action: str, minimum: tuple[int, ...], lab if marker not in packager: fail(f"release packaging marker missing: {marker}") -print("Licora v5.6.1 local verification passed.") +print("Licora v5.7.1 local verification passed.") diff --git a/tests/compatibility_regression.php b/tests/compatibility_regression.php index 4c1c2e3..369dd30 100644 --- a/tests/compatibility_regression.php +++ b/tests/compatibility_regression.php @@ -97,7 +97,7 @@ } $config = $read('includes/config.php'); -$assert(strpos($config, "env_value('APP_VERSION', '5.6.1')") !== false, 'application version is v5.6.1'); +$assert(strpos($config, "env_value('APP_VERSION', '5.7.1')") !== false, 'application version is v5.7.1'); $assert(strpos($config, "if (!defined('DB_PORT'))") !== false, 'database port support is additive'); $assert(strpos($config, 'licora_enforce_installation_guard') !== false, 'first-run guard is enabled before application boot'); diff --git a/tests/dashboard_browser_runtime.js b/tests/dashboard_browser_runtime.js new file mode 100644 index 0000000..7d03c8f --- /dev/null +++ b/tests/dashboard_browser_runtime.js @@ -0,0 +1,240 @@ +'use strict'; + + +function fakeClassList() { + const values = new Set(); + return { + add: (...names) => names.forEach(name => values.add(name)), + remove: (...names) => names.forEach(name => values.delete(name)), + toggle: (name, force) => { + if (force === true) { values.add(name); return true; } + if (force === false) { values.delete(name); return false; } + if (values.has(name)) { values.delete(name); return false; } + values.add(name); return true; + }, + contains: name => values.has(name) + }; +} + +function fakeNode() { + return { + disabled: false, + hidden: false, + textContent: '', + classList: fakeClassList() + }; +} + +function createMinimalDocument() { + const nodes = { + '[data-dashboard-refresh]': fakeNode(), + '[data-dashboard-refresh-text]': fakeNode(), + '[data-dashboard-updated-at]': fakeNode(), + '[data-dashboard-state]': fakeNode(), + '[data-dashboard-state-text]': fakeNode(), + '[data-dashboard-signin]': fakeNode() + }; + return { + nodes, + querySelector: selector => nodes[selector] || null, + defaultView: { getComputedStyle: () => ({ getPropertyValue: () => '' }) }, + documentElement: {} + }; +} + +const assert = require('assert'); +const dashboard = require('../admin/assets/js/dashboard.js'); + +function payload(generatedAt) { + return { + success: true, + generated_at: generatedAt || '2026-08-20T22:00:00+00:00', + data: { + licenses: { total: 10, active: 7, expired: 2, suspended: 1, expiring_soon: 3 }, + devices: { recently_seen: 4, active_flagged: 5, total_records: 6 }, + api_keys: { total: 2, active: 1 }, + api_activity: { + v1_tracked: { + last_14_days: [{ date: '2026-08-19', count: 2 }, { date: '2026-08-20', count: 3 }], + top_licenses: [{ license_key: 'AAAA1111-BBBB2222', count: 5 }] + }, + v2_tracked: { last_14_days: [{ date: '2026-08-20', count: 4 }] } + }, + recent_activity: { + v1_tracked: [{ endpoint: 'verify', license_key: 'AAAA1111-BBBB2222', response_code: 200, created_at: '2026-08-20 22:00:00' }], + v2_tracked: [{ event_type: 'activation_success', app_id: 'desktop', license_id: 1, created_at: '2026-08-20 22:01:00' }] + }, + expiration: { + expired_last_30_days: [{ date: '2026-08-18', count: 1 }], + expiring_next_30_days: [{ date: '2026-08-25', count: 2 }] + }, + health: { + database: { ok: true, label: 'Connected' }, + php: { ok: true, version: '8.4.24' }, + environment: { value: 'production' }, + cron_scripts: { available: true }, + api_v2: { schema_ready: true, key_pair_ready: true } + } + } + }; +} + +function response(status, body) { + return { + status, + ok: status >= 200 && status < 300, + json: async () => body + }; +} + +async function run() { + const aligned = dashboard.alignSeries( + [{ date: '2026-08-19', count: 2 }, { date: '2026-08-20', count: 3 }], + [{ date: '2026-08-20', count: 4 }, { date: '2026-08-21', count: 5 }] + ); + assert.deepStrictEqual(aligned.labels, ['2026-08-19', '2026-08-20', '2026-08-21']); + assert.deepStrictEqual(aligned.left, [2, 3, 0]); + assert.deepStrictEqual(aligned.right, [0, 4, 5]); + + const activity = dashboard.combineRecentActivity(payload().data.recent_activity); + assert.strictEqual(activity.length, 2); + assert.strictEqual(activity[0].source, 'API v2'); + assert.strictEqual(activity[1].source, 'API v1'); + assert.strictEqual(activity[1].result, '200'); + + assert.doesNotThrow(() => dashboard.validatePayload(payload())); + assert.throws(() => dashboard.validatePayload({ success: true, data: {} }), /missing licenses/); + + const events = []; + let timerCallback = null; + let timerMs = null; + let clearedTimer = null; + const view = { + render: value => events.push(['render', value.generated_at]), + showFresh: value => events.push(['fresh', value]), + showStale: value => events.push(['stale', value]), + showAuthRequired: () => events.push(['auth']), + setLoading: (value, reason) => events.push(['loading', value, reason]) + }; + const controller = dashboard.createController({ + view, + pollMs: 30000, + request: async () => response(200, payload('2026-08-20T22:02:00+00:00')), + setTimer: (callback, ms) => { timerCallback = callback; timerMs = ms; return 77; }, + clearTimer: id => { clearedTimer = id; } + }); + controller.seed(payload()); + assert.strictEqual(controller.getLastSuccessAt(), '2026-08-20T22:00:00+00:00'); + controller.start(); + assert.strictEqual(timerMs, 30000, 'polling keeps the approved 30-second cadence'); + assert.strictEqual(typeof timerCallback, 'function', 'poll callback is registered'); + await controller.refresh('manual'); + assert.strictEqual(controller.getLastSuccessAt(), '2026-08-20T22:02:00+00:00'); + assert(events.some(event => event[0] === 'loading' && event[1] === true && event[2] === 'manual')); + assert(events.some(event => event[0] === 'loading' && event[1] === false && event[2] === 'manual')); + + let resolvePending; + let requestCount = 0; + const overlapController = dashboard.createController({ + view: {}, + request: () => { + requestCount += 1; + return new Promise(resolve => { resolvePending = resolve; }); + } + }); + const first = overlapController.refresh('poll'); + const second = await overlapController.refresh('poll'); + assert.deepStrictEqual(second, { skipped: true }); + assert.strictEqual(requestCount, 1, 'overlapping refresh does not start a second request'); + resolvePending(response(200, payload())); + await first; + + let staleCount = 0; + let renderCount = 0; + const staleController = dashboard.createController({ + view: { render: () => { renderCount += 1; }, showFresh: () => {}, showStale: () => { staleCount += 1; }, setLoading: () => {} }, + request: async () => response(500, { success: false, code: 'DASHBOARD_DATA_ERROR' }) + }); + staleController.seed(payload()); + await staleController.refresh('manual'); + assert.strictEqual(staleCount, 1, 'failed refresh enters stale state'); + assert.strictEqual(renderCount, 1, 'failed refresh preserves the last successful rendered snapshot'); + + let authShown = 0; + let authTimerCleared = null; + const authController = dashboard.createController({ + view: { showAuthRequired: () => { authShown += 1; }, setLoading: () => {} }, + request: async () => response(401, { success: false, code: 'AUTH_REQUIRED' }), + setTimer: () => 91, + clearTimer: id => { authTimerCleared = id; } + }); + authController.start(); + const authResult = await authController.refresh('poll'); + assert.strictEqual(authResult.authRequired, true); + assert.strictEqual(authShown, 1, '401 surfaces the auth-required state'); + assert.strictEqual(authController.isStopped(), true, '401 stops future automatic polling'); + assert.strictEqual(authTimerCleared, 91, '401 clears the active polling timer'); + + let syncThrowStale = 0; + const syncThrowController = dashboard.createController({ + view: { showStale: () => { syncThrowStale += 1; }, setLoading: () => {} }, + request: () => { throw new Error('transport unavailable'); } + }); + const syncThrowResult = await syncThrowController.refresh('manual'); + assert(syncThrowResult.error instanceof Error, 'synchronous transport errors are converted into the normal stale result'); + assert.strictEqual(syncThrowStale, 1, 'synchronous transport errors enter stale state'); + assert.strictEqual(syncThrowController.isInFlight(), false, 'synchronous transport errors always release the request lock'); + + let renderFailureStale = 0; + const renderFailureController = dashboard.createController({ + view: { + render: value => { + if (value.generated_at === '2026-08-20T22:03:00+00:00') throw new Error('render failed'); + }, + showFresh: () => {}, + showStale: () => { renderFailureStale += 1; }, + setLoading: () => {} + }, + request: async () => response(200, payload('2026-08-20T22:03:00+00:00')) + }); + renderFailureController.seed(payload('2026-08-20T22:00:00+00:00')); + await renderFailureController.refresh('poll'); + assert.strictEqual(renderFailureStale, 1, 'render failures enter stale state'); + assert.strictEqual( + renderFailureController.getLastSuccessAt(), + '2026-08-20T22:00:00+00:00', + 'last-success time advances only after a complete successful render' + ); + + const fakeDocument = createMinimalDocument(); + const domView = dashboard.createDomView(fakeDocument, null); + domView.showStale('2026-08-20T22:00:00+00:00'); + domView.setLoading(false); + assert.strictEqual( + fakeDocument.nodes['[data-dashboard-refresh-text]'].textContent, + 'Retry', + 'stale state keeps the Retry label after loading completes' + ); + domView.showAuthRequired(); + domView.setLoading(false); + assert.strictEqual( + fakeDocument.nodes['[data-dashboard-refresh-text]'].textContent, + 'Refresh paused', + 'auth-required state keeps the paused label after loading completes' + ); + assert.strictEqual( + fakeDocument.nodes['[data-dashboard-refresh]'].disabled, + true, + 'auth-required state keeps manual refresh disabled after loading completes' + ); + + controller.stop(); + assert.strictEqual(clearedTimer, 77, 'controller stop clears its timer'); + + console.log('Dashboard browser/runtime checks passed.'); +} + +run().catch(error => { + console.error(error && error.stack ? error.stack : error); + process.exit(1); +}); diff --git a/tests/dashboard_data_contract.php b/tests/dashboard_data_contract.php index 62445d0..502a345 100644 --- a/tests/dashboard_data_contract.php +++ b/tests/dashboard_data_contract.php @@ -52,13 +52,13 @@ $assert(strpos($dashboard, 'Tracked API Activity') !== false, 'dashboard uses truthful API activity label'); $assert(strpos($dashboard, 'Expiration Timeline') !== false, 'dashboard uses truthful expiration label'); $assert(strpos($dashboard, 'Top Licenses — API v1 Verify') !== false, 'top licenses identify API v1 source'); -$assert(strpos($dashboard, 'Recent API v1 Verify Calls') !== false, 'recent calls identify API v1 source'); +$assert(strpos($dashboard, 'Recent Activity') !== false, 'Phase 2 recent activity panel consumes the source-separated Phase 1 contract'); $assert(strpos($dashboard, '>Security') === false, 'hardcoded Security Active dashboard row removed'); $assert(strpos($dashboard, '>API Server') === false, 'hardcoded API Server Running dashboard row removed'); $assert(strpos($dashboard, "ENVIRONMENT === 'production' ? 'Live' : 'Dev'") === false, 'production environment is not labeled as live health'); $assert(strpos($dashboard, "\$health['api_v2']['key_pair_ready']") !== false, 'API v2 Ready UI requires a verified signing key pair'); -$assert(strpos($dashboard, 'window.location.reload()') !== false, 'Phase 1 intentionally preserves baseline 30-second full reload for Phase 2'); -$assert(strpos($dashboard, 'dashboard.js') === false, 'Phase 2 dashboard polling controller is not introduced early'); +$assert(strpos($dashboard, 'window.location.reload()') === false, 'Phase 2 removes the former 30-second full-page reload'); +$assert(strpos($dashboard, 'dashboard.js') !== false, 'Phase 2 Dashboard controller consumes the frozen Phase 1 endpoint'); if ($failures !== []) { fwrite(STDERR, "Dashboard data contract test failed:\n- " . implode("\n- ", $failures) . "\n"); diff --git a/tests/dashboard_phase2_contract.php b/tests/dashboard_phase2_contract.php new file mode 100644 index 0000000..5bc396a --- /dev/null +++ b/tests/dashboard_phase2_contract.php @@ -0,0 +1,75 @@ + '5.6.1'") !== false, 'generated installer configuration targets v5.6.1'); +$assert(strpos($installation, "'APP_VERSION' => '5.7.1'") !== false, 'generated installer configuration targets v5.7.1'); $assert(strpos($installer, 'First-Run Installer') !== false, 'installer uses the compact Licora first-run branding'); $assert(strpos($installer, 'assets/brand/logos/logo-md.png') !== false, 'installer uses the supplied Licora logo asset'); $assert(strpos($installer, 'name="app_name" value="Licora"') !== false, 'installer fixes the visible product identity to Licora'); @@ -106,6 +106,12 @@ ); foreach ([ + 'RELEASE_NOTES_v5.7.1.md', + 'RELEASE_COMMANDS_v5.7.1.md', + 'BASELINE_v5.7.1.md', + 'RELEASE_NOTES_v5.7.0.md', + 'RELEASE_COMMANDS_v5.7.0.md', + 'BASELINE_v5.7.0.md', 'RELEASE_NOTES_v5.6.1.md', 'RELEASE_COMMANDS_v5.6.1.md', 'RELEASE_NOTES_v5.6.0.md', @@ -127,10 +133,10 @@ $assert(is_file($root . '/' . $path), 'release documentation exists: ' . $path); } -$assert(strpos($releaseNotes, 'Licora v5.6.1') !== false, 'release notes identify v5.6.1'); -$assert(strpos($changelog, '## [5.6.1] - 2026-08-20') !== false, 'changelog contains the v5.6.1 release date'); -$assert(strpos($configuration, '`APP_VERSION` | `APP_VERSION` | `5.6.1`') !== false, 'configuration reference matches v5.6.1 runtime version'); -$assert(strpos($releaseGuide, 'scripts/package-release.sh v5.6.1 v5.6.1') !== false, 'release guide uses the v5.6.1 packager command'); +$assert(strpos($releaseNotes, 'Licora v5.7.1') !== false, 'release notes identify v5.7.1'); +$assert(strpos($changelog, '## [5.7.1] - 2026-08-20') !== false, 'changelog contains the v5.7.1 release date'); +$assert(strpos($configuration, '`APP_VERSION` | `APP_VERSION` | `5.7.1`') !== false, 'configuration reference matches v5.7.1 runtime version'); +$assert(strpos($releaseGuide, 'scripts/package-release.sh v5.7.1 v5.7.1') !== false, 'release guide uses the v5.7.1 packager command'); $assert(strpos($packager, 'git archive --format=zip') !== false, 'release package is created from a Git ref'); $assert(strpos($packager, 'git diff --quiet') !== false, 'release packager rejects tracked working-tree changes'); @@ -139,10 +145,10 @@ 'includes/installation.php', 'install.php', 'config.sample.php', - 'RELEASE_NOTES_v5.6.1.md', + 'RELEASE_NOTES_v5.7.1.md', ] as $path) { $content = $read($path); - $assert(strpos($content, '5.6.2') === false, 'v5.6.1 release file does not contain future version marker: ' . $path); + $assert(strpos($content, '5.7.2') === false, 'v5.7.1 release file does not contain future version marker: ' . $path); } if ($failures !== []) { diff --git a/tests/updater_state_machine.php b/tests/updater_state_machine.php index 3b4d4d3..715b598 100644 --- a/tests/updater_state_machine.php +++ b/tests/updater_state_machine.php @@ -2,4 +2,4 @@ declare(strict_types=1); $root=dirname(__DIR__);require_once $root.'/includes/updater/UpdateException.php';require_once $root.'/includes/updater/UpdateRuntime.php';require_once $root.'/includes/updater/UpdateService.php'; function us_ok($v,string $m):void{if(!$v){fwrite(STDERR,"FAIL: {$m}\n");exit(1);}} -$expected=['fetch_manifest','preflight','download','stage_archive','backup_source','backup_database','lock_update','migrate','apply_files','post_verify','cleanup','rollback_migrations','rollback_source','rollback_finalize'];foreach($expected as $stage){us_ok(in_array($stage,UpdateService::STAGES,true),'missing state '.$stage);} $lock=(string)file_get_contents($root.'/includes/updater/UpdateLock.php');foreach(['Retry-After: 5','UPDATE_IN_PROGRESS','updates\\.php','login\\.php','update-[A-Za-z0-9_-]+\\.php'] as $marker){us_ok(strpos($lock,$marker)!==false,'update-lock recovery contract missing '.$marker);} $spec=json_decode((string)file_get_contents($root.'/update/release-spec.json'),true);us_ok(($spec['version']??'')==='5.6.1','release spec version');us_ok(($spec['channel']??'')==='stable','release spec stable channel');us_ok(($spec['upgrade_from']??[])===['5.5.1','5.6.0'],'v5.6.1 release spec accepts the v5.5.1 official baseline and applied v5.6.0 baseline');us_ok(($spec['migrations']??[])===[],'v5.6.1 dashboard Phase 1 corrective must not declare a database migration');foreach($spec['migrations']??[] as $m){us_ok(!empty($m['idempotent'])||(!empty($m['destructive'])&&!empty($m['rollback_path'])),'migration must be idempotent or rollback-protected');}echo "Updater state-machine checks passed.\n"; +$expected=['fetch_manifest','preflight','download','stage_archive','backup_source','backup_database','lock_update','migrate','apply_files','post_verify','cleanup','rollback_migrations','rollback_source','rollback_finalize'];foreach($expected as $stage){us_ok(in_array($stage,UpdateService::STAGES,true),'missing state '.$stage);} $lock=(string)file_get_contents($root.'/includes/updater/UpdateLock.php');foreach(['Retry-After: 5','UPDATE_IN_PROGRESS','updates\\.php','login\\.php','update-[A-Za-z0-9_-]+\\.php'] as $marker){us_ok(strpos($lock,$marker)!==false,'update-lock recovery contract missing '.$marker);} $spec=json_decode((string)file_get_contents($root.'/update/release-spec.json'),true);us_ok(($spec['version']??'')==='5.7.1','release spec version');us_ok(($spec['channel']??'')==='stable','release spec stable channel');us_ok(($spec['upgrade_from']??[])===['5.6.1','5.7.0'],'v5.7.1 release spec accepts published v5.6.1 and applied v5.7.0 source baselines');us_ok(($spec['migrations']??[])===[],'v5.7.1 Dashboard Phase 2 UI release must not declare a database migration');foreach($spec['migrations']??[] as $m){us_ok(!empty($m['idempotent'])||(!empty($m['destructive'])&&!empty($m['rollback_path'])),'migration must be idempotent or rollback-protected');}echo "Updater state-machine checks passed.\n"; diff --git a/update/release-spec.json b/update/release-spec.json index 856303c..da32cfe 100644 --- a/update/release-spec.json +++ b/update/release-spec.json @@ -1,13 +1,13 @@ { "protocol_version": 1, "application": "Licora", - "version": "5.6.1", + "version": "5.7.1", "channel": "stable", "minimum_updater": "5.3.0", "minimum_php": "8.0", "upgrade_from": [ - "5.5.1", - "5.6.0" + "5.6.1", + "5.7.0" ], "delete_files": [], "migrations": []