@javierfrancoleal/dsh-supervisor 1.0.39 → 1.0.40

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@javierfrancoleal/dsh-supervisor",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "The Veilleux central control plane of the J.Franco agentic workflow suite: user accounts, the live conversation flow (listen → per-person summary → confirm → orders), telemetry, and the universal sector dashboard with 10 languages. Created by J.Franco.",
5
5
  "author": "J.Franco",
6
6
  "license": "MIT",
@@ -17,10 +17,7 @@
17
17
  "icons",
18
18
  "sector-packs",
19
19
  "bin",
20
- "README.md",
21
- "dashboard.css",
22
- "dashboard.js",
23
- "i18n.js"
20
+ "README.md"
24
21
  ],
25
22
  "engines": {
26
23
  "node": ">=24"
package/server.js CHANGED
@@ -44,7 +44,7 @@ const LIVE_FILE = path.join(DATA_DIR, "live.json");
44
44
  const SECTOR_FILE = path.join(DATA_DIR, "sector.json");
45
45
  const REG_FILE = path.join(DATA_DIR, "register.json");
46
46
  const PRICE_PER_MILLION = Number(process.env.DSH_PRICE_PER_MILLION || 0.28);
47
- const VERSION = "1.0.38";
47
+ const VERSION = "1.0.39";
48
48
  // Homeostasis: the daily token budget (metabolic limit). The brain rests
49
49
  // when the budget is spent — no runaway costs.
50
50
  const DAILY_BUDGET_USD = Number(process.env.DSH_DAILY_BUDGET_USD || 0); // 0 = unlimited: consumption is the business
@@ -519,7 +519,6 @@ const MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; cha
519
519
  function serveStatic(req, res, pathname) {
520
520
  let p;
521
521
  if (pathname === "/" || pathname === "/dashboard" || pathname === "/dashboard.html") p = path.join(__dirname, "dashboard.html");
522
- else if (pathname === "/dashboard.css" || pathname === "/dashboard.js" || pathname === "/i18n.js") p = path.join(__dirname, pathname.replace(/^\//, ""));
523
522
  else if (pathname === "/panel" || pathname === "/panel.html") p = path.join(UI_DIR, "panel.html");
524
523
  else { const rel = pathname.replace(/^\/+/, ""); p = path.join(UI_DIR, rel); if (!p.startsWith(UI_DIR)) { res.writeHead(403); return res.end(); } }
525
524
  fs.readFile(p, (err, data) => {
package/dashboard.css DELETED
@@ -1,171 +0,0 @@
1
- /* Le Veilleux dashboard — stage 1 of the structural refactor.
2
- Extracted from dashboard.html. One design language per EL FUEGO. */
3
- :root{--blue:#4D6BFE;--violet:#7c5cff;--green:#22c55e;--amber:#facc15;--red:#ef4444;--ink:#101828;--muted:#667085;--bg:#eef2ff;--card:rgba(255,255,255,.78);--card2:rgba(255,255,255,.62);--line:rgba(15,23,42,.10);--line2:rgba(15,23,42,.06)}
4
- body.dark{background:linear-gradient(165deg,#0b1220 0%,#141f31 50%,#1e1b4b 100%);background-attachment:fixed;--ink:#e6ecf7;--muted:#94a3b8;--bg:#0b1220;--card:rgba(20,31,49,.8);--card2:rgba(24,37,58,.62);--line:rgba(148,163,184,.16);--line2:rgba(148,163,184,.10)}
5
- .anim{animation:slidein .28s ease}
6
- @keyframes slidein{from{opacity:0;transform:translateX(26px)}to{opacity:1;transform:none}}
7
- .empty{text-align:center;padding:26px 12px;color:var(--muted);font-size:14px;border:1px dashed var(--line);border-radius:14px}
8
- .empty .ico{font-size:30px;display:block;margin-bottom:6px}
9
- .skel{background:linear-gradient(90deg,var(--line) 25%,var(--line2) 50%,var(--line) 75%);background-size:200% 100%;animation:shimmer 1.2s infinite;border-radius:10px;min-height:16px}
10
- @keyframes shimmer{to{background-position:-200% 0}}
11
- .toast{position:fixed;bottom:90px;left:50%;transform:translateX(-50%);background:#111827;color:#fff;padding:10px 18px;border-radius:999px;font-size:14px;font-weight:600;z-index:60;opacity:0;transition:opacity .25s;pointer-events:none;max-width:86%;text-align:center}
12
- .toast.show{opacity:1}
13
- body.dark .toast{background:#e6ecf7;color:#0b1220}
14
- .bnav{position:fixed;bottom:0;left:0;right:0;background:var(--card);border-top:1px solid var(--line);display:none;justify-content:space-around;padding:6px 0 8px;z-index:50}
15
- .bnav button{background:none;border:none;color:var(--muted);font-size:11px;display:flex;flex-direction:column;align-items:center;gap:3px;padding:6px 12px;border-radius:12px;min-height:44px;font-family:inherit}
16
- .bnav button.active{color:var(--blue);font-weight:700}
17
- .checkpop{animation:pop .3s ease}
18
- @keyframes pop{0%{transform:scale(.6);opacity:0}70%{transform:scale(1.15)}100%{transform:scale(1);opacity:1}}
19
- @keyframes flick{0%,100%{opacity:1}42%{opacity:.82}50%{opacity:1}58%{opacity:.9}}
20
- @keyframes drawStroke{from{stroke-dashoffset:100}to{stroke-dashoffset:0}}
21
- /* Visible by default; the self-draw runs only when motion is allowed */
22
- .mark svg path{stroke-dasharray:100}
23
- @media (prefers-reduced-motion: no-preference){.mark svg path{stroke-dashoffset:100;animation:drawStroke 1.6s cubic-bezier(.4,0,.2,1) forwards}}
24
-
25
- .person{animation:rise .3s ease backwards}
26
- .person:nth-child(1){animation-delay:.03s}
27
- .person:nth-child(2){animation-delay:.09s}
28
- .person:nth-child(3){animation-delay:.15s}
29
- .person:nth-child(4){animation-delay:.21s}
30
- @keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
31
- @media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
32
- body.loginpage{background:linear-gradient(135deg,#4D6BFE 0%,#7c5cff 55%,#a78bfa 100%);min-height:100vh}
33
- body.loginpage.dark{background:linear-gradient(135deg,#0b1220 0%,#1e1b4b 100%)}
34
- body.loginpage header{background:transparent;box-shadow:none}
35
- #loginView{backdrop-filter:blur(14px);background:rgba(255,255,255,.93);border:1px solid rgba(255,255,255,.55);box-shadow:0 24px 60px rgba(15,23,42,.28);max-width:420px;margin:6vh auto 0;padding:26px 22px;animation:rise .5s ease backwards;animation-delay:.12s}
36
- body.dark #loginView{background:rgba(20,31,49,.93)}
37
- .loginbrand{text-align:center;margin-bottom:16px;animation:rise .5s ease backwards;animation-delay:.02s}
38
- .loginbrand .mark{width:64px;height:64px;border-radius:18px;background:linear-gradient(135deg,#4D6BFE,#7c5cff);display:inline-flex;align-items:center;justify-content:center;font-size:30px;box-shadow:0 10px 24px rgba(77,107,254,.45)}
39
- .loginbrand h2{margin:10px 0 2px;font-size:20px;font-weight:800}
40
- .loginbrand p{color:var(--muted);font-size:13px;margin:0}
41
- .spin{display:inline-block;width:15px;height:15px;border:2px solid rgba(255,255,255,.4);border-top-color:#fff;border-radius:50%;animation:rot .7s linear infinite;vertical-align:-3px;margin-right:8px}
42
- @keyframes rot{to{transform:rotate(360deg)}}
43
- .shake{animation:shake .4s ease}
44
- @keyframes shake{0%,100%{transform:translateX(0)}20%{transform:translateX(-6px)}40%{transform:translateX(6px)}60%{transform:translateX(-4px)}80%{transform:translateX(4px)}}
45
- input:focus,select:focus,textarea:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 3px rgba(77,107,254,.18)}
46
- @media(max-width:560px){.bnav{display:flex}.tabs{display:none}body{padding-bottom:70px}button,.btn,.pill,.confirmBtn{min-height:44px}}
47
- *{box-sizing:border-box}
48
- body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:linear-gradient(165deg,#e8edff 0%,#f6f7fb 45%,#fdf1f8 100%);background-attachment:fixed;color:var(--ink);-webkit-tap-highlight-color:transparent}
49
- header{background:linear-gradient(135deg,#4D6BFE,#7c5cff);box-shadow:0 4px 18px rgba(77,107,254,.28);color:#fff;padding:12px 16px;display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap}
50
- header h1{margin:0;font-size:18px}
51
- header .who{font-size:13px;opacity:.9}
52
- button{font-family:inherit;cursor:pointer}
53
- .btn{border:none;border-radius:12px;padding:12px 16px;font-size:15px;font-weight:600;color:#fff;background:linear-gradient(135deg,#4D6BFE,#7c5cff);box-shadow:0 4px 12px rgba(77,107,254,.22)}
54
- .btn.gray{background:var(--line2);color:var(--ink)}
55
- .btn.small{padding:8px 12px;font-size:13px}
56
- main{max-width:960px;margin:0 auto;padding:14px}
57
- .card{background:var(--card);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--line);border-radius:20px;box-shadow:0 8px 24px rgba(15,23,42,.06);padding:16px;margin:12px 0}
58
- .card h2{margin:0 0 10px;font-size:16px}
59
- input,select,textarea{width:100%;border:1px solid var(--line);border-radius:12px;padding:12px;font-size:16px;margin-bottom:8px;font-family:inherit;background:var(--card)}
60
- textarea{resize:vertical;min-height:80px}
61
- pre{background:#0f172a;color:#d7e3f4;border-radius:12px;padding:12px;font-size:12.5px;overflow:auto;max-height:320px;white-space:pre-wrap;margin:0}
62
- .metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px}
63
- .metric{background:var(--card);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--line);border-radius:16px;box-shadow:0 4px 14px rgba(15,23,42,.05);padding:12px;text-align:center}
64
- .metric .v{font-size:26px;font-weight:700;color:var(--blue)}
65
- .metric .l{font-size:12px;color:var(--muted);margin-top:2px}
66
- .tabs{display:flex;gap:8px;flex-wrap:wrap;margin:12px 0}
67
- .tabs button{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:10px 16px;font-size:15px;font-weight:600}
68
- .tabs button.active{background:var(--blue);color:#fff;border-color:var(--blue)}
69
- table{width:100%;border-collapse:collapse;font-size:14px}
70
- td,th{border-bottom:1px solid var(--line);padding:9px 6px;text-align:left}
71
- #splash{position:fixed;inset:0;background:linear-gradient(135deg,#4D6BFE,#7c5cff);display:flex;align-items:center;justify-content:center;z-index:100;transition:opacity .5s,visibility .5s}
72
- #splash.done{opacity:0;visibility:hidden}
73
- .splash-inner{text-align:center;color:#fff;padding:20px}
74
- .splash-inner b{display:block;font-size:22px;margin-top:14px;letter-spacing:.02em}
75
- .splash-inner span{font-size:12.5px;opacity:.85}
76
- @media (prefers-reduced-motion: no-preference){.splash-inner svg path{stroke-dasharray:100;stroke-dashoffset:100;animation:drawStroke 1.2s cubic-bezier(.4,0,.2,1) forwards}}
77
- @media (prefers-reduced-motion: reduce){#splash{display:none}}
78
- .hidden{display:none}
79
- .kban{background:#fef3c7;border:1px solid #f59e0b;color:#92400e;border-radius:12px;padding:12px 14px;margin:10px 0;font-weight:700;font-size:15px}
80
- .chip{background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.35);border-radius:999px;padding:5px 12px;font-size:13px;font-weight:600}
81
- .foot{text-align:center;padding:6px 16px 16px}
82
- .foot .copyr{font-size:12px;color:var(--muted)}
83
- .eye{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;font-size:18px;cursor:pointer;padding:4px}
84
- .passwrap{position:relative}
85
- .loginerr{background:#fee2e2;color:#b91c1c;border-radius:10px;padding:10px 12px;margin:8px 0;font-size:14px;display:block}
86
- .loginok{background:#dcfce7;color:#15803d;border-radius:10px;padding:10px 12px;margin:8px 0;font-size:14px;display:block}
87
- .note{color:var(--muted);font-size:13px}
88
- .hint{font-size:12.5px;color:var(--muted);margin-top:4px}
89
- .listen{display:block;width:100%;border:none;border-radius:24px;padding:34px 16px;font-size:28px;font-weight:800;color:#fff;background:var(--green);box-shadow:0 6px 20px rgba(34,197,94,.4);margin:14px 0}
90
- .listen:active{transform:scale(.98)}
91
- .listen.pulse{animation:pulse 1.6s infinite}
92
- @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(34,197,94,.55)}70%{box-shadow:0 0 0 26px rgba(34,197,94,0)}100%{box-shadow:0 0 0 0 rgba(34,197,94,0)}}
93
- .stopbtn{background:var(--red)!important;box-shadow:0 6px 20px rgba(239,68,68,.35)!important}
94
- .row{display:flex;gap:8px;flex-wrap:wrap}
95
- .row .btn{flex:1;min-width:110px}
96
- .switchuser{margin-top:26px;text-align:center}
97
- .langs{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:8px 0 14px}
98
- .langov{position:fixed;inset:0;background:linear-gradient(160deg,#4D6BFE,#7c5cff 60%,#a78bfa);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:110;animation:rise .3s ease}
99
- .langov .top{position:absolute;top:22px;right:26px}
100
- .langov .close{background:rgba(255,255,255,.15);border:1px solid rgba(255,255,255,.3);color:#fff;border-radius:999px;width:44px;height:44px;font-size:18px;cursor:pointer}
101
- .orow{width:min(520px,92vw);display:flex;justify-content:space-between;align-items:center;padding:15px 22px;border-radius:14px;font-size:19px;color:#fff;cursor:pointer;transition:background .15s,transform .1s}
102
- .orow:hover{background:rgba(255,255,255,.12)}
103
- .orow:active{transform:scale(.98)}
104
- .orow .sub{font-size:13px;opacity:.7}
105
- .orow.center{background:rgba(255,255,255,.30);font-size:23px;font-weight:800;border:1px solid rgba(255,255,255,.45)}
106
- .orow.center .sub{opacity:.9}
107
- #langStep{position:fixed;inset:0;background:linear-gradient(160deg,#4D6BFE,#7c5cff 60%,#a78bfa);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:95;transition:opacity .45s,visibility .45s;animation:stepin .55s ease}
108
- @keyframes stepin{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
109
- #langStep.done{opacity:0;visibility:hidden}
110
- .langstep-inner{text-align:center}
111
- .langstep-hint{color:rgba(255,255,255,.9);font-size:14px;margin:18px 0 4px}
112
- .langstep-inner .langreel{width:min(340px,86vw);background:rgba(167,139,250,.55)!important;border:1px solid rgba(255,255,255,.6)!important;backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px)}
113
- .langstep-inner svg{width:150px;height:150px}
114
- .langstep-inner svg path.draw{stroke-dasharray:100;animation:drawStroke 1.2s cubic-bezier(.4,0,.2,1) forwards}
115
- @media (prefers-reduced-motion: reduce){.langstep-inner svg path.draw{animation:none}}
116
- #loginView{background:transparent;backdrop-filter:none;-webkit-backdrop-filter:none;border:none;box-shadow:none;text-align:center}
117
- #loginView h2,#loginView .loginbrand h2{color:#fff}
118
- #loginView .loginbrand p,#loginView .hint,#loginView .note{color:rgba(255,255,255,.82)}
119
- #loginView input,#loginView select{background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.35);color:#fff}
120
- #loginView input::placeholder{color:rgba(255,255,255,.7)}
121
- #loginView .loginerr{background:rgba(239,68,68,.25);color:#fff;border:1px solid rgba(255,255,255,.3)}
122
- #loginView .eye{color:#fff}
123
- #loginView .btn.gray{background:rgba(255,255,255,.16);color:#fff;border:1px solid rgba(255,255,255,.3)}
124
- .langreel{width:min(340px,86vw);height:58px;margin:10px auto;background:rgba(167,139,250,.45);border:1px solid rgba(255,255,255,.55);border-radius:10px;backdrop-filter:blur(14px);overflow:hidden;cursor:pointer;position:relative;box-shadow:0 10px 30px rgba(76,29,149,.20)}
125
- body.dark .langreel{background:rgba(124,92,255,.35)}
126
- .ticks{display:flex;flex-direction:column;will-change:transform}
127
- .tick{height:58px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:600;opacity:.85;color:#fff}
128
- .pill{background:var(--card);border:1px solid var(--line);border-radius:999px;padding:8px 13px;font-size:13px;cursor:pointer}
129
- .pill.active{background:var(--blue);color:#fff;border-color:var(--blue)}
130
- .person{background:var(--card2);border:1px solid var(--line);border-radius:16px;padding:14px;margin:10px 0}
131
- .person .pname{font-size:17px;font-weight:700;margin-bottom:8px}
132
- .req{display:flex;align-items:center;gap:10px;padding:10px;border-radius:12px;background:var(--card);border:1px solid var(--line2);margin:6px 0}
133
- .req .txt{flex:1;font-size:15px}
134
- .req .tier{font-size:12px;font-weight:700;padding:4px 8px;border-radius:999px}
135
- .sentline{font-size:12px;color:#16a34a;margin-top:4px;font-weight:600}
136
- .pcnt{font-size:12.5px;color:var(--muted);font-weight:600;margin-left:6px}
137
- .tier-GREEN{background:#dcfce7;color:#15803d}
138
- .tier-SEMI{background:#fef9c3;color:#a16207}
139
- .tier-YELLOW{background:#fef9c3;color:#a16207}
140
- .tier-RED{background:#fee2e2;color:#b91c1c}
141
- .confirmBtn{background:var(--blue);color:#fff;border:none;border-radius:12px;padding:10px 14px;font-size:14px;font-weight:700;white-space:nowrap}
142
- .confirmBtn.done{background:#dcfce7;color:#15803d}
143
- .order{display:flex;align-items:center;gap:10px;padding:10px;border-radius:12px;background:var(--card2);border:1px solid var(--line);margin:6px 0;font-size:14px}
144
- .order .stat{font-size:12px;font-weight:700;padding:4px 10px;border-radius:999px;background:#e0e7ff;color:#3730a3}
145
- #wallView{max-width:1100px}
146
- .wall-head{font-size:34px;font-weight:800;color:#fff;text-align:center;margin:10px 0}
147
- .wall-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
148
- .wall-card{background:#1b2436;color:#fff;border-radius:16px;padding:20px;text-align:center}
149
- .wall-card .v{font-size:44px;font-weight:800}
150
- .wall-card .l{font-size:15px;opacity:.85}
151
- .wall-top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:6px 0 16px;flex-wrap:wrap}
152
- .wall-brand{display:flex;align-items:center;gap:10px;color:#fff}
153
- .wall-brand b{font-size:20px;display:block}
154
- .wall-brand span{font-size:12px;opacity:.75;text-transform:uppercase;letter-spacing:.06em}
155
- .wall-clock{text-align:right;color:#fff}
156
- .wall-clock b{font-size:20px;display:block}
157
- .wall-clock span{font-size:12px;opacity:.75}
158
- .live-dot{display:flex;align-items:center;gap:7px;color:#4ade80;font-weight:800;font-size:13px;letter-spacing:.08em}
159
- .live-dot::before{content:"";width:9px;height:9px;border-radius:50%;background:#4ade80;animation:pulse 1.6s infinite}
160
- .wtick{background:#1b2436;color:#d7e3f4;border-radius:10px;padding:8px 12px;font-size:13.5px;margin:6px 0}
161
- .wtick.dim{color:#64748b;background:none;border:1px dashed #334155}
162
- .wall-note{color:#64748b;font-size:12px;text-align:center;margin-top:10px}
163
- .welcome{position:relative;border-left:4px solid var(--blue);animation:rise .4s ease}
164
- .welcome .wclose{position:absolute;top:10px;right:10px;background:none;border:none;font-size:16px;color:var(--muted);cursor:pointer}
165
- button{transition:transform .12s ease}
166
- button:active{transform:scale(.97)}
167
- button:focus-visible,.pill:focus-visible,input:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
168
- body.wall{background:linear-gradient(165deg,#0b1220 0%,#131c30 60%,#1e1b4b 100%);background-attachment:fixed}
169
- body.firstboot header,body.firstboot main,body.firstboot footer{display:none}
170
- body.wall pre{max-height:40vh}
171
- @media (max-width:560px){header h1{font-size:15px}.listen{font-size:24px;padding:26px 12px}input,select,textarea,button{font-size:16px}}
package/dashboard.js DELETED
@@ -1,361 +0,0 @@
1
- // Le Veilleux dashboard logic — stage 2 of the structural refactor.
2
- // Extracted from dashboard.html. i18n comes from i18n.js (global S).
3
- "use strict";
4
- const LANG={es:"ES",en:"EN",fr:"FR",de:"DE",it:"IT",pt:"PT",zh:"中文",ja:"日本語",ko:"한국어",ar:"العربية"};
5
-
6
- function haptic(){try{if(navigator.vibrate)navigator.vibrate(25);}catch(e){}if(soundOn){try{const C=window.AudioContext||window.webkitAudioContext;if(!window.__ac)window.__ac=new C();const c=window.__ac;const o=c.createOscillator(),g=c.createGain();o.frequency.value=880;o.connect(g);g.connect(c.destination);g.gain.setValueAtTime(.12,c.currentTime);g.gain.exponentialRampToValueAtTime(.001,c.currentTime+.12);o.start();o.stop(c.currentTime+.12);}catch(e){}}}
7
- const $=id=>document.getElementById(id);
8
- let token=null,me=null,auto=false,cur="en",livePoll=null,PACK=null,recording=false,rec=null,soundOn=true,darkOn=false,kitchenPoll=null,lastOrders=0;
9
- function t(k){return (S[cur]&&S[cur][k])||S.en[k]||k;}
10
- function applyToggles(){document.body.classList.toggle("dark",darkOn);$("darkBtn").textContent=darkOn?"☀️":"🌙";$("soundBtn").textContent=soundOn?"🔊":"🔇";}
11
- $("darkBtn").onclick=()=>{darkOn=!darkOn;try{localStorage.setItem("dsh-dark",darkOn?"1":"0");}catch(e){}applyToggles();};
12
- $("soundBtn").onclick=()=>{soundOn=!soundOn;try{localStorage.setItem("dsh-sound",soundOn?"1":"0");}catch(e){}applyToggles();};
13
- try{darkOn=localStorage.getItem("dsh-dark")==="1";soundOn=localStorage.getItem("dsh-sound")!=="0";}catch(e){}
14
- applyToggles();
15
- document.body.classList.add("loginpage");
16
- const firstRun=(()=>{try{return !localStorage.getItem("dsh-lang");}catch(e){return true;}})();
17
- if(firstRun){
18
- $("splash").style.display="none";
19
- $("loginView").style.display="none";
20
- document.body.classList.add("firstboot");
21
- $("langStep").style.display="flex";
22
- }else{
23
- setTimeout(()=>{$("splash").classList.add("done");},1300);
24
- }
25
- async function reRenderDynamic(){
26
- if(!me)return;
27
- if(me.role==="owner"){loadMetrics($("ownerMetrics"));loadUsers();loadUsage();loadField();renderOrders();renderSuggest();}
28
- else if(me.role==="lead"){loadMetrics($("leadMetrics"));loadLead();renderOrders();}
29
- else{$("workerHi").textContent=t("hi")+(me.label||me.name);workerFlow();}
30
- if(location.search.indexOf("wall=1")>=0)wallTick();
31
- }
32
- async function kitchenWatch(){
33
- const live=await api("/api/live");
34
- const n=(live.orders||[]).length;
35
- if(n>lastOrders&&lastOrders>0){
36
- const o=live.orders[n-1];
37
- $("kitchenBanner").textContent="🔔 "+t("newOrder")+": "+o.person+" — "+o.request;
38
- $("kitchenBanner").classList.remove("hidden");
39
- haptic();
40
- try{if("Notification" in window&&Notification.permission==="granted"){new Notification(t("newOrder"),{body:o.person+" — "+o.request});}}catch(e){}
41
- setTimeout(()=>$("kitchenBanner").classList.add("hidden"),6000);
42
- }
43
- lastOrders=n;
44
- }
45
- $("notifBtn").onclick=()=>{
46
- try{
47
- if("Notification" in window&&Notification.permission!=="granted"){
48
- Notification.requestPermission().then(()=>{renderOrders();});
49
- }
50
- }catch(e){}
51
- };
52
- async function doExport(type){
53
- try{
54
- const r=await fetch("/api/export?type="+type,{headers:{"Authorization":"Bearer "+token}});
55
- if(!r.ok){toast("Error");return;}
56
- const blob=await r.blob();
57
- const a=document.createElement("a");a.href=URL.createObjectURL(blob);
58
- const cd=r.headers.get("Content-Disposition")||"";
59
- const m=/filename="?([^";]+)"?/.exec(cd);
60
- a.download=m?m[1]:("le-veilleux-"+type+".md");
61
- document.body.appendChild(a);a.click();a.remove();
62
- toast(t("exportDone"));
63
- }catch(e){toast(t("cannotReach"));}
64
- }
65
- async function loadRegister(){
66
- const r=await api("/api/register");
67
- if(!r||r.error)return;
68
- const open=r.status==="open";
69
- $("regClosed").classList.toggle("hidden",open);
70
- $("regOpenState").classList.toggle("hidden",!open);
71
- if(open){
72
- $("regItems").textContent=(r.items||[]).map(x=>x.name+" — $"+x.price).join(" · ");
73
- const cur=Math.round((r.items||[]).reduce((a,x)=>a+x.price,0)*100)/100;
74
- $("regTotal").textContent=t("regTotal")+": $"+cur;
75
- }
76
- if(me&&me.role==="owner"){
77
- if(r.status==="closed"){
78
- $("regDay").innerHTML='<p class="note">'+t("regRecorded")+": $"+r.recorded+" · "+t("regDeclared2")+": $"+(r.declared==null?"—":r.declared)+'</p><p class="note" style="'+(r.variance!==null&&Math.abs(r.variance)>0.01?"color:#b45309;font-weight:700":"")+'">'+t("regVariance")+": $"+(r.variance==null?"—":r.variance)+'</p>';
79
- } else {
80
- $("regDay").innerHTML='<p class="hint">—</p>';
81
- }
82
- renderRegSales(r);
83
- }
84
- if(me&&me.role==="employee"){$("regCount").textContent=(r.sales||[]).length+" "+t("salesToday");}
85
- try{api("/api/health").then(hp=>{if(hp&&hp.version)$("verBadge").textContent="supervisor v"+hp.version+" · app 0.1.0 · dsh "+((hp.workers!==undefined)?"":"")+"runtime";});}catch(e){}
86
- }
87
- function renderRegSales(r){
88
- const q=($("regSearch").value||"").toLowerCase();
89
- const m=$("regMethod").value;
90
- const list=(r.sales||[]).slice().reverse().filter(x=>{
91
- if(m!=="all"&&x.method!==m)return false;
92
- if(q&&!(x.items||[]).some(it=>it.name.toLowerCase().indexOf(q)>=0))return false;
93
- return true;
94
- });
95
- $("regSales").innerHTML=list.length?list.map(x=>'<div class="order"><b>'+esc(x.ts.slice(11,16))+'</b><span class="txt">'+esc((x.items||[]).map(it=>it.name).join(", "))+' · '+esc(x.worker||"?")+'</span><span class="stat">$ '+x.total+'</span></div>').join(""):empty("🧾","—");
96
- }
97
- $("regSearch").oninput=()=>{api("/api/register").then(renderRegSales);};
98
- $("regMethod").onchange=()=>{api("/api/register").then(renderRegSales);};
99
- $("regOpen").onclick=async()=>{await api("/api/register/open",{worker:me?me.name:"worker"});loadRegister();};
100
- $("regAdd").onclick=async()=>{const n=$("regItem").value.trim();const p=$("regPrice").value;if(!n||!p)return;await api("/api/register/add",{name:n,price:Number(p)});$("regItem").value="";$("regPrice").value="";loadRegister();};
101
- $("regSale").onclick=async()=>{await api("/api/register/charge",{method:$("regSaleMethod").value});loadRegister();toast(t("regSale"));};
102
- $("regClose").onclick=async()=>{await api("/api/register/close",{declared:Number($("regDeclared").value)||0});loadRegister();};
103
- $("exportRegisterBtn").onclick=()=>doExport("register");
104
- $("deepSend").onclick=async()=>{const v=$("deepText").value.trim();if(!v)return;const r=await api("/api/deep",{worker:me?me.name:"owner",text:v});if(r.error){toast(String(r.error).indexOf("budget")>=0?t("budgetReached"):"Error");return;}$("deepText").value="";toast(t("deepSent"));};
105
- $("exportBtn").onclick=()=>doExport("reports");
106
- $("exportOrdersBtn").onclick=()=>doExport("orders");
107
- function panelLinks(){const u=me?encodeURIComponent(me.name):"",r=me?encodeURIComponent(me.role):"",lb=me?encodeURIComponent(me.label||me.name):"";$("openPanelLink").setAttribute("href","/panel?user="+u+"&role="+r+"&label="+lb+"&lang="+cur);$("leadOpenPanelLink").setAttribute("href","/panel?user="+u+"&role="+r+"&label="+lb+"&lang="+cur);}
108
- function applyLang(){
109
- // (the language is persisted only on an explicit human choice)
110
- $("welcomeClose").onclick=()=>{$("welcomeCard").style.display="none";try{localStorage.setItem("lv-welcome","1");}catch(e){}};
111
- $("splashTag").textContent=t("tagline");
112
- $("welcomeTitle").textContent=t("welcomeTitle");$("welcomeBody").textContent=t("welcomeBody");$("wallNote").textContent=t("wallNote");
113
- $("setupUser").placeholder=t("setupUser");$("setupPass").placeholder=t("setupPass");$("setupBtn").textContent=t("setupBtn");$("setupHint").textContent=t("setupHint");
114
- $("loginTitle").textContent=t("login");$("tagline").textContent=t("tagline");$("loginUser").placeholder=t("user");
115
- ["loginUser","loginPass"].forEach(id=>$(id).addEventListener("input",()=>$("loginError").classList.add("hidden")));$("loginPass").placeholder=t("password");
116
- $("loginBtn").textContent=t("loginBtn");$("loginHint").textContent=t("loginHint");$("openWall").textContent=t("wallOpen");$("logoutBtn").textContent=t("logout");
117
- $("tabOverview").textContent=t("overview");$("tabField").textContent=t("field");$("tabRules").textContent=t("rules");
118
- $("workersTitle").textContent=t("workers");$("thUser").textContent=t("thUser");$("thRole").textContent=t("thRole");
119
- $("addWorkerTitle").textContent=t("addWorker");$("newUser").placeholder=t("userName");$("addUserBtn").textContent=t("addBtn");
120
- $("ordersTitle").textContent=t("ordersTitle");$("leadOrdersTitle").textContent=t("ordersTitle");$("ordersTitle2").textContent=t("ordersTitle");
121
- $("usageTitle").textContent=t("usage");$("usageSub").textContent=t("usageSub");
122
- $("thWorker").textContent=t("thWorker");$("thSessions").textContent=t("thSessions");$("thTokens").textContent=t("thTokens");$("thCost").textContent=t("thCost");
123
- $("panelTitle").textContent=t("panel");$("panelNote").textContent=t("panelNote");$("openPanel").textContent=t("openPanel");
124
- $("fieldReportsTitle").textContent=t("fieldReports");$("feedbackTitle").textContent=t("feedbackQueue");$("feedbackHint").textContent=t("feedbackHint");
125
- $("regSearch").placeholder=t("regSearch");$("regMethod").options[0].textContent=t("allMethods");$("regMethod").options[1].textContent=t("cash");$("regMethod").options[2].textContent=t("card");$("regMethod").options[3].textContent=t("terminal");$("regSaleMethod").options[0].textContent=t("terminal");$("regSaleMethod").options[1].textContent=t("cash");$("regSaleMethod").options[2].textContent=t("card");
126
- $("regTitle").textContent=t("regTitle");$("regOpen").textContent=t("regOpen");$("regItem").placeholder=t("regItem");$("regPrice").placeholder=t("regPrice");$("regAdd").textContent=t("regAdd");$("regSale").textContent=t("regSale");$("regHint").textContent=t("regHint");$("regClose").textContent=t("regClose");$("regDeclared").placeholder=t("regDeclared");$("regDayTitle").textContent=t("regDayTitle");$("exportRegisterBtn").textContent=t("exportReg");
127
- $("deepTitle").textContent=t("deepTitle");$("deepText").placeholder=t("deepPh");$("deepSend").textContent=t("deepSend");$("deepHint").textContent=t("deepHint");
128
- $("refreshReports").textContent=t("refresh");$("exportBtn").textContent=t("export");$("exportOrdersBtn").textContent=t("exportOrders");if(!auto)$("autoRefresh").textContent=t("autoOff");
129
- $("rulesTitle").textContent=t("rulesTitle");$("rulesHint").textContent=t("rulesHint");$("saveDecisions").textContent=t("saveRules");
130
- $("leadReportsTitle").textContent=t("fieldReports");$("leadFeedbackTitle").textContent=t("feedbackQueue");$("leadRefresh").textContent=t("refresh");$("leadOpenPanel").textContent=t("openPanel");
131
- $("startBtn").textContent=t("start");$("listeningText").textContent=t("listening");$("listeningHint").textContent=t("listeningHint");
132
- $("stopBtn").textContent=t("stopListening");$("summaryTitle").textContent=t("summaryTitle");$("newConvBtn").textContent=t("newConv");$("toHomeBtn").textContent=t("back");
133
- $("reportTitle").textContent=t("reportTitle");$("reportText").placeholder=t("reportPh");$("reportBtn").textContent=t("sendReport");
134
- $("fbTitle").textContent=t("fbTitle");$("empFbText").placeholder=t("fbPh");$("empFbBtn").textContent=t("sendFeedback");$("switchBtn").textContent=t("switchUser");
135
- $("wallPulseTitle").textContent=t("wallPulse");
136
- $("notifBtn").textContent=t("enableNotif");$("notifHint").textContent=t("notifHint");
137
- $("optEmployee").textContent=t("lvlEmployee");$("optLead").textContent=t("lvlLead");$("optOwner").textContent=t("lvlOwner");$("newLabel").placeholder=t("labelPh");
138
- $("tabSector").textContent=t("sectorTab");$("bnavOverview").textContent=t("overview");$("bnavField").textContent=t("field");$("bnavRules").textContent=t("rules");$("bnavSector").textContent=t("sectorTab");$("sectorPresetsTitle").textContent=t("sectorPresets");$("customSectorTitle").textContent=t("customSector");$("saveSectorBtn").textContent=t("saveSector");$("aiSectorBtn").textContent=t("aiSuggest");
139
- $("trailTitle").textContent=t("conversationTrail");$("trailHint").textContent=t("trailHint");
140
- $("sayText").placeholder=t("sayPh");$("sayBtn").textContent=t("sendPhrase");$("aiHint").textContent=t("aiHint");if(!recording)$("micBtn").textContent=t("dictate");
141
- document.querySelectorAll(".pill").forEach(p=>p.classList.toggle("active",p.dataset.l===cur));
142
- panelLinks();
143
- reRenderDynamic();
144
- }
145
- const LANGNAMES={es:"Español",en:"English",fr:"Français",de:"Deutsch",it:"Italiano",pt:"Português",zh:"中文",ja:"日本語",ko:"한국어",ar:"العربية"};
146
- const LANGORDER=["it","zh","de","es","en","fr","ja","pt","ko","ar"];
147
- function buildLangMenu(){
148
- const rows=$("langRows");
149
- for(const l of LANGORDER){
150
- const r=document.createElement("div");
151
- r.className="orow"+(l==="en"?" center":"");
152
- r.innerHTML='<span>'+LANGNAMES[l]+'</span><span class="sub">'+l.toUpperCase()+'</span>';
153
- r.onclick=()=>{cur=l;try{localStorage.setItem("dsh-lang",cur);}catch(e){}applyLang();$("langOverlay").style.display="none";markReel();$("langStep").classList.add("done");const sp=$("splash");sp.classList.remove("done");const p=sp.querySelector(".draw");if(p){p.style.animation="none";void p.offsetWidth;p.style.animation="";}setTimeout(()=>{$("loginView").style.display="";document.body.classList.remove("firstboot");},2100);setTimeout(()=>{sp.classList.add("done");},2600);};
154
- rows.appendChild(r);
155
- }
156
- }
157
- function markReel(){
158
- document.querySelectorAll("#langStepTicks .tick").forEach(t=>t.classList.toggle("active",t.dataset.l===cur));
159
- }
160
- function buildReel(ticksId){
161
- for(let pass=0;pass<2;pass++){
162
- for(const l of Object.keys(LANG)){
163
- const d=document.createElement("div");
164
- d.className="tick";d.dataset.l=l;d.textContent=LANGNAMES[l];
165
- $(ticksId).appendChild(d);
166
- }
167
- }
168
- const N=Object.keys(LANG).length,H=58,CYCLE=20000;
169
- const t0=performance.now();
170
- (function frame(now){
171
- const t=(now-t0)%CYCLE;
172
- $(ticksId).style.transform="translateY(-"+((t/CYCLE)*N*H)+"px)";
173
- requestAnimationFrame(frame);
174
- })(t0);
175
- }
176
- buildLangMenu();
177
- buildReel("langStepTicks");
178
- markReel();
179
- $("langStepReel").onclick=()=>{$("langOverlay").style.display="flex";const en=document.querySelector("#langRows .center");if(en)en.scrollIntoView({block:"center"});};
180
- $("closeOverlay").onclick=()=>{$("langOverlay").style.display="none";};
181
- // The Apple flow: after the splash, the language menu opens once (until chosen).
182
-
183
- function pill(l){for(const host of ["footLangs"]){const el=$(host);if(!el)continue;const b=document.createElement("button");b.className="pill";b.dataset.l=l;b.textContent=LANG[l];b.onclick=()=>{cur=l;try{localStorage.setItem("dsh-lang",cur);}catch(e){}applyLang();};$(host).appendChild(b);}}
184
- for(const l of Object.keys(LANG))pill(l);
185
- try{const q=new URLSearchParams(location.search);const ql=q.get("lang");if(ql&&S[ql])cur=ql;}catch(e){}
186
- try{const saved=localStorage.getItem("dsh-lang");if(saved&&S[saved])cur=saved;}catch(e){}
187
- applyLang();
188
- function applyPack(){if(!PACK)return;$("tableInput").placeholder=(PACK.tableLabel&&PACK.tableLabel[cur])||"Table";const ot=(PACK.ordersTarget&&PACK.ordersTarget[cur])||t("ordersTitle");$("ordersTitle").textContent=ot;$("leadOrdersTitle").textContent=ot;$("ordersTitle2").textContent=ot;}
189
- const api=async(p,body)=>{try{const r=await fetch(p,{method:body?"POST":"GET",headers:{"Content-Type":"application/json",...(token?{Authorization:"Bearer "+token}:{})},body:body?JSON.stringify(body):undefined});return r.json();}catch(e){return {error:"network"};}};
190
- function show(view){["loginView","ownerView","leadView","employeeView","wallView"].forEach(v=>$(v).classList.add("hidden"));const el=$(view);el.classList.remove("hidden");el.classList.remove("anim");void el.offsetWidth;el.classList.add("anim");document.body.classList.toggle("loginpage",view==="loginView");}
191
- const LVL={employee:"lvlEmployee",lead:"lvlLead",owner:"lvlOwner"};
192
- function setWho(){const w=$("who");w.textContent=me?(me.label||me.name):"";const c=$("idChip");if(me){c.style.display="inline-block";c.textContent=me.name.slice(0,1).toUpperCase()+" · "+(me.label||me.name)+" · "+t(LVL[me.role]||"lvlEmployee");}else{c.style.display="none";}$("logoutBtn").style.display=me?"inline-block":"none";}
193
- function toast(m){const el=$("toast");el.textContent=m;el.classList.add("show");clearTimeout(el.__t);el.__t=setTimeout(()=>el.classList.remove("show"),2200);}
194
- function skel(n){let out="";for(let i=0;i<n;i++)out+='<div class="skel" style="height:38px;margin:8px 0"></div>';return out;}
195
- const esc=(s)=>String(s==null?"":s).replace(/[&<>"']/g,(c)=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]));
196
- function empty(ico,txt){return '<div class="empty"><span class="ico">'+ico+'</span>'+txt+'</div>';}
197
- function showMsg(x){$("msg").textContent=x;toast(x);setTimeout(()=>$("msg").textContent="",2500);}
198
- function countUp(el,target,dur){dur=dur||650;const t0=performance.now();const step=(now)=>{const p=Math.min(1,(now-t0)/dur);el.textContent=Math.round(target*p).toLocaleString();if(p<1)requestAnimationFrame(step);};requestAnimationFrame(step);}
199
- async function loadMetrics(into){into.innerHTML=skel(2);const m=await api("/api/metrics");into.innerHTML="";const cards=[[t("reports"),m.reports],[t("week"),m.reportsWeek],[t("feedback"),m.feedback],[t("workersActive"),m.workers]];for(const [l,v] of cards){const d=document.createElement("div");d.className="metric";d.innerHTML='<div class="v">–</div><div class="l">'+l+'</div>';into.appendChild(d);if(typeof v==="number")countUp(d.querySelector(".v"),v);}}
200
- async function loadUsage(){const tb=$("usageTable")?$("usageTable").querySelector("tbody"):null;if(!tb)return;tb.innerHTML='<tr><td colspan="4">'+skel(2)+'</td></tr>';const r=await api("/api/usage");tb.innerHTML="";
201
- if(!(r.rows||[]).length){tb.innerHTML='<tr><td colspan="4">'+empty("📊",t("noUsage"))+'</td></tr>';return;}
202
- (r.rows||[]).forEach(x=>{const tr=document.createElement("tr");tr.innerHTML="<td>"+esc(x.worker)+"</td><td>"+x.sessions+"</td><td>"+(x.tokens||0).toLocaleString()+"</td><td>$"+(x.cost||0).toFixed(2)+"</td>";tb.appendChild(tr);});try{const hp=await api("/api/health");if(hp&&hp.budgetUsd!==undefined){const spent=hp.spentTodayUsd||0;if(hp.budgetUsd>0){const pct=hp.budgetPct||0;$("usageBudget").textContent=t("budget")+": $"+spent+" / $"+hp.budgetUsd+" ("+pct+"%)";$("usageBudget").style.color=pct>=80?"#b45309":"";}else{$("usageBudget").textContent=t("budget")+": $"+spent+" ∞";$("usageBudget").style.color="";}}}catch(e){}}
203
- function orderRow(o,canUpdate){
204
- const d=document.createElement("div");d.className="order";
205
- const stat=document.createElement("span");stat.className="stat";const ICON={sent:"📤",prep:"🍳",ready:"✅",delivered:"🛎️"};
206
- stat.textContent=(ICON[o.status]||"📤")+" "+t(o.status||"sent");
207
- if(o.paid){const p=document.createElement("span");p.className="stat";p.style.background="#dcfce7";p.style.color="#15803d";p.textContent=t("paid");d.appendChild(p);}
208
- d.innerHTML="<b>"+(o.table?((PACK&&PACK.tableLabel&&PACK.tableLabel[cur]?PACK.tableLabel[cur]:"Table")+" "+o.table+" · "):"")+esc(o.person)+"</b>";
209
- const txt=document.createElement("span");txt.className="txt";txt.textContent=o.request;
210
- d.appendChild(txt);d.appendChild(stat);
211
- if(canUpdate&&!o.paid){const pb=document.createElement("button");pb.className="btn small";pb.textContent=t("markPaid");pb.style.background="#16a34a";pb.onclick=async()=>{await api("/api/live/order",{orderId:o.id,status:"paid"});haptic();renderOrders();};d.appendChild(pb);}
212
- if(canUpdate&&o.status!=="delivered"){
213
- const next=o.status==="sent"?"prep":o.status==="prep"?"ready":"delivered";
214
- const b=document.createElement("button");b.className="btn small";b.textContent=t(next)+" →";
215
- b.onclick=async()=>{await api("/api/live/order",{orderId:o.id,status:next});haptic();renderOrders();};
216
- d.appendChild(b);
217
- }
218
- return d;
219
- }
220
- async function renderOrders(){
221
- for(const id of ["ownerOrders","leadOrders","workerOrders"]){const el=$(id);if(el)el.innerHTML=skel(2);}
222
- const live=await api("/api/live");
223
- for(const id of ["ownerOrders","leadOrders","workerOrders"]){
224
- const el=$(id);if(!el)continue;
225
- el.innerHTML="";
226
- (live.orders||[]).forEach(o=>el.appendChild(orderRow(o,id!=="workerOrders")));
227
- if(!(live.orders||[]).length)el.innerHTML=empty("📦",t("noOrders"));
228
- if(id==="ownerOrders"){const os=live.orders||[];const paid=os.filter(o=>o.paid).length;$("billSummary").textContent=t("billSummary").replace("X",paid).replace("Y",os.length);}
229
- }
230
- }
231
- $("sayBtn").onclick=async()=>{const v=$("sayText").value.trim();if(!v)return;const r=await api("/api/live/transcript",{worker:me?me.name:"worker",text:v});if(r&&r.error){toast(String(r.error).indexOf("budget")>=0?t("budgetReached"):r.error);return;}$("sayText").value="";showMsg(t("processing"));};
232
- const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
233
- if(SR){$("micBtn").onclick=()=>{
234
- if(rec){rec.stop();return;}
235
- rec=new SR();rec.lang=(cur==="zh"?"zh-CN":cur==="ja"?"ja-JP":cur==="ko"?"ko-KR":cur==="ar"?"ar-SA":cur+"-"+(cur==="en"?"US":cur==="pt"?"PT":cur.toUpperCase()));
236
- rec.continuous=false;rec.interimResults=false;recording=true;$("micBtn").textContent="⏹";
237
- rec.onresult=async(e)=>{let txt="";for(let i=0;i<e.results.length;i++)txt+=e.results[i][0].transcript;await api("/api/live/transcript",{worker:me?me.name:"worker",text:txt});showMsg(t("processing"));};
238
- rec.onend=()=>{recording=false;$("micBtn").textContent=t("dictate");rec=null;};
239
- rec.start();
240
- };}else{$("micBtn").disabled=true;$("micBtn").textContent=t("micUnsupported");}
241
- function packFieldsHTML(){if(!PACK||!PACK.fields||!PACK.fields.length)return "";return PACK.fields.map(f=>'<input class="packField" data-fid="'+f.id+'" placeholder="'+(f.label&&f.label[cur]?f.label[cur]:f.id)+'">').join("");}
242
- async function workerFlow(){
243
- const live=await api("/api/live");
244
- const wv=$("employeeView");
245
- if(!me){return;}
246
- $("wHome").classList.add("hidden");$("wListen").classList.add("hidden");$("wSummary").classList.add("hidden");
247
- if(live.status==="listening"){$("wListen").classList.remove("hidden");$("listeningBtn").textContent="🎙️ "+ (live.worker||"");$("packFields").innerHTML=packFieldsHTML();$("tableInput").value=live.table||"";}
248
- else if(live.status==="summary"){$("wSummary").classList.remove("hidden");$("summaryTitle").textContent=t("summaryTitle")+(live.table?(" — "+(PACK&&PACK.tableLabel&&PACK.tableLabel[cur]?PACK.tableLabel[cur]:"Table")+" "+live.table):"")+Object.entries(live.fields||{}).filter(([k,v])=>v).map(([k,v])=>" · "+v).join("");renderSummary(live);}
249
- else{$("wHome").classList.remove("hidden");}
250
- renderOrders();
251
- }
252
- function renderSummary(live){
253
- const body=$("summaryBody");body.innerHTML="";
254
- if(!(live.speakers||[]).length){body.innerHTML='<div class="card"><p class="note">'+t("noSummary")+'</p></div>';return;}
255
- for(const p of live.speakers){
256
- const d=document.createElement("div");d.className="person";
257
- const done=p.requests.filter(r=>r.status==="confirmed").length;
258
- let h='<div class="pname">'+t("person")+" "+(p.id.replace("p",""))+" — "+esc(p.name)+'<span class="pcnt">('+done+'/'+p.requests.length+')</span></div>';
259
- for(const r of p.requests){
260
- const confirmed=r.status==="confirmed";
261
- h+='<div class="req"><span class="txt">'+esc(r.text)+'</span>'+
262
- '<span class="tier tier-'+r.tier+'">'+t("tier"+r.tier)+'</span>'+
263
- '<button class="confirmBtn'+(confirmed?" done":"")+'" data-req="'+r.id+'">'+(confirmed?t("confirmed"):t("confirm"))+'</button></div>'+
264
- (confirmed?'<div class="sentline">✓ '+t("sentNote")+'</div>':'');
265
- }
266
- d.innerHTML=h;
267
- d.querySelectorAll(".confirmBtn").forEach(b=>b.onclick=async()=>{
268
- if(b.classList.contains("done"))return;
269
- b.textContent=t("confirmed");b.classList.add("done","checkpop");haptic();toast(t("confirmed"));
270
- await api("/api/live/confirm",{requestId:b.dataset.req});
271
- workerFlow();
272
- });
273
- body.appendChild(d);
274
- }
275
- }
276
- $("startBtn").onclick=async()=>{const fields={};document.querySelectorAll(".packField").forEach(i=>{fields[i.dataset.fid]=i.value.trim();});await api("/api/live/start",{worker:me.name,table:$("tableInput").value.trim()||null,fields});workerFlow();};
277
- $("stopBtn").onclick=async()=>{const live=await api("/api/live");if((live.speakers||[]).length){live.status="summary";await api("/api/live/summary",{speakers:live.speakers});}else{await api("/api/live/close",{});}workerFlow();};
278
- $("newConvBtn").onclick=async()=>{await api("/api/live/close",{});workerFlow();};
279
- $("toHomeBtn").onclick=async()=>{await api("/api/live/close",{});workerFlow();};
280
- async function enter(){
281
- if(!PACK){PACK=await api("/api/sector");applyPack();}
282
- setWho();
283
- if(me.role==="owner"){show("ownerView");try{if(!localStorage.getItem("lv-welcome"))$("welcomeCard").style.display="block";}catch(e){}loadRegister();loadMetrics($("ownerMetrics"));loadUsers();loadField();loadRules();loadUsage();renderOrders();loadPresets();}
284
- else if(me.role==="lead"){show("leadView");loadMetrics($("leadMetrics"));loadLead();renderOrders();if(kitchenPoll)clearInterval(kitchenPoll);kitchenPoll=setInterval(kitchenWatch,4000);}
285
- else{show("employeeView");$("workerHi").textContent=t("hi")+(me.label||me.name);loadRegister();workerFlow();if(livePoll)clearInterval(livePoll);livePoll=setInterval(()=>{if(me&&me.role==="employee")workerFlow();},3000);}
286
- }
287
- let pwVisible=false;
288
- $("eyeBtn").onclick=()=>{pwVisible=!pwVisible;$("loginPass").type=pwVisible?"text":"password";$("eyeOn").classList.toggle("hidden",pwVisible);$("eyeOff").classList.toggle("hidden",!pwVisible);};
289
- $("setupBtn").onclick=async()=>{
290
- const user=$("setupUser").value.trim();
291
- const password=$("setupPass").value;
292
- if(!user||password.length<8){toast(t("fillBoth"));return;}
293
- $("setupBtn").disabled=true;$("setupBtn").innerHTML='<span class="spin"></span>'+t("loggingIn");
294
- const r=await api("/api/setup",{user,password,sector:$("setupSector").value});
295
- if(r.error){$("setupBtn").disabled=false;$("setupBtn").textContent=t("setupBtn");toast(String(r.error).indexOf("already")>=0?t("loginFailed"):r.error);return;}
296
- token=r.token;me={name:r.name,role:r.role,label:r.label||r.name};
297
- sessionStorage.setItem("dsh-sup-token",token);sessionStorage.setItem("dsh-sup-me",JSON.stringify(me));
298
- enter();
299
- };
300
- $("loginBtn").onclick=async()=>{
301
- const err=$("loginError");
302
- err.classList.add("hidden");
303
- const u=$("loginUser").value.trim(),p=$("loginPass").value;
304
- if(!u||!p){err.textContent=t("fillBoth");err.classList.remove("hidden");return;}
305
- $("loginBtn").disabled=true;$("loginBtn").innerHTML='<span class="spin"></span>'+t("loggingIn");
306
- const r=await api("/api/login",{user:u,password:p});
307
- $("loginBtn").disabled=false;$("loginBtn").textContent=t("loginBtn");
308
- if(r.error){err.textContent=r.error==="network"?t("cannotReach"):t("loginFailed");err.classList.remove("hidden");err.classList.remove("shake");void err.offsetWidth;err.classList.add("shake");return;}
309
- err.classList.add("hidden");
310
- token=r.token;me={name:r.name,role:r.role,label:r.label||r.name};
311
- sessionStorage.setItem("dsh-sup-token",token);sessionStorage.setItem("dsh-sup-me",JSON.stringify(me));
312
- $("loginPass").value="";enter();
313
- };
314
- function logout(){api("/api/logout",{});token=null;me=null;sessionStorage.clear();if(livePoll)clearInterval(livePoll);if(kitchenPoll)clearInterval(kitchenPoll);setWho();show("loginView");}
315
- $("logoutBtn").onclick=logout;
316
- const sb=$("switchBtn");if(sb)sb.onclick=logout;
317
- async function loadUsers(){const tb=$("usersTable").querySelector("tbody");tb.innerHTML='<tr><td colspan="2">'+skel(2)+'</td></tr>';const r=await api("/api/users");tb.innerHTML="";(r.users||[]).forEach(u=>{const tr=document.createElement("tr");tr.innerHTML="<td>"+esc(u.name)+"</td><td>"+esc(u.label||u.name)+" · "+t(LVL[u.role]||"lvlEmployee")+"</td>";tb.appendChild(tr);});}
318
- $("addUserBtn").onclick=async()=>{const r=await api("/api/users",{name:$("newUser").value,password:$("newPass").value,role:$("newRole").value,label:$("newLabel").value});showMsg(r.error?"Error: "+r.error:t("workerAdded"));if(!r.error){loadUsers();$("newUser").value=$("newPass").value="";}};
319
- async function loadField(){$("reports").innerHTML=skel(2);$("feedback").innerHTML=skel(1);$("trail").innerHTML=skel(1);const rc=(await api("/api/reports")).content;if(rc){$("reports").innerHTML="<pre>"+esc(rc)+"</pre>";}else{$("reports").innerHTML=empty("📋",t("noReports"));}const fc=(await api("/api/feedback")).content;if(fc){$("feedback").innerHTML="<pre>"+esc(fc)+"</pre>";}else{$("feedback").innerHTML=empty("💬",t("feedbackQueue"));}const live=await api("/api/live");const ib=(live.inbox||[]).map(x=>"[•] "+esc(x.worker)+": "+esc(x.text)).join("\n");if(ib){$("trail").innerHTML="<pre>"+ib+"</pre>";}else{$("trail").innerHTML=empty("🗒️",t("conversationTrail"));}}
320
- async function loadLead(){$("leadReports").textContent=(await api("/api/reports")).content||"(empty)";$("leadFeedback").textContent=(await api("/api/feedback")).content||"(empty)";}
321
- $("refreshReports").onclick=loadField;$("leadRefresh").onclick=loadLead;
322
- const ar=$("autoRefresh");
323
- if(ar)ar.onclick=()=>{auto=!auto;ar.textContent=auto?t("autoOn"):t("autoOff");if(auto){const iv=setInterval(()=>{if(!auto){clearInterval(iv);return;}if(me&&me.role==="owner"){loadField();renderOrders();}},30000);}};
324
- async function loadPresets(){
325
- const r=await api("/api/sector-presets");
326
- const g=$("presetGrid");if(!g)return;g.innerHTML="";
327
- (r.presets||[]).forEach(p=>{const b=document.createElement("button");b.className="btn small gray";b.textContent=p.id;b.onclick=async()=>{await api("/api/sector",p);PACK=p;applyPack();showMsg(t("sectorSaved"));renderSuggest();};g.appendChild(b);});
328
- renderSuggest();
329
- }
330
- function renderSuggest(){const el=$("suggestUsers");if(!el||!PACK||!PACK.suggestedUsers)return;el.innerHTML="";(PACK.suggestedUsers||[]).forEach(su=>{const b=document.createElement("button");b.className="btn small gray";b.textContent="+ "+(su.label&&(su.label[cur]||su.label.en)||su.level);b.onclick=()=>{$("newUser").value="";$("newLabel").value=su.label&&(su.label[cur]||su.label.en)||"";$("newRole").value=su.level;};el.appendChild(b);});}
331
- $("saveSectorBtn").onclick=async()=>{
332
- const pack={id:$("csId").value.trim()||"custom",
333
- tableLabel:{en:$("csLabelEn").value.trim()||"Table",fr:$("csLabelFr").value.trim(),es:$("csLabelEn").value.trim()||"Table"},
334
- ordersTarget:{en:$("csOrdersEn").value.trim()||"Orders",fr:$("csOrdersFr").value.trim()},
335
- entity:{en:"person",fr:"personne"},
336
- fields:[]};
337
- if($("csF1Id").value.trim())pack.fields.push({id:$("csF1Id").value.trim(),type:"text",label:{en:$("csF1Label").value.trim()||$("csF1Id").value.trim()}});
338
- if($("csF2Id").value.trim())pack.fields.push({id:$("csF2Id").value.trim(),type:"text",label:{en:$("csF2Label").value.trim()||$("csF2Id").value.trim()}});
339
- const r=await api("/api/sector",pack);showMsg(r.error?"Error":t("sectorSaved"));PACK=pack;applyPack();
340
- };
341
- $("aiSectorBtn").onclick=async()=>{
342
- const prompt="Live mirror mode (workflow-master). Design a sector pack JSON for this business, in this exact shape: {\"id\":\"<sector-id>\",\"tableLabel\":{\"en\":\"<entity label>\",\"es\":\"…\",\"fr\":\"…\"},\"ordersTarget\":{\"en\":\"<orders title>\",\"fr\":\"…\"},\"entity\":{\"en\":\"<entity>\",\"fr\":\"…\"},\"fields\":[{\"id\":\"<field>\",\"type\":\"number|text\",\"label\":{\"en\":\"<label>\",\"fr\":\"…\"}}]}. Describe your business here: "+($("csLabelEn").value||"(your business)")+". Reply ONLY with the JSON.";
343
- try{await navigator.clipboard.writeText(prompt);}catch(e){}
344
- showMsg(t("aiSectorCopied"));
345
- };
346
- async function loadRules(){$("decisions").value=(await api("/api/decisions")).content||"";}
347
- $("saveDecisions").onclick=async()=>{await api("/api/decisions",{content:$("decisions").value});$("decisionsSaved").textContent=t("saved");setTimeout(()=>$("decisionsSaved").textContent="",2000);};
348
- document.querySelectorAll(".tabs button, .bnav button").forEach(b=>b.onclick=()=>{document.querySelectorAll(".tabs button, .bnav button").forEach(x=>x.classList.remove("active"));b.classList.add("active");["overview","field","rules","sector"].forEach(x=>$("tab-"+x).classList.add("hidden"));$("tab-"+b.dataset.tab).classList.remove("hidden");});
349
- $("reportBtn").onclick=async()=>{const r=await api("/api/report",{text:$("reportText").value});if(r.error){showMsg("Error: "+r.error);return;}$("reportText").value="";showMsg(t("reportSent"));};
350
- $("empFbBtn").onclick=async()=>{const r=await api("/api/feedback",{worker:me.name,role:me.role,category:$("empFbCat").value,text:$("empFbText").value});if(r.error){showMsg("Error: "+r.error);return;}$("empFbText").value="";showMsg(t("feedbackSent"));};
351
- api("/api/setup-status").then((st)=>{if(st&&st.users===0){$("setupSection").classList.remove("hidden");$("loginUser").parentNode.style.display="none";$("loginBtn").style.display="none";$("loginPass").closest(".passwrap").style.display="none";$("loginTitle").textContent=t("setupTitle");}}).catch(()=>{});
352
- const st=sessionStorage.getItem("dsh-sup-token");
353
- if(st){token=st;me=JSON.parse(sessionStorage.getItem("dsh-sup-me")||"null");(async()=>{const r=await api("/api/me");if(!r||!r.name){logout();return;}enter();})();}
354
- else setWho();
355
- async function wallTick(){try{const r=await (await fetch("/api/wall")).json();const cards=[[t("week"),r.reportsWeek],[t("ordersTitle"),r.orders],[t("feedback"),r.feedback],[t("workersActive"),r.workers],[t("reportsTotal"),r.reports]];$("wallCards").innerHTML=cards.map(c=>'<div class="wall-card"><div class="v">'+(c[1]===undefined?"–":c[1])+'</div><div class="l">'+c[0]+'</div></div>').join("");const ticks=[...(r.ordersTail||[]).map(x=>"🔔 "+esc(x)),...(r.feedbackTail||"").split("\n").filter(Boolean).slice(-4).map(esc)];$("wallFeed").innerHTML=ticks.length?ticks.map(x=>'<div class="wtick">'+x+'</div>').join(""):'<div class="wtick dim">'+t("noActivity")+'</div>';try{const s=await (await fetch("/api/sector")).json();$("wallSector").textContent=(s&&s.id)?s.id:"";}catch(e){}}catch(e){$("wallFeed").innerHTML='<div class="wtick dim">'+t("cannotReach")+'</div>';}}
356
- function clockTick(){const d=new Date();$("wallClock").textContent=d.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"});$("wallDate").textContent=d.toLocaleDateString([],{weekday:"short",day:"numeric",month:"short"});}
357
- if(location.search.indexOf("wall=1")>=0){
358
- document.body.classList.add("wall");show("wallView");
359
- clockTick();setInterval(clockTick,1000);
360
- wallTick();setInterval(wallTick,30000);
361
- }