@caddis/cli 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -1
  3. package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
  4. package/bundles/antigravity-plugin/agents/preflight.md +53 -0
  5. package/bundles/antigravity-plugin/guard_agy.py +338 -0
  6. package/bundles/antigravity-plugin/hooks.json +36 -0
  7. package/bundles/antigravity-plugin/mcp_config.json +8 -0
  8. package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
  9. package/bundles/antigravity-plugin/plugin.json +5 -0
  10. package/bundles/antigravity-plugin/session_end_agy.py +57 -0
  11. package/bundles/antigravity-plugin/skills/_registry.md +115 -0
  12. package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
  13. package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
  14. package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
  15. package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
  16. package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
  17. package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
  18. package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
  19. package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
  20. package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
  21. package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
  22. package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
  23. package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
  24. package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
  25. package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
  26. package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
  27. package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
  28. package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
  29. package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
  30. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
  31. package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
  32. package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
  33. package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
  34. package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
  35. package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
  36. package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
  37. package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
  38. package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
  39. package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
  40. package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
  41. package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
  42. package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
  43. package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
  44. package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
  45. package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
  46. package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
  47. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
  48. package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
  49. package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
  50. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
  51. package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
  52. package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
  53. package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
  54. package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
  55. package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
  56. package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
  57. package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
  58. package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
  59. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
  60. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
  61. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
  62. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
  63. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
  64. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
  65. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
  66. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
  67. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
  68. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
  69. package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
  70. package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
  71. package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
  72. package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
  73. package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
  74. package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
  75. package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
  76. package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
  77. package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
  78. package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
  79. package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
  80. package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
  81. package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
  82. package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
  83. package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
  84. package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
  85. package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
  86. package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
  87. package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
  88. package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
  89. package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
  90. package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
  91. package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
  92. package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
  93. package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
  94. package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
  95. package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
  96. package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
  97. package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
  98. package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
  99. package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
  100. package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
  101. package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
  102. package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
  103. package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
  104. package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
  105. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
  106. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
  107. package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
  108. package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
  109. package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
  110. package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
  111. package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
  112. package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
  113. package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
  114. package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
  115. package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
  116. package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
  117. package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  118. package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
  119. package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
  120. package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
  121. package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
  122. package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
  123. package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
  124. package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
  125. package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
  126. package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
  127. package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
  128. package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
  129. package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
  130. package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
  131. package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
  132. package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
  133. package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
  134. package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
  135. package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
  136. package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
  137. package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
  138. package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
  139. package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
  140. package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
  141. package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
  142. package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
  143. package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
  144. package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
  145. package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
  146. package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
  147. package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
  148. package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
  149. package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
  150. package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
  151. package/bundles/manifest.json +6 -0
  152. package/dist/cli.js +5363 -0
  153. package/package.json +61 -4
@@ -0,0 +1,880 @@
1
+ ---
2
+ name: windows-deployment
3
+ description: "Deploy FastAPI + React/Vite apps to Windows Server with NSSM services, reverse proxy (IIS or nginx), and git-pull workflow. Use when deploying any web app to a Windows prod server, setting up NSSM services, configuring IIS or nginx reverse proxy, making code environment-aware for dev/prod, or troubleshooting prod deployment issues."
4
+ ---
5
+
6
+ # Windows Server Deployment Skill
7
+
8
+ Deploy FastAPI + React/Vite web applications to Windows Server using NSSM services, a reverse proxy (IIS or nginx), and a git-pull workflow.
9
+
10
+ ## 1. When to Apply This Skill
11
+
12
+ **Trigger conditions:**
13
+ - "Deploy this app to prod" / "Deploy to Windows Server"
14
+ - Setting up a new web application on a Windows prod server
15
+ - Adding a reverse proxy (IIS or nginx) for an app
16
+ - Registering NSSM services
17
+ - Configuring environment-aware dev/prod code (subpath, auth, static serving)
18
+ - Troubleshooting prod deployment (502, 404, service won't start)
19
+ - "Why can't the frontend reach the API?"
20
+
21
+ **Prerequisites the user should have:**
22
+ - A FastAPI + React/Vite app
23
+ - A Windows Server target machine
24
+ - NSSM installed on the target
25
+ - A git remote (Gitea, GitHub, etc.) accessible from both dev and prod
26
+ - Python and Node.js installed on the target
27
+
28
+ ---
29
+
30
+ ## 2. Architecture Overview
31
+
32
+ Two reverse proxy strategies are available. Choose based on what the server already runs.
33
+
34
+ ### Strategy A — IIS (Recommended When IIS Is Already Running)
35
+
36
+ ```
37
+ Browser → IIS (:80/:443)
38
+ ├─ Windows Auth + Active Directory (handled by IIS)
39
+ ├─ URL Rewrite: /{subpath}/api/* → http://localhost:{api-port}/api/*
40
+ └─ URL Rewrite: /{subpath}/* → http://localhost:{api-port}/*
41
+ ├─ /api/* → FastAPI routers
42
+ └─ /* → FastAPI serves dist/ static files
43
+
44
+ NSSM manages:
45
+ - <org>-{app}-api-prod (one per app)
46
+ ```
47
+
48
+ **Key characteristics:**
49
+ - IIS handles authentication (Windows Auth, AD groups) — app code has zero auth logic
50
+ - App is deployed under a subpath (e.g. `/my-app/`) — IIS strips the prefix before forwarding
51
+ - FastAPI serves both API routes and static frontend files in production
52
+ - No separate frontend service/port needed
53
+
54
+ ### Strategy B — nginx (Standalone Reverse Proxy)
55
+
56
+ ```
57
+ Browser → nginx (:51xx) → serves static dist/ files
58
+ → proxies /api/* → uvicorn (:81xx)
59
+
60
+ NSSM manages:
61
+ - <org>-nginx-prod (single instance, all apps)
62
+ - <org>-{app}-api-prod (one per app)
63
+ ```
64
+
65
+ **Key characteristics:**
66
+ - nginx handles static file serving and API proxying
67
+ - Each app gets its own port (e.g. `:5103`, `:5104`)
68
+ - No built-in auth — add separately if needed
69
+ - Clean separation: nginx = static + proxy, uvicorn = API only
70
+
71
+ ### Decision Matrix
72
+
73
+ | Factor | Choose IIS | Choose nginx |
74
+ |--------|-----------|-------------|
75
+ | IIS already running on the server | ✅ | |
76
+ | Need Windows Authentication / AD | ✅ | |
77
+ | Apps must be on port 80/443 | ✅ | |
78
+ | Multiple apps under one domain with subpaths | ✅ | |
79
+ | No IIS on the server | | ✅ |
80
+ | Need lightweight, portable setup | | ✅ |
81
+ | Each app gets its own port | | ✅ |
82
+
83
+ ### Port Convention
84
+
85
+ | Environment | API (uvicorn) | Frontend |
86
+ |-------------|--------------|----------|
87
+ | Dev | 81x2 | 51x2 (Vite dev server) |
88
+ | Prod (IIS) | 81x3 | IIS on :80/:443 (no separate FE port) |
89
+ | Prod (nginx)| 81x3 | 51x3 (nginx) |
90
+
91
+ Where `x` is the app number. Prod API ports increment per app on the shared server (8103, 8104, 8105...).
92
+
93
+ ---
94
+
95
+ ## 3. Deployment Flow
96
+
97
+ ### First-Time Setup (Both Strategies)
98
+
99
+ ```
100
+ 1. Clone repo on prod via git
101
+ 2. Set up Python venv + install deps
102
+ 3. Build frontend (npm ci && npm run build:prod)
103
+ 4. Create config/.env.api.prod
104
+ 5. Register NSSM API service
105
+ 6. Configure reverse proxy (IIS or nginx)
106
+ 7. Open firewall ports
107
+ 8. Verify
108
+ ```
109
+
110
+ ### Subsequent Deploys
111
+
112
+ ```
113
+ 1. Dev: git push origin main
114
+ 2. Prod: git pull
115
+ 3. pip install -e . (if Python deps changed)
116
+ 4. cd frontend && npm ci && npm run build:prod (if FE changed)
117
+ 5. nssm restart <org>-{app}-api-prod
118
+ 6. Verify
119
+ ```
120
+
121
+ ### 3.1 Remote Capability Handshake (Required Before Saying "Can't Access Prod")
122
+
123
+ When a user asks for remote prod actions, do **not** assume remote access is unavailable.
124
+
125
+ Run this handshake first:
126
+
127
+ ```powershell
128
+ $server = "{hostname}"
129
+
130
+ # 1) Reachability
131
+ Test-Connection -ComputerName $server -Count 1
132
+
133
+ # 2) WinRM availability
134
+ Test-WSMan -ComputerName $server
135
+
136
+ # 3) Actual command execution
137
+ Invoke-Command -ComputerName $server -ScriptBlock {
138
+ hostname
139
+ Get-Location
140
+ }
141
+ ```
142
+
143
+ Decision rule:
144
+ - If steps 1-3 succeed, proceed with remote deploy actions.
145
+ - If any step fails, report the exact failing step and provide fallback instructions.
146
+
147
+ **Hard rule:** Never claim "I can't remote into prod" until this probe fails in the current session.
148
+
149
+ ---
150
+
151
+ ## 4. Common Steps (Both Strategies)
152
+
153
+ ### 4.1 Git Setup on Prod
154
+
155
+ ```powershell
156
+ cd G:\Projects
157
+ git clone http://{git-host}/{org}/{app-name}.git
158
+ cd {app-name}
159
+
160
+ # Credential storage (one-time)
161
+ git config credential.helper store
162
+ [System.Environment]::SetEnvironmentVariable("GCM_PROVIDER", "generic", "User")
163
+
164
+ git pull # enter credentials once, stored thereafter
165
+ ```
166
+
167
+ If using a self-hosted git server, ensure the `hosts` file maps the hostname.
168
+
169
+ ### 4.2 Python Environment
170
+
171
+ ```powershell
172
+ python -m venv .venv
173
+ .venv\Scripts\Activate.ps1
174
+ pip install -e .
175
+ ```
176
+
177
+ **Verify Python version** matches dev: `python --version`.
178
+
179
+ ### 4.3 Frontend Build
180
+
181
+ ```powershell
182
+ cd frontend
183
+ npm ci
184
+ npm run build:prod # ← uses production Vite mode (see §7)
185
+ Get-ChildItem dist -Recurse | Measure-Object
186
+ ```
187
+
188
+ ### 4.4 Environment Config
189
+
190
+ Create `config/.env.api.prod`:
191
+
192
+ ```env
193
+ APP_ENV=production
194
+ APP_PORT=81x3
195
+ # Add app-specific vars (DB connection, etc.)
196
+ ```
197
+
198
+ **Never commit real `.env` files.** Only `.env.example` with placeholder values.
199
+
200
+ ### 4.5 Register API Service with NSSM
201
+
202
+ ```powershell
203
+ $projectRoot = "G:\Projects\{app-name}"
204
+ $pythonExe = "$projectRoot\.venv\Scripts\python.exe"
205
+ $svcName = "<org>-{short}-api-prod"
206
+ $port = 81x3 # Use actual port
207
+
208
+ nssm install $svcName $pythonExe "-m uvicorn src.api.main:app --host 0.0.0.0 --port $port"
209
+ nssm set $svcName AppDirectory $projectRoot
210
+ nssm set $svcName DisplayName "{App Name} API (PROD)"
211
+ nssm set $svcName Description "{App Name} FastAPI backend - prod environment"
212
+ nssm set $svcName AppEnvironmentExtra "APP_ENV=production"
213
+
214
+ # Logging
215
+ nssm set $svcName AppStdout "$projectRoot\logs\$svcName-stdout.log"
216
+ nssm set $svcName AppStderr "$projectRoot\logs\$svcName-stderr.log"
217
+ nssm set $svcName AppRotateFiles 1
218
+ nssm set $svcName AppRotateBytes 10485760 # 10 MB
219
+
220
+ # Restart on failure
221
+ nssm set $svcName AppRestartDelay 5000
222
+
223
+ nssm start $svcName
224
+ ```
225
+
226
+ ---
227
+
228
+ ## 5. Strategy A — IIS Reverse Proxy Setup
229
+
230
+ ### 5.1 Prerequisites
231
+
232
+ Install IIS features (if not already present):
233
+ ```powershell
234
+ Install-WindowsFeature Web-Server, Web-Mgmt-Console
235
+ ```
236
+
237
+ Install these IIS modules:
238
+ - **URL Rewrite** — https://www.iis.net/downloads/microsoft/url-rewrite
239
+ - **Application Request Routing (ARR)** — https://www.iis.net/downloads/microsoft/application-request-routing
240
+
241
+ Enable ARR proxy functionality:
242
+ ```powershell
243
+ # In IIS Manager → Server level → Application Request Routing → Server Proxy Settings
244
+ # Check "Enable proxy" → Apply
245
+ # Or via appcmd:
246
+ & "$env:SystemRoot\system32\inetsrv\appcmd.exe" set config -section:system.webServer/proxy /enabled:true /commit:apphost
247
+ ```
248
+
249
+ ### 5.2 IIS Rewrite Rules
250
+
251
+ Create rewrite rules under the Default Web Site (or a dedicated site). Two rules are needed — the API rule must come first:
252
+
253
+ **Rule 1 — API proxy (matches first):**
254
+ ```xml
255
+ <rule name="{app-name}-api" stopProcessing="true">
256
+ <match url="^{subpath}/api/(.*)" />
257
+ <action type="Rewrite" url="http://localhost:{api-port}/api/{R:1}" />
258
+ </rule>
259
+ ```
260
+
261
+ **Rule 2 — Frontend catch-all:**
262
+ ```xml
263
+ <rule name="{app-name}-frontend" stopProcessing="true">
264
+ <match url="^{subpath}/(.*)" />
265
+ <action type="Rewrite" url="http://localhost:{api-port}/{R:1}" />
266
+ </rule>
267
+ ```
268
+
269
+ **How it works:** IIS strips the subpath prefix before forwarding. FastAPI receives clean paths (`/api/v1/...` and `/index.html`).
270
+
271
+ ### 5.3 Windows Authentication
272
+
273
+ Enable Windows Auth on the IIS site/app:
274
+
275
+ ```powershell
276
+ # Disable anonymous access, enable Windows Auth
277
+ Set-WebConfigurationProperty -Filter '/system.webServer/security/authentication/anonymousAuthentication' `
278
+ -PSPath 'IIS:\Sites\Default Web Site' -Name 'enabled' -Value 'false'
279
+ Set-WebConfigurationProperty -Filter '/system.webServer/security/authentication/windowsAuthentication' `
280
+ -PSPath 'IIS:\Sites\Default Web Site' -Name 'enabled' -Value 'true'
281
+ ```
282
+
283
+ **Key insight:** Auth is fully external to the application. FastAPI endpoints have no auth dependencies or middleware — IIS rejects unauthenticated requests before they reach uvicorn.
284
+
285
+ ### 5.4 FastAPI Static File Serving (Required for IIS Strategy)
286
+
287
+ With IIS, FastAPI must serve the frontend `dist/` files in production. Add this to `main.py` — gated on a settings flag so it only activates in production:
288
+
289
+ ```python
290
+ from pathlib import Path
291
+ from fastapi.responses import FileResponse
292
+ from fastapi.staticfiles import StaticFiles
293
+
294
+ _FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
295
+
296
+ # ... app setup, routers ...
297
+
298
+ # ── Static file serving (production only) ──
299
+ if not settings.debug and _FRONTEND_DIST.is_dir():
300
+ app.mount("/assets", StaticFiles(directory=_FRONTEND_DIST / "assets"), name="static")
301
+
302
+ @app.get("/{full_path:path}")
303
+ async def serve_spa(full_path: str) -> FileResponse:
304
+ # Path traversal guard (OWASP A04)
305
+ file = (_FRONTEND_DIST / full_path).resolve()
306
+ if file.is_file() and str(file).startswith(str(_FRONTEND_DIST)):
307
+ return FileResponse(file)
308
+ return FileResponse(_FRONTEND_DIST / "index.html")
309
+ ```
310
+
311
+ **Critical ordering rule:** Mount this AFTER all API routers. The SPA catch-all must be the last route.
312
+
313
+ **Dev behavior:** `settings.debug == True` → static block is skipped → Vite dev server handles frontend.
314
+ **Prod behavior:** `settings.debug == False` → FastAPI serves `dist/assets/*` and falls back to `index.html` for SPA routing.
315
+
316
+ ### 5.5 Firewall (IIS)
317
+
318
+ No per-app firewall rules needed — IIS listens on ports 80/443, which are typically already open. API ports (81xx) stay on localhost.
319
+
320
+ ### 5.6 Verify (IIS)
321
+
322
+ ```powershell
323
+ # IIS site running?
324
+ Get-Website | Where-Object { $_.State -eq 'Started' }
325
+
326
+ # Frontend loads?
327
+ Invoke-WebRequest -Uri "http://localhost/{subpath}/" -UseBasicParsing -UseDefaultCredentials | Select-Object StatusCode
328
+
329
+ # API proxied?
330
+ (Invoke-WebRequest -Uri "http://localhost/{subpath}/api/v1/{endpoint}" -UseBasicParsing -UseDefaultCredentials).Content
331
+
332
+ # Health?
333
+ (Invoke-WebRequest -Uri "http://localhost/{subpath}/api/health" -UseBasicParsing -UseDefaultCredentials).Content
334
+ ```
335
+
336
+ ---
337
+
338
+ ## 6. Strategy B — nginx Reverse Proxy Setup
339
+
340
+ ### 6.1 nginx as a Shared Service
341
+
342
+ nginx runs as a **single NSSM service** serving all apps. Each app gets its own `server {}` block on a different port.
343
+
344
+ ```powershell
345
+ # First-time nginx setup
346
+ nssm install <org>-nginx-prod "C:\nginx\nginx.exe"
347
+ nssm set <org>-nginx-prod AppDirectory "C:\nginx"
348
+ nssm set <org>-nginx-prod DisplayName "nginx Reverse Proxy (PROD)"
349
+ nssm set <org>-nginx-prod AppStdout "G:\Projects\logs\<org>-nginx-prod-stdout.log"
350
+ nssm set <org>-nginx-prod AppStderr "G:\Projects\logs\<org>-nginx-prod-stderr.log"
351
+ nssm set <org>-nginx-prod AppRotateFiles 1
352
+ nssm set <org>-nginx-prod AppRotateBytes 10485760
353
+ nssm set <org>-nginx-prod AppRestartDelay 5000
354
+
355
+ nssm start <org>-nginx-prod
356
+ ```
357
+
358
+ ### 6.2 nginx Server Block Template
359
+
360
+ Add to `C:\nginx\conf\nginx.conf`:
361
+
362
+ ```nginx
363
+ server {
364
+ listen {51x3};
365
+ server_name {hostname} localhost;
366
+
367
+ location /api/ {
368
+ proxy_pass http://127.0.0.1:{81x3}/api/;
369
+ proxy_http_version 1.1;
370
+ proxy_set_header Host $host;
371
+ proxy_set_header X-Real-IP $remote_addr;
372
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
373
+ proxy_read_timeout 120s;
374
+ }
375
+
376
+ location /health {
377
+ proxy_pass http://127.0.0.1:{81x3}/health;
378
+ proxy_http_version 1.1;
379
+ proxy_set_header Host $host;
380
+ }
381
+
382
+ location / {
383
+ root G:/Projects/{app-name}/frontend/dist;
384
+ try_files $uri $uri/ /index.html;
385
+ expires 1h;
386
+ add_header Cache-Control "public, no-transform";
387
+ }
388
+ }
389
+ ```
390
+
391
+ Test and reload:
392
+ ```powershell
393
+ C:\nginx\nginx.exe -t -c C:\nginx\conf\nginx.conf
394
+ nssm restart <org>-nginx-prod
395
+ ```
396
+
397
+ ### 6.3 Firewall (nginx)
398
+
399
+ ```powershell
400
+ # Only expose the FE port — API stays on localhost
401
+ New-NetFirewallRule -DisplayName "{App Name} ({51x3})" -Direction Inbound -LocalPort {51x3} -Protocol TCP -Action Allow
402
+ ```
403
+
404
+ ### 6.4 CORS Configuration (nginx only)
405
+
406
+ When nginx serves on a different port than the API, CORS is needed:
407
+
408
+ ```python
409
+ app.add_middleware(
410
+ CORSMiddleware,
411
+ allow_origins=[
412
+ "http://localhost:5102", # Vite dev
413
+ "http://localhost:51x3", # nginx prod
414
+ ],
415
+ allow_credentials=True,
416
+ allow_methods=["*"],
417
+ allow_headers=["*"],
418
+ )
419
+ ```
420
+
421
+ **IIS note:** CORS is generally not needed with IIS because the browser hits IIS on the same origin as the page — no cross-origin request occurs.
422
+
423
+ ---
424
+
425
+ ## 7. Environment-Aware Code — The One-Codebase Pattern
426
+
427
+ The single most important deployment principle: **one codebase serves both dev and prod.** Environment differences are handled by **runtime/build-time detection**, not separate code.
428
+
429
+ ### 7.1 Vite Config — Conditional Base Path
430
+
431
+ When deployed under a subpath (IIS strategy), asset URLs must include the subpath prefix. Vite bakes this in at build time via the `base` config.
432
+
433
+ ```typescript
434
+ // vite.config.ts
435
+ import { defineConfig } from "vite";
436
+ import react from "@vitejs/plugin-react";
437
+
438
+ export default defineConfig(({ mode }) => {
439
+ const isProd = mode === "production";
440
+ const base = isProd ? "/{subpath}/" : "/";
441
+
442
+ return {
443
+ base,
444
+ plugins: [react()],
445
+ server: {
446
+ port: 5102,
447
+ proxy: isProd ? undefined : {
448
+ "/api": {
449
+ target: "http://localhost:8102",
450
+ changeOrigin: true,
451
+ },
452
+ },
453
+ },
454
+ };
455
+ });
456
+ ```
457
+
458
+ **Dev:** `base: "/"`, proxy active → Vite dev server handles everything.
459
+ **Prod build:** `base: "/{subpath}/"` → all asset paths in `dist/index.html` prefixed correctly.
460
+
461
+ If not deploying under a subpath (nginx or root path), keep `base: "/"` for both modes and simplify.
462
+
463
+ ### 7.2 Client-Side Router — basepath Configuration
464
+
465
+ **This is the most common cause of "Not Found" on subpath deployments.** The reverse proxy (IIS) strips the subpath before forwarding to FastAPI, but the browser URL still shows the subpath. Client-side routers need to know about this prefix.
466
+
467
+ #### TanStack Router
468
+
469
+ ```typescript
470
+ // frontend/src/main.tsx
471
+ import { createRouter } from "@tanstack/react-router";
472
+
473
+ const router = createRouter({
474
+ routeTree,
475
+ basepath: import.meta.env.BASE_URL, // dev: "/", prod: "/{subpath}/"
476
+ });
477
+ ```
478
+
479
+ #### React Router (v6+)
480
+
481
+ ```tsx
482
+ <BrowserRouter basename={import.meta.env.BASE_URL}>
483
+ <Routes>...</Routes>
484
+ </BrowserRouter>
485
+ ```
486
+
487
+ `import.meta.env.BASE_URL` is automatically set by Vite from the `base` config in `vite.config.ts` (§7.1). This is the single source of truth for the subpath prefix across the entire frontend.
488
+
489
+ **Why this breaks:** IIS rewrites `/my-app/dashboard` → `/dashboard` server-side. FastAPI serves `index.html`. But the browser URL bar still shows `/my-app/dashboard`. The client-side router sees that URL, and without `basepath`, tries to match `/my-app/dashboard` against your route tree — which only has `/dashboard`. No match → "Not Found".
490
+
491
+ ### 7.3 Axios Client — Dynamic Base URL
492
+
493
+ ```typescript
494
+ // frontend/src/api/client.ts
495
+ import axios from "axios";
496
+
497
+ const apiClient = axios.create({
498
+ baseURL: `${import.meta.env.BASE_URL}api/v1`,
499
+ withCredentials: true, // Required if using Windows Auth (IIS)
500
+ });
501
+
502
+ export default apiClient;
503
+ ```
504
+
505
+ `import.meta.env.BASE_URL` is set by Vite automatically from the `base` config:
506
+ - Dev: `"/"` → `baseURL = "/api/v1"` → Vite proxy forwards to FastAPI
507
+ - Prod (subpath): `"/{subpath}/"` → `baseURL = "/{subpath}/api/v1"` → IIS rewrites to FastAPI
508
+
509
+ **`withCredentials: true`** is required when the reverse proxy handles auth (e.g. IIS Windows Auth). With nginx or no auth, it's harmless.
510
+
511
+ ### 7.4 Static Asset Fetches — The Hidden Subpath Trap
512
+
513
+ Router basepath (§7.2) and API baseURL (§7.3) are well-known subpath requirements. **Runtime fetches of files from the `public/` directory are easily missed** because they work fine in dev (where `BASE_URL` is `"/"`) and only break under a subpath.
514
+
515
+ Examples of static asset fetches that break under a subpath:
516
+ - `fetch("/config.json")` — app configuration
517
+ - `fetch("/ireland-counties.geojson")` — map data
518
+ - `fetch("/manifest.json")` — PWA manifest
519
+ - Any `fetch("/<file>")` for files in `public/`
520
+
521
+ **Fix:** Create a `toAppUrl()` helper and use it everywhere:
522
+
523
+ ```typescript
524
+ // frontend/src/lib/utils.ts
525
+ export function toAppUrl(path: string): string {
526
+ const base = import.meta.env.BASE_URL ?? "/";
527
+ const stripped = path.startsWith("/") ? path.slice(1) : path;
528
+ return `${base}${stripped}`;
529
+ }
530
+ ```
531
+
532
+ Usage:
533
+ ```typescript
534
+ // ❌ BAD: Root-absolute — breaks under /my-app/ subpath
535
+ const GEOJSON_PATH = "/ireland-counties.geojson";
536
+ fetch("/config.json");
537
+
538
+ // ✅ GOOD: Subpath-aware
539
+ import { toAppUrl } from "@/lib/utils";
540
+ const GEOJSON_PATH = toAppUrl("ireland-counties.geojson");
541
+ fetch(toAppUrl("config.json"));
542
+ ```
543
+
544
+ **Detection:** Search for root-absolute fetch patterns that bypass `BASE_URL`:
545
+ ```bash
546
+ grep -rn 'fetch("/' frontend/src/
547
+ grep -rn '= "/[a-zA-Z]' frontend/src/
548
+ ```
549
+
550
+ **The three categories of subpath-sensitive URLs:**
551
+
552
+ | Category | What | Where to fix | Reference |
553
+ |----------|------|-------------|----------|
554
+ | Router basepath | Route matching in browser | `createRouter({ basepath })` | §7.2 |
555
+ | API baseURL | Backend API calls | `axios.create({ baseURL })` | §7.3 |
556
+ | Static asset fetches | `public/` files loaded at runtime | `toAppUrl()` helper | §7.4 |
557
+
558
+ All three must use `import.meta.env.BASE_URL`. Missing any one causes prod-only failures.
559
+
560
+ ### 7.5 package.json Build Scripts
561
+
562
+ ```json
563
+ {
564
+ "scripts": {
565
+ "dev": "vite",
566
+ "build": "tsc -b && vite build",
567
+ "build:prod": "tsc -b && vite build --mode production"
568
+ }
569
+ }
570
+ ```
571
+
572
+ - `npm run build` — default build (dev mode, `base: "/"`)
573
+ - `npm run build:prod` — production build with subpath prefix
574
+
575
+ ### 7.6 FastAPI Settings — Debug Flag
576
+
577
+ Use a settings property to distinguish dev from prod behavior:
578
+
579
+ ```python
580
+ # src/config/settings.py
581
+ from pydantic_settings import BaseSettings
582
+
583
+ class Settings(BaseSettings):
584
+ app_env: str = "development"
585
+ app_port: int = 8102
586
+
587
+ @property
588
+ def debug(self) -> bool:
589
+ return self.app_env == "development"
590
+ ```
591
+
592
+ `config/.env.api.dev` → `APP_ENV=development` → `debug=True` → no static serving, docs enabled
593
+ `config/.env.api.prod` → `APP_ENV=production` → `debug=False` → static serving active, docs disabled
594
+
595
+ ### 7.7 What This Means for Development
596
+
597
+ | Action | Dev | Prod |
598
+ |--------|-----|------|
599
+ | Frontend served by | Vite dev server (`:5102`) | FastAPI (IIS) or nginx |
600
+ | API requests proxied by | Vite `proxy` config | IIS URL Rewrite or nginx |
601
+ | Auth | None | IIS Windows Auth (external) |
602
+ | `npm run build` needed? | No (Vite serves source) | Yes (`build:prod`) |
603
+ | FastAPI serves `dist/`? | No (`debug=True`) | Yes (IIS strategy) or No (nginx strategy) |
604
+
605
+ **All behavior switches automatically based on environment config.** You never maintain separate codebases.
606
+
607
+ ---
608
+
609
+ ## 8. NSSM Script (Reusable)
610
+
611
+ Projects can include a reusable NSSM install script:
612
+
613
+ ```powershell
614
+ # Usage:
615
+ # .\nssm-install.ps1 -ProjectShort nps -Port 8103
616
+ param(
617
+ [Parameter(Mandatory)][string]$ProjectShort,
618
+ [Parameter(Mandatory)][int]$Port,
619
+ [string]$ProjectRoot = (Get-Location).Path
620
+ )
621
+
622
+ $svcName = "<org>-$ProjectShort-api-prod"
623
+ $pythonExe = "$ProjectRoot\.venv\Scripts\python.exe"
624
+
625
+ nssm install $svcName $pythonExe "-m uvicorn src.api.main:app --host 0.0.0.0 --port $Port"
626
+ nssm set $svcName AppDirectory $ProjectRoot
627
+ nssm set $svcName AppEnvironmentExtra "APP_ENV=production"
628
+ nssm set $svcName AppStdout "$ProjectRoot\logs\$svcName-stdout.log"
629
+ nssm set $svcName AppStderr "$ProjectRoot\logs\$svcName-stderr.log"
630
+ nssm set $svcName AppRotateFiles 1
631
+ nssm set $svcName AppRotateBytes 10485760
632
+ nssm set $svcName AppRestartDelay 5000
633
+
634
+ nssm start $svcName
635
+ ```
636
+
637
+ ---
638
+
639
+ ## 9. Troubleshooting
640
+
641
+ | Symptom | Cause | Fix |
642
+ |---------|-------|-----|
643
+ | "Unable to load periods" / API 404 | Frontend hitting static server, no proxy to API | Configure reverse proxy (IIS §5 or nginx §6) |
644
+ | Assets load but API calls fail on subpath | `vite.config.ts` not setting `base` for prod | Use `defineConfig(({ mode }) => ...)` pattern (§7.1) |
645
+ | Site works on prod but breaks after `git pull` | Sysadmin made manual edits — overwritten by pull | Make code environment-aware (§7), never hand-edit on prod |
646
+ | Next `npm run build` breaks prod | Build uses wrong mode (no subpath prefix) | Use `npm run build:prod` with `--mode production` (§7.3) |
647
+ | 502 Bad Gateway (IIS or nginx) | uvicorn not running | `Get-Service <org>-{short}-api-prod` → restart |
648
+ | IIS returns 401 Unauthorized | Windows Auth blocking — user not in AD | Check IIS auth settings, verify user is in allowed AD group |
649
+ | IIS returns 404 on subpath routes | URL Rewrite rules missing or wrong order | API rule must come before frontend catch-all (§5.2) |
650
+ | `bind() failed` on nginx start | Port already in use | `netstat -ano \| findstr {port}` → stop conflicting process |
651
+ | Stale frontend after `git pull` | `dist/` not rebuilt | `cd frontend && npm ci && npm run build:prod` |
652
+ | `ENOTEMPTY: rmdir dist/assets` | Process has `dist/` files locked | Stop service → build → restart |
653
+ | Service starts then stops immediately | Bad path or missing dependency | Check NSSM stderr log: `Get-Content logs\<org>-{short}-api-prod-stderr.log -Tail 50` |
654
+ | `git pull` fails with auth error | Credentials expired or GCM_PROVIDER not set | `git config credential.helper store` + `GCM_PROVIDER=generic` as user env var |
655
+ | Unclear whether remote control works | Assumed lack of access without probing | Run Remote Capability Handshake (§3.1) before deciding |
656
+ | Frontend shows blank page | SPA routing broken (no catch-all) | IIS: check FastAPI SPA catch-all (§5.4). nginx: check `try_files` (§6.2) |
657
+ | Frontend shows blank page (assets 404) | Vite `base` flattened to `"/"` under subpath | Restore `base: mode === "production" ? "/{subpath}/" : "/"` in `vite.config.ts`, rebuild, add deploy guard (§12) |
658
+ | Users see old version after deploy | Browser caching stale `index.html` | Add `Cache-Control: no-cache, no-store, must-revalidate` on HTML responses (§13) |
659
+ | Frontend shows "Not Found" on subpath | Client-side router missing `basepath` | Add `basepath: import.meta.env.BASE_URL` to router config (§7.2) |
660
+ | `Unexpected token '<'` parsing JSON | Static asset fetch uses root-absolute path (`/file.json`) under subpath | Use `toAppUrl()` helper for all `public/` file fetches (§7.4) |
661
+ | `withCredentials` CORS error in browser | Missing `allow_credentials=True` or wrong origin | Add CORS config (§6.4) or verify same-origin (IIS usually doesn't need CORS) |
662
+
663
+ ---
664
+
665
+ ## 10. Deploy Checklists (Copy-Paste)
666
+
667
+ ### First-Time Deploy — IIS Strategy
668
+
669
+ ```
670
+ - [ ] Git clone on prod + credential setup
671
+ - [ ] Python venv + pip install -e .
672
+ - [ ] config/.env.api.prod created (APP_ENV=production)
673
+ - [ ] Frontend: npm ci && npm run build:prod
674
+ - [ ] NSSM: API service registered and started
675
+ - [ ] IIS: URL Rewrite + ARR installed and proxy enabled
676
+ - [ ] IIS: Two rewrite rules added (API first, then frontend catch-all)
677
+ - [ ] IIS: Windows Auth enabled, anonymous disabled (if needed)
678
+ - [ ] FastAPI main.py has conditional static serving block (§5.4)
679
+ - [ ] vite.config.ts uses defineConfig(({ mode }) => ...) with conditional base (§7.1)
680
+ - [ ] client.ts uses import.meta.env.BASE_URL for baseURL (§7.3)
681
+ - [ ] Client-side router has basepath set to import.meta.env.BASE_URL (§7.2)
682
+ - [ ] All `fetch("/...")` calls for public/ assets use `toAppUrl()` helper (§7.4)
683
+ - [ ] Grep for root-absolute fetches: `grep -rn 'fetch("/' frontend/src/` returns no matches
684
+ - [ ] Deploy guard: post-build HTML check for `/{subpath}/assets/` prefix (§12.1)
685
+ - [ ] Cache-Control: `no-cache` on HTML, `immutable` on hashed assets (§13)
686
+ - [ ] Verify: frontend loads, API responds, auth works
687
+ ```
688
+
689
+ ### First-Time Deploy — nginx Strategy
690
+
691
+ ```
692
+ - [ ] Git clone on prod + credential setup
693
+ - [ ] Python venv + pip install -e .
694
+ - [ ] config/.env.api.prod created (APP_ENV=production)
695
+ - [ ] Frontend: npm ci && npm run build
696
+ - [ ] NSSM: API service registered and started
697
+ - [ ] nginx: server block added, tested, reloaded
698
+ - [ ] Firewall: FE port opened (API port NOT exposed)
699
+ - [ ] CORS: prod FE port added to FastAPI allow_origins
700
+ - [ ] Verify: static files, API proxy, health all return 200
701
+ ```
702
+
703
+ ### Subsequent Deploy (Both Strategies)
704
+
705
+ ```
706
+ - [ ] Remote Capability Handshake run (ping + WSMan + Invoke-Command) (§3.1)
707
+ - [ ] git pull on prod
708
+ - [ ] pip install -e . (if deps changed)
709
+ - [ ] cd frontend && npm ci && npm run build:prod (if FE changed, IIS strategy)
710
+ OR npm run build (nginx strategy)
711
+ - [ ] nssm restart <org>-{short}-api-prod
712
+ - [ ] Verify app loads and API responds
713
+ ```
714
+
715
+ ---
716
+
717
+ ## 11. Lessons Learned
718
+
719
+ Hard-won lessons from real deployments:
720
+
721
+ 1. **One codebase, one repo.** Dev and prod share identical code. Environment differences are handled by build-time mode (`vite --mode production`) and runtime config (`APP_ENV`). Never maintain separate dev/prod codebases.
722
+
723
+ 2. **Auth is infrastructure, not application code.** When IIS handles Windows Auth, FastAPI endpoints have zero auth dependencies. IIS rejects unauthenticated requests before they reach uvicorn. Don't add auth middleware to match prod — it's not needed.
724
+
725
+ 3. **`serve` cannot proxy API calls.** Vercel's `serve` is static-only. `/api/v1/...` requests hit serve → 404. You need a reverse proxy (IIS or nginx) or FastAPI must serve static files. Never use `serve` for apps with API backends.
726
+
727
+ 4. **Sysadmin hand-edits on prod cause `git pull` disasters.** If a sysadmin manually edits `vite.config.ts` or `client.ts` on the prod server, the next `git pull` will overwrite those changes and break the site. Make the code environment-aware in the repo so `git pull` is always safe.
728
+
729
+ 5. **Vite is a BUILD tool, not a runtime.** `vite.config.ts` is read at `npm run build` time. The `base` config gets baked into every asset URL in `dist/index.html`. The file doesn't exist in prod at runtime. If the `base` is wrong at build time, the site breaks — even though the config "looks right" in the repo.
730
+
731
+ 6. **Rebuild frontend after `git pull`.** `git pull` updates source files, not the compiled `dist/` folder. Always run the appropriate build command after pulling frontend changes.
732
+
733
+ 7. **IIS subpath deployment eliminates the need for a separate FE port.** With IIS serving on :80/:443 and rewriting to FastAPI, there is no port 5103 or nginx. One entry point, one port.
734
+
735
+ 8. **API ports stay on localhost.** Only expose the entry-point ports (80/443 for IIS, 51xx for nginx). API ports (81xx) are proxied internally — no external access needed.
736
+
737
+ 9. **`withCredentials: true` in axios is needed for IIS Windows Auth.** Without it, the browser won't send Negotiate/NTLM credentials in the API requests. It's harmless when no auth is used.
738
+
739
+ 10. **Check the NSSM stderr log first** when a service won't start: `Get-Content logs\<org>-{short}-api-prod-stderr.log -Tail 50`.
740
+
741
+ 11. **`GCM_PROVIDER=generic` is required** on Windows Server for git credential storage to work with self-hosted git servers. Set as a persistent user env var.
742
+
743
+ 12. **Port conflicts between apps.** When multiple apps share a prod server, API backends must use different ports. Follow a consistent convention: 8103, 8104, 8105, etc.
744
+
745
+ 13. **Client-side router `basepath` is mandatory for subpath deployments.** When IIS serves an app under `/my-app/`, the browser URL retains that prefix. IIS strips it server-side before forwarding to FastAPI, so FastAPI never sees it. But the client-side router runs in the *browser*, where the URL still has `/my-app/`. Without `basepath` (TanStack Router) or `basename` (React Router), the router can't match any route → "Not Found". This is the #1 gotcha in subpath deployments.
746
+
747
+ 14. **Path traversal guard on SPA catch-all.** The `serve_spa` endpoint resolves file paths from user-supplied URL segments. Always `.resolve()` the path and verify it starts with the `dist/` directory to prevent path traversal attacks (OWASP A04).
748
+
749
+ 15. **Static asset fetches are the hidden third subpath trap.** Router basepath and API baseURL are well-documented. Runtime `fetch("/config.json")` or `fetch("/data.geojson")` from `public/` are not — they work in dev (base is `/`) and silently break in prod under a subpath. The symptom is `Unexpected token '<'` because the root-absolute URL hits the wrong app or fallback page, returning HTML instead of JSON. Prevention: create a `toAppUrl()` helper and audit with `grep -rn 'fetch("/' frontend/src/`.
750
+
751
+ 16. **Probe remote capability before declaring no access.** In Windows environments, agents may be able to execute remote commands via WinRM (`Invoke-Command`) even when interactive RDP control is not available. Always run the Remote Capability Handshake (§3.1) before stating remote access is unavailable.
752
+
753
+ 17. **Never flatten `base` to `"/"` when the app is deployed under a subpath.** The Vite `base` config controls every `<script src>` and `<link href>` in the built `index.html`. Changing `base: mode === "production" ? "/{subpath}/" : "/"` → `base: "/"` causes all asset URLs to lose the subpath prefix. If the server has a catch-all rewrite rule (e.g. a Streamlit proxy at root), those asset requests hit the wrong backend → blank page. This is a silent, catastrophic break that is invisible in dev because dev always uses `base: "/"`.
754
+
755
+ 18. **Deploy guards must validate built HTML before restarting services.** A wrong `base` path produces a valid build (exit code 0) but broken output. The deploy script must check the built `index.html` for the expected asset prefix **before** restarting services. If the check fails, the old working version stays live — the deploy aborts without downtime. See §12.
756
+
757
+ 19. **Cache-Control headers prevent stale HTML after deploys.** Vite hashes asset filenames (`index-Crzj2IZk.js`) so old assets never collide with new ones. But `index.html` has a fixed path — without `no-cache`, browsers serve the old HTML (with old asset hashes) even after a deploy. Set `Cache-Control: no-cache, no-store, must-revalidate` on HTML and `public, max-age=31536000, immutable` on hashed assets. See §13.
758
+
759
+ ---
760
+
761
+ ## 12. Deploy Guards — Post-Build Validation
762
+
763
+ A deploy guard is a check that runs **after the build but before the service restart**. If the guard fails, the deploy aborts and the currently-running (working) version stays live. This prevents broken builds from causing downtime.
764
+
765
+ ### 12.1 Base Path Guard (IIS Strategy — Required)
766
+
767
+ When deploying under a subpath, verify the built HTML references the correct asset prefix:
768
+
769
+ ```powershell
770
+ # In deploy.ps1 — after vite build, before nssm restart
771
+ $indexHtml = Join-Path $frontendDir "dist\index.html"
772
+ if (Test-Path $indexHtml) {
773
+ $html = Get-Content $indexHtml -Raw
774
+ if ($html -notmatch '/{subpath}/assets/') {
775
+ throw "DEPLOY GUARD FAILED: Built index.html does not reference /{subpath}/assets/. " +
776
+ "The Vite base path is wrong — aborting deploy before service restart."
777
+ }
778
+ Write-Host "Deploy guard passed: assets reference /{subpath}/"
779
+ } else {
780
+ throw "DEPLOY GUARD FAILED: $indexHtml not found after build"
781
+ }
782
+ ```
783
+
784
+ **Why this matters:** A wrong `base` in `vite.config.ts` produces a successful build (exit 0) but broken output. Without this guard, the deploy script would restart the service, serve the broken HTML, and take the app down. With the guard, the old working version stays live.
785
+
786
+ ### 12.2 CI Build Validation (Belt-and-Suspenders)
787
+
788
+ The same check should run in CI so the broken build is caught **before** it reaches the deploy step:
789
+
790
+ ```yaml
791
+ # In CI workflow — after frontend build step
792
+ - name: Validate production base path
793
+ shell: powershell
794
+ working-directory: frontend
795
+ run: |
796
+ $html = Get-Content dist\index.html -Raw
797
+ if ($html -notmatch '/{subpath}/assets/') {
798
+ throw "DEPLOY GUARD: Built index.html does not reference /{subpath}/assets/. " +
799
+ "Check vite.config.ts base setting."
800
+ }
801
+ Write-Host 'Base-path guard passed'
802
+ ```
803
+
804
+ **Key:** Build with `--mode production` in CI (not just `vite build`), otherwise the mode-conditional `base` won't activate and the guard will always fail.
805
+
806
+ ### 12.3 Additional Deploy Guards (Optional)
807
+
808
+ | Guard | What it checks | When to add |
809
+ |-------|---------------|-------------|
810
+ | **Health endpoint** | API responds 200 after restart | Always |
811
+ | **Base path** | Asset prefix in built HTML | Subpath deployments |
812
+ | **Dist size** | `dist/` is non-empty and reasonable size | Prevent empty-build deploys |
813
+ | **Git SHA convergence** | prod HEAD matches expected commit | Prevent stale-code deploys |
814
+
815
+ ---
816
+
817
+ ## 13. Cache-Control Headers for SPA Serving
818
+
819
+ When FastAPI serves the built frontend (IIS strategy, §5.4), set appropriate cache headers to ensure browsers always get fresh HTML after deploys while caching hashed assets aggressively.
820
+
821
+ ### 13.1 The Problem
822
+
823
+ Vite produces content-hashed asset filenames (`index-Crzj2IZk.js`), so old and new assets never collide. But `index.html` has a **fixed path** — without cache-control headers, browsers may serve a cached `index.html` with stale asset references after a deploy. The user sees a blank page or JS errors because the old hashed files no longer exist.
824
+
825
+ ### 13.2 The Fix — Two Cache Policies
826
+
827
+ ```python
828
+ from fastapi.responses import FileResponse
829
+ from fastapi.staticfiles import StaticFiles
830
+
831
+ # Cache policies
832
+ _NO_CACHE = {"Cache-Control": "no-cache, no-store, must-revalidate", "Pragma": "no-cache", "Expires": "0"}
833
+ _LONG_CACHE = {"Cache-Control": "public, max-age=31536000, immutable"}
834
+
835
+ _FRONTEND_DIST = Path(__file__).resolve().parent.parent.parent / "frontend" / "dist"
836
+
837
+ if not settings.debug and _FRONTEND_DIST.is_dir():
838
+ app.mount("/assets", StaticFiles(directory=_FRONTEND_DIST / "assets"), name="static")
839
+
840
+ @app.get("/{full_path:path}")
841
+ async def serve_spa(full_path: str) -> FileResponse:
842
+ file = (_FRONTEND_DIST / full_path).resolve()
843
+ if file.is_file() and str(file).startswith(str(_FRONTEND_DIST)):
844
+ # Hashed assets → cache forever; everything else → no cache
845
+ headers = _LONG_CACHE if "/assets/" in full_path else _NO_CACHE
846
+ return FileResponse(file, headers=headers)
847
+ return FileResponse(_FRONTEND_DIST / "index.html", headers=_NO_CACHE)
848
+ ```
849
+
850
+ ### 13.3 Cache Policy Summary
851
+
852
+ | Resource | Cache-Control | Why |
853
+ |----------|--------------|-----|
854
+ | `index.html` (SPA shell) | `no-cache, no-store, must-revalidate` | Must always fetch latest — contains hashed asset references |
855
+ | `/assets/*.js`, `/assets/*.css` | `public, max-age=31536000, immutable` | Content-hashed filenames — safe to cache forever |
856
+ | Other static files (`favicon.ico`, etc.) | `no-cache, no-store, must-revalidate` | No content hash — may change between deploys |
857
+
858
+ ### 13.4 nginx Equivalent
859
+
860
+ For the nginx strategy (§6.2), set the same headers in the server block:
861
+
862
+ ```nginx
863
+ location / {
864
+ root G:/Projects/{app-name}/frontend/dist;
865
+ try_files $uri $uri/ /index.html;
866
+
867
+ # SPA shell — never cache
868
+ location = /index.html {
869
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
870
+ add_header Pragma "no-cache";
871
+ add_header Expires "0";
872
+ }
873
+
874
+ # Hashed assets — cache forever
875
+ location /assets/ {
876
+ expires 1y;
877
+ add_header Cache-Control "public, immutable";
878
+ }
879
+ }
880
+ ```