Skip to content

feat: sync: add instances and allow pushing entries#18

Open
MoustaphaCamara wants to merge 30 commits into
masterfrom
mouss-sync
Open

feat: sync: add instances and allow pushing entries#18
MoustaphaCamara wants to merge 30 commits into
masterfrom
mouss-sync

Conversation

@MoustaphaCamara
Copy link
Copy Markdown
Contributor

@MoustaphaCamara MoustaphaCamara commented May 22, 2026

fix #16

Summary by CodeRabbit

  • New Features

    • Edit and update entries with visible “Last edited” timestamps
    • Manage eLabFTW instances including adding, editing, deleting, and testing connections
    • Push single or all entries to eLabFTW with detailed per-entry results and conflict detection
  • UI Improvements

    • Integrated new modal and push workflows into the entry editor and index views
    • Enhanced instance management interface and push modal with instance selection options
    • Updated form validation with required fields and improved checkbox/input styling

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Captain’s log: entry timestamps migrated to modified_at; UpdateEntry persists encrypted edits; per-profile eLabFTW instances and local→remote mappings added; an HTTP client and push sync (POST/PATCH) with conflict checks were implemented, plus frontend instance management, push modal, and bindings.

Changes

eLabFTW Entry Sync Integration

Layer / File(s) Summary
Entry Update Support and Timestamp Migration
app.go, sqlite.go, frontend/wailsjs/go/models.ts
Renames entry timestamp to modified_at across schema, app queries, and frontend models; adds UpdateEntry to encrypt and persist edited entries.
eLabFTW Instance Configuration Management
sync_instances.go, sqlite.go, frontend/wailsjs/go/main/App.d.ts, frontend/wailsjs/go/main/App.js, frontend/wailsjs/go/models.ts
Adds elabftw_instances and local2remote tables; implements List/Add/Delete/Update for instances with URL normalization and encrypted API key storage.
eLabFTW API Client and Request Handling
elabftw_client.go
Loads per-instance config (decrypts API key), configures TLS-aware HTTP client, builds authenticated requests, and decodes JSON responses with robust non-2xx error handling.
Push Synchronization with Conflict Detection
sync_push.go, sqlite.go
Push logic chooses POST or PATCH based on local2remote, detects remote modified_at conflicts before PATCH, updates/creates local2remote rows, and returns PushEntryResult records.
Frontend TypeScript Models and Wails Bindings
frontend/wailsjs/go/models.ts, frontend/wailsjs/go/main/App.d.ts, frontend/wailsjs/go/main/App.js
Adds models for ElabftwInfo, ElabftwInstance, PushEntryResult; updates Entry/EntrySummary to modifiedAt; regenerates Wails bindings for new APIs.
Frontend UI Components: Modal, Instances View, Push Modal
frontend/src/components/Modal.svelte, frontend/src/components/Instances/InstancesView.svelte, frontend/src/components/Instances/InstancesPushModal.svelte
Adds reusable Modal, InstancesView (CRUD + test), and InstancesPushModal (select instance and entity type before push).
MainApp Integration: Entry Updates and Push Workflow
frontend/src/components/MainApp.svelte
Wires UpdateEntry into save flow, adds instances view and push modal control, and displays push results and classified errors.
Frontend Styling and Minor UX Refinements
frontend/src/css/inputs.css, frontend/src/css/main.css, frontend/src/components/ProfileSelector/ProfileSelectorUnlockForm.svelte, frontend/src/utils/helpers.ts
Expanded input/select styling, custom checkbox visuals, modal CSS, adjusted logo padding, updated passphrase placeholder, added openExternalURL helper, and small form required attributes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • NicolasCARPi

Poem

Captain speaks: timestamps set to starboard line by line,
Encrypted titles stowed away, integrity sublime,
Instances charted, TLS set and keys kept tight,
We POST, we PATCH, we reconcile by starry light,
Engage—sync complete; the log reports all right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main feature: adding eLabFTW instance management and entry push functionality.
Linked Issues check ✅ Passed The PR implements all core requirements from #16: elabftw_instances and local2remote tables, push buttons in index/editor, instance selection modal, POST/PATCH logic, and remote modified_at conflict checking.
Out of Scope Changes check ✅ Passed All changes directly support sync functionality; minor UI improvements (checkbox styling, logo padding, cross entity in Alert) are acceptable polish for the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mouss-sync

Comment @coderabbitai help to get the list of available commands and usage tips.

@MoustaphaCamara MoustaphaCamara changed the title (wip) feat: sync: add instances and allow pushing entries feat: sync: add instances and allow pushing entries Jun 3, 2026
@MoustaphaCamara MoustaphaCamara marked this pull request as ready for review June 3, 2026 09:54
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
sqlite.go (1)

81-93: ⚡ Quick win

Captain, sensors detect a potential data integrity gap in the local2remote table.

The foreign key constraint references elabftw_instances(id) with ON DELETE CASCADE, which is commendable. However, there is no foreign key from local_id to entries(id). When an entry is deleted via DeleteEntry, orphan rows will persist in local2remote.

Recommend adding a foreign key constraint to maintain referential integrity, Number One's assessment:

🛡️ Proposed schema amendment
 CREATE TABLE IF NOT EXISTS local2remote (
 	id INTEGER PRIMARY KEY AUTOINCREMENT,
 	instance INTEGER NOT NULL,
 	remote_id INTEGER NOT NULL,
 	local_id INTEGER NOT NULL,
 	type TEXT NOT NULL CHECK (type IN ('experiment', 'resource', 'template')),
 	created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
 	modified_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),

 	FOREIGN KEY (instance) REFERENCES elabftw_instances(id) ON DELETE CASCADE,
+	FOREIGN KEY (local_id) REFERENCES entries(id) ON DELETE CASCADE,
 	UNIQUE(instance, local_id, type),
 	UNIQUE(instance, remote_id, type)
 );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sqlite.go` around lines 81 - 93, The local2remote table lacks a foreign key
from local_id to entries(id), so deleted entries can leave orphaned local2remote
rows; update the CREATE TABLE for local2remote to add a FOREIGN KEY (local_id)
REFERENCES entries(id) ON DELETE CASCADE (or the appropriate ON DELETE behavior)
so that DeleteEntry will cascade and maintain referential integrity; locate the
local2remote table definition in sqlite.go and add the foreign key constraint
alongside the existing FOREIGN KEY (instance) definition.
sync_push.go (2)

64-71: 💤 Low value

Recommend distinguishing "entry not found" from other database errors, Commander.

When the entry does not exist, sql.ErrNoRows is wrapped generically. Consider providing a clearer error message for the crew:

🛡️ Proposed enhancement
 	err = db.QueryRow(`
 		SELECT title, body
 		FROM entries
 		WHERE id = ?
 	`, entryID).Scan(&encryptedTitle, &encryptedBody)
+	if err == sql.ErrNoRows {
+		return nil, fmt.Errorf("entry not found")
+	}
 	if err != nil {
 		return nil, fmt.Errorf("query entry: %w", err)
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sync_push.go` around lines 64 - 71, The code handling
db.QueryRow(...).Scan(&encryptedTitle, &encryptedBody) currently wraps all
errors the same; modify the error handling to detect sql.ErrNoRows and return a
distinct, clearer error (e.g., "entry not found" or a sentinel error) instead of
the generic "query entry" wrap. Update the block around QueryRow/Scan in
sync_push.go (the QueryRow + Scan call) to if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("entry not found: %w", err) } else { return nil,
fmt.Errorf("query entry: %w", err) } so callers can distinguish not-found from
other DB failures.

24-33: 💤 Low value

Sensors detect a discrepancy in entity type handling, Number One.

The local2remote schema permits type IN ('experiment', 'resource', 'template'), yet elabftwEntityPath only handles experiment and resource. If template push support is planned for a future mission, this is acceptable. Otherwise, consider aligning the schema constraint with the supported entity types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sync_push.go` around lines 24 - 33, The elabftwEntityPath function only maps
"experiment" and "resource" but the local2remote schema allows "template" as
well; either add a "template" case in elabftwEntityPath returning the correct
eLabFTW path (e.g., the template endpoint) so pushes for templates are
supported, or update the schema to remove "template" from the allowed types to
match current implementation (modify the validation/schema that defines type IN
('experiment','resource','template') accordingly). Ensure you edit the
elabftwEntityPath function (and any callers relying on it) if you add the
template case, or the schema/validation logic if you remove template from
allowed values.
frontend/src/components/Modal.svelte (2)

14-37: ⚡ Quick win

Recommend raising shields for keyboard navigators. A bare <dialog open> with a hand-rolled backdrop is not a true modal: there is no focus trap and, more importantly, no Escape-to-close. A mouse pilot can dismiss via the backdrop, but a keyboard-only crew member is stranded. Add an Escape handler (and aria-modal='true') at minimum.

🛡️ Minimal keyboard escape hatch
   function stopClick(event: MouseEvent): void {
     event.stopPropagation();
   }
+
+  function onKeydown(event: KeyboardEvent): void {
+    if (event.key === 'Escape') onClose();
+  }
 </script>
+
+<svelte:window onkeydown={onKeydown} />

 <div class='modal-backdrop' role='presentation' onclick={onClose}>
   <dialog
     class='modal panel'
     aria-labelledby='modal-title'
+    aria-modal='true'
     open
     onclick={stopClick}
   >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Modal.svelte` around lines 14 - 37, The dialog lacks
keyboard escape and modal semantics; add aria-modal="true" to the <dialog> and
wire an Escape key handler that calls the existing onClose handler (use onMount
to add a keydown listener that checks for event.key === 'Escape' and invokes
onClose, and onDestroy to remove the listener) and keep stopClick to prevent
backdrop clicks from propagating into the dialog; this ensures the dialog
element (and functions onClose and stopClick) will close on Escape and the event
listener is cleaned up.

30-34: ⚖️ Poor tradeoff

Number One to Helm: replace Modal’s legacy <slot> projection with Svelte 5 snippets (in runes/$props mode)
In frontend/src/components/Modal.svelte, the legacy <slot /> / <slot name="actions" /> content-projection is deprecated in Svelte 5 in favor of snippets ({#snippet ...} + {@render ...}). Only make this coordinated refactor if Modal.svelte is actually using runes/$props, and then migrate the named actions consumer (e.g., InstancesPushModal.svelte) to match—otherwise legacy slots remain supported in legacy mode.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/Modal.svelte` around lines 30 - 34, Check whether
Modal.svelte is using runes/$props; if it is, replace the legacy <slot/> and
<slot name='actions'/> usage with Svelte 5 snippets: change the consumer pattern
in Modal.svelte to define and render a snippet (use {`#snippet` 'default' $props}
/ {`#snippet` 'actions' $props}) and update callers (e.g.,
InstancesPushModal.svelte) to provide content via {`@render` 'default' $props} and
{`@render` 'actions' $props}; if Modal.svelte is not using runes/$props, leave
legacy slots unchanged. Ensure the snippet names match ('default' and 'actions')
and preserve any props passed through runes/$props.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/components/Instances/InstancesView.svelte`:
- Around line 129-146: The site URL input isn't marked required so saveInstance
runs (preventDefault) and dispatches an empty siteUrl; add client-side
validation to stop bad submits by marking the URL input (id=instanceSiteUrl)
with required and also add a guard in the saveInstance handler to check the
instanceSiteUrl value (and, if desired, require instanceApiKey when
editingInstanceId is falsy) before dispatching—if validation fails, return early
and surface a user-facing error instead of sending an empty payload to the
backend.

In `@frontend/src/components/Modal.svelte`:
- Around line 17-22: The modal uses aria-labelledby='modal-title' but no element
has that id; add the id "modal-title" to the modal heading (the <h1> that
renders {title}) or change aria-labelledby to reference the existing element id
if you prefer; update the <h1> in the Modal component so it includes
id="modal-title" to ensure the dialog is properly labeled for assistive tech.

In `@frontend/src/css/inputs.css`:
- Around line 44-72: The custom checkbox lacks a keyboard focus indicator
because the hidden input (".checkbox-row input") only controls ".checkbox-box"
for :checked and :hover; add a rule that targets the hidden input's
:focus-visible state and styles the adjacent ".checkbox-box" (e.g.,
input:focus-visible + .checkbox-box) to show an accessible focus ring/outline or
box-shadow using the existing color variables (like --orange or --orange-hover)
and appropriate outline-offset so keyboard users can see focus without altering
mouse hover behavior.

---

Nitpick comments:
In `@frontend/src/components/Modal.svelte`:
- Around line 14-37: The dialog lacks keyboard escape and modal semantics; add
aria-modal="true" to the <dialog> and wire an Escape key handler that calls the
existing onClose handler (use onMount to add a keydown listener that checks for
event.key === 'Escape' and invokes onClose, and onDestroy to remove the
listener) and keep stopClick to prevent backdrop clicks from propagating into
the dialog; this ensures the dialog element (and functions onClose and
stopClick) will close on Escape and the event listener is cleaned up.
- Around line 30-34: Check whether Modal.svelte is using runes/$props; if it is,
replace the legacy <slot/> and <slot name='actions'/> usage with Svelte 5
snippets: change the consumer pattern in Modal.svelte to define and render a
snippet (use {`#snippet` 'default' $props} / {`#snippet` 'actions' $props}) and
update callers (e.g., InstancesPushModal.svelte) to provide content via {`@render`
'default' $props} and {`@render` 'actions' $props}; if Modal.svelte is not using
runes/$props, leave legacy slots unchanged. Ensure the snippet names match
('default' and 'actions') and preserve any props passed through runes/$props.

In `@sqlite.go`:
- Around line 81-93: The local2remote table lacks a foreign key from local_id to
entries(id), so deleted entries can leave orphaned local2remote rows; update the
CREATE TABLE for local2remote to add a FOREIGN KEY (local_id) REFERENCES
entries(id) ON DELETE CASCADE (or the appropriate ON DELETE behavior) so that
DeleteEntry will cascade and maintain referential integrity; locate the
local2remote table definition in sqlite.go and add the foreign key constraint
alongside the existing FOREIGN KEY (instance) definition.

In `@sync_push.go`:
- Around line 64-71: The code handling db.QueryRow(...).Scan(&encryptedTitle,
&encryptedBody) currently wraps all errors the same; modify the error handling
to detect sql.ErrNoRows and return a distinct, clearer error (e.g., "entry not
found" or a sentinel error) instead of the generic "query entry" wrap. Update
the block around QueryRow/Scan in sync_push.go (the QueryRow + Scan call) to if
errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("entry not found: %w",
err) } else { return nil, fmt.Errorf("query entry: %w", err) } so callers can
distinguish not-found from other DB failures.
- Around line 24-33: The elabftwEntityPath function only maps "experiment" and
"resource" but the local2remote schema allows "template" as well; either add a
"template" case in elabftwEntityPath returning the correct eLabFTW path (e.g.,
the template endpoint) so pushes for templates are supported, or update the
schema to remove "template" from the allowed types to match current
implementation (modify the validation/schema that defines type IN
('experiment','resource','template') accordingly). Ensure you edit the
elabftwEntityPath function (and any callers relying on it) if you add the
template case, or the schema/validation logic if you remove template from
allowed values.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1d385cf1-fb27-46c9-81be-5e7780f00e32

📥 Commits

Reviewing files that changed from the base of the PR and between db8928d and 0f65cdf.

📒 Files selected for processing (15)
  • app.go
  • elabftw_client.go
  • frontend/src/components/Instances/InstancesPushModal.svelte
  • frontend/src/components/Instances/InstancesView.svelte
  • frontend/src/components/MainApp.svelte
  • frontend/src/components/Modal.svelte
  • frontend/src/components/ProfileSelector/ProfileSelectorUnlockForm.svelte
  • frontend/src/css/inputs.css
  • frontend/src/css/main.css
  • frontend/wailsjs/go/main/App.d.ts
  • frontend/wailsjs/go/main/App.js
  • frontend/wailsjs/go/models.ts
  • sqlite.go
  • sync_instances.go
  • sync_push.go

Comment thread frontend/src/components/Instances/InstancesView.svelte
Comment thread frontend/src/components/Modal.svelte Outdated
Comment thread frontend/src/css/inputs.css
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
frontend/src/utils/helpers.ts (1)

33-36: ⚡ Quick win

Captain’s security note: harden external URL opening with protocol validation.

Line 35 opens whatever string is passed. Even though today’s caller uses https, this exported helper should defensively allow only http:/https: before calling BrowserOpenURL.

Proposed patch
 export function openExternalURL(url: string): void {
-  if (!url) return;
-  BrowserOpenURL(url);
+  const trimmed = url?.trim();
+  if (!trimmed) return;
+
+  try {
+    const parsed = new URL(trimmed);
+    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return;
+    BrowserOpenURL(parsed.toString());
+  } catch {
+    return;
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/helpers.ts` around lines 33 - 36, openExternalURL
currently calls BrowserOpenURL on any non-empty string; update it to defensively
validate the URL protocol first: parse the input string with the URL constructor
inside a try/catch, ensure urlObj.protocol is exactly "http:" or "https:"
(case-insensitive by lowercasing), and only then call BrowserOpenURL(url);
otherwise return without calling BrowserOpenURL (or optionally log/reject).
Reference the openExternalURL function and BrowserOpenURL symbol when
implementing the protocol check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend/src/utils/helpers.ts`:
- Around line 33-36: openExternalURL currently calls BrowserOpenURL on any
non-empty string; update it to defensively validate the URL protocol first:
parse the input string with the URL constructor inside a try/catch, ensure
urlObj.protocol is exactly "http:" or "https:" (case-insensitive by
lowercasing), and only then call BrowserOpenURL(url); otherwise return without
calling BrowserOpenURL (or optionally log/reject). Reference the openExternalURL
function and BrowserOpenURL symbol when implementing the protocol check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7e84c1f-1995-4bd0-b001-9f33870c4c04

📥 Commits

Reviewing files that changed from the base of the PR and between f292cda and 57ef356.

📒 Files selected for processing (7)
  • frontend/src/components/Alert.svelte
  • frontend/src/components/Instances/InstancesPushModal.svelte
  • frontend/src/components/Instances/InstancesView.svelte
  • frontend/src/components/MainApp.svelte
  • frontend/src/components/Modal.svelte
  • frontend/src/css/main.css
  • frontend/src/utils/helpers.ts
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/components/Alert.svelte
🚧 Files skipped from review as they are similar to previous changes (5)
  • frontend/src/components/Modal.svelte
  • frontend/src/css/main.css
  • frontend/src/components/Instances/InstancesPushModal.svelte
  • frontend/src/components/Instances/InstancesView.svelte
  • frontend/src/components/MainApp.svelte

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

Labels

None yet

Development

Successfully merging this pull request may close these issues.

sync

1 participant