M3 reminders: notes.remind_at + Reminders view
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 7s
CI & Build / Build & push image (push) Successful in 32s

- Migration 0010: notes.remind_at (nullable tz). PATCH accepts remind_at
  (ISO set / null clear); GET /api/notes/reminders (soonest first, non-trashed);
  serialize includes remind_at.
- Frontend: datetime util (local<->ISO, format, overdue); notes store setReminder;
  editor datetime-local picker + clear; card reminder chip (overdue = red);
  sidebar Reminders entry + /reminders view.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
2026-07-20 08:12:14 -04:00
co-authored by Claude Opus 4.8
parent ad006ccb58
commit c57982d910
12 changed files with 205 additions and 1 deletions
+3
View File
@@ -48,6 +48,8 @@ class Note(Base):
archived: Mapped[bool] = mapped_column(Boolean(), nullable=False, server_default=func.false())
# Soft delete: non-null => in Trash. Restore sets it back to null.
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
# Optional reminder time (surfaced in the Reminders view; no push in M3).
remind_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
@@ -64,6 +66,7 @@ class Note(Base):
"pinned": self.pinned,
"archived": self.archived,
"trashed": self.deleted_at is not None,
"remind_at": self.remind_at.isoformat() if self.remind_at else None,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
+26
View File
@@ -198,6 +198,23 @@ async def search_notes():
return jsonify({"notes": await _serialize_notes(db, notes)})
@bp.get("/reminders")
@login_required
async def list_reminders():
async with session_scope() as db:
stmt = (
select(Note)
.where(
visible_to_user("note", Note.owner_id, Note.id, g.user_id),
Note.deleted_at.is_(None),
Note.remind_at.is_not(None),
)
.order_by(Note.remind_at.asc())
)
notes = (await db.scalars(stmt)).all()
return jsonify({"notes": await _serialize_notes(db, notes)})
@bp.get("/titles")
@login_required
async def list_titles():
@@ -343,6 +360,15 @@ async def update_note(note_id: str):
note.pinned = bool(data["pinned"])
if "archived" in data:
note.archived = bool(data["archived"])
if "remind_at" in data:
raw = data["remind_at"]
if raw in (None, ""):
note.remind_at = None
else:
try:
note.remind_at = datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
except ValueError:
return jsonify({"error": "invalid remind_at"}), 400
if "body" in data:
await _rewrite_links(db, note)
await db.commit()