Note (2026-04-24): After this document was written, legal_entity was renamed to tenant and the old tenant was renamed to organization. Read references to these terms with the pre-rename meaning.

File Store Admin UI — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Admin UI to configure FP&A file store per legal entity, with connection testing on save.

Architecture: New file-store-admin multimethod for backend descriptors with Malli form schemas. Generic schema-to-form renderer emits hiccup from Malli schemas. HTMX-driven admin page at /tenants/-/legal-entities/:le-id/file-store with save (validates + tests connection), test existing, and remove actions.

Tech Stack: Clojure, Malli (schemas + coercion + validation), HTMX, Hiccup, Reitit, next-jdbc, HoneySQL

Design doc: docs/plans/2026-03-06-file-store-admin-design.md


Task 1: Add file-store-admin Multimethod

Files:

Step 1: Write the failing test

Add to test/com/getorcha/link/mcp/file_store/local_test.clj:

(deftest test-file-store-admin-descriptor
  (let [desc (file-store/file-store-admin "file")]
    (testing "returns display name"
      (is (= "Local Filesystem" (:display-name desc))))
    (testing "returns a Malli map schema"
      (is (= :map (m/type (:form-schema desc)))))
    (testing "schema has :path field"
      (let [children (m/children (:form-schema desc))
            field-keys (set (map first children))]
        (is (contains? field-keys :path))))))

Requires adding [malli.core :as m] to the test ns require.

Step 2: Run test to verify it fails

Run: clj -X:test:silent :nses '[com.getorcha.link.mcp.file-store.local-test]' Expected: FAIL — file-store-admin does not exist.

Step 3: Add the multimethod to file_store.clj

Add after make-file-store (after line 42):

(defmulti file-store-admin
  "Returns an admin descriptor for a file store type.

  Only backends that register a method here appear in the admin UI.
  Dispatches on the protocol string (e.g., `\"file\"`, `\"s3\"`).

  Returns a map:
  - `:display-name` — human-readable label for the type selector
  - `:form-schema`  — flat Malli `:map` schema with `:form/*` properties"
  identity)

Step 4: Register the local backend descriptor in local.clj

Add at the bottom of src/com/getorcha/link/mcp/file_store/local.clj:

(defmethod file-store/file-store-admin "file" [_]
  {:display-name "Local Filesystem"
   :form-schema  [:map
                  [:path [:string {:min           1
                                   :form/label    "Directory Path"
                                   :form/placeholder "/data/acme-gmbh/"
                                   :error/message "Directory path is required"}]]]})

Step 5: Run test to verify it passes

Run: clj -X:test:silent :nses '[com.getorcha.link.mcp.file-store.local-test]' Expected: PASS

Step 6: Commit

git add src/com/getorcha/link/mcp/file_store.clj src/com/getorcha/link/mcp/file_store/local.clj test/com/getorcha/link/mcp/file_store/local_test.clj
git commit -m "feat: add file-store-admin multimethod with local backend descriptor"

Task 2: Schema-to-Form Renderer

A generic utility that renders hiccup form fields from a flat Malli :map schema. Not file-store-specific — reusable for any admin form.

Files:

Step 1: Write the failing tests

Create test/com/getorcha/admin/ui/schema_form_test.clj:

(ns com.getorcha.admin.ui.schema-form-test
  (:require [clojure.test :refer [deftest is testing]]
            [com.getorcha.admin.ui.schema-form :as schema-form]))


(def test-schema
  [:map
   [:name [:string {:min       1
                    :form/label "Full Name"
                    :form/placeholder "John Doe"}]]
   [:count [:int {:form/label "Count"
                  :min        0
                  :max        100}]]
   [:role [:enum {:form/label "Role"}
           "admin" "viewer"]]
   [:active [:boolean {:form/label "Active?"}]]
   [:notes {:optional true}
    [:string {:form/label "Notes"
              :form/type  :textarea
              :form/rows  4}]]])


(deftest test-render-fields-string
  (let [fields (schema-form/render-fields test-schema {})]
    (testing "renders a string field as text input"
      (let [name-field (first (filter #(= "name" (get-in % [2 :name])) (rest fields)))]
        (is (some? name-field))
        ;; Should contain a label
        (is (some #(and (vector? %) (= :label (first %))) (rest name-field)))
        ;; Should contain an input
        (is (some #(and (vector? %) (= :input (first %))) (rest name-field)))))))


(deftest test-render-fields-enum
  (let [fields (schema-form/render-fields test-schema {})]
    (testing "renders an enum field as select"
      (let [role-field (first (filter #(= "role" (get-in % [2 :name])) (rest fields)))]
        (is (some? role-field))
        (is (some #(and (vector? %) (= :select (first %))) (rest role-field)))))))


(deftest test-render-fields-with-values
  (let [fields (schema-form/render-fields test-schema {:values {:name "Alice" :count 42}})]
    (testing "pre-fills values"
      (let [name-field (first (filter #(= "name" (get-in % [2 :name])) (rest fields)))
            input (first (filter #(and (vector? %) (= :input (first %))) (rest name-field)))]
        (is (= "Alice" (get-in input [1 :value])))))))


(deftest test-render-fields-with-errors
  (let [fields (schema-form/render-fields test-schema {:errors {:name ["is required"]}})]
    (testing "renders error messages"
      (let [name-field (first (filter #(= "name" (get-in % [2 :name])) (rest fields)))]
        (is (some #(and (vector? %)
                        (= :div.field-errors (first %)))
                  (rest name-field)))))))


(deftest test-render-fields-textarea
  (let [fields (schema-form/render-fields test-schema {})]
    (testing "renders :form/type :textarea as textarea element"
      (let [notes-field (first (filter #(= "notes" (get-in % [2 :name])) (rest fields)))]
        (is (some #(and (vector? %) (= :textarea (first %))) (rest notes-field)))))))


(deftest test-render-fields-boolean
  (let [fields (schema-form/render-fields test-schema {})]
    (testing "renders boolean as checkbox"
      (let [active-field (first (filter #(= "active" (get-in % [2 :name])) (rest fields)))]
        (is (some? active-field))
        (let [input (first (filter #(and (vector? %) (= :input (first %))) (rest active-field)))]
          (is (= "checkbox" (get-in input [1 :type]))))))))


(deftest test-render-fields-int
  (let [fields (schema-form/render-fields test-schema {})]
    (testing "renders int as number input"
      (let [count-field (first (filter #(= "count" (get-in % [2 :name])) (rest fields)))]
        (is (some? count-field))
        (let [input (first (filter #(and (vector? %) (= :input (first %))) (rest count-field)))]
          (is (= "number" (get-in input [1 :type]))))))))

Step 2: Run tests to verify they fail

Run: clj -X:test:silent :nses '[com.getorcha.admin.ui.schema-form-test]' Expected: FAIL — namespace does not exist.

Step 3: Implement the schema-to-form renderer

Create src/com/getorcha/admin/ui/schema_form.clj:

(ns com.getorcha.admin.ui.schema-form
  "Renders hiccup form fields from a flat Malli :map schema.

  Schema fields use `:form/*` properties for UI hints:
  - `:form/label`       — field label text
  - `:form/placeholder` — input placeholder
  - `:form/type`        — override input type (e.g., `:textarea`, `:email`)
  - `:form/rows`        — textarea row count"
  (:require [malli.core :as m]))


(defmulti ^:private render-field
  "Renders a single form field based on its Malli type."
  (fn [_key _entry-props schema _opts] (m/type schema)))


(defmethod render-field :default [key entry-props schema opts]
  (let [props  (m/properties schema)
        label  (or (:form/label props) (name key))
        fname  (name key)
        ftype  (or (some-> (:form/type props) name) "text")
        value  (get (:values opts) key "")]
    [:div.form-group {:name fname}
     [:label.form-label {:for fname} label]
     [:input.form-input {:id          fname
                         :name        fname
                         :type        ftype
                         :placeholder (:form/placeholder props)
                         :value       value
                         :required    (not (:optional entry-props))
                         :minlength   (:min props)
                         :maxlength   (:max props)}]
     (when-let [errors (get (:errors opts) key)]
       [:div.field-errors
        (for [msg errors]
          [:span.field-error msg])])]))


(defmethod render-field :string [key entry-props schema opts]
  (let [props  (m/properties schema)
        label  (or (:form/label props) (name key))
        fname  (name key)
        value  (get (:values opts) key "")]
    (if (= :textarea (:form/type props))
      [:div.form-group {:name fname}
       [:label.form-label {:for fname} label]
       [:textarea.form-input {:id          fname
                              :name        fname
                              :rows        (or (:form/rows props) 3)
                              :placeholder (:form/placeholder props)
                              :required    (not (:optional entry-props))}
        value]
       (when-let [errors (get (:errors opts) key)]
         [:div.field-errors
          (for [msg errors]
            [:span.field-error msg])])]
      [:div.form-group {:name fname}
       [:label.form-label {:for fname} label]
       [:input.form-input {:id          fname
                           :name        fname
                           :type        (or (some-> (:form/type props) name) "text")
                           :placeholder (:form/placeholder props)
                           :value       value
                           :required    (not (:optional entry-props))
                           :minlength   (:min props)
                           :maxlength   (:max props)}]
       (when-let [errors (get (:errors opts) key)]
         [:div.field-errors
          (for [msg errors]
            [:span.field-error msg])])])))


(defmethod render-field :int [key entry-props schema opts]
  (let [props  (m/properties schema)
        label  (or (:form/label props) (name key))
        fname  (name key)
        value  (get (:values opts) key "")]
    [:div.form-group {:name fname}
     [:label.form-label {:for fname} label]
     [:input.form-input {:id       fname
                         :name     fname
                         :type     "number"
                         :value    value
                         :min      (:min props)
                         :max      (:max props)
                         :required (not (:optional entry-props))}]
     (when-let [errors (get (:errors opts) key)]
       [:div.field-errors
        (for [msg errors]
          [:span.field-error msg])])]))


(defmethod render-field :enum [key entry-props schema opts]
  (let [props    (m/properties schema)
        label    (or (:form/label props) (name key))
        fname    (name key)
        children (m/children schema)
        value    (get (:values opts) key)]
    [:div.form-group {:name fname}
     [:label.form-label {:for fname} label]
     [:select.form-select {:id       fname
                           :name     fname
                           :required (not (:optional entry-props))}
      [:option {:value ""} "Select..."]
      (for [child children]
        [:option {:value    (str child)
                  :selected (= child value)}
         (str child)])]
     (when-let [errors (get (:errors opts) key)]
       [:div.field-errors
        (for [msg errors]
          [:span.field-error msg])])]))


(defmethod render-field :boolean [key _entry-props schema opts]
  (let [props  (m/properties schema)
        label  (or (:form/label props) (name key))
        fname  (name key)
        value  (get (:values opts) key false)]
    [:div.form-group {:name fname}
     [:input.form-checkbox {:id      fname
                            :name    fname
                            :type    "checkbox"
                            :checked value}]
     [:label.form-label {:for fname} label]
     (when-let [errors (get (:errors opts) key)]
       [:div.field-errors
        (for [msg errors]
          [:span.field-error msg])])]))


(defn render-fields
  "Renders hiccup form fields from a flat Malli `:map` schema.

  `schema` — a Malli `:map` schema with `:form/*` properties on fields.
  `opts`   — a map:
    - `:values` — map of field keyword → current value (for pre-filling)
    - `:errors` — map of field keyword → seq of error strings (from `me/humanize`)

  Returns a `:div.form-fields` containing one `:div.form-group` per field."
  [schema opts]
  (into [:div.form-fields]
        (for [[key entry-props value-schema] (m/children schema)]
          (render-field key entry-props value-schema opts))))

Step 4: Run tests to verify they pass

Run: clj -X:test:silent :nses '[com.getorcha.admin.ui.schema-form-test]' Expected: PASS

Step 5: Commit

git add src/com/getorcha/admin/ui/schema_form.clj test/com/getorcha/admin/ui/schema_form_test.clj
git commit -m "feat: generic Malli schema-to-form renderer for admin UI"

Task 3: File Store Admin Routes, Handlers & Views

Files:

Context:

Step 1: Create the file store admin namespace

Create src/com/getorcha/admin/http/tenants/file_store.clj:

(ns com.getorcha.admin.http.tenants.file-store
  "File store configuration admin routes and views.

  Allows admins to configure, test, and remove FP&A file store
  settings per legal entity."
  (:require [cheshire.core :as json]
            [com.getorcha.admin.ui.layout :as layout]
            [com.getorcha.admin.ui.schema-form :as schema-form]
            [com.getorcha.db.sql :as db.sql]
            [com.getorcha.link.mcp.file-store :as file-store]
            ;; Load backends so their multimethods are registered
            [com.getorcha.link.mcp.file-store.local]
            [malli.core :as m]
            [malli.error :as me]
            [malli.transform :as mt]
            [next.jdbc :as jdbc]
            [ring.util.http-response :as ring.resp]))


;; Helpers
;; -----------------------------------------------------------------------------

(defn ^:private get-legal-entity
  "Fetch a legal entity by ID (name + fpna_data_source)."
  [db-pool legal-entity-id]
  (jdbc/execute-one! db-pool
                     ["SELECT id, name, fpna_data_source FROM legal_entity WHERE id = ?"
                      legal-entity-id]))


(defn ^:private available-backends
  "Returns a seq of [protocol-key descriptor] for all registered file store admin backends."
  []
  (for [protocol-key (keys (methods file-store/file-store-admin))]
    [protocol-key (file-store/file-store-admin protocol-key)]))


(defn ^:private save-data-source!
  "Persists a file store config as JSONB on the legal entity."
  [db-pool legal-entity-id config]
  (db.sql/execute-one!
   db-pool
   {:update :legal-entity
    :set    {:fpna-data-source [:cast (json/generate-string config) :jsonb]}
    :where  [:= :id legal-entity-id]}))


(defn ^:private clear-data-source!
  "Removes the file store config from a legal entity."
  [db-pool legal-entity-id]
  (db.sql/execute-one!
   db-pool
   {:update :legal-entity
    :set    {:fpna-data-source nil}
    :where  [:= :id legal-entity-id]}))


(defn ^:private test-file-store
  "Attempts to construct a FileStore and list root. Returns nil on success,
  or an error message string on failure."
  [config]
  (try
    (let [store (file-store/make-file-store config)]
      (file-store/list-files store "" {})
      nil)
    (catch Exception e
      (str "Connection test failed: " (.getMessage e)))))


(def ^:private form-transformer
  (mt/transformer
   (mt/strip-extra-keys-transformer {:accept (constantly true)})
   mt/string-transformer))


(defn ^:private decode-and-validate
  "Decodes form params with the given Malli schema and validates.
  Returns {:ok decoded-data} or {:errors humanized-errors}."
  [schema params]
  (let [decoded (m/decode schema params form-transformer)
        explain (m/explain schema decoded)]
    (if explain
      {:errors (me/humanize explain)}
      {:ok decoded})))


;; Views
;; -----------------------------------------------------------------------------

(defn ^:private status-message
  "Renders a success or error status message."
  [type message]
  [:div {:id "file-store-status" :class (str "alert " (case type :success "alert-success" "alert-danger"))}
   message])


(defn ^:private render-form-fields
  "Renders the backend-specific form fields for a protocol type."
  [protocol-key {:keys [values errors]}]
  (when-let [desc (file-store/file-store-admin protocol-key)]
    [:div#file-store-form-fields
     [:input {:type "hidden" :name "protocol" :value protocol-key}]
     (schema-form/render-fields (:form-schema desc) {:values values :errors errors})]))


(defn ^:private render-page
  "Renders the file store configuration page."
  [legal-entity data-source & [{:keys [values errors status] :as _opts}]]
  (let [{:legal-entity/keys [id name]} legal-entity
        backends   (available-backends)
        protocol   (:protocol data-source)
        configured (some? data-source)]
    [:div.page-content
     [:div.page-header
      [:a.btn.btn-ghost {:href "/tenants"} "← Back to tenants"]
      [:h1 (str name " — File Store Configuration")]]

     [:div#file-store-content
      (when status
        (status-message (:type status) (:message status)))

      (if configured
        ;; Configured state: show filled form
        [:div
         [:div.form-group
          [:label.form-label "Type"]
          [:div.form-value
           (:display-name (file-store/file-store-admin protocol))]]

         [:form {:hx-post   (str "/tenants/-/legal-entities/" id "/file-store")
                 :hx-target "#file-store-content"
                 :hx-swap   "innerHTML"}
          (render-form-fields protocol {:values (or values (dissoc data-source :protocol))
                                        :errors errors})
          [:div.form-actions
           [:button.btn.btn-primary {:type "submit"} "Save"]
           [:button.btn.btn-secondary {:type      "button"
                                       :hx-post   (str "/tenants/-/legal-entities/" id "/file-store/test")
                                       :hx-target "#file-store-status"
                                       :hx-swap   "outerHTML"}
            "Test Connection"]
           [:button.btn.btn-danger {:type       "button"
                                    :hx-delete  (str "/tenants/-/legal-entities/" id "/file-store")
                                    :hx-target  "#file-store-content"
                                    :hx-swap    "innerHTML"
                                    :hx-confirm "Remove file store configuration?"}
            "Remove"]]]
         [:div#file-store-status]]

        ;; Unconfigured state: show type selector + empty form
        [:div
         [:form {:hx-post   (str "/tenants/-/legal-entities/" id "/file-store")
                 :hx-target "#file-store-content"
                 :hx-swap   "innerHTML"}
          [:div.form-group
           [:label.form-label {:for "protocol"} "File Store Type"]
           [:select.form-select {:id       "protocol"
                                 :name     "protocol"
                                 :hx-get   (str "/tenants/-/legal-entities/" id "/file-store")
                                 :hx-target "#file-store-form-fields"
                                 :hx-swap   "outerHTML"
                                 :hx-include "this"
                                 :required  true}
            [:option {:value ""} "Select a file store type..."]
            (for [[proto-key desc] backends]
              [:option {:value proto-key} (:display-name desc)])]]

          [:div#file-store-form-fields
           (when-let [selected-protocol (or (:protocol values) (when (= 1 (count backends))
                                                                  (ffirst backends)))]
             (render-form-fields selected-protocol {:values values :errors errors}))]

          [:div.form-actions
           [:button.btn.btn-primary {:type "submit"} "Save"]]]]))]))


;; Route Handlers
;; -----------------------------------------------------------------------------

(defn ^:private index
  "GET - Show file store config page."
  [{:keys [db-pool parameters] :as request} respond _raise]
  (let [legal-entity-id (get-in parameters [:path :id])
        protocol-param  (get-in parameters [:query :protocol])]
    (if-let [legal-entity (get-legal-entity db-pool legal-entity-id)]
      (let [data-source (:legal-entity/fpna-data-source legal-entity)]
        (if protocol-param
          ;; HTMX request: return just the form fields for selected protocol
          (respond
           (ring.resp/ok
            (layout/partial-content
             (or (render-form-fields protocol-param {})
                 [:div#file-store-form-fields]))))
          ;; Full page request
          (respond
           (ring.resp/ok
            (layout/render request
                           {:title "File Store Configuration" :current-path "/tenants"}
                           (render-page legal-entity data-source))))))
      (respond
       (ring.resp/not-found
        (layout/partial-content
         [:div.alert.alert-danger "Legal entity not found."]))))))


(defn ^:private save!
  "POST - Save file store config (decode, validate, test connection, store)."
  [{:keys [db-pool parameters] :as _request} respond _raise]
  (let [legal-entity-id (get-in parameters [:path :id])
        protocol-key    (get-in parameters [:form :protocol])]
    (if-let [legal-entity (get-legal-entity db-pool legal-entity-id)]
      (if-let [desc (when (seq protocol-key) (file-store/file-store-admin protocol-key))]
        (let [{:keys [ok errors]} (decode-and-validate (:form-schema desc)
                                                       (get-in parameters [:form]))]
          (if errors
            ;; Validation failed — re-render with errors
            (let [data-source (:legal-entity/fpna-data-source legal-entity)]
              (respond
               (ring.resp/ok
                (layout/partial-content
                 (render-page legal-entity data-source
                              {:values (get-in parameters [:form])
                               :errors errors})))))
            ;; Validation passed — test connection
            (let [config     (assoc ok :protocol protocol-key)
                  test-error (test-file-store config)]
              (if test-error
                ;; Connection test failed
                (let [data-source (:legal-entity/fpna-data-source legal-entity)]
                  (respond
                   (ring.resp/ok
                    (layout/partial-content
                     (render-page legal-entity data-source
                                  {:values ok
                                   :status {:type :error :message test-error}})))))
                ;; All good — save
                (do
                  (save-data-source! db-pool legal-entity-id config)
                  (respond
                   (ring.resp/ok
                    (layout/partial-content
                     (render-page legal-entity config
                                  {:status {:type    :success
                                            :message "File store configured and connection verified."}})))))))))
        ;; Invalid protocol
        (respond
         (ring.resp/ok
          (layout/partial-content
           (render-page legal-entity nil
                        {:status {:type :error :message "Please select a file store type."}})))))
      (respond
       (ring.resp/not-found
        (layout/partial-content
         [:div.alert.alert-danger "Legal entity not found."]))))))


(defn ^:private test-connection!
  "POST /test - Test an existing saved file store config."
  [{:keys [db-pool parameters]} respond _raise]
  (let [legal-entity-id (get-in parameters [:path :id])
        legal-entity    (get-legal-entity db-pool legal-entity-id)
        data-source     (:legal-entity/fpna-data-source legal-entity)]
    (respond
     (ring.resp/ok
      (layout/partial-content
       (if-not data-source
         (status-message :error "No file store configured.")
         (if-let [error (test-file-store data-source)]
           (status-message :error error)
           (status-message :success "Connection successful."))))))))


(defn ^:private remove!
  "DELETE - Remove file store config."
  [{:keys [db-pool parameters]} respond _raise]
  (let [legal-entity-id (get-in parameters [:path :id])
        legal-entity    (get-legal-entity db-pool legal-entity-id)]
    (clear-data-source! db-pool legal-entity-id)
    (respond
     (ring.resp/ok
      (layout/partial-content
       (render-page legal-entity nil
                    {:status {:type :success :message "File store configuration removed."}}))))))


(defn routes
  "File store configuration routes."
  [_config]
  [["/tenants/-/legal-entities/:id/file-store"
    [""
     {:name ::index
      :get  {:parameters {:path  {:id :uuid}
                          :query [:map
                                  [:protocol {:optional true} :string]]}
             :handler    #'index}
      :post {:parameters {:path {:id :uuid}
                          :form [:map
                                 [:protocol :string]]}
             :handler    #'save!}
      :delete {:parameters {:path {:id :uuid}}
               :handler    #'remove!}}]
    ["/test"
     {:name ::test-connection
      :post {:parameters {:path {:id :uuid}}
             :handler    #'test-connection!}}]]])

Step 2: Register routes in admin HTTP router

Modify src/com/getorcha/admin/http.clj:

Add to requires (after line 11, alphabetically):

[com.getorcha.admin.http.tenants.file-store :as admin.http.tenants.file-store]

Add route registration (after line 46, next to prompt-customizations):

(admin.http.tenants.file-store/routes config)

Step 3: Add link in legal entity row

Modify src/com/getorcha/admin/http/tenants.clj:377-380.

Add a file-store link before the prompts link. After line 377 ([:td.actions), insert:

[:a.btn-icon {:href  (str "/tenants/-/legal-entities/" id "/file-store")
              :title "Configure file store"}
 [:iconify-icon {:icon "lucide:folder-cog"}]]

So the actions cell becomes:

[:td.actions
 [:a.btn-icon {:href  (str "/tenants/-/legal-entities/" id "/file-store")
               :title "Configure file store"}
  [:iconify-icon {:icon "lucide:folder-cog"}]]
 [:a.btn-icon {:href  (str "/tenants/-/legal-entities/" id "/prompts")
               :title "Customize prompts"}
  [:iconify-icon {:icon "lucide:message-square-text"}]]
 [:button.btn-icon {:type      "button"
                    :title     "Edit legal entity"
                    :hx-get    (str "/tenants/-/legal-entities/" id)
                    :hx-target "#modal"
                    :hx-swap   "innerHTML"}
  [:iconify-icon {:icon "lucide:pencil"}]]]

Step 4: Verify linting passes

Run: clj-kondo --lint src/com/getorcha/admin/http/tenants/file_store.clj src/com/getorcha/admin/http.clj Fix any issues.

Step 5: Commit

git add src/com/getorcha/admin/http/tenants/file_store.clj src/com/getorcha/admin/http.clj src/com/getorcha/admin/http/tenants.clj
git commit -m "feat: file store admin page with save, test, and remove"

Task 4: Manual Verification

Step 1: Start the system

Evaluate (reset) in the REPL (allow ~10s for worker pool shutdown).

Step 2: Navigate to admin

Open http://localhost:3001/tenants (or whatever the admin port is). Find a legal entity row — confirm the new folder icon link appears.

Step 3: Test unconfigured state

Click the file store icon for a legal entity that has no fpna_data_source. Verify:

Step 4: Test save with invalid path

Enter a non-existent path like /tmp/does-not-exist-xyz and click Save. Verify:

Step 5: Test save with valid path

Create a test directory: mkdir -p /tmp/orcha-test-fs Enter /tmp/orcha-test-fs and click Save. Verify:

Step 6: Test connection button

Click "Test Connection". Verify success message appears.

Step 7: Test remove

Click "Remove", confirm the dialog. Verify:

Step 8: Verify DB

psql -h localhost -U postgres -d orcha -c "SELECT id, name, fpna_data_source FROM legal_entity LIMIT 5"

Confirm the column is NULL after removal.

Step 9: Commit any fixes from manual testing

If any fixes were needed, commit them:

git add <fixed-files>
git commit -m "fix: file store admin adjustments from manual testing"