Commit Graph
739 Commits
Author SHA1 Message Date
Jamie Pine 7b6f8ac9fb Size the scope toggle to the pill row
ToggleGroup gained size variants on the SelectPill scale in spaceui
(cd42fd2 there); the search bar's scope control uses sm so the whole
refinement row sits on one h-7 baseline. Requires @spacedrive/
primitives 0.2.4; the workspace is bun-linked to the local package
until that publishes.
2026-09-15 22:25:53 -07:00
Jamie Pine 3f4b671008 Recompose the search bar from primitives
The refinement bar is now one quiet row of @spacedrive/primitives: a
ToggleGroup for scope, SelectPill popovers for kind, size, modified
date, and extension, a Switch for hidden files, the true result count,
and a subtle Clear button. Pills carry their own state in their label
and tint accent while filtering, so the separate Filters toggle, the
badge, and the second panel row are gone; an active filter is legible
without opening anything. The kind popover keeps facet counts and
stays open for multi-select. Colors move from the sidebar palette to
the app palette the bar actually sits in, and the popover composition
follows the SelectPill/OptionList idiom the composer established.
2026-09-15 22:13:02 -07:00
Jamie Pine 81accc3ad5 Build the search refinement bar with working filters
Phase 3 of docs/plans/2026-09-15-search-refinement.md. The Filters
button opens a panel whose every control maps onto a wire filter the
daemon answers: kind, extension, size presets, modified-date presets,
and a hidden-files toggle. Active filters badge the button, and the
toolbar shows the true result count from total_found.

Facets are now folded over each partition's full filtered match set
before narrowing, so a facet count describes the whole match set
rather than one page, and they gain a content-kind map keyed by the
wire spelling so the panel can echo a facet key straight back as a
content_types value. The search functions return one SearchPage
carrying results, total, and facets.

Filter state is stored in wire shape through the previously dead
searchFilters context state; the parallel camelCase SearchFilters
interface that nothing ever read is gone, and useExplorerFiles sends
the live filters instead of a hardcoded all-null literal. Verified
live: an uppercase extension filter matches lowercase files with the
true filtered total.
2026-09-15 21:46:51 -07:00
Jamie Pine 57d969ce3a Add inset window layout with floating panels as an option
From a parallel session. Inset content is the default: navigation sits
on the window background and the explorer renders as an inset sheet.
Settings > Appearance > Layout retains the floating-panels mode. Both
modes share mounted content and stable preview hosts through a shell
geometry context, the layout preference propagates to other windows on
the same origin, and the sidebar footer returns to circular Activity
and Settings controls with the activity count and busy indicator.
Preview and Size view follow the sheet bounds, with Size thumbnails in
container coordinates.
2026-09-15 21:37:46 -07:00
Jamie Pine 25fd868a2f Add path-scoped thumbnail generation
From a parallel session. thumbs.generate accepts a file or an indexed
folder with missing, stale, and force modes, runs in resumable batches,
and versions video tiles against host FFmpeg availability so a tile
regenerated with better tooling replaces its predecessor in place. File
and folder context menus and the new sd thumbs CLI domain call the same
operation. paths.context now reports whether the exact path has a
committed source record, and the path bar surfaces it with a refresh
affordance. Coverage and resume semantics are documented in
docs/core/thumbnail-generation.mdx.
2026-09-15 21:37:35 -07:00
Jamie Pine 7733fe8a98 Replace the Location scope with a working Source scope
The middle scope button said Location, had no wire representation, and
fell through to Library, so two of the three buttons did the same
thing. Locations are pins now and pins do not define search domains; a
source does.

The button reads the containing source from paths.context, shows the
source's name, and disables itself when nothing contains the current
path. Picking it searches the source's whole subtree through the
existing Path scope with the daemon-normalized source root and the
current device's slug, so it works identically on replicas browsed
through their owning device. Source scope with an unresolved source
sends no query rather than silently widening to the library.
2026-09-15 21:34:18 -07:00
Jamie Pine f34fc3c8ae Debounce search for real and stop resetting the picked scope
The search input's debounce returned a cleanup closure from an onChange
handler that nothing ever invoked, so typing a six-letter word fired
five separate searches 300ms apart, and each dispatch re-entered search
mode with the default scope, silently reverting a Library or Location
selection to This Folder on the next keystroke.

An effect owns the timer now: each keystroke cancels the previous one,
one search fires per settled query, and entering search mode carries
the scope already picked. Dropping below the minimum length exits
search deterministically, including the backspace to one character
that previously left stale two-character results on screen.

The minimum query length lives in one exported constant instead of
three synchronized literals, and Cmd+F finally does something: the
already-defined focusSearchBar keybind expands and focuses the search
bar through a handle on ExpandableSearchButton.

Per-tab search state and search as a navigation target remain with the
search refinement plan's Phase 2; they touch the shell layout another
session is actively changing.
2026-09-15 21:28:10 -07:00
Jamie Pine ed46af4f10 Show paired devices' replica sources on the Storage page
sources.list has carried peer replicas since 3ac1f0af7, but the Storage
page filtered the response to adapter sources and rendered filesystem
sources from the local ephemeral registry only, so synced sources fell
between the two queries. Replicas now render with their own card in the
same language as local drives: the owning device as the subtitle, a
replica badge, item count and size, and the sync age. Opening one routes
the explorer through the owning device's slug, which serves the listing
from the replicated index, same as the sidebar rows. devices.list gains
show_paired so the slug lookup can see paired devices at all.
2026-09-15 19:26:51 -07:00
Jamie Pine b653676e80 Docs, register, and interface polish from parallel sessions
README and CONTRIBUTING updates, the working register brought current,
locations and data-model pages reconciled, redundancy and overview
polish, media view tweaks, regenerated schemas, and the new Spacedrive
icon asset.
2026-09-15 17:09:40 -07:00
Jamie Pine c32768f2e4 Make startup discovery explicit and permission-aware
Library loading no longer dispatches a filesystem discovery pass. The
desktop requests one idempotent pass after the connected shell
renders, a bare daemon initiates nothing, and sd index start
--defaults gives CLI and server operators the same explicit path.
Restored volume snapshots prevent repeat full walks.
2026-09-15 17:09:40 -07:00
Jamie Pine 4e53d58dbb Keep baked thumbnails through remounts, fix Quick Preview originals
Hot-tier bake completion epochs survive virtualized cell unmounts, and
a thumbnail hides its fallback only after the current image element
loads its current URL. The Tauri asset boundary maps canonical macOS
APFS data-volume paths back to the scoped /Users and /Volumes aliases
so originals load without a 403, with tests over the alias mapping.
2026-09-15 17:09:30 -07:00
Jamie Pine f3a1ede2f8 Discover host media tools, bake video thumbstrips on demand
A machine-scoped registry discovers host FFmpeg and companion tools,
surfaced in Services settings. The thumbnail service uses them for
video posters and on-demand scrub sheets: thumbstrips bake per request
into the volume hot tier and serve over the local HTTP servers with
immutable content-versioned URLs, wired to the timeline scrubber.
2026-09-15 17:09:30 -07:00
Jamie Pine cf5f3f45a3 Add paths.context and finish demoting locations
The path bar reads a typed path-context query backed by the daemon's
source, volume map, watcher, persistence, and pin state, replacing the
location/index-mode indicator. Containment resolves on the daemon with
path components after volume alias normalization; a pin no longer
implies indexing or watching, and the docs say so.
2026-09-15 17:09:17 -07:00
Jamie Pine 2097fc1410 Simplify the sidebar around one library scope
One library scope, flat daily destinations, Places computed from
system folders without persisting them as locations, canonical rows,
and an Activity/Settings footer. Topology groups retire; their pins
survive seed convergence. Replica sources render under their owning
device and route into the explorer with that device's slug.
2026-09-15 17:09:17 -07:00
Jamie Pine 9d948817e7 Request paired devices explicitly in overview and sidebar
Peer device cards only appeared when a connect or disconnect event
pushed the device into the normalized cache, which the presence
flapping supplied constantly. With presence stable, a fresh page never
learned the peer existed. Both views now pass show_paired so paired
network devices come back with the device list itself, and the sidebar
gets the owning device's slug for routing replica sources.
2026-09-15 17:04:16 -07:00
Jamie Pine aa589a767b Stop device presence flapping, recognize Tailscale paths
Every closed connection fired ConnectionLost for its device, but sync
and remote ops open a connection per request and drop it, so paired
devices cycled offline/online on every 30-second peer refresh. A loss
now prunes closed connections and marks the device disconnected only
when none remain — the durable job-activity stream keeps presence
honest. devices.list also treats the registry's Connected state as
connected when iroh's path watcher momentarily reads None.

Direct paths on Tailscale's CGNAT range (100.64/10, fd7a:115c:a1e0::)
classify as a new Tailscale connection method with its own badge,
instead of reading as generic internet. Verified live from titan: 20
consecutive samples online, alternating LocalNetwork/Tailscale as iroh
picks between the LAN and tailnet routes.
2026-09-15 16:27:13 -07:00
Jamie Pine 5f49a0141e Report the same fleet-wide statistics on every device
Three layers of wrong in the overview numbers, now fixed:

The daemon shelled out to bare zfs/zpool, which a non-login launch
(TrueNAS, systemd) does not have on PATH, so the pool-root capacity
correction never ran and a 68 TB pool reported 5.6 TB with 0 B used.
Binaries now resolve against /sbin and /usr/sbin. Statistics also
overlay live volume-manager capacity over the persisted rows, which
froze at track time, and a re-sighted volume refreshes its row's
capacity. The duplicated volume-capacity implementation now delegates
to one static body.

The Files Indexed card added source counts to library statistics that
were already derived from the same source rows, roughly doubling every
number. It reads the statistics alone now.

Each device answers a byterange DeviceSummary with its own statistics,
peers cache it on every sync, and libraries.info adds the cached
summaries at read time. Every device reports the same fleet totals,
computed once by the machine that owns the data.
2026-09-15 15:52:01 -07:00
Jamie Pine 3ac1f0af78 Surface peer replicas in sources.list and serve remote listings
sources.list now appends paired devices' sources from the peer-mount
replica set, attributed with the owning device's id and label and
marked status "replica". The overview device card renders them, so a
paired NAS shows its sources instead of "No volumes".

directory_listing serves paths on another device straight from that
device's replicated index: same conversion as the local cached path
(extracted into shared helpers), no indexer dispatch, since indexing
happens on the owner. Verified from titan against the Mac's replica.

sync_device now holds a per-device lock; the refresh loop clears the
debounce before running, so a reconnect in that window was loading the
same snapshots twice in parallel.
2026-09-15 05:20:07 -07:00
Jamie Pine b1b0d28b72 Refresh project context and working registers
README rewritten for the 2.0 release framing. AGENTS.md points at
product-direction.mdx for the product boundary and makes
PROJECT_STATUS.md the project-wide working register (the entries
teardown table stays authoritative for that track). CLAUDE.md and
PROJECT_STATUS.md are now tracked. Spacebot VISION marked retired.
justfile gains dev-docs/build-docs and splits dev-desktop-watch out of
dev-desktop. Tauri gen schemas regenerated.
2026-09-15 00:59:41 -07:00
Jamie PineandClaude Fable 5 f545663e19 Compute the duplicate bar in the store's own bytes
ZFS compression makes apparent file sizes exceed allocated blocks, so
comparing the store's unique bytes against statfs usage clamped a
compressed dataset's duplicate segment to nothing. The volume now
carries indexed_bytes beside unique_bytes, both from the source rows,
and the bar draws the duplicate fraction of that pair scaled onto the
used segment: both sides of the ratio come from one ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-14 07:21:43 -07:00
Jamie PineandClaude Fable 5 672d472e1a Make the unique-bytes bar real
The overview bar's duplicate segment was Math.floor(used * 0.7): the
domain Volume had no unique_bytes field, nothing ever wrote the column,
and the two calculators queried entry tables that no longer exist.

The store now counts it where it is knowable: each distinct content
once, plus files with no identity yet, which are unique until proven
otherwise. The figure rides the existing rollup onto the source row,
volumes.list resolves it per volume (whole-volume source wins, subtree
sources sum), and the refresh path reads the same rows. A volume
nothing has indexed reports None and the bar shows plain usage, never
an estimate. Also adds the missing directory_count, content_count and
unique_bytes columns to the source upsert's update list, which only
ever updated counts on first insert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-14 01:37:21 -07:00
Jamie PineandClaude Opus 5 59520ee5e9 Demote a location to a pin
A location was fifteen fields rooted at an entry. It is six now: a name, the
source holding it, a path relative to that source's root, and whether someone
put it there. It owns nothing, so unpinning is a DELETE of one row and the
volume index does not notice.

Everything dropped lives somewhere that already had it, and mostly had it
better. scan_state and last_scan_at are the source's. total_size and file_count
come off the arena's rollups, which are current rather than as of the last scan
and can say a subtree was summarised where a stored integer cannot. index_mode
and job_policies configured an indexer and a job pipeline that are both gone.
entry_id is the ownership this phase exists to remove.

The path is relative so a pin survives its drive mounting somewhere else, which
is the whole reason a source carries a root and a location does not. Sources
nest, so the innermost one containing the path wins.

IndexMode goes with it. It was set on every IndexerJobConfig, threaded through
the walk, and never read: the phase that consulted it was deleted with the
persistent indexer, and content identity is its own job now. Four CLI flags and
two modals were choosing between values nothing looked at.

No rows carry over. Every one was anchored to an entry_id in a table this
series removes. The five folders a person recognises are written at library
creation instead, which is why there is no longer anything to add: Home,
Desktop, Documents, Downloads and Pictures are simply there.

Three more things fell out. File::from_entry_uuids and its siblings became
File::at_path, for_record and announce, which is the pattern five call sites
had each written by hand. SdPath::Content resolves through copies_of_content
rather than joining entries to locations to devices, so it finds a copy on any
drive rather than only where a location happened to be. And the peer file
protocols allow source roots instead of location paths, which reaches at least
as far and costs no query.

One thing removed rather than ported: user_metadata_tag checked that a
file-scoped tag came from the device owning the entry, via the location holding
it. A record belongs to whichever machine walked it and carries no device
column, so that check wants re-deriving when sync returns on top of sources
rather than being rewritten to always pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 01:02:04 -07:00
Jamie PineandClaude Opus 5 f32da47771 Tag the bytes or tag the file, and mean it either way
user_metadata already had the right shape: entry_uuid or content_identity_uuid,
exactly one set. Nothing needed redesigning. The uuids just start meaning the
record the volume index minted and the content uuid the source store derives
from the bytes, and the joins that went through the entry table go through the
arena instead.

Tags stay in library.db, which is right rather than a compromise. A tag is a
claim by a person, so it syncs and travels with the library. That is the
opposite of a sidecar, which is a property of the bytes and belongs beside them.

TagTargets loses a variant it cannot have. Entry(Vec<i32>) addressed rows by
database id and there are no rows and no ids; the client never sent it. What is
left is Content and File, which is the real distinction: tag the bytes and reach
every copy on every drive, or tag this one. The client already chose content
when the file had been identified and fell back otherwise, so the policy was
already right and only the names were wrong.

ApplyToTargets was a second copy of the same enum with the same three variants.
tags.create takes TagTargets now, and both inputs share one validate.

Three helpers landed on the cache because four call sites wanted them:
content_of, copies_of_content and path_of_record. alternate_instances and
files.by_id had hand-rolled versions of the first and last; they use these now
and lost 100 lines between them.

Content-scoped changes emit events for every copy of the bytes, not just the
one that was named. Tagging a photo on one drive updates the row showing it on
another.

Two dead things fell out: find_entries_by_semantic_tags and find_files_by_tags,
both returning Vec<i32> entry ids, neither called.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 23:38:20 -07:00
Jamie PineandClaude Opus 5 ba8efcdda5 Delete the media pipeline that nothing could reach
Deleting locations.triggerJob cut the last live dispatcher for every media
job. What was left was six jobs, six processors, six registered actions and
an 853-line SidecarManager, all keyed on entry rows nothing writes, reachable
from nothing. The UI buttons that fired them were the only thing keeping the
registrations honest, and they were firing into a pipeline that had no input.

The hot tier is unaffected. ThumbService over sources/<id>/thumbs.pvcache is
the live thumbnail path, it is source-scoped already, and it shares no code
with any of this.

Two things survive because they outlive their writers. SidecarKind, variant,
format and status move to domain::sidecar: they are the vocabulary a derived
artifact is addressed by, they are what SdPath::Sidecar parses, and
content_sidecar will be keyed on the same three. And File.sidecars keeps its
shape, empty until something fills it, so the subtitle and thumbstrip lookups
in the client keep compiling.

The EXIF and ffmpeg extraction went with the rest. It read into
image_media_data and video_media_data, which are entity rows on their way
out, and its field mapping is a straight match for the facets declared in
df965b851. When the facet writer gets built it wants rewriting against those
columns rather than resurrecting; git show df965b851~1:core/src/ops/media/
metadata_extractor.rs has the EXIF handling worth copying.

sd-server's sidecar route now builds its path through sd-sidecar-path, the
same way the tauri one already did. Both still serve from <library>/sidecars,
both are equally empty, and L5 re-roots them together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 20:30:06 -07:00
Jamie PineandClaude Opus 5 9d5465832f Stop writing entries
Nothing produces a row in `entry` after this. The walk fills the volume
index and its source store, which is what the arena has been doing since
the storage map landed; the persistent half of the indexer was a second
writer to a second store that no source ever read.

Gone: IndexPersistence, and with it the branch in every phase. The state
machine is Discovery then Processing, since Aggregation existed to bubble
directory sizes the arena now maintains incrementally, and
ContentIdentification is ContentIdentityJob's job against the source store.
DatabaseAdapter, PersistentEventHandler and the location watching that fed
them go too, so ArenaWriter is the only change handler and the walk and the
watcher differ only in Notify.

DatabaseStorage was 1230 lines of entry writes wrapped around three things
the arena actually uses. Those are ops::indexing::metadata now: EntryMetadata,
extract_metadata and get_inode, plus is_hidden_path. delete_subtree moves onto
the entry entity, which is what it operates on and what it dies with.

locations.rescan, locations.enable_indexing and locations.triggerJob dispatched
the deleted indexer or its media jobs, so the ops and their UI go. Reindex and
Run Now are gone from the inspector; the job policy toggles stay, since they
outlive the ops that read them. Adding a location no longer dispatches a walk,
which is the point: the drive is already mapped.

The entry tables and their readers are still here. That is the next commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 04:21:15 -07:00
Jamie Pine c40989cb93 Put the home folder in the library, and give the gesture a name
"Track this folder as a source" is our vocabulary, not a person's, and it
cannot answer what they get for doing it. Everything on the drive is already
searchable before anyone chooses anything. What the library adds is the part a
walk cannot rebuild: tags and notes that stay put, files still listed when the
drive is unplugged, sync to another device.

So the gesture is Add to Library, on a directory, in the explorer context menu.
Photos, Music and Lightroom all landed on the same word because people already
have it, and Spacedrive has libraries too.

Home goes in on the first library open, because it is the answer almost
everyone would give if asked which folder that should be, and asking is worse
than doing it. Once only: any registered source means the person has made their
own arrangements, so one removed on purpose does not come back next launch.

Anchoring needed fixing first. volume_for_path is a prefix test, and
/Users/me is not under /System/Volumes/Data, so tracking home would have
dropped the anchor and started a second map of files the drive already holds.
locate_path returns the volume and the path as that volume spells it, deciding
it by inode rather than by a platform table, and track_and_index takes the
volume's spelling over the caller's.
2026-08-27 19:23:45 -07:00
Jamie Pine 25be7ec66e Make tracking a drive one click
Tracking a volume did not index it. There were three flows and none of
them was one gesture: the context menu offered Track and Index as
separate items, and AddStorageModal tracked the volume and then created a
Location, which is the model being deleted.

volumes.track now calls the same track_and_index that sources.track does.
The medium and the index stay separate underneath, since a drive can be
tracked with nothing on it indexed and a folder can be indexed on a
volume nobody tracks; what converges is the click.

The shared body matters more than the saving. Both entry points forget
the same two steps otherwise, seeding from the snapshot and clearing what
the new pass will not revisit, which is exactly what sources.track was
missing when it was written separately.

An external mount indexes unfiltered, since a drive being archived wants
completeness rather than rules that hide files on purpose. An unmounted
drive tracks without walking and gets its map when it returns.

Index Volume becomes Re-index Volume and only shows for tracked drives,
since the first index is no longer something to ask for.
2026-08-25 22:13:17 -07:00
Jamie Pine 0ab0598136 Let a drive be tracked on purpose
A filesystem source used to appear as a side effect: browsing a directory
dispatched a walk and the walk registered whatever partition it landed
in. sources.track is that intent made explicit. Whole drive and
folder-within-a-drive are the same call with a different path, since the
volume resolves from the path and the source anchors to it.

Indexing starts immediately rather than waiting to be asked, and the
action returns as soon as the row exists, because registration is instant
and a walk over a drive is not. The job then reports against a source
that already exists.

sources.list serves both kinds now, so the sidebar stopped needing a
group per registry: filesystem rows render as walked roots with their
attachment state, adapter rows as before. The union is transitional,
reading filesystem rows from the sources table and adapter rows from
registry.db, and folding the second into the first is the rest of P3.

Content ids stay out of the walk on purpose. The walk is fast and worth
keeping fast, and records_needing_content_identity is already the query a
separate pass would drive. Nothing generates them yet, which the plan now
says plainly rather than implying the sampled tier is wired.
2026-08-25 20:59:27 -07:00
Jamie Pine 831a4de09b Make sources a table and anchor them to volumes
volumes and sources.json described the same drive twice, joined by a
fingerprint string that was a foreign key in neither direction and
updated at different moments, so their counts drifted with nothing to
notice. The line that holds is medium against index: a volume is a device
fact, true whether or not anything is indexed, and a source is a
Spacedrive fact.

So sources get a row in library.db with a nullable volume_uuid. Nullable
because the cardinality is many sources to optionally one volume: nested
roots are distinct sources, and adapter, cloud and fingerprint-less
network sources have no volume at all. One table serves both kinds,
forked by data_type, matching _schema.data_type_id in the source's own
store.

Anchoring replaces fingerprint matching outright. A volume-backed source
stores its root relative to the volume, so a remount re-derives the
absolute path instead of being a case anything handles: the volume keeps
its identity, the source keeps its path within it. A whole drive has an
empty relative root. Roots that fall outside the mount point they claim
stay unanchored rather than binding to a volume that cannot produce them.

SourceRegistry is now pure and does no I/O, so the arena's read path
never waits on a database to answer which source owns a path. The cache
holds the memory half and the open library holds the durable one. That
seam is awkward on purpose and visible in the code: the cache is
machine-scoped, a registration is library metadata, and until sources are
reachable from wherever they are mutated the cache follows the open
library.

sources.json is deleted rather than migrated, per the reindex-from-zero
cutover. Index facts still on volumes move when their writers do.
2026-08-25 19:10:19 -07:00
Jamie Pine 60a471edc1 Retire the Apple Photos adapter, keep the reader as sd-photos
The adapter minted a parallel record universe over bytes a filesystem source
will index — the shape file-backed-sources.md exists to forbid, and the reason
photos showed 0-byte sizes and UUID names in the UI. The Photos app rewrite
already went the other way: it follows an explorer window and renders
thumbnails the daemon bakes from filesystem records.

What was worth keeping is the reading: the ZASSET projection, the album and
person joins resolved by shape across Core Data ordinals, the derivative
fallback chain, the APFS-clone snapshot. That is now crates/photos, a
standalone read-only library reader that mints nothing. Its API speaks
ordinary units — unix milliseconds instead of Core Data seconds, a MediaKind
instead of ZKIND — so every Apple-specific decode stays behind the boundary.
It is the harvest path for file-backed enrichment when P2 lands.

Falling out with it: AdapterKind (apple-photos was the only Native one, and
core never projected the field to the frontend), register_native_adapters,
the rusqlite/libc/dirs dependencies and the Sqlite error variant, and the
Import -> Photos entry that pointed at the adapter.
2026-08-22 16:52:53 -07:00
Jamie Pine 33a86dacd5 Rewrite Photos as a daemon-following app with a real thumbnail hot tier
Photos no longer runs its own demo bake; it follows a Spacedrive explorer
window via a new navigation-focus resource and renders that window's
folder as a media grid, sourced from thumbnails baked by a new daemon-owned
thumbs service. Supporting changes: pvcache moves to a v2 format that
stores each tile's true aspect (envelope + frame) instead of forcing a
square crop, peer mounts now refresh on a generation check instead of only
at reconnect, and the archive crate's `spine` module is renamed to `record`
throughout.
2026-08-22 16:16:02 -07:00
Jamie Pine 35eb522e39 Remove hardcoded local paths
Absolute paths from one developer's machine sat in an example, a shell script,
recorded benchmark metadata, task docs and design docs. The example and the
script were broken for anyone else as a result: the example now resolves against
CARGO_MANIFEST_DIR and the script derives the repo root from git. The duplicate
copy of that script under core/scripts is gone.

Also drops a pointer to a repository that is not public.
2026-08-21 13:47:42 -07:00
Jamie Pine 2ab99b036c Surface indexed drives and persistent counts in the interface
Sidebar lists indexed drives including detached ones, volume rows and overview
cards carry persisted entry counts, and the explorer gains a list view over
daemon ops with event-driven refresh.
2026-08-21 13:47:42 -07:00
Jamie Pine 6dfeccf211 Add public shares design docs and remote-access server improvements
- Add SHARE-000..011 task specs and docs/design/shares.md defining
  the sd.app public sharing architecture and wire protocols
- Rework INDEX-010 to scope ephemeral UUID reconciliation per-library
  instead of a single global map, since core can have multiple
  libraries loaded at once
- Add sidecar serving and configurable bind host to apps/server, and
  wire the web platform's daemon status to the server's own origin
- Add quick preview button to the file inspector, with a null-safe
  useOptionalExplorer for use outside the explorer context
2026-07-28 17:31:23 -07:00
Jamie PineandClaude Opus 4.7 60369e9f00 feat: redundancy awareness & cross-volume file comparison (#3053)
* feat: add redundancy awareness & cross-volume file comparison

Surface content redundancy data so users can answer "if this drive dies,
what do I lose?" — builds on existing content identity and volume systems.

Backend:
- New `redundancy.summary` library query with per-volume at-risk vs
  redundant byte/file counts and a library-wide replication score
- Extend `SearchFilters` with `at_risk`, `on_volumes`, `not_on_volumes`,
  `min_volume_count`, `max_volume_count` filters
- Add composite index migration on entries(content_id, volume_id)

Frontend:
- `/redundancy` dashboard with replication score, volume bars, at-risk callout
- `/redundancy/at-risk` paginated file list sorted by size
- `/redundancy/compare` two-volume comparison (unique/shared toggle)
- Sidebar ShieldCheck button linking to redundancy view

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

* redundancy UI improvements + ZFS volume detection fix

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

* fix ZFS pool capacity reporting and stats filtering

- ZFS: override total_capacity for pool-root volumes using zfs list
  used+available. df under-reports pool-root Size because it only
  counts the root dataset's own used bytes plus avail — on a 60 TB
  raidz2 pool this shows as ~15 TB instead of ~62 TB. The pool root's
  own used property includes descendants, so used+available is the
  real usable capacity.

- Library stats: drop volumes where is_user_visible=false AND
  re-apply should_hide_by_mount_path retroactively so stale DB rows
  (detected before the Linux visibility filters existed) don't
  inflate reported capacity.

- Extract should_hide_by_mount_path into volume/utils as a shared
  helper used by both the list query and the stats calculation.

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

* show capacity and visibility in sd volume list

Makes it possible to verify library-level capacity aggregation from
the CLI — previously the list only showed mount, fingerprint, and
tracked/mounted state, which meant debugging the ZFS pool capacity
issue required querying the library DB directly.

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

* document filesystem support matrix and detection

New docs/core/filesystems.mdx covering per-filesystem capabilities
(CoW, pool-awareness, visibility filtering, capacity correction),
platform detection strategies, the FilesystemHandler trait, Linux/
macOS/ZFS visibility rules, the ZFS pool-root capacity problem and
fix, copy strategy selection, and known limitations.

Registered under File Management in both mint.json and docs.json.

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

* WIP: redundancy filter wiring across search, CLI, and UI

- core/src/ops/search: wire redundancy filters (at_risk, on_volumes,
  not_on_volumes, min/max volume_count) through the search query;
  fix UUID-to-SQLite BLOB literal so volume UUID comparisons actually
  match (volumes.uuid is stored as a 16-byte BLOB, quoted-string
  comparison silently returned zero rows).

- apps/cli: new redundancy subcommand + populate the new
  SearchFilters fields from search args.

- packages/interface: redundancy at-risk and compare pages reworked
  to consume the new filter surface; explorer context/hook updates
  to support redundancy-scoped views.

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

* add web context menu renderer and UI polish

- New WebContextMenuProvider + Radix DropdownMenu-based renderer anchored
  at cursor via a 1x1 virtual trigger. Handles separators, submenus,
  disabled, and the danger variant via text-status-error.
- useContextMenu now routes web clicks through the provider instead of
  parking data in unused local state, and trims leading/trailing/adjacent
  separators so condition-filtered menus don't render orphaned lines.
- Drop app-frame corner rounding on the web build.
- Add shrink-0 to the sidebar space switcher so the scrollable sibling
  can't compress it vertically.

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

* sd-server dev workflow: auto web build, shutdown watchdog, stable data dir

- build.rs runs `bun run build` in apps/web so `just dev-server` always
  embeds the latest UI. rerun-if-changed covers apps/web/src,
  packages/interface/src, and packages/ts-client/src so Rust-only edits
  skip the rebuild. Skips gracefully when bun isn't on PATH or
  SD_SKIP_WEB_BUILD is set; Dockerfile sets the latter since dist is
  pre-built and bun isn't in the Rust stage.
- Graceful shutdown was hanging because the browser holds the /events
  SSE stream open forever and axum waits for all connections to drain.
  After the first signal, arm a background force-exit on second Ctrl+C
  or 5s timeout so the process can't stick.
- Debug builds were starting from a fresh tempfile::tempdir() on every
  run (the TempDir handle dropped at end of the closure, deleting the
  dir we just took a path to). Default to ~/.spacedrive in debug so data
  persists and `just dev-server` shares a data dir with the Tauri app.

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

* add Sources space item alongside Redundancy in default library layout

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

* add TrueNAS native build script

Uses zig cc as C/C++ compiler on TrueNAS Scale where /usr is
read-only and no system gcc exists. Dev tools live at
/mnt/pool/dev-tools/ (zig, cmake, make, extracted deb headers).

Builds sd-server + sd-cli in ~4 min on a 12-core NAS. AI feature
disabled (whisper.cpp C11 atomics incompatible with zig clang-18).

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

* never block RPC on synchronous statistics calculation

On first load (fresh library, all stats zero), libraries.info used
to calculate statistics synchronously before responding. On large
libraries during active indexing this hangs indefinitely — the
closure-table walk in calculate_file_statistics loads every
descendant ID into a Vec then issues a WHERE IN(...) with millions
of entries, which SQLite can't finish while the indexer is writing.

Now always return cached (possibly zero) stats and let the
background recalculate_statistics task fill them in. The UI
refreshes via the ResourceChanged event when the calculation
completes.

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

* self-heal protocol handler registration on re-init

Core::new() registers default protocol handlers after starting networking,
but swallows any failure (error is only logged). If the initial registration
fails — e.g. on a host where start_networking hasn't fully set up the event
loop command sender by the time register_default_protocol_handlers runs —
the registry is left empty. A subsequent call to Core::init_networking()
would see `services.networking().is_some()` and skip re-registration,
permanently leaving protocols unregistered for the life of the process.

sd-server calls init_networking() right after Core::new(), so it's the
client most exposed to this. Symptom: pairing over the web UI returns
"Pairing protocol not registered" while the same library works fine
from Tauri and mobile.

Fix: init_networking now queries the registry directly for the pairing
handler and re-registers the default set if it's missing, independent of
whether networking is already initialized.

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

* fall back to pkarr+DNS discovery when mDNS port is unavailable

Iroh's endpoint.bind() fails wholesale if any configured discovery service
fails to initialize. MdnsDiscovery requires binding UDP :5353, which on
most Linux systems (including TrueNAS) is already owned by avahi-daemon.
Result: endpoint creation errors out with "Service 'mdns' error", the
event loop never starts, command_sender stays None, and protocol
registration fails — so sd-server has no working networking at all.

Make mDNS best-effort: on any error whose message mentions "mdns",
retry endpoint creation with only pkarr + DNS discovery. Local-network
auto-discovery is lost but remote pairing via node ID (which uses n0's
DNS infrastructure, not mDNS) continues to work normally.

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

* succeed pairing if either mDNS or relay discovery wins

The dual-path discovery in start_pairing_as_joiner_with_code used
tokio::select! to race mDNS and relay. select! resolves on the first
branch to complete — including errors — so a host that can't bind
mDNS (e.g. a Linux box where avahi already owns UDP :5353) would fail
pairing wholesale: mDNS discovery errors out in <1ms with
"Failed to create mDNS discovery: Service 'mdns' error", that Err
wins the race, and relay discovery gets cancelled before it can even
begin.

Switch to futures::select_ok so we only return the error if EVERY
discovery path has failed. mDNS failing immediately now leaves relay
running to completion, which is the common case for remote pairing
into a NAS.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 18:56:51 -07:00
88e1412175 feat(tags): tag system with explorer integration, and media context menu fixes (#3054)
* fix(search): apply TagFilter in search.files query

Tags were silently ignored by FilterBuilder. Adds find_entry_ids_for_tag()
(batch lookup via user_metadata_tag → user_metadata → entry, handles both
entry_uuid and content_identity_uuid paths) and resolve_tag_filter() (AND
logic for include, OR for exclude). Applied in both execute_fast_search_no_fts()
and execute_fast_search() FTS path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(tags): implement tags.by_id, tags.ancestors, tags.children, files.by_tag

Fixes "Tag not found" when clicking a tag in the sidebar, and shows the
actual tagged files in the tag view.

Backend (register_library_query via inventory):
- tags.by_id: find tag by UUID via TagManager::get_tags_by_ids
- tags.ancestors: get ancestor tags via TagManager::get_ancestors
- tags.children: get descendant tags via TagManager::get_descendants
- files.by_tag: find entries via user_metadata_tag → user_metadata → entry
  (handles both entry_uuid and content_identity_uuid paths)

Frontend:
- TagView: replace ExplorerView (used global Explorer context, ignored
  files.by_tag results) with a direct file list rendered from TaggedFileSummary

TODOs for tag feature follow-up:
- files.by_tag: return full File objects with sd_path so files are
  clickable/actionable (currently: id, name, extension, size only)
- tags.related handler (sidebar shows related tags)
- "Filters" button in TagView: secondary filters (type/date/size) within
  tagged files
- tags.children in TagView currently uses get_descendants (all levels);
  should use get_direct_children for the quick-filter chips
- DEL key binding for removing a tag from a file (#21 dependency resolved)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tags): prevent duplicate tag applications on the same file

The apply_tags_to_metadata() relied on catching a unique constraint error
to detect duplicates, but no such constraint existed — so every call to
tags.apply would silently create a new row.

- Migration m20260125: deduplicates existing rows (keeps MIN(id) per pair),
  then adds UNIQUE INDEX(user_metadata_id, tag_id)
- apply_tags_to_metadata(): explicit check-before-insert (upsert pattern),
  independent of DB constraint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(tags,ui): make tag view files navigable and wire Overview search button

- files.by_tag query now joins directory_paths/volumes/devices to build
  SdPath::Physical, enabling navigation from tag view to explorer
- Tag view: double-click navigates to parent folder (files) or into
  directory; use react-router navigate() instead of window.location.href
- Overview: search button now navigates to /explorer instead of no-op

* feat(tags): render tag view using standard explorer with full File objects

Backend:
- files.by_tag now returns Vec<File> (full domain objects with
  File::from_entity_model) instead of lightweight TaggedFileSummary,
  matching the same data format as directory_listing and search.files

Frontend:
- Add tag mode to explorer context (ENTER_TAG_MODE/EXIT_TAG_MODE)
- useExplorerFiles supports tag source via files.by_tag query
- Tag route activates tag mode and renders ExplorerView directly,
  giving tagged files the same UI as normal file browsing (list/grid
  views, thumbnails, selection, context menus, keyboard shortcuts)
- Fix ExplorerView empty state guard to allow tag/recents/search modes
  without requiring a currentPath

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

* feat(tags): add unapply/delete actions, fix tag sync and Inspector UX

Backend:
- Add tags.unapply action: remove tags from files by entry UUID,
  resolves via both entry_uuid and content_identity_uuid paths
- Add tags.delete action: delete a tag and all its relationships
  via TagManager::delete_tag()
- Add EntryUuid variant to TagTargets and ApplyToTargets to accept
  frontend UUIDs (fixes parseInt(uuid) bug that tagged wrong files)
- files.by_tag: batch load tags for returned files (same pattern as
  directory_listing) so Inspector shows tags in tag view
- navigateToPath exits tag mode to prevent empty directory on nav

Frontend:
- Tag primitive: add onRemove prop with X button for inline removal
- FileInspector: optimistic tag updates via updateSelectedFileTags,
  refetchQueries with correct query keys (query:files.by_tag prefix)
- TagsGroup: right-click delete with confirmation, active state
- useFileContextMenu: "Remove tag" option when in tag mode
- TagSelector: fix create+apply with EntryUuid fallback
- Generated types: add DeleteTagInput/Output, UnapplyTagsInput/Output,
  EntryUuid variant to TagTargets and ApplyToTargets

* refactor: extract shared useRefetchTagQueries hook

Extract duplicated refetchQueries calls from FileInspector,
useFileContextMenu, TagsGroup, and TagSelector into a single
useRefetchTagQueries hook. Removes direct useQueryClient usage
from those files.

* fix(core): use current device slug instead of \"unknown-device\" fallback

When the SQL join to devices table returns no result (volume_id or
device_id NULL), fall back to get_current_device_slug() instead of
the hardcoded \"unknown-device\" string. The previous fallback caused
SdPath::is_local() to return false, breaking ephemeral indexing when
navigating to directories from the tag view.

Fixed in both files.by_tag and search.files queries.

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

* fix(media): replace broken useJobDispatch with direct mutations

Context menu used useJobDispatch/jobs.dispatch which has no backend
handler, causing all media processing (thumbnail, OCR, transcribe,
thumbstrip, proxy) to fail from the context menu.

- Replace all 15 runJob() calls with direct useLibraryMutation calls
  (media.thumbnail.regenerate, media.ocr.extract, etc.)
- Add forEachTarget helper for batch operations
- Add mime_from_extension() fallback in RegenerateThumbnailAction for
  indexed files where content_identity MIME lookup fails
- useJobDispatch.ts is now dead code (no remaining imports)

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

* fix(tags): address CodeRabbit review findings on tag system

- TOCTOU race: replace check-then-insert with atomic ON CONFLICT upsert
  in metadata manager (prevents duplicate tag applications under concurrency)
- children query: use get_direct_children (depth=1) instead of
  get_descendants (entire subtree) for tags.children endpoint
- delete atomicity: wrap tag cascade deletion in a transaction
  (relationships, closure, applications, usage patterns, tag)
- files_by_tag: implement include_children and min_confidence filters
  (were declared in input but ignored)
- files_by_tag: map content_id from SQL result instead of fabricating None
- files_by_tag: merge entry-scoped and content-scoped tags with dedup
  (previously content-scoped tags were silently dropped)
- unapply: emit resource events for all entries sharing content, not just
  the directly specified entries
- frontend: derive tagModeActive from mode.type instead of storing
  separately (prevents state desynchronization)
- Document sync deletion gaps with TODO(sync) comments

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

* fix(migration): keep newest row (MAX id) when deduplicating tag applications

The dedup query before creating the unique index on (user_metadata_id, tag_id)
was keeping MIN(id) — the oldest row. Since user_metadata_tag rows carry mutable
state (version, updated_at, device_uuid), keeping MAX(id) preserves the most
recent state.

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

* revert(tags): restore independent tagModeActive state

The previous commit incorrectly derived tagModeActive from mode.type,
conflating two separate concepts:
- mode: {type: "tag"} = viewing files by tag (sidebar navigation)
- tagModeActive = bulk tag assignment UI bar

These are independent: clicking a tag in the sidebar should show tagged
files without opening the assignment bar. Reverts the context.tsx portion
of 04a181535.

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

* fix(tags): address second round of CodeRabbit review

- Increment version on ON CONFLICT update path so sync detects changes
- Only report/notify entries that actually lost a tag (skip when 0 rows deleted)
- Exit tag mode on all navigation paths (navigateToView, goBack, goForward)
  to prevent tag mode leaking through non-path navigation

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

* fix(tags): skip rows with undecodable required fields instead of fabricating

entry_id=0 and modified_at=now() hide real decode failures. Required fields
(entry_id, entry_name, created_at, modified_at) now skip the row with a
warning log. Optional/numeric fields (size, child_count) keep sensible
defaults since 0 is a valid value.

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

* fix(tags): remove broken optimistic update and alert() dialog

- FileInspector: remove updateSelectedFileTags() which mutated
  selectedFiles while the pane renders from file.tags — the refetch
  on mutation success is what actually updates the UI
- TagsGroup: remove alert() on delete error (console.error suffices,
  alert() breaks platform-agnostic design)

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

* fix(tags): emit file events on tag delete, refetch files.by_id for inspector

- delete/action.rs: collect affected entry UUIDs before deleting the tag,
  then emit "file" resource events so the explorer grid updates (removes
  tag dots). Follows the same pattern as apply and unapply actions.
- useRefetchTagQueries: add files.by_id to the refetch list so the
  Inspector panel updates immediately after tag mutations.

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

* fix(tags): add extension to root-level file paths, validate entry UUIDs

- files_by_tag.rs: root-level files (no parent_path) were missing their
  extension in the constructed path — now uses the same name+ext logic
  as the parent_path branch
- apply/action.rs: validate that entry UUIDs exist in the entry table
  before creating user_metadata rows, since there is no FK constraint
  at the SQLite level on user_metadata.entry_uuid — prevents orphaned
  metadata from invalid UUIDs

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

* fix(tags): pre-index content rows to avoid O(n²) tag merge, require entry_kind

- load_tags_for_entries: pre-build HashMap<content_uuid, Vec<entry_uuid>>
  from rows in a single pass, then lookup in the content-scoped branch
  instead of rescanning all rows per metadata record
- entry_kind treated as required field (skip row with warning instead of
  silently defaulting to 0, which would misclassify directories as files)

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

* fix(tags): secure FTS5 escaping, batch entry lookups for performance

- FTS5 search: wrap each query token in double quotes to prevent
  operator injection (AND, OR, NOT, -, *, etc.)
- apply/action.rs: replace per-entry UUID lookups with batch query
  (Entry branch: single WHERE IN instead of N round trips)
- apply/action.rs: replace per-entry existence validation with batch
  query (EntryUuid branch: single WHERE IN instead of N round trips)

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

* fix(tags): remove redundant inline sea_orm imports

ColumnTrait, EntityTrait, QueryFilter are already imported at top-level.

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

* fix(tags): validate entry UUIDs in create action before applying

* fix: address code review feedback from CodeRabbit

- Resolve merge conflict markers in TagSelector.tsx
- Add type: 'all' to refetchQueries for inactive cache refresh
- Replace browser confirm() with useContextMenu in TagsGroup
- Add onSuccess refetch to createTag mutation in TagsGroup
- Remove unnecessary 'as any' casts in OverviewTopBar
- Replace alert() with toast.error() in useFileContextMenu
- Remove 'any' casts in useExplorerFiles tag/directory queries
- Add nil UUID rejection in tag apply input validation
- Propagate SeaORM errors in thumbnail MIME lookup
- Wrap tag upsert loop in DB transaction for atomicity
- Fix tab indentation in migration mod.rs

* fix: validate create tag targets, confirm before delete, escape LIKE wildcards, remove dead code

* fix: prevent tagging ephemeral files and improve empty tag view UX

* fix: platform confirm, parameterized SQL, remove dead code, handle serialization errors

* fix: use native Tauri dialog for confirm on Windows (WebView2 broken)

* fix: prevent double callback in platform.confirm and distinguish tag error types

* fix: separate missing-target, null-UUID, and execution errors in tag apply

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: James Pine <ijamespine@me.com>
2026-04-18 18:53:12 -07:00
James PineandClaude Opus 4.6 d313d5a12a fix: isolate @spacebot/api-client from web/server builds
Remove VoiceOverlay and Spacebot re-exports from @sd/interface barrel
export. Tauri imports them directly via subpath exports. Add importmap
shim in web index.html so externalized @spacebot/api-client doesn't
break at runtime. Add @sd/interface subpath exports for Spacebot and
VoiceOverlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 22:15:11 -07:00
James PineandClaude Opus 4.6 37bb1b2f26 fix release CI + rustfmt + storage navigation
- Add bun setup and web frontend build steps to server-build job in
  release workflow (rust-embed needs apps/web/dist/ at compile time)
- Fix rustfmt violation in volume detection
- Fix storage dialog callback to use sdPath instead of location_id
- Navigate to explorer path after adding storage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 17:58:40 -07:00
James Pine 1e58784482 fix volume detection + redundancy doc 2026-04-14 15:50:44 -07:00
Jamie Pine 8a7fc53cdf Fix ~750 TypeScript typecheck errors
- Add ReactComponent export to *.svg module declarations
- Fix SdPath imports to use generated types (device_slug, Cloud/Sidecar variants)
- Create useJobs barrel file for JobManager hooks
- Remove unused imports across ~65 files
- Add type annotations for implicit any params (d3, callbacks, map iterators)
- Remove stale @ts-expect-error directives in MeshViewer
- Add declare module for gaussian-splats-3d and qrcode
- Fix Location field names (path→sd_path, total_file_count→file_count, etc)
- Fix SdPath discriminated union narrowing (remove stale Local variant)
- Fix React 19 RefObject<T|null> vs RefObject<T> mismatches
- Fix null vs undefined mismatches throughout
- Add missing required fields to ApplyTagsInput, policy types, etc
2026-04-14 08:56:21 -07:00
Jamie Pine acde17c088 Fix CI: rustfmt, xtask empty TARGET_TRIPLE, and package linking
- Sort `pub mod adapters` alphabetically in ops/mod.rs
- Wrap long line in volume/fs/refs.rs
- Handle empty TARGET_TRIPLE env var in xtask setup
- Replace `link:@spacedrive/*` with published `^0.2.3` versions
2026-04-12 20:33:43 -07:00
Jamie Pine 74cb8c46bb Merge main into spacedrive-data 2026-04-12 13:40:59 -07:00
Jamie Pine 6c5e656e6d spacedrive server work 2026-04-10 16:58:43 -07:00
Jamie Pine efa8d07992 Rename @spaceui/* deps to @spacedrive/* 2026-04-04 21:35:20 -07:00
James PineandClaude Opus 4.6 3383fb7cdb Remove deprecated explorer sidebar (replaced by SpacesSidebar)
Delete Sidebar.tsx, SidebarItem.tsx, Section.tsx, LocationsSection.tsx
from the old explorer sidebar. The active sidebar is SpacesSidebar
which uses SpaceItem from @spaceui/primitives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:49:55 -07:00
James PineandClaude Opus 4.6 adac2f3c45 Source sync jobs, voice overlay rewrite, TabBar migration, docs
- Refactor source sync to dispatch SourceSyncJob instead of inline sync
- Rewrite VoiceOverlay with audio recorder and TTS playback hooks
- Migrate TabBar to @spaceui/primitives
- Update SpacebotContext query invalidation
- Add SpaceUI section to CONTRIBUTING.md and README.md
- Update sources UI (Adapters, SourceDetail)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 01:12:52 -07:00
James PineandClaude Opus 4.6 907ab4e267 Add archive sources UI with full Rust integration
- Create core/src/data/ module with SourceManager wrapping sd-archive Engine
- Add Sources to GroupType and Source to ItemType enums
- Add default Sources group to new library creation
- Register source operations: create, list, get, delete, sync, list_items
- Register adapter operations: list, config, update
- Add bundled adapter sync from workspace adapters/ directory
- Add adapter update system with BLAKE3 change detection and backup/rollback
- Frontend: Sources home, source detail with virtualized list, adapters screen
- Frontend: SourcesGroup sidebar, SpaceGroup dispatch, spaceItemUtils
- Frontend: TopBar integration (path bar, search, sync, actions menu)
- Frontend: Tab title sync, adapter icon lookup hook
- Regenerate TypeScript types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 03:24:53 -07:00
Jamie Pine 9f0685abc9 ui 2026-03-28 18:19:48 -07:00
Jamie Pine 457833ee19 remove ui refs 2026-03-26 22:48:27 -07:00
James Pine 0c19c64a17 ui 2026-03-26 22:23:29 -07:00