diff --git a/frontend/src/components/NoteEditor.vue b/frontend/src/components/NoteEditor.vue
index 205f6a0..a5e77a1 100644
--- a/frontend/src/components/NoteEditor.vue
+++ b/frontend/src/components/NoteEditor.vue
@@ -7,6 +7,7 @@ import ColorPicker from "./ColorPicker.vue";
import Icon from "./Icon.vue";
import LabelPicker from "./LabelPicker.vue";
import NoteChecklist from "./NoteChecklist.vue";
+import { fromLocalInput, toLocalInput } from "../notes/datetime";
import type { Note, NoteLabel } from "../stores/notes";
import type { NoteColor } from "../notes/colors";
@@ -86,6 +87,12 @@ async function openLink(link: { title: string; id: string | null }) {
emit("navigate", created.id);
}
+const reminderLocal = computed(() => toLocalInput(liveNote.value.remind_at));
+
+function onReminderChange(e: Event) {
+ void notes.setReminder(props.note.id, fromLocalInput((e.target as HTMLInputElement).value));
+}
+
async function onLabelsChange(next: NoteLabel[]) {
labelList.value = next;
await notes.setLabels(
@@ -226,6 +233,24 @@ async function act(fn: () => Promise) {
+ works in local time.
+
+export function toLocalInput(iso: string | null): string {
+ if (!iso) return "";
+ const d = new Date(iso);
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
+}
+
+export function fromLocalInput(local: string): string | null {
+ if (!local) return null;
+ return new Date(local).toISOString();
+}
+
+export function formatReminder(iso: string | null): string {
+ if (!iso) return "";
+ return new Date(iso).toLocaleString(undefined, {
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
+}
+
+export function isOverdue(iso: string | null): boolean {
+ return !!iso && new Date(iso).getTime() < Date.now();
+}
diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts
index 2933cb3..fd0108d 100644
--- a/frontend/src/router/index.ts
+++ b/frontend/src/router/index.ts
@@ -17,6 +17,7 @@ const router = createRouter({
{ path: "label/:id", name: "label", component: () => import("../views/BoardView.vue") },
{ path: "search", name: "search", component: () => import("../views/SearchView.vue") },
{ path: "graph", name: "graph", component: () => import("../views/GraphView.vue") },
+ { path: "reminders", name: "reminders", component: () => import("../views/RemindersView.vue") },
],
},
{
diff --git a/frontend/src/stores/notes.ts b/frontend/src/stores/notes.ts
index 02fa9fd..bbadae0 100644
--- a/frontend/src/stores/notes.ts
+++ b/frontend/src/stores/notes.ts
@@ -34,6 +34,7 @@ export interface Note {
pinned: boolean;
archived: boolean;
trashed: boolean;
+ remind_at: string | null;
labels: NoteLabel[];
items: ChecklistItem[];
attachments: Attachment[];
@@ -95,7 +96,7 @@ export const useNotesStore = defineStore("notes", () => {
async function mutate(
id: string,
- changes: Partial
>,
+ changes: Partial>,
): Promise {
reconcile(await api.patch(`/api/notes/${id}`, changes));
}
@@ -104,6 +105,7 @@ export const useNotesStore = defineStore("notes", () => {
const setArchived = (id: string, archived: boolean) => mutate(id, { archived });
const setColor = (id: string, color: NoteColor) => mutate(id, { color });
const setKind = (id: string, kind: NoteKind) => mutate(id, { kind });
+ const setReminder = (id: string, remindAt: string | null) => mutate(id, { remind_at: remindAt });
const saveEdit = (id: string, changes: { title: string; body: string; color: NoteColor }) => mutate(id, changes);
async function setLabels(id: string, labelIds: string[]): Promise {
@@ -192,6 +194,7 @@ export const useNotesStore = defineStore("notes", () => {
setArchived,
setColor,
setKind,
+ setReminder,
saveEdit,
setLabels,
addItem,
diff --git a/frontend/src/views/RemindersView.vue b/frontend/src/views/RemindersView.vue
new file mode 100644
index 0000000..873d720
--- /dev/null
+++ b/frontend/src/views/RemindersView.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
Reminders
+
+
Loading…
+
+
+
No reminders
+
Set a reminder on a note (in its editor) to see it here.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/thoughtsync/models/note.py b/src/thoughtsync/models/note.py
index 39a5a33..3884028 100644
--- a/src/thoughtsync/models/note.py
+++ b/src/thoughtsync/models/note.py
@@ -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,
}
diff --git a/src/thoughtsync/notes.py b/src/thoughtsync/notes.py
index dc1b3b6..c4a35b7 100644
--- a/src/thoughtsync/notes.py
+++ b/src/thoughtsync/notes.py
@@ -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()
diff --git a/tests/test_notes.py b/tests/test_notes.py
index 12591ae..4601e7f 100644
--- a/tests/test_notes.py
+++ b/tests/test_notes.py
@@ -96,3 +96,9 @@ async def test_graph_requires_auth(app):
client = app.test_client()
resp = await client.get("/api/graph")
assert resp.status_code == 401
+
+
+async def test_reminders_requires_auth(app):
+ client = app.test_client()
+ resp = await client.get("/api/notes/reminders")
+ assert resp.status_code == 401