Ana içeriğe atla

Değişiklik Günlüğü

BoltHub'a yapılan her sürüm. Major ve minor güncellemeler önce, düzeltmeler altta listelenir.

Git
  1. Kırıcı@bolthub/landing·v0.4.0

    feat(i18n): add Turkish, Dutch, Danish, Swedish, French, Finnish, Greek locales

    • Dashboard and landing now ship tr, nl, da, sv, fr, fi, el message catalogs alongside en and de; both locales arrays extended and language switchers show the new flags and endonym labels
    • Guild Settings language picker, landing layout message imports, and the shared CookieConsent LocaleValue union all accept the new locales
    • Bot t() now resolves the seven new languages for event/RSVP/social-alert/music/access-control keys; English fallback unchanged

    feat(landing): full SEO overhaul — subpath i18n, hreflang, JSON-LD, dynamic OG, security headers

    Multi-block rework that lifts the landing site from cookie-based locale to
    crawlable subpath URLs and fills out every piece of metadata that
    multilingual SEO needs.

    - Subpath i18n routing (as-needed mode): English stays at /, the
    other eight locales live at /de, /tr, /nl, /da, /sv, /fr,
    /fi, /el. App tree restructured under app/[locale]/; the language
    switcher now navigates via next-intl's router with useTransition,
    so URL + visible language stay in sync and Google can crawl every
    variant. The next-intl middleware lives at src/proxy.ts (Next 16's
    successor to middleware.ts).
    - Metadata foundation: new lib/seo.ts with buildMetadata /
    canonicalUrl / localeUrl / languageAlternates helpers. Every page
    now ships its own canonical, hreflang map, OG, and Twitter card. Legal
    pages carry noindex. Root layout adds author/publisher/keywords/
    applicationName/Twitter handles and a dedicated not-found.tsx /
    app/[locale]/not-found.tsx pair.
    - Structured data: Organization, WebSite (with Sitelinks
    SearchAction), SoftwareApplication, FAQPage (mirrors the visible
    FAQ via the same i18n keys), and BreadcrumbList on legal pages.
    - Sitemap: dropped static public/sitemap.xml for a dynamic
    app/sitemap.ts that emits one URL per locale with hreflang
    alternates and env-aware base URL.
    - Resource hints + assets: preconnect/dns-prefetch to Discord CDN,
    PWA manifest.webmanifest, sized apple-touch-icon, Navbar logo gets
    priority + fetchPriority="high" (LCP).
    - Security headers: HSTS (prod-only), Permissions-Policy,
    Cross-Origin-Opener-Policy, plus CSP extension to allow
    cdn.discordapp.com images so event-page avatars actually load.
    - Dynamic OG image: app/[locale]/opengraph-image.tsx via
    next/og's ImageResponse replaces the static /og-image.jpg for
    per-page branded cards.
    - New i18n notFound namespace across all nine locales.

    feat(landing): SEO polish + public changelog route

    Builds on the SEO overhaul (subpath i18n, hreflang, JSON-LD, dynamic OG)
    with a public changelog page and the polish items the prior PR deferred.

    - `/changelog` route (locale-aware, indexable). Reads each app's
    CHANGELOG.md from disk at build time, parses Changeset section
    headers, and renders one card per release with badges per level
    (Breaking / Feature / Fix). Mechanical "Updated dependencies" lines
    are dropped. Anchor IDs per entry (#landing-0.2.5) make releases
    deep-linkable. Footer link in Resources, sitemap entry, breadcrumb
    JSON-LD. Auto-syncs at every release because the parser re-reads
    fresh CHANGELOG.md on each build.
    - RSS 2.0 feed at /changelog/feed.xml, surfaced via
    <link rel="alternate" type="application/rss+xml">.
    - Article JSON-LD per release. datePublished resolved at build
    time by walking git log -p --follow on each CHANGELOG.md and
    picking the commit that first introduced the version header.
    Releases without resolvable history are emitted without a date.
    - Per-locale OG cards. app/[locale]/opengraph-image.tsx reads
    the route param and pulls headline + tagline from the matching
    og.* namespace in messages/{locale}.json.
    - `favicon.ico` generated from logo-color.png (multi-size
    16/32/48/64/128/256, trimmed + zoomed to fill canvas).
    - Differentiated `changeFrequency` + `priority` in sitemap.ts
    (RouteSpec records, translated variants × 0.9 multiplier).
    - `<noscript>` Hero fallback for non-JS crawlers.
    - Footer icons for every link (IoGridOutline, IoPricetagOutline,
    IoSpeedometerOutline, IoSparklesOutline, IoDocumentTextOutline,
    IoShieldCheckmarkOutline, IoBusinessOutline).
    - Hash-anchor routing fix. Footer + Navbar #features /
    #pricing style links now route through IntlLink href="/#…" so
    they work from any sub-route, not only from the home page.
    - Locale switch keeps scroll position (router.replace(..., { scroll: false })).
    - Smooth-scroll opt-in via data-scroll-behavior="smooth" on
    <html> so route transitions are instant while in-page hash jumps
    stay smooth.
    - `LegalPageTemplate` server/client boundary fix. Icon prop now
    takes a pre-rendered ReactNode instead of a component reference,
    which is not serializable across the boundary.
    - `scripts/build.sh` mirrors scripts/dev.sh up but boots every
    app in production-build mode (separate bolt-build tmux session,
    green status accent) for realistic perf/SEO checks.
    - `docs/SEO.md` captures the full surface — what is shipped, the
    decisions behind it, and the remaining open items.

  2. Güncelleme@bolthub/landing·v0.4.0

    feat(landing): TOC sidebar on /changelog with grouped per-app navigation

    Two-column layout on lg+: sticky TOC sidebar (left) lists every release
    grouped by app with a level-coloured dot per version, anchored at the
    matching #{app}-{version} IDs in the main content. Below lg the TOC
    collapses into a <details> "Jump to" summary so the page stays
    mobile-friendly without hiding content from crawlers.

    Adds changelog.tocTitle + changelog.jumpToLabel keys across all
    nine locales.

    perf(hero): lazy-hydrate LogoBeam canvas via dynamic import

    Splits the ~380-line Canvas 2D animation out of the initial Hero JS
    chunk. The heading, subtitle, version badge, and CTAs hydrate first;
    the decorative beam canvas (aria-hidden) streams in afterwards as its
    own chunk. Shortens time-to-interactive on the initial visit without
    changing the visual output — same DPR cap, same beam count, same
    off-screen RAF pause as before.

    Reduced-motion overhaul: custom useReducedMotionPref hook + smooth scroll.

    useReducedMotion() from motion/react only reads matchMedia — it ignores both MotionConfig AND our cookie override, so every custom RAF / setInterval / IntersectionObserver-driven animation kept running even after the user toggled "Reduce Motion" in the Cookie Consent panel.

    New useReducedMotionPref hook in @bolthub/ui:

    • Reactive: reads html[data-reduced-motion] attribute + prefers-reduced-motion media query
    • MutationObserver on the attribute + matchMedia.change listener
    • Starts false on server + first client render (SSR-safe), reconciles via effect

    Swapped into: FlipWords, MovingBorder(+Button/BorderGlow), Lamp, TracingBeam, FloatingHexagons, Navbar EdgeBeam, LogoBeam, Reveal, SocialProof AnimatedCounter.

    Cookie Consent toggle now writes the cookie and reloads so both the <html data-reduced-motion> attribute AND MotionConfig reducedMotion context come from the same server render.

    Landing: html { scroll-behavior: smooth } with reduced-motion override to auto. Tailwind motion-reduce:hidden extended to respect the cookie-driven html[data-reduced-motion="on"] selector.

  3. Kırıcı@bolthub/api·v0.3.0

    Manager Roles + granular per-module role access.

    The landing page has been promising Manager Roles + Granular Dashboard Access Control for a while; the managerRoles table existed but checkGuildAccess() never consulted it. This changeset wires that promise end-to-end and extends it with per-module role grants that cover both dashboard configuration and Discord-side usage.

    Permission hierarchy (final):

    1. Guild Owner — unconstrained; only owner can edit the Manager Roles list.
    2. Global Managermembers.isManager = true or any Discord role in managerRoles. Full dashboard + module access.
    3. Module Configure Grant — role in module_role_grants with kind = 'configure'. May edit that module's dashboard settings + manage its use grants.
    4. Module Use Grant — role in module_role_grants with kind = 'use'. May use the module in Discord (slash commands, buttons, WS controls, public player controls). If a module has zero use grants, it defaults to open for every guild member.

    New schema: module_role_grants (guildId, module, roleId, kind) with unique + composite indexes.

    New API routes:

    • GET /guilds/:guildId/access — self-access resolver (returns empty on no-access rather than 403 so dashboard can render a useful state).
    • GET|POST|DELETE /guilds/:guildId/manager-roles (POST/DELETE owner-only).
    • GET|POST|DELETE /guilds/:guildId/modules/:module/grants.

    Bot: new Redis channel bolthub:guilds:get-member-roles (API-side cached 60s). interactionCreate runs checkModuleUse before executing music buttons and RSVP buttons. New guildMemberUpdate listener invalidates the member-roles cache key on role diff so a demoted user loses access immediately (60s TTL is only the safety-net).

    API: new requireModuleConfigure(module) middleware replaces requireGuildAccess on feature routes (embeds, events, social-alerts, music config). Music WS handshake now gates on access.useModules.has('music').

    Dashboard: Settings page gains a Manager Roles section (owner-only mutation). Each live-module page gains a Module Access panel (configure / use grants). Sidebar filters nav items for module-configure grantees who aren't blanket managers.

    Docs: docs/schema.md, ARCHITECTURE.md, BUILD_ORDER.md, redis-protocol.md updated.

    i18n: new managerRoles + access namespaces in apps/dashboard/messages/{en,de}.json; new access.* keys in apps/bot/src/lib/i18n.ts (en + de).

    Known limitations (follow-ups): orphaned grants (role deleted in Discord) are not cleaned up; configure-grantees without Discord MANAGE_GUILD don't yet appear in the /guilds switcher.

  4. Güncelleme@bolthub/api·v0.3.0

    Channel-permission awareness across the bot/API/dashboard stack.

    The dashboard's ChannelSelect used to show every channel the bot could
    _see_, not every channel the bot could _post in_. Admins could pick a
    channel where the bot silently failed to send — especially painful on
    the onboarding Update-Channel picker where that misconfig wasn't caught
    until the first broadcast went out.

    Fix is full-stack so it can't be bypassed from any surface:

    - Bot (apps/bot/src/modules/guilds/guilds.handler.ts):
    bolthub:guilds:get-channels now resolves the bot member's real
    permission overwrites per channel and returns writable + speakable
    booleans alongside the existing shape. writable requires
    ViewChannel + SendMessages + EmbedLinks; speakable requires
    ViewChannel + Connect + Speak. Voice-type channels report
    writable: false on purpose — BoltHub never posts embeds into a
    voice-text buffer.

    - API (apps/api/src/lib/channel-permissions.ts): new
    assertChannelCapability(guildId, channelId, need) helper. Throws
    ConflictError (409) with a user-facing message listing the missing
    Discord permissions, or NotFoundError if the channel id is unknown
    to the bot. Wired into every save-path that persists a channelId:

    - PUT /guilds/:guildId/broadcast-subscription
    - POST /guilds/:guildId/embeds, PATCH /:embedId, POST /:embedId/send
    - POST /guilds/:guildId/events, PATCH /:eventId,
    POST /schedules, PATCH /schedules/:scheduleId
    - POST /guilds/:guildId/social-alerts, PATCH /:alertId

    guilds.controller GET /channels inline response type updated to
    match the new shape.

    - Dashboard (apps/dashboard/src/components/ChannelSelect.tsx,
    hooks/use-guild-channels.ts):

    - DiscordChannel interface gains writable + speakable.
    - New useRefreshGuildChannels(guildId) hook invalidates the cache so
    admins who just granted permissions can pick the channel without a
    full page reload.
    - ChannelSelect renders inaccessible channels disabled with a
    warning icon, a (Kein Zugriff) badge inline, and an animate-ui
    Tooltip ("Bot lacks permission in this channel — grant View
    Channel, Send Messages, and Embed Links…") on hover. Radix's
    default pointer-events: none on disabled items is overridden with
    !pointer-events-auto so hover events reach the Tooltip trigger;
    Radix still refuses selection via aria-disabled internally so the
    gate can't be bypassed through CSS.
    - showRefresh prop (default true) mounts a refresh icon-button
    next to the dropdown that spins while fetching.

    - i18n: three new common keys in both en.json + de.json
    channelNoAccess (full tooltip text), channelNoAccessShort
    ("No access" / "Kein Zugriff" inline badge), and
    refreshChannelPermissions (refresh button label).

    - Docs: docs/redis-protocol.md bolthub:guilds:get-channels
    section updated with the new response shape. docs/brand.md
    ChannelSelect section expanded with the permission-handling rules
    and the API-side guard reference.

    Verified: typecheck + lint green across all 7 packages. Bot + API
    restarted under tsx watch. Channel dropdown now disables
    non-writable channels with tooltip.

    Fix configure-grantees editing module grants + self-role preservation.

    module_role_grants routes were guarded by requireGuildAccess
    (blanket managers only). Configure-grantees therefore couldn't even
    GET the grant list for their module — the Use Roles dropdown on the
    Access Panel rendered but stayed empty, and the Add-button 403'd.

    Routes now use requireAnyGuildAccess; service-level
    assertMayMutate already scoped mutations correctly (configure-only
    for blanket, use-only for configure-grantees).

    Added assertNotSelfRole: non-blanket callers cannot add or remove
    grants for one of their own Discord roles. Without this a configure-
    grantee could revoke the very role that grants them access and lock
    out the whole role. Owners + global-managers bypass.

    /guilds/:guildId/access now returns the caller's roleIds so the
    dashboard hides the remove-button + disables the add-button for
    self-role grants. Self-role grants show a "Your role" badge so it's
    obvious why they're read-only.

    Fix: dashboard Play button dead after Stop / Leave.

    The persisted-queue work landed but the dashboard had no way to
    actually trigger a resume — the center Play button was wired to
    onResume (unpause) and disabled whenever state.status === "idle",
    which is exactly the state Stop + Leave produce.

    New WS command resume-queue → bot channel bolthub:music:resume-queue:

    - Ensures player (auto-join caller's voice) + restores Redis snapshot
    (if not already loaded) + kicks off playback
    - Throws ValidationError when nothing is persisted (UI keeps the
    button disabled in that case)

    Center Play button now branches on status:

    • playing → Pause
    • paused → Resume (unpause existing player)
    • idle + queue has tracks → Resume Queue (new path)
    • idle + queue empty → disabled

    Page-width wrapper + HTTP cache hardening.

    Dashboard: new <Page width="narrow|default|wide"> component applied
    to every /guilds/:guildId/** route. Lists + overview get
    max-w-6xl, settings forms get max-w-3xl, music player + embed
    editor get max-w-none. Player (/player/**) and Admin routes are
    intentionally untouched. Picked the component-wrapper approach over
    Next route-groups because /embeds list and /embeds/[id] editor
    need different widths under the same segment — route-groups don't
    support segment-sharing across groups, a wrapper keeps the
    decision co-located with the page.

    API: every response now gets Cache-Control: private, no-store
    unless the handler already set one. Fixes a list-stale-after-mutate
    bug where the client invalidated ["embeds", guildId] correctly,
    but the refetch was served from browser heuristic cache instead of
    hitting the API. React Query already owns client-side freshness —
    HTTP cache on top was working against it.

    Guild picker: hide bot-present guilds where user has no BoltHub access.

    Previously the /guilds picker was filtered only by Discord
    MANAGE_GUILD, so a user with MANAGE_GUILD but no BoltHub grant saw
    the server with a "Manage" CTA that then landed on the No Access page.

    filterAccessibleGuildIds now gates bot-present guilds on owner
    status, members.isManager, manager_roles role match, or ANY
    module_role_grants row (configure OR use) that matches a role the
    user holds. Bot-absent guilds continue to use the MANAGE_GUILD
    filter so invite-flow discovery still works.

  5. Kırıcı@bolthub/bot·v0.3.0

    feat(i18n): add Turkish, Dutch, Danish, Swedish, French, Finnish, Greek locales

    • Dashboard and landing now ship tr, nl, da, sv, fr, fi, el message catalogs alongside en and de; both locales arrays extended and language switchers show the new flags and endonym labels
    • Guild Settings language picker, landing layout message imports, and the shared CookieConsent LocaleValue union all accept the new locales
    • Bot t() now resolves the seven new languages for event/RSVP/social-alert/music/access-control keys; English fallback unchanged

    Manager Roles + granular per-module role access.

    The landing page has been promising Manager Roles + Granular Dashboard Access Control for a while; the managerRoles table existed but checkGuildAccess() never consulted it. This changeset wires that promise end-to-end and extends it with per-module role grants that cover both dashboard configuration and Discord-side usage.

    Permission hierarchy (final):

    1. Guild Owner — unconstrained; only owner can edit the Manager Roles list.
    2. Global Managermembers.isManager = true or any Discord role in managerRoles. Full dashboard + module access.
    3. Module Configure Grant — role in module_role_grants with kind = 'configure'. May edit that module's dashboard settings + manage its use grants.
    4. Module Use Grant — role in module_role_grants with kind = 'use'. May use the module in Discord (slash commands, buttons, WS controls, public player controls). If a module has zero use grants, it defaults to open for every guild member.

    New schema: module_role_grants (guildId, module, roleId, kind) with unique + composite indexes.

    New API routes:

    • GET /guilds/:guildId/access — self-access resolver (returns empty on no-access rather than 403 so dashboard can render a useful state).
    • GET|POST|DELETE /guilds/:guildId/manager-roles (POST/DELETE owner-only).
    • GET|POST|DELETE /guilds/:guildId/modules/:module/grants.

    Bot: new Redis channel bolthub:guilds:get-member-roles (API-side cached 60s). interactionCreate runs checkModuleUse before executing music buttons and RSVP buttons. New guildMemberUpdate listener invalidates the member-roles cache key on role diff so a demoted user loses access immediately (60s TTL is only the safety-net).

    API: new requireModuleConfigure(module) middleware replaces requireGuildAccess on feature routes (embeds, events, social-alerts, music config). Music WS handshake now gates on access.useModules.has('music').

    Dashboard: Settings page gains a Manager Roles section (owner-only mutation). Each live-module page gains a Module Access panel (configure / use grants). Sidebar filters nav items for module-configure grantees who aren't blanket managers.

    Docs: docs/schema.md, ARCHITECTURE.md, BUILD_ORDER.md, redis-protocol.md updated.

    i18n: new managerRoles + access namespaces in apps/dashboard/messages/{en,de}.json; new access.* keys in apps/bot/src/lib/i18n.ts (en + de).

    Known limitations (follow-ups): orphaned grants (role deleted in Discord) are not cleaned up; configure-grantees without Discord MANAGE_GUILD don't yet appear in the /guilds switcher.

    Persist music queue across bot disconnects.

    Live queue (current track + upcoming + volume + loop + shuffle) is
    mirrored to Redis under bolthub:music:queue:{guildId} with a 7-day
    TTL. The queue now survives:

    - Idle timeout (5min alone in voice) — bot leaves but queue stays
    - User presses Stop — bot leaves, queue stays, "Clear Queue" button
    wipes it explicitly
    - User presses Leave — same as Stop
    - Voice-channel switch (Join other VC) — queue carries over
    - Bot process restart / crash
    - Lavalink node crash

    Cleared only on:

    - Natural queue end (playerEmpty with nothing left)
    - User clicks Clear Queue button (already present in MusicQueue UI —
    backend now also wipes Redis)
    - User triggers queue:replace-from-playlist (old queue replaced)

    Implementation:

    - persistQueueState(player) serializes the live state; wired into
    Kazagumo playerStart / playerEnd events and every mutating
    handler (queue-add / remove / reorder, volume, loop, shuffle).
    - restorePersistedQueue(player, requestedBy) runs immediately after
    every createPlayer() in handlePlay / handleQueueAdd / handleJoin.
    Tracks are re-searched through Lavalink by URI (slower than
    decoding cached blobs, but robust across Lavalink upgrades and
    reuses the existing search-with-fallback pattern).
    - Current-track position is NOT preserved; resume starts the track
    from 0s. Simpler, 99% UX-OK.
    - handleState falls back to the Redis snapshot when no live player
    exists, so the dashboard keeps showing the persisted queue after
    Stop / Leave / idle-timeout / restart — users see what will resume
    when they press Play.

    Redis itself uses RDB snapshots + volume mount, so Redis restarts
    don't drop state either.

  6. Güncelleme@bolthub/bot·v0.3.0

    Channel-permission awareness across the bot/API/dashboard stack.

    The dashboard's ChannelSelect used to show every channel the bot could
    _see_, not every channel the bot could _post in_. Admins could pick a
    channel where the bot silently failed to send — especially painful on
    the onboarding Update-Channel picker where that misconfig wasn't caught
    until the first broadcast went out.

    Fix is full-stack so it can't be bypassed from any surface:

    - Bot (apps/bot/src/modules/guilds/guilds.handler.ts):
    bolthub:guilds:get-channels now resolves the bot member's real
    permission overwrites per channel and returns writable + speakable
    booleans alongside the existing shape. writable requires
    ViewChannel + SendMessages + EmbedLinks; speakable requires
    ViewChannel + Connect + Speak. Voice-type channels report
    writable: false on purpose — BoltHub never posts embeds into a
    voice-text buffer.

    - API (apps/api/src/lib/channel-permissions.ts): new
    assertChannelCapability(guildId, channelId, need) helper. Throws
    ConflictError (409) with a user-facing message listing the missing
    Discord permissions, or NotFoundError if the channel id is unknown
    to the bot. Wired into every save-path that persists a channelId:

    - PUT /guilds/:guildId/broadcast-subscription
    - POST /guilds/:guildId/embeds, PATCH /:embedId, POST /:embedId/send
    - POST /guilds/:guildId/events, PATCH /:eventId,
    POST /schedules, PATCH /schedules/:scheduleId
    - POST /guilds/:guildId/social-alerts, PATCH /:alertId

    guilds.controller GET /channels inline response type updated to
    match the new shape.

    - Dashboard (apps/dashboard/src/components/ChannelSelect.tsx,
    hooks/use-guild-channels.ts):

    - DiscordChannel interface gains writable + speakable.
    - New useRefreshGuildChannels(guildId) hook invalidates the cache so
    admins who just granted permissions can pick the channel without a
    full page reload.
    - ChannelSelect renders inaccessible channels disabled with a
    warning icon, a (Kein Zugriff) badge inline, and an animate-ui
    Tooltip ("Bot lacks permission in this channel — grant View
    Channel, Send Messages, and Embed Links…") on hover. Radix's
    default pointer-events: none on disabled items is overridden with
    !pointer-events-auto so hover events reach the Tooltip trigger;
    Radix still refuses selection via aria-disabled internally so the
    gate can't be bypassed through CSS.
    - showRefresh prop (default true) mounts a refresh icon-button
    next to the dropdown that spins while fetching.

    - i18n: three new common keys in both en.json + de.json
    channelNoAccess (full tooltip text), channelNoAccessShort
    ("No access" / "Kein Zugriff" inline badge), and
    refreshChannelPermissions (refresh button label).

    - Docs: docs/redis-protocol.md bolthub:guilds:get-channels
    section updated with the new response shape. docs/brand.md
    ChannelSelect section expanded with the permission-handling rules
    and the API-side guard reference.

    Verified: typecheck + lint green across all 7 packages. Bot + API
    restarted under tsx watch. Channel dropdown now disables
    non-writable channels with tooltip.

    Fix: dashboard Play button dead after Stop / Leave.

    The persisted-queue work landed but the dashboard had no way to
    actually trigger a resume — the center Play button was wired to
    onResume (unpause) and disabled whenever state.status === "idle",
    which is exactly the state Stop + Leave produce.

    New WS command resume-queue → bot channel bolthub:music:resume-queue:

    - Ensures player (auto-join caller's voice) + restores Redis snapshot
    (if not already loaded) + kicks off playback
    - Throws ValidationError when nothing is persisted (UI keeps the
    button disabled in that case)

    Center Play button now branches on status:

    • playing → Pause
    • paused → Resume (unpause existing player)
    • idle + queue has tracks → Resume Queue (new path)
    • idle + queue empty → disabled
  7. Kırıcı@bolthub/dashboard·v0.3.0

    feat(i18n): add Turkish, Dutch, Danish, Swedish, French, Finnish, Greek locales

    • Dashboard and landing now ship tr, nl, da, sv, fr, fi, el message catalogs alongside en and de; both locales arrays extended and language switchers show the new flags and endonym labels
    • Guild Settings language picker, landing layout message imports, and the shared CookieConsent LocaleValue union all accept the new locales
    • Bot t() now resolves the seven new languages for event/RSVP/social-alert/music/access-control keys; English fallback unchanged

    Manager Roles + granular per-module role access.

    The landing page has been promising Manager Roles + Granular Dashboard Access Control for a while; the managerRoles table existed but checkGuildAccess() never consulted it. This changeset wires that promise end-to-end and extends it with per-module role grants that cover both dashboard configuration and Discord-side usage.

    Permission hierarchy (final):

    1. Guild Owner — unconstrained; only owner can edit the Manager Roles list.
    2. Global Managermembers.isManager = true or any Discord role in managerRoles. Full dashboard + module access.
    3. Module Configure Grant — role in module_role_grants with kind = 'configure'. May edit that module's dashboard settings + manage its use grants.
    4. Module Use Grant — role in module_role_grants with kind = 'use'. May use the module in Discord (slash commands, buttons, WS controls, public player controls). If a module has zero use grants, it defaults to open for every guild member.

    New schema: module_role_grants (guildId, module, roleId, kind) with unique + composite indexes.

    New API routes:

    • GET /guilds/:guildId/access — self-access resolver (returns empty on no-access rather than 403 so dashboard can render a useful state).
    • GET|POST|DELETE /guilds/:guildId/manager-roles (POST/DELETE owner-only).
    • GET|POST|DELETE /guilds/:guildId/modules/:module/grants.

    Bot: new Redis channel bolthub:guilds:get-member-roles (API-side cached 60s). interactionCreate runs checkModuleUse before executing music buttons and RSVP buttons. New guildMemberUpdate listener invalidates the member-roles cache key on role diff so a demoted user loses access immediately (60s TTL is only the safety-net).

    API: new requireModuleConfigure(module) middleware replaces requireGuildAccess on feature routes (embeds, events, social-alerts, music config). Music WS handshake now gates on access.useModules.has('music').

    Dashboard: Settings page gains a Manager Roles section (owner-only mutation). Each live-module page gains a Module Access panel (configure / use grants). Sidebar filters nav items for module-configure grantees who aren't blanket managers.

    Docs: docs/schema.md, ARCHITECTURE.md, BUILD_ORDER.md, redis-protocol.md updated.

    i18n: new managerRoles + access namespaces in apps/dashboard/messages/{en,de}.json; new access.* keys in apps/bot/src/lib/i18n.ts (en + de).

    Known limitations (follow-ups): orphaned grants (role deleted in Discord) are not cleaned up; configure-grantees without Discord MANAGE_GUILD don't yet appear in the /guilds switcher.

  8. Güncelleme@bolthub/dashboard·v0.3.0

    Move Module Access Panel into module settings route.

    Access configuration now lives at
    /guilds/:guildId/modules/:module/settings alongside the module's
    other settings, instead of being a card on the module's main list
    page. Keeps access + module config in one place and declutters the
    embeds + music landing pages.

    Channel-permission awareness across the bot/API/dashboard stack.

    The dashboard's ChannelSelect used to show every channel the bot could
    _see_, not every channel the bot could _post in_. Admins could pick a
    channel where the bot silently failed to send — especially painful on
    the onboarding Update-Channel picker where that misconfig wasn't caught
    until the first broadcast went out.

    Fix is full-stack so it can't be bypassed from any surface:

    - Bot (apps/bot/src/modules/guilds/guilds.handler.ts):
    bolthub:guilds:get-channels now resolves the bot member's real
    permission overwrites per channel and returns writable + speakable
    booleans alongside the existing shape. writable requires
    ViewChannel + SendMessages + EmbedLinks; speakable requires
    ViewChannel + Connect + Speak. Voice-type channels report
    writable: false on purpose — BoltHub never posts embeds into a
    voice-text buffer.

    - API (apps/api/src/lib/channel-permissions.ts): new
    assertChannelCapability(guildId, channelId, need) helper. Throws
    ConflictError (409) with a user-facing message listing the missing
    Discord permissions, or NotFoundError if the channel id is unknown
    to the bot. Wired into every save-path that persists a channelId:

    - PUT /guilds/:guildId/broadcast-subscription
    - POST /guilds/:guildId/embeds, PATCH /:embedId, POST /:embedId/send
    - POST /guilds/:guildId/events, PATCH /:eventId,
    POST /schedules, PATCH /schedules/:scheduleId
    - POST /guilds/:guildId/social-alerts, PATCH /:alertId

    guilds.controller GET /channels inline response type updated to
    match the new shape.

    - Dashboard (apps/dashboard/src/components/ChannelSelect.tsx,
    hooks/use-guild-channels.ts):

    - DiscordChannel interface gains writable + speakable.
    - New useRefreshGuildChannels(guildId) hook invalidates the cache so
    admins who just granted permissions can pick the channel without a
    full page reload.
    - ChannelSelect renders inaccessible channels disabled with a
    warning icon, a (Kein Zugriff) badge inline, and an animate-ui
    Tooltip ("Bot lacks permission in this channel — grant View
    Channel, Send Messages, and Embed Links…") on hover. Radix's
    default pointer-events: none on disabled items is overridden with
    !pointer-events-auto so hover events reach the Tooltip trigger;
    Radix still refuses selection via aria-disabled internally so the
    gate can't be bypassed through CSS.
    - showRefresh prop (default true) mounts a refresh icon-button
    next to the dropdown that spins while fetching.

    - i18n: three new common keys in both en.json + de.json
    channelNoAccess (full tooltip text), channelNoAccessShort
    ("No access" / "Kein Zugriff" inline badge), and
    refreshChannelPermissions (refresh button label).

    - Docs: docs/redis-protocol.md bolthub:guilds:get-channels
    section updated with the new response shape. docs/brand.md
    ChannelSelect section expanded with the permission-handling rules
    and the API-side guard reference.

    Verified: typecheck + lint green across all 7 packages. Bot + API
    restarted under tsx watch. Channel dropdown now disables
    non-writable channels with tooltip.

    Fix configure-grantees editing module grants + self-role preservation.

    module_role_grants routes were guarded by requireGuildAccess
    (blanket managers only). Configure-grantees therefore couldn't even
    GET the grant list for their module — the Use Roles dropdown on the
    Access Panel rendered but stayed empty, and the Add-button 403'd.

    Routes now use requireAnyGuildAccess; service-level
    assertMayMutate already scoped mutations correctly (configure-only
    for blanket, use-only for configure-grantees).

    Added assertNotSelfRole: non-blanket callers cannot add or remove
    grants for one of their own Discord roles. Without this a configure-
    grantee could revoke the very role that grants them access and lock
    out the whole role. Owners + global-managers bypass.

    /guilds/:guildId/access now returns the caller's roleIds so the
    dashboard hides the remove-button + disables the add-button for
    self-role grants. Self-role grants show a "Your role" badge so it's
    obvious why they're read-only.

    Fix: dashboard Play button dead after Stop / Leave.

    The persisted-queue work landed but the dashboard had no way to
    actually trigger a resume — the center Play button was wired to
    onResume (unpause) and disabled whenever state.status === "idle",
    which is exactly the state Stop + Leave produce.

    New WS command resume-queue → bot channel bolthub:music:resume-queue:

    - Ensures player (auto-join caller's voice) + restores Redis snapshot
    (if not already loaded) + kicks off playback
    - Throws ValidationError when nothing is persisted (UI keeps the
    button disabled in that case)

    Center Play button now branches on status:

    • playing → Pause
    • paused → Resume (unpause existing player)
    • idle + queue has tracks → Resume Queue (new path)
    • idle + queue empty → disabled

    Page-width wrapper + HTTP cache hardening.

    Dashboard: new <Page width="narrow|default|wide"> component applied
    to every /guilds/:guildId/** route. Lists + overview get
    max-w-6xl, settings forms get max-w-3xl, music player + embed
    editor get max-w-none. Player (/player/**) and Admin routes are
    intentionally untouched. Picked the component-wrapper approach over
    Next route-groups because /embeds list and /embeds/[id] editor
    need different widths under the same segment — route-groups don't
    support segment-sharing across groups, a wrapper keeps the
    decision co-located with the page.

    API: every response now gets Cache-Control: private, no-store
    unless the handler already set one. Fixes a list-stale-after-mutate
    bug where the client invalidated ["embeds", guildId] correctly,
    but the refetch was served from browser heuristic cache instead of
    hitting the API. React Query already owns client-side freshness —
    HTTP cache on top was working against it.

    fix(sidebar): drive active/hover indicator with motion layoutId

    Replaces the ref-based getBoundingClientRect + useEffect measurement
    in Sidebar with motion/react's layoutId FLIP. The indicator pill and
    orange active bracket now live inside the active <li> itself, so they
    reposition automatically whenever nav items resize — including when
    labels wrap to two lines on locale switch, when fonts finish loading, or
    when the Modules-enabled dot appears/disappears. Previously the rect was
    only recomputed on activeKey / hoveredKey / pathname changes, so a
    locale swap left the indicator stranded at the old coordinates.

  9. Kırıcı@bolthub/landing·v0.3.0

    feat(i18n): add Turkish, Dutch, Danish, Swedish, French, Finnish, Greek locales

    • Dashboard and landing now ship tr, nl, da, sv, fr, fi, el message catalogs alongside en and de; both locales arrays extended and language switchers show the new flags and endonym labels
    • Guild Settings language picker, landing layout message imports, and the shared CookieConsent LocaleValue union all accept the new locales
    • Bot t() now resolves the seven new languages for event/RSVP/social-alert/music/access-control keys; English fallback unchanged

    feat(landing): full SEO overhaul — subpath i18n, hreflang, JSON-LD, dynamic OG, security headers

    Multi-block rework that lifts the landing site from cookie-based locale to
    crawlable subpath URLs and fills out every piece of metadata that
    multilingual SEO needs.

    - Subpath i18n routing (as-needed mode): English stays at /, the
    other eight locales live at /de, /tr, /nl, /da, /sv, /fr,
    /fi, /el. App tree restructured under app/[locale]/; the language
    switcher now navigates via next-intl's router with useTransition,
    so URL + visible language stay in sync and Google can crawl every
    variant. The next-intl middleware lives at src/proxy.ts (Next 16's
    successor to middleware.ts).
    - Metadata foundation: new lib/seo.ts with buildMetadata /
    canonicalUrl / localeUrl / languageAlternates helpers. Every page
    now ships its own canonical, hreflang map, OG, and Twitter card. Legal
    pages carry noindex. Root layout adds author/publisher/keywords/
    applicationName/Twitter handles and a dedicated not-found.tsx /
    app/[locale]/not-found.tsx pair.
    - Structured data: Organization, WebSite (with Sitelinks
    SearchAction), SoftwareApplication, FAQPage (mirrors the visible
    FAQ via the same i18n keys), and BreadcrumbList on legal pages.
    - Sitemap: dropped static public/sitemap.xml for a dynamic
    app/sitemap.ts that emits one URL per locale with hreflang
    alternates and env-aware base URL.
    - Resource hints + assets: preconnect/dns-prefetch to Discord CDN,
    PWA manifest.webmanifest, sized apple-touch-icon, Navbar logo gets
    priority + fetchPriority="high" (LCP).
    - Security headers: HSTS (prod-only), Permissions-Policy,
    Cross-Origin-Opener-Policy, plus CSP extension to allow
    cdn.discordapp.com images so event-page avatars actually load.
    - Dynamic OG image: app/[locale]/opengraph-image.tsx via
    next/og's ImageResponse replaces the static /og-image.jpg for
    per-page branded cards.
    - New i18n notFound namespace across all nine locales.

    feat(landing): SEO polish + public changelog route

    Builds on the SEO overhaul (subpath i18n, hreflang, JSON-LD, dynamic OG)
    with a public changelog page and the polish items the prior PR deferred.

    - `/changelog` route (locale-aware, indexable). Reads each app's
    CHANGELOG.md from disk at build time, parses Changeset section
    headers, and renders one card per release with badges per level
    (Breaking / Feature / Fix). Mechanical "Updated dependencies" lines
    are dropped. Anchor IDs per entry (#landing-0.2.5) make releases
    deep-linkable. Footer link in Resources, sitemap entry, breadcrumb
    JSON-LD. Auto-syncs at every release because the parser re-reads
    fresh CHANGELOG.md on each build.
    - RSS 2.0 feed at /changelog/feed.xml, surfaced via
    <link rel="alternate" type="application/rss+xml">.
    - Article JSON-LD per release. datePublished resolved at build
    time by walking git log -p --follow on each CHANGELOG.md and
    picking the commit that first introduced the version header.
    Releases without resolvable history are emitted without a date.
    - Per-locale OG cards. app/[locale]/opengraph-image.tsx reads
    the route param and pulls headline + tagline from the matching
    og.* namespace in messages/{locale}.json.
    - `favicon.ico` generated from logo-color.png (multi-size
    16/32/48/64/128/256, trimmed + zoomed to fill canvas).
    - Differentiated `changeFrequency` + `priority` in sitemap.ts
    (RouteSpec records, translated variants × 0.9 multiplier).
    - `<noscript>` Hero fallback for non-JS crawlers.
    - Footer icons for every link (IoGridOutline, IoPricetagOutline,
    IoSpeedometerOutline, IoSparklesOutline, IoDocumentTextOutline,
    IoShieldCheckmarkOutline, IoBusinessOutline).
    - Hash-anchor routing fix. Footer + Navbar #features /
    #pricing style links now route through IntlLink href="/#…" so
    they work from any sub-route, not only from the home page.
    - Locale switch keeps scroll position (router.replace(..., { scroll: false })).
    - Smooth-scroll opt-in via data-scroll-behavior="smooth" on
    <html> so route transitions are instant while in-page hash jumps
    stay smooth.
    - `LegalPageTemplate` server/client boundary fix. Icon prop now
    takes a pre-rendered ReactNode instead of a component reference,
    which is not serializable across the boundary.
    - `scripts/build.sh` mirrors scripts/dev.sh up but boots every
    app in production-build mode (separate bolt-build tmux session,
    green status accent) for realistic perf/SEO checks.
    - `docs/SEO.md` captures the full surface — what is shipped, the
    decisions behind it, and the remaining open items.

  10. Güncelleme@bolthub/landing·v0.3.0

    perf(hero): lazy-hydrate LogoBeam canvas via dynamic import

    Splits the ~380-line Canvas 2D animation out of the initial Hero JS
    chunk. The heading, subtitle, version badge, and CTAs hydrate first;
    the decorative beam canvas (aria-hidden) streams in afterwards as its
    own chunk. Shortens time-to-interactive on the initial visit without
    changing the visual output — same DPR cap, same beam count, same
    off-screen RAF pause as before.

    Reduced-motion overhaul: custom useReducedMotionPref hook + smooth scroll.

    useReducedMotion() from motion/react only reads matchMedia — it ignores both MotionConfig AND our cookie override, so every custom RAF / setInterval / IntersectionObserver-driven animation kept running even after the user toggled "Reduce Motion" in the Cookie Consent panel.

    New useReducedMotionPref hook in @bolthub/ui:

    • Reactive: reads html[data-reduced-motion] attribute + prefers-reduced-motion media query
    • MutationObserver on the attribute + matchMedia.change listener
    • Starts false on server + first client render (SSR-safe), reconciles via effect

    Swapped into: FlipWords, MovingBorder(+Button/BorderGlow), Lamp, TracingBeam, FloatingHexagons, Navbar EdgeBeam, LogoBeam, Reveal, SocialProof AnimatedCounter.

    Cookie Consent toggle now writes the cookie and reloads so both the <html data-reduced-motion> attribute AND MotionConfig reducedMotion context come from the same server render.

    Landing: html { scroll-behavior: smooth } with reduced-motion override to auto. Tailwind motion-reduce:hidden extended to respect the cookie-driven html[data-reduced-motion="on"] selector.

  11. Güncelleme@bolthub/landing·v0.2.4

    Gate events + social_alerts as "coming soon" until finalized.

    - @bolthub/types: new LIVE_MODULES set + isModuleLive() helper —
    single source of truth for which modules can be enabled. Currently
    embeds + music only.
    - @bolthub/api: modules.service.updateModule throws ForbiddenError
    when enabling a non-live module, even if the plan would allow it.
    - @bolthub/dashboard: Sidebar + modules page consume isModuleLive
    instead of a local hardcoded set. ModuleGate shows a dedicated
    "Coming Soon" empty state (no Activate button) for non-live modules.
    New i18n keys modules.comingSoonTitle + comingSoonDescription
    (en + de).
    - @bolthub/landing: Features section marks events + socialAlerts
    as comingSoon; live modules (embeds, music) lead the grid.

  12. Kırıcı@bolthub/api·v0.2.0

    Manager Roles + granular per-module role access.

    The landing page has been promising Manager Roles + Granular Dashboard Access Control for a while; the managerRoles table existed but checkGuildAccess() never consulted it. This changeset wires that promise end-to-end and extends it with per-module role grants that cover both dashboard configuration and Discord-side usage.

    Permission hierarchy (final):

    1. Guild Owner — unconstrained; only owner can edit the Manager Roles list.
    2. Global Managermembers.isManager = true or any Discord role in managerRoles. Full dashboard + module access.
    3. Module Configure Grant — role in module_role_grants with kind = 'configure'. May edit that module's dashboard settings + manage its use grants.
    4. Module Use Grant — role in module_role_grants with kind = 'use'. May use the module in Discord (slash commands, buttons, WS controls, public player controls). If a module has zero use grants, it defaults to open for every guild member.

    New schema: module_role_grants (guildId, module, roleId, kind) with unique + composite indexes.

    New API routes:

    • GET /guilds/:guildId/access — self-access resolver (returns empty on no-access rather than 403 so dashboard can render a useful state).
    • GET|POST|DELETE /guilds/:guildId/manager-roles (POST/DELETE owner-only).
    • GET|POST|DELETE /guilds/:guildId/modules/:module/grants.

    Bot: new Redis channel bolthub:guilds:get-member-roles (API-side cached 60s). interactionCreate runs checkModuleUse before executing music buttons and RSVP buttons. New guildMemberUpdate listener invalidates the member-roles cache key on role diff so a demoted user loses access immediately (60s TTL is only the safety-net).

    API: new requireModuleConfigure(module) middleware replaces requireGuildAccess on feature routes (embeds, events, social-alerts, music config). Music WS handshake now gates on access.useModules.has('music').

    Dashboard: Settings page gains a Manager Roles section (owner-only mutation). Each live-module page gains a Module Access panel (configure / use grants). Sidebar filters nav items for module-configure grantees who aren't blanket managers.

    Docs: docs/schema.md, ARCHITECTURE.md, BUILD_ORDER.md, redis-protocol.md updated.

    i18n: new managerRoles + access namespaces in apps/dashboard/messages/{en,de}.json; new access.* keys in apps/bot/src/lib/i18n.ts (en + de).

    Known limitations (follow-ups): orphaned grants (role deleted in Discord) are not cleaned up; configure-grantees without Discord MANAGE_GUILD don't yet appear in the /guilds switcher.

  13. Güncelleme@bolthub/api·v0.2.0

    Channel-permission awareness across the bot/API/dashboard stack.

    The dashboard's ChannelSelect used to show every channel the bot could
    _see_, not every channel the bot could _post in_. Admins could pick a
    channel where the bot silently failed to send — especially painful on
    the onboarding Update-Channel picker where that misconfig wasn't caught
    until the first broadcast went out.

    Fix is full-stack so it can't be bypassed from any surface:

    - Bot (apps/bot/src/modules/guilds/guilds.handler.ts):
    bolthub:guilds:get-channels now resolves the bot member's real
    permission overwrites per channel and returns writable + speakable
    booleans alongside the existing shape. writable requires
    ViewChannel + SendMessages + EmbedLinks; speakable requires
    ViewChannel + Connect + Speak. Voice-type channels report
    writable: false on purpose — BoltHub never posts embeds into a
    voice-text buffer.

    - API (apps/api/src/lib/channel-permissions.ts): new
    assertChannelCapability(guildId, channelId, need) helper. Throws
    ConflictError (409) with a user-facing message listing the missing
    Discord permissions, or NotFoundError if the channel id is unknown
    to the bot. Wired into every save-path that persists a channelId:

    - PUT /guilds/:guildId/broadcast-subscription
    - POST /guilds/:guildId/embeds, PATCH /:embedId, POST /:embedId/send
    - POST /guilds/:guildId/events, PATCH /:eventId,
    POST /schedules, PATCH /schedules/:scheduleId
    - POST /guilds/:guildId/social-alerts, PATCH /:alertId

    guilds.controller GET /channels inline response type updated to
    match the new shape.

    - Dashboard (apps/dashboard/src/components/ChannelSelect.tsx,
    hooks/use-guild-channels.ts):

    - DiscordChannel interface gains writable + speakable.
    - New useRefreshGuildChannels(guildId) hook invalidates the cache so
    admins who just granted permissions can pick the channel without a
    full page reload.
    - ChannelSelect renders inaccessible channels disabled with a
    warning icon, a (Kein Zugriff) badge inline, and an animate-ui
    Tooltip ("Bot lacks permission in this channel — grant View
    Channel, Send Messages, and Embed Links…") on hover. Radix's
    default pointer-events: none on disabled items is overridden with
    !pointer-events-auto so hover events reach the Tooltip trigger;
    Radix still refuses selection via aria-disabled internally so the
    gate can't be bypassed through CSS.
    - showRefresh prop (default true) mounts a refresh icon-button
    next to the dropdown that spins while fetching.

    - i18n: three new common keys in both en.json + de.json
    channelNoAccess (full tooltip text), channelNoAccessShort
    ("No access" / "Kein Zugriff" inline badge), and
    refreshChannelPermissions (refresh button label).

    - Docs: docs/redis-protocol.md bolthub:guilds:get-channels
    section updated with the new response shape. docs/brand.md
    ChannelSelect section expanded with the permission-handling rules
    and the API-side guard reference.

    Verified: typecheck + lint green across all 7 packages. Bot + API
    restarted under tsx watch. Channel dropdown now disables
    non-writable channels with tooltip.

    Fix configure-grantees editing module grants + self-role preservation.

    module_role_grants routes were guarded by requireGuildAccess
    (blanket managers only). Configure-grantees therefore couldn't even
    GET the grant list for their module — the Use Roles dropdown on the
    Access Panel rendered but stayed empty, and the Add-button 403'd.

    Routes now use requireAnyGuildAccess; service-level
    assertMayMutate already scoped mutations correctly (configure-only
    for blanket, use-only for configure-grantees).

    Added assertNotSelfRole: non-blanket callers cannot add or remove
    grants for one of their own Discord roles. Without this a configure-
    grantee could revoke the very role that grants them access and lock
    out the whole role. Owners + global-managers bypass.

    /guilds/:guildId/access now returns the caller's roleIds so the
    dashboard hides the remove-button + disables the add-button for
    self-role grants. Self-role grants show a "Your role" badge so it's
    obvious why they're read-only.

    Fix: dashboard Play button dead after Stop / Leave.

    The persisted-queue work landed but the dashboard had no way to
    actually trigger a resume — the center Play button was wired to
    onResume (unpause) and disabled whenever state.status === "idle",
    which is exactly the state Stop + Leave produce.

    New WS command resume-queue → bot channel bolthub:music:resume-queue:

    - Ensures player (auto-join caller's voice) + restores Redis snapshot
    (if not already loaded) + kicks off playback
    - Throws ValidationError when nothing is persisted (UI keeps the
    button disabled in that case)

    Center Play button now branches on status:

    • playing → Pause
    • paused → Resume (unpause existing player)
    • idle + queue has tracks → Resume Queue (new path)
    • idle + queue empty → disabled

    Page-width wrapper + HTTP cache hardening.

    Dashboard: new <Page width="narrow|default|wide"> component applied
    to every /guilds/:guildId/** route. Lists + overview get
    max-w-6xl, settings forms get max-w-3xl, music player + embed
    editor get max-w-none. Player (/player/**) and Admin routes are
    intentionally untouched. Picked the component-wrapper approach over
    Next route-groups because /embeds list and /embeds/[id] editor
    need different widths under the same segment — route-groups don't
    support segment-sharing across groups, a wrapper keeps the
    decision co-located with the page.

    API: every response now gets Cache-Control: private, no-store
    unless the handler already set one. Fixes a list-stale-after-mutate
    bug where the client invalidated ["embeds", guildId] correctly,
    but the refetch was served from browser heuristic cache instead of
    hitting the API. React Query already owns client-side freshness —
    HTTP cache on top was working against it.

    Guild picker: hide bot-present guilds where user has no BoltHub access.

    Previously the /guilds picker was filtered only by Discord
    MANAGE_GUILD, so a user with MANAGE_GUILD but no BoltHub grant saw
    the server with a "Manage" CTA that then landed on the No Access page.

    filterAccessibleGuildIds now gates bot-present guilds on owner
    status, members.isManager, manager_roles role match, or ANY
    module_role_grants row (configure OR use) that matches a role the
    user holds. Bot-absent guilds continue to use the MANAGE_GUILD
    filter so invite-flow discovery still works.

  14. Kırıcı@bolthub/bot·v0.2.0

    feat(i18n): add Turkish, Dutch, Danish, Swedish, French, Finnish, Greek locales

    • Dashboard and landing now ship tr, nl, da, sv, fr, fi, el message catalogs alongside en and de; both locales arrays extended and language switchers show the new flags and endonym labels
    • Guild Settings language picker, landing layout message imports, and the shared CookieConsent LocaleValue union all accept the new locales
    • Bot t() now resolves the seven new languages for event/RSVP/social-alert/music/access-control keys; English fallback unchanged

    Manager Roles + granular per-module role access.

    The landing page has been promising Manager Roles + Granular Dashboard Access Control for a while; the managerRoles table existed but checkGuildAccess() never consulted it. This changeset wires that promise end-to-end and extends it with per-module role grants that cover both dashboard configuration and Discord-side usage.

    Permission hierarchy (final):

    1. Guild Owner — unconstrained; only owner can edit the Manager Roles list.
    2. Global Managermembers.isManager = true or any Discord role in managerRoles. Full dashboard + module access.
    3. Module Configure Grant — role in module_role_grants with kind = 'configure'. May edit that module's dashboard settings + manage its use grants.
    4. Module Use Grant — role in module_role_grants with kind = 'use'. May use the module in Discord (slash commands, buttons, WS controls, public player controls). If a module has zero use grants, it defaults to open for every guild member.

    New schema: module_role_grants (guildId, module, roleId, kind) with unique + composite indexes.

    New API routes:

    • GET /guilds/:guildId/access — self-access resolver (returns empty on no-access rather than 403 so dashboard can render a useful state).
    • GET|POST|DELETE /guilds/:guildId/manager-roles (POST/DELETE owner-only).
    • GET|POST|DELETE /guilds/:guildId/modules/:module/grants.

    Bot: new Redis channel bolthub:guilds:get-member-roles (API-side cached 60s). interactionCreate runs checkModuleUse before executing music buttons and RSVP buttons. New guildMemberUpdate listener invalidates the member-roles cache key on role diff so a demoted user loses access immediately (60s TTL is only the safety-net).

    API: new requireModuleConfigure(module) middleware replaces requireGuildAccess on feature routes (embeds, events, social-alerts, music config). Music WS handshake now gates on access.useModules.has('music').

    Dashboard: Settings page gains a Manager Roles section (owner-only mutation). Each live-module page gains a Module Access panel (configure / use grants). Sidebar filters nav items for module-configure grantees who aren't blanket managers.

    Docs: docs/schema.md, ARCHITECTURE.md, BUILD_ORDER.md, redis-protocol.md updated.

    i18n: new managerRoles + access namespaces in apps/dashboard/messages/{en,de}.json; new access.* keys in apps/bot/src/lib/i18n.ts (en + de).

    Known limitations (follow-ups): orphaned grants (role deleted in Discord) are not cleaned up; configure-grantees without Discord MANAGE_GUILD don't yet appear in the /guilds switcher.

    Persist music queue across bot disconnects.

    Live queue (current track + upcoming + volume + loop + shuffle) is
    mirrored to Redis under bolthub:music:queue:{guildId} with a 7-day
    TTL. The queue now survives:

    - Idle timeout (5min alone in voice) — bot leaves but queue stays
    - User presses Stop — bot leaves, queue stays, "Clear Queue" button
    wipes it explicitly
    - User presses Leave — same as Stop
    - Voice-channel switch (Join other VC) — queue carries over
    - Bot process restart / crash
    - Lavalink node crash

    Cleared only on:

    - Natural queue end (playerEmpty with nothing left)
    - User clicks Clear Queue button (already present in MusicQueue UI —
    backend now also wipes Redis)
    - User triggers queue:replace-from-playlist (old queue replaced)

    Implementation:

    - persistQueueState(player) serializes the live state; wired into
    Kazagumo playerStart / playerEnd events and every mutating
    handler (queue-add / remove / reorder, volume, loop, shuffle).
    - restorePersistedQueue(player, requestedBy) runs immediately after
    every createPlayer() in handlePlay / handleQueueAdd / handleJoin.
    Tracks are re-searched through Lavalink by URI (slower than
    decoding cached blobs, but robust across Lavalink upgrades and
    reuses the existing search-with-fallback pattern).
    - Current-track position is NOT preserved; resume starts the track
    from 0s. Simpler, 99% UX-OK.
    - handleState falls back to the Redis snapshot when no live player
    exists, so the dashboard keeps showing the persisted queue after
    Stop / Leave / idle-timeout / restart — users see what will resume
    when they press Play.

    Redis itself uses RDB snapshots + volume mount, so Redis restarts
    don't drop state either.

  15. Güncelleme@bolthub/bot·v0.2.0

    Channel-permission awareness across the bot/API/dashboard stack.

    The dashboard's ChannelSelect used to show every channel the bot could
    _see_, not every channel the bot could _post in_. Admins could pick a
    channel where the bot silently failed to send — especially painful on
    the onboarding Update-Channel picker where that misconfig wasn't caught
    until the first broadcast went out.

    Fix is full-stack so it can't be bypassed from any surface:

    - Bot (apps/bot/src/modules/guilds/guilds.handler.ts):
    bolthub:guilds:get-channels now resolves the bot member's real
    permission overwrites per channel and returns writable + speakable
    booleans alongside the existing shape. writable requires
    ViewChannel + SendMessages + EmbedLinks; speakable requires
    ViewChannel + Connect + Speak. Voice-type channels report
    writable: false on purpose — BoltHub never posts embeds into a
    voice-text buffer.

    - API (apps/api/src/lib/channel-permissions.ts): new
    assertChannelCapability(guildId, channelId, need) helper. Throws
    ConflictError (409) with a user-facing message listing the missing
    Discord permissions, or NotFoundError if the channel id is unknown
    to the bot. Wired into every save-path that persists a channelId:

    - PUT /guilds/:guildId/broadcast-subscription
    - POST /guilds/:guildId/embeds, PATCH /:embedId, POST /:embedId/send
    - POST /guilds/:guildId/events, PATCH /:eventId,
    POST /schedules, PATCH /schedules/:scheduleId
    - POST /guilds/:guildId/social-alerts, PATCH /:alertId

    guilds.controller GET /channels inline response type updated to
    match the new shape.

    - Dashboard (apps/dashboard/src/components/ChannelSelect.tsx,
    hooks/use-guild-channels.ts):

    - DiscordChannel interface gains writable + speakable.
    - New useRefreshGuildChannels(guildId) hook invalidates the cache so
    admins who just granted permissions can pick the channel without a
    full page reload.
    - ChannelSelect renders inaccessible channels disabled with a
    warning icon, a (Kein Zugriff) badge inline, and an animate-ui
    Tooltip ("Bot lacks permission in this channel — grant View
    Channel, Send Messages, and Embed Links…") on hover. Radix's
    default pointer-events: none on disabled items is overridden with
    !pointer-events-auto so hover events reach the Tooltip trigger;
    Radix still refuses selection via aria-disabled internally so the
    gate can't be bypassed through CSS.
    - showRefresh prop (default true) mounts a refresh icon-button
    next to the dropdown that spins while fetching.

    - i18n: three new common keys in both en.json + de.json
    channelNoAccess (full tooltip text), channelNoAccessShort
    ("No access" / "Kein Zugriff" inline badge), and
    refreshChannelPermissions (refresh button label).

    - Docs: docs/redis-protocol.md bolthub:guilds:get-channels
    section updated with the new response shape. docs/brand.md
    ChannelSelect section expanded with the permission-handling rules
    and the API-side guard reference.

    Verified: typecheck + lint green across all 7 packages. Bot + API
    restarted under tsx watch. Channel dropdown now disables
    non-writable channels with tooltip.

    Fix: dashboard Play button dead after Stop / Leave.

    The persisted-queue work landed but the dashboard had no way to
    actually trigger a resume — the center Play button was wired to
    onResume (unpause) and disabled whenever state.status === "idle",
    which is exactly the state Stop + Leave produce.

    New WS command resume-queue → bot channel bolthub:music:resume-queue:

    - Ensures player (auto-join caller's voice) + restores Redis snapshot
    (if not already loaded) + kicks off playback
    - Throws ValidationError when nothing is persisted (UI keeps the
    button disabled in that case)

    Center Play button now branches on status:

    • playing → Pause
    • paused → Resume (unpause existing player)
    • idle + queue has tracks → Resume Queue (new path)
    • idle + queue empty → disabled
  16. Kırıcı@bolthub/dashboard·v0.2.0

    feat(i18n): add Turkish, Dutch, Danish, Swedish, French, Finnish, Greek locales

    • Dashboard and landing now ship tr, nl, da, sv, fr, fi, el message catalogs alongside en and de; both locales arrays extended and language switchers show the new flags and endonym labels
    • Guild Settings language picker, landing layout message imports, and the shared CookieConsent LocaleValue union all accept the new locales
    • Bot t() now resolves the seven new languages for event/RSVP/social-alert/music/access-control keys; English fallback unchanged

    Manager Roles + granular per-module role access.

    The landing page has been promising Manager Roles + Granular Dashboard Access Control for a while; the managerRoles table existed but checkGuildAccess() never consulted it. This changeset wires that promise end-to-end and extends it with per-module role grants that cover both dashboard configuration and Discord-side usage.

    Permission hierarchy (final):

    1. Guild Owner — unconstrained; only owner can edit the Manager Roles list.
    2. Global Managermembers.isManager = true or any Discord role in managerRoles. Full dashboard + module access.
    3. Module Configure Grant — role in module_role_grants with kind = 'configure'. May edit that module's dashboard settings + manage its use grants.
    4. Module Use Grant — role in module_role_grants with kind = 'use'. May use the module in Discord (slash commands, buttons, WS controls, public player controls). If a module has zero use grants, it defaults to open for every guild member.

    New schema: module_role_grants (guildId, module, roleId, kind) with unique + composite indexes.

    New API routes:

    • GET /guilds/:guildId/access — self-access resolver (returns empty on no-access rather than 403 so dashboard can render a useful state).
    • GET|POST|DELETE /guilds/:guildId/manager-roles (POST/DELETE owner-only).
    • GET|POST|DELETE /guilds/:guildId/modules/:module/grants.

    Bot: new Redis channel bolthub:guilds:get-member-roles (API-side cached 60s). interactionCreate runs checkModuleUse before executing music buttons and RSVP buttons. New guildMemberUpdate listener invalidates the member-roles cache key on role diff so a demoted user loses access immediately (60s TTL is only the safety-net).

    API: new requireModuleConfigure(module) middleware replaces requireGuildAccess on feature routes (embeds, events, social-alerts, music config). Music WS handshake now gates on access.useModules.has('music').

    Dashboard: Settings page gains a Manager Roles section (owner-only mutation). Each live-module page gains a Module Access panel (configure / use grants). Sidebar filters nav items for module-configure grantees who aren't blanket managers.

    Docs: docs/schema.md, ARCHITECTURE.md, BUILD_ORDER.md, redis-protocol.md updated.

    i18n: new managerRoles + access namespaces in apps/dashboard/messages/{en,de}.json; new access.* keys in apps/bot/src/lib/i18n.ts (en + de).

    Known limitations (follow-ups): orphaned grants (role deleted in Discord) are not cleaned up; configure-grantees without Discord MANAGE_GUILD don't yet appear in the /guilds switcher.

  17. Güncelleme@bolthub/dashboard·v0.2.0

    Move Module Access Panel into module settings route.

    Access configuration now lives at
    /guilds/:guildId/modules/:module/settings alongside the module's
    other settings, instead of being a card on the module's main list
    page. Keeps access + module config in one place and declutters the
    embeds + music landing pages.

    Channel-permission awareness across the bot/API/dashboard stack.

    The dashboard's ChannelSelect used to show every channel the bot could
    _see_, not every channel the bot could _post in_. Admins could pick a
    channel where the bot silently failed to send — especially painful on
    the onboarding Update-Channel picker where that misconfig wasn't caught
    until the first broadcast went out.

    Fix is full-stack so it can't be bypassed from any surface:

    - Bot (apps/bot/src/modules/guilds/guilds.handler.ts):
    bolthub:guilds:get-channels now resolves the bot member's real
    permission overwrites per channel and returns writable + speakable
    booleans alongside the existing shape. writable requires
    ViewChannel + SendMessages + EmbedLinks; speakable requires
    ViewChannel + Connect + Speak. Voice-type channels report
    writable: false on purpose — BoltHub never posts embeds into a
    voice-text buffer.

    - API (apps/api/src/lib/channel-permissions.ts): new
    assertChannelCapability(guildId, channelId, need) helper. Throws
    ConflictError (409) with a user-facing message listing the missing
    Discord permissions, or NotFoundError if the channel id is unknown
    to the bot. Wired into every save-path that persists a channelId:

    - PUT /guilds/:guildId/broadcast-subscription
    - POST /guilds/:guildId/embeds, PATCH /:embedId, POST /:embedId/send
    - POST /guilds/:guildId/events, PATCH /:eventId,
    POST /schedules, PATCH /schedules/:scheduleId
    - POST /guilds/:guildId/social-alerts, PATCH /:alertId

    guilds.controller GET /channels inline response type updated to
    match the new shape.

    - Dashboard (apps/dashboard/src/components/ChannelSelect.tsx,
    hooks/use-guild-channels.ts):

    - DiscordChannel interface gains writable + speakable.
    - New useRefreshGuildChannels(guildId) hook invalidates the cache so
    admins who just granted permissions can pick the channel without a
    full page reload.
    - ChannelSelect renders inaccessible channels disabled with a
    warning icon, a (Kein Zugriff) badge inline, and an animate-ui
    Tooltip ("Bot lacks permission in this channel — grant View
    Channel, Send Messages, and Embed Links…") on hover. Radix's
    default pointer-events: none on disabled items is overridden with
    !pointer-events-auto so hover events reach the Tooltip trigger;
    Radix still refuses selection via aria-disabled internally so the
    gate can't be bypassed through CSS.
    - showRefresh prop (default true) mounts a refresh icon-button
    next to the dropdown that spins while fetching.

    - i18n: three new common keys in both en.json + de.json
    channelNoAccess (full tooltip text), channelNoAccessShort
    ("No access" / "Kein Zugriff" inline badge), and
    refreshChannelPermissions (refresh button label).

    - Docs: docs/redis-protocol.md bolthub:guilds:get-channels
    section updated with the new response shape. docs/brand.md
    ChannelSelect section expanded with the permission-handling rules
    and the API-side guard reference.

    Verified: typecheck + lint green across all 7 packages. Bot + API
    restarted under tsx watch. Channel dropdown now disables
    non-writable channels with tooltip.

    Fix configure-grantees editing module grants + self-role preservation.

    module_role_grants routes were guarded by requireGuildAccess
    (blanket managers only). Configure-grantees therefore couldn't even
    GET the grant list for their module — the Use Roles dropdown on the
    Access Panel rendered but stayed empty, and the Add-button 403'd.

    Routes now use requireAnyGuildAccess; service-level
    assertMayMutate already scoped mutations correctly (configure-only
    for blanket, use-only for configure-grantees).

    Added assertNotSelfRole: non-blanket callers cannot add or remove
    grants for one of their own Discord roles. Without this a configure-
    grantee could revoke the very role that grants them access and lock
    out the whole role. Owners + global-managers bypass.

    /guilds/:guildId/access now returns the caller's roleIds so the
    dashboard hides the remove-button + disables the add-button for
    self-role grants. Self-role grants show a "Your role" badge so it's
    obvious why they're read-only.

    Fix: dashboard Play button dead after Stop / Leave.

    The persisted-queue work landed but the dashboard had no way to
    actually trigger a resume — the center Play button was wired to
    onResume (unpause) and disabled whenever state.status === "idle",
    which is exactly the state Stop + Leave produce.

    New WS command resume-queue → bot channel bolthub:music:resume-queue:

    - Ensures player (auto-join caller's voice) + restores Redis snapshot
    (if not already loaded) + kicks off playback
    - Throws ValidationError when nothing is persisted (UI keeps the
    button disabled in that case)

    Center Play button now branches on status:

    • playing → Pause
    • paused → Resume (unpause existing player)
    • idle + queue has tracks → Resume Queue (new path)
    • idle + queue empty → disabled

    Page-width wrapper + HTTP cache hardening.

    Dashboard: new <Page width="narrow|default|wide"> component applied
    to every /guilds/:guildId/** route. Lists + overview get
    max-w-6xl, settings forms get max-w-3xl, music player + embed
    editor get max-w-none. Player (/player/**) and Admin routes are
    intentionally untouched. Picked the component-wrapper approach over
    Next route-groups because /embeds list and /embeds/[id] editor
    need different widths under the same segment — route-groups don't
    support segment-sharing across groups, a wrapper keeps the
    decision co-located with the page.

    API: every response now gets Cache-Control: private, no-store
    unless the handler already set one. Fixes a list-stale-after-mutate
    bug where the client invalidated ["embeds", guildId] correctly,
    but the refetch was served from browser heuristic cache instead of
    hitting the API. React Query already owns client-side freshness —
    HTTP cache on top was working against it.

    fix(sidebar): drive active/hover indicator with motion layoutId

    Replaces the ref-based getBoundingClientRect + useEffect measurement
    in Sidebar with motion/react's layoutId FLIP. The indicator pill and
    orange active bracket now live inside the active <li> itself, so they
    reposition automatically whenever nav items resize — including when
    labels wrap to two lines on locale switch, when fonts finish loading, or
    when the Modules-enabled dot appears/disappears. Previously the rect was
    only recomputed on activeKey / hoveredKey / pathname changes, so a
    locale swap left the indicator stranded at the old coordinates.

  18. Güncelleme@bolthub/dashboard·v0.1.11

    Gate events + social_alerts as "coming soon" until finalized.

    - @bolthub/types: new LIVE_MODULES set + isModuleLive() helper —
    single source of truth for which modules can be enabled. Currently
    embeds + music only.
    - @bolthub/api: modules.service.updateModule throws ForbiddenError
    when enabling a non-live module, even if the plan would allow it.
    - @bolthub/dashboard: Sidebar + modules page consume isModuleLive
    instead of a local hardcoded set. ModuleGate shows a dedicated
    "Coming Soon" empty state (no Activate button) for non-live modules.
    New i18n keys modules.comingSoonTitle + comingSoonDescription
    (en + de).
    - @bolthub/landing: Features section marks events + socialAlerts
    as comingSoon; live modules (embeds, music) lead the grid.

  19. Güncelleme@bolthub/api·v0.1.9

    Gate events + social_alerts as "coming soon" until finalized.

    - @bolthub/types: new LIVE_MODULES set + isModuleLive() helper —
    single source of truth for which modules can be enabled. Currently
    embeds + music only.
    - @bolthub/api: modules.service.updateModule throws ForbiddenError
    when enabling a non-live module, even if the plan would allow it.
    - @bolthub/dashboard: Sidebar + modules page consume isModuleLive
    instead of a local hardcoded set. ModuleGate shows a dedicated
    "Coming Soon" empty state (no Activate button) for non-live modules.
    New i18n keys modules.comingSoonTitle + comingSoonDescription
    (en + de).
    - @bolthub/landing: Features section marks events + socialAlerts
    as comingSoon; live modules (embeds, music) lead the grid.