andrewbejjani commited on
Commit
45049a5
·
1 Parent(s): 7583b3e

Split sessions to not have 1 session state for all users

Browse files

modified: README.md
modified: ui/static/app.js
modified: ui_app.py

Files changed (3) hide show
  1. README.md +1 -0
  2. ui/static/app.js +3 -2
  3. ui_app.py +105 -58
README.md CHANGED
@@ -12,6 +12,7 @@ Set production secrets in the Hugging Face Space settings, not in committed file
12
  - `GROQ_API_KEY`: required for Groq model calls.
13
  - `APP_PASSWORD`: required to password-protect the deployed Space; set it as a Hugging Face Secret.
14
  - `APP_USERNAME`: optional, defaults to `mastermap` when `APP_PASSWORD` is set.
 
15
  - `HF_TOKEN`: Hugging Face Space Secret only; optional, required only for the `Save Manual References` button.
16
 
17
  `Save Manual References` only enables on Hugging Face Spaces when `SPACE_ID` is present and `HF_TOKEN` is configured. It commits the current `refdata/manual_references.json` back to the Space repository.
 
12
  - `GROQ_API_KEY`: required for Groq model calls.
13
  - `APP_PASSWORD`: required to password-protect the deployed Space; set it as a Hugging Face Secret.
14
  - `APP_USERNAME`: optional, defaults to `mastermap` when `APP_PASSWORD` is set.
15
+ - `APP_SECRET_KEY`: recommended for stable, isolated per-browser sessions.
16
  - `HF_TOKEN`: Hugging Face Space Secret only; optional, required only for the `Save Manual References` button.
17
 
18
  `Save Manual References` only enables on Hugging Face Spaces when `SPACE_ID` is present and `HF_TOKEN` is configured. It commits the current `refdata/manual_references.json` back to the Space repository.
ui/static/app.js CHANGED
@@ -396,13 +396,14 @@
396
  output_sheet: outputSheet.value || defaultOutputSheet,
397
  models: models.value.trim()
398
  });
 
399
  const stream = new EventSource(`/run?${params.toString()}`);
400
  activeRunStream = stream;
401
  stream.onmessage = event => appendCleanLogChunk(JSON.parse(event.data));
402
  stream.addEventListener("done", async () => {
403
  stream.close();
404
  if (!stopRequested) {
405
- applyBlueprintPath = "data/Blueprint.xlsx";
406
  const targetSheet = outputSheet.value || defaultOutputSheet;
407
  await refreshApplySheets(targetSheet);
408
  applyButton.disabled = !(applyWorkbookPath && applyBlueprintPath && applySheetSelect.value);
@@ -410,7 +411,7 @@
410
  cleanResult.classList.add("active");
411
  cleanResult.innerHTML = `
412
  <strong>Blueprint generated</strong>
413
- <div class="status">Blueprint saved at data/Blueprint.xlsx</div>
414
  <a class="download-link" href="/download-blueprint">Download Blueprint</a>
415
  <a class="download-link" href="/download-cleaned-workbook">Download Cleaned Workbook</a>
416
  `;
 
396
  output_sheet: outputSheet.value || defaultOutputSheet,
397
  models: models.value.trim()
398
  });
399
+ const generatedBlueprintPath = `data/uploads/Blueprint_${activeRunJobId}.xlsx`;
400
  const stream = new EventSource(`/run?${params.toString()}`);
401
  activeRunStream = stream;
402
  stream.onmessage = event => appendCleanLogChunk(JSON.parse(event.data));
403
  stream.addEventListener("done", async () => {
404
  stream.close();
405
  if (!stopRequested) {
406
+ applyBlueprintPath = generatedBlueprintPath;
407
  const targetSheet = outputSheet.value || defaultOutputSheet;
408
  await refreshApplySheets(targetSheet);
409
  applyButton.disabled = !(applyWorkbookPath && applyBlueprintPath && applySheetSelect.value);
 
411
  cleanResult.classList.add("active");
412
  cleanResult.innerHTML = `
413
  <strong>Blueprint generated</strong>
414
+ <div class="status">Blueprint saved for this session.</div>
415
  <a class="download-link" href="/download-blueprint">Download Blueprint</a>
416
  <a class="download-link" href="/download-cleaned-workbook">Download Cleaned Workbook</a>
417
  `;
ui_app.py CHANGED
@@ -1,9 +1,10 @@
 
1
  import secrets
2
  import sys
3
  import uuid
4
  from pathlib import Path
5
 
6
- from flask import Flask, Response, jsonify, render_template, request, send_file
7
 
8
  from newest_model import PREFERRED_PRODUCTION_CHAT_MODELS, select_groq_chat_models
9
  from src.config import (
@@ -30,8 +31,9 @@ app = Flask(
30
  static_folder=str(APP_ROOT / "ui" / "static"),
31
  )
32
  app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024
 
33
 
34
- STATE = {
35
  "clean_path": "",
36
  "clean_filename": "",
37
  "clean_sheets": [],
@@ -47,6 +49,23 @@ STATE = {
47
  }
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  def auth_required_response() -> Response:
51
  return Response(
52
  "Authentication required",
@@ -101,14 +120,16 @@ def default_models() -> str:
101
 
102
 
103
  def render_page(message: str = "", error: str = ""):
104
- if STATE["clean_sheets"]:
105
- STATE["clean_selected_sheet"] = pick_sheet(STATE["clean_sheets"], STATE["clean_selected_sheet"])
106
- if STATE["apply_sheets"]:
107
- STATE["apply_selected_sheet"] = pick_sheet(STATE["apply_sheets"], STATE["apply_selected_sheet"])
 
 
108
 
109
  return render_template(
110
  "index.html",
111
- **STATE,
112
  default_output_sheet=DEFAULT_OUTPUT_SHEET_NAME,
113
  default_models=default_models(),
114
  can_apply=can_apply_blueprint(),
@@ -118,11 +139,12 @@ def render_page(message: str = "", error: str = ""):
118
 
119
 
120
  def can_apply_blueprint() -> bool:
 
121
  return bool(
122
- STATE["apply_workbook_path"]
123
- and STATE["apply_blueprint_path"]
124
- and STATE["apply_sheets"]
125
- and STATE["apply_selected_sheet"]
126
  )
127
 
128
 
@@ -131,32 +153,36 @@ def wants_json_response() -> bool:
131
 
132
 
133
  def ui_state_payload(message: str = "", error: str = ""):
 
134
  return {
135
  "message": message,
136
  "error": error,
137
- "apply_workbook_path": STATE["apply_workbook_path"],
138
- "apply_workbook_filename": STATE["apply_workbook_filename"],
139
- "apply_sheets": STATE["apply_sheets"],
140
- "apply_selected_sheet": STATE["apply_selected_sheet"],
141
- "apply_blueprint_path": STATE["apply_blueprint_path"],
142
- "apply_blueprint_filename": STATE["apply_blueprint_filename"],
143
  "can_apply": can_apply_blueprint(),
144
  }
145
 
146
 
147
- def pick_sheet(sheets, preferred_sheet=None):
 
148
  if preferred_sheet and preferred_sheet in sheets:
149
  return preferred_sheet
150
- if STATE["output_sheet"] in sheets:
151
- return STATE["output_sheet"]
152
  return sheets[0] if sheets else ""
153
 
154
 
155
  def update_ui_state_from_form(form):
156
- STATE["clean_selected_sheet"] = form.get("clean_selected_sheet") or STATE["clean_selected_sheet"]
157
- STATE["output_sheet"] = form.get("output_sheet") or STATE["output_sheet"] or DEFAULT_OUTPUT_SHEET_NAME
158
- STATE["models"] = form.get("models") or STATE["models"]
159
- STATE["apply_selected_sheet"] = form.get("apply_selected_sheet") or STATE["apply_selected_sheet"]
 
 
160
 
161
 
162
  @app.route("/")
@@ -168,35 +194,39 @@ def index():
168
  def prepare_clean():
169
  try:
170
  update_ui_state_from_form(request.form)
 
171
  filename, path = save_uploaded_excel(request.files.get("file"), UPLOAD_DIR)
172
  sheets = read_workbook_sheets(path)
173
  except Exception as exc:
174
  return render_page(error=str(exc))
175
 
176
- STATE["clean_path"] = str(path)
177
- STATE["clean_filename"] = filename
178
- STATE["clean_sheets"] = sheets
179
- STATE["clean_selected_sheet"] = pick_sheet(sheets, request.form.get("clean_selected_sheet"))
180
- STATE["apply_workbook_path"] = str(path)
181
- STATE["apply_workbook_filename"] = filename
182
- STATE["apply_sheets"] = sheets
183
- STATE["apply_selected_sheet"] = pick_sheet(sheets, request.form.get("apply_selected_sheet") or STATE["output_sheet"])
 
184
  return render_page(message=f"Loaded {filename}.")
185
 
186
 
187
  @app.route("/remove-clean", methods=["POST"])
188
  def remove_clean():
189
  update_ui_state_from_form(request.form)
190
- old_path = STATE["clean_path"]
191
- STATE["clean_path"] = ""
192
- STATE["clean_filename"] = ""
193
- STATE["clean_sheets"] = []
194
- STATE["clean_selected_sheet"] = ""
195
- if STATE["apply_workbook_path"] == old_path:
196
- STATE["apply_workbook_path"] = ""
197
- STATE["apply_workbook_filename"] = ""
198
- STATE["apply_sheets"] = []
199
- STATE["apply_selected_sheet"] = ""
 
 
200
  return render_page(message="File removed.")
201
 
202
 
@@ -204,6 +234,7 @@ def remove_clean():
204
  def prepare_apply_workbook():
205
  try:
206
  update_ui_state_from_form(request.form)
 
207
  filename, path = save_uploaded_excel(request.files.get("file"), UPLOAD_DIR)
208
  sheets = read_workbook_sheets(path)
209
  except Exception as exc:
@@ -211,10 +242,11 @@ def prepare_apply_workbook():
211
  return jsonify(ui_state_payload(error=str(exc))), 400
212
  return render_page(error=str(exc))
213
 
214
- STATE["apply_workbook_path"] = str(path)
215
- STATE["apply_workbook_filename"] = filename
216
- STATE["apply_sheets"] = sheets
217
- STATE["apply_selected_sheet"] = pick_sheet(sheets, request.form.get("apply_selected_sheet"))
 
218
  if wants_json_response():
219
  return jsonify(ui_state_payload(message=f"Loaded apply workbook {filename}."))
220
  return render_page(message=f"Loaded apply workbook {filename}.")
@@ -224,17 +256,19 @@ def prepare_apply_workbook():
224
  def prepare_apply_blueprint():
225
  try:
226
  update_ui_state_from_form(request.form)
227
- if STATE["apply_workbook_path"] and Path(STATE["apply_workbook_path"]).is_file():
228
- STATE["apply_sheets"] = read_workbook_sheets(Path(STATE["apply_workbook_path"]))
229
- STATE["apply_selected_sheet"] = pick_sheet(STATE["apply_sheets"], request.form.get("apply_selected_sheet"))
 
230
  filename, path = save_uploaded_excel(request.files.get("file"), UPLOAD_DIR)
231
  except Exception as exc:
232
  if wants_json_response():
233
  return jsonify(ui_state_payload(error=str(exc))), 400
234
  return render_page(error=str(exc))
235
 
236
- STATE["apply_blueprint_path"] = str(path)
237
- STATE["apply_blueprint_filename"] = filename
 
238
  if wants_json_response():
239
  return jsonify(ui_state_payload(message=f"Loaded blueprint {filename}."))
240
  return render_page(message=f"Loaded blueprint {filename}.")
@@ -276,7 +310,10 @@ def sheets_endpoint():
276
 
277
  @app.route("/download-blueprint")
278
  def download_blueprint():
279
- blueprint_path = APP_ROOT / DATA_DIR / "Blueprint.xlsx"
 
 
 
280
  if not blueprint_path.exists():
281
  return jsonify({"error": "Blueprint has not been generated yet."}), 404
282
  return send_file(blueprint_path, as_attachment=True, download_name="Blueprint.xlsx")
@@ -284,29 +321,31 @@ def download_blueprint():
284
 
285
  @app.route("/download-cleaned-workbook")
286
  def download_cleaned_workbook():
287
- if not STATE["clean_path"]:
 
288
  return jsonify({"error": "Cleaned workbook is not available."}), 404
289
- workbook_path = resolve_allowed_path(STATE["clean_path"], APP_ROOT, ALLOWED_FILE_ROOTS)
290
  if not workbook_path.is_file():
291
  return jsonify({"error": "Cleaned workbook is not available."}), 404
292
  return send_file(
293
  workbook_path,
294
  as_attachment=True,
295
- download_name=f"cleaned_{STATE['clean_filename'] or workbook_path.name}",
296
  )
297
 
298
 
299
  @app.route("/download-applied-workbook")
300
  def download_applied_workbook():
301
- if not STATE["apply_workbook_path"]:
 
302
  return jsonify({"error": "Applied workbook is not available."}), 404
303
- workbook_path = resolve_allowed_path(STATE["apply_workbook_path"], APP_ROOT, ALLOWED_FILE_ROOTS)
304
  if not workbook_path.is_file():
305
  return jsonify({"error": "Applied workbook is not available."}), 404
306
  return send_file(
307
  workbook_path,
308
  as_attachment=True,
309
- download_name=f"cleaned_{STATE['apply_workbook_filename'] or workbook_path.name}",
310
  )
311
 
312
 
@@ -326,6 +365,12 @@ def run():
326
  except ValueError as exc:
327
  return jsonify({"error": str(exc)}), 400
328
 
 
 
 
 
 
 
329
  command = [
330
  sys.executable,
331
  "-u",
@@ -336,6 +381,8 @@ def run():
336
  sheet,
337
  "--output_sheet",
338
  output_sheet,
 
 
339
  ]
340
  if model_list:
341
  command.extend(["--models", model_list])
 
1
+ import os
2
  import secrets
3
  import sys
4
  import uuid
5
  from pathlib import Path
6
 
7
+ from flask import Flask, Response, jsonify, render_template, request, send_file, session
8
 
9
  from newest_model import PREFERRED_PRODUCTION_CHAT_MODELS, select_groq_chat_models
10
  from src.config import (
 
31
  static_folder=str(APP_ROOT / "ui" / "static"),
32
  )
33
  app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024
34
+ app.secret_key = os.getenv("APP_SECRET_KEY", "mastermap-local-dev-secret")
35
 
36
+ DEFAULT_STATE = {
37
  "clean_path": "",
38
  "clean_filename": "",
39
  "clean_sheets": [],
 
49
  }
50
 
51
 
52
+ def fresh_state():
53
+ return {
54
+ key: list(value) if isinstance(value, list) else value
55
+ for key, value in DEFAULT_STATE.items()
56
+ }
57
+
58
+
59
+ def get_state():
60
+ if "ui_state" not in session:
61
+ session["ui_state"] = fresh_state()
62
+ return session["ui_state"]
63
+
64
+
65
+ def mark_state_changed():
66
+ session.modified = True
67
+
68
+
69
  def auth_required_response() -> Response:
70
  return Response(
71
  "Authentication required",
 
120
 
121
 
122
  def render_page(message: str = "", error: str = ""):
123
+ state = get_state()
124
+ if state["clean_sheets"]:
125
+ state["clean_selected_sheet"] = pick_sheet(state["clean_sheets"], state["clean_selected_sheet"], state)
126
+ if state["apply_sheets"]:
127
+ state["apply_selected_sheet"] = pick_sheet(state["apply_sheets"], state["apply_selected_sheet"], state)
128
+ mark_state_changed()
129
 
130
  return render_template(
131
  "index.html",
132
+ **state,
133
  default_output_sheet=DEFAULT_OUTPUT_SHEET_NAME,
134
  default_models=default_models(),
135
  can_apply=can_apply_blueprint(),
 
139
 
140
 
141
  def can_apply_blueprint() -> bool:
142
+ state = get_state()
143
  return bool(
144
+ state["apply_workbook_path"]
145
+ and state["apply_blueprint_path"]
146
+ and state["apply_sheets"]
147
+ and state["apply_selected_sheet"]
148
  )
149
 
150
 
 
153
 
154
 
155
  def ui_state_payload(message: str = "", error: str = ""):
156
+ state = get_state()
157
  return {
158
  "message": message,
159
  "error": error,
160
+ "apply_workbook_path": state["apply_workbook_path"],
161
+ "apply_workbook_filename": state["apply_workbook_filename"],
162
+ "apply_sheets": state["apply_sheets"],
163
+ "apply_selected_sheet": state["apply_selected_sheet"],
164
+ "apply_blueprint_path": state["apply_blueprint_path"],
165
+ "apply_blueprint_filename": state["apply_blueprint_filename"],
166
  "can_apply": can_apply_blueprint(),
167
  }
168
 
169
 
170
+ def pick_sheet(sheets, preferred_sheet=None, state=None):
171
+ state = state or get_state()
172
  if preferred_sheet and preferred_sheet in sheets:
173
  return preferred_sheet
174
+ if state["output_sheet"] in sheets:
175
+ return state["output_sheet"]
176
  return sheets[0] if sheets else ""
177
 
178
 
179
  def update_ui_state_from_form(form):
180
+ state = get_state()
181
+ state["clean_selected_sheet"] = form.get("clean_selected_sheet") or state["clean_selected_sheet"]
182
+ state["output_sheet"] = form.get("output_sheet") or state["output_sheet"] or DEFAULT_OUTPUT_SHEET_NAME
183
+ state["models"] = form.get("models") or state["models"]
184
+ state["apply_selected_sheet"] = form.get("apply_selected_sheet") or state["apply_selected_sheet"]
185
+ mark_state_changed()
186
 
187
 
188
  @app.route("/")
 
194
  def prepare_clean():
195
  try:
196
  update_ui_state_from_form(request.form)
197
+ state = get_state()
198
  filename, path = save_uploaded_excel(request.files.get("file"), UPLOAD_DIR)
199
  sheets = read_workbook_sheets(path)
200
  except Exception as exc:
201
  return render_page(error=str(exc))
202
 
203
+ state["clean_path"] = str(path)
204
+ state["clean_filename"] = filename
205
+ state["clean_sheets"] = sheets
206
+ state["clean_selected_sheet"] = pick_sheet(sheets, request.form.get("clean_selected_sheet"), state)
207
+ state["apply_workbook_path"] = str(path)
208
+ state["apply_workbook_filename"] = filename
209
+ state["apply_sheets"] = sheets
210
+ state["apply_selected_sheet"] = pick_sheet(sheets, request.form.get("apply_selected_sheet") or state["output_sheet"], state)
211
+ mark_state_changed()
212
  return render_page(message=f"Loaded {filename}.")
213
 
214
 
215
  @app.route("/remove-clean", methods=["POST"])
216
  def remove_clean():
217
  update_ui_state_from_form(request.form)
218
+ state = get_state()
219
+ old_path = state["clean_path"]
220
+ state["clean_path"] = ""
221
+ state["clean_filename"] = ""
222
+ state["clean_sheets"] = []
223
+ state["clean_selected_sheet"] = ""
224
+ if state["apply_workbook_path"] == old_path:
225
+ state["apply_workbook_path"] = ""
226
+ state["apply_workbook_filename"] = ""
227
+ state["apply_sheets"] = []
228
+ state["apply_selected_sheet"] = ""
229
+ mark_state_changed()
230
  return render_page(message="File removed.")
231
 
232
 
 
234
  def prepare_apply_workbook():
235
  try:
236
  update_ui_state_from_form(request.form)
237
+ state = get_state()
238
  filename, path = save_uploaded_excel(request.files.get("file"), UPLOAD_DIR)
239
  sheets = read_workbook_sheets(path)
240
  except Exception as exc:
 
242
  return jsonify(ui_state_payload(error=str(exc))), 400
243
  return render_page(error=str(exc))
244
 
245
+ state["apply_workbook_path"] = str(path)
246
+ state["apply_workbook_filename"] = filename
247
+ state["apply_sheets"] = sheets
248
+ state["apply_selected_sheet"] = pick_sheet(sheets, request.form.get("apply_selected_sheet"), state)
249
+ mark_state_changed()
250
  if wants_json_response():
251
  return jsonify(ui_state_payload(message=f"Loaded apply workbook {filename}."))
252
  return render_page(message=f"Loaded apply workbook {filename}.")
 
256
  def prepare_apply_blueprint():
257
  try:
258
  update_ui_state_from_form(request.form)
259
+ state = get_state()
260
+ if state["apply_workbook_path"] and Path(state["apply_workbook_path"]).is_file():
261
+ state["apply_sheets"] = read_workbook_sheets(Path(state["apply_workbook_path"]))
262
+ state["apply_selected_sheet"] = pick_sheet(state["apply_sheets"], request.form.get("apply_selected_sheet"), state)
263
  filename, path = save_uploaded_excel(request.files.get("file"), UPLOAD_DIR)
264
  except Exception as exc:
265
  if wants_json_response():
266
  return jsonify(ui_state_payload(error=str(exc))), 400
267
  return render_page(error=str(exc))
268
 
269
+ state["apply_blueprint_path"] = str(path)
270
+ state["apply_blueprint_filename"] = filename
271
+ mark_state_changed()
272
  if wants_json_response():
273
  return jsonify(ui_state_payload(message=f"Loaded blueprint {filename}."))
274
  return render_page(message=f"Loaded blueprint {filename}.")
 
310
 
311
  @app.route("/download-blueprint")
312
  def download_blueprint():
313
+ state = get_state()
314
+ if not state["apply_blueprint_path"]:
315
+ return jsonify({"error": "Blueprint has not been generated yet."}), 404
316
+ blueprint_path = resolve_allowed_path(state["apply_blueprint_path"], APP_ROOT, ALLOWED_FILE_ROOTS)
317
  if not blueprint_path.exists():
318
  return jsonify({"error": "Blueprint has not been generated yet."}), 404
319
  return send_file(blueprint_path, as_attachment=True, download_name="Blueprint.xlsx")
 
321
 
322
  @app.route("/download-cleaned-workbook")
323
  def download_cleaned_workbook():
324
+ state = get_state()
325
+ if not state["clean_path"]:
326
  return jsonify({"error": "Cleaned workbook is not available."}), 404
327
+ workbook_path = resolve_allowed_path(state["clean_path"], APP_ROOT, ALLOWED_FILE_ROOTS)
328
  if not workbook_path.is_file():
329
  return jsonify({"error": "Cleaned workbook is not available."}), 404
330
  return send_file(
331
  workbook_path,
332
  as_attachment=True,
333
+ download_name=f"cleaned_{state['clean_filename'] or workbook_path.name}",
334
  )
335
 
336
 
337
  @app.route("/download-applied-workbook")
338
  def download_applied_workbook():
339
+ state = get_state()
340
+ if not state["apply_workbook_path"]:
341
  return jsonify({"error": "Applied workbook is not available."}), 404
342
+ workbook_path = resolve_allowed_path(state["apply_workbook_path"], APP_ROOT, ALLOWED_FILE_ROOTS)
343
  if not workbook_path.is_file():
344
  return jsonify({"error": "Applied workbook is not available."}), 404
345
  return send_file(
346
  workbook_path,
347
  as_attachment=True,
348
+ download_name=f"cleaned_{state['apply_workbook_filename'] or workbook_path.name}",
349
  )
350
 
351
 
 
365
  except ValueError as exc:
366
  return jsonify({"error": str(exc)}), 400
367
 
368
+ blueprint_path = UPLOAD_DIR / f"Blueprint_{job_id}.xlsx"
369
+ state = get_state()
370
+ state["apply_blueprint_path"] = str(blueprint_path)
371
+ state["apply_blueprint_filename"] = blueprint_path.name
372
+ mark_state_changed()
373
+
374
  command = [
375
  sys.executable,
376
  "-u",
 
381
  sheet,
382
  "--output_sheet",
383
  output_sheet,
384
+ "--blueprint",
385
+ str(blueprint_path),
386
  ]
387
  if model_list:
388
  command.extend(["--models", model_list])