diff --git a/app/web/router.py b/app/web/router.py index 80d5030..fb12c40 100644 --- a/app/web/router.py +++ b/app/web/router.py @@ -120,7 +120,7 @@ _URL_MAP = { "email_auth.login_email_form": "/login/email", "email_auth.send_magic_link": "/auth/email/send-link", "register": "/auth/password/setup", - "setup": "/setup", + "setup": "/first-time-setup", } @@ -322,9 +322,9 @@ async def index(request: Request, user: Optional[dict] = Depends(get_optional_us return RedirectResponse(url="/login", status_code=302) -@router.get("/setup", response_class=HTMLResponse) +@router.get("/first-time-setup", response_class=HTMLResponse) async def setup_wizard(request: Request, conn: duckdb.DuckDBPyConnection = Depends(_get_db)): - """First-time setup wizard. Redirects to dashboard if users already exist.""" + """First-time setup wizard. Redirects to login if users already exist.""" try: user_count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0] if user_count > 0: @@ -720,13 +720,13 @@ async def activity_center( return templates.TemplateResponse(request, "activity_center.html", ctx) -@router.get("/install", response_class=HTMLResponse) -async def install_page( +@router.get("/setup", response_class=HTMLResponse) +async def setup_page( request: Request, user: Optional[dict] = Depends(get_optional_user), conn: duckdb.DuckDBPyConnection = Depends(_get_db), ): - """Public install instructions for the CLI.""" + """Setup instructions for the local agent (CLI + Claude Code).""" base_url = str(request.base_url).rstrip("/") ctx = _build_context( request, @@ -738,6 +738,12 @@ async def install_page( return templates.TemplateResponse(request, "install.html", ctx) +@router.get("/install", response_class=HTMLResponse) +async def install_redirect(request: Request): + """Backwards-compat redirect: /install → /setup (301).""" + return RedirectResponse(url="/setup", status_code=301) + + @router.get("/admin/tables", response_class=HTMLResponse) async def admin_tables( request: Request, diff --git a/app/web/templates/_app_header.html b/app/web/templates/_app_header.html index 52f8f47..ce38243 100644 --- a/app/web/templates/_app_header.html +++ b/app/web/templates/_app_header.html @@ -11,7 +11,7 @@
{% set _path = request.url.path %} Dashboard - Install CLI + Setup local agent {% if session.user.is_admin %} Marketplaces {% set _admin_active = _path.startswith('/admin/tables') or _path.startswith('/admin/tokens') or _path.startswith('/admin/users') or _path.startswith('/admin/groups') or _path.startswith('/admin/access') or _path.startswith('/admin/server-config') or _path.startswith('/admin/welcome') %} diff --git a/app/web/templates/_claude_setup_instructions.jinja b/app/web/templates/_claude_setup_instructions.jinja index 30d0431..a69c3c2 100644 --- a/app/web/templates/_claude_setup_instructions.jinja +++ b/app/web/templates/_claude_setup_instructions.jinja @@ -5,7 +5,7 @@ * preview_mode=True → emits a read-only HTML
 block rendered
                            with the real server_url and a visible placeholder
                            for the token. Used inline on /dashboard and
-                           /install so the reader can see exactly what will
+                           /setup so the reader can see exactly what will
                            land in their clipboard.
    * preview_mode=False →  emits the JS `SETUP_INSTRUCTIONS_TEMPLATE` array +
                            `renderSetupInstructions(server, token)` function.
diff --git a/app/web/templates/install.html b/app/web/templates/install.html
index 9d37fc3..39ffea6 100644
--- a/app/web/templates/install.html
+++ b/app/web/templates/install.html
@@ -3,7 +3,7 @@
 
     
     
-    Install CLI — {{ config.INSTANCE_NAME }}
+    Setup local agent — {{ config.INSTANCE_NAME }}
     {% if not config.THEME_FONT_URL %}
     
     
diff --git a/tests/snapshots/openapi.json b/tests/snapshots/openapi.json
index 4b0b9cd..76e7c8b 100644
--- a/tests/snapshots/openapi.json
+++ b/tests/snapshots/openapi.json
@@ -1893,6 +1893,8 @@
           },
           "profile_after_sync": {
             "default": true,
+            "deprecated": true,
+            "description": "DEPRECATED: not consumed by the runtime (Agent 1 finding 2026-05-01). Profiler runs unconditionally on every synced table; this flag has no effect. Field stays for back-compat.",
             "title": "Profile After Sync",
             "type": "boolean"
           },
@@ -1901,6 +1903,17 @@
             "title": "Query Mode",
             "type": "string"
           },
+          "source_query": {
+            "anyOf": [
+              {
+                "type": "string"
+              },
+              {
+                "type": "null"
+              }
+            ],
+            "title": "Source Query"
+          },
           "source_table": {
             "anyOf": [
               {
@@ -1936,6 +1949,8 @@
           },
           "sync_strategy": {
             "default": "full_refresh",
+            "deprecated": true,
+            "description": "DEPRECATED: catalog/profiler metadata only. No extractor reads this field; every sync is a full overwrite regardless of value. profiler.is_partitioned() consumes it for parquet-layout detection. Field stays for back-compat; will be removed in a future major release.",
             "title": "Sync Strategy",
             "type": "string"
           }
@@ -2067,6 +2082,83 @@
         "title": "TableSubscriptionUpdate",
         "type": "object"
       },
+      "TemplateGetResponse": {
+        "properties": {
+          "content": {
+            "anyOf": [
+              {
+                "type": "string"
+              },
+              {
+                "type": "null"
+              }
+            ],
+            "title": "Content"
+          },
+          "default": {
+            "title": "Default",
+            "type": "string"
+          },
+          "updated_at": {
+            "anyOf": [
+              {
+                "type": "string"
+              },
+              {
+                "type": "null"
+              }
+            ],
+            "title": "Updated At"
+          },
+          "updated_by": {
+            "anyOf": [
+              {
+                "type": "string"
+              },
+              {
+                "type": "null"
+              }
+            ],
+            "title": "Updated By"
+          }
+        },
+        "required": [
+          "content",
+          "default"
+        ],
+        "title": "TemplateGetResponse",
+        "type": "object"
+      },
+      "TemplatePreviewRequest": {
+        "properties": {
+          "content": {
+            "maxLength": 200000,
+            "minLength": 1,
+            "title": "Content",
+            "type": "string"
+          }
+        },
+        "required": [
+          "content"
+        ],
+        "title": "TemplatePreviewRequest",
+        "type": "object"
+      },
+      "TemplatePutRequest": {
+        "properties": {
+          "content": {
+            "maxLength": 200000,
+            "minLength": 1,
+            "title": "Content",
+            "type": "string"
+          }
+        },
+        "required": [
+          "content"
+        ],
+        "title": "TemplatePutRequest",
+        "type": "object"
+      },
       "TokenListItem": {
         "properties": {
           "created_at": {
@@ -2329,6 +2421,8 @@
                 "type": "null"
               }
             ],
+            "deprecated": true,
+            "description": "DEPRECATED: not consumed by the runtime. See RegisterTableRequest.profile_after_sync.",
             "title": "Profile After Sync"
           },
           "query_mode": {
@@ -2342,6 +2436,17 @@
             ],
             "title": "Query Mode"
           },
+          "source_query": {
+            "anyOf": [
+              {
+                "type": "string"
+              },
+              {
+                "type": "null"
+              }
+            ],
+            "title": "Source Query"
+          },
           "source_table": {
             "anyOf": [
               {
@@ -2384,6 +2489,8 @@
                 "type": "null"
               }
             ],
+            "deprecated": true,
+            "description": "DEPRECATED: catalog/profiler metadata only. See RegisterTableRequest.sync_strategy.",
             "title": "Sync Strategy"
           }
         },
@@ -2641,13 +2748,26 @@
         ],
         "title": "VoteRequest",
         "type": "object"
+      },
+      "WelcomeResponse": {
+        "properties": {
+          "content": {
+            "title": "Content",
+            "type": "string"
+          }
+        },
+        "required": [
+          "content"
+        ],
+        "title": "WelcomeResponse",
+        "type": "object"
       }
     }
   },
   "info": {
     "description": "Data distribution platform for AI analytical systems",
     "title": "AI Data Analyst",
-    "version": "2.1.0"
+    "version": "2.0.0"
   },
   "openapi": "3.1.0",
   "paths": {
@@ -3238,6 +3358,55 @@
         ]
       }
     },
+    "/admin/welcome": {
+      "get": {
+        "operationId": "admin_welcome_page_admin_welcome_get",
+        "parameters": [
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "text/html": {
+                "schema": {
+                  "type": "string"
+                }
+              }
+            },
+            "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
+          }
+        },
+        "summary": "Admin Welcome Page",
+        "tags": [
+          "web"
+        ]
+      }
+    },
     "/api/admin/access-overview": {
       "get": {
         "description": "One-shot snapshot for the /admin/access page.\n\nReturns:\n  - ``groups``: every user_group with member + grant counts\n  - ``grants``: every (group_id, resource_type, resource_id) row\n  - ``resources``: per-resource-type hierarchical layout, where each\n    type has a list of *blocks* (parent entities, e.g. a marketplace)\n    and each block has *items* (concrete grantable resources).\n\nUI stitches the three pieces into the two-column layout: groups on\nthe left, resources tree on the right with per-item checkboxes whose\nstate derives from ``grants``.",
@@ -3398,9 +3567,25 @@
     },
     "/api/admin/discover-tables": {
       "get": {
-        "description": "Discover all available tables from the configured data source.",
+        "description": "Discover available tables from the configured data source.\n\nFor ``data_source.type='keboola'`` returns the full Storage API table\nlist (single round-trip). For ``data_source.type='bigquery'``:\n\n- Without ``dataset``: list datasets in the configured project.\n- With ``dataset=name``: list tables (BASE TABLE + VIEW) in that dataset.\n\nTwo-step shape avoids paying the per-dataset list_tables cost up-front\non projects with hundreds of datasets \u2014 the UI populates the dataset\ndropdown first, then fetches tables only for the selected dataset.",
         "operationId": "discover_tables_api_admin_discover_tables_get",
         "parameters": [
+          {
+            "in": "query",
+            "name": "dataset",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Dataset"
+            }
+          },
           {
             "in": "header",
             "name": "authorization",
@@ -4591,7 +4776,7 @@
     },
     "/api/admin/registry": {
       "get": {
-        "description": "Get full table registry.",
+        "description": "Get full table registry.\n\nEach table row is enriched with `last_sync_error` from sync_state so\noperators can see WHY a row isn't materializing without trawling\nscheduler logs. None for rows that have never errored or have already\nrecovered (status='ok'); the per-row error message string otherwise.",
         "operationId": "list_registry_api_admin_registry_get",
         "parameters": [
           {
@@ -4639,7 +4824,7 @@
     },
     "/api/admin/registry/{table_id}": {
       "delete": {
-        "description": "Unregister a table from the system.\n\nFor BQ rows, schedules a background rebuild so the dropped row's\nmaster view is removed from analytics.duckdb (rather than hanging\naround until the next scheduled sync).",
+        "description": "Unregister a table from the system.\n\nFor BQ rows, schedules a background rebuild so the dropped row's\nmaster view is removed from analytics.duckdb (rather than hanging\naround until the next scheduled sync).\n\nFor materialized rows, also removes the canonical parquet at\n`${DATA_DIR}/extracts//data/.parquet` and clears\nthe matching `sync_state` row. Without these two cleanups, the\nmanifest endpoint kept advertising the dropped table to `da sync`\n(sync_state-driven) and the orchestrator's next rebuild could\nresurrect a master view from the leftover parquet (E2E sub-agent\nfinding 2026-05-01).",
         "operationId": "unregister_table_api_admin_registry__table_id__delete",
         "parameters": [
           {
@@ -5163,6 +5348,210 @@
         ]
       }
     },
+    "/api/admin/welcome-template": {
+      "delete": {
+        "operationId": "admin_reset_template_api_admin_welcome_template_delete",
+        "parameters": [
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
+        "responses": {
+          "204": {
+            "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
+          }
+        },
+        "summary": "Admin Reset Template",
+        "tags": [
+          "welcome"
+        ]
+      },
+      "get": {
+        "operationId": "admin_get_template_api_admin_welcome_template_get",
+        "parameters": [
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/TemplateGetResponse"
+                }
+              }
+            },
+            "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
+          }
+        },
+        "summary": "Admin Get Template",
+        "tags": [
+          "welcome"
+        ]
+      },
+      "put": {
+        "operationId": "admin_put_template_api_admin_welcome_template_put",
+        "parameters": [
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/TemplatePutRequest"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {}
+              }
+            },
+            "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
+          }
+        },
+        "summary": "Admin Put Template",
+        "tags": [
+          "welcome"
+        ]
+      }
+    },
+    "/api/admin/welcome-template/preview": {
+      "post": {
+        "description": "Render arbitrary template content against the live context for the\ncalling admin, without persisting. Used by the /admin/welcome editor's\nPreview button so admins can see their edits before saving.",
+        "operationId": "admin_preview_template_api_admin_welcome_template_preview_post",
+        "parameters": [
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
+        "requestBody": {
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/TemplatePreviewRequest"
+              }
+            }
+          },
+          "required": true
+        },
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/WelcomeResponse"
+                }
+              }
+            },
+            "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
+          }
+        },
+        "summary": "Admin Preview Template",
+        "tags": [
+          "welcome"
+        ]
+      }
+    },
     "/api/catalog/metrics/{metric_path}": {
       "get": {
         "deprecated": true,
@@ -9900,6 +10289,67 @@
         ]
       }
     },
+    "/api/welcome": {
+      "get": {
+        "description": "Render the welcome prompt for the calling user. Returns rendered markdown.",
+        "operationId": "get_welcome_api_welcome_get",
+        "parameters": [
+          {
+            "description": "The server URL the analyst is bootstrapping against",
+            "in": "query",
+            "name": "server_url",
+            "required": true,
+            "schema": {
+              "description": "The server URL the analyst is bootstrapping against",
+              "title": "Server Url",
+              "type": "string"
+            }
+          },
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/WelcomeResponse"
+                }
+              }
+            },
+            "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
+          }
+        },
+        "summary": "Get Welcome",
+        "tags": [
+          "welcome"
+        ]
+      }
+    },
     "/auth/admin/tokens": {
       "get": {
         "operationId": "admin_list_tokens_auth_admin_tokens_get",
@@ -11153,28 +11603,10 @@
         ]
       }
     },
-    "/install": {
+    "/first-time-setup": {
       "get": {
-        "description": "Public install instructions for the CLI.",
-        "operationId": "install_page_install_get",
-        "parameters": [
-          {
-            "in": "header",
-            "name": "authorization",
-            "required": false,
-            "schema": {
-              "anyOf": [
-                {
-                  "type": "string"
-                },
-                {
-                  "type": "null"
-                }
-              ],
-              "title": "Authorization"
-            }
-          }
-        ],
+        "description": "First-time setup wizard. Redirects to login if users already exist.",
+        "operationId": "setup_wizard_first_time_setup_get",
         "responses": {
           "200": {
             "content": {
@@ -11185,19 +11617,31 @@
               }
             },
             "description": "Successful Response"
-          },
-          "422": {
+          }
+        },
+        "summary": "Setup Wizard",
+        "tags": [
+          "web"
+        ]
+      }
+    },
+    "/install": {
+      "get": {
+        "description": "Backwards-compat redirect: /install \u2192 /setup (301).",
+        "operationId": "install_redirect_install_get",
+        "responses": {
+          "200": {
             "content": {
-              "application/json": {
+              "text/html": {
                 "schema": {
-                  "$ref": "#/components/schemas/HTTPValidationError"
+                  "type": "string"
                 }
               }
             },
-            "description": "Validation Error"
+            "description": "Successful Response"
           }
         },
-        "summary": "Install Page",
+        "summary": "Install Redirect",
         "tags": [
           "web"
         ]
@@ -11511,8 +11955,26 @@
     },
     "/setup": {
       "get": {
-        "description": "First-time setup wizard. Redirects to dashboard if users already exist.",
-        "operationId": "setup_wizard_setup_get",
+        "description": "Setup instructions for the local agent (CLI + Claude Code).",
+        "operationId": "setup_page_setup_get",
+        "parameters": [
+          {
+            "in": "header",
+            "name": "authorization",
+            "required": false,
+            "schema": {
+              "anyOf": [
+                {
+                  "type": "string"
+                },
+                {
+                  "type": "null"
+                }
+              ],
+              "title": "Authorization"
+            }
+          }
+        ],
         "responses": {
           "200": {
             "content": {
@@ -11523,9 +11985,19 @@
               }
             },
             "description": "Successful Response"
+          },
+          "422": {
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/HTTPValidationError"
+                }
+              }
+            },
+            "description": "Validation Error"
           }
         },
-        "summary": "Setup Wizard",
+        "summary": "Setup Page",
         "tags": [
           "web"
         ]
diff --git a/tests/test_cli_artifacts.py b/tests/test_cli_artifacts.py
index 1ba9824..a18cafe 100644
--- a/tests/test_cli_artifacts.py
+++ b/tests/test_cli_artifacts.py
@@ -100,7 +100,7 @@ def test_install_page_renders_with_server_url(tmp_path, monkeypatch):
     from fastapi.testclient import TestClient
     from app.main import app
     client = TestClient(app)
-    resp = client.get("/install", headers={"host": "agnes.test", "Accept": "text/html"})
+    resp = client.get("/setup", headers={"host": "agnes.test", "Accept": "text/html"})
     assert resp.status_code == 200
     assert "agnes.test" in resp.text
     assert "da auth whoami" in resp.text
diff --git a/tests/test_selective_gzip.py b/tests/test_selective_gzip.py
index 7c17779..48decfb 100644
--- a/tests/test_selective_gzip.py
+++ b/tests/test_selective_gzip.py
@@ -41,20 +41,20 @@ def test_parquet_path_is_not_gzipped(isolated_client, tmp_path, monkeypatch):
 
 
 def test_install_page_is_gzipped(isolated_client):
-    """/install is HTML above the threshold — gzip should kick in when the
+    """/setup is HTML above the threshold — gzip should kick in when the
     client advertises gzip support. TestClient may decompress transparently,
     so we accept either the header or readable body as proof that the
     middleware decided to handle the response (i.e. did not skip)."""
-    resp = isolated_client.get("/install", headers={"Accept-Encoding": "gzip"})
+    resp = isolated_client.get("/setup", headers={"Accept-Encoding": "gzip"})
     assert resp.status_code == 200
     enc = resp.headers.get("content-encoding", "")
     # Either we see the encoding on the wire OR TestClient auto-decoded it.
-    assert "gzip" in enc or "install" in resp.text.lower()
+    assert "gzip" in enc or "setup" in resp.text.lower()
 
 
 def test_no_accept_encoding_means_no_gzip_anywhere(isolated_client):
     """Client that doesn't advertise gzip gets uncompressed body."""
-    resp = isolated_client.get("/install", headers={"Accept-Encoding": "identity"})
+    resp = isolated_client.get("/setup", headers={"Accept-Encoding": "identity"})
     assert resp.status_code == 200
     assert "gzip" not in resp.headers.get("content-encoding", "")
 
diff --git a/tests/test_setup_instructions.py b/tests/test_setup_instructions.py
index 6239037..a41a889 100644
--- a/tests/test_setup_instructions.py
+++ b/tests/test_setup_instructions.py
@@ -789,7 +789,7 @@ def test_install_page_uses_versioned_wheel_url(monkeypatch, tmp_path):
     from fastapi.testclient import TestClient
     from app.main import app
     client = TestClient(app)
-    resp = client.get("/install", headers={"host": "agnes.test", "Accept": "text/html"})
+    resp = client.get("/setup", headers={"host": "agnes.test", "Accept": "text/html"})
     assert resp.status_code == 200
     assert "/cli/wheel/agnes_the_ai_analyst-2.0.0-py3-none-any.whl" in resp.text
     # The bare alias must no longer appear in the rendered snippet.
diff --git a/tests/test_toolbar_integration.py b/tests/test_toolbar_integration.py
index bb1c200..47c0d89 100644
--- a/tests/test_toolbar_integration.py
+++ b/tests/test_toolbar_integration.py
@@ -54,7 +54,7 @@ def app_no_toolbar(monkeypatch, tmp_path, reset_logging_state):
 @pytest.mark.integration
 def test_no_toolbar_when_debug_off(app_no_toolbar):
     client = TestClient(app_no_toolbar)
-    resp = client.get("/setup", follow_redirects=False)
+    resp = client.get("/first-time-setup", follow_redirects=False)
     if resp.status_code in (302, 401):
         # Auth redirect — toolbar wouldn't render anyway. The point of this
         # test is to assert markup ABSENCE; no markup, no failure.
@@ -76,7 +76,7 @@ def test_toolbar_html_present_when_debug(app_with_toolbar):
     client = TestClient(app_with_toolbar)
     # Try several HTML routes — at least one should respond 200 under
     # LOCAL_DEV_MODE=1 (auth bypass).
-    for path in ("/dashboard", "/setup", "/login", "/admin/access"):
+    for path in ("/dashboard", "/first-time-setup", "/login", "/admin/access"):
         resp = client.get(path, follow_redirects=False)
         if resp.status_code == 200 and "text/html" in resp.headers.get("content-type", ""):
             body = resp.text.lower()
diff --git a/tests/test_web_ui.py b/tests/test_web_ui.py
index 0441ce1..e054a24 100644
--- a/tests/test_web_ui.py
+++ b/tests/test_web_ui.py
@@ -214,7 +214,7 @@ class TestClaudeSetupPreview:
     """
 
     def test_install_preview_visible_for_signed_in_user(self, web_client, admin_cookie):
-        resp = web_client.get("/install", cookies=admin_cookie)
+        resp = web_client.get("/setup", cookies=admin_cookie)
         assert resp.status_code == 200
         body = resp.text
         # Preview card + placeholder token render
@@ -243,10 +243,10 @@ class TestClaudeSetupPreview:
         assert "<will be generated on click>" in body
 
     def test_install_mcp_card_removed(self, web_client):
-        """The stale 'Use with Claude Code / MCP' card on /install has been
+        """The stale 'Use with Claude Code / MCP' card on /setup has been
         removed — there is no Agnes MCP server today.
         """
-        resp = web_client.get("/install")
+        resp = web_client.get("/setup")
         assert resp.status_code == 200
         body = resp.text
         assert "Use with Claude Code / MCP" not in body