For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add a development toolbar to the app UI that shows request/SQL metrics and provides snapshot/restore/delete actions for the dev database and S3.
Architecture: A middleware injected via requiring-resolve when :dev? is true renders a toolbar into HTML responses via </body> string replacement. SQL metrics come from next.jdbc/with-logging. Snapshot/restore serializes 9 document tables as EDN + syncs S3 via the AWS SDK. All dev code lives in dev/ so it's excluded from production uberjars.
Tech Stack: Clojure, Ring/Reitit, Hiccup2, next.jdbc with-logging, AWS SDK v2 (S3 ListObjectsV2), HoneySQL, EDN serialization.
:extra-pathsFiles:
scripts/ci/build.cljStep 1: Update the uber function signature and paths
Change the function to accept a map with optional :extra-paths:
(defn uber
"Build an uberjar at target/orcha.jar.
Cleans target/, copies source paths, and packages all dependencies.
Accepts optional `:extra-paths` vector to include additional source directories."
[{:keys [extra-paths]}]
(let [artifact-dir "target"
uber-file (format "%s/orcha.jar" artifact-dir)
basis (c.t.build.api/create-basis {:project "deps.edn"})
src-dirs (into (:paths basis) extra-paths)]
(c.t.build.api/delete
{:path artifact-dir})
(c.t.build.api/copy-dir
{:target-dir artifact-dir
:src-dirs src-dirs})
(c.t.build.api/uber
{:basis basis
:class-dir artifact-dir
:uber-file uber-file
:main 'clojure.main
:conflict-handlers log4j2-conflict-handler})))
Step 2: Verify default build still works
Run: clojure -X:build
Expected: Builds target/orcha.jar as before (no extra paths).
Step 3: Verify extra-paths build works
Run: clojure -X:build :extra-paths '["dev"]'
Expected: Builds uberjar that includes dev/ directory contents.
Verify: jar tf target/orcha.jar | grep user.clj should show user.clj.
Step 4: Update Tolaria run-demo.sh
Modify ../spikes/tolaria/scripts/run-demo.sh line 39 — change:
clojure -X:build
to:
clojure -X:build :extra-paths '["dev"]'
Also update the staleness check on line 37 to include dev/:
if [[ ! -f "$JAR_FILE" ]] || [[ -n "$(find "$ORCHA_DIR/src" "$ORCHA_DIR/resources" "$ORCHA_DIR/dev" -newer "$JAR_FILE" 2>/dev/null | head -1)" ]]; then
Step 5: Commit
git add scripts/ci/build.clj
git commit -m "feat(build): accept :extra-paths CLI arg for uberjar"
Then:
git add ../spikes/tolaria/scripts/run-demo.sh
git commit -m "feat(tolaria): include dev/ in demo uberjar build"
:dev? to app handlerFiles:
resources/com/getorcha/config.ednStep 1: Add :dev? to the app handler config
In config.edn, inside :com.getorcha.app.http/handler (after line 148), add:
:dev? #ref [:com.getorcha/dev?]
This makes (:dev? request) available to handlers and middleware after inject-config merges it.
Step 2: Verify via REPL
Run: (get-in (user/config) [:integrant/config :com.getorcha.app.http/handler :dev?])
Expected: true (in local-dev profile).
Step 3: Commit
git add resources/com/getorcha/config.edn
git commit -m "feat(config): pass :dev? flag to app HTTP handler"
dev/snapshots/Files:
.gitignoreStep 1: Add snapshot directory
Add to .gitignore:
# Dev toolbar snapshots
dev/snapshots/
Step 2: Commit
git add .gitignore
git commit -m "chore: gitignore dev/snapshots/"
wrap-dev-toolbarThis is the core middleware that collects metrics and injects the toolbar HTML.
Files:
dev/com/getorcha/dev/toolbar.cljStep 1: Create the namespace with middleware
(ns com.getorcha.dev.toolbar
"Dev toolbar middleware and routes.
Conditionally loaded via requiring-resolve when :dev? is true.
Lives in dev/ so it's excluded from production uberjars."
(:require [clojure.string :as str]
[com.getorcha.db.sql :as db.sql]
[hiccup2.core :as hiccup]
[next.jdbc :as jdbc]
[reitit.core :as reitit]
[ring.util.http-response :as ring.resp])
(:import (hiccup.util RawString)))
Step 2: Implement SQL metrics tracking
The middleware creates an atom and wraps the db pool with jdbc/with-logging:
(defn ^:private wrap-db-pool-with-logging
"Wraps a db pool with next.jdbc/with-logging to track query count and timing.
Returns [wrapped-pool metrics-atom]."
[db-pool]
(let [metrics (atom {:queries [] :total-sql-ms 0.0})]
[(jdbc/with-logging db-pool
(fn [_sym _sql-params]
(System/nanoTime))
(fn [_sym start-ns result]
(let [elapsed-ms (/ (- (System/nanoTime) (long start-ns)) 1e6)]
(swap! metrics (fn [m]
(-> m
(update :queries conj {:ms elapsed-ms
:error? (instance? Throwable result)})
(update :total-sql-ms + elapsed-ms)))))))
metrics]))
Step 3: Implement toolbar Hiccup rendering
See Task 5 for the full rendering code. For now, create a placeholder:
(defn ^:private render-toolbar
"Renders the dev toolbar as an HTML string."
[request-ms sql-metrics request]
(str (hiccup/html
[:div {:style "background:#0d1117;color:#c9d1d9;padding:4px 12px;border-top:1px solid #30363d;font:12px monospace"}
(format "%.0fms | %d SQL %.1fms" request-ms (count (:queries sql-metrics)) (:total-sql-ms sql-metrics))])))
Step 4: Implement the middleware
The middleware must handle the async Ring 3-arity pattern (used by Jetty async):
(def wrap-dev-toolbar
"Reitit middleware that wraps request handling with metrics collection
and injects a toolbar into HTML responses."
{:name ::dev-toolbar
:wrap (fn [handler]
(fn dev-toolbar [request respond raise]
(let [start-ns (System/nanoTime)
[logged-pool sql-metrics] (wrap-db-pool-with-logging (:db-pool request))
request' (assoc request :db-pool logged-pool)]
(handler request'
(fn [response]
(let [elapsed-ms (/ (- (System/nanoTime) start-ns) 1e6)
metrics @sql-metrics]
(respond
(if (instance? RawString (:body response))
(let [html-str (str (:body response))
toolbar-str (render-toolbar elapsed-ms metrics request')]
(assoc response :body (str/replace html-str "</body>" (str toolbar-str "</body>"))))
response))))
raise))))})
Step 5: Verify the middleware compiles
Run: /clojure-eval (require 'com.getorcha.dev.toolbar :reload)
Expected: No errors.
Step 6: Commit
git add dev/com/getorcha/dev/toolbar.clj
git commit -m "feat(dev-toolbar): add middleware with SQL metrics tracking"
Files:
dev/com/getorcha/dev/toolbar.cljStep 1: Replace the placeholder render-toolbar with the full implementation
The toolbar uses a CSS checkbox hack for expand/collapse. It renders metrics on the left and actions on the right. It's a normal-flow block element (not fixed/floating).
(defn ^:private snapshot-controls
"Renders snapshot action controls (take/restore)."
[snapshots]
(list
[:form {:method "post" :action "/dev/snapshot"
:style "display:inline-flex;align-items:center;gap:6px"}
[:button {:type "submit"
:style "background:#10b981;color:#0d1117;border:none;padding:2px 8px;border-radius:3px;cursor:pointer;font:12px monospace"}
"Take Snapshot"]]
(when (seq snapshots)
[:form {:method "post"
:style "display:inline-flex;align-items:center;gap:6px;margin-left:12px"}
[:select {:name "timestamp"
:style "background:#21262d;color:#c9d1d9;border:1px solid #30363d;padding:2px 4px;border-radius:3px;font:11px monospace"}
(for [s (reverse snapshots)]
[:option {:value s} s])]
[:button {:type "submit"
:formaction "/dev/snapshot/restore"
:style "background:#f59e0b;color:#0d1117;border:none;padding:2px 8px;border-radius:3px;cursor:pointer;font:12px monospace"
:onclick "return confirm('Restore snapshot? This will replace all document data.')"}
"Restore"]])))
(defn ^:private route-actions
"Renders route-specific action buttons."
[request]
(let [match (reitit/match-by-path (::reitit/router request) (:uri request))
route-name (some-> match :data :name)]
(when (= route-name :com.getorcha.app.http.documents.view/detail)
(let [doc-id (get-in match [:path-params :document-id])]
[:form {:method "post" :action (str "/dev/documents/" doc-id "/delete")
:style "display:inline;margin-left:12px"
:onsubmit "return confirm('Delete this document and all related data?')"}
[:button {:type "submit"
:style "background:#f85149;color:#fff;border:none;padding:2px 8px;border-radius:3px;cursor:pointer;font:12px monospace"}
"Delete Document"]]))))
(defn ^:private list-snapshots
"Returns a sorted vector of snapshot timestamp directory names."
[]
(let [dir (java.io.File. "dev/snapshots")]
(if (.isDirectory dir)
(->> (.listFiles dir)
(filter #(.isDirectory ^java.io.File %))
(mapv #(.getName ^java.io.File %))
sort
vec)
[])))
(defn ^:private render-toolbar
"Renders the dev toolbar as an HTML string.
The toolbar is a normal-flow element (not fixed) that pushes the footer up."
[request-ms sql-metrics request]
(let [query-count (count (:queries sql-metrics))
sql-ms (:total-sql-ms sql-metrics)
snapshots (list-snapshots)]
(str (hiccup/html
;; Hidden checkbox for expand/collapse — CSS-only, no JS
[:input#dev-toolbar-toggle
{:type "checkbox"
:style "display:none"}]
[:div {:style "background:#0d1117;border-top:2px solid #30363d;font:12px/1.4 monospace;color:#c9d1d9"}
;; Collapsed bar — always visible
[:label {:for "dev-toolbar-toggle"
:style "display:flex;justify-content:space-between;align-items:center;padding:4px 12px;cursor:pointer;user-select:none"}
[:span {:style "color:#8b949e"} "Dev Toolbar"]
[:span {:style "color:#58a6ff"}
(format "%.0fms" request-ms)
[:span {:style "color:#484f58;margin:0 6px"} "|"]
(format "%d SQL %.1fms" query-count sql-ms)]]
;; Expanded panel — shown when checkbox is checked via CSS sibling selector
;; The #dev-toolbar-toggle:checked ~ div > .dev-toolbar-expanded selector
;; is handled by an inline <style> tag below
[:div.dev-toolbar-expanded
{:style "display:none;padding:8px 12px;border-top:1px solid #21262d"}
[:div {:style "display:flex;justify-content:space-between;align-items:flex-start"}
;; Left: metrics
[:div {:style "display:flex;gap:24px"}
[:div
[:span {:style "color:#8b949e"} "Request "]
[:span {:style "color:#58a6ff"} (format "%.1fms" request-ms)]]
[:div
[:span {:style "color:#8b949e"} "SQL "]
[:span {:style "color:#58a6ff"} (str query-count " queries")]
[:span {:style "color:#484f58;margin:0 4px"} "/"]
[:span {:style "color:#58a6ff"} (format "%.1fms" sql-ms)]]]
;; Right: actions
[:div {:style "display:flex;align-items:center"}
(snapshot-controls snapshots)
(route-actions request)]]]]
;; Style tag for the checkbox hack — sibling selector
[:style (hiccup.util/raw-string
"#dev-toolbar-toggle:checked ~ div .dev-toolbar-expanded { display: block !important; }")]))))
Step 2: Verify rendering via REPL
Run:
(require 'com.getorcha.dev.toolbar :reload)
(@(requiring-resolve 'com.getorcha.dev.toolbar/render-toolbar) 42.5 {:queries [{:ms 3.2}] :total-sql-ms 3.2} {})
Expected: HTML string containing "Dev Toolbar", metrics, and action forms.
Step 3: Commit
git add dev/com/getorcha/dev/toolbar.clj
git commit -m "feat(dev-toolbar): full toolbar UI with expand/collapse and actions"
Files:
dev/com/getorcha/dev/toolbar.cljStep 1: Add the routes function
This returns a Reitit route vector. The snapshot and delete logic is delegated to
com.getorcha.dev.toolbar.snapshot (Task 7-8). For now, stub the handlers:
(defn routes
"Dev-only routes for snapshot/restore/delete actions.
Registered conditionally via requiring-resolve in app.http."
[_config]
["/dev" {:middleware [(app.http.middleware.auth/wrap-authentication)]}
["/snapshot"
["" {:name ::take-snapshot
:post {:handler (fn [{:keys [db-pool aws] :as _request} respond _raise]
;; Task 7 will implement the real logic
(let [snapshot-fn (requiring-resolve 'com.getorcha.dev.toolbar.snapshot/take-snapshot!)]
(snapshot-fn db-pool aws)
(respond (ring.resp/found "/ap"))))}}]
["/restore" {:name ::restore-snapshot
:post {:handler (fn [{:keys [db-pool aws] :as request} respond _raise]
(let [timestamp (get-in request [:form-params "timestamp"])
restore-fn (requiring-resolve 'com.getorcha.dev.toolbar.snapshot/restore-snapshot!)]
(restore-fn db-pool aws timestamp)
(respond (ring.resp/found "/ap"))))}}]]
["/documents/:document-id/delete"
{:name ::delete-document
:post {:handler (fn [{:keys [db-pool aws] :as request} respond _raise]
(let [doc-id (parse-uuid (get-in request [:path-params :document-id]))
delete-fn (requiring-resolve 'com.getorcha.dev.toolbar.snapshot/delete-document!)]
(delete-fn db-pool aws doc-id)
(respond (ring.resp/found "/ap"))))
:parameters {:path [:map [:document-id :string]]}}}]])
Note: The routes use requiring-resolve for snapshot functions too — this means
toolbar/snapshot.clj is only loaded when a snapshot action is first triggered, not on
every request.
Step 2: Commit
git add dev/com/getorcha/dev/toolbar.clj
git commit -m "feat(dev-toolbar): add dev routes for snapshot/restore/delete"
Files:
dev/com/getorcha/dev/toolbar/snapshot.cljReference files:
scripts/debug_fetch_document.clj — EDN serialization patterns for PGobject, enumsscripts/debug_common.clj — DB type handling for inserts (JSONB cast, enum cast, etc.)Step 1: Create the namespace with EDN serialization
(ns com.getorcha.dev.toolbar.snapshot
"Snapshot/restore logic for dev toolbar.
Dumps 9 document-related tables to EDN and syncs S3 bucket contents.
Handles EDN serialization of PGobject (JSONB), UUID, java.time types."
(:require [clojure.java.io :as io]
[clojure.string :as str]
[clojure.tools.reader.edn :as edn]
[com.getorcha.db.sql :as db.sql]
[honey.sql :as honey]
[next.jdbc :as jdbc]
[next.jdbc.result-set :as jdbc.rs])
(:import (java.io PushbackReader)
(java.time Instant LocalDate LocalDateTime OffsetDateTime)
(java.time.format DateTimeFormatter)
(java.util UUID)
(org.postgresql.util PGobject)))
Step 2: EDN writer — custom print methods
Register print methods for types that don't have default EDN representation.
These are only loaded in dev (this namespace lives in dev/):
;; Tagged literal writers for EDN round-tripping
(defmethod print-method Instant [^Instant v ^java.io.Writer w]
(.write w (str "#inst \"" (.toString v) "\"")))
(defmethod print-method LocalDate [^LocalDate v ^java.io.Writer w]
(.write w (str "#local-date \"" (.toString v) "\"")))
(defmethod print-method LocalDateTime [^LocalDateTime v ^java.io.Writer w]
(.write w (str "#local-date-time \"" (.toString v) "\"")))
(defmethod print-method OffsetDateTime [^OffsetDateTime v ^java.io.Writer w]
(.write w (str "#offset-date-time \"" (.toString v) "\"")))
(defmethod print-method PGobject [^PGobject v ^java.io.Writer w]
(.write w (str "#pg \"" (.getValue v) "\"")))
Step 3: EDN reader — tagged literal readers
(def ^:private edn-readers
"Tagged literal readers for snapshot EDN files."
{'inst #(Instant/parse %)
'local-date #(LocalDate/parse %)
'local-date-time #(LocalDateTime/parse %)
'offset-date-time #(OffsetDateTime/parse %)
'pg (fn [s]
(doto (PGobject.)
(.setType "jsonb")
(.setValue s)))
'uuid #(UUID/fromString %)})
Step 4: Table definitions
(def ^:private snapshot-tables
"Tables to snapshot, in dependency order for inserts.
Reverse this order for truncation."
[:document_cluster
:document
:ingestion
:ingestion_post_process_stat
:document_match
:supplier_verification
:supplier_verification_document
:qa_dataset_item
:datev_export_audit])
Step 5: Dump functions
(defn ^:private snapshot-dir
"Returns the path for a snapshot directory."
^java.io.File [timestamp]
(io/file "dev" "snapshots" timestamp))
(defn ^:private dump-tables
"Dumps all snapshot tables to a single EDN file."
[db-pool ^java.io.File dir]
(let [data (into {}
(for [table snapshot-tables]
[table (db.sql/execute! db-pool {:select [:*] :from [table]})]))]
(spit (io/file dir "snapshot.edn")
(pr-str data))))
Step 6: Restore functions
(defn ^:private current-columns
"Returns the set of column names (as keywords) for a table."
[db-pool table-name]
(->> (jdbc/execute! db-pool
[(str "SELECT column_name FROM information_schema.columns WHERE table_name = ?")
(name table-name)]
{:builder-fn jdbc.rs/as-unqualified-kebab-maps})
(mapv (comp keyword :column-name))
set))
(defn ^:private filter-to-schema
"Filters row maps to only include keys that exist in the current schema."
[rows valid-columns]
(mapv #(select-keys % valid-columns) rows))
(defn ^:private restore-tables
"Restores tables from a snapshot EDN file.
Truncates in reverse dependency order, inserts in dependency order.
Filters columns to match current schema."
[db-pool ^java.io.File dir]
(let [data (with-open [r (PushbackReader. (io/reader (io/file dir "snapshot.edn")))]
(edn/read {:readers edn-readers} r))]
(jdbc/with-transaction [tx db-pool]
;; Truncate in reverse order
(doseq [table (reverse snapshot-tables)]
(jdbc/execute! tx [(str "TRUNCATE TABLE " (honey/format-entity table) " CASCADE")]))
;; Insert in dependency order
(doseq [table snapshot-tables
:let [rows (get data table)]
:when (seq rows)]
(let [valid-cols (current-columns tx table)
;; next-jdbc needs unqualified keys for insert
clean-rows (->> rows
(filter-to-schema valid-cols)
(mapv (fn [row]
(into {} (map (fn [[k v]] [(keyword (name k)) v])) row))))]
(jdbc/execute-batch! tx
(str "INSERT INTO " (honey/format-entity table)
" (" (str/join ", " (map (comp honey/format-entity name) (keys (first clean-rows)))) ")"
" VALUES (" (str/join ", " (repeat (count (keys (first clean-rows))) "?")) ")")
(mapv (fn [row] (mapv row (keys (first clean-rows)))) clean-rows)
{}))))))
Step 7: Timestamp generation
(defn ^:private timestamp-now
"Returns a filesystem-safe timestamp string."
[]
(.format (LocalDateTime/now) (DateTimeFormatter/ofPattern "yyyy-MM-dd'T'HH-mm-ss")))
Step 8: Public API — take-snapshot!
(defn take-snapshot!
"Takes a snapshot of document tables and S3 bucket."
[db-pool aws]
(let [ts (timestamp-now)
dir (snapshot-dir ts)]
(.mkdirs dir)
(dump-tables db-pool dir)
(sync-s3-down! aws dir) ;; Task 8
ts))
Step 9: Public API — restore-snapshot!
(defn restore-snapshot!
"Restores a snapshot. Truncates tables + clears S3 bucket, then repopulates."
[db-pool aws timestamp]
(let [dir (snapshot-dir timestamp)]
(when-not (.isDirectory dir)
(throw (ex-info (str "Snapshot not found: " timestamp) {:timestamp timestamp})))
(restore-tables db-pool dir)
(sync-s3-up! aws dir))) ;; Task 8
Step 10: Commit
git add dev/com/getorcha/dev/toolbar/snapshot.clj
git commit -m "feat(dev-toolbar): snapshot dump/restore for document tables"
Files:
dev/com/getorcha/dev/toolbar/snapshot.cljReference: src/com/getorcha/aws.clj — for S3Client type hints and existing get/put/delete patterns.
Step 1: Add S3 imports
Add to the :import block:
(software.amazon.awssdk.services.s3 S3Client)
(software.amazon.awssdk.services.s3.model
DeleteObjectRequest GetObjectRequest ListObjectsV2Request
PutObjectRequest S3Object)
(software.amazon.awssdk.core.sync RequestBody)
Step 2: Implement list-all-objects
(defn ^:private list-all-objects
"Lists all object keys in an S3 bucket. Handles pagination."
[^S3Client s3-client bucket]
(loop [keys []
c-token nil]
(let [request (cond-> (-> (ListObjectsV2Request/builder)
(.bucket bucket))
c-token (.continuationToken c-token)
true (.build))
resp (.listObjectsV2 s3-client request)
new-keys (into keys (map #(.key ^S3Object %) (.contents resp)))]
(if (.isTruncated resp)
(recur new-keys (.nextContinuationToken resp))
new-keys))))
Step 3: Implement S3 download (sync down)
(defn ^:private sync-s3-down!
"Downloads all objects from the storage bucket to the snapshot directory."
[{:keys [clients s3-buckets] :as _aws} ^java.io.File snapshot-dir]
(let [s3-client ^S3Client (:s3 clients)
bucket (:storage s3-buckets)
s3-dir (io/file snapshot-dir "s3")
all-keys (list-all-objects s3-client bucket)]
(doseq [k all-keys]
(let [target (io/file s3-dir k)]
(.mkdirs (.getParentFile target))
(let [bytes (com.getorcha.aws/get-object s3-client bucket k)]
(with-open [out (io/output-stream target)]
(.write out ^bytes bytes)))))))
Step 4: Implement S3 upload (sync up)
(defn ^:private sync-s3-up!
"Clears the S3 bucket and uploads all files from the snapshot's s3/ directory."
[{:keys [clients s3-buckets] :as _aws} ^java.io.File snapshot-dir]
(let [s3-client ^S3Client (:s3 clients)
bucket (:storage s3-buckets)
s3-dir (io/file snapshot-dir "s3")]
;; Delete all existing objects
(doseq [k (list-all-objects s3-client bucket)]
(com.getorcha.aws/delete-object! s3-client bucket k))
;; Upload snapshot files
(when (.isDirectory s3-dir)
(let [s3-path (.toPath s3-dir)]
(doseq [^java.io.File f (file-seq s3-dir)
:when (.isFile f)]
(let [key (str (.relativize s3-path (.toPath f)))]
(com.getorcha.aws/put-object! s3-client bucket key
(java.nio.file.Files/readAllBytes (.toPath f))
"application/octet-stream")))))))
Step 5: Implement delete-document!
(defn delete-document!
"Deletes a document and all related data from DB and S3."
[db-pool {:keys [clients s3-buckets] :as _aws} document-id]
(let [s3-client ^S3Client (:s3 clients)
bucket (:storage s3-buckets)]
(jdbc/with-transaction [tx db-pool]
;; Find related ingestion IDs for S3 cleanup
(let [ingestion-ids (->> (db.sql/execute! tx {:select [:id] :from [:ingestion]
:where [:= :document-id document-id]})
(mapv :ingestion/id))]
;; Delete from tables in reverse dependency order
(doseq [table [:datev_export_audit :qa_dataset_item
:supplier_verification_document :supplier_verification
:document_match :ingestion_post_process_stat :ingestion]]
(db.sql/execute! tx {:delete-from table
:where [:= :document-id document-id]}))
;; document_match has document_a_id and document_b_id
(db.sql/execute! tx {:delete-from :document_match
:where [:or
[:= :document-a-id document-id]
[:= :document-b-id document-id]]})
;; Finally delete the document itself
(db.sql/execute! tx {:delete-from :document
:where [:= :id document-id]})
;; S3 cleanup — delete document files and ingestion artifacts
(doseq [k (list-all-objects s3-client bucket)]
(when (or (str/starts-with? k (str "documents/" document-id))
(some #(str/starts-with? k (str "ingestions/" %)) ingestion-ids))
(com.getorcha.aws/delete-object! s3-client bucket k)))))))
Step 6: Verify compilation
Run: /clojure-eval (require 'com.getorcha.dev.toolbar.snapshot :reload)
Expected: No errors.
Step 7: Commit
git add dev/com/getorcha/dev/toolbar/snapshot.clj
git commit -m "feat(dev-toolbar): S3 sync and document delete for snapshots"
app/http.cljFiles:
src/com/getorcha/app/http.cljStep 1: Modify the router function
Add dev? to the destructured keys and conditionally load dev routes and middleware.
The key changes are in two places:
(defn router
[{:keys [session dev?] :as config}]
(let [dev-routes (when dev?
(try ((requiring-resolve 'com.getorcha.dev.toolbar/routes) config)
(catch Exception _ nil)))
dev-middleware (when dev?
(try (requiring-resolve 'com.getorcha.dev.toolbar/wrap-dev-toolbar)
(catch Exception _ nil)))]
(r.ring/router
[;; Health check
["/health" {:handler (fn [_request respond _raise]
(log/debug "ok health")
(respond {:status 200 :body [:p "ok"]}))}]
;; Unauthenticated views
(app.http.connect.datev-rewe/routes config)
(app.http.login/routes config)
(app.http.webhooks/routes config)
(app.http.settings.notifications/verify-routes config)
;; Dev routes (nil when not in dev mode)
dev-routes
;; Authenticated views
["" {:middleware [(app.http.middleware.auth/wrap-authentication)]}
(app.http.ap/routes config)
(app.http.documents/routes config)
(app.http.oauth/routes config)
(app.http.profile/routes config)
(app.http.settings.booking-history/routes config)
(app.http.settings.data/routes config)
(app.http.settings.google-drive/routes config)
(app.http.settings.integrations/routes config)
(app.http.settings.notifications/routes config)
(app.http.notifications/routes config)]]
{:data {:muuntaja http.formats/muuntaja
:coercion reitit.coercion.malli/coercion
:middleware (cond-> [xray/wrap-xray-tracing
ring.middleware.cookies/wrap-cookies
[ring.session/wrap-session
{:store (http.session/postgres-session-store config)
:cookie-name "orcha-app-session"
:cookie-attrs {:http-only true
:same-site :lax
:secure (:secure? session)
:path "/"}}]
r.ring.middleware.parameters/parameters-middleware
r.ring.middleware.muuntaja/format-negotiate-middleware
r.ring.middleware.muuntaja/format-response-middleware
[app.http.error/wrap-error-pages]
http.middleware/exception-middleware
r.ring.middleware.muuntaja/format-request-middleware
r.ring.coercion/coerce-response-middleware
r.ring.coercion/coerce-request-middleware
r.ring.middleware.multipart/multipart-middleware
[http.middleware/inject-config (dissoc config :session)]]
dev-middleware (conj dev-middleware))}
#_#_
:reitit.middleware/transform (requiring-resolve 'reitit.ring.middleware.dev/print-request-diffs)})))
Step 2: Verify — reset the system
Run: /clojure-eval (reset) (with 30s timeout since system restart is slow)
Expected: System restarts. Console shows "Started server on 8888."
Step 3: Verify — visit a page
Open https://orcha.barreto.tech/ap in a browser. The toolbar should appear at the
bottom of the page (collapsed). Click it to expand and see metrics.
Step 4: Commit
git add src/com/getorcha/app/http.clj
git commit -m "feat(dev-toolbar): wire up conditional loading via requiring-resolve"
No automated tests for this feature — it's dev-only UI tooling. Verify manually:
Step 1: Toolbar appears on pages
/ap — toolbar at bottom, collapsed/documents/<some-id> — toolbar shows "Delete Document" buttonStep 2: Snapshot/restore cycle
/ap/ap, snapshot created in dev/snapshots/dev/snapshots/<timestamp>/snapshot.edn exists and contains datadev/snapshots/<timestamp>/s3/ contains document files/ap — dropdown should show the snapshotStep 3: Lint
Run: clj-kondo --lint dev
Expected: No errors.
Step 4: Final commit if any fixes needed
Review all commits, ensure they're clean. The feature is complete when:
:extra-pathsdev/ included:dev? to handlerdev/snapshots/ is gitignoredrequiring-resolve with try/catch