1. 21 Apr, 2026 6 commits
    • erio's avatar
      feat(sidebar+groups): available-channels above channel-status; show rate for subscription groups · 9dae6c7a
      erio authored
      - Sidebar user-side order: /available-channels now sits directly above
        /monitor (渠道状态) for regular users, mirroring the admin section where
        it sits above /admin/channels.
      - GroupBadge gains an alwaysShowRate prop. Subscription groups default to
        a "订阅"/days-remaining label; the new flag swaps that for the rate
        multiplier while keeping the subscription theme color, so the Available
        Channels page can surface rates on every group type.
      9dae6c7a
    • erio's avatar
      feat(channels): themed model popover + group-badge with rate, subscription & exclusivity · ff4ef1b5
      erio authored
      Pricing popover:
      - Teleport to body + fixed-position re-measuring on hover/scroll/resize so it
        escapes the card's overflow-hidden clip that was chopping off the lower
        half of the panel.
      - Header + border adopt the platform theme via platformBorderClass /
        platformBadgeLightClass so each model card reads at a glance.
      
      Accessible groups:
      - Backend AvailableGroupRef / user DTO now expose subscription_type,
        rate_multiplier and is_exclusive. User-specific rates continue to come
        from /groups/rates (same pattern as the API keys page).
      - Table renders groups through the shared GroupBadge, which already deepens
        subscription-type colors and shows default vs custom rate as
        <s>default</s> <b>custom</b>.
      - Exclusive groups are labelled with a purple shield row, public groups
        with a grey globe row so admins and users can tell at a glance which
        groups they got via explicit grant vs. public access.
      
      i18n keys for exclusive / public / their tooltips added to zh + en.
      ff4ef1b5
    • erio's avatar
      feat(channels): aggregate by channel with platform sections + rowspan table · 3cdd5754
      erio authored
      Switch the user-facing 'Available Channels' view from "one row per
      platform" to "one channel row-group with N platform sections".
      
      Backend: userAvailableChannel now holds Platforms []section instead
      of being exploded. buildPlatformSections replaces
      explodeChannelByPlatform with the same per-platform grouping logic.
      
      Frontend: drop the DataTable wrapper for this view and write a
      four-column grid table (渠道名 / 平台 / 分组 / 支持模型) where the
      channel name only renders on the first platform row of each channel —
      visual rowspan without hacking DataTable.
      
      - api/channels.ts: UserChannelPlatformSection + platforms[]
      - AvailableChannelsTable: rewritten as native grid (header + per-
        channel section with hover row highlight)
      - AvailableChannelsView: search now filters platforms sub-array;
        channel-name / description hits still keep the whole channel
      - i18n: add availableChannels.columns.platform (zh/en)
      3cdd5754
    • erio's avatar
      feat(channels): explode available channels by platform + apply platform theme · 800802b8
      erio authored
      Backend: one source channel → N output rows, one per platform that
      has user-visible groups. Each row carries a single platform, so the
      frontend can color/icon an entire row without mixing sources.
      
      - userAvailableChannel: add Platform field
      - new explodeChannelByPlatform helper; drop now-redundant
        collectGroupPlatforms
      
      Frontend: use the row platform to drive theming and stop repeating
      "ANTHROPIC" / "OPENAI" labels on every model chip.
      
      - api/channels.ts: UserAvailableChannel.platform
      - AvailableChannelsTable: name cell — PlatformBadge next to channel
        name (replaces the two-line name/description block; description
        moves to the badge's title tooltip); groups cell — each chip uses
        platformBadgeLightClass + PlatformIcon; model list passes
        show-platform=false + platform-hint to child chips
      - SupportedModelChip: chip bg/border driven by platformBadgeClass,
        leading PlatformIcon; platform-hint fallback when model.platform
        missing
      800802b8
    • erio's avatar
      refactor(channels): normalize at cache fill and eliminate frontend as-cast · 4a3652ec
      erio authored
      - channel.go: convert normalizeBillingModelSource into a (*Channel) method for entity cohesion
      - channel_service.go: normalize in populateChannelCache so every cache-backed reader (gateway, billing, future endpoints) sees the default; drop the duplicate fallback inside resolveMapping
      - table: tighten Row with status?: ChannelStatus / billing_model_source?: BillingModelSource, remove the [key: string]: unknown index signature
      - admin view: drop the `as ChannelStatus` / `as BillingModelSource` assertions and add statusStyleOf / billingSourceLabelOf helpers with runtime fallback so unseen values render as "-" instead of crashing
      4a3652ec
    • erio's avatar
      refactor(channels): centralize BillingModelSource normalization and exhaustive enum maps · 375aefa2
      erio authored
      - service: add normalizeBillingModelSource helper, apply in Create/GetByID/Update/List/ListAvailable outputs
      - handler: drop channelToResponse fallback now that service owns the default; add passthrough test
      - frontend: replace ternary status/billing-source lookups with Record<Enum, ...> maps so new union members fail the build
      - chip/table: drop local type aliases, reuse UserSupportedModel/UserPricingInterval directly
      - tests: assert short-circuit on ListAll error, wrap-prefix preservation, and Name-based default lookup
      375aefa2
  2. 20 Apr, 2026 3 commits
    • erio's avatar
      refactor(channels): tighten types and error paths per second review · 88decb6e
      erio authored
      - service: drop groupRepo nil guard (DI must inject), switch SupportedModels to SliceStable to match doc
      - frontend: reuse user-side DTO types in SupportedModelChip/AvailableChannelsTable instead of duplicating shapes; narrow admin statusLabel param to ChannelStatus
      - tests: replace nil-groupRepo case with ListAll/ListActive error propagation and BillingModelSource default-backfill coverage
      88decb6e
    • erio's avatar
      refactor(channels): consolidate pricing index, tighten types, polish DTOs · 365ef1fd
      erio authored
      Follow-up to the available-channels review pass. No behavior change for
      end users; tightens internals based on three independent code reviews.
      
      Backend
      - service/channel.go: collapse buildPricingLookup + pricedNamesFor
        into a single platformPricingIndex (byLower + originalCase + ordered
        names), built once per SupportedModels call. Fixes a casing-
        consistency bug where the same logical model appeared with mapping
        case in the exact branch but pricing case in the wildcard branch —
        pricing's original case now wins everywhere.
      - service/channel.go: doc that a mapping key of just "*" expands to
        every priced model on the platform (intentional "passthrough all").
      - service/channel_available.go: normalize empty BillingModelSource to
        channel_mapped at construction time, removing the same fallback
        duplicated in the admin DTO mapper and the admin Vue template.
      - handler/admin/available_channel_handler.go: unexport
        availableChannelToAdminResponse (same-package usage only); mapper
        is now a pure passthrough.
      - handler/available_channel_handler.go: drop the middleware2 alias
        (no name collision in this file).
      
      Frontend
      - utils/pricing.ts: extract formatScaled, used by SupportedModelChip
        and PricingRow.
      - api/admin/channels.ts: re-export BillingMode from constants/channel;
        tighten Channel.status / billing_model_source to ChannelStatus /
        BillingModelSource (and same for AvailableChannel).
      - components/channels/AvailableChannelsTable.vue: drop dead
        withDefaults wrapper (loading is required, both call sites pass it).
      - views/admin/AvailableChannelsView.vue: drop the redundant
        || BILLING_MODEL_SOURCE_CHANNEL_MAPPED fallback (now applied in
        service layer); remove unused import.
      - i18n zh + en: delete unused tierLabel and tokenRange keys from
        both availableChannels.pricing and admin.availableChannels.pricing.
      
      Tests
      - New: SupportedModels_ExactKeyUsesPricedCaseWhenAvailable locks the
        pricing-case-wins rule.
      - New: SupportedModels_AsteriskOnlyMappingExpandsAllPriced documents
        the "*" expansion rule.
      - Admin handler: existing tests adjusted to pass an explicit
        BillingModelSource (default-fill is now exercised by service tests).
      365ef1fd
    • erio's avatar
      feat(channels): add "Available Channels" aggregate view · 654cfb64
      erio authored
      Add a read-only aggregate view per channel: its linked groups and a
      deterministic wildcard-free supported-model list with pricing details.
      
      Backend
      - service.Channel.SupportedModels(): combine ModelMapping keys with
        same-platform ModelPricing.Models; trailing "*" keys expand via
        pricing prefix match; platforms without a mapping produce no
        entries (intentional "no mapping = not shown" rule).
      - Extract splitWildcardSuffix() shared with toModelEntry.
      - Build a per-call pricing lookup map (platform+lowerName -> *pricing)
        to avoid O(N*M) scans in SupportedModels.
      - ChannelService.ListAvailable() aggregates channels + active groups;
        filters out group IDs no longer active.
      - Admin route GET /api/v1/admin/channels/available returns the full
        DTO (id, status, billing_model_source, restrict_models, groups,
        supported_models).
      - User route GET /api/v1/channels/available applies three filters:
        Status==active, visible-group intersection, and platform filter
        on supported_models (prevents cross-platform leak when a channel
        links to both a user-accessible group and an inaccessible one on
        another platform). Response is a plain array (matches the
        /groups/available sibling shape). Field whitelist omits
        billing_model_source, restrict_models, ids, status, sort_order.
      
      Frontend
      - New /admin/available-channels and /available-channels views backed
        by a shared AvailableChannelsTable component (admin adds status +
        billing-source columns via slots).
      - PricingRow extracted to its own SFC; SupportedModelChip references
        shared billing-mode constants in constants/channel.ts.
      - Sidebar: new entry above "渠道管理" for admin; matching entry in
        user nav.
      - i18n: zh + en coverage for both namespaces.
      
      Tests
      - SupportedModels: wildcard-only pricing skipped, prefix-matches-
        nothing, cross-platform bleed, case-insensitive dedup, empty
        platform mapping.
      - ListAvailable: nil groupRepo, inactive-group-ID dropped, stable
        case-insensitive name sort.
      - User handler: 401 on unauthenticated, visible-group intersection,
        platform filter on supported_models, JSON whitelist.
      - Admin handler: full DTO including default BillingModelSource
        fallback.
      
      Refs: issue #1729
      654cfb64