openvibers/openvibe.watch Commits
OpenVibe Link: the new name in pages, docs and messages (#243)
Changes
@@@ -5,7 +5,7 @@> the site's own ETag will do.**Status:** alpha (plan T18). The registry, the check engine, the pull carriers (`http`, `feed`, `api`), the push--sources (OpenVibe events and signed webhooks), Node probes from the owner's own machine, the actions (a notification,++sources (OpenVibe events and signed webhooks), Link probes from the owner's own machine, the actions (a notification,an event, a webhook, an Actor task or agent run, a Run job, a Codes task) and the public site (sign-in, seven startingpoints, forms) exist. The Run rung (browser or code polling through OpenVibe.Run) and the `ai` extraction do notyet.@@@ -21,7 +21,7 @@ condition, action, budget, retention".Sources ingests the sources a product chose; Watch is the place a **person or an agent** definestheir own observation: "this page's price", "this feed's latest release", "this API's stock", andthe condition on it. Sources performs permitted structured ingestion; **Watch owns user-defined--persistent observations and conditions**; Run executes expensive browser/code checks; Node observes++persistent observations and conditions**; Run executes expensive browser/code checks; Link observeslocal/LAN things; Actor interprets and acts; Events transports the trigger.A Watch is a first-class OpenVibe resource (`watch.watch@1`), so products compose it instead of@@@ -149,11 +149,11 @@ This release carries the pull rungs and event sources:| `event` | delivered by OpenVibe.Events (see [Event sources](#event-sources)) | implemented || `webhook` | a signed POST to `/hooks/<watch id>` (see [Webhook sources](#webhook-sources)) | implemented || `run` | browser/code polling through OpenVibe.Run | step 6 |--| `node` | a probe from the owner's own machine through OpenVibe.Bot and OpenVibe.Node (see below) | implemented (step 7) |++| `node` | a probe from the owner's own machine through OpenVibe.Bot and OpenVibe Link (see below) | implemented (step 7) |--**Node sources** (plan T18 step 7, contracts 0.167.0, [server/carriers/node.js](server/carriers/node.js)): `{ kind: 'node', node_id: 'dev_…', probe: { target, timeout_s } }`. The check runs from the owner's own machine:++**Link sources** (plan T18 step 7, contracts 0.167.0, [server/carriers/node.js](server/carriers/node.js)): `{ kind: 'node', node_id: 'dev_…', probe: { target, timeout_s } }`. The check runs from the owner's own machine:- **The call**: Watch asks OpenVibe.Bot (`WATCH_BOT_URL`, default `http://127.0.0.1:4630`) with its own token (`bot.computer.control`) and the owner in `X-OV-Subject`. Bot answers only for that person's machine, only where they turned on "Let my watches check things from this computer", and Watch may send nothing but the read-only `probe`.--- **Targets**: the machine's OpenVibe Node reaches only its own network (private, loopback or link-local addresses).++- **Targets**: the machine's OpenVibe Link reaches only its own network (private, loopback or link-local addresses).- **http(s) targets**: the answer's body is extracted like a page, and a status other than 200/203 is an `http_error`.- **tcp targets**: the document is `{ open, status: 'up' | 'down', ms }`.- **Owners**: an agent-owned watch is never probed (`node_person_only`).@@@ -555,7 +555,7 @@ Reporting: [SECURITY.md](SECURITY.md). The rules the code keeps:- structured source ingestion for publication (OpenVibe.Sources' registry and adapters): Watchobserves what one person or agent asked for, not what a product publishes--- expensive browser/code execution (OpenVibe.Run, step 6) and local/LAN probes (OpenVibe.Node,++- expensive browser/code execution (OpenVibe.Run, step 6) and local/LAN probes (OpenVibe Link,step 7) — Watch asks them, they run- interpreting a trigger and acting on it (OpenVibe.Actor, OpenVibe.Codes, actions = step 4)- delivery of a notification (OpenVibe.Network consumes `watch.watch.triggered`)@@@ -60,7 +60,7 @@],"notYet": ["no check has run against a real external site in production yet: on 2026-10-10 nobody had created a watch (the tests use stub sites only)",-- "the event and webhook carriers (step 5), the Run rung (step 6), Node probes (step 7) and the actions (step 4): a watch of such a kind is accepted but never fetched",++ "the event and webhook carriers (step 5), the Run rung (step 6), Link probes (step 7) and the actions (step 4): a watch of such a kind is accepted but never fetched","an ai extraction is not a check (step 10); such a watch is recorded skipped and nothing is fetched","the site offers only page/price/feed/json watches: the kinds and the AI reading above are not in the form","notifications are the watch.watch.triggered event OpenVibe.Network turns into WATCH_TRIGGERED; no other action is dispatched"@@@ -1,7 +1,7 @@'use strict';/*** The node carrier (plan T18 step 7): a check made FROM the owner's own machine, of something on that machine's own-- * network (a NAS's status page, a printer, a port on the router), through OpenVibe.Bot and the machine's OpenVibe Node++ * network (a NAS's status page, a printer, a port on the router), through OpenVibe.Bot and the machine's OpenVibe Link* (the computer plugin's read-only `probe`). Watch never reaches the person's network itself.** source: { kind: 'node', node_id: 'dev_…', probe: { target: 'http(s)://…' | 'tcp://host:port', timeout_s? } }@@@ -494,7 +494,7 @@ ${field('Value path', html`<input type="text" id="${idOf('value_path')}" name="vconst whatBlock = template === 'lan'? html`<fieldset><legend>What to watch</legend>${field('Name', html`<input type="text" id="${idOf('name')}" name="name" value="${fields.name}" maxlength="120" placeholder="e.g. The NAS">`, { name: 'name', error: errors.name, hint: 'Optional. Without one, Watch names it after the address.' })}--${field('Your machine', html`<input type="text" id="${idOf('machine')}" name="machine" value="${fields.machine}" maxlength="30" placeholder="dev_…" required>`, { name: 'machine', error: errors.machine, hint: html`A computer on that network with OpenVibe Node and its computer plugin: copy its id from <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>, where you also turn on “Let my watches check things from this computer”.` })}++${field('Your machine', html`<input type="text" id="${idOf('machine')}" name="machine" value="${fields.machine}" maxlength="30" placeholder="dev_…" required>`, { name: 'machine', error: errors.machine, hint: html`A computer on that network with OpenVibe Link and its computer plugin: copy its id from <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>, where you also turn on “Let my watches check things from this computer”.` })}${field('Address and port', html`<input type="text" id="${idOf('lan_address')}" name="lan_address" value="${fields.lan_address}" maxlength="260" placeholder="192.168.1.20:80" required>`, { name: 'lan_address', error: errors.lan_address, hint: 'On your own network only (a private or local address): your machine checks that it accepts a connection.' })}</fieldset>`: template === 'event'@@@ -548,7 +548,7 @@ ${field('The coding task', html`<textarea id="${idOf('codes_brief')}" name="codeconst newWatch = (q) => `/watches/new?${new URLSearchParams(q)}`;// Feeds anyone may watch, each checked to answer with a feed: what a person most often wants told about.const FEEDS = [-- { title: 'New releases of the OpenVibe Node', detail: 'The agent that runs on robots and computers, from GitHub.', url: 'https://github.com/OpenVibers/OpenVibe.Node/releases.atom', where: 'GitHub' },++ { title: 'New releases of the OpenVibe Link', detail: 'The agent that runs on robots and computers, from GitHub.', url: 'https://github.com/OpenVibers/OpenVibe.Node/releases.atom', where: 'GitHub' },{ title: 'New Node.js releases', detail: 'Every Node.js release, current and LTS.', url: 'https://nodejs.org/en/feed/releases.xml', where: 'nodejs.org' },{ title: 'New remote jobs on OpenVibe.Work', detail: 'Every new remote listing OpenVibe.Work gathers.', url: 'https://openvibe.work/jobs.atom?remote=true', where: 'OpenVibe.Work' },];@@@ -1245,7 +1245,7 @@ ${watchForm({ template, fields, errors, action: `/watches/${id}/edit`, submit: '<li><strong>A feed</strong> — an RSS or Atom feed is read before the page it describes, and the feed's own conditional headers apply.</li><li><strong>An API</strong> — a JSON endpoint is read the same way, mapped by the watch itself.</li><li><strong>A page</strong> — the page's text is read, as the whole body or one element.</li>--<li><strong>Your own computer</strong> — for something on your home or office network (a router, a NAS, a printer page), one read-only check from your own machine through the OpenVibe Node, only where you turned that on for it.</li>++<li><strong>Your own computer</strong> — for something on your home or office network (a router, a NAS, a printer page), one read-only check from your own machine through the OpenVibe Link, only where you turned that on for it.</li><li><strong>An expensive browser check through OpenVibe.Run</strong> — the last resort. It waits for a browser worker, so a <code>run</code> watch is accepted but not checked yet, and says so on its page.</li></ol><h2>The manners</h2>@@@ -1280,7 +1280,7 @@ ${table(['State', 'In plain words'], [<li>A value the source did not state is <code>null</code>; Watch never invents one, and an absent value stays absent.</li><li>A failed check is a recorded state, never a silent skip; a failed check records no observation.</li><li>A site that is unavailable is retried with backoff, not hammered.</li>--<li>Run and an AI reading are not offered in the form, because Watch will not pretend to check what it cannot. Event, webhook and Node watches are.</li>++<li>Run and an AI reading are not offered in the form, because Watch will not pretend to check what it cannot. Event, webhook and Link watches are.</li></ul><p><a href="/watches/new?template=page">Start a watch</a> · <a href="/">Back home</a></p>`,}));
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
> the site's own ETag will do.
> the site's own ETag will do.
**Status:** alpha (plan T18). The registry, the check engine, the pull carriers (`http`, `feed`, `api`), the push
**Status:** alpha (plan T18). The registry, the check engine, the pull carriers (`http`, `feed`, `api`), the push
-sources (OpenVibe events and signed webhooks), Node probes from the owner's own machine, the actions (a notification,
+sources (OpenVibe events and signed webhooks), Link probes from the owner's own machine, the actions (a notification,
an event, a webhook, an Actor task or agent run, a Run job, a Codes task) and the public site (sign-in, seven starting
an event, a webhook, an Actor task or agent run, a Run job, a Codes task) and the public site (sign-in, seven starting
points, forms) exist. The Run rung (browser or code polling through OpenVibe.Run) and the `ai` extraction do not
points, forms) exist. The Run rung (browser or code polling through OpenVibe.Run) and the `ai` extraction do not
yet.
yet.
@@ -21,7 +21,7 @@ condition, action, budget, retention".
@@ -21,7 +21,7 @@ condition, action, budget, retention".
Sources ingests the sources a product chose; Watch is the place a **person or an agent** defines
Sources ingests the sources a product chose; Watch is the place a **person or an agent** defines
their own observation: "this page's price", "this feed's latest release", "this API's stock", and
their own observation: "this page's price", "this feed's latest release", "this API's stock", and
the condition on it. Sources performs permitted structured ingestion; **Watch owns user-defined
the condition on it. Sources performs permitted structured ingestion; **Watch owns user-defined
-persistent observations and conditions**; Run executes expensive browser/code checks; Node observes
+persistent observations and conditions**; Run executes expensive browser/code checks; Link observes
local/LAN things; Actor interprets and acts; Events transports the trigger.
local/LAN things; Actor interprets and acts; Events transports the trigger.
A Watch is a first-class OpenVibe resource (`watch.watch@1`), so products compose it instead of
A Watch is a first-class OpenVibe resource (`watch.watch@1`), so products compose it instead of
@@ -149,11 +149,11 @@ This release carries the pull rungs and event sources:
@@ -149,11 +149,11 @@ This release carries the pull rungs and event sources:
| `event` | delivered by OpenVibe.Events (see [Event sources](#event-sources)) | implemented |
| `event` | delivered by OpenVibe.Events (see [Event sources](#event-sources)) | implemented |
| `webhook` | a signed POST to `/hooks/<watch id>` (see [Webhook sources](#webhook-sources)) | implemented |
| `webhook` | a signed POST to `/hooks/<watch id>` (see [Webhook sources](#webhook-sources)) | implemented |
| `run` | browser/code polling through OpenVibe.Run | step 6 |
| `run` | browser/code polling through OpenVibe.Run | step 6 |
-| `node` | a probe from the owner's own machine through OpenVibe.Bot and OpenVibe.Node (see below) | implemented (step 7) |
+| `node` | a probe from the owner's own machine through OpenVibe.Bot and OpenVibe Link (see below) | implemented (step 7) |
-**Node sources** (plan T18 step 7, contracts 0.167.0, [server/carriers/node.js](server/carriers/node.js)): `{ kind: 'node', node_id: 'dev_…', probe: { target, timeout_s } }`. The check runs from the owner's own machine:
+**Link sources** (plan T18 step 7, contracts 0.167.0, [server/carriers/node.js](server/carriers/node.js)): `{ kind: 'node', node_id: 'dev_…', probe: { target, timeout_s } }`. The check runs from the owner's own machine:
- **The call**: Watch asks OpenVibe.Bot (`WATCH_BOT_URL`, default `http://127.0.0.1:4630`) with its own token (`bot.computer.control`) and the owner in `X-OV-Subject`. Bot answers only for that person's machine, only where they turned on "Let my watches check things from this computer", and Watch may send nothing but the read-only `probe`.
- **The call**: Watch asks OpenVibe.Bot (`WATCH_BOT_URL`, default `http://127.0.0.1:4630`) with its own token (`bot.computer.control`) and the owner in `X-OV-Subject`. Bot answers only for that person's machine, only where they turned on "Let my watches check things from this computer", and Watch may send nothing but the read-only `probe`.
-- **Targets**: the machine's OpenVibe Node reaches only its own network (private, loopback or link-local addresses).
+- **Targets**: the machine's OpenVibe Link reaches only its own network (private, loopback or link-local addresses).
- **http(s) targets**: the answer's body is extracted like a page, and a status other than 200/203 is an `http_error`.
- **http(s) targets**: the answer's body is extracted like a page, and a status other than 200/203 is an `http_error`.
- **tcp targets**: the document is `{ open, status: 'up' | 'down', ms }`.
- **tcp targets**: the document is `{ open, status: 'up' | 'down', ms }`.
- **Owners**: an agent-owned watch is never probed (`node_person_only`).
- **Owners**: an agent-owned watch is never probed (`node_person_only`).
@@ -555,7 +555,7 @@ Reporting: [SECURITY.md](SECURITY.md). The rules the code keeps:
@@ -555,7 +555,7 @@ Reporting: [SECURITY.md](SECURITY.md). The rules the code keeps:
- structured source ingestion for publication (OpenVibe.Sources' registry and adapters): Watch
- structured source ingestion for publication (OpenVibe.Sources' registry and adapters): Watch
observes what one person or agent asked for, not what a product publishes
observes what one person or agent asked for, not what a product publishes
-- expensive browser/code execution (OpenVibe.Run, step 6) and local/LAN probes (OpenVibe.Node,
+- expensive browser/code execution (OpenVibe.Run, step 6) and local/LAN probes (OpenVibe Link,
step 7) — Watch asks them, they run
step 7) — Watch asks them, they run
- interpreting a trigger and acting on it (OpenVibe.Actor, OpenVibe.Codes, actions = step 4)
- interpreting a trigger and acting on it (OpenVibe.Actor, OpenVibe.Codes, actions = step 4)
- delivery of a notification (OpenVibe.Network consumes `watch.watch.triggered`)
- delivery of a notification (OpenVibe.Network consumes `watch.watch.triggered`)
@@ -60,7 +60,7 @@
@@ -60,7 +60,7 @@
],
],
"notYet": [
"notYet": [
"no check has run against a real external site in production yet: on 2026-10-10 nobody had created a watch (the tests use stub sites only)",
"no check has run against a real external site in production yet: on 2026-10-10 nobody had created a watch (the tests use stub sites only)",
- "the event and webhook carriers (step 5), the Run rung (step 6), Node probes (step 7) and the actions (step 4): a watch of such a kind is accepted but never fetched",
+ "the event and webhook carriers (step 5), the Run rung (step 6), Link probes (step 7) and the actions (step 4): a watch of such a kind is accepted but never fetched",
"an ai extraction is not a check (step 10); such a watch is recorded skipped and nothing is fetched",
"an ai extraction is not a check (step 10); such a watch is recorded skipped and nothing is fetched",
"the site offers only page/price/feed/json watches: the kinds and the AI reading above are not in the form",
"the site offers only page/price/feed/json watches: the kinds and the AI reading above are not in the form",
"notifications are the watch.watch.triggered event OpenVibe.Network turns into WATCH_TRIGGERED; no other action is dispatched"
"notifications are the watch.watch.triggered event OpenVibe.Network turns into WATCH_TRIGGERED; no other action is dispatched"
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* The node carrier (plan T18 step 7): a check made FROM the owner's own machine, of something on that machine's own
* The node carrier (plan T18 step 7): a check made FROM the owner's own machine, of something on that machine's own
- * network (a NAS's status page, a printer, a port on the router), through OpenVibe.Bot and the machine's OpenVibe Node
+ * network (a NAS's status page, a printer, a port on the router), through OpenVibe.Bot and the machine's OpenVibe Link
* (the computer plugin's read-only `probe`). Watch never reaches the person's network itself.
* (the computer plugin's read-only `probe`). Watch never reaches the person's network itself.
*
*
* source: { kind: 'node', node_id: 'dev_…', probe: { target: 'http(s)://…' | 'tcp://host:port', timeout_s? } }
* source: { kind: 'node', node_id: 'dev_…', probe: { target: 'http(s)://…' | 'tcp://host:port', timeout_s? } }
@@ -494,7 +494,7 @@ ${field('Value path', html`<input type="text" id="${idOf('value_path')}" name="v
@@ -494,7 +494,7 @@ ${field('Value path', html`<input type="text" id="${idOf('value_path')}" name="v
const whatBlock = template === 'lan'
const whatBlock = template === 'lan'
? html`<fieldset><legend>What to watch</legend>
? html`<fieldset><legend>What to watch</legend>
${field('Name', html`<input type="text" id="${idOf('name')}" name="name" value="${fields.name}" maxlength="120" placeholder="e.g. The NAS">`, { name: 'name', error: errors.name, hint: 'Optional. Without one, Watch names it after the address.' })}
${field('Name', html`<input type="text" id="${idOf('name')}" name="name" value="${fields.name}" maxlength="120" placeholder="e.g. The NAS">`, { name: 'name', error: errors.name, hint: 'Optional. Without one, Watch names it after the address.' })}
-${field('Your machine', html`<input type="text" id="${idOf('machine')}" name="machine" value="${fields.machine}" maxlength="30" placeholder="dev_…" required>`, { name: 'machine', error: errors.machine, hint: html`A computer on that network with OpenVibe Node and its computer plugin: copy its id from <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>, where you also turn on “Let my watches check things from this computer”.` })}
+${field('Your machine', html`<input type="text" id="${idOf('machine')}" name="machine" value="${fields.machine}" maxlength="30" placeholder="dev_…" required>`, { name: 'machine', error: errors.machine, hint: html`A computer on that network with OpenVibe Link and its computer plugin: copy its id from <a href="https://openvibe.bot/machines">openvibe.bot/machines</a>, where you also turn on “Let my watches check things from this computer”.` })}
${field('Address and port', html`<input type="text" id="${idOf('lan_address')}" name="lan_address" value="${fields.lan_address}" maxlength="260" placeholder="192.168.1.20:80" required>`, { name: 'lan_address', error: errors.lan_address, hint: 'On your own network only (a private or local address): your machine checks that it accepts a connection.' })}
${field('Address and port', html`<input type="text" id="${idOf('lan_address')}" name="lan_address" value="${fields.lan_address}" maxlength="260" placeholder="192.168.1.20:80" required>`, { name: 'lan_address', error: errors.lan_address, hint: 'On your own network only (a private or local address): your machine checks that it accepts a connection.' })}
</fieldset>`
</fieldset>`
: template === 'event'
: template === 'event'
@@ -548,7 +548,7 @@ ${field('The coding task', html`<textarea id="${idOf('codes_brief')}" name="code
@@ -548,7 +548,7 @@ ${field('The coding task', html`<textarea id="${idOf('codes_brief')}" name="code
const newWatch = (q) => `/watches/new?${new URLSearchParams(q)}`;
const newWatch = (q) => `/watches/new?${new URLSearchParams(q)}`;
// Feeds anyone may watch, each checked to answer with a feed: what a person most often wants told about.
// Feeds anyone may watch, each checked to answer with a feed: what a person most often wants told about.
const FEEDS = [
const FEEDS = [
- { title: 'New releases of the OpenVibe Node', detail: 'The agent that runs on robots and computers, from GitHub.', url: 'https://github.com/OpenVibers/OpenVibe.Node/releases.atom', where: 'GitHub' },
+ { title: 'New releases of the OpenVibe Link', detail: 'The agent that runs on robots and computers, from GitHub.', url: 'https://github.com/OpenVibers/OpenVibe.Node/releases.atom', where: 'GitHub' },
{ title: 'New Node.js releases', detail: 'Every Node.js release, current and LTS.', url: 'https://nodejs.org/en/feed/releases.xml', where: 'nodejs.org' },
{ title: 'New Node.js releases', detail: 'Every Node.js release, current and LTS.', url: 'https://nodejs.org/en/feed/releases.xml', where: 'nodejs.org' },
{ title: 'New remote jobs on OpenVibe.Work', detail: 'Every new remote listing OpenVibe.Work gathers.', url: 'https://openvibe.work/jobs.atom?remote=true', where: 'OpenVibe.Work' },
{ title: 'New remote jobs on OpenVibe.Work', detail: 'Every new remote listing OpenVibe.Work gathers.', url: 'https://openvibe.work/jobs.atom?remote=true', where: 'OpenVibe.Work' },
];
];
@@ -1245,7 +1245,7 @@ ${watchForm({ template, fields, errors, action: `/watches/${id}/edit`, submit: '
@@ -1245,7 +1245,7 @@ ${watchForm({ template, fields, errors, action: `/watches/${id}/edit`, submit: '
<li><strong>A feed</strong> — an RSS or Atom feed is read before the page it describes, and the feed's own conditional headers apply.</li>
<li><strong>A feed</strong> — an RSS or Atom feed is read before the page it describes, and the feed's own conditional headers apply.</li>
<li><strong>An API</strong> — a JSON endpoint is read the same way, mapped by the watch itself.</li>
<li><strong>An API</strong> — a JSON endpoint is read the same way, mapped by the watch itself.</li>
<li><strong>A page</strong> — the page's text is read, as the whole body or one element.</li>
<li><strong>A page</strong> — the page's text is read, as the whole body or one element.</li>
-<li><strong>Your own computer</strong> — for something on your home or office network (a router, a NAS, a printer page), one read-only check from your own machine through the OpenVibe Node, only where you turned that on for it.</li>
+<li><strong>Your own computer</strong> — for something on your home or office network (a router, a NAS, a printer page), one read-only check from your own machine through the OpenVibe Link, only where you turned that on for it.</li>
<li><strong>An expensive browser check through OpenVibe.Run</strong> — the last resort. It waits for a browser worker, so a <code>run</code> watch is accepted but not checked yet, and says so on its page.</li>
<li><strong>An expensive browser check through OpenVibe.Run</strong> — the last resort. It waits for a browser worker, so a <code>run</code> watch is accepted but not checked yet, and says so on its page.</li>
</ol>
</ol>
<h2>The manners</h2>
<h2>The manners</h2>
@@ -1280,7 +1280,7 @@ ${table(['State', 'In plain words'], [
@@ -1280,7 +1280,7 @@ ${table(['State', 'In plain words'], [
<li>A value the source did not state is <code>null</code>; Watch never invents one, and an absent value stays absent.</li>
<li>A value the source did not state is <code>null</code>; Watch never invents one, and an absent value stays absent.</li>
<li>A failed check is a recorded state, never a silent skip; a failed check records no observation.</li>
<li>A failed check is a recorded state, never a silent skip; a failed check records no observation.</li>
<li>A site that is unavailable is retried with backoff, not hammered.</li>
<li>A site that is unavailable is retried with backoff, not hammered.</li>
-<li>Run and an AI reading are not offered in the form, because Watch will not pretend to check what it cannot. Event, webhook and Node watches are.</li>
+<li>Run and an AI reading are not offered in the form, because Watch will not pretend to check what it cannot. Event, webhook and Link watches are.</li>
</ul>
</ul>
<p><a href="/watches/new?template=page">Start a watch</a> · <a href="/">Back home</a></p>`,
<p><a href="/watches/new?template=page">Start a watch</a> · <a href="/">Back home</a></p>`,
}));
}));