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
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ COPY package.json bun.lock ./
COPY web/package.json ./web/
RUN bun install --frozen-lockfile
COPY web ./web
# The web app shares the answer-formatting parser with the server.
COPY src/core/format.ts ./src/core/format.ts
RUN bun run web:build

# Stage 2: the runtime image — server source plus the built web bundle.
Expand Down
56 changes: 28 additions & 28 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions docs/guide/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ Today's run needs `standups.view`; the run list and insights need `reports.view`
| `GET /standups/:id/runs/:date` | One run with full submissions and answers |
| `GET /standups/:id/insights?weeks=8` | Weekly participation, mood and blocker series |

Requests to edit answers after the deadline are decided by anyone holding `standups.configure` on the standup:

| Method | Path | Notes |
| --- | --- | --- |
| `GET` | `/edit-requests` | Pending requests the caller can decide, newest first: `standupId`, `standupName`, `date`, `userName`, `displayName`, `reason`, `askedAt`, `answeredAt` and `until` (midnight in the standup's timezone, as `23:59 Europe/London`) |
| `POST` | `/edit-requests/:id/approve` · `/edit-requests/:id/decline` | `{ note? }` on decline goes to the person; `404` for an unknown request, `403` without `standups.configure` on its standup, `409 refused` once decided or lapsed, or for the caller's own request |

Moods are withheld per person when the standup keeps them anonymous; only the team average is returned. A run includes `moodComments` only for callers holding `moods.comments` on the standup: each with `person`, `mood` and `comment`, or with `person: null`, sorted by mood then text, when moods are anonymous.

### Blockers
Expand Down
16 changes: 13 additions & 3 deletions docs/guide/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,18 @@ Anyone can DM the bot directly:
naming what's missing and your answers kept; nothing posts until all are
filled. (Google Chat can't grey out Submit for apps yet.) Answer the blockers
question with "none" when there are none. The skip reason works the same way.
- **Formatting:** `*bold*`, `_italic_`, `~strike~`, `__underline__`,
`` `code` `` and `- ` lists show as formatting on the posted card, in
blocker cards and in the web app. Exports, webhooks, the API and MCP keep
the text exactly as typed. Plain-text messages such as nudges and the
digest show Chat's own syntax, without underline.
- **Pre-fill:** the "yesterday" answer is pre-filled from your previous
"today" answer.
- **Late submissions** after the deadline still post, flagged *late*; the
wrap-up isn't recalculated and late entries can no longer be edited.
- **Late submissions** after the deadline still post, flagged *late*, until
midnight in the standup's timezone; the wrap-up isn't recalculated.
- **Editing after the deadline** needs a manager's approval: **Ask to edit** on
the prompt card sends the standup's managers a card. Once approved, answers
can change until midnight, and the posted card names who approved it.
- **Untagged blockers** auto-resolve when the same person submits a
blocker-free standup on a later day; tagged ones need an explicit
`blocker <id> resolve`. With `escalate @user` configured, the contact gets
Expand All @@ -145,7 +153,9 @@ Anyone can DM the bot directly:

The DM card that asks for today's standup keeps up with you: once you
submit it shows when (with **Edit answers** until the deadline), and once
you skip it shows your reason with **↩️ Undo skip**. Reminders and nudges
you skip it shows your reason with **↩️ Undo skip**. At the deadline it offers
**Ask to edit**, then shows whether your request is waiting, approved (with
**Edit answers** until midnight), declined or lapsed. Reminders and nudges
update the same way.

On a standup whose roster follows a Google Group, `add` keeps working: people
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Zapier/n8n, or your own service:

| Event | When | Body |
| --- | --- | --- |
| `submission` | Every submission and edit | `standup`, `date`, `user`, `answers`, `mood`, `late`, `edited`. Mood comments are never sent |
| `submission` | Every submission and edit | `standup`, `date`, `user`, `answers`, `mood`, `late`, `edited`, `editApprovedBy` (who approved an edit after the deadline, otherwise `null`). Mood comments are never sent |
| `wrap_up` | The run closes | `standup`, `date`, `summary` (the posted wrap-up's numbers, plus `teamMood` on anonymous standups) |
| `schedule_change` | Someone's week or days off change | person, summary, who, channel — see [Personal schedules](./schedules#integrations) |
| `time_off_request` | A request needs approval | person, dates, working, reason, request ids |
Expand Down
16 changes: 13 additions & 3 deletions docs/guide/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,24 @@ in a Google Chat DM.
Reveiwed → Reviewed*. Submit again and your answers post exactly as typed.
Code in backticks, links, @mentions, IDs like `PROJ-1423`, words with
digits or capitals, and names are left alone.
Answers take Chat's shorthand: `*bold*`, `_italic_`, `~strike~`,
`__underline__`, `` `code` ``, and `- ` at the start of a line for a
list. Each field's hint lists them. Markers only count right against
the text, so `snake_case` and `2*3*4` stay as typed.
2. Your answers post as your card under today's thread in the team space.
Your mood never appears there: it's seen in the web app only.
"Yesterday" comes pre-filled from your previous "today".
3. Plans changed? Your DM card now shows *Submitted at …* with **Edit
answers** until the deadline. Editing updates the posted card in place,
marked *edited*. After the deadline a first submission still posts, marked
*late*.
4. Not today? Press **🏖️ Skip today** and say why in one line. The reason goes
marked *edited*. After the deadline a first answer still posts, marked
*late*, until midnight in the standup's timezone.
4. Need to change an answer after the deadline? Press **Ask to edit** on your
card, and say why if you like. Your standup's managers get a card with
**Approve** and **Decline**. If one of them approves, you can edit until
midnight, and your posted card reads *Edited after the deadline · approved
by …*. You can ask once a day, and a request nobody decides lapses at
midnight. The wrap-up already posted stays as it was.
5. Not today? Press **🏖️ Skip today** and say why in one line. The reason goes
in today's thread, and you're listed as away, never as missing. Changed your
mind? **↩️ Undo skip** (or DM `unskip`) before the deadline removes that
reply and brings the form back.
Expand Down
6 changes: 6 additions & 0 deletions docs/guide/web-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ Personal weeks, days off and time-off approvals also live under **Team → Edit
schedule**, **My page → My schedule** and **Team → Requests** — see
[Personal schedules](./schedules).

**Team → Requests** also lists requests to edit answers after the deadline,
for anyone who can configure the standup: who asked, which standup and day,
their reason, and when the request lapses (midnight in the standup's
timezone). **Approve** lets them edit until then; **Decline…** takes an
optional note sent to them. The first decision wins, in Chat or here.

## Naming, archiving and deleting

- Standup names are unique across the workspace, archived standups included,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
"harper.js": "^2.10.0",
"luxon": "^3.7.2",
"pg": "^8.23.1",
"zod": "^4.6.5"
"zod": "^4.6.7"
},
"devDependencies": {
"@modelcontextprotocol/client": "^2.3.1",
"@seriousme/openapi-schema-validator": "^2.11.0",
"@types/bun": "^1.3.1",
"@types/bun": "^1.4.3",
"@types/express": "^5.0.6",
"@types/luxon": "^3.7.6",
"@types/node": "^26.6.5",
Expand Down
2 changes: 1 addition & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ <h1>Daily standups that run themselves.<br>No meeting. No SaaS. Your data.</h1>
<section class="how">
<h2>How it works</h2>
<div class="grid-3">
<article class="card"><span class="badge">09:30</span><h3>Everyone gets a card</h3><p>One DM, one form: yesterday, today, blockers, and a private mood with an optional comment. Likely typos are pointed out before posting, checked on your own server. Edit until the deadline; the posted card updates in place.</p></article>
<article class="card"><span class="badge">09:30</span><h3>Everyone gets a card</h3><p>One DM, one form: yesterday, today, blockers, and a private mood with an optional comment. Chat’s *bold*, _italic_ and - lists work in answers. Likely typos are pointed out before posting, checked on your own server. Edit until the deadline; the posted card updates in place. After it, a manager can approve a change.</p></article>
<article class="card"><span class="badge">As they answer</span><h3>Answers land in one thread</h3><p>Each answer is a card under today’s thread in your space. Away people are never nagged or counted as missing.</p></article>
<article class="card"><span class="badge">11:30</span><h3>The wrap-up tells the truth</h3><p>How many submitted and exactly who didn’t. Blockers open as items you can tag, acknowledge and resolve.</p></article>
</div>
Expand Down
133 changes: 133 additions & 0 deletions specs/005-late-edits.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 005: Editing answers after the deadline, with approval

| | |
| --- | --- |
| Status | Done |
| Size | Cross-cutting (migration, API, Chat cards, web app) |
| Figma | [Specs › 005 Late edits](https://www.figma.com/design/OQfcL61rbaGakS2FjBKasI?node-id=162-56) |
| PR | [#106](https://github.com/asyncup-dev/asyncup/pull/106) |

## Problem

Once a standup's deadline passes, an answer can't be changed. Someone
who spots a mistake, or whose plan changed, has to post a correction
by hand, and the wrong answer stays in History, exports and webhooks.
Opening edits to everyone would let answers change after the wrap-up
has been read, so a manager should agree first. Separately, a missed
standup can be answered late at any time today, even days later,
which makes old runs change long after anyone looks at them.

## User stories

- As someone who answered, I want to ask to change my answers after
the deadline, so the record is right.
- As a standup manager, I want to approve or decline those requests in
Chat or the web app, so answers only change when I agree.
- As someone who missed the standup, I can still answer late that day,
without asking anyone, as today.

## Acceptance criteria

Late first answers

1. Given someone has not answered, when they submit after the deadline
but on the run's day (standup timezone), then the answer posts,
marked late, with no approval, as today.
2. Given the run's day has ended, when they open or submit the form,
then it is refused: "This standup has closed. Answers for Thu
15 Oct can no longer be added." MCP `submit_answers` refuses the
same way.

Asking to edit

3. When a run closes, the prompt card of everyone who answered is
redrawn: "Submitted at 09:12. The deadline has passed. Need to
change something? Ask a manager." with an **Ask to edit** button.
4. **Ask to edit**, or **Edit answers** on a card drawn before the
deadline, opens a dialog with an optional reason (up to 200
characters) and **Send request**.
5. On sending, every manager of the standup gets a DM card: who, which
standup and day, the reason, **Approve** and **Decline…**. The
person is told "Sent to your managers. You'll hear back here."
6. Given a pending request, a second ask says "Your request is waiting
for a manager." One request per person per run: once declined, they
can't ask again that day.
7. Given the run's day has ended, **Ask to edit** says the day has
closed and sends nothing.

Deciding

8. Anyone holding `standups.configure` on the standup can decide, in
Chat or on the web app's Team › Requests. Nobody decides their own
request: "Your own requests go to your managers."
9. The first decision wins. Every manager's card is redrawn to
"Approved by Asha" or "Declined by Asha", and a later click says it
was already decided.
10. **Decline…** asks for an optional note, sent to the person.
11. Given no decision by the end of the run's day, the request lapses:
the person is told, and the managers' cards say "Lapsed".

Editing after approval

12. On approval, the person gets a DM: "Asha approved your request.
Edit your answers before 23:59 Europe/London." with **Edit
answers**, and their prompt card shows the same button.
13. Until the end of the run's day they can edit as often as they like,
from Chat or MCP. After that, edits are refused as in criterion 2.
14. The posted answer is updated in place, and its card reads "Edited
after the deadline · approved by Asha".
15. The `submission` webhook fires with `edited: true` and a new field
`editApprovedBy` (the approver's name, otherwise `null`).
16. The wrap-up already posted is not changed.

## Non-goals

- Approval for a first late answer.
- Asking to edit from the web app or MCP. Asking happens in Chat.
- Deciding from MCP.
- Reposting or editing the wrap-up after an approved edit.
- Editing runs from earlier days.

## Design impact

| Area | Change |
| --- | --- |
| Web app | Team › Requests: an **Edit requests** list beside time-off requests, with Approve and Decline…. The tab shows to anyone who can decide either kind |
| Chat cards and dialogs | Prompt card after the deadline (**Ask to edit**). Ask dialog. Manager's request card, pending and decided. DMs for approved, declined and lapsed. "Day closed" dialog error. Posted answer's "Edited after the deadline · approved by" line |
| API and OpenAPI | `GET /edit-requests` (pending requests the caller can decide), `POST /edit-requests/:id/approve` and `/decline` taking `DecisionBody`. Permission `standups.configure` on the request's standup. The `submission` webhook gains `editApprovedBy` |
| MCP | `submit_answers` refuses after the run's day ends, and refuses an edit after the deadline without approval: "Ask a manager to let you edit, from your prompt card in Chat." |
| Database | Migration 20, both dialects: `edit_requests` (run, person, reason, status `pending`, `approved`, `declined` or `lapsed`, asked at, decided at, decided by, note, and the managers' DM message names, one per person and run), and `submissions.edit_approved_by` for the posted card and the webhook |
| Permissions | None new. Deciding needs `standups.configure` |
| Docs | `user-guide.md` (late answers and asking to edit), `web-app.md` (Team › Requests), `api.md` (routes), `configuration.md` (webhook field) |
| Website | `site/index.html`: one line under answering in Chat |
| Google APIs and scopes | None |

How it works:

- "The run's day" ends at midnight in the standup's timezone.
`StandupService.submit` refuses after that, so Chat and MCP agree.
- `submit` lets an existing answer change after the deadline only with
an approved request for that run and person, before midnight.
- A new `EditRequests` service in `src/core` asks, decides and lapses.
The scheduler tick lapses pending requests whose day has ended. It
runs from the database, so a restart doesn't send anything twice.
- Manager cards are sent through `ChatAdapter`, and their message names
are stored so a decision can redraw all of them.
- When a run closes, the prompt cards of people who answered are
redrawn with the existing `refreshCards`.
- The prompt card then follows the request: waiting, approved (with
**Edit answers**), declined or lapsed.
- Request rows on Team › Requests wrap their buttons under the text on a
phone; time-off rows had squeezed their text there too.

## Open questions

None. Decided with the maintainer on 16 Oct 2026:

- Approval is needed only to edit an answer after the deadline.
- A first late answer posts without approval, until the end of that day.
- Approvers are the standup's managers, and anyone with
`standups.configure` on it, in the web app.
- The window ends at midnight in the standup's timezone.
- The wrap-up is not reposted after an approved edit.
- One request per person per run.
Loading
Loading