flock-core 0.4.0b34__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.
- flock/__init__.py +53 -8
- flock/cli/loaded_flock_cli.py +38 -18
- flock/core/api/main.py +138 -39
- flock/core/flock.py +48 -3
- flock/themes/alabaster.toml +43 -43
- flock/themes/guezwhoz.toml +43 -43
- flock/themes/wildcherry.toml +43 -43
- flock/themes/wombat.toml +43 -43
- flock/themes/zenburn.toml +43 -43
- flock/webapp/__init__.py +1 -0
- flock/webapp/app/__init__.py +0 -0
- flock/webapp/app/api/__init__.py +0 -0
- flock/webapp/app/api/agent_management.py +270 -0
- flock/webapp/app/api/execution.py +173 -0
- flock/webapp/app/api/flock_management.py +102 -0
- flock/webapp/app/api/registry_viewer.py +30 -0
- flock/webapp/app/config.py +87 -0
- flock/webapp/app/main.py +1074 -0
- flock/webapp/app/models_ui.py +7 -0
- flock/webapp/app/services/__init__.py +0 -0
- flock/webapp/app/services/flock_service.py +291 -0
- flock/webapp/app/templates/theme_mapper.html +326 -0
- flock/webapp/app/theme_mapper.py +812 -0
- flock/webapp/app/utils.py +85 -0
- flock/webapp/run.py +132 -0
- flock/webapp/static/css/custom.css +612 -0
- flock/webapp/templates/base.html +105 -0
- flock/webapp/templates/flock_editor.html +17 -0
- flock/webapp/templates/index.html +12 -0
- flock/webapp/templates/partials/_agent_detail_form.html +98 -0
- flock/webapp/templates/partials/_agent_list.html +19 -0
- flock/webapp/templates/partials/_agent_manager_view.html +53 -0
- flock/webapp/templates/partials/_agent_manager_view_old.html +19 -0
- flock/webapp/templates/partials/_agent_tools_checklist.html +14 -0
- flock/webapp/templates/partials/_create_flock_form.html +52 -0
- flock/webapp/templates/partials/_dashboard_flock_detail.html +18 -0
- flock/webapp/templates/partials/_dashboard_flock_file_list.html +17 -0
- flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +29 -0
- flock/webapp/templates/partials/_dashboard_upload_flock_form.html +17 -0
- flock/webapp/templates/partials/_dynamic_input_form_content.html +22 -0
- flock/webapp/templates/partials/_env_vars_table.html +25 -0
- flock/webapp/templates/partials/_execution_form.html +48 -0
- flock/webapp/templates/partials/_execution_view_container.html +20 -0
- flock/webapp/templates/partials/_flock_file_list.html +24 -0
- flock/webapp/templates/partials/_flock_properties_form.html +52 -0
- flock/webapp/templates/partials/_flock_upload_form.html +17 -0
- flock/webapp/templates/partials/_header_flock_status.html +5 -0
- flock/webapp/templates/partials/_load_manager_view.html +50 -0
- flock/webapp/templates/partials/_registry_table.html +25 -0
- flock/webapp/templates/partials/_registry_viewer_content.html +47 -0
- flock/webapp/templates/partials/_results_display.html +35 -0
- flock/webapp/templates/partials/_settings_env_content.html +10 -0
- flock/webapp/templates/partials/_settings_theme_content.html +15 -0
- flock/webapp/templates/partials/_settings_view.html +36 -0
- flock/webapp/templates/partials/_sidebar.html +70 -0
- flock/webapp/templates/partials/_structured_data_view.html +40 -0
- flock/webapp/templates/partials/_theme_preview.html +23 -0
- flock/webapp/templates/registry_viewer.html +84 -0
- {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/METADATA +1 -1
- {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/RECORD +63 -14
- {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/WHEEL +0 -0
- {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.0b34.dist-info → flock_core-0.4.0b36.dist-info}/licenses/LICENSE +0 -0
flock/webapp/app/main.py
ADDED
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
# ... (keep existing imports and app setup) ...
|
|
2
|
+
import json
|
|
3
|
+
import os # Needed for environment variable helpers
|
|
4
|
+
import shutil
|
|
5
|
+
import sys # For path
|
|
6
|
+
import urllib.parse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from fastapi import FastAPI, File, Form, Query, Request, UploadFile
|
|
10
|
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
11
|
+
from fastapi.staticfiles import StaticFiles
|
|
12
|
+
from fastapi.templating import Jinja2Templates
|
|
13
|
+
|
|
14
|
+
from flock.webapp.app.api import (
|
|
15
|
+
agent_management,
|
|
16
|
+
execution,
|
|
17
|
+
flock_management,
|
|
18
|
+
registry_viewer,
|
|
19
|
+
)
|
|
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
|
+
)
|
|
29
|
+
from flock.webapp.app.services.flock_service import (
|
|
30
|
+
clear_current_flock,
|
|
31
|
+
create_new_flock_service,
|
|
32
|
+
get_available_flock_files,
|
|
33
|
+
get_current_flock_filename,
|
|
34
|
+
get_current_flock_instance,
|
|
35
|
+
get_flock_preview_service,
|
|
36
|
+
load_flock_from_file_service,
|
|
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
|
+
# -------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
app = FastAPI(title="Flock UI")
|
|
134
|
+
|
|
135
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
136
|
+
app.mount(
|
|
137
|
+
"/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static"
|
|
138
|
+
)
|
|
139
|
+
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
|
140
|
+
|
|
141
|
+
app.include_router(
|
|
142
|
+
flock_management.router, prefix="/api/flocks", tags=["Flock Management API"]
|
|
143
|
+
)
|
|
144
|
+
app.include_router(
|
|
145
|
+
agent_management.router, prefix="/api/flocks", tags=["Agent Management API"]
|
|
146
|
+
)
|
|
147
|
+
# Ensure execution router is imported and included BEFORE it's referenced by the renamed route
|
|
148
|
+
app.include_router(
|
|
149
|
+
execution.router, prefix="/api/flocks", tags=["Execution API"]
|
|
150
|
+
)
|
|
151
|
+
app.include_router(
|
|
152
|
+
registry_viewer.router, prefix="/api/registry", tags=["Registry API"]
|
|
153
|
+
)
|
|
154
|
+
|
|
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
|
+
|
|
305
|
+
def get_base_context(
|
|
306
|
+
request: Request,
|
|
307
|
+
error: str = None,
|
|
308
|
+
success: str = None,
|
|
309
|
+
ui_mode: str = "standalone",
|
|
310
|
+
) -> dict:
|
|
311
|
+
theme_name = get_current_theme_name() # Get theme from config
|
|
312
|
+
theme_css = generate_theme_css(theme_name)
|
|
313
|
+
return {
|
|
314
|
+
"request": request,
|
|
315
|
+
"current_flock": get_current_flock_instance(),
|
|
316
|
+
"current_filename": get_current_flock_filename(),
|
|
317
|
+
"error_message": error,
|
|
318
|
+
"success_message": success,
|
|
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
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# --- Main Page Routes ---
|
|
326
|
+
@app.get("/", response_class=HTMLResponse)
|
|
327
|
+
async def page_dashboard(
|
|
328
|
+
request: Request,
|
|
329
|
+
error: str = None,
|
|
330
|
+
success: str = None,
|
|
331
|
+
ui_mode: str = Query(
|
|
332
|
+
None
|
|
333
|
+
), # Default to None to detect if it was explicitly passed
|
|
334
|
+
):
|
|
335
|
+
# Determine effective ui_mode
|
|
336
|
+
effective_ui_mode = ui_mode
|
|
337
|
+
flock_is_preloaded = get_current_flock_instance() is not None
|
|
338
|
+
|
|
339
|
+
if effective_ui_mode is None: # ui_mode not in query parameters
|
|
340
|
+
if flock_is_preloaded:
|
|
341
|
+
# If a flock is preloaded (likely by API server) and no mode specified,
|
|
342
|
+
# default to scoped and redirect to make the URL explicit.
|
|
343
|
+
return RedirectResponse(url="/?ui_mode=scoped", status_code=307)
|
|
344
|
+
else:
|
|
345
|
+
effective_ui_mode = "standalone" # True standalone launch
|
|
346
|
+
elif effective_ui_mode == "scoped" and not flock_is_preloaded:
|
|
347
|
+
# If explicitly asked for scoped mode but no flock is loaded (e.g. user bookmarked URL after server restart)
|
|
348
|
+
# It will show the "scoped-no-flock-view". We could also redirect to standalone.
|
|
349
|
+
# For now, let it show the "no flock loaded in scoped mode" message.
|
|
350
|
+
pass
|
|
351
|
+
|
|
352
|
+
# Conditional flock clearing based on the *effective* ui_mode
|
|
353
|
+
if effective_ui_mode != "scoped":
|
|
354
|
+
# If we are about to enter standalone mode, and a flock might have been
|
|
355
|
+
# preloaded (e.g. user navigated from /?ui_mode=scoped to /?ui_mode=standalone),
|
|
356
|
+
# ensure it's cleared for a true standalone experience.
|
|
357
|
+
if flock_is_preloaded: # Clear only if one was there
|
|
358
|
+
clear_current_flock()
|
|
359
|
+
|
|
360
|
+
context = get_base_context(request, error, success, effective_ui_mode)
|
|
361
|
+
|
|
362
|
+
if effective_ui_mode == "scoped":
|
|
363
|
+
if get_current_flock_instance(): # Re-check, as clear_current_flock might have run if user switched modes
|
|
364
|
+
context["initial_content_url"] = (
|
|
365
|
+
"/api/flocks/htmx/flock-properties-form"
|
|
366
|
+
)
|
|
367
|
+
else:
|
|
368
|
+
context["initial_content_url"] = "/ui/htmx/scoped-no-flock-view"
|
|
369
|
+
else: # Standalone mode
|
|
370
|
+
context["initial_content_url"] = "/ui/htmx/load-flock-view"
|
|
371
|
+
|
|
372
|
+
return templates.TemplateResponse("base.html", context)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
@app.get("/ui/editor/properties", response_class=HTMLResponse)
|
|
376
|
+
async def page_editor_properties(
|
|
377
|
+
request: Request,
|
|
378
|
+
success: str = None,
|
|
379
|
+
error: str = None,
|
|
380
|
+
ui_mode: str = Query("standalone"),
|
|
381
|
+
):
|
|
382
|
+
# ... (same as before) ...
|
|
383
|
+
flock = get_current_flock_instance()
|
|
384
|
+
if not flock:
|
|
385
|
+
err_msg = "No flock loaded. Please load or create a flock first."
|
|
386
|
+
# Preserve ui_mode on redirect if it was passed
|
|
387
|
+
redirect_url = f"/?error={urllib.parse.quote(err_msg)}"
|
|
388
|
+
if ui_mode == "scoped":
|
|
389
|
+
redirect_url += "&ui_mode=scoped"
|
|
390
|
+
return RedirectResponse(url=redirect_url, status_code=303)
|
|
391
|
+
context = get_base_context(request, error, success, ui_mode)
|
|
392
|
+
context["initial_content_url"] = "/api/flocks/htmx/flock-properties-form"
|
|
393
|
+
return templates.TemplateResponse("base.html", context)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@app.get("/ui/editor/agents", response_class=HTMLResponse)
|
|
397
|
+
async def page_editor_agents(
|
|
398
|
+
request: Request,
|
|
399
|
+
success: str = None,
|
|
400
|
+
error: str = None,
|
|
401
|
+
ui_mode: str = Query("standalone"),
|
|
402
|
+
):
|
|
403
|
+
# ... (same as before) ...
|
|
404
|
+
flock = get_current_flock_instance()
|
|
405
|
+
if not flock:
|
|
406
|
+
# Preserve ui_mode on redirect
|
|
407
|
+
redirect_url = (
|
|
408
|
+
f"/?error={urllib.parse.quote('No flock loaded for agent view.')}"
|
|
409
|
+
)
|
|
410
|
+
if ui_mode == "scoped":
|
|
411
|
+
redirect_url += "&ui_mode=scoped"
|
|
412
|
+
return RedirectResponse(url=redirect_url, status_code=303)
|
|
413
|
+
context = get_base_context(request, error, success, ui_mode)
|
|
414
|
+
context["initial_content_url"] = "/ui/htmx/agent-manager-view"
|
|
415
|
+
return templates.TemplateResponse("base.html", context)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@app.get("/ui/editor/execute", response_class=HTMLResponse)
|
|
419
|
+
async def page_editor_execute(
|
|
420
|
+
request: Request,
|
|
421
|
+
success: str = None,
|
|
422
|
+
error: str = None,
|
|
423
|
+
ui_mode: str = Query("standalone"),
|
|
424
|
+
):
|
|
425
|
+
flock = get_current_flock_instance()
|
|
426
|
+
if not flock:
|
|
427
|
+
# Preserve ui_mode on redirect
|
|
428
|
+
redirect_url = (
|
|
429
|
+
f"/?error={urllib.parse.quote('No flock loaded to execute.')}"
|
|
430
|
+
)
|
|
431
|
+
if ui_mode == "scoped":
|
|
432
|
+
redirect_url += "&ui_mode=scoped"
|
|
433
|
+
return RedirectResponse(url=redirect_url, status_code=303)
|
|
434
|
+
context = get_base_context(request, error, success, ui_mode)
|
|
435
|
+
# UPDATED initial_content_url
|
|
436
|
+
context["initial_content_url"] = "/ui/htmx/execution-view-container"
|
|
437
|
+
return templates.TemplateResponse("base.html", context)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ... (registry and create page routes remain the same) ...
|
|
441
|
+
@app.get("/ui/registry", response_class=HTMLResponse)
|
|
442
|
+
async def page_registry(
|
|
443
|
+
request: Request,
|
|
444
|
+
error: str = None,
|
|
445
|
+
success: str = None,
|
|
446
|
+
ui_mode: str = Query("standalone"),
|
|
447
|
+
):
|
|
448
|
+
context = get_base_context(request, error, success, ui_mode)
|
|
449
|
+
context["initial_content_url"] = "/ui/htmx/registry-viewer"
|
|
450
|
+
return templates.TemplateResponse("base.html", context)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@app.get("/ui/create", response_class=HTMLResponse)
|
|
454
|
+
async def page_create(
|
|
455
|
+
request: Request,
|
|
456
|
+
error: str = None,
|
|
457
|
+
success: str = None,
|
|
458
|
+
ui_mode: str = Query("standalone"),
|
|
459
|
+
):
|
|
460
|
+
clear_current_flock()
|
|
461
|
+
# Create page should arguably not be accessible in scoped mode directly via URL,
|
|
462
|
+
# as the sidebar link will be hidden. If accessed, treat as standalone.
|
|
463
|
+
context = get_base_context(
|
|
464
|
+
request, error, success, "standalone"
|
|
465
|
+
) # Force standalone for direct access
|
|
466
|
+
context["initial_content_url"] = "/ui/htmx/create-flock-form"
|
|
467
|
+
return templates.TemplateResponse("base.html", context)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
# --- HTMX Content Routes ---
|
|
471
|
+
@app.get("/ui/htmx/sidebar", response_class=HTMLResponse)
|
|
472
|
+
async def htmx_get_sidebar(
|
|
473
|
+
request: Request, ui_mode: str = Query("standalone")
|
|
474
|
+
):
|
|
475
|
+
# ... (same as before) ...
|
|
476
|
+
return templates.TemplateResponse(
|
|
477
|
+
"partials/_sidebar.html",
|
|
478
|
+
{
|
|
479
|
+
"request": request,
|
|
480
|
+
"current_flock": get_current_flock_instance(),
|
|
481
|
+
"ui_mode": ui_mode,
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
|
|
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
|
+
|
|
501
|
+
@app.get("/ui/htmx/load-flock-view", response_class=HTMLResponse)
|
|
502
|
+
async def htmx_get_load_flock_view(
|
|
503
|
+
request: Request,
|
|
504
|
+
error: str = None,
|
|
505
|
+
success: str = None,
|
|
506
|
+
ui_mode: str = Query("standalone"),
|
|
507
|
+
):
|
|
508
|
+
# ... (same as before) ...
|
|
509
|
+
# This view is part of the "standalone" functionality.
|
|
510
|
+
# If somehow accessed in scoped mode, it might be confusing, but let it render.
|
|
511
|
+
return templates.TemplateResponse(
|
|
512
|
+
"partials/_load_manager_view.html",
|
|
513
|
+
{
|
|
514
|
+
"request": request,
|
|
515
|
+
"error_message": error,
|
|
516
|
+
"success_message": success,
|
|
517
|
+
"ui_mode": ui_mode, # Pass for consistency, though not directly used in this partial
|
|
518
|
+
},
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@app.get("/ui/htmx/dashboard-flock-file-list", response_class=HTMLResponse)
|
|
523
|
+
async def htmx_get_dashboard_flock_file_list_partial(request: Request):
|
|
524
|
+
# ... (same as before) ...
|
|
525
|
+
return templates.TemplateResponse(
|
|
526
|
+
"partials/_dashboard_flock_file_list.html",
|
|
527
|
+
{"request": request, "flock_files": get_available_flock_files()},
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@app.get("/ui/htmx/dashboard-default-action-pane", response_class=HTMLResponse)
|
|
532
|
+
async def htmx_get_dashboard_default_action_pane(request: Request):
|
|
533
|
+
# ... (same as before) ...
|
|
534
|
+
return HTMLResponse("""
|
|
535
|
+
<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;">
|
|
536
|
+
<p>Select a Flock from the list to view its details and load it into the editor.</p>
|
|
537
|
+
<hr>
|
|
538
|
+
<p>Or, create a new Flock or upload an existing one using the "Create New Flock" option in the sidebar.</p>
|
|
539
|
+
</article>
|
|
540
|
+
""")
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@app.get(
|
|
544
|
+
"/ui/htmx/dashboard-flock-properties-preview/{filename}",
|
|
545
|
+
response_class=HTMLResponse,
|
|
546
|
+
)
|
|
547
|
+
async def htmx_get_dashboard_flock_properties_preview(
|
|
548
|
+
request: Request, filename: str
|
|
549
|
+
):
|
|
550
|
+
# ... (same as before) ...
|
|
551
|
+
preview_flock_data = get_flock_preview_service(filename)
|
|
552
|
+
return templates.TemplateResponse(
|
|
553
|
+
"partials/_dashboard_flock_properties_preview.html",
|
|
554
|
+
{
|
|
555
|
+
"request": request,
|
|
556
|
+
"selected_filename": filename,
|
|
557
|
+
"preview_flock": preview_flock_data,
|
|
558
|
+
},
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@app.get("/ui/htmx/create-flock-form", response_class=HTMLResponse)
|
|
563
|
+
async def htmx_get_create_flock_form(
|
|
564
|
+
request: Request,
|
|
565
|
+
error: str = None,
|
|
566
|
+
success: str = None,
|
|
567
|
+
ui_mode: str = Query("standalone"),
|
|
568
|
+
):
|
|
569
|
+
# ... (same as before) ...
|
|
570
|
+
# This view is part of the "standalone" functionality.
|
|
571
|
+
return templates.TemplateResponse(
|
|
572
|
+
"partials/_create_flock_form.html",
|
|
573
|
+
{
|
|
574
|
+
"request": request,
|
|
575
|
+
"error_message": error,
|
|
576
|
+
"success_message": success,
|
|
577
|
+
"ui_mode": ui_mode, # Pass for consistency
|
|
578
|
+
},
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
@app.get("/ui/htmx/agent-manager-view", response_class=HTMLResponse)
|
|
583
|
+
async def htmx_get_agent_manager_view(request: Request):
|
|
584
|
+
# ... (same as before) ...
|
|
585
|
+
flock = get_current_flock_instance()
|
|
586
|
+
if not flock:
|
|
587
|
+
return HTMLResponse(
|
|
588
|
+
"<article class='error'><p>No flock loaded. Cannot manage agents.</p></article>"
|
|
589
|
+
)
|
|
590
|
+
return templates.TemplateResponse(
|
|
591
|
+
"partials/_agent_manager_view.html",
|
|
592
|
+
{"request": request, "flock": flock},
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
@app.get("/ui/htmx/registry-viewer", response_class=HTMLResponse)
|
|
597
|
+
async def htmx_get_registry_viewer(request: Request):
|
|
598
|
+
# ... (same as before) ...
|
|
599
|
+
return templates.TemplateResponse(
|
|
600
|
+
"partials/_registry_viewer_content.html", {"request": request}
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
# --- NEW HTMX ROUTE FOR THE EXECUTION VIEW CONTAINER ---
|
|
605
|
+
@app.get("/ui/htmx/execution-view-container", response_class=HTMLResponse)
|
|
606
|
+
async def htmx_get_execution_view_container(request: Request):
|
|
607
|
+
flock = get_current_flock_instance()
|
|
608
|
+
if not flock:
|
|
609
|
+
return HTMLResponse(
|
|
610
|
+
"<article class='error'><p>No Flock loaded. Cannot execute.</p></article>"
|
|
611
|
+
)
|
|
612
|
+
return templates.TemplateResponse(
|
|
613
|
+
"partials/_execution_view_container.html", {"request": request}
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
# A new HTMX route for scoped mode when no flock is initially loaded (should ideally not happen)
|
|
618
|
+
@app.get("/ui/htmx/scoped-no-flock-view", response_class=HTMLResponse)
|
|
619
|
+
async def htmx_scoped_no_flock_view(request: Request):
|
|
620
|
+
return HTMLResponse("""
|
|
621
|
+
<article style="text-align:center; margin-top: 2rem; border: none; background: transparent;">
|
|
622
|
+
<hgroup>
|
|
623
|
+
<h2>Scoped Flock Mode</h2>
|
|
624
|
+
<h3>No Flock Loaded</h3>
|
|
625
|
+
</hgroup>
|
|
626
|
+
<p>This UI is in a scoped mode, expecting a Flock to be pre-loaded.</p>
|
|
627
|
+
<p>Please ensure the calling application provides a Flock instance.</p>
|
|
628
|
+
</article>
|
|
629
|
+
""")
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# Endpoint to launch the UI in scoped mode with a preloaded flock
|
|
633
|
+
@app.post("/ui/launch-scoped", response_class=RedirectResponse)
|
|
634
|
+
async def launch_scoped_ui(
|
|
635
|
+
request: Request,
|
|
636
|
+
flock_data: dict, # This would be the flock's JSON data
|
|
637
|
+
# Potentially also receive filename if it's from a saved file
|
|
638
|
+
):
|
|
639
|
+
# Here, you would parse flock_data, create a Flock instance,
|
|
640
|
+
# and set it as the current flock using your flock_service methods.
|
|
641
|
+
# For now, let's assume flock_service has a method like:
|
|
642
|
+
# set_current_flock_from_data(data) -> bool (returns True if successful)
|
|
643
|
+
|
|
644
|
+
# This is a placeholder for actual flock loading logic
|
|
645
|
+
# from flock.core.entities.flock import Flock # Assuming Flock can be instantiated from dict
|
|
646
|
+
# from flock.webapp.app.services.flock_service import set_current_flock_instance, set_current_flock_filename
|
|
647
|
+
|
|
648
|
+
# try:
|
|
649
|
+
# # Assuming flock_data is a dict that can initialize a Flock object
|
|
650
|
+
# # You might need a more robust way to deserialize, e.g., using Pydantic models
|
|
651
|
+
# loaded_flock = Flock(**flock_data) # This is a simplistic example
|
|
652
|
+
# set_current_flock_instance(loaded_flock)
|
|
653
|
+
# # If the flock has a name or identifier, you might set it as well
|
|
654
|
+
# # set_current_flock_filename(flock_data.get("name", "scoped_flock")) # Example
|
|
655
|
+
#
|
|
656
|
+
# # Redirect to the agent editor or properties page in scoped mode
|
|
657
|
+
# # The page_dashboard will handle ui_mode=scoped and redirect/set initial content appropriately
|
|
658
|
+
# return RedirectResponse(url="/?ui_mode=scoped", status_code=303)
|
|
659
|
+
# except Exception as e:
|
|
660
|
+
# # Log error e
|
|
661
|
+
# # Redirect to an error page or the standalone dashboard with an error message
|
|
662
|
+
# error_msg = f"Failed to load flock for scoped view: {e}"
|
|
663
|
+
# return RedirectResponse(url=f"/?error={urllib.parse.quote(error_msg)}&ui_mode=standalone", status_code=303)
|
|
664
|
+
|
|
665
|
+
# For now, since we don't have the flock loading logic here,
|
|
666
|
+
# we'll just redirect. The calling service (`src/flock/core/api`)
|
|
667
|
+
# will need to ensure the flock is loaded into the webapp's session/state
|
|
668
|
+
# *before* redirecting to this UI.
|
|
669
|
+
|
|
670
|
+
# A more direct way if `load_flock_from_data_service` exists and sets it globally for the session:
|
|
671
|
+
# success = load_flock_from_data_service(flock_data, "scoped_runtime_flock") # example filename
|
|
672
|
+
# if success:
|
|
673
|
+
# return RedirectResponse(url="/ui/editor/agents?ui_mode=scoped", status_code=303) # or properties
|
|
674
|
+
# else:
|
|
675
|
+
# return RedirectResponse(url="/?error=Failed+to+load+scoped+flock&ui_mode=standalone", status_code=303)
|
|
676
|
+
|
|
677
|
+
# Given the current structure, the simplest way for an external service to "preload" a flock
|
|
678
|
+
# is to use the existing `load_flock_from_file_service` if the flock can be temporarily saved,
|
|
679
|
+
# or by enhancing `flock_service` to allow setting a Flock instance directly.
|
|
680
|
+
# Let's assume the flock is already loaded into the session by the calling API for now.
|
|
681
|
+
# The calling API will be responsible for calling a service function within the webapp's context.
|
|
682
|
+
|
|
683
|
+
# This endpoint's primary job is now to redirect to the UI in the correct mode.
|
|
684
|
+
# The actual loading of the flock should happen *before* this redirect,
|
|
685
|
+
# by the API server calling a service function within the webapp's context.
|
|
686
|
+
|
|
687
|
+
# For demonstration, let's imagine the calling API has already used a service
|
|
688
|
+
# to set the flock. We just redirect.
|
|
689
|
+
if get_current_flock_instance():
|
|
690
|
+
return RedirectResponse(
|
|
691
|
+
url="/ui/editor/agents?ui_mode=scoped", status_code=303
|
|
692
|
+
)
|
|
693
|
+
else:
|
|
694
|
+
# If no flock is loaded, go to the main page in scoped mode, which will show the "no flock" message.
|
|
695
|
+
return RedirectResponse(url="/?ui_mode=scoped", status_code=303)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
# --- Action Routes ...
|
|
699
|
+
# The `load-flock-action/*` and `create-flock` POST routes should remain the same as they already
|
|
700
|
+
# correctly target `#main-content-area` and trigger `flockLoaded`.
|
|
701
|
+
# ... (rest of action routes: load-flock-action/by-name, by-upload, create-flock)
|
|
702
|
+
@app.post("/ui/load-flock-action/by-name", response_class=HTMLResponse)
|
|
703
|
+
async def ui_load_flock_by_name_action(
|
|
704
|
+
request: Request, selected_flock_filename: str = Form(...)
|
|
705
|
+
):
|
|
706
|
+
loaded_flock = load_flock_from_file_service(selected_flock_filename)
|
|
707
|
+
response_headers = {}
|
|
708
|
+
if loaded_flock:
|
|
709
|
+
success_message = f"Flock '{loaded_flock.name}' loaded from '{selected_flock_filename}'."
|
|
710
|
+
response_headers["HX-Push-Url"] = "/ui/editor/properties"
|
|
711
|
+
response_headers["HX-Trigger"] = json.dumps(
|
|
712
|
+
{
|
|
713
|
+
"flockLoaded": None,
|
|
714
|
+
"notify": {"type": "success", "message": success_message},
|
|
715
|
+
}
|
|
716
|
+
)
|
|
717
|
+
return templates.TemplateResponse(
|
|
718
|
+
"partials/_flock_properties_form.html",
|
|
719
|
+
{
|
|
720
|
+
"request": request,
|
|
721
|
+
"flock": loaded_flock,
|
|
722
|
+
"current_filename": get_current_flock_filename(),
|
|
723
|
+
},
|
|
724
|
+
headers=response_headers,
|
|
725
|
+
)
|
|
726
|
+
else:
|
|
727
|
+
error_message = (
|
|
728
|
+
f"Failed to load flock file '{selected_flock_filename}'."
|
|
729
|
+
)
|
|
730
|
+
response_headers["HX-Trigger"] = json.dumps(
|
|
731
|
+
{"notify": {"type": "error", "message": error_message}}
|
|
732
|
+
)
|
|
733
|
+
return templates.TemplateResponse(
|
|
734
|
+
"partials/_load_manager_view.html",
|
|
735
|
+
{"request": request, "error_message_inline": error_message},
|
|
736
|
+
headers=response_headers,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
@app.post("/ui/load-flock-action/by-upload", response_class=HTMLResponse)
|
|
741
|
+
async def ui_load_flock_by_upload_action(
|
|
742
|
+
request: Request, flock_file_upload: UploadFile = File(...)
|
|
743
|
+
):
|
|
744
|
+
error_message = None
|
|
745
|
+
filename_to_load = None
|
|
746
|
+
response_headers = {}
|
|
747
|
+
if flock_file_upload and flock_file_upload.filename:
|
|
748
|
+
if not flock_file_upload.filename.endswith((".yaml", ".yml", ".flock")):
|
|
749
|
+
error_message = "Invalid file type."
|
|
750
|
+
else:
|
|
751
|
+
upload_path = FLOCK_FILES_DIR / flock_file_upload.filename
|
|
752
|
+
try:
|
|
753
|
+
with upload_path.open("wb") as buffer:
|
|
754
|
+
shutil.copyfileobj(flock_file_upload.file, buffer)
|
|
755
|
+
filename_to_load = flock_file_upload.filename
|
|
756
|
+
except Exception as e:
|
|
757
|
+
error_message = f"Upload failed: {e}"
|
|
758
|
+
finally:
|
|
759
|
+
await flock_file_upload.close()
|
|
760
|
+
else:
|
|
761
|
+
error_message = "No file uploaded."
|
|
762
|
+
|
|
763
|
+
if filename_to_load and not error_message:
|
|
764
|
+
loaded_flock = load_flock_from_file_service(filename_to_load)
|
|
765
|
+
if loaded_flock:
|
|
766
|
+
success_message = (
|
|
767
|
+
f"Flock '{loaded_flock.name}' loaded from '{filename_to_load}'."
|
|
768
|
+
)
|
|
769
|
+
response_headers["HX-Push-Url"] = "/ui/editor/properties"
|
|
770
|
+
response_headers["HX-Trigger"] = json.dumps(
|
|
771
|
+
{
|
|
772
|
+
"flockLoaded": None,
|
|
773
|
+
"flockFileListChanged": None,
|
|
774
|
+
"notify": {"type": "success", "message": success_message},
|
|
775
|
+
}
|
|
776
|
+
)
|
|
777
|
+
return templates.TemplateResponse(
|
|
778
|
+
"partials/_flock_properties_form.html",
|
|
779
|
+
{
|
|
780
|
+
"request": request,
|
|
781
|
+
"flock": loaded_flock,
|
|
782
|
+
"current_filename": get_current_flock_filename(),
|
|
783
|
+
},
|
|
784
|
+
headers=response_headers,
|
|
785
|
+
)
|
|
786
|
+
else:
|
|
787
|
+
error_message = f"Failed to process uploaded '{filename_to_load}'."
|
|
788
|
+
|
|
789
|
+
response_headers["HX-Trigger"] = json.dumps(
|
|
790
|
+
{
|
|
791
|
+
"notify": {
|
|
792
|
+
"type": "error",
|
|
793
|
+
"message": error_message or "Upload failed.",
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
)
|
|
797
|
+
return templates.TemplateResponse(
|
|
798
|
+
"partials/_create_flock_form.html",
|
|
799
|
+
{ # Changed target to create form on upload error
|
|
800
|
+
"request": request,
|
|
801
|
+
"error_message": error_message or "Upload action failed.",
|
|
802
|
+
},
|
|
803
|
+
headers=response_headers,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
@app.post("/ui/create-flock", response_class=HTMLResponse)
|
|
808
|
+
async def ui_create_flock_action(
|
|
809
|
+
request: Request,
|
|
810
|
+
flock_name: str = Form(...),
|
|
811
|
+
default_model: str = Form(None),
|
|
812
|
+
description: str = Form(None),
|
|
813
|
+
):
|
|
814
|
+
if not flock_name.strip():
|
|
815
|
+
return templates.TemplateResponse(
|
|
816
|
+
"partials/_create_flock_form.html",
|
|
817
|
+
{
|
|
818
|
+
"request": request,
|
|
819
|
+
"error_message": "Flock name cannot be empty.",
|
|
820
|
+
},
|
|
821
|
+
)
|
|
822
|
+
new_flock = create_new_flock_service(flock_name, default_model, description)
|
|
823
|
+
success_msg = (
|
|
824
|
+
f"New flock '{new_flock.name}' created. Configure properties and save."
|
|
825
|
+
)
|
|
826
|
+
response_headers = {
|
|
827
|
+
"HX-Push-Url": "/ui/editor/properties",
|
|
828
|
+
"HX-Trigger": json.dumps(
|
|
829
|
+
{
|
|
830
|
+
"flockLoaded": None,
|
|
831
|
+
"notify": {"type": "success", "message": success_msg},
|
|
832
|
+
}
|
|
833
|
+
),
|
|
834
|
+
}
|
|
835
|
+
return templates.TemplateResponse(
|
|
836
|
+
"partials/_flock_properties_form.html",
|
|
837
|
+
{
|
|
838
|
+
"request": request,
|
|
839
|
+
"flock": new_flock,
|
|
840
|
+
"current_filename": get_current_flock_filename(),
|
|
841
|
+
},
|
|
842
|
+
headers=response_headers,
|
|
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
|
+
)
|