# BW Guides — Architecture

**Version:** 0.1.0 | **Last Updated:** 2026-07-23

## Overview

Client half of the BW Guides system. Pulls guides from the central hub
(`bw-guides-server` on plugins.bowden.works) over an authenticated REST API, stores
them as a local CPT, and provides a wp-admin reading UI plus local authoring, tagging,
and private notes.

## File Layout

```
bw-guides/
├── bw-guides.php              Main plugin file, bootstrap + hub-coexistence guard
├── includes/                  Core classes (one class per file)
├── admin/views/               guides-list.php, guide-single.php, settings.php
├── assets/                    Admin CSS/JS
├── vendor/                    Third-party libs (plugin-update-checker)
└── docs/                      Dev docs (not shipped)
```

## Core Classes

| Class | File | Responsibility |
|---|---|---|
| `BW_Guides_CPT` | includes/class-bw-guides-cpt.php | `bw_guide` CPT + `bw_guide_tag` taxonomy; `map_meta_cap` read-only lock for hub guides; post states; defaults `_bw_guides_source` to `local` |
| `BW_Guides_Settings` | includes/class-bw-guides-settings.php | Single option `bw_guides_settings` (autoload off); admin-post handlers save/sync-now/test-connection; HTTPS-enforced hub URL (constant `BW_GUIDES_HUB_URL` overrides) |
| `BW_Guides_Client` | includes/class-bw-guides-client.php | HTTP to the hub (`wp_remote_get`, 15s, `sslverify`, `X-BW-Guides-Key` header): health / manifest / guides |
| `BW_Guides_Sync` | includes/class-bw-guides-sync.php | Sync engine (flow below); daily backstop cron `bw_guides_sync_event`; `needs_check()` throttle for check-on-view; 5-min transient lock |
| `BW_Guides_Admin` | includes/class-bw-guides-admin.php | Guides menu, view dispatch, asset enqueues (incl. `wp-block-library` on the reading view), JS config incl. `checkUpdates` |
| `BW_Guides_Ajax` | includes/class-bw-guides-ajax.php | `wp_ajax_bw_guides_save_tags` / `_save_note` / `_check_updates` — nonce + `edit_posts` (+ post-type check) each |

## Sync flow (`BW_Guides_Sync::do_sync`)

1. Fetch `/wp-json/bw-guides/v1/manifest`. Any failure (HTTP, JSON, schema_version > 1)
   → record error, abort, **zero local changes**.
2. Index local hub guides by `_bw_guides_remote_id` (all statuses incl. trash).
3. Diff: fetch remote ids that are new, whose `content_hash`/`modified_gmt` differ, or
   whose local copy is not `publish` (covers re-targeted → previously trashed).
4. Fetch in batches of 20 (`/guides?include=`); upsert with `wp_kses_post`-sanitized
   content (`wp_slash`ed for the WP write APIs). Trashed matches get `wp_untrash_post`
   + update — no duplicates. **Never touches terms or `_bw_guides_notes`.**
5. Removal pass (only after a fully successful run): local hub guides absent from the
   manifest → `wp_trash_post`.

## Hooks

### Actions
- `bw_guides_sync_event` — the cron hook; fire manually to force a sync.

### Filters
- `bw_guides_check_interval` — seconds between opened-Guides update checks
  (default 600, floor 60).

## Update freshness (check-on-view)

Opening any Guides screen fires `bw_guides_check_updates` (admin-ajax) ~0.8s
after load. Server side: no key or throttle not elapsed → no-op; otherwise a
full sync runs (the manifest request is ~1KB; content only downloads for
changed hashes). If anything changed the page shows a refresh banner; the
single view also detects that the guide being read was updated (`post_modified_gmt`
comparison) or withdrawn (trashed). The throttle keys off `last_sync_at`, which
is written on every attempt, so an unreachable hub is retried at most once per
interval. The daily cron remains as a backstop and keeps the hub's
last-sync/site-liveness bookkeeping fresh for sites nobody opens.

## Data Storage

- Option `bw_guides_settings` (autoload off): `hub_url`, `site_key`, `site_key_hint`,
  `last_sync_at`, `last_sync_status`, `last_error`, `last_sync_counts`.
- Post meta on `bw_guide`:
  - `_bw_guides_source` = `hub` | `local`
  - `_bw_guides_remote_id` — hub post ID, the upsert key
  - `_bw_guides_remote_modified`, `_bw_guides_content_hash` — change detection
  - `_bw_guides_remote_meta` — raw JSON of the payload `meta` object (forward-compat
    carrier for min_wp_version / requires_plugins / tokens)
  - `_bw_guides_notes` — client's private notes (never written by sync)
- Taxonomy `bw_guide_tag` — local-only; sync never calls `wp_set_object_terms`.
- Transient `bw_guides_sync_lock` — 5-minute sync mutex.

## External Dependencies

- `vendor/plugin-update-checker` — YahnisElsts plugin update checker v5

## Security Notes

- Site key is a bearer credential stored in the option (masked in the UI, never
  logged). The hub stores only its sha256 hash.
- Hub content is sanitized with `wp_kses_post` on receipt and rendered with
  `do_blocks()` only — no shortcode execution, no third-party content filters.
- All state-changing actions: nonce + capability (+ post-type check on AJAX).
- Read-only lock: `map_meta_cap` adds `do_not_allow` for `edit_post`/`delete_post` on
  hub-sourced guides. The sync engine is unaffected (`wp_update_post` performs no
  capability checks).
- Hub URL sanitizer requires HTTPS except for `localhost`/`127.0.0.1`/`*.demoing.info`/
  `*.test`/`*.local` dev hosts.
