From e2abb1dee1fe62e35e535eaca5f8f19214e9eac8 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 15 Jul 2026 07:48:40 +0200 Subject: [PATCH] :sparkles: Incrementally sync debug graph from Penpot file changes --- .../app/templates/graph-console.tmpl | 58 ++- backend/src/app/graph/debug.clj | 102 ++++- backend/src/app/graph/ingest.clj | 4 +- backend/src/app/graph/sync.clj | 419 ++++++++++++++++++ backend/src/app/http/debug.clj | 11 + backend/src/app/main.clj | 1 + 6 files changed, 555 insertions(+), 40 deletions(-) create mode 100644 backend/src/app/graph/sync.clj diff --git a/backend/resources/app/templates/graph-console.tmpl b/backend/resources/app/templates/graph-console.tmpl index dc859f4467..066c520786 100644 --- a/backend/resources/app/templates/graph-console.tmpl +++ b/backend/resources/app/templates/graph-console.tmpl @@ -45,18 +45,17 @@ Graph Console

File: {{session.name}} ({{session.file-id}})
- Revision: {{session.revn}}
+ Loaded at revision: {{session.revn}}
+ Graph revision: {% if session.graph-revn %}{{session.graph-revn}}{% else %}{{session.revn}}{% endif %}
Schema: {{session.schema-version}}
Loaded at: {{session.loaded-at}}

- Sync: connecting… - + Feed: connecting… +

- {% if session.projection.stats %}

@@ -72,10 +71,9 @@ Graph Console

File changes (live) - Subscribes to the same WebSocket channel the workspace uses - (:subscribe-file:file-change via msgbus). - Edits from other tabs or users appear here; the in-memory graph stays at - the loaded revision until you reload it. + Subscribes to the workspace WebSocket feed for visibility. The backend + applies supported changes incrementally to the in-memory Ladybug graph + via msgbus (:file-change).
Waiting for changes…
(function () { const fileId = "{{session.file-id}}"; - const sessionRevn = {{session.revn}}; const sessionId = crypto.randomUUID(); const wsScheme = location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = wsScheme + "//" + location.host + "/ws/notifications?session-id=" + sessionId; const wsStatus = document.getElementById("graph-ws-status"); - const staleBadge = document.getElementById("graph-stale-badge"); - const currentRevnEl = document.getElementById("graph-current-revn"); - const reloadForm = document.getElementById("graph-reload-form"); + const syncRevnEl = document.getElementById("graph-sync-revn"); + const syncErrorEl = document.getElementById("graph-sync-error"); const changelog = document.getElementById("graph-changelog"); const changelogBody = document.getElementById("graph-changelog-body"); const changelogEmpty = document.getElementById("graph-changelog-empty"); - let latestRevn = sessionRevn; let ws = null; function encodeTransitUuid(uuid) { @@ -212,11 +207,27 @@ Graph Console return changes.map(summarizeChange).join("; "); } - function updateStaleUi() { - const stale = latestRevn > sessionRevn; - staleBadge.style.display = stale ? "inline" : "none"; - reloadForm.style.display = stale ? "block" : "none"; - currentRevnEl.textContent = String(latestRevn); + function refreshSyncStatus() { + fetch("/dbg/actions/graph-sync-status") + .then(function (resp) { return resp.text(); }) + .then(function (text) { + const status = parseTransitMap(JSON.parse(text)); + if (status["graph-revn"] !== undefined) { + syncRevnEl.textContent = String(status["graph-revn"]); + } + if (status.sync && status.sync.error) { + syncErrorEl.style.display = "inline"; + syncErrorEl.textContent = "sync error: " + status.sync.error; + } else if (status.sync && status.sync["last-skipped"] + && status.sync["last-skipped"].length) { + syncErrorEl.style.display = "inline"; + syncErrorEl.textContent = "some changes skipped (use full reload if needed)"; + } else { + syncErrorEl.style.display = "none"; + syncErrorEl.textContent = ""; + } + }) + .catch(function () {}); } function appendChange(revn, summary) { @@ -245,9 +256,8 @@ Graph Console if (msg.type !== "file-change" || msg["file-id"] !== fileId) return; - latestRevn = Math.max(latestRevn, msg.revn || latestRevn); - updateStaleUi(); appendChange(msg.revn, summarizeChanges(msg.changes)); + setTimeout(refreshSyncStatus, 150); } function subscribe() { @@ -278,8 +288,8 @@ Graph Console }); } - updateStaleUi(); connect(); + refreshSyncStatus(); window.addEventListener("beforeunload", function () { if (ws && ws.readyState === WebSocket.OPEN) { diff --git a/backend/src/app/graph/debug.clj b/backend/src/app/graph/debug.clj index b1e6d32416..deeda78c14 100644 --- a/backend/src/app/graph/debug.clj +++ b/backend/src/app/graph/debug.clj @@ -8,10 +8,14 @@ "In-memory Ladybug sessions for the debug graph console." (:require [app.common.exceptions :as ex] + [app.common.logging :as l] [app.common.time :as ct] [app.graph.ingest :as graph.ingest] [app.graph.ladybug :as ladybug] - [clojure.string :as str]) + [app.graph.sync :as graph.sync] + [app.msgbus :as mbus] + [clojure.string :as str] + [promesa.exec.csp :as sp]) (:import com.ladybugdb.Connection com.ladybugdb.Database)) @@ -29,7 +33,11 @@ (str profile-id)) (defn- destroy-session! - [{:keys [conn db]}] + [{:keys [conn db sync-ch msgbus]}] + (when sync-ch + (sp/close! sync-ch) + (when msgbus + (mbus/purge! msgbus [sync-ch]))) (when conn (ex/ignoring (.close ^Connection conn))) (when db @@ -51,17 +59,74 @@ :truncated? truncated? :row-count (count rows)}) +(defn- apply-file-change! + [conn profile-id {:keys [changes revn file-id]}] + (try + (some-> (get @sessions (session-key profile-id)) + (as-> current + (when (= file-id (:file-id current)) + (let [result (graph.sync/apply-changes! + conn (:index current) changes revn) + sync-at (ct/now)] + (swap! sessions assoc-in [(session-key profile-id) :index] + (:index result)) + (swap! sessions update-in [(session-key profile-id) :meta] + (fn [meta] + (cond-> (-> meta + (assoc-in [:sync :last-at] sync-at) + (assoc-in [:sync :last-applied] (:applied result)) + (assoc-in [:sync :last-skipped] (:skipped result))) + (seq (:applied result)) + (assoc :revn (:revn result))))) + (when (seq (:skipped result)) + (l/dbg :hint "graph sync skipped changes" + :file-id (str file-id) + :revn revn + :skipped (:skipped result))))))) + (catch Throwable cause + (l/wrn :hint "graph sync failed" + :file-id (str file-id) + :cause cause) + (swap! sessions assoc-in [(session-key profile-id) :meta :sync :error] + (ex-message cause))))) + +(defn- start-sync-loop! + [{:keys [conn profile-id file-id] :as session}] + (if-let [msgbus (:msgbus session)] + (let [sync-ch (sp/chan :buf (sp/dropping-buffer 64))] + (mbus/sub! msgbus :topic file-id :chan sync-ch) + (sp/go-loop [] + (when-let [message (sp/take! sync-ch)] + (when (= :file-change (:type message)) + (apply-file-change! conn profile-id message))) + (recur)) + (assoc session :sync-ch sync-ch)) + session)) + (defn session-info "Return a public view of the current session for `profile-id`, if any." [profile-id] - (when-let [{:keys [file-id meta loaded-at]} (get @sessions (session-key profile-id))] + (when-let [{:keys [file-id meta loaded-at index]} (get @sessions (session-key profile-id))] {:file-id file-id :name (:name meta) :revn (:revn meta) + :graph-revn (:revn index) :schema-version (:schema-version meta) :projection (:projection meta) + :sync (:sync meta) :loaded-at (ct/format-inst loaded-at :iso)})) +(defn sync-status + "Return incremental sync status for the active session." + [profile-id] + (when-let [session (get @sessions (session-key profile-id))] + (let [{:keys [file-id meta index loaded-at]} session] + {:file-id file-id + :revn (:revn meta) + :graph-revn (:revn index) + :sync (:sync meta) + :loaded-at (ct/format-inst loaded-at :iso)}))) + (defn unload-session! "Close and discard the in-memory graph for `profile-id`." [profile-id] @@ -74,22 +139,29 @@ [cfg profile-id file-id] (unload-session! profile-id) (let [^Database db (Database.) - ^Connection conn (Connection. db)] + ^Connection conn (Connection. db) + msgbus (::mbus/msgbus cfg)] (.setQueryTimeout conn 0) (try - (let [meta (graph.ingest/ingest-on-connection! cfg conn file-id - :db-path ":memory:" - :skip-stats? true - :skip-validation? true)] - (swap! sessions assoc (session-key profile-id) - {:db db - :conn conn - :file-id file-id - :meta meta - :loaded-at (ct/now)}) + (let [meta (graph.ingest/ingest-on-connection! cfg conn file-id + :db-path ":memory:" + :skip-stats? true + :skip-validation? true) + index (graph.sync/build-index file-id (:revn meta) (:projection meta)) + session + (-> {:db db + :conn conn + :file-id file-id + :meta meta + :index index + :msgbus msgbus + :profile-id profile-id + :loaded-at (ct/now)} + start-sync-loop!)] + (swap! sessions assoc (session-key profile-id) session) meta) (catch Throwable cause - (destroy-session! {:conn conn :db db}) + (destroy-session! {:conn conn :db db :msgbus msgbus}) (throw cause))))) (defn query-session! diff --git a/backend/src/app/graph/ingest.clj b/backend/src/app/graph/ingest.clj index c17cc5320f..b5bfb9dac7 100644 --- a/backend/src/app/graph/ingest.clj +++ b/backend/src/app/graph/ingest.clj @@ -63,7 +63,9 @@ :name (or (:name data) (:name file)) :db-path db-path :schema-version schema/schema-version - :projection {:stats stats} + :projection {:stats stats + :nodes nodes + :edges edges} :transforms (project.transforms/apply-transforms! system db-path data file) :stats (when-not skip-stats? (stats/summarize-connection conn))}))) diff --git a/backend/src/app/graph/sync.clj b/backend/src/app/graph/sync.clj new file mode 100644 index 0000000000..6c0e6d484f --- /dev/null +++ b/backend/src/app/graph/sync.clj @@ -0,0 +1,419 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.graph.sync + "Incremental Ladybug graph updates from Penpot file-change events." + (:require + [app.common.logging :as l] + [app.common.uuid :as uuid] + [app.graph.ladybug :as ladybug] + [app.graph.project.specs :as specs] + [clojure.string :as str]) + (:import + com.ladybugdb.Connection)) + +(set! *warn-on-reflection* true) + +(def ^:private shape-type->table + {:frame "Frame" + :rect "Rectangle" + :group "Group" + :circle "Circle" + :path "Path" + :text "Text" + :bool "Boolean" + :image "Image" + :svg-raw "SVGRaw"}) + +(def ^:private supported-change-types + #{:add-obj :mod-obj :del-obj :add-page :del-page :mod-page}) + +(defn- node-label + [table] + (if (#{"Group" "Boolean"} table) + (str "`" table "`") + table)) + +(defn- shape-table + [shape] + (get shape-type->table (keyword (:type shape)))) + +(defn- build-parent-map + [edges] + (into {} + (map (fn [{:keys [from-id to-id to-table]}] + [from-id {:parent-id to-id :parent-table to-table}])) + edges)) + +(defn- build-children-map + [edges] + (reduce (fn [acc {:keys [from-id to-id]}] + (update acc to-id (fnil conj #{}) from-id)) + {} + edges)) + +(defn- resolve-page-id + [shape-id parents pages] + (loop [id shape-id] + (cond + (contains? pages id) id + (get parents id) (recur (:parent-id (parents id))) + :else nil))) + +(defn- node-attrs-id + [attrs] + (cond + (map? attrs) (or (:id attrs) (get attrs "id")) + (and (vector? attrs) (= 2 (count attrs))) + (let [[k v] attrs] + (when (or (= k :id) (= k "id")) v)))) + +(defn- table-rows + "Normalize a projection table value to a vector of attribute maps." + [nodes table] + (let [rows (or (get nodes table) (get nodes (keyword table)))] + (cond + (nil? rows) [] + (map? rows) [rows] + (sequential? rows) (vec rows) + :else []))) + +(defn- document-id-from-nodes + [nodes file-id] + (or (some node-attrs-id (table-rows nodes "Document")) + file-id)) + +(defn- page-index-entry + [attrs] + (let [id (node-attrs-id attrs)] + [id {:id id + :name (:name attrs) + :index (long (:index attrs 0))}])) + +(defn- index-pages + [nodes] + (into {} (map page-index-entry (table-rows nodes "Page")))) + +(defn- shape-index-table? + [table] + (not (contains? #{"Document" "Page" :Document :Page} table))) + +(defn- shape-index-entry + [table attrs parents pages edges] + (let [shape-id (node-attrs-id attrs) + {:keys [parent-id parent-table]} (parents shape-id) + edge (first (filter #(= shape-id (:from-id %)) edges))] + [shape-id {:id shape-id + :name (:name attrs) + :table table + :parent-id parent-id + :parent-table parent-table + :position (long (:position edge 0)) + :page-id (resolve-page-id shape-id parents pages)}])) + +(defn- index-shapes + [nodes edges parents pages] + (reduce + (fn [acc [table _]] + (into acc (map #(shape-index-entry table % parents pages edges) + (table-rows nodes table)))) + {} + (filter (fn [[table _]] (shape-index-table? table)) nodes))) + +(defn build-index + "Build a sync index from a full graph projection." + [file-id revn {:keys [nodes edges]}] + (let [doc-id (document-id-from-nodes nodes file-id) + pages (index-pages nodes) + parents (build-parent-map edges) + children-index (build-children-map edges) + shapes (index-shapes nodes edges parents pages)] + {:file-id file-id + :doc-id doc-id + :revn (long revn) + :pages pages + :shapes shapes + :children children-index})) + + +(defn- create-node-statement + [table {:keys [id name version revision index]}] + (let [label (node-label table) + attrs (cond-> [(str "id: " (ladybug/format-uuid id)) + (str "name: " (ladybug/format-string name))] + (some? version) (conj (str "version: " (ladybug/format-int version))) + (some? revision) (conj (str "revision: " (ladybug/format-int revision))) + (some? index) (conj (str "index: " (ladybug/format-int index))))] + (str "CREATE (:" label " {" (str/join ", " attrs) "});"))) + +(defn- delete-node-statement + [table shape-id] + (str "MATCH (n:" (node-label table) " {id: " (ladybug/format-uuid shape-id) "}) " + "DETACH DELETE n;")) + +(defn- create-edge-statement + [{:keys [from-table from-id to-table to-id position]}] + (str "MATCH (s:" (node-label from-table) " {id: " (ladybug/format-uuid from-id) "}), " + "(p:" (node-label to-table) " {id: " (ladybug/format-uuid to-id) "}) " + "CREATE (s)-[:IsChildOf {position: " (ladybug/format-int position) "}]->(p);")) + +(defn- delete-edge-statement + [{:keys [from-table from-id to-table to-id]}] + (str "MATCH (s:" (node-label from-table) " {id: " (ladybug/format-uuid from-id) "})" + "-[r:IsChildOf]->" + "(p:" (node-label to-table) " {id: " (ladybug/format-uuid to-id) "}) " + "DELETE r;")) + +(defn- set-shape-name-statement + [table shape-id name] + (str "MATCH (s:" (node-label table) " {id: " (ladybug/format-uuid shape-id) "}) " + "SET s.name = " (ladybug/format-string name) ";")) + +(defn- set-page-name-statement + [page-id name] + (str "MATCH (p:Page {id: " (ladybug/format-uuid page-id) "}) " + "SET p.name = " (ladybug/format-string name) ";")) + + +(defn- set-document-revision-statement + [doc-id revn] + (str "MATCH (d:Document {id: " (ladybug/format-uuid doc-id) "}) " + "SET d.revision = " (ladybug/format-int revn) ";")) + +(defn- resolve-parent-for-add + [index {:keys [parent-id frame-id page-id]}] + (let [pid (or parent-id frame-id)] + (if (or (nil? pid) (uuid/zero? pid)) + (when page-id + {:parent-id page-id :parent-table "Page"}) + (if-let [shape (get-in index [:shapes pid])] + {:parent-id pid :parent-table (:table shape)} + (when (get-in index [:pages pid]) + {:parent-id pid :parent-table "Page"}))))) + +(defn- default-position + [index parent-id] + (count (get-in index [:children parent-id] #{}))) + +(defn- index-add-shape! + [index {:keys [id name table parent-id parent-table position page-id]}] + (-> index + (assoc-in [:shapes id] + {:id id + :name name + :table table + :parent-id parent-id + :parent-table parent-table + :position position + :page-id page-id}) + (update :children update parent-id (fnil conj #{}) id))) + +(defn- index-remove-shape! + [index shape-id] + (if-let [shape (get-in index [:shapes shape-id])] + (-> index + (update :shapes dissoc shape-id) + (update :children update (:parent-id shape) + #(disj (or % #{}) shape-id)) + (update :children dissoc shape-id)) + index)) + +(defn- index-add-page! + [index {:keys [id name index doc-id]}] + (-> index + (assoc-in [:pages id] {:id id :name name :index index}) + (update :children update doc-id (fnil conj #{}) id))) + +(defn- index-remove-page! + [index page-id] + (let [doc-id (:doc-id index)] + (-> index + (update :pages dissoc page-id) + (update :children update doc-id #(disj (or % #{}) page-id)) + (update :children dissoc page-id)))) + +(defn- apply-add-obj + [index change] + (let [{:keys [id obj page-id parent-id frame-id index]} change + table (shape-table obj)] + (if-not table + {:index index :statements [] :applied? false :reason :unsupported-shape-type} + (let [parent (resolve-parent-for-add index change)] + (if-not parent + {:index index :statements [] :applied? false :reason :missing-parent} + (let [position (long (or index (default-position index (:parent-id parent)))) + attrs (specs/check-shape-node {:id id :name (:name obj)}) + edge (merge {:from-table table + :from-id id + :to-table (:parent-table parent) + :to-id (:parent-id parent) + :position position} + )] + {:index (index-add-shape! index + {:id id + :name (:name attrs) + :table table + :parent-id (:parent-id parent) + :parent-table (:parent-table parent) + :position position + :page-id (or page-id (when (= (:parent-table parent) "Page") + (:parent-id parent)))}) + :statements [(create-node-statement table attrs) + (create-edge-statement edge)] + :applied? true})))))) + +(defn- apply-mod-obj + [index {:keys [id operations]}] + (if-let [shape (get-in index [:shapes id])] + (let [name-ops (filter #(and (= :set (:type %)) (= :name (:attr %))) operations)] + (if (empty? name-ops) + {:index index :statements [] :applied? false :reason :unsupported-operations} + (let [name (:val (last name-ops)) + table (:table shape) + attrs (specs/check-shape-node {:id id :name name})] + {:index (assoc-in index [:shapes id :name] (:name attrs)) + :statements [(set-shape-name-statement table id (:name attrs))] + :applied? true}))) + {:index index :statements [] :applied? false :reason :missing-shape})) + +(defn- delete-order-deepest-first + [children root-id] + (letfn [(post-order [id] + (into (mapcat post-order (get children id #{})) + [id]))] + (post-order root-id))) + +(defn- apply-del-obj + [index {:keys [id]}] + (if-let [shape (get-in index [:shapes id])] + (let [to-delete (delete-order-deepest-first (:children index) id) + statements + (vec (concat + (mapcat (fn [shape-id] + (let [{:keys [table parent-id parent-table]} + (get-in index [:shapes shape-id])] + [(delete-edge-statement + {:from-table table + :from-id shape-id + :to-table parent-table + :to-id parent-id}) + (delete-node-statement table shape-id)])) + to-delete)))] + {:index (reduce index-remove-shape! index to-delete) + :statements statements + :applied? true}) + {:index index :statements [] :applied? false :reason :missing-shape})) + +(defn- apply-add-page + [index {:keys [id name page]}] + (let [page-id (or id (:id page)) + page (or page {:id page-id :name name}) + page (specs/check-page {:id page-id + :name (or (:name page) "Page") + :index (count (:pages index))}) + doc-id (:doc-id index) + position (count (:pages index)) + edge {:from-table "Page" + :from-id page-id + :to-table "Document" + :to-id doc-id + :position position}] + {:index (index-add-page! index + {:id page-id + :name (:name page) + :index (:index page) + :doc-id doc-id}) + :statements [(create-node-statement "Page" page) + (create-edge-statement edge)] + :applied? true})) + +(defn- apply-del-page + [index {:keys [id]}] + (if-let [page (get-in index [:pages id])] + (let [shape-ids (into #{} + (comp (filter #(= id (get-in index [:shapes % :page-id]))) + (filter #(= "Page" (get-in index [:shapes % :parent-table]))) + (keys (:shapes index)))) + del-shapes + (reduce (fn [acc shape-id] + (let [result (apply-del-obj acc {:type :del-obj :id shape-id})] + (if (:applied? result) + (-> acc + (assoc :index (:index result)) + (update :statements into (:statements result))) + acc))) + {:index index :statements []} + shape-ids) + statements + (conj (:statements del-shapes) + (delete-edge-statement {:from-table "Page" + :from-id id + :to-table "Document" + :to-id (:doc-id index)}) + (delete-node-statement "Page" id))] + {:index (-> (:index del-shapes) (index-remove-page! id)) + :statements statements + :applied? true}) + {:index index :statements [] :applied? false :reason :missing-page})) + +(defn- apply-mod-page + [index {:keys [id name]}] + (if (and (string? name) (get-in index [:pages id])) + {:index (assoc-in index [:pages id :name] name) + :statements [(set-page-name-statement id name)] + :applied? true} + {:index index :statements [] :applied? false :reason :unsupported-page-change})) + +(defn- apply-change + [index change] + (case (:type change) + :add-obj (apply-add-obj index change) + :mod-obj (apply-mod-obj index change) + :del-obj (apply-del-obj index change) + :add-page (apply-add-page index change) + :del-page (apply-del-page index change) + :mod-page (apply-mod-page index change) + {:index index :statements [] :applied? false :reason :unsupported-type})) + +(defn apply-changes! + "Apply Penpot `changes` to an open Ladybug `conn` and return the updated index. + + Returns `{:index ... :revn ... :applied [...] :skipped [...]}`." + [^Connection conn index changes revn] + (when (> (long revn) (:revn index)) + (l/wrn :hint "graph sync revn gap" + :file-id (str (:file-id index)) + :index-revn (:revn index) + :change-revn revn)) + (loop [index index + applied [] + skipped [] + stmts [] + changes (seq changes)] + (if-let [change (first changes)] + (let [{:keys [index statements applied? reason]} + (apply-change index change)] + (recur index + (cond-> applied applied? (conj (:type change))) + (cond-> skipped (not applied?) (conj {:type (:type change) :reason reason})) + (into stmts statements) + (rest changes))) + (let [final-stmts (cond-> stmts + (and (seq applied) (:doc-id index)) + (conj (set-document-revision-statement (:doc-id index) revn))) + index' (if (seq applied) + (assoc index :revn (long revn)) + index)] + (when (seq final-stmts) + (ladybug/exec-on-connection! conn final-stmts)) + {:index index' + :revn (if (seq applied) (long revn) (:revn index')) + :applied applied + :skipped skipped})))) + +(defn supported-change? + [change] + (contains? supported-change-types (:type change))) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 1a03f4cf92..125ef49318 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -393,6 +393,16 @@ :code :graph-session-not-loaded :hint "load a file graph before reloading"))) +(defn graph-sync-status-handler + [_cfg {:keys [::session/profile-id]}] + (if-let [status (graph.debug/sync-status profile-id)] + {::yres/status 200 + ::yres/headers {"content-type" "application/json; charset=utf-8"} + ::yres/body (t/encode-str status {:type :json-verbose})} + {::yres/status 404 + ::yres/headers {"content-type" "application/json; charset=utf-8"} + ::yres/body (t/encode-str {:error "no-session"} {:type :json-verbose})})) + (defn graph-query-handler [_cfg {:keys [params ::session/profile-id]}] (let [query (:query params)] @@ -662,6 +672,7 @@ ["/graph-query" {:handler (partial graph-query-handler cfg)}] ["/graph-unload" {:handler (partial graph-unload-handler cfg)}] ["/graph-reload" {:handler (partial graph-reload-handler cfg)}] + ["/graph-sync-status" {:handler (partial graph-sync-status-handler cfg)}] ["/file-import" {:handler (partial import-handler cfg)}] ["/file-raw-export-import" {:handler (partial raw-export-import-handler cfg)}]]]]) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index ca8fba8af1..c9f445a6fc 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -284,6 +284,7 @@ ::http.debug/routes {::db/pool (ig/ref ::db/pool) ::session/manager (ig/ref ::session/manager) + ::mbus/msgbus (ig/ref ::mbus/msgbus) ::sto/storage (ig/ref ::sto/storage) ::setup/props (ig/ref ::setup/props)}