diff --git a/README.md b/README.md
index d7c7dd9f..2cd4dbb5 100644
--- a/README.md
+++ b/README.md
@@ -21,10 +21,6 @@ When running locally, set the `is_auth_disabled` flag to `true` in [local-config
If you want to test with Okta OAuth, set the `is_auth_disabled` flag to `false`, and fill in the `okta_client_secret` with the value under "Okta localhost deployment" in 1Password.
-### Working on the UI
-
-Per the above setup steps, the UI runs on `http://localhost:8089/`. To see your local UI changes reflected in the browser, you will need to hard reload (`Crtl+Shift+R`) while on the specific web page you have changed.
-
## V2 API
The v2 API is based on individual route provider classes. Each class should provide exactly one endpoint and must implement IRouteProvider or IBlockingRouteProvider.
@@ -36,3 +32,174 @@ The v2 API is based on individual route provider classes. Each class should prov
IRouteProvider requires a `getHandler` method, which should return a valid handler function - see `GetClientSideKeypairsBySite.java`. This method *must* be annotated with the Path, Method, and Roles annotations.
The route handler will automatically be wrapped by the Auth middleware based on the roles specified in the Roles annotation.
+
+## Working on the UI
+Per the above setup steps, the UI runs on `http://localhost:8089/`. To see your local UI changes reflected in the browser, you will need to hard reload (`Crtl+Shift+R`) while on the specific web page you have changed.
+
+### Page setup
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Operations
+```javascript
+const myOperation = {
+ id: 'operationId', // Required: Unique identifier
+ title: 'Operation Title', // Required: Display name
+ role: 'maintainer', // Required: `maintainer`, `elevated` or `superuser`
+ inputs: [ // Optional: Inputs (see below)
+ {
+ name: 'field1', // Value of this input will be available as property matching name on `inputs` object below.
+ label: 'Field 1', // Shown on the UI above the field
+ type: 'text' // Optional, text by default
+ }
+ ],
+ description: 'text', // Optional: Description will appear within the accordion
+ confirmationText: 'text', // Optional: Danger zone confirmation text, will replace stock text if used. Only works in danger zone.
+ apiCall: { // Optional: API configuration
+ method: 'POST',
+ url: '', // Either hardcore URL here or use `getUrl` below.
+ getUrl: (inputs) => { // Either construct a URL from the inputs or use `url` above.
+ return `/api/my/api?field1=${encodeURIComponent(inputs.field1)}`
+ },
+ getPayload: (inputs) => { // Optional: Construct a JS object that will be sent as body
+ return { field1: inputs.field1 };
+ }
+ },
+ preProcess: async (inputs) => inputs, // Optional: Run before getUrl/getPayload and the API call
+ postProcess: async (data, inputs) => data // Optional: Run after apiCall, if you want to adjust the response
+};
+
+```
+
+### Inputs
+- Inputs on the same page that share a `name` will have their values synced.
+
+#### text (default)
+Basic small text input
+```javascript
+const myText = {
+ name: 'fieldName',
+ label: 'Field Label',
+ required: true, // Optional: Exclude button won't be enabled until all fields with requireds are filled. Default false.
+ size: 2, // Optional: grid span 1-3, grid has 3 columns. Default 1.
+ placeholder: 'Enter text', // Optional
+ defaultValue: 'default' // Optional
+};
+```
+
+#### multi-line
+A larger multi-line text input box
+```javascript
+const myTextArea = {
+ name: 'textArea',
+ type: 'multi-line',
+ label: 'Multi-line Text',
+ placeholder: 'Enter text...',
+ defaultValue: 'default text'
+};
+```
+
+#### number
+Number-specific input, won't allow non-number text
+```javascript
+const myNum = {
+ name: 'numField',
+ type: 'number',
+ label: 'Number Field'
+};
+```
+
+#### date
+Date-specific input with date picker
+```javascript
+const myDate = {
+ name: 'dateField',
+ type: 'date',
+ defaultValue: '2024-01-01'
+};
+```
+
+#### checkbox
+```javascript
+const myCheckbox = {
+ name: 'boolField',
+ type: 'checkbox',
+ label: 'Is it true?',
+ defaultValue: true
+};
+```
+
+#### select
+Dropdown to select one option from a list
+```javascript
+const myDropdown = {
+ name: 'Dropdown',
+ type: 'select',
+ label: 'Select Option',
+ options: [
+ 'simple', // Can pass as string if option values are same as UI text
+ { value: 'val', label: 'Display' } // Or as Objects if they are different
+ ],
+ defaultValue: 'val'
+};
+```
+
+#### multi-select
+Multiple checkboxes to allow selecting more than one option
+
+```javascript
+const myMultiSelect = {
+ name: 'Multi Select',
+ type: 'multi-select',
+ label: 'Multiple Options',
+ required: true,
+ options: [
+ {
+ value: 'OPTION_1',
+ label: 'Option One',
+ hint: 'Tooltip explanation' // Optional, appear as info icons next to options
+ }
+ ],
+ defaultValue: ['OPTION_1'] // Array or comma-separated string
+}
+```
diff --git a/webroot/adm/client-key.html b/webroot/adm/client-key.html
index 309261ae..d667b28c 100644
--- a/webroot/adm/client-key.html
+++ b/webroot/adm/client-key.html
@@ -1,257 +1,285 @@
-
-
-
+
+
+
+