From d74c925eb90e397a17352e3b2a29d9f89eb4d0c1 Mon Sep 17 00:00:00 2001 From: Aaron Vu Date: Wed, 9 Sep 2026 00:19:27 +0700 Subject: [PATCH 1/3] feat(push): read a workspace chat message by id for the mobile push banner The mobile push worker now quotes the message a chat.post or channel.post event is about, at delivery time, so the queue keeps carrying identifiers only. Direct messages already had p2p_get_message on the sender's database; this is its counterpart on the hub: one channel row by message_id, trashed rows excluded so a deleted message is never quoted after the fact. (cherry picked from commit 006ff85e3f8afa72bdd365a0e1da5869c11915e4) --- .../channel/channel_get_message.sql | 22 +++++++++++++++++++ patches/manifest.txt | 1 + 2 files changed, 23 insertions(+) create mode 100644 hub/procedures/channel/channel_get_message.sql diff --git a/hub/procedures/channel/channel_get_message.sql b/hub/procedures/channel/channel_get_message.sql new file mode 100644 index 00000000..8961af27 --- /dev/null +++ b/hub/procedures/channel/channel_get_message.sql @@ -0,0 +1,22 @@ +DELIMITER $ + +-- ========================================================= +-- channel_get_message +-- +-- One workspace or folder chat message by id, for the mobile push worker to +-- quote in a banner at delivery time. Mirrors `p2p_get_message` in the +-- drumate schema: the push queue carries only identifiers, so the text is +-- read here, once, when the notification is composed. A trashed message is +-- not returned, so a deleted message is never quoted after the fact. +-- ========================================================= +DROP PROCEDURE IF EXISTS `channel_get_message`$ +CREATE PROCEDURE `channel_get_message`( + IN _message_id VARCHAR(16) CHARACTER SET ascii +) +BEGIN + SELECT message_id, author_id, message, thread_id, attachment + FROM channel + WHERE message_id = _message_id AND status != 'trashed'; +END $ + +DELIMITER ; diff --git a/patches/manifest.txt b/patches/manifest.txt index cbd94bc5..a689a5e5 100644 --- a/patches/manifest.txt +++ b/patches/manifest.txt @@ -372,3 +372,4 @@ yellow_page/procedures/analytics/feature_mark.sql yellow_page/procedures/analytics/session_stats.sql yellow_page/procedures/analytics/cohort_retention.sql + hub/procedures/channel/channel_get_message.sql From 1feded355352c8f9a17388b712d180cfe13181b5 Mon Sep 17 00:00:00 2001 From: Aaron Vu Date: Mon, 21 Sep 2026 21:37:52 +0700 Subject: [PATCH 2/3] chore(changelog): record the task_delete cascade deployment on stage The routine has carried the cascade here since 569976f, but stage was still running the parent-only version, so a task with subtasks left orphans behind. Applied to all 1492 entity databases and the factory template caches, which had to be rebuilt because a new workspace is cloned from them. Also records that bin/patch.js on the stage host is older than this repository and its common target silently skips every organization database. (cherry picked from commit 00067a053592900f1ddf70972c7c8c2202628fd4) --- patches/changelog.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/patches/changelog.txt b/patches/changelog.txt index 350e516c..794ba113 100644 --- a/patches/changelog.txt +++ b/patches/changelog.txt @@ -1,3 +1,29 @@ +2026-09-21 (Deployed the task_delete cascade onto stage. The routine has carried + the cascade in this repository since 569976f (2026-08-21) and is identical on + preview and main, but stage was still running the version that deletes only + the parent row: a task with subtasks left every child behind as an orphan, + reachable by no board and removable by nothing. + The stale copy is detectable from a client without reading the database: the + old body ends in SELECT ROW_COUNT() AS affected, and ROW_COUNT() is BIGINT, so + `affected` arrives over the wire as a string; the current body assigns it to a + DECLARE ... INT local first, so it arrives as a number. Same server, same + session, task.comment_delete already answered with a number while task.delete + answered with a string -- which is what proved this was a deployment gap and + not a logic bug. + Applied to all 1492 entity databases (989 hub, 450 drumate, 53 organization). + NOTE for whoever patches next: bin/patch.js on the stage host is older than + this repository and its `common` target reads + type IN ('drumate','hub') -- it silently skipped all 53 organization + databases, which had to be patched by listing them explicitly. 150 further + databases still carry the old routine; none has an entity row and none holds + any task row, so nothing reachable by the product runs them. + The factory template caches in /tmp were rebuilt too, because a new workspace + is cloned from them and they still held the old routine -- patching the live + databases alone would have left every workspace created afterwards broken. + Verified on a freshly provisioned hub: affected came back a number, the + subtask id was returned, and the subtask was actually gone.) + common/procedures/task/task_delete.sql + 2026-09-14 (Reverted: media.copy stays in the notification surfaces after all. Earlier today these four routines were changed to drop media.copy from the feed, the unread feed, the badge and push, because the rows Lexis was seeing From 4d5bed0a36451074f0b538e77af6ec7e07ce2914 Mon Sep 17 00:00:00 2001 From: Aaron Vu Date: Tue, 22 Sep 2026 13:11:31 +0700 Subject: [PATCH 3/3] fix(channel): return the message row for a folder post on the owner's own desk channel.post writes into the poster's own database when the desk folder chat is used, and that database runs the drumate variant of channel_post_message. Its non-hub branch is written for P2P: it ends by bumping time_channel with the peer id, which channel.post never sends because a folder post has no peer. The insert failed on the NOT NULL primary key after the channel row was already written, and the EXIT HANDLER returned the error JSON in place of the message row. The client keys its attachment fetch on the message_id in that row, so the bubble stayed optimistic with no file card until a reload. Skip the time_channel bump when there is no peer. The P2P path, which always has one, is unchanged. (cherry picked from commit b81ce57f404557117c8117ab3457207fe2334df3) --- .../channel/channel_post_message.sql | 10 +++++++- patches/changelog.txt | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/drumate/procedures/channel/channel_post_message.sql b/drumate/procedures/channel/channel_post_message.sql index 0b5ba39a..d0bbf07e 100644 --- a/drumate/procedures/channel/channel_post_message.sql +++ b/drumate/procedures/channel/channel_post_message.sql @@ -193,7 +193,15 @@ DECLARE _is_duplicate INTEGER DEFAULT 0; -- the top — matching Slack/Teams/Discord threads & Google-Docs/Figma comments. -- Mirrors hub's symmetric guard on its read-pointer advance. Normal messages -- (file_thread_id NULL) take this branch exactly as before → no regression. - IF _file_thread_id IS NULL THEN + -- + -- _entity_id is the peer of a P2P conversation and only chat.post sends it. + -- A folder-scoped post on the owner's own desk arrives through channel.post + -- with no entity_id: there is no peer conversation to bump, and inserting + -- NULL into time_channel.entity_id (NOT NULL, primary key) raised an + -- SQLEXCEPTION *after* the channel row was written. The EXIT HANDLER then + -- returned the error JSON instead of the message row, so the client never + -- received message_id and the attachment card never loaded until reload. + IF _file_thread_id IS NULL AND _entity_id IS NOT NULL THEN INSERT INTO time_channel(entity_id, ref_sys_id,message,ctime) SELECT _entity_id, _ref_sys_id,_message, _ctime ON DUPLICATE KEY UPDATE ref_sys_id= _ref_sys_id, ctime =_ctime ,message=_message; END IF; diff --git a/patches/changelog.txt b/patches/changelog.txt index 794ba113..48df587e 100644 --- a/patches/changelog.txt +++ b/patches/changelog.txt @@ -1,3 +1,18 @@ +2026-09-22 (Personal desk, folder chat: a message with a file attached was + stored but the sender saw no file card, and sometimes no message, until the + page was reloaded. channel.post writes into the owner's own database, whose + channel_post_message is the drumate variant. Its non-hub branch is written + for P2P and ends by bumping time_channel with the peer id, which channel.post + never sends because a folder post has no peer. The insert failed on the NOT + NULL primary key, the EXIT HANDLER returned {SUCCESS:0} in place of the + message row, and the client, which keys its attachment fetch on the + message_id in that row, kept an optimistic bubble that could never complete. + Reproduced on stage with a test account: the channel row was present with its + sbox attachment, the response carried _db_err "Column 'entity_id' cannot be + null" and no message_id. Skip the time_channel bump when there is no peer; + the P2P path, which always has one, is unchanged.) + drumate/procedures/channel/channel_post_message.sql + 2026-09-21 (Deployed the task_delete cascade onto stage. The routine has carried the cascade in this repository since 569976f (2026-08-21) and is identical on preview and main, but stage was still running the version that deletes only @@ -59,6 +74,15 @@ templates/factory/drumate.sql templates/factory/seed/yp.sql +2026-09-09 (channel_get_message reads one workspace chat message by id, for the + mobile push worker: the push queue and Redis carry identifiers only, so the + banner text (sender, workspace, excerpt) is read at delivery time through a + stored procedure. Direct messages already had p2p_get_message on the sender's + database; workspace and folder chat had no single-row read. Skips a trashed + message so a deleted message is never quoted after the fact. Applied on stage + to every database holding channel_post_message, 1620 schemas, 0 failures.) + hub/procedures/channel/channel_get_message.sql + 2026-09-09 (Workspace search returns results again: seo_search_unified called vhost() unqualified. vhost is a yellow_page function -- it exists only in yp, never in a hub or a