=== LiveCal Publisher ===
Contributors: livecal
Tags: calendar, events, track, livecal, gutenberg
Requires at least: 6.1
Tested up to: 6.5
Requires PHP: 7.4
Stable tag: 0.2.0
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Map WordPress posts to LiveCal Sources. Visitors Track your event and add it to
their real calendar; you push updates and every tracker's calendar event
updates in seconds.

== Description ==

LiveCal is the live layer for calendars — a calendar entry that updates itself.
This plugin maps a WordPress post to a LiveCal **Source** via stable post meta,
upserts it to the LiveCal Publisher API, and renders a **Track** widget so
visitors add the event to their own calendar.

The plugin is a thin writer in front of the shipped Publisher API. A WordPress
post maps to ONE Source; many trackers' calendar events hang off it. When facts
or live-state change, LiveCal PATCHes every tracker's calendar in seconds — the
moat versus a static ICS file.

= Two writers, one contract =

The post-meta keys below are the contract. BOTH the block editor (a human) and a
programmatic pipeline (via `update_post_meta()` or the WP REST API) are
first-class writers. The sync engine fires on either.

* `_livecal_manifest` (string) — manifest slug, e.g. `airshow`.
* `_livecal_facts` (object JSON) — confidence-tagged facts: `{ field: { value, confidence: "known"|"provisional" } }`.
* `_livecal_state` (object JSON) — `{ status, starts_at, live_state }`.
* `_livecal_source_id` (string) — **written back by the plugin** after the first successful sync (read-only to writers).

= Editor surfaces =

* **Gutenberg block** ("LiveCal Track") — edits the contract meta in the block
  sidebar and renders the widget on the published page.
* **Shortcode** — `[livecal_track source="..."]` (omit `source` to use the
  current post's Source).
* **Classic meta box** — a fallback in the post sidebar for the classic editor.

All three render the same element: `<livecal-track source="{source_id}">`.

== Installation ==

1. Upload the `livecal-publisher` folder to `/wp-content/plugins/`.
2. Activate **LiveCal Publisher** in the Plugins screen.
3. Go to **Settings → LiveCal Publisher** and paste the per-site API token LiveCal
   issued you (delivered out-of-band — never committed to code). Optionally paste
   the **publishable key** (`livecal_pk_…`) to give the rendered widget origin-pinned
   analytics + live-state enrichment (it is not a secret and is safe in page source),
   and set the API base URL and which post types sync.
4. Edit a post: set a manifest slug (and starts_at / facts) via the LiveCal Track
   block or the LiveCal meta box, then Publish. The plugin upserts the Source and
   writes back `_livecal_source_id`.
5. Add the LiveCal Track block (or `[livecal_track]`) where you want the widget.

= Multisite / 120-site networks =

Place this `livecal-publisher` folder in `/wp-content/plugins/` once, then copy
`livecal-publisher-mu-loader.php` into `/wp-content/mu-plugins/`. Every site in
the network then runs the plugin without per-site activation. Each site still
holds its OWN token under its own Settings → LiveCal Publisher, so a leak on one
site can't touch the network.

== Frequently Asked Questions ==

= Where does the API token go? =

Settings → LiveCal Publisher. It is stored in the site's options table, rendered
masked, and never written to code or committed. LiveCal delivers it out-of-band.

= What happens when a sync fails? =

Retryable failures (HTTP 429 / 5xx / network) are retried with exponential
backoff via WP-Cron (up to 5 attempts, honoring `Retry-After`). Terminal failures
(HTTP 4xx / 422) stop and show an admin notice on the post-edit screen, with the
stable error `code` and offending `field`.

= Is the upsert idempotent? =

Yes. The Source is keyed on `external_id = {site_url}/{post_id}` and upserted via
`PUT /v1/sources/{external_id}`, so re-saving a post never creates a duplicate.

== Changelog ==

= 0.2.0 =
* Settings: added an optional **Publishable key** field (`livecal_pk_…`). Unlike the
  API token it is not a secret — it is rendered into the widget markup, where the
  embed uses it to origin-pin + rate-limit the analytics beacon and to enrich the
  widget with live-state. The field validates the `livecal_pk_` prefix as a guard
  against pasting the secret token into a public field.
* Render: `<livecal-track>` now carries `pk="livecal_pk_…"` when a publishable key
  is set (site-wide). With no key set, the markup is unchanged — fully keyless.

= 0.1.0 =
* Initial build: post-meta contract, sync engine (upsert + state push with
  retry/backoff), Gutenberg block, shortcode, classic meta box, settings page,
  multisite mu-loader.
