@@ -31,14 +31,7 @@
. brand strong { font-size : 14 px ; letter-spacing : .02 em ; }
. brand span { color : var ( - - muted ) ; font-size : 12 px ; }
. auth-box { display : flex ; align-items : center ; gap : 8 px ; }
. auth-box label { color : var ( - - muted ) ; font-size : 12 px ; }
. mode-switch { display : flex ; padding : 2 px ; border : 1 px solid var ( - - border ) ; border-radius : 8 px ; background : var ( - - bg - secondary ) ; }
. mode-button { border : 0 ; padding : 5 px 9 px ; background : transparent ; color : var ( - - muted ) ; }
. mode-button : hover { border-color : transparent ; background : var ( - - bg - hover ) ; }
. mode-button . active { background : var ( - - bg - tertiary ) ; color : var ( - - text ) ; }
. demo-mode . mode-button [ data-mode = "demo" ] { background : color-mix ( in srgb , var ( - - amber - color ) 38 % , transparent ) ; color : #fcd34d ; }
. demo-banner { display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; padding : 8 px 22 px ; border-bottom : 1 px solid color-mix ( in srgb , var ( - - amber - color ) 45 % , transparent ) ; background : color-mix ( in srgb , var ( - - amber - color ) 14 % , var ( - - bg - secondary ) ) ; color : #fcd34d ; font-size : 11 px ; }
. demo-banner button { border-color : color-mix ( in srgb , var ( - - amber - color ) 45 % , transparent ) ; background : color-mix ( in srgb , var ( - - amber - color ) 24 % , transparent ) ; color : #fcd34d ; }
body . read-only . admin-only { display : none !important ; }
input , select { min-width : 0 ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; padding : 8 px 10 px ; outline : none ; background : var ( - - bg - tertiary ) ; color : inherit ; transition : border-color .15 s , box-shadow .15 s ; }
input : focus , select : focus { border-color : var ( - - primary - active ) ; box-shadow : 0 0 0 3 px color-mix ( in srgb , var ( - - primary - color ) 20 % , transparent ) ; }
input : disabled , select : disabled { cursor : not-allowed ; opacity : .55 ; }
@@ -102,11 +95,12 @@
. price-sidebar-header { display : flex ; align-items : center ; justify-content : space-between ; gap : 10 px ; margin-bottom : 10 px ; }
. price-sidebar-header h2 { margin : 0 ; font-size : 12 px ; }
. price-list { display : grid ; gap : 6 px ; }
. price-row { display : grid ; grid-template-columns : minmax ( 0 , 1 fr ) auto ; align-items : center ; gap : 8 px ; padding : 9 px 9 px 9 px 11 px ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; background : var ( - - bg - primary ) ; }
. price-row { display : block ; padding : 9 px 11 px ; border : 1 px solid var ( - - border ) ; border-radius : 7 px ; background : var ( - - bg - primary ) ; cursor : pointer ; outline : none ; }
. price-row : hover { border-color : var ( - - border - hover ) ; background : var ( - - bg - hover ) ; }
. price-row : focus-visible { border-color : var ( - - primary - active ) ; box-shadow : 0 0 0 3 px color-mix ( in srgb , var ( - - primary - color ) 20 % , transparent ) ; }
. price-row . selected { border-color : var ( - - border - primary ) ; background : var ( - - bg - tertiary ) ; }
. price-row . price-name { overflow : hidden ; font-size : 11 px ; font-weight : 600 ; text-overflow : ellipsis ; }
. price-row small { display : block ; margin-top : 3 px ; overflow : hidden ; color : var ( - - muted ) ; font-size : 9 px ; text-overflow : ellipsis ; white-space : nowrap ; }
. price-row . key-actions { flex-wrap : nowrap ; }
. price-editor { padding : 16 px ; }
. price-editor-header { display : flex ; align-items : center ; justify-content : space-between ; gap : 12 px ; margin-bottom : 12 px ; }
. price-editor-header h2 { margin : 0 ; }
@@ -197,22 +191,25 @@
. page-buttons button : disabled { cursor : default ; opacity : .4 ; }
. page-ellipsis { width : 22 px ; color : var ( - - muted ) ; text-align : center ; font-size : 10 px ; }
. usage-filters { display : grid ; grid-template-columns : repeat ( 6 , minmax ( 120 px , 1 fr ) ) ; gap : 9 px ; margin-bottom : 10 px ; padding : 11 px ; }
. section-heading . tools { position : relative ; }
. usage-filter-panel { position : static ; margin : 0 ; }
. usage-filter-panel > summary { display : inline-flex ; align-items : center ; }
. usage-filter-panel . usage-filters { position : absolute ; z-index : 15 ; top : calc ( 100 % + 8 px ) ; right : 0 ; width : min ( 960 px , calc ( 100 vw - 44 px ) ) ; margin-bottom : 0 ; box-shadow : 0 16 px 40 px #0009 ; }
. usage-filter { display : grid ; gap : 5 px ; min-width : 0 ; color : var ( - - muted ) ; font-size : 9 px ; }
. usage-filter input , . usage-filter select { height : 32 px ; padding : 6 px 8 px ; font-size : 11 px ; }
. usage-filter . request-filter { grid-column : span 2 ; }
. usage-filter-actions { display : flex ; align-items : end ; justify-content : flex-end ; gap : 7 px ; grid-column : span 2 ; }
@ media ( max-width : 1100px ) { . usage-filters { grid-template-columns : repeat ( 3 , minmax ( 140 px , 1 fr ) ) ; } }
@ media ( max-width : 900px ) { . price-layout { grid-template-columns : 220 px minmax ( 0 , 1 fr ) ; } . price-grid , . price-grid . long-grid { grid-template-columns : repeat ( 2 , minmax ( 130 px , 1 fr ) ) ; } . summary-grid { grid-template-columns : repeat ( 3 , minmax ( 130 px , 1 fr ) ) ; overflow-x : auto ; } . chart-grid { grid-template-columns : 1 fr ; } }
@ media ( max-width : 680px ) { . app-shell { padding-top : 14 px ; } . topbar { align-items : flex-start ; min-height : 0 ; padding : 11 px 12 px ; } . brand span , . auth-box label { display : none ; } . auth-box { flex-wrap : wrap ; justify-content : flex-end ; } # key { width : 128 px ; } . demo-banner { padding-right : 12 px ; padding-left : 12 px ; } . workspace-nav { padding-right : 12 px ; padding-left : 12 px ; } main { padding : 14 px 12 px 22 px ; } . section-heading { align-items : stretch ; flex-direction : column ; } . section-heading . tools { justify-content : flex-start ; } . price-layout { grid-template-columns : 1 fr ; } . price-sidebar { border-right : 0 ; border-bottom : 1 px solid var ( - - border - soft ) ; } . price-grid , . price-grid . long-grid , . field-row , . fast-row { grid-template-columns : 1 fr ; } . drawer { width : 100 vw ; } . summary-grid { margin-right : -12 px ; padding-right : 12 px ; } . pagination { align-items : flex-start ; flex-direction : column ; } . usage-filters { grid-template-columns : 1 fr 1 fr ; } . usage-filter . request-filter , . usage-filter-actions { grid-column : span 2 ; } }
@ media ( max-width : 680px ) { . app-shell { padding-top : 14 px ; } . topbar { align-items : flex-start ; min-height : 0 ; padding : 11 px 12 px ; } . brand span { display : none ; } . auth-box { flex-wrap : wrap ; justify-content : flex-end ; } # key { width : 128 px ; } . workspace-nav { padding-right : 12 px ; padding-left : 12 px ; } main { padding : 14 px 12 px 22 px ; } . section-heading { align-items : stretch ; flex-direction : column ; } . section-heading . tools { justify-content : flex-start ; } . price-layout { grid-template-columns : 1 fr ; } . price-sidebar { border-right : 0 ; border-bottom : 1 px solid var ( - - border - soft ) ; } . price-grid , . price-grid . long-grid , . field-row , . fast-row { grid-template-columns : 1 fr ; } . drawer { width : 100 vw ; } . summary-grid { margin-right : -12 px ; padding-right : 12 px ; } . pagination { align-items : flex-start ; flex-direction : column ; } . usage-filters { grid-template-columns : 1 fr 1 fr ; } . usage-filter . request-filter , . usage-filter-actions { grid-column : span 2 ; } }
< / style >
< / head >
< body >
< body class = "read-only" >
< div class = "app-shell" >
< header class = "topbar" >
< div class = "brand" > < div class = "brand-mark" > B< / div > < div > < strong > Billing< / strong > < span > · 管理控制台< / span > < / div > < / div >
< div class = "auth-box" > < div class = "mode-switch" aria-label = "数据模式" > < button class = "mode-button active" type = "button" data-mode = "live" > 实时 < / button > < button class = "mode-button" type = "button" data-mode = "demo" > 测试 < / button > < / div > < label for = "key" > 管理密钥 < / label > < input id = "key" type = "password" autocomplete = "off" placeholder = "输入 管理密钥" > < / div >
< div class = "auth-box" > < input id = "key" type = "password" autocomplete = "off" aria-label = "管理密钥" > < / div >
< / header >
< div id = "demo-banner" class = "demo-banner hidden" > < span > < strong > 测试模式< / strong > · 页面使用浏览器本地样例数据,任何创建、编辑、归档和价格操作都不会访问 CPA。< / span > < button id = "reset-demo" type = "button" > 重置测试数据< / button > < / div >
< nav class = "workspace-nav" aria-label = "工作区" >
< button class = "nav-button active" type = "button" data-view = "keys" > 用户 Key< / button >
< button class = "nav-button" type = "button" data-view = "usage" > 请求明细< / button >
@@ -220,51 +217,54 @@
< / nav >
< main >
< section id = "view-keys" class = "view active" >
< div class = "section-heading actions-only" > < div class = "tools" > < label class = "inline-check" > < input id = "include-archived" type = "checkbox" > 显示已归档< / label > < button id = "reload-keys" type = "button" > 刷新< / button > < button id = "create-key" class = "primary" type = "button" > 创建 Key< / button > < / div > < / div >
< div class = "summary-grid" >
< article class = "summary-card" > < span > 今日请求< / span > < strong id = "metric-requests" > —< / strong > < small > 自然日内实际请求< / small > < / article >
< article class = "summary-card" > < span > 今日 Token< / span > < strong id = "metric-tokens" > —< / strong > < small > 输入与输出合计< / small > < / article >
< article class = "summary-card" > < span > 今日成本< / span > < strong id = "metric-cost" > —< / strong > < small > 按已配置价格计算< / small > < / article >
< / div >
< div class = "chart-grid" > < article class = "chart-card" > < header > < div > < h2 > 今日用户用量< / h2 > < / div > < / header > < div id = "user-usage" class = "user-usage" > < / div > < / article > < article class = "chart-card" > < header > < div > < h2 > 近 7 日 Token< / h2 > < / div > < / header > < div id = "daily-chart" class = "bar-chart" > < / div > < / article > < / div >
< div class = "surface" >
< div class = "surface-toolbar" > < span id = "key-status" class = "status" > < / span > < / div >
< div class = "table-wrap" > < table class = "key-table" > < thead > < tr > < th class = "left" > 用户< / th > < th class = "left" > 完整 Key< / th > < th class = "left" > 状态< / th > < th class = "left" > 余额 / 额度< / th > < th class = "left" > 并发< / th > < th class = "left" > 路由< / th > < th class = "left" > 允许模型< / th > < th class = "left" > 操作 < / th > < / tr > < / thead > < tbody id = "key-rows" > < / tbody > < / table > < / div >
< div id = "key-management" class = "surface admin-only " >
< div class = "surface-toolbar" > < span id = "key-status" class = "status" > < / span > < button id = "create-key" class = "primary" type = "button" > 创建 Key < / button > < / div >
< div class = "table-wrap" > < table class = "key-table" > < thead > < tr > < th class = "left" > 用户< / th > < th class = "left" > Key< / th > < th class = "left" > 状态< / th > < th class = "left" > 余额 / 额度< / th > < th class = "left" > 并发< / th > < th class = "left" > 路由< / th > < th class = "left" > 允许模型< / th > < th class = "left" > 查看 < / th > < / tr > < / thead > < tbody id = "key-rows" > < / tbody > < / table > < / div >
< / div >
< / section >
< section id = "view-usage" class = "view" >
< div class = "section-heading" > < div > < div id = "page-buttons" class = "page-buttons" > < / div > < span id = "status" class = "status" > < / span > < / div > < div class = "tools" > < details > < summary > 选择列 < / summary > < div id = "column-options" class = "column-options" > < / div > < / details > < button id = "refresh" type = "button" > 刷新 < / button > < / div > < / div >
< div class = "surface usage-filters" >
< label class = "usage-filter" > 开始时间< input id = "usage-from" type = "datetime-local" step = "1" > < / label >
< label class = "usage-filter" > 结束时间< input id = "usage-to" type = "datetime-local" step = "1" > < / label >
< label class = "usage-filter" > 用户< select id = "usage-key" > < option value = "" > 全部用户< / option > < / select > < / label >
< label class = "usage-filter" > 模型< input id = "usage-model" list = "usage-model-options" placeholder = "全部模型" > < datalist id = "usage-model-options" > < / datalist > < / label >
< label class = "usage-filter" > 结果< select id = "usage-result" > < option value = "" > 全部结果< / option > < option value = "succeeded" > 成功< / option > < option value = "failed" > 失败< / option > < option value = "rejected" > 已拒绝< / option > < option value = "canceled" > 已取消< / option > < / select > < / label >
< label class = "usage-filter" > 上游< select id = "usage-auth" > < option value = "" > 全部上游< / option > < / select > < / label >
< label class = "usage-filter" > 端点< select id = "usage-endpoint" > < option value = "" > 全部端点< / option > < option value = "responses" > responses< / option > < option value = "compact" > compact< / option > < option value = "chat" > chat< / option > < / select > < / label >
< label class = "usage-filter request-filter" > 请求 ID< input id = "usage-request-id" placeholder = "粘贴完整 Request ID" > < / label >
< div class = "usage-filter-actions" > < button id = "clear-usage-filters" type = "button" > 清空< / button > < button id = "apply-usage-filters" class = "primary" type = "button" > 查询< / button > < / div >
< / div >
< div class = "section-heading" > < div > < div id = "page-buttons" class = "page-buttons" > < / div > < span id = "status" class = "status" > < / span > < / div > < div class = "tools" >
< details id = "usage-filter-panel" class = "usage-filter-panel" > < summary > 查询过滤 < / summary > < div class = "surface usage-filters" >
< label class = "usage-filter" > 开始时间< input id = "usage-from" type = "datetime-local" step = "1" > < / label >
< label class = "usage-filter" > 结束时间< input id = "usage-to" type = "datetime-local" step = "1" > < / label >
< label class = "usage-filter" > 用户< select id = "usage-key" > < option value = "" > 全部用户< / option > < / select > < / label >
< label class = "usage-filter" > 模型< input id = "usage-model" list = "usage-model-options" placeholder = "全部模型" > < datalist id = "usage-model-options" > < / datalist > < / label >
< label class = "usage-filter" > 结果< select id = "usage-result" > < option value = "" > 全部结果< / option > < option value = "succeeded" > 成功< / option > < option value = "failed" > 失败< / option > < option value = "rejected" > 已拒绝< / option > < option value = "canceled" > 已取消< / option > < / select > < / label >
< label class = "usage-filter" > 上游< select id = "usage-auth" > < option value = "" > 全部上游< / option > < / select > < / label >
< label class = "usage-filter" > 端点< select id = "usage-endpoint" > < option value = "" > 全部端点< / option > < option value = "responses" > responses< / option > < option value = "compact" > compact< / option > < option value = "chat" > chat< / option > < / select > < / label >
< label class = "usage-filter request-filter" > 请求 ID< input id = "usage-request-id" placeholder = "粘贴完整 Request ID" > < / label >
< div class = "usage-filter-actions" > < button id = "clear-usage-filters" type = "button" > 清空< / button > < button id = "apply-usage-filters" class = "primary" type = "button" > 查询< / button > < / div >
< / div > < / details >
< details > < summary > 列< / summary > < div id = "column-options" class = "column-options" > < / div > < / details > < button id = "refresh" type = "button" > 刷新< / button >
< / div > < / div >
< div class = "surface" > < div class = "table-wrap" > < table > < thead > < tr id = "headers" > < / tr > < / thead > < tbody id = "rows" > < / tbody > < / table > < / div > < / div >
< / section >
< section id = "view-pricing" class = "view" >
< div class = "section-heading" > < div > < h1 > 价格配置< / h1 > < / div > < div id = "price-status" class = "status" > < / div > < / div >
< div class = "surface price-panel price-layout" >
< aside class = "price-sidebar" > < div class = "price-sidebar-header" > < h2 > 已配置模型< / h2 > < button id = "new-price" class = "small" type = "button" > 新增< / button > < / div > < div id = "price-list" class = "price-list" > < / div > < / aside >
< aside class = "price-sidebar" > < div class = "price-sidebar-header" > < h2 > 已配置模型< / h2 > < button id = "new-price" class = "small admin-only " type = "button" > 新增< / button > < / div > < div id = "price-list" class = "price-list" > < / div > < / aside >
< section class = "price-editor" >
< div id = "price-editor-content" class = "hidden" >
< div class = "price-editor-header" > < h2 > 模型价格< / h2 > < / div >
< div class = "price-section" > < label class = "field price-model-field" > 模型名称< input id = "price-model" placeholder = "gpt-5.6-sol" > < / label > < div id = "price-source" class = "price-source" > 手动配置< / div > < / div >
< div class = "price-section" > < div class = "price-section-header" > < h3 > models.dev 参考价格< / h3 > < button id = "refresh-catalog" class = "small" type = "button" > 更新目录< / button > < / div > < div id = "catalog-summary" class = "catalog-summary" > < / div > < div class = "catalog-toolbar" > < input id = "catalog-query" placeholder = "搜索模型或供应商" > < button id = "search-catalog" class = "small" type = "button" > 搜索< / button > < / div > < div id = "catalog-results" class = "catalog-results" > < / div > < / div >
< div class = "price-section admin-only " > < div class = "price-section-header" > < h3 > models.dev 参考价格< / h3 > < button id = "refresh-catalog" class = "small" type = "button" > 更新目录< / button > < / div > < div id = "catalog-summary" class = "catalog-summary" > < / div > < div class = "catalog-toolbar" > < input id = "catalog-query" placeholder = "搜索模型或供应商" > < button id = "search-catalog" class = "small" type = "button" > 搜索< / button > < / div > < div id = "catalog-results" class = "catalog-results" > < / div > < / div >
< div class = "price-section" > < div class = "price-section-header" > < h3 > 基础价格 · $ / 1M Token< / h3 > < / div > < div class = "price-grid" >
< label > 输入< input id = "price-input" inputmode = "decimal" > < / label > < label > 缓存读取< input id = "price-cache-read" inputmode = "decimal" > < / label > < label > 缓存写入< input id = "price-cache-write" inputmode = "decimal" > < / label > < label > 输出< input id = "price-output" inputmode = "decimal" > < / label >
< / div > < / div >
< div class = "price-section" > < div class = "price-section-header" > < h3 > 长上下文价格< / h3 > < label class = "inline-check" > < input id = "long-enabled" type = "checkbox" > 启用< / label > < / div > < div class = "price-grid long-grid" >
< div id = "long-price-section" class = "price-section hidden " > < div class = "price-section-header" > < h3 > 长上下文价格< / h3 > < label class = "inline-check" > < input id = "long-enabled" type = "checkbox" > 启用< / label > < / div > < div class = "price-grid long-grid" >
< label class = "long-field hidden" > 输入门槛< input id = "long-threshold" type = "number" min = "0" step = "1" > < / label > < label class = "long-field hidden" > 门槛比较< select id = "long-comparison" > < option value = "gt" > 大于< / option > < option value = "gte" > 大于等于< / option > < / select > < / label > < label class = "long-field hidden" > 输入< input id = "long-input" inputmode = "decimal" > < / label > < label class = "long-field hidden" > 缓存读取< input id = "long-cache-read" inputmode = "decimal" > < / label > < label class = "long-field hidden" > 缓存写入< input id = "long-cache-write" inputmode = "decimal" > < / label > < label class = "long-field hidden" > 输出< input id = "long-output" inputmode = "decimal" > < / label >
< / div > < / div >
< div class = "price-section" > < div class = "price-section-header" > < h3 > Fast 价格< / h3 > < / div > < div class = "fast-row" > < label class = "inline-check" > < input id = "fast-pricing-enabled" type = "checkbox" > 使用倍率计价< / label > < label class = "field" > 倍率< input id = "fast-multiplier" inputmode = "decimal" value = "2.5" > < / label > < / div > < / div >
< div class = "price-actions" > < button id = "clear-price" type = "button" > 清空< / button > < button id = "save-price" class = "primary" type = "button" > 保存价格< / button > < / div >
< div class = "price-actions admin-only" > < button id = "delete-price" class = "danger hidden" type = "button" > 删除 < / button > < button id = "clear-price" type = "button" > 清空< / button > < button id = "save-price" class = "primary" type = "button" > 保存价格< / button > < / div >
< / div >
< / section >
< / div >
< / section >
@@ -313,6 +313,7 @@
const MODELS _API = MANAGEMENT _API + "/model-suggestions" ;
const BILLING _RESET _API = MANAGEMENT _API + "/billing-reset" ;
const BILLING _LEDGER _API = MANAGEMENT _API + "/billing-ledger" ;
const READONLY _API = location . pathname ;
const keyInput = document . querySelector ( "#key" ) ;
const statusNode = document . querySelector ( "#status" ) ;
const rowsNode = document . querySelector ( "#rows" ) ;
@@ -321,6 +322,7 @@
const columnStoreKey = "billing:usage-columns" ;
const priceStatusNode = document . querySelector ( "#price-status" ) ;
const priceListNode = document . querySelector ( "#price-list" ) ;
const priceEditorContentNode = document . querySelector ( "#price-editor-content" ) ;
const priceSourceNode = document . querySelector ( "#price-source" ) ;
const catalogSummaryNode = document . querySelector ( "#catalog-summary" ) ;
const catalogResultsNode = document . querySelector ( "#catalog-results" ) ;
@@ -336,92 +338,19 @@
const statsRecentNode = document . querySelector ( "#stats-recent" ) ;
const pageButtonsNode = document . querySelector ( "#page-buttons" ) ;
const PAGE _SIZE = 100 ;
const DEMO _STORE _KEY = "billing:demo-state:v4" ;
let currentRecords = [ ] ;
let currentPage = 1 ;
let currentPagination = { page : 1 , page _size : PAGE _SIZE , total : 0 , total _pages : 0 , previous _cursor : "" , next _cursor : "" } ;
let usageDashboard = { today : { requests : 0 , total _tokens : 0 , cost _usd : 0 } , users : [ ] , days : [ ] } ;
let lastSignature = "" ;
let loading = false ;
let currentPrices = [ ] ;
let selectedPriceModel = "" ;
let currentCatalog = null ;
let managedKeys = [ ] ;
let upstreamAccounts = [ ] ;
let modelSuggestions = [ ] ;
let editingKey = null ;
let testMode = false ;
let demoState = loadDemoState ( ) ;
function demoPrice ( ) {
return {
model : "deepseek-v4-flash" ,
base : { input _per _1m : "2.5" , cache _read _per _1m : "0.25" , cache _write _per _1m : "3.125" , output _per _1m : "15" } ,
long _context : { threshold _input _tokens : 272000 , comparison : "gt" , input _per _1m : "5" , cache _read _per _1m : "0.5" , cache _write _per _1m : "6.25" , output _per _1m : "22.5" } ,
fast _pricing _enabled : true , fast _multiplier : "2.5" ,
source : { kind : "manual" }
} ;
}
function demoCatalog ( ) {
return [
{ id : "openai/gpt-5.6-sol" , provider : "openai" , provider _name : "OpenAI" , model : "gpt-5.6-sol" , model _name : "GPT-5.6 Sol" , base : { input _per _1m : "5" , cache _read _per _1m : "0.5" , cache _write _per _1m : "6.25" , output _per _1m : "30" } , long _context : { threshold _input _tokens : 272000 , comparison : "gt" , input _per _1m : "10" , cache _read _per _1m : "1" , cache _write _per _1m : "12.5" , output _per _1m : "45" } } ,
{ id : "deepseek/deepseek-v4-flash" , provider : "deepseek" , provider _name : "DeepSeek" , model : "deepseek-v4-flash" , model _name : "DeepSeek V4 Flash" , base : { input _per _1m : "0.14" , cache _read _per _1m : "0.014" , cache _write _per _1m : "0.14" , output _per _1m : "0.28" } } ,
{ id : "google/gemini-3.7-flash" , provider : "google" , provider _name : "Google" , model : "gemini-3.7-flash" , model _name : "Gemini 3.7 Flash" , base : { input _per _1m : "0.38" , cache _read _per _1m : "0.038" , cache _write _per _1m : "0.38" , output _per _1m : "1.88" } }
] ;
}
function freshDemoState ( ) {
const upstreams = [ "A" , "B" , "C" ] . map ( ( name , index ) => ( {
id : "demo-upstream-" + name . toLowerCase ( ) , cpa _auth _id : "codex:demo:" + name . toLowerCase ( ) ,
provider : "codex" , display _name : "DeepSeek 测试源 " + name , priority : 10 ,
disabled : false , unavailable : false
} ) ) ;
const keys = [
{ id : "demo-key-alice" , name : "alice" , secret : "alice-000000" , status : "active" , route _mode : "strict" , upstream _account _id : upstreams [ 0 ] . id , all _models : false , models : [ "deepseek-*" ] } ,
{ id : "demo-key-bob" , name : "bob" , secret : "bob-000000" , status : "active" , route _mode : "strict" , upstream _account _id : upstreams [ 0 ] . id , all _models : true , models : [ ] } ,
{ id : "demo-key-carol" , name : "carol" , secret : "carol-000000" , status : "active" , route _mode : "strict" , upstream _account _id : upstreams [ 1 ] . id , all _models : false , models : [ "deepseek-source-b" , "deepseek-v4-flash" ] } ,
{ id : "demo-key-eve" , name : "eve" , secret : "eve-000000" , status : "disabled" , route _mode : "auto" , upstream _account _id : "" , all _models : false , models : [ "deepseek-source-c" ] }
] ;
const now = Date . now ( ) ;
const usage = Array . from ( { length : 2370 } , ( _ , index ) => {
const key = keys [ index % keys . length ] ;
const failed = index % 13 === 0 ;
const input = 900 + ( index * 137 ) % 18000 ;
const output = 24 + ( index * 17 ) % 420 ;
return {
request _id : "demo-request-" + String ( index + 1 ) . padStart ( 4 , "0" ) , execution _id : "demo-exec-" + index ,
requested _at : new Date ( now - index * 187000 ) . toISOString ( ) , managed _key _id : key . id , key _alias : key . name , api _key : key . secret ,
auth _id : upstreams [ index % upstreams . length ] . cpa _auth _id , auth _index : "demo-" + ( index % upstreams . length + 1 ) ,
model : index % 7 === 0 ? "deepseek-source-c" : "deepseek-v4-flash" , reasoning _effort : "-" , service _tier : "auto" , speed : "" ,
failed , outcome : failed ? "failed" : "succeeded" , status _code : failed ? 503 : 200 ,
request _type : "SSE" , endpoint : "/v1/responses" , ttft _ms : 120 + index % 360 , speed _tps : 32 + index % 28 ,
input _tokens : input , output _tokens : output , reasoning _tokens : 0 , cache _read _tokens : Math . floor ( input * . 72 ) , cache _write _tokens : 0 ,
cache _rate : 72 , total _tokens : input + output , cost _usd : ( input * 2.5 + output * 15 ) / 1000000 ,
cost _available : true , client _ip : "127.0.0.1"
} ;
} ) ;
const ledger = [ ] ;
keys . forEach ( ( key , keyIndex ) => {
const records = usage . filter ( item => item . key _alias === key . name ) ;
const spent = records . reduce ( ( sum , item ) => sum + ( item . cost _usd || 0 ) , 0 ) ;
const quota = 250 + keyIndex * 50 ;
key . billing = { quota _usd : String ( quota ) , spent _usd : spent . toFixed ( 6 ) , balance _usd : ( quota - spent ) . toFixed ( 6 ) , reset _period : keyIndex % 2 ? "monthly" : "none" , next _reset _at : keyIndex % 2 ? new Date ( now + 86400000 * 20 ) . toISOString ( ) : null , max _concurrency : 4 , active _requests : keyIndex % 3 , cycle _started _at : new Date ( now - 86400000 * 10 ) . toISOString ( ) } ;
records . slice ( 0 , 12 ) . forEach ( ( record , index ) => ledger . push ( { id : keyIndex * 100 + index + 1 , key _id : key . id , kind : "charge" , amount _usd : ( - record . cost _usd ) . toFixed ( 6 ) , balance _after _usd : ( quota - spent + index * record . cost _usd ) . toFixed ( 6 ) , request _id : record . request _id , model : record . model , occurred _at : record . requested _at } ) ) ;
} ) ;
return { keys , upstreams , usage , prices : [ demoPrice ( ) ] , ledger } ;
}
function loadDemoState ( ) {
try {
const value = JSON . parse ( localStorage . getItem ( DEMO _STORE _KEY ) ) ;
if ( value && Array . isArray ( value . keys ) && Array . isArray ( value . usage ) && Array . isArray ( value . prices ) && Array . isArray ( value . ledger ) ) return value ;
} catch ( _ ) { }
return freshDemoState ( ) ;
}
function saveDemoState ( ) {
localStorage . setItem ( DEMO _STORE _KEY , JSON . stringify ( demoState ) ) ;
}
let managementAuthorized = false ;
function storedPanelKey ( ) {
const prefix = "enc::v1::" ;
@@ -627,44 +556,6 @@
return params ;
}
function demoUsagePage ( page ) {
const params = usageFilterQuery ( ) ;
const from = params . get ( "from" ) ? new Date ( params . get ( "from" ) ) : null ;
const to = params . get ( "to" ) ? new Date ( params . get ( "to" ) ) : null ;
const resultValue = record => record . outcome === "canceled" ? "canceled" : record . outcome === "rejected" ? "rejected" : ( record . failed || record . outcome === "failed" ) ? "failed" : "succeeded" ;
const endpointValue = record => endpoint ( record . endpoint ) ;
const records = demoState . usage . filter ( record => {
const requestedAt = new Date ( record . requested _at ) ;
return ( ! from || requestedAt >= from ) && ( ! to || requestedAt < to ) &&
( ! params . get ( "key_id" ) || record . managed _key _id === params . get ( "key_id" ) ) &&
( ! params . get ( "model" ) || String ( record . model || "" ) . toLowerCase ( ) === params . get ( "model" ) . toLowerCase ( ) ) &&
( ! params . get ( "result" ) || resultValue ( record ) === params . get ( "result" ) ) &&
( ! params . get ( "auth_id" ) || record . auth _id === params . get ( "auth_id" ) ) &&
( ! params . get ( "endpoint" ) || endpointValue ( record ) === params . get ( "endpoint" ) ) &&
( ! params . get ( "request_id" ) || record . request _id === params . get ( "request_id" ) ) ;
} ) ;
const totalPages = Math . ceil ( records . length / PAGE _SIZE ) ;
const safePage = Math . max ( 1 , Math . min ( page || 1 , Math . max ( 1 , totalPages ) ) ) ;
const start = ( safePage - 1 ) * PAGE _SIZE ;
return { records : records . slice ( start , start + PAGE _SIZE ) , pagination : { page : safePage , page _size : PAGE _SIZE , total : records . length , total _pages : totalPages , previous _cursor : safePage > 1 ? "demo-previous" : "" , next _cursor : safePage < totalPages ? "demo-next" : "" } } ;
}
function demoUsageDashboard ( ) {
const today = startOfDay ( new Date ( ) ) ;
const summarize = records => ( { requests : records . length , input _tokens : records . reduce ( ( sum , item ) => sum + ( item . input _tokens || 0 ) , 0 ) , output _tokens : records . reduce ( ( sum , item ) => sum + ( item . output _tokens || 0 ) , 0 ) , total _tokens : records . reduce ( ( sum , item ) => sum + ( item . total _tokens || 0 ) , 0 ) , cost _usd : records . reduce ( ( sum , item ) => sum + ( item . cost _available ? item . cost _usd || 0 : 0 ) , 0 ) } ) ;
const todayRecords = demoState . usage . filter ( record => new Date ( record . requested _at ) >= today ) ;
const users = demoState . keys . map ( key => {
const records = demoState . usage . filter ( record => record . managed _key _id === key . id ) ;
return { key _id : key . id , key _alias : key . name , today : summarize ( records . filter ( record => new Date ( record . requested _at ) >= today ) ) , last _used _at : records [ 0 ] ? . requested _at || null } ;
} ) ;
const days = Array . from ( { length : 7 } , ( _ , index ) => {
const date = startOfDay ( new Date ( ) ) ; date . setDate ( date . getDate ( ) - ( 6 - index ) ) ;
const end = new Date ( date ) ; end . setDate ( end . getDate ( ) + 1 ) ;
return { date : ` ${ date . getFullYear ( ) } - ${ String ( date . getMonth ( ) + 1 ) . padStart ( 2 , "0" ) } - ${ String ( date . getDate ( ) ) . padStart ( 2 , "0" ) } ` , total _tokens : demoState . usage . filter ( record => { const value = new Date ( record . requested _at ) ; return value >= date && value < end ; } ) . reduce ( ( sum , record ) => sum + ( record . total _tokens || 0 ) , 0 ) } ;
} ) ;
return { today : summarize ( todayRecords ) , users , days } ;
}
function populateUsageFilterOptions ( keys , upstreams , models ) {
const keySelect = document . querySelector ( "#usage-key" ) ;
const authSelect = document . querySelector ( "#usage-auth" ) ;
@@ -675,13 +566,8 @@
}
async function loadUsageFilterOptions ( ) {
if ( testMode ) {
populateUsageFilterOptions ( demoState . keys , demoState . upstreams , [ ... new Set ( demoState . usage . map ( record => record . model ) . filter ( Boolean ) ) ] ) ;
return ;
}
if ( ! keyInput . value . trim ( ) ) return ;
try {
const [ keys , upstreams , models ] = await Promise . all ( [ managedFetch ( KEYS _API + "?include_archived=1" ) , managedFetch ( UPSTREAMS _API ) , managedFetch ( MODELS _API ) ] ) ;
const [ keys , upstreams , models ] = await Promise . all ( [ dataFetch ( KEYS _API + "?include_archived=1" ) , dataFetch ( UPSTREAMS _API ) , dataFetch ( MODELS _API ) ] ) ;
populateUsageFilterOptions ( keys . keys || [ ] , upstreams . accounts || [ ] , models . models || [ ] ) ;
} catch ( error ) { statusNode . textContent = "读取筛选项失败: " + error . message ; }
}
@@ -692,12 +578,60 @@
return headers ;
}
function setAccessState ( authorized ) {
managementAuthorized = authorized ;
document . body . classList . toggle ( "read-only" , ! authorized ) ;
document . querySelectorAll ( "#view-pricing .price-editor input, #view-pricing .price-editor select" ) . forEach ( node => {
if ( node . id !== "catalog-query" ) node . disabled = ! authorized ;
} ) ;
syncLongSectionVisibility ( ) ;
if ( ! authorized ) closeDrawer ( ) ;
}
function readOnlyURL ( managementURL ) {
const source = new URL ( managementURL , location . origin ) ;
const views = {
[ API ] : "usage" , [ SUMMARY _API ] : "usage-summary" , [ PRICE _API ] : "prices" ,
[ CATALOG _API ] : "price-catalog" , [ KEYS _API ] : "keys" , [ KEY _STATS _API ] : "key-stats" ,
[ UPSTREAMS _API ] : "upstreams" , [ MODELS _API ] : "model-suggestions"
} ;
const view = views [ source . pathname ] ;
if ( ! view ) throw new Error ( "该数据不支持匿名读取" ) ;
source . searchParams . set ( "view" , view ) ;
return READONLY _API + "?" + source . searchParams . toString ( ) ;
}
async function responsePayload ( response ) {
try { return await response . json ( ) ; } catch ( _ ) { return { } ; }
}
async function dataFetch ( url ) {
const key = keyInput . value . trim ( ) ;
if ( key ) {
const response = await fetch ( url , { headers : authHeaders ( ) } ) ;
const payload = await responsePayload ( response ) ;
if ( response . ok ) { setAccessState ( true ) ; return payload ; }
if ( response . status !== 401 && response . status !== 403 ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
setAccessState ( false ) ;
} else {
setAccessState ( false ) ;
}
const response = await fetch ( readOnlyURL ( url ) , { headers : { Accept : "application/json" } } ) ;
const payload = await responsePayload ( response ) ;
if ( ! response . ok ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
return payload ;
}
async function managedFetch ( url , options = { } ) {
if ( testMode ) throw new Error ( "测试模式禁止访问真实管理接口 ") ;
if ( ! keyInput . value . trim ( ) ) { setAccessState ( false ) ; throw new Error ( "请输入管理密钥 ") ; }
const headers = authHeaders ( Boolean ( options . body ) ) ;
const response = await fetch ( url , { ... options , headers : { ... headers , ... ( options . headers || { } ) } } ) ;
const payload = await response . json ( ) ;
if ( ! response . ok ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
const payload = await responsePayload ( response ) ;
if ( ! response . ok ) {
if ( response . status === 401 || response . status === 403 ) setAccessState ( false ) ;
throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
}
setAccessState ( true ) ;
return payload ;
}
@@ -803,8 +737,9 @@
const identity = document . createElement ( "div" ) ; identity . className = "key-identity" ;
const avatar = document . createElement ( "span" ) ; avatar . className = "avatar" ; avatar . textContent = ( key . name || "K" ) . slice ( 0 , 2 ) . toUpperCase ( ) ;
const identityText = document . createElement ( "div" ) ; const name = document . createElement ( "strong" ) ; name . textContent = key . name ; const id = document . createElement ( "small" ) ; id . textContent = key . id ; identityText . append ( name , id ) ; identity . append ( avatar , identityText ) ;
const credential = document . createElement ( "div" ) ; credential . className = "credential" ; const secret = document . createElement ( "code" ) ; secret . className = "credential-copy" ; secret . textContent = maskedSecret ( key . secret ) ; secret . tabIndex = 0 ; secret . setAttribute ( "role" , "button" ) ; secret . setAttribute ( "aria-label" , "复制完整 Key" ) ; secret . title = "点击复制完整 Key" ;
secret . addEventListener ( "click" , ( ) => copyText ( key . secret , secret ) ) ; secret . addEventListener ( "keydown" , event => { if ( event . key === "Enter" || event . key === " " ) { event . preventDefault ( ) ; copyText ( key . secret , secret ) ; } } ) ; credential . append ( secret ) ;
const credential = document . createElement ( "div" ) ; credential . className = "credential" ; const secret = document . createElement ( "code" ) ; secret . className = "credential-copy" ; secret . textContent = managementAuthorized ? maskedSecret ( key . secret ) : ( key . masked _secret || "******" ) ;
if ( managementAuthorized && key . secret ) { secret . tabIndex = 0 ; secret . setAttribute ( "role" , "button" ) ; secret . setAttribute ( "aria-label" , "复制完整 Key" ) ; secret . title = "点击复制完整 Key" ; secret . addEventListener ( "click" , ( ) => copyText ( key . secret , secret ) ) ; secret . addEventListener ( "keydown" , event => { if ( event . key === "Enter" || event . key === " " ) { event . preventDefault ( ) ; copyText ( key . secret , secret ) ; } } ) ; }
credential . append ( secret ) ;
const status = document . createElement ( "span" ) ; status . className = ` badge ${ key . status } ` ; status . textContent = key . status === "active" ? "启用" : key . status === "disabled" ? "禁用" : "已归档" ;
const billing = key . billing || { } ; const balance = document . createElement ( "div" ) ; const balanceMain = document . createElement ( "div" ) ; balanceMain . className = "route-main" ; balanceMain . textContent = money ( billing . balance _usd ) ; const balanceSub = document . createElement ( "div" ) ; balanceSub . className = "route-sub" ; balanceSub . textContent = ` 额度 ${ money ( billing . quota _usd ) } ${ billing . reset _period && billing . reset _period !== "none" ? " · " + ( { daily : "每天" , weekly : "每周" , monthly : "每月" } [billing.reset_period] || billing.reset_period) : ""} ` ; balance . append ( balanceMain , balanceSub ) ;
const concurrency = document . createElement ( "span" ) ; concurrency . textContent = ` ${ billing . active _requests || 0 } / ${ billing . max _concurrency || 4 } ` ;
@@ -812,7 +747,8 @@
const models = document . createElement ( "span" ) ; models . textContent = key . all _models ? "全部模型" : ( key . models || [ ] ) . join ( ", " ) || "未配置" ;
const actions = document . createElement ( "div" ) ; actions . className = "key-actions" ;
const stats = document . createElement ( "button" ) ; stats . type = "button" ; stats . className = "small" ; stats . textContent = "统计" ; stats . addEventListener ( "click" , ( ) => loadKeyStats ( key ) ) ;
const manage = document . createElement ( "button" ) ; manage . type = "button" ; manage . className = "small" ; manage . textContent = archived ? "查看" : "管理" ; manage . addEventListener ( "click" , ( ) => openKeyEditor ( key ) ) ; actions . append ( stats , manage ) ;
actions . append ( stats ) ;
if ( managementAuthorized ) { const manage = document . createElement ( "button" ) ; manage . type = "button" ; manage . className = "small admin-only" ; manage . textContent = archived ? "查看" : "管理" ; manage . addEventListener ( "click" , ( ) => openKeyEditor ( key ) ) ; actions . append ( manage ) ; }
[ identity , credential , status , balance , concurrency , routeNode , models , actions ] . forEach ( node => { const cell = document . createElement ( "td" ) ; cell . className = "left" ; cell . append ( node ) ; row . append ( cell ) ; } ) ;
return row ;
} ) ) ;
@@ -823,10 +759,6 @@
renderUserCharts ( ) ;
}
function startOfDay ( value ) {
return new Date ( value . getFullYear ( ) , value . getMonth ( ) , value . getDate ( ) ) ;
}
function compactNumber ( value ) {
return new Intl . NumberFormat ( "zh-CN" , { notation : "compact" , maximumFractionDigits : 1 } ) . format ( value || 0 ) ;
}
@@ -851,27 +783,16 @@
}
async function loadKeyManagement ( ) {
if ( testMode ) {
managedKeys = demoState . keys . filter ( key => document . querySelector ( "#include-archived" ) . checked || key . status !== "archived" ) ;
upstreamAccounts = demoState . upstreams ;
modelSuggestions = [ "deepseek-v4-flash" , "deepseek-source-a" , "deepseek-source-b" , "deepseek-source-c" ] ;
usageDashboard = demoUsageDashboard ( ) ;
renderManagedKeys ( ) ;
keyStatusNode . textContent = ` 测试数据 · ${ managedKeys . length } 个 Key, ${ upstreamAccounts . length } 个上游账号 ` ;
return ;
}
if ( ! keyInput . value . trim ( ) ) return ;
keyStatusNode . textContent = "正在同步" ;
keyStatusNode . textContent = "正在读取" ;
try {
const [ upstreams , models , summary ] = await Promise . all ( [ managedFetch ( UPSTREAMS _API ) , managedFetch ( MODELS _API ) , managedFetch ( SUMMARY _API ) ] ) ;
const [ upstreams , models , summary ] = await Promise . all ( [ dataFetch ( UPSTREAMS _API ) , dataFetch ( MODELS _API ) , dataFetch ( SUMMARY _API ) ] ) ;
upstreamAccounts = upstreams . accounts || [ ] ;
modelSuggestions = models . models || [ ] ;
usageDashboard = summary ;
const archived = document . querySelector ( "#include-archived" ) . checked ? "?include_archived=1" : "" ;
const keys = await managedFetch ( KEYS _API + archived ) ;
const keys = await dataFetch ( KEYS _API ) ;
managedKeys = keys . keys || [ ] ;
renderManagedKeys ( ) ;
keyStatusNode . textContent = ` 共 ${ managedKeys . length } 个 Key, ${ upstreamAccounts . length } 个上游账号 ${ upstreams . warning ? ";同步警告:" + upstreams . warning : "" } ` ;
keyStatusNode . textContent = managementAuthorized ? ` 共 ${ managedKeys . length } 个 Key, ${ upstreamAccounts . length } 个上游账号 ${ upstreams . warning ? ";同步警告:" + upstreams . warning : "" } ` : "" ;
} catch ( error ) { keyStatusNode . textContent = "读取失败: " + error . message ; }
}
@@ -882,15 +803,6 @@
const allModels = document . querySelector ( "#editor-all-models" ) . checked ;
const payload = { name , secret , route _mode : route , upstream _account _id : route === "strict" ? document . querySelector ( "#editor-upstream" ) . value : "" , all _models : allModels , models : document . querySelector ( "#editor-models" ) . value . split ( "," ) . map ( value => value . trim ( ) ) . filter ( Boolean ) , billing : billingPayload ( ) } ;
try {
if ( testMode ) {
if ( ! name ) throw new Error ( "请输入名称" ) ;
if ( secret && secret . length < 6 ) throw new Error ( "Key 至少需要 6 个字符" ) ;
if ( demoState . keys . some ( key => key . name . toLowerCase ( ) === name . toLowerCase ( ) ) ) throw new Error ( "名称已经存在" ) ;
const created = { id : "demo-key-" + Date . now ( ) , ... payload , secret : secret || ` demo- ${ Date . now ( ) } -000000 ` , status : "active" , billing : { ... payload . billing , spent _usd : "0" , balance _usd : payload . billing . quota _usd , active _requests : 0 , cycle _started _at : new Date ( ) . toISOString ( ) } } ;
demoState . keys . push ( created ) ; saveDemoState ( ) ; closeDrawer ( ) ; await loadKeyManagement ( ) ;
keyStatusNode . textContent = ` 测试数据已创建 ${ created . name } : ${ created . secret } ` ;
return ;
}
const created = await managedFetch ( KEYS _API , { method : "POST" , body : JSON . stringify ( payload ) } ) ;
keyStatusNode . textContent = ` 已创建 ${ created . name } : ${ created . secret } ` ;
closeDrawer ( ) ; await loadKeyManagement ( ) ;
@@ -899,15 +811,6 @@
async function updateManagedKey ( payload ) {
try {
if ( testMode ) {
const index = demoState . keys . findIndex ( key => key . id === payload . id ) ;
if ( index < 0 ) throw new Error ( "测试 Key 不存在" ) ;
const previous = demoState . keys [ index ] ; const spent = Number ( previous . billing ? . spent _usd || 0 ) ; const previousQuota = Number ( previous . billing ? . quota _usd || 0 ) ; const nextQuota = Number ( payload . billing ? . quota _usd || 0 ) ;
const billing = { ... previous . billing , ... payload . billing , spent _usd : String ( spent ) , balance _usd : String ( nextQuota - spent ) , active _requests : previous . billing ? . active _requests || 0 } ;
demoState . keys [ index ] = { ... previous , ... payload , billing } ;
if ( previousQuota !== nextQuota ) demoState . ledger . push ( { id : Date . now ( ) , key _id : previous . id , kind : "quota_change" , amount _usd : String ( nextQuota - previousQuota ) , balance _after _usd : String ( nextQuota - spent ) , occurred _at : new Date ( ) . toISOString ( ) } ) ;
saveDemoState ( ) ; closeDrawer ( ) ; await loadKeyManagement ( ) ; return ;
}
await managedFetch ( KEYS _API , { method : "PATCH" , body : JSON . stringify ( payload ) } ) ; closeDrawer ( ) ; await loadKeyManagement ( ) ;
}
catch ( error ) { keyStatusNode . textContent = "保存失败: " + error . message ; }
@@ -933,7 +836,7 @@
const node = document . querySelector ( "#billing-ledger" ) ;
node . replaceChildren ( Object . assign ( document . createElement ( "div" ) , { className : "empty" , textContent : "正在读取" } ) ) ;
try {
const entries = testMode ? demoState . ledger . filter ( item => item . key _id === key . id ) . sort ( ( a , b ) => b . id - a . id ) . slice ( 0 , 50 ) : ( await managedFetch ( BILLING _LEDGER _API + "?id=" + encodeURIComponent ( key . id ) + "&limit=50" ) ) . entries || [ ] ;
const entries = ( await managedFetch ( BILLING _LEDGER _API + "?id=" + encodeURIComponent ( key . id ) + "&limit=50" ) ) . entries || [ ] ;
node . replaceChildren ( ... ( entries . length ? entries . map ( entry => {
const row = document . createElement ( "div" ) ; row . className = "ledger-item" ;
const label = document . createElement ( "strong" ) ; label . textContent = ( { charge : "请求扣费" , quota _change : "调整额度" , cycle _reset : "额度重置" } [ entry . kind ] || entry . kind ) ;
@@ -947,14 +850,7 @@
async function resetManagedBilling ( ) {
if ( ! editingKey || ! confirm ( ` 立即重置 ${ editingKey . name } 的额度?当前剩余余额不会结转。 ` ) ) return ;
try {
if ( testMode ) {
const key = demoState . keys . find ( item => item . id === editingKey . id ) ; const quota = Number ( key . billing . quota _usd || 0 ) ; const now = new Date ( ) ;
key . billing . spent _usd = "0" ; key . billing . balance _usd = String ( quota ) ; key . billing . cycle _started _at = now . toISOString ( ) ;
if ( key . billing . reset _period !== "none" ) { const next = new Date ( now ) ; if ( key . billing . reset _period === "daily" ) next . setDate ( next . getDate ( ) + 1 ) ; else if ( key . billing . reset _period === "weekly" ) next . setDate ( next . getDate ( ) + 7 ) ; else next . setMonth ( next . getMonth ( ) + 1 ) ; key . billing . next _reset _at = next . toISOString ( ) ; }
demoState . ledger . push ( { id : Date . now ( ) , key _id : key . id , kind : "cycle_reset" , amount _usd : String ( quota ) , balance _after _usd : String ( quota ) , occurred _at : now . toISOString ( ) } ) ; saveDemoState ( ) ; editingKey = key ;
} else {
await managedFetch ( BILLING _RESET _API , { method : "POST" , body : JSON . stringify ( { id : editingKey . id } ) } ) ;
}
await managedFetch ( BILLING _RESET _API , { method : "POST" , body : JSON . stringify ( { id : editingKey . id } ) } ) ;
await loadKeyManagement ( ) ; const refreshed = managedKeys . find ( item => item . id === editingKey . id ) ; if ( refreshed ) openKeyEditor ( refreshed ) ;
} catch ( error ) { document . querySelector ( "#billing-status" ) . textContent = "重置失败:" + error . message ; }
}
@@ -962,9 +858,6 @@
async function archiveManagedKey ( key ) {
if ( ! confirm ( ` 永久归档 ${ key . name } ?该 Key 将不能恢复,但历史统计会保留。 ` ) ) return ;
try {
if ( testMode ) {
const found = demoState . keys . find ( item => item . id === key . id ) ; if ( ! found ) throw new Error ( "测试 Key 不存在" ) ; found . status = "archived" ; saveDemoState ( ) ; closeDrawer ( ) ; await loadKeyManagement ( ) ; return ;
}
await managedFetch ( KEYS _API , { method : "DELETE" , body : JSON . stringify ( { id : key . id } ) } ) ; closeDrawer ( ) ; await loadKeyManagement ( ) ;
}
catch ( error ) { keyStatusNode . textContent = "归档失败: " + error . message ; }
@@ -972,15 +865,7 @@
async function loadKeyStats ( key ) {
try {
let payload ;
if ( testMode ) {
const recent = demoState . usage . filter ( item => item . key _alias === key . name ) ;
const since = new Date ( ) ; since . setHours ( 0 , 0 , 0 , 0 ) ;
const summarize = records => ( { requests : records . length , total _tokens : records . reduce ( ( sum , item ) => sum + item . total _tokens , 0 ) , cost _micros : Math . round ( records . reduce ( ( sum , item ) => sum + ( item . cost _usd || 0 ) , 0 ) * 1000000 ) } ) ;
payload = { stats : { total : summarize ( recent ) , today : summarize ( recent . filter ( item => new Date ( item . requested _at ) >= since ) ) } , recent } ;
} else {
payload = await managedFetch ( KEY _STATS _API + "?id=" + encodeURIComponent ( key . id ) ) ;
}
const payload = await dataFetch ( KEY _STATS _API + "?id=" + encodeURIComponent ( key . id ) ) ;
const total = payload . stats . total , today = payload . stats . today ;
document . querySelector ( "#drawer-title" ) . textContent = ` ${ key . name } · 统计 ` ;
document . querySelector ( "#drawer-subtitle" ) . textContent = "历史统计永久保留" ;
@@ -999,6 +884,10 @@
document . querySelectorAll ( ".long-field" ) . forEach ( node => node . classList . toggle ( "hidden" , ! enabled ) ) ;
}
function syncLongSectionVisibility ( hasLong = document . querySelector ( "#long-enabled" ) . checked ) {
document . querySelector ( "#long-price-section" ) . classList . toggle ( "hidden" , ! managementAuthorized && ! hasLong ) ;
}
function setView ( view ) {
document . querySelectorAll ( ".nav-button" ) . forEach ( button => button . classList . toggle ( "active" , button . dataset . view === view ) ) ;
document . querySelectorAll ( ".view" ) . forEach ( panel => panel . classList . toggle ( "active" , panel . id === "view-" + view ) ) ;
@@ -1008,25 +897,21 @@
if ( view === "keys" ) loadKeyManagement ( ) ;
}
function setMode ( mode ) {
testMode = mode === "demo" ;
document . body . classList . toggle ( "demo-mode" , testMode ) ;
document . querySelectorAll ( ".mode-button" ) . forEach ( button => button . classList . toggle ( "active" , button . dataset . mode === mode ) ) ;
document . querySelector ( "#demo-banner" ) . classList . toggle ( "hidden" , ! testMode ) ;
keyInput . disabled = testMode ;
keyInput . placeholder = testMode ? "测试模式无需密钥" : "输入管理密钥" ;
lastSignature = "" ; currentPage = 1 ; currentPagination = { page : 1 , page _size : PAGE _SIZE , total : 0 , total _pages : 0 , previous _cursor : "" , next _cursor : "" } ; closeDrawer ( ) ;
currentRecords = [ ] ; managedKeys = [ ] ; upstreamAccounts = [ ] ; currentPrices = [ ] ; currentCatalog = null ;
usageDashboard = { today : { requests : 0 , total _tokens : 0 , cost _usd : 0 } , users : [ ] , days : [ ] } ;
render ( [ ] ) ; renderManagedKeys ( ) ; renderPrices ( ) ; renderCatalogResults ( [ ] ) ; catalogSummaryNode . textContent = "" ;
try { sessionStorage . setItem ( "billing:data-mode" , mode ) ; } catch ( _ ) { }
function reloadCurrentView ( ) {
currentPage = 1 ; currentPagination = { page : 1 , page _size : PAGE _SIZE , total : 0 , total _pages : 0 , previous _cursor : "" , next _cursor : "" } ; closeDrawer ( ) ;
const active = document . querySelector ( ".nav-button.active" ) ? . dataset . view || "keys" ;
if ( active === "keys" ) loadKeyManagement ( ) ;
if ( active === "usage" ) { loadUsageFilterOptions ( ) ; load ( true , 1 ) ; }
if ( active === "pricing" ) loadPrices ( ) ;
}
function clearPriceForm ( ) {
function showPriceEditor ( show ) {
priceEditorContentNode . classList . toggle ( "hidden" , ! show ) ;
document . querySelector ( "#delete-price" ) . classList . toggle ( "hidden" , ! selectedPriceModel ) ;
}
function clearPriceForm ( showEditor = true ) {
selectedPriceModel = "" ;
[ "price-model" , "price-input" , "price-cache-read" , "price-cache-write" , "price-output" , "long-threshold" , "long-input" , "long-cache-read" , "long-cache-write" , "long-output" ] . forEach ( id => document . querySelector ( "#" + id ) . value = "" ) ;
document . querySelector ( "#long-enabled" ) . checked = false ;
document . querySelector ( "#long-comparison" ) . value = "gt" ;
@@ -1034,6 +919,9 @@
document . querySelector ( "#fast-multiplier" ) . value = "2.5" ;
showPriceSource ( { kind : "manual" } ) ;
setLongFields ( false ) ;
syncLongSectionVisibility ( false ) ;
showPriceEditor ( showEditor ) ;
renderPrices ( ) ;
}
function priceSourceText ( source ) {
@@ -1047,6 +935,7 @@
}
function fillPriceForm ( price ) {
selectedPriceModel = price . model ;
document . querySelector ( "#price-model" ) . value = price . model ;
document . querySelector ( "#price-input" ) . value = price . base . input _per _1m ;
document . querySelector ( "#price-cache-read" ) . value = price . base . cache _read _per _1m ;
@@ -1064,6 +953,9 @@
document . querySelector ( "#fast-multiplier" ) . value = price . fast _multiplier || "2.5" ;
showPriceSource ( price . source || { kind : "manual" } ) ;
setLongFields ( Boolean ( long ) ) ;
syncLongSectionVisibility ( Boolean ( long ) ) ;
showPriceEditor ( true ) ;
document . querySelectorAll ( ".price-row" ) . forEach ( row => row . classList . toggle ( "selected" , row . dataset . model === selectedPriceModel ) ) ;
}
function pricePayload ( ) {
@@ -1090,7 +982,10 @@
}
priceListNode . replaceChildren ( ... currentPrices . map ( price => {
const row = document . createElement ( "div" ) ;
row . className = "price-row" ;
row . className = "price-row" + ( price . model === selectedPriceModel ? " selected" : "" ) ;
row . dataset . model = price . model ;
row . tabIndex = 0 ;
row . setAttribute ( "role" , "button" ) ;
const text = document . createElement ( "div" ) ;
const model = document . createElement ( "div" ) ;
model . className = "price-name" ;
@@ -1098,44 +993,32 @@
const detail = document . createElement ( "small" ) ;
detail . textContent = ` 输入 ${ price . base . input _per _1m } · 输出 ${ price . base . output _per _1m } ${ price . long _context ? " · 长上下文" : "" } ${ price . fast _pricing _enabled ? " · Fast × " + price . fast _multiplier : "" } · ${ priceSourceText ( price . source ) } ` ;
text . append ( model , detail ) ;
const actions = document . createElement ( "div" ) ; actions . className = "key-actions" ;
const edit = document . createElement ( "button" ) ;
edit . type = "button" ; edit . className = "small" ; edit . textContent = "编辑" ; edit . addEventListener ( "click" , ( ) => fillPriceForm ( price ) ) ;
const remove = document . createElement ( "button" ) ;
remove . type = "button" ; remove . className = "small danger" ; remove . textContent = "删除" ; remove . addEventListener ( "click" , ( ) => deletePrice ( price . model ) ) ;
actions . append ( edit , remove ) ; row . append ( text , actions ) ; return row ;
row . addEventListener ( "click" , ( ) => fillPriceForm ( price ) ) ;
row . addEventListener ( "keydown" , event => { if ( event . key === "Enter" || event . key === " " ) { event . preventDefault ( ) ; fillPriceForm ( price ) ; } } ) ;
row . append ( text ) ; return row ;
} ) ) ;
}
async function loadPrices ( ) {
if ( testMode ) {
currentPrices = demoState . prices . map ( price => structuredClone ( price ) ) ; renderPrices ( ) ;
priceStatusNode . textContent = ` 测试数据 · 已配置 ${ currentPrices . length } 个模型 ` ; await loadCatalogStatus ( ) ; return ;
}
if ( ! keyInput . value . trim ( ) ) return ;
try {
const response = await fetch ( PRICE _API , { headers : authHeaders ( ) } ) ;
const payload = await response . json ( ) ;
if ( ! response . ok ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
const payload = await dataFetch ( PRICE _API ) ;
currentPrices = payload . prices || [ ] ;
renderPrices ( ) ;
const selected = currentPrices . find ( price => price . model === selectedPriceModel ) ;
if ( selected ) fillPriceForm ( selected ) ;
else if ( selectedPriceModel ) clearPriceForm ( false ) ;
else showPriceEditor ( false ) ;
priceStatusNode . textContent = currentPrices . length ? ` 已配置 ${ currentPrices . length } 个模型 ` : "尚未配置模型价格" ;
await loadCatalogStatus ( ) ;
if ( managementAuthorized ) await loadCatalogStatus ( ) ;
else { currentCatalog = null ; catalogSummaryNode . textContent = "" ; renderCatalogResults ( [ ] ) ; }
} catch ( error ) { priceStatusNode . textContent = "读取价格失败: " + error . message ; }
}
async function savePrice ( ) {
try {
if ( testMode ) {
const payload = pricePayload ( ) ; const index = demoState . prices . findIndex ( price => price . model === payload . model ) ;
if ( ! payload . model ) throw new Error ( "请输入模型" ) ;
payload . source = { kind : "manual" } ;
if ( index >= 0 ) demoState . prices [ index ] = payload ; else demoState . prices . push ( payload ) ;
saveDemoState ( ) ; priceStatusNode . textContent = "测试价格已保存" ; await loadPrices ( ) ; showPriceSource ( { kind : "manual" } ) ; return ;
}
const response = await fetch ( PRICE _API , { method : "PUT" , headers : authHeaders ( true ) , body : JSON . stringify ( pricePayload ( ) ) } ) ;
const payload = await response . json ( ) ;
if ( ! response . ok ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
const payload = pricePayload ( ) ;
selectedPriceModel = payload . model ;
await managedFetch ( PRICE _API , { method : "PUT" , body : JSON . stringify ( payload ) } ) ;
priceStatusNode . textContent = "价格已保存" ;
await loadPrices ( ) ;
showPriceSource ( { kind : "manual" } ) ;
@@ -1152,14 +1035,14 @@
const text = document . createElement ( "div" ) ;
const title = document . createElement ( "strong" ) ; title . textContent = ` ${ entry . provider _name || entry . provider } · ${ entry . model _name || entry . model } ` ;
const detail = document . createElement ( "small" ) ; detail . textContent = ` ${ entry . id } · 输入 ${ entry . base . input _per _1m } / 输出 ${ entry . base . output _per _1m } ${ entry . long _context ? " · 长上下文" : "" } ` ;
const use = document . createElement ( "button" ) ; use . type = "button" ; use . className = "small" ; use . textContent = "使用" ; use . addEventListener ( "click" , ( ) => importCatalogPrice ( entry ) ) ;
text . append ( title , detail ) ; row . append ( text , use ) ; return row ;
text . append ( title , detail ) ; row . append ( text ) ;
if ( managementAuthorized ) { const use = document . createElement ( "button" ) ; use . type = "button" ; use . className = "small admin-only" ; use . textContent = "使用" ; use . addEventListener ( "click" , ( ) => importCatalogPrice ( entry ) ) ; row . append ( use ) ; }
return row ;
} ) ) ;
}
function catalogSummary ( payload ) {
currentCatalog = payload ? . catalog || null ;
if ( testMode ) return "测试目录 · 不访问 models.dev" ;
if ( ! payload ? . loaded ) return payload ? . last _error ? ` 尚无可用目录 · ${ payload . last _error } ` : "尚未下载目录" ;
const updated = currentCatalog ? . fetched _at ? new Date ( currentCatalog . fetched _at ) . toLocaleString ( ) : "-" ;
return ` ${ currentCatalog ? . models || 0 } 条参考价格 · ${ updated } ` ;
@@ -1168,14 +1051,7 @@
async function searchCatalog ( ) {
const query = document . querySelector ( "#catalog-query" ) . value . trim ( ) . toLowerCase ( ) ;
try {
if ( testMode ) {
const models = query ? demoCatalog ( ) . filter ( entry => [ entry . id , entry . provider , entry . provider _name , entry . model , entry . model _name ] . some ( value => value . toLowerCase ( ) . includes ( query ) ) ) : [ ] ;
currentCatalog = { revision : "demo-v1" , models : demoCatalog ( ) . length , fetched _at : new Date ( ) . toISOString ( ) } ;
catalogSummaryNode . textContent = "测试目录 · 不访问 models.dev" ;
renderCatalogResults ( models ) ; return ;
}
if ( ! keyInput . value . trim ( ) ) { catalogSummaryNode . textContent = "输入管理密钥后读取目录" ; return ; }
const payload = await managedFetch ( CATALOG _API + "?q=" + encodeURIComponent ( query ) ) ;
const payload = await dataFetch ( CATALOG _API + "?q=" + encodeURIComponent ( query ) ) ;
catalogSummaryNode . textContent = catalogSummary ( payload ) ;
renderCatalogResults ( payload . models || [ ] ) ;
} catch ( error ) { catalogSummaryNode . textContent = "读取参考价格失败:" + error . message ; renderCatalogResults ( [ ] ) ; }
@@ -1189,28 +1065,13 @@
const localModel = document . querySelector ( "#price-model" ) . value . trim ( ) || entry . model ;
if ( ! localModel ) { priceStatusNode . textContent = "请先输入本地模型名称" ; return ; }
try {
let imported ;
if ( testMode ) {
const existing = demoState . prices . find ( price => price . model . toLowerCase ( ) === localModel . toLowerCase ( ) ) ;
imported = {
model : localModel , base : structuredClone ( entry . base ) , long _context : entry . long _context ? structuredClone ( entry . long _context ) : undefined ,
fast _pricing _enabled : existing ? . fast _pricing _enabled || false , fast _multiplier : existing ? . fast _multiplier || "2.5" ,
source : { kind : "models.dev" , catalog _id : entry . id , provider : entry . provider , model : entry . model , revision : "demo-v1" , fetched _at : new Date ( ) . toISOString ( ) }
} ;
const index = demoState . prices . findIndex ( price => price . model . toLowerCase ( ) === localModel . toLowerCase ( ) ) ;
if ( index >= 0 ) demoState . prices [ index ] = imported ; else demoState . prices . push ( imported ) ;
saveDemoState ( ) ;
} else {
imported = await managedFetch ( PRICE _IMPORT _API , { method : "POST" , body : JSON . stringify ( { model : localModel , catalog _id : entry . id } ) } ) ;
}
const imported = await managedFetch ( PRICE _IMPORT _API , { method : "POST" , body : JSON . stringify ( { model : localModel , catalog _id : entry . id } ) } ) ;
await loadPrices ( ) ; fillPriceForm ( imported ) ; priceStatusNode . textContent = ` ${ localModel } 已采用 ${ entry . provider _name || entry . provider } / ${ entry . model } 参考价 ` ;
} catch ( error ) { priceStatusNode . textContent = "导入参考价格失败:" + error . message ; }
}
async function refreshCatalog ( ) {
try {
if ( testMode ) { catalogSummaryNode . textContent = "测试目录已刷新 · 未访问 models.dev" ; return ; }
if ( ! keyInput . value . trim ( ) ) throw new Error ( "请输入管理密钥" ) ;
catalogSummaryNode . textContent = "正在更新 models.dev 目录" ;
const preview = await managedFetch ( CATALOG _REFRESH _API , { method : "POST" } ) ;
currentCatalog = preview . catalog ;
@@ -1231,46 +1092,25 @@
async function deletePrice ( model ) {
try {
if ( testMode ) {
demoState . prices = demoState . prices . filter ( price => price . model !== model ) ; saveDemoState ( ) ;
if ( document . querySelector ( "#price-model" ) . value . trim ( ) === model ) clearPriceForm ( ) ; await loadPrices ( ) ; return ;
}
if ( ! confirm ( ` 删除 ${ model } 的真实价格配置? ` ) ) return ;
const response = await fetch ( PRICE _API , { method : "DELETE" , headers : authHeaders ( true ) , body : JSON . stringify ( { model } ) } ) ;
const payload = await response . json ( ) ;
if ( ! response . ok ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
if ( document . querySelector ( "#price-model" ) . value . trim ( ) === model ) clearPriceForm ( ) ;
await managedFetch ( PRICE _API , { method : "DELETE" , body : JSON . stringify ( { model } ) } ) ;
if ( selectedPriceModel === model ) clearPriceForm ( false ) ;
await loadPrices ( ) ;
} catch ( error ) { priceStatusNode . textContent = "删除失败: " + error . message ; }
}
async function load ( manual = false , page = currentPage , cursor = "" ) {
if ( loading ) return ;
if ( testMode ) {
const payload = demoUsagePage ( page ) ;
currentPagination = payload . pagination ;
render ( payload . records ) ;
statusNode . textContent = "" ;
return ;
}
const key = keyInput . value . trim ( ) ;
if ( ! key ) {
statusNode . textContent = "请输入管理密钥" ;
return ;
}
loading = true ;
if ( manual ) statusNode . textContent = "正在读取" ;
try {
const params = usageFilterQuery ( ) ;
params . set ( "page" , String ( page || 1 ) ) ;
params . set ( "page_size" , String ( PAGE _SIZE ) ) ;
if ( cursor && ! cursor . startsWith ( "demo-" ) ) params . set ( "cursor" , cursor ) ;
const response = await fetch ( API + "?" + params . toString ( ) , { headers : authHeaders ( ) } ) ;
const payload = await response . json ( ) ;
if ( ! response . ok ) throw new Error ( payload ? . error ? . message || "HTTP " + response . status ) ;
if ( cursor ) params . set ( "cursor" , cursor ) ;
const payload = await dataFetch ( API + "?" + params . toString ( ) ) ;
currentPagination = payload . pagination || { page : 1 , page _size : PAGE _SIZE , total : 0 , total _pages : 0 } ;
const records = payload . records || [ ] ;
lastSignature = JSON . stringify ( payload ) ;
render ( records ) ;
statusNode . textContent = "" ;
} catch ( error ) {
@@ -1281,7 +1121,8 @@
}
keyInput . value = storedPanelKey ( ) || sessionStorage . getItem ( "billing:management-key" ) || "" ;
keyInput . addEventListener ( "change" , ( ) => sessionStorage . setItem ( "billing:management-key" , keyInput . value . trim ( ) ) ) ;
keyInput . addEventListener ( "change" , ( ) => { sessionStorage . setItem ( "billing:management-key" , keyInput . value . trim ( ) ) ; setAccessState ( false ) ; reloadCurrentView ( ) ; } ) ;
keyInput . addEventListener ( "keydown" , event => { if ( event . key === "Enter" ) { event . preventDefault ( ) ; keyInput . blur ( ) ; } } ) ;
document . querySelector ( "#refresh" ) . addEventListener ( "click" , ( ) => load ( true , currentPage ) ) ;
document . querySelector ( "#apply-usage-filters" ) . addEventListener ( "click" , ( ) => { currentPage = 1 ; load ( true , 1 ) ; } ) ;
document . querySelector ( "#clear-usage-filters" ) . addEventListener ( "click" , ( ) => { [ "usage-from" , "usage-to" , "usage-model" , "usage-request-id" ] . forEach ( id => document . querySelector ( "#" + id ) . value = "" ) ; [ "usage-key" , "usage-result" , "usage-auth" , "usage-endpoint" ] . forEach ( id => document . querySelector ( "#" + id ) . value = "" ) ; currentPage = 1 ; load ( true , 1 ) ; } ) ;
@@ -1290,12 +1131,11 @@
document . querySelector ( "#save-price" ) . addEventListener ( "click" , savePrice ) ;
document . querySelector ( "#clear-price" ) . addEventListener ( "click" , clearPriceForm ) ;
document . querySelector ( "#new-price" ) . addEventListener ( "click" , clearPriceForm ) ;
document . querySelector ( "#delete-price" ) . addEventListener ( "click" , ( ) => selectedPriceModel && deletePrice ( selectedPriceModel ) ) ;
document . querySelector ( "#search-catalog" ) . addEventListener ( "click" , searchCatalog ) ;
document . querySelector ( "#catalog-query" ) . addEventListener ( "keydown" , event => { if ( event . key === "Enter" ) { event . preventDefault ( ) ; searchCatalog ( ) ; } } ) ;
document . querySelector ( "#refresh-catalog" ) . addEventListener ( "click" , refreshCatalog ) ;
document . querySelector ( "#create-key" ) . addEventListener ( "click" , ( ) => openKeyEditor ( ) ) ;
document . querySelector ( "#reload-keys" ) . addEventListener ( "click" , loadKeyManagement ) ;
document . querySelector ( "#include-archived" ) . addEventListener ( "change" , loadKeyManagement ) ;
document . querySelector ( "#close-drawer" ) . addEventListener ( "click" , closeDrawer ) ;
document . querySelector ( "#cancel-editor" ) . addEventListener ( "click" , closeDrawer ) ;
drawerOverlayNode . addEventListener ( "click" , closeDrawer ) ;
@@ -1306,14 +1146,12 @@
document . querySelector ( "#editor-reset-period" ) . addEventListener ( "change" , syncEditorRoute ) ;
document . querySelector ( "#reset-billing" ) . addEventListener ( "click" , resetManagedBilling ) ;
document . querySelectorAll ( ".nav-button" ) . forEach ( button => button . addEventListener ( "click" , ( ) => setView ( button . dataset . view ) ) ) ;
document . querySelectorAll ( ".mode-button" ) . forEach ( button => button . addEventListener ( "click" , ( ) => setMode ( button . dataset . mode ) ) ) ;
document . querySelector ( "#reset-demo" ) . addEventListener ( "click" , ( ) => { demoState = freshDemoState ( ) ; saveDemoState ( ) ; currentPage = 1 ; setMode ( "demo" ) ; } ) ;
document . addEventListener ( "keydown" , event => { if ( event . key === "Escape" ) closeDrawer ( ) ; } ) ;
document . addEventListener ( "visibilitychange" , ( ) => { if ( ! testMode && currentPage === 1 && ! document . hidden && document . querySelector ( '[data-view="usage"]' ) . classList . contains ( "active" ) ) load ( false , 1 ) ; } ) ;
document . addEventListener ( "visibilitychange" , ( ) => { if ( currentPage === 1 && ! document . hidden && document . querySelector ( '[data-view="usage"]' ) . classList . contains ( "active" ) ) load ( false , 1 ) ; } ) ;
renderColumnControls ( ) ;
setMode ( sessionStorage . getItem ( "billing:data-mode" ) || "live" ) ;
setAccessState ( false ) ;
setView ( sessionStorage . getItem ( "billing:active-view" ) || "keys" ) ;
setInterval ( ( ) => { if ( ! testMode && currentPage === 1 && ! document . hidden && document . querySelector ( '[data-view="usage"]' ) . classList . contains ( "active" ) ) load ( false , 1 ) ; } , 3000 ) ;
setInterval ( ( ) => { if ( currentPage === 1 && ! document . hidden && document . querySelector ( '[data-view="usage"]' ) . classList . contains ( "active" ) ) load ( false , 1 ) ; } , 3000 ) ;
< / script >
< / body >
< / html >