Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ uv run paperpi run --config paperpi.toml --out screen/ --state-dir state/ --heal

### The web interface

`paperpi run` also starts the web interface, on port 8080. Open it by the Pi's IP address on a phone or computer on the same home network, for example `http://192.168.1.20:8080` (your router's list of devices shows the Pi's address; on the Pi, `hostname -I` prints it). Names such as `paperpi.local` are refused for now, for safety (see [docs/decisions/web-interface.md](docs/decisions/web-interface.md)). It has two pages for plugins: **Active Plugins** (switch plugins on or off, move them up or down, remove one) and **Plugin Library** (every plugin that comes with PaperPi; add one at the end of the list). A change there is saved in the config file and applies at once. Comments in the file stay, except the comments of a plugin you remove and a comment on the same line as `enabled =`. If the config file changed since the page was opened, is written in a way the web interface can't change safely, or has a problem that stops PaperPi from using it, nothing is saved and the page says so. A plugin that needs settings (such as `met_no`, which needs a place and your email address) is added switched off; until the settings page follows (issue #238), fill those in in the config file.
`paperpi run` also starts the web interface, on port 8080. Open it by the Pi's IP address on a phone or computer on the same home network, for example `http://192.168.1.20:8080` (your router's list of devices shows the Pi's address; on the Pi, `hostname -I` prints it). Names such as `paperpi.local` are refused for now, for safety (see [docs/decisions/web-interface.md](docs/decisions/web-interface.md)). It has three pages for plugins: **Active Plugins** (switch plugins on or off, move them up or down, remove one, open its settings), **Plugin Library** (every plugin that comes with PaperPi; add one at the end of the list) and each plugin's **Settings** (every setting with its help text and default; errors are shown next to the field). A change there is saved in the config file and applies at once. Comments in the file stay, except the comments of a plugin you remove and a comment written after a setting, on the same line, when the web interface changes that setting. If the config file changed since the page was opened, is written in a way the web interface can't change safely, or has a problem that stops PaperPi from using it, nothing is saved and the page says so. A plugin that needs settings (such as `met_no`, which needs a place and your email address) is added switched off, and its Settings page opens to fill them in.

- **First visit:** the first person to open it sets the web password (at least 8 characters). The browser then stays logged in for a year, until **Log out**. Log out only logs out that browser; a new password logs out every browser.
- **Forgotten password:** you need access to the Pi itself (a keyboard and screen, or SSH).
Expand Down
6 changes: 3 additions & 3 deletions docs/decisions/web-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ In v2 the web interface is the main way to set up and change PaperPi: plugins, s
*Added in M5 part 2b (issue #238), agreed with txoof on 2026-10-07.*
- **Active Plugins** lists every `[[plugin]]` block in the config file, in file order (the order the plugins take turns), also blocks with errors. Each has: Switch on / Switch off, **Up** and **Down** buttons (no dragging, so no extra JavaScript library; works the same on a phone), and Remove, which asks first.
- **Plugin Library** lists every plugin type that comes with PaperPi, with its one-line description, except `default` (PaperPi's own "nothing to show" message) and `debugging` (for testing PaperPi). Choosing one asks for a name (a free one is suggested, for example "Basic clock 2"; it may be left empty) and adds the block at the end of Active Plugins with a new `id` (see `config-format.md`, M5 part 3a-0). Active Plugins shows the name, and the `id` next to the type when it differs, so two plugins with the same name can be told apart. Sample pictures follow in part 3b.
- A plugin with required settings (see `plugin-interface.md`) is added **switched off**, and the add page lists those settings with their help text (for `met_no`: a real email address, as met.no's terms ask). Active Plugins shows "Needs settings: ..." and can't switch it on until they are filled in.
- Each change is saved at once (nothing else in the config file changes: comments stay, and the comments just above a `[[plugin]]` line move and are removed with its block; a comment on the same line as `enabled =` is lost) and PaperPi reloads the file, so it applies at once.
- A plugin with required settings (see `plugin-interface.md`) is added **switched off**, and the add page lists those settings with their help text (for `met_no`: a real email address, as met.no's terms ask). Active Plugins shows "Needs settings: ..." and can't switch it on until they are filled in. *M5 part 3a-2 (agreed with txoof on 2026-10-08):* after adding, the new plugin's settings page opens at once, and each plugin in Active Plugins has a Settings link. The settings page also has a folded "Technical information" part with the plugin's `id`, type and storage folder.
- Each change is saved at once (nothing else in the config file changes: comments stay, and the comments just above a `[[plugin]]` line move and are removed with its block; a comment on the same line as a setting that changes, such as `enabled =`, is lost) and PaperPi reloads the file, so it applies at once.
- **Hand edits not applied yet:** a change from the web interface is made to the file as it is on disk, so hand edits stay and apply together with it. The page then says "Your hand edits to the config file were applied too." (The other choice, refusing to save until the hand edits are applied, needs an extra step every time.)
- A change names its block by place and `id`. If the block was moved or its `id` changed by hand since the page was shown, nothing is changed and the page says the file changed meanwhile. A file the web interface can't change safely (for example a `[[plugin]]` line written in an unusual way) is never saved wrongly: every change is read back and compared before saving. A change is also refused while the file has a problem that stops PaperPi from using it (PaperPi then runs on the last good copy, so the change would not apply), and when the file would become larger than PaperPi reads.

### Plugin settings forms

- One page handles every plugin. It reads the plugin's settings description (see `plugin-interface.md`) and builds the form from it: number field, dropdown, password field for API keys, and so on.
- On Save, the values are checked with the same description. Errors are shown next to the field. A saved change applies at once (see `live-config-reload.md`).
- A setting that needs more than a plain field can name an input type from a small list in the web interface, e.g. `location` (lat/lon lookup), `server_search` (find music servers), `layout_picker`. New input types are added when a plugin needs one.
- A setting that needs more than a plain field can name an input type from a small list in the web interface, e.g. `location` (lat/lon lookup), `server_search` (find music servers), `layout_picker`. New input types are added when a plugin needs one. *Built in M5 part 3a-2:* a plugin names one with `paperpi.plugin.setting(..., helper="location")`; the web interface's list (`paperpi.web.helpers`) holds a function per name that adds its part under the field. A name it doesn't know adds nothing, so the plain field still works. The first helper is `location` (part 3c).
- Plugins added later show up without changes to the web code.
- *Agreed with txoof on 2026-10-08 (M5 part 3a):* the page shows the plugin's `name` first, then its own settings, then display time, refresh, layout and level; the other shared settings (time limit, alert and storage settings) are folded under "More settings", which opens by itself when one of them has an error or a value that is not the default. Each field shows its value, or the default when the file doesn't set it.
- How a save works (M5 part 3a-1): only settings that changed are written. An empty field means "the default" (an empty check box: off; an empty list: nothing picked); a value changed to the default is taken out of the file and becomes the comment `# key = default` again (a value left as it is stays, also one written in the file that equals the default), so the help text above it still fits. A new setting takes the place of its `# key = ...` comment line when there is one. If any value has an error, nothing is saved; each error is shown next to its field, with what was typed (never a secret). A secret (such as an API key) is never shown on the page; leaving its field empty keeps the saved one. A setting the form can't show (a group of settings, a list of numbers) is shown as it is in the file, to be changed there. A setting written over several lines in the file is never changed by the page.
Expand Down
4 changes: 2 additions & 2 deletions docs/writing-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ Each setting is a field of the plugin's `Settings` class, with a type, a default
- Every setting needs a default.
- Don't use the names of the shared settings, which every `[[plugin]]` block already has: `id`, `name`, `type`, `enabled`, `level`, `display_time`, `refresh`, `time_limit`, `layout`, `alert_reminder`, `alert_max_time`.
- Use `pydantic.SecretStr` as the type for API keys and passwords. PaperPi then never shows their values in error messages or logs.
- A setting the user must fill in before the plugin can work (a place, an email address, an API key) is made with `paperpi.plugin.setting(..., required=True)` instead of `Field`. It takes the same arguments as `Field`. Its default must be "not set": `None`, empty text or an empty `SecretStr` (text of only spaces also counts as not set). Until every required setting is filled in, the plugin is not shown and not counted as broken; the config check, `paperpi list` and the web interface's Active Plugins page say which settings are missing. The example block marks them "(required)". `setting` is also where later options for a single setting are added, such as a web interface helper that looks up latitude and longitude (M5 part 3c).
- The web interface shows the plugin's `description` in its Plugin Library, and the help text of each required setting on the page that adds the plugin. Write both for someone at home who is not a programmer.
- A setting the user must fill in before the plugin can work (a place, an email address, an API key) is made with `paperpi.plugin.setting(..., required=True)` instead of `Field`. It takes the same arguments as `Field`. Its default must be "not set": `None`, empty text or an empty `SecretStr` (text of only spaces also counts as not set). Until every required setting is filled in, the plugin is not shown and not counted as broken; the config check, `paperpi list` and the web interface's Active Plugins page say which settings are missing. The example block marks them "(required)". `setting` is also where other options for a single setting go: `helper="location"` names a web interface helper shown under the setting's field (the first one, a latitude and longitude lookup, comes in M5 part 3c; see `paperpi.web.helpers`).
- The web interface shows the plugin's `description` in its Plugin Library, and the help text (`description`) of every setting on the plugin's Settings page, next to its field. Write both for someone at home who is not a programmer. The settings page is built from the settings description: a `Literal` becomes a choice list, `bool` a check box, `int`/`float` a number field (with its `ge`, `gt`, `le` and `lt` limits), `str` a text field (with `max_length`), `SecretStr` a password field, and a tuple or list of a `Literal` a list to tick. A setting of another kind is shown as it is in the file, to be changed there.

```python
from paperpi.plugin import PluginSettings, setting
Expand Down
36 changes: 28 additions & 8 deletions src/paperpi/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,32 +102,52 @@ class Settings(PluginSettings):
_OPTIONS = "paperpi"


def setting(default: Any, *, required: bool = False, **field: Any) -> Any:
def setting(
default: Any, *, required: bool = False, helper: str | None = None, **field: Any
) -> Any:
"""A plugin setting: pydantic's ``Field`` (with the same ``description``, ``ge``,
``max_length``, ...) plus what PaperPi needs to know about it. This is the one place a
setting gets PaperPi's own options; later ones (such as a web interface helper that
looks up a place's latitude and longitude, M5 part 3c) are added here too::
setting gets PaperPi's own options::

lat: float | None = setting(None, required=True, description="Latitude")

``required``: the plugin can't work until the user fills it in (a place, an email
address, an API key). Its default must be "not set" (see :func:`is_set`). A plugin
with a required setting that is not set is not shown; the config check and
``paperpi list`` and the web interface's Active Plugins page say which settings it needs.

``helper``: the name of a web interface helper for this setting, shown under its field
on the settings page, for example ``"location"`` (look up a place's latitude and
longitude, M5 part 3c). A name the web interface doesn't know is left out, so the
plain field still works. See ``paperpi.web.helpers``.
"""
extra = field.pop("json_schema_extra", None) or {}
if not isinstance(extra, dict):
raise TypeError("setting() takes json_schema_extra only as a dictionary")
if required:
extra = extra | {_OPTIONS: {"required": True}}
options = {"required": True} if required else {}
if helper is not None:
options["helper"] = helper
if options:
extra = extra | {_OPTIONS: options}
return Field(default, json_schema_extra=extra or None, **field)


def is_required(info: FieldInfo) -> bool:
"""True for a setting made with ``setting(required=True)``."""
def _options(info: FieldInfo) -> dict[str, Any]:
"""PaperPi's own options of a setting made with :func:`setting`."""
extra = info.json_schema_extra
options = extra.get(_OPTIONS) if isinstance(extra, dict) else None
return isinstance(options, dict) and bool(options.get("required"))
return options if isinstance(options, dict) else {}


def is_required(info: FieldInfo) -> bool:
"""True for a setting made with ``setting(required=True)``."""
return bool(_options(info).get("required"))


def helper_of(info: FieldInfo) -> str | None:
"""The web interface helper a setting names (``setting(helper=...)``), or ``None``."""
helper = _options(info).get("helper")
return helper if isinstance(helper, str) else None


def is_set(value: Any) -> bool:
Expand Down
Loading
Loading