{"v":[{"id":1459,"html_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165#issuecomment-1459","pull_request_url":"","issue_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165","user":{"id":3,"login":"connect-bot","login_name":"","source_id":0,"full_name":"","email":"connect-bot@noreply.www-s01.apinet.com","avatar_url":"https:\/\/www-s01.apinet.com\/codeforge\/avatars\/68a2723f7ad70dee2e1415be7488d84b","html_url":"https:\/\/www-s01.apinet.com\/codeforge\/connect-bot","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2026-10-01T08:49:35-05:00","restricted":false,"active":false,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"connect-bot"},"original_author":"","original_author_id":0,"body":"**Charles W.** _(via Connect)_\n<!-- connect:user=1 -->\n\nThere may be a modal pop-up if a change is made just to confirm.","assets":[],"created_at":"2026-10-07T16:23:23-05:00","updated_at":"2026-10-07T16:23:23-05:00"},{"id":1482,"html_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165#issuecomment-1482","pull_request_url":"","issue_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165","user":{"id":1,"login":"charles","login_name":"","source_id":0,"full_name":"Charles Willgren","email":"charles@noreply.www-s01.apinet.com","avatar_url":"https:\/\/www-s01.apinet.com\/codeforge\/avatars\/d3c894f7297c5a18eb3f6ac837c335b11a00aa9cc0961323771632b1e2d61bb2","html_url":"https:\/\/www-s01.apinet.com\/codeforge\/charles","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2025-12-08T10:03:41-06:00","restricted":false,"active":false,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"charles"},"original_author":"","original_author_id":0,"body":"## Where this stands today\n\nMost of the safety rule is already in place on the server. It runs in the same transaction as the save:\n\n* **A changed answer withdraws the generated forms and every signature on them.** In `form_v2_visit_save()` (`code\/form_v2_visit.inc.php`), any accepted change calls `form_v2_visit_supersede_candidates()`. The candidate becomes `superseded`, and every tap on it stops counting. The taps are kept as the record. An unchanged answer is not counted as a change, so re-saving the same value withdraws nothing.\n* **Nothing can be signed against old copies.** Every sign, adopt and finish request checks `form_v2_candidate_is_current()` under the visit lock, and a superseded printing is refused. This is pinned in `scripts\/test-form-v2-sign-state.php` (\"a change withdraws\").\n* **The forms are always generated again from the current answers.** Signing stamps only the kept review copy, which is proved against its hash. There is no way to sign a copy that does not match the answers.\n* **A signed visit takes no more answers** (`closed` \/ `signed` refusal).\n\nWhat is missing is all on the person's side:\n\n1. **No warning before the change.** The page says *afterwards*: \"An answer changed after the forms were generated, so those copies, and the signatures on them, were withdrawn.\" By then the signatures are already gone.\n2. **Later sections are not flagged** when the change reveals new questions in them. That is #166, and I'd build it there (see that issue) rather than twice.\n3. **A signed visit cannot be corrected at all.** That needs amendments (FormsV2Plan \u00a78.5), which are not built.\n\n## Proposed plan\n\n**A. Lock the answers once forms are generated, and confirm before unlocking** (the modal from the comment above).\nWhile a current printing exists, the questions show read-only, with a **Change an answer** button. That button opens a modal:\n\n> The forms have been generated *(and N lines have been signed)*. Changing an answer withdraws these copies and their signatures. You will generate the forms again, and everyone signs again.\n> **[Change answers]  [Keep the forms]**\n\nI'd use a lock rather than catching the first keystroke. The answers autosave as they are typed, so a modal fired by an `input` event would interrupt typing and is easy to dismiss by accident. Unlocking withdraws nothing by itself. Only the first actual saved change does, so a person who unlocks and then changes nothing keeps the forms.\n\n**B. Enforce the confirmation on the server, not only in the page.**\nWhile a current candidate exists, a save must carry `withdraw_candidate = <pk>` naming it. Otherwise it is refused with code `printed` (and the page shows the modal). This stops a stale tab, a retried request or a future client from silently wiping 36 signatures. The lease already allows only one writing window, so this is a second guard, not the only one.\n\n**C. After the change, say what to do next.** Keep the existing withdrawal notice. Add, once #166 exists, \"New questions appeared in: *Emergencies*, *Coordination of Care*\" with links. The Generate button stays available, but see question 3.\n\n**D. Tests:** a save without the acknowledgement is refused while a candidate is current and accepted when there is none. A save with an acknowledgement naming an old candidate is refused as stale.\n\n## Questions\n\n1. **Does any change withdraw everything?** Today it does: one typo in a notes field means a new printing and every line signed again. The plan (\u00a76) chose that for the first release. The alternative is to keep the taps on forms whose generated copy is **byte-identical** to the one that was signed, so only the forms the change actually reached need signing again. That is safe because a tap is bound to the copy's hash, but the candidate digest currently binds the whole set, so it is a real change. **My recommendation: keep \"everything again\" for the pilot, and revisit if staff complain.**\n2. **Is correcting a *signed* visit in scope here, or a separate issue?** That is amendments (\u00a78.5): a new revision, the old signed copies kept, new copies signed. I'd make it its own issue, after submission.\n3. **Should Generate forms be blocked while a section has new questions nobody has opened** (#166)? Or should it warn and allow? I lean towards **block**, because the answers are printed exactly as they stand.\n\n","assets":[],"created_at":"2026-10-07T23:17:03-05:00","updated_at":"2026-10-07T23:17:03-05:00"},{"id":1521,"html_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165#issuecomment-1521","pull_request_url":"","issue_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165","user":{"id":1,"login":"charles","login_name":"","source_id":0,"full_name":"Charles Willgren","email":"charles@noreply.www-s01.apinet.com","avatar_url":"https:\/\/www-s01.apinet.com\/codeforge\/avatars\/d3c894f7297c5a18eb3f6ac837c335b11a00aa9cc0961323771632b1e2d61bb2","html_url":"https:\/\/www-s01.apinet.com\/codeforge\/charles","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2025-12-08T10:03:41-06:00","restricted":false,"active":false,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"charles"},"original_author":"","original_author_id":0,"body":"## Update: #166 is built (fcc5d40)\n\nThat covers the \"revisit these sections\" half of this issue:\n* When a changed answer brings up questions in a later section, that section shows a dot and \"N new\" in the sidebar. A notice also names the section, with a link.\n* A question that was looked at, hidden by a change, then shown again counts as new again, even with its old answer still in it.\n* **Question 3 is settled: block.** Generate forms is refused (code `unseen`) while any section has questions nobody has looked at. The Check step lists those sections.\n\n**Still open here:** plan items A\u2013C (lock the answers once forms are generated, with the confirm modal, and enforce it on the server with `withdraw_candidate`), and questions 1 and 2 (whether every change withdraws all signatures, and amendments to a signed visit). C's \"New questions appeared in\u2026\" wording can now use the notice #166 added.","assets":[],"created_at":"2026-10-08T10:13:36-05:00","updated_at":"2026-10-08T10:13:36-05:00"},{"id":1523,"html_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165#issuecomment-1523","pull_request_url":"","issue_url":"https:\/\/www-s01.apinet.com\/codeforge\/apiintl\/hhmanager\/issues\/165","user":{"id":1,"login":"charles","login_name":"","source_id":0,"full_name":"Charles Willgren","email":"charles@noreply.www-s01.apinet.com","avatar_url":"https:\/\/www-s01.apinet.com\/codeforge\/avatars\/d3c894f7297c5a18eb3f6ac837c335b11a00aa9cc0961323771632b1e2d61bb2","html_url":"https:\/\/www-s01.apinet.com\/codeforge\/charles","language":"","is_admin":false,"last_login":"0001-01-01T00:00:00Z","created":"2025-12-08T10:03:41-06:00","restricted":false,"active":false,"prohibit_login":false,"location":"","pronouns":"","website":"","description":"","visibility":"public","followers_count":0,"following_count":0,"starred_repos_count":0,"username":"charles"},"original_author":"","original_author_id":0,"body":"## Built (cce1ffb)\n\n**Question 1 decided: only the forms a change reaches are generated and signed again.** Signatures on forms the change doesn't touch stay good.\n\n**How a correction works**\n* **The answers lock once forms exist.** After the forms are generated, the questions are read-only behind **Change an answer**. That opens a confirmation: \"Changing an answer withdraws the forms it changes, and any signatures on them\u2026 Forms the change does not reach keep their signatures.\"\n* **There is a way back from signing.** The signing screen has \"An answer is wrong? Change it\", for when the client or employee finds an error and won't sign.\n* **The server enforces the confirmation.** While a printing is current, `form_v2_visit_save()` refuses a changed answer with code `printed`, unless the save carries `withdraw_candidate` naming that printing. Moving between sections needs nothing. A stale tab or a retried request gets the same confirmation instead of silently withdrawing signatures.\n* **Generating again keeps unchanged forms.** Each form now carries a fingerprint, `form_v2_candidate_form_digest()`: its pinned version and blank form, plus its own answers. On the next generate, a form whose fingerprint matches the last printing (`form_v2_candidate_carry()`) is not printed again. Its review copy is reused, and its signed and \"reviewed\" taps are copied onto the new printing. If every form matches, the new printing is ready at once. The forms list marks kept forms \"unchanged, signatures kept\".\n* **Adopted signatures carry too** (`form_v2_sign_carry_signers()`), so nobody draws theirs again. The exceptions:\n  * a signer the correction renamed, such as an attendant whose name changed;\n  * a client whose adoption now contradicts how the interview says the client signs.\n* **A decline does not carry.** A refusal is what the correction answers, so the line is asked again.\n* **The audit trail is kept.** Every copied tap and adoption has `fk_carried_from` pointing at the original row (`migrations\/20261008-02_carried-signatures.sql`), with the original time and the hash and digest it cited. A copy of a copy still points at the original.\n\n**Tests:** new cases in `test-form-v2-visit-rules`, `test-form-v2-sign-rules`, `test-form-v2-sign-state` (98 pass) and `test-form-v2-candidate-state`. The sign-state test covers: a signed form kept while the changed one is signed again, the decline not carried, nobody adopting again, and a refused change followed by a confirmed one. The lock and the confirmation were also checked in the browser. Keeping a signed form could not be checked in the browser locally, because the dev document store isn't configured.\n\n**To deploy:** `php scripts\/migrate.php --apply`.\n\n**Question 2 (correcting a visit that is already *signed*) is #119.** That is the second stage of correcting a mistake: the office finds it while reviewing the forms after the visit. This issue covered the first stage, the supervisor on site while signatures are being collected.","assets":[],"created_at":"2026-10-08T10:52:43-05:00","updated_at":"2026-10-08T11:00:48-05:00"}]}