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
9 changes: 9 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ Reference material for Zaparoo Core's architecture, APIs, and subsystems. For de
- **Thread-safe**: `config.Instance` uses `syncutil.RWMutex`
- Maintain backward compatibility — use migrations for breaking changes

## Profiles

Device profiles are named buckets of preferences and limits, with no passwords or accounts. See `pkg/service/profiles/`.

- **Active profile**: one per device, held as a snapshot in service state (`pkg/service/state/`) and persisted in the UserDB `DeviceState` table so it survives restarts. No active profile = pre-profiles behavior exactly.
- **Switching**: via API (`profiles.switch`, PIN-checked) or by scanning a card containing `**profile.switch:<switchId>`. The switch ID is a word phrase (e.g. `corn-arm-truck`) generated from an embedded wordlist — a selector, never a credential. Card scans bypass the PIN: possession of the card is the authorization. PINs gate entry only; deactivating is always free.
- **Playtime limits**: profiles can override the global daily/session limits. `pkg/service/playtime.LimitsManager` reads limits through a `LimitsProvider`; the profile-aware resolver (`pkg/service/profiles.LimitsResolver`) layers the active profile's overrides over global config. Daily usage accounting is scoped to the active profile via the `ProfileID` column on `MediaHistory` (rows are attributed at launch time). Switching profiles resets the limit session.
- **Require-profile gate**: the `[profiles] require_for_launch` config setting blocks media launches while no profile is active (profile switch commands still run, so scanning a card unparks the device).

## Reader Auto-Detection

10 reader types: acr122pcsc, externaldrive, file, libnfc, mqtt, opticaldrive, pn532, rs232barcode, simpleserial, tty2oled
118 changes: 118 additions & 0 deletions docs/api/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,124 @@ Returns `null` on success.
}
```

## Profiles

Profiles are lightweight device profiles: named buckets of preferences and limits, with no passwords or accounts. One profile is active per device at a time, switched via the API or by scanning an NFC card containing the profile's switch ID (`**profile.switch:<switchId>`).

A profile may have an optional 4-8 digit PIN. Switching to a PIN-protected profile via the API requires the PIN; scanning the profile's physical card bypasses it (possession of the card is the authorization). Leaving a profile is always free — PINs gate entry only. To prevent a profile-less device from being an escape hatch, enable the `profilesRequireForLaunch` setting (see [settings](#settings)), which blocks media launches while no profile is active.

When no profile is active the device behaves exactly as it did before profiles existed: global playtime limits apply and history is unattributed.

##### Profile object

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix heading level jump in the Profiles section.

Line 2458 jumps from ## to #####, which violates markdown heading increment rules and hurts generated outline/navigation.

Suggested diff
-##### Profile object
+### Profile object
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
##### Profile object
### Profile object
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 2458-2458: Heading levels should only increment by one level at a time
Expected: h3; Actual: h5

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api/methods.md` at line 2458, The "Profile object" heading is using too
many hashes (shown as "##### Profile object") and jumps from the previous "##"
level; update that heading to the correct level to follow the surrounding
section hierarchy (e.g., change "##### Profile object" to "### Profile object"
or the appropriate level used by the nearby "Profiles" section) so the markdown
outline/navigation is consistent; locate the literal "Profile object" heading in
docs/api/methods.md and adjust the hash count to match the surrounding headings.

Source: Linters/SAST tools


| Key | Type | Required | Description |
| :------------ | :------ | :------- | :------------------------------------------------------------------------------------------------------- |
| profileId | string | Yes | Unique identifier of the profile. |
| name | string | Yes | Display name, e.g. "Dad" or "Kid A". |
| switchId | string | Yes | Word phrase written to profile switch cards, e.g. `corn-arm-truck`. A selector, not a secret. |
| hasPin | boolean | Yes | True when the profile has a PIN set. The PIN itself is never returned. |
| limitsEnabled | boolean | No | Playtime limits enabled override. Omitted = inherit the global setting. |
| dailyLimit | string | No | Daily playtime limit override as a duration string (e.g. `2h30m`). Omitted = inherit; `0` = unlimited. |
| sessionLimit | string | No | Session playtime limit override as a duration string. Omitted = inherit; `0` = unlimited. |
| createdAt | number | Yes | Unix timestamp of profile creation. |
| lastUpdatedAt | number | Yes | Unix timestamp of last modification. |

### profiles

List all profiles.

#### Parameters

None.

#### Result

| Key | Type | Required | Description |
| :------- | :--------------------------- | :------- | :---------------- |
| profiles | [Profile](#profile-object)[] | Yes | List of profiles. |

### profiles.new

Create a new profile. The switch ID is generated automatically and returned in the result — write it to a card as `**profile.switch:<switchId>`.

#### Parameters

| Key | Type | Required | Description |
| :------------ | :------ | :------- | :---------------------------------------------------------------- |
| name | string | Yes | Display name. |
| pin | string | No | Optional 4-8 digit PIN required to switch to this profile via API. |
| limitsEnabled | boolean | No | Playtime limits enabled override. |
| dailyLimit | string | No | Daily limit duration override. |
| sessionLimit | string | No | Session limit duration override. |

#### Result

The created [profile object](#profile-object).

### profiles.update

Update a profile. Omitted fields are unchanged. If the updated profile is currently active, its limit changes apply immediately.

#### Parameters

| Key | Type | Required | Description |
| :----------------- | :------ | :------- | :---------------------------------------------------------------------- |
| profileId | string | Yes | Profile to update. |
| name | string | No | New display name. |
| pin | string | No | Set or replace the PIN. |
| clearPin | boolean | No | Remove the PIN. |
| limitsEnabled | boolean | No | Playtime limits enabled override. |
| dailyLimit | string | No | Daily limit duration override. |
| sessionLimit | string | No | Session limit duration override. |
| clearLimits | boolean | No | Reset all limit overrides back to inheriting the global config. |
| regenerateSwitchId | boolean | No | Issue a new switch ID (lost-card replacement). Old cards stop working. |

#### Result

The updated [profile object](#profile-object).

### profiles.delete

Delete a profile. If it is the active profile, the device deactivates. Past play history keeps its attribution to the deleted profile.

#### Parameters

| Key | Type | Required | Description |
| :-------- | :----- | :------- | :----------------- |
| profileId | string | Yes | Profile to delete. |

#### Result

Null.

### profiles.active

Get the device's currently active profile.

#### Parameters

None.

#### Result

The active profile (a subset of the [profile object](#profile-object) without `switchId` and timestamps), or null when no profile is active.

### profiles.switch

Switch the device's active profile. Switching to a PIN-protected profile requires its PIN, whether selected by `profileId` or `switchId` — only physical card scans bypass the PIN. Calling with neither `profileId` nor `switchId` deactivates the current profile, which never requires a PIN.

#### Parameters

| Key | Type | Required | Description |
| :-------- | :----- | :------- | :------------------------------------------------------ |
| profileId | string | No | Profile to activate, by ID. |
| switchId | string | No | Profile to activate, by switch ID. |
| pin | string | No | The profile's PIN, when one is set. |

#### Result

The new active profile, or null when deactivated.

## Mappings

Mappings are used to modify the contents of tokens before they're launched, based on different types of matching parameters. Stored mappings are queried before every launch and applied to the token if there's a match. This allows, for example, adding ZapScript to a read-only NFC tag based on its UID.
Expand Down
32 changes: 32 additions & 0 deletions docs/api/notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -408,3 +408,35 @@ Sent when a new inbox message is added to the server.
}
}
```

## Profiles

### profiles.active

Sent when the device's active profile changes, including deactivation.

#### Parameters

| Key | Type | Required | Description |
| :------ | :----- | :------- | :------------------------------------------------------------------ |
| profile | object | Yes | The new active profile, or null when the device deactivated. |

Comment on lines +420 to +423

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document profile as nullable in the parameter type.

Line 420 currently lists profile as object, but the description says it may be null when deactivated. Please make the type explicit (object | null) to keep the contract unambiguous.

Suggested diff
-| profile | object | Yes      | The new active profile, or null when the device deactivated.       |
+| profile | object \| null | Yes | The new active profile, or null when the device deactivated.       |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| Key | Type | Required | Description |
| :------ | :----- | :------- | :------------------------------------------------------------------ |
| profile | object | Yes | The new active profile, or null when the device deactivated. |
| Key | Type | Required | Description |
| :------ | :----- | :------- | :------------------------------------------------------------------ |
| profile | object \| null | Yes | The new active profile, or null when the device deactivated. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/api/notifications.md` around lines 420 - 423, The docs declare the
notifications parameter "profile" with type "object" but its description says it
can be null; update the parameter type to explicitly show it is nullable (use
"object | null") so the contract is unambiguous—edit the table row for the
"profile" field in the notifications API docs and change the Type column from
"object" to "object | null" while keeping the existing description.

The profile object contains `profileId`, `name`, `hasPin` and any playtime limit overrides (`limitsEnabled`, `dailyLimit`, `sessionLimit`).

#### Example

```json
{
"jsonrpc": "2.0",
"method": "profiles.active",
"params": {
"profile": {
"profileId": "1ad28b9a-7aef-11ef-9817-020304050607",
"name": "Kid A",
"hasPin": true,
"limitsEnabled": true,
"dailyLimit": "2h"
}
}
}
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/Microsoft/go-winio v0.6.2
github.com/ZaparooProject/go-pn532 v0.22.1
github.com/ZaparooProject/go-zapscript v0.13.0
github.com/ZaparooProject/go-zapscript v0.14.0
github.com/adrg/xdg v0.5.3
github.com/andygrunwald/vdf v1.1.0
github.com/bendahl/uinput v1.7.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ github.com/TheTitanrain/w32 v0.0.0-20200114052255-2654d97dbd3d h1:2xp1BQbqcDDaik
github.com/TheTitanrain/w32 v0.0.0-20200114052255-2654d97dbd3d/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I=
github.com/ZaparooProject/go-pn532 v0.22.1 h1:Dtuc+sXYZtuNZP+8/DQv68V1MOHUxRAOMVYsqvVFAfQ=
github.com/ZaparooProject/go-pn532 v0.22.1/go.mod h1:NwYx5IE0zAU70ZikNpoPiOF5MUlDn3fD8xImpZixW1k=
github.com/ZaparooProject/go-zapscript v0.13.0 h1:qiYhSoVzenFvmAeU+b0AwFYT5jAmiC7RMwNroYtBl2o=
github.com/ZaparooProject/go-zapscript v0.13.0/go.mod h1:Z3rFyQq/GA+ESpYUtCOA/2Xyftbygv4MfDCajOVDmag=
github.com/ZaparooProject/go-zapscript v0.14.0 h1:DJp4KsbqDN2My/mwH3DBc4MXBfmeabe8PWjurlRpRnM=
github.com/ZaparooProject/go-zapscript v0.14.0/go.mod h1:Z3rFyQq/GA+ESpYUtCOA/2Xyftbygv4MfDCajOVDmag=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/andygrunwald/vdf v1.1.0 h1:gmstp0R7DOepIZvWoSJY97ix7QOrsxpGPU6KusKXqvw=
Expand Down
Loading
Loading