PR 3 of the schedule-types feature — see
``memory/project_auto_sync_schedule_types.md``. Backend
``next_run_at`` + ``weekly_time`` trigger handler landed in PRs 1-2.
This PR exposes them in the Auto-Sync manager so users can finally
schedule playlists by day-of-week + time instead of only hourly
intervals.
**UI layout:**
The Auto-Sync modal grows a ``Weekly Board`` tab between
``Hourly Board`` (renamed from ``Schedule Board``) and
``Automation Pipelines``. Same sidebar (mirrored playlists grouped
by source, with filter). Main panel is 7 day columns Mon-Sun
instead of 10 hour buckets. Drag a playlist onto a day column →
creates a single-day weekly schedule at the default time
(09:00 in the browser's IANA tz from
``Intl.DateTimeFormat().resolvedOptions().timeZone``). Click any
scheduled card → opens an editor popover for time, multi-day
toggles, tz override, and unschedule.
Multi-day schedules render under every matching column (Mon-Wed-Fri
schedule appears as three cards, one per column) — matches how
users think about "this playlist runs on Mon AND Wed AND Fri".
**Mutual exclusion:** one schedule per playlist. The save path on
either tab deletes any existing schedule of the OTHER kind before
installing the new one. Backend can technically run both as two
separate automation rows, but two cards under the same playlist
would surprise users and the engine has no merge semantic for
"daily-and-hourly".
**Pure-function helpers** (testable via node:test, matching the
existing ``tests/static/test_auto_sync.mjs`` pattern):
- ``detectBrowserTimezone()`` — Intl tz with UTC fallback for
browsers where Intl is absent.
- ``autoSyncWeeklyTrigger({time, days, tz})`` — defensive payload
builder: garbage time → 09:00, unrecognised days dropped,
missing tz → browser tz.
- ``autoSyncWeeklyFromTrigger(config)`` — inverse parser with
the same defensive shape. Empty days expands to every weekday
(matches ``next_run_at`` engine semantic). Returns null for
non-object configs so ``buildAutoSyncScheduleState`` can route
broken rows to automationPipelines instead of silently
bucketing them as every-day weekly.
- ``autoSyncWeeklyLabel(parsed)`` — sorted "Mon, Wed, Fri @
09:00" / collapses to "Daily @ HH:MM" for full-week / "Unscheduled"
for null. Canonical Mon-Sun ordering regardless of input order.
**Tests:** 26 new node:test cases across ``detectBrowserTimezone``
x1, ``autoSyncWeeklyTrigger`` x6, ``autoSyncWeeklyFromTrigger`` x6,
``autoSyncWeeklyLabel`` x5, and ``buildAutoSyncScheduleState``
weekly bucketing x5 (covering owned weekly_time → weeklySchedules,
hourly stays in playlistSchedules, non-owned falls through to
automationPipelines, legacy-named auto-sync rows still recognised,
garbage trigger_config falls through). All 62 node:test cases pass;
261 across the automation pytest suite still green (zero regression
on PRs 1-2's plumbing). Python wrapper at
``tests/test_auto_sync_js.py`` shells out cleanly.
**CSS** (themed to the existing Auto-Sync gradient + accent
variables):
- 7-column grid for the weekly board, narrower than the 10
hour-bucket layout.
- Editor popover with backdrop-blur, accent-tinted save / delete
buttons, hover states that pick up the user's accent color.
- ``scheduled-elsewhere`` state for playlists with an hourly
schedule visible on the weekly board (dashed border + opacity)
so the user knows a drop will replace, not stack.
**WHATS_NEW entry** under 2.6.3 unreleased — first user-visible
slice of the schedule-types feature.
PR 4 (Monthly UI tab) deferred until weekly proves wanted.
|
||
|---|---|---|
| .. | ||
| docs/migration | ||
| src | ||
| static | ||
| tests | ||
| .gitignore | ||
| .oxfmtrc.json | ||
| .oxlintrc.json | ||
| index.html | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| README.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.setup.ts | ||
WebUI Hybrid Rendering
SoulSync's web UI is in a transition phase:
- most pages still render through the legacy vanilla JS shell
/issuesis rendered by the new React app- a small shell bridge keeps both runtimes aware of the active page, profile context, and navigation state
How It Fits Together
flowchart LR
Browser["Browser parses /webui/index.html"]
Legacy["Legacy shell scripts\n(core.js -> ... -> init.js)"]
Bridge["shell-bridge.js\nwindow.SoulSyncWebShellBridge"]
React["Vite React app\nsrc/app/main.tsx"]
Router["TanStack Router\nwindow.SoulSyncWebRouter"]
Browser --> Legacy
Browser --> React
Legacy --> Bridge
React --> Router
Router --> Bridge
Bridge --> Legacy
Runtime Roles
-
webui/static/init.js- boots the legacy shell
- selects the active profile
- handles the legacy page loading flow
-
webui/static/shell-bridge.js- owns the browser-side bridge object
- exposes
window.SoulSyncWebShellBridge - owns the shared page chrome and route handoff helpers
-
webui/src/app/main.tsx- mounts the React app
- binds
window.SoulSyncWebRouter
-
webui/src/platform/shell/route-controllers.tsx- listens for bridge readiness
- keeps React pages aligned with the shell
Load Order
The current order in index.html matters:
- legacy shell scripts load first
init.jssets up the shell runtimeshell-bridge.jspublishes the bridge and shared chrome helpers after the shell state exists- the Vite React app is injected through
{{ vite_assets('body') }}and boots as a module after parsing
That order avoids load-time references to missing globals and keeps the React side able to react to bridge readiness events. The React entry can start fetching early, but the shell bridge and legacy globals are already available by the time the React runtime starts acting on them.
Notes
- The bridge is intentionally small and browser-only.
- This is the start of the migration, not a full replacement of the legacy shell.
- When adding another React page, check whether it needs:
- a route entry in
webui/src/platform/shell/route-manifest.ts - bridge typings in
webui/src/platform/shell/globals.d.ts - a legacy fallback path in
webui/static/init.js - bridge glue or handoff logic in
webui/static/shell-bridge.js
- a route entry in
Folder Layout
The React webui uses a small set of predictable folders so route slices stay easy to extend, test, and understand.
webui/src/
app/ React bootstrap, router, query client, shared API client
components/ Shared UI primitives
platform/ Shell bridge and browser/platform integration
routes/ Route-local code and TanStack Router pages
test/ Shared test utilities and setup helpers
Migration planning docs live under webui/docs/migration/.
- keep the high-level route backlog there
- add one route-specific sketch per migration task
- keep migration notes close to the WebUI code rather than the repo root
Route Slices
- Keep route-specific code inside
webui/src/routes/<route>/. - Put the routing entry in
route.tsx. - Put route-local UI in a
-ui/folder. - Prefix non-routing files with
-so TanStack Router ignores them. - Keep the route slice small and cohesive.
- Prefer a few files with clear responsibilities over many tiny files with overlapping names.
Example:
webui/src/routes/issues/
route.tsx
-issues.types.ts
-issues.api.ts
-issues.helpers.ts
-issues.api.test.ts
-issues.helpers.test.ts
-ui/
issues-page.tsx
issue-detail-modal.tsx
issue-domain-host.tsx
The initial issues slice is the model to follow:
-issues.api.tsholds request code and query options-issues.helpers.tsholds pure normalization and formatting-issues.types.tsholds shared types-ui/holds the page, modal, and legacy handoff UI
Shared Code
- Put reusable UI in
webui/src/components/. - Put shell integration in
webui/src/platform/. - Put bootstrap and app-wide wiring in
webui/src/app/. - Move code up a level only when it is genuinely shared.
- Avoid creating new conventions that overlap with existing ones.
Testing Choices
We have a lot of testing tools available, but we do not need all of them for every feature.
- Use plain unit tests for pure functions and small transforms.
- Use React component or route tests when the behavior lives in the UI or router.
- Use MSW-backed tests when request shape, response handling, or error handling matters.
- Use Playwright when the behavior is best proven end-to-end with the server and browser together.
- Prefer the smallest test setup that still proves the thing that can regress.
Development
The repo root now owns the full local-dev instructions. Start there for the portable launcher and backend/frontend setup:
- README.md for the end-to-end dev flow
npm run checkandnpm run fixfor React-side linting and formatting