Explorar el Código

fix: tell path settings from expression settings, and stop caching index.html

PATH VERSUS EXPRESSION

Some settings take a bare path - Loop's Input Array Field wants
data.result.imagePosts - and others take an expression. Nothing said which, and
the available-data panel always hands over the {{ }} form, so dragging a field
into a path setting produced something that reads correctly and is not what the
setting takes.

Fourteen settings across the node library are now marked valueKind: 'path'.
Dragging a field into one inserts the path itself, and the field says which form
it takes. Only the plain {{ path }} form is unwrapped: {{$node["X"].y}} names a
different node, which a path cannot express, so that is left alone rather than
being silently turned into something that reads the wrong node.

INDEX.HTML WAS CACHEABLE

"nodesApi.options is not a function" on the SD.cpp model Browse button. The
method was there and the endpoint answered 200 - the browser was running a
bundle from an earlier build.

The server sent index.html with no cache headers at all, leaving the browser to
decide. index.html names the hashed bundle to load, so a stale copy points at a
file that no longer exists and the app breaks somewhere unrelated to the cause -
here, a missing method on an object.

The SPA fallback did set no-cache, but it never saw "/" : httplib answers file
requests before it consults registered handlers, so the mount point served the
root document first and a Get("/") handler would never have run either. The
headers now go on in the file request handler, where they cannot be bypassed.
Everything under /assets carries a content hash, so it is now marked immutable
rather than revalidated on every navigation.

Verified: index.html answers no-cache, /assets answers immutable, and Browse on
SD.cpp Load Model listed 33 models in a browser instead of failing. Full suite
58/58.
fszontagh hace 1 mes
padre
commit
c4b90e36e9

+ 1 - 0
nodes/ai/ollama-chat.js

@@ -74,6 +74,7 @@ const configSchema = {
             default: 'auto'
         },
         imageField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Image Field Override',
             description: 'Optional dot path to base64 image data, for example data.file.data',

+ 1 - 0
nodes/core/aggregate.js

@@ -11,6 +11,7 @@ const configSchema = {
     type: 'object',
     properties: {
         inputField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Array Field',
             description: 'Path to the items to aggregate',

+ 2 - 0
nodes/core/datetime.js

@@ -17,12 +17,14 @@ const configSchema = {
             default: 'format'
         },
         inputField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Input Field',
             description: 'Path to the date, as an ISO string or a millisecond timestamp',
             default: 'data'
         },
         secondField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Second Date Field',
             description: 'Path to the date to compare against, for the diff operation. diff returns the input date minus this one, so a later input gives a positive result'

+ 1 - 0
nodes/core/filter.js

@@ -16,6 +16,7 @@ const configSchema = {
     type: 'object',
     properties: {
         inputField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Array Field',
             description: 'Path to the array to filter, such as data.result.items',

+ 2 - 0
nodes/core/json.js

@@ -17,12 +17,14 @@ const configSchema = {
             default: 'parse'
         },
         inputField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Input Field',
             description: 'Path to the value to work on',
             default: 'data'
         },
         path: {
+            valueKind: 'path',
             type: 'string',
             title: 'Path',
             description: 'Dotted path to pull out, for the extract operation'

+ 1 - 0
nodes/core/loop.js

@@ -17,6 +17,7 @@ const configSchema = {
   type: 'object',
   properties: {
     inputField: {
+        valueKind: 'path',
       type: 'string',
       title: 'Input Array Field',
       description: 'Path to the array field to iterate over (e.g., data.items)',

+ 2 - 0
nodes/core/merge.js

@@ -28,11 +28,13 @@ const configSchema = {
             description: 'Field to match on, for combine-by-key'
         },
         path1: {
+            valueKind: 'path',
             type: 'string',
             title: 'Path In Input 1',
             description: 'Dotted path to the array inside input 1. Leave empty to use the input as it arrives'
         },
         path2: {
+            valueKind: 'path',
             type: 'string',
             title: 'Path In Input 2',
             description: 'Dotted path to the array inside input 2. Leave empty to use the input as it arrives'

+ 1 - 0
nodes/core/respond-to-webhook.js

@@ -30,6 +30,7 @@ const configSchema = {
             format: 'textarea'
         },
         bodyField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Body Field',
             description: 'Path to the value to send, for the input source, such as data.result',

+ 1 - 0
nodes/core/sort-limit-dedupe.js

@@ -11,6 +11,7 @@ const configSchema = {
     type: 'object',
     properties: {
         inputField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Array Field',
             description: 'Path to the array to process',

+ 1 - 0
nodes/core/split-out.js

@@ -11,6 +11,7 @@ const configSchema = {
     type: 'object',
     properties: {
         inputField: {
+            valueKind: 'path',
             type: 'string',
             title: 'Array Field',
             description: 'Path to the array to split out, such as data.result.orders',

+ 1 - 0
nodes/core/switch.js

@@ -22,6 +22,7 @@ const configSchema = {
     type: 'object',
     properties: {
         field: {
+            valueKind: 'path',
             type: 'string',
             title: 'Field',
             description: 'Path to the value to test, such as data.result.status'

+ 24 - 0
src/webserver/http_server.cpp

@@ -128,6 +128,30 @@ void HttpServer::setupStaticFiles() {
     // Serve static files with mount point as fallback
     server_.set_mount_point("/", static_path.string());
 
+    // Cache headers for anything the mount point serves.
+    //
+    // httplib answers file requests before it looks at registered handlers, so
+    // a Get("/") handler never runs and "/" was going out with no headers at
+    // all. The browser then applied its own judgement to the one document that
+    // must not be cached: index.html names the hashed bundle to load, so a
+    // stale copy points at a file that no longer exists, and the app breaks in
+    // a way that looks like a bug in whatever the person was doing - a missing
+    // function on an object, say.
+    //
+    // Everything under /assets carries a content hash in its name, so a given
+    // address never changes what it holds and can be kept for good.
+    server_.set_file_request_handler([](const httplib::Request& req, httplib::Response& res) {
+        if (req.path.starts_with("/assets/")) {
+            res.set_header("Cache-Control", "public, max-age=31536000, immutable");
+            return;
+        }
+        if (req.path == "/" || req.path.ends_with(".html")) {
+            res.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
+            res.set_header("Pragma", "no-cache");
+            res.set_header("Expires", "0");
+        }
+    });
+
     // SPA fallback - catch-all route for client-side routing
     // Note: This handler serves index.html for SPA routes (paths without extensions)
     // Static files (with extensions) are handled by the mount point above

+ 29 - 1
webui/src/components/ExpressionInput.tsx

@@ -17,6 +17,11 @@ interface ExpressionInputProps {
   availableFields?: AvailableField[]
   multiline?: boolean
   className?: string
+  // 'path' when the setting wants a bare path - data.result.items - rather than
+  // an expression. The available-data panel always hands over the {{ }} form,
+  // because that is what most settings take; dropping one into a path field
+  // would otherwise leave braces the node has to be lenient about.
+  valueKind?: 'path' | 'expression'
 }
 
 type ContentKind = 'plain' | 'json' | 'templated-json' | 'broken-json' | 'markdown'
@@ -102,7 +107,24 @@ export function ExpressionInput({
   availableFields = [],
   multiline = false,
   className = '',
+  valueKind = 'expression',
 }: ExpressionInputProps) {
+
+  // A setting that wants a path gets the path, not the {{ }} wrapping it. Only
+  // the plain form is unwrapped: {{$node["X"].y}} names a different node, which
+  // a path cannot express, so that is left alone rather than silently turned
+  // into something that would read the wrong node.
+  const handleDrop = (event: React.DragEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+    const dropped = event.dataTransfer.getData('text/plain')
+    if (!dropped) return
+    if (valueKind !== 'path') return
+
+    const plain = /^\{\{\s*([^${}][^{}]*?)\s*\}\}$/.exec(dropped)
+    if (!plain) return
+
+    event.preventDefault()
+    onChange(plain[1])
+  }
   const [showAutocomplete, setShowAutocomplete] = useState(false)
   const [autocompletePosition, setAutocompletePosition] = useState({ top: 0, left: 0 })
   const [filterText, setFilterText] = useState('')
@@ -309,7 +331,13 @@ export function ExpressionInput({
             onChange={handleChange}
             onSelect={handleSelect}
             onKeyDown={handleKeyDown}
-            placeholder={placeholder || 'Enter value or use {{ }} for expressions'}
+            onDrop={handleDrop}
+            placeholder={
+              placeholder ||
+              (valueKind === 'path'
+                ? 'A path such as data.result.items'
+                : 'Enter value or use {{ }} for expressions')
+            }
             className={`w-full px-3 py-2 pr-10 border border-gray-200 dark:border-slate-600 rounded-l-lg text-sm bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 resize-y overflow-y-auto ${
               isCode ? 'font-mono text-xs' : ''
             }`}

+ 11 - 0
webui/src/components/workflow/NodeConfigModal.tsx

@@ -279,6 +279,7 @@ export function NodeConfigModal({
                     onChange={(value) =>
                       onConfigChange({ ...editingConfig, inputField: value })
                     }
+                    valueKind="path"
                     placeholder="Path to array (e.g., data.items)"
                     availableFields={getUpstreamOutputFields(selectedNodeData.id).map((f) => ({
                       path: f.path,
@@ -690,6 +691,7 @@ export function NodeConfigModal({
                       </div>
                     ) : (
                       <ExpressionInput
+                        valueKind={prop.valueKind === 'path' ? 'path' : 'expression'}
                         value={editingConfig[key] ?? prop.default ?? ''}
                         onChange={(value) =>
                           onConfigChange({ ...editingConfig, [key]: value })
@@ -708,6 +710,15 @@ export function NodeConfigModal({
                     {prop.description && prop.type !== 'boolean' && (
                       <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{prop.description}</p>
                     )}
+                    {/* The two forms look alike and behave differently, and the
+                        data panel always offers the {{ }} one. Saying which
+                        this field takes costs a line and saves a puzzle. */}
+                    {prop.valueKind === 'path' && (
+                      <p className="text-[11px] text-gray-400 dark:text-gray-500 mt-0.5">
+                        Takes a path, not an expression - <span className="font-mono">data.result.items</span>,
+                        not <span className="font-mono">{'{{ }}'}</span>. Dragging a field in inserts the path.
+                      </p>
+                    )}
                   </div>
                 )}