M3 reminders: notes.remind_at + Reminders view
- 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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user