Compare commits

..

26 Commits

Author SHA1 Message Date
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00
kdletters 18af49c27f 收紧编辑器媒体引用契约
禁止当前画布与外部编辑器接口接收 Data URL 和 Blob URL
生成请求统一先上传 OSS 并使用稳定资源引用
角色动画和视频签名前校验资源归属并支持资源与素材 ID
同步前后端 256KB 引用限制及相关文档测试
2026-07-17 16:44:01 +08:00
kdletters 93b27a2307 修复图集拆分运行时身份传递
在 procedure 外层捕获调用 identity 并显式传入事务
补齐素材归组持久表的迁移导入导出清单
修正 DDD 门禁注释解析并补齐编辑器 Agent 事件落位
同步后端架构约束与项目排障记忆
2026-07-17 14:45:11 +08:00
kdletters 8febb799f3 升级SpacetimeDB至2.6.1
统一Rust crates、CLI、容器镜像和生产provision版本
使用2.6.1 CLI重新生成Rust bindings
更新运行检查、架构运维文档和项目共享记忆
2026-07-17 13:44:12 +08:00
kdletters ed1a972c9e 禁止快速编辑任务中继续框选
快速编辑进入生成中后移除框选工具和覆盖层
新增挂起请求回归测试覆盖任务开始后的锁定状态
更新图片画布快速编辑项目记忆与验证口径
2026-07-16 22:23:29 +08:00
kdletters a0242e3547 修复图集快速编辑模型路由
快速编辑请求补齐模型比例和清晰度参数
后端按模型分流nanobanana与GPT图片编辑协议
兼容旧队列参数并收紧图片编辑计费边界
补充图集回归测试并同步OpenAPI与编辑器文档
2026-07-16 22:08:05 +08:00
kdletters 77e931ed96 修复外部编辑器接口测试格式门禁
调整图片生成警告断言的 rustfmt 换行格式。
2026-07-16 21:07:10 +08:00
kdletters 0253a35e72 补齐归档清单上传限速
让已有全量归档的 manifest 继承同一上传带宽限制器
收紧并发上传共享累计带宽预算测试
2026-07-16 19:02:42 +08:00
kdletters 947f59c894 限制数据库备份上传带宽
新增共享上传带宽限制器并覆盖数据对象、分片、catalog 与 latest pointer
补齐并发聚合限速、源流错误传播和大型 manifest 分块测试
更新备份环境变量示例与生产运维文档
2026-07-16 19:00:35 +08:00
lhk229 aecabdacfd 新抠图算法 (#85)
Co-authored-by: 段舒康 <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/85
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-07-16 18:33:06 +08:00
kdletters 94014392ed 优化逐文件小对象上传
小于等于16MiB的CAS对象改用单次PUT并执行HEAD验真
大对象继续保留multipart上传与失败清理
补齐单次PUT请求路径测试和运维说明
2026-07-16 18:28:04 +08:00
kdletters 40684aafcd 优化逐文件备份并发上传
增加files对象PUT和HEAD受控并发
限制并发范围并降低大目录进度日志量
补齐并发上限测试、环境示例与运维文档
2026-07-16 18:18:26 +08:00
kdletters 56d70bf720 补齐逐文件备份符号链接恢复
记录data-dir内部相对符号链接并拒绝绝对或越界目标
恢复full catalog时安全重建符号链接
补齐备份测试与生产运维文档
2026-07-16 18:16:38 +08:00
kdletters 9ae3f53989 完善SpacetimeDB逐文件增量备份
新增data-dir逐文件CAS基线、history归档与安全清理
发布并校验OSS latest pointer,支持异机自动恢复
接入dev和release可选的files-history定时备份profile
补齐备份测试、生产门禁、环境示例与运维文档
2026-07-16 18:00:06 +08:00
kdletters b303605bf2 新增后台新增用户付费率
扩展 Dashboard 聚合与契约,统计筛选期新增用户的真实付费转化
生成 SpacetimeDB typed bindings 并补充后端聚合测试
后台新增付费率卡片与响应式布局,覆盖零分母和时段文案
同步 Dashboard 运营口径与项目决策记录
2026-07-15 22:14:52 +08:00
kdletters ea8d0ef064 修复泥点余额自动刷新
顶部泥点总余额优先展示实时钱包余额,避免缓存明细覆盖刷新结果。

补充缓存明细场景的余额变更回归测试。

同步主站与图片画布的泥点余额数据口径文档。
2026-07-15 22:01:25 +08:00
kdletters cfdba640ee 修复图集快速编辑后端校验与错误提示
后端允许完整图标图集进入图片快速编辑,仅继续禁止单个图标。

快速编辑失败时向红色错误框传递具体错误文案。

补充前后端回归测试并同步画布编辑器技术文档。
2026-07-15 21:11:46 +08:00
kdletters 271b153308 统一Rust格式化工具链与构建门禁
固定Rust 1.96.0与rustfmt格式配置

接入本地lint、Codex提交前检查和Jenkins构建门禁

提交server-rs全workspace一次性格式化基线

同步开发运维文档与production-ops防回退检查
2026-07-15 21:06:05 +08:00
kdletters 25f6027c4a 补齐外部素材创建归组字段
为 External Editor 素材创建补充归组 Task ID 默认值

为 External Editor 素材创建补充拆分预期数量默认值
2026-07-15 20:34:07 +08:00
kdletters 793ec1b6b0 修复后台素材查询分页与任务归组
支持陶泥号解析、稳定游标和后台素材多页查询

补齐查询防抖、请求取消、刷新保留与加载更多状态

新增可信来源索引和拆分批次完成事实,修复跨项目与删除切片归组

同步生成绑定、数据契约、项目记忆和回归测试
2026-07-15 20:02:50 +08:00
kdletters 8d1c640eb5 修复精选图片读取与审核返还
为已通过、已展示且返还完成的精选资产补充精确匿名读取授权

将精选批准、泥点返还和返还完成标记收进同一SpacetimeDB事务

保留确定性返还流水的幂等语义并支持修复历史半完成记录

补充精选公开门禁测试并同步后端与创作主页文档
2026-07-15 16:58:18 +08:00
kdletters 7ed99d7353 拆分SpacetimeDB调用池与缓存读连接
将HTTP调用池改为无订阅连接并增加单一共享缓存读连接

统一缓存重建总超时并拒绝发布断线连接

补充连接拓扑回归测试与生产运维说明
2026-07-15 15:11:11 +08:00
kdletters 2f092c3566 统一素材任务层级与成本归因
后台素材查询按任务分页并将中间产物逐行收纳为子项
生成原始产物沿用真实素材类型并承载任务生成成本
抠图、拆分和逐帧后处理记录实际生成器且阶段成本归零
补齐历史类型映射、角色动作链路、测试与文档
2026-07-15 03:04:41 +08:00
kdletters 3f3a6c4ab2 修复画布生图交付与素材成本展示
修复 nanobanana 标量尺寸被误判为宽高的问题。

普通生图和快速编辑在内存完成尺寸变换后仅上传一次,变换失败保留 provider 原图。

移除快速编辑重复的原始输出素材,保留角色、图集和 UI 的独立中间产物。

后台将不重复计费的中间产物显示为不单独计费。

补充单次上传、降级交付和成本展示回归测试及相关文档。
2026-07-15 01:36:16 +08:00
kdletters 5c1434511b 修复后台看板留存统计与素材详情布局
修正周月日期范围、每日访问人数、零值柱和趋势图滚动同步
新增次日与七日留存及数据库侧精确聚合,移除五万行访问明细截断
聚合失败时拒绝返回伪零值并补齐契约、客户端绑定和定向测试
修复素材详情陶泥号竖排并完善移动端单列布局
同步运营口径与本地数据库版本漂移排障文档
2026-07-15 01:25:29 +08:00
kdletters 9807295a7d 调整画布生成输入与图集快速编辑
隐藏生成面板资源名称字段并恢复提示词输入边框

恢复图标图集快速编辑并仅对单图标禁用

补充工具栏右键菜单工作流回归测试与编辑器文档
2026-07-14 23:26:35 +08:00
422 changed files with 15550 additions and 4770 deletions
-17
View File
@@ -1,17 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(netstat -ano)",
"Bash(npm run:*)",
"Bash(findstr :8081)",
"Bash(taskkill:*)",
"Bash(findstr LISTENING)",
"Bash(npx tsc:*)",
"Bash(lsof -ti:8081)",
"Bash(curl -s http://localhost:8081/health)",
"Skill(code-review)",
"Skill(code-review:*)"
]
}
}
+31 -6
View File
@@ -14,20 +14,40 @@ if (hookInput && !isGitCommitCommand(extractShellCommand(hookInput))) {
}
const validationSteps = [
{
label: 'Rust format check',
command: npmCommand,
args:
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run check:rustfmt']
: ['run', 'check:rustfmt'],
},
{
label: 'TypeScript typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run typecheck'] : ['run', 'typecheck'],
args:
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run typecheck']
: ['run', 'typecheck'],
},
{
label: 'Admin web typecheck',
command: npmCommand,
args: process.platform === 'win32' ? ['/d', '/s', '/c', 'npm run admin-web:typecheck'] : ['run', 'admin-web:typecheck'],
args:
process.platform === 'win32'
? ['/d', '/s', '/c', 'npm run admin-web:typecheck']
: ['run', 'admin-web:typecheck'],
},
{
label: 'Rust api-server compile check',
command: 'cargo',
args: ['check', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'],
args: [
'check',
'-p',
'api-server',
'--manifest-path',
'server-rs/Cargo.toml',
],
},
];
@@ -66,7 +86,9 @@ function runStep(step) {
}
if (result.error) {
console.error(`[codex-hook] ${step.label} 启动失败:${result.error.message}`);
console.error(
`[codex-hook] ${step.label} 启动失败:${result.error.message}`,
);
return { ok: false, status: 1 };
}
@@ -104,12 +126,15 @@ function extractShellCommand(input) {
input?.command,
];
const command = candidates.find(value => typeof value === 'string' && value.trim().length > 0);
const command = candidates.find(
(value) => typeof value === 'string' && value.trim().length > 0,
);
if (command) {
return command;
}
const shellCommand = input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
const shellCommand =
input?.tool_input?.cmd ?? input?.toolInput?.cmd ?? input?.arguments?.cmd;
if (Array.isArray(shellCommand)) {
return shellCommand.join(' ');
}
@@ -318,6 +318,14 @@ For image edit/redraw that should replace an existing canvas layer, pass `projec
For sound effects and BGM, `assetFolderId` and `assetLabel` can write the generated audio to the account asset library, same as image/video generation.
## Successful Responses with Warnings
Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction can return HTTP 2xx with an optional structured `warning`. A 2xx response means the task completed, but it does not guarantee that every requested post-processed derivative exists.
- Apply the returned `project` and media snapshots before interpreting optional derivatives: character responses use `resource` / `asset`, while icon spritesheet and UI extraction responses use `spritesheetResource` / `spritesheetAsset`. When `warning.code` is `postprocess-failed-source-preserved`, the saved provider source image is the authoritative main result. Character output has no transparent derivative; icon spritesheet and UI extraction output have neither a transparent spritesheet nor slices. Display `warning.reason` directly, and do not synthesize missing derivatives or restart generation.
- `sliceWarning` is a separate condition used only when transparent spritesheet post-processing succeeded but automatic slicing failed. Keep `sliceWarning.reason` as the original diagnostic and continue using the complete transparent spritesheet; a UI may add context when displaying it, but must not rewrite the stored reason.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. As defensive handling for a malformed response containing both, treat the general `warning` as authoritative and do not misclassify the source-preserved result as a slicing-only warning.
## Guardrails
- Do not invent endpoints outside the OpenAPI, especially internal worker or runtime task-list routes.
@@ -78,6 +78,14 @@ Ask a follow-up only when two routes could both be correct and produce different
All generation requests should be placed into both the current canvas and its same-name asset-library folder. For endpoints that support `assetLabel`, pass it. For UI extraction, use `spritesheetLabel`. For icon spritesheet, the folder is enough. For character animation, the endpoint does not return `asset`; after success call `POST /api/external/v1/editor/assets` using the first returned frame as `imageSrc`, the session `assetFolderId`, and `assetKind: "character-animation"`.
## HTTP 2xx Warning Handling
Character image generation (including character redraw through `kind: "character"`), icon spritesheet generation, and UI asset extraction may return HTTP 2xx while carrying a structured `warning`; completion does not imply that all post-processed derivatives exist.
- Consume the returned `project` and media snapshots as authoritative: character responses use `resource` / `asset`, while icon spritesheet and UI extraction responses use `spritesheetResource` / `spritesheetAsset`. `warning.code: "postprocess-failed-source-preserved"` means the saved provider source is the main result. Character output has no transparent derivative, while icon spritesheet and UI extraction have no transparent spritesheet and no slices. Display `warning.reason` directly; do not construct missing assets or retry the provider generation from scratch.
- `sliceWarning` is only for a transparent spritesheet that was created successfully but could not be split automatically. Use the complete transparent spritesheet and preserve `sliceWarning.reason` as the original diagnostic; it is not a post-processing/source-preserved warning.
- The service contract keeps `warning` and `sliceWarning` mutually exclusive. If a malformed response contains both, prioritize the general `warning` over `sliceWarning` defensively.
## Reference Image Upload
If the user provides a local file as a reference image, run upload before the generation request:
+2 -1
View File
@@ -78,7 +78,7 @@ Best practices:
- Avoid overlapping queries that duplicate row delivery.
- Use indexes for subscribed filters.
## 2.2.0 to 2.6.0 Delta
## 2.2.0 to 2.6.1 Delta
Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
@@ -88,6 +88,7 @@ Genarrative introduced SpacetimeDB around 2.2.0. Important changes since then:
- **2.4.1**: Rust and TypeScript procedural views can declare primary keys, enabling `OnUpdate` events for subscribed views; fixed index schema from ST tables.
- **2.5.0**: procedures are stable, C# procedural views gain primary keys, event tables allow broader layout-altering automigrations, BTreeSet storage makes row insertion deterministic and avoids accidentally quadratic bulk insert behavior, `wasm_memory_bytes` billing metric semantics changed, template version constraints unified, `publish --delete-data` config fallback fixed, CLI `call` accepts hex Identity arguments.
- **2.6.0**: procedural-view primary keys are available across Rust, TypeScript, and C#, commitlog gains `max_segment_size` / `write_buffer_size` / `preallocate_segments`, the default write buffer increases for throughput, event-table automigrations improve, and CLI binary distribution expands.
- **2.6.1**: procedure contexts again receive the caller `Identity` and `ConnectionId`; generated TypeScript `Option<T>` fields use optional keys; `spacetime init --template` lists available templates when no template argument is supplied.
## Debugging Checklist
+2
View File
@@ -29,6 +29,7 @@ GENARRATIVE_LLM_PROVIDER="ark"
GENARRATIVE_LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3"
GENARRATIVE_LLM_API_KEY="eb750614-e0b5-402a-bfea-4224862d251e"
GENARRATIVE_LLM_MODEL="doubao-1-5-pro-32k-character-250715"
GENARRATIVE_EDITOR_BGFILTER_BASE_URL="https://u1082648-b442-cd409e05.westx.seetacloud.com:8443"
APIMART_BASE_URL="https://api.apimart.ai/v1"
APIMART_API_KEY=""
APIMART_IMAGE_REQUEST_TIMEOUT_MS=180000
@@ -36,6 +37,7 @@ DASHSCOPE_SCENE_IMAGE_MODEL="wan2.2-t2i-flash"
DASHSCOPE_REFERENCE_IMAGE_MODEL="qwen-image-2.0"
DASHSCOPE_COVER_IMAGE_MODEL="wan2.2-t2i-flash"
ARK_CHARACTER_VIDEO_REQUEST_TIMEOUT_MS=420000
# 启用服务端大模型调试日志(记录所有输入输出)
LLM_DEBUG_LOG="true"
+52
View File
@@ -24,6 +24,40 @@ module.exports = {
'no-console': 'off',
},
},
{
files: ['**/scripts/**/*.{ts,js,mjs,cjs}'],
rules: {
'no-console': 'off',
},
},
{
files: ['**/*.test.{ts,tsx,js,mjs,cjs}'],
rules: {
'no-console': 'off',
'unused-imports/no-unused-vars': 'off',
},
},
{
files: ['miniprogram/**/*.js'],
globals: {
App: 'readonly',
},
},
{
files: [
'apps/admin-web/src/pages/*.tsx',
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
],
rules: {
'react-refresh/only-export-components': 'off',
},
},
{
files: ['src/components/platform-entry/PlatformEntryFlowShellImpl.tsx'],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: ['src/components/game-canvas/**/*.tsx'],
rules: {
@@ -61,7 +95,25 @@ module.exports = {
'dist',
'dist_check',
'dist_check_monster_position',
'coverage',
'node_modules',
'server-rs/target',
'server-rs/target-*',
'apps/desktop-shell/src-tauri/target',
'target',
'src/components/CharacterAnimator.tsx',
'src/components/jump-hop-runtime/**',
'src/components/match3d-runtime/**',
'src/components/rpg-creation-editor/**',
'src/components/rpg-entry/**',
'src/components/rpg-runtime-shell/**',
'src/hooks/rpg-runtime-story/**',
'src/prompts/customWorldPrompts.ts',
'src/services/ai.ts',
'src/services/miniGameDraftGenerationProgress.ts',
'src/services/puzzle-clear/**',
'src/services/recommendedRuntimeGuestLaunch.test.ts',
'src/data/sceneEncounterPreviews.ts',
'public/Icons',
'media',
'.codex-logs',
+2
View File
@@ -42,6 +42,7 @@ temp*build*/
/.app/
/target/
/logs
/.claude/settings.local.json
/.codegraph/
/.playwright-cli/
**/.playwright-cli/
@@ -50,6 +51,7 @@ temp*build*/
.worktrees/
.rag/
.env.secrets.local
nohup.out
spacetime.local.json
deploy/container/api-server.env
deploy/container/worker-smoke/
+5 -6
View File
@@ -98,21 +98,20 @@ npm run check:content
主运行时:
- [src/App.tsx](./src/App.tsx)
- [src/components/GameShell.tsx](./src/components/GameShell.tsx)
- [src/AuthenticatedApp.tsx](./src/AuthenticatedApp.tsx)
- [src/routing/appRoutes.tsx](./src/routing/appRoutes.tsx)
- [src/hooks/useCombatFlow.ts](./src/hooks/useCombatFlow.ts)
- [src/hooks/useStoryGeneration.ts](./src/hooks/useStoryGeneration.ts)
主流程内嵌编辑能力:
- [src/components/CustomWorldEntityEditorModal.tsx](./src/components/CustomWorldEntityEditorModal.tsx)
- [src/components/CustomWorldNpcVisualEditor.tsx](./src/components/CustomWorldNpcVisualEditor.tsx)
- [src/components/CustomWorldRoleAssetStudioModal.tsx](./src/components/CustomWorldRoleAssetStudioModal.tsx)
- [src/components/image-editor/ImageCanvasEditorView.tsx](./src/components/image-editor/ImageCanvasEditorView.tsx)
- [src/components/rpg-creation-editor/RpgCreationEntityEditorModal.tsx](./src/components/rpg-creation-editor/RpgCreationEntityEditorModal.tsx)
- [src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.tsx](./src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.tsx)
核心数据:
- [src/data/scenePresets.ts](./src/data/scenePresets.ts)
- [src/data/characterPresets.ts](./src/data/characterPresets.ts)
- [src/data/monsterPresets.ts](./src/data/monsterPresets.ts)
- [src/data/npcInteractions.ts](./src/data/npcInteractions.ts)
- [src/data/treasureInteractions.ts](./src/data/treasureInteractions.ts)
+15 -14
View File
@@ -2,23 +2,21 @@ import type {
AdminAccountListResponse,
AdminCreateAccountRequest,
AdminCreateAccountResponse,
AdminUpsertCreationEntryEventBannersRequest,
AdminUpsertCreationEntryTypeConfigRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
AdminCreationEntryConfigResponse,
AdminDashboardQuery,
AdminDashboardResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest,
AdminDatabaseTableListResponse,
AdminDatabaseTableRowsQuery,
AdminDatabaseTableRowsResponse,
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
AdminDebugHttpRequest,
AdminDebugHttpResponse,
AdminDirectUploadTicketPayload,
AdminDisableProfileRedeemCodeRequest,
AdminDisableProfileTaskConfigRequest,
AdminEditorAssetListQuery,
AdminEditorAssetListResponse,
AdminDirectUploadTicketPayload,
AdminEditorShowcaseAssetResponse,
AdminEditorShowcaseCampaignResponse,
AdminEditorShowcaseDisplayRequest,
@@ -37,14 +35,16 @@ import type {
AdminRechargeRefundPreviewRequest,
AdminRechargeRefundPreviewResponse,
AdminRechargeRefundRegisterRequest,
AdminTrackingEventListQuery,
AdminTrackingEventKeyListResponse,
AdminTrackingEventListQuery,
AdminTrackingEventListResponse,
AdminUpdateWorkVisibilityRequest,
AdminUpdateWorkVisibilityResponse,
AdminUpdateAccountRequest,
AdminUpdateAccountResponse,
AdminUpdateWorkVisibilityRequest,
AdminUpdateWorkVisibilityResponse,
AdminUploadedEditorShowcaseCampaignImage,
AdminUpsertCreationEntryEventBannersRequest,
AdminUpsertCreationEntryTypeConfigRequest,
AdminUpsertEditorShowcaseCampaignRequest,
AdminUpsertFeatureGateConfigRequest,
AdminUpsertProfileInviteCodeRequest,
@@ -53,11 +53,11 @@ import type {
AdminUpsertProfileTaskConfigRequest,
AdminUpsertProfileWalletConfigRequest,
AdminUpsertPublicWorkInteractionConfigRequest,
AdminWorkVisibilityListResponse,
AdminUserDetailQuery,
AdminUserDetailResponse,
AdminWalletRestrictionRequest,
AdminWalletRestrictionResponse,
AdminWorkVisibilityListResponse,
ApiErrorEnvelope,
ApiMeta,
ApiSuccessEnvelope,
@@ -398,10 +398,11 @@ export function getAdminAssetReadUrl(
export function listAdminEditorAssets(
token: string,
query: AdminEditorAssetListQuery = {},
signal?: AbortSignal,
) {
return request<AdminEditorAssetListResponse>(
`/admin/api/editor-assets${buildEditorAssetListQuery(query)}`,
{ token },
{ token, signal },
);
}
+20
View File
@@ -128,6 +128,9 @@ export interface AdminDashboardMetricsPayload {
consumedMudPoints: number;
totalRegisteredUsers: number;
newRegisteredUsers: number;
newUserPaymentConversion: AdminDashboardPaymentConversionPayload;
day1Retention: AdminDashboardRetentionMetricPayload;
day7Retention: AdminDashboardRetentionMetricPayload;
visitUsers: number;
totalVisitUsers: number;
visitCount: number;
@@ -135,6 +138,18 @@ export interface AdminDashboardMetricsPayload {
currentUsers: number;
}
export interface AdminDashboardPaymentConversionPayload {
paidUsers: number;
newRegisteredUsers: number;
rateBasisPoints: number;
}
export interface AdminDashboardRetentionMetricPayload {
eligibleUsers: number;
retainedUsers: number;
rateBasisPoints: number;
}
export interface AdminDashboardChartPayload {
id: string;
title: string;
@@ -452,6 +467,7 @@ export interface AdminEditorAssetPayload {
model?: string | null;
provider?: string | null;
taskId?: string | null;
groupTaskId?: string | null;
assetKind?: string | null;
generationInputs?: Record<string, unknown> | null;
sourceResourceId?: string | null;
@@ -459,6 +475,10 @@ export interface AdminEditorAssetPayload {
generationCostMudPoints: number;
createdAt: string;
updatedAt: string;
generator: string;
taskGenerator: string;
taskCostMudPoints: number;
children: AdminEditorAssetPayload[];
}
export interface AdminEditorAssetListResponse {
+7 -7
View File
@@ -17,26 +17,25 @@ import {
getStoredAdminToken,
setStoredAdminToken,
} from '../auth/adminAuthStore';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminAccountsPage} from '../pages/AdminAccountsPage';
import {AdminCreationEntrySwitchPage} from '../pages/AdminCreationEntrySwitchPage';
import {AdminDashboardPage} from '../pages/AdminDashboardPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminDebugHttpPage} from '../pages/AdminDebugHttpPage';
import {AdminEditorAssetQueryPage} from '../pages/AdminEditorAssetQueryPage';
import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage';
import {AdminEditorShowcaseReviewPage} from '../pages/AdminEditorShowcaseReviewPage';
import {AdminGrayReleaseConfigPage} from '../pages/AdminGrayReleaseConfigPage';
import {AdminInviteCodePage} from '../pages/AdminInviteCodePage';
import {AdminLoginPage} from '../pages/AdminLoginPage';
import {AdminOverviewPage} from '../pages/AdminOverviewPage';
import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
import {AdminRechargeOrderPage} from '../pages/AdminRechargeOrderPage';
import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
import {AdminWorkVisibilityPage} from '../pages/AdminWorkVisibilityPage';
import {AdminShell} from './AdminShell';
import type {AdminRouteId} from './adminRoutes';
import {
getAccessibleAdminRoutes,
@@ -44,6 +43,7 @@ import {
resolveAdminRoute,
routeHash,
} from './adminRoutes';
import {AdminShell} from './AdminShell';
type SessionStatus = 'checking' | 'guest' | 'authenticated';
+9 -9
View File
@@ -1,25 +1,25 @@
import {
Activity,
Bug,
BadgeDollarSign,
Bug,
Coins,
LayoutDashboard,
LogOut,
Megaphone,
Database,
Eye,
GitBranch,
Images,
Star,
WalletCards,
ShieldCheck,
LayoutDashboard,
ListChecks,
LogOut,
Megaphone,
ReceiptText,
ShieldCheck,
SlidersHorizontal,
Database,
Star,
Table2,
TicketCheck,
TicketPercent,
ReceiptText,
Users,
WalletCards,
} from 'lucide-react';
import type {ReactNode} from 'react';
@@ -5,8 +5,8 @@ import {
adminTrackingEventDefinitions,
buildAdminTrackingEventKeyOptions,
filterAdminProfileTaskTrackingEventDefinitions,
filterAdminTrackingEventKeyOptions,
filterAdminTrackingEventDefinitions,
filterAdminTrackingEventKeyOptions,
findAdminTrackingEventDefinition,
} from './trackingEventDefinitions';
@@ -1,6 +1,13 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
act,
fireEvent,
render,
screen,
waitFor,
within,
} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
@@ -29,6 +36,21 @@ const dashboardResponse: AdminDashboardResponse = {
consumedMudPoints: 88,
totalRegisteredUsers: 1200,
newRegisteredUsers: 16,
newUserPaymentConversion: {
paidUsers: 5,
newRegisteredUsers: 16,
rateBasisPoints: 3125,
},
day1Retention: {
eligibleUsers: 12,
retainedUsers: 3,
rateBasisPoints: 2500,
},
day7Retention: {
eligibleUsers: 0,
retainedUsers: 0,
rateBasisPoints: 0,
},
visitUsers: 34,
totalVisitUsers: 456,
visitCount: 98,
@@ -41,7 +63,10 @@ const dashboardResponse: AdminDashboardResponse = {
title: '生产素材',
unit: '个',
total: 12,
buckets: [{ key: '2026-06-23', label: '2026-06-23', value: 12 }],
buckets: [
{ key: '2026-06-23', label: '2026-06-23', value: 12 },
{ key: '2026-06-24', label: '2026-06-24', value: 0 },
],
},
],
operations: {
@@ -82,8 +107,34 @@ test('Dashboard 默认加载今日指标并支持运营汇总页签', async () =
expect(screen.getByText('本日生产素材数')).toBeTruthy();
expect(screen.getByText('总注册用户')).toBeTruthy();
expect(screen.getByText('本日新增用户数')).toBeTruthy();
expect(screen.getByText('当前使用人数(五分钟统计一次)')).toBeTruthy();
expect(screen.getByText('新增用户转化与留存')).toBeTruthy();
const paymentRateCard = screen
.getByText('本日新增用户付费率')
.closest('article');
expect(paymentRateCard).toBeTruthy();
expect(
within(paymentRateCard as HTMLElement).getByText('31.25%'),
).toBeTruthy();
expect(
within(paymentRateCard as HTMLElement).getByText('付费人数 / 新增人数'),
).toBeTruthy();
expect(
within(paymentRateCard as HTMLElement).getByText('5 / 16 人'),
).toBeTruthy();
expect(screen.getByText('次日留存')).toBeTruthy();
expect(screen.getByText('25%')).toBeTruthy();
expect(screen.getByText('3 / 12 人')).toBeTruthy();
const day7Card = screen.getByText('七日留存').closest('article');
expect(day7Card).toBeTruthy();
expect(within(day7Card as HTMLElement).getByText('-')).toBeTruthy();
expect(within(day7Card as HTMLElement).getByText('0 / 0 人')).toBeTruthy();
expect(screen.getByText('近 5 分钟活跃用户')).toBeTruthy();
expect(screen.getByText('生产素材')).toBeTruthy();
expect(
screen
.getByTitle('2026-06-24: 0 个')
.firstElementChild?.getAttribute('style'),
).toContain('height: 0%');
expect(screen.queryByRole('button', { name: '本时段' })).toBeNull();
await user.click(screen.getByRole('button', { name: '运营汇总' }));
@@ -108,7 +159,7 @@ test('Dashboard 默认日期使用北京时间', async () => {
});
});
test('Dashboard 选择本周时按今天填充整周范围', async () => {
test('Dashboard 选择本周时按北京时间今天截断未来日期', async () => {
const user = setupUser();
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
@@ -123,12 +174,12 @@ test('Dashboard 选择本周时按今天填充整周范围', async () => {
granularity: 'period',
anchor: undefined,
startDate: '2026-06-22',
endDate: '2026-06-28',
endDate: '2026-06-27',
});
});
});
test('Dashboard 选择本月时按今天填充整月范围', async () => {
test('Dashboard 选择本月时按北京时间今天截断未来日期', async () => {
const user = setupUser();
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
@@ -143,7 +194,7 @@ test('Dashboard 选择本月时按今天填充整月范围', async () => {
granularity: 'period',
anchor: undefined,
startDate: '2026-06-01',
endDate: '2026-06-30',
endDate: '2026-06-27',
});
});
});
@@ -186,8 +237,139 @@ test('Dashboard 手动选择起止日期时使用本时段查询', async () => {
});
});
expect(screen.getByText('本时段新增用户数')).toBeTruthy();
expect(screen.getByText('本时段新增用户付费率')).toBeTruthy();
});
test('Dashboard 新增用户付费率分母为零时显示横线', async () => {
vi.mocked(getAdminDashboard).mockResolvedValue({
...dashboardResponse,
metrics: {
...dashboardResponse.metrics,
newRegisteredUsers: 0,
newUserPaymentConversion: {
paidUsers: 0,
newRegisteredUsers: 0,
rateBasisPoints: 0,
},
},
});
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
const paymentRateCard = (
await screen.findByText('本日新增用户付费率')
).closest('article');
expect(paymentRateCard).toBeTruthy();
expect(within(paymentRateCard as HTMLElement).getByText('-')).toBeTruthy();
expect(
within(paymentRateCard as HTMLElement).getByText('0 / 0 人'),
).toBeTruthy();
});
test('Dashboard 手动选择日期时不允许查询北京时间今天之后', async () => {
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
await screen.findByText('本日生产素材数');
const endDateInput = screen.getByLabelText('终止日期') as HTMLInputElement;
expect(endDateInput.max).toBe('2026-06-27');
fireEvent.change(endDateInput, { target: { value: '2026-07-12' } });
await waitFor(() => {
expect(endDateInput.value).toBe('2026-06-27');
expect(getAdminDashboard).toHaveBeenLastCalledWith('admin-token', {
granularity: 'period',
anchor: undefined,
startDate: '2026-06-27',
endDate: '2026-06-27',
});
});
});
test('Dashboard 四张趋势图共享横向日期窗口', async () => {
const baseChart = dashboardResponse.charts[0]!;
vi.mocked(getAdminDashboard).mockResolvedValue({
...dashboardResponse,
charts: [
baseChart,
{
...baseChart,
id: 'consumed-mud-points',
title: '消耗泥点',
unit: '泥点',
},
],
});
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
const chartRegions = await screen.findAllByRole('region', {
name: /趋势图$/,
});
const [sourceChart, targetChart] = chartRegions;
if (!sourceChart || !targetChart) {
throw new Error('趋势图未完整渲染');
}
await act(async () => {
await Promise.resolve();
});
setScrollableDimensions(sourceChart, {
clientWidth: 100,
scrollWidth: 500,
scrollLeft: 200,
});
setScrollableDimensions(targetChart, {
clientWidth: 100,
scrollWidth: 500,
scrollLeft: 0,
});
await act(async () => {
fireEvent.scroll(sourceChart);
});
expect(targetChart.scrollLeft).toBe(200);
});
test('Dashboard 访问人数趋势明确区分每日桶与时段去重总数', async () => {
const baseChart = dashboardResponse.charts[0]!;
vi.mocked(getAdminDashboard).mockResolvedValue({
...dashboardResponse,
charts: [
{
...baseChart,
id: 'visit-users',
title: '每日访问人数',
unit: '人',
total: 34,
},
],
});
render(<AdminDashboardPage token="admin-token" onUnauthorized={vi.fn()} />);
expect(await screen.findByText('每日访问人数')).toBeTruthy();
expect(screen.getByText('时段去重 34 人')).toBeTruthy();
});
function setupUser() {
return userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
}
function setScrollableDimensions(
element: HTMLElement,
dimensions: {
clientWidth: number;
scrollWidth: number;
scrollLeft: number;
},
) {
Object.defineProperties(element, {
clientWidth: { configurable: true, value: dimensions.clientWidth },
scrollWidth: { configurable: true, value: dimensions.scrollWidth },
scrollLeft: {
configurable: true,
value: dimensions.scrollLeft,
writable: true,
},
});
}
+158 -31
View File
@@ -1,5 +1,5 @@
import { RefreshCw } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getAdminDashboard } from '../api/adminApiClient';
import type {
@@ -42,24 +42,29 @@ export function AdminDashboardPage({
const [activeTab, setActiveTab] = useState<AdminDashboardTab>('metrics');
const [errorMessage, setErrorMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [chartScrollLeft, setChartScrollLeft] = useState(0);
const today = formatBeijingDateInput(new Date());
const loadDashboard = useCallback(async (range = dateRange) => {
setIsLoading(true);
setErrorMessage('');
try {
const response = await getAdminDashboard(token, {
granularity: 'period',
anchor: undefined,
startDate: range.startDate,
endDate: range.endDate,
});
setDashboard(response);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
}, [dateRange, onUnauthorized, token]);
const loadDashboard = useCallback(
async (range = dateRange) => {
setIsLoading(true);
setErrorMessage('');
try {
const response = await getAdminDashboard(token, {
granularity: 'period',
anchor: undefined,
startDate: range.startDate,
endDate: range.endDate,
});
setDashboard(response);
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsLoading(false);
}
},
[dateRange, onUnauthorized, token],
);
useEffect(() => {
void loadDashboard();
@@ -75,6 +80,10 @@ export function AdminDashboardPage({
return () => window.clearInterval(timer);
}, [loadDashboard]);
useEffect(() => {
setChartScrollLeft(0);
}, [dashboard?.range.periodEndDate, dashboard?.range.periodStartDate]);
const metrics = dashboard?.metrics;
const totalMetricCards = useMemo(
() => [
@@ -98,7 +107,7 @@ export function AdminDashboardPage({
},
{
id: 'current-users',
label: '当前使用人数(五分钟统计一次)',
label: '近 5 分钟活跃用户',
value: metrics?.currentUsers ?? 0,
unit: '人',
},
@@ -154,6 +163,7 @@ export function AdminDashboardPage({
<span></span>
<input
type="date"
max={today}
value={dateRange.startDate}
onChange={(event) =>
handleDateRangeChange('startDate', event.target.value)
@@ -164,6 +174,7 @@ export function AdminDashboardPage({
<span></span>
<input
type="date"
max={today}
value={dateRange.endDate}
onChange={(event) =>
handleDateRangeChange('endDate', event.target.value)
@@ -278,9 +289,51 @@ export function AdminDashboardPage({
</div>
</section>
<section className="admin-panel">
<div className="admin-panel-heading">
<h3></h3>
<span>{dashboard?.range.periodLabel ?? '-'}</span>
</div>
<div className="admin-dashboard-retention-grid">
<RateCard
label={`${rangePrefix(granularity)}新增用户付费率`}
numeratorLabel="付费人数"
denominatorLabel="新增人数"
numerator={metrics?.newUserPaymentConversion?.paidUsers}
denominator={
metrics?.newUserPaymentConversion?.newRegisteredUsers
}
rateBasisPoints={
metrics?.newUserPaymentConversion?.rateBasisPoints
}
/>
<RateCard
label="次日留存"
numeratorLabel="留存人数"
denominatorLabel="可观察新增人数"
numerator={metrics?.day1Retention.retainedUsers}
denominator={metrics?.day1Retention.eligibleUsers}
rateBasisPoints={metrics?.day1Retention.rateBasisPoints}
/>
<RateCard
label="七日留存"
numeratorLabel="留存人数"
denominatorLabel="可观察新增人数"
numerator={metrics?.day7Retention.retainedUsers}
denominator={metrics?.day7Retention.eligibleUsers}
rateBasisPoints={metrics?.day7Retention.rateBasisPoints}
/>
</div>
</section>
<div className="admin-dashboard-chart-grid">
{(dashboard?.charts ?? []).map((chart) => (
<ChartPanel key={chart.id} chart={chart} />
<ChartPanel
key={chart.id}
chart={chart}
scrollLeft={chartScrollLeft}
onScrollLeftChange={setChartScrollLeft}
/>
))}
{dashboard && dashboard.charts.length === 0 ? (
<div className="admin-empty-state"></div>
@@ -347,9 +400,10 @@ export function AdminDashboardPage({
if (!parseDateValueAsUtc(value)) {
return;
}
const clampedValue = value > today ? today : value;
setGranularity('period');
setDateRange((current) =>
normalizeDateRange({ ...current, [field]: value }),
normalizeDateRange({ ...current, [field]: clampedValue }),
);
}
}
@@ -377,17 +431,85 @@ function MetricCard({
);
}
function ChartPanel({ chart }: { chart: AdminDashboardChartPayload }) {
function RateCard({
label,
numeratorLabel,
denominatorLabel,
numerator,
denominator,
rateBasisPoints,
}: {
label: string;
numeratorLabel: string;
denominatorLabel: string;
numerator?: number;
denominator?: number;
rateBasisPoints?: number;
}) {
const hasDenominator = Boolean(denominator);
return (
<article className="admin-dashboard-retention-card">
<span>{label}</span>
<strong>
{hasDenominator && rateBasisPoints !== undefined
? formatRateBasisPoints(rateBasisPoints)
: '-'}
</strong>
<div>
<small>
{numeratorLabel} / {denominatorLabel}
</small>
<b>
{numerator !== undefined && denominator !== undefined
? `${formatNumber(numerator)} / ${formatNumber(denominator)}`
: '-'}
</b>
</div>
</article>
);
}
function ChartPanel({
chart,
scrollLeft,
onScrollLeftChange,
}: {
chart: AdminDashboardChartPayload;
scrollLeft: number;
onScrollLeftChange: (scrollLeft: number) => void;
}) {
const barsRef = useRef<HTMLDivElement>(null);
const maxValue = Math.max(1, ...chart.buckets.map((bucket) => bucket.value));
useEffect(() => {
const element = barsRef.current;
if (!element) {
return;
}
if (Math.abs(element.scrollLeft - scrollLeft) > 1) {
element.scrollLeft = scrollLeft;
}
}, [chart.buckets.length, scrollLeft]);
return (
<section className="admin-panel admin-dashboard-chart-card">
<div className="admin-panel-heading">
<h3>{chart.title}</h3>
<span>
{chart.id === 'visit-users' ? '时段去重 ' : ''}
{formatNumber(chart.total)} {chart.unit}
</span>
</div>
<div className="admin-dashboard-bars">
<div
ref={barsRef}
className="admin-dashboard-bars"
role="region"
aria-label={`${chart.title}趋势图`}
tabIndex={0}
onScroll={(event) => {
onScrollLeftChange(event.currentTarget.scrollLeft);
}}
>
{chart.buckets.map((bucket) => (
<div className="admin-dashboard-bar-item" key={bucket.key}>
<div
@@ -396,7 +518,10 @@ function ChartPanel({ chart }: { chart: AdminDashboardChartPayload }) {
>
<span
style={{
height: `${Math.max(4, (bucket.value / maxValue) * 100)}%`,
height:
bucket.value === 0
? '0%'
: `${Math.max(4, (bucket.value / maxValue) * 100)}%`,
}}
/>
</div>
@@ -450,6 +575,13 @@ function formatNumber(value: number) {
return new Intl.NumberFormat('zh-CN').format(value);
}
function formatRateBasisPoints(value: number) {
return new Intl.NumberFormat('zh-CN', {
style: 'percent',
maximumFractionDigits: 2,
}).format(value / 10_000);
}
function formatBeijingDateInput(date: Date) {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
@@ -498,9 +630,7 @@ function buildWeekDateRange(dateValue: string) {
const weekday = date.getUTCDay() || 7;
const monday = new Date(date);
monday.setUTCDate(date.getUTCDate() - weekday + 1);
const sunday = new Date(monday);
sunday.setUTCDate(monday.getUTCDate() + 6);
return { startDate: formatUtcDate(monday), endDate: formatUtcDate(sunday) };
return { startDate: formatUtcDate(monday), endDate: dateValue };
}
function buildMonthDateRange(dateValue: string) {
@@ -512,12 +642,9 @@ function buildMonthDateRange(dateValue: string) {
const firstDate = new Date(
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1),
);
const lastDate = new Date(
Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0),
);
return {
startDate: formatUtcDate(firstDate),
endDate: formatUtcDate(lastDate),
endDate: dateValue,
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,7 @@ import { Eye, FileText, RefreshCcw, Upload, X } from 'lucide-react';
import type { ReactNode } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import {
getAdminAssetReadUrl,
getAdminEditorShowcaseCampaign,
@@ -11,7 +12,6 @@ import {
uploadAdminEditorShowcaseCampaignImage,
upsertAdminEditorShowcaseCampaign,
} from '../api/adminApiClient';
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
import type {
AdminEditorShowcaseAssetPayload,
AdminEditorShowcaseCampaignPayload,
@@ -10,13 +10,13 @@ import type {
AdminTrackingEventKeyPayload,
TrackingScopeKind,
} from '../api/adminApiTypes';
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
import {
buildAdminTrackingEventKeyOptions,
filterAdminTrackingEventKeyOptions,
filterAdminTrackingEventDefinitions,
filterAdminTrackingEventKeyOptions,
findAdminTrackingEventDefinition,
} from '../config/trackingEventDefinitions';
import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton';
import { handlePageError } from './pageUtils';
interface AdminTrackingEventsPageProps {
+172 -8
View File
@@ -361,6 +361,63 @@ button:disabled {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.admin-dashboard-retention-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.admin-dashboard-retention-card {
display: grid;
min-width: 0;
min-height: 112px;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: auto 1fr;
gap: 8px 16px;
align-items: center;
padding: 16px;
}
.admin-dashboard-retention-card + .admin-dashboard-retention-card {
border-left: 1px solid #ead8ca;
}
.admin-dashboard-retention-card > span {
color: #8f7868;
font-size: 13px;
font-weight: 750;
}
.admin-dashboard-retention-card > strong {
grid-row: 1 / -1;
grid-column: 2;
color: #8f3f27;
font-size: 30px;
font-variant-numeric: tabular-nums;
line-height: 1.1;
white-space: nowrap;
}
.admin-dashboard-retention-card > div {
display: flex;
min-width: 0;
align-items: baseline;
gap: 8px;
}
.admin-dashboard-retention-card small {
min-width: 0;
color: #a38f80;
font-size: 12px;
}
.admin-dashboard-retention-card b {
flex: 0 0 auto;
color: #755a49;
font-size: 13px;
font-variant-numeric: tabular-nums;
}
.admin-dashboard-chart-grid,
.admin-dashboard-operations {
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -417,7 +474,10 @@ button:disabled {
gap: 8px;
min-height: 196px;
overflow-x: auto;
overscroll-behavior-inline: contain;
padding: 4px 2px 0;
scrollbar-color: #bdaea3 #f4e5d7;
scrollbar-width: thin;
}
.admin-dashboard-bar-item {
@@ -661,7 +721,17 @@ button:disabled {
}
.admin-inline-identity > div {
display: grid;
min-width: 0;
gap: 2px;
}
.admin-inline-identity small {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-database-user-cell,
@@ -750,6 +820,55 @@ button:disabled {
display: block;
}
.admin-asset-query-resource-cell {
display: flex;
align-items: flex-start;
gap: 6px;
}
.admin-asset-query-resource-cell > div:last-child small {
display: block;
max-width: 92px;
margin-top: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-asset-query-expand-button {
display: inline-flex;
flex: 0 0 28px;
width: 28px;
height: 28px;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
color: #78523e;
cursor: pointer;
}
.admin-asset-query-expand-spacer,
.admin-asset-query-child-branch {
display: block;
flex: 0 0 28px;
width: 28px;
}
.admin-asset-query-child-row {
background: #fffaf5;
}
.admin-asset-query-resource-cell-child {
padding-left: 12px;
}
.admin-asset-query-child-branch {
height: 34px;
border-bottom: 1px solid #d8c3b3;
border-left: 1px solid #d8c3b3;
}
.admin-asset-query-prompt-text {
display: block;
max-width: 100%;
@@ -1212,7 +1331,7 @@ button:disabled {
margin: 0;
}
.admin-info-list div {
.admin-info-list > div {
display: grid;
grid-template-columns: minmax(90px, 0.34fr) minmax(0, 1fr);
gap: 12px;
@@ -1402,29 +1521,30 @@ button:disabled {
}
.admin-asset-query-table {
min-width: 1080px;
table-layout: fixed;
}
.admin-asset-query-table th:nth-child(1),
.admin-asset-query-table td:nth-child(1) {
width: 10%;
width: 13%;
}
.admin-asset-query-table th:nth-child(2),
.admin-asset-query-table td:nth-child(2),
.admin-asset-query-table th:nth-child(3),
.admin-asset-query-table td:nth-child(3) {
width: 14%;
width: 13%;
}
.admin-asset-query-table th:nth-child(4),
.admin-asset-query-table td:nth-child(4) {
width: 34%;
width: 27%;
}
.admin-asset-query-table th:nth-child(5),
.admin-asset-query-table td:nth-child(5) {
width: 8%;
width: 10%;
}
.admin-asset-query-table th:nth-child(6),
@@ -1432,6 +1552,11 @@ button:disabled {
width: 10%;
}
.admin-asset-query-table th:nth-child(7),
.admin-asset-query-table td:nth-child(7) {
width: 10%;
}
.admin-showcase-review-table {
table-layout: fixed;
}
@@ -1990,10 +2115,19 @@ button:disabled {
}
.admin-dashboard-metric-grid,
.admin-dashboard-operation-grid {
.admin-dashboard-operation-grid,
.admin-dashboard-retention-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.admin-dashboard-retention-card:nth-child(odd) {
border-left: 0;
}
.admin-dashboard-retention-card:nth-child(n + 3) {
border-top: 1px solid #ead8ca;
}
.admin-field-compact {
max-width: none;
}
@@ -2126,11 +2260,19 @@ button:disabled {
font-size: 22px;
}
.admin-info-list div {
.admin-info-list > div {
grid-template-columns: 1fr;
gap: 3px;
}
.admin-asset-query-detail-layout {
grid-template-columns: 1fr;
}
.admin-asset-query-detail-thumb-button {
justify-self: center;
}
.admin-dashboard-tabs {
width: 100%;
}
@@ -2139,8 +2281,30 @@ button:disabled {
grid-template-columns: 1fr;
}
.admin-dashboard-retention-card {
grid-template-columns: 1fr;
grid-template-rows: auto;
}
.admin-dashboard-retention-card + .admin-dashboard-retention-card {
border-top: 1px solid #ead8ca;
border-left: 0;
}
.admin-dashboard-retention-card > strong {
grid-row: auto;
grid-column: auto;
}
.admin-dashboard-retention-card > div {
align-items: flex-start;
flex-direction: column;
gap: 3px;
}
.admin-dashboard-metric-grid,
.admin-dashboard-operation-grid {
.admin-dashboard-operation-grid,
.admin-dashboard-retention-grid {
grid-template-columns: 1fr;
}
+3 -7
View File
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url);
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
@@ -1117,7 +1117,7 @@ function extractDesktopCapabilities(source) {
}
function extractDesktopHandledMethods(source) {
const matchBodies = [...source.matchAll(/match request\.method\.as_str\(\) \{([\s\S]*?)\n \}/g)].map(
const matchBodies = [...source.matchAll(/match request\.method\.as_str\(\) \{([\s\S]*?)\n {4}\}/g)].map(
(match) => match[1],
);
if (matchBodies.length === 0) {
@@ -1152,7 +1152,7 @@ function extractDesktopHostBridgeMethodBody(source, method) {
const nextMethodMatch = source
.slice(methodStart + method.length)
.match(/\n (?:"[^"]+"|_)\s*=>/);
.match(/\n {8}(?:"[^"]+"|_)\s*=>/);
const nextMethodStart = nextMethodMatch
? methodStart + method.length + nextMethodMatch.index
: -1;
@@ -1410,10 +1410,6 @@ function extractTauriInvokeCommands(source) {
.filter(Boolean);
}
function collectRustSourceBasenames(files) {
return files.map((file) => file.pathname.split('/').pop()).sort();
}
function collectRustSourceRelativePaths(files) {
const rootPath = rustSourceDir.pathname.endsWith('/')
? rustSourceDir.pathname
@@ -1,6 +1,6 @@
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import {spawnSync} from 'node:child_process';
const shellRoot = new URL('../', import.meta.url);
const repoRoot = path.resolve(shellRoot.pathname, '../..');
+4 -13
View File
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import fs from 'node:fs';
import { PNG } from 'pngjs';
@@ -82,11 +82,6 @@ const filesPath = new URL('../src/host-bridge/files.ts', import.meta.url);
const filesSource = fs.readFileSync(filesPath, 'utf8');
const filesTestPath = new URL('../src/host-bridge/files.test.ts', import.meta.url);
const filesTestSource = fs.readFileSync(filesTestPath, 'utf8');
const filePayloadsPath = new URL(
'../src/host-bridge/filePayloads.ts',
import.meta.url,
);
const filePayloadsSource = fs.readFileSync(filePayloadsPath, 'utf8');
const hapticsPath = new URL('../src/host-bridge/haptics.ts', import.meta.url);
const hapticsSource = fs.readFileSync(hapticsPath, 'utf8');
const hapticsTestPath = new URL(
@@ -495,7 +490,7 @@ function extractNumberConstExport(source, exportName) {
function extractMobileBridgeHandledMethods(source) {
const match = source.match(
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n {2}\}/,
);
if (!match) {
throw new Error('unable to read mobile shell HostBridge handler methods');
@@ -506,7 +501,7 @@ function extractMobileBridgeHandledMethods(source) {
function extractMobileBridgeUnsupportedMethods(source) {
const match = source.match(
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n {2}\}/,
);
if (!match) {
throw new Error('unable to read mobile shell HostBridge unsupported methods');
@@ -514,7 +509,7 @@ function extractMobileBridgeUnsupportedMethods(source) {
const unsupportedMethods = new Set();
const casePattern =
/case '([^']+)':([\s\S]*?)(?=\n case '|\n default:|\n \})/g;
/case '([^']+)':([\s\S]*?)(?=\n {4}case '|\n {4}default:|\n {2}\})/g;
for (const entry of match[1].matchAll(casePattern)) {
if (entry[2].includes('unsupported(request.method)')) {
unsupportedMethods.add(entry[1]);
@@ -990,10 +985,6 @@ const sharedEvents = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_EVENTS',
);
const sharedBlockedDownloadProtocols = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS',
);
const sharedMobileBaseCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES',
@@ -1,5 +1,5 @@
import fs from 'node:fs';
import {spawnSync} from 'node:child_process';
import fs from 'node:fs';
const shellRoot = new URL('../', import.meta.url);
const easConfigPath = new URL('eas.json', shellRoot);

Some files were not shown because too many files have changed in this diff Show More