diff --git a/cron/blueprint_catalog.py b/cron/blueprint_catalog.py index 4a938e4a3a..4de10f142e 100644 --- a/cron/blueprint_catalog.py +++ b/cron/blueprint_catalog.py @@ -329,6 +329,52 @@ CATALOG: List[AutomationBlueprint] = [ skills=("product-price-monitor",), tags=("prices", "shopping", "travel", "monitor"), ), + AutomationBlueprint( + # Inspired by Energy's (getenergy.com) one-sentence live dashboards: + # describe what you want to see, the agent builds a persistent view + # and keeps it current from email/web/file sources. + key="live-dashboard", + title="Live status dashboard", + description="A self-updating dashboard for any project or process — " + "describe what to track and it refreshes from your email, sites, " + "and files.", + category="general", + schedule_template="{minute} {hour} * * {dow}", + prompt_template=( + "Load the live-dashboard skill and run the refresh tick for this " + "dashboard: {purpose}. Sources: {sources}. Re-read each field from " + "its named source, diff against the stored state, mark failed " + "reads stale without overwriting last-known-good values, " + "regenerate the HTML from the state file, and deliver a short " + "summary ONLY on material change or new needs-attention items — " + "otherwise respond with [SILENT]. On the first run, execute the " + "skill's setup phase first: pin the dashboard contract, verify " + "one live read per source, and write the dashboard state file " + "and HTML under ~/.hermes/dashboards/." + ), + slots=[ + BlueprintSlot( + name="purpose", type="text", label="What should it track?", + default="the status of an ongoing project or process", + help="one sentence — the entities and what you want to see " + "about them", + ), + BlueprintSlot( + name="sources", type="text", label="Where does the data live?", + default="my email threads about it and the relevant website", + help="email, websites, calendars, or files to read each refresh", + ), + _TIME("08:00"), + BlueprintSlot( + name="recurrence", type="weekdays", label="Refresh on", + default="everyday", + options=tuple(WEEKDAY_PRESETS.keys()), + ), + _DELIVER, + ], + skills=("live-dashboard",), + tags=("dashboard", "monitor", "status"), + ), AutomationBlueprint( key="competitor-watch", title="Competitor news watch", diff --git a/skills/productivity/live-dashboard/SKILL.md b/skills/productivity/live-dashboard/SKILL.md new file mode 100644 index 0000000000..a0801386f2 --- /dev/null +++ b/skills/productivity/live-dashboard/SKILL.md @@ -0,0 +1,89 @@ +--- +name: live-dashboard +description: "Build self-updating dashboards from live sources." +version: 0.1.0 +author: Hermes Agent +license: MIT +platforms: [linux, macos, windows] +metadata: + hermes: + tags: [Dashboards, Monitoring, Status, Automation, Reporting] + related_skills: [product-price-monitor, competitor-news-monitor, email-inbox-triage, google-workspace] +--- + +# Live Dashboard + +Turn one sentence — "make a dashboard for our visa applications, update it daily from the email threads and the case-status site" — into a persistent, self-refreshing status page. The user describes what they want to see; you define the data contract, build a self-contained HTML dashboard, verify one live refresh, then schedule the recurring tick. Inspired by Energy's (getenergy.com) natural-language live dashboards, adapted to Hermes's cron + connector architecture. + +Setup runs once in the foreground; the recurring refresh runs as a `cronjob` tick (the `live-dashboard` automation blueprint scaffolds this). + +## When to Use + +- "Make a dashboard for and keep it updated." +- "I want one place to see the status of ." +- "Track across my email and and show me where it stands." +- A cron tick fires for an existing dashboard (steps 5-7). + +Don't use for: one-off status questions (answer directly), price/availability thresholds on a single item (use `product-price-monitor`), or company news tracking (use `competitor-news-monitor`). + +## Procedure — Setup (foreground, once) + +### 1. Define the dashboard contract + +From the user's sentence, pin down: the dashboard's purpose in one line, the entities being tracked (rows), the fields per entity (columns/indicators), what "needs attention" means, the sources each field is read from, and the refresh cadence. Ask about anything ambiguous — a dashboard that tracks the wrong grain is worthless. Done when every field on the dashboard names the source it will be read from. + +### 2. Verify each source with one live read + +For each source, do one bounded foreground read now: email/calendar via the connector skills (`himalaya`, `google-workspace`), websites via `web_extract` or `browser_navigate`, local files via `read_file`. Record what was actually retrievable — auth walls, missing permissions, or empty results surface here, not on the first scheduled run. Drop or replace sources that fail. Done when every field's source returned real data or was explicitly renegotiated with the user. + +### 3. Build the dashboard artifact + +Write two files under `~/.hermes/dashboards//`: + +- `dashboard.json` — the contract plus current state: purpose, entities, per-field values, per-field source + retrieval timestamp, a `needs_attention` list, and a change log (append-only, most recent first). +- `index.html` — a single self-contained HTML page (inline CSS, no external requests) rendering the state: a header with purpose and last-updated time, a "Needs attention" section on top, the entity table, and the recent-changes list. Regenerate it from `dashboard.json` on every refresh; never hand-edit HTML state. + +Populate both from the step-2 reads and tell the user the file path (and open it where the platform allows). Done when the page renders the live data and every value on it carries a retrieval timestamp in `dashboard.json`. + +### 4. Schedule the refresh + +Only after step 3 succeeded, create the job: + +``` +cronjob(action="create", + schedule=, + prompt="Load the live-dashboard skill and run the refresh tick for the dashboard at ~/.hermes/dashboards//dashboard.json.", + deliver=) +``` + +Pick a cadence that respects source rate limits. Done when the job exists and its prompt names the state-file path. + +## Procedure — Tick (each scheduled run) + +### 5. Re-read sources and diff + +Load `dashboard.json`, re-read each field from its named source, and compute a field-level diff against the stored state. A failed source read means unknown state: keep the last good value, mark the field stale with the failure time, and never overwrite good data with an error. Done when every field is either updated, unchanged, or explicitly marked stale. + +### 6. Update state and re-render + +Apply the diff to `dashboard.json`: update values and timestamps, append material changes to the change log, and recompute `needs_attention` against the contract's attention rules. Regenerate `index.html` from the updated state. Done when the JSON and HTML agree and the change log entry for this run exists (or the run is recorded as no-change). + +### 7. Deliver on material change, else stay silent + +If the diff contains material changes or new needs-attention items, deliver a short summary: what changed, what needs attention, and the dashboard path. Otherwise respond with `[SILENT]` — no "still watching" noise unless the user asked for a periodic digest. Done when delivery matches the diff. + +## Pitfalls + +- Building the page before verifying the sources — auth failures then surface on an unattended run. +- Overwriting last-known-good values with an error page or empty read. +- Rendering state into HTML only — `dashboard.json` is the source of truth; HTML is a projection. +- Alerting on every refresh instead of on material change. +- Tracking the wrong grain (per-thread when the user thinks per-application). + +## Verification + +- [ ] Every dashboard field names its source, and each source passed one foreground read before scheduling. +- [ ] `dashboard.json` and `index.html` exist and agree; every value carries a retrieval timestamp. +- [ ] Failed reads marked fields stale without destroying last-known-good state. +- [ ] Ticks deliver only on material change; no-change runs were `[SILENT]`. +- [ ] The change log replays the dashboard's history from the state file alone. diff --git a/tests/skills/test_live_dashboard_skill.py b/tests/skills/test_live_dashboard_skill.py new file mode 100644 index 0000000000..1f2ee7b823 --- /dev/null +++ b/tests/skills/test_live_dashboard_skill.py @@ -0,0 +1,134 @@ +"""Tests for the live-dashboard skill and its live-dashboard blueprint. + +Inspired by Energy's (getenergy.com) natural-language live dashboards — +describe what you want to see in one sentence, get a persistent +self-refreshing status page fed by email/web/file sources. +""" +import re +from pathlib import Path + +import yaml + +SKILL_PATH = ( + Path(__file__).resolve().parents[2] + / "skills" + / "productivity" + / "live-dashboard" + / "SKILL.md" +) + + +def _frontmatter_and_body(): + content = SKILL_PATH.read_text(encoding="utf-8") + assert content.startswith("---") + m = re.search(r"\n---\s*\n", content[3:]) + assert m, "frontmatter must close with ---" + fm = yaml.safe_load(content[3 : m.start() + 3]) + body = content[m.end() + 3 :] + return fm, body + + +def test_skill_file_exists(): + assert SKILL_PATH.is_file() + + +def test_frontmatter_required_fields(): + fm, _ = _frontmatter_and_body() + for field in ("name", "description", "version", "author", "license", "platforms"): + assert field in fm, f"missing frontmatter field: {field}" + assert fm["name"] == "live-dashboard" + + +def test_description_hardline(): + fm, _ = _frontmatter_and_body() + desc = fm["description"] + assert len(desc) <= 60, f"description is {len(desc)} chars; hardline is 60" + assert desc.endswith(".") + + +def test_related_skills_resolve_in_repo(): + fm, _ = _frontmatter_and_body() + repo_root = SKILL_PATH.parents[3] + for name in fm["metadata"]["hermes"]["related_skills"]: + hits = ( + list(repo_root.glob(f"skills/*/{name}/SKILL.md")) + + list(repo_root.glob(f"optional-skills/*/{name}/SKILL.md")) + + list(repo_root.glob(f"skills/*/*/{name}/SKILL.md")) + ) + assert hits, f"related_skills entry does not resolve in-repo: {name}" + + +def test_setup_tick_split(): + """The skill must separate one-time setup from the recurring cron tick.""" + _, body = _frontmatter_and_body() + assert "Setup (foreground, once)" in body + assert "Tick (each scheduled run)" in body + assert "cronjob(action=" in body, "must wire scheduling through the cronjob tool" + + +def test_state_discipline_present(): + """State-file source of truth + stale-read handling must be explicit.""" + _, body = _frontmatter_and_body() + assert "dashboard.json" in body + assert "source of truth" in body + assert "last-known-good" in body or "last good value" in body + + +def test_source_verification_before_scheduling(): + _, body = _frontmatter_and_body() + assert "Only after step 3 succeeded" in body + assert "one bounded foreground read" in body + + +def test_silent_path_explicit(): + _, body = _frontmatter_and_body() + assert "[SILENT]" in body, "no-change ticks must stay silent" + + +def test_steps_have_completion_criteria(): + _, body = _frontmatter_and_body() + steps = re.findall(r"^### \d+\..*?(?=^### \d+\.|^## )", body, re.MULTILINE | re.DOTALL) + assert len(steps) >= 6 + for step in steps: + assert "Done when" in step, f"step missing completion criterion: {step[:60]!r}" + + +def test_html_is_projection_not_state(): + _, body = _frontmatter_and_body() + assert "never hand-edit HTML state" in body + assert "self-contained HTML" in body + + +def test_live_dashboard_blueprint_registered(): + from cron.blueprint_catalog import CATALOG + + bp = next((b for b in CATALOG if b.key == "live-dashboard"), None) + assert bp is not None, "live-dashboard blueprint missing from catalog" + assert "live-dashboard" in bp.skills, "blueprint must load the skill" + slot_names = {s.name for s in bp.slots} + assert {"purpose", "sources", "time", "recurrence", "deliver"} <= slot_names + assert "[SILENT]" in bp.prompt_template, "silent path must be explicit" + assert "{purpose}" in bp.prompt_template and "{sources}" in bp.prompt_template + + +def test_live_dashboard_blueprint_fills(): + """fill_blueprint must produce a valid cron job kwargs dict.""" + from cron.blueprint_catalog import CATALOG, fill_blueprint + + bp = next(b for b in CATALOG if b.key == "live-dashboard") + job = fill_blueprint( + bp, + { + "purpose": "team visa applications", + "sources": "email threads and the case-status site", + "time": "07:30", + "recurrence": "weekdays", + "deliver": "origin", + }, + ) + assert "team visa applications" in job["prompt"] + assert "email threads and the case-status site" in job["prompt"] + fields = job["schedule"].split() + assert len(fields) == 5, f"invalid cron expr: {job['schedule']}" + assert fields[0] == "30" and fields[1] == "7" + assert fields[4] == "1-5" diff --git a/website/docs/user-guide/skills/bundled/productivity/productivity-live-dashboard.md b/website/docs/user-guide/skills/bundled/productivity/productivity-live-dashboard.md new file mode 100644 index 0000000000..bf1cb40aa9 --- /dev/null +++ b/website/docs/user-guide/skills/bundled/productivity/productivity-live-dashboard.md @@ -0,0 +1,107 @@ +--- +title: "Live Dashboard — Build self-updating dashboards from live sources" +sidebar_label: "Live Dashboard" +description: "Build self-updating dashboards from live sources" +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Live Dashboard + +Build self-updating dashboards from live sources. + +## Skill metadata + +| | | +|---|---| +| Source | Bundled (installed by default) | +| Path | `skills/productivity/live-dashboard` | +| Version | `0.1.0` | +| Author | Hermes Agent | +| License | MIT | +| Platforms | linux, macos, windows | +| Tags | `Dashboards`, `Monitoring`, `Status`, `Automation`, `Reporting` | +| Related skills | [`product-price-monitor`](/docs/user-guide/skills/bundled/productivity/productivity-product-price-monitor), [`competitor-news-monitor`](/docs/user-guide/skills/bundled/research/research-competitor-news-monitor), [`email-inbox-triage`](/docs/user-guide/skills/bundled/email/email-email-inbox-triage), [`google-workspace`](/docs/user-guide/skills/bundled/productivity/productivity-google-workspace) | + +## Reference: full SKILL.md + +:::info +The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. +::: + +# Live Dashboard + +Turn one sentence — "make a dashboard for our visa applications, update it daily from the email threads and the case-status site" — into a persistent, self-refreshing status page. The user describes what they want to see; you define the data contract, build a self-contained HTML dashboard, verify one live refresh, then schedule the recurring tick. Inspired by Energy's (getenergy.com) natural-language live dashboards, adapted to Hermes's cron + connector architecture. + +Setup runs once in the foreground; the recurring refresh runs as a `cronjob` tick (the `live-dashboard` automation blueprint scaffolds this). + +## When to Use + +- "Make a dashboard for <project/process> and keep it updated." +- "I want one place to see the status of <deals / applications / bugs / shipments>." +- "Track <thing> across my email and <website> and show me where it stands." +- A cron tick fires for an existing dashboard (steps 5-7). + +Don't use for: one-off status questions (answer directly), price/availability thresholds on a single item (use `product-price-monitor`), or company news tracking (use `competitor-news-monitor`). + +## Procedure — Setup (foreground, once) + +### 1. Define the dashboard contract + +From the user's sentence, pin down: the dashboard's purpose in one line, the entities being tracked (rows), the fields per entity (columns/indicators), what "needs attention" means, the sources each field is read from, and the refresh cadence. Ask about anything ambiguous — a dashboard that tracks the wrong grain is worthless. Done when every field on the dashboard names the source it will be read from. + +### 2. Verify each source with one live read + +For each source, do one bounded foreground read now: email/calendar via the connector skills (`himalaya`, `google-workspace`), websites via `web_extract` or `browser_navigate`, local files via `read_file`. Record what was actually retrievable — auth walls, missing permissions, or empty results surface here, not on the first scheduled run. Drop or replace sources that fail. Done when every field's source returned real data or was explicitly renegotiated with the user. + +### 3. Build the dashboard artifact + +Write two files under `~/.hermes/dashboards//`: + +- `dashboard.json` — the contract plus current state: purpose, entities, per-field values, per-field source + retrieval timestamp, a `needs_attention` list, and a change log (append-only, most recent first). +- `index.html` — a single self-contained HTML page (inline CSS, no external requests) rendering the state: a header with purpose and last-updated time, a "Needs attention" section on top, the entity table, and the recent-changes list. Regenerate it from `dashboard.json` on every refresh; never hand-edit HTML state. + +Populate both from the step-2 reads and tell the user the file path (and open it where the platform allows). Done when the page renders the live data and every value on it carries a retrieval timestamp in `dashboard.json`. + +### 4. Schedule the refresh + +Only after step 3 succeeded, create the job: + +``` +cronjob(action="create", + schedule=, + prompt="Load the live-dashboard skill and run the refresh tick for the dashboard at ~/.hermes/dashboards//dashboard.json.", + deliver=) +``` + +Pick a cadence that respects source rate limits. Done when the job exists and its prompt names the state-file path. + +## Procedure — Tick (each scheduled run) + +### 5. Re-read sources and diff + +Load `dashboard.json`, re-read each field from its named source, and compute a field-level diff against the stored state. A failed source read means unknown state: keep the last good value, mark the field stale with the failure time, and never overwrite good data with an error. Done when every field is either updated, unchanged, or explicitly marked stale. + +### 6. Update state and re-render + +Apply the diff to `dashboard.json`: update values and timestamps, append material changes to the change log, and recompute `needs_attention` against the contract's attention rules. Regenerate `index.html` from the updated state. Done when the JSON and HTML agree and the change log entry for this run exists (or the run is recorded as no-change). + +### 7. Deliver on material change, else stay silent + +If the diff contains material changes or new needs-attention items, deliver a short summary: what changed, what needs attention, and the dashboard path. Otherwise respond with `[SILENT]` — no "still watching" noise unless the user asked for a periodic digest. Done when delivery matches the diff. + +## Pitfalls + +- Building the page before verifying the sources — auth failures then surface on an unattended run. +- Overwriting last-known-good values with an error page or empty read. +- Rendering state into HTML only — `dashboard.json` is the source of truth; HTML is a projection. +- Alerting on every refresh instead of on material change. +- Tracking the wrong grain (per-thread when the user thinks per-application). + +## Verification + +- [ ] Every dashboard field names its source, and each source passed one foreground read before scheduling. +- [ ] `dashboard.json` and `index.html` exist and agree; every value carries a retrieval timestamp. +- [ ] Failed reads marked fields stale without destroying last-known-good state. +- [ ] Ticks deliver only on material change; no-change runs were `[SILENT]`. +- [ ] The change log replays the dashboard's history from the state file alone. diff --git a/website/sidebars.ts b/website/sidebars.ts index 02eb603eea..7c60d1e65e 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -247,6 +247,7 @@ const sidebars: SidebarsConfig = { 'user-guide/skills/bundled/productivity/productivity-document-to-action-items', 'user-guide/skills/bundled/productivity/productivity-docx', 'user-guide/skills/bundled/productivity/productivity-google-workspace', + 'user-guide/skills/bundled/productivity/productivity-live-dashboard', 'user-guide/skills/bundled/productivity/productivity-maps', 'user-guide/skills/bundled/productivity/productivity-meeting-action-items', 'user-guide/skills/bundled/productivity/productivity-notion',