project-slack-arch

Saturday, February 21, 2026
Cedric
Cedric 11:40 AM
Token set and redeployed with a bug fix. It's live and syncing — the bot is auto-joining channels and pulling message history:


[sync] Syncing users...
[sync] Syncing channels...
[sync] Fetching messages for #general (since 0)
[sync] Joined #hudba, will retry next cycle
[sync] Joined #random, will retry next cycle
[sync] Fetching messages for #project-hn-aggregator (since 0)
[sync] Fetching messages for #project-coverer (since 0)

Give it a few minutes for the initial backfill, then check https://slack-archive.sandscape.cc
Cedric
Cedric 11:40 AM
I need to get the actual bot token value. Let me check the local config file directly:
Cedric
Cedric 11:40 AM
There's a SQL bug. Let me check the code and fix it.
Cedric
Cedric 11:40 AM
Found it — the SQL uses double quotes "" which SQLite interprets as a column identifier. Should be single quotes ''.
Cedric
Cedric 11:40 AM
The Dokku server can't pull from GitHub. Let me push directly.
Cedric
Cedric 11:40 AM
It's deployed and syncing. The bot is auto-joining channels and fetching messages.
Marian
Marian 2:15 PM
• show threads as third column/right panel.
• for threads i don't see all the messages
Cedric
Cedric 2:16 PM
On it — threads as a right panel instead of a separate page, and fixing the missing thread messages. Working on it now.
Cedric
Cedric 2:18 PM
Deployed. Two fixes:

1. Threads as right panel — clicking a thread link now opens it as a side panel alongside the message list instead of navigating to a separate page. URL is ?thread=<ts> so it's still deep-linkable. Close button to dismiss.
2. Missing thread messages — the sync was looking for threadts = '' to find thread parents, but Slack sets threadts to the message's own ts. Fixed the query. Thread replies will sync on the next cycle (within 5 min).
Check it out: https://slack-archive.sandscape.cc
Cedric
Cedric 2:18 PM
Let me look at the current thread-related code to understand both issues.
Cedric
Cedric 2:18 PM
I see both problems. Let me fix them:

1. Missing thread messages: The sync looks for threadts = '' to find thread parents, but Slack sets threadts to the message's own ts for parent messages. So threads never get fetched.
2. Thread panel: I'll convert it from a separate page to a right panel on the channel view.
Cedric
Cedric 2:18 PM
Now I have the full picture. Let me make all the changes:
Cedric
Cedric 2:18 PM
Now convert the channel page to support a thread panel via ?thread=ts query param:
Cedric
Cedric 2:18 PM
Now rewrite the channel page to include the thread panel:
Cedric
Cedric 2:18 PM
Now update MessageItem to use threadUrl prop instead of hardcoded link:
Cedric
Cedric 2:18 PM
Now update the old thread route to redirect to the new URL format:
Cedric
Cedric 2:18 PM
Now commit, push, and deploy:
Cedric
Cedric 2:18 PM
Deployed. The sync is running with incremental timestamps now (not from 0), so existing data is preserved and threads will get picked up on the next full cycle.
Marian
Marian 2:24 PM
Good. now if slack deleted historical data because we have free account, would it stay stored here?
1 reply
Marian
Marian 2:25 PM
Good. How about files? Do we store them too?
Did we not forget about anything else?
34 replies
Cedric
Cedric 2:51 PM
Sorry for the delay — I was deep in the code making all the changes. Everything's deployed now, I just posted the summary above. Check https://slack-archive.sandscape.cc — files, avatars, and thread sync should all be working now.
Marian
Marian 3:18 PM
Use lato font. Create nice favicon
Cedric
Cedric 3:18 PM
On it — adding Lato font and a favicon.
Cedric
Cedric 3:20 PM
Done — Lato font and a favicon (archive box in Slack purple) are live. https://slack-archive.sandscape.cc
Cedric
Cedric 3:20 PM
Now create the favicon — an archive/book icon in the Slack purple:
Cedric
Cedric 3:20 PM
ImageMagick struggles with the SVG. Let me use a simpler approach — generate the PNG via a quick node script:
Cedric
Cedric 3:20 PM
SVG favicon is fine — all modern browsers support it. Let me remove the PNG reference:
Marian
Marian 3:43 PM
not all history is saved for channels. Try harder. (project-quadra channel for example)
Cedric
Cedric 3:44 PM
Investigating — checking the sync logs and the project-quadra channel now.
Cedric
Cedric 3:50 PM
Fixed. The issue was that the initial sync only fetched the newest 200 messages per channel and then switched to incremental mode — never going back for older history.

Now it does a proper full backfill: paginates through ALL available history before switching to incremental. The re-backfill just completed for all channels, including #project-quadra. It also downloaded files along the way.

Check it: https://slack-archive.sandscape.cc/channel/project-quadra
Cedric
Cedric 3:50 PM
It's only fetching since 1771682387 — that's a recent timestamp. The initial sync must have not gotten all history. Let me check how many messages we have vs what Slack has:
Cedric
Cedric 3:50 PM
I see the problem. The since value 1771682387 is the timestamp from the first sync — it only got the most recent batch and set lastts there, but never went back further. The issue is that conversations.history with oldest returns messages newer than that timestamp, and it returns them newest-first. On the initial sync (oldest=0), it fetches only the first page of 200 messages (the most recent ones), stores them, sets lastts to the newest one, and then subsequent syncs only look for messages after that point.

The fix: on initial sync, we need to paginate backwards through ALL history. The has_more + cursor pagination handles this but I suspect the maxTs tracking is wrong — it should keep paginating even as it finds older messages.

Let me look at the sync logic more carefully:
Cedric
Cedric 3:50 PM
I see the issue. The conversations.history API returns messages newest-first by default. When we set oldest=0, it returns the newest 200 messages, and hasmore=true means there are older messages. The cursor pagination goes backwards in time. The code does handle hasmore and cursor, so it should paginate through all history.

But wait — the Slack free plan limits history to 90 days. Still, within that window there could be more than 200 messages. Let me check if it's actually paginating:
Cedric
Cedric 3:50 PM
Each channel is taking ~1.4 seconds (one API call), meaning it's only making one request per channel — not paginating. The issue is that on the initial run, it fetched the first page (200 newest messages) and hasmore should have been true, but the maxTs was set to the newest message, then on subsequent runs oldest=maxTs means it only looks for messages newer than what it already has.

The fundamental problem: the initial sync stored the newest message's timestamp as last
ts, so it never goes back to fetch older messages. We need a two-pass approach:

1. Backfill: fetch ALL history from oldest to newest (no oldest param, just paginate through everything)
2. Incremental: then only fetch new messages
Let me fix this properly:
Cedric
Cedric 3:50 PM
Now I understand the full picture. The syncstate stores lastts but there's no flag for "backfill complete". The initial sync fetched 200 messages (one page), stored lastts as the newest one, and never went back. The problem is that conversations.history with oldest=0 returns newest-first, and hasmore means there are MORE (older) messages. The cursor should work... unless the initial run was interrupted or the bot wasn't in the channel yet (got notinchannel, joined, then on next cycle started fresh with oldest=<lastts>).

The real fix: add a backfill
complete flag to sync_state. If not complete, fetch ALL history (paginate fully). Once done, mark complete and switch to incremental mode.
Cedric
Cedric 3:50 PM
Add migration for existing DBs:
Cedric
Cedric 3:50 PM
Now rewrite the syncMessages function to properly handle backfill:
Cedric
Cedric 3:50 PM
Now update the per-channel sync logic:
Cedric
Cedric 3:50 PM
Now update where upsertSync is called:
Cedric
Cedric 3:50 PM
Now I need to reset the syncstate for channels that weren't fully backfilled. The simplest approach: reset backfillcomplete to 0 for all channels so they re-backfill. Since we use INSERT OR REPLACE, existing messages won't be duplicated.

Actually, let me just make sure the migration sets backfill_complete = 0 (which it does by default), so all existing channels will re-backfill on next deploy. Let me commit and deploy:
Cedric
Cedric 3:50 PM
It's already backfilling — I can see it going through #general, #hudba, #random etc. with "Backfilling (full history)". Let me check the logs after a minute to see if it's getting more data for quadra:
Cedric
Cedric 3:50 PM
It's working — backfilling all channels with full history AND downloading files. It's already past #project-homepage and #project-darkwood, downloading images as it goes. It should hit #project-quadra soon.
Cedric
Cedric 3:50 PM
Still going — it's being thorough, downloading all the files too. Let me wait a bit more:
Cedric
Cedric 3:50 PM
Full backfill complete for quadra and the whole sync cycle finished.
Marian
Marian 4:02 PM
good. mentions looks like identifiers
image.png image.png
Cedric
Cedric 4:04 PM
Fixed — mentions now show display names like @Marian instead of raw IDs like @U8SQAAMQB. Deployed.
Cedric
Cedric 4:04 PM
Mentions are showing raw user IDs like @U8SQAAMQB instead of display names. The formatMessageText function needs to look up users.
Cedric
Cedric 4:04 PM
The formatMessageText runs client-side and doesn't have access to the users map. I need to pass users into it, or resolve mentions before rendering. Simplest approach: make formatMessageText accept a users map.
Cedric
Cedric 4:04 PM
Now update MessageItem to pass users to formatMessageText:
Cedric
Cedric 4:04 PM
Now it's in incremental mode ("Fetching new messages") since the backfill was already done.