diff --git a/rpa-dashboard-no-ai/SKILL.md b/rpa-dashboard-no-ai/SKILL.md index dcfdcd6..0a6b95b 100644 --- a/rpa-dashboard-no-ai/SKILL.md +++ b/rpa-dashboard-no-ai/SKILL.md @@ -15,7 +15,8 @@ source projects. | `templates/contract.md` | The data contract: events the bot sends, actions the page sends. | | `templates/themes.md` | Theme presets (Tebra Sky, Superbill Ember) and the custom-colour steps. | | `templates/palette.py` | Derives a custom palette from one brand hex and checks contrast. | -| `templates/reference/` | Raw Tebra extracts. Use them only to port optional sections. | +| `templates/sections.py`, `templates/sections.md` | Keeps the chosen optional sections in the page and deletes the rest; what each section needs from the host. | +| `templates/reference/` | Raw Tebra extracts, for reference only. The optional sections are already in `dashboard.html`. | Follow the steps in order. Ask one question at a time, and wait for the answer. If the user's request already answers a question, don't ask it again. @@ -59,10 +60,20 @@ Ask only if there is no frontend. If there is one, use its stack. Always built: the shell (sidebar, top bar, dark toggle, status dot), KPI cards, the status donut, the run panel and the report table. -Ask which extras to add (choose any, or none): **checkpoint tracker**, -**admin/roles screen**, **login page**. Build only the ones chosen. Unchosen -sections must leave no markup, script or style behind, so delete their -`` markers. +Ask which extras to add, as one multi-select question (choose any, or none): +- **Checkpoint tracker**: numbered boxes on the Run tab that show each checkpoint as + pending, in progress, done or stuck. +- **Admin/roles screen**: an Admin tab, for admins only, to give people admin or + operator access. +- **Login page**: a sign-in screen in the dashboard's theme. It's UI only and hands + off to the project's own auth. + +Build only the chosen ones. In step 6, right after copying the page, run +`python templates/sections.py `. +It deletes the unchosen sections completely (markup, script and style), so nothing +is left behind. Never cut the blocks by hand. Then follow `templates/sections.md` for +each kept section: what to fill, which events the bot sends, and which host actions +to answer. ## 6. Build diff --git a/rpa-dashboard-no-ai/templates/contract.md b/rpa-dashboard-no-ai/templates/contract.md index 9e33309..58a5c5d 100644 --- a/rpa-dashboard-no-ai/templates/contract.md +++ b/rpa-dashboard-no-ai/templates/contract.md @@ -18,7 +18,7 @@ All updates use one envelope: `{"event": "", "data": {...}}`. | `set_user` | `{username, emp_id, role}` | | `apply_dark` | `{on: bool}` | -Optional (only if the section was chosen): `cpstate {kind: pending|active|done|stuck, cp: int}`, `admin_data`, `batch_detail`. +Optional sections (only if chosen) add `cpstate`, `admin_data` and `login_result`, and the actions `load_admin`, `assign_role`, `remove_role` and `login`. See `sections.md`. ## Page → bot/backend (Qt `@pyqtSlot` names / web `POST {API_BASE}/`) diff --git a/rpa-dashboard-no-ai/templates/dashboard.html b/rpa-dashboard-no-ai/templates/dashboard.html index a40fcae..fac248a 100644 --- a/rpa-dashboard-no-ai/templates/dashboard.html +++ b/rpa-dashboard-no-ai/templates/dashboard.html @@ -117,6 +117,32 @@ table{border-collapse:collapse;width:100%;font-size:12px;white-space:nowrap} th{background:var(--glass-strong);text-align:left;padding:11px 14px;position:sticky;top:0;font-weight:700} td{padding:10px 14px;border-bottom:1px solid var(--border)} .empty{text-align:center;padding:40px;color:var(--muted);font-size:13px} +/* OPTIONAL:checkpoints BEGIN */ +.cp-track{display:grid;grid-template-columns:repeat(auto-fit,minmax(118px,1fr));gap:8px;margin-bottom:16px} +.cp-box{display:flex;flex-direction:column;gap:4px;padding:10px 11px;border-radius:11px;border:1.5px solid var(--border);background:var(--glass-strong);color:var(--muted);font-size:11.5px;font-weight:700} +.cp-name{color:var(--ink);font-size:12.5px} +.cp-box.active{border-color:var(--warn);background:var(--warn-bg);color:var(--warn)} +.cp-box.done{border-color:var(--ok);background:var(--ok-bg);color:var(--ok)} +.cp-box.stuck{border-color:var(--bad);background:var(--bad-bg);color:var(--bad)} +/* OPTIONAL:checkpoints END */ +/* OPTIONAL:admin BEGIN */ +body:not([data-role="admin" i]) .adm-only{display:none!important} +.toolbar select{padding:8px 10px;border:1px solid var(--border);border-radius:10px;background:var(--glass-strong);color:var(--ink);font:inherit;font-size:12.5px} +.toolbar select option{background:var(--bg-bottom);color:var(--ink)} +.pill.adm-role{background:var(--muted);text-transform:capitalize}.pill.adm-role.admin{background:var(--accent)} +.adm-note{font-size:12.5px;color:var(--muted)} +/* OPTIONAL:admin END */ +/* OPTIONAL:login BEGIN */ +.login-screen{position:fixed;inset:0;z-index:50;display:flex;align-items:center;justify-content:center;padding:16px; + background:linear-gradient(180deg,var(--bg-top) 0%,var(--bg-mid) 45%,var(--bg-bottom) 100%)} +.login-card{width:min(360px,100%);display:flex;flex-direction:column;gap:12px;margin:0} +.login-card .c-title{font-size:19px;margin:0} +.login-sub{font-size:12.5px;color:var(--muted);margin-top:-8px} +.login-card label{display:flex;flex-direction:column;gap:5px;font-size:12px;font-weight:700;color:var(--muted)} +.login-card input{padding:10px 12px;border:1px solid var(--border);border-radius:10px;background:var(--glass-strong);color:var(--ink);font:inherit;font-size:13px} +.login-err{color:var(--bad);font-size:12.5px;font-weight:600;min-height:1.2em} +.login-out svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:2;vertical-align:middle} +/* OPTIONAL:login END */ @media (max-width:820px){.sidebar{width:66px;min-width:66px}.logo-title,.logo-sub,.nav-item span,.u-name,.u-role{display:none}.kpi-row{grid-template-columns:1fr 1fr}.dash-grid{grid-template-columns:1fr}} @@ -131,7 +157,9 @@ td{padding:10px 14px;border-bottom:1px solid var(--border)} - + + + @@ -141,6 +169,9 @@ td{padding:10px 14px;border-bottom:1px solid var(--border)}
Dashboard
Overview of every batch
+ + +
System ready
@@ -169,7 +200,10 @@ td{padding:10px 14px;border-bottom:1px solid var(--border)}
Waiting to start0%
    - + + +
      +
      @@ -181,10 +215,41 @@ td{padding:10px 14px;border-bottom:1px solid var(--border)}
      + + + + + + + + + + diff --git a/rpa-dashboard-no-ai/templates/sections.md b/rpa-dashboard-no-ai/templates/sections.md new file mode 100644 index 0000000..157f767 --- /dev/null +++ b/rpa-dashboard-no-ai/templates/sections.md @@ -0,0 +1,106 @@ +# Optional sections + +Three extras live in `dashboard.html` as marked blocks (`OPTIONAL: BEGIN` ... `END`) +in the style, the markup and the script. Build only the ones the user picked: + +``` +python /templates/sections.py /dashboard/dashboard.html checkpoints admin login +``` + +Name only the chosen sections; name none to remove all three. The script deletes every +unchosen block (markup, script and style) and the marker lines of the chosen ones, and it +stops if a marker is broken. Never cut the blocks by hand. + +With `SAMPLE_DATA = true`, every section works on its own sample answers. Once the bot is +wired, the host must answer the section's actions as below. Desktop hosts add the slots to +the `Bridge` class in `bridge.py`. Web hosts serve `POST {API_BASE}/`, +like the core actions, and send the events in the `/run/status` events list. + +## Checkpoint tracker (`checkpoints`) + +Numbered boxes on the Run tab. Each one is pending, in progress, done or stuck. + +- Fill `{{CHECKPOINTS_JSON}}` with the checkpoint names, in order, from the bot's real + stages, for example `["Open the portal","Load the list","Process items","Finish up"]`. + They can be finer than `STEPS`. +- Event `cpstate {kind: "pending"|"active"|"done"|"stuck", cp}`. `cp` counts from 1. + `active` also marks the earlier boxes done and the later ones pending, so the bot can + send `active` for each checkpoint as it starts, and `stuck` when one fails. +- Desktop: add a marker table next to `STEP_MARKERS` in `bridge.py`, and send the event + from `_on_line`, using specific substrings the bot really prints: + + ```python + CHECKPOINT_MARKERS = [("[cp 1]", "active", 1), ("[cp 1 failed]", "stuck", 1)] # (substring, kind, cp) + + # in _on_line, after the step markers: + for marker, kind, cp in CHECKPOINT_MARKERS: + if marker in low: + self.send_to_js("cpstate", {"kind": kind, "cp": cp}) + break + ``` + +## Admin/roles screen (`admin`) + +An Admin tab where an admin assigns or removes admin and operator access. The Admin nav +item and the screen appear only while `set_user` says `role: "admin"`; operators never see +them. Hiding them is not security: **the host re-checks that the signed-in user is an admin +on every admin action**, and refuses otherwise. + +- Event `admin_data {rows: [{username, emp_id, role: "admin"|"operator"}], msg?}`. `msg` is + shown above the list, for example "Access assigned." or "Only admins can do that." +- Actions: `load_admin()` when the tab opens, `assign_role(who, role)`, `remove_role(who)`. + `who` is a username or employee ID. Answer each one with a fresh `admin_data`. +- Desktop slots, using the project's own user store: + + ```python + @pyqtSlot() + def load_admin(self): + if self.role != "admin": # self.role: set when the user signs in + return + self.send_to_js("admin_data", {"rows": read_access_list()}) + + @pyqtSlot(str, str) + def assign_role(self, who, role): + if self.role == "admin" and role in ("admin", "operator"): + save_access(who, role) + self.load_admin() + + @pyqtSlot(str) + def remove_role(self, who): + if self.role == "admin": + delete_access(who) + self.load_admin() + ``` + +## Login page (`login`) + +A sign-in screen in the page's own theme, shown over the dashboard until sign-in succeeds, +plus a Sign out button in the top bar. It is UI only: it hands the username and password to +the project's existing auth. Don't build an auth backend. + +- Desktop action `login(username, password)`. The host checks them and sends + `login_result {ok, msg?, user?: {username, emp_id, role}}`. On `ok` the screen closes and + `user` fills the sidebar (and the role, if the admin screen is built). Otherwise `msg` + shows under the form. +- Web: `POST {API_BASE}/login` with JSON `{username, password}`, answered with the same + `{ok, msg?, user?}` body. +- Action `logout()` when the user signs out. The screen comes back. +- The page sits behind the screen, but the host must still refuse data actions until + sign-in succeeds. +- No auth in the project yet: keep a stub that lets any non-empty username in, and tell the + user it is a stub. In sample mode the page has its own stub. + + ```python + @pyqtSlot(str, str) + def login(self, username, password): + role = check_login(username, password) # the project's auth: a role, or None + self.role = role or "" + if role: + self.send_to_js("login_result", {"ok": True, "user": {"username": username, "emp_id": "", "role": role}}) + else: + self.send_to_js("login_result", {"ok": False, "msg": "Wrong username or password."}) + + @pyqtSlot() + def logout(self): + self.role = "" + ``` diff --git a/rpa-dashboard-no-ai/templates/sections.py b/rpa-dashboard-no-ai/templates/sections.py new file mode 100644 index 0000000..01b5c5e --- /dev/null +++ b/rpa-dashboard-no-ai/templates/sections.py @@ -0,0 +1,35 @@ +"""Keep the optional sections the user chose in dashboard.html, and delete the rest. + +Usage: python sections.py [checkpoints] [admin] [login] + +Each section sits between marker lines, in HTML, CSS and script: + /* OPTIONAL:admin BEGIN */ // OPTIONAL:admin BEGIN + ... ... ... + /* OPTIONAL:admin END */ // OPTIONAL:admin END +Chosen sections lose only their marker lines. Unchosen ones lose everything. +The file is rewritten in place. Name no sections to remove all of them. +""" +import re +import sys +from pathlib import Path + +SECTIONS = ("checkpoints", "admin", "login") +BLOCK = re.compile(r"^[ \t]*(?: