@floless/app 0.5.1
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/bin/floless.mjs +13 -0
- package/dist/floless-server.cjs +56801 -0
- package/dist/skills/floless-app-bridge/SKILL.md +80 -0
- package/dist/skills/floless-app-routines/SKILL.md +168 -0
- package/dist/skills/floless-app-routines/references/routines-api.md +130 -0
- package/dist/skills/floless-app-workflows/SKILL.md +352 -0
- package/dist/skills/floless-app-workflows/references/dev-server-and-run-trace.md +119 -0
- package/dist/skills/floless-app-workflows/references/exec-contract.md +104 -0
- package/dist/web/app.css +2129 -0
- package/dist/web/app.js +1334 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/aware.js +3274 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +98 -0
- package/dist/web/index.html +484 -0
- package/launch.mjs +543 -0
- package/package.json +43 -0
- package/teardown.mjs +128 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floless-app-bridge
|
|
3
|
+
description: This skill should be used when the user is driving the floless.app web UI (the local prompt-native front door at http://127.0.0.1:4317) and wants the terminal AI to act on what they did there — picking up queued Tweak requests, "use this template" requests, or the context of a node they selected/edited on the canvas. Use it when the user says things like "apply the tweak I just queued", "I clicked Tweak on the bom node — make that change", "pick up my floless request", "what's pending in floless", or after they double-click/Tweak a node in the UI. It reads the floless.app local API, applies the change to the app's .flo, recompiles, and clears the request.
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# floless.app ↔ terminal bridge
|
|
9
|
+
|
|
10
|
+
floless.app is the **thin web UI**; the **terminal AI (you) is the brain**. The UI cannot
|
|
11
|
+
edit `.flo` files or push into this terminal — instead it **queues requests** to a local API,
|
|
12
|
+
and you **pull** them here, do the real work (edit the `.flo`, recompile), and clear them. This
|
|
13
|
+
skill is that pull side. Pair it with the **`floless-app-workflows`** skill, which owns
|
|
14
|
+
the actual `.flo` authoring / install → compile → run loop.
|
|
15
|
+
|
|
16
|
+
## The local API (http://127.0.0.1:4317)
|
|
17
|
+
|
|
18
|
+
The floless.app server runs locally on a **fixed port 4317** (override: `$PORT`). Confirm it's up
|
|
19
|
+
first: `curl -s http://127.0.0.1:4317/api/health` → `{"ok":true,"awareVersion":"…"}`. If it's not
|
|
20
|
+
running, the user starts it from the repo: `cd server && npm run dev` (or `npm run app`).
|
|
21
|
+
|
|
22
|
+
| Call | Purpose |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `GET /api/requests` | List pending UI requests → `{ ok, requests: [...] }` |
|
|
25
|
+
| `DELETE /api/requests/:id` | **Clear** a request once you've handled it |
|
|
26
|
+
| `GET /api/app/:id` | The app's normalized topology + source + lock (selected-node context) |
|
|
27
|
+
| `POST /api/tweak` | (UI uses this to enqueue; you usually only read/clear) |
|
|
28
|
+
|
|
29
|
+
### Request shapes (`/api/requests`)
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
// A node Tweak (the ✎ Tweak button): change ONE node in plain English.
|
|
33
|
+
{ "id": "uuid", "type": "tweak", "status": "pending",
|
|
34
|
+
"appId": "tekla-bom-by-phase", "nodeId": "bom",
|
|
35
|
+
"instruction": "also group by ASSEMBLY_POS and add a per-assembly subtotal" }
|
|
36
|
+
|
|
37
|
+
// A "use this template" request (a favorite dragged in / "Use in this app").
|
|
38
|
+
{ "id": "uuid", "type": "use-template", "status": "pending",
|
|
39
|
+
"appId": "tekla-bom-by-phase", "templateId": "uuid",
|
|
40
|
+
"template": { "name": "…", "node": { "agent": "tekla", "command": "…", "config": {…} } } }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The **selected-node context** you need is carried *in the request* (`appId` + `nodeId`), so you do
|
|
44
|
+
not need the UI's live selection — read that node's current source from the `.flo` to understand
|
|
45
|
+
what to change.
|
|
46
|
+
|
|
47
|
+
## Workflow — pull, apply, clear
|
|
48
|
+
|
|
49
|
+
```dot
|
|
50
|
+
digraph { "GET /api/requests" -> "for each pending" -> "read app .flo + node" ->
|
|
51
|
+
"apply change (edit .flo)" -> "recompile + reinstall" -> "DELETE /api/requests/:id" }
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
1. **Pull**: `curl -s http://127.0.0.1:4317/api/requests` → take the `pending` ones (oldest first).
|
|
55
|
+
Summarize them for the user before acting on anything destructive.
|
|
56
|
+
2. **Locate the app source.** The editable `.flo` is the repo copy `demos/<appId>/<appId>.flo`
|
|
57
|
+
(preferred — it's version-controlled); the installed copy is `~/.aware/apps/<appId>/<appId>.flo`.
|
|
58
|
+
Edit the **repo** copy. `GET /api/app/<appId>` returns the parsed nodes + each node's
|
|
59
|
+
`config` (incl. the exec `code`) if you want the resolved view.
|
|
60
|
+
3. **Apply the change** to the target `nodeId`:
|
|
61
|
+
- `type: tweak` → edit that node's `config.code` (exec C#) or `config.args` per the plain-English
|
|
62
|
+
`instruction`. Keep the node's contract (id, mode, edges) intact unless the instruction says
|
|
63
|
+
otherwise.
|
|
64
|
+
- `type: use-template` → add `template.node` as a new node and wire it in (see
|
|
65
|
+
`floless-app-workflows` for node/connection rules).
|
|
66
|
+
4. **Recompile** so the UI's Run gate re-arms (it's gated on the lock's source-hash):
|
|
67
|
+
reinstall from the dir + `aware app compile <appId>` from `~/.aware/apps` (full loop +
|
|
68
|
+
gotchas live in `floless-app-workflows`).
|
|
69
|
+
5. **Clear the request** so it leaves the UI's queue:
|
|
70
|
+
`curl -s -X DELETE http://127.0.0.1:4317/api/requests/<id>`. The UI's "requests" badge updates
|
|
71
|
+
live over SSE.
|
|
72
|
+
6. **Verify** with a real run (the `verify`/Playwright standing rule) before reporting done.
|
|
73
|
+
|
|
74
|
+
## Guardrails
|
|
75
|
+
|
|
76
|
+
- **You are the brain, the UI is not.** Never ask the UI to compose or mutate the `.flo`; it only
|
|
77
|
+
queues intent. All authoring happens here, through the `.flo` + `aware` CLI.
|
|
78
|
+
- Edit the **repo** `demos/<appId>/` source (tracked), then recompile into `~/.aware/apps/`.
|
|
79
|
+
- Always **clear** a request after handling it, so the user's "requests" count reflects reality.
|
|
80
|
+
- Don't invent node/command names — verify against the app's existing nodes (`GET /api/app/:id`).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: floless-app-routines
|
|
3
|
+
description: This skill should be used when the user wants to set up a floless.app workflow (.flo app) to run automatically — "routines" — either on a timer (schedule) or on a live event (trigger). Triggers on requests like "schedule tekla-bom-by-phase every weekday at 7am", "run this workflow every 2 hours", "run my BOM whenever the Tekla model changes", "watch the model and run X on each change", "set up a trigger routine", "list my routines", "disable the morning routine", "delete that routine". Covers the floless.app routines REST API (create / list / edit / enable-disable / delete / run-now), the schedule presets, the event-trigger kind, the confirm-first rule, and the runnable gate.
|
|
4
|
+
metadata:
|
|
5
|
+
version: 0.1.0
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Setting up floless.app routines (automatic .flo runs)
|
|
9
|
+
|
|
10
|
+
A **routine** runs an installed AWARE workflow (`.flo` app) automatically — the hands-off
|
|
11
|
+
equivalent of clicking ▶ Run workflow in floless.app. There are two **kinds**, both authored
|
|
12
|
+
through the same **`/api/routines`** REST surface:
|
|
13
|
+
|
|
14
|
+
- **`schedule`** — a **time-trigger**: the floless.app server's internal scheduler fires the
|
|
15
|
+
workflow on a clock (every weekday at 7am, every 2 hours, …).
|
|
16
|
+
- **`trigger`** — an **event-trigger**: a workflow whose source node is a *streaming*
|
|
17
|
+
`lifecycle:start` command (e.g. `tekla.watch`) runs once per emitted event. The server hosts a
|
|
18
|
+
long-lived `aware app run`; each change to the watched host (e.g. the Tekla model) fires the
|
|
19
|
+
chain. Only workflows that declare such a source are eligible.
|
|
20
|
+
|
|
21
|
+
The floless.app server is a thin localhost host on **port 4317**. Both kinds serialize host work
|
|
22
|
+
through the same run lock, and the server records/streams state. This skill drives that server so
|
|
23
|
+
the terminal AI can author routines on the user's behalf.
|
|
24
|
+
|
|
25
|
+
floless.app owns no engine: a routine is just a time- or event-trigger of `aware app run`. This
|
|
26
|
+
skill never composes a workflow — it sets up automation for one that already exists.
|
|
27
|
+
|
|
28
|
+
## When to use
|
|
29
|
+
|
|
30
|
+
Use this skill when the user asks to **schedule / time / automate / trigger / watch-and-run** an
|
|
31
|
+
existing floless.app workflow, or to manage existing routines (list, edit, enable/disable, delete,
|
|
32
|
+
run now). Do **not** use it to build or edit a workflow itself — that is
|
|
33
|
+
`floless-app-workflows`.
|
|
34
|
+
|
|
35
|
+
**Which kind?** If the user names a clock ("at 7am", "every 2 hours", "daily") → **schedule**. If
|
|
36
|
+
they describe an event ("whenever the model changes", "on each change", "watch X and run") →
|
|
37
|
+
**trigger** — but a trigger only works if the workflow is trigger-eligible (see the trigger
|
|
38
|
+
workflow below); if it isn't, say so and offer a schedule instead. When unsure, ask.
|
|
39
|
+
|
|
40
|
+
## The base URL + preflight
|
|
41
|
+
|
|
42
|
+
The server is at **`http://127.0.0.1:4317`** (the fixed floless.app port). Call it from the
|
|
43
|
+
terminal with `curl` (a non-browser caller sends no `Origin`, which the server's cross-origin
|
|
44
|
+
guard allows).
|
|
45
|
+
|
|
46
|
+
Before any routine call, confirm the server is up and licensed:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -s http://127.0.0.1:4317/api/health
|
|
50
|
+
# → {"ok":true,"license":"valid",...}. If it doesn't respond, the floless.app desktop app
|
|
51
|
+
# isn't running — ask the user to start it. If license != valid/offline-grace, /api/routines
|
|
52
|
+
# returns 402; the user must sign in (the app surfaces a sign-in gate).
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## The SCHEDULE workflow (always, in order)
|
|
56
|
+
|
|
57
|
+
1. **Resolve + verify the workflow.** List installed apps and confirm the target exists:
|
|
58
|
+
`curl -s http://127.0.0.1:4317/api/apps`. Then confirm it is **runnable** (compiled, fresh
|
|
59
|
+
lock) — only runnable apps fire cleanly:
|
|
60
|
+
`curl -s http://127.0.0.1:4317/api/app/<id>` → check `app.runnable === true`. If it is not
|
|
61
|
+
runnable, tell the user it needs Compile first (don't schedule a broken app — the scheduler
|
|
62
|
+
would just record `skipped`).
|
|
63
|
+
2. **Map the request to a structured schedule — never invent one.** The user says
|
|
64
|
+
"every weekday at 7am"; translate it to a `ScheduleSpec` (see below). If anything is ambiguous
|
|
65
|
+
(which days? what time? what inputs?), **ask before creating it.** Echo the parsed schedule and
|
|
66
|
+
inputs back in plain English ("Weekdays at 07:00, phase 3 — create this?") and wait for a yes.
|
|
67
|
+
3. **Coerce inputs against the app's declared inputs.** Read `app.inputs` from
|
|
68
|
+
`/api/app/<id>`; pass only declared inputs, with the values the user gave (or omit one to use
|
|
69
|
+
the app's default). The server coerces values + drops unknown keys, but pass clean values.
|
|
70
|
+
4. **Create it.** `POST /api/routines` with `{ name, workflow, schedule, inputs?, enabled? }` (see
|
|
71
|
+
the API reference). On success the routine appears live in the floless.app Routines panel (the
|
|
72
|
+
server broadcasts over SSE).
|
|
73
|
+
5. **Confirm back to the user** what was scheduled and when it next fires (`routine.nextFireAt`).
|
|
74
|
+
|
|
75
|
+
## The TRIGGER workflow (event-driven — always, in order)
|
|
76
|
+
|
|
77
|
+
1. **Resolve + verify the workflow is trigger-eligible.** `curl -s http://127.0.0.1:4317/api/app/<id>`
|
|
78
|
+
and check **`app.triggerSource`** — it must be non-null (`{ nodeId, agent, command, inputs }`).
|
|
79
|
+
Null means the workflow has no streaming `lifecycle:start` source and **cannot** be a trigger
|
|
80
|
+
routine — tell the user, and offer a schedule instead. Also confirm `app.runnable === true`
|
|
81
|
+
(a not-runnable app starts `blocked` — needs Compile).
|
|
82
|
+
2. **Confirm — never invent.** Echo back, in plain English, what will be watched and what runs
|
|
83
|
+
("Run `tekla-bom-by-phase` whenever the Tekla model changes (tekla/watch) — set this up?") and
|
|
84
|
+
wait for a yes. **Source params are not settable in v1** (the watch's `filter` etc. live in the
|
|
85
|
+
workflow's node config, not as routine inputs) — don't promise to set them; if the user needs a
|
|
86
|
+
different filter, that's a workflow edit (`floless-app-workflows`), not a routine knob.
|
|
87
|
+
3. **Coerce inputs** the same way as a schedule (only the app's declared top-level `inputs`). Many
|
|
88
|
+
watch apps declare none — then omit `inputs`.
|
|
89
|
+
4. **Create it.** `POST /api/routines` with **`{ kind:"trigger", name, workflow, inputs?, enabled? }`**
|
|
90
|
+
— **no `schedule`**. The server validates eligibility (re-checks `app.triggerSource`), records the
|
|
91
|
+
source binding, and — if `enabled` — starts the live watcher session immediately.
|
|
92
|
+
5. **Confirm back to the user.** A trigger routine has **no `nextFireAt`**; instead report its live
|
|
93
|
+
state from `routine.session` (`{ state, firedCount, lastEvent, error }`) — e.g. "Listening now;
|
|
94
|
+
it'll run on each change." Enable/disable is the control; **run-now does not apply** (the API
|
|
95
|
+
rejects it — see gotchas).
|
|
96
|
+
|
|
97
|
+
## Schedule presets (structured, NOT cron)
|
|
98
|
+
|
|
99
|
+
Pick exactly one shape. Times are **local machine time**, `HH:MM` 24-hour. The finest granularity
|
|
100
|
+
is **hourly** by design (no sub-hour schedules).
|
|
101
|
+
|
|
102
|
+
| User intent | ScheduleSpec |
|
|
103
|
+
|---|---|
|
|
104
|
+
| "every 2 hours" | `{"kind":"hourly","everyHours":2}` (must divide 24: 1, 2, 3, 4, 6, 8, 12, 24) |
|
|
105
|
+
| "every day at 7am" | `{"kind":"daily","time":"07:00"}` |
|
|
106
|
+
| "every weekday at 7am" | `{"kind":"weekdays","time":"07:00"}` (Mon–Fri) |
|
|
107
|
+
| "Mondays and Thursdays at 7am" | `{"kind":"weekly","days":["mon","thu"],"time":"07:00"}` |
|
|
108
|
+
| anything a preset can't express | `{"kind":"cron","expr":"0 4 * * 1"}` (5-field; local time) |
|
|
109
|
+
|
|
110
|
+
Weekday tokens: `sun mon tue wed thu fri sat`.
|
|
111
|
+
|
|
112
|
+
**Cron** is the "Custom" escape hatch — a standard 5-field expression
|
|
113
|
+
(`min hour day-of-month month day-of-week`, e.g. `*/15 9-17 * * 1-5` = every 15 min,
|
|
114
|
+
9am–5pm, weekdays). Prefer a preset when one fits (more readable); reach for cron only
|
|
115
|
+
when none does. Granularity is bounded by the ~1-minute scheduler tick.
|
|
116
|
+
|
|
117
|
+
## Quick examples
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Create a SCHEDULE: BOM every weekday at 07:00, phase 3
|
|
121
|
+
curl -s -X POST http://127.0.0.1:4317/api/routines -H 'content-type: application/json' -d '{
|
|
122
|
+
"name":"Morning BOM export","workflow":"tekla-bom-by-phase",
|
|
123
|
+
"schedule":{"kind":"weekdays","time":"07:00"},"inputs":{"phase":3}}'
|
|
124
|
+
|
|
125
|
+
# Create a TRIGGER: run a watch workflow on every model change (no schedule)
|
|
126
|
+
curl -s -X POST http://127.0.0.1:4317/api/routines -H 'content-type: application/json' -d '{
|
|
127
|
+
"kind":"trigger","name":"Watch model","workflow":"tekla-watch-smoke","enabled":true}'
|
|
128
|
+
|
|
129
|
+
# List (trigger routines carry a live `session`; schedule routines carry `nextFireAt`)
|
|
130
|
+
curl -s http://127.0.0.1:4317/api/routines
|
|
131
|
+
|
|
132
|
+
# Disable (or re-enable) — for a TRIGGER this STOPS / STARTS the live watcher
|
|
133
|
+
curl -s -X PATCH http://127.0.0.1:4317/api/routines/<id> -H 'content-type: application/json' -d '{"enabled":false}'
|
|
134
|
+
|
|
135
|
+
# Change a schedule's timing — PATCH only the fields that change (schedule kind only)
|
|
136
|
+
curl -s -X PATCH http://127.0.0.1:4317/api/routines/<id> -H 'content-type: application/json' -d '{"schedule":{"kind":"daily","time":"06:30"}}'
|
|
137
|
+
|
|
138
|
+
# Run now — SCHEDULE kind only (a trigger 400s: its control is enable/disable)
|
|
139
|
+
curl -s -X POST http://127.0.0.1:4317/api/routines/<id>/run
|
|
140
|
+
|
|
141
|
+
# Delete (a trigger's live watcher is stopped first)
|
|
142
|
+
curl -s -X DELETE http://127.0.0.1:4317/api/routines/<id>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Rules + gotchas
|
|
146
|
+
|
|
147
|
+
- **Confirm before create.** Never invent a schedule, a trigger, or inputs; echo the parse and get
|
|
148
|
+
a yes. For a trigger, confirm the workflow is eligible (`app.triggerSource != null`) first.
|
|
149
|
+
- **Cap is 15** routines per install (both kinds share the cap). The 16th `POST` returns **409**
|
|
150
|
+
with a clear message — relay it and offer to delete one.
|
|
151
|
+
- **Workflow is fixed after create** (both kinds). `PATCH` cannot change a routine's workflow or its
|
|
152
|
+
`kind`. To switch workflows or convert schedule↔trigger, delete and recreate.
|
|
153
|
+
- **Run-now is schedule-only.** `POST /api/routines/:id/run` on a trigger returns **400**
|
|
154
|
+
("a trigger routine runs on its event — enable/disable it instead"). Don't offer run-now for a
|
|
155
|
+
trigger; the control is the enable/disable toggle (start/stop the live watcher).
|
|
156
|
+
- **The server runs it, not Claude.** Once created, floless.app runs the routine locally — the
|
|
157
|
+
scheduler fires a schedule on its timer; a trigger's long-lived watcher runs as long as it's
|
|
158
|
+
enabled and the desktop app is up. A host-unavailable schedule fire records an honest `failed`
|
|
159
|
+
run; a trigger that can't start (uncompiled/drift, or a host-exclusivity conflict) reports
|
|
160
|
+
`session.state:"blocked"` with a reason. No AI interpretation of results in Phase 1.
|
|
161
|
+
- **Host exclusivity (trigger).** A host-backed watcher (e.g. Tekla) and a manual/scheduled host run
|
|
162
|
+
are mutually exclusive — one live model, one consumer. Starting one while the other is live is
|
|
163
|
+
refused with a clear reason (surfaced as `blocked`). Generic/cloud sources are unconstrained.
|
|
164
|
+
- **One seam, no drift.** This skill and the in-app Routines panel both write through
|
|
165
|
+
`/api/routines`, so validation, eligibility, and the cap apply identically.
|
|
166
|
+
|
|
167
|
+
See `references/routines-api.md` for the full request/response shapes, both `Routine` kinds, the
|
|
168
|
+
trigger `session` snapshot, run records, and error statuses.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# floless.app routines REST API
|
|
2
|
+
|
|
3
|
+
Base URL: `http://127.0.0.1:4317`. All bodies are JSON (`content-type: application/json`).
|
|
4
|
+
Every `/api/*` route requires a valid subscription (else `402`); the floless.app desktop app holds
|
|
5
|
+
the token. A non-browser caller (curl) sends no `Origin`, so the cross-origin guard allows it.
|
|
6
|
+
|
|
7
|
+
## The `Routine` object
|
|
8
|
+
|
|
9
|
+
A routine is one of two **kinds**. `schedule` carries a `schedule` + `nextFireAt`; `trigger` carries
|
|
10
|
+
a `trigger` binding and, in `GET /api/routines`, a live `session` snapshot (no `schedule`/`nextFireAt`).
|
|
11
|
+
|
|
12
|
+
```jsonc
|
|
13
|
+
// kind: "schedule" — a time-trigger
|
|
14
|
+
{
|
|
15
|
+
"id": "morning-bom-export", // slug, generated from the name (unique)
|
|
16
|
+
"name": "Morning BOM export",
|
|
17
|
+
"kind": "schedule", // "schedule" | "trigger" (defaults to schedule if omitted)
|
|
18
|
+
"workflow": "tekla-bom-by-phase", // installed app id (immutable after create)
|
|
19
|
+
"inputs": { "phase": 3 }, // coerced to the app's declared input types
|
|
20
|
+
"schedule": { "kind": "weekdays", "time": "07:00" },
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"created": "2026-05-28T14:54:03.859Z",
|
|
23
|
+
"updated": "2026-05-28T14:54:03.859Z",
|
|
24
|
+
"nextFireAt": "2026-05-29T05:00:00.000Z", // computed (UTC ISO); null when disabled/broken/trigger
|
|
25
|
+
"lastRun": { "at": "...", "status": "ok", "runId": "...", "durationMs": 42551 },
|
|
26
|
+
"history": [ /* most-recent 20 RunRecords */ ],
|
|
27
|
+
"broken": { "reason": "workflow not installed" }, // present only when the app is gone
|
|
28
|
+
"onResult": null // PHASE-2 reserved (AI handoff) — ignore in Phase 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// kind: "trigger" — an event-trigger (streaming lifecycle:start source)
|
|
32
|
+
{
|
|
33
|
+
"id": "watch-model",
|
|
34
|
+
"name": "Watch model",
|
|
35
|
+
"kind": "trigger",
|
|
36
|
+
"workflow": "tekla-watch-smoke",
|
|
37
|
+
"inputs": {}, // app's declared top-level inputs only (often none)
|
|
38
|
+
"trigger": { "nodeId": "watch", "agent": "tekla", "command": "watch" }, // the streaming source
|
|
39
|
+
"enabled": true,
|
|
40
|
+
"created": "…", "updated": "…",
|
|
41
|
+
"nextFireAt": null, // always null for triggers (no clock)
|
|
42
|
+
"session": { // LIVE state — present in GET /api/routines only
|
|
43
|
+
"state": "listening", // "listening" | "blocked" | "error" | "stopped"
|
|
44
|
+
"firedCount": 5, // events seen this session
|
|
45
|
+
"lastEvent": { "at": "ISO", "summary": "Beam modified · B/4" }, // newest fire, or null
|
|
46
|
+
"error": null // stderr tail when state==="error"/"blocked", else null
|
|
47
|
+
},
|
|
48
|
+
"lastRun": null, "history": [],
|
|
49
|
+
"broken": { "reason": "workflow not installed" }, // app gone / no longer trigger-eligible
|
|
50
|
+
"onResult": null
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Trigger `session` states (live, server-pushed)
|
|
55
|
+
|
|
56
|
+
- **`listening`** — the watcher process is up and watching; the healthy steady state. Each event
|
|
57
|
+
increments `firedCount` and updates `lastEvent`.
|
|
58
|
+
- **`blocked`** — couldn't start: app not runnable (needs Compile / drift), or a host-exclusivity
|
|
59
|
+
refusal (a Tekla watcher and a manual/scheduled Tekla run can't both be live). `error` carries why.
|
|
60
|
+
- **`error`** — the watcher exited non-zero; `error` carries the stderr tail. Re-enable to retry
|
|
61
|
+
(no auto-restart, to avoid flapping).
|
|
62
|
+
- **`stopped`** — not running (disabled, or a finite stream ended). Enable to (re)start.
|
|
63
|
+
|
|
64
|
+
### `ScheduleSpec` (structured presets, local machine time)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
{ kind: "hourly", everyHours: number } // must evenly divide 24: 1,2,3,4,6,8,12,24
|
|
68
|
+
{ kind: "daily", time: "HH:MM" }
|
|
69
|
+
{ kind: "weekdays", time: "HH:MM" } // Mon–Fri
|
|
70
|
+
{ kind: "weekly", days: Weekday[], time: "HH:MM" } // Weekday = sun|mon|tue|wed|thu|fri|sat
|
|
71
|
+
{ kind: "cron", expr: string } // standard 5-field: min hour dom month dow
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`cron` is the "Custom" power-user variant (e.g. `"0 4 * * 1"` = Mondays 04:00,
|
|
75
|
+
`"*/15 9-17 * * 1-5"` = every 15 min 9–5 on weekdays). Local time; granularity is
|
|
76
|
+
bounded by the ~1-minute scheduler tick. Supports `* , - /` and lists.
|
|
77
|
+
|
|
78
|
+
### `RunRecord`
|
|
79
|
+
|
|
80
|
+
```jsonc
|
|
81
|
+
{ "at": "ISO", "status": "ok" | "failed" | "skipped", "reason": "…?", "runId": "…?", "durationMs": 0 }
|
|
82
|
+
```
|
|
83
|
+
- `ok` — the run completed and no node reported failure.
|
|
84
|
+
- `failed` — `aware app run` errored, or a node returned `{ ok:false }` (e.g. "No live Tekla model").
|
|
85
|
+
- `skipped` — the app was not runnable at fire time (needs Compile / drift).
|
|
86
|
+
|
|
87
|
+
## Endpoints
|
|
88
|
+
|
|
89
|
+
| Method | Path | Body | Result |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| GET | `/api/routines` | — | `{ ok, routines: Routine[], max: 15 }` (trigger routines include a live `session`) |
|
|
92
|
+
| POST | `/api/routines` | schedule: `{ name, workflow, schedule, inputs?, enabled? }` · trigger: `{ kind:"trigger", name, workflow, inputs?, enabled? }` | `201 { ok, routine }` · `400` invalid/ineligible · `409` at cap |
|
|
93
|
+
| PATCH | `/api/routines/:id` | any of `{ name, schedule, inputs, enabled }` (a trigger ignores `schedule`; `enabled` start/stops its watcher) | `{ ok, routine }` · `404` · `400` |
|
|
94
|
+
| DELETE | `/api/routines/:id` | — | `{ ok }` · `404` (stops the watcher first for a trigger) |
|
|
95
|
+
| POST | `/api/routines/:id/run` | — | `{ ok }` (schedule: enqueues a real run now) · `400` for a **trigger** · `404` |
|
|
96
|
+
|
|
97
|
+
Supporting reads (same server):
|
|
98
|
+
- `GET /api/health` → `{ ok, license, ... }` — preflight.
|
|
99
|
+
- `GET /api/apps` → `{ ok, apps: [{ id, nodes, layout, provider }] }` — does the workflow exist?
|
|
100
|
+
- `GET /api/app/:id` → `{ ok, app }` — `app.runnable` (compiled+fresh) and `app.inputs`
|
|
101
|
+
(`[{ name, type, default, description }]`) to seed/validate routine inputs, **plus
|
|
102
|
+
`app.triggerSource`** (`{ nodeId, agent, command, inputs } | null`) — non-null = trigger-eligible.
|
|
103
|
+
|
|
104
|
+
## Error statuses
|
|
105
|
+
|
|
106
|
+
- `400` — validation: missing name, unknown/invalid workflow id, malformed schedule
|
|
107
|
+
(bad `HH:MM`, empty weekly `days`, `everyHours` not a divisor of 24); a `kind:"trigger"` create
|
|
108
|
+
on a workflow that is **not trigger-eligible** (`{ "error": "workflow is not trigger-eligible …" }`);
|
|
109
|
+
or `POST /:id/run` on a **trigger** (`{ "error": "a trigger routine runs on its event — enable/disable it instead" }`).
|
|
110
|
+
- `402` — no valid subscription (the app must be signed in).
|
|
111
|
+
- `404` — no routine with that id.
|
|
112
|
+
- `409` — `{ "error": "Routine limit reached (15). Delete one first." }` — the cap (both kinds share
|
|
113
|
+
it). Relay it and offer to delete an existing routine.
|
|
114
|
+
|
|
115
|
+
## Notes on behavior (so expectations are right)
|
|
116
|
+
|
|
117
|
+
- **Local time, DST-safe.** Schedules are wall-clock local; 07:00 stays 07:00 across a DST change.
|
|
118
|
+
- **Missed fires skip forward.** If the server was down at fire time, the next fire is the next
|
|
119
|
+
future occurrence (no catch-up burst on boot).
|
|
120
|
+
- **Serialized.** Scheduled runs share the single active-run lock with manual ▶ Run; a fire that
|
|
121
|
+
coincides with another run waits its turn (the host bridge does one exec at a time).
|
|
122
|
+
- **Inputs.** Only the app's declared inputs are kept, coerced to their declared type; omit an
|
|
123
|
+
input to use the app's default. Keys are validated `[A-Za-z0-9._-]`.
|
|
124
|
+
- **Triggers (event-driven).** A `kind:"trigger"` routine hosts a long-lived `aware app run` of a
|
|
125
|
+
streaming `lifecycle:start` source (outside the schedule run-lock); `enabled` start/stops that
|
|
126
|
+
watcher. State is **ephemeral** — `session` is recomputed live (server restart re-derives it) and
|
|
127
|
+
pushed over the `trigger-session-changed` SSE event. **v1 sets no source params**: the watch's
|
|
128
|
+
`filter` etc. live in the workflow's node config, not as routine inputs — to change them, edit the
|
|
129
|
+
workflow (`floless-app-workflows`), not the routine. A host-backed watcher is mutually
|
|
130
|
+
exclusive with manual/scheduled host runs (one live model = one consumer).
|