=== Ledgable ===
Contributors: ledgable
Tags: journal, archive, import, notes, handwriting
Requires at least: 6.4
Tested up to: 6.8
Requires PHP: 7.4
Stable tag: 1.0.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

A field ledger for WordPress: day pages, handwritten capture, a garden of your own archive, and an importer built for very large personal exports.

== Description ==

Ledgable turns a WordPress site into a private field ledger — a day-by-day record of what you wrote, made, photographed and thought, held on hardware you control.

It does three things.

**It keeps a ledger.** One private post type, one day index, and a front end that renders inside your own theme. A day page, a timeline, threads, an on-this-day view, a year heatmap, and a search that links a hit to the part of the day it was found in. No page to create, no shortcode to paste, no block to configure: it registers real rewrite rules from one setting and lets your active theme supply the header, footer, navigation and width.

**It takes handwriting.** A REST surface built for a capture device: send a page of ink and its transcript, get back an entry filed on the right day, in your own timezone. Stroke data is kept in a private, deny-guarded store rather than a guessable public path. Device credentials are hashed, scoped, revocable, and shown to you exactly once.

**It imports your archive.** Point it at a social export — a 40GB one, on shared hosting, in a browser tab you are going to close — and it finishes. The upload is sliced into pieces and resumes from the byte it stopped on. The JSON is read one element at a time by a streaming reader, so a 400MB file does not need 400MB of memory. Every record is written with a deterministic identifier, so running the same archive again is free and produces no duplicates. Days are grouped in your site's timezone, not UTC. Text is repaired on the way in, so an archive full of `Ã©` arrives readable.

Everything it imports lands **private**. You publish it, in one action, after you have looked at it.

= Integrations are optional, all of them =

Ledgable is built as modules. Each one lights up when its plugin is detected and stays dark otherwise. Dark means its REST paths answer `501` and name what is missing — never a fatal, never a 404, and never a crippled core. The ledger, the handwriting capture and the archive import all work on a bare WordPress with nothing else installed.

**Optional dependency matrix**

| Integration | What it adds | What happens without it |
| --- | --- | --- |
| FluentBoards | Handwritten cards, board and card routes, due cards | Those paths answer 501. Nothing else changes. |
| FluentBoards Pro | The pro-only card fields | Board routes work; those fields are skipped. |
| FluentCRM | Contacts, audiences, contact notes, draft campaigns | A rolodex built from WordPress users answers instead; notes and campaigns answer 501. |
| FluentCommunity | Spaces, feeds, posts, comments, reactions | Those paths answer 501. The ledger and inbox are unaffected. |
| FluentCommunity Courses | Course and lesson routes | Those paths answer 501. |
| FluentCommunity Chat | Writing ink into a chat thread | That path answers 501; the rest of the inbox still merges. |
| FluentCommunity Pro Cloud Storage | Media offload to cloud storage | Media goes to the WordPress media library. |
| FluentBooking | Booking list, detail, cancel, reschedule, slots | Those paths answer 501. |
| FluentSMTP | Naming which connection carried a message | Mail still sends and is still logged; the connection is simply unnamed. |
| AI Engine | Better theme classification, entity extraction, OCR repair, captions, summaries | The shipped keyword classifier answers instead. Nothing leaves your server. |
| Code Engine | Ledgable's services as snippet abilities | Those abilities are not registered. Nothing else depends on them. |
| Action Scheduler | The best background-work driver | Falls back to WP-Cron, then a browser keep-alive, then WP-CLI. Imports still finish. |
| PHP ZipArchive | Unpacking a zipped archive on the server | Unzip the archive yourself and point Ledgable at the folder. |

Nothing in this list is bundled. Ledgable detects; it never installs.

= Command line =

    wp ledgable doctor
    wp ledgable import meta --path=my-export --dry-run
    wp ledgable import run --resume
    wp ledgable index rebuild
    wp ledgable index verify
    wp ledgable threads build
    wp ledgable migrate meta --dry-run

== Installation ==

1. Upload the `ledgable` folder to `wp-content/plugins/`, or install the zip from **Plugins → Add New**.
2. Activate it. You are taken to a four-step setup once, and never nagged about it again.
3. Step two asks where your ledger lives and who may read it. Private is the default.
4. Step four creates a device credential and immediately makes a real HTTP request back to your own site with it, so a host that strips authentication headers is diagnosed on the spot.

You can re-run setup at any time from **Ledgable → Tools**.

== Frequently Asked Questions ==

= How do I import a Facebook or Instagram export? =

1. Request your export from the service and choose **JSON** rather than HTML. Meta will email you a download link, usually within a day.
2. Download the zip. Do not unzip it if you do not want to — Ledgable can unpack it.
3. Go to **Ledgable → Import** and drop the zip on the upload area. It is sent to your site in small pieces, so it works on hosts with a 64MB upload limit and survives your connection dropping. If you close the tab, reopen it and drop the same file: the upload resumes from where it stopped.
4. If you would rather not upload through a browser, put the export in `wp-content/uploads/ledgable-import/` with SFTP or your host's file manager. It then appears in the picker on the same screen.
5. Press **Scan**. Ledgable reads the archive's structure and shows you a preview before writing anything: how many records it found, from which parts of the export, over what date range, how much media there is, and what it is leaving out.
6. Check the four choices. The defaults are **private**, **you** as the author, **all dates**, and **link** for media, which leaves your photos in the archive folder and serves them from there rather than duplicating them.
7. Press **Start**. The screen shows records done, records failed, and the worker actually doing the work. If that worker is the browser keep-alive, the screen says so plainly and asks you to leave the tab open; otherwise it tells you that you can close it.
8. When it finishes you get a report by kind and by year, a grid of a few entries to look at, a list of anything that failed with the reason for each and a Retry button, and one button that publishes everything.

Private messages, advertising records and security logs are excluded by default. Importing somebody's direct messages because a checkbox defaulted the wrong way is not acceptable, so it does not happen.

= I already ran the import once. Can I run it again? =

Yes, and it costs nothing. Every record is written with an identifier derived from the record itself, so a second run matches what is already there and updates it rather than creating a copy. Overlapping exports, a re-download, and a resume after a crash are all safe.

= The import stopped. Did I lose it? =

No. The position in the archive — which file, which byte — is committed at the end of every batch. Reopen the Import screen and press Resume, or run `wp ledgable import run --resume`. It carries on from the same byte.

= Nothing seems to be happening. =

Open **Ledgable → Status**. It names the background worker Ledgable resolved and why. If your host has `DISABLE_WP_CRON` set with no real cron job behind it, WordPress schedules work that never runs; Ledgable falls back to a browser keep-alive in that case and says so on the Import screen.

= My device gets a 401 but the credential is right. =

Many shared hosts strip the `Authorization` header before PHP sees it, which makes application passwords fail while everything else works. Run the self-test on **Ledgable → Access**: it makes three real requests to your own site — one anonymous, one with a device token, one with an application password — and tells you which failed. If only the application password fails, use the device token, which travels in `X-Ledgable-Token`.

= Will this change my existing posts? =

No. Ledgable registers its own post type and its own taxonomies and writes to its own tables. It does not modify your posts or pages. If you want your existing posts to appear in the ledger alongside your entries, there is a setting that weaves in whichever post types you choose — that setting changes what Ledgable reads, not what your posts are.

= Does anything go public automatically? =

No. Imported entries land private, captured entries land private, and the ledger itself is private until you change it. There is no automatic publishing anywhere in the plugin.

= Can I get my content back out? =

It is already out. Entries are ordinary WordPress posts in your database, exportable with the built-in **Tools → Export**, readable over the WordPress REST API, and untouched if you delete this plugin. Uninstalling deletes Ledgable's own tables and settings; deleting your entries as well is a separate checkbox that is off.

= What happens if I delete a module's folder? =

The plugin still activates and still works. A module class that is not on disk is skipped rather than fatal. That is deliberate: it is what makes "every integration is optional" a property of the code rather than a promise.

== Data handling and external services ==

**Ledgable makes no outbound network request of its own.** It has no analytics, no telemetry, no licence check, no update server, no phone-home, and no bundled third-party service. Nothing about your site or your writing is sent anywhere by installing or using this plugin.

There is exactly one path by which text can leave your server, and it is off unless you build it yourself:

* **AI Engine (optional, off for most tasks by default).** If you have installed the AI Engine plugin *and* enabled an enrichment task in **Ledgable → Integrations**, Ledgable asks AI Engine to classify, summarise, clean up or caption the item. AI Engine then sends that content to whichever model provider *you* have configured in *its* settings, on *your* account, under *your* usage limits. Ledgable never contacts a model provider directly, never ships or accepts an API key, and never names a provider. Turn the tasks off, or do not install AI Engine, and no content leaves your server: theme classification falls back to a keyword classifier that runs entirely in PHP on your own machine. The Integrations screen always names which of the two is currently answering, so the state is never ambiguous.

Everything else stays local:

* **Where data is stored.** Entries are WordPress posts. Ledgable adds six tables prefixed `{prefix}ledgable_` (index, jobs, queue, idempotency, tokens, mail log) and two options, `ledgable_settings` and `ledgable_secrets`. `ledgable_secrets` is not autoloaded and is never included in a settings export, in the REST discovery payload, or in the system report.
* **Where files are written.** Only inside `wp_upload_dir()`. Uploaded archives go to `wp-content/uploads/ledgable-import/` in a directory with a random suffix, protected by `.htaccess`, `web.config` and an `index.php`. Private artefacts such as stroke archives go to `wp-content/uploads/ledgable-private/`, likewise deny-guarded. Nothing is written outside the uploads directory.
* **Credentials.** Device tokens are stored as SHA-256 hashes and compared with `hash_equals`. The plaintext is shown once, at creation, and cannot be recovered afterwards. Tokens are only accepted in the `X-Ledgable-Token` header, never as a query parameter. Writes from a device are refused over plain HTTP.
* **Logging.** The debug log is off, and even when switched on it only writes if `WP_DEBUG` is also on. It is capped, rotated, kept in the deny-guarded private directory, and redacts email addresses, tokens and secrets. No email address, subject or message body is ever written to the PHP error log.
* **Mail.** Ledgable has one outbound mail path and it is off by default. When on, it sends through `wp_mail()` — that is, through whatever your site already uses — and records the result, so a failed send is visible instead of silently discarded.
* **Personal data.** Ledgable does not create user accounts, does not read other plugins' user data unless the matching module is active, and only ever emits a member's email address to a caller who can already list users.

== Screenshots ==

1. A day page, rendered inside the active theme.
2. The import preview: what was found, over what range, and what is being left out.
3. Import progress, with the background worker named and failures listed separately.
4. The Integrations screen: what was detected, at what version, and what goes dark without it.
5. The Status screen, including the route table and which paths another plugin already owns.

== Changelog ==

= 1.0.0 =
* First release: ledger, day and timeline rendering, handwriting capture, unified inbox, streaming archive importer, admin, setup wizard and WP-CLI.
* Every integration is optional and detected at runtime; a missing dependency answers 501 and names what is missing.
* Imports are resumable to the byte, idempotent on re-run, and grouped in the site timezone.
* Authorisation narrowed: board, card, thread, community and booking reads and writes are scoped to objects the caller can actually reach. The previous, wider behaviour is available as an explicit compatibility setting for a migration in progress.

== Upgrade Notice ==

= 1.0.0 =
First release.
