syntaxmatrix 2.5.5.5__py3-none-any.whl → 2.5.6.1__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.
syntaxmatrix/routes.py CHANGED
@@ -35,7 +35,9 @@ from syntaxmatrix.settings.string_navbar import string_navbar_items
35
35
  from syntaxmatrix.settings.model_map import GPT_MODELS_LATEST, PROVIDERS_MODELS, MODEL_DESCRIPTIONS, PURPOSE_TAGS, EMBEDDING_MODELS
36
36
  from syntaxmatrix.project_root import detect_project_root
37
37
  from syntaxmatrix import generate_page as _genpage
38
- from syntaxmatrix import auth as _auth
38
+ from syntaxmatrix import auth as _auth
39
+ from .auth import register_user, authenticate, login_required, admin_required, superadmin_required, update_password
40
+
39
41
  from syntaxmatrix import profiles as _prof
40
42
  from syntaxmatrix.gpt_models_latest import set_args, extract_output_text as _out
41
43
  from syntaxmatrix.agentic.agents import classify_ml_job_agent, refine_question_agent, text_formatter_agent
@@ -219,24 +221,30 @@ def setup_routes(smx):
219
221
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
220
222
  <title>{smx.page}</title>
221
223
  <style>
224
+ /* ----- HTML/BODY ----------------------------------- */
225
+ html {{
226
+ font-size: clamp(12px, 1.7vw, 18px);
227
+ /* scrollbar-gutter: stable both-edges; */
228
+ }}
222
229
  body {{
223
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
224
- margin: 0 20px;
225
230
  padding: 0;
231
+ margin: 0;
226
232
  background: {smx.theme["background"]};
227
233
  color: {smx.theme["text_color"]};
228
234
  }}
229
- /* Responsive typography using clamp */
230
- html {{
231
- font-size: clamp(12px, 1.7vw, 18px);
232
- }}
235
+ html, body {{ scroll-behavior: auto; }}
236
+ .admin-grid, .admin-shell .card {{ min-width: 0; }}
237
+ html, body, .admin-shell {{ overflow-x: visible !important; }}
238
+ </style>
239
+ <style>
240
+ /* ----- NAVBAR -------------------------------- */
233
241
  /* Desktop Navbar */
234
242
  nav {{
235
243
  display: flex;
236
244
  justify-content: space-between;
237
245
  align-items: center;
238
246
  background: {smx.theme["nav_background"]};
239
- padding: 10px 20px;
247
+ padding: 10px 24px;
240
248
  position: fixed;
241
249
  top: 0;
242
250
  left: 0;
@@ -246,19 +254,36 @@ def setup_routes(smx):
246
254
  .nav-left {{
247
255
  display: flex;
248
256
  align-items: center;
249
- }}
250
- .nav-left .logo {{
251
- font-size: clamp(1.3rem, 2vw, 1.5rem);
252
- font-weight: bold;
253
257
  color: {smx.theme["nav_text"]};
254
- margin-right: 20px;
258
+ gap: 8px;
255
259
  }}
256
- .nav-left .nav-links a {{
257
- font-size: clamp(1rem, 1.2vw, 1.2rem);
260
+ .nav-left .logo {{
261
+ align-items: center;
262
+ font-weight: bold;
263
+ font-size: clamp(1.4rem, 1.8vw, 1.8rem);
264
+ margin-right: 0;
265
+ }}
266
+ .logo img {{
267
+ display: block;
268
+ width: clamp(1.4rem, 1.8vw, 1.8rem);
269
+ }}
270
+ .nav-left a {{
258
271
  color: {smx.theme["nav_text"]};
259
272
  text-decoration: none;
260
273
  margin-right: 15px;
261
274
  }}
275
+
276
+ .nav-left .nav-links a.active,
277
+ .nav-left .nav-links a.active:hover,
278
+ #mobile-nav a.active,
279
+ #mobile-nav a.active:hover {{
280
+ background-color: var(--nav-bg) !important; /* keep the same base */
281
+ box-shadow: inset 0 0 0 9999px rgba(0,0,0,.52); /* darken ~52% */
282
+ border-radius: 6px;
283
+ padding: 2px 8px;
284
+ color:cyan;
285
+ }}
286
+
262
287
  .nav-right a {{
263
288
  font-size: clamp(1rem, 1.2vw, 1.2rem);
264
289
  color: {smx.theme["nav_text"]};
@@ -289,7 +314,7 @@ def setup_routes(smx):
289
314
  display: flex;
290
315
  flex-direction: column;
291
316
  gap: 10px;
292
- z-index: 900;
317
+ z-index: 1000;
293
318
  color: {mobile_text_color};
294
319
  }}
295
320
  #mobile-nav a {{
@@ -308,24 +333,29 @@ def setup_routes(smx):
308
333
  /* Responsive adjustments for mobile */
309
334
  @media (max-width: 768px) {{
310
335
  .nav-left .nav-links, .nav-right {{
311
- display: none;
336
+ display: none;
312
337
  }}
313
338
  #hamburger-btn {{
314
339
  display: block;
315
340
  }}
316
-
341
+ body {{
342
+ padding: 0 10px;
343
+ }}
317
344
  }}
318
- /* Sidebar styles */
345
+ </style>
346
+
347
+ <style>
348
+ /* ----- SIDEBAR ---------------------------------------------------------- */
319
349
  #sidebar {{
320
350
  position: fixed;
321
351
  top: 40px;
322
- left: -240px;
352
+ left: -260px;
323
353
  width: var(--sidebar-w);
324
- height: calc(100% - 10px);
354
+ height: calc(100% - 2px);
325
355
  background: {smx.theme["sidebar_background"]};
326
356
  overflow-y: auto;
327
357
  padding: 10px; 5px;
328
- font-size: 1.2rem;
358
+ font-size: clamp(1.2rem, 1.4vw, 1.6rem);
329
359
  gap: 10px;
330
360
  box-shadow: 2px 0 5px rgba(0,0,0,0.3);
331
361
  transition: left 0.3s ease;
@@ -334,7 +364,7 @@ def setup_routes(smx):
334
364
  }}
335
365
  #sidebar a {{
336
366
  color: {get_contrast_color(smx.theme["sidebar_background"])};
337
- margin:3px;
367
+ padding:3px;
338
368
  text-decoration: none;
339
369
  }}
340
370
  #sidebar.open {{
@@ -359,18 +389,28 @@ def setup_routes(smx):
359
389
  background-color: rgba(0, 0, 0, 0.05);
360
390
  transform: scale(1.2);
361
391
  }}
392
+ </style>
393
+ <style>
394
+ /* ----- CHAT HISTORY ---------------------------------------------------- */
362
395
  #chat-history {{
363
396
  width: 100%;
364
397
  max-width: 980px;
365
- margin: 50px auto 10px auto;
366
- padding: 10px 5px;
367
398
  background: {smx.theme["chat_background"]};
368
399
  border-radius: 20px;
369
400
  overflow-y: auto;
370
401
  min-height: 360px;
402
+ margin: 50px auto 10px auto;
403
+ padding: 10px 5px 0 5px;
404
+ padding-bottom: calc(var(--composer-h, 104px) + 78);
371
405
  }}
406
+ #chat-history .chat-message {{
407
+ scroll-margin-bottom: calc(var(--composer-h, 104px) + 78);
408
+ }}
409
+ #chat-history, #widget-container {{ overflow-anchor: none; }}
410
+
372
411
  #chat-history-default {{
373
412
  width: 100%;
413
+ max-width: 100%;
374
414
  margin: 45px auto 10px auto;
375
415
  padding: 10px 5px;
376
416
  background: {smx.theme["chat_background"]};
@@ -384,13 +424,13 @@ def setup_routes(smx):
384
424
  transform:scale(1.2);
385
425
  transition: all 0.3s ease;
386
426
  }}
427
+
428
+ { _chat_css() }
429
+
387
430
  #widget-container {{
388
- max-width: 850px;
431
+ max-width: 100%;
389
432
  margin: 0 auto 40px auto;
390
433
  }}
391
-
392
- { _chat_css() }
393
-
394
434
  .closeable-div {{
395
435
  position: relative;
396
436
  padding: 20px;
@@ -410,27 +450,20 @@ def setup_routes(smx):
410
450
  .close-btn:hover {{
411
451
  color: #ff0000;
412
452
  }}
413
- </style>
414
- <style>
453
+
415
454
  @keyframes spin {{
416
455
  0% {{ transform: rotate(0deg); }}
417
456
  100% {{ transform: rotate(360deg); }}
418
457
  }}
419
-
420
- </style>
421
- <style>
422
458
  .dropdown:hover .dropdown-content {{
423
459
  display: block;
424
460
  }}
425
- </style>
426
- <style>
427
461
  /* Keep the shift amount equal to the actual sidebar width */
428
462
  :root {{ --sidebar-w: 16vw; --nav-bg: {{smx.theme["nav_background"]}}; }}
429
463
 
430
464
  /* Messages slide; composer doesn't stay shifted */
431
- #chat-history,
432
- #widget-container {{ transition: transform .45s ease; }}
433
-
465
+ #chat-history, #widget-container {{ transition: transform .45s ease; }}
466
+
434
467
  /* Messages move fully clear of the sidebar */
435
468
  body.sidebar-open #chat-history {{ transform: translateX(calc(var(--sidebar-w) * 0.30)); }}
436
469
 
@@ -447,56 +480,23 @@ def setup_routes(smx):
447
480
  #widget-container, #smx-widgets {{
448
481
  position: sticky;
449
482
  bottom: 0;
450
- z-index: 1100; /* > sidebar (999) */
483
+ z-index: 1100;
451
484
  background: inherit;
452
485
  }}
453
- #chat-history{{
454
- padding-bottom: calc(var(--composer-h, 104px) + 78);
455
- }}
456
- #chat-history .chat-message {{
457
- scroll-margin-bottom: calc(var(--composer-h, 104px) + 78);
458
- }}
459
- /* Stop browser scroll-anchoring from fighting our autoscroll */
460
- #chat-history,
461
- #widget-container {{
462
- overflow-anchor: none;
463
- }}
464
-
465
- /* Reduce visual “jump” when the scrollbar appears/disappears */
466
- html {{
467
- scrollbar-gutter: stable both-edges;
468
- }}
469
-
470
- /* Avoid unexpected smooth scrolling that can look like a jerk */
471
- html, body {{
472
- scroll-behavior: auto;
473
- }}
474
-
486
+
475
487
  /* Textarea bounds */
476
488
  .chat-composer {{ min-width:0; max-height:12vh; }}
477
- @media (max-width:900px){{
489
+ @media (max-width:1200px){{
478
490
  .chat-composer {{
479
491
  min-height:56px;
480
492
  line-height:1.4;
481
493
  white-space: pre-wrap;
482
- padding: 10px 10px 16px 24px;
483
- font-size: 16px; /* prevents iOS zoom + improves legibility */
484
- overflow-y: auto; /* scroll after cap */
494
+ padding: 10px 10px 16px 12px;
495
+ font-size: 16px;
496
+ overflow-y: auto;
485
497
  box-sizing: border-box;
486
498
  }}
487
499
  }}
488
-
489
- .nav-left .nav-links a.active,
490
- .nav-left .nav-links a.active:hover,
491
- #mobile-nav a.active,
492
- #mobile-nav a.active:hover {{
493
- background-color: var(--nav-bg) !important; /* keep the same base */
494
- box-shadow: inset 0 0 0 9999px rgba(0,0,0,.52); /* darken ~52% */
495
- border-radius: 6px;
496
- padding: 2px 8px;
497
- color:cyan;
498
- }}
499
-
500
500
  </style>
501
501
 
502
502
  <!-- Add MathJax -->
@@ -513,7 +513,7 @@ def setup_routes(smx):
513
513
  }});
514
514
  </script>
515
515
  <script>
516
- /** Turn the latest bot <p> into fade-in “lines” and reveal them sequentially */
516
+ // Turn the latest bot <p> into fade-in “lines” and reveal them sequentially
517
517
  function splitToLines(node){{
518
518
  // If there are list items, animate them item-by-item.
519
519
  const lis = node.querySelectorAll('li');
@@ -629,10 +629,13 @@ def setup_routes(smx):
629
629
  '</form>'
630
630
  )
631
631
  else:
632
+ # Only show Register link if the consumer app explicitly enabled it.
633
+ reg_link = ""
634
+ if getattr(smx, "registration_enabled", False):
635
+ reg_link = f'|<a href="{url_for("register")}" class="nav-link">Register</a>'
632
636
  auth_links = (
633
637
  f'<a href="{url_for("login")}" class="nav-link">Login</a>'
634
- '|'
635
- f'<a href="{url_for("register")}" class="nav-link">Register</a>'
638
+ f'{reg_link}'
636
639
  )
637
640
 
638
641
  desktop_nav = f"""
@@ -708,7 +711,7 @@ def setup_routes(smx):
708
711
  .chat-message.user {{
709
712
  background: #e4e8ed;
710
713
  float: right;
711
- margin-right: 15px;
714
+ margin-right: 20px;
712
715
  border-top-right-radius: 2px;
713
716
  }}
714
717
  .chat-message.user::after {{
@@ -1501,33 +1504,7 @@ def setup_routes(smx):
1501
1504
  @smx.app.route("/", methods=["GET", "POST"])
1502
1505
  def home():
1503
1506
  smx.page = ""
1504
- # if not session.get("current_session"):
1505
- # session["current_session"] = {"id": str(uuid.uuid4()), "title": "Current", "history": []}
1506
- # session.setdefault("past_sessions", [])
1507
- # session.setdefault("chat_history", [])
1508
- # session["active_chat_id"] = session["current_session"]["id"]
1509
-
1510
- # if session.pop("needs_end_chat", False):
1511
- # current_history = session.get("chat_history", [])
1512
- # current_session = session.get("current_session", {"id": str(uuid.uuid4()), "title": "Current", "history": []})
1513
- # past_sessions = session.get("past_sessions", [])
1514
-
1515
- # generated_title = smx.generate_contextual_title(current_history)
1516
- # current_session["title"] = generated_title
1517
- # past_sessions.insert(0, {"id": current_session["id"], "title": current_session["title"]})
1518
-
1519
- # session["past_sessions"] = past_sessions
1520
- # session["current_session"] = {"id": str(uuid.uuid4()), "title": "Current", "history": []}
1521
- # session["active_chat_id"] = session["current_session"]["id"]
1522
- # session["chat_history"] = []
1523
-
1524
- # session["user_query"] = ""
1525
- # session["app_token"] = smx.app_token
1526
-
1527
- # cur = session.get("current_session")
1528
- # if cur and cur.get("id") and session.get("active_chat_id") != cur["id"]:
1529
- # session["active_chat_id"] = cur["id"]
1530
-
1507
+
1531
1508
  if not session.get("current_session"):
1532
1509
  # metadata only: id + title
1533
1510
  session["current_session"] = {"id": str(uuid.uuid4()), "title": "Current"}
@@ -2397,31 +2374,18 @@ def setup_routes(smx):
2397
2374
  home_page_html = f"""
2398
2375
  {head_html()}
2399
2376
  <meta name="viewport" content="width=device-width, initial-scale=1" />
2377
+
2400
2378
  <style>
2401
- /* Match /dashboard font scale */
2402
- :root{{
2403
- --smx-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
2404
- Arial, "Noto Sans", "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
2405
- --smx-font-size: 16px; /* >=16px prevents iOS zoom */
2406
- --smx-line: 1.55;
2407
- }}
2408
- html{{ -webkit-text-size-adjust: 100%; }}
2409
- body{{ font-family: var(--smx-font); font-size: var(--smx-font-size); line-height: var(--smx-line); }}
2410
- </style>
2411
- <style>
2412
- /* Container sizing & equal gutters */
2413
2379
  .chat-container{{
2414
2380
  max-width: 820px;
2415
2381
  margin-inline: auto;
2416
2382
  padding-inline: 12px;
2417
2383
  box-sizing: border-box;
2418
2384
  }}
2419
-
2420
- /* Bubbles never overflow the viewport width */
2421
2385
  .chat-messages{{
2422
2386
  overflow-wrap: anywhere;
2423
2387
  word-break: break-word;
2424
- padding-bottom: 84px; /* room for sticky footer input */
2388
+ padding-bottom: 84px;
2425
2389
  }}
2426
2390
 
2427
2391
  /* Sticky footer input area (safe on iOS address-bar) */
@@ -2466,8 +2430,7 @@ def setup_routes(smx):
2466
2430
  @supports (-webkit-touch-callout: none){{
2467
2431
  .chat-footer textarea{{ resize: none; }}
2468
2432
  }}
2469
- </style>
2470
- <style>
2433
+
2471
2434
  /* Desktop: push chat-history a little more than the base shift */
2472
2435
  body.sidebar-open #chat-history{{
2473
2436
  transform: translateX(calc(var(--sidebar-shift, var(--sidebar-w)) - 90px));
@@ -2499,10 +2462,9 @@ def setup_routes(smx):
2499
2462
  }}
2500
2463
  /* Typewriter look during streaming only */
2501
2464
  .chat-message.bot.streaming .stream-target{{
2502
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
2503
2465
  font-variant-ligatures: none;
2504
2466
  white-space: pre-wrap; /* keep line breaks as they stream */
2505
- letter-spacing: 0.01em; /* subtle spacing for “typed” feel */
2467
+ letter-spacing: 0.02em; /* subtle spacing for “typed” feel */
2506
2468
  }}
2507
2469
 
2508
2470
  /* Blinking caret visible only while streaming */
@@ -2525,8 +2487,7 @@ def setup_routes(smx):
2525
2487
  0%, 100% {{ opacity: 0; }}
2526
2488
  50% {{ opacity: 1; }}
2527
2489
  }}
2528
- </style>
2529
- <style id="smx-structured-style">
2490
+
2530
2491
  /* Container for structured bot content */
2531
2492
  .chat-message.bot .smx-structured {{
2532
2493
  margin-top: 4px;
@@ -2540,10 +2501,11 @@ def setup_routes(smx):
2540
2501
  margin: 8px 0 4px;
2541
2502
  font-weight: 700;
2542
2503
  }}
2543
- .chat-message.bot .smx-structured h1 {{ font-size: 1.15rem; }}
2544
- .chat-message.bot .smx-structured h2 {{ font-size: 1.06rem; }}
2545
- .chat-message.bot .smx-structured h3 {{ font-size: 1.0rem; }}
2546
-
2504
+
2505
+ .chat-message.bot .smx-structured h1 {{ font-size: 1.3rem; }}
2506
+ .chat-message.bot .smx-structured h2 {{ font-size: 1.2rem; }}
2507
+ .chat-message.bot .smx-structured h3 {{ font-size: 1.1rem; }}
2508
+
2547
2509
  /* Paragraphs */
2548
2510
  .chat-message.bot .smx-structured p {{
2549
2511
  margin: 6px 0;
@@ -2563,15 +2525,13 @@ def setup_routes(smx):
2563
2525
  padding: 8px 10px;
2564
2526
  border-radius: 8px;
2565
2527
  overflow: auto;
2566
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
2567
2528
  }}
2568
2529
 
2569
- /* While streaming we still use a live typing box */
2530
+ /* While streaming, still use a live typing box */
2570
2531
  .chat-message.bot.streaming .stream-target {{
2571
2532
  white-space: pre-wrap; /* so newlines render during typing */
2572
2533
  }}
2573
- </style>
2574
- <style>
2534
+
2575
2535
  /* --- Stop-in-a-ring spinner --- */
2576
2536
  #submit-button.stop {{
2577
2537
  display: inline-flex;
@@ -2611,8 +2571,7 @@ def setup_routes(smx):
2611
2571
  @keyframes smxSpin {{
2612
2572
  to {{ transform: rotate(360deg); }}
2613
2573
  }}
2614
- </style>
2615
- <style>
2574
+
2616
2575
  /* Force strict top→bottom stacking and align sides without floats */
2617
2576
  #chat-history{{
2618
2577
  display: flex;
@@ -2661,6 +2620,14 @@ def setup_routes(smx):
2661
2620
  body {{
2662
2621
  padding-bottom:0;
2663
2622
  }}
2623
+
2624
+ html, body {{
2625
+ margin: 0;
2626
+ padding: 0;
2627
+ width: 100%;
2628
+ height: 100%; /* If you want it to be full height too */
2629
+ overflow-x: hidden; /* Optional: Prevents horizontal scrollbar appearing if a slight overflow */
2630
+ }}
2664
2631
  </style>
2665
2632
 
2666
2633
  <body>
@@ -3396,17 +3363,11 @@ def setup_routes(smx):
3396
3363
  box-sizing: border-box;
3397
3364
  max-width: 100%;
3398
3365
  }
3399
- @media (max-width: 768px) {
3366
+ @media (max-width: 1200px) {
3400
3367
  body {
3401
3368
  padding-top: 0;
3402
3369
  }
3403
3370
  }
3404
- /* undo the mobile overflow clamp on large screens */
3405
- html, body, .admin-shell{ overflow-x: visible !important; }
3406
- /* guard against grid items forcing overflow */
3407
- .admin-grid, .admin-shell .card { min-width: 0; }
3408
- }
3409
-
3410
3371
  /* Section demarcation */
3411
3372
  .section{
3412
3373
  background: var(--section-bg);
@@ -3522,10 +3483,6 @@ def setup_routes(smx):
3522
3483
  /* force all grid items to stack */
3523
3484
  .span-3, .span-4, .span-6, .span-8, .span-12 { grid-column: span 12; }
3524
3485
  }
3525
-
3526
- /* Global overflow guards (safe, won’t hide useful content inside lists) */
3527
- html, body, .admin-shell { overflow-x: hidden; }
3528
-
3529
3486
  /* Prevent any inner block from insisting on a width that causes overflow */
3530
3487
  .admin-shell .card, .admin-grid { min-width: 0; }
3531
3488
 
@@ -3820,6 +3777,34 @@ def setup_routes(smx):
3820
3777
  )
3821
3778
  flash(f"Role '{name}' created.", "info") if ok else flash("Could not create role (reserved/exists/invalid).", "error")
3822
3779
 
3780
+ elif action == "create_user":
3781
+ viewer_role = (session.get("role") or "").lower()
3782
+ if viewer_role not in ("admin", "superadmin"):
3783
+ flash("You don't have permission to create users.", "error")
3784
+ else:
3785
+ email = (request.form.get("email") or "").strip()
3786
+ username = (request.form.get("username") or "").strip()
3787
+ temp_password = request.form.get("password") or ""
3788
+ role = (request.form.get("role") or "user").strip().lower()
3789
+
3790
+ if not email or not temp_password:
3791
+ flash("Email and password are required to create a user.", "error")
3792
+ elif role not in ("user", "employee"):
3793
+ flash("Invalid role for new user.", "error")
3794
+ else:
3795
+ ok = register_user(email, username, temp_password, role)
3796
+ if ok:
3797
+ # Force this new account to change password on first login
3798
+ _auth.set_must_reset_by_email(email, must_reset=True)
3799
+ flash(
3800
+ "User created. They must change the temporary password on first login.",
3801
+ "success",
3802
+ )
3803
+ else:
3804
+ flash("Could not create user (email or username already in use).", "error")
3805
+
3806
+ return redirect(url_for("admin_panel"))
3807
+
3823
3808
  elif action == "set_user_role":
3824
3809
  actor_role = (session.get("role") or "").lower()
3825
3810
  actor_id = session.get("user_id")
@@ -4382,14 +4367,45 @@ def setup_routes(smx):
4382
4367
  """
4383
4368
 
4384
4369
  employees_card = f"""
4385
- <div class="card span-12">
4386
- <h4>Employees</h4>
4387
- <ul class="catalog-list">
4388
- {''.join(emp_items) or "<li>No employees yet.</li>"}
4389
- </ul>
4390
- {add_form}
4391
- </div>
4370
+ <div class="card span-12">
4371
+ <h4>Employees</h4>
4372
+ <ul class="catalog-list">
4373
+ {''.join(emp_items) or "<li>No employees yet.</li>"}
4374
+ </ul>
4375
+ {add_form}
4376
+ </div>
4392
4377
  """
4378
+ # Admin-only: create users directly (useful when public registration is disabled)
4379
+ create_user_card = ""
4380
+ if viewer_role in ("admin", "superadmin"):
4381
+ create_user_card = """
4382
+ <div class="card span-4">
4383
+ <h4>Create User</h4>
4384
+ <form method="post" class="form-vertical">
4385
+ <input type="hidden" name="action" value="create_user">
4386
+ <label>Email</label>
4387
+ <input type="email" name="email" required>
4388
+
4389
+ <label>Username (optional)</label>
4390
+ <input type="text" name="username" placeholder="e.g. jsmith">
4391
+
4392
+ <label>Temporary password</label>
4393
+ <input type="password" name="password" required>
4394
+
4395
+ <label>Role</label>
4396
+ <select name="role">
4397
+ <option value="user">User</option>
4398
+ <option value="employee">Employee</option>
4399
+ </select>
4400
+
4401
+ <button type="submit" style="margin-top:.5rem;">Create User</button>
4402
+ </form>
4403
+ <p style="font-size:.75rem;opacity:.7;margin-top:.5rem;">
4404
+ Share the temporary password securely and ask the user to change it after first login.
4405
+ </p>
4406
+ </div>
4407
+ """
4408
+
4393
4409
  from datetime import datetime, timedelta
4394
4410
  # Audit (always its own row)
4395
4411
  audit_card = ""
@@ -4528,6 +4544,7 @@ def setup_routes(smx):
4528
4544
  <div class="admin-grid">
4529
4545
  {roles_card}
4530
4546
  {employees_card}
4547
+ {create_user_card}
4531
4548
  </div>
4532
4549
  </section>
4533
4550
  """
@@ -5214,7 +5231,6 @@ def setup_routes(smx):
5214
5231
  <title>Edit Page - {{ page_name }}</title>
5215
5232
  <style>
5216
5233
  body {
5217
- font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
5218
5234
  background: #f4f7f9;
5219
5235
  padding: 20px;
5220
5236
  }
@@ -5279,20 +5295,25 @@ def setup_routes(smx):
5279
5295
  # ----Register ---------------------------------------
5280
5296
  @smx.app.route("/register", methods=["GET", "POST"])
5281
5297
  def register():
5298
+
5299
+ # If the consumer app has not enabled registration, redirect to login.
5300
+ if not getattr(smx, "registration_enabled", False):
5301
+ return redirect(url_for("login"))
5302
+
5282
5303
  if request.method == "POST":
5283
- email = request.form["email"].strip()
5284
- username = request.form["username"].strip()
5285
- password = request.form["password"]
5286
- role = request.form.get("role", "user")
5287
- if not email or not password:
5288
- flash("email and password required.")
5289
- else:
5290
- success = register_user(email, username, password, role)
5291
- if success:
5292
- flash("Registration successful—please log in.")
5293
- return redirect(url_for("login"))
5294
- else:
5295
- flash("Email already taken.")
5304
+ email = request.form["email"].strip()
5305
+ username = request.form["username"].strip()
5306
+ password = request.form["password"]
5307
+ role = request.form.get("role", "user")
5308
+ if not email or not password:
5309
+ flash("email and password required.")
5310
+ else:
5311
+ success = register_user(email, username, password, role)
5312
+ if success:
5313
+ flash("Registration successful—please log in.")
5314
+ return redirect(url_for("login"))
5315
+ else:
5316
+ flash("Email already taken.")
5296
5317
  return render_template("register.html")
5297
5318
 
5298
5319
  # ----- Login --------------------------------------------
@@ -5309,8 +5330,15 @@ def setup_routes(smx):
5309
5330
  session["username"] = user["username"]
5310
5331
  session["role"] = user["role"]
5311
5332
 
5312
- # ensure the just-logged-in user’s “Current” chat is closed on next GET
5313
- # session["needs_end_chat"] = True
5333
+ # If this account was created with a temporary password,
5334
+ # force them through the change-password flow first.
5335
+ if _auth.user_must_reset_password(user["id"]):
5336
+ session["must_reset_password"] = True
5337
+ flash("Please set a new password before continuing.", "warning")
5338
+ return redirect(url_for("change_password"))
5339
+
5340
+ # Clear any stale flag for accounts that no longer need a reset
5341
+ session.pop("must_reset_password", None)
5314
5342
 
5315
5343
  # — Load past chats from chats.db for this user —
5316
5344
  chat_ids = SQLHistoryStore.list_chats(user["id"])
@@ -5341,7 +5369,40 @@ def setup_routes(smx):
5341
5369
  flash("Invalid username or password.")
5342
5370
  return render_template("login.html")
5343
5371
 
5344
- # ----- Logout -------------------------------------------
5372
+
5373
+ @smx.app.route("/change_password", methods=["GET", "POST"])
5374
+ @login_required
5375
+ def change_password():
5376
+ user_id = session.get("user_id")
5377
+ if not user_id:
5378
+ flash("Please log in again.", "error")
5379
+ return redirect(url_for("login"))
5380
+
5381
+ if request.method == "POST":
5382
+ current = (request.form.get("current_password") or "").strip()
5383
+ new1 = (request.form.get("new_password") or "").strip()
5384
+ new2 = (request.form.get("confirm_password") or "").strip()
5385
+
5386
+ if not new1:
5387
+ flash("New password is required.", "error")
5388
+ elif new1 != new2:
5389
+ flash("New passwords do not match.", "error")
5390
+ elif not _auth.verify_password(user_id, current):
5391
+ flash("Current password is incorrect.", "error")
5392
+ else:
5393
+ # Update password + clear the mandatory-reset flag
5394
+ _auth.update_password(user_id, new1)
5395
+ _auth.clear_must_reset(user_id)
5396
+ session.pop("must_reset_password", None)
5397
+ flash("Password updated successfully.", "success")
5398
+
5399
+ next_url = request.args.get("next") or url_for("dashboard")
5400
+ return redirect(next_url)
5401
+
5402
+ return render_template("change_password.html")
5403
+
5404
+
5405
+ # ----- Logout -------------------------------------------
5345
5406
  @smx.app.route("/logout", methods=["POST"])
5346
5407
  def logout():
5347
5408
  """Clear session and return to login."""
@@ -5649,7 +5710,6 @@ def setup_routes(smx):
5649
5710
  "<meta charset='utf-8'>"
5650
5711
  "<title>Result</title>"
5651
5712
  "<style>"
5652
- " body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 42px; padding:24 32; }"
5653
5713
  " img { max-width: 100%; height: auto; }"
5654
5714
  " table { border-collapse: collapse; margin: 16px 0; }"
5655
5715
  " th, td { border: 1px solid #ddd; padding: 6px 10px; }"
@@ -6453,6 +6513,7 @@ def setup_routes(smx):
6453
6513
  cell["highlighted_code"] = Markup(_pygmentize(cell["code"]))
6454
6514
 
6455
6515
  highlighted_ai_code = _pygmentize(ai_code)
6516
+ tasks = [tag.replace("_", " ").replace('"', '').capitalize() for tag in tags]
6456
6517
 
6457
6518
  return render_template(
6458
6519
  "dashboard.html",
@@ -6464,7 +6525,7 @@ def setup_routes(smx):
6464
6525
  highlighted_ai_code=highlighted_ai_code if ai_code else None,
6465
6526
  askai_question=smx.sanitize_rough_to_markdown_task(askai_question),
6466
6527
  refined_question=refined_question,
6467
- tasks=tags,
6528
+ tasks=tasks,
6468
6529
  data_cells=data_cells,
6469
6530
  session_id=session_id,
6470
6531
  llm_usage=llm_usage