flock-core 0.4.0b35__py3-none-any.whl → 0.4.0b36__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (39) hide show
  1. flock/__init__.py +34 -11
  2. flock/cli/loaded_flock_cli.py +38 -18
  3. flock/core/flock.py +48 -3
  4. flock/themes/alabaster.toml +43 -43
  5. flock/themes/guezwhoz.toml +43 -43
  6. flock/themes/wildcherry.toml +43 -43
  7. flock/themes/wombat.toml +43 -43
  8. flock/themes/zenburn.toml +43 -43
  9. flock/webapp/app/config.py +80 -2
  10. flock/webapp/app/main.py +506 -3
  11. flock/webapp/app/templates/theme_mapper.html +326 -0
  12. flock/webapp/app/theme_mapper.py +812 -0
  13. flock/webapp/run.py +116 -14
  14. flock/webapp/static/css/custom.css +168 -83
  15. flock/webapp/templates/base.html +14 -7
  16. flock/webapp/templates/partials/_agent_detail_form.html +4 -3
  17. flock/webapp/templates/partials/_agent_list.html +1 -6
  18. flock/webapp/templates/partials/_agent_manager_view.html +52 -14
  19. flock/webapp/templates/partials/_agent_manager_view_old.html +19 -0
  20. flock/webapp/templates/partials/_create_flock_form.html +1 -1
  21. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +1 -1
  22. flock/webapp/templates/partials/_env_vars_table.html +25 -0
  23. flock/webapp/templates/partials/_execution_form.html +1 -1
  24. flock/webapp/templates/partials/_execution_view_container.html +13 -12
  25. flock/webapp/templates/partials/_flock_properties_form.html +2 -1
  26. flock/webapp/templates/partials/_header_flock_status.html +5 -0
  27. flock/webapp/templates/partials/_load_manager_view.html +50 -0
  28. flock/webapp/templates/partials/_settings_env_content.html +10 -0
  29. flock/webapp/templates/partials/_settings_theme_content.html +15 -0
  30. flock/webapp/templates/partials/_settings_view.html +36 -0
  31. flock/webapp/templates/partials/_sidebar.html +13 -6
  32. flock/webapp/templates/partials/_structured_data_view.html +4 -4
  33. flock/webapp/templates/partials/_theme_preview.html +23 -0
  34. {flock_core-0.4.0b35.dist-info → flock_core-0.4.0b36.dist-info}/METADATA +1 -1
  35. {flock_core-0.4.0b35.dist-info → flock_core-0.4.0b36.dist-info}/RECORD +38 -29
  36. flock/webapp/templates/partials/_load_manage_view.html +0 -88
  37. {flock_core-0.4.0b35.dist-info → flock_core-0.4.0b36.dist-info}/WHEEL +0 -0
  38. {flock_core-0.4.0b35.dist-info → flock_core-0.4.0b36.dist-info}/entry_points.txt +0 -0
  39. {flock_core-0.4.0b35.dist-info → flock_core-0.4.0b36.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/main.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # ... (keep existing imports and app setup) ...
2
2
  import json
3
+ import os # Needed for environment variable helpers
3
4
  import shutil
5
+ import sys # For path
4
6
  import urllib.parse
5
7
  from pathlib import Path
6
8
 
@@ -15,7 +17,15 @@ from flock.webapp.app.api import (
15
17
  flock_management,
16
18
  registry_viewer,
17
19
  )
18
- from flock.webapp.app.config import FLOCK_FILES_DIR
20
+
21
+ # Import config functions
22
+ from flock.webapp.app.config import (
23
+ DEFAULT_THEME_NAME, # Import default for fallback
24
+ FLOCK_FILES_DIR,
25
+ THEMES_DIR, # Import THEMES_DIR from config
26
+ get_current_theme_name,
27
+ # set_current_theme_name, # Not directly used in main.py, but available
28
+ )
19
29
  from flock.webapp.app.services.flock_service import (
20
30
  clear_current_flock,
21
31
  create_new_flock_service,
@@ -25,6 +35,100 @@ from flock.webapp.app.services.flock_service import (
25
35
  get_flock_preview_service,
26
36
  load_flock_from_file_service,
27
37
  )
38
+ from flock.webapp.app.theme_mapper import alacritty_to_pico
39
+
40
+ # Helper for theme loading
41
+
42
+ # Find the 'src/flock' directory - This can be removed if THEMES_DIR from config is sufficient
43
+ # flock_base_dir = (
44
+ # Path(__file__).resolve().parent.parent.parent
45
+ # ) # src/flock/webapp/app -> src/flock
46
+
47
+ # Calculate themes directory relative to the flock base dir - This can be removed
48
+ # themes_dir = flock_base_dir / "themes"
49
+
50
+ # Ensure the parent ('src') is in the path for core imports
51
+ # This path manipulation might still be needed if core imports are relative in a specific way
52
+ flock_webapp_dir = Path(__file__).resolve().parent.parent # src/flock/webapp/
53
+ flock_base_dir = flock_webapp_dir.parent # src/flock/
54
+ src_dir = flock_base_dir.parent # src/
55
+ if str(src_dir) not in sys.path:
56
+ sys.path.insert(0, str(src_dir))
57
+
58
+ try:
59
+ from flock.core.logging.formatters.themed_formatter import (
60
+ load_theme_from_file,
61
+ )
62
+
63
+ THEME_LOADER_AVAILABLE = True
64
+ # themes_dir is now imported from config
65
+ except ImportError:
66
+ print(
67
+ "Warning: Could not import flock.core theme loading utilities.",
68
+ file=sys.stderr,
69
+ )
70
+ THEME_LOADER_AVAILABLE = False
71
+ # THEMES_DIR will be None if not imported, or its value from config
72
+
73
+ # --- Lightweight .env helpers (self-contained, no external deps) ---
74
+ ENV_FILE = ".env"
75
+ SHOW_SECRETS_KEY = "SHOW_SECRETS"
76
+
77
+ def load_env_file() -> dict[str, str]:
78
+ env_vars: dict[str, str] = {}
79
+ if not os.path.exists(ENV_FILE):
80
+ return env_vars
81
+ with open(ENV_FILE) as f:
82
+ lines = f.readlines()
83
+ for line in lines:
84
+ line = line.strip()
85
+ if not line:
86
+ env_vars[""] = ""
87
+ continue
88
+ if line.startswith("#"):
89
+ env_vars[line] = ""
90
+ continue
91
+ if "=" in line:
92
+ k, v = line.split("=", 1)
93
+ env_vars[k] = v
94
+ else:
95
+ env_vars[line] = ""
96
+ return env_vars
97
+
98
+ def save_env_file(env_vars: dict[str, str]):
99
+ try:
100
+ with open(ENV_FILE, "w") as f:
101
+ for k, v in env_vars.items():
102
+ if k.startswith("#"):
103
+ f.write(f"{k}\n")
104
+ elif not k:
105
+ f.write("\n")
106
+ else:
107
+ f.write(f"{k}={v}\n")
108
+ except Exception as e:
109
+ print(f"[Settings] Failed to save .env: {e}")
110
+
111
+ def is_sensitive(key: str) -> bool:
112
+ patterns = ["key", "token", "secret", "password", "api", "pat"]
113
+ low = key.lower()
114
+ return any(p in low for p in patterns)
115
+
116
+ def mask_sensitive_value(value: str) -> str:
117
+ if not value:
118
+ return value
119
+ if len(value) <= 4:
120
+ return "••••"
121
+ return value[:2] + "•" * (len(value) - 4) + value[-2:]
122
+
123
+ def get_show_secrets_setting(env_vars: dict[str, str]) -> bool:
124
+ return env_vars.get(SHOW_SECRETS_KEY, "false").lower() == "true"
125
+
126
+ def set_show_secrets_setting(show: bool):
127
+ env_vars = load_env_file()
128
+ env_vars[SHOW_SECRETS_KEY] = str(show)
129
+ save_env_file(env_vars)
130
+
131
+ # -------------------------------------------------------------------
28
132
 
29
133
  app = FastAPI(title="Flock UI")
30
134
 
@@ -49,12 +153,163 @@ app.include_router(
49
153
  )
50
154
 
51
155
 
156
+ def generate_theme_css(theme_name: str | None) -> str:
157
+ """Loads a theme TOML and generates CSS variable overrides."""
158
+ if not THEME_LOADER_AVAILABLE or THEMES_DIR is None: # Use imported THEMES_DIR
159
+ return "" # Return empty if theme loading isn't possible
160
+
161
+ active_theme_name = theme_name or DEFAULT_THEME_NAME
162
+ theme_filename = f"{active_theme_name}.toml"
163
+ theme_path = THEMES_DIR / theme_filename # Use imported THEMES_DIR
164
+
165
+ if not theme_path.exists():
166
+ print(
167
+ f"Warning: Theme file not found: {theme_path}. Using default theme.",
168
+ file=sys.stderr,
169
+ )
170
+ # Optionally load the default theme file if the requested one isn't found
171
+ theme_filename = f"{DEFAULT_THEME_NAME}.toml"
172
+ theme_path = THEMES_DIR / theme_filename
173
+ if not theme_path.exists():
174
+ print(
175
+ f"Warning: Default theme file not found: {theme_path}. No theme CSS generated.",
176
+ file=sys.stderr,
177
+ )
178
+ return "" # Return empty if even default isn't found
179
+ active_theme_name = (
180
+ DEFAULT_THEME_NAME # Update active name if defaulted
181
+ )
182
+
183
+ try:
184
+ theme_dict = load_theme_from_file(str(theme_path))
185
+ except Exception as e:
186
+ print(f"Error loading theme file {theme_path}: {e}", file=sys.stderr)
187
+ return "" # Return empty on error
188
+
189
+ # --- Define TOML Color -> CSS Variable Mapping ---
190
+ # This mapping is crucial and may need adjustment based on theme intent & Pico usage
191
+ css_vars = {}
192
+ try:
193
+ # Basic Colors
194
+ # Base colors
195
+ css_vars["--pico-background-color"] = theme_dict["colors"]["primary"].get("background") # Main background
196
+ css_vars["--pico-color"] = theme_dict["colors"]["primary"].get("foreground") # Main text
197
+
198
+ # Headings
199
+ css_vars["--pico-h1-color"] = theme_dict["colors"]["selection"].get("text") # Primary heading
200
+ css_vars["--pico-h2-color"] = theme_dict["colors"]["selection"].get("text") # Secondary heading
201
+ css_vars["--pico-h3-color"] = theme_dict["colors"]["primary"].get("foreground") # Body heading
202
+ css_vars["--pico-muted-color"] = theme_dict["colors"]["selection"].get("text") # Muted/subtext
203
+ css_vars["--pico-primary-inverse"] = theme_dict["colors"]["cursor"].get("text") # Contrast on primary
204
+ css_vars["--pico-contrast"] = theme_dict["colors"]["primary"].get("background") # Contrast text on dark
205
+ css_vars["--pico-contrast-inverse"] = theme_dict["colors"]["primary"].get("foreground") # Contrast text on dark
206
+
207
+ # Primary interaction
208
+ css_vars["--pico-primary"] = theme_dict["colors"]["normal"].get("blue")
209
+ css_vars["--pico-primary-hover"] = theme_dict["colors"]["bright"].get("blue")
210
+ css_vars["--pico-primary-focus"] = f"rgba({theme_dict['colors']['bright'].get('blue')}, 0.25)"
211
+ css_vars["--pico-primary-active"] = theme_dict["colors"]["normal"].get("blue")
212
+
213
+
214
+
215
+ # Secondary interaction
216
+ css_vars["--pico-secondary"] = theme_dict["colors"]["normal"].get("magenta")
217
+ css_vars["--pico-secondary-hover"] = theme_dict["colors"]["bright"].get("magenta")
218
+ css_vars["--pico-secondary-focus"] = f"rgba({theme_dict['colors']['bright'].get('magenta')}, 0.25)"
219
+ css_vars["--pico-secondary-active"] = theme_dict["colors"]["normal"].get("magenta")
220
+
221
+ # Cards and containers
222
+ css_vars["--pico-card-background-color"] = theme_dict["colors"]["primary"].get("background")
223
+ css_vars["--pico-card-border-color"] = theme_dict["colors"]["bright"].get("black") # Mid-tone, visible on bright bg
224
+ css_vars["--pico-card-sectioning-background-color"] = theme_dict["colors"]["selection"].get("background") # Subtle contrast
225
+ css_vars["--pico-border-color"] = theme_dict["colors"]["bright"].get("black")
226
+ css_vars["--pico-muted-border-color"] = theme_dict["colors"]["normal"].get("black") # More subtle than main border
227
+
228
+ # Forms
229
+ css_vars["--pico-form-element-background-color"] = theme_dict["colors"]["primary"].get("background")
230
+ css_vars["--pico-form-element-border-color"] = theme_dict["colors"]["bright"].get("black")
231
+ css_vars["--pico-form-element-color"] = theme_dict["colors"]["primary"].get("foreground")
232
+ css_vars["--pico-form-element-focus-color"] = theme_dict["colors"]["bright"].get("blue")
233
+ css_vars["--pico-form-element-placeholder-color"] = theme_dict["colors"]["bright"].get("black")
234
+ css_vars["--pico-form-element-active-border-color"] = theme_dict["colors"]["bright"].get("blue")
235
+ css_vars["--pico-form-element-active-background-color"] = theme_dict["colors"]["selection"].get("background")
236
+ css_vars["--pico-form-element-disabled-background-color"] = theme_dict["colors"]["normal"].get("black")
237
+ css_vars["--pico-form-element-disabled-border-color"] = theme_dict["colors"]["bright"].get("black")
238
+ css_vars["--pico-form-element-invalid-border-color"] = theme_dict["colors"]["normal"].get("red")
239
+ css_vars["--pico-form-element-invalid-focus-color"] = theme_dict["colors"]["bright"].get("red")
240
+
241
+ # Buttons
242
+ css_vars["--pico-button-base-background-color"] = theme_dict["colors"]["primary"].get("background")
243
+ css_vars["--pico-button-base-color"] = theme_dict["colors"]["primary"].get("foreground")
244
+ css_vars["--pico-button-hover-background-color"] = theme_dict["colors"]["selection"].get("background")
245
+ css_vars["--pico-button-hover-color"] = theme_dict["colors"]["selection"].get("text")
246
+
247
+ # Code blocks
248
+ css_vars["--pico-code-background-color"] = theme_dict["colors"]["cursor"].get("text") # Background behind code
249
+ css_vars["--pico-code-color"] = theme_dict["colors"]["primary"].get("foreground") # Code text
250
+ css_vars["--pico-code-kbd-background-color"] = theme_dict["colors"]["selection"].get("background")
251
+ css_vars["--pico-code-kbd-color"] = theme_dict["colors"]["selection"].get("text")
252
+ css_vars["--pico-code-tag-color"] = theme_dict["colors"]["normal"].get("blue") # Tag elements
253
+ css_vars["--pico-code-property-color"] = theme_dict["colors"]["normal"].get("green") # CSS property names
254
+ css_vars["--pico-code-value-color"] = theme_dict["colors"]["normal"].get("red") # Values and literals
255
+ css_vars["--pico-code-comment-color"] = theme_dict["colors"]["bright"].get("black") # Dim comment color
256
+
257
+
258
+ # Semantic markup
259
+ css_vars["--pico-mark-background-color"] = theme_dict["colors"]["normal"].get("yellow") + "33"
260
+ css_vars["--pico-mark-color"] = theme_dict["colors"]["primary"].get("foreground")
261
+ css_vars["--pico-ins-color"] = theme_dict["colors"]["normal"].get("green")
262
+ css_vars["--pico-del-color"] = theme_dict["colors"]["normal"].get("red")
263
+ # Deleted content - red
264
+ # Custom flock vars (mapped previously, ensure they are kept)
265
+ css_vars["--flock-sidebar-background"] = theme_dict["colors"]["primary"].get("background")# css_vars["--pico-card-background-color"] # Example: Link sidebar to card background
266
+ css_vars["--flock-header-background"] = theme_dict["colors"]["selection"].get("background") # Example: Link header to card background
267
+ css_vars["--flock-error-color"] = theme_dict["colors"]["normal"].get("red", "#dc3545")
268
+ css_vars["--flock-success-color"] = theme_dict["colors"]["normal"].get("green", "#28a745")
269
+
270
+
271
+
272
+ #css_vars.update(pico_vars)
273
+
274
+ except KeyError as e:
275
+ print(
276
+ f"Warning: Missing expected key in theme '{active_theme_name}': {e}. CSS may be incomplete.",
277
+ file=sys.stderr,
278
+ )
279
+
280
+ if not css_vars:
281
+ return "" # Return empty if no variables were mapped
282
+
283
+ pico_vars = alacritty_to_pico(theme_dict)
284
+ css_rules = [f" {name}: {value};" for name, value in pico_vars.items()]
285
+
286
+ # Apply overrides within the currently active theme selector for better specificity
287
+ # We could get the theme name passed in, or maybe check the <html> tag attribute?
288
+ # For simplicity, let's assume we want to override Pico's dark theme vars when a theme is loaded.
289
+ # A better approach might involve removing data-theme="dark" and applying theme to :root
290
+ # or having specific data-theme selectors for each flock theme.
291
+ # Let's try applying to [data-theme="dark"] first.
292
+ selector = '[data-theme="dark"]'
293
+ css_string = ":root {\n" + "\n".join(css_rules) + "\n}"
294
+
295
+ print(
296
+ f"--- Generated CSS for theme '{active_theme_name}' ---"
297
+ ) # Debugging print
298
+ print(css_string) # Debugging print
299
+ print(
300
+ "----------------------------------------------------"
301
+ ) # Debugging print
302
+ return css_string
303
+
304
+
52
305
  def get_base_context(
53
306
  request: Request,
54
307
  error: str = None,
55
308
  success: str = None,
56
309
  ui_mode: str = "standalone",
57
310
  ) -> dict:
311
+ theme_name = get_current_theme_name() # Get theme from config
312
+ theme_css = generate_theme_css(theme_name)
58
313
  return {
59
314
  "request": request,
60
315
  "current_flock": get_current_flock_instance(),
@@ -62,6 +317,8 @@ def get_base_context(
62
317
  "error_message": error,
63
318
  "success_message": success,
64
319
  "ui_mode": ui_mode,
320
+ "theme_css": theme_css, # Add generated CSS to context
321
+ "active_theme_name": theme_name, # Added active theme name
65
322
  }
66
323
 
67
324
 
@@ -226,6 +483,21 @@ async def htmx_get_sidebar(
226
483
  )
227
484
 
228
485
 
486
+ @app.get("/ui/htmx/header-flock-status", response_class=HTMLResponse)
487
+ async def htmx_get_header_flock_status(
488
+ request: Request, ui_mode: str = Query("standalone")
489
+ ):
490
+ # ui_mode isn't strictly needed for this partial's content, but good to accept if passed by hx-get
491
+ return templates.TemplateResponse(
492
+ "partials/_header_flock_status.html",
493
+ {
494
+ "request": request,
495
+ "current_flock": get_current_flock_instance(),
496
+ "current_filename": get_current_flock_filename(),
497
+ },
498
+ )
499
+
500
+
229
501
  @app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse)
230
502
  async def htmx_get_load_flock_view(
231
503
  request: Request,
@@ -237,7 +509,7 @@ async def htmx_get_load_flock_view(
237
509
  # This view is part of the "standalone" functionality.
238
510
  # If somehow accessed in scoped mode, it might be confusing, but let it render.
239
511
  return templates.TemplateResponse(
240
- "partials/_load_manage_view.html",
512
+ "partials/_load_manager_view.html",
241
513
  {
242
514
  "request": request,
243
515
  "error_message": error,
@@ -459,7 +731,7 @@ async def ui_load_flock_by_name_action(
459
731
  {"notify": {"type": "error", "message": error_message}}
460
732
  )
461
733
  return templates.TemplateResponse(
462
- "partials/_load_manage_view.html",
734
+ "partials/_load_manager_view.html",
463
735
  {"request": request, "error_message_inline": error_message},
464
736
  headers=response_headers,
465
737
  )
@@ -569,3 +841,234 @@ async def ui_create_flock_action(
569
841
  },
570
842
  headers=response_headers,
571
843
  )
844
+
845
+
846
+ # --- Settings Page ---
847
+ @app.get("/ui/settings", response_class=HTMLResponse)
848
+ async def page_settings(
849
+ request: Request,
850
+ error: str = None,
851
+ success: str = None,
852
+ ui_mode: str = Query("standalone"),
853
+ ):
854
+ """Render the Settings top-level page which in turn loads the HTMX settings view."""
855
+ context = get_base_context(request, error, success, ui_mode)
856
+ context["initial_content_url"] = "/ui/htmx/settings-view"
857
+ return templates.TemplateResponse("base.html", context)
858
+
859
+
860
+ # Helper to build env var list for templates
861
+
862
+ def _prepare_env_vars_for_template():
863
+ env_vars_raw = load_env_file()
864
+ show_secrets = get_show_secrets_setting(env_vars_raw)
865
+ env_vars_list = []
866
+ for name, value in env_vars_raw.items():
867
+ if name.startswith("#") or name == "":
868
+ # skip comments/blank for table
869
+ continue
870
+ display_value = (
871
+ value
872
+ if (not is_sensitive(name) or show_secrets)
873
+ else mask_sensitive_value(value)
874
+ )
875
+ env_vars_list.append({"name": name, "value": display_value})
876
+ return env_vars_list, show_secrets
877
+
878
+
879
+ @app.get("/ui/htmx/settings-view", response_class=HTMLResponse)
880
+ async def htmx_get_settings_view(request: Request):
881
+ """Return the settings composite view (env vars + theme switcher)."""
882
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
883
+ theme_name = get_current_theme_name()
884
+ themes_available = []
885
+ if THEMES_DIR and THEMES_DIR.exists():
886
+ themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")]
887
+ return templates.TemplateResponse(
888
+ "partials/_settings_view.html",
889
+ {
890
+ "request": request,
891
+ "env_vars": env_vars_list,
892
+ "show_secrets": show_secrets,
893
+ "themes": themes_available,
894
+ "current_theme": theme_name,
895
+ },
896
+ )
897
+
898
+
899
+ # --- Env Var Manager Endpoints ---
900
+ @app.post("/ui/htmx/toggle-show-secrets", response_class=HTMLResponse)
901
+ async def htmx_toggle_show_secrets(request: Request):
902
+ # Toggle and return updated table
903
+ env_vars_raw = load_env_file()
904
+ current = get_show_secrets_setting(env_vars_raw)
905
+ set_show_secrets_setting(not current)
906
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
907
+ return templates.TemplateResponse(
908
+ "partials/_env_vars_table.html",
909
+ {
910
+ "request": request,
911
+ "env_vars": env_vars_list,
912
+ "show_secrets": show_secrets,
913
+ },
914
+ )
915
+
916
+
917
+ @app.post("/ui/htmx/env-delete", response_class=HTMLResponse)
918
+ async def htmx_env_delete(request: Request, var_name: str = Form(...)):
919
+ env_vars_raw = load_env_file()
920
+ if var_name in env_vars_raw:
921
+ del env_vars_raw[var_name]
922
+ save_env_file(env_vars_raw)
923
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
924
+ return templates.TemplateResponse(
925
+ "partials/_env_vars_table.html",
926
+ {
927
+ "request": request,
928
+ "env_vars": env_vars_list,
929
+ "show_secrets": show_secrets,
930
+ },
931
+ )
932
+
933
+
934
+ @app.post("/ui/htmx/env-edit", response_class=HTMLResponse)
935
+ async def htmx_env_edit(
936
+ request: Request,
937
+ var_name: str = Form(...),
938
+ ):
939
+ # New value is provided via HX-Prompt header
940
+ new_value = request.headers.get("HX-Prompt")
941
+ if new_value is None:
942
+ # Nothing entered; just return current table
943
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
944
+ return templates.TemplateResponse(
945
+ "partials/_env_vars_table.html",
946
+ {
947
+ "request": request,
948
+ "env_vars": env_vars_list,
949
+ "show_secrets": show_secrets,
950
+ },
951
+ )
952
+ env_vars_raw = load_env_file()
953
+ env_vars_raw[var_name] = new_value
954
+ save_env_file(env_vars_raw)
955
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
956
+ return templates.TemplateResponse(
957
+ "partials/_env_vars_table.html",
958
+ {
959
+ "request": request,
960
+ "env_vars": env_vars_list,
961
+ "show_secrets": show_secrets,
962
+ },
963
+ )
964
+
965
+
966
+ @app.get("/ui/htmx/env-add-form", response_class=HTMLResponse)
967
+ async def htmx_env_add_form(request: Request):
968
+ # Return simple form row at top of table
969
+ return HTMLResponse(
970
+ """
971
+ <form hx-post='/ui/htmx/env-add' hx-target='#env-vars-container' hx-swap='outerHTML' style='display:flex; gap:0.5rem; margin-bottom:0.5rem;'>
972
+ <input name='var_name' placeholder='NAME' required style='flex:2;'>
973
+ <input name='var_value' placeholder='VALUE' style='flex:3;'>
974
+ <button type='submit'>Add</button>
975
+ </form>
976
+ """
977
+ )
978
+
979
+
980
+ @app.post("/ui/htmx/env-add", response_class=HTMLResponse)
981
+ async def htmx_env_add(request: Request, var_name: str = Form(...), var_value: str = Form("")):
982
+ env_vars_raw = load_env_file()
983
+ env_vars_raw[var_name] = var_value
984
+ save_env_file(env_vars_raw)
985
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
986
+ return templates.TemplateResponse(
987
+ "partials/_env_vars_table.html",
988
+ {
989
+ "request": request,
990
+ "env_vars": env_vars_list,
991
+ "show_secrets": show_secrets,
992
+ },
993
+ )
994
+
995
+
996
+ # --- Theme Preview and Apply Endpoints ---
997
+ @app.get("/ui/htmx/theme-preview", response_class=HTMLResponse)
998
+ async def htmx_theme_preview(request: Request, theme: str = Query(None)):
999
+ theme_name = theme or get_current_theme_name()
1000
+ # Load theme data
1001
+ try:
1002
+ theme_path = THEMES_DIR / f"{theme_name}.toml" if THEMES_DIR else None
1003
+ if not (theme_path and theme_path.exists()):
1004
+ return HTMLResponse("<p>Theme not found.</p>")
1005
+ from flock.core.logging.formatters.themed_formatter import (
1006
+ load_theme_from_file,
1007
+ )
1008
+ theme_data = load_theme_from_file(str(theme_path))
1009
+ except Exception as e:
1010
+ return HTMLResponse(f"<p>Error loading theme: {e}</p>")
1011
+
1012
+ css_vars = alacritty_to_pico(theme_data)
1013
+ css_vars_str = ":root {\n" + "\n".join([f" {k}: {v};" for k, v in css_vars.items()]) + "\n}"
1014
+
1015
+ main_colors = [
1016
+ ("Background", css_vars["--pico-background-color"]),
1017
+ ("Text", css_vars["--pico-color"]),
1018
+ ("Primary", css_vars["--pico-primary"]),
1019
+ ("Secondary", css_vars["--pico-secondary"]),
1020
+ ("Muted", css_vars["--pico-muted-color"]),
1021
+ ]
1022
+
1023
+ return templates.TemplateResponse(
1024
+ "partials/_theme_preview.html",
1025
+ {
1026
+ "request": request,
1027
+ "theme_name": theme_name,
1028
+ "css_vars_str": css_vars_str,
1029
+ "main_colors": main_colors,
1030
+ },
1031
+ )
1032
+
1033
+
1034
+ @app.post("/ui/apply-theme")
1035
+ async def apply_theme(request: Request, theme: str = Form(...)):
1036
+ try:
1037
+ from flock.webapp.app.config import set_current_theme_name
1038
+
1039
+ set_current_theme_name(theme)
1040
+ # Trigger full refresh via HTMX
1041
+ headers = {"HX-Refresh": "true"}
1042
+ return HTMLResponse("", headers=headers)
1043
+ except Exception as e:
1044
+ return HTMLResponse(f"Failed to apply theme: {e}", status_code=500)
1045
+
1046
+
1047
+ # --- Settings Content Endpoints (for tab navigation) ---
1048
+ @app.get("/ui/htmx/settings/env-vars", response_class=HTMLResponse)
1049
+ async def htmx_settings_env_vars(request: Request):
1050
+ env_vars_list, show_secrets = _prepare_env_vars_for_template()
1051
+ return templates.TemplateResponse(
1052
+ "partials/_settings_env_content.html",
1053
+ {
1054
+ "request": request,
1055
+ "env_vars": env_vars_list,
1056
+ "show_secrets": show_secrets,
1057
+ },
1058
+ )
1059
+
1060
+
1061
+ @app.get("/ui/htmx/settings/theme", response_class=HTMLResponse)
1062
+ async def htmx_settings_theme(request: Request):
1063
+ theme_name = get_current_theme_name()
1064
+ themes_available = []
1065
+ if THEMES_DIR and THEMES_DIR.exists():
1066
+ themes_available = [p.stem for p in THEMES_DIR.glob("*.toml")]
1067
+ return templates.TemplateResponse(
1068
+ "partials/_settings_theme_content.html",
1069
+ {
1070
+ "request": request,
1071
+ "themes": themes_available,
1072
+ "current_theme": theme_name,
1073
+ },
1074
+ )