fix(activities): stamp local tz offset on event datetimes before submit

The form sent naive "YYYY-MM-DDTHH:MM" to the LNbits events backend,
where _to_unix (nostr_publisher.py) assumes UTC when tzinfo is None.
So 08:00 entered in CEST got stored as 08:00 UTC, and the NIP-52 start
tag landed on the relays at the wrong instant — the detail page then
re-localized it to 10:00 (offset doubly applied).

Stamp the wall-clock value with the user's UTC offset before sending so
the backend builds the correct unix and the detail page renders the
intended wall-clock. Seconds (`:00`) included for pre-3.11 Python
fromisoformat compatibility. Round-trips through edit mode unchanged:
splitDateTime trims to "HH:MM" so the suffix drops cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-23 15:21:40 +02:00
commit c556d28587

View file

@ -87,6 +87,28 @@ function foldDateTime(date: string, time: string): string {
return time ? `${date}T${time}` : date return time ? `${date}T${time}` : date
} }
// Stamp the form's wall-clock datetime with the user's local UTC offset
// before sending it to the LNbits events backend. Without this, the
// backend's `_to_unix` (nostr_publisher.py) treats a naive ISO string
// as UTC, so e.g. "08:00" entered in CEST gets stored as 08:00 UTC and
// the NIP-52 `start` tag is off by the user's offset on the relay
// the detail page then renders it +offset (08:00 10:00 in CEST).
// Preserving the user's intended wall-clock means stamping it here.
// Date-only values (no "T") pass through unchanged.
function withLocalTzOffset(value: string): string {
if (!value || !value.includes('T')) return value
// The form's "YYYY-MM-DDTHH:MM" is parsed by JS Date as local time;
// getTimezoneOffset() returns minutes west of UTC, so negate it.
const offMin = -new Date(value).getTimezoneOffset()
const sign = offMin >= 0 ? '+' : '-'
const abs = Math.abs(offMin)
const hh = String(Math.floor(abs / 60)).padStart(2, '0')
const mm = String(abs % 60).padStart(2, '0')
// Include `:00` seconds for compatibility with older Python
// `datetime.fromisoformat` (pre-3.11 won't accept "HH:MM+HH:MM").
return `${value}:00${sign}${hh}:${mm}`
}
const formSchema = toTypedSchema( const formSchema = toTypedSchema(
z z
.object({ .object({
@ -138,8 +160,11 @@ interface BannerImage extends UploadedImage {
} }
const bannerImages = ref<BannerImage[]>([]) const bannerImages = ref<BannerImage[]>([])
// Inverse of foldDateTime: split a stored "YYYY-MM-DD[THH:MM]" back // Inverse of foldDateTime: split a stored "YYYY-MM-DD[THH:MM[:SS][±HH:MM]]"
// into separate date + time pieces for the form inputs. // back into separate date + time pieces for the form inputs. The
// time slice trims to "HH:MM" so any seconds + offset suffix added by
// withLocalTzOffset on submit drops cleanly the user sees the same
// wall-clock they originally entered when re-editing.
function splitDateTime(value: string | null | undefined): { date: string; time: string } { function splitDateTime(value: string | null | undefined): { date: string; time: string } {
if (!value) return { date: '', time: '' } if (!value) return { date: '', time: '' }
const [date, time = ''] = value.split('T') const [date, time = ''] = value.split('T')
@ -267,9 +292,8 @@ const onSubmit = form.handleSubmit(async (formValues) => {
try { try {
const eventData: CreateEventRequest = { const eventData: CreateEventRequest = {
name: formValues.name, name: formValues.name,
event_start_date: foldDateTime( event_start_date: withLocalTzOffset(
formValues.event_start_date, foldDateTime(formValues.event_start_date, formValues.event_start_time)
formValues.event_start_time
), ),
} }
if (!isEditMode.value) { if (!isEditMode.value) {
@ -281,9 +305,8 @@ const onSubmit = form.handleSubmit(async (formValues) => {
// Optional fields only include if provided // Optional fields only include if provided
if (formValues.info) eventData.info = formValues.info if (formValues.info) eventData.info = formValues.info
if (formValues.event_end_date) { if (formValues.event_end_date) {
eventData.event_end_date = foldDateTime( eventData.event_end_date = withLocalTzOffset(
formValues.event_end_date, foldDateTime(formValues.event_end_date, formValues.event_end_time)
formValues.event_end_time
) )
} }
if (formValues.location) eventData.location = formValues.location if (formValues.location) eventData.location = formValues.location