Commit Graph
17 Commits
Author SHA1 Message Date
arnefandCopilot d0e1dab837 Use display name instead of username for shared calendar owners
Add db.DisplayName(username), returning the user's display name if
set, otherwise the raw username. Use it everywhere a shared calendar's
owner is shown: the web calendar legend ("shared by ..."), the "new
event"/edit calendar select label, the dashboard's "Shared with you"
list, and the CalDAV-side description/name-collision disambiguation
("Name (Owner)") shown to CalDAV clients like DAVx5.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-21 06:41:51 +02:00
arnefandCopilot 8b10386c91 Add account management page
Add a new /web/account page, reachable by clicking the username in
the nav, with two forms: updating display name/email, and changing
the password (requires the current password, min 8 chars, confirm
match). Add db.SetProfile to persist display name/email.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-21 06:32:18 +02:00
arnefandCopilot 1c039e6396 Add week view to web calendar with Month/Week toggle
Extract the shared per-calendar event-collection logic out of
buildMonthView into collectCalendarEvents, keyed by day so it can feed
either the existing 42-cell month grid or a new 7-cell week grid.

Add templates.WeekDay/WeekViewData, a WeekView templ, and a
viewSwitcher component shown in both views' headers to switch between
Month and Week. Add buildWeekView, handleCalendarWeek, parseWeekStart,
and mondayOf in internal/web/calendar.go, and register the new
/calendar/week route.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-21 06:19:59 +02:00
arnefandCopilot 28c55804d8 Fix unreadable event text when a calendar's color is white/very light
The month view rendered each event's text (and its little leading dot)
in the calendar's own color, on a very lightly tinted (~13% opacity)
background of that same color. For a light color like white or pale
yellow, the text ended up effectively invisible against its own
near-white background.

internal/web/templates/calendar.templ: add eventTextColor(hex), which
darkens colors above a perceived-luminance threshold (keeping the hue,
e.g. white -> a mid gray, pale yellow -> olive) while leaving already-
legible colors untouched; used for both the event text and its leading
dot. Also add a subtle ring to the event dot and to the calendar legend's
color swatch so a white/near-white swatch stays visible against the
page's white background, independent of the text-color fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 23:27:19 +02:00
arnefandCopilot 213b22f821 Fix 500 on REPORT for recurring events with Windows timezone IDs
DAVx5 reported a 500 Internal Server Error on a time-range REPORT
against a calendar containing a recurring event whose DTSTART/DTEND used
a Windows-style TZID (e.g. "W. Europe Standard Time", as commonly written
by Outlook/Exchange and some Thunderbird/Lightning setups) instead of an
IANA zone name.

go-ical resolves TZID via a plain time.LoadLocation call, which only
understands IANA names. A simple decode of such an event succeeds (RRULE
dates aren't parsed eagerly), but expanding its recurrence - which
go-webdav's caldav.Filter does for every time-range REPORT - calls
Component.RecurrenceSet, which does call time.LoadLocation(tzid) and
fails with "ical: error parsing start time: unknown time zone ...".

Add internal/icalfix, a small shared helper that rewrites recognized
Windows timezone identifiers (TZID parameters and VTIMEZONE TZID: lines)
to their IANA equivalent in raw ICS bytes before decoding. Wire it into
every ical.NewDecoder call site: internal/caldav/backend.go's
decodeObject (fixes the reported bug), internal/web/calendar.go's event
rendering/ICS import, and internal/icssub's remote feed fetching, so a
subscribed feed with the same issue doesn't hit it either.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 22:23:11 +02:00
arnefandCopilot 1707ab4060 Add ICS/webcal HTTP subscription calendars
Users can now subscribe to a remote ICS/webcal feed from the dashboard
(name + color), the same way they set up a real calendar or the virtual
Birthdays calendar. Subscriptions are read-only, per-user, and share the
"/cal/home/<name>/" namespace with real calendars and "birthdays" (name
collisions are rejected in both directions).

- internal/db: new ics_subscriptions table + CRUD (internal/db/ics.go);
  CreateCalendarWithColor checks for a colliding subscription name.
- internal/icssub: shared HTTP-fetch + TTL cache (15 min) for remote ICS
  calendars, with webcal:// -> https:// rewriting and stale-on-error
  fallback, used by both the web UI and the CalDAV backend.
- internal/web: dashboard "Subscribe to an ICS/webcal calendar" form,
  color picker, delete button (internal/web/ics.go,
  templates/dashboard.templ); month view renders subscription events in
  their chosen color, read-only (internal/web/calendar.go).
- internal/caldav: subscriptions are exposed as read-only calendars
  (internal/caldav/ics.go) - listed in PROPFIND, events served via
  GET/REPORT, PUT/DELETE on individual events rejected with 403, but
  DELETE on the calendar itself unsubscribes; calendar-color is injected
  the same way as for real calendars and Birthdays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 22:10:49 +02:00
arnefandCopilot b451f1a76e Make birthdays calendar color configurable and expose it via CalDAV
- Add a per-user birthday_calendars table (color, cascade-deletes with
  the user) and GetBirthdayCalendarColor/SetBirthdayCalendarColor in
  internal/db, plus a reservedCalendarNames guard ("birthdays") in
  CreateCalendarWithColor so no real calendar can collide with the
  synthetic one, whether created via the web UI, nidusctl, or CalDAV
  MKCALENDAR.
- Add a "Birthdays" virtual resource card to the dashboard (color
  picker only, no delete/share controls) backed by a new
  ResourceCard.Virtual flag and POST /web/resources/birthdays/color
  handler.
- Extract the birthday-parsing/generation logic shared by the web
  calendar view and CalDAV into internal/birthdays (ParseBirthday,
  Collect, OccurrenceDate, Summary) instead of duplicating it.
- Expose the Birthdays calendar over real CalDAV in
  internal/caldav/backend.go + birthdays.go: it's always listed for
  every user, generates one VEVENT per (contact, year) for a rolling
  window (current year -2..+8) with "🎂 Name (Age)" titles, is
  read-only (Put/Delete/DeleteCalendar all return 403), and its
  Apple/DAVx5 calendar-color is injected from the same per-user
  setting used by the dashboard/web view.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 20:09:59 +02:00
arnefandCopilot cd96b365d0 Add virtual Birthdays calendar computed from contacts
The combined month view now always includes a read-only "Birthdays"
calendar (ref "@birthdays"), generated on the fly from every contact's
BDAY field across the user's address books — no calendar objects are
stored for it. Each birthday appears as an all-day event titled
"🎂 Name (Age)" (age omitted if BDAY has no year, e.g. "--MM-DD"), colored
pink, and links to the contact's edit page instead of an event editor.
Handles both "YYYY-MM-DD"/"YYYYMMDD" and year-less vCard BDAY formats,
and skips Feb 29 birthdays in non-leap years.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 19:27:55 +02:00
arnefandCopilot adabfd22ca Add ICS import/export and shared calendar support to calendar UI
- Combine own and shared calendars into a single /web/calendar month
  view, each event colored per its source calendar.
- New event creation now uses a calendar <select> (only writable
  calendars offered); editing keeps the event's original calendar fixed.
- Shared calendars use an "owner~name" reference in URLs, resolved via
  resolveCalRef which also enforces read/write permissions from
  calendar_shares (read-only shares can view/export but not edit/delete).
- Add per-calendar ICS export (single event and export-all) and import,
  splitting multi-VEVENT uploads into individual stored objects while
  preserving VTIMEZONE definitions and source VERSION/PRODID.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 18:58:04 +02:00
arnefandCopilot cd2cfa6c06 Add web UI for CalDAV calendars (web/calendar)
- List of the user's own calendars (web/calendar)
- Month view per calendar with a Monday-first 6-week grid, showing
  all-day and timed events, colored dot matching the calendar's color
- Navigation between months (prev/next/today) via query params
- Create/edit/delete events: title, description, location, all-day
  toggle, start/end date+time
- Clicking a day cell prefills a new all-day event on that date; the
  "New event" button defaults to a one-hour slot starting next full hour
- Deleting an event or invalid time ranges (end before/equal start) are
  validated with inline error messages
- Verified round-trip with the real CalDAV protocol handler (events
  created via the web UI are correctly visible to a REPORT query)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 16:58:59 +02:00
arnefandCopilot 3fa0297715 Add web UI for WebDAV files and CardDAV contacts
Files browser (web/files):
- Alphabetical listing grouped folders-then-files
- Upload via button or drag-and-drop, including whole folders
- Folder creation
- Fixed layout for long filenames without spaces (table-fixed + break-all)

Contacts (web/contacts):
- Full CRUD for CardDAV contacts (create/edit/delete)
- VCF import (multi-card files) and export (single/all)
- Photo upload with preview, birthday field
- TYPE labels (private/business) for phone, email, address
- Repeatable multi-input rows for phones/emails/addresses instead of textareas
- Sanitizes a known malformed TYPE parameter pattern from some vCard
  exporters (e.g. Nextcloud Contacts) that otherwise caused phone numbers
  to be silently dropped on import

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 15:06:39 +02:00
arnefandCopilot ebbc7a2a2b Add calendar color support (DAVx5 calendar-color)
Calendars can now have a color (hex, e.g. #3b82f6) that DAVx5 and other
CalDAV clients pick up via the Apple/dav4jvm calendar-color property.

- db: add calendars.color column with migration for existing DBs;
  CreateCalendarWithColor, SetCalendarColor, GetCalendarColor;
  ListCalendars now returns []Calendar{Name, Color} instead of []string
- caldav: since go-webdav's caldav.Backend interface has no extension
  point for vendor properties, wrap the handler with a response-rewriting
  middleware that injects <calendar-color xmlns="http://apple.com/ns/ical/">
  into PROPFIND responses for calendars that have a color set
- web: color picker on the "New calendar" form and an inline color swatch/
  picker on each calendar card (calendars only, not address books)
- nidusctl: `calendar create --color` flag and a new `calendar color`
  subcommand; `calendar list` now also prints the color if set

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-20 06:43:56 +02:00
arnefandCopilot 37399d1ceb Stack dashboard form fields on mobile instead of breaking layout
The new-calendar/new-address-book creation forms and the per-resource
share form used flex-row layouts that overflowed on narrow viewports.
Switch to flex-col on mobile, flex-row from sm: up, with inputs/buttons
going full-width below the sm breakpoint.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 12:57:05 +02:00
arnefandCopilot 7d4f28de3c Move users, calendars, and address books from config.yaml into the database
BREAKING CHANGE: the users:/config-based collection setup is gone. All
user, calendar, and address-book data now lives in the SQLite DB
(internal/db) and is managed exclusively via nidusctl or the web UI.
Existing deployments must recreate their users after upgrading:
  nidusctl user create <username>
  nidusctl calendar create <username> <name>
  nidusctl addressbook create <username> <name>

- internal/db: new users, calendars, addressbooks tables with FK cascade
  delete; foreign_keys pragma enabled; internal/db/users.go implements
  full CRUD + bcrypt auth (CreateUser, VerifyPassword, ListUsers,
  CreateCalendar/AddressBook, etc).
- internal/config: removed Users/UserConfig entirely.
- internal/auth: Basic Auth now checks credentials via db.DB instead of
  cfg.Users.
- internal/caldav, internal/carddav: ListCalendars/ListAddressBooks and
  Create/Delete now backed by the DB.
- internal/web: login uses db.VerifyPassword; new resources.go adds
  create/delete handlers for calendars/address books at
  /web/resources/{calendar,addressbook}; dashboard gained create forms
  and per-card delete buttons (templ + htmx, no hyperscript).
- tools/nidusctl: new user create/delete/list/passwd commands (masked
  interactive password prompt via golang.org/x/term) plus create/delete/
  list subcommands for calendar/addressbook.
- cmd/server/main.go: pre-creates on-disk collections from the DB at
  startup instead of cfg.Users; warns when no users exist yet.
- Updated tests to seed data via the DB; added resources_test.go for the
  new web UI handlers.
- README.md and .github/copilot-instructions.md updated to document the
  new nidusctl commands and the DB-backed architecture.

Verified end-to-end against a live test server: nidusctl user/calendar/
addressbook create, DAV Basic Auth PROPFIND, web login, dashboard
rendering, and web UI create/delete of resources all confirmed working.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 12:43:34 +02:00
arnefandCopilot 3dc49b6b22 Add password-visibility toggle to login page (TypeScript)
- web/ts/login.ts: small vanilla TS module toggling the password input's
  type between 'password'/'text' via a 'Show/Hide' button, so users can
  rule out typos before submitting. Compiled to web/static/login.js
  (ES module) via tsc (web/tsconfig.json, new 'make web-ts'/'web-assets'
  Makefile targets) and loaded via <script type="module">. Compiled JS
  is committed/embedded the same way as the compiled CSS — no Node.js
  needed at runtime.
- Verified end-to-end against the live proxied server
  (https://local.unqr.dev/web/login): POST /web/login correctly returns
  303 + Set-Cookie, and the session then authorizes GET /web/ — the
  server-side login flow is confirmed working correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 10:13:37 +02:00
arnefandCopilot 4dba55c807 Move web UI from /ui/ to /web/ path prefix
The web UI is now mounted at /web/ (previously /ui/) — cmd/server/main.go
wraps web.Server.Handler with http.StripPrefix("/web", ...), so
internal/web's own routes stay unprefixed (/, /login, /logout,
/shares/..., /static/...) and only the outer mux adds the prefix. All
templates, redirects, and cookie paths updated accordingly. The root '/'
route reverts to the original unauthenticated welcome page (linking to
/web/), and /cal/, /card/, /files/ are unaffected.

Also gitignore the bin/ build output directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 07:29:32 +02:00
arnefandCopilot ab3c7f44d5 Add web UI: login, dashboard, and share management (templ + Tailwind + htmx)
New internal/web package mounted at /ui/, separate from DAV Basic Auth:

- Cookie-based sessions (opaque random tokens in a new web_sessions
  SQLite table, internal/db/sessions.go), checked against the same
  cfg.Users/bcrypt credentials as DAV Basic Auth.
- Dashboard listing the logged-in user's own calendars/address books,
  who they're shared with, and what's shared with them.
- Share/unshare directly from the dashboard, updated in place via htmx
  partial swaps (POST to create/update, DELETE to revoke). Always
  verifies the resource actually belongs to the logged-in user before
  granting a share.
- Templates written in templ (internal/web/templates/*.templ, generated
  *_templ.go committed), styled with Tailwind CSS v4 (web/input.css,
  compiled to web/static/app.css), with htmx vendored as a static file
  for the dynamic bits. Both are embedded into the binary at build time
  (web/staticassets.go) so the compiled server has no Node.js/web/
  runtime dependency.
- Wired into cmd/server/main.go at /ui/ alongside the existing /cal/,
  /card/, /files/ routes; welcome page links to it.
- Tests: internal/web/server_test.go covers login success/failure, the
  login-required redirect, dashboard rendering, share/unshare including
  the htmx-v2-sends-DELETE-params-as-query-string quirk, and rejecting
  shares of resources the user doesn't own.
- Docs: README (new 'Web UI' section, updated sharing section, project
  layout, dependencies) and copilot-instructions updated accordingly.
  Makefile: new templ-generate/web-deps/web-css targets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-08-19 07:12:56 +02:00