From 9971a6363526ed863378a67bf6f0b3d7e4bb46ff Mon Sep 17 00:00:00 2001 From: mathew Date: Wed, 30 Sep 2026 21:06:15 +0100 Subject: [PATCH] Prepare clean 0.2.1 Gate B candidate --- Directory.Build.props | 2 +- MILESTONES.md | 10 +++++----- README.md | 2 +- RELEASE_NOTES.md | 10 +++++++--- deploy/backup-restore-acceptance.example.json | 2 +- deploy/backup_restore_acceptance.py | 2 +- deploy/debian-host-acceptance.example.json | 2 +- deploy/debian_acceptance.py | 2 +- deploy/persistence-acceptance.example.json | 2 +- deploy/pilot_approval.py | 2 +- docs/gate-b-prerequisites.md | 2 +- docs/operations.md | 10 +++++----- docs/pilot-release.md | 2 +- tests/test_backup_restore_acceptance.py | 2 +- tests/test_debian_acceptance.py | 2 +- tests/test_release_record.py | 4 ++-- tests/test_verify_release.py | 14 +++++++------- web/package-lock.json | 4 ++-- web/package.json | 2 +- 19 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4ad83ea..8ccdb6b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ net10.0 - 0.2.0 + 0.2.1 enable enable true diff --git a/MILESTONES.md b/MILESTONES.md index 02a4e3e..69435b6 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -1,6 +1,6 @@ # GuestOps Milestone Report -Version: **0.2.0 release candidate** +Version: **0.2.1 release candidate** Last updated: **30 September 2026** This is the working delivery tracker for GuestOps Web. Update a milestone when its state changes and link the pull request, release artifact, test run, or acceptance record that proves the change. @@ -29,7 +29,7 @@ This summary explains what each milestone delivers and where it currently stands | 16 | Identity, preferences, and privacy | Account/session controls, hotel preferences, privacy inventory, retention decisions, and audit review. | **Implemented; acceptance required.** Legal and operational decisions, identity checks, and independent review remain outstanding. | | 17 | Inbox usability and desktop parity | Stable pagination, protected unsaved drafts, hotel-timezone display, and desktop workflow parity. | **Implemented; acceptance required.** Automated checks pass; the supervised desktop exercise and independent approval remain outstanding. | | 18 | Pilot, capacity, and release approval | Capacity proof, incident exercise, five-business-day hotel pilot, findings closure, and Gate B approval. | **In progress.** Validators and targets exist; Gate A/B prerequisites, capacity evidence, incident rehearsal, pilot, and named approvals remain open. | -| 19 | Account security and self-service | TOTP MFA, recovery codes, transactional email, granular roles, preferences, and security notifications. | **Implemented on the development branch; acceptance required.** Keep it separate until `0.2.0` is approved and tagged, then review, merge, and version it as `0.3.0`. | +| 19 | Account security and self-service | TOTP MFA, recovery codes, transactional email, granular roles, preferences, and security notifications. | **Implemented on the development branch; acceptance required.** Keep it separate until `0.2.1` is approved and tagged, then review, merge, and version it as `0.3.0`. | ## Status key @@ -68,7 +68,7 @@ This summary explains what each milestone delivers and where it currently stands | 15 | Knowledge, AI, and FAQ activation | B | Implemented / acceptance required | Owners can run a bounded no-send batch evaluation, and a release-bound acceptance record enforces positive/negative coverage, zero FAQ errors, separate AI review, staff training, stop-control evidence and named monitoring/rollback owners. Complete the supervised evaluation and retain independent approval. | | 16 | Identity, preferences, and privacy | B/C | Implemented / acceptance required | Login throttling trusts the client address only after one-hop processing by the configured proxy. A release-bound review now covers owner-controlled preferences, account/session controls, data inventory, retention/deletion/legal-hold ownership, provider decisions, audit evidence and known identity limitations. Complete the legal/operational decisions and independently approve the record. | | 17 | Inbox usability and desktop parity | B | Implemented / acceptance required | The inbox uses tenant-scoped stable cursor pagination in pages of 50 and protects unsaved drafts during route/history navigation, reload, conversation selection, filtering and search. Operational timestamps use the saved hotel timezone, and a release-bound desktop-parity acceptance record is implemented. The implementation and preview HTTP suite pass; run the supervised exercise against the approved release and retain independent approval. | -| 18 | Pilot, capacity, and release approval | B/C | In progress | The `0.2.0` Gate B candidate has bounded capacity, five-business-day pilot, incident and final-decision record validators with agreed targets. Retain the exact source-package and Ansible-install evidence, complete Gate A and Gate B prerequisites, run the probe and supervised exercises, resolve or contain findings, and retain separate hotel-owner and technical approval. Gate C remains dependent on milestones 13 and 14. | +| 18 | Pilot, capacity, and release approval | B/C | In progress | The `0.2.1` Gate B candidate has bounded capacity, five-business-day pilot, incident and final-decision record validators with agreed targets. Retain the exact source-package and Ansible-install evidence, complete Gate A and Gate B prerequisites, run the probe and supervised exercises, resolve or contain findings, and retain separate hotel-owner and technical approval. Gate C remains dependent on milestones 13 and 14. | ## Delivery sequence @@ -81,9 +81,9 @@ Milestones 13 (Guestline/Rezlynx) and 14 (payments) can progress as parallel pro ## Next actions - [ ] Merge the Action removal and release-process update to the intended default branch; that resulting commit becomes the new package candidate. -- [ ] Confirm the intended release commit/version because remote `main` and the published `0.2.0` tag currently identify different histories; do not move or reuse the published tag. +- [x] Preserve the published `0.2.0` tag unchanged and use the clean `0.2.1` candidate line from `main`, excluding Milestone 19 application code. - [ ] Create and checksum the approved source archive with `deploy/package_source.py`, add/select it in the GuestOps Ansible playbook, and retain the package and installation evidence. -- [ ] Create and archive the immutable `0.2.0` approval tag only after Gate B approval. +- [ ] Create and archive the immutable `0.2.1` approval tag only after Gate B approval. - [ ] Deploy to the target Debian environment with persistent MongoDB and data-protection keys. - [ ] Run and record backup, restore, restart, monitoring, and rollback exercises. - [ ] Complete real Google mailbox acceptance without using production guest data. diff --git a/README.md b/README.md index 70507ec..c04c7fd 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Linux-hosted hotel email workspace, developed separately from the Windows GuestOps application. **This migration now includes AI draft generation, staff-approved Gmail sending, reviewed OHIP reservation updates, NMI hosted invoices, controlled FAQ auto-replies and team onboarding. It is not yet a production-complete replacement.** -Current release-candidate version: **0.2.0** +Current release-candidate version: **0.2.1** Project progress is tracked in the [milestone report](MILESTONES.md). User-visible changes and release limitations are recorded in the [release notes](RELEASE_NOTES.md). diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b87ba0c..9d6e5cf 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,8 +1,8 @@ # GuestOps Release Notes -## 0.2.0 — Gate B release candidate +## 0.2.1 — Gate B release candidate -This candidate freezes the implemented Gate B scope for controlled acceptance. It is not yet approved for live hotel operations and does not become a release until the exact commit is pushed, a checksummed source package and operational evidence are retained, the supervised pilot is approved and the immutable `0.2.0` tag is created. +This candidate freezes the implemented Gate B scope for controlled acceptance. It is not yet approved for live hotel operations and does not become a release until the exact commit is pushed, a checksummed source package and operational evidence are retained, the supervised pilot is approved and the immutable `0.2.1` tag is created. ### Promoted scope @@ -26,6 +26,10 @@ These capabilities still require their separately documented provider, host and See [MILESTONES.md](MILESTONES.md) for the gate assessment, delivery sequence, and remaining work. +## 0.2.0 — Superseded candidate identifier + +The published `0.2.0` tag is retained unchanged for auditability but is not the approved deployment candidate. It identifies a development-line commit that is not on the clean Gate B release branch. Do not package or deploy it; `0.2.1` supersedes it. + ## 0.1.0 — 29 September 2026 The `0.1.0` tag identifies the initial GuestOps Web foundation. It is not approved for live hotel operations. @@ -41,4 +45,4 @@ The `0.1.0` tag identifies the initial GuestOps Web foundation. It is not approv ### Versioning -The foundation remains tagged `0.1.0`. The .NET projects and frontend package now share candidate version `0.2.0`; create that immutable tag only after the exact commit, checksummed artifacts and Gate B acceptance evidence have been approved. +The foundation remains tagged `0.1.0`. The .NET projects and frontend package now share candidate version `0.2.1`; create that immutable tag only after the exact commit, checksummed artifacts and Gate B acceptance evidence have been approved. diff --git a/deploy/backup-restore-acceptance.example.json b/deploy/backup-restore-acceptance.example.json index ff32ff1..5a51045 100644 --- a/deploy/backup-restore-acceptance.example.json +++ b/deploy/backup-restore-acceptance.example.json @@ -3,7 +3,7 @@ "system": "guestops-backup-recovery", "evidenceId": "backup-restore", "dataClassification": "synthetic-only", - "releaseVersion": "0.2.0", + "releaseVersion": "0.2.1", "releaseCommit": "0000000000000000000000000000000000000000", "releaseRecordSha256": "0000000000000000000000000000000000000000000000000000000000000000", "archiveSha256": "0000000000000000000000000000000000000000000000000000000000000000", diff --git a/deploy/backup_restore_acceptance.py b/deploy/backup_restore_acceptance.py index 2f76bc3..ea74e55 100644 --- a/deploy/backup_restore_acceptance.py +++ b/deploy/backup_restore_acceptance.py @@ -11,7 +11,7 @@ import re from urllib.parse import urlparse -VERSION = "0.2.0" +VERSION = "0.2.1" SCENARIOS = { "encrypted-manual-backup", "scheduled-backup", diff --git a/deploy/debian-host-acceptance.example.json b/deploy/debian-host-acceptance.example.json index 544b96b..48e226f 100644 --- a/deploy/debian-host-acceptance.example.json +++ b/deploy/debian-host-acceptance.example.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "system": "guestops-debian-host", "evidenceId": "debian-host", - "releaseVersion": "0.2.0", + "releaseVersion": "0.2.1", "releaseCommit": "0000000000000000000000000000000000000000", "releaseRecordSha256": "0000000000000000000000000000000000000000000000000000000000000000", "archiveSha256": "0000000000000000000000000000000000000000000000000000000000000000", diff --git a/deploy/debian_acceptance.py b/deploy/debian_acceptance.py index d0ca02e..250d8fa 100644 --- a/deploy/debian_acceptance.py +++ b/deploy/debian_acceptance.py @@ -11,7 +11,7 @@ import re from urllib.parse import urlparse -VERSION = "0.2.0" +VERSION = "0.2.1" SYSTEMS = { "guestops-debian-host": { "evidenceId": "debian-host", diff --git a/deploy/persistence-acceptance.example.json b/deploy/persistence-acceptance.example.json index bc34f89..8dc4ff7 100644 --- a/deploy/persistence-acceptance.example.json +++ b/deploy/persistence-acceptance.example.json @@ -2,7 +2,7 @@ "schemaVersion": 1, "system": "guestops-persistence", "evidenceId": "persistence", - "releaseVersion": "0.2.0", + "releaseVersion": "0.2.1", "releaseCommit": "0000000000000000000000000000000000000000", "releaseRecordSha256": "0000000000000000000000000000000000000000000000000000000000000000", "archiveSha256": "0000000000000000000000000000000000000000000000000000000000000000", diff --git a/deploy/pilot_approval.py b/deploy/pilot_approval.py index 047bdc8..f5681aa 100644 --- a/deploy/pilot_approval.py +++ b/deploy/pilot_approval.py @@ -34,7 +34,7 @@ def timestamp(value: object, field: str) -> dt.datetime: def validate(record: object) -> None: require(isinstance(record, dict), "Approval record must be a JSON object.") - require(record.get("schemaVersion") == 2, "Unsupported approval schema; Gate B 0.2.0 requires schemaVersion 2.") + require(record.get("schemaVersion") == 2, "Unsupported approval schema; Gate B 0.2.1 requires schemaVersion 2.") gate = record.get("targetGate") require(gate in ("B", "C"), "targetGate must be B or C.") require(record.get("decision") == "approved", "Only an explicit approved decision passes validation.") diff --git a/docs/gate-b-prerequisites.md b/docs/gate-b-prerequisites.md index fe4a394..cfe6182 100644 --- a/docs/gate-b-prerequisites.md +++ b/docs/gate-b-prerequisites.md @@ -1,6 +1,6 @@ # Gate B automation, identity and privacy acceptance -Run these reviews against the exact `0.2.0` candidate on the accepted HTTPS sandbox. Keep guest data, staff addresses, provider agreements, screenshots and raw reports in the restricted evidence store. Repository records contain opaque references only. +Run these reviews against the exact `0.2.1` candidate on the accepted HTTPS sandbox. Keep guest data, staff addresses, provider agreements, screenshots and raw reports in the restricted evidence store. Repository records contain opaque references only. ## Knowledge, AI and FAQ automation diff --git a/docs/operations.md b/docs/operations.md index 54516c6..f753440 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -18,7 +18,7 @@ Before deployment, verify the archive against its source record: ```sh python3 - <<'PY' import hashlib, json, pathlib -r = json.load(open('GuestOps-0.2.0-COMMIT.source.json', encoding='utf-8')) +r = json.load(open('GuestOps-0.2.1-COMMIT.source.json', encoding='utf-8')) p = pathlib.Path(r['artifact']['name']) assert hashlib.sha256(p.read_bytes()).hexdigest() == r['artifact']['sha256'] print(r['commit'], r['version'], r['artifact']) @@ -29,17 +29,17 @@ After Ansible has built the commit-tagged images, the repository verifier perfor ```sh python3 deploy/verify_release.py \ - --archive GuestOps-0.2.0.tar.gz \ + --archive GuestOps-0.2.1.tar.gz \ --record release-record.json \ --commit FULL_40_CHARACTER_SHA \ - --version 0.2.0 \ + --version 0.2.1 \ --output release-verification.json python3 deploy/verify_release.py \ - --archive GuestOps-0.2.0.tar.gz \ + --archive GuestOps-0.2.1.tar.gz \ --record release-record.json \ --commit FULL_40_CHARACTER_SHA \ - --version 0.2.0 \ + --version 0.2.1 \ --verify-loaded-images \ --output loaded-image-verification.json ``` diff --git a/docs/pilot-release.md b/docs/pilot-release.md index 9de0939..ce67efb 100644 --- a/docs/pilot-release.md +++ b/docs/pilot-release.md @@ -22,7 +22,7 @@ python3 deploy/capacity_probe.py \ unset CAPACITY_EMAIL CAPACITY_PASSWORD ``` -For the `0.2.0` Gate B candidate, the approved targets are concurrency 10, p95 latency at or below 500 ms, error rate at or below 1%, and at least 25% CPU and memory headroom on the documented four-core, 7.6 GiB host. The generated result reports HTTP observations, not a pass/fail claim. Repeat after a warm-up, investigate every error, and retain independently captured host metrics with the report. Hash both retained files for the approval record. Do not point the probe at a live hotel or increase its built-in bounds to simulate a denial of service. +For the `0.2.1` Gate B candidate, the approved targets are concurrency 10, p95 latency at or below 500 ms, error rate at or below 1%, and at least 25% CPU and memory headroom on the documented four-core, 7.6 GiB host. The generated result reports HTTP observations, not a pass/fail claim. Repeat after a warm-up, investigate every error, and retain independently captured host metrics with the report. Hash both retained files for the approval record. Do not point the probe at a live hotel or increase its built-in bounds to simulate a denial of service. ## Five-business-day supervised pilot diff --git a/tests/test_backup_restore_acceptance.py b/tests/test_backup_restore_acceptance.py index a3a33f8..0b1be9d 100644 --- a/tests/test_backup_restore_acceptance.py +++ b/tests/test_backup_restore_acceptance.py @@ -19,7 +19,7 @@ def valid_record(): "system": "guestops-backup-recovery", "evidenceId": "backup-restore", "dataClassification": "synthetic-only", - "releaseVersion": "0.2.0", + "releaseVersion": "0.2.1", "releaseCommit": COMMIT, "releaseRecordSha256": RELEASE_SHA, "archiveSha256": "c" * 64, diff --git a/tests/test_debian_acceptance.py b/tests/test_debian_acceptance.py index 742538d..f59e842 100644 --- a/tests/test_debian_acceptance.py +++ b/tests/test_debian_acceptance.py @@ -17,7 +17,7 @@ def common(system): "schemaVersion": 1, "system": system, "evidenceId": acceptance.SYSTEMS[system]["evidenceId"], - "releaseVersion": "0.2.0", + "releaseVersion": "0.2.1", "releaseCommit": COMMIT, "releaseRecordSha256": RELEASE_SHA, "archiveSha256": "c" * 64, diff --git a/tests/test_release_record.py b/tests/test_release_record.py index 072eb30..378d7b7 100644 --- a/tests/test_release_record.py +++ b/tests/test_release_record.py @@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[1] class ReleaseRecordTests(unittest.TestCase): def test_writes_versions_checksum_and_immutable_image_ids(self): with tempfile.TemporaryDirectory() as directory: - artifact = Path(directory) / "GuestOps-0.2.0.tar.gz" + artifact = Path(directory) / "GuestOps-0.2.1.tar.gz" output = Path(directory) / "release-record.json" artifact.write_bytes(b"reviewed image archive") @@ -40,7 +40,7 @@ class ReleaseRecordTests(unittest.TestCase): ) record = json.loads(output.read_text(encoding="utf-8")) - self.assertEqual(record["version"], "0.2.0") + self.assertEqual(record["version"], "0.2.1") self.assertEqual(record["commit"], "a" * 40) self.assertEqual(record["images"]["api"]["id"], "sha256:api") self.assertEqual( diff --git a/tests/test_verify_release.py b/tests/test_verify_release.py index a0c6a13..4b286e9 100644 --- a/tests/test_verify_release.py +++ b/tests/test_verify_release.py @@ -21,12 +21,12 @@ WORKER_ID = "sha256:" + "c" * 64 class VerifyReleaseTests(unittest.TestCase): def fixture(self, directory: str): root = Path(directory) - archive = root / "GuestOps-0.2.0.tar.gz" + archive = root / "GuestOps-0.2.1.tar.gz" record = root / "release-record.json" archive.write_bytes(b"reviewed image archive") release = { "schemaVersion": 1, - "version": "0.2.0", + "version": "0.2.1", "commit": COMMIT, "artifact": { "name": archive.name, @@ -44,7 +44,7 @@ class VerifyReleaseTests(unittest.TestCase): def test_verifies_candidate_archive_record_and_record_checksum(self): with tempfile.TemporaryDirectory() as directory: archive, record, _ = self.fixture(directory) - result = verify_release.validate(archive, record, COMMIT, "0.2.0") + result = verify_release.validate(archive, record, COMMIT, "0.2.1") self.assertTrue(result["verified"]) self.assertEqual(result["archive"]["sha256"], hashlib.sha256(archive.read_bytes()).hexdigest()) self.assertEqual(result["releaseRecord"]["sha256"], hashlib.sha256(record.read_bytes()).hexdigest()) @@ -55,7 +55,7 @@ class VerifyReleaseTests(unittest.TestCase): archive, record, _ = self.fixture(directory) archive.write_bytes(b"changed") with self.assertRaisesRegex(ValueError, "size does not match"): - verify_release.validate(archive, record, COMMIT, "0.2.0") + verify_release.validate(archive, record, COMMIT, "0.2.1") def test_rejects_wrong_commit_version_reference_and_mutable_id(self): cases = [ @@ -70,17 +70,17 @@ class VerifyReleaseTests(unittest.TestCase): mutate(release) record.write_text(json.dumps(release), encoding="utf-8") with self.assertRaisesRegex(ValueError, message): - verify_release.validate(archive, record, COMMIT, "0.2.0") + verify_release.validate(archive, record, COMMIT, "0.2.1") def test_loaded_image_ids_must_match(self): with tempfile.TemporaryDirectory() as directory: archive, record, _ = self.fixture(directory) with patch.object(verify_release, "loaded_image_id", side_effect=[API_ID, WORKER_ID]): - result = verify_release.validate(archive, record, COMMIT, "0.2.0", True) + result = verify_release.validate(archive, record, COMMIT, "0.2.1", True) self.assertTrue(result["loadedImageIdsVerified"]) with patch.object(verify_release, "loaded_image_id", return_value="sha256:" + "d" * 64): with self.assertRaisesRegex(ValueError, "Loaded api image ID"): - verify_release.validate(archive, record, COMMIT, "0.2.0", True) + verify_release.validate(archive, record, COMMIT, "0.2.1", True) if __name__ == "__main__": diff --git a/web/package-lock.json b/web/package-lock.json index 8ec7573..31deacc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "guestops-web", - "version": "0.2.0", + "version": "0.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "guestops-web", - "version": "0.2.0", + "version": "0.2.1", "dependencies": { "lucide-react": "^0.577.0", "react": "19.2.8", diff --git a/web/package.json b/web/package.json index 9b7063b..ccea170 100644 --- a/web/package.json +++ b/web/package.json @@ -1 +1 @@ -{"name":"guestops-web","private":true,"version":"0.2.0","type":"module","scripts":{"dev":"vite --host 127.0.0.1","build":"tsc -b && vite build","check":"tsc -b"},"dependencies":{"react":"19.2.8","react-dom":"19.2.8","lucide-react":"^0.577.0"},"devDependencies":{"@types/react":"^19.2.0","@types/react-dom":"^19.2.0","@vitejs/plugin-react":"6.1.1","typescript":"~5.9.3","vite":"8.2.2"}} +{"name":"guestops-web","private":true,"version":"0.2.1","type":"module","scripts":{"dev":"vite --host 127.0.0.1","build":"tsc -b && vite build","check":"tsc -b"},"dependencies":{"react":"19.2.8","react-dom":"19.2.8","lucide-react":"^0.577.0"},"devDependencies":{"@types/react":"^19.2.0","@types/react-dom":"^19.2.0","@vitejs/plugin-react":"6.1.1","typescript":"~5.9.3","vite":"8.2.2"}}