-
Notifications
You must be signed in to change notification settings - Fork 6
feat(sync): federated media-collection sync parity + per-category sync integrity #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
26cd376
docs(sync): federated media collection sync parity + per-category int…
atomantic c0ecdef
docs(sync): implementation plan for federated media sync parity + int…
atomantic 091c115
feat(sync): soft-delete shape + includeDeleted on media collections
atomantic ecb58e0
feat(sync): soft-delete deleteCollection + tombstone-aware LWW merge
atomantic e4f0afe
feat(sync): register mediaCollection kind + createCollection auto-sub…
atomantic 67308cc
feat(sync): mediaCollection push payload + receiver apply + subscribe…
atomantic 68aca03
fix(sync): mediaCollection reverse-subscription + video asset manifest
atomantic d2de86e
feat(sync): orchestrator snapshot-skip + tombstone GC for media colle…
atomantic 344e9ba
fix(sync): count pruned collections in manual tombstone-sweep UI
atomantic 72d422d
feat(sync): sync image gen-params sidecars + manual backfill
atomantic 00992f1
fix(sync): hash sidecar gen-params canonically + sanitize sidecar pul…
atomantic e582fea
feat(sync): per-category integrity diff + peer manifest + routes
atomantic 184b71c
feat(sync): manual force-push, sync-now, and pull-metadata routes
atomantic 4d49d00
feat(sync): client peer-sync API wrappers + useSyncIntegrity hook
atomantic 1d08a3c
feat(sync): SyncBadge + deep-linkable SyncDetailDrawer + media collec…
atomantic 73ec68b
fix(sync): drawer scroll-lock + single-fetch collection state (no dou…
atomantic 62248c4
feat(sync): sync badges + detail drawer on universes and pipeline series
atomantic d862b59
feat(sync): Pull missing prompts action on the Unsorted media view
atomantic 405b853
test(sync): run useSyncIntegrity hook test in jsdom only (.test.jsx)
atomantic eea1b34
chore(sync): changelog + PLAN follow-ups + minor cleanups (unused sch…
atomantic c0f478e
chore(sync): align client PEER_SUBSCRIBABLE_KINDS with server (add me…
atomantic 4585ba8
Merge remote-tracking branch 'origin/main' into feat/federated-media-…
atomantic 91d8c3d
fix(sync): address Copilot review on #468
atomantic e70b887
fix(sync): address Copilot re-review on #468
atomantic dcdf6ab
fix(sync): address Copilot 3rd re-review on #468
atomantic 0b165a6
fix(sync): address Copilot 4th re-review on #468
atomantic cc90ab6
fix(sync): address Copilot 5th re-review on #468
atomantic 93078c7
fix(sync): address Copilot 6th re-review on #468
atomantic b970430
fix(sync): address Copilot 7th re-review on #468
atomantic 0a5f8bd
fix(sync): address Copilot 8th re-review on #468
atomantic c9cf193
fix(sync): address Copilot 9th re-review on #468
atomantic a5c4612
fix(sync): address Copilot 10th re-review on #468
atomantic 3401f42
fix(sync): address Copilot 11th re-review on #468
atomantic 9de0239
fix(sync): address Copilot 12th re-review on #468
atomantic 1116d0c
fix(sync): address Copilot 13th re-review on #468
atomantic 7e023b6
fix(sync): address Copilot 14th re-review on #468
atomantic c6e93fb
fix(sync): address Copilot 15th re-review on #468
atomantic 5c33d43
fix(sync): address Copilot 16th re-review on #468
atomantic 68c2abc
fix(sync): address Copilot 17th re-review on #468
atomantic 9b5493d
fix(sync): address Copilot 18th re-review on #468
atomantic 7c11e6e
fix(sync): address Copilot 19th re-review on #468
atomantic 3192537
fix(sync): address Copilot 20th re-review on #468
atomantic d73f682
test(sync): fix flaky forcePushRecord test that failed in CI (not local)
atomantic f30b10e
fix(sync): address Copilot 21st re-review on #468
atomantic c08825a
fix(sync): address Copilot 22nd re-review on #468
atomantic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| /** | ||
| * SyncBadge — presentational-only sync status badge. | ||
| * | ||
| * Props: | ||
| * status — 'in-parity' | 'diverged' | 'assets-missing' | 'local-only' | | ||
| * 'peer-only' | 'not-syncing' | 'unknown' | null | undefined | ||
| * onClick — called when the badge button is clicked (e.g. open detail drawer) | ||
| */ | ||
|
|
||
| import { CheckCircle2, AlertTriangle, WifiOff, HelpCircle } from 'lucide-react'; | ||
|
|
||
| const STATUS_CONFIG = { | ||
| 'in-parity': { | ||
| label: 'In sync', | ||
| className: 'bg-port-success/15 text-port-success hover:bg-port-success/25', | ||
| Icon: CheckCircle2, | ||
| title: 'All peers in parity', | ||
| }, | ||
| diverged: { | ||
| label: 'Diverged', | ||
| className: 'bg-port-warning/15 text-port-warning hover:bg-port-warning/25', | ||
| Icon: AlertTriangle, | ||
| title: 'Content differs from at least one peer', | ||
| }, | ||
| 'assets-missing': { | ||
| label: 'Assets missing', | ||
| className: 'bg-port-warning/15 text-port-warning hover:bg-port-warning/25', | ||
| Icon: AlertTriangle, | ||
| title: 'Record present but associated files are missing on a peer', | ||
| }, | ||
| 'local-only': { | ||
| label: 'Local only', | ||
| className: 'bg-port-warning/15 text-port-warning hover:bg-port-warning/25', | ||
| Icon: AlertTriangle, | ||
| title: 'Not present on at least one peer', | ||
| }, | ||
| 'peer-only': { | ||
| label: 'On peer only', | ||
| className: 'bg-port-warning/15 text-port-warning hover:bg-port-warning/25', | ||
| Icon: AlertTriangle, | ||
| title: 'Present on peer but not local', | ||
| }, | ||
| 'not-syncing': { | ||
| label: 'Not syncing', | ||
| className: 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30', | ||
| Icon: WifiOff, | ||
| // True when no online peer is syncing THIS category — either no peers are | ||
| // sync-enabled at all, or they are but have this category turned off. | ||
| title: 'No peers syncing this category — enable it for a peer?', | ||
| }, | ||
| unknown: { | ||
| label: 'Sync unknown', | ||
| className: 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30', | ||
| Icon: HelpCircle, | ||
| // Sync IS configured, but every eligible peer was unreachable / too-old / | ||
| // errored, so we couldn't compute parity. Distinct from 'not-syncing'. | ||
| title: 'Sync status unavailable — peer offline, unreachable, or on an older PortOS', | ||
| }, | ||
| }; | ||
|
|
||
| export default function SyncBadge({ status, onClick }) { | ||
| if (!status) return null; | ||
|
|
||
| const config = STATUS_CONFIG[status]; | ||
| if (!config) return null; | ||
|
|
||
| const { label, className, Icon, title } = config; | ||
|
|
||
| return ( | ||
| <button | ||
| type="button" | ||
| onClick={onClick} | ||
| title={title} | ||
| className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium transition-colors ${className}`} | ||
| > | ||
| <Icon className="w-3 h-3 flex-shrink-0" aria-hidden="true" /> | ||
| <span>{label}</span> | ||
| </button> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { describe, it, expect, vi } from 'vitest'; | ||
| import { render, screen, fireEvent } from '@testing-library/react'; | ||
| import SyncBadge from './SyncBadge'; | ||
|
|
||
| describe('SyncBadge', () => { | ||
| it('renders "In sync" for in-parity status', () => { | ||
| render(<SyncBadge status="in-parity" onClick={() => {}} />); | ||
| expect(screen.getByRole('button', { name: /in sync/i })).toBeInTheDocument(); | ||
| expect(screen.getByRole('button').className).toMatch(/port-success/); | ||
| }); | ||
|
|
||
| it('renders "Diverged" for diverged status', () => { | ||
| render(<SyncBadge status="diverged" onClick={() => {}} />); | ||
| expect(screen.getByRole('button', { name: /diverged/i })).toBeInTheDocument(); | ||
| expect(screen.getByRole('button').className).toMatch(/port-warning/); | ||
| }); | ||
|
|
||
| it('renders "Assets missing" for assets-missing status', () => { | ||
| render(<SyncBadge status="assets-missing" onClick={() => {}} />); | ||
| expect(screen.getByText('Assets missing')).toBeInTheDocument(); | ||
| expect(screen.getByRole('button').className).toMatch(/port-warning/); | ||
| }); | ||
|
|
||
| it('renders "Local only" for local-only status', () => { | ||
| render(<SyncBadge status="local-only" onClick={() => {}} />); | ||
| expect(screen.getByText('Local only')).toBeInTheDocument(); | ||
| expect(screen.getByRole('button').className).toMatch(/port-warning/); | ||
| }); | ||
|
|
||
| it('renders "On peer only" for peer-only status', () => { | ||
| render(<SyncBadge status="peer-only" onClick={() => {}} />); | ||
| expect(screen.getByText('On peer only')).toBeInTheDocument(); | ||
| expect(screen.getByRole('button').className).toMatch(/port-warning/); | ||
| }); | ||
|
|
||
| it('renders "Not syncing" with distinct neutral styling for not-syncing status', () => { | ||
| render(<SyncBadge status="not-syncing" onClick={() => {}} />); | ||
| const btn = screen.getByRole('button', { name: /not syncing/i }); | ||
| expect(btn).toBeInTheDocument(); | ||
| // Must not use warning or success colors — visually distinct/neutral | ||
| expect(btn.className).not.toMatch(/port-warning/); | ||
| expect(btn.className).not.toMatch(/port-success/); | ||
| expect(btn.className).toMatch(/gray/); | ||
| }); | ||
|
|
||
| it('renders "Sync unknown" with neutral styling for unknown status', () => { | ||
| render(<SyncBadge status="unknown" onClick={() => {}} />); | ||
| const btn = screen.getByRole('button', { name: /sync unknown/i }); | ||
| expect(btn).toBeInTheDocument(); | ||
| // Neutral, like not-syncing — not a warning/success state. | ||
| expect(btn.className).not.toMatch(/port-warning/); | ||
| expect(btn.className).not.toMatch(/port-success/); | ||
| expect(btn.className).toMatch(/gray/); | ||
| }); | ||
|
|
||
| it('calls onClick when clicked', () => { | ||
| const onClick = vi.fn(); | ||
| render(<SyncBadge status="in-parity" onClick={onClick} />); | ||
| fireEvent.click(screen.getByRole('button')); | ||
| expect(onClick).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it('renders nothing for null status', () => { | ||
| const { container } = render(<SyncBadge status={null} onClick={() => {}} />); | ||
| expect(container.firstChild).toBeNull(); | ||
| }); | ||
|
|
||
| it('renders nothing for undefined status', () => { | ||
| const { container } = render(<SyncBadge onClick={() => {}} />); | ||
| expect(container.firstChild).toBeNull(); | ||
| }); | ||
|
|
||
| it('has a descriptive title attribute', () => { | ||
| render(<SyncBadge status="diverged" onClick={() => {}} />); | ||
| const btn = screen.getByRole('button'); | ||
| expect(btn.title).toBeTruthy(); | ||
| expect(btn.title.length).toBeGreaterThan(5); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.