Replace folder sync with an HTTP sync server, keeping the app offline-first
Folder sync left too much to the user's sync tool: platform file access
(the Android SAF plugin), choosing the document, merging conflict copies —
and its merge base could fall out of step with the document (absorbing
conflict copies, "forget", picking a new Android folder), reading every
difference as a deletion.
The new mtgc-server crate stores one versioned library per user and accepts
a write only when it names the version it replaces. It never merges: devices
still do that with merge3, through a new sync cycle over a `Remote` trait in
mtgc-core. The base advances only on knowledge — to what was read, or to a
write the server confirmed — and is stored with the server and account it
belongs to, so a different account starts from an empty ancestor instead of
deleting. The working copy is only rewritten under the app-state lock, and
sync commands now run off the main thread.
Sign-in happens in the browser on the server's pages, via the owner's OIDC
provider and/or local username/password accounts (Argon2id, managed with
`mtgc-server user ...`), and returns a per-device revocable token to the app
by PKCE-protected code: a loopback port on desktop, the mtgc:// scheme on
Android.
The Info tab gains sign-in, the first-sync combine/replace choice, the
account's devices, and JSON export/import; the top bar shows sync state.
Includes a Dockerfile, Compose/Caddy example, and scripts to build and push
${DOCKER_REGISTRY}/mtgc/sync-server.
This commit is contained in:
@@ -489,6 +489,9 @@ input[type="checkbox"] {
|
||||
.status-dot.off {
|
||||
background: var(--faint);
|
||||
}
|
||||
.status-dot.warn {
|
||||
background: var(--warn);
|
||||
}
|
||||
|
||||
/* Sidebar "your library" summary panel. */
|
||||
.lib-stats-head {
|
||||
@@ -784,7 +787,57 @@ input[type="checkbox"] {
|
||||
gap: var(--space-3);
|
||||
max-width: 480px;
|
||||
}
|
||||
/* The currently chosen synced folder. */
|
||||
/* Top-bar sync state; opens the Info tab. Quiet when all is well, so it
|
||||
only draws the eye when something needs attention. */
|
||||
.sync-indicator {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--fs-caption);
|
||||
padding: 5px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sync-indicator-warn {
|
||||
border-color: var(--warn);
|
||||
color: var(--warn);
|
||||
}
|
||||
/* First sync with an account that already has a library: a decision only the
|
||||
user can make, so it's set apart from the surrounding status text. */
|
||||
.sync-choice {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-3);
|
||||
border: 1px solid var(--warn);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.sync-devices {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.sync-devices li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sync-device-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
font-weight: 600;
|
||||
color: var(--text-strong);
|
||||
}
|
||||
.sync-devices .link-button {
|
||||
margin-left: auto;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 2px 4px;
|
||||
color: var(--danger);
|
||||
}
|
||||
/* The signed-in account. */
|
||||
.sync-folder {
|
||||
font-size: var(--fs-body);
|
||||
font-weight: 600;
|
||||
|
||||
Reference in New Issue
Block a user