diff --git a/.github/workflows/f6-1-apply-cancel-ux.yml b/.github/workflows/f6-1-apply-cancel-ux.yml
new file mode 100644
index 0000000..7bdda4a
--- /dev/null
+++ b/.github/workflows/f6-1-apply-cancel-ux.yml
@@ -0,0 +1,113 @@
+name: F6.1 apply cancellation UX
+
+on:
+ push:
+ branches:
+ - 'hotfix/f6-1-operator-cancel-ux'
+ paths:
+ - '.github/workflows/f6-1-apply-cancel-ux.yml'
+
+permissions:
+ contents: write
+
+jobs:
+ apply:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: hotfix/f6-1-operator-cancel-ux
+ fetch-depth: 0
+
+ - name: Replace prompt cancellation with explicit audited form
+ run: |
+ python - <<'PY'
+ from pathlib import Path
+ import re
+
+ path = Path('web-v2/src/pages/InspectionVisitEditorF4Page.tsx')
+ text = path.read_text()
+
+ state_before = " const [success, setSuccess] = useState('');\n"
+ state_after = """ const [success, setSuccess] = useState('');
+ const [showCancelForm, setShowCancelForm] = useState(false);
+ const [cancelReason, setCancelReason] = useState('');
+"""
+ if text.count(state_before) != 1:
+ raise SystemExit('Could not locate success state exactly once')
+ text = text.replace(state_before, state_after)
+
+ lifecycle_pattern = re.compile(
+ r" const lifecycleAction = async \(action: 'PLAN' \| 'UNPLAN' \| 'CANCEL'\) => \{.*?\n \};\n\n if \(loading\)",
+ re.S,
+ )
+ lifecycle_replacement = """ const lifecycleAction = async (action: 'PLAN' | 'UNPLAN') => {
+ if (!id) return;
+ setBusy(true); setError(''); setSuccess('');
+ try {
+ let updated: InspectionVisit;
+ if (action === 'PLAN') {
+ updated = await planInspectionVisit(id);
+ setSuccess('Inspección planificada. Ya puede iniciarse desde la APK.');
+ } else {
+ updated = await unplanInspectionVisit(id);
+ setSuccess('La Inspección volvió a borrador para corregir la planificación.');
+ }
+ applyVisit(updated);
+ } catch (requestError) { setError(errorMessage(requestError)); }
+ finally { setBusy(false); }
+ };
+
+ const cancelCurrentVisit = async (event: FormEvent) => {
+ event.preventDefault();
+ if (!id) return;
+ const reason = cancelReason.trim();
+ if (reason.length < 10) {
+ setError('El motivo de cancelación debe tener al menos 10 caracteres.');
+ return;
+ }
+ if (reason.length > 500) {
+ setError('El motivo de cancelación no puede superar los 500 caracteres.');
+ return;
+ }
+ setBusy(true); setError(''); setSuccess('');
+ try {
+ const updated = await cancelInspectionVisit(id, reason);
+ applyVisit(updated);
+ setCancelReason('');
+ setShowCancelForm(false);
+ setSuccess('Inspección cancelada. El motivo quedó registrado y auditado.');
+ } catch (requestError) { setError(errorMessage(requestError)); }
+ finally { setBusy(false); }
+ };
+
+ if (loading)"""
+ text, count = lifecycle_pattern.subn(lifecycle_replacement, text, count=1)
+ if count != 1:
+ raise SystemExit(f'Lifecycle block replacements: {count}')
+
+ actions_pattern = re.compile(
+ r" \{visit && La oficina planifica. El inicio y cierre operativo se realizan desde la APK. Una Inspección no puede cerrarse hasta que todas sus Actas estén selladas.