Rebounder Tech Blog

Written by the people who actually run these systems in production.

copyOneDay Overwrite Silently Drops Pinned Notices

Published About 4 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

Copy-from-another-day overwrote a day's notices wholesale; preservePinnedNotices, already used by the AI-edit path, was never called on the copy path, so pinned notices vanished silently on copy.

Conclusion

copyOneDay, the internal function behind “copy from another day,” overwrote a target day’s notices wholesale with the source day’s content. The AI-edit path already used preservePinnedNotices to protect pinned notices (the “always show” flag), but the copy path’s write never called it, so any pinned notice a teacher had entered on the target day vanished silently the instant the copy ran.

Symptom

The signage editor has a “copy from another day” feature that duplicates the previous day’s or previous week’s notices and schedule onto a target day. The notice section lets a teacher pin an item so it keeps showing (“always”), and the path where the AI rewrites notices already preserved pinned items so they wouldn’t be lost.

But copyOneDay’s core replaced the target day’s notices directly with the source day’s daily.get("notice").

// As of 2026-07-18 14:36 (right after #1289 merged): a plain overwrite, blind to the target day's pinned notices
if (block === "schedule" || block === "notice" || block === "assignment") {
  const items = daily.get(block) ?? [];
  const field =
    block === "schedule" ? "schedules" : block === "notice" ? "notices" : "assignments";
  await upsertDailySectionForTarget(tx, actor, target, toDate, field, items);
  count = items.length;
}

Regardless of whether the source day’s notices contained any pinned rows, whatever pinned notices existed on the target day were wiped out by this upsertDailySectionForTarget call. From a teacher’s point of view, a notice they had pinned to “always show” on the target day disappeared for no visible reason right after running a copy.

Cause

This copy feature reached its current shape in #1289, which unified two separate implementations — previous-day copy and previous-week copy — into a single “copy from another day” tool. Overwriting the whole notice section wholesale was inherited behavior from the original previous-day/previous-week copy, and as a design for “make this day a duplicate of that one,” it’s correct on its own terms.

The problem was that a separate path — where the AI rewrites the notice section (assistant-chat-core) — already had a preservePinnedNotices function for exactly this protection. Two paths do the same kind of write to the notice section, yet only one of them preserved pinned notices. Review on #1289 flagged this asymmetry.

#1289 merged at 14:29, and #1292, which added an undo feature, merged right after at 14:36. #1292 is a safety net for “something went wrong after copying, so let me revert it” — it does nothing to stop pinned notices from disappearing in the first place. A teacher had no reason to reach for undo unless they noticed the pinned notice was gone, so in practice it offered little real relief.

Fixing it

Only copyOneDay’s notice write was changed to route through preservePinnedNotices, so it now matches the AI-edit path.

// #1310 (merged 16:39): merge pinned rows from the raw "before" snapshot of the target day,
// taken before the overwrite, back into what gets written
const toWrite =
  block === "notice"
    ? preservePinnedNotices(
        [{ date: toDate, items: (before.notice ?? []) as NoticeItem[] }],
        toDate,
        items as NoticeItem[],
      )
    : items;
await upsertDailySectionForTarget(tx, actor, target, toDate, field, toWrite);

before is the pre-overwrite snapshot of the target day that was already being read for undo, reused as-is inside the same transaction as the write. No extra read was added.

There’s no duplication with the source day’s pinned notices either. copyableNoticeItems, which reads the source day inside readCopySource, already excludes the source day’s pinned notices from what it hands back as items, so all preservePinnedNotices adds are the pinned notices that already existed on the target day.

Preventing a repeat

What makes this pattern dangerous is that, of the three block types — schedule, notice, assignment — only notice needed this preservation. Anyone editing inside the same if branch has to remember to single out the notice case, and the next time a similar write path is added, the same gap can happen again.

So copy-pinned-notice.test.ts stubs the surrounding seams — DB, authorization, pattern resolution — but leaves the pure pinned-detection logic (copyableNoticeItems, preservePinnedNotices) real and unmocked, asserting directly on the notices that actually reach upsertDailySectionForTarget. By stopping the periphery and measuring only the core for real, this test will fail the next time a write path is added without the preservation call.

Frequently asked questions

Q1If there's an undo, is there no real harm?

Undo can restore the notices, but teachers had no way to notice the pinned notice was gone in the first place. Undo only helps after someone happens to spot the loss; it does nothing to prevent the accident at copy time. Undo itself had already landed earlier the same day in a separate PR (#1292).

Q2Doesn't preserving the target day's pinned notices duplicate the source day's?

No. copyableNoticeItems, which reads the source day, already excludes the source day's pinned notices from what gets copied. The preservation step only adds back the pinned notices that were already on the target day; the source day's pinned notices are never copied in the first place.

Q3Where was pinned-notice preservation implemented?

In a function called preservePinnedNotices inside assistant-chat-core. The path where the AI rewrites the notice section already ran through this function, but the copy feature's write path never called it.

Q4How long was this bug live in production?

As far as can be confirmed, under a day: from #1289's merge (14:29), which unified previous-day/previous-week copy into one tool, to #1310's merge (16:39), which added the preservation call — about two hours. Caught in review, fixed the same day.

Q5How was the regression test written?

A new copy-pinned-notice.test.ts. It stubs the surrounding seams — DB, authorization, pattern resolution — but leaves the pure pinned-detection logic (copyableNoticeItems, preservePinnedNotices) unmocked and real, asserting on the actual notices passed into upsertDailySectionForTarget.

Environment verified

  • Next.js 16.0 / React 19.0 / TypeScript 6.0.3 / Vitest 3.2.6
  • Found in review 2026-07-18, fixed the same day (about two hours later)

What this article is based on

  • TypeScript file lines 255-262commit 7e83b3c
  • TypeScript file lines 261-283commit f6db3a0
  • TypeScript file lines 1-20commit f6db3a0

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.