18 KiB
Content Item Types
All content.json files use an items array of typed objects. Every object has a type field; all other fields depend on the type. Optional fields are marked with ?.
Token substitution is available in most string fields: any %TOKEN% placeholder is replaced with the corresponding value. Where noted, a string that resolves to a JSON array or object is automatically parsed back into the appropriate structure.
Access control
Any item (and any table column or row action) can carry a client_requirement field. Items whose requirement is not satisfied are omitted entirely from the rendered output.
Roles are ranked from lowest to highest:
| Rank | Role | Description |
|---|---|---|
| 3 | manager |
Superuser; can manage accounts and administrators |
| 2 | administrator |
Full configuration access |
| 1 | viewer |
Read-only authenticated user |
| 0 | nothing |
Unauthenticated (no session) |
The value is a role name with a suffix that controls the comparison:
| Suffix | Meaning |
|---|---|
+ |
Client rank must be greater than or equal to the named role |
= |
Client rank must equal the named role exactly |
- |
Client rank must be less than or equal to the named role |
Possible values for client_requirement field:
| Value | Meaning |
|---|---|
client_is_manager= |
Client must be manager |
client_is_manager- |
Client must be manager or lower |
client_is_administrator+ |
Client must be administrator or higher |
client_is_administrator= |
Client must be administrator |
client_is_administrator- |
Client must be administrator or lower |
client_is_viewer+ |
Client must be viewer or higher |
client_is_viewer= |
Client must be viewer |
client_is_viewer- |
Client must be viewer or lower |
client_is_nothing+ |
Client may be anybody (logged in or not logged in) |
client_is_nothing= |
Client must be nothing (not logged in) |
Example:
{ "client_requirement": "client_is_administrator+", "type": "card", "items": [] }
Layout / structure
h1
{ "type": "h1", "text": "Page Title" }
Renders as <h1>.
hr
{ "type": "hr" }
Full-width horizontal rule.
p
{
"type": "p",
"text": "Introductory text.",
"link?": { "text": "Click here", "action": "/some-path" }
}
Paragraph with an optional inline link appended after the text.
spacer
{ "type": "spacer" }
Blank vertical space (<div class="spacer">).
header_page_title
{
"type": "header_page_title",
"items": [ ... ]
}
Wraps children in the page header div. Typically contains h1 and toolbar buttons.
section
{
"type": "section",
"items": [ ... ]
}
Generic section wrapper (<div class="section">).
auth_wrapper
{
"type": "auth_wrapper",
"items": [ ... ]
}
Full-page centering wrapper for login/auth pages.
auth_card
{
"type": "auth_card",
"items": [ ... ]
}
Contained card used on auth pages.
card
{
"type": "card",
"label?": "Card Title",
"id?": "my-card-id",
"hidden?": true,
"items": [ ... ]
}
General-purpose card with an optional header label. Setting hidden: true renders the card with the hidden attribute, which is used for js_edit reveal targets. The id attribute wires it up as a js_edit target.
grid
{
"type": "grid",
"rows": [
{ "cells": [ { "type": "grid_label", ... }, { "type": "grid_value", ... } ] }
]
}
Two-column info grid. Each row contains cells rendered inline.
grid_label
{ "type": "grid_label", "text": "Label" }
Label cell in an info grid row.
grid_value
{ "type": "grid_value", "text": "%TOKEN%" }
Value cell in an info grid row. Token substitution applies.
raw_html
{ "type": "raw_html", "html": "<strong>Some markup</strong>" }
Injects raw HTML without escaping. Token substitution applies. Use sparingly.
Buttons
All button types share these optional fields:
| Field | Meaning |
|---|---|
text |
Button label |
class? |
Extra CSS classes appended to the default |
disabled? |
Token expression; disables button if truthy (not "false" / "0") |
button_primary / button_secondary / button_danger / button_ghost
{
"type": "button_primary",
"text": "Save",
"action?": "/some/path",
"method?": "post",
"formaction?": "/alternate/path",
"disabled?": "%SOME_TOKEN%"
}
- No
action: renders<button type="submit">inside an existing form. action+method: "post": wraps in a standalone<form method="post">.actiononly (no method): renders<a href="...">styled as a button.formaction: overrides the form action on this submit button (for multi-target forms).
button_cancel
{ "type": "button_cancel", "text?": "Cancel", "class?": "extra-cls" }
Always rendered as a disabled button (btn-secondary). JS enables it when the form becomes dirty.
button_row
{
"type": "button_row",
"justify?": "flex-end",
"items": [ ... ]
}
Flex container for buttons. Children are typically button types. justify sets justify-content.
Stat cards
stat_card_grid
{
"type": "stat_card_grid",
"items": [ ... ]
}
Responsive grid container for stat cards.
stat_card
Three variants:
Read-only:
{
"type": "stat_card",
"label": "Active Sessions",
"value": "%VPN_SESSION_COUNT%",
"sub?": "Secondary text",
"variant?": "wide"
}
Inline editable:
{
"type": "stat_card",
"label": "Max Log Size",
"value": "%GENERAL_LOG_MAX_KB%",
"edit_action": "/action/update_log_max_kb",
"edit_field": "log_max_kb",
"edit_input_type?": "number",
"edit_value?": "%GENERAL_LOG_MAX_KB%",
"edit_suffix?": "KB",
"edit_min?": "64"
}
Reveal (opens a card):
{
"type": "stat_card",
"label": "VPN Subnet",
"value": "%VPN_SUBNET%",
"reveal_card_id": "edit-vpn-subnet-card"
}
Info / status
info_bar
{
"type": "info_bar",
"variant": "info",
"text": "Some note about this section."
}
Renders inline (not as a full-width layout bar). Variants: info, warning, danger.
field_status
{
"type": "field_status",
"label": "WAN Interface",
"value": "%GENERAL_WAN_STATUS%"
}
Form-group layout with a badge showing interface state (UP/DOWN/INVALID/other).
pre_block
{
"type": "pre_block",
"text": "%LOG_OUTPUT%",
"scroll_to_bottom?": true
}
<pre> block for log output. Setting scroll_to_bottom: true adds a data-scroll-bottom attribute that JS uses to auto-scroll.
Forms
form
{
"type": "form",
"action": "/action/save_settings",
"method?": "post",
"items": [ ... ]
}
HTML form. Automatically injects:
config_hashhidden input (CSRF / stale-config detection)original_valueshidden input (JSON map of field names to original values, used for change detection)
Also generates a validation <script> block if any child fields have validate or number input type.
hidden
{ "type": "hidden", "name": "provider", "value": "%PROVIDER%" }
Hidden input. Token substitution applies.
field
The general form field. Behavior is driven by input_type.
Common fields:
{
"type": "field",
"input_type": "text",
"label": "Hostname",
"name": "hostname",
"value?": "%HOSTNAME%",
"placeholder?": "e.g. myhost.example.com",
"hint?": "Helper text shown below the input.",
"readonly?": true,
"validate?": "hostname",
"client_requirement?": "client_is_administrator+"
}
input_type values:
| Value | Renders | Extra fields |
|---|---|---|
text (default) |
<input type="text"> |
validate?, depends? |
password |
<input type="password"> |
validate?, depends? |
number |
<input type="number"> |
min?, max?, validate? (default positive_int), depends?, layout? ("inline") |
checkbox |
Toggle checkbox | checkbox_label? (inline label next to box) |
checkbox_group |
Multiple checkboxes | options: JSON array of {value, label} |
select |
<select> |
options: JSON array of {value, label} or token string; validate?, depends? |
textarea |
<textarea> |
rows?, validate? |
interface_picker |
Table picker for network interfaces | data?: token resolving to JSON array of interface rows; value? |
depends is a list of field names whose values are sent as data-depends for JS-driven conditional validation.
field_row
{
"type": "field_row",
"cols?": 2,
"items": [ ... ]
}
Horizontal row of fields in cols equal columns (default 2). Children are typically field items.
subnet_row
{
"type": "subnet_row",
"label?": "Subnet",
"subnet_name?": "subnet",
"prefix_name?": "subnet_mask",
"subnet_value?": "%VLAN_SUBNET%",
"prefix_value?": "24",
"subnet_placeholder?": "e.g. 192.168.1.0"
}
Combined subnet address and prefix length inputs. Displays the dotted mask equivalent (e.g. /24 becomes 255.255.255.0) as a hint below. Auto-validates via JS.
select
{
"type": "select",
"name": "filter_field",
"options": "<option value=\"a\">A</option>",
"filter_col?": "status"
}
Bare <select> outside a form-group. options is raw HTML. filter_col wires it to filter a sibling table column via JS.
record_editor
{
"type": "record_editor",
"label": "Hostnames",
"name": "hostnames",
"empty_message?": "No hostnames added.",
"fields": [
{
"label": "Hostname",
"name": "hostname",
"placeholder?": "e.g. myhost.example.com",
"required?": true,
"validate?": "hostname",
"valtype?": "string",
"attrs?": { "data-custom": "value" }
}
]
}
JS-driven table where the user can add, edit, and remove rows. Submits as a JSON array in the named hidden input. Each entry in fields gets its own column and a corresponding input in the add/edit form.
readonly_select
{
"type": "readonly_select",
"label?": "Gateway",
"name?": "gateway"
}
Disabled <select> placeholder shown while dependent data is loading. JS replaces it with real options once identities are available.
overridable_textarea
{
"type": "overridable_textarea",
"label": "Custom Rules",
"name": "custom_rules",
"override_name?": "custom_rules_override",
"validate?": "ip_in_subnet"
}
Read-only textarea with an "Override" checkbox. Checking the box enables editing. override_name is the checkbox's name attribute and defaults to {name}_override.
editable_list
{
"type": "editable_list",
"name": "blocklist_urls",
"item_placeholder?": "https://...",
"add_label?": "Add URL",
"hint?": "One URL per entry.",
"items?": "%BLOCKLIST_URLS_JSON%"
}
Simple list where the user can add and remove text items. Submits as a JSON array.
credential_fields
{
"type": "credential_fields",
"provider_select?": "provider"
}
Group of hidden credential sub-groups (API token vs. No-IP username/password). JS shows the correct group based on the named provider <select>. provider_select is the name of that select and defaults to "provider".
Validation
The validate field attaches a named validator to an input. Validators run live as the user types, show inline hint messages, and keep the form's submit button disabled until all validated fields pass.
Three mechanisms exist depending on context:
field,editable_list: setsdata-validateon the input. The named classifier runs on every keystroke and marks the field valid, incomplete (warning), or invalid (error).overridable_textarea: setsdata-validate-lineson the textarea. When the override checkbox is checked, each non-empty line is validated as an IP address. If subnet context is available (from sibling subnet/mask inputs), each line is also checked against that subnet.record_editorfields: usevaltypeinstead ofvalidate. The field is wired to sibling subnet/mask inputs viadata-dep-subnetanddata-dep-maskattributes.
validate values (for field and editable_list)
| Value | Accepts |
|---|---|
ipv4 |
IPv4 address. Four dot-separated octets, each 0-255. |
mac |
MAC address. Six colon-separated two-digit hex groups. |
url |
HTTP or HTTPS URL. Must begin with http:// or https://, have a valid hostname, and an optional port in range 1-65535. |
port |
Port number. Integer 1-65535, digits only. |
ipv4cidr |
IPv4 address or CIDR notation. Accepts a bare IP or IP/prefix where prefix is 0-32. |
endpoint |
Network endpoint. Accepts an IPv4 address, IPv6 address, or hostname. Hostnames may include a :port suffix. |
dashname |
Dash-delimited identifier. Lowercase letters, digits, and hyphens. No leading, trailing, or consecutive hyphens. |
domainname |
Domain name. Letters, digits, hyphens, and dots. Each label must not start or end with a hyphen. |
networkname |
Network identifier. Letters, digits, hyphens, and underscores. No leading, trailing, or consecutive special characters. |
time_24h |
24-hour time. Format HH:MM. Hours 00-23, minutes 00-59. |
positive_int |
Positive integer. Digits only. Respects the field's min and max bounds when present. |
vlan_id |
VLAN ID. Integer 1-4094. Also checks uniqueness against existing IDs read from the input's data-existing-ids attribute. |
validate value (for overridable_textarea)
| Value | Behavior |
|---|---|
ip_in_subnet |
Each non-empty line must be a valid IPv4 address. When the VLAN subnet and mask are both filled in, also verifies each address falls within that subnet. |
valtype values (for record_editor fields)
valtype wires subnet-aware real-time validation using sibling inputs pointed to by data-dep-subnet and data-dep-mask attributes on the field.
| Value | Validates |
|---|---|
address |
IPv4 host address. Must be a valid IP within the known subnet, and must not be the network or broadcast address. |
subnet |
IPv4 subnet address. All host bits must be zero for the given mask. |
mask |
Prefix length. Integer 1-30. |
format |
IPv4 address format only. No subnet membership check. |
Table
table
{
"type": "table",
"datasource": "config:banned_ips",
"empty_message?": "No banned IPs.",
"columns": [ ... ],
"row_actions?": [ ... ],
"toolbar?": { "client_requirement?": "...", "items": [ ... ] }
}
Datasource prefixes:
| Prefix | Source |
|---|---|
config:<name> |
Reads from config.json. Available names: vlans, banned_ips, host_overrides, port_forwardings, ddns_providers, upstream_dns_servers, accounts |
live:<name> |
Live system data. Available names: dhcp_leases, vpn_sessions |
Toolbar renders content items (typically buttons) above the table, gated by client_requirement.
Column definition
{
"label": "Enabled",
"field": "enabled",
"class?": "col-center",
"render?": "badge_enabled_disabled",
"render_options?": { "title_true": "...", "title_false": "..." },
"toggle_action?": "/action/toggle_vlan",
"client_requirement?": "client_is_administrator+"
}
render values:
| Value | Output |
|---|---|
| (none) | Escaped plain text |
badge_enabled_disabled |
Green Enabled / red Disabled badge |
badge_yes_no |
Green Yes / red No badge. render_options.title_true and title_false add tooltips |
badge_recording_on_off |
Green Recording On / red Recording Off badge |
badge_toggle |
Badge that submits a POST to toggle_action when clicked (if allowed by client_requirement) |
badge_active_inactive |
active gets a green badge, pending gets yellow, anything else gets grey |
raw_html |
Value injected as raw HTML (no escaping) |
tag_list |
JSON array of tag objects {n, d?, short?, mini?} rendered as pill tags |
interface_status |
UP gets a green badge, DOWN gets yellow, INVALID gets red |
Row action definition
{
"method": "post",
"text": "Delete",
"action": "/action/delete_ban",
"class?": "btn-danger btn-sm",
"client_requirement?": "client_is_administrator+",
"disable_if?": { "field": "status", "value": "locked" }
}
method values:
"post" renders a form with row_index and config_hash hidden inputs. disable_if can disable the button when row[field] == value.
"js_edit" opens a reveal card by ID. Row data is passed as data-row JSON. The card element (a card item with matching id) is populated and shown by JS.
{
"method": "js_edit",
"text": "Edit",
"target": "edit-vlan-form"
}
"inline_edit" converts the table row into editable inputs in place. Fields are defined inline:
{
"method": "inline_edit",
"text": "Edit",
"action": "/action/save_host_override",
"fields": [
{
"label": "Hostname",
"name": "hostname",
"input_type": "text",
"validate?": "hostname"
},
{
"label": "Provider",
"name": "provider",
"input_type": "select",
"options": [
{ "value": "noip", "label": "No-IP" },
{ "value": "other", "label": "Other (API Token)" }
]
}
]
}
Non-standard input_type values in inline_edit fields (anything not in text, password, number, checkbox, select, textarea) require a table worker registered in JS. The factory emits a <script> block via registerTableWorker(id, impl) automatically when a non-standard type is detected. Currently the only non-standard inline type is:
input_type |
Page | What the worker renders |
|---|---|---|
credentials |
DDNS Accounts | Username/password or API token based on the row's provider field |
Navigation (navbar.json only)
These types are used only in app/navbar.json, not in page content.json files.
nav_item
A link or action in the navbar.
{
"type": "nav_item",
"label": "Dashboard",
"map_to": "overview",
"client_requirement?": "client_is_viewer+"
}
nav_action
Same as nav_item but uses an action field (POST) instead of a link.
{ "type": "nav_action", "label": "Logout", "action": "logout" }
nav_menu
Dropdown menu.
{
"type": "nav_menu",
"label": "Configure",
"items": [ ... ]
}
If label is "%MENU_LABEL%", it renders as "Configure" for administrators and above, or "View" for viewers.