Skip to content
Merged
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
32 changes: 25 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Every module needs a `manifest.json`:
| `version` | string | Semantic version (`major.minor.patch`) |
| `author` | string | Your GitHub username |
| `minAppVersion` | string | Minimum DOCSight version (currently `2026.2`) |
| `type` | string | One of: `integration`, `analysis`, `theme` |
| `type` | string | One of: `driver`, `integration`, `analysis`, `theme` |
| `contributes` | object | What this module provides (see [Contribution Types](#contribution-types)) |

#### Optional Fields
Expand All @@ -98,6 +98,7 @@ Every module needs a `manifest.json`:
| `license` | string | SPDX license identifier (e.g., `MIT`) |
| `config` | object | Default configuration values ([details](#config-defaults)) |
| `config_secrets` | array | Module-owned sensitive config keys ([details](#module-owned-secrets)) |
| `hints` | object | Driver connection form hints ([details](#driver-modem-support)) |
| `menu` | object | Sidebar navigation entry ([details](#menu-entry)) |

#### Menu Entry
Expand Down Expand Up @@ -146,7 +147,7 @@ Use `config_secrets` for module passwords, API tokens, and other sensitive setti
- Every key in `config_secrets` must also exist in the same manifest's `config` object with a string default, normally `""`; non-string defaults are rejected so encrypted values never enter scalar coercion.
- `config_secrets` must be a list of unique strings.
- Secret keys are encrypted at rest and masked in Settings after they are saved.
- Community collectors can read only their own declared secret keys; they still cannot read DOCSight core secrets such as modem passwords or global API tokens.
- The collector config proxy exposes only the module's own declared secret keys and hides core secrets. This API boundary is not a Python sandbox; installed code must be trusted.
- Settings templates should render secret fields as empty password/token inputs with `data-config-secret="true"`. Add `data-saved-secret="true"` only when the masked config value indicates an existing saved secret. This explicit metadata makes an untouched field post the mask while an edited field posts the new value. Do not write saved secret values back into HTML `value` attributes.

The manifest capability contract is owned by DOCSight core rather than duplicated in this catalog. With sibling checkouts, run `python3 ../docsight/app/manifest_contract.py .`; catalog CI performs the same check against current core before running the registry-specific validator.
Expand Down Expand Up @@ -189,6 +190,20 @@ def api_data():
return jsonify({...})
```

### `driver`: Modem Support

Community modem driver support is restored on main and in images built from this fix. The tagged v2026-09-16.1 release does not include this restoration.

Declare `"type": "driver"` and `"contributes": {"driver": "driver.py:ExampleDriver"}`. The entry point must be a plain module-local Python filename and a concrete `ModemDriver` subclass implementing `login()`, `get_docsis_data()`, `get_device_info()`, and `get_connection_info()`. Inherit the constructor `(url, user, password)` or provide a compatible one. Relative imports such as `from .helper import parse_channels` work within the module directory.

The manifest `id` is the modem selection key, for example `community.example`. IDs use lowercase letters, digits, dots and underscores, start with a letter and must fit the browser's 128-character driver-key limit. Use a unique community prefix for new modem support. To explicitly override a built-in driver, use its exact key as the module ID, for example `fritzbox`. No separate override field is needed. The enabled module supplies the implementation and displayed name for that key within its application; the global built-in catalog is unchanged. If `hints` is omitted or empty, the built-in hints are inherited. Nonempty module hints replace the complete built-in hint object rather than merging with it. Community constructors receive only `(url, user, password)`, without built-in-specific keyword arguments. Only enabled modules whose complete contribution plan validates are registered. Restart after installing, enabling or disabling a module. The driver then appears in Settings > Extensions with a Community badge and in the setup/settings modem choices. Select it and save the connection settings. If an override is disabled, removed or rejected, a newly started application uses the original built-in implementation, name and hints for that key. If an unavailable module has its own key with no built-in fallback, connection tests and polling fail safely. The saved selection is retained in both cases.

Driver contributions cannot be combined with collectors or publishers, even under another module type. Themes cannot contribute drivers. These are contribution rules, not a sandbox: community modules execute trusted Python in the DOCSight process and drivers receive modem credentials. Install only code you trust; these rules do not prevent malicious code from accessing data or the network.

Optional `hints` can contain boolean `needs_user`, `needs_password`, `username_required`, `credentials_required`, and string or null `default_url`, `default_user`, `url_hint`, `user_hint`, `password_hint`. A nonempty `default_url` must be an HTTP(S) URL without embedded credentials. Invalid hints reject the module before they can break setup/settings initialization.

Copy [`TEMPLATE-DRIVER/`](TEMPLATE-DRIVER/) for a minimal, inert starting point. Threshold profiles, including VFKD thresholds, remain `analysis` modules.

### `collector` — Scheduled Data Collection

```json
Expand Down Expand Up @@ -219,7 +234,7 @@ class MyCollector(Collector):

DOCSight passes `config_mgr`, `storage`, and `web` to every module collector. The base class provides exponential backoff on repeated failures (30s to 3600s max, auto-reset after 24h idle).

> **Note:** Community modules receive a `_ModuleConfigProxy` instead of the raw `ConfigManager`. This proxy hides DOCSight core secret keys such as modem passwords and API tokens. Declare module-owned passwords or tokens in `config_secrets`; only those declared keys are readable by that module's collector. Keep saved secret values out of rendered HTML and handle them through dedicated password/token fields.
> **Note:** Community modules receive a `_ModuleConfigProxy` instead of the raw `ConfigManager`. This proxy hides DOCSight core secret keys such as modem passwords and API tokens. Declare module-owned passwords or tokens in `config_secrets`; only those declared secret keys are exposed through that proxy. Keep saved secret values out of rendered HTML and handle them through dedicated password/token fields.

### `publisher` — Data Export (e.g., MQTT)

Expand Down Expand Up @@ -385,7 +400,7 @@ A theme module provides dark and light color schemes. The `theme.json` must cont

Only one theme can be active at a time. Users select themes in Settings > Appearance.

**Security restriction:** Theme modules cannot contribute `collector`, `routes`, or `publisher`.
**Security restriction:** Theme modules cannot contribute `collector`, `routes`, `publisher`, or `driver`.

### `static` — CSS & JavaScript

Expand Down Expand Up @@ -447,7 +462,7 @@ The Smart Capture engine is wired to your collector in `main.py` after `discover
image: ghcr.io/itsdnns/docsight:latest
volumes:
- docsight_data:/data
- ./modules:/modules # <-- add this line
- ./modules:/data/modules
ports:
- "8765:8765"
```
Expand All @@ -466,17 +481,19 @@ The Smart Capture engine is wired to your collector in `main.py` after `discover
docker compose restart docsight
```

4. **Verify** in Settings > Modules — your module should appear with a "Community" badge.
4. **Verify** in Settings > Extensions. Your module should appear with a "Community" badge.

5. **Check logs** for any loading errors:

```bash
docker compose logs docsight | grep -i module
```

The standard persistent module path is `/data/modules`. An existing mount at `/modules` requires `MODULES_DIR=/modules` explicitly.

### Error Handling

DOCSight never crashes due to a broken module:
Contribution resolution failures reject the complete module plan while other modules continue loading. Installed Python code remains trusted:

- Invalid manifests are skipped with a warning
- Load failures are caught per-module and stored as error state
Expand Down Expand Up @@ -559,6 +576,7 @@ Before opening your PR, verify:

| Type | Purpose | Example |
|------|---------|---------|
| `driver` | Modem support | [Minimal driver template](TEMPLATE-DRIVER/), explicit same-ID built-in override |
| `integration` | External service connection | Ping test, uptime monitor, API bridge |
| `analysis` | Data analysis/visualization | Custom charts, reports, threshold profiles |
| `theme` | UI customization | Color schemes, layouts |
Expand Down
12 changes: 12 additions & 0 deletions TEMPLATE-DRIVER/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Driver template

Community modem driver support is restored on main and in images built from this fix. The tagged v2026-09-16.1 release does not include this restoration. Replace `NEXT_RELEASE` in `minAppVersion` with the released version containing this restoration before publishing your module.

1. Copy this folder to `/data/modules/community.example_driver` in DOCSight's persistent data volume, or bind-mount it there.
2. Set the manifest ID, name, author and description. The ID is the modem selection key: use a unique community prefix for new support, or the exact built-in key for an explicit override. For overrides, absent or empty hints inherit the built-in hints; nonempty hints replace them entirely. Disabling or removing the override restores the built-in implementation, name and hints after restart.
3. Implement the four methods in `driver.py`, importing `ModemDriver` from `app.drivers.base`. The inherited constructor receives URL, username and password. Relative imports of sibling helpers are supported.
4. Restart DOCSight, enable the module in Settings > Extensions if needed and restart again. Select your driver in the modem settings and save.

The example performs no network requests and returns empty channel lists. A successful connection test with this template only confirms that the driver loaded. Replace its methods before using it for monitoring, and set credential hints appropriate to the modem. See the core [Adding Modem Support guide](https://github.com/itsDNNS/docsight/wiki/Adding-Modem-Support) for channel formats.

An old `/modules` mount requires `MODULES_DIR=/modules`. Driver code runs as trusted Python with access to modem credentials. The collector/publisher contribution restrictions are not a sandbox. See the [driver contract](../README.md#driver-modem-support).
1 change: 1 addition & 0 deletions TEMPLATE-DRIVER/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Example community modem driver package."""
20 changes: 20 additions & 0 deletions TEMPLATE-DRIVER/driver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""Minimal inert driver; replace these methods with your modem implementation."""

from app.drivers.base import ModemDriver


class ExampleDriver(ModemDriver):
def login(self):
pass

def get_docsis_data(self):
return {
"channelDs": {"docsis30": [], "docsis31": []},
"channelUs": {"docsis30": [], "docsis31": []},
}

def get_device_info(self):
return {"model": "Example Modem", "sw_version": "unknown"}

def get_connection_info(self):
return {}
15 changes: 15 additions & 0 deletions TEMPLATE-DRIVER/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "community.example_driver",
"name": "Example Driver",
"description": "Minimal modem driver template",
"version": "1.0.0",
"author": "your-github-username",
"minAppVersion": "NEXT_RELEASE",
"type": "driver",
"contributes": {
"driver": "driver.py:ExampleDriver"
},
"hints": {
"credentials_required": false
}
}
2 changes: 1 addition & 1 deletion registry.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
},
"type": {
"type": "string",
"enum": ["integration", "analysis", "theme"],
"enum": ["driver", "integration", "analysis", "theme"],
"description": "Module type"
},
"download_url": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
}
REGISTRY_OPTIONAL_FIELDS = {"verified"}
REGISTRY_ALLOWED_FIELDS = REGISTRY_REQUIRED_FIELDS | REGISTRY_OPTIONAL_FIELDS
ALLOWED_TYPES = {"integration", "analysis", "theme"}
ALLOWED_TYPES = {"driver", "integration", "analysis", "theme"}


def load_json(path: Path, errors: list[str]) -> object | None:
Expand Down
5 changes: 2 additions & 3 deletions tests/test_validate_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,14 @@ def test_registry_id_must_match_manifest_id(self):

self.assertTrue(any("manifest ID" in error and "community.sample" in error for error in errors))

def test_removed_driver_type_is_rejected_for_registry_and_manifest(self):
def test_driver_type_is_accepted_for_registry_and_manifest(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
make_valid_fixture(root, registry_type="driver", manifest_type="driver")

errors = validate_repository(root)

self.assertTrue(any("invalid type 'driver'" in error for error in errors))
self.assertTrue(any("manifest type 'driver' is not supported" in error for error in errors))
self.assertEqual(errors, [])

def test_registry_type_must_match_manifest_type(self):
with tempfile.TemporaryDirectory() as tmp:
Expand Down
Loading