Files
Anthias/website/layouts/_default/features.html
Viktor Petersson 2bb4e6df04 feat(webview): per-asset auto-refresh interval for webpage assets (#2841)
* feat(webview): per-asset auto-refresh interval for webpage assets

- Add metadata.refresh_interval_s to Asset, surfaced as a top-level field
  on AssetSerializerV2 (read) and as a write knob on Create/UpdateAssetSerializerV2;
  metadata stays read-only on the API so the upload pipeline keeps owning
  original_ext / transcoded / error_message
- Validate 0..86400 (24h cap); update merge preserves existing metadata keys
- Wire assets_update form handler to merge refresh_interval_s into metadata
- Add edit-modal field inside the existing Advanced section, only rendered
  for mimetype === 'webpage'; auto-expand Advanced when interval is set
- viewer.view_webpage now passes the interval through and always calls the
  new browser_bus.setReloadInterval slot (so an interval-only edit takes
  effect even when the URI is unchanged)
- Webview: QTimer in View; new setReloadInterval D-Bus slot on MainWindow;
  loadPage / loadImage clear the timer; reload() targets currentWebView
- Bump WebView default version to 2026.05.0 (D-Bus contract change)
- Tests: 7 v2 API (POST/PATCH round-trip, negative/over-cap rejection,
  pipeline-metadata preservation, 0-disables), 3 form-handler (merge,
  empty-clears, oversize-clamps), 3 viewer pass-through

Closes #2813

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(website): rewrite features page; mention webpage auto-refresh

Folds the in-progress copy refresh of the marketing features page
into the auto-refresh PR so the new "set an auto-refresh interval"
capability ships in the same change as the feature itself. The card
"Live web pages and dashboards" now explicitly mentions the per-asset
reload cadence backed by metadata.refresh_interval_s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(format): apply ruff format to test_viewer.py

CI's run-python-linter rejects the as-written current_browser_url
keyword arg wraps; ruff-format collapses them onto single lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): address four Copilot comments on PR #2841

- viewer: ``current_browser_url is not uri`` was an identity check;
  switch to ``!=`` so a same-text URL reconstructed each tick still
  short-circuits the loadPage hop.
- viewer: ``elif 'video' or 'streaming' in mime`` evaluated as
  ``elif 'video' or ('streaming' in mime)`` — the literal short-
  circuited and the branch ran for any mimetype not caught earlier,
  making the ``Unknown MimeType`` else arm unreachable. Use
  ``'video' in mime or 'streaming' in mime``.
- v2 serializer: clamp ``get_refresh_interval_s`` to ``>= 0`` so a
  hand-edited / legacy row with a negative metadata value never
  surfaces a value the write path would have rejected.
- webview: setReloadInterval was arming the QTimer immediately, but
  loadPage loads into nextWebView and currentWebView remains the
  *previous* page until the swap. A short interval could fire
  reload() on the prior page. Stash the cadence in
  ``pendingReloadIntervalS`` and arm in ``switchToNextWebView``
  once the new page is actually visible. The URL-unchanged path
  (no load in flight, ``pageLoadConnection`` empty) still arms
  immediately.

Tests: add a v2 GET test that a negative refresh_interval_s in
metadata is clamped to 0 on read.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): four more Copilot comments on PR #2841

- webview: drop the per-reload qDebug() — short intervals (5–10s)
  flooded journald to no diagnostic gain; failures already surface
  via loadFinished.
- webview: setReloadInterval clamps ``seconds`` to [0, 86400] before
  multiplying by 1000. Defends the ``int`` math in armReloadTimer
  from a hostile / buggy D-Bus caller — server-side validation
  already caps to 86400 but the D-Bus contract is trust-no-one.
- viewer: wrap ``browser_bus.setReloadInterval`` in try/except. An
  older AnthiasWebview (version skew during rollout, or a pinned
  WEBVIEW_VERSION) doesn't expose the slot; raising would abort the
  asset loop and black the screen. Log a warning, keep playing.
- v2 serializer: ``get_refresh_interval_s`` clamps the upper bound
  on read in addition to negatives, so a hand-edited / legacy row
  with 999999 surfaces as 86400 (the documented max) rather than
  echoing a value the next PATCH would 400 on.

Tests: parametrise the GET-clamp test to cover both bounds; add a
viewer test that ``setReloadInterval`` raising falls back gracefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(review): drop Copilot-attribution noise from inline comments

Strip "Per Copilot review" / "Per-Copilot" / PR-number references
introduced in the previous two review rounds. They don't help future
maintainers understand the code and rot once the PR is merged. Keep
the underlying technical rationale (string identity vs value
comparison, version-skew fallback, dual-buffer race for the timer
arm, GET-side clamp).

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(webview): add scheme to README D-Bus example URL

The webview only handles ``http://`` / ``https://`` schemes (anything
else gets handed to ``QUrl`` which would interpret a schemeless string
as a relative URL). Update the documented example so copy/paste works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(refresh-interval): centralise REFRESH_INTERVAL_S_MAX on the model

Hoist the 86400-second cap from api/serializers/v2.py to
app/models.py so the v2 serializer, the assets_update form handler,
and the edit-modal ``<input max>`` attribute all read it from one
place. Without this, the form handler had a hard-coded ``86400`` and
the API path had ``REFRESH_INTERVAL_S_MAX``; bumping one and
forgetting the other was a real risk.

Mirrored separately by ``kMaxReloadIntervalS`` in webview/src/view.cpp
because that's a different language; the docstring on the model
constant flags the duplication so a future bump knows where to look.

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): latch setReloadInterval capability after first failure

Without the latch, every webpage rotation against an older
AnthiasWebview re-pays the D-Bus round-trip and refills journald with
the same warning. Once the slot is known to be missing, skip both —
the version skew can't change for the lifetime of the viewer process
(an upgrade requires a viewer restart, which resets the cache).

Test updated to assert the second view_webpage call doesn't re-hit
setReloadInterval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): three Copilot comments on PR #2841 round 6

- viewer: latch the setReloadInterval capability OFF only when the
  D-Bus error indicates the method is missing (UnknownMethod /
  "no such method"). Transient errors (bus disconnect, timeout, race
  during webview restart) get logged at debug and retried next
  rotation, so a momentary blip doesn't permanently disable
  auto-refresh on a webview that actually supports the slot.
- v2 response: ``metadata['refresh_interval_s']`` is now clamped to
  the same [0, REFRESH_INTERVAL_S_MAX] range as the top-level
  ``refresh_interval_s`` field, so a legacy / hand-edited row can't
  produce a response where the two halves disagree (and a client
  reading metadata can't accidentally re-submit the raw bad value).
  Other pipeline-owned metadata keys pass through unchanged.
- ``to_json`` template filter: same sanitisation on
  ``metadata['refresh_interval_s']`` so the edit modal's
  ``<input type="number" max="...">`` doesn't land in :invalid
  state when an existing row carries an out-of-range value — that
  used to block the operator from saving *any* changes in the modal.

Tests: split the version-skew viewer test into the latching
(UnknownMethod) and non-latching (transient) branches; assert the
v2 GET response for ``metadata.refresh_interval_s`` matches the
clamped top-level field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(review): reset capability flag on browser restart + view_image !=

- Reset ``_webview_supports_set_reload_interval = True`` at the top
  of ``load_browser()``: a webview crash + relaunch (or in-place
  upgrade then process bounce) might bring up a binary that supports
  the slot, and we don't want to leave auto-refresh disabled for the
  rest of the viewer's lifetime because the *old* process lacked it.
- ``view_image`` was still using ``is not`` for the URL dedup check,
  same identity-vs-value bug already fixed in ``view_webpage``. Fix
  for consistency so a JSON-reconstructed URL doesn't bypass the
  short-circuit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): clamp refresh_interval_s upper bound in asset_loop

A legacy / hand-edited DB row with a huge ``metadata.refresh_interval_s``
would previously pass through asset_loop's ``int(...)`` unchanged and
hit the D-Bus marshalling layer. The C++ webview already clamps
defensively, but doing it here too saves the round-trip and uses the
same shared ``REFRESH_INTERVAL_S_MAX`` constant the v2 serializer and
page-form handler enforce.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(refresh-interval): extract clamp_refresh_interval helper

CI was unhappy on two fronts:

- ruff format check: I'd run ``ruff check`` (lint) but not
  ``ruff format --check`` after the round-7 edits, so two files
  landed unformatted. Now reformatted.
- SonarCloud: 3.4% duplication on new code (cap is 3%). The clamp
  pattern (``int(value)`` with try/except + ``max(0, min(_, MAX))``)
  was duplicated across 4 read sites: v2 serializer, edit-modal
  ``to_json`` filter, viewer ``asset_loop``, and the page-form
  handler. Lift it into a single ``clamp_refresh_interval`` helper
  next to the model constant so all readers funnel through one
  implementation.

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(viewer): import clamp_refresh_interval after django.setup()

Importing ``anthias_server.app.models`` at module top — before the
explicit ``django.setup()`` call later in the file — can raise
``AppRegistryNotReady`` during a fresh ``python -m anthias_viewer``
startup, because ``Asset(models.Model)``'s class body runs at import
time and needs the app registry. Move the helper import into the
existing post-setup block, alongside the other Django-aware imports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): mypy ``Any`` arg + parametrise duplicate tests for sonar

- ``clamp_refresh_interval`` was typed ``value: object`` with a
  ``# type: ignore[arg-type]`` band-aid on the ``int(value)`` line.
  mypy started rejecting both the ignore (unused) and the call
  (object isn't accepted by ``int()``). Switch to ``value: Any`` —
  the function is the catch-all for unknown JSON the column might
  carry, so ``Any`` is the honest annotation.
- SonarCloud was flagging 3.4% duplication on new code (cap 3%) on
  near-identical PATCH-rejection tests in test_assets.py and the
  two latches-off / does-not-latch viewer tests. Parametrise both
  pairs so the body lives once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 07:17:42 +01:00

286 lines
21 KiB
HTML

{{ define "main" }}
<header class="bg-brand-dark py-16 md:py-20 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h1 class="text-white text-4xl md:text-5xl font-extrabold tracking-tight">Features</h1>
<p class="text-white/60 text-lg mt-4 max-w-xl">Everything Anthias does, in plain language &mdash; what you can show, when it plays, and how you manage it all.</p>
</div>
</header>
<section class="bg-white py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h2 class="text-2xl font-bold">What you can show</h2>
<p class="text-zinc-500 mt-2 max-w-xl">Photos, videos, web pages, YouTube, live streams &mdash; if it goes on a screen, Anthias plays it.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="12" rx="2"/><path d="M7 20h10M9 16v4M15 16v4"/></svg>
</div>
<h3 class="text-lg font-bold">Anything that goes on a screen</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Photos, videos, web pages, and live video feeds, all in one playlist. Drag a file in or paste a link &mdash; that's the whole workflow.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="6" y="2" width="12" height="20" rx="2"/><path d="M11 18h2"/></svg>
</div>
<h3 class="text-lg font-bold">Phone photos just work</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">iPhone photos, screenshots, and pictures straight off a camera all upload and play without anyone having to convert them first.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 5h16v14H4z"/><path d="M9 9l6 3-6 3z" fill="currentColor"/></svg>
</div>
<h3 class="text-lg font-bold">Uploads never interrupt the screen</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">If a file isn't quite right for your hardware, Anthias quietly prepares it in the background while whatever's already on screen keeps playing.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M10 8l6 4-6 4z" fill="currentColor"/></svg>
</div>
<h3 class="text-lg font-bold">YouTube without ads or buffering</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Paste a YouTube link. Anthias downloads the video and plays it locally, so your screen never buffers and never shows ads or recommended videos.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2c2.5 3 4 6.5 4 10s-1.5 7-4 10c-2.5-3-4-6.5-4-10s1.5-7 4-10z"/></svg>
</div>
<h3 class="text-lg font-bold">Live web pages and dashboards</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Point Anthias at any web page &mdash; a status board, a weather widget, a live feed, an internal report &mdash; and set an auto-refresh interval so the page reloads on its own and stays current.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
</div>
<h3 class="text-lg font-bold">Sharp full-HD picture and sound</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">1080p output with smooth, hardware-accelerated video. Sound goes out through HDMI or the headphone jack &mdash; your choice.</p>
</div>
</div>
</div>
</section>
<section class="bg-bg-light py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h2 class="text-2xl font-bold">When things play</h2>
<p class="text-zinc-500 mt-2 max-w-xl">Schedule by date, day of the week, or time of day. Anthias takes care of switching things on and off for you.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>
</div>
<h3 class="text-lg font-bold">Schedule by date range</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Set a start and end date so an asset appears for a campaign and disappears on its own when it's over &mdash; no reminders, no manual cleanup.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h10"/><circle cx="6" cy="7" r="1" fill="currentColor"/><circle cx="10" cy="7" r="1" fill="currentColor"/><circle cx="6" cy="12" r="1" fill="currentColor"/><circle cx="14" cy="12" r="1" fill="currentColor"/><circle cx="6" cy="17" r="1" fill="currentColor"/></svg>
</div>
<h3 class="text-lg font-bold">Pick the days of the week</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Run one playlist on weekdays, another on weekends, or something special only on Fridays. Each item picks its own days.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
</div>
<h3 class="text-lg font-bold">Pick the time of day</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Show "we're open" content from 9 to 5, then switch to something else overnight. Schedules that cross midnight work too.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 7v5l3 2"/><circle cx="12" cy="12" r="9"/></svg>
</div>
<h3 class="text-lg font-bold">Set how long each item plays</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Tell each photo or web page exactly how long to stay on screen. Videos figure out their own length automatically.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 6h16M4 10h16M4 14h10M4 18h7"/></svg>
</div>
<h3 class="text-lg font-bold">Drag to reorder, or shuffle</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Arrange your playlist by dragging items into place, or turn on shuffle to mix things up on every loop.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M5 4l8 8-8 8M14 4v16"/></svg>
</div>
<h3 class="text-lg font-bold">Skip ahead, skip back, pause an item</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Jump forward, jump back, or temporarily turn an item off without deleting it &mdash; all from the dashboard.</p>
</div>
</div>
</div>
</section>
<section class="bg-white py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h2 class="text-2xl font-bold">Run it from your browser</h2>
<p class="text-zinc-500 mt-2 max-w-xl">A clean dashboard for managing your screen, with a clear view of how the device is doing.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2c2.5 3 4 6.5 4 10s-1.5 7-4 10c-2.5-3-4-6.5-4-10s1.5-7 4-10z"/></svg>
</div>
<h3 class="text-lg font-bold">No app to install</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Open the dashboard from your laptop, phone, or tablet. There's no cloud account &mdash; Anthias runs on the device itself, on your own network.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="6"/><path d="M21 21l-5-5"/></svg>
</div>
<h3 class="text-lg font-bold">Preview before you publish</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Click any photo, video, or web page to see exactly what your screen will show, right inside the dashboard.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
</div>
<h3 class="text-lg font-bold">Know when your screen is dark</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Anthias can tell whether the connected TV is actually powered on, so you can spot a dark display at a glance instead of walking over to check.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</div>
<h3 class="text-lg font-bold">System info at a glance</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">See how long the device has been running, how much storage and memory is in use, the software version, and the device's address on your network.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 4v8M12 16h.01"/><circle cx="12" cy="12" r="10"/></svg>
</div>
<h3 class="text-lg font-bold">Tells you when there's an update</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">When a newer version of Anthias is released, the dashboard quietly says so. No mailing lists, no checking the website.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 2v10"/><path d="M5 6a8 8 0 1 0 14 0"/></svg>
</div>
<h3 class="text-lg font-bold">Restart and shut down remotely</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Reboot or power off the player from the dashboard, without walking over to the screen. Useful when your displays are scattered around a building.</p>
</div>
</div>
</div>
</section>
<section class="bg-bg-light py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h2 class="text-2xl font-bold">First-boot and access</h2>
<p class="text-zinc-500 mt-2 max-w-xl">Set up a new screen in minutes, then lock it down to whoever should have the keys.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><path d="M14 14h3v3h-3zM18 14h3M14 18h3v3h-3zM18 18h3v3"/></svg>
</div>
<h3 class="text-lg font-bold">Onboarding right on the screen</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">On first boot, your TV shows the device's network address and a QR code. Scan it with your phone and you land straight on the dashboard.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>
</div>
<h3 class="text-lg font-bold">Optional password protection</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Turn on a username and password so only the people you trust can change what's playing. Off by default for quick setup, on whenever you want it.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>
</div>
<h3 class="text-lg font-bold">Easy HTTPS encryption</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Encrypt the dashboard with one command. Pick a self-signed certificate for a private setup, a free auto-renewing one for your own domain, or bring your own certificate.</p>
</div>
</div>
</div>
</section>
<section class="bg-white py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h2 class="text-2xl font-bold">Backups and integrations</h2>
<p class="text-zinc-500 mt-2 max-w-xl">Move things around, plug Anthias into your tools, or manage many screens at once.</p>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-6 mt-8">
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 7V4h16v3M9 20h6M12 4v16"/></svg>
</div>
<h3 class="text-lg font-bold">Backup and restore in one file</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Download a single file with your settings and asset list. Upload it later to put everything back, or move it to a brand-new device.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-purple/10 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-purple" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M16 18l2-2-2-2M8 6L6 8l2 2"/><path d="M14.5 4l-5 16"/></svg>
</div>
<h3 class="text-lg font-bold">Plays well with your tools</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Push content and control playback from your own scripts. The programmable interface is fully documented and stays compatible with older integrations.</p>
</div>
<div class="feature-card">
<div class="w-10 h-10 rounded-lg bg-brand-yellow/20 flex items-center justify-center mb-4">
<svg class="w-5 h-5 text-brand-dark" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><path d="M3.3 7L12 12l8.7-5M12 22V12"/></svg>
</div>
<h3 class="text-lg font-bold">Manage many screens with Balena</h3>
<p class="text-zinc-500 mt-2 text-sm leading-relaxed">Deploy and update Anthias across a fleet of devices through Balena, with over-the-air updates handled for you.</p>
</div>
</div>
</div>
</section>
<section class="bg-bg-light py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto">
<h2 class="text-2xl font-bold">Supported hardware</h2>
<p class="text-zinc-500 mt-2 max-w-xl">Anthias runs on Raspberry Pi single-board computers and 64-bit x86 PCs.</p>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mt-8">
<div class="bg-white rounded-lg p-4 text-center">
<p class="font-bold text-sm">Pi 5</p>
<p class="text-zinc-500 text-xs mt-1">64-bit</p>
</div>
<div class="bg-white rounded-lg p-4 text-center">
<p class="font-bold text-sm">Pi 4</p>
<p class="text-zinc-500 text-xs mt-1">64-bit</p>
</div>
<div class="bg-white rounded-lg p-4 text-center">
<p class="font-bold text-sm">Pi 3 B+</p>
<p class="text-zinc-500 text-xs mt-1">64-bit</p>
</div>
<div class="bg-white rounded-lg p-4 text-center">
<p class="font-bold text-sm">Pi 3 B</p>
<p class="text-zinc-500 text-xs mt-1">64-bit</p>
</div>
<div class="bg-white rounded-lg p-4 text-center">
<p class="font-bold text-sm">Pi 2 B</p>
<p class="text-zinc-500 text-xs mt-1">32-bit</p>
</div>
<div class="bg-white rounded-lg p-4 text-center">
<p class="font-bold text-sm">x86 PC</p>
<p class="text-zinc-500 text-xs mt-1">64-bit</p>
</div>
</div>
<p class="text-zinc-400 text-xs mt-4">Pi 2 and Pi 3 are in maintenance mode. We recommend Pi 4 or later for new installations.</p>
</div>
</section>
<section class="bg-brand-dark py-16 lg:py-24 px-6 md:px-12 lg:px-20">
<div class="max-w-7xl mx-auto text-center">
<h2 class="text-white text-3xl lg:text-4xl font-extrabold tracking-tight">Ready to try Anthias?</h2>
<p class="text-white/50 mt-4 text-lg max-w-lg mx-auto">Free, open source, and yours to run on any supported device.</p>
<div class="flex flex-wrap justify-center gap-4 mt-8">
<a class="btn-primary" href="/get-started/">Get Started</a>
<a class="btn-secondary" href="https://github.com/Screenly/Anthias" target="_blank" rel="noopener">View on GitHub</a>
</div>
</div>
</section>
{{ end }}