Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -1329,3 +1329,16 @@ System User token before go-live` (warn). Best-effort, never throws, never
- GOTCHA: `npm run build` runs a prebuild env check (`scripts/check-env.ts`)
that fails without auth secrets; bypass locally with `SKIP_ENV_VALIDATION=true`
(as CI does) to exercise the real Next.js compile.

## 2026-06-12 (overnight orchestration) — git-proxy pushes + gate plumbing
- The remote-exec harness proxies git (127.0.0.1 local_proxy): a branch's
FIRST push succeeds; follow-up pushes can 403 / "remote end hung up"
while `git push` misreports "Everything up-to-date". Recurred tonight on
feature/43 and feature/41. Working recovery: push the commit via GitHub
MCP `push_files`, then `git fetch` + verify empty diff vs remote, then
`git reset --hard` to the remote tip. ALWAYS verify pushes via
ls-remote/API, never trust push output text.
- Two gate-runner bugs tonight were pipes swallowing exit codes
(`cmd | tail -1`, `prisma validate | grep`). Use pipefail or unpiped
gates. Local `npm run build` needs SKIP_ENV_VALIDATION=true; prisma
validate needs any DATABASE_URL set.
13 changes: 13 additions & 0 deletions __tests__/unit/services/reschedule.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,19 @@ describe('rescheduleService', () => {
expect(result.cancelled).toBe(true);
expect(result.gapFill).toBeUndefined();
});

it('rescheduleAppointment carries the gap-fill report through to its result (Bugbot #252)', async () => {
armTransaction('CONFIRMED', new Date(Date.now() + 5 * DAY_MS));
mockProposeGapFill.mockResolvedValue({ taskCreated: true, taskId: 'task-9', candidateCount: 3 });
// Post-cancel acknowledgement lookup — returning null skips the send
// path, which is exercised by its own tests above.
mockAppointmentFindUnique.mockResolvedValueOnce(null);

const result = await rescheduleService.rescheduleAppointment('appt-gf', 'new date soon');

expect(result.gapFill).toEqual({ taskCreated: true, taskId: 'task-9', candidateCount: 3 });
expect(result.note).toBe('new date soon');
});
});

describe('cancelAppointment actor attribution', () => {
Expand Down
24 changes: 17 additions & 7 deletions app/[locale]/appointments/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,10 @@ export default function AppointmentDetailPage({ params }: { params: Promise<{ id
setRequestReview(false);
};

const handleCancel = async () => {
if (!confirm(t('cancelConfirm'))) return;
const reason = prompt(t('cancelReason'));
const payload = await handleAction('cancel', { reason: reason ?? undefined });
// Phase 42 — the cancel response reports whether a gap-fill proposal
// task was raised; surface a small non-blocking pointer to it.
// Phase 42 — cancel AND reschedule free the slot via the same service
// path, so either response may report a raised gap-fill proposal task;
// surface the same non-blocking pointer for both.
const surfaceGapFillNotice = (payload: Awaited<ReturnType<typeof handleAction>>) => {
const gapFill = payload?.gapFill as
| { taskCreated: boolean; taskId: string | null; candidateCount: number }
| undefined;
Expand All @@ -193,6 +191,18 @@ export default function AppointmentDetailPage({ params }: { params: Promise<{ id
}
};

const handleCancel = async () => {
if (!confirm(t('cancelConfirm'))) return;
const reason = prompt(t('cancelReason'));
const payload = await handleAction('cancel', { reason: reason ?? undefined });
surfaceGapFillNotice(payload);
};

const handleReschedule = async () => {
const payload = await handleAction('reschedule', {});
surfaceGapFillNotice(payload);
};

const handleNoShow = async () => {
if (!confirm(t('noShowConfirm'))) return;
await handleAction('no-show');
Expand Down Expand Up @@ -466,7 +476,7 @@ export default function AppointmentDetailPage({ params }: { params: Promise<{ id
</Button>
<Button
variant="secondary"
onClick={() => handleAction('reschedule', {})}
onClick={handleReschedule}
disabled={actionLoading}
>
{t('reschedule')}
Expand Down
3 changes: 3 additions & 0 deletions lib/services/reschedule.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ interface RescheduleResult {
cancelled: boolean;
returnedToPool: boolean;
note: string;
/** Phase 42 — rescheduling frees the old slot via the cancel path, so it
* can raise a gap-fill proposal exactly like a plain cancellation. */
gapFill?: GapFillProposalResult;
}

interface ParseResult {
Expand Down
Loading