Skip to content

release: v1.17.0#295

Merged
TemujinCalidius merged 12 commits into
mainfrom
dev
Jul 19, 2026
Merged

release: v1.17.0#295
TemujinCalidius merged 12 commits into
mainfrom
dev

Conversation

@TemujinCalidius

Copy link
Copy Markdown
Owner

v1.17.0 — merge devmain (a true merge, not squash, so history is shared).

Release notes go on the GitHub Release + Discussions announcement. Highlights:

🤖 Generated with Claude Code

TemujinCalidius and others added 12 commits July 18, 2026 09:38
… (#280)

The accent colour re-themes the whole UI but could only be set from the native
macOS app — and docs/theming.md has falsely claimed a web control since v1.0.
Add one, and make accent PER-THEME so it composes with the theme system.

- Admin → Site settings → Appearance gains an accent picker (colour input +
  hex + "Use theme's accent"). It's bound to the selected theme and POSTs to
  update_profile (the profile overlay — same store /api/account + the AP actor
  read), not site-config, so there's one source of truth.
- Storage: new `SiteSettings.themeAccents` Json column, `{ themeId: "#rrggbb" }`.
  The legacy `accentColor` stays the DEFAULT theme's accent (what the macOS app
  reads/writes), so this is additive + backward-compatible. `prisma generate`
  offline; `db push` adds the nullable column at deploy.
- New pure `resolveAccent(themeId, profile)`: per-theme override wins; else the
  legacy accentColor for the default theme only (schema-default #3b82f6 treated
  as "unset" → inherit, so existing instances render identically); else null =
  inherit the theme's own accent.
- Fix the silent-override trap: buildThemeStyle drops the DEFAULT_ACCENT no-op
  guard and overlays any resolved hex — the inherit-vs-custom decision now lives
  in resolveAccent. A custom accent no longer bleeds onto other themes, and blue
  is finally selectable on Editorial.
- Federation guard: an accent-only save no longer blasts a byte-identical actor
  Update to every follower (accent isn't in the actor doc). Name/avatar/summary
  edits still federate.
- /api/account exposes themeAccents; docs/theming.md corrected.

Tests: resolveAccent (per-theme, legacy-default, inherit, junk); buildThemeStyle
(overlays any hex, default inherit → ""); updateProfile (themeAccents merge +
clear, unknown-theme reject, federation guard); site-profile themeAccents parse.
501 pass.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The generate-token reveal now also offers a `fedihome://connect?instance=…&
token=…` link bundling the instance URL + token, so a native app (macOS/iOS)
can onboard from a single copy instead of two separate fields — greasing the
click-to-connect flow the apps are built around.

- Instance URL comes from siteConfig.url (passed as a prop, since SITE_URL
  isn't in the client bundle); both params are encodeURIComponent-escaped.
- Token stays reveal-once; the link is only shown in the same box, never
  persisted.
- Documented in docs/app-api.md as a PROPOSED onboarding contract — flagged to
  confirm the scheme/params against the macOS/iOS app before it's stable.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…olour (#250) (#282)

Themes could vary hue + type but not texture — globals.css hardcoded
.glass-card blur(12px)/radius 12px and the button radius 8px. Add a `feel`
group to ThemeTokens (radiusCard, radiusButton, glassFilter) surfaced as
--radius-card / --radius-button / --glass-filter, referenced by those classes,
with defaults declared at plain :root so buildThemeStyle's :root:root wins.

- glassFilter is the WHOLE backdrop-filter (e.g. "blur(12px)" or "none"), so a
  theme can turn glass off — blur(0px) would still create a compositing layer.
- default's feel equals the literals it replaced, so buildThemeStyle emits
  nothing for it → byte-identical. Editorial goes crisp/flat: 4px corners,
  glass-filter none — a print counterpoint to the rounded frosted glass.
- The fedi-badge pill (9999px) stays hardcoded — it's intrinsic, not "feel".

Verified: default injects "" (unchanged); editorial injects the 3 feel vars +
renders visibly crisper (screenshotted). 502 tests.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… (#283)

Another slice toward "configure everything from the web, not the server's
.env". Adds contact.email + podcast.{title,author,description,email,image} to
the shipped site-config overlay (SITE_CONFIG_FIELDS + RuntimeSiteConfig +
validation + a "Contact & podcast" section in Admin → Site settings). Empty =
derive from the profile, same as the env-var defaults.

Also rewires src/app/audio/feed.xml to read getRuntimeSiteConfig() +
getRuntimeProfile() instead of process.env / siteConfig directly — which fixes
a latent bug: the podcast feed ignored a web-edited author name / site config
and only reflected the env vars.

Identity + secrets deliberately stay env-only (the deferred .env→DB migration).
Tests extend site-settings.test.ts. 503 pass.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Gallery categories were hard-coded in 4 places (3 divergent lists) with no
server validation, and the /videos + /audio galleries had no category filter at
all. Make them web-editable via the shipped #59 overlay, with one source of
truth.

- New pure `src/lib/categories.ts`: DEFAULT_CATEGORIES (the old hard-coded
  values) + parse/resolve/label/union/tabs/normalize helpers. Client-safe.
- Config overlay: categories.photos/videos/audio ("text" CSV → slug arrays),
  RuntimeSiteConfig.categories, validation (slug-only, dedupe, cap 24, ""=
  defaults), site.config.ts group. A Categories section in Site settings.
- Compose: the 3 dropdowns read the resolved lists (passed from the server
  page); write path uses normalizeCategory (slug-safe).
- Galleries: /photography reads the list; NEW VideoGrid + AudioGrid add the
  filter tabs those galleries never had (shared CategoryTabs, extracted from
  PhotoGrid). Each gallery's tabs = configured list ∪ categories still in the
  DB, so removing a category never hides existing media (orphan safety).
- Labels auto-title-case the slug ("photo-walk" → "Photo Walk"); "general" is
  always guaranteed. Softened hard-coded category names in gallery copy.

Tests: categories.test.ts (13) + site-settings overlay/validation. 516 pass.
No local DB, so gallery rendering is verified via next build + the pure-helper
logic tests (noted); union-with-DB is unit-tested via unionCategories.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The admin theme picker shipped, but the setup wizard had no theme step. Add an
Appearance block (Theme + feed layout selects) to the wizard's Site Features
step — backend unchanged, since theme.id/layout.feed are already valid
site-config keys written via the wizard's existing applySiteConfig path. Theme
options come from the THEMES registry; only non-default choices are sent as
overrides. Folded into the existing step (no step-count renumber). A fresh
install now lands on the owner's chosen look with no file editing.

Verified: build clean; eyeballed the Appearance selects in the wizard step 5.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Move Tinylytics' public embed ids web-side: analytics.siteId + analytics.embedId
as plain site-config text fields (RuntimeSiteConfig.analytics), an "Analytics"
section in Site settings, and rewire the Tinylytics embed to take the resolved
siteCode as a prop from layout.tsx (which already resolves runtime config).
Collection now starts with no .env editing or restart.

Also: the CSP allow-list for tinylytics.app was env-gated, which would have
silently blocked a web-configured id (headers() is static and can't read the DB
config). Made it unconditional — the domain is only ever contacted when the
embed renders, so it's inert when analytics is off.

Scope: the in-app dashboard/kudos/footer counter go through the API-KEY path
(lib/tinylytics.ts), which stays env-only until an encrypted-secret panel lands.
So this slice is collection-only, by design.

Tests: analytics overlay in site-settings; the embed test moves to the prop
signature. 516 pass.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…y works (#288) (#289)

The on-page collecting embed loads tinylytics.app/embed/<code>.js, and that
endpoint needs the site's `uid` — NOT the numeric TINYLYTICS_SITE_ID. With only
the numeric id set, the embed was `…/embed/3461.js` → 404 → zero pageviews
collected, silently, while the dashboard (which correctly uses the numeric id
for the API) looked fine.

- resolveTinylyticsEmbed (src/lib/tinylytics.ts): explicit embedId override →
  a site id that's already a uid (non-numeric) used as-is → a numeric site id
  derived to its uid via GET /sites/{id} (needs the API key). Never emits a
  numeric id (it 404s). getSiteUid caches the immutable uid in-memory (this runs
  on the root-layout render path), with a short negative TTL so it self-heals,
  a 4s timeout so it never blocks the render, and a console warning when it
  can't resolve.
- layout.tsx renders the resolved code (nothing when unresolvable, vs a broken
  404ing script before). Works from web-config or env — a numeric site id +
  API key "just works", no manual uid lookup (addresses the non-technical /
  web-settings case).
- Admin → Site settings → Analytics surfaces it: "✓ Collecting" with the embed
  code, or "⚠️ not collecting" with how to fix — live-refreshed from the save
  response (the site-config route now returns the resolved status). Clarified
  the field labels + the misleading .env.example copy.

Tests: resolveTinylyticsEmbed across all paths (override / uid passthrough /
derive+cache / no-key / 404 / unset). 523 pass.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#288 follow-up) (#290)

Verified on the demo: the auto-derive path collects site-wide. Two rough edges
from the build reported there:

- The build-time warning claimed "pageviews are NOT being collected" during
  `next build`, even though dynamic pages resolve the uid at request time and
  collection is fine — alarming in build logs. Reworded to be phase-aware and
  honest (transient at build; the scary claim is reserved for a real runtime
  miss). The admin panel remains the authoritative surface.
- `next build` renders across parallel workers, each with a cold uid cache, so
  concurrent /sites/{id} lookups can race the 4s timeout / get rate-limited. If
  a route were ever statically generated, a lost race would bake an empty embed
  into it — the exact silent-non-collection this fixes, per page. Hardened the
  build lookup with an 8s timeout + one retry (runtime stays a short 4s single
  shot), and a build-time miss is no longer cached (the server retries fresh).

Test: build-phase retry (first call fails, retry succeeds). 524 pass.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…config (#284) (#291)

Reopened #284: the web side of customisable gallery categories shipped in #285,
but the one item the native Mac app needs was missing — a canonical source of
truth for the photo/video/audio categories on the app API. Without it a client
can only derive categories by paging existing posts, so a configured-but-unused
category (e.g. a freshly-added "landscape" with nothing in it yet) is invisible.

GET /api/micropub?q=config now also returns:

  "mediaCategories": {
    "photos": [{ "slug": "wildlife", "label": "Wildlife" }, ...],
    "videos": [...],
    "audio":  [...]
  }

- Same union the galleries render: the owner's configured list ∪ the categories
  still in use (published only), via the shared unionCategories helper — so a
  removed-but-still-used category never disappears (issue decision 1).
- Structured { slug, label } (issue decision 4 / app comment item 4) — clients
  show the label, submit the slug, and don't re-implement title-casing. Reserves
  the shape for a future slug|Label without a breaking change.
- Distinct `mediaCategories` key, separate from the post-type `categories` array
  (no collision).
- Compose writes are untouched — still lenient (issue decision 3): free-typing a
  brand-new category keeps working for web and the app.
- Per-media failure isolation: the three in-use lookups settle independently, so
  one media type's DB hiccup degrades only its own list to configured-only (the
  endpoint is unauthenticated and polled — it must not 500).

New pure helper categoryEntries(configured, present) in @/lib/categories.
Tests: categoryEntries units + a micropub-config route test (structured shape,
published-only filter, per-media failure isolation). 529 pass.

Unblocks TemujinCalidius/FediHome-macOS#61.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ost reader (#292) (#293)

The native "My Posts" manager could only show the ~200-char `preview`, so the
owner couldn't read a post in full from the app. `q=source` returns markdown
(for the editor) and the app has no markdown renderer — it renders sanitized
HTML via its existing FediHTML formatter, the same path the feed uses.

The Prisma `select` already fetched `contentHtml` (read only to derive `preview`),
so this just returns it on each post — `string | null`, as-is. Drafts + scheduled
posts are included exactly as before. 20 posts of HTML per page is bounded and
fine for a single-user manager (no pagination change).

Documented in docs/app-api.md; test asserts the field ships (value + null).

Unblocks TemujinCalidius/FediHome-macOS#65.

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Convert the Unreleased block to 1.17.0 (2026-07-19) and bump package.json +
lock, via scripts/prepare-release.mjs. Ships the accumulated dev batch: web
accent (#276), feel/texture tokens + setup theme step (#250), web-editable
gallery categories + q=config mediaCategories (#284), contact/podcast + analytics
config (#59), one-paste app sign-in link (#255), contentHtml on /api/posts (#292),
and the Tinylytics embed-uid fix (#288).

Co-authored-by: Samuel Lison <samuel@slcolonies.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@TemujinCalidius
TemujinCalidius merged commit c38649b into main Jul 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant