Skip to content

feat: folder picker UI for "Scan Specific Path" modal#56

Merged
johnpc merged 2 commits into
johnpc:mainfrom
klamouri:feat/scan-path-folder-picker
May 24, 2026
Merged

feat: folder picker UI for "Scan Specific Path" modal#56
johnpc merged 2 commits into
johnpc:mainfrom
klamouri:feat/scan-path-folder-picker

Conversation

@klamouri
Copy link
Copy Markdown
Contributor

Why

Typing absolute container paths into a textarea is annoying. If I have to hand-construct /movies/Action/2024 somewhere else first, I might as well skip the UI and call the CLI. The modal needed to be something worth using.

What changed

Replaces the textarea in the Scan Specific Paths modal with a clickable folder picker. Backend adds a single GET /api/browse endpoint, scoped to the configured SCAN_PATHS.

Before

before-02-modal

After

Opening the modal lists the configured SCAN_PATHS as collapsed roots.
after-01-modal-empty

Expanding a folder lazy-loads its children. Folders show a chevron, folder icon, and an Add button on hover. Files appear for context as display-only rows (file icon, dimmed, no +) — the sync engine still only operates on directories, but seeing what's inside helps confirm you're picking the right folder.
after-02-modal-expanded

+ pushes the folder into the chip list. × removes it.
after-03-modal-selected

Works across multiple watched roots — one pick from each:
after-05-multi-root

Long paths get truncated with . Hovering a chip expands it inline to show the full path:
after-06-hover-tooltip

Design decisions

  • Backend scoped to SCAN_PATHS, not the whole filesystem. GET /api/browse resolves both the requested path and the configured roots through realpath, and rejects anything that isn't a descendant of one of those roots. The operator already trusts SCAN_PATHS — reusing it as the picker's scope avoided shipping a general-purpose filesystem-listing endpoint that could leak directory structure if the container were ever exposed. Also keeps the feature useful even when CRON_SCHEDULE=disabled — the watched folders alone drive the picker.

  • No third-party library. The dominant vanilla-JS tree widget (jsTree) requires jQuery, and this project has zero frontend dependencies and no build step. Adding two CDN scripts (jQuery + jsTree) for one modal wasn't worth it. The picker is ~120 LOC of vanilla JS in public/app.js, written in the same style as the rest of the file.

  • Files shown but not selectable. Initial pass listed only directories, which made it hard to tell if a folder was the right one to add. Showing files as display-only rows gives the same context Finder/Explorer does, without changing what the sync engine operates on.

  • Inline chip expansion on hover. Native title tooltips have a ~1s delay. A floating CSS tooltip kept getting clipped by the chip list's overflow: auto. Expanding the chip text inline on :hover avoids the clip entirely and feels more direct.

  • Request path preserved in the response. realpath is used internally for the security check, but the endpoint returns the path as the user supplied it. So a click on /tmp/subsyncarr/tree-picker shows up in the chip list as /tmp/..., not /private/tmp/... (macOS /tmp symlink). Operationally invisible in Docker, just less surprising during local dev.

AI disclosure

Built with Claude Code; this PR description was also drafted with Claude. I didn't see anything in the contribution guidelines against AI-assisted contributions — happy to discuss if that's not the case. The feature commit has a Co-Authored-By trailer making the tool use explicit. I designed the changes (scope, library choice, UX behavior); the AI executed against my direction. I stand behind every line.


🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

klamouri and others added 2 commits May 23, 2026 23:57
Adds .idea/ (IntelliJ), CLAUDE.local.md, and .claude/CLAUDE.local.md
so personal IDE state and local-only AI prompt overrides don't leak
into the repo. The shared CLAUDE.md / .claude/ stay local-only via
.git/info/exclude rather than .gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the free-text textarea in the Scan Specific Paths modal with
a clickable folder tree. Users browse the configured SCAN_PATHS roots,
expand subdirectories, and pick paths via a + button into a chip list.

Backend: new GET /api/browse?path=<abs> in src/server.ts. Returns the
configured SCAN_PATHS as virtual top-level entries when no path is
given, otherwise enumerates immediate children of the requested path.
Refuses any path that does not resolve (via realpath, on both the
request and the configured roots) inside one of the configured roots,
so the endpoint cannot be coaxed into leaking arbitrary filesystem
structure. Returns the user-supplied path in the response rather than
the realpath so the UI shows paths as the user clicked them.

Frontend: hand-rolled in vanilla JS to match the project's existing
no-build, no-deps frontend style — no jQuery, no tree-widget library.
Lazy-loads children on expand. Folder rows have a chevron, folder
icon, and + button (visible on hover). File rows are shown for context
but are display-only — file icon, dimmed text, no chevron, no + (the
sync engine still operates on directories, this just lets users see
what's inside before picking). Selected paths render as a chip list
with × to remove. Hovering a chip expands it inline so the full path
is visible even when truncated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@johnpc
Copy link
Copy Markdown
Owner

johnpc commented May 24, 2026

This is awesome, thanks for the contribution !

@johnpc johnpc merged commit 631f633 into johnpc:main May 24, 2026
@johnpc
Copy link
Copy Markdown
Owner

johnpc commented May 24, 2026

This is now available in v1.2.16 on Docker Hub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants