Commit Graph
291 Commits
Author SHA1 Message Date
LeoVasanko cc14303179 contact messages: trigger button uses the navbar font and weight
Nav family/weight become --font-nav/--nav-weight (base defaults body/400; nitro heading/700, corporate 600), referenced by both #nav and .contact-trigger. The button keeps the article's size and line-height.
2026-09-28 02:16:18 +00:00
LeoVasanko e555714c2e contact messages: 1.38em corner icon, ✉️ prefix on the trigger button 2026-09-28 02:13:28 +00:00
LeoVasanko 2e08f4861f contact messages: fix icon size specificity, scroll to end on open
.editor-pens button's font: inherit out-specified .contact-icon; scope the size bump to .editor-pens .contact-icon. The visitor panel only ever existed while open (the overlay is v-if'd), so its state.open watch never fired: scroll to the newest message on mount instead, with the message-count watch covering sent/received while open.
2026-09-28 02:07:48 +00:00
LeoVasanko 3ab28d0d7d contact messages: bump corner ✉️ icon size to match its neighbors 2026-09-28 02:01:52 +00:00
LeoVasanko d6088b90ff contact messages: unread nudge animation instead of dot/pill badges
Unread state (new reply for visitors, unanswered threads for admins) now animates the trigger button and the corner icon themselves: a scale-up shake every 4.5 s, gated on prefers-reduced-motion, stopping when read (the has-unread class goes away). The static accent dot and the admin count pill are gone.
2026-09-28 01:46:07 +00:00
LeoVasanko afa6b967e6 contact messages: unified overlay, corner ✉️ icon for everyone
The admin inbox leaves the short-lived /_m page and joins the visitor panel as one overlay (ContactOverlay shell over paskia's shared blur backdrop, centered dialog, full-screen on mobile): the corner ✉️ icon opens the visitor thread for visitors and the wider sidebar inbox for admins, while {contact:} trigger buttons always open the visitor side so admins can test both. The icon highlights unread state without extra connections: visitors via localStorage seen/last-admin timestamps, admins via the contact_unread count added to the /_api/settings probe, both kept live by the bundle once loaded. MessagesView owns its admin socket only while the overlay is open.
2026-09-28 01:37:37 +00:00
LeoVasanko 9a2435f03a contact messages: centered panel on paskia's shared blur backdrop
The panel was a right-docked sidebar with its own translucent backdrop. It is main content: now centered at the paskia dialog's dimensions (min(36rem,100%) x min(42rem,92vh)), full-screen below 36rem, holding the refcounted global blur backdrop (z 1099) while open with the panel's click-catch overlay at 1100 (below the auth iframe at 9999). Escape closes too.
2026-09-28 01:04:33 +00:00
LeoVasanko f3c288c76c contact messages: trigger hover follows the navbar tab idiom
The filled button now hovers like nitro's accent nav tab: text --ink (falling back to --bg on themes without it) switching to the bright --tab-hover with the same text-shadow glow, instead of the invented fill shift.
2026-09-28 00:58:23 +00:00
LeoVasanko 78b1fbc16e contact messages: filled accent trigger button, subtle hover
The {contact:} button was a hollow outline that inverted wholesale on hover; now it is filled with the accent (--bg text, the pairing accents are chosen to contrast with), hover only nudges the fill toward the page text color, and keyboard focus gets an outline. The button also gains margins so it separates from surrounding text both standalone and inline.
2026-09-28 00:45:00 +00:00
LeoVasanko 4ad4e3bdff contact messages: standalone /_m admin inbox, site-name reply label
The inbox moves out of the analytics tab to its own page at /_m (own pens-row icon, own entry and socket lifecycle; the admin-gated /_api/ws/analytics socket keeps serving both, with typed messaging frames ignored by AnalyticsView). Admin replies are labeled with the currently configured site name in the wire payload (the SSO name stays stored for audit), and the thread view is capped to roughly the visitor panel's width.
2026-09-27 18:59:22 +00:00
LeoVasanko f4d1f0ac58 contact messages: optional unvalidated email, plain relative timestamps
Email is no longer required and its format is not checked anywhere
(placeholder notes it's optional); only the name gates the first
message. Timestamps are just the analytics relative format — no full
date on hover.
2026-09-27 18:31:21 +00:00
LeoVasanko 67c19230b4 contact messages: visitor identity, relative timestamps, alternating headers
- The first message of a thread must carry name + email introducing the
  visitor; the panel shows the two fields until the thread has an
  identity (send stays disabled while they look empty, so the draft is
  never lost), prefilled from and saved to localStorage. The identity
  is stored on ContactThread (msgspec defaults — old databases decode
  unchanged) and fixed afterwards; it labels the visitor's messages
  instead of 'you', and the admin inbox lists threads by name with the
  email as a mailto in the thread header.
- Timestamps use the analytics relative formatter (formatWhen), with
  the full date on hover.
- Message headers alternate alignment (own messages right, theirs
  left) as the visual side cue; the sent-from page link is dropped
  from the display (kept as stored metadata).
- Composer gains a sendDisabled prop for the intro gate.
2026-09-27 18:19:03 +00:00
LeoVasanko e810c5f664 contact messages: plain thread view instead of bubbles
Drop the bubble styling (muted/accent backgrounds, rounded boxes,
left/right alignment) from both the visitor panel and the admin inbox:
messages now stack full-width as a flat thread with hairline
separators, a small muted meta line (author + time) above the body,
and 'you'/'visitor' fallback labels carrying the side distinction
instead of color.
2026-09-27 17:41:59 +00:00
LeoVasanko e3080d70c0 contact messages: hold the PoW challenge as a WS-handler local
Move the PoW state from a module-level dict keyed by WebSocket in
messages.py to a plain per-connection local in tracking.activity_ws,
making PoW a WS-level concern instead of being buried in the message
handler:

- activity_ws validates and burns the solution (raw.pop('pow')) before
  forwarding a message frame, and issues a fresh challenge after every
  registration and every vetted message frame; a failed validation is
  answered with a 'required' challenge and the frame never reaches
  messages.py.
- messages.py loses the _pow dict, _issue_pow and the disconnect
  cleanup for it; visitor_frame needs no return value since a vetted
  frame always triggers a fresh challenge (a too-long message simply
  comes with contact-error and required=false, so the client does not
  retry it).

The wire protocol and the frontend are unchanged.
2026-09-27 16:35:14 +00:00
LeoVasanko 0adcd0bf90 contact messages: browser-side PoW solving in the contact panel
The bridge routes {type: pow} frames to the panel. contact-main solves
each fresh challenge in the background with the backend-served easypow
solver module (imported by URL from a variable so Rollup leaves it
dynamic), aborting stale solves on replacement challenges. Sending
awaits the current solution and burns it with the message; a
'required' rejection re-solves and retries the message once.

Composer and ContactPanel need no changes: uploads stay ungated, and
the send path just awaits the in-flight solve.
2026-09-27 06:14:04 +00:00
LeoVasanko ec35bc71e8 contact messages: easypow proof-of-work for sends, pending-upload cleanup
New dependency: easypow (PBKDF2-SHA512 challenges, bundled JS solver).

Sends are PoW-gated: registration/resync and every accepted message
come with a fresh single-use challenge (~5 s solve), held in per-socket
RAM state only — nothing in the database. A message burns one
solution; a missing, stale or spent solution is dropped and answered
with a fresh 'required' challenge. The bundled browser solver is
served at /_contact/easypow.js straight from the installed package, so
client and validator versions always match.

Uploads are deliberately not PoW-gated (the transfer rate is the real
cost). Instead, uploads are tracked as pending per session and a quota
caps pending files at 10 — answered 429, not 403, which could trip
auth flows. A sent message strikes the files it references off the
pending set; when the session's last socket closes, the remaining
pending files are deleted from the store.
2026-09-27 06:13:53 +00:00
LeoVasanko 6b34719397 contact messages: inert .file attachment blobs, universal forced download
Rework attachment storage and serving per the refined separation
scheme:

- Non-media uploads are stored as {hash}.file blobs (store_attachment)
  — the original extension never lands on the filesystem, so the store
  directory holds nothing double-click-executable. The original name
  lives only in the URL: /_f/{hash}/{original-name}. The .file suffix
  itself is rejected in URLs.
- Serving is strictly separated: /_f/{name} serves media only (hash
  with or without extension, .orig. still blocked) and 404s .file
  names; /_f/{name}/{filename} serves only {name}.file blobs, never
  media, never by hash alone.
- Every /_f/ response now carries bare 'content-disposition:
 attachment' (the URL's last segment already names the download) and
  x-content-type-options: nosniff — media included. Subresource
  fetches (<img>, <video>, favicons, the zoom viewer) ignore the
  disposition, so inline media is unaffected, while a direct
  navigation can never render or sniff a store file into something
  executable in the site's origin. The mime-based script-carrier
  forcing and the header filename are gone (subsumed).
- Message uploads treat SVG as media (rasterized derivatives; an
  as-is-served .svg is script-inert inside <img>), and undecodable
  fake images fall through to the .file attachment path.
2026-09-26 03:35:08 +00:00
LeoVasanko 95554b4165 contact messages: allow all attachment types, enforce downloads
Drop the script-carrier blocklist: any file type (html, exe, svg, ...)
may be attached now, with safety from enforced downloads instead of
type filtering, verified twice:

- the attachment chip anchor carries a download attribute (already),
- the link is /_f/{name}/{original-filename}, a new file-store route
  that answers content-disposition: attachment naming the original
  file (ASCII-safe fallback + RFC 5987 UTF-8 form). The filename rides
  in the URL itself — no filename registry in the database.

Close the residual gap on the plain /_f/{name} route: a stored file
whose mime executes script in origin (html/xml/svg/js) is forced to
download there too, so stripping the filename segment off an
attachment URL cannot turn it into an in-origin page. All /_f/
responses now carry x-content-type-options: nosniff. Subresource
fetches (<img>, favicons) ignore Content-Disposition, so inline
images and icons are unaffected.

Undecodable fake images (store_image falls back to as-is storage) now
attach as files instead of being marked inline images.
2026-09-26 01:59:08 +00:00
LeoVasanko 49bdedd938 contact messages: fix inline formatting loss, attachment uploads
The serializer treated only DIV/P as blocks: a formatting element
directly under the editable root (a bolded segment of an undivided
line is a top-level <b>) became a separate PLAIN block, losing both
the formatting and the paragraph structure. Top-level inline runs now
accumulate into one paragraph, and style-based formatting (Firefox's
font-weight/font-style spans) maps back to markers.

Attachments: the upload allowlist rejected everything but a handful of
types with a console-only 415. The logic inverts — raster images get
derivatives, script carriers (svg/html/js/xml...) are refused, and any
other file stores as a download chip. Upload failures now show in the
composer, and toolbar buttons keep the contenteditable selection on
click (mousedown.prevent).
2026-09-26 01:31:45 +00:00
LeoVasanko 975ebfe5e7 contact messages: docs
docs/messages.md covers the trigger, both socket protocols, the storage
model, the XSS stance (grammar restriction via a strict renderer, never
HTML sanitization), uploads, and the MVP gaps (PoW seams, moderation,
orphan GC, i18n). AGENTS.md and frontend-runtime.md learn the new
module, bundles and asset keys.
2026-09-26 01:17:30 +00:00
LeoVasanko bbb11bf17e contact messages: admin inbox on /_a
The analytics viewer gains a messages tab over the same socket: frames
with a type field are inbox payloads (thread list on connect, live
thread upserts), the type-less snapshot stays analytics. MessagesView
shows the thread list with unread dots and the selected thread with the
shared composer for replies; opening a thread marks it read server-side.
The header badge counts unread threads.
2026-09-26 01:14:52 +00:00
LeoVasanko eea89032a2 contact messages: visitor panel and composer
The composer is a constrained contenteditable (plain-text paste,
toolbar formatting, upload chips for images/attachments) serialized to
a Markdown subset by richtext.js — the wire format is text and the
server re-renders it, so no user markup ever reaches a page. Uploads go
to /_contact/files with the session token (admins share the component
via /_api/files). ContactPanel shows the thread pushed live over the
activity socket: pagerite.js lazy-loads the bundle on trigger clicks,
routes socket pushes to it, re-registers the session token on every
reconnect (the resync), and dots trigger buttons on unread replies.
2026-09-26 01:11:14 +00:00
LeoVasanko 776f408743 contact messages: {contact: Label} article directive
A lone {contact: Label} line in article Markdown renders a
.contact-trigger button that opens the visitor panel (pagerite.js binds
the click and lazy-loads the contact bundle; inert in the editor
preview, whose resolver now matches the served page). The on-demand
bundle's URLs ride in the pagerite-assets config like langselect's, and
the build gets a contact entry point. Description/card mining renders
resolve the tag to nothing.
2026-09-26 01:05:38 +00:00
LeoVasanko 98c66440f6 contact messages: public upload endpoint
PUT /_contact/files/{name} stores panel uploads in the content-addressed
file store: raster images through the usual derivative pipeline, a small
allowlist of document types as-is. SVG/HTML are excluded (script
carriers). Gates for now: a session-shaped token plus size/type caps;
the proof-of-work stamp plugs in here later. The dev proxy learns the
/_contact path.
2026-09-26 01:02:28 +00:00
LeoVasanko 2fb0cc219b contact messages: wire the sockets
/_ws routes frames carrying a session token to pagerite.messages (and
now also pushes thread state back down the same socket); the visitor
registry is cleaned on disconnect. /_api/ws/analytics becomes
bidirectional: admins get the contact inbox on connect, visitor messages
are pushed live, and reply/read frames are handled. Analytics payloads
keep their type-less shape; messaging frames always carry a type field.
2026-09-26 01:00:59 +00:00
LeoVasanko 1d827da62a contact messages: data model, strict renderer, socket protocol core
Data.contact holds visitor threads keyed by blake3(session token).
Message bodies are a Markdown subset rendered with a strict MarkdownIt
(html=False): the stored html is always markdown-it output, never
sanitized user markup. pagerite/messages.py owns the store ops, wire
payloads and both socket protocols (visitor frames on /_ws, admin frames
on /_api/ws/analytics); the socket wiring follows.
2026-09-26 00:59:45 +00:00
LeoVasanko 3c9ef04ffd Transition map beads: connector-colored elongated pulses
Beads now take their connector's color — accent for internal links,
plain text color for external ones (matching the tconn-exit styling) —
and render as an elongated pulse along the direction of travel instead
of a plain dot: a rounded capsule with a gradient-faded tail, rotated
to the edge's angle (cached per flow). Flow objects carry an 'external'
tag from buildFlows; the gradients live in the svg defs with stop
colors resolved from CSS so theme variables apply.
2026-09-25 20:23:20 +00:00
LeoVasanko fb2914d853 Don't accrue reading time across sleep
The inactivity cutoff timer cannot fire while the computer is asleep, and
performance.now() keeps running through sleep on some platforms, so after
a wake the whole slept span could be credited as reading time (inflating
min/visit stats). A gap longer than the inactivity timeout now closes the
span at its intended cutoff, and the cutoff timer clamps its accumulation
to its scheduled fire time — covering both event-first and timer-first
wake orderings.
2026-09-25 18:22:43 +00:00
LeoVasanko d4add54f10 Editor sockets: auth-aware reconnect, shared with the analytics view
An expired session failed the /_api/ws/editor handshake exactly like a dead
network, so the page/banner editors retried forever — the WS API cannot
open the login dialog. reconnect.js gains authReconnect(): on close, probe
/_api/settings (dialog-less fetchJson); a 401/403 runs the paskia login
flow and reconnects the moment it completes, a declined login holds at a
'session expired' ConnNote, and only transient failures take the paced
backoff. A paskia auth-success message from anywhere in the page
reconnects a waiting editor at once. AnalyticsView moves from its inline
copy of the same flow onto the shared helper.
2026-09-25 18:19:05 +00:00
LeoVasanko 31b56c3e5e Serve each article's Markdown at .md URLs (and via Accept: text/markdown)
/pages.md (front page /.md) serves the page's source Markdown — per-language
hybrid assembly, title injected as '# title' when the content has no h1 —
with the same ETag/Last-Modified/no-cache scheme as the HTML, which now
carries vary: accept. Pages advertise the counterpart via
<link rel=alternate type=text/markdown> and llms.txt links point at the
.md URLs.
2026-09-25 18:04:47 +00:00
LeoVasanko 7780d190d0 Analytics viewer: auth-aware reconnect, wake recovery, sliding time window
- Expired session dropped the socket into an endless paced retry loop the
  WS handshake can never escape: probe /_api/settings on close, run the
  paskia login flow on 401/403 and reconnect the moment it resolves; an
  aborted login leaves for the front page (history replace).
- Follow paskia's auth-success/auth-logout window messages: logins anywhere
  in the page reconnect at once instead of waiting out the backoff; logout
  leaves /_a. pagerite.js re-probes its pens on the same events.
- Recycle the socket on visibilitychange/online/bfcache restore: a socket
  that survived sleep looks OPEN but is dead, so no updates ever landed.
- The stats range window was frozen at mount (no reactive time dependency);
  it now slides on a minute clock.
- Transition map beads: fast-forward emitter schedules after long gaps
  instead of spawning a bead per missed interval in one tick.
2026-09-25 18:01:05 +00:00
LeoVasanko 204c6ab608 Transition map: always show the site structure, omit content-less index pages
Node inclusion no longer depends on views in the window: every page
from /_api/pages is shown (the front page used to vanish when quiet).
Omission follows the navigation instead — index pages without content
of their own (category labels and empty pages, the nodes nav links
bypass to the first child) are omitted, children promoted, first
visible child taking the slot. Deleted pages named by traffic are
inserted as ghosts where they would have been, materializing missing
ancestors as content-less category lanes.

/_api/pages gains the "empty" flag (not node.chunks) for the nav
notion; has_content (chunks is not None) stays for the structure
editor's delete affordance.
2026-09-25 03:23:53 +00:00
LeoVasanko 0ff5c6b526 ruff v0.8.1 2026-09-23 07:32:57 +00:00
LeoVasanko 2aed176c9e Cache feed/llms.txt bodies, serve with content-hash ETags
Rendering every article per request is expensive. Bodies are now
RAM-cached keyed by the public base URL and cleared from
_invalidate_pages (the same hook that drops the page render cache on
any content/settings write). Responses carry a blake3 content-hash
ETag and answer 304, so polling feed readers revalidate cheaply;
matching page behavior, 304 revalidations are not recorded in
analytics.
2026-09-23 07:30:32 +00:00
LeoVasanko 9ff22016d1 Add llms.txt, JSON Feed and RSS feed exports
New pagerite/feeds.py: /llms.txt (Markdown site map with excerpts for
LLM agents), /feed.json (JSON Feed 1.1) and /feed.xml (RSS 2.0 +
atom:link), all carrying every published article with full content and
absolutized URLs. Linked from every page <head> (feed alternates +
llms-txt) and from the sitemap, recorded in analytics like page GETs
and emoji-marked in the trails (🧠 llms.txt, 📡 feeds).
2026-09-23 07:25:08 +00:00
LeoVasanko 97bde49296 Record robots.txt/sitemap.xml GETs, emoji-mark machinery steps in trails
The robots and sitemap routes never called _record_get, so those
accesses were invisible. Recorded like page GETs, they surface as
crawler hits at display time (no activity message ever follows). In
the trail rendering, known non-article machinery paths (robots,
sitemap, future feed paths) get an emoji marker instead of a bare
slug.
2026-09-23 07:12:15 +00:00
LeoVasanko 4f97a6592c Relax automatic large-card pick: w > 600, h > 400, aspect > 1.05
The old 1.4-2.5 aspect window sent big near-square images (e.g.
1350x1200) to the compact layout despite ample resolution. The new
rule keeps square and portrait images compact (the compact box fits
them) while letting anything big and clearly wider than tall render
as a full-card cover.
2026-09-23 07:07:42 +00:00
LeoVasanko 7e86efeb9f Place external source/exit pills near their connection targets
Replace count-sorted, group-centered rows with a spring-like placement:
pills order by their weighted median target x and settle by clamped
coordinate descent, minimizing weighted horizontal connection distance
while keeping the minimum pill spacing.
2026-09-23 07:02:17 +00:00
LeoVasanko 580ebac06c Card image: paste button and site-icon (@favicon) option
The banner panel's card-image block gains a pasteboard button (an image
uploads; an image URL goes as the save message's image setting itself and
is fetched/stored server-side, cross-origin being CORS-blocked for the
browser) and a site-icon button saving the @favicon sentinel, which
resolves to Data.favicon at render time and follows favicon changes.
2026-09-23 06:31:06 +00:00
LeoVasanko 4d6735f609 Fix card image priority: own > mined from article > inherited
The resolved (inheriting) Node.image was checked before mining the
article, so a front-page banner image shadowed the articles' own
extracted figures everywhere: card covers, og:image and the editor's
card previews.
2026-09-23 06:10:01 +00:00
LeoVasanko 6c0de19ae8 Analytics charts: translucent typical fill, last-week tail on week view
Render the seasonal typical estimate as a translucent muted fill (no
stroke, same 0.6 opacity as the data) instead of a grey line, on both day
and week views. Continue the fixed Monday-Monday week view with last
week's curve from the current bucket to the end of the week (secondary
accent, translucent fill), so the chart is useful early in the week and
last week is gradually replaced by the current one.

Gate the typical estimate on history spanning twice the view's full time
(third day / third week on) and the last-week tail on data reaching into
last week at all; the legend shows each entry only when present.
v0.8.0
2026-09-21 18:31:34 +00:00
LeoVasanko 91a16f56c5 Redesign translation overrides: keyed, structural, hash-anchored
Replace the old "patches" format (path:lang composite keys, ordered
hunk lists, text-anchored matching) with Data.overrides:
path -> lang -> LangEdits, keyed throughout so a save's database diff
touches only the edited chunks. The old "patches" key is ignored on
decode, discarding legacy data without a migration.

- Whole-paragraph additions/deletions are structural: a drop flag on
  the original chunk hash, and additions in their own dict anchored
  from the neighboring chunks' before/after (first live referrer wins),
  so they stay in place across retranslation and one-sided original
  edits.
- Within-paragraph edits (up to a full paragraph rewrite or split) are
  full-chunk replace patches applied by chunk hash alone: a
  retranslation is overridden wholesale, so user edits survive AI
  re-runs; editing the original changes the hash and orphans the patch.
  The old search-matching staleness gate is gone.
- Saving a translation on a page without original chunks is rejected
  (REST 400 / WS error); emptying the original afterwards renders the
  translation empty, with the orphaned overrides inert.
2026-09-21 18:14:48 +00:00
LeoVasanko cc2cc23b3b Fix ruff lint errors 2026-09-21 14:43:37 +00:00
LeoVasanko d0ce619db7 Improved auto translation support (#2)
- Added full article / with markdown mode for advanced LLMs, with local ollama and API support, new llm_translator script.
- Translate navigational titles as a whole (preserving site structure), and then retranslate as article headings with article context - potentially changing nav label
- Improved human overrides to translations, in particular when structure is affected

Reviewed-on: https://git.zi.fi/LeoVasanko/pagerite/pulls/2
2026-09-21 14:22:00 +00:00
LeoVasanko 1f5e52505f Double Ctrl-C or other situation could cause ugly traceback, avoid that situation by exiting quietly on CancelledError and KeyboardInterrupt. 2026-09-20 01:14:47 +00:00
LeoVasanko e8e56ce2ea Dependency version bumps. Silence pyvips log messages (mediapreview). 2026-09-20 01:08:31 +00:00
LeoVasanko aae8d58c9f Normalize the hybrid after patches; anchor ambiguous patch hunks in context
Extra paragraphs exist only inside a patch hunk's replace text (anchored
on the preceding original block). Deleting one produced a (X, "") hunk,
and apply_patch removed the text but not a surrounding separator, leaving
an accumulating blank line in the served/editor Markdown. hybrid_markdown
now re-chunks the patched text (join_chunks(chunk_markdown(...))), which
drops the stray blank lines on the read path — repairing gaps left by
already-stored patches too.

Adjacent fix in make_patch: a hunk whose search text occurred more than
once in the page hit the FIRST occurrence at apply time, blanking or
rewriting the wrong instance when identical paragraph text appeared
earlier. Ambiguous hunks now grow block context (preceding block first)
until unique; unique hunks stay context-free so they keep surviving
translation refreshes.
2026-09-20 00:32:53 +00:00
LeoVasanko c337651020 Keep editor open across in-site navigation, retargeting to the new page
load() closed the editor on every navigation, and the close raced the
navigation: the shell's close-time loadPlain re-render resolved last,
swapping the old page back in and replaceStating its URL. Navigation now
leaves the panel open and the existing pagerite:switch-editor retarget
fires while editing; the exception is /_a, where closeEditor({ navigating:
true }) closes the panel without the re-swap or title restore.
2026-09-20 00:23:31 +00:00
LeoVasanko b248241af0 Card images: hierarchical setting, compact-card image, explicit card-mode override 2026-09-20 00:13:00 +00:00
LeoVasanko 4bccf6855e Auth via paskia-js: profile() dialog, apiFetch/apiJson in admin components
- Banner-corner auth link is now a button opening paskia-js's profile()
  dialog (handles login too); auth re-probed when the dialog closes.
- Editor/analytics components call /_api via apiFetch/apiJson: an
  expired session opens the login dialog and the request retries.
- pagerite.js: task-checkbox toggle uses apiJson (explicit edit attempt,
  reverts on any failure incl. cancelled login); auth probes use
  fetchJson (never a dialog); page-cache/navigation stay on plain fetch.
- Add the paskia npm dependency; document the convention.
v0.7.9
2026-09-18 20:14:52 +00:00