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. Lines do not wrap; content scrolls horizontally when lines exceed the container width. Setting scroll_to_bottom: true adds a data-scroll-bottom attribute that JS uses to auto-scroll to the bottom on load and on update.
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",
"hint?": "Helper text shown below the select."
}
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",
"hint?": "Helper text shown below the textarea."
}
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 one or more VALIDATION_* flags 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. Flags are combined with |:
{ "type": "field", "name": "nat_ip", "validate": "VALIDATION_IPV4_FORMAT|VALIDATION_UNRESTRICTED" }
All three contexts use the same flag system:
field,editable_list: setsdata-validate(bitmask integer) on the input. Validation fires on every keystroke.overridable_textarea: setsdata-validate-lines. When the override checkbox is checked, each non-empty line is validated individually.record_editorfields:validate(or its legacy aliasvaltype) setsdata-validateon the per-field input.
number input_type defaults to VALIDATION_RANGE_INT when no validate is specified.
VALIDATION_* flags
| Flag | Accepts |
|---|---|
VALIDATION_IPV4_FORMAT |
IPv4 address. Four dot-separated octets, each 0-255. |
VALIDATION_IPV6_FORMAT |
IPv6 address. |
VALIDATION_SUBNET |
IPv4 subnet address. All host bits must be zero for the prefix length implied by sibling inputs. |
VALIDATION_ADDRESS |
IPv4 host address. Must fall within the subnet implied by sibling inputs and must not be the network or broadcast address. |
VALIDATION_MAC |
MAC address. Six colon-separated two-digit hex groups. |
VALIDATION_URL |
HTTP or HTTPS URL. Must begin with http:// or https:// and have a valid hostname. |
VALIDATION_PORT |
Port number. Integer 1-65535. |
VALIDATION_DASH_NAME |
Lowercase letters, digits, and hyphens. No leading, trailing, or consecutive hyphens. |
VALIDATION_NETWORK_NAME |
Letters, digits, hyphens, and underscores. No leading, trailing, or consecutive special characters. |
VALIDATION_DOMAIN_NAME |
Domain name. Labels of letters, digits, and hyphens separated by dots. No label may start or end with a hyphen. |
VALIDATION_TIME24H |
24-hour time. Format HH:MM. Hours 00-23, minutes 00-59. |
VALIDATION_RANGE_INT |
Integer. Respects the field's min and max bounds when present. Default for number input_type. |
VALIDATION_IPV4_CIDR |
Strict CIDR. A prefix is always required (192.168.1.0/24). Host bits must be zero. |
VALIDATION_IPV4_CIDRFLEX |
Flexible CIDR. Accepts a bare IPv4 address (yellow/incomplete when last octet is 0) or IP/prefix. |
VALIDATION_UNRESTRICTED |
IPv4 address must not fall within any restricted VLAN subnet. Factory automatically injects the current restricted subnets into data-existing-ids on the input. |
VALIDATION_IP_OR_DOMAIN_NAME |
IPv4 address or hostname/domain name. |
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.