diff --git a/frontend/src/components/FilterBar.vue b/frontend/src/components/FilterBar.vue
index 002a1d2..e04406a 100644
--- a/frontend/src/components/FilterBar.vue
+++ b/frontend/src/components/FilterBar.vue
@@ -6,6 +6,7 @@ import { useSavedFiltersStore } from "../stores/savedFilters";
import { useUiStore } from "../stores/ui";
import type { NoteFacets } from "../stores/notes";
import { facetCount, facetsFromQuery, facetsToQuery } from "../notes/facets";
+import { addLocalDays, formatLocalDay, parseLocalDate } from "../notes/datetime";
import { NOTE_COLOR_KEYS, NOTE_COLOR_LABELS, NOTE_SWATCH_CLASSES, type NoteColor } from "../notes/colors";
import Icon from "./Icon.vue";
@@ -56,30 +57,25 @@ function onQ(e: Event) {
qTimer = setTimeout(() => patch({ q: v.trim() || undefined }), 300);
}
-function isoDay(d: Date): string {
- return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
-}
function onFrom(e: Event) {
const v = (e.target as HTMLInputElement).value;
patch({ created_after: v ? `${v}T00:00:00` : undefined });
}
function onTo(e: Event) {
const v = (e.target as HTMLInputElement).value;
- if (!v) {
+ const d = v ? parseLocalDate(v) : null;
+ if (!d) {
patch({ created_before: undefined });
return;
}
// Half-open upper bound: the start of the day AFTER the chosen date (so it's inclusive).
- const d = new Date(`${v}T00:00:00`);
- d.setDate(d.getDate() + 1);
- patch({ created_before: `${isoDay(d)}T00:00:00` });
+ patch({ created_before: `${formatLocalDay(addLocalDays(d, 1))}T00:00:00` });
}
const fromInput = computed(() => (facets.value.created_after ?? "").slice(0, 10));
const toInput = computed(() => {
- if (!facets.value.created_before) return "";
- const d = new Date(facets.value.created_before);
- d.setDate(d.getDate() - 1);
- return isoDay(d);
+ // Reverse the half-open bound (day-after) back to the chosen end day for the input.
+ const d = facets.value.created_before ? parseLocalDate(facets.value.created_before.slice(0, 10)) : null;
+ return d ? formatLocalDay(addLocalDays(d, -1)) : "";
});
async function saveView() {
diff --git a/frontend/src/notes/datetime.ts b/frontend/src/notes/datetime.ts
index 8da1cfe..c821733 100644
--- a/frontend/src/notes/datetime.ts
+++ b/frontend/src/notes/datetime.ts
@@ -26,3 +26,26 @@ export function formatReminder(iso: string | null): string {
export function isOverdue(iso: string | null): boolean {
return !!iso && new Date(iso).getTime() < Date.now();
}
+
+// --- Local calendar-date helpers (the created-date range facets work in the viewer's
+// LOCAL day; the board's FilterBar and the Timeline view both build day bounds). ---
+
+// Parse an value ("YYYY-MM-DD") as a LOCAL calendar date (midnight
+// local), or null if it isn't a valid YYYY-MM-DD.
+export function parseLocalDate(value: string): Date | null {
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
+ if (!m) return null;
+ return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
+}
+
+// A new local Date shifted by whole days — e.g. +1 for the half-open upper bound that
+// includes the whole end day, -1 to reverse it for display. Non-mutating.
+export function addLocalDays(d: Date, days: number): Date {
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + days);
+}
+
+// Format a local Date as "YYYY-MM-DD" (the shape an reads).
+export function formatLocalDay(d: Date): string {
+ const pad = (n: number) => String(n).padStart(2, "0");
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
+}
diff --git a/frontend/src/views/TimelineView.vue b/frontend/src/views/TimelineView.vue
index 6cb2c9a..8ddf86d 100644
--- a/frontend/src/views/TimelineView.vue
+++ b/frontend/src/views/TimelineView.vue
@@ -2,6 +2,7 @@
import { computed, onMounted, ref, watch } from "vue";
import { api } from "../api/client";
import { type Note } from "../stores/notes";
+import { addLocalDays, parseLocalDate } from "../notes/datetime";
import { useNoteList } from "../composables/useNoteList";
import { useNoteEditor } from "../composables/useNoteEditor";
import AsyncState from "../components/AsyncState.vue";
@@ -18,23 +19,13 @@ const fromDate = ref("");
const toDate = ref("");
const hasRange = computed(() => !!fromDate.value || !!toDate.value);
-// Parse an value ("YYYY-MM-DD") as a LOCAL calendar date.
-function localDate(value: string): Date | null {
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
- if (!m) return null;
- return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
-}
-
function buildQuery(): string {
const params = new URLSearchParams({ filter: "active", sort: "created" });
- const from = localDate(fromDate.value);
+ const from = parseLocalDate(fromDate.value);
if (from) params.set("created_after", from.toISOString());
- const to = localDate(toDate.value);
- if (to) {
- // Half-open upper bound: start of the day AFTER `to`, so the whole `to` day is included.
- const end = new Date(to.getFullYear(), to.getMonth(), to.getDate() + 1);
- params.set("created_before", end.toISOString());
- }
+ const to = parseLocalDate(toDate.value);
+ // Half-open upper bound: start of the day AFTER `to`, so the whole `to` day is included.
+ if (to) params.set("created_before", addLocalDays(to, 1).toISOString());
return params.toString();
}