diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86bb617..e92803d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: run: | node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js + node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js - name: Release readiness diff --git a/AGENTS.md b/AGENTS.md index 9fb1eab..900a68e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -144,6 +144,7 @@ python -m pytest --cov=codex_usage_tracker --cov-report=term-missing python -m compileall src node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js python scripts/check_release.py @@ -218,6 +219,7 @@ python -m pytest --cov=codex_usage_tracker --cov-report=term-missing python -m compileall src node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js python scripts/check_release.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c16f835..e4074db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,21 @@ ## Unreleased +- Remove low-value call/thread anchor diagnostics from the experimental call investigator to avoid an extra source-log scan per context load. +- Persist call-origin metadata as categorical aggregate fields during indexing so normal dashboard payloads do not reopen source JSONL logs to infer user-vs-Codex initiation. +- Persist archived-session scope, conservative thread keys, and per-thread previous/next call links as aggregate helper fields for faster dashboard filtering and investigator navigation. +- Add opt-in localhost API timing diagnostics for `/api/usage` and `/api/context` without exposing raw transcript content. +- Reduce explicit context loading to a quick default mode that omits tool output and serialized buckets, with full serialized JSONL bucket analysis still available on demand. +- Add source-log-aware synthetic benchmark coverage that verifies normal dashboard payload assembly does not open generated source JSONL files. +- Add SQL-backed live dashboard API slices for status, calls, one call, threads, thread calls, summary, and recommendations while preserving the compatibility `/api/usage` endpoint. +- Materialize active and all-history thread summaries in SQLite so live thread APIs can read pre-aggregated totals. +- Add source-file refresh cursors so live refresh skips unchanged logs, seeks to appended JSONL bytes when safe, and safely replaces aggregate rows for changed or truncated source logs. +- Hydrate direct call-investigator links from the aggregate `/api/call` endpoint when the selected record is outside the currently loaded table slice or filter state. +- Replace placeholder non-English dashboard locale catalogs with translated UI catalogs and add regression coverage for core visible labels. + ## 0.5.0 - 2026-06-10 -- Add the dashboard localization foundation, including starter locale catalogs, language metadata, local browser language selection, `--lang`, and `CODEX_USAGE_TRACKER_LANG`. +- Add the dashboard localization foundation, including initial locale catalogs, language metadata, local browser language selection, `--lang`, and `CODEX_USAGE_TRACKER_LANG`. - Add Vietnamese dashboard localization and focused validation coverage for translated dashboard labels. - Keep the README landing page focused on dashboard screenshots and companion usage workflows before detailed localization guidance. - Stabilize the CI synthetic benchmark smoke so coverage instrumentation does not create false release failures. diff --git a/README.md b/README.md index 707c0c0..7c61677 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ The tracker cannot read your logged-in ChatGPT plan or live remaining usage auto ## Dashboard Language -The dashboard supports localized UI text. English is the canonical catalog, and the project includes starter locale catalogs for common dashboard languages. +The dashboard supports localized UI text. English is the canonical catalog, and the project includes translated locale catalogs for common dashboard languages. Set the initial dashboard language with `--lang`: @@ -171,7 +171,7 @@ CODEX_USAGE_TRACKER_LANG=vi codex-usage-tracker serve-dashboard --open The dashboard also includes a language selector. Browser selections are stored locally and can override the generated default for that browser. -Supported starter locales include English, Vietnamese, Spanish, French, German, Portuguese, Japanese, Simplified Chinese, Korean, Russian, Italian, and Arabic. This localizes dashboard UI text, not the full CLI output or data exports. +Supported dashboard locales include English, Vietnamese, Spanish, French, German, Portuguese, Japanese, Simplified Chinese, Korean, Russian, Italian, and Arabic. This localizes dashboard UI text, not raw Codex log content, thread names, project names, paths, full CLI output, or data exports. ### Adding A Dashboard Language diff --git a/docs/architecture.md b/docs/architecture.md index a7d6051..f2d873b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,31 +4,32 @@ Codex Usage Tracker is a local sidecar app. It reads aggregate token counters fr ## Boundaries -- `parser.py` converts local JSONL events into aggregate `UsageEvent` records. It must not persist prompts, assistant text, tool output, or transcript snippets. +- `parser.py` converts local JSONL events into aggregate `UsageEvent` records. It also attaches metadata-only call-origin categories, archived-session flags, and conservative thread keys. It must not persist prompts, assistant text, tool output, or transcript snippets. +- `call_origin.py` owns the pure call-origin classifier and migrated-row fallback. It must not open source JSONL files; source-log reads belong in parser refresh or explicit context loading only. - `schema.py` owns persisted `usage_events` columns. Add columns there before changing SQLite migrations or export behavior. -- `store.py` owns SQLite setup, refresh, rebuild, and query access. Keep filesystem scanning, database writes, SQL prefilters, counts, limits, and offsets here. +- `store.py` owns SQLite setup, refresh, rebuild, query access, persisted per-thread previous/next call links, materialized thread summaries, source-file refresh cursors, and SQL-backed live dashboard API slices. Keep filesystem scanning, database writes, SQL prefilters, counts, limits, offsets, and incremental refresh decisions here. - `reports.py` is the application-service layer for summaries, expensive-call reports, recommendations, pricing coverage, and filtered query payloads. CLI and MCP should call this layer instead of duplicating report assembly. -- `api_payloads.py` owns stable JSON payload helpers shared by CLI and MCP. `json_contracts.py` owns the lightweight contract checks for schema-versioned CLI/MCP payloads. Add payload builders and contract entries together when both surfaces need the same shape. +- `api_payloads.py` owns stable JSON payload helpers shared by CLI and MCP. `json_contracts.py` owns the lightweight contract checks for schema-versioned CLI/MCP payloads and localhost live API payloads. Add payload builders and contract entries together when surfaces need the same shape. - `costing.py`, `pricing_config.py`, `pricing_openai.py`, `pricing_estimates.py`, and `allowance.py` own cost, credit, rate-card, and allowance annotation. Keep estimate confidence and source metadata attached to rows. - `projects.py`, `threads.py`, and `recommendations.py` annotate aggregate rows with project identity, thread relationships, and actionable signals. Project privacy redaction also belongs in `projects.py` so CLI, MCP, dashboard, CSV, and support-bundle surfaces share the same behavior. -- `dashboard.py` builds aggregate-only dashboard payloads and writes HTML/assets. `server.py` adds localhost refresh and explicit lazy context loading. +- `dashboard.py` builds aggregate-only static dashboard payloads and writes HTML/assets. `server.py` adds localhost refresh, the compatibility `/api/usage` endpoint, SQL-backed live API slices, and explicit lazy context loading. - `plugin_data/dashboard/dashboard_format.js` owns dashboard formatting primitives. `dashboard_data.js` owns row payload and thread relationship helpers. `dashboard_state.js` owns URL, CSV, and download state utilities. `dashboard.js` owns DOM rendering, event handling, API refresh, and detail-panel behavior. -- `context.py` is the only normal path that reads raw log context, and it does so only for one selected record on demand with redaction and size limits. +- `context.py` is the only normal path that reads raw log context, and it does so only for one selected record on demand with redaction and size limits. Its default quick mode omits tool output and serialized groups; full serialized JSONL group analysis is explicit. - `plugin_installer.py`, `.mcp.json`, `skills/`, and `scripts/check_release.py` own install and packaging behavior. -- `scripts/benchmark_synthetic_history.py` owns generated large-history query timing and threshold enforcement for 10k, 100k, and 500k aggregate-row fixtures. It must stay synthetic-only and must not read real Codex logs. +- `scripts/benchmark_synthetic_history.py` owns generated large-history query timing and threshold enforcement for 10k, 100k, and 500k aggregate-row fixtures. Its optional `--with-source-logs` mode writes synthetic JSONL source logs to time explicit context loading and to guard normal dashboard payload assembly against source-log reads. It must stay synthetic-only and must not read real Codex logs. - `skills/codex-usage-tracker/` is the source copy for the operational Codex skill. It should stay focused on setup, dashboard, export, doctor, and direct MCP workflows. - `skills/codex-usage-api/` is the source copy for the conversational analyst skill. It should stay focused on aggregate-only API routing, interpretation, and limitations. - `src/codex_usage_tracker/plugin_data/skills/` contains the wheel-bundled copies installed by `codex-usage-tracker install-plugin`. ## Extension Rules -1. Add new persisted metrics through `UsageEvent`, `schema.py`, migrations, store queries, dashboard payload tests, and CSV/export checks. +1. Add new persisted usage-event metrics through `UsageEvent`, `schema.py`, migrations, store queries, dashboard payload tests, and CSV/export checks. Add auxiliary aggregate tables such as `thread_summaries` or `source_files` through `store.py` migrations plus focused migration/privacy tests. 2. Add new report views through `reports.py` first, then wire CLI and MCP wrappers to that shared service. 3. Add new machine-readable outputs through `api_payloads.py` or report payload methods with a `schema` value, a `json_contracts.py` entry, and focused tests. 4. Add dashboard-only interactions in `plugin_data/dashboard/dashboard.js` and keep URL state in `dashboard_state.js`. 5. Keep all examples, screenshots, mocks, and tests synthetic. Never derive fixtures from real logs. 6. When editing skill instructions, update both the source `skills/...` file and the bundled `src/codex_usage_tracker/plugin_data/skills/...` copy. `scripts/check_release.py` verifies that installable plugin assets stay complete and synced. -7. When adding fields derived from `cwd`, Git metadata, or source paths, decide how they behave in `normal`, `redacted`, and `strict` privacy modes before exposing them in dashboard, JSON, CSV, MCP, or support-bundle output. +7. When adding fields derived from `cwd`, Git metadata, source paths, or log-event metadata, decide how they behave in `normal`, `redacted`, and `strict` privacy modes before exposing them in dashboard, JSON, CSV, MCP, or support-bundle output. ## Validation @@ -40,6 +41,7 @@ python -m compileall src python -m mypy node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js python scripts/check_release.py @@ -50,4 +52,4 @@ git diff --check Dashboard UI changes should also be opened in a browser and checked on desktop and mobile widths for overlap, stale state, and aggregate-only output. -Run `python scripts/benchmark_synthetic_history.py --rows 10000 100000 --json --enforce-thresholds` after changing SQLite filters, dashboard payload loading, or indexes. Run the 500k benchmark before release work when practical. +Run `python scripts/benchmark_synthetic_history.py --rows 10000 100000 --json --enforce-thresholds` after changing SQLite filters, dashboard payload loading, or indexes. Run `python scripts/benchmark_synthetic_history.py --rows 1000 --with-source-logs --json --enforce-thresholds` after changing explicit context loading or source-log diagnostics. Run the 500k benchmark before release work when practical. diff --git a/docs/call-drilldown-performance-checklist.md b/docs/call-drilldown-performance-checklist.md new file mode 100644 index 0000000..f0b4afe --- /dev/null +++ b/docs/call-drilldown-performance-checklist.md @@ -0,0 +1,274 @@ +# Call Drilldown Performance Hardening Checklist + +This checklist tracks the focused `perf/call-drilldown-performance-hardening` branch built from `experimental/call-drilldown-diagnostics`. + +## Goals + +- Keep the useful call investigator, token accounting, cache diagnostics, compaction evidence, and call-origin concepts. +- Prevent normal dashboard rendering and `/api/usage` from opening or parsing raw source JSONL logs. +- Keep raw/source JSONL access limited to indexing/refresh, explicit one-call context loading, and explicit diagnostic or benchmark tools. +- Make large-history dashboard behavior measurable and progressively more SQLite-backed. +- Keep the static dashboard export path supported for compatibility. + +## Non-goals + +- Do not implement an evidence cache in this branch. +- Do not publish, tag, push to `main`, rename packages, or change the stable CLI command. +- Do not use real Codex logs in tests, docs, fixtures, screenshots, or benchmarks. +- Do not remove the existing static dashboard mode while live APIs are being hardened. + +## Privacy Boundary + +- Default SQLite may store aggregate counters and derived categorical diagnostics only. +- Default SQLite must not store prompts, assistant messages, tool output, raw JSONL fragments, compaction replacement text, raw context, or reconstructed transcript evidence. +- Any future evidence cache must be explicit opt-in, redacted, purgeable, and documented before implementation. +- Context evidence must remain on-demand for one selected call and redacted before display. + +## Performance Invariants + +- `dashboard_payload` must not open or parse source JSONL files. +- `/api/usage` must not do raw-log analysis. +- Source JSONL reads are allowed only in refresh/indexing, explicit `/api/context` for one selected call, and explicit diagnostic/benchmark tools. +- Live dashboard work should move toward SQLite-backed API slices instead of shipping all rows to the browser. +- Static dashboard generation remains supported as export/compat mode. + +## Current Inventory + +Milestone 0 inspection ran on `perf/call-drilldown-performance-hardening` after fast-forwarding `experimental/call-drilldown-diagnostics`. + +Suspected hot paths confirmed by source inspection: + +- M3 removed the `dashboard_payload` source-log call-origin scan. Call origin is now persisted as aggregate categorical metadata during parser refresh, with a cheap fallback for migrated rows. +- M3 converted `src/codex_usage_tracker/call_origin.py` to pure classifiers that do not open source JSONL files. +- `src/codex_usage_tracker/server.py` serves `/api/usage` by calling `dashboard_payload`; after M3, this no longer inherits call-origin source-log reads. +- M4 persists `is_archived`, `thread_key`, `thread_call_index`, `previous_record_id`, and `next_record_id` in `usage_events`. +- M4 updates active-history SQL filtering to use `is_archived` while still excluding migrated archived source paths when the new flag has only its default value. +- M2 removed `_read_call_anchors(...)` from `load_call_context`, so explicit context loading no longer performs the extra anchor scan. +- M2 removed all dashboard reads of `payload.call_anchors` and `payload.thread_anchors`. +- `src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js` builds helper indexes, but adjacent-call lookup and render paths still need a focused large-history review. +- `scripts/benchmark_synthetic_history.py` benchmarks synthetic SQLite rows, but currently uses synthetic `source_file` paths that do not exercise source-log scanning. +- `.github/workflows/ci.yml`, `docs/development.md`, `docs/architecture.md`, and `AGENTS.md` run Node syntax checks for dashboard JS assets, but not yet for `dashboard_call_investigator.js`. + +Already implemented before this branch: + +- `scripts/check_release.py` already requires `src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js`. +- `scripts/check_release.py` already requires `codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js` in packaged distributions. +- `pyproject.toml` package-data coverage should include the dashboard asset through the existing dashboard asset glob; keep this verified in Milestone 1. + +## Milestone Checklist + +- [x] M0 inventory current state and create this checklist. +- [x] M0.1 contain calls-table horizontal overflow inside the table card. +- [x] M1 validate and package the call investigator dashboard asset in CI, docs, and release checks. +- [x] M2 remove low-value call/thread anchor diagnostics and their extra context source scan. +- [x] M3 persist aggregate call-origin metadata during indexing so dashboard payloads do not scan source logs. +- [x] M4 persist cheap performance-critical dashboard query helper fields where feasible. +- [x] M5 add optional timing diagnostics to `/api/usage` and `/api/context`. +- [x] M6 make explicit context loading single-pass where practical. +- [x] M7 precompute client-side call adjacency for investigator rendering. +- [x] M8 add source-log-aware synthetic benchmark coverage. +- [x] M9 add SQLite-backed live dashboard API slices while preserving `/api/usage`. +- [x] M10 optionally materialize thread summaries after APIs are stable. +- [x] M11 optionally add incremental source-file refresh metadata after parser-time call origin is stable. +- [x] M12 finalize docs, validation, benchmark results, and merge-readiness notes. + +## Validation Commands + +Focused commands expected during this branch: + +```bash +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +python -m pytest tests/test_store_dashboard_mcp.py +python -m pytest tests/test_json_contracts.py +python -m pytest tests/test_privacy.py +python -m pytest tests/test_parser.py +python -m pytest tests/test_call_origin.py +python -m pytest tests/test_store_migrations.py +python scripts/check_release.py +python scripts/benchmark_synthetic_history.py --rows 10000 --json --enforce-thresholds +python scripts/benchmark_synthetic_history.py --rows 100 --batch-size 25 --with-source-logs --json --enforce-thresholds +``` + +Full branch closeout should also run the release validation listed in `docs/development.md`. + +## Files Touched + +- `docs/call-drilldown-performance-checklist.md` +- `.github/workflows/ci.yml` +- `AGENTS.md` +- `CHANGELOG.md` +- `docs/architecture.md` +- `docs/development.md` +- `scripts/benchmark_synthetic_history.py` +- `src/codex_usage_tracker/plugin_data/dashboard/dashboard.css` +- `src/codex_usage_tracker/context.py` +- `src/codex_usage_tracker/call_origin.py` +- `src/codex_usage_tracker/dashboard.py` +- `src/codex_usage_tracker/models.py` +- `src/codex_usage_tracker/parser.py` +- `src/codex_usage_tracker/schema.py` +- `src/codex_usage_tracker/store.py` +- `src/codex_usage_tracker/server.py` +- `src/codex_usage_tracker/json_contracts.py` +- `src/codex_usage_tracker/plugin_data/dashboard/dashboard.js` +- `src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js` +- `src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js` +- `docs/privacy.md` +- `docs/cli-json-schemas.md` +- `tests/test_dashboard_data.py` +- `tests/test_privacy.py` +- `tests/test_call_origin.py` +- `tests/test_parser.py` +- `tests/test_schema.py` +- `tests/test_store_dashboard_mcp.py` +- `tests/test_store_migrations.py` + +## Tests Run + +- M0 inventory: + - `git status --short --branch` + - `wc -l` over the requested source, docs, script, and CI files + - `rg` source inspection for raw-log, context, dashboard payload, and JS validation hot paths +- M0.1 table overflow containment: + - `python -m pytest tests/test_store_dashboard_mcp.py -q` +- M1 asset validation: + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js` + - `python scripts/check_release.py` +- M2 anchor removal: + - `python -m pytest tests/test_store_dashboard_mcp.py::test_dashboard_and_csv_are_aggregate_only tests/test_store_dashboard_mcp.py::test_context_loads_raw_log_only_on_demand tests/test_privacy.py::test_context_loading_is_explicit_redacted_and_not_static_html -q` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js` + - `python -m pytest tests/test_json_contracts.py -q` + - `python -m pytest tests/test_privacy.py -q` + - `python -m pytest tests/test_store_dashboard_mcp.py -q` + - `python scripts/check_release.py` +- M3 persisted call-origin metadata: + - `python -m pytest tests/test_call_origin.py tests/test_parser.py::test_parser_ignores_known_non_token_context_compaction_event tests/test_parser.py::test_parser_persists_call_origin_from_metadata_segments tests/test_store_dashboard_mcp.py::test_dashboard_payload_uses_persisted_call_origin_without_source_scan -q` failed before implementation because the pure classifier API was missing. + - `python -m pytest tests/test_call_origin.py tests/test_parser.py::test_parser_ignores_known_non_token_context_compaction_event tests/test_parser.py::test_parser_persists_call_origin_from_metadata_segments tests/test_schema.py tests/test_store_migrations.py::test_init_db_migrates_legacy_aggregate_table_without_data_loss tests/test_store_migrations.py::test_csv_export_keeps_current_columns_after_legacy_migration tests/test_store_dashboard_mcp.py::test_dashboard_payload_uses_persisted_call_origin_without_source_scan -q` + - `python -m pytest tests/test_parser.py tests/test_call_origin.py tests/test_store_migrations.py tests/test_privacy.py tests/test_store_dashboard_mcp.py -q` + - `python scripts/check_release.py` +- M4 dashboard query helper fields: + - `python -m pytest tests/test_parser.py::test_parser_persists_dashboard_helper_metadata tests/test_store_dashboard_mcp.py::test_upsert_refreshes_thread_adjacency_fields tests/test_store_dashboard_mcp.py::test_dashboard_history_scope_excludes_archived_rows_by_default -q` failed before implementation because the helper fields were missing. + - `python -m pytest tests/test_parser.py::test_parser_persists_dashboard_helper_metadata tests/test_store_dashboard_mcp.py::test_upsert_refreshes_thread_adjacency_fields tests/test_store_dashboard_mcp.py::test_dashboard_history_scope_excludes_archived_rows_by_default tests/test_store_migrations.py::test_init_db_migrates_legacy_aggregate_table_without_data_loss tests/test_schema.py -q` + - `python -m pytest tests/test_store_migrations.py tests/test_store_dashboard_mcp.py tests/test_privacy.py -q` + - `python scripts/check_release.py` + - `git diff --check` +- M5 optional API timing diagnostics: + - `python -m pytest tests/test_store_dashboard_mcp.py::test_dashboard_server_api_timing_diagnostics_are_opt_in_and_technical -q` failed before implementation because `diagnostics=true` did not return a diagnostics object. + - `python -m pytest tests/test_store_dashboard_mcp.py::test_dashboard_server_api_timing_diagnostics_are_opt_in_and_technical -q` + - `python -m pytest tests/test_store_dashboard_mcp.py -q` + - `python -m pytest tests/test_privacy.py -q` + - `python scripts/check_release.py` +- M6 single-pass context loading: + - `python -m pytest tests/test_store_dashboard_mcp.py::test_context_loading_uses_one_source_scan_for_evidence_and_serialized_estimate -q` failed before implementation because one context load opened the same source JSONL twice. + - `python -m pytest tests/test_store_dashboard_mcp.py::test_context_loading_uses_one_source_scan_for_evidence_and_serialized_estimate -q` + - `python -m pytest tests/test_store_dashboard_mcp.py -q` + - `python -m pytest tests/test_privacy.py -q` + - `python -m pytest tests/test_json_contracts.py -q` +- M7 client-side call adjacency index: + - `python -m pytest tests/test_dashboard_data.py -q` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js` + - `python scripts/check_release.py` +- M8 source-log-aware synthetic benchmark coverage: + - `python scripts/benchmark_synthetic_history.py --rows 100 --batch-size 25 --with-source-logs --json --enforce-thresholds` could not run because bare `python` is not installed on this machine. + - `.venv/bin/python scripts/benchmark_synthetic_history.py --rows 100 --batch-size 25 --with-source-logs --json --enforce-thresholds` + - `.venv/bin/python -m pytest tests/test_cli_release.py -q` + - `.venv/bin/python scripts/benchmark_synthetic_history.py --rows 2000 --with-source-logs --json --enforce-thresholds` +- M9 SQLite-backed live dashboard API slices: + - `.venv/bin/python -m pytest tests/test_store_dashboard_mcp.py::test_dashboard_server_live_sql_api_slices_are_aggregate_only -q` + - `.venv/bin/python -m pytest tests/test_store_dashboard_mcp.py -q` + - `.venv/bin/python -m pytest tests/test_json_contracts.py -q` initially failed because the new live API schema ids were not tracked; after adding contracts and docs, it passed. + - `.venv/bin/python -m pytest tests/test_privacy.py -q` + - `.venv/bin/python scripts/check_release.py` +- M10 materialized thread summaries: + - `.venv/bin/python -m pytest tests/test_store_dashboard_mcp.py::test_upsert_materializes_thread_summaries tests/test_store_dashboard_mcp.py::test_thread_summaries_keep_active_and_all_history_scopes_separate tests/test_store_dashboard_mcp.py::test_dashboard_server_live_sql_api_slices_are_aggregate_only tests/test_store_migrations.py::test_init_db_migrates_legacy_aggregate_table_without_data_loss -q` + - `.venv/bin/python -m ruff check src/codex_usage_tracker/store.py tests/test_store_dashboard_mcp.py tests/test_store_migrations.py` +- M11 incremental source-file refresh metadata: + - `.venv/bin/python -m pytest tests/test_store_dashboard_mcp.py::test_refresh_is_idempotent_and_summary_works tests/test_store_dashboard_mcp.py::test_refresh_indexes_only_appended_token_events_when_source_grows tests/test_store_dashboard_mcp.py::test_refresh_reports_skipped_corrupt_token_events tests/test_store_migrations.py::test_refresh_is_idempotent_after_legacy_migration tests/test_store_migrations.py::test_init_db_migrates_legacy_aggregate_table_without_data_loss -q` + - `.venv/bin/python -m ruff check src/codex_usage_tracker/store.py tests/test_store_dashboard_mcp.py tests/test_store_migrations.py` + - `.venv/bin/python -m pytest tests/test_store_dashboard_mcp.py tests/test_store_migrations.py tests/test_privacy.py -q` + - `.venv/bin/python -m mypy` +- M12 final validation: + - `.venv/bin/python -m ruff check .` + - `.venv/bin/python -m mypy` + - `.venv/bin/python -m pytest -q` + - `.venv/bin/python -m pytest --cov=codex_usage_tracker --cov-report=term-missing` + - `.venv/bin/python -m compileall src` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js` + - `node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js` + - `.venv/bin/python scripts/check_release.py` + - `git diff --check` + - `rm -rf dist build src/codex_usage_tracker.egg-info src/codex_usage_tracking.egg-info` + - `.venv/bin/python -m build` + - `.venv/bin/python -m twine check dist/*` + - `.venv/bin/python scripts/check_release.py --dist` + - `.venv/bin/python scripts/smoke_installed_package.py` + - `.venv/bin/python scripts/smoke_installed_package.py --docker` + +## Benchmarks Run + +- M4: + - `python scripts/benchmark_synthetic_history.py --rows 10000 --json --enforce-thresholds` initially failed after adjacency updates because population took `9.479820s` against a `1.600000s` threshold. + - After deferring bulk link recomputation and materializing the link window update, the same command passed. Latest recorded 10k timings included `populate_seconds: 0.357407`, `active_dashboard_query_seconds: 0.017709`, `dashboard_payload_active_seconds: 0.072064`, and no threshold failures. +- M8: + - `.venv/bin/python scripts/benchmark_synthetic_history.py --rows 100 --batch-size 25 --with-source-logs --json --enforce-thresholds` passed. Latest recorded source-log timings included `dashboard_payload_with_source_logs_seconds: 0.039320`, `context_load_early_line_seconds: 0.211319`, `context_load_middle_line_seconds: 0.003791`, and `context_load_late_line_seconds: 0.003682`, with `2` synthetic source logs and no threshold failures. + - `.venv/bin/python scripts/benchmark_synthetic_history.py --rows 2000 --with-source-logs --json --enforce-thresholds` passed. Latest recorded source-log timings included `dashboard_payload_with_source_logs_seconds: 0.047490`, `context_load_early_line_seconds: 0.209456`, `context_load_middle_line_seconds: 0.002840`, and `context_load_late_line_seconds: 0.012454`, with `8` synthetic source logs and no threshold failures. +- M12: + - `.venv/bin/python scripts/benchmark_synthetic_history.py --rows 10000 100000 --json --enforce-thresholds` initially failed only the `populate_seconds` threshold after M10/M11 made populate maintain materialized thread summaries and source-file metadata. The populate sentinel was recalibrated from `0.60` to `0.90` seconds per 10k rows while leaving read-path thresholds unchanged. Final 100k run passed with `populate_seconds: 6.887096`, `dashboard_payload_active_seconds: 0.163591`, `thread_summary_seconds: 0.144897`, and no threshold failures. + - `.venv/bin/python scripts/benchmark_synthetic_history.py --rows 1000 --with-source-logs --json --enforce-thresholds` passed with `dashboard_payload_with_source_logs_seconds: 0.040977`, `context_load_early_line_seconds: 0.188511`, `context_load_middle_line_seconds: 0.002758`, `context_load_late_line_seconds: 0.011821`, `4` synthetic source logs, and no threshold failures. + +## Known Remaining Slow Paths + +- Normal `dashboard_payload` no longer runs source-file call-origin annotation. +- Live `/api/usage` still calls `dashboard_payload`, but after M3 it should not open source JSONL files for call-origin metadata. M9 preserves this compatibility endpoint while adding SQL-backed live API slices; only direct investigator hydration currently consumes `/api/call` from the frontend. +- Active/all-history filtering now has a persisted `is_archived` flag and path fallback; future SQLite-backed API slices should reuse that helper instead of reintroducing path-only filtering. +- Per-thread adjacency is persisted after upsert and M7 makes the browser build a `record_id` adjacency index once per payload. Investigator lookup now uses that index and prefers loaded `previous_record_id`/`next_record_id` neighbors when available. +- Context loading defaults to `mode=quick`, which reads selected-turn evidence in one source-file scan, omits tool output, and returns a fast serialized upper-bound estimate without building tokenizer-counted serialized groups. `mode=full` / `Run full serialized analysis` keeps the richer serialized group analysis available on demand and times it separately as `serialized_estimate_ms`. +- M8 source-log benchmark mode now generates synthetic JSONL files, points synthetic aggregate rows at matching `token_count` lines, measures early/middle/late explicit context loads, and wraps source-log dashboard payload assembly with a guard that fails if a synthetic source file is opened. +- M9 adds additive SQL-backed live endpoints: `/api/status`, `/api/calls`, `/api/call`, `/api/threads`, `/api/thread-calls`, `/api/summary`, and `/api/recommendations`. The list/table frontend still uses `/api/usage` until a later split, while direct call-investigator fallback uses `/api/call`. +- Direct `view=call&record=...` investigator links can hydrate the selected aggregate row through `/api/call` when the current dashboard payload is filtered, stale, or does not include that record. +- M10 materializes per-thread active and all-history summary rows in SQLite so `/api/threads` can read pre-aggregated thread totals without grouping every usage row on each request. +- M11 adds a `source_files` metadata table with aggregate-only parser cursors. Refresh skips unchanged logs, seeks to the last indexed byte for append-only growth when a cursor is available, and fully replaces aggregate rows for changed/truncated source files. +- M5 adds opt-in timing fields for `/api/usage?diagnostics=true` and `/api/context?...&diagnostics=true`; diagnostics are technical metrics only and are absent unless explicitly requested. +- Static dashboard generation remains supported as the export/compatibility path; M9 added live API slices without removing generated dashboard HTML. +- Source-log reads are limited to refresh/indexing, explicit `/api/context` loading for one selected call, and explicit benchmark/diagnostic tooling. Normal `dashboard_payload` and `/api/usage` must remain aggregate-only. +- No evidence cache was implemented in this branch. +- Per-call byte offsets for context loading remain future work. Late calls in very large source JSONL files can still require scanning to the selected token line, although the default quick mode avoids the previous serialized-group cost. + +## Privacy Notes + +- Milestone 0 made no product behavior changes. +- The branch must keep all test data synthetic and must not persist raw transcript content. +- Persisted call-origin stores only categorical labels, reasons, and confidence values. Parser tests and privacy tests cover this with synthetic secret-bearing message/tool/compaction payloads. +- M4 persisted only aggregate navigation/scope fields: archived flag, conservative thread key, call index, and adjacent aggregate record ids. +- M5 diagnostics do not include raw text, prompts, tool output, source paths, or JSONL filenames. Context diagnostics include source file byte count and source line number only because the context payload itself already requires explicit token-protected on-demand loading. +- M9 live dashboard APIs return aggregate SQLite data and explicitly keep raw context out of status, calls, call, threads, thread-calls, summary, recommendations, and compatibility usage payloads. + +## Merge Blockers + +- `dashboard_payload` and `/api/usage` must stop opening source JSONL files. M3 covers the call-origin path; future milestones must preserve that invariant as APIs are split. +- The call investigator asset must be syntax-checked in CI and release validation. +- Raw call/thread anchors are removed; keep regression tests proving `call_anchors` and `thread_anchors` stay out of context payloads. +- Focused privacy tests must prove no raw prompts, assistant messages, tool output, replacement history, or raw JSONL fragments are persisted by default. +- Release checks and focused dashboard/context tests must pass before merge. + +## Deferred Work + +- Evidence cache is explicitly deferred. +- Any frontend rewrite from `/api/usage` to the new SQLite-backed endpoints should be split if it becomes broad. + +## Open Risks + +- Persisting `previous_record_id` and `next_record_id` may require careful thread-key semantics to avoid misleading adjacency across attached sessions. +- Call-origin classification is heuristic and must be confidence-labeled. +- SQL-backed thread and recommendation endpoints may need additional indexes to avoid moving the bottleneck from browser JS to SQLite queries. +- Existing compatibility tests may encode static-dashboard assumptions that need narrow updates as live APIs are introduced. diff --git a/docs/cli-json-schemas.md b/docs/cli-json-schemas.md index 5995634..cc3d2a6 100644 --- a/docs/cli-json-schemas.md +++ b/docs/cli-json-schemas.md @@ -51,6 +51,12 @@ Tracked schema ids: | `codex-usage-tracker-context-disabled-v1` | MCP `usage_call_context` when raw context is disabled | | `codex-usage-tracker-context-settings-v1` | Dashboard server `/api/context-settings` response | | `codex-usage-tracker-open-investigator-v1` | Dashboard server `/api/open-investigator` response | +| `codex-usage-tracker-live-api-v1` | Dashboard server live API payload family marker | +| `codex-usage-tracker-status-v1` | Dashboard server `/api/status` response | +| `codex-usage-tracker-calls-v1` | Dashboard server `/api/calls` response | +| `codex-usage-tracker-call-v1` | Dashboard server `/api/call` response | +| `codex-usage-tracker-threads-v1` | Dashboard server `/api/threads` response | +| `codex-usage-tracker-thread-calls-v1` | Dashboard server `/api/thread-calls` response | | `codex-usage-tracker-dashboard-v1` | CLI `dashboard --json`, MCP `generate_usage_dashboard()` | | `codex-usage-tracker-open-dashboard-v1` | CLI `open-dashboard --json` | | `codex-usage-tracker-serve-dashboard-v1` | CLI `serve-dashboard --json` startup payload | diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 3a1c1fc..076d320 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -136,7 +136,7 @@ The investigator separates evidence by confidence: - `Exact`: logged token callback counts, cost, Codex credits, cache ratio, model, effort, source, and context-window pressure. - `Derived`: previous/next calls in the same resolved thread and cache/accounting deltas versus the previous chronological call. -- `Estimated`: visible new-context estimates, serialized local JSONL upper bounds, candidate serialized-overhead buckets, and any remaining gap after that upper bound. These are attribution aids, not exact cached text spans. +- `Estimated`: visible new-context estimates, serialized local JSONL upper bounds, candidate serialized-overhead groups, and any remaining gap after that upper bound. These are attribution aids, not exact cached text spans. - `Evidence`: redacted local JSONL turn-log evidence loaded at runtime for the selected investigator call. Previous and next buttons move chronologically within the same resolved thread and keep the selected call in the URL. Cache diagnostics label common patterns such as warm cache reuse, cold resume or stale cache, partial cache miss, uncached spike, and post-compaction. Delta cards compare input, cached input, uncached input, output/reasoning output, and cache ratio to the previous call and use "cache/accounting delta" terminology because logs do not expose exact cached text spans. @@ -147,7 +147,9 @@ Previous and next buttons move chronologically within the same resolved thread a The details panel is structured for progressive disclosure. On desktop, it sticks inside the viewport and scrolls internally when the selected call has more fields or loaded context than can fit on screen. -The call investigator loads a bounded redacted turn-log evidence window by default when served from localhost with the context API enabled. Tool output is included by default and can be hidden with `Hide tool output`. Older surrounding evidence is collapsed by default and can be expanded or loaded explicitly. Visible evidence token estimates are calculated from the full selected-turn evidence set before display limiting, using `tiktoken` when available and a conservative character fallback only when the tokenizer is unavailable. The investigator also tokenizes a redacted raw-JSON representation of the same selected-turn log slice and reports it only as a serialized local upper bound. This upper bound can explain why visible text is much smaller than exact uncached input, but it can overcount because local JSONL includes client metadata that may not be prompt text. Bucket labels such as encrypted reasoning/state, local goal metadata, token callback metadata, and rate-limit metadata are counts only; raw text is not returned. `encrypted_content` is an opaque encrypted field found on some reasoning response items. The tracker cannot decrypt it and treats it as serialized state, not readable prompt, assistant, or tool text. Token-count context entries are labeled as the selected call, previous token count in the same turn, or earlier token count in the same turn when possible, and show call/session cumulative totals for input, cached input, uncached input, output, reasoning output, and total tokens. The evidence view also shows call anchors: the nearest visible message before the selected call and the selected call's reasoning output summary when the local log exposes one. These anchors are redacted and loaded only at runtime through the context API. +The call investigator loads a bounded redacted turn-log evidence window by default when served from localhost with the context API enabled. The default request uses `mode=quick`: tool output is omitted, normal size limits apply, and serialized local JSONL is reported as a fast character-based upper bound without bucket analysis. Older surrounding evidence is collapsed by default and can be expanded or loaded explicitly. Visible evidence token estimates are calculated from the full selected-turn evidence set before display limiting, using `tiktoken` when available and a conservative character fallback only when the tokenizer is unavailable. + +Use `Run full serialized analysis` when you specifically want tokenizer-counted serialized JSONL groups such as encrypted reasoning/state, local goal metadata, token callback metadata, and rate-limit metadata. This full mode can explain why visible text is much smaller than exact uncached input, but it can overcount because local JSONL includes client metadata that may not be prompt text. Raw grouped text is not returned. `encrypted_content` is an opaque encrypted field found on some reasoning response items. The tracker cannot decrypt it and treats it as serialized state, not readable prompt, assistant, or tool text. Token-count context entries are labeled as the selected call, previous token count in the same turn, or earlier token count in the same turn when possible, and show call/session cumulative totals for input, cached input, uncached input, output, reasoning output, and total tokens. For selected calls, the panel shows: @@ -164,10 +166,10 @@ For selected threads, the panel shows: - a compact thread timeline with recent calls, cost, credits, cache, context, and pricing cues - direct, subagent, auto-review, attached-call, and spawned-thread relationship counts -When served from localhost, the call investigator automatically fetches a full, redacted source excerpt for only that call. The details panel still uses an explicit `Show turn log evidence` action so hovering rows does not pull raw context unexpectedly. +When served from localhost, the call investigator automatically fetches quick, redacted source evidence for only that call. The details panel still uses an explicit `Show turn log evidence` action so hovering rows does not pull raw context unexpectedly. -- `Hide tool output` repeats the investigator request without tool output when you want a quieter evidence stream. -- If tool output is hidden, omitted tool-output entries can show a `Show tool output` button so you can reload from the specific context card that needs inspection. +- `Show tool output` reloads evidence with redacted, size-limited tool output included. +- `Run full serialized analysis` reloads evidence with tokenizer-counted serialized JSONL group analysis. - Compaction events are shown as metadata first. Replacement history is transcript-like content and is returned only after an explicit `Show compaction history` action, with redaction still applied. - Raw context is not written to SQLite, CSV, or the generated dashboard HTML. - If the server was started with `--no-context-api`, context loading starts off. Use `Enable context loading` in the details panel when you want to allow explicit row actions without restarting the dashboard server. diff --git a/docs/development.md b/docs/development.md index 25da28f..1479b63 100644 --- a/docs/development.md +++ b/docs/development.md @@ -118,6 +118,7 @@ python -m pytest --cov=codex_usage_tracker --cov-report=term-missing python -m compileall src node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js python scripts/check_release.py @@ -195,10 +196,11 @@ Use the synthetic benchmark script when changing SQLite filters, dashboard paylo ```bash python scripts/benchmark_synthetic_history.py --rows 10000 100000 --json --enforce-thresholds +python scripts/benchmark_synthetic_history.py --rows 1000 --with-source-logs --json --enforce-thresholds python scripts/benchmark_synthetic_history.py --rows 500000 --json --enforce-thresholds ``` -The script creates synthetic aggregate-only SQLite databases and times common release-sensitive paths. It does not read real Codex logs. +The default mode creates synthetic aggregate-only SQLite databases and times common release-sensitive paths. The optional `--with-source-logs` mode writes synthetic JSONL source files, points aggregate rows at matching synthetic `token_count` lines, times explicit one-call context loading, and fails if normal dashboard payload assembly opens those generated source files. Neither mode reads real Codex logs. Thresholds are regression sentinels, not universal performance guarantees. Each timed path uses: @@ -223,8 +225,14 @@ Tracked timings: | `recommendations_report_seconds` | Recommendation report and thread rollup | | `pricing_coverage_seconds` | Pricing coverage report | | `project_summary_seconds` | Project summary report | +| `dashboard_payload_with_source_logs_seconds` | Dashboard payload assembly while synthetic source logs exist; this path must not open those source logs | +| `context_load_early_line_seconds` | Explicit context load for an early synthetic source-log line | +| `context_load_middle_line_seconds` | Explicit context load for a middle synthetic source-log line | +| `context_load_late_line_seconds` | Explicit context load for a late synthetic source-log line | -The normal CI smoke uses a tiny synthetic history with `--enforce-thresholds` and a small `--threshold-scale` allowance so coverage instrumentation and shared runner noise do not create false failures. The 10k/100k runs are a practical local gate for performance-sensitive changes; the 500k run is the release-sized gate and can take about a minute on a modern laptop because recommendations and project summary intentionally scan all aggregate rows. +Source-log benchmark JSON also reports `source_logs_generated`, `source_log_bytes`, `context_loads`, `context_payload_json_bytes`, `source_scan_ms`, and `serialized_estimate_ms` for explicit context loading. + +The normal CI smoke uses a tiny synthetic history with `--enforce-thresholds` and a small `--threshold-scale` allowance so coverage instrumentation and shared runner noise do not create false failures. The 10k/100k runs are a practical local gate for performance-sensitive changes; the source-log run is the local gate for context/evidence work; the 500k run is the release-sized gate and can take about a minute on a modern laptop because recommendations and project summary intentionally scan all aggregate rows. ## Release Checklist @@ -240,6 +248,7 @@ python -m pytest --cov=codex_usage_tracker --cov-report=term-missing python -m compileall src node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_format.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard.js node --check src/codex_usage_tracker/plugin_data/dashboard/dashboard_state.js python scripts/check_release.py @@ -276,7 +285,7 @@ Do not create or push release tags without maintainer approval. Publishing uses GitHub Actions Trusted Publishing through `.github/workflows/publish.yml`; do not upload from a local machine and do not add PyPI or TestPyPI API tokens. -The first public package release, `0.3.0`, was published on June 8, 2026. Patch release `0.3.1` followed the same day to ship the live-dashboard skill launch fix. Patch release `0.3.2` made dashboard launch refresh the default and added runtime enablement for context loading. Minor release `0.4.0` added Python 3.14 support, release recovery docs, stricter privacy/support-bundle regression coverage, and large-history benchmark thresholds. Patch release `0.4.1` was published by workflow dispatch from `main`; it hardened the PyPI publish workflow and checked off completed 1.0 readiness gates. Minor release `0.5.0` added dashboard localization support and starter language catalogs. +The first public package release, `0.3.0`, was published on June 8, 2026. Patch release `0.3.1` followed the same day to ship the live-dashboard skill launch fix. Patch release `0.3.2` made dashboard launch refresh the default and added runtime enablement for context loading. Minor release `0.4.0` added Python 3.14 support, release recovery docs, stricter privacy/support-bundle regression coverage, and large-history benchmark thresholds. Patch release `0.4.1` was published by workflow dispatch from `main`; it hardened the PyPI publish workflow and checked off completed 1.0 readiness gates. Minor release `0.5.0` added dashboard localization support and initial language catalogs. - GitHub Release: `https://github.com/douglasmonsky/codex-usage-tracker/releases/tag/v0.3.0` - GitHub Release: `https://github.com/douglasmonsky/codex-usage-tracker/releases/tag/v0.3.1` diff --git a/docs/privacy.md b/docs/privacy.md index 6657904..2ff8ac3 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -10,6 +10,10 @@ The local SQLite database is stored at `~/.codex-usage-tracker/usage.sqlite3` by - model, reasoning effort, context window - token counts and derived efficiency ratios - subagent source, role, nickname, parent session id, and parent thread name when present +- call-origin category, reason, and confidence labels derived from event metadata during indexing +- archived-session flag, conservative thread key, and adjacent aggregate record ids for dashboard navigation +- materialized thread-level aggregate summaries for active and all-history scopes +- source-file refresh metadata such as path, path hash, size, mtime, indexed line/byte offsets, latest aggregate record id, parser diagnostics, and last indexed time - pricing, credit, allowance, recommendation, and project metadata derived from aggregate fields ## Not Stored @@ -25,11 +29,13 @@ The parser intentionally does not store: Those fields are not written to SQLite, CSV exports, generated dashboard HTML, or synthetic screenshots. +Call-origin metadata is heuristic and confidence-labeled. It stores categories such as `user`, `codex`, or `unknown` plus a reason such as `user_message`, `tool_result`, `post_compaction`, or `agent_continuation`. It does not store the message text, tool output, compaction replacement text, or raw JSONL fragment that produced the category. + ## On-Demand Context -`usage_call_context`, `codex-usage-tracker context`, and the `serve-dashboard` context endpoint read a single source JSONL file only when explicitly requested. Returned context is redacted for common secret patterns and capped in size by default for CLI/MCP requests. The call investigator uses the same endpoint at runtime and requests full redacted evidence for the selected call when the local context API is enabled; that still does not persist raw context into SQLite, CSV, support bundles, or generated dashboard HTML. +`usage_call_context`, `codex-usage-tracker context`, and the `serve-dashboard` context endpoint read a single source JSONL file only when explicitly requested. Returned context is redacted for common secret patterns and capped in size by default for CLI/MCP requests. The call investigator uses the same endpoint at runtime and requests quick redacted evidence for the selected call when the local context API is enabled; that still does not persist raw context into SQLite, CSV, support bundles, or generated dashboard HTML. -Tool output is omitted by default for CLI/MCP context requests. The call investigator includes tool output by default and offers `Hide tool output` for a quieter evidence stream. Compacted replacement history remains omitted by default everywhere. Compaction metadata can show that replacement history exists, its entry count, and the source line, but the replacement text is returned only when explicitly requested for that selected call and is redacted before display. +Tool output is omitted by default for CLI/MCP and dashboard investigator context requests. The call investigator offers `Show tool output` when redacted, size-limited tool output is needed. Full serialized JSONL group analysis is also opt-in through `mode=full` / `Run full serialized analysis`; default quick mode returns only a fast serialized upper-bound estimate. Compacted replacement history remains omitted by default everywhere. Compaction metadata can show that replacement history exists, its entry count, and the source line, but the replacement text is returned only when explicitly requested for that selected call and is redacted before display. Dashboard context loading can start off and then be enabled from the local details panel without restarting: @@ -56,6 +62,9 @@ The localhost server: - protects refresh/context API calls with a random per-server token - can disable the context API entirely - refreshes aggregate rows without embedding raw transcript content into the dashboard +- serves `/api/status`, `/api/calls`, `/api/call`, `/api/threads`, `/api/thread-calls`, `/api/summary`, `/api/recommendations`, and the compatibility `/api/usage` endpoint from aggregate SQLite data without loading raw source JSONL context + +Source JSONL reads happen during refresh/indexing, explicit on-demand context loading for one selected call, and explicit synthetic benchmark/diagnostic runs. Live refresh records metadata and aggregate-only parser cursors about source files so unchanged logs can be skipped and append-only growth can be parsed from the last indexed byte; that metadata does not store prompts, assistant messages, tool output, compaction replacement text, raw JSONL fragments, raw context, or reconstructed transcript evidence. The live aggregate APIs do not return that raw content either. ## Privacy Modes diff --git a/scripts/benchmark_synthetic_history.py b/scripts/benchmark_synthetic_history.py index aef169a..ac2858b 100755 --- a/scripts/benchmark_synthetic_history.py +++ b/scripts/benchmark_synthetic_history.py @@ -9,7 +9,9 @@ import sys import tempfile import time -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Iterator +from contextlib import contextmanager +from dataclasses import dataclass from pathlib import Path from typing import Any, TypeVar @@ -18,6 +20,7 @@ if str(SRC_PATH) not in sys.path: sys.path.insert(0, str(SRC_PATH)) +from codex_usage_tracker.context import load_call_context # noqa: E402 from codex_usage_tracker.dashboard import dashboard_payload # noqa: E402 from codex_usage_tracker.models import UsageEvent # noqa: E402 from codex_usage_tracker.reports import ( # noqa: E402 @@ -30,12 +33,13 @@ init_db, query_dashboard_event_count, query_dashboard_events, + refresh_usage_event_links, upsert_usage_events, ) DEFAULT_ROW_COUNTS = (10_000, 100_000, 500_000) BENCHMARK_THRESHOLDS: dict[str, dict[str, float]] = { - "populate_seconds": {"base_seconds": 1.0, "per_10k_seconds": 0.60}, + "populate_seconds": {"base_seconds": 1.0, "per_10k_seconds": 0.90}, "active_dashboard_query_seconds": {"base_seconds": 0.25, "per_10k_seconds": 0.05}, "all_history_dashboard_query_seconds": {"base_seconds": 0.25, "per_10k_seconds": 0.05}, "since_until_query_seconds": {"base_seconds": 0.25, "per_10k_seconds": 0.05}, @@ -46,10 +50,30 @@ "recommendations_report_seconds": {"base_seconds": 1.0, "per_10k_seconds": 0.65}, "pricing_coverage_seconds": {"base_seconds": 0.50, "per_10k_seconds": 0.06}, "project_summary_seconds": {"base_seconds": 1.0, "per_10k_seconds": 0.45}, + "dashboard_payload_with_source_logs_seconds": { + "base_seconds": 0.75, + "per_10k_seconds": 0.20, + }, + "context_load_early_line_seconds": {"base_seconds": 0.50, "per_10k_seconds": 0.08}, + "context_load_middle_line_seconds": {"base_seconds": 0.75, "per_10k_seconds": 0.12}, + "context_load_late_line_seconds": {"base_seconds": 1.00, "per_10k_seconds": 0.16}, } T = TypeVar("T") +@dataclass(frozen=True) +class SourceLogRecord: + source_file: Path + line_number: int + + +@dataclass(frozen=True) +class SourceLogBundle: + records: list[SourceLogRecord] + paths: frozenset[Path] + bytes_written: int + + def main() -> int: parser = argparse.ArgumentParser() parser.add_argument( @@ -74,6 +98,14 @@ def main() -> int: default=1.0, help="Multiplier for timing thresholds on slower local machines. Defaults to 1.0.", ) + parser.add_argument( + "--with-source-logs", + action="store_true", + help=( + "Generate synthetic JSONL source files and benchmark explicit context loading. " + "Generated content is synthetic only and is never copied from real Codex logs." + ), + ) args = parser.parse_args() if any(count <= 0 for count in args.rows): @@ -98,6 +130,7 @@ def main() -> int: db_dir=db_dir, batch_size=args.batch_size, threshold_scale=args.threshold_scale, + with_source_logs=args.with_source_logs, ) for row_count in args.rows ] @@ -138,15 +171,30 @@ def benchmark_size( db_dir: Path, batch_size: int, threshold_scale: float = 1.0, + with_source_logs: bool = False, ) -> dict[str, Any]: db_path = db_dir / f"synthetic-{row_count}.sqlite3" if db_path.exists(): db_path.unlink() config = _write_benchmark_config(db_dir) + source_bundle = ( + _write_synthetic_source_logs(db_dir / f"source-logs-{row_count}", row_count) + if with_source_logs + else None + ) populate_start = time.perf_counter() for start in range(0, row_count, batch_size): end = min(start + batch_size, row_count) - upsert_usage_events(_synthetic_events(start, end), db_path=db_path) + upsert_usage_events( + _synthetic_events( + start, + end, + source_records=source_bundle.records if source_bundle else None, + ), + db_path=db_path, + refresh_links=False, + ) + refresh_usage_event_links(db_path=db_path) populate_seconds = time.perf_counter() - populate_start active_rows, active_dashboard_query_seconds = _time_call( @@ -181,8 +229,8 @@ def benchmark_size( min_tokens=9_000, ) ) - active_payload, dashboard_payload_active_seconds = _time_call( - lambda: dashboard_payload( + def payload_action() -> dict[str, Any]: + return dashboard_payload( db_path=db_path, limit=500, pricing_path=config["pricing_path"], @@ -191,7 +239,13 @@ def benchmark_size( projects_path=config["projects_path"], include_archived=False, ) - ) + + if source_bundle: + active_payload, dashboard_payload_active_seconds = _time_call( + lambda: _run_without_source_log_reads(source_bundle.paths, payload_action) + ) + else: + active_payload, dashboard_payload_active_seconds = _time_call(payload_action) thread_summary, thread_summary_seconds = _time_call( lambda: build_summary_report( db_path=db_path, @@ -255,6 +309,16 @@ def benchmark_size( "pricing_coverage_seconds": pricing_coverage_seconds, "project_summary_seconds": project_summary_seconds, } + context_metrics: dict[str, Any] = {} + if source_bundle: + timings["dashboard_payload_with_source_logs_seconds"] = ( + dashboard_payload_active_seconds + ) + context_metrics = _benchmark_context_loads( + db_path=db_path, + row_count=row_count, + ) + timings.update(context_metrics["timings"]) threshold_results, threshold_failures = _evaluate_thresholds( timings, row_count=row_count, @@ -286,6 +350,13 @@ def benchmark_size( "recommendations_rows": recommendations.payload["row_count"], "pricing_coverage_rows": len(pricing_coverage.payload["rows"]), "project_summary_rows": len(project_summary.rows), + "source_logs_generated": len(source_bundle.paths) if source_bundle else 0, + "source_log_bytes": source_bundle.bytes_written if source_bundle else 0, + "context_loads": context_metrics.get("loads", {}), + "context_load_seconds": context_metrics.get("context_load_seconds"), + "context_payload_json_bytes": context_metrics.get("context_payload_json_bytes"), + "source_scan_ms": context_metrics.get("source_scan_ms"), + "serialized_estimate_ms": context_metrics.get("serialized_estimate_ms"), "threshold_status": "fail" if threshold_failures else "pass", "thresholds": threshold_results, "threshold_failures": threshold_failures, @@ -342,6 +413,92 @@ def _time_call(action: Callable[[], T]) -> tuple[T, float]: return value, round(time.perf_counter() - start, 6) +def _run_without_source_log_reads( + source_paths: frozenset[Path], + action: Callable[[], T], +) -> T: + with _fail_on_source_log_open(source_paths): + return action() + + +@contextmanager +def _fail_on_source_log_open(source_paths: frozenset[Path]) -> Iterator[None]: + blocked = {path.resolve() for path in source_paths} + original_open = Path.open + + def guarded_open(self: Path, *args: Any, **kwargs: Any) -> Any: + try: + resolved = self.resolve() + except OSError: + resolved = self + if resolved in blocked: + raise RuntimeError(f"dashboard_payload opened synthetic source log: {self}") + return original_open(self, *args, **kwargs) + + Path.open = guarded_open # type: ignore[method-assign] + try: + yield + finally: + Path.open = original_open # type: ignore[method-assign] + + +def _benchmark_context_loads( + *, + db_path: Path, + row_count: int, +) -> dict[str, Any]: + targets = { + "early": 0, + "middle": row_count // 2, + "late": row_count - 1, + } + timings: dict[str, float] = {} + loads: dict[str, dict[str, Any]] = {} + context_load_seconds: float | None = None + context_payload_json_bytes: int | None = None + source_scan_ms: float | None = None + serialized_estimate_ms: float | None = None + for label, index in targets.items(): + record_id = f"record-{index:08d}" + payload, elapsed = _time_call( + lambda record_id=record_id: load_call_context( + record_id=record_id, + db_path=db_path, + max_chars=0, + max_entries=0, + include_tool_output=True, + include_compaction_history=True, + diagnostics=True, + ) + ) + timing_name = f"context_load_{label}_line_seconds" + timings[timing_name] = elapsed + diagnostics = payload.get("diagnostics") if isinstance(payload.get("diagnostics"), dict) else {} + loads[label] = { + "record_id": record_id, + "seconds": elapsed, + "entries_returned": len(payload.get("entries") or []), + "visible_char_count": payload.get("visible_char_count"), + "visible_token_estimate": payload.get("visible_token_estimate"), + "context_payload_json_bytes": diagnostics.get("json_bytes"), + "source_scan_ms": diagnostics.get("source_scan_ms"), + "serialized_estimate_ms": diagnostics.get("serialized_estimate_ms"), + } + if label == "middle": + context_load_seconds = elapsed + context_payload_json_bytes = _optional_int(diagnostics.get("json_bytes")) + source_scan_ms = _optional_float(diagnostics.get("source_scan_ms")) + serialized_estimate_ms = _optional_float(diagnostics.get("serialized_estimate_ms")) + return { + "timings": timings, + "loads": loads, + "context_load_seconds": context_load_seconds, + "context_payload_json_bytes": context_payload_json_bytes, + "source_scan_ms": source_scan_ms, + "serialized_estimate_ms": serialized_estimate_ms, + } + + def _evaluate_thresholds( timings: dict[str, float], *, @@ -373,39 +530,267 @@ def _evaluate_thresholds( return results, failures -def _synthetic_events(start: int, end: int) -> Iterable[UsageEvent]: +def _optional_int(value: object) -> int | None: + try: + return int(value) if value is not None else None + except (TypeError, ValueError): + return None + + +def _optional_float(value: object) -> float | None: + try: + return float(value) if value is not None else None + except (TypeError, ValueError): + return None + + +def _write_synthetic_source_logs(source_dir: Path, row_count: int) -> SourceLogBundle: + events_per_file = 500 + records: list[SourceLogRecord | None] = [None] * row_count + lines_by_path: dict[Path, list[str]] = {} + for index in range(row_count): + path = _synthetic_source_file(source_dir, index, events_per_file=events_per_file) + lines = lines_by_path.setdefault(path, []) + if not lines: + lines.append(_jsonl_line("session_meta", {"id": f"session-{index % 2500:04d}"})) + turn_id = f"turn-{index:08d}" + for envelope in _synthetic_source_envelopes(index, turn_id): + lines.append(json.dumps(envelope, separators=(",", ":"), sort_keys=True) + "\n") + records[index] = SourceLogRecord( + source_file=path, + line_number=len(lines), + ) + + bytes_written = 0 + for path, lines in lines_by_path.items(): + path.parent.mkdir(parents=True, exist_ok=True) + payload = "".join(lines) + path.write_text(payload, encoding="utf-8") + bytes_written += len(payload.encode("utf-8")) + + return SourceLogBundle( + records=[record for record in records if record is not None], + paths=frozenset(lines_by_path), + bytes_written=bytes_written, + ) + + +def _synthetic_source_file( + source_dir: Path, + index: int, + *, + events_per_file: int, +) -> Path: + scope = "archived_sessions" if index % 11 == 0 else "sessions" + return source_dir / scope / f"rollout-synthetic-{index // events_per_file:05d}.jsonl" + + +def _synthetic_source_envelopes(index: int, turn_id: str) -> list[dict[str, Any]]: + metrics = _synthetic_token_metrics(index) + day = (index % 28) + 1 + timestamp = f"2026-05-{day:02d}T12:{index % 60:02d}:00Z" + envelopes = [ + _envelope( + "turn_context", + timestamp, + { + "turn_id": turn_id, + "model": metrics["model"], + "effort": metrics["effort"], + "cwd": f"/tmp/project-{index % 50}", + "current_date": f"2026-05-{day:02d}", + "timezone": "UTC", + "summary": f"Synthetic benchmark turn {index}", + }, + ), + _envelope( + "response_item", + timestamp, + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": ( + "Synthetic user request for benchmark coverage. " + f"Thread bucket {index % 500}, call {index}." + ), + } + ], + }, + ), + _envelope( + "response_item", + timestamp, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": ( + "Synthetic assistant progress update for benchmark coverage. " + "No real transcript content is used." + ), + } + ], + }, + ), + _envelope( + "response_item", + timestamp, + { + "type": "function_call_output", + "name": "exec_command", + "output": ( + "Synthetic tool output placeholder. " + "This is deterministic benchmark text, not a real command result." + ), + }, + ), + ] + if index % 37 == 0: + envelopes.append( + _envelope( + "compacted", + timestamp, + { + "message": "Synthetic compaction marker.", + "replacement_history": [ + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "Synthetic compacted replacement summary.", + } + ], + } + ], + }, + ) + ) + envelopes.append(_synthetic_token_envelope(index, timestamp, metrics)) + return envelopes + + +def _synthetic_token_envelope( + index: int, + timestamp: str, + metrics: dict[str, Any], +) -> dict[str, Any]: + cumulative_input_tokens = metrics["input_tokens"] + index * 5 + cumulative_cached_input_tokens = metrics["cached_input_tokens"] + index + cumulative_output_tokens = metrics["output_tokens"] + index + cumulative_reasoning_output_tokens = metrics["reasoning_tokens"] + index // 2 + cumulative_total_tokens = metrics["total_tokens"] + index * 10 + return _envelope( + "event_msg", + timestamp, + { + "type": "token_count", + "info": { + "last_token_usage": { + "input_tokens": metrics["input_tokens"], + "cached_input_tokens": metrics["cached_input_tokens"], + "output_tokens": metrics["output_tokens"], + "reasoning_output_tokens": metrics["reasoning_tokens"], + "total_tokens": metrics["total_tokens"], + }, + "total_token_usage": { + "input_tokens": cumulative_input_tokens, + "cached_input_tokens": cumulative_cached_input_tokens, + "output_tokens": cumulative_output_tokens, + "reasoning_output_tokens": cumulative_reasoning_output_tokens, + "total_tokens": cumulative_total_tokens, + }, + "model_context_window": 200_000, + }, + }, + ) + + +def _jsonl_line(entry_type: str, payload: dict[str, Any]) -> str: + return json.dumps(_envelope(entry_type, "2026-05-01T00:00:00Z", payload)) + "\n" + + +def _envelope(entry_type: str, timestamp: str, payload: dict[str, Any]) -> dict[str, Any]: + return { + "timestamp": timestamp, + "type": entry_type, + "payload": payload, + } + + +def _synthetic_token_metrics(index: int) -> dict[str, Any]: + is_review = index % 17 == 0 + model = "codex-auto-review" if is_review else "gpt-5.5" + effort = "high" if index % 3 == 0 else "low" + input_tokens = 8_000 + (index % 9_000) + cached_input_tokens = index % 2_500 + output_tokens = 80 + (index % 450) + reasoning_tokens = 10 + (index % 120) + return { + "model": model, + "effort": effort, + "input_tokens": input_tokens, + "cached_input_tokens": cached_input_tokens, + "output_tokens": output_tokens, + "reasoning_tokens": reasoning_tokens, + "total_tokens": input_tokens + output_tokens, + } + + +def _synthetic_events( + start: int, + end: int, + *, + source_records: list[SourceLogRecord] | None = None, +) -> Iterable[UsageEvent]: for index in range(start, end): day = (index % 28) + 1 is_review = index % 17 == 0 is_subagent = index % 13 == 0 - model = "codex-auto-review" if is_review else "gpt-5.5" - effort = "high" if index % 3 == 0 else "low" - input_tokens = 8_000 + (index % 9_000) - cached_input_tokens = index % 2_500 - output_tokens = 80 + (index % 450) - reasoning_tokens = 10 + (index % 120) - total_tokens = input_tokens + output_tokens + metrics = _synthetic_token_metrics(index) session_id = f"session-{index % 2500:04d}" + source_record = source_records[index] if source_records is not None else None source_file = ( - f"/tmp/synthetic/archived_sessions/{index % 2500}.jsonl" - if index % 11 == 0 - else f"/tmp/synthetic/{index % 2500}.jsonl" + str(source_record.source_file) + if source_record is not None + else ( + f"/tmp/synthetic/archived_sessions/{index % 2500}.jsonl" + if index % 11 == 0 + else f"/tmp/synthetic/{index % 2500}.jsonl" + ) ) + thread_name = f"Thread {index % 500}" + call_initiator = "codex" if is_subagent or is_review else "user" + call_reason = "thread_source" if is_subagent or is_review else "user_message" yield UsageEvent( record_id=f"record-{index:08d}", session_id=session_id, - thread_name=f"Thread {index % 500}", + thread_name=thread_name, session_updated_at=f"2026-05-{day:02d}T23:00:00Z", event_timestamp=f"2026-05-{day:02d}T12:{index % 60:02d}:00Z", source_file=source_file, - line_number=index + 1, + line_number=source_record.line_number if source_record is not None else index + 1, turn_id=f"turn-{index:08d}", turn_timestamp=f"2026-05-{day:02d}T12:{index % 60:02d}:00Z", cwd=f"/tmp/project-{index % 50}", - model=model, - effort=effort, + model=metrics["model"], + effort=metrics["effort"], current_date=f"2026-05-{day:02d}", timezone="UTC", + call_initiator=call_initiator, + call_initiator_reason=call_reason, + call_initiator_confidence="medium" if call_initiator == "codex" else "high", + is_archived=1 if "/archived_sessions/" in source_file else 0, + thread_key=f"thread:{thread_name}", + thread_call_index=None, + previous_record_id=None, + next_record_id=None, thread_source="subagent" if is_subagent or is_review else "user", subagent_type="guardian" if is_review else "thread_spawn" if is_subagent else None, agent_role="reviewer" if is_review else "worker" if is_subagent else None, @@ -414,16 +799,16 @@ def _synthetic_events(start: int, end: int) -> Iterable[UsageEvent]: parent_thread_name=f"Thread {(index - 1) % 500}" if is_subagent or is_review else None, parent_session_updated_at=f"2026-05-{day:02d}T22:00:00Z" if is_subagent or is_review else None, model_context_window=200_000, - input_tokens=input_tokens, - cached_input_tokens=cached_input_tokens, - output_tokens=output_tokens, - reasoning_output_tokens=reasoning_tokens, - total_tokens=total_tokens, - cumulative_input_tokens=input_tokens + index * 5, - cumulative_cached_input_tokens=cached_input_tokens + index, - cumulative_output_tokens=output_tokens + index, - cumulative_reasoning_output_tokens=reasoning_tokens + index // 2, - cumulative_total_tokens=total_tokens + index * 10, + input_tokens=metrics["input_tokens"], + cached_input_tokens=metrics["cached_input_tokens"], + output_tokens=metrics["output_tokens"], + reasoning_output_tokens=metrics["reasoning_tokens"], + total_tokens=metrics["total_tokens"], + cumulative_input_tokens=metrics["input_tokens"] + index * 5, + cumulative_cached_input_tokens=metrics["cached_input_tokens"] + index, + cumulative_output_tokens=metrics["output_tokens"] + index, + cumulative_reasoning_output_tokens=metrics["reasoning_tokens"] + index // 2, + cumulative_total_tokens=metrics["total_tokens"] + index * 10, ) diff --git a/src/codex_usage_tracker/call_origin.py b/src/codex_usage_tracker/call_origin.py index f67847a..f05794a 100644 --- a/src/codex_usage_tracker/call_origin.py +++ b/src/codex_usage_tracker/call_origin.py @@ -2,93 +2,35 @@ from __future__ import annotations -import json -from collections import defaultdict +from collections.abc import Iterable, Mapping from dataclasses import dataclass -from pathlib import Path from typing import Any @dataclass(frozen=True) -class _EventFlags: +class CallOriginFlags: + """Metadata-only signals observed before one token_count callback.""" + user_message: bool = False compaction: bool = False tool_result: bool = False codex_activity: bool = False + @property + def has_signal(self) -> bool: + return ( + self.user_message + or self.compaction + or self.tool_result + or self.codex_activity + ) -def annotate_rows_with_call_origin(rows: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Annotate dashboard rows with derived call-level initiator metadata. - The persisted ``thread_source`` field is session-level. A normal user-created - thread can still contain many Codex-initiated model calls after tool results, - agent continuations, or compactions. This helper reads only source JSONL event - metadata around token-count lines. It does not copy prompt, assistant, or tool - text into the returned rows. - """ +def event_flags_from_envelope(envelope: object) -> CallOriginFlags: + """Return categorical call-origin flags without reading raw text fields.""" - annotated = [dict(row) for row in rows] - rows_by_file: dict[str, dict[int, list[dict[str, Any]]]] = defaultdict( - lambda: defaultdict(list) - ) - for row in annotated: - source_file = row.get("source_file") - line_number = _positive_int(row.get("line_number")) - if isinstance(source_file, str) and source_file and line_number is not None: - rows_by_file[source_file][line_number].append(row) - else: - row.update(_fallback_origin(row, reason="missing_source")) - - for source_file, rows_by_line in rows_by_file.items(): - annotations = _classify_source_file(Path(source_file), set(rows_by_line)) - for line_number, line_rows in rows_by_line.items(): - annotation = annotations.get(line_number) - for row in line_rows: - row.update(annotation or _fallback_origin(row, reason="source_unavailable")) - return annotated - - -def _classify_source_file(path: Path, target_lines: set[int]) -> dict[int, dict[str, str]]: - if not target_lines or not path.exists(): - return {} - max_line = max(target_lines) - annotations: dict[int, dict[str, str]] = {} - segment: list[_EventFlags] = [] - try: - with path.open(encoding="utf-8") as handle: - for line_number, line in enumerate(handle, start=1): - if line_number > max_line: - break - try: - envelope = json.loads(line) - except json.JSONDecodeError: - continue - if _is_token_count(envelope): - if line_number in target_lines: - annotations[line_number] = _classify_segment(segment) - segment = [] - continue - segment.append(_event_flags(envelope)) - except OSError: - return {} - return annotations - - -def _classify_segment(segment: list[_EventFlags]) -> dict[str, str]: - if any(event.user_message for event in segment): - return _origin("user", "user_message", "high") - if any(event.compaction for event in segment): - return _origin("codex", "post_compaction", "high") - if any(event.tool_result for event in segment): - return _origin("codex", "tool_result", "high") - if any(event.codex_activity for event in segment): - return _origin("codex", "agent_continuation", "medium") - return _origin("unknown", "no_signal", "low") - - -def _event_flags(envelope: object) -> _EventFlags: if not isinstance(envelope, dict): - return _EventFlags() + return CallOriginFlags() payload = envelope.get("payload") if not isinstance(payload, dict): payload = {} @@ -116,7 +58,7 @@ def _event_flags(envelope: object) -> _EventFlags: and payload_type in {"message", "reasoning", "function_call", "tool_search_call"} and role != "user" ) - return _EventFlags( + return CallOriginFlags( user_message=user_message, compaction=compaction, tool_result=tool_result, @@ -124,18 +66,24 @@ def _event_flags(envelope: object) -> _EventFlags: ) -def _is_token_count(envelope: object) -> bool: - if not isinstance(envelope, dict): - return False - payload = envelope.get("payload") - return ( - envelope.get("type") == "event_msg" - and isinstance(payload, dict) - and payload.get("type") == "token_count" - ) +def classify_call_origin(segment: Iterable[CallOriginFlags]) -> dict[str, str]: + """Classify who most likely initiated a model call from metadata-only signals.""" + + flags = list(segment) + if any(event.user_message for event in flags): + return _origin("user", "user_message", "high") + if any(event.compaction for event in flags): + return _origin("codex", "post_compaction", "high") + if any(event.tool_result for event in flags): + return _origin("codex", "tool_result", "high") + if any(event.codex_activity for event in flags): + return _origin("codex", "agent_continuation", "medium") + return _origin("unknown", "no_signal", "low") + +def fallback_call_origin(row: Mapping[str, Any]) -> dict[str, str]: + """Return cheap categorical origin for migrated rows missing persisted metadata.""" -def _fallback_origin(row: dict[str, Any], *, reason: str) -> dict[str, str]: if ( row.get("model") == "codex-auto-review" or row.get("thread_source") == "subagent" @@ -143,7 +91,24 @@ def _fallback_origin(row: dict[str, Any], *, reason: str) -> dict[str, str]: or row.get("parent_session_id") ): return _origin("codex", "thread_source", "medium") - return _origin("unknown", reason, "low") + return _origin("unknown", "missing_origin", "low") + + +def ensure_call_origin(row: Mapping[str, Any]) -> dict[str, Any]: + """Copy a row and fill missing persisted origin fields without source-log reads.""" + + copied = dict(row) + if ( + isinstance(copied.get("call_initiator"), str) + and copied["call_initiator"] + and isinstance(copied.get("call_initiator_reason"), str) + and copied["call_initiator_reason"] + and isinstance(copied.get("call_initiator_confidence"), str) + and copied["call_initiator_confidence"] + ): + return copied + copied.update(fallback_call_origin(copied)) + return copied def _origin(initiator: str, reason: str, confidence: str) -> dict[str, str]: @@ -152,11 +117,3 @@ def _origin(initiator: str, reason: str, confidence: str) -> dict[str, str]: "call_initiator_reason": reason, "call_initiator_confidence": confidence, } - - -def _positive_int(value: object) -> int | None: - try: - parsed = int(value) # type: ignore[arg-type] - except (TypeError, ValueError): - return None - return parsed if parsed > 0 else None diff --git a/src/codex_usage_tracker/context.py b/src/codex_usage_tracker/context.py index 95c1fcb..759a0e6 100644 --- a/src/codex_usage_tracker/context.py +++ b/src/codex_usage_tracker/context.py @@ -6,6 +6,7 @@ from functools import lru_cache from math import ceil from pathlib import Path +from time import perf_counter from typing import Any from codex_usage_tracker.paths import DEFAULT_DB_PATH @@ -14,7 +15,9 @@ DEFAULT_CONTEXT_CHARS = 20_000 DEFAULT_CONTEXT_ENTRIES = 80 -ANCHOR_TEXT_CHARS = 1_200 +CONTEXT_MODE_QUICK = "quick" +CONTEXT_MODE_FULL = "full" +CONTEXT_MODES = {CONTEXT_MODE_QUICK, CONTEXT_MODE_FULL} _OUTPUT_OMITTED = ( "Tool output hidden for this request. Reload with include_tool_output=true to inspect " @@ -29,6 +32,8 @@ def load_call_context( max_entries: int = DEFAULT_CONTEXT_ENTRIES, include_tool_output: bool = False, include_compaction_history: bool = False, + diagnostics: bool = False, + mode: str = CONTEXT_MODE_QUICK, ) -> dict[str, Any]: """Load logged turn context for one model call from the source JSONL file. @@ -36,40 +41,54 @@ def load_call_context( context is not written back to SQLite or embedded in dashboard HTML. """ + context_mode = _normalize_context_mode(mode) + diagnostic_payload: dict[str, Any] | None = {} if diagnostics else None + db_lookup_started = perf_counter() row = query_usage_record(db_path=db_path, record_id=record_id) + if diagnostic_payload is not None: + diagnostic_payload["db_lookup_ms"] = _elapsed_ms(db_lookup_started) if row is None: raise ValueError(f"No usage record found for record_id: {record_id}") source_file = Path(str(row.get("source_file") or "")) if not source_file.exists(): raise FileNotFoundError(f"Source log not found: {source_file}") + source_file_bytes = source_file.stat().st_size line_number = _positive_int(row.get("line_number")) if line_number is None: raise ValueError(f"Usage record has no valid source line: {record_id}") target_turn_id = _optional_str(row.get("turn_id")) - entries, omitted, estimate_entries = _read_context_entries( - path=source_file, - token_line=line_number, - target_turn_id=target_turn_id, - max_chars=max_chars if max_chars <= 0 else max(1_000, max_chars), - max_entries=max_entries if max_entries <= 0 else max(1, max_entries), - include_tool_output=include_tool_output, - include_compaction_history=include_compaction_history, + source_scan_started = perf_counter() + entries, omitted, estimate_entries, serialized_estimate, serialized_estimate_ms = ( + _read_context_entries( + path=source_file, + token_line=line_number, + target_turn_id=target_turn_id, + max_chars=max_chars if max_chars <= 0 else max(1_000, max_chars), + max_entries=max_entries if max_entries <= 0 else max(1, max_entries), + include_tool_output=include_tool_output, + include_compaction_history=include_compaction_history, + model=_optional_str(row.get("model")), + context_mode=context_mode, + ) ) - call_anchors = _read_call_anchors(source_file, token_line=line_number) + source_scan_ms = _elapsed_ms(source_scan_started) + if diagnostic_payload is not None: + diagnostic_payload["source_scan_ms"] = source_scan_ms visible_estimate = _estimate_visible_tokens(estimate_entries, _optional_str(row.get("model"))) - serialized_estimate = _estimate_serialized_context( - path=source_file, - token_line=line_number, - target_turn_id=target_turn_id, - model=_optional_str(row.get("model")), - ) - return { + if diagnostic_payload is not None: + diagnostic_payload["serialized_estimate_ms"] = serialized_estimate_ms + diagnostic_payload["source_file_bytes"] = source_file_bytes + diagnostic_payload["source_line_number"] = line_number + diagnostic_payload["entries_before_limit"] = int(omitted.get("total_entries") or 0) + diagnostic_payload["entries_returned"] = len(entries) + payload = { "schema": "codex-usage-tracker-context-v1", "loaded_on_demand": True, "raw_context_persisted": False, + "context_mode": context_mode, "include_tool_output": include_tool_output, "include_compaction_history": include_compaction_history, "visible_char_count": visible_estimate["visible_char_count"], @@ -93,11 +112,38 @@ def load_call_context( "file": str(source_file), "line_number": line_number, }, - "call_anchors": call_anchors, - "thread_anchors": call_anchors, "entries": entries, "omitted": omitted, } + if diagnostic_payload is not None: + payload["diagnostics"] = diagnostic_payload + diagnostic_payload["json_bytes"] = _json_byte_count(payload) + return payload + + +def _normalize_context_mode(mode: str) -> str: + normalized = str(mode or CONTEXT_MODE_QUICK).strip().lower() + if normalized not in CONTEXT_MODES: + raise ValueError( + f"Unsupported context mode: {mode}. Expected one of: " + f"{', '.join(sorted(CONTEXT_MODES))}" + ) + return normalized + + +def _elapsed_ms(started_at: float) -> float: + return round((perf_counter() - started_at) * 1000, 3) + + +def _json_byte_count(payload: dict[str, Any]) -> int: + previous_size: int | None = None + diagnostics = payload.get("diagnostics") + while True: + size = len(json.dumps(payload, ensure_ascii=True).encode("utf-8")) + if size == previous_size or not isinstance(diagnostics, dict): + return size + diagnostics["json_bytes"] = size + previous_size = size def _read_context_entries( @@ -108,12 +154,24 @@ def _read_context_entries( max_entries: int, include_tool_output: bool, include_compaction_history: bool, -) -> tuple[list[dict[str, Any]], dict[str, Any], list[dict[str, Any]]]: + model: str | None, + context_mode: str, +) -> tuple[list[dict[str, Any]], dict[str, Any], list[dict[str, Any]], dict[str, Any], float]: candidates: list[dict[str, Any]] = [] + raw_entries: list[dict[str, Any]] = [] + field_buckets: dict[str, dict[str, Any]] = {} + serialized_line_count = 0 + serialized_raw_char_count = 0 omitted_parse_errors = 0 current_turn_id: str | None = None collecting = target_turn_id is None pending_compactions: list[dict[str, Any]] = [] + full_serialized_analysis = context_mode == CONTEXT_MODE_FULL + encoding, estimator = ( + _context_encoding(model or "") + if full_serialized_analysis + else (None, "chars_per_4_fallback") + ) with path.open(encoding="utf-8") as handle: for line_number, line in enumerate(handle, 1): @@ -135,6 +193,22 @@ def _read_context_entries( current_turn_id = _optional_str(payload.get("turn_id")) collecting = target_turn_id is None or current_turn_id == target_turn_id if collecting: + raw_entries = [] + field_buckets = {} + serialized_line_count = 0 + serialized_raw_char_count = 0 + if full_serialized_analysis: + _collect_serialized_envelope( + raw_entries=raw_entries, + field_buckets=field_buckets, + envelope=envelope, + entry_type=entry_type, + payload=payload, + encoding=encoding, + ) + else: + serialized_line_count += 1 + serialized_raw_char_count += len(line) carried_compactions = ( [entry for entry in candidates if entry.get("type") == "compacted"] if was_collecting and target_turn_id is not None @@ -155,6 +229,20 @@ def _read_context_entries( pending_compactions = [] continue + if collecting: + if full_serialized_analysis: + _collect_serialized_envelope( + raw_entries=raw_entries, + field_buckets=field_buckets, + envelope=envelope, + entry_type=entry_type, + payload=payload, + encoding=encoding, + ) + else: + serialized_line_count += 1 + serialized_raw_char_count += len(line) + summarized = _summarize_payload( entry_type=entry_type, payload=payload, @@ -186,12 +274,99 @@ def _read_context_entries( ): break + serialized_started = perf_counter() + if full_serialized_analysis: + serialized_estimate = _serialized_context_estimate( + raw_entries=raw_entries, + field_buckets=field_buckets, + parse_errors=omitted_parse_errors, + encoding=encoding, + estimator=estimator, + ) + else: + serialized_estimate = _quick_serialized_context_estimate( + raw_line_count=serialized_line_count, + raw_json_char_count=serialized_raw_char_count, + parse_errors=omitted_parse_errors, + ) + serialized_estimate_ms = _elapsed_ms(serialized_started) candidates = _dedupe_chat_message_echoes(candidates) limited, omitted = _limit_entries(candidates, max_chars=max_chars, max_entries=max_entries) omitted["parse_errors"] = omitted_parse_errors omitted["target_turn_id"] = target_turn_id omitted["total_entries"] = len(candidates) - return limited, omitted, candidates + return limited, omitted, candidates, serialized_estimate, serialized_estimate_ms + + +def _collect_serialized_envelope( + *, + raw_entries: list[dict[str, Any]], + field_buckets: dict[str, dict[str, Any]], + envelope: dict[str, Any], + entry_type: str, + payload: dict[str, Any], + encoding: Any | None, +) -> None: + raw_entries.append(envelope) + _collect_serialized_field_buckets( + buckets=field_buckets, + entry_type=entry_type, + payload=payload, + encoding=encoding, + ) + + +def _serialized_context_estimate( + *, + raw_entries: list[dict[str, Any]], + field_buckets: dict[str, dict[str, Any]], + parse_errors: int, + encoding: Any | None, + estimator: str, +) -> dict[str, Any]: + raw_json = "\n".join(_compact_json(_redact_json_value(entry)) for entry in raw_entries) + top_buckets = sorted( + field_buckets.values(), + key=lambda bucket: int(bucket.get("token_estimate") or 0), + reverse=True, + )[:8] + return { + "available": bool(raw_entries), + "scope": "selected_turn_raw_jsonl", + "raw_line_count": len(raw_entries), + "raw_json_char_count": len(raw_json), + "raw_json_token_estimate": _token_estimate(raw_json, encoding), + "token_estimator": estimator, + "parse_errors": parse_errors, + "upper_bound": True, + "raw_text_returned": False, + "buckets": top_buckets, + "deferred": False, + "deferred_buckets": False, + } + + +def _quick_serialized_context_estimate( + *, + raw_line_count: int, + raw_json_char_count: int, + parse_errors: int, +) -> dict[str, Any]: + return { + "available": raw_line_count > 0, + "scope": "selected_turn_raw_jsonl_fast_estimate", + "raw_line_count": raw_line_count, + "raw_json_char_count": raw_json_char_count, + "raw_json_token_estimate": ceil(raw_json_char_count / 4) if raw_json_char_count else 0, + "token_estimator": "chars_per_4_fallback", + "parse_errors": parse_errors, + "upper_bound": True, + "raw_text_returned": False, + "buckets": [], + "deferred": True, + "deferred_buckets": True, + "reason": "full_serialized_analysis_not_requested", + } def _summarized_context_entry( @@ -251,134 +426,6 @@ def _is_structured_chat_message(entry: dict[str, Any]) -> bool: return (_optional_str(entry.get("label")) or "") in {"message / user", "message / assistant"} -def _read_call_anchors(path: Path, token_line: int) -> dict[str, Any]: - before: dict[str, Any] | None = None - reasoning_output: dict[str, Any] | None = None - parse_errors = 0 - try: - with path.open(encoding="utf-8") as handle: - for line_number, line in enumerate(handle, 1): - if line_number > token_line: - break - try: - envelope = json.loads(line) - except json.JSONDecodeError: - parse_errors += 1 - continue - if not isinstance(envelope, dict): - continue - payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {} - entry_type = _optional_str(envelope.get("type")) or "unknown" - summarized = _summarize_payload( - entry_type=entry_type, - payload=payload, - include_tool_output=False, - include_compaction_history=False, - ) - if summarized is None: - continue - entry = _summarized_context_entry( - line_number, - _optional_str(envelope.get("timestamp")), - entry_type, - summarized, - ) - if _is_token_count_summary(summarized): - if line_number < token_line: - reasoning_output = None - continue - if _is_reasoning_output_anchor(summarized): - reasoning_output = entry - continue - if _is_anchor_message(summarized): - if _is_runtime_instruction_anchor(entry): - continue - before = _prefer_structured_anchor(before, entry) - except OSError: - return { - "scope": "selected_call_reasoning_output", - "available": False, - "parse_errors": parse_errors, - } - - return { - "scope": "selected_call_reasoning_output", - "available": bool(before or reasoning_output), - "parse_errors": parse_errors, - "before_message": _anchor_from_entry(before), - "reasoning_output": _anchor_from_entry(reasoning_output), - } - - -def _prefer_structured_anchor(current: dict[str, Any] | None, candidate: dict[str, Any]) -> dict[str, Any]: - if current is None: - return candidate - if _chat_message_echo_key(current) == _chat_message_echo_key(candidate): - if _is_structured_chat_message(candidate) and not _is_structured_chat_message(current): - return candidate - return current - return candidate - - -def _is_anchor_message(summarized: dict[str, Any]) -> bool: - return (_optional_str(summarized.get("label")) or "") in { - "agent_message", - "message / assistant", - "message / user", - "user_message", - } and bool(_optional_str(summarized.get("text"))) - - -def _is_reasoning_output_anchor(summarized: dict[str, Any]) -> bool: - label = _optional_str(summarized.get("label")) or "" - return label.split(" / ", 1)[0] == "reasoning" and bool( - _optional_str(summarized.get("text")) - ) - - -def _is_token_count_summary(summarized: dict[str, Any]) -> bool: - return "token_usage" in summarized - - -def _anchor_from_entry(entry: dict[str, Any] | None) -> dict[str, Any] | None: - if not entry: - return None - text = str(entry.get("text") or "") - truncated = bool(entry.get("truncated")) - if len(text) > ANCHOR_TEXT_CHARS: - text = text[:ANCHOR_TEXT_CHARS].rstrip() + "\n[TRUNCATED]" - truncated = True - return { - "line_number": entry.get("line_number"), - "timestamp": entry.get("timestamp"), - "type": entry.get("type"), - "label": entry.get("label"), - "role": _anchor_role(entry), - "text": text, - "truncated": truncated, - } - - -def _anchor_role(entry: dict[str, Any]) -> str | None: - label = _optional_str(entry.get("label")) or "" - if label in {"message / user", "user_message"}: - return "user" - if label in {"message / assistant", "agent_message"}: - return "assistant" - if label.split(" / ", 1)[0] == "reasoning": - return "reasoning" - return None - - -def _is_runtime_instruction_anchor(entry: dict[str, Any]) -> bool: - text = str(entry.get("text") or "").lstrip() - if not text: - return False - if text.startswith("# AGENTS.md instructions for "): - return True - return text.startswith("") or text.startswith("# Codex desktop context") - - def _summarize_payload( entry_type: str, payload: dict[str, Any], @@ -663,79 +710,6 @@ def _estimate_visible_tokens(entries: list[dict[str, Any]], model: str | None) - } -def _estimate_serialized_context( - *, - path: Path, - token_line: int, - target_turn_id: str | None, - model: str | None, -) -> dict[str, Any]: - raw_entries: list[dict[str, Any]] = [] - field_buckets: dict[str, dict[str, Any]] = {} - parse_errors = 0 - current_turn_id: str | None = None - collecting = target_turn_id is None - encoding, estimator = _context_encoding(model or "") - - with path.open(encoding="utf-8") as handle: - for line_number, line in enumerate(handle, 1): - if line_number > token_line: - break - try: - envelope = json.loads(line) - except json.JSONDecodeError: - parse_errors += 1 - continue - if not isinstance(envelope, dict): - continue - - entry_type = _optional_str(envelope.get("type")) or "unknown" - payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {} - if entry_type == "turn_context": - current_turn_id = _optional_str(payload.get("turn_id")) - collecting = target_turn_id is None or current_turn_id == target_turn_id - if collecting: - raw_entries = [] - field_buckets = {} - - if not collecting: - continue - - raw_entries.append(envelope) - _collect_serialized_field_buckets( - buckets=field_buckets, - entry_type=entry_type, - payload=payload, - encoding=encoding, - ) - - if ( - line_number >= token_line - and entry_type == "event_msg" - and payload.get("type") == "token_count" - ): - break - - raw_json = "\n".join(_compact_json(_redact_json_value(entry)) for entry in raw_entries) - top_buckets = sorted( - field_buckets.values(), - key=lambda bucket: int(bucket.get("token_estimate") or 0), - reverse=True, - )[:8] - return { - "available": bool(raw_entries), - "scope": "selected_turn_raw_jsonl", - "raw_line_count": len(raw_entries), - "raw_json_char_count": len(raw_json), - "raw_json_token_estimate": _token_estimate(raw_json, encoding), - "token_estimator": estimator, - "parse_errors": parse_errors, - "upper_bound": True, - "raw_text_returned": False, - "buckets": top_buckets, - } - - def _collect_serialized_field_buckets( *, buckets: dict[str, dict[str, Any]], diff --git a/src/codex_usage_tracker/dashboard.py b/src/codex_usage_tracker/dashboard.py index a73f3fa..d5be832 100644 --- a/src/codex_usage_tracker/dashboard.py +++ b/src/codex_usage_tracker/dashboard.py @@ -17,7 +17,7 @@ load_allowance_config, summarize_allowance_usage, ) -from codex_usage_tracker.call_origin import annotate_rows_with_call_origin +from codex_usage_tracker.call_origin import ensure_call_origin from codex_usage_tracker.i18n import dashboard_i18n_payload, language_direction, translations_for from codex_usage_tracker.paths import ( DEFAULT_ALLOWANCE_PATH, @@ -42,6 +42,7 @@ from codex_usage_tracker.store import ( query_dashboard_event_count, query_dashboard_events, + query_dashboard_token_summary, refresh_metadata, ) from codex_usage_tracker.threads import annotate_thread_attachments @@ -62,21 +63,27 @@ def dashboard_payload( privacy_mode: str = "normal", include_archived: bool = False, language: str | None = None, + include_rows: bool = True, ) -> dict[str, object]: """Return aggregate-only dashboard data without rendering HTML.""" privacy_mode = validate_privacy_mode(privacy_mode) normalized_offset = _normalize_offset(offset) - rows = annotate_thread_attachments( - annotate_rows_with_call_origin( - query_dashboard_events( - db_path=db_path, - limit=limit, - offset=normalized_offset, - since=since, - include_archived=include_archived, - ) + rows = ( + annotate_thread_attachments( + [ + ensure_call_origin(row) + for row in query_dashboard_events( + db_path=db_path, + limit=limit, + offset=normalized_offset, + since=since, + include_archived=include_archived, + ) + ] ) + if include_rows + else [] ) pricing = load_pricing_config(pricing_path) allowance = load_allowance_config(allowance_path, rate_card_path=rate_card_path) @@ -89,7 +96,17 @@ def dashboard_payload( annotated_rows = annotate_rows_with_recommendations(annotated_rows, thresholds) annotated_rows = annotate_rows_with_project_identity(annotated_rows, projects) annotated_rows = apply_project_privacy_to_rows(annotated_rows, privacy_mode=privacy_mode) - allowance_summary = summarize_allowance_usage(annotated_rows, allowance) + token_summary = _dashboard_summary( + db_path=db_path, + since=since, + include_archived=include_archived, + pricing=pricing, + allowance=allowance, + ) + allowance_summary = summarize_allowance_usage( + token_summary["priced_model_rows"], + allowance, + ) normalized_limit = _normalize_limit(limit) total_available_rows = query_dashboard_event_count( db_path=db_path, @@ -115,6 +132,8 @@ def dashboard_payload( return { **dashboard_i18n_payload(language), "rows": annotated_rows, + "summary": token_summary["summary"], + "shell_boot": not include_rows, "pricing_configured": pricing.loaded and not pricing.error, "pricing_source": pricing.source, "pricing_snapshot": _pricing_snapshot(pricing.loaded, pricing.source, pricing.models), @@ -146,6 +165,15 @@ def dashboard_payload( "limit_label": "All" if normalized_limit is None else str(normalized_limit), "parser_diagnostics": parser_diagnostics, "parser_adapter": metadata.get("parser_adapter"), + "latest_refresh_at": metadata.get("latest_refresh_at"), + "payload_cache_key": _payload_cache_key( + db_path=db_path, + api_token=api_token, + include_archived=include_archived, + since=since, + privacy_mode=privacy_mode, + ), + "payload_cache_version": 1, "api_token": api_token or "", "context_api_enabled": context_api_enabled, "action_thresholds": thresholds.thresholds, @@ -173,6 +201,7 @@ def generate_dashboard( privacy_mode: str = "normal", include_archived: bool = False, language: str | None = None, + include_rows: bool = True, ) -> Path: output_path.parent.mkdir(parents=True, exist_ok=True) guide_href = _dashboard_guide_href(output_path) @@ -200,17 +229,16 @@ def generate_dashboard( privacy_mode=privacy_mode, include_archived=include_archived, language=language, + include_rows=include_rows, ) payload_dict["pricing_snapshot_warning"] = _pricing_snapshot_warning( previous_payload, payload_dict ) - payload = json.dumps(payload_dict, ensure_ascii=True).replace(" str: + """Render dashboard HTML for a prepared aggregate payload.""" + + asset_base = "codex-usage-tracker-assets" + payload = json.dumps(payload_dict, ensure_ascii=True).replace(" dict[str, object]: + token_summary = query_dashboard_token_summary( + db_path=db_path, + since=since, + include_archived=include_archived, + ) + model_rows = [ + {key: value for key, value in row.items() if key != "row_count"} + for row in token_summary["model_rows"] + ] + priced_model_rows = annotate_rows_with_allowance( + annotate_rows_with_efficiency(model_rows, pricing, model_field="model"), + allowance, + model_field="model", + ) + estimated_cost = sum( + float(row.get("estimated_cost_usd") or 0) + for row in priced_model_rows + if isinstance(row.get("estimated_cost_usd"), int | float) + ) + usage_credits = sum( + float(row.get("usage_credits") or 0) + for row in priced_model_rows + if isinstance(row.get("usage_credits"), int | float) + ) + return { + "summary": { + "visible_calls": token_summary["row_count"], + "input_tokens": token_summary["input_tokens"], + "cached_input_tokens": token_summary["cached_input_tokens"], + "uncached_input_tokens": token_summary["uncached_input_tokens"], + "output_tokens": token_summary["output_tokens"], + "reasoning_output_tokens": token_summary["reasoning_output_tokens"], + "total_tokens": token_summary["total_tokens"], + "estimated_cost_usd": estimated_cost, + "usage_credits": usage_credits, + }, + "priced_model_rows": priced_model_rows, + } + + def _normalize_limit(limit: int | None) -> int | None: if limit is None or limit <= 0: return None @@ -277,6 +394,27 @@ def _pricing_snapshot( } +def _payload_cache_key( + *, + db_path: Path, + api_token: str | None, + include_archived: bool, + since: str | None, + privacy_mode: str, +) -> str: + source = "|".join( + [ + str(db_path), + api_token or "static", + "all" if include_archived else "active", + since or "", + privacy_mode, + "dashboard-payload-v1", + ] + ) + return hashlib.sha256(source.encode("utf-8")).hexdigest()[:24] + + def _pricing_snapshot_warning( previous_payload: dict[str, Any] | None, current_payload: dict[str, object] ) -> str | None: @@ -369,6 +507,7 @@ def _html( state_script_src: str = "codex-usage-tracker-assets/dashboard_state.js", call_investigator_script_src: str = "codex-usage-tracker-assets/dashboard_call_investigator.js", script_src: str = "codex-usage-tracker-assets/dashboard.js", + body_attrs: str = "", ) -> str: template = _read_dashboard_asset("dashboard_template.html") translations = translations_for(language) @@ -382,6 +521,7 @@ def _html( return ( template.replace("__HTML_LANG__", html.escape(language, quote=True)) .replace("__HTML_DIR__", html.escape(html_direction, quote=True)) + .replace("__BODY_ATTRS__", body_attrs) .replace("__TITLE__", html.escape(translations["dashboard.title"])) .replace("__STYLESHEET_HREF__", html.escape(stylesheet_href, quote=True)) .replace("__GUIDE_LINK__", guide_link) @@ -397,6 +537,17 @@ def _html( ) +def _format_body_attrs(attrs: dict[str, str] | None) -> str: + if not attrs: + return "" + rendered = [] + for key, value in attrs.items(): + if not key: + continue + rendered.append(f'{html.escape(key, quote=True)}="{html.escape(str(value), quote=True)}"') + return " " + " ".join(rendered) if rendered else "" + + def _read_dashboard_asset(name: str) -> str: asset = resources.files("codex_usage_tracker.plugin_data").joinpath("dashboard", name) return asset.read_text(encoding="utf-8") diff --git a/src/codex_usage_tracker/json_contracts.py b/src/codex_usage_tracker/json_contracts.py index 7c725f9..08973b3 100644 --- a/src/codex_usage_tracker/json_contracts.py +++ b/src/codex_usage_tracker/json_contracts.py @@ -166,6 +166,61 @@ "url": str, } }, + "codex-usage-tracker-live-api-v1": {"required": {}}, + "codex-usage-tracker-status-v1": { + "required": { + "payload_schema": str, + "latest_refresh_at": (str, NoneType), + "include_archived": bool, + "row_counts": dict, + "max_event_timestamp": (str, NoneType), + "parser_diagnostics": dict, + } + }, + "codex-usage-tracker-calls-v1": { + "required": { + "rows": list, + "row_count": int, + "total_matched_rows": int, + "limit": (int, NoneType), + "offset": int, + "has_more": bool, + "next_offset": (int, NoneType), + "filters": dict, + "raw_context_included": bool, + } + }, + "codex-usage-tracker-call-v1": { + "required": { + "record": dict, + "previous_record_id": (str, NoneType), + "next_record_id": (str, NoneType), + "raw_context_included": bool, + } + }, + "codex-usage-tracker-threads-v1": { + "required": { + "rows": list, + "row_count": int, + "limit": (int, NoneType), + "offset": int, + "include_archived": bool, + "raw_context_included": bool, + } + }, + "codex-usage-tracker-thread-calls-v1": { + "required": { + "thread_key": str, + "rows": list, + "row_count": int, + "total_matched_rows": int, + "limit": (int, NoneType), + "offset": int, + "has_more": bool, + "next_offset": (int, NoneType), + "raw_context_included": bool, + } + }, "codex-usage-tracker-dashboard-v1": { "required": { "dashboard_path": str, diff --git a/src/codex_usage_tracker/models.py b/src/codex_usage_tracker/models.py index a3ffb24..cc98d91 100644 --- a/src/codex_usage_tracker/models.py +++ b/src/codex_usage_tracker/models.py @@ -32,6 +32,14 @@ class UsageEvent: effort: str | None current_date: str | None timezone: str | None + call_initiator: str | None + call_initiator_reason: str | None + call_initiator_confidence: str | None + is_archived: int + thread_key: str | None + thread_call_index: int | None + previous_record_id: str | None + next_record_id: str | None thread_source: str | None subagent_type: str | None agent_role: str | None diff --git a/src/codex_usage_tracker/parser.py b/src/codex_usage_tracker/parser.py index 904287c..2a07b0b 100644 --- a/src/codex_usage_tracker/parser.py +++ b/src/codex_usage_tracker/parser.py @@ -6,10 +6,15 @@ import json import re from collections.abc import Iterable, MutableMapping -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path from typing import Any +from codex_usage_tracker.call_origin import ( + CallOriginFlags, + classify_call_origin, + event_flags_from_envelope, +) from codex_usage_tracker.models import SessionInfo, UsageEvent from codex_usage_tracker.paths import DEFAULT_CODEX_HOME @@ -51,12 +56,56 @@ def parse_file( session_index: dict[str, SessionInfo] | None = None, stats: MutableMapping[str, int] | None = None, ) -> list[UsageEvent]: - return _parse_codex_jsonl_v1(path, session_index=session_index, stats=stats) + return self.parse_file_with_state( + path, + session_index=session_index, + stats=stats, + ).events + + def parse_file_with_state( + self, + path: Path, + session_index: dict[str, SessionInfo] | None = None, + stats: MutableMapping[str, int] | None = None, + *, + start_byte: int = 0, + start_line: int = 0, + initial_state: ParserState | None = None, + ) -> ParsedUsageFile: + return _parse_codex_jsonl_v1( + path, + session_index=session_index, + stats=stats, + start_byte=start_byte, + start_line=start_line, + initial_state=initial_state, + ) DEFAULT_PARSER_ADAPTER = ParserAdapter() +@dataclass(frozen=True) +class ParserState: + """Aggregate-only parser cursor for continuing append-only JSONL parsing.""" + + session_id: str | None = None + session_meta: dict[str, str | None] = field(default_factory=dict) + current_turn: dict[str, Any] = field(default_factory=dict) + last_cumulative_total: int = -1 + call_origin_segment: tuple[CallOriginFlags, ...] = () + latest_record_id: str | None = None + latest_event_timestamp: str | None = None + + +@dataclass(frozen=True) +class ParsedUsageFile: + """Parsed aggregate usage events plus the final parser cursor.""" + + events: list[UsageEvent] + state: ParserState + + def load_session_index(codex_home: Path = DEFAULT_CODEX_HOME) -> dict[str, SessionInfo]: """Load Codex thread names without reading transcript content.""" @@ -117,6 +166,78 @@ def parse_usage_events_from_file( return DEFAULT_PARSER_ADAPTER.parse_file(path, session_index=session_index, stats=stats) +def parse_usage_events_from_file_with_state( + path: Path, + session_index: dict[str, SessionInfo] | None = None, + stats: MutableMapping[str, int] | None = None, + *, + start_byte: int = 0, + start_line: int = 0, + initial_state: ParserState | None = None, +) -> ParsedUsageFile: + """Parse one Codex JSONL log and return an aggregate-only continuation cursor.""" + + return DEFAULT_PARSER_ADAPTER.parse_file_with_state( + path, + session_index=session_index, + stats=stats, + start_byte=start_byte, + start_line=start_line, + initial_state=initial_state, + ) + + +def parser_state_from_json(raw: str | None) -> ParserState | None: + """Decode a persisted aggregate-only parser cursor.""" + + if not raw: + return None + try: + payload = json.loads(raw) + except json.JSONDecodeError: + return None + if not isinstance(payload, dict) or payload.get("version") != 1: + return None + segment = payload.get("call_origin_segment") + if not isinstance(segment, list): + segment = [] + return ParserState( + session_id=_optional_str(payload.get("session_id")), + session_meta=_string_dict(payload.get("session_meta")), + current_turn=_string_dict(payload.get("current_turn")), + last_cumulative_total=_json_int(payload.get("last_cumulative_total"), -1), + call_origin_segment=tuple(_call_origin_flags_from_json(item) for item in segment), + latest_record_id=_optional_str(payload.get("latest_record_id")), + latest_event_timestamp=_optional_str(payload.get("latest_event_timestamp")), + ) + + +def parser_state_to_json(state: ParserState) -> str: + """Encode an aggregate-only parser cursor for source-file refresh metadata.""" + + return json.dumps( + { + "version": 1, + "session_id": state.session_id, + "session_meta": state.session_meta, + "current_turn": state.current_turn, + "last_cumulative_total": state.last_cumulative_total, + "call_origin_segment": [ + { + "user_message": flags.user_message, + "compaction": flags.compaction, + "tool_result": flags.tool_result, + "codex_activity": flags.codex_activity, + } + for flags in state.call_origin_segment + ], + "latest_record_id": state.latest_record_id, + "latest_event_timestamp": state.latest_event_timestamp, + }, + sort_keys=True, + ) + + def inspect_log( path: Path, session_index: dict[str, SessionInfo] | None = None, @@ -157,6 +278,8 @@ def inspect_log( "reasoning_output_tokens": event.reasoning_output_tokens, "total_tokens": event.total_tokens, "cumulative_total_tokens": event.cumulative_total_tokens, + "is_archived": event.is_archived, + "thread_key": event.thread_key, } for event in events ], @@ -179,25 +302,40 @@ def _parse_codex_jsonl_v1( path: Path, session_index: dict[str, SessionInfo] | None = None, stats: MutableMapping[str, int] | None = None, -) -> list[UsageEvent]: + *, + start_byte: int = 0, + start_line: int = 0, + initial_state: ParserState | None = None, +) -> ParsedUsageFile: """Parse one Codex JSONL v1 log without storing raw message content.""" index = session_index or {} file_session_id = _session_id_from_path(path) - if not file_session_id: + if not file_session_id and start_byte <= 0: _increment_stat(stats, "unknown_filename_format") - session_id = file_session_id + previous_state = initial_state or ParserState() + session_id = previous_state.session_id or file_session_id session_info = index.get(session_id) if session_id else None - current_turn: dict[str, Any] = {} - session_meta: dict[str, str | None] = {} - last_cumulative_total = -1 + current_turn: dict[str, Any] = dict(previous_state.current_turn) + session_meta: dict[str, str | None] = ( + dict(previous_state.session_meta) + if previous_state.session_meta + else _empty_session_metadata() + ) + last_cumulative_total = previous_state.last_cumulative_total events: list[UsageEvent] = [] - - with path.open("r", encoding="utf-8") as handle: - for line_number, line in enumerate(handle, 1): + call_origin_segment: list[CallOriginFlags] = list(previous_state.call_origin_segment) + latest_record_id = previous_state.latest_record_id + latest_event_timestamp = previous_state.latest_event_timestamp + + with path.open("rb") as handle: + if start_byte > 0: + handle.seek(start_byte) + for line_number, raw_line in enumerate(handle, start_line + 1): try: + line = raw_line.decode("utf-8") envelope = json.loads(line) - except json.JSONDecodeError: + except (UnicodeDecodeError, json.JSONDecodeError): _increment_stat(stats, "invalid_json") continue @@ -230,10 +368,15 @@ def _parse_codex_jsonl_v1( payload_type = payload.get("type") if entry_type != "event_msg" or payload_type != "token_count": + flags = event_flags_from_envelope(envelope) + if flags.has_signal: + call_origin_segment.append(flags) if entry_type == "event_msg" and payload_type not in KNOWN_NON_TOKEN_EVENT_MSG_TYPES: _increment_stat(stats, "unknown_event_shape") continue + call_origin = classify_call_origin(call_origin_segment) + call_origin_segment = [] info = payload.get("info") if not isinstance(info, dict): _increment_stat(stats, "missing_info") @@ -275,6 +418,7 @@ def _parse_codex_jsonl_v1( session_info=session_info, session_meta=session_meta, current_turn=current_turn, + call_origin=call_origin, model_context_window=_nullable_int( info.get("model_context_window"), stats=stats, @@ -288,9 +432,22 @@ def _parse_codex_jsonl_v1( _increment_stat(stats, "skipped_events") continue last_cumulative_total = cumulative_total + latest_record_id = event.record_id + latest_event_timestamp = event.event_timestamp events.append(event) - return events + return ParsedUsageFile( + events=events, + state=ParserState( + session_id=session_id, + session_meta=session_meta, + current_turn=current_turn, + last_cumulative_total=last_cumulative_total, + call_origin_segment=tuple(call_origin_segment), + latest_record_id=latest_record_id, + latest_event_timestamp=latest_event_timestamp, + ), + ) def _build_event( @@ -301,6 +458,7 @@ def _build_event( session_info: SessionInfo | None, session_meta: dict[str, str | None], current_turn: dict[str, Any], + call_origin: dict[str, str], model_context_window: int | None, last_usage: dict[str, Any], total_usage: dict[str, Any], @@ -341,6 +499,18 @@ def _build_event( effort=_optional_str(current_turn.get("effort")), current_date=_optional_str(current_turn.get("current_date")), timezone=_optional_str(current_turn.get("timezone")), + call_initiator=call_origin.get("call_initiator"), + call_initiator_reason=call_origin.get("call_initiator_reason"), + call_initiator_confidence=call_origin.get("call_initiator_confidence"), + is_archived=_is_archived_source(path), + thread_key=_thread_key( + session_id=session_id, + session_info=session_info, + session_meta=session_meta, + ), + thread_call_index=None, + previous_record_id=None, + next_record_id=None, thread_source=session_meta.get("thread_source"), subagent_type=session_meta.get("subagent_type"), agent_role=session_meta.get("agent_role"), @@ -371,15 +541,8 @@ def _session_metadata( session_index: dict[str, SessionInfo], ) -> dict[str, str | None]: source = payload.get("source") - metadata: dict[str, str | None] = { - "thread_source": _optional_str(payload.get("thread_source")), - "subagent_type": None, - "agent_role": None, - "agent_nickname": None, - "parent_session_id": None, - "parent_thread_name": None, - "parent_session_updated_at": None, - } + metadata = _empty_session_metadata() + metadata["thread_source"] = _optional_str(payload.get("thread_source")) if not isinstance(source, dict): return metadata @@ -407,6 +570,18 @@ def _session_metadata( return metadata +def _empty_session_metadata() -> dict[str, str | None]: + return { + "thread_source": None, + "subagent_type": None, + "agent_role": None, + "agent_nickname": None, + "parent_session_id": None, + "parent_thread_name": None, + "parent_session_updated_at": None, + } + + def _record_id( session_id: str, turn_id: str | None, @@ -426,6 +601,28 @@ def _record_id( return hashlib.sha256(raw.encode("utf-8")).hexdigest() +def _is_archived_source(path: Path) -> int: + return 1 if "archived_sessions" in path.parts else 0 + + +def _thread_key( + *, + session_id: str, + session_info: SessionInfo | None, + session_meta: dict[str, str | None], +) -> str: + thread_name = session_info.thread_name if session_info else None + if thread_name: + return f"thread:{thread_name}" + parent_thread_name = session_meta.get("parent_thread_name") + if parent_thread_name: + return f"thread:{parent_thread_name}" + parent_session_id = session_meta.get("parent_session_id") + if parent_session_id: + return f"session:{parent_session_id}" + return f"session:{session_id}" + + def _session_id_from_path(path: Path) -> str | None: match = SESSION_ID_RE.search(path.name) if not match: @@ -439,6 +636,31 @@ def _optional_str(value: object) -> str | None: return None +def _string_dict(value: object) -> dict[str, str | None]: + if not isinstance(value, dict): + return {} + return { + str(key): item if isinstance(item, str) else None + for key, item in value.items() + if isinstance(key, str) + } + + +def _json_int(value: object, default: int) -> int: + return value if isinstance(value, int) and not isinstance(value, bool) else default + + +def _call_origin_flags_from_json(value: object) -> CallOriginFlags: + if not isinstance(value, dict): + return CallOriginFlags() + return CallOriginFlags( + user_message=value.get("user_message") is True, + compaction=value.get("compaction") is True, + tool_result=value.get("tool_result") is True, + codex_activity=value.get("codex_activity") is True, + ) + + def _nullable_int( value: object, *, diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard.css b/src/codex_usage_tracker/plugin_data/dashboard/dashboard.css index 52bb5e8..a6fe6ed 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard.css +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard.css @@ -221,7 +221,47 @@ } .card span { display: block; color: var(--muted); font-size: 12px; font-weight: 680; } .card strong { display: block; margin-top: 7px; font-size: 22px; } + .row-load-progress { + display: grid; + gap: 8px; + margin: 0 20px 14px; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: #f8fbff; + } + .row-load-progress[hidden] { display: none; } + .row-load-progress-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + color: var(--muted); + font-size: 12px; + font-weight: 760; + } + .row-load-progress-meta strong { + color: var(--ink); + font-size: 13px; + } + .row-load-progress-track { + height: 7px; + overflow: hidden; + border-radius: 999px; + background: #e7edf7; + } + .row-load-progress-track span { + display: block; + width: 0%; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--blue), #74a3ff); + transition: width 160ms ease; + } #allowanceImpact { white-space: pre-line; line-height: 1.18; } + body[data-active-view="call"] > header { + display: none; + } body[data-active-view="call"] .filters, body[data-active-view="call"] .cards, body[data-active-view="call"] .detail-section, @@ -552,6 +592,9 @@ color: var(--ink); font-size: 12px; } + .serialized-action { + justify-self: start; + } .serialized-bucket-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); @@ -830,7 +873,11 @@ box-shadow: var(--shadow); } .grid > section:not(.detail-section) { - overflow: visible; + min-width: 0; + overflow-x: auto; + overflow-y: visible; + scrollbar-gutter: stable; + overscroll-behavior-x: contain; } .detail-section { position: sticky; @@ -1583,75 +1630,6 @@ gap: 8px; margin: 0 0 10px; } - .context-anchor-panel { - margin: 12px 0; - padding: 12px; - border: 1px solid var(--line); - border-radius: 8px; - background: #f8fafc; - } - .context-anchor-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 10px; - color: var(--muted); - font-size: 12px; - font-weight: 720; - } - .context-anchor-header strong { - display: block; - color: var(--ink); - font-size: 14px; - font-weight: 850; - } - .context-anchor-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); - gap: 10px; - } - .context-anchor-card { - min-width: 0; - border: 1px solid #dbe3f0; - border-radius: 8px; - background: white; - overflow: hidden; - } - .context-anchor-card-head { - display: flex; - justify-content: space-between; - gap: 8px; - padding: 8px 10px 0; - color: var(--ink); - font-size: 12px; - font-weight: 850; - } - .context-anchor-role { - flex: 0 0 auto; - padding: 1px 6px; - border-radius: 999px; - background: #eef2ff; - color: #4338ca; - font-size: 10px; - text-transform: capitalize; - } - .context-anchor-meta { - padding: 3px 10px 0; - color: var(--muted); - font-size: 11px; - font-weight: 700; - } - .context-anchor-card pre { - max-height: 150px; - margin: 0; - padding: 8px 10px 10px; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; - color: var(--ink); - font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; - } .context-entry { margin: 10px 0; border: 1px solid var(--line); diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard.js b/src/codex_usage_tracker/plugin_data/dashboard/dashboard.js index 074f711..1547a9c 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard.js +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard.js @@ -35,6 +35,7 @@ resolveThreadAttachment, chronological, adjacentThreadCalls, + buildCallAdjacencyIndex, classifyCacheDiagnostic, callAccountingDelta, rowInputTokens: dataRowInputTokens, @@ -44,6 +45,54 @@ rowReasoningTokens: dataRowReasoningTokens, } = dashboardData; const initialPayload = JSON.parse(document.getElementById('usage-data').textContent); + const aggregateCacheNamespace = 'codexUsageDashboardPayload:v1'; + function aggregatePayloadCacheKey(payload = initialPayload) { + const key = payload && payload.payload_cache_key ? String(payload.payload_cache_key) : ''; + return key ? `${aggregateCacheNamespace}:${key}` : ''; + } + function cacheableAggregatePayload(payload) { + if (!payload || !Array.isArray(payload.rows) || payload.investigator_boot || payload.shell_boot) return null; + const cached = { ...payload }; + delete cached.api_token; + cached.context_api_enabled = false; + cached.investigator_boot = false; + cached.shell_boot = false; + return cached; + } + function readAggregatePayloadCache(payload = initialPayload) { + const key = aggregatePayloadCacheKey(payload); + if (!key) return null; + try { + const parsed = JSON.parse(window.sessionStorage?.getItem(key) || 'null'); + if (!parsed || parsed.payload_cache_key !== payload.payload_cache_key || !Array.isArray(parsed.rows)) return null; + return parsed; + } catch (_error) { + return null; + } + } + function writeAggregatePayloadCache(payload) { + const key = aggregatePayloadCacheKey(payload); + const cached = cacheableAggregatePayload(payload); + if (!key || !cached) return false; + try { + cached.cached_at = new Date().toISOString(); + window.sessionStorage?.setItem(key, JSON.stringify(cached)); + return true; + } catch (_error) { + return false; + } + } + const cachedInitialPayload = initialPayload.investigator_boot || initialPayload.shell_boot ? readAggregatePayloadCache(initialPayload) : null; + const activeInitialPayload = cachedInitialPayload + ? { + ...cachedInitialPayload, + api_token: initialPayload.api_token || '', + context_api_enabled: Boolean(initialPayload.context_api_enabled), + investigator_boot: Boolean(initialPayload.investigator_boot), + shell_boot: Boolean(initialPayload.shell_boot), + } + : initialPayload; + let restoredAggregatePayloadFromCache = Boolean(cachedInitialPayload); const builtInFallbackTranslations = { 'dashboard.title': 'Usage Dashboard', 'dashboard.eyebrow': 'Local Codex analytics', @@ -63,6 +112,7 @@ 'button.include_tool_output': 'Include tool output', 'button.hide_tool_output': 'Hide tool output', 'button.show_tool_output': 'Show tool output', + 'button.full_serialized_analysis': 'Run full serialized analysis', 'button.load_older_context': 'Load older entries', 'button.no_char_limit': 'No char limit', 'button.open_investigator': 'Open investigator', @@ -90,11 +140,17 @@ 'option.all_confidence': 'All confidence', 'section.needs_attention': 'Needs Attention', 'section.investigation_presets': 'Investigation Presets', + 'state.error': 'Error', 'state.no_data': 'No data', + 'state.loading_rows': 'Loading rows', 'state.no_rows': 'No rows', 'state.no_calls': 'No calls match the current filters.', 'state.no_threads': 'No threads match the current filters.', 'state.requires_evidence': 'Evidence needed', + 'caption.rows_loading_background': 'Dashboard totals are ready. Rows are loading in the background.', + 'caption.rows_loading_progress': 'Loading rows: {loaded} of {total}', + 'caption.rows_loaded_progress': 'Rows loaded: {loaded} of {total}', + 'live.loading_rows': 'Loading rows in the background...', 'table.time': 'Time', 'table.thread': 'Thread', 'table.initiated': 'Initiated', @@ -137,9 +193,11 @@ 'call.visible_gap': 'Uncached input minus visible estimate', 'call.serialized_candidate_hint': 'Serialized upper bound minus visible estimate, capped by exact uncached input', 'call.remaining_after_serialized_hint': 'Uncached input not covered even by serialized upper bound', - 'call.serialized_breakdown': 'Serialized evidence buckets', + 'call.serialized_breakdown': 'Serialized evidence groups', 'call.serialized_bound_hint': 'Upper-bound local JSONL structure; not exact prompt text.', 'call.serialized_bucket_detail': '{count} fields · {chars} chars', + 'call.serialized_deferred': 'Fast estimate loaded; full serialized grouping is deferred.', + 'call.serialized_quick_hint': 'fast estimate', 'call.open_hint': 'Click a call row for deep diagnostics.', 'call.not_found': 'Selected call was not found in the loaded dashboard rows.', 'call.position': 'Call {position} in this resolved thread.', @@ -163,20 +221,15 @@ 'context.token_total': 'Total', 'context.no_char_limit_active': 'No character limit applied.', 'context.auto_loading': 'Loading selected-turn evidence with tool output included.', - 'context.thread_anchors': 'Call anchors', - 'context.thread_anchors_hint': 'Nearest visible message before this call and reasoning output for the selected call when available.', - 'context.anchor_before': 'Before this call', - 'context.anchor_reasoning_output': 'Reasoning output', - 'context.anchor_count': '{count} anchors loaded.', 'source.user_initiated': 'User initiated', 'source.codex_initiated': 'Codex initiated', }; - let availableLanguages = Array.isArray(initialPayload.available_languages) && initialPayload.available_languages.length - ? initialPayload.available_languages + let availableLanguages = Array.isArray(activeInitialPayload.available_languages) && activeInitialPayload.available_languages.length + ? activeInitialPayload.available_languages : [{ code: 'en', english_name: 'English', native_name: 'English', dir: 'ltr' }]; let supportedLanguages = new Set(availableLanguages.map(language => language.code)); - let translationCatalog = initialPayload.translation_catalog || { [initialPayload.language || 'en']: initialPayload.translations || {} }; - let fallbackTranslations = { ...builtInFallbackTranslations, ...(translationCatalog.en || initialPayload.translations || {}) }; + let translationCatalog = activeInitialPayload.translation_catalog || { [activeInitialPayload.language || 'en']: activeInitialPayload.translations || {} }; + let fallbackTranslations = { ...builtInFallbackTranslations, ...(translationCatalog.en || activeInitialPayload.translations || {}) }; function storedLanguage() { try { return window.localStorage ? window.localStorage.getItem('codex-usage-dashboard-language') : ''; @@ -229,7 +282,7 @@ const candidate = aliases[normalized] || raw; return supportedLanguages.has(candidate) ? candidate : 'en'; } - let currentLanguage = normalizeLanguage(storedLanguage() || initialPayload.language || 'en'); + let currentLanguage = normalizeLanguage(storedLanguage() || activeInitialPayload.language || 'en'); let translations = translationCatalog[currentLanguage] || fallbackTranslations; let liveStatusKey = window.location.protocol !== 'file:' ? 'badge.live' : 'status.static'; let liveStatusDetail = ''; @@ -269,25 +322,28 @@ const stateManager = window.CodexUsageDashboardState; const urlParams = new URLSearchParams(window.location.search); const initialState = stateManager ? stateManager.read(urlParams) : {}; - let data = payloadRows(initialPayload); - let pricingConfigured = Boolean(initialPayload.pricing_configured); - let pricingSource = initialPayload.pricing_source || {}; - let pricingSnapshotWarning = initialPayload.pricing_snapshot_warning || ''; - let allowanceConfigured = Boolean(initialPayload.allowance_configured); - let allowanceSource = initialPayload.allowance_source || {}; - let allowanceWindows = Array.isArray(initialPayload.allowance_windows) ? initialPayload.allowance_windows : []; - let allowanceError = initialPayload.allowance_error || ''; - let rateCardError = initialPayload.rate_card_error || ''; - let projectMetadataPrivacy = initialPayload.project_metadata_privacy || { mode: initialPayload.privacy_mode || 'normal' }; - let parserDiagnostics = initialPayload.parser_diagnostics || {}; - let apiToken = initialPayload.api_token || ''; - let contextApiEnabled = Boolean(initialPayload.context_api_enabled); - let actionThresholds = initialPayload.action_thresholds || {}; - let totalAvailableRows = Number(initialPayload.total_available_rows || data.length); - let activeAvailableRows = Number(initialPayload.active_available_rows || data.length); - let allHistoryAvailableRows = Number(initialPayload.all_history_available_rows || totalAvailableRows); - let archivedAvailableRows = Number(initialPayload.archived_available_rows || Math.max(allHistoryAvailableRows - activeAvailableRows, 0)); - let loadedLimit = payloadLimit(initialPayload); + let data = payloadRows(activeInitialPayload); + let summaryData = activeInitialPayload.summary || null; + let shellBoot = Boolean(activeInitialPayload.shell_boot); + let pricingConfigured = Boolean(activeInitialPayload.pricing_configured); + let pricingSource = activeInitialPayload.pricing_source || {}; + let pricingSnapshotWarning = activeInitialPayload.pricing_snapshot_warning || ''; + let latestRefreshAt = activeInitialPayload.latest_refresh_at || ''; + let allowanceConfigured = Boolean(activeInitialPayload.allowance_configured); + let allowanceSource = activeInitialPayload.allowance_source || {}; + let allowanceWindows = Array.isArray(activeInitialPayload.allowance_windows) ? activeInitialPayload.allowance_windows : []; + let allowanceError = activeInitialPayload.allowance_error || ''; + let rateCardError = activeInitialPayload.rate_card_error || ''; + let projectMetadataPrivacy = activeInitialPayload.project_metadata_privacy || { mode: activeInitialPayload.privacy_mode || 'normal' }; + let parserDiagnostics = activeInitialPayload.parser_diagnostics || {}; + let apiToken = initialPayload.api_token || activeInitialPayload.api_token || ''; + let contextApiEnabled = Boolean(initialPayload.context_api_enabled || activeInitialPayload.context_api_enabled); + let actionThresholds = activeInitialPayload.action_thresholds || {}; + let totalAvailableRows = Number(activeInitialPayload.total_available_rows || data.length); + let activeAvailableRows = Number(activeInitialPayload.active_available_rows || data.length); + let allHistoryAvailableRows = Number(activeInitialPayload.all_history_available_rows || totalAvailableRows); + let archivedAvailableRows = Number(activeInitialPayload.archived_available_rows || Math.max(allHistoryAvailableRows - activeAvailableRows, 0)); + let loadedLimit = payloadLimit(activeInitialPayload); const rowsEl = document.getElementById('rows'); const detailEl = document.getElementById('detail'); const detailToggleEl = document.getElementById('detailToggle'); @@ -323,17 +379,26 @@ const loadMoreRowsEl = document.getElementById('loadMoreRows'); const pageStatusEl = document.getElementById('pageStatus'); const pagerEl = document.getElementById('pager'); + const rowLoadProgressEl = document.getElementById('rowLoadProgress'); + const rowLoadProgressLabelEl = document.getElementById('rowLoadProgressLabel'); + const rowLoadProgressCountEl = document.getElementById('rowLoadProgressCount'); + const rowLoadProgressBarEl = document.getElementById('rowLoadProgressBar'); const toTopEl = document.getElementById('toTop'); let rowByRecordId = new Map(); let threadAttachmentByRecordId = new Map(); + let callAdjacencyByRecordId = new Map(); + let supplementalRowsByRecordId = new Map(); + const callFetchInFlightByRecordId = new Set(); const expandedThreads = new Set(); const liveRefreshSupported = window.location.protocol !== 'file:'; - const initialPayloadIncludeArchived = Boolean(initialPayload.include_archived); + const initialPayloadIncludeArchived = Boolean(activeInitialPayload.include_archived); let includeArchived = initialPayloadIncludeArchived; if (liveRefreshSupported && initialState.historyScope === 'all') includeArchived = true; const needsInitialHistoryRefresh = liveRefreshSupported && includeArchived !== initialPayloadIncludeArchived; const liveRefreshIntervalMs = 10000; const pageSize = 500; + const initialHydrationChunkSize = 500; + const backgroundHydrationChunkSize = 2000; const threadCallPageSize = 100; const defaultContextEntries = 80; const datePresetLabels = { @@ -346,6 +411,7 @@ }; const allowedDatePresets = new Set(Object.keys(datePresetLabels)); let activeView = ['calls', 'threads', 'insights', 'call'].includes(initialState.view) ? initialState.view : 'insights'; + document.body.dataset.activeView = activeView; let sortKey = optionValueExists(sortEl, initialState.sort) ? initialState.sort : sortEl.value || 'attention'; let sortDirection = ['asc', 'desc'].includes(initialState.direction) ? initialState.direction : defaultSortDirection(sortKey); let threadCallSortKey = 'time'; @@ -354,6 +420,11 @@ let selectedRecordId = initialState.record || ''; let selectedThreadKey = initialState.thread || ''; let refreshInFlight = false; + let rowHydrationInFlight = false; + let rowHydrationComplete = !shellBoot && data.length > 0; + let rowHydrationError = ''; + let rowHydrationGeneration = 0; + let rowHydrationRestartRequested = false; let autoRefreshTimer = null; let currentPage = 1; const threadCallVisiblePages = new Map(); @@ -618,6 +689,56 @@ ? tf('caption.loaded_capped', { loaded, available }) : tf('caption.loaded', { loaded }); } + function rowHydrationTarget() { + const available = Math.max(0, Number(totalAvailableRows || 0)); + if (!available) return 0; + return loadedLimit === null ? available : Math.min(available, Number(loadedLimit || available)); + } + function rowsNeedHydration() { + const target = rowHydrationTarget(); + return liveRefreshSupported && target > 0 && data.length < target; + } + function clientFiltersActive(dateRange = currentDateRange()) { + return Boolean( + searchEl.value.trim() + || modelEl.value + || effortEl.value + || pricingStatusEl.value + || activePreset + || dateRange.active + || dateRange.invalid + ); + } + function summaryForCards(dateRange, rows) { + if (!summaryData || loadedLimit !== null || clientFiltersActive(dateRange) || data.length >= rowHydrationTarget()) return null; + return { + visibleCalls: Number(summaryData.visible_calls ?? summaryData.row_count ?? totalAvailableRows ?? rows.length), + totalTokens: Number(summaryData.total_tokens || 0), + cachedInputTokens: Number(summaryData.cached_input_tokens || 0), + uncachedInputTokens: Number(summaryData.uncached_input_tokens || 0), + reasoningOutputTokens: Number(summaryData.reasoning_output_tokens || 0), + estimatedCost: Number(summaryData.estimated_cost_usd || 0), + usageCredits: Number(summaryData.usage_credits || 0), + }; + } + function updateRowLoadProgress() { + if (!rowLoadProgressEl) return; + const target = rowHydrationTarget(); + const loaded = Math.min(data.length, target || data.length); + const shouldShow = activeView !== 'call' && liveRefreshSupported && (rowHydrationInFlight || rowsNeedHydration() || rowHydrationError); + rowLoadProgressEl.hidden = !shouldShow; + if (!shouldShow) return; + const totalText = number.format(target || totalAvailableRows || loaded); + const loadedText = number.format(loaded); + rowLoadProgressLabelEl.textContent = rowHydrationError ? t('state.error') : t('state.loading_rows'); + rowLoadProgressCountEl.textContent = rowHydrationError + ? rowHydrationError + : (rowHydrationComplete + ? tf('caption.rows_loaded_progress', { loaded: loadedText, total: totalText }) + : tf('caption.rows_loading_progress', { loaded: loadedText, total: totalText })); + const ratio = target ? Math.max(0, Math.min(100, (loaded / target) * 100)) : 0; + rowLoadProgressBarEl.style.width = `${ratio}%`; + } function historyRowsDescription() { const archived = Number(archivedAvailableRows || 0); if (includeArchived) { @@ -647,8 +768,24 @@ loadLimitEl.value = value; } function rebuildDashboardIndexes() { - rowByRecordId = new Map(data.map(row => [row.record_id, row])); - threadAttachmentByRecordId = new Map(data.map(row => [row.record_id, resolveThreadAttachment(row)])); + const indexedRows = [...data]; + for (const row of supplementalRowsByRecordId.values()) { + if (!row?.record_id || indexedRows.some(candidate => candidate.record_id === row.record_id)) continue; + indexedRows.push(row); + } + rowByRecordId = new Map(indexedRows.map(row => [row.record_id, row])); + threadAttachmentByRecordId = new Map(indexedRows.map(row => [row.record_id, resolveThreadAttachment(row)])); + callAdjacencyByRecordId = buildCallAdjacencyIndex(indexedRows); + } + function mergedRows(existingRows, nextRows) { + const seen = new Set(existingRows.map(row => row.record_id).filter(Boolean)); + const merged = [...existingRows]; + for (const row of nextRows) { + if (!row?.record_id || seen.has(row.record_id)) continue; + seen.add(row.record_id); + merged.push(row); + } + return merged; } function usageCreditStatusLabel(row) { if (usageCreditValue(row) === null) return t('allowance.row_no_rate'); @@ -897,7 +1034,7 @@ return dataRowReasoningTokens(row); } function adjacentCalls(row) { - return adjacentThreadCalls(data, row); + return adjacentThreadCalls(data, row, callAdjacencyByRecordId); } function cacheDiagnostic(row, previous = null) { const diagnostic = classifyCacheDiagnostic(row, previous); @@ -2063,22 +2200,29 @@ }); } function render() { - const dateRange = updateDateFilterControls(); - const rows = filtered(dateRange); rowsEl.textContent = ''; document.body.dataset.activeView = activeView; updateSortControls(); - const totalTokens = rows.reduce((sum, row) => sum + Number(row.total_tokens || 0), 0); - const cachedInputTokens = rows.reduce((sum, row) => sum + Number(row.cached_input_tokens || 0), 0); - const uncachedInputTokens = rows.reduce((sum, row) => sum + Number(row.uncached_input_tokens || 0), 0); - const reasoningOutputTokens = rows.reduce((sum, row) => sum + Number(row.reasoning_output_tokens || 0), 0); - setSummaryNumber('visibleCalls', rows.length, 'metric.visible_calls'); + if (activeView === 'call') { + callInvestigator.renderCallInvestigator(Array.from(rowByRecordId.values())); + fitModelPills(); + syncUrlState(); + return; + } + const dateRange = updateDateFilterControls(); + const rows = filtered(dateRange); + const shellSummary = summaryForCards(dateRange, rows); + const totalTokens = shellSummary ? shellSummary.totalTokens : rows.reduce((sum, row) => sum + Number(row.total_tokens || 0), 0); + const cachedInputTokens = shellSummary ? shellSummary.cachedInputTokens : rows.reduce((sum, row) => sum + Number(row.cached_input_tokens || 0), 0); + const uncachedInputTokens = shellSummary ? shellSummary.uncachedInputTokens : rows.reduce((sum, row) => sum + Number(row.uncached_input_tokens || 0), 0); + const reasoningOutputTokens = shellSummary ? shellSummary.reasoningOutputTokens : rows.reduce((sum, row) => sum + Number(row.reasoning_output_tokens || 0), 0); + setSummaryNumber('visibleCalls', shellSummary ? shellSummary.visibleCalls : rows.length, 'metric.visible_calls'); setSummaryNumber('totalTokens', totalTokens, 'metric.total_tokens'); setSummaryNumber('cachedTokens', cachedInputTokens, 'metric.cached_input'); setSummaryNumber('uncachedTokens', uncachedInputTokens, 'metric.uncached_input'); setSummaryNumber('reasoningTokens', reasoningOutputTokens, 'metric.reasoning_output'); - const estimatedCost = rows.reduce((sum, row) => sum + Number(row.estimated_cost_usd || 0), 0); - const usageCredits = sumUsageCredits(rows); + const estimatedCost = shellSummary ? shellSummary.estimatedCost : rows.reduce((sum, row) => sum + Number(row.estimated_cost_usd || 0), 0); + const usageCredits = shellSummary ? shellSummary.usageCredits : sumUsageCredits(rows); document.getElementById('estimatedCost').textContent = pricingConfigured ? moneyText(estimatedCost) : t('state.not_configured'); document.getElementById('usageCredits').textContent = credits(usageCredits); document.getElementById('allowanceImpact').textContent = allowanceImpactText(usageCredits); @@ -2097,6 +2241,7 @@ renderCalls(rows); } fitModelPills(); + updateRowLoadProgress(); syncUrlState(); scheduleFocusPendingTarget(); } @@ -2142,7 +2287,10 @@ showDetail(page.items[0]); } if (!rows.length) { - rowsEl.innerHTML = `${escapeHtml(t('state.no_calls'))}`; + const message = rowsNeedHydration() + ? t('caption.rows_loading_background') + : t('state.no_calls'); + rowsEl.innerHTML = `${escapeHtml(message)}`; } } function renderThreads(rows, mode = 'threads') { @@ -2516,6 +2664,20 @@ resetVisibleRows(); render(); } + async function routeBackToDashboard(view = 'calls') { + activeView = ['calls', 'threads', 'insights'].includes(view) ? view : 'calls'; + resetVisibleRows(); + if (liveRefreshSupported && !data.length) { + autoRefreshEl.checked = true; + await refreshDashboardData(false, { refreshLogs: false, resetRows: true }); + } else { + render(); + } + if (liveRefreshSupported && autoRefreshEl.checked) { + scheduleAutoRefresh(); + refreshDashboardIfStale(); + } + } function renderLiveStatus() { const label = t(liveStatusKey); const detail = liveStatusDetail || label; @@ -2531,7 +2693,8 @@ function updateToTopVisibility() { toTopEl.dataset.visible = window.scrollY > 320 ? 'true' : 'false'; } - function applyDashboardPayload(nextPayload) { + function applyDashboardPayload(nextPayload, options = null) { + const applyOptions = options || {}; if (nextPayload.translation_catalog) { translationCatalog = nextPayload.translation_catalog; fallbackTranslations = { ...builtInFallbackTranslations, ...(translationCatalog.en || fallbackTranslations) }; @@ -2543,7 +2706,16 @@ } currentLanguage = normalizeLanguage(nextPayload.language || currentLanguage); applyTranslations(); - data = payloadRows(nextPayload); + const nextRows = payloadRows(nextPayload); + if (applyOptions.appendRows) { + data = mergedRows(data, nextRows); + } else if (applyOptions.preserveRows) { + data = data.length ? data : nextRows; + } else { + data = nextRows; + } + summaryData = nextPayload.summary || summaryData; + if (!applyOptions.appendRows) shellBoot = Boolean(nextPayload.shell_boot); pricingConfigured = Boolean(nextPayload.pricing_configured); pricingSource = nextPayload.pricing_source || {}; pricingSnapshotWarning = nextPayload.pricing_snapshot_warning || ''; @@ -2557,12 +2729,18 @@ apiToken = nextPayload.api_token || apiToken; contextApiEnabled = Boolean(nextPayload.context_api_enabled); actionThresholds = nextPayload.action_thresholds || actionThresholds; + latestRefreshAt = nextPayload.latest_refresh_at || latestRefreshAt; totalAvailableRows = Number(nextPayload.total_available_rows || data.length); activeAvailableRows = Number(nextPayload.active_available_rows || data.length); allHistoryAvailableRows = Number(nextPayload.all_history_available_rows || totalAvailableRows); archivedAvailableRows = Number(nextPayload.archived_available_rows || Math.max(allHistoryAvailableRows - activeAvailableRows, 0)); includeArchived = Boolean(nextPayload.include_archived); - loadedLimit = payloadLimit(nextPayload); + if (!applyOptions.appendRows) loadedLimit = payloadLimit(nextPayload); + if (!applyOptions.appendRows) supplementalRowsByRecordId = new Map(); + restoredAggregatePayloadFromCache = false; + if (!nextPayload.shell_boot && !applyOptions.appendRows) { + writeAggregatePayloadCache({ ...nextPayload, api_token: apiToken }); + } rebuildDashboardIndexes(); rebuildFilterOptions(); updatePricingSourceLine(); @@ -2608,6 +2786,7 @@ getSelectedRecordId: () => selectedRecordId, setSelectedRecordId: value => { selectedRecordId = value || ''; }, getRowByRecordId: () => rowByRecordId, + fetchCallRecord, getContextRuntime: () => ({ apiToken, contextApiEnabled, activeView }), setContextApiEnabled: value => { contextApiEnabled = Boolean(value); }, renderDashboard: render, @@ -2620,24 +2799,193 @@ tableCaptionEl, defaultContextEntries, }); - async function refreshDashboardData(manual = false) { + async function fetchCallRecord(recordId) { + const normalizedRecordId = recordId || ''; + if (!liveRefreshSupported || !normalizedRecordId || !apiToken) return null; + const existing = rowByRecordId.get(normalizedRecordId); + if (existing) return existing; + if (callFetchInFlightByRecordId.has(normalizedRecordId)) return null; + callFetchInFlightByRecordId.add(normalizedRecordId); + try { + const params = new URLSearchParams({ record_id: normalizedRecordId, _: String(Date.now()) }); + const response = await fetch(`/api/call?${params.toString()}`, { + headers: { + 'Accept': 'application/json', + 'X-Codex-Usage-Token': apiToken, + }, + cache: 'no-store', + }); + if (!response.ok) return null; + const payload = await response.json(); + if (!payload?.record?.record_id) return null; + const adjacentRecords = Array.isArray(payload.adjacent_records) && payload.adjacent_records.length + ? payload.adjacent_records + : [payload.previous_record, payload.record, payload.next_record].filter(Boolean); + adjacentRecords.forEach(record => { + if (record?.record_id) supplementalRowsByRecordId.set(record.record_id, record); + }); + rebuildDashboardIndexes(); + if (activeView === 'call' && selectedRecordId === normalizedRecordId) { + selectedThreadKey = rowAttachment(payload.record).key; + render(); + } + return payload.record; + } catch (_error) { + return null; + } finally { + callFetchInFlightByRecordId.delete(normalizedRecordId); + } + } + async function navigateToCallRecord(recordId) { + const normalizedRecordId = recordId || ''; + if (!normalizedRecordId) return; + selectedRecordId = normalizedRecordId; + const existing = rowByRecordId.get(normalizedRecordId); + if (existing) selectedThreadKey = rowAttachment(existing).key; + activeView = 'call'; + render(); + if (!existing) { + const fetched = await fetchCallRecord(normalizedRecordId); + if (fetched && selectedRecordId === normalizedRecordId) { + selectedThreadKey = rowAttachment(fetched).key; + render(); + } + } + } + async function hydrateDashboardRows(options = null) { + if (!liveRefreshSupported || activeView === 'call') return; + const hydrateOptions = options || {}; + if (rowHydrationInFlight) { + if (hydrateOptions.reset) rowHydrationRestartRequested = true; + return; + } + const target = rowHydrationTarget(); + if (!target) { + rowHydrationComplete = true; + updateRowLoadProgress(); + return; + } + if (hydrateOptions.reset) { + data = []; + supplementalRowsByRecordId = new Map(); + rowHydrationComplete = false; + rowHydrationGeneration += 1; + rebuildDashboardIndexes(); + rebuildFilterOptions(); + render(); + } + if (data.length >= target) { + rowHydrationComplete = true; + updateRowLoadProgress(); + return; + } + const generation = rowHydrationGeneration; + rowHydrationInFlight = true; + rowHydrationError = ''; + updateLiveStatus('status.checking', t('live.loading_rows')); + updateRowLoadProgress(); + try { + while (data.length < target && generation === rowHydrationGeneration && activeView !== 'call') { + const offset = data.length; + const remaining = target - offset; + const chunkSize = Math.min( + offset === 0 ? initialHydrationChunkSize : backgroundHydrationChunkSize, + remaining, + ); + const params = new URLSearchParams({ + limit: String(chunkSize), + offset: String(offset), + include_archived: includeArchived ? '1' : '0', + lang: currentLanguage, + _: String(Date.now()), + }); + const response = await fetch(`/api/usage?${params.toString()}`, { + headers: { + 'Accept': 'application/json', + 'X-Codex-Usage-Token': apiToken, + }, + cache: 'no-store', + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const payload = await response.json(); + if (payload.error) throw new Error(payload.error); + if (generation !== rowHydrationGeneration || activeView === 'call') break; + const rows = payloadRows(payload); + if (!rows.length) break; + applyDashboardPayload(payload, { appendRows: true }); + updateRowLoadProgress(); + if (!payload.has_more || rows.length < chunkSize) break; + } + rowHydrationComplete = data.length >= rowHydrationTarget(); + updateLiveStatus(autoRefreshEl.checked ? 'badge.live' : 'status.updated', `${loadedRowsDescription()}. ${historyRowsDescription()}`); + } catch (error) { + rowHydrationError = error.message || String(error); + updateLiveStatus('status.refresh_error', tf('live.refresh_unavailable', { message: rowHydrationError, suffix: '' })); + } finally { + rowHydrationInFlight = false; + updateRowLoadProgress(); + const shouldRestart = rowHydrationRestartRequested && activeView !== 'call'; + rowHydrationRestartRequested = false; + if (shouldRestart) { + hydrateDashboardRows(); + } else { + render(); + } + } + } + async function refreshDashboardIfStale() { + if (!liveRefreshSupported || !apiToken || activeView === 'call') return; + try { + const params = new URLSearchParams({ + include_archived: includeArchived ? '1' : '0', + _: String(Date.now()), + }); + const response = await fetch(`/api/status?${params.toString()}`, { + headers: { + 'Accept': 'application/json', + }, + cache: 'no-store', + }); + if (!response.ok) return; + const payload = await response.json(); + const statusRefreshAt = payload.latest_refresh_at || ''; + const scopedRows = Number(payload.row_counts?.scoped_rows); + const rowCountChanged = Number.isFinite(scopedRows) && scopedRows !== totalAvailableRows; + const refreshChanged = statusRefreshAt && statusRefreshAt !== latestRefreshAt; + if (rowCountChanged || refreshChanged) { + refreshDashboardData(false, { refreshLogs: false, resetRows: true }); + } else if (rowsNeedHydration()) { + hydrateDashboardRows(); + } + } catch (_error) { + // Background freshness checks must never interrupt the local dashboard. + } + } + async function refreshDashboardData(manual = false, options = null) { if (!liveRefreshSupported) { updateLiveStatus('status.reloading', t('live.reloading_static')); window.location.reload(); return; } + if (activeView === 'call' && !manual) return; if (refreshInFlight) return; + const refreshOptions = options || {}; + const refreshLogs = refreshOptions.refreshLogs === undefined ? manual : Boolean(refreshOptions.refreshLogs); + const resetRows = refreshOptions.resetRows !== undefined + ? Boolean(refreshOptions.resetRows) + : Boolean(manual || refreshLogs); refreshInFlight = true; refreshDashboardEl.disabled = true; - updateLiveStatus(manual ? 'status.refreshing' : 'status.checking', manual ? t('live.refreshing_index') : t('live.checking_usage')); + updateLiveStatus(refreshLogs ? 'status.refreshing' : 'status.checking', refreshLogs ? t('live.refreshing_index') : t('live.checking_usage')); try { const params = new URLSearchParams({ - refresh: '1', limit: loadLimitEl.value, include_archived: includeArchived ? '1' : '0', lang: currentLanguage, + shell: '1', _: String(Date.now()), }); + if (refreshLogs) params.set('refresh', '1'); const response = await fetch(`/api/usage?${params.toString()}`, { headers: { 'Accept': 'application/json', @@ -2650,7 +2998,14 @@ } const nextPayload = await response.json(); if (nextPayload.error) throw new Error(nextPayload.error); + if (resetRows) { + data = []; + supplementalRowsByRecordId = new Map(); + rowHydrationGeneration += 1; + rowHydrationComplete = false; + } applyDashboardPayload(nextPayload); + if (activeView !== 'call') hydrateDashboardRows({ reset: resetRows }); const result = nextPayload.refresh_result || {}; const indexed = result.inserted_or_updated_events === undefined ? '' @@ -2671,9 +3026,9 @@ function scheduleAutoRefresh() { if (autoRefreshTimer) window.clearInterval(autoRefreshTimer); autoRefreshTimer = null; - if (!autoRefreshEl.checked || !liveRefreshSupported) return; + if (!autoRefreshEl.checked || !liveRefreshSupported || activeView === 'call') return; autoRefreshTimer = window.setInterval(() => { - if (document.visibilityState === 'visible') refreshDashboardData(false); + if (document.visibilityState === 'visible') refreshDashboardIfStale(); }, liveRefreshIntervalMs); } insightsViewEl.addEventListener('click', () => setView('insights')); @@ -2689,7 +3044,7 @@ loadLimitEl.addEventListener('change', () => { resetVisibleRows(); if (liveRefreshSupported) { - refreshDashboardData(true); + refreshDashboardData(false, { refreshLogs: false, resetRows: true }); } else { updateLiveStatus('status.static', t('live.load_static_hint')); } @@ -2700,7 +3055,7 @@ updateHistoryScopeControl(); syncUrlState(); if (liveRefreshSupported) { - refreshDashboardData(true); + refreshDashboardData(false, { refreshLogs: false, resetRows: true }); } else { updateLiveStatus('status.static', t('live.history_static_hint')); } @@ -2708,10 +3063,10 @@ autoRefreshEl.addEventListener('change', () => { scheduleAutoRefresh(); updateLiveStatus(autoRefreshEl.checked ? 'badge.live' : 'status.paused', `${autoRefreshEl.checked ? tf('live.every', { seconds: liveRefreshIntervalMs / 1000 }) : t('live.paused')}. ${loadedRowsDescription()}. ${historyRowsDescription()}`); - if (autoRefreshEl.checked) refreshDashboardData(false); + if (autoRefreshEl.checked) refreshDashboardIfStale(); }); document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible' && autoRefreshEl.checked) refreshDashboardData(false); + if (document.visibilityState === 'visible' && autoRefreshEl.checked) refreshDashboardIfStale(); }); document.addEventListener('keydown', event => { const target = event.target; @@ -2742,6 +3097,13 @@ if (row) selectRow(row); }); rowsEl.addEventListener('click', event => { + const dashboardRoute = event.target.closest('[data-dashboard-route]'); + if (dashboardRoute && rowsEl.contains(dashboardRoute)) { + event.preventDefault(); + event.stopPropagation(); + routeBackToDashboard(dashboardRoute.dataset.dashboardRoute || 'calls'); + return; + } const rowLink = event.target.closest('a.row-investigator-link'); if (rowLink && rowsEl.contains(rowLink)) { if (!liveRefreshSupported) return; @@ -2771,13 +3133,7 @@ event.preventDefault(); event.stopPropagation(); const recordId = navButton.dataset.callNavRecord; - const row = rowByRecordId.get(recordId); - if (row) { - selectedRecordId = row.record_id || ''; - selectedThreadKey = rowAttachment(row).key; - activeView = 'call'; - render(); - } + navigateToCallRecord(recordId); return; } const sortButton = event.target.closest('[data-thread-call-sort-key]'); @@ -2796,22 +3152,23 @@ render(); return; } - const callRow = event.target.closest('.thread-call-row'); + const callRow = event.target.closest('.call-row, .thread-call-row'); if (!callRow || !rowsEl.contains(callRow)) return; + event.preventDefault(); + event.stopPropagation(); const row = rowByRecordId.get(callRow.dataset.recordId); if (row) openInvestigator(row); }); rowsEl.addEventListener('dblclick', event => { - if (event.target.closest('a.row-investigator-link')) return; - const callRow = event.target.closest('.thread-call-row'); + const callRow = event.target.closest('.call-row, .thread-call-row'); if (!callRow || !rowsEl.contains(callRow)) return; - const row = rowByRecordId.get(callRow.dataset.recordId); - if (row) openInvestigator(row); + event.preventDefault(); + event.stopPropagation(); }); rowsEl.addEventListener('keydown', event => { if (event.target.closest('a.row-investigator-link')) return; if (event.key !== 'Enter' && event.key !== ' ') return; - const callRow = event.target.closest('.thread-call-row'); + const callRow = event.target.closest('.call-row, .thread-call-row'); if (!callRow || !rowsEl.contains(callRow)) return; event.preventDefault(); const row = rowByRecordId.get(callRow.dataset.recordId); @@ -2861,6 +3218,7 @@ applyTranslations(); rebuildFilterOptions(); applyInitialState(); + if (!initialPayload.investigator_boot) writeAggregatePayloadCache(activeInitialPayload); updatePricingSourceLine(); updateAllowanceSourceLine(); updatePrivacyModeLine(); @@ -2873,10 +3231,19 @@ loadLimitEl.disabled = true; historyScopeEl.disabled = true; updateLiveStatus('status.static', `${t('status.static')}. ${loadedRowsDescription()}. ${historyRowsDescription()}`); + } else if (activeView === 'call') { + autoRefreshEl.checked = false; + updateLiveStatus('badge.live', `${t('dashboard.view.call')}. ${loadedRowsDescription()}. ${historyRowsDescription()}`); } else { updateLiveStatus('badge.live', `${tf('live.every', { seconds: liveRefreshIntervalMs / 1000 })}. ${loadedRowsDescription()}. ${historyRowsDescription()}`); scheduleAutoRefresh(); - if (needsInitialHistoryRefresh) refreshDashboardData(false); + if (needsInitialHistoryRefresh) { + refreshDashboardData(false, { refreshLogs: false, resetRows: true }); + } else if (rowsNeedHydration()) { + hydrateDashboardRows(); + } else if (restoredAggregatePayloadFromCache) { + refreshDashboardIfStale(); + } } updateToTopVisibility(); render(); diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js index 2ace094..7cfe5a9 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_call_investigator.js @@ -35,6 +35,7 @@ getSelectedRecordId, setSelectedRecordId, getRowByRecordId, + fetchCallRecord, getContextRuntime, setContextApiEnabled, renderDashboard, @@ -170,6 +171,7 @@ const serialized = payload.serialized_evidence || {}; const serializedTokens = Number(serialized.raw_json_token_estimate || 0); const serializedChars = Number(serialized.raw_json_char_count || 0); + const serializedDeferred = Boolean(serialized.deferred || serialized.deferred_buckets); const serializedBound = serializedTokens > 0 ? Math.min(serializedTokens, uncached) : 0; const visibleGap = Math.max(uncached - visibleTokenEstimate, 0); const serializedCandidate = serializedBound > visibleTokenEstimate ? serializedBound - visibleTokenEstimate : 0; @@ -186,10 +188,12 @@ serializedChars, serializedLineCount: Number(serialized.raw_line_count || 0), serializedEstimator: serialized.token_estimator || payload.visible_token_estimator || 'chars_per_4_fallback', + serializedDeferred, serializedCandidate, remainingAfterSerialized, serializedBuckets: Array.isArray(serialized.buckets) ? serialized.buckets : [], serializedUpperBound: Boolean(serialized.upper_bound), + contextMode: payload.context_mode || 'quick', source: payload.source || {}, }; } @@ -240,7 +244,7 @@ : 'No previous call is loaded for this resolved thread, so call-to-call deltas are unavailable.'; const stats = contextEvidenceStats(row); const evidence = stats - ? `Evidence analyzed: ${number.format(stats.totalEntries)} selected-turn entries, ${number.format(stats.visibleChars)} visible redacted chars, ${number.format(stats.visibleTokenEstimate)} visible tokens via ${stats.estimator}. Serialized local upper bound: ${number.format(stats.serializedTokens)} tokens from ${number.format(stats.serializedChars)} raw JSON chars. ${number.format(stats.entries)} entries rendered initially.` + ? `Evidence analyzed: ${number.format(stats.totalEntries)} selected-turn entries, ${number.format(stats.visibleChars)} visible redacted chars, ${number.format(stats.visibleTokenEstimate)} visible tokens via ${stats.estimator}. ${stats.serializedDeferred ? 'Fast serialized estimate only; full serialized grouping is deferred.' : `Serialized local upper bound: ${number.format(stats.serializedTokens)} tokens from ${number.format(stats.serializedChars)} raw JSON chars.`} ${number.format(stats.entries)} entries rendered initially.` : 'Evidence is loading from the local JSONL source. Aggregate token counts are exact, but visible-context attribution needs that runtime evidence.'; return `
@@ -327,11 +331,13 @@ function renderCallNavigation(row, previous, next) { const backUrl = tableUrlForRow(row); + const previousRecordId = previous?.record_id || row.previous_record_id || ''; + const nextRecordId = next?.record_id || row.next_record_id || ''; return `
- ${escapeHtml(t('button.back_to_dashboard'))} - - + ${escapeHtml(t('button.back_to_dashboard'))} + +
`; @@ -348,6 +354,17 @@ ? tf('caption.call_investigator', { record: short(getSelectedRecordId(), '').slice(0, 12) }) : t('call.open_hint'); if (!row) { + const selectedRecordId = getSelectedRecordId(); + if (selectedRecordId && fetchCallRecord) { + rowsEl.innerHTML = `${escapeHtml(t('context.loading'))}`; + detailEl.textContent = t('dashboard.detail.empty'); + fetchCallRecord(selectedRecordId).then(fetchedRow => { + if (!fetchedRow && getSelectedRecordId() === selectedRecordId) { + rowsEl.innerHTML = `${escapeHtml(t('call.not_found'))}`; + } + }); + return; + } rowsEl.innerHTML = `${escapeHtml(t('call.not_found'))}`; detailEl.textContent = t('dashboard.detail.empty'); return; @@ -418,12 +435,12 @@
${callMetricCard(t('metric.uncached_input'), number.format(uncachedInputTokens(row)), t('call.exact_label'))} ${callMetricCard(t('call.visible_estimate'), visibleEstimateValue, evidenceStats ? `${number.format(evidenceStats.visibleChars)} analyzed chars · ${evidenceStats.estimator}` : t('call.evidence_label'))} - ${callMetricCard(t('call.serialized_upper_bound'), serializedUpperBoundValue, evidenceStats ? `${number.format(evidenceStats.serializedChars)} raw JSON chars · ${evidenceStats.serializedEstimator}` : t('call.evidence_label'))} + ${callMetricCard(t('call.serialized_upper_bound'), serializedUpperBoundValue, evidenceStats ? `${number.format(evidenceStats.serializedChars)} raw JSON chars · ${evidenceStats.serializedEstimator}${evidenceStats.serializedDeferred ? ` · ${t('call.serialized_quick_hint')}` : ''}` : t('call.evidence_label'))} ${callMetricCard(t('call.hidden_estimate'), hiddenEstimateValue, evidenceStats ? t('call.visible_gap') : t('call.evidence_label'))} ${callMetricCard(t('call.serialized_candidate'), serializedCandidateValue, evidenceStats ? t('call.serialized_candidate_hint') : t('call.evidence_label'))} ${callMetricCard(t('call.remaining_after_serialized'), remainingAfterSerializedValue, evidenceStats ? t('call.remaining_after_serialized_hint') : t('call.evidence_label'))}
- ${renderSerializedEvidenceBreakdown(evidenceStats)} + ${renderSerializedEvidenceBreakdown(row, evidenceStats)}

${escapeHtml(t('call.context_estimate_hint'))}

@@ -570,8 +587,24 @@ `; } - function renderSerializedEvidenceBreakdown(stats) { + function renderSerializedEvidenceBreakdown(row, stats) { if (!stats || !stats.serializedTokens) return ''; + const requestState = contextStateForRow(row); + const disabled = contextDisabledAttr(); + const fullAnalysisButton = stats.serializedDeferred && requestState.mode !== 'full' && !disabled + ? `` + : ''; + if (stats.serializedDeferred) { + return ` +
+
+ ${escapeHtml(t('call.serialized_breakdown'))} + ${escapeHtml(t('call.serialized_deferred'))} +
+ ${fullAnalysisButton} +
+ `; + } const buckets = stats.serializedBuckets.slice(0, 6); const bucketHtml = buckets.map(bucket => `
@@ -607,6 +640,11 @@ loadContext(row, { includeToolOutput: !current.includeToolOutput }, contextResult); }); }); + root.querySelectorAll('[data-context-full-analysis]').forEach(button => { + button.addEventListener('click', () => { + loadContext(row, { mode: 'full' }, contextResult); + }); + }); root.querySelectorAll('[data-context-enable]').forEach(button => { button.addEventListener('click', () => enableContextApi(row, contextResult)); }); @@ -691,9 +729,10 @@ function defaultContextRequest() { return { - includeToolOutput: true, + mode: 'quick', + includeToolOutput: false, includeCompactionHistory: false, - maxChars: 0, + maxChars: null, maxEntries: defaultContextEntries, }; } @@ -702,6 +741,7 @@ const base = contextStateForRow(row); const updates = typeof options === 'boolean' ? { includeToolOutput: options } : (options || {}); const next = { ...base, ...updates }; + next.mode = next.mode === 'full' ? 'full' : 'quick'; next.includeToolOutput = Boolean(next.includeToolOutput); next.includeCompactionHistory = Boolean(next.includeCompactionHistory); if (next.maxEntries === undefined) next.maxEntries = defaultContextEntries; @@ -722,6 +762,7 @@ target.innerHTML = `

${escapeHtml(t('context.loading'))}

`; const requestState = nextContextState(row, options); const params = new URLSearchParams({ record_id: row.record_id }); + params.set('mode', requestState.mode || 'quick'); if (requestState.includeToolOutput) params.set('include_tool_output', '1'); if (requestState.includeCompactionHistory) params.set('include_compaction_history', '1'); if (requestState.maxChars !== null && requestState.maxChars !== undefined) { @@ -851,58 +892,6 @@ `; } - function renderThreadAnchors(payload) { - const anchors = payload.call_anchors || payload.thread_anchors || {}; - if (!anchors.available) return ''; - const seen = new Set(); - const candidates = [ - ['before', t('context.anchor_before'), anchors.before_message || anchors.selected_lead_in], - [ - 'reasoning', - t('context.anchor_reasoning_output'), - anchors.reasoning_output || anchors.latest_message || anchors.after_message, - ], - ].filter(([, , anchor]) => anchor && anchor.text).filter(([, , anchor]) => { - const identity = `${anchor.line_number || ''}:${anchor.role || ''}:${anchor.text || ''}`; - if (seen.has(identity)) return false; - seen.add(identity); - return true; - }); - if (!candidates.length) return ''; - return ` -
-
-
- ${escapeHtml(t('context.thread_anchors'))} - ${escapeHtml(t('context.thread_anchors_hint'))} -
- ${escapeHtml(tf('context.anchor_count', { count: number.format(anchors.message_count || candidates.length) }))} -
-
- ${candidates.map(([key, label, anchor]) => renderThreadAnchorCard(key, label, anchor)).join('')} -
-
- `; - } - - function renderThreadAnchorCard(key, label, anchor) { - const role = anchor.role || 'unknown'; - const meta = [ - formatTimestamp(anchor.timestamp, ''), - anchor.line_number ? tf('context.line', { line: anchor.line_number }) : '', - ].filter(Boolean).join(' - '); - return ` -
-
- ${escapeHtml(label)} - ${escapeHtml(role)} -
- ${meta ? `
${escapeHtml(meta)}
` : ''} -
${escapeHtml(anchor.text || '')}
-
- `; - } - function contextEntryWindow(entries, payload) { const sourceLine = Number(payload?.source?.line_number || 0); const selectedIndex = entries.findIndex(entry => sourceLine && Number(entry.line_number || 0) === sourceLine); @@ -984,7 +973,7 @@
`; }).join(''); - return `

${escapeHtml(note)}

${renderThreadAnchors(payload)}${contextLimitActions(payload)}${body || `

${escapeHtml(t('state.no_context_entries'))}

`}`; + return `

${escapeHtml(note)}

${contextLimitActions(payload)}${body || `

${escapeHtml(t('state.no_context_entries'))}

`}`; } function contextEntryKey(entry, index) { diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js index dc25b4a..ec833dc 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_data.js @@ -143,7 +143,43 @@ .sort(chronological); } - function adjacentThreadCalls(rows, row) { + function persistedAdjacentRow(recordById, row, key) { + const recordId = row ? row[key] : null; + if (!recordId) return undefined; + return recordById.get(recordId) || null; + } + + function buildCallAdjacencyIndex(rows) { + const recordById = new Map(rows.filter(row => row.record_id).map(row => [row.record_id, row])); + const threadRows = new Map(); + rows.forEach(row => { + const key = resolveThreadAttachment(row).key; + if (!threadRows.has(key)) threadRows.set(key, []); + threadRows.get(key).push(row); + }); + const adjacencyByRecordId = new Map(); + threadRows.forEach(calls => { + calls.sort(chronological); + calls.forEach((row, index) => { + if (!row.record_id) return; + const persistedPrevious = persistedAdjacentRow(recordById, row, 'previous_record_id'); + const persistedNext = persistedAdjacentRow(recordById, row, 'next_record_id'); + adjacencyByRecordId.set(row.record_id, { + calls, + index, + previous: persistedPrevious !== undefined ? persistedPrevious : index > 0 ? calls[index - 1] : null, + next: persistedNext !== undefined ? persistedNext : index < calls.length - 1 ? calls[index + 1] : null, + }); + }); + }); + return adjacencyByRecordId; + } + + function adjacentThreadCalls(rows, row, adjacencyByRecordId = null) { + if (adjacencyByRecordId && row && row.record_id) { + const adjacent = adjacencyByRecordId.get(row.record_id); + if (adjacent) return adjacent; + } const calls = resolvedThreadRows(rows, row); const index = calls.findIndex(candidate => candidate.record_id === row.record_id); return { @@ -235,6 +271,7 @@ resolveThreadAttachment, chronological, resolvedThreadRows, + buildCallAdjacencyIndex, adjacentThreadCalls, classifyCacheDiagnostic, callAccountingDelta, diff --git a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_template.html b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_template.html index 3ff3a68..3c88943 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +++ b/src/codex_usage_tracker/plugin_data/dashboard/dashboard_template.html @@ -7,7 +7,7 @@ __TITLE__ - +
@@ -109,6 +109,15 @@

Model Calls

+ diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/ar.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/ar.json index ef2ecf4..273b0b5 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/ar.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/ar.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "تحقق من الملفات المعاد تقديمها أو مخرجات الأداة بعد انقطاع إعادة استخدام ذاكرة التخزين المؤقت.", + "action.compare_fresh_input": "قارن المدخلات الجديدة مع المنعطف السابق قبل المتابعة.", + "action.compare_subagent_calls": "قارن الوكيل الفرعي المرفق أو قم بمراجعة المكالمات قبل تغيير سير العمل الأصلي.", + "action.configure_pricing": "قم بتكوين التسعير قبل الثقة بإجماليات التكلفة.", + "action.copied": "منقول", + "action.copy_failed": "فشل النسخ", + "action.expand_or_select_recommendations": "قم بتوسيع المكالمات أو حدد صفًا للتوصيات على مستوى المكالمة.", + "action.exported": "تم التصدير {count}", + "action.inspect_thread_timeline": "افحص الجدول الزمني لسلسلة الرسائل وفكر في بدء سلسلة رسائل جديدة.", + "action.review_context_growth": "راجع المكان الذي يبدأ فيه نمو السياق وفكر في بدء موضوع جديد.", + "action.review_reasoning_effort": "راجع ما إذا كان جهد الاستدلال مناسبًا لهذه المهمة.", + "action.run": "تشغيل", + "action.set_limits": "ضع الحدود", + "action.use_aggregate_first": "استخدم الحقول المجمعة أولاً؛ تحميل السياق فقط إذا كانت الإشارة لا تزال غير واضحة.", + "allowance.counted": "يتم احتساب {value} ضمن حدود الاستخدام Codex", + "allowance.cr_left": "{value} cr اليسار", + "allowance.credit_coverage": "التغطية الائتمانية {ratio} للرموز المحملة.", + "allowance.credit_rates": "معدلات الائتمان: {source}.", + "allowance.credits_remaining": "{value} الاعتمادات المتبقية", + "allowance.init_hint": "قم بتشغيل codex-usage-tracker init-allowance لإضافة نوافذ الاستخدام المتبقية.", + "allowance.of_allowance": "{ratio} من البدل", + "allowance.of_total": "{used} من {total} الاعتمادات", + "allowance.rate_card_error": "خطأ في بطاقة السعر: {error}", + "allowance.remaining": "{value} المتبقي", + "allowance.resets": "إعادة الضبط: {resets}", + "allowance.row_no_rate": "لم يتم تعيين معدل ائتمان Codex", + "allowance.title_hint": "أضف ~/.codex-usage-tracker/allowance.json لإظهار الاستخدام المتبقي لمدة 5 ساعات والأسبوع.", + "allowance.used_vs_remaining": "{used} المستخدم مقابل {remaining} المتبقي", + "allowance.window_configured": "تم تكوين {label}", + "allowance.windows": "النوافذ المسموح بها: {windows}", + "aria.current_view_actions": "إجراءات العرض الحالية", + "aria.dashboard_status": "حالة لوحة القيادة", + "aria.dashboard_view": "عرض لوحة القيادة", + "aria.history_title": "الجلسات النشطة فقط هي الافتراضية. يقوم كل السجل بمسح سجلات الجلسات المؤرشفة أثناء التحديث المباشر.", + "aria.inspect_thread": "افحص استخدام {thread}", + "aria.refresh_controls": "ضوابط تحديث لوحة القيادة", + "aria.table_pages": "صفحات الجدول", + "badge.costs": "التكاليف", + "badge.credits": "الاعتمادات", + "badge.live": "مباشر", + "badge.metadata_mode": "البيانات الوصفية {mode}", + "badge.metadata_normal": "البيانات الوصفية عادية", + "badge.no_costs": "لا توجد تكاليف", + "badge.parser_warnings": "تحذيرات المحلل اللغوي", + "badge.static": "ثابت", + "badge.unofficial_project": "مشروع غير رسمي", + "badge.unofficial_project_title": "Codex Usage Tracker مستقلة وليست من إنتاج OpenAI أو تابعة لها أو معتمدة منها أو مدعومة منها. OpenAI وCodex هي علامات تجارية مملوكة لشركة OpenAI.", + "button.back_to_dashboard": "العودة إلى لوحة القيادة", + "button.clear": "واضح", + "button.copy_link": "انسخ الرابط", + "button.enable_context_loading": "تمكين تحميل السياق", + "button.export_csv": "تصدير CSV", + "button.full_serialized_analysis": "تشغيل التحليل المتسلسل الكامل", + "button.hide_details": "إخفاء التفاصيل", + "button.hide_tool_output": "إخفاء مخرجات الأداة", + "button.include_tool_output": "تضمين مخرجات الأداة", + "button.load_context": "تحميل السياق", + "button.load_more": "تحميل المزيد", + "button.load_older_context": "تحميل الإدخالات الأقدم", + "button.next": "التالي", + "button.next_call": "المكالمة التالية", + "button.no_char_limit": "لا يوجد حد للحرف", + "button.open_investigator": "فتح المحقق", + "button.previous": "السابق", + "button.previous_call": "المكالمة السابقة", + "button.refresh": "تحديث", + "button.show_compaction_history": "عرض الاستبدال المضغوط", + "button.show_tool_output": "إظهار مخرجات الأداة", + "button.show_turn_evidence": "إظهار دليل سجل الدوران", + "button.top": "أعلى", + "call.cache_accounting_delta": "دلتا التخزين المؤقت/المحاسبة", + "call.cache_cold": "السيرة الذاتية الباردة / ذاكرة التخزين المؤقت التي لا معنى لها", + "call.cache_diagnostics": "تشخيص ذاكرة التخزين المؤقت", + "call.cache_partial": "ذاكرة التخزين المؤقت الجزئية مفقودة", + "call.cache_spike": "ارتفاع غير مخبأ", + "call.cache_steady": "ملف تعريف ذاكرة التخزين المؤقت ثابت", + "call.cache_warm": "إعادة استخدام ذاكرة التخزين المؤقت الدافئة", + "call.compaction_diagnostics": "تشخيص الضغط", + "call.compaction_hint": "يمكن أن تظهر الأدلة المحملة أحداث الضغط الواضحة. يتم عرض سجل الاستبدال المنقح فقط بعد إجراء الاستبدال المضغوط.", + "call.context_estimate": "تقدير تغير السياق", + "call.context_estimate_hint": "قارن المدخلات الدقيقة غير المخزنة مؤقتًا مع أدلة السجل المرئية التي تم حسابها باستخدام الرمز المميز. يجب التعامل مع الفجوة على أنها سقالات مخفية أو تسلسل أو خطأ في تقدير أداة الرمز المميز.", + "call.derived_label": "مشتقة من المكالمات المجمعة المجاورة", + "call.estimated_label": "مقدر من حجم السجل المرئي", + "call.evidence_label": "أدلة وقت التشغيل", + "call.exact_accounting": "المحاسبة الرمزية الدقيقة", + "call.exact_label": "بالضبط من رد الاتصال الرمز المميز", + "call.hidden_estimate": "تقدير المدخلات المخفية/التسلسلية غير المبررة", + "call.no_previous": "لا توجد مكالمة سابقة في هذا الموضوع الذي تم حله.", + "call.not_found": "لم يتم العثور على المكالمة المحددة في صفوف لوحة المعلومات المحملة.", + "call.open_hint": "انقر فوق صف الاتصال لإجراء تشخيصات عميقة.", + "call.position": "اتصل بـ {position} في هذا الموضوع الذي تم حله.", + "call.post_compaction": "ما بعد الضغط ممكن", + "call.raw_evidence": "الأدلة الخام", + "call.remaining_after_serialized": "المتبقية بعد تسلسل ملزمة", + "call.remaining_after_serialized_hint": "لا يتم تغطية المدخلات غير المخزنة مؤقتًا حتى من خلال الحد الأعلى المتسلسل", + "call.serialized_bound_hint": "هيكل JSONL المحلي العلوي؛ ليس النص الفوري الدقيق.", + "call.serialized_breakdown": "مجموعات الأدلة المتسلسلة", + "call.serialized_bucket_detail": "{count} الحقول · {chars} الأحرف", + "call.serialized_candidate": "الحمل المتسلسل المحتمل", + "call.serialized_candidate_hint": "الحد الأعلى المتسلسل ناقص التقدير المرئي، مع تحديد المدخلات غير المخزنة مؤقتًا بالضبط", + "call.serialized_deferred": "تم تحميل التقدير السريع؛ تم تأجيل تحليل المجموعات المتسلسلة الكامل.", + "call.serialized_quick_hint": "تقدير سريع", + "call.serialized_upper_bound": "الحد الأعلى المحلي المتسلسل", + "call.visible_estimate": "تقدير السياق الجديد المرئي", + "call.visible_gap": "المدخلات غير المخزنة مؤقتًا مطروحًا منها التقدير المرئي", + "caption.ascending": "تصاعدي", + "caption.call_investigator": "التحقيق في المكالمة {record}.", + "caption.calls": "عرض استدعاءات النماذج الفردية مرتبة حسب {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "تنازلي", + "caption.initial_calls": "عرض المكالمات النموذجية الفردية.", + "caption.insights": "تم تصنيفها حسب التكلفة، وأرصدة الاستخدام، وإعادة استخدام ذاكرة التخزين المؤقت، وضغط السياق، والثقة في التسعير.", + "caption.loaded": "{loaded} تم تحميل المكالمات", + "caption.loaded_capped": "تم تحميل {loaded} من {available} للمكالمات", + "caption.rows_loaded_progress": "تم تحميل الصفوف: {loaded} من {total}", + "caption.rows_loading_background": "إجماليات لوحة المعلومات جاهزة. يتم تحميل الصفوف في الخلفية.", + "caption.rows_loading_progress": "جار تحميل الصفوف: {loaded} من {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "عرض سلاسل {threads} من المكالمات المصفاة {calls}، مرتبة حسب {sort}. {loaded}. انقر فوق سلسلة رسائل لتوسيع مكالماتها.", + "context.api_http": "تم إرجاع السياق API HTTP {status}.", + "context.api_unavailable": "السياق API غير متوفر هنا. قم بتشغيل codex-usage-tracker serve-dashboard --open لتحميل السياق عند الطلب.", + "context.auto_loading": "تحميل دليل الدوران المحدد مع تضمين مخرجات الأداة.", + "context.chars_omitted": "تم حذف {count} من الأحرف الزائدة عن الميزانية.", + "context.compaction_detected": "تم الكشف عن الضغط", + "context.compaction_replacement": "سياق الاستبدال المضغوط", + "context.compaction_replacement_count": "{count} إدخالات سجل الاستبدال متاحة.", + "context.disabled_hint": "تم إيقاف تحميل السياق لخادم لوحة المعلومات هذا. قم بتمكينه هنا لتحميل سياق JSONL المحلي عند الطلب.", + "context.enabled_note": "تم تمكين تحميل السياق. اضغط على \"إظهار دليل سجل الدوران\" لقراءة هذه المكالمة من مصدر JSONL المحلي.", + "context.file_hint": "افتح لوحة المعلومات هذه باستخدام codex-usage-tracker serve-dashboard لتحميل السياق الأولي عند الطلب.", + "context.line": "السطر {line}", + "context.loading": "جارٍ تحميل السياق المحلي...", + "context.local_redacted": "تم تحميل سياق JSONL المحلي عند الطلب. يتم تنقيح الموجهات ومخرجات الأداة للأنماط السرية الشائعة ولا يتم الاحتفاظ بها في SQLite أو لوحة المعلومات HTML.", + "context.no_char_limit_active": "لم يتم تطبيق حد لعدد الأحرف.", + "context.no_record_id": "لا يحتوي هذا الصف على معرف سجل للبحث عن السياق.", + "context.no_response": "لا توجد هيئة استجابة", + "context.older_omitted": "{count} تم حذف الإدخالات الأقدم.", + "context.ready_hint": "السياق غير مضمن في لوحة المعلومات هذه. اضغط على زر لقراءة هذه المكالمة من مصدر JSONL المحلي.", + "context.settings_http": "تم إرجاع إعدادات السياق HTTP {status}.", + "context.source": "المصدر: {file}:{line}", + "context.token_breakdown": "انهيار الرمز المميز", + "context.token_cached": "مخبأة", + "context.token_input": "الإدخال", + "context.token_output": "الإخراج", + "context.token_reasoning": "المنطق", + "context.token_required": "يتطلب تحميل السياق رمزًا مميزًا للوحة المعلومات localhost API.", + "context.token_scope_call": "هذه المكالمة", + "context.token_scope_earlier": "عدد الرموز المميزة في وقت سابق في نفس المنعطف", + "context.token_scope_previous": "عدد الرموز المميزة السابقة في نفس المنعطف", + "context.token_scope_selected": "عدد الرموز المميزة للمكالمات المحددة", + "context.token_scope_session": "الجلسة تراكمية", + "context.token_total": "المجموع", + "context.token_type": "اكتب", + "context.token_uncached": "غير مخزنة مؤقتا", + "context.tool_included": "تم تضمين مخرجات الأداة مع حدود التنقيح والحجم.", + "context.tool_omitted": "تم إخفاء مخرجات الأداة لهذا العرض.", + "credit.configured_rate": "معدل التكوين", + "credit.estimated_mapping": "رسم الخرائط المقدرة", + "credit.inferred_mapping": "تعيين النموذج المستنتج", + "credit.no_mapped_rate": "لا يوجد معدل معين", + "credit.no_rate": "لا يوجد معدل ائتماني", + "credit.official_match": "مطابقة بطاقة السعر الرسمية", + "credit.user_rate": "معدل الائتمان المقدم من المستخدم", + "credit.with_status": "{value} الاعتمادات · {status}", + "dashboard.call_details": "تفاصيل المكالمة", + "dashboard.detail.empty": "قم بتحريك الماوس أو النقر فوق أحد الصفوف لفحص حقول الاستخدام المجمعة.", + "dashboard.eyebrow": "تحليلات Codex المحلية", + "dashboard.local_storage_note": "يتذكر رأس لوحة المعلومات أيضًا اختيارك للغة محليًا.", + "dashboard.model_calls": "المكالمات النموذجية", + "dashboard.title": "لوحة تحكم الاستخدام", + "dashboard.top_threads_by_attention": "أعلى المواضيع حسب نقاط الاهتمام", + "dashboard.view.call": "اتصل بالمحقق", + "dashboard.view.calls": "المكالمات", + "dashboard.view.insights": "رؤى", + "dashboard.view.threads": "المواضيع", + "date.custom": "مخصص", + "date.invalid_range": "النطاق الزمني غير صالح", + "date.range_between": "{prefix} {start} إلى {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", - "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", - "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "date.range_from": "{prefix} من {start}", + "date.range_through": "{prefix} إلى {end}", + "detail.agent_nickname": "لقب الوكيل", + "detail.agent_role": "دور الوكيل", + "detail.allowance_impact": "تأثير البدل", + "detail.attached_calls": "المكالمات المرفقة", + "detail.auto_review_calls": "مكالمات المراجعة التلقائية", + "detail.cache_savings": "وفورات ذاكرة التخزين المؤقت", + "detail.call_number": "اتصل بـ {number}", + "detail.calls": "المكالمات", + "detail.context_window": "نافذة السياق", + "detail.cost_usage_context": "التكلفة والاستخدام والسياق", + "detail.credit_confidence": "الثقة الائتمانية", + "detail.credit_model": "نموذج الائتمان", + "detail.credit_note": "مذكرة الائتمان", + "detail.credit_source": "مصدر الائتمان", + "detail.credit_source_fetched": "تم جلب مصدر الائتمان", + "detail.credit_tier": "الطبقة الائتمانية", + "detail.cwd": "CWD", + "detail.efficiency_signals": "إشارات الكفاءة", + "detail.first_expensive_turn": "أول منعطف باهظ الثمن", + "detail.git_branch": "فرع جيت", + "detail.largest_cumulative_jump": "أكبر قفزة تراكمية", + "detail.latest_activity": "آخر النشاط", + "detail.model_mix": "مزيج النموذج", + "detail.next_action": "الإجراء التالي", + "detail.no_above_thresholds": "لا شيء فوق العتبات", + "detail.no_aggregate_action": "لم يتم وضع علامة على أي إجراء مجمع.", + "detail.parent_session": "جلسة الوالدين", + "detail.parent_thread": "موضوع الوالدين", + "detail.parent_updated": "تم تحديث الوالدين", + "detail.pricing_model": "نموذج التسعير", + "detail.pricing_status": "حالة التسعير", + "detail.project_cwd": "مشروع سي دي", + "detail.project_tags": "علامات المشروع", + "detail.raw_identifiers": "معرفات التجميع الخام", + "detail.reasoning_mix": "مزيج المنطق", + "detail.relationships": "العلاقات", + "detail.remote_hash": "التجزئة عن بعد", + "detail.remote_label": "التسمية عن بعد", + "detail.secondary_thread_fields": "حقول الخيوط الثانوية", + "detail.source_file_line": "الملف المصدر والخط", + "detail.source_line": "خط المصدر", + "detail.spawned_child_calls": "مكالمات الأطفال المولودة", + "detail.spawned_from": "نشأت من", + "detail.spawned_threads": "المواضيع المولدة", + "detail.subagent_before_spike": "وكيل فرعي قبل سبايك", + "detail.subagent_calls": "مكالمات الوكيل الفرعي", + "detail.subagent_type": "نوع الوكيل الفرعي", + "detail.thread_attachment": "مرفق الموضوع", + "detail.thread_attention_summary": "ملخص اهتمام الموضوع", + "detail.thread_lifecycle": "دورة حياة الخيط", + "detail.thread_narrative": "رواية الموضوع", + "detail.thread_source": "مصدر الموضوع", + "detail.thread_timeline": "الجدول الزمني للموضوع", + "detail.timeline_context": "السياق {value}", + "detail.timeline_empty": "لا توجد مكالمات في هذا الموضوع.", + "detail.timeline_meta": "{tokens} الرموز المميزة · {cost} · {credits} · ذاكرة التخزين المؤقت {cache}", + "detail.timestamp": "الطابع الزمني", + "detail.token_pricing_breakdown": "الرمز المميز وانهيار الأسعار", + "detail.tokens_at": "{tokens} الرموز المميزة في {time}", + "detail.turn": "بدوره", + "detail.why_flagged": "لماذا تم وضع علامة", + "docs.dashboard_guide": "دليل لوحة القيادة", + "effort.high": "عالية", + "effort.low": "منخفض", + "effort.medium": "متوسطة", + "filter.confidence": "الثقة", + "filter.effort": "المنطق", + "filter.end": "نهاية", + "filter.model": "نموذج", + "filter.project": "مشروع", + "filter.reasoning": "المنطق", + "filter.search": "بحث", + "filter.search_placeholder": "الموضوع، CWD، النموذج", + "filter.session": "جلسة", + "filter.sort": "فرز", + "filter.start": "ابدأ", + "filter.thread": "الموضوع", + "filter.time": "الوقت", + "flag.elevated_context_use": "استخدام السياق المرتفع", + "flag.expensive_low_output_call": "مكالمة باهظة الثمن منخفضة الإخراج", + "flag.high_context_use": "استخدام السياق العالي", + "flag.high_estimated_cost": "ارتفاع التكلفة المقدرة", + "flag.high_reasoning_share": "حصة المنطق عالية", + "flag.low_cache_reuse": "إعادة استخدام ذاكرة التخزين المؤقت منخفضة", + "history.active_hidden": "الجلسات النشطة فقط؛ {count} المكالمات المؤرشفة مخفية", + "history.active_only": "الجلسات النشطة فقط", + "history.all_empty": "تم تحديد كل التاريخ؛ لم تتم فهرسة أي مكالمات مؤرشفة حتى الآن", + "history.all_includes": "يتضمن كل السجل {count} المكالمات المؤرشفة", + "history.archived_scan_hint": "{detail}. يتم فحص الجلسات المؤرشفة فقط عند تحديد كل المحفوظات أثناء التحديث المباشر.", + "insight.apply_cache_misses": "تطبيق الإعداد المسبق لأخطاء ذاكرة التخزين المؤقت", + "insight.apply_context_bloat": "قم بتطبيق الإعداد المسبق للسياق", + "insight.codex_allowance_usage": "Codex استخدام البدل", + "insight.context_bloat": "انتفاخ السياق", + "insight.context_bloat_body": "تكون مكالمات {calls} عند استخدام سياق {ratio} أو أعلى منه.", + "insight.costliest_thread": "أغلى الخيط", + "insight.costliest_thread_body": "يحتوي {thread} على {calls} مكالمات و{tokens} رموز مميزة.", + "insight.credit_coverage_body": "{ratio} من الرموز المرئية تحدد معدلات الائتمان Codex.", + "insight.estimated_pricing": "التسعير المقدر", + "insight.estimated_pricing_body": "يتم تضمين أسعار أفضل التخمين، ولكن يجب مراجعتها بشكل منفصل.", + "insight.inspect_selected_call": "فحص المكالمة المحددة", + "insight.low_cache_reuse": "إعادة استخدام ذاكرة التخزين المؤقت منخفضة", + "insight.low_cache_reuse_body": "{calls} المكالمات ضمن {ratio} إعادة استخدام ذاكرة التخزين المؤقت. ابدأ بـ {thread}.", + "insight.open_thread_timeline": "فتح الجدول الزمني للموضوع", + "insight.reasoning_output_spike": "ارتفاع الناتج المنطق", + "insight.reasoning_spike_body": "يحتوي {thread} على أكبر استدعاء لإخراج المنطق في عامل التصفية الحالي.", + "insight.review_estimates": "مراجعة التقديرات", + "insight.review_highest_credit": "مراجعة المكالمات ذات الائتمان الأعلى", + "insight.review_pricing_gaps": "مراجعة فجوات التسعير", + "insight.unpriced_usage": "استخدام غير مسعر", + "insight.unpriced_usage_body": "يتم حذف هذه الرموز المميزة من إجماليات التكلفة المقدرة حتى يتم تكوين التسعير.", + "language.english": "الإنجليزية", + "language.label": "اللغة", + "language.vietnamese": "تينغ فيت", + "live.checking_usage": "جارٍ التحقق من الاستخدام الجديد...", + "live.every": "تحديث مباشر كل {seconds}s", + "live.history_static_hint": "قم بتشغيل codex-usage-tracker serve-dashboard للتبديل بين الجلسات النشطة وكل السجل من لوحة المعلومات.", + "live.indexed": "صفوف مجمعة مفهرسة {rows} من سجلات {files}.", + "live.load_static_hint": "قم بتشغيل codex-usage-tracker serve-dashboard لتحميل حجم سجل مختلف من لوحة المعلومات.", + "live.loading_rows": "جار تحميل الصفوف في الخلفية...", + "live.paused": "تم إيقاف التحديث المباشر مؤقتًا", + "live.refresh_suffix": ". أعد تحميل هذه الصفحة بعد إعادة إنشاء لوحة المعلومات الثابتة، أو قم بتشغيل codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "التحديث المباشر غير متاح: {message}{suffix}", + "live.refreshing_index": "جارٍ تحديث فهرس الاستخدام المحلي...", + "live.reloading_static": "جارٍ إعادة تحميل لقطة لوحة المعلومات الثابتة...", + "live.skipped": "تم تخطي {count} أحداث عدد الرموز المميزة المشوهة.", + "live.updated_detail": "تم التحديث {time}. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "درجة الاهتمام", + "metric.cache_ratio": "نسبة ذاكرة التخزين المؤقت", + "metric.cache_trend": "اتجاه ذاكرة التخزين المؤقت", + "metric.cached_input": "المدخلات المخزنة مؤقتا", + "metric.codex_credits": "Codex الاعتمادات", + "metric.context_trend": "اتجاه السياق", + "metric.context_use": "استخدام السياق", + "metric.estimated_cost": "التكلفة المقدرة", + "metric.input_tokens": "رموز الإدخال", + "metric.last_call_input": "إدخال المكالمة الأخيرة", + "metric.last_call_total": "إجمالي المكالمة الأخيرة", + "metric.max_context_use": "الحد الأقصى لاستخدام السياق", + "metric.output": "الإخراج", + "metric.output_tokens": "رموز الإخراج", + "metric.reasoning_output": "الاستنتاج المنطقي", + "metric.remaining_usage": "الاستخدام المتبقي", + "metric.session_cumulative": "الجلسة تراكمية", + "metric.total": "المجموع", + "metric.total_tokens": "مجموع الرموز", + "metric.uncached_input": "الإدخال غير المخزن مؤقتًا", + "metric.usage_credits": "اعتمادات الاستخدام", + "metric.usage_remaining": "الاستخدام المتبقي", + "metric.visible_calls": "مكالمات مرئية", + "nav.history": "التاريخ", + "nav.live": "مباشر", + "nav.load": "تحميل", + "option.active_sessions_only": "الجلسات النشطة فقط", + "option.all_confidence": "كل الثقة", + "option.all_efforts": "كل الجهود", + "option.all_history": "كل التاريخ", + "option.all_models": "جميع الموديلات", + "option.all_time": "كل الوقت", + "option.custom_range": "نطاق مخصص", + "option.estimated_cost": "التكلفة المقدرة", + "option.estimated_credit_mapping": "رسم الخرائط الائتمانية المقدرة", + "option.exact_cost": "التكلفة الدقيقة", + "option.exact_credit_rate": "معدل الائتمان الدقيق", + "option.highest_codex_credits": "أعلى Codex الاعتمادات", + "option.highest_context_use": "أعلى استخدام للسياق", + "option.highest_estimated_cost": "أعلى تكلفة تقديرية", + "option.last_7_days": "آخر 7 أيام", + "option.load_10000": "10.000 مكالمة", + "option.load_20000": "20.000 مكالمة", + "option.load_5000": "5000 مكالمة", + "option.load_all": "جميع المكالمات", + "option.lowest_cache_ratio": "أدنى نسبة ذاكرة التخزين المؤقت", + "option.missing_credit_rate": "معدل الائتمان المفقود", + "option.most_signals": "معظم الإشارات", + "option.most_tokens": "معظم الرموز", + "option.needs_attention": "يحتاج إلى اهتمام", + "option.newest_calls": "أحدث المكالمات", + "option.this_month": "هذا الشهر", + "option.this_week": "هذا الاسبوع", + "option.thread_name": "اسم الموضوع", + "option.today": "اليوم", + "option.unpriced_cost": "تكلفة غير مسعرة", + "option.user_credit_override": "تجاوز رصيد المستخدم", + "parser.warnings_title": "تم الإبلاغ عن آخر تحديث لتشخيصات المحلل اللغوي {count}: {entries}. قم بتشغيل codex-usage-tracker inspect-log لفحص انحراف المخطط.", + "preset.cache_misses": "يفتقد ذاكرة التخزين المؤقت", + "preset.cache_misses_caption": "ذاكرة التخزين المؤقت يفتقد الإعداد المسبق", + "preset.cache_misses_desc": "مكالمات ذات نسبة ذاكرة تخزين مؤقت منخفضة مجمعة حسب cwd والطراز والخيط.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", - "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "preset.context_bloat": "انتفاخ السياق", + "preset.context_bloat_caption": "انتفاخ السياق مسبقا", + "preset.context_bloat_desc": "تستخدم المكالمات أكثر من 60% من السياق أو ذات رموز تراكمية عالية جدًا.", + "preset.description": "نقاط البداية بنقرة واحدة لأسئلة الاستخدام الشائعة.", + "preset.estimated_price_review": "مراجعة السعر المقدر", + "preset.estimated_price_review_caption": "مراجعة السعر المقدر مسبقًا", + "preset.estimated_price_review_desc": "يتم تسعير الاستخدام بتقديرات أفضل تخمين.", + "preset.highest_codex_credits": "أعلى Codex الاعتمادات", + "preset.highest_codex_credits_caption": "أعلى Codex أرصدة محددة مسبقًا", + "preset.highest_codex_credits_desc": "تم فرز المكالمات حسب التأثير المقدر على بدل استخدام Codex.", + "preset.highest_cost_threads": "المواضيع الأعلى تكلفة", + "preset.highest_cost_threads_caption": "المواضيع ذات التكلفة الأعلى محددة مسبقًا", + "preset.highest_cost_threads_desc": "المواضيع مرتبة حسب الإنفاق المقدر، مع إرفاق الوكلاء الفرعيين.", + "preset.investigation_presets": "إعدادات التحقيق المسبقة", + "preset.no_preset": "لم يتم تطبيق أي إعداد مسبق.", + "preset.pricing_gaps": "فجوات التسعير", + "preset.pricing_gaps_caption": "فجوات التسعير محددة مسبقا", + "preset.pricing_gaps_desc": "الاستخدام غير المسعر الذي يجعل إجماليات التكلفة المقدرة غير مكتملة.", + "pricing.configure_hint": "قم بتشغيل codex-usage-tracker update-pricing لتكوين التكاليف المقدرة.", + "pricing.fetched": "تم جلبه {time}", + "pricing.pinned": "لقطة مثبتة", + "pricing.source": "مصدر التسعير", + "pricing.tier": "{tier} الطبقة", + "pricing.title": "{parts}. قد تستخدم تسميات Codex الداخلية تقديرات أفضل تخمين مميزة.{warning}", + "pricing.title_fetched": "{parts}. تم الجلب من {url} في {time}. قد تستخدم تسميات Codex الداخلية تقديرات أفضل تخمين مميزة.{warning}", + "privacy.aliases_preserved": "يتم التعامل مع الأسماء المستعارة للمشروع التي تم تكوينها على أنها اختيارات عرض صريحة.", + "privacy.cwd_redacted": "يتم تنقيح مسارات cwd الأولية.", + "privacy.git_branch_hidden": "فرع Git مخفي.", + "privacy.git_remote_label_hidden": "يتم إخفاء تسميات Git البعيدة.", + "privacy.mode": "وضع خصوصية البيانات التعريفية للمشروع: {mode}.", + "privacy.normal_title": "يتم عرض بيانات تعريف المشروع باستخدام تسميات cwd المحلية والمشروع والفرع والتسميات التي تم تكوينها.", + "privacy.project_names_redacted": "تستخدم المشاريع غير المسماة تسميات مجزأة مستقرة.", + "privacy.relative_cwd_hidden": "CWD النسبي مخفي.", + "privacy.tags_hidden": "علامات المشروع مخفية.", + "recommendation.context_bloat.action": "فكر في بدء سلسلة رسائل Codex جديدة إذا لم يعد السياق القديم ذا صلة.", + "recommendation.context_bloat.title": "ارتفاع ضغط السياق", + "recommendation.context_bloat.why": "يستخدم هذا الاستدعاء حصة كبيرة من نافذة سياق النموذج.", + "recommendation.elevated_context.action": "تحقق مما إذا كان من الممكن تضييق الخيط قبل إضافة المزيد من العمل.", + "recommendation.elevated_context.title": "ارتفاع ضغط السياق", + "recommendation.elevated_context.why": "استخدام السياق مرتفع وقد يصبح مكلفًا في المنعطفات اللاحقة.", + "recommendation.estimated_pricing.action": "راجع تغطية الأسعار وقم بتثبيت سعر النموذج أو تجاوزه إذا كانت هذه المكالمة مهمة.", + "recommendation.estimated_pricing.title": "التسعير المقدر", + "recommendation.estimated_pricing.why": "تستخدم هذه التكلفة تعيين نموذج مستنتج بدلاً من صف التسعير المباشر.", + "recommendation.high_cost.action": "افتح المخطط الزمني للخيط وافحص المنعطف السابق قبل المتابعة.", + "recommendation.high_cost.title": "ارتفاع التكلفة المقدرة", + "recommendation.high_cost.why": "لقد تجاوزت هذه المكالمة حد التكلفة العالية الذي تم تكوينه.", + "recommendation.large_thread.action": "تفضل موضوعًا جديدًا لأعمال المتابعة غير ذات الصلة.", + "recommendation.large_thread.title": "موضوع تراكمي كبير", + "recommendation.large_thread.why": "الإجمالي التراكمي للجلسة مرتفع بما يكفي لجعل المنعطفات اللاحقة باهظة الثمن.", + "recommendation.low_cache.action": "تحقق مما إذا كان قد تم إعادة تقديم الملفات أو مخرجات الأداة أو السياق العام دون داعٍ.", + "recommendation.low_cache.title": "إعادة استخدام ذاكرة التخزين المؤقت منخفضة", + "recommendation.low_cache.why": "المدخلات الجديدة غير المخزنة مؤقتًا عالية بينما تكون إعادة استخدام ذاكرة التخزين المؤقت منخفضة.", + "recommendation.low_output.action": "فحص السياق الكلي أولا؛ تحميل السياق الخام فقط إذا كان السبب غير واضح.", + "recommendation.low_output.title": "مكالمة كبيرة منخفضة الإخراج", + "recommendation.low_output.why": "استهلكت المكالمة العديد من الرموز ولكنها أنتجت القليل من المخرجات.", + "recommendation.none.action": "لم يتم وضع علامة على أي إجراء مجمع؛ مواصلة مراقبة أنماط الاستخدام.", + "recommendation.pricing_gap.action": "قم بتحديث الأسعار أو أضف اسمًا مستعارًا محليًا قبل الوثوق بإجماليات التكلفة.", + "recommendation.pricing_gap.title": "فجوة التسعير", + "recommendation.pricing_gap.why": "لا يوجد سعر محدد لاستدعاء النموذج هذا، لذا فإن إجماليات التكلفة تقلل من الاستخدام المرئي.", + "recommendation.reasoning_spike.action": "راجع ما إذا كانت هذه المهمة تحتاج إلى جهد التفكير المحدد.", + "recommendation.reasoning_spike.title": "حصة المنطق عالية", + "recommendation.reasoning_spike.why": "يهيمن إخراج المنطق على الإخراج المرئي لهذه المكالمة.", + "recommendation.subagent_attribution.action": "قارن المكالمات المباشرة مع الوكيل الفرعي المرفق أو قم بمراجعة المكالمات قبل تغيير سير العمل.", + "recommendation.subagent_attribution.title": "إسناد الوكيل الفرعي", + "recommendation.subagent_attribution.why": "هذه المكالمة مرفقة بالعمل المفوض وقد تشرح نمو الموضوع الأصلي.", + "section.allowance": "بدل", + "section.needs_attention": "يحتاج إلى اهتمام", + "section.pricing": "التسعير", + "section.recommendations": "التوصيات", + "severity.high": "عالية", + "severity.medium": "متوسط", + "severity.review": "مراجعة", + "source.auto_review": "المراجعة التلقائية", + "source.codex_initiated": "بدأ Codex", + "source.subagent": "وكيل فرعي", + "source.subagent_role": "الوكيل الفرعي: {role}", + "source.user": "المستخدم", + "source.user_initiated": "بدأ المستخدم", + "state.allowance_config_error": "خطأ في تكوين البدل", + "state.allowance_configured": "تم تكوين البدل", + "state.best_guess_estimate": "تقدير أفضل تخمين", + "state.configured": "تم تكوينه", + "state.configured_price": "سعر التكوين", + "state.error": "خطأ", + "state.estimated": "يقدر", + "state.loading": "جاري التحميل", + "state.loading_rows": "جار تحميل الصفوف", + "state.mixed": "مختلط", + "state.no": "لا", + "state.no_calls": "لا توجد مكالمات تتطابق مع المرشحات الحالية.", + "state.no_configured_price": "لا يوجد سعر محدد", + "state.no_context_entries": "لم يتم العثور على إدخالات سياق لهذه المكالمة.", + "state.no_data": "لا توجد بيانات", + "state.no_mapped_rate": "لا يوجد معدل معين", + "state.no_price": "لا يوجد سعر", + "state.no_rate": "لا يوجد معدل", + "state.no_rows": "لا توجد صفوف", + "state.no_threads": "لا توجد مواضيع تتطابق مع المرشحات الحالية.", + "state.none": "لا شيء", + "state.not_configured": "لم يتم تكوينه", + "state.requires_evidence": "الأدلة اللازمة", + "state.unknown": "غير معروف", + "state.yes": "نعم", + "status.checking": "التحقق", + "status.paused": "متوقف مؤقتًا", + "status.refresh_error": "خطأ في التحديث", + "status.refreshing": "منعش", + "status.reloading": "إعادة التحميل", + "status.static": "ثابت", + "status.updated": "تم التحديث", + "table.cache": "ذاكرة التخزين المؤقت", + "table.cached": "مخبأة", + "table.calls": "المكالمات", + "table.cost": "التكلفة", + "table.effort": "جهد", + "table.initiated": "بدأ", + "table.last_call": "المكالمة الأخيرة", + "table.model": "نموذج", + "table.more_efforts": "{effort} +{count} الجهود", + "table.more_models": "نماذج {model} +{count}", + "table.output": "الإخراج", + "table.page_status": "{start}-{end} من {total} {items} · الصفحة {page}/{pages}", + "table.rows": "صفوف", + "table.signals": "إشارات", + "table.source": "المصدر", + "table.thread": "الموضوع", + "table.threads": "المواضيع", + "table.time": "الوقت", + "table.tokens": "الرموز", + "table.uncached": "غير مخزنة مؤقتا", + "table.visible_status": "عرض {end} من {total} {items}", + "thread.attached": "مرفق", + "thread.attention": "انتباه {score}", + "thread.auto_review": "{count} المراجعة التلقائية", + "thread.collapse": "طي", + "thread.direct": "مباشر", + "thread.expand": "توسيع", + "thread.expand_label": "{action} {thread} المكالمات. درجة الاهتمام {score}.", + "thread.explicit_parent": "الوالد الصريح", + "thread.explicit_parent_thread": "موضوع الأصل صريح", + "thread.parent": "الوالد {id}", + "thread.session": "جلسة", + "thread.spawned": "ولدت", + "thread.spawned_from": "نشأت من {thread}", + "thread.spawned_threads": "{count} المواضيع الناتجة", + "thread.subagent": "{count} الوكيل الفرعي", + "thread.unknown": "خيط غير معروف", + "thread.unmatched_subagent": "وكيل فرعي لا مثيل له" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/de.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/de.json index ef2ecf4..bbf230c 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/de.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/de.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", + "action.check_cache_drop": "Suchen Sie nach wieder eingeführten Dateien oder Tool-Ausgaben, nachdem die Cache-Wiederverwendung eingestellt wurde.", + "action.compare_fresh_input": "Vergleichen Sie die neuen Eingaben mit der vorherigen Runde, bevor Sie fortfahren.", + "action.compare_subagent_calls": "Vergleichen Sie angehängte Subagenten oder überprüfen Sie Aufrufe, bevor Sie den übergeordneten Workflow ändern.", + "action.configure_pricing": "Konfigurieren Sie die Preise, bevor Sie den Gesamtkosten vertrauen.", + "action.copied": "Kopiert", + "action.copy_failed": "Der Kopiervorgang ist fehlgeschlagen", + "action.expand_or_select_recommendations": "Erweitern Sie Aufrufe oder wählen Sie eine Zeile für Empfehlungen auf Aufrufebene aus.", + "action.exported": "Exportiert {count}", + "action.inspect_thread_timeline": "Sehen Sie sich die Zeitleiste des Threads an und überlegen Sie, einen neuen Thread zu starten.", + "action.review_context_growth": "Überprüfen Sie, wo das Kontextwachstum beginnt, und überlegen Sie, einen neuen Thread zu eröffnen.", + "action.review_reasoning_effort": "Überprüfen Sie, ob der Argumentationsaufwand für diese Aufgabe angemessen ist.", + "action.run": "Lauf", + "action.set_limits": "Grenzen setzen", + "action.use_aggregate_first": "Verwenden Sie zuerst die Aggregatfelder. Kontext nur laden, wenn das Signal noch unklar ist.", + "allowance.counted": "{value} Credits werden auf die Nutzungslimits von Codex angerechnet", + "allowance.cr_left": "{value} cr übrig", + "allowance.credit_coverage": "Kreditdeckung {ratio} der geladenen Token.", + "allowance.credit_rates": "Kreditzinsen: {source}.", + "allowance.credits_remaining": "{value} Credits verbleiben", + "allowance.init_hint": "Führen Sie codex-usage-tracker init-allowance aus, um verbleibende Nutzungsfenster hinzuzufügen.", + "allowance.of_allowance": "{ratio} der Zulage", + "allowance.of_total": "{used} von {total} Credits", + "allowance.rate_card_error": "Preislistenfehler: {error}", + "allowance.remaining": "{value} übrig", + "allowance.resets": "Zurückgesetzt: {resets}", + "allowance.row_no_rate": "Kein zugeordneter Codex-Kreditsatz", + "allowance.title_hint": "Fügen Sie ~/.codex-usage-tracker/allowance.json hinzu, um die verbleibende 5-Stunden- und wöchentliche Nutzung anzuzeigen.", + "allowance.used_vs_remaining": "{used} verwendet vs. {remaining} übrig", + "allowance.window_configured": "{label} konfiguriert", + "allowance.windows": "Zuschussfenster: {windows}", + "aria.current_view_actions": "Aktuelle Ansichtsaktionen", + "aria.dashboard_status": "Dashboard-Status", + "aria.dashboard_view": "Dashboard-Ansicht", + "aria.history_title": "Nur aktive Sitzungen sind die Standardeinstellung. Der gesamte Verlauf scannt archivierte Sitzungsprotokolle während der Live-Aktualisierung.", + "aria.inspect_thread": "Überprüfen Sie die Nutzung von {thread}", + "aria.refresh_controls": "Steuerelemente für die Dashboard-Aktualisierung", + "aria.table_pages": "Tabellenseiten", + "badge.costs": "Kosten", "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", + "badge.live": "Lebe", + "badge.metadata_mode": "Metadaten {mode}", + "badge.metadata_normal": "Metadaten normal", + "badge.no_costs": "Keine Kosten", + "badge.parser_warnings": "Parser-Warnungen", + "badge.static": "Statisch", + "badge.unofficial_project": "Inoffizielles Projekt", + "badge.unofficial_project_title": "Codex Usage Tracker ist unabhängig und wird nicht von OpenAI erstellt, mit ihm verbunden, von ihm unterstützt, gesponsert oder unterstützt. OpenAI und Codex sind Marken von OpenAI.", + "button.back_to_dashboard": "Zurück zum Dashboard", + "button.clear": "Klar", + "button.copy_link": "Link kopieren", + "button.enable_context_loading": "Aktivieren Sie das Laden von Kontexten", + "button.export_csv": "CSV exportieren", + "button.full_serialized_analysis": "Führen Sie eine vollständige serialisierte Analyse durch", + "button.hide_details": "Details ausblenden", + "button.hide_tool_output": "Werkzeugausgabe ausblenden", + "button.include_tool_output": "Beziehen Sie die Werkzeugausgabe ein", + "button.load_context": "Kontext laden", + "button.load_more": "Mehr laden", + "button.load_older_context": "Ältere Einträge laden", + "button.next": "Als nächstes", + "button.next_call": "Nächster Aufruf", + "button.no_char_limit": "Keine Zeichenbeschränkung", + "button.open_investigator": "Offener Ermittler", + "button.previous": "Zurück", + "button.previous_call": "Vorheriger Aufruf", + "button.refresh": "Aktualisieren", + "button.show_compaction_history": "Verdichteten Ersatz anzeigen", + "button.show_tool_output": "Werkzeugausgabe anzeigen", + "button.show_turn_evidence": "Beweise aus dem Abbiegeprotokoll vorlegen", "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "call.cache_accounting_delta": "Cache-/Buchhaltungsdelta", + "call.cache_cold": "Kalter Lebenslauf / veralteter Cache", + "call.cache_diagnostics": "Cache-Diagnose", + "call.cache_partial": "Teilweiser Cache-Fehler", + "call.cache_spike": "Nicht zwischengespeicherter Spike", + "call.cache_steady": "Stabiles Cache-Profil", + "call.cache_warm": "Warm-Cache-Wiederverwendung", + "call.compaction_diagnostics": "Verdichtungsdiagnostik", + "call.compaction_hint": "Geladene Beweise können explizite Verdichtungsereignisse anzeigen. Der geschwärzte Ersetzungsverlauf wird erst nach der komprimierten Ersetzungsaktion angezeigt.", + "call.context_estimate": "Schätzung der Kontextänderung", + "call.context_estimate_hint": "Vergleichen Sie genaue, nicht zwischengespeicherte Eingaben mit vom Tokenizer gezählten sichtbaren Protokollbeweisen. Die Lücke sollte als versteckter Gerüst-, Serialisierungs- oder Tokenizer-Schätzungsfehler behandelt werden.", + "call.derived_label": "Abgeleitet von benachbarten Aggregataufrufen", + "call.estimated_label": "Geschätzt anhand des sichtbaren Protokollvolumens", + "call.evidence_label": "Laufzeitbeweise", + "call.exact_accounting": "Exakte Token-Abrechnung", + "call.exact_label": "Exakt vom Token-Rückruf", + "call.hidden_estimate": "Ungeklärte versteckte/serialisierte Eingabeschätzung", + "call.no_previous": "Kein vorheriger Aufruf in diesem gelösten Thread.", + "call.not_found": "Der ausgewählte Aufruf wurde in den geladenen Dashboard-Zeilen nicht gefunden.", + "call.open_hint": "Klicken Sie auf eine Aufrufzeile, um eine umfassende Diagnose zu erhalten.", + "call.position": "Rufen Sie {position} in diesem gelösten Thread auf.", + "call.post_compaction": "Nachverdichtung möglich", + "call.raw_evidence": "Rohe Beweise", + "call.remaining_after_serialized": "Verbleibend nach der Serialisierungsbindung", + "call.remaining_after_serialized_hint": "Nicht zwischengespeicherte Eingaben werden nicht einmal durch die serialisierte Obergrenze abgedeckt", + "call.serialized_bound_hint": "Lokale JSONL-Struktur mit Obergrenze; Kein exakter Eingabeaufforderungstext.", + "call.serialized_breakdown": "Gruppen serialisierter Evidenz", + "call.serialized_bucket_detail": "{count} Felder · {chars} Zeichen", + "call.serialized_candidate": "Möglicher serialisierter Overhead", + "call.serialized_candidate_hint": "Serialisierte Obergrenze minus sichtbarer Schätzung, begrenzt durch exakte, nicht zwischengespeicherte Eingabe", + "call.serialized_deferred": "Schnelle Schätzung geladen; vollständige serialisierte Gruppierung wird zurückgestellt.", + "call.serialized_quick_hint": "schnelle Schätzung", + "call.serialized_upper_bound": "Serialisierte lokale Obergrenze", + "call.visible_estimate": "Sichtbare neue Kontextschätzung", + "call.visible_gap": "Nicht zwischengespeicherte Eingabe minus sichtbare Schätzung", + "caption.ascending": "aufsteigend", + "caption.call_investigator": "Der Aufruf wird untersucht: {record}.", + "caption.calls": "Zeigt einzelne Modellaufrufe sortiert nach {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "absteigend", + "caption.initial_calls": "Anzeige einzelner Modellaufrufe.", + "caption.insights": "Geordnet nach Kosten, Nutzungsgutschriften, Cache-Wiederverwendung, Kontextdruck und Preisvertrauen.", + "caption.loaded": "{loaded} Aufrufe geladen", + "caption.loaded_capped": "{loaded} von {available} Aufrufen geladen", + "caption.rows_loaded_progress": "Zeilen geladen: {loaded} von {total}", + "caption.rows_loading_background": "Die Dashboard-Summen sind bereit. Zeilen werden im Hintergrund geladen.", + "caption.rows_loading_progress": "Zeilen werden geladen: {loaded} von {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "Zeigt {threads} Threads von {calls} gefilterten Aufrufen, sortiert nach {sort}. {loaded}. Klicken Sie auf einen Thread, um seine Aufrufe zu erweitern.", + "context.api_http": "Kontext API gab HTTP {status} zurück.", + "context.api_unavailable": "Der Kontext API ist hier nicht verfügbar. Führen Sie codex-usage-tracker serve-dashboard --open aus, um den Kontext bei Bedarf zu laden.", + "context.auto_loading": "Laden ausgewählter Turn-Beweise inklusive Werkzeugausgabe.", + "context.chars_omitted": "{count} Zeichen über Budget weggelassen.", + "context.compaction_detected": "Verdichtung erkannt", + "context.compaction_replacement": "Komprimierter Ersetzungskontext", + "context.compaction_replacement_count": "{count} Ersetzungsverlaufseinträge verfügbar.", + "context.disabled_hint": "Das Laden von Kontexten ist für diesen Dashboard-Server deaktiviert. Aktivieren Sie es hier, um den lokalen JSONL-Kontext bei Bedarf zu laden.", + "context.enabled_note": "Das Laden von Kontexten ist aktiviert. Klicken Sie auf Abbiegeprotokollbeweise anzeigen, um diesen Aufruf aus der lokalen JSONL-Quelle zu lesen.", + "context.file_hint": "Öffnen Sie dieses Dashboard mit codex-usage-tracker serve-dashboard, um bei Bedarf Rohkontext zu laden.", + "context.line": "Zeile {line}", + "context.loading": "Lokaler Kontext wird geladen...", + "context.local_redacted": "Lokaler JSONL-Kontext, der bei Bedarf geladen wird. Eingabeaufforderungen und Tool-Ausgaben werden hinsichtlich allgemeiner geheimer Muster geschwärzt und nicht auf SQLite oder dem Dashboard HTML gespeichert.", + "context.no_char_limit_active": "Es gilt keine Zeichenbeschränkung.", + "context.no_record_id": "Diese Zeile hat keine Datensatz-ID für die Kontextsuche.", + "context.no_response": "Kein Antworttext", + "context.older_omitted": "{count} ältere Einträge weggelassen.", + "context.ready_hint": "Der Kontext ist in diesem Dashboard nicht eingebettet. Drücken Sie eine Taste, um diesen Aufruf aus der lokalen JSONL-Quelle zu lesen.", + "context.settings_http": "Die Kontexteinstellungen haben HTTP {status} zurückgegeben.", + "context.source": "Quelle: {file}:{line}", + "context.token_breakdown": "Token-Aufschlüsselung", + "context.token_cached": "Zwischengespeichert", + "context.token_input": "Eingabe", + "context.token_output": "Ausgabe", + "context.token_reasoning": "Begründung", + "context.token_required": "Das Laden des Kontexts erfordert ein localhost-Dashboard-API-Token.", + "context.token_scope_call": "Dieser Aufruf", + "context.token_scope_earlier": "Frühere Token zählen im selben Zug", + "context.token_scope_previous": "Der vorherige Spielstein zählt im selben Zug", + "context.token_scope_selected": "Ausgewählte Aufruf-Token-Anzahl", + "context.token_scope_session": "Sitzung kumulativ", + "context.token_total": "Insgesamt", + "context.token_type": "Typ", + "context.token_uncached": "Nicht zwischengespeichert", + "context.tool_included": "Werkzeugausgabe inklusive Schwärzung und Größenbeschränkungen.", + "context.tool_omitted": "Die Werkzeugausgabe ist für diese Ansicht ausgeblendet.", + "credit.configured_rate": "Konfigurierter Tarif", + "credit.estimated_mapping": "Geschätzte Zuordnung", + "credit.inferred_mapping": "Abgeleitete Modellzuordnung", + "credit.no_mapped_rate": "Kein zugeordneter Tarif", + "credit.no_rate": "Kein Kreditzins", + "credit.official_match": "Offizielles Rate-Card-Match", + "credit.user_rate": "Vom Benutzer bereitgestellter Kreditzinssatz", + "credit.with_status": "{value} Credits · {status}", + "dashboard.call_details": "Aufrufdetails", + "dashboard.detail.empty": "Bewegen Sie den Mauszeiger oder klicken Sie auf eine Zeile, um die aggregierten Nutzungsfelder zu überprüfen.", + "dashboard.eyebrow": "Lokale Codex-Analyse", + "dashboard.local_storage_note": "Der Dashboard-Header merkt sich auch lokal Ihre Sprachauswahl.", + "dashboard.model_calls": "Modellaufrufe", + "dashboard.title": "Nutzungs-Dashboard", + "dashboard.top_threads_by_attention": "Top-Threads nach Aufmerksamkeitswert", + "dashboard.view.call": "Rufen Sie den Ermittler an", + "dashboard.view.calls": "Aufrufe", + "dashboard.view.insights": "Einblicke", + "dashboard.view.threads": "Themen", + "date.custom": "Benutzerdefiniert", + "date.invalid_range": "Ungültiger Datumsbereich", + "date.range_between": "{prefix} {start} bis {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", + "date.range_from": "{prefix} von {start}", + "date.range_through": "{prefix} bis {end}", + "detail.agent_nickname": "Spitzname des Agenten", + "detail.agent_role": "Agentenrolle", + "detail.allowance_impact": "Auswirkungen auf die Zulage", + "detail.attached_calls": "Angehängte Aufrufe", + "detail.auto_review_calls": "Aufrufe automatisch überprüfen", + "detail.cache_savings": "Cache-Einsparungen", + "detail.call_number": "{number} anrufen", + "detail.calls": "Aufrufe", + "detail.context_window": "Kontextfenster", + "detail.cost_usage_context": "Kosten, Nutzung und Kontext", + "detail.credit_confidence": "Kreditvertrauen", + "detail.credit_model": "Kreditmodell", + "detail.credit_note": "Gutschrift", + "detail.credit_source": "Kreditquelle", + "detail.credit_source_fetched": "Kreditquelle abgerufen", + "detail.credit_tier": "Kreditstufe", "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", + "detail.efficiency_signals": "Effizienzsignale", + "detail.first_expensive_turn": "Erster teurer Zug", + "detail.git_branch": "Git-Zweig", + "detail.largest_cumulative_jump": "Größter kumulativer Sprung", + "detail.latest_activity": "Letzte Aktivität", + "detail.model_mix": "Modellmix", + "detail.next_action": "Nächste Aktion", + "detail.no_above_thresholds": "Keine über den Schwellenwerten", + "detail.no_aggregate_action": "Es ist keine aggregierte Aktion gekennzeichnet.", + "detail.parent_session": "Elternsitzung", + "detail.parent_thread": "Übergeordneter Thread", + "detail.parent_updated": "Übergeordnetes Element aktualisiert", + "detail.pricing_model": "Preismodell", + "detail.pricing_status": "Preisstatus", + "detail.project_cwd": "Projekt cwd", + "detail.project_tags": "Projekt-Tags", + "detail.raw_identifiers": "Rohe Aggregatbezeichner", + "detail.reasoning_mix": "Argumentationsmix", + "detail.relationships": "Beziehungen", + "detail.remote_hash": "Remote-Hash", + "detail.remote_label": "Remote-Label", + "detail.secondary_thread_fields": "Sekundäre Thread-Felder", + "detail.source_file_line": "Quelldatei und Zeile", + "detail.source_line": "Quellzeile", + "detail.spawned_child_calls": "Gespawnte Kinderanrufe", + "detail.spawned_from": "Entstanden aus", + "detail.spawned_threads": "Threads erzeugt", + "detail.subagent_before_spike": "Subagent vor Spike", + "detail.subagent_calls": "Subagenten-Aufrufe", + "detail.subagent_type": "Subagententyp", + "detail.thread_attachment": "Gewindebefestigung", + "detail.thread_attention_summary": "Zusammenfassung der Thread-Aufmerksamkeit", + "detail.thread_lifecycle": "Thread-Lebenszyklus", + "detail.thread_narrative": "Thread-Erzählung", + "detail.thread_source": "Thread-Quelle", + "detail.thread_timeline": "Thread-Zeitleiste", + "detail.timeline_context": "Kontext {value}", + "detail.timeline_empty": "Keine Aufrufe in diesem Thread.", + "detail.timeline_meta": "{tokens} Token · {cost} · {credits} · Cache {cache}", + "detail.timestamp": "Zeitstempel", + "detail.token_pricing_breakdown": "Aufschlüsselung der Token und Preise", + "detail.tokens_at": "{tokens} Token bei {time}", + "detail.turn": "Drehen", + "detail.why_flagged": "Warum markiert", + "docs.dashboard_guide": "Dashboard-Anleitung", + "effort.high": "hoch", + "effort.low": "niedrig", + "effort.medium": "mittel", + "filter.confidence": "Vertrauen", + "filter.effort": "Begründung", + "filter.end": "Ende", + "filter.model": "Modell", + "filter.project": "Projekt", + "filter.reasoning": "Begründung", + "filter.search": "Suchen", + "filter.search_placeholder": "Thread, cwd, Modell", + "filter.session": "Sitzung", + "filter.sort": "Sortieren", + "filter.start": "Starten", "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", + "filter.time": "Zeit", + "flag.elevated_context_use": "Erweiterte Kontextnutzung", + "flag.expensive_low_output_call": "Teurer Aufruf mit geringer Leistung", + "flag.high_context_use": "Hohe Kontextnutzung", + "flag.high_estimated_cost": "Hohe geschätzte Kosten", + "flag.high_reasoning_share": "Hoher Argumentationsanteil", + "flag.low_cache_reuse": "Geringe Cache-Wiederverwendung", + "history.active_hidden": "Nur aktive Sitzungen; {count} archivierte Aufrufe ausgeblendet", + "history.active_only": "Nur aktive Sitzungen", + "history.all_empty": "Gesamter Verlauf ausgewählt; Es sind noch keine archivierten Aufrufe indiziert", + "history.all_includes": "Der gesamte Verlauf umfasst {count} archivierte Aufrufe", + "history.archived_scan_hint": "{detail}. Archivierte Sitzungen werden nur gescannt, wenn während der Live-Aktualisierung „Gesamter Verlauf“ ausgewählt ist.", + "insight.apply_cache_misses": "Cache-Misses-Voreinstellung anwenden", + "insight.apply_context_bloat": "Wenden Sie die Voreinstellung „Kontextaufblähung“ an", + "insight.codex_allowance_usage": "Codex-Zulagennutzung", + "insight.context_bloat": "Kontext aufgebläht", + "insight.context_bloat_body": "{calls}-Aufrufe liegen bei oder über der Kontextnutzung von {ratio}.", + "insight.costliest_thread": "Teuerster Thread", + "insight.costliest_thread_body": "{thread} hat {calls} Aufrufe und {tokens} Token.", + "insight.credit_coverage_body": "{ratio} der sichtbaren Token entsprechen Codex Kreditraten.", + "insight.estimated_pricing": "Geschätzter Preis", + "insight.estimated_pricing_body": "Ausgewiesene Best-Guest-Preise sind im Preis enthalten, sollten aber gesondert überprüft werden.", + "insight.inspect_selected_call": "Ausgewählten Aufruf prüfen", + "insight.low_cache_reuse": "Geringe Cache-Wiederverwendung", + "insight.low_cache_reuse_body": "{calls}-Aufrufe unterliegen der {ratio}-Cache-Wiederverwendung. Beginnen Sie mit {thread}.", + "insight.open_thread_timeline": "Thread-Timeline öffnen", + "insight.reasoning_output_spike": "Spitze der Argumentationsausgabe", + "insight.reasoning_spike_body": "{thread} hat den größten Argumentationsausgabeaufruf im aktuellen Filter.", + "insight.review_estimates": "Überprüfen Sie die Schätzungen", + "insight.review_highest_credit": "Überprüfen Sie Aufrufe mit der höchsten Kreditwürdigkeit", + "insight.review_pricing_gaps": "Überprüfen Sie Preisunterschiede", + "insight.unpriced_usage": "Unbezahlte Nutzung", + "insight.unpriced_usage_body": "Diese Token werden in den geschätzten Gesamtkosten nicht berücksichtigt, bis die Preisgestaltung konfiguriert ist.", + "language.english": "Englisch", + "language.label": "Sprache", "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "live.checking_usage": "Suche nach neuer Verwendung...", + "live.every": "Live-Aktualisierung alle {seconds}s", + "live.history_static_hint": "Führen Sie codex-usage-tracker serve-dashboard aus, um im Dashboard zwischen aktiven Sitzungen und dem gesamten Verlauf zu wechseln.", + "live.indexed": "Indizierte {rows}-Aggregatzeilen aus {files}-Protokollen.", + "live.load_static_hint": "Führen Sie codex-usage-tracker serve-dashboard aus, um eine andere Verlaufsgröße aus dem Dashboard zu laden.", + "live.loading_rows": "Zeilen werden im Hintergrund geladen...", + "live.paused": "Live-Aktualisierung angehalten", + "live.refresh_suffix": ". Laden Sie diese Seite neu, nachdem Sie ein statisches Dashboard neu generiert haben, oder führen Sie codex-usage-tracker serve-dashboard aus.", + "live.refresh_unavailable": "Live-Aktualisierung nicht verfügbar: {message}{suffix}", + "live.refreshing_index": "Lokaler Nutzungsindex wird aktualisiert...", + "live.reloading_static": "Statischer Dashboard-Snapshot wird neu geladen...", + "live.skipped": "{count} fehlerhafte Token-Zählungsereignisse wurden übersprungen.", + "live.updated_detail": "Aktualisiert {time}. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "Aufmerksamkeitswert", + "metric.cache_ratio": "Cache-Verhältnis", + "metric.cache_trend": "Cache-Trend", + "metric.cached_input": "Zwischengespeicherte Eingabe", + "metric.codex_credits": "Codex Credits", + "metric.context_trend": "Kontexttrend", + "metric.context_use": "Kontextverwendung", + "metric.estimated_cost": "Geschätzte Kosten", + "metric.input_tokens": "Eingabe-Tokens", + "metric.last_call_input": "Letzte Aufrufeingabe", + "metric.last_call_total": "Letzte Aufrufsumme", + "metric.max_context_use": "Maximale Kontextnutzung", + "metric.output": "Ausgabe", + "metric.output_tokens": "Ausgabetoken", + "metric.reasoning_output": "Argumentationsausgabe", + "metric.remaining_usage": "Restnutzung", + "metric.session_cumulative": "Sitzung kumulativ", + "metric.total": "Insgesamt", + "metric.total_tokens": "Gesamtzahl der Token", + "metric.uncached_input": "Nicht zwischengespeicherte Eingabe", + "metric.usage_credits": "Nutzungsguthaben", + "metric.usage_remaining": "Restnutzung", + "metric.visible_calls": "Sichtbare Aufrufe", + "nav.history": "Geschichte", + "nav.live": "Lebe", + "nav.load": "Laden", + "option.active_sessions_only": "Nur aktive Sitzungen", + "option.all_confidence": "Alles Vertrauen", + "option.all_efforts": "Alle Bemühungen", + "option.all_history": "Alles Geschichte", + "option.all_models": "Alle Modelle", + "option.all_time": "Alle Zeiten", + "option.custom_range": "Benutzerdefiniertes Sortiment", + "option.estimated_cost": "Geschätzte Kosten", + "option.estimated_credit_mapping": "Geschätzte Kreditzuordnung", + "option.exact_cost": "Genaue Kosten", + "option.exact_credit_rate": "Genauer Kreditzins", + "option.highest_codex_credits": "Höchste Codex Credits", + "option.highest_context_use": "Höchste Kontextverwendung", + "option.highest_estimated_cost": "Höchste geschätzte Kosten", + "option.last_7_days": "Letzte 7 Tage", + "option.load_10000": "10.000 Aufrufe", + "option.load_20000": "20.000 Aufrufe", + "option.load_5000": "5.000 Aufrufe", + "option.load_all": "Alle Aufrufe", + "option.lowest_cache_ratio": "Niedrigstes Cache-Verhältnis", + "option.missing_credit_rate": "Fehlender Kreditzins", + "option.most_signals": "Die meisten Signale", + "option.most_tokens": "Die meisten Token", + "option.needs_attention": "Braucht Aufmerksamkeit", + "option.newest_calls": "Neueste Aufrufe", + "option.this_month": "Diesen Monat", + "option.this_week": "Diese Woche", + "option.thread_name": "Threadname", + "option.today": "Heute", + "option.unpriced_cost": "Unbezahlter Preis", + "option.user_credit_override": "Überschreibung des Benutzerguthabens", + "parser.warnings_title": "Letzte Aktualisierung gemeldet {count} Parser-Diagnose: {entries}. Führen Sie codex-usage-tracker inspect-log aus, um die Schemadrift zu untersuchen.", + "preset.cache_misses": "Cache-Fehler", + "preset.cache_misses_caption": "Voreingestellte Cache-Fehler", + "preset.cache_misses_desc": "Aufrufe mit niedrigem Cache-Verhältnis, gruppiert nach CWD, Modell und Thread.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", + "preset.context_bloat": "Kontext aufgebläht", + "preset.context_bloat_caption": "Kontextaufblähungsvoreinstellung", + "preset.context_bloat_desc": "Aufrufe mit mehr als 60 % Kontextnutzung oder mit sehr hohen kumulativen Tokens.", + "preset.description": "Ein-Klick-Startpunkte für häufige Nutzungsfragen.", + "preset.estimated_price_review": "Schätzpreisbewertung", + "preset.estimated_price_review_caption": "Voreinstellung für die Überprüfung des geschätzten Preises", + "preset.estimated_price_review_desc": "Nutzungspreis mit ausgewiesenen Best-Guess-Schätzungen.", + "preset.highest_codex_credits": "Höchste Codex Credits", + "preset.highest_codex_credits_caption": "Höchste voreingestellte Codex Credits", + "preset.highest_codex_credits_desc": "Aufrufe sortiert nach geschätzter Auswirkung auf das Nutzungskontingent Codex.", + "preset.highest_cost_threads": "Threads mit den höchsten Kosten", + "preset.highest_cost_threads_caption": "Voreingestellte Threads mit den höchsten Kosten", + "preset.highest_cost_threads_desc": "Threads nach geschätzten Ausgaben sortiert, mit angehängten Subagenten.", + "preset.investigation_presets": "Untersuchungsvoreinstellungen", + "preset.no_preset": "Keine Voreinstellung angewendet.", + "preset.pricing_gaps": "Preislücken", + "preset.pricing_gaps_caption": "Preislücken voreingestellt", + "preset.pricing_gaps_desc": "Unbepreiste Nutzung, die dazu führt, dass die geschätzten Gesamtkosten unvollständig sind.", + "pricing.configure_hint": "Führen Sie codex-usage-tracker update-pricing aus, um die geschätzten Kosten zu konfigurieren.", + "pricing.fetched": "abgerufen {time}", + "pricing.pinned": "Schnappschuss angepinnt", + "pricing.source": "Preisquelle", + "pricing.tier": "{tier}-Stufe", + "pricing.title": "{parts}. Interne Codex-Labels können markierte, bestmögliche Schätzungen verwenden.{warning}", + "pricing.title_fetched": "{parts}. Abgerufen von {url} bei {time}. Interne Codex-Labels verwenden möglicherweise markierte, bestmögliche Schätzungen.{warning}", + "privacy.aliases_preserved": "Konfigurierte Projektaliase werden als explizite Anzeige-Opt-Ins behandelt.", + "privacy.cwd_redacted": "Rohe CWD-Pfade werden geschwärzt.", + "privacy.git_branch_hidden": "Der Git-Zweig ist ausgeblendet.", + "privacy.git_remote_label_hidden": "Git-Remote-Labels sind ausgeblendet.", + "privacy.mode": "Datenschutzmodus für Projektmetadaten: {mode}.", + "privacy.normal_title": "Projektmetadaten werden mit lokalen CWD-, Projekt-, Zweig- und konfigurierten Bezeichnungen angezeigt.", + "privacy.project_names_redacted": "Unbenannte Projekte verwenden stabile Hash-Labels.", + "privacy.relative_cwd_hidden": "Der relative cwd ist ausgeblendet.", + "privacy.tags_hidden": "Projekt-Tags sind ausgeblendet.", + "recommendation.context_bloat.action": "Erwägen Sie, einen neuen Codex-Thread zu starten, wenn der ältere Kontext nicht mehr relevant ist.", + "recommendation.context_bloat.title": "Hoher Kontextdruck", + "recommendation.context_bloat.why": "Dieser Aufruf beansprucht einen großen Teil des Modellkontextfensters.", + "recommendation.elevated_context.action": "Prüfen Sie, ob der Faden eingeengt werden kann, bevor Sie weitere Arbeiten hinzufügen.", + "recommendation.elevated_context.title": "Erhöhter Kontextdruck", + "recommendation.elevated_context.why": "Die Kontextnutzung ist erhöht und kann in späteren Runden kostspielig werden.", + "recommendation.estimated_pricing.action": "Überprüfen Sie die Preisabdeckung und heften Sie den Modellpreis an oder überschreiben Sie ihn, wenn dieser Aufruf von Bedeutung ist.", + "recommendation.estimated_pricing.title": "Geschätzter Preis", + "recommendation.estimated_pricing.why": "Für diese Kosten wird eine abgeleitete Modellzuordnung anstelle einer direkten Preiszeile verwendet.", + "recommendation.high_cost.action": "Öffnen Sie die Thread-Timeline und überprüfen Sie die vorherige Runde, bevor Sie fortfahren.", + "recommendation.high_cost.title": "Hohe geschätzte Kosten", + "recommendation.high_cost.why": "Dieser Aufruf hat den konfigurierten Schwellenwert für hohe Kosten überschritten.", + "recommendation.large_thread.action": "Bevorzugen Sie einen neuen Thread für unabhängige Folgearbeiten.", + "recommendation.large_thread.title": "Großer Sammelthread", + "recommendation.large_thread.why": "Die kumulierte Gesamtsumme der Sitzungen ist hoch genug, um spätere Runden teuer zu machen.", + "recommendation.low_cache.action": "Überprüfen Sie, ob Dateien, Tool-Ausgaben oder allgemeiner Kontext unnötigerweise erneut eingeführt wurden.", + "recommendation.low_cache.title": "Geringe Cache-Wiederverwendung", + "recommendation.low_cache.why": "Die Anzahl frischer, nicht zwischengespeicherter Eingaben ist hoch, während die Cache-Wiederverwendung gering ist.", + "recommendation.low_output.action": "Überprüfen Sie zunächst den aggregierten Kontext. Rohkontext nur laden, wenn die Ursache unklar ist.", + "recommendation.low_output.title": "Großer Aufruf mit geringer Leistung", + "recommendation.low_output.why": "Der Aufruf verbrauchte viele Token, erzeugte aber wenig Output.", + "recommendation.none.action": "Es ist keine aggregierte Aktion gekennzeichnet. weiterhin die Nutzungsmuster überwachen.", + "recommendation.pricing_gap.action": "Aktualisieren Sie die Preise oder fügen Sie einen lokalen Alias ​​hinzu, bevor Sie den Gesamtkosten vertrauen.", + "recommendation.pricing_gap.title": "Preisunterschied", + "recommendation.pricing_gap.why": "Für diesen Modellaufruf ist kein Preis konfiguriert, sodass die Gesamtkosten die sichtbare Nutzung unterschätzen.", + "recommendation.reasoning_spike.action": "Überprüfen Sie, ob diese Aufgabe den ausgewählten Argumentationsaufwand erfordert.", + "recommendation.reasoning_spike.title": "Hoher Argumentationsanteil", + "recommendation.reasoning_spike.why": "Die Argumentationsausgabe dominiert bei diesem Aufruf die sichtbare Ausgabe.", + "recommendation.subagent_attribution.action": "Vergleichen Sie direkte Aufrufe mit angeschlossenen Subagenten oder überprüfen Sie Aufrufe, bevor Sie den Arbeitsablauf ändern.", + "recommendation.subagent_attribution.title": "Subagentenzuordnung", + "recommendation.subagent_attribution.why": "Dieser Aufruf ist an die delegierte Arbeit angehängt und kann das Wachstum des übergeordneten Threads erklären.", + "section.allowance": "Zulage", + "section.needs_attention": "Braucht Aufmerksamkeit", + "section.pricing": "Preise", + "section.recommendations": "Empfehlungen", + "severity.high": "Hoch", + "severity.medium": "Mittel", + "severity.review": "Rezension", + "source.auto_review": "Automatische Überprüfung", + "source.codex_initiated": "Codex initiiert", "source.subagent": "Subagent", "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", + "source.user": "Benutzer", + "source.user_initiated": "Vom Benutzer initiiert", + "state.allowance_config_error": "Fehler bei der Zulassungskonfiguration", + "state.allowance_configured": "Zuschuss konfiguriert", + "state.best_guess_estimate": "Schätzung nach bestem Ermessen", + "state.configured": "Konfiguriert", + "state.configured_price": "Konfigurierter Preis", + "state.error": "Fehler", + "state.estimated": "Geschätzte", + "state.loading": "Laden", + "state.loading_rows": "Zeilen werden geladen", + "state.mixed": "Gemischt", + "state.no": "Nein", + "state.no_calls": "Keine Aufrufe entsprechen den aktuellen Filtern.", + "state.no_configured_price": "Kein konfigurierter Preis", + "state.no_context_entries": "Für diesen Aufruf wurden keine Kontexteinträge gefunden.", + "state.no_data": "Keine Daten", + "state.no_mapped_rate": "Kein zugeordneter Tarif", + "state.no_price": "Kein Preis", + "state.no_rate": "Kein Tarif", + "state.no_rows": "Keine Reihen", + "state.no_threads": "Keine Threads entsprechen den aktuellen Filtern.", + "state.none": "Keine", + "state.not_configured": "Nicht konfiguriert", + "state.requires_evidence": "Beweise erforderlich", + "state.unknown": "Unbekannt", + "state.yes": "Ja", + "status.checking": "Überprüfen", + "status.paused": "Angehalten", + "status.refresh_error": "Aktualisierungsfehler", + "status.refreshing": "Erfrischend", + "status.reloading": "Nachladen", + "status.static": "Statisch", + "status.updated": "Aktualisiert", "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", + "table.cached": "Zwischengespeichert", + "table.calls": "Aufrufe", + "table.cost": "Kosten", + "table.effort": "Aufwand", + "table.initiated": "Initiiert", + "table.last_call": "Letzter Aufruf", + "table.model": "Modell", + "table.more_efforts": "{effort} +{count} Bemühungen", + "table.more_models": "{model} +{count} Modelle", + "table.output": "Ausgabe", + "table.page_status": "{start}-{end} von {total} {items} · Seite {page}/{pages}", + "table.rows": "Reihen", + "table.signals": "Signale", + "table.source": "Quelle", "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "table.threads": "Threads", + "table.time": "Zeit", + "table.tokens": "Token", + "table.uncached": "Nicht zwischengespeichert", + "table.visible_status": "Zeigt {end} von {total} {items}", + "thread.attached": "beigefügt", + "thread.attention": "Achtung {score}", + "thread.auto_review": "{count} automatische Überprüfung", + "thread.collapse": "Zusammenbruch", + "thread.direct": "direkt", + "thread.expand": "Erweitern", + "thread.expand_label": "{action} {thread} Aufrufe. Aufmerksamkeitswert {score}.", + "thread.explicit_parent": "explizites übergeordnetes Element", + "thread.explicit_parent_thread": "expliziter übergeordneter Thread", + "thread.parent": "Übergeordnetes {id}", + "thread.session": "Sitzung", + "thread.spawned": "hervorgebracht", + "thread.spawned_from": "gespawnt von {thread}", + "thread.spawned_threads": "{count} hat Threads erzeugt", + "thread.subagent": "{count} Subagent", + "thread.unknown": "Unbekannter Thread", + "thread.unmatched_subagent": "unübertroffener Subagent" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/en.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/en.json index 28a555a..3d921bf 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/en.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/en.json @@ -51,6 +51,7 @@ "button.copy_link": "Copy link", "button.enable_context_loading": "Enable context loading", "button.export_csv": "Export CSV", + "button.full_serialized_analysis": "Run full serialized analysis", "button.hide_details": "Hide details", "button.hide_tool_output": "Hide tool output", "button.include_tool_output": "Include tool output", @@ -85,22 +86,24 @@ "call.exact_accounting": "Exact token accounting", "call.exact_label": "Exact from token callback", "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", "call.no_previous": "No previous call in this resolved thread.", "call.not_found": "Selected call was not found in the loaded dashboard rows.", "call.open_hint": "Click a call row for deep diagnostics.", "call.position": "Call {position} in this resolved thread.", "call.post_compaction": "Post-compaction possible", "call.raw_evidence": "Raw evidence", + "call.remaining_after_serialized": "Remaining after serialized bound", + "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", + "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", + "call.serialized_breakdown": "Serialized evidence groups", + "call.serialized_bucket_detail": "{count} fields · {chars} chars", + "call.serialized_candidate": "Possible serialized overhead", + "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", + "call.serialized_deferred": "Fast estimate loaded; full serialized grouping is deferred.", + "call.serialized_quick_hint": "fast estimate", + "call.serialized_upper_bound": "Serialized local upper bound", "call.visible_estimate": "Visible new context estimate", + "call.visible_gap": "Uncached input minus visible estimate", "caption.ascending": "ascending", "caption.call_investigator": "Investigating call {record}.", "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", @@ -110,6 +113,9 @@ "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", "caption.loaded": "{loaded} calls loaded", "caption.loaded_capped": "{loaded} of {available} calls loaded", + "caption.rows_loaded_progress": "Rows loaded: {loaded} of {total}", + "caption.rows_loading_background": "Dashboard totals are ready. Rows are loading in the background.", + "caption.rows_loading_progress": "Loading rows: {loaded} of {total}", "caption.sort_direction": "{label} {direction}", "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", "context.api_http": "Context API returned HTTP {status}.", @@ -291,6 +297,7 @@ "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", + "live.loading_rows": "Loading rows in the background...", "live.paused": "Live refresh paused", "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", @@ -446,6 +453,7 @@ "state.error": "Error", "state.estimated": "Estimated", "state.loading": "Loading", + "state.loading_rows": "Loading rows", "state.mixed": "Mixed", "state.no": "No", "state.no_calls": "No calls match the current filters.", diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/es.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/es.json index ef2ecf4..9006f9c 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/es.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/es.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "Verifique si hay archivos reintroducidos o resultados de herramientas después de que se abandonó la reutilización de la caché.", + "action.compare_fresh_input": "Compare la nueva entrada con el turno anterior antes de continuar.", + "action.compare_subagent_calls": "Compare las llamadas de subagente adjunto o revise antes de cambiar el flujo de trabajo principal.", + "action.configure_pricing": "Configure los precios antes de confiar en los costos totales.", + "action.copied": "Copiado", + "action.copy_failed": "Copia fallida", + "action.expand_or_select_recommendations": "Amplíe las llamadas o seleccione una fila para obtener recomendaciones a nivel de llamada.", + "action.exported": "Exportado {count}", + "action.inspect_thread_timeline": "Inspeccione la línea de tiempo del hilo y considere iniciar un hilo nuevo.", + "action.review_context_growth": "Revise dónde comienza el crecimiento del contexto y considere iniciar un hilo nuevo.", + "action.review_reasoning_effort": "Revise si el esfuerzo de razonamiento es apropiado para esta tarea.", + "action.run": "correr", + "action.set_limits": "Establecer límites", + "action.use_aggregate_first": "Utilice primero los campos agregados; Cargue el contexto solo si la señal aún no está clara.", + "allowance.counted": "{value} créditos contados para los límites de uso de Codex", + "allowance.cr_left": "{value} cr izquierda", + "allowance.credit_coverage": "Cobertura de crédito {ratio} de tokens cargados.", + "allowance.credit_rates": "Tasas de crédito: {source}.", + "allowance.credits_remaining": "{value} créditos restantes", + "allowance.init_hint": "Ejecute codex-usage-tracker init-allowance para agregar las ventanas de uso restantes.", + "allowance.of_allowance": "{ratio} de asignación", + "allowance.of_total": "{used} de {total} créditos", + "allowance.rate_card_error": "Error de hoja de tarifas: {error}", + "allowance.remaining": "{value} restante", + "allowance.resets": "Restablece: {resets}", + "allowance.row_no_rate": "No hay tasa de crédito Codex asignada", + "allowance.title_hint": "Agregue ~/.codex-usage-tracker/allowance.json para mostrar las 5 horas y el uso restante semanal.", + "allowance.used_vs_remaining": "{used} usado frente a {remaining} restante", + "allowance.window_configured": "{label} configurado", + "allowance.windows": "Ventanas de asignación: {windows}", + "aria.current_view_actions": "Acciones de vista actual", + "aria.dashboard_status": "Estado del panel", + "aria.dashboard_view": "Vista del panel", + "aria.history_title": "Solo sesiones activas es el valor predeterminado. Todo el historial analiza los registros de sesiones archivados durante la actualización en vivo.", + "aria.inspect_thread": "Inspeccionar el uso de {thread}", + "aria.refresh_controls": "Controles de actualización del panel", + "aria.table_pages": "Páginas de tabla", + "badge.costs": "Costos", + "badge.credits": "Créditos", + "badge.live": "en vivo", + "badge.metadata_mode": "Metadatos {mode}", + "badge.metadata_normal": "Metadatos normales", + "badge.no_costs": "Sin costes", + "badge.parser_warnings": "Advertencias del analizador", + "badge.static": "estática", + "badge.unofficial_project": "Proyecto no oficial", + "badge.unofficial_project_title": "Codex Usage Tracker es independiente y no está fabricado, afiliado, respaldado, patrocinado ni respaldado por OpenAI. OpenAI y Codex son marcas comerciales de OpenAI.", + "button.back_to_dashboard": "Volver al tablero", + "button.clear": "Borrar", + "button.copy_link": "Copiar enlace", + "button.enable_context_loading": "Habilitar la carga de contexto", + "button.export_csv": "Exportar CSV", + "button.full_serialized_analysis": "Ejecute un análisis serializado completo", + "button.hide_details": "Ocultar detalles", + "button.hide_tool_output": "Ocultar salida de herramienta", + "button.include_tool_output": "Incluir salida de herramienta", + "button.load_context": "Cargar contexto", + "button.load_more": "Cargar más", + "button.load_older_context": "Cargar entradas más antiguas", + "button.next": "Siguiente", + "button.next_call": "Próxima llamada", + "button.no_char_limit": "Sin límite de caracteres", + "button.open_investigator": "Investigador abierto", + "button.previous": "Anterior", + "button.previous_call": "Convocatoria anterior", + "button.refresh": "Actualizar", + "button.show_compaction_history": "Mostrar reemplazo compactado", + "button.show_tool_output": "Mostrar salida de herramienta", + "button.show_turn_evidence": "Mostrar evidencia del registro de turnos", + "button.top": "Arriba", + "call.cache_accounting_delta": "Caché/delta de contabilidad", + "call.cache_cold": "Reanudación en frío/caché obsoleto", + "call.cache_diagnostics": "Diagnóstico de caché", + "call.cache_partial": "Falta de caché parcial", + "call.cache_spike": "Pico sin caché", + "call.cache_steady": "Perfil de caché estable", + "call.cache_warm": "Reutilización de caché en caliente", + "call.compaction_diagnostics": "Diagnóstico de compactación", + "call.compaction_hint": "La evidencia cargada puede mostrar eventos de compactación explícitos. El historial de reemplazo redactado se muestra solo después de la acción de reemplazo compactada.", + "call.context_estimate": "Estimación del cambio de contexto", + "call.context_estimate_hint": "Compare la entrada exacta sin caché con la evidencia de registro visible contada por el tokenizador. La brecha debe tratarse como un error de estimación de andamiaje oculto, serialización o tokenizador.", + "call.derived_label": "Derivado de llamadas agregadas adyacentes", + "call.estimated_label": "Estimado a partir del volumen de registro visible", + "call.evidence_label": "Evidencia en tiempo de ejecución", + "call.exact_accounting": "Contabilidad exacta de tokens", + "call.exact_label": "Exacto desde la devolución de llamada del token", + "call.hidden_estimate": "Estimación de entrada serializada/oculta inexplicable", + "call.no_previous": "No hay llamadas anteriores en este hilo resuelto.", + "call.not_found": "La llamada seleccionada no se encontró en las filas cargadas del panel.", + "call.open_hint": "Haga clic en una fila de llamada para obtener un diagnóstico profundo.", + "call.position": "Llame a {position} en este hilo resuelto.", + "call.post_compaction": "Posible poscompactación", + "call.raw_evidence": "Evidencia cruda", + "call.remaining_after_serialized": "Restante después del enlace serializado", + "call.remaining_after_serialized_hint": "La entrada no almacenada en caché no está cubierta ni siquiera por el límite superior serializado", + "call.serialized_bound_hint": "Estructura JSONL local de límite superior; texto rápido no exacto.", + "call.serialized_breakdown": "Grupos de evidencia serializada", + "call.serialized_bucket_detail": "{count} campos · {chars} caracteres", + "call.serialized_candidate": "Posible sobrecarga serializada", + "call.serialized_candidate_hint": "Límite superior serializado menos estimación visible, limitado por entrada exacta sin caché", + "call.serialized_deferred": "Estimación rápida cargada; el agrupamiento serializado completo queda diferido.", + "call.serialized_quick_hint": "estimación rápida", + "call.serialized_upper_bound": "Límite superior local serializado", + "call.visible_estimate": "Estimación del nuevo contexto visible", + "call.visible_gap": "Entrada sin caché menos estimación visible", + "caption.ascending": "ascendiendo", + "caption.call_investigator": "Llamada de investigación {record}.", + "caption.calls": "Mostrando llamadas de modelos individuales ordenadas por {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "descendiendo", + "caption.initial_calls": "Mostrando llamadas de modelos individuales.", + "caption.insights": "Clasificados por costo, créditos de uso, reutilización de caché, presión contextual y confianza en los precios.", + "caption.loaded": "{loaded} llamadas cargadas", + "caption.loaded_capped": "{loaded} de {available} llamadas cargadas", + "caption.rows_loaded_progress": "Filas cargadas: {loaded} de {total}", + "caption.rows_loading_background": "Los totales del dashboard están listos. Las filas se están cargando en segundo plano.", + "caption.rows_loading_progress": "Cargando filas: {loaded} de {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "Mostrando {threads} hilos de {calls} llamadas filtradas, ordenados por {sort}. {loaded}. Haga clic en un hilo para expandir sus llamadas.", + "context.api_http": "El contexto API devolvió HTTP {status}.", + "context.api_unavailable": "El contexto API no está disponible aquí. Ejecute codex-usage-tracker serve-dashboard --open para la carga de contexto bajo demanda.", + "context.auto_loading": "Cargando evidencia de giro seleccionado con salida de herramienta incluida.", + "context.chars_omitted": "{count} caracteres por encima del presupuesto omitidos.", + "context.compaction_detected": "Compactación detectada", + "context.compaction_replacement": "Contexto de reemplazo compactado", + "context.compaction_replacement_count": "{count} entradas del historial de reemplazo disponibles.", + "context.disabled_hint": "La carga de contexto está desactivada para este servidor de panel. Habilítelo aquí para cargar el contexto JSONL local a pedido.", + "context.enabled_note": "La carga de contexto está habilitada. Presione Mostrar evidencia del registro de turnos para leer esta llamada de la fuente local JSONL.", + "context.file_hint": "Abra este panel con codex-usage-tracker serve-dashboard para cargar contexto sin formato a pedido.", + "context.line": "línea {line}", + "context.loading": "Cargando contexto local...", + "context.local_redacted": "Contexto local JSONL cargado bajo demanda. Las indicaciones y los resultados de las herramientas se redactan según patrones secretos comunes y no se conservan en SQLite ni en el panel HTML.", + "context.no_char_limit_active": "No se aplica límite de caracteres.", + "context.no_record_id": "Esta fila no tiene ningún ID de registro para la búsqueda de contexto.", + "context.no_response": "Sin cuerpo de respuesta", + "context.older_omitted": "{count} entradas anteriores omitidas.", + "context.ready_hint": "El contexto no está integrado en este panel. Presione un botón para leer esta llamada desde la fuente local JSONL.", + "context.settings_http": "La configuración de contexto devolvió HTTP {status}.", + "context.source": "Fuente: {file}:{line}", + "context.token_breakdown": "Desglose de tokens", + "context.token_cached": "En caché", + "context.token_input": "Entrada", + "context.token_output": "Salida", + "context.token_reasoning": "Razonamiento", + "context.token_required": "La carga de contexto requiere un token localhost del panel API.", + "context.token_scope_call": "esta llamada", + "context.token_scope_earlier": "Recuento de tokens anterior en el mismo turno", + "context.token_scope_previous": "Recuento de tokens anterior en el mismo turno", + "context.token_scope_selected": "Recuento de tokens de llamada seleccionados", + "context.token_scope_session": "Sesión acumulativa", + "context.token_total": "totales", + "context.token_type": "Tipo", + "context.token_uncached": "sin caché", + "context.tool_included": "Salida de herramienta incluida con límites de redacción y tamaño.", + "context.tool_omitted": "Salida de herramienta oculta para esta vista.", + "credit.configured_rate": "Tarifa configurada", + "credit.estimated_mapping": "Mapeo estimado", + "credit.inferred_mapping": "Mapeo de modelo inferido", + "credit.no_mapped_rate": "Sin tarifa asignada", + "credit.no_rate": "Sin tasa de crédito", + "credit.official_match": "Coincidencia oficial de la hoja de tarifas", + "credit.user_rate": "Tasa de crédito proporcionada por el usuario", + "credit.with_status": "{value} créditos · {status}", + "dashboard.call_details": "Detalles de la llamada", + "dashboard.detail.empty": "Pase el cursor sobre una fila o haga clic para inspeccionar los campos de uso agregados.", + "dashboard.eyebrow": "Análisis locales de Codex", + "dashboard.local_storage_note": "El encabezado del panel también recuerda su elección de idioma localmente.", + "dashboard.model_calls": "Llamadas modelo", + "dashboard.title": "Panel de uso", + "dashboard.top_threads_by_attention": "Temas principales por puntuación de atención", + "dashboard.view.call": "Llamar al investigador", + "dashboard.view.calls": "llamadas", + "dashboard.view.insights": "Perspectivas", + "dashboard.view.threads": "Hilos", + "date.custom": "personalizado", + "date.invalid_range": "Intervalo de fechas no válido", + "date.range_between": "{prefix} {start} a {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", - "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", + "date.range_from": "{prefix} de {start}", + "date.range_through": "{prefix} a {end}", + "detail.agent_nickname": "Apodo del agente", + "detail.agent_role": "Rol de agente", + "detail.allowance_impact": "Impacto de la provisión", + "detail.attached_calls": "llamadas adjuntas", + "detail.auto_review_calls": "Llamadas de revisión automática", + "detail.cache_savings": "Ahorro de caché", + "detail.call_number": "llamar a {number}", + "detail.calls": "llamadas", + "detail.context_window": "ventana contextual", + "detail.cost_usage_context": "Costo, uso y contexto", + "detail.credit_confidence": "Confianza crediticia", + "detail.credit_model": "Modelo de crédito", + "detail.credit_note": "nota de crédito", + "detail.credit_source": "Fuente de crédito", + "detail.credit_source_fetched": "Fuente de crédito obtenida", + "detail.credit_tier": "Nivel de crédito", + "detail.cwd": "CWD", + "detail.efficiency_signals": "Señales de eficiencia", + "detail.first_expensive_turn": "Primer turno caro", + "detail.git_branch": "rama git", + "detail.largest_cumulative_jump": "Mayor salto acumulado", + "detail.latest_activity": "Última actividad", + "detail.model_mix": "Mezcla de modelos", + "detail.next_action": "Siguiente acción", + "detail.no_above_thresholds": "Ninguno por encima de los umbrales", + "detail.no_aggregate_action": "No se marca ninguna acción agregada.", + "detail.parent_session": "Sesión para padres", + "detail.parent_thread": "Hilo principal", + "detail.parent_updated": "Padre actualizado", + "detail.pricing_model": "Modelo de precios", + "detail.pricing_status": "Estado de precios", + "detail.project_cwd": "proyecto cwd", + "detail.project_tags": "Etiquetas de proyecto", + "detail.raw_identifiers": "Identificadores de agregados sin procesar", + "detail.reasoning_mix": "mezcla de razonamiento", + "detail.relationships": "Relaciones", + "detail.remote_hash": "hash remoto", + "detail.remote_label": "Etiqueta remota", + "detail.secondary_thread_fields": "Campos de hilo secundario", + "detail.source_file_line": "Archivo fuente y línea", + "detail.source_line": "línea fuente", + "detail.spawned_child_calls": "Llamadas infantiles generadas", + "detail.spawned_from": "Engendrado de", + "detail.spawned_threads": "Hilos generados", + "detail.subagent_before_spike": "Subagente antes del pico", + "detail.subagent_calls": "Llamadas de subagente", + "detail.subagent_type": "Tipo de subagente", + "detail.thread_attachment": "Accesorio de hilo", + "detail.thread_attention_summary": "Resumen de atención del hilo", + "detail.thread_lifecycle": "Ciclo de vida del hilo", + "detail.thread_narrative": "Narrativa del hilo", + "detail.thread_source": "Fuente del hilo", + "detail.thread_timeline": "Línea de tiempo del hilo", + "detail.timeline_context": "contexto {value}", + "detail.timeline_empty": "No hay llamadas en este hilo.", + "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · caché {cache}", + "detail.timestamp": "Marca de tiempo", + "detail.token_pricing_breakdown": "Desglose de tokens y precios", + "detail.tokens_at": "{tokens} tokens en {time}", + "detail.turn": "girar", + "detail.why_flagged": "Por qué marcado", + "docs.dashboard_guide": "Guía del panel", + "effort.high": "alto", + "effort.low": "bajo", + "effort.medium": "medio", + "filter.confidence": "confianza", + "filter.effort": "Razonamiento", + "filter.end": "Fin", + "filter.model": "modelo", + "filter.project": "Proyecto", + "filter.reasoning": "Razonamiento", + "filter.search": "Buscar", + "filter.search_placeholder": "Hilo, cwd, modelo", + "filter.session": "Sesión", + "filter.sort": "ordenar", + "filter.start": "Empezar", + "filter.thread": "Hilo", + "filter.time": "tiempo", + "flag.elevated_context_use": "Uso de contexto elevado", + "flag.expensive_low_output_call": "Llamada costosa de bajo rendimiento", + "flag.high_context_use": "Uso de alto contexto", + "flag.high_estimated_cost": "Costo estimado alto", + "flag.high_reasoning_share": "Alto porcentaje de razonamiento", + "flag.low_cache_reuse": "Baja reutilización de caché", + "history.active_hidden": "Solo sesiones activas; {count} llamadas archivadas ocultas", + "history.active_only": "Solo sesiones activas", + "history.all_empty": "Todo el historial seleccionado; aún no se han indexado llamadas archivadas", + "history.all_includes": "Todo el historial incluye {count} llamadas archivadas", + "history.archived_scan_hint": "{detail}. Las sesiones archivadas se analizan solo cuando se selecciona Todo el historial durante la actualización en vivo.", + "insight.apply_cache_misses": "Aplicar preajuste de errores de caché", + "insight.apply_context_bloat": "Aplicar ajuste preestablecido de aumento de contexto", + "insight.codex_allowance_usage": "Codex uso de asignación", + "insight.context_bloat": "Inflación del contexto", + "insight.context_bloat_body": "Las llamadas {calls} tienen un uso de contexto igual o superior a {ratio}.", + "insight.costliest_thread": "Hilo más costoso", + "insight.costliest_thread_body": "{thread} tiene {calls} llamadas y {tokens} tokens.", + "insight.credit_coverage_body": "{ratio} de tokens visibles se asignan a Codex tasas de crédito.", + "insight.estimated_pricing": "Precio estimado", + "insight.estimated_pricing_body": "Se incluyen los precios marcados como las mejores estimaciones, pero deben revisarse por separado.", + "insight.inspect_selected_call": "Inspeccionar llamada seleccionada", + "insight.low_cache_reuse": "Baja reutilización de caché", + "insight.low_cache_reuse_body": "Las llamadas {calls} están bajo reutilización de caché {ratio}. Comience con {thread}.", + "insight.open_thread_timeline": "Abrir línea de tiempo del hilo", + "insight.reasoning_output_spike": "Aumento de la producción de razonamiento", + "insight.reasoning_spike_body": "{thread} tiene la llamada de salida de razonamiento más grande en el filtro actual.", + "insight.review_estimates": "Revisar estimaciones", + "insight.review_highest_credit": "Revisar las llamadas con mayor crédito", + "insight.review_pricing_gaps": "Revisar las brechas de precios", + "insight.unpriced_usage": "Uso sin precio", + "insight.unpriced_usage_body": "Estos tokens se omiten de los costos totales estimados hasta que se configura el precio.", + "language.english": "ingles", + "language.label": "Idioma", "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "live.checking_usage": "Comprobando nuevos usos...", + "live.every": "Actualización en vivo cada {seconds}s", + "live.history_static_hint": "Ejecute codex-usage-tracker serve-dashboard para cambiar entre sesiones activas y todo el historial desde el panel.", + "live.indexed": "Filas agregadas {rows} indexadas de registros {files}.", + "live.load_static_hint": "Ejecute codex-usage-tracker serve-dashboard para cargar un tamaño de historial diferente desde el panel.", + "live.loading_rows": "Cargando filas en segundo plano...", + "live.paused": "Actualización en vivo pausada", + "live.refresh_suffix": ". Vuelva a cargar esta página después de regenerar un panel estático o ejecute codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "Actualización en vivo no disponible: {message}{suffix}", + "live.refreshing_index": "Actualizando índice de uso local...", + "live.reloading_static": "Recargando instantánea del panel estático...", + "live.skipped": "Se omitieron {count} eventos de recuento de tokens con formato incorrecto.", + "live.updated_detail": "Actualizado {time}. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "Puntuación de atención", + "metric.cache_ratio": "Proporción de caché", + "metric.cache_trend": "Tendencia de caché", + "metric.cached_input": "Entrada en caché", + "metric.codex_credits": "Codex créditos", + "metric.context_trend": "Tendencia del contexto", + "metric.context_use": "Uso del contexto", + "metric.estimated_cost": "Costo estimado", + "metric.input_tokens": "Tokens de entrada", + "metric.last_call_input": "Entrada de última llamada", + "metric.last_call_total": "Total de la última llamada", + "metric.max_context_use": "Uso máximo del contexto", + "metric.output": "Salida", + "metric.output_tokens": "Tokens de salida", + "metric.reasoning_output": "Salida de razonamiento", + "metric.remaining_usage": "Uso restante", + "metric.session_cumulative": "Sesión acumulativa", + "metric.total": "totales", + "metric.total_tokens": "tokens totales", + "metric.uncached_input": "Entrada sin caché", + "metric.usage_credits": "Créditos de uso", + "metric.usage_remaining": "Uso restante", + "metric.visible_calls": "llamadas visibles", + "nav.history": "Historia", + "nav.live": "en vivo", + "nav.load": "Cargar", + "option.active_sessions_only": "Solo sesiones activas", + "option.all_confidence": "toda confianza", + "option.all_efforts": "Todos los esfuerzos", + "option.all_history": "toda la historia", + "option.all_models": "Todos los modelos", + "option.all_time": "Todo el tiempo", + "option.custom_range": "Rango personalizado", + "option.estimated_cost": "Costo estimado", + "option.estimated_credit_mapping": "Mapeo de crédito estimado", + "option.exact_cost": "Costo exacto", + "option.exact_credit_rate": "Tasa de crédito exacta", + "option.highest_codex_credits": "Créditos Codex más altos", + "option.highest_context_use": "Uso de contexto más alto", + "option.highest_estimated_cost": "Costo estimado más alto", + "option.last_7_days": "últimos 7 días", + "option.load_10000": "10.000 llamadas", + "option.load_20000": "20.000 llamadas", + "option.load_5000": "5.000 llamadas", + "option.load_all": "todas las llamadas", + "option.lowest_cache_ratio": "Proporción de caché más baja", + "option.missing_credit_rate": "Tasa de crédito faltante", + "option.most_signals": "La mayoría de las señales", + "option.most_tokens": "La mayoría de las tokens", + "option.needs_attention": "necesita atencion", + "option.newest_calls": "Llamadas más recientes", + "option.this_month": "este mes", + "option.this_week": "esta semana", + "option.thread_name": "Nombre del hilo", + "option.today": "hoy", + "option.unpriced_cost": "Costo sin precio", + "option.user_credit_override": "Anulación de crédito de usuario", + "parser.warnings_title": "La última actualización informó {count} diagnóstico del analizador: {entries}. Ejecute codex-usage-tracker inspect-log para investigar la desviación del esquema.", + "preset.cache_misses": "Errores de caché", + "preset.cache_misses_caption": "La caché no está preestablecida", + "preset.cache_misses_desc": "Llamadas con una proporción de caché baja agrupadas por cwd, modelo y subproceso.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", + "preset.context_bloat": "Inflación del contexto", + "preset.context_bloat_caption": "Ajuste preestablecido de inflación de contexto", + "preset.context_bloat_desc": "Llamadas con más del 60% de uso de contexto o con tokens acumulativos muy altos.", + "preset.description": "Puntos de partida con un solo clic para preguntas de uso común.", + "preset.estimated_price_review": "Revisión del precio estimado", + "preset.estimated_price_review_caption": "Preajuste de revisión de precio estimado", + "preset.estimated_price_review_desc": "Precio de uso con estimaciones marcadas.", + "preset.highest_codex_credits": "Créditos Codex más altos", + "preset.highest_codex_credits_caption": "Preajuste de Codex créditos más alto", + "preset.highest_codex_credits_desc": "Llamadas clasificadas por impacto estimado en la asignación de uso de Codex.", + "preset.highest_cost_threads": "Hilos de mayor costo", + "preset.highest_cost_threads_caption": "Preajuste de hilos de mayor costo", + "preset.highest_cost_threads_desc": "Hilos ordenados por gasto estimado, con subagentes adjuntos.", + "preset.investigation_presets": "Ajustes preestablecidos de investigación", + "preset.no_preset": "No se ha aplicado ningún ajuste preestablecido.", + "preset.pricing_gaps": "Brechas de precios", + "preset.pricing_gaps_caption": "Diferencias de precios preestablecidas", + "preset.pricing_gaps_desc": "Uso sin precio que hace que los costos totales estimados estén incompletos.", + "pricing.configure_hint": "Ejecute codex-usage-tracker update-pricing para configurar los costos estimados.", + "pricing.fetched": "recuperado {time}", + "pricing.pinned": "instantánea fijada", + "pricing.source": "Fuente de precios", + "pricing.tier": "Nivel {tier}", + "pricing.title": "{parts}. Las etiquetas internas Codex pueden utilizar estimaciones marcadas.{warning}", + "pricing.title_fetched": "{parts}. Obtenido de {url} en {time}. Las etiquetas internas Codex pueden utilizar estimaciones marcadas.{warning}", + "privacy.aliases_preserved": "Los alias de proyectos configurados se tratan como opciones de visualización explícitas.", + "privacy.cwd_redacted": "Las rutas cwd sin procesar están redactadas.", + "privacy.git_branch_hidden": "La rama de Git está oculta.", + "privacy.git_remote_label_hidden": "Las etiquetas remotas de Git están ocultas.", + "privacy.mode": "Modo de privacidad de metadatos del proyecto: {mode}.", + "privacy.normal_title": "Los metadatos del proyecto se muestran con etiquetas cwd, proyecto, rama y configuración locales.", + "privacy.project_names_redacted": "Los proyectos sin nombre utilizan etiquetas hash estables.", + "privacy.relative_cwd_hidden": "La cwd relativa está oculta.", + "privacy.tags_hidden": "Las etiquetas del proyecto están ocultas.", + "recommendation.context_bloat.action": "Considere iniciar un nuevo hilo Codex si el contexto anterior ya no es relevante.", + "recommendation.context_bloat.title": "Alta presión del contexto", + "recommendation.context_bloat.why": "Esta llamada utiliza una gran parte de la ventana de contexto del modelo.", + "recommendation.elevated_context.action": "Verifique si el hilo se puede estrechar antes de agregar más trabajo.", + "recommendation.elevated_context.title": "Elevada presión del contexto", + "recommendation.elevated_context.why": "El uso del contexto es elevado y puede resultar costoso en turnos posteriores.", + "recommendation.estimated_pricing.action": "Revise la cobertura de precios y fije o anule la tarifa del modelo si esta llamada es importante.", + "recommendation.estimated_pricing.title": "Precio estimado", + "recommendation.estimated_pricing.why": "Este costo utiliza un mapeo de modelo inferido en lugar de una fila de precios directos.", + "recommendation.high_cost.action": "Abra la línea de tiempo del hilo e inspeccione el turno anterior antes de continuar.", + "recommendation.high_cost.title": "Costo estimado alto", + "recommendation.high_cost.why": "Esta llamada cruzó el umbral de alto costo configurado.", + "recommendation.large_thread.action": "Prefiero un hilo nuevo para trabajos de seguimiento no relacionados.", + "recommendation.large_thread.title": "Hilo acumulativo grande", + "recommendation.large_thread.why": "El total acumulado de la sesión es lo suficientemente alto como para encarecer los turnos posteriores.", + "recommendation.low_cache.action": "Compruebe si se reintrodujeron innecesariamente archivos, resultados de herramientas o contexto amplio.", + "recommendation.low_cache.title": "Baja reutilización de caché", + "recommendation.low_cache.why": "La entrada nueva sin caché es alta mientras que la reutilización de la caché es baja.", + "recommendation.low_output.action": "Inspeccione primero el contexto agregado; cargue contexto sin formato solo si la causa no está clara.", + "recommendation.low_output.title": "Llamada grande de bajo rendimiento", + "recommendation.low_output.why": "La llamada consumió muchos tokens pero produjo pocos resultados.", + "recommendation.none.action": "No se marca ninguna acción agregada; Continuar monitoreando los patrones de uso.", + "recommendation.pricing_gap.action": "Actualice los precios o agregue un alias local antes de confiar en los costos totales.", + "recommendation.pricing_gap.title": "Brecha de precios", + "recommendation.pricing_gap.why": "Este modelo de llamada no tiene precio configurado, por lo que los costos totales subestiman el uso visible.", + "recommendation.reasoning_spike.action": "Revise si esta tarea necesita el esfuerzo de razonamiento seleccionado.", + "recommendation.reasoning_spike.title": "Alto porcentaje de razonamiento", + "recommendation.reasoning_spike.why": "El resultado del razonamiento domina el resultado visible de esta llamada.", + "recommendation.subagent_attribution.action": "Compare las llamadas directas con el subagente adjunto o revise las llamadas antes de cambiar el flujo de trabajo.", + "recommendation.subagent_attribution.title": "Atribución de subagente", + "recommendation.subagent_attribution.why": "Esta convocatoria está adjunta al trabajo delegado y puede explicar el crecimiento del hilo principal.", + "section.allowance": "Asignación", + "section.needs_attention": "Necesita atención", + "section.pricing": "Precios", + "section.recommendations": "Recomendaciones", + "severity.high": "Alto", + "severity.medium": "Medio", + "severity.review": "Revisión", + "source.auto_review": "Revisión automática", + "source.codex_initiated": "Codex iniciado", + "source.subagent": "Subagente", + "source.subagent_role": "Subagente: {role}", + "source.user": "Usuario", + "source.user_initiated": "Iniciado por el usuario", + "state.allowance_config_error": "Error de configuración de asignación", + "state.allowance_configured": "Asignación configurada", + "state.best_guess_estimate": "Estimación más aproximada", + "state.configured": "Configurado", + "state.configured_price": "Precio configurado", "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", + "state.estimated": "Estimado", + "state.loading": "Cargando", + "state.loading_rows": "Cargando filas", + "state.mixed": "Mixto", "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", - "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", + "state.no_calls": "Ninguna llamada coincide con los filtros actuales.", + "state.no_configured_price": "Sin precio configurado", + "state.no_context_entries": "No se encontraron entradas de contexto para esta llamada.", + "state.no_data": "Sin datos", + "state.no_mapped_rate": "Sin tarifa asignada", + "state.no_price": "Sin precio", + "state.no_rate": "Sin tarifa", + "state.no_rows": "Sin filas", + "state.no_threads": "Ningún hilo coincide con los filtros actuales.", + "state.none": "Ninguno", + "state.not_configured": "No configurado", + "state.requires_evidence": "Se necesita evidencia", + "state.unknown": "Desconocido", + "state.yes": "si", + "status.checking": "comprobando", + "status.paused": "En pausa", + "status.refresh_error": "Error al actualizar", + "status.refreshing": "refrescante", + "status.reloading": "Recargar", + "status.static": "estática", + "status.updated": "Actualizado", + "table.cache": "caché", + "table.cached": "En caché", + "table.calls": "llamadas", + "table.cost": "Costo", + "table.effort": "Esfuerzo", + "table.initiated": "iniciado", + "table.last_call": "Última llamada", + "table.model": "modelo", + "table.more_efforts": "{effort} +{count} esfuerzos", + "table.more_models": "Modelos {model} +{count}", + "table.output": "Salida", + "table.page_status": "{start}-{end} de {total} {items} · página {page}/{pages}", + "table.rows": "filas", + "table.signals": "Señales", + "table.source": "Fuente", + "table.thread": "Hilo", + "table.threads": "hilos", + "table.time": "tiempo", "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "table.uncached": "sin caché", + "table.visible_status": "Mostrando {end} de {total} {items}", + "thread.attached": "adjunto", + "thread.attention": "atención {score}", + "thread.auto_review": "{count} revisión automática", + "thread.collapse": "Colapso", + "thread.direct": "directo", + "thread.expand": "Expandir", + "thread.expand_label": "{action} {thread} llamadas. Puntuación de atención {score}.", + "thread.explicit_parent": "padre explícito", + "thread.explicit_parent_thread": "hilo principal explícito", + "thread.parent": "Padre {id}", + "thread.session": "sesión", + "thread.spawned": "engendrado", + "thread.spawned_from": "generado a partir de {thread}", + "thread.spawned_threads": "{count} hilos generados", + "thread.subagent": "{count} subagente", + "thread.unknown": "Hilo desconocido", + "thread.unmatched_subagent": "subagente inigualable" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/fr.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/fr.json index ef2ecf4..20747bc 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/fr.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/fr.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "Recherchez les fichiers réintroduits ou la sortie de l'outil après la suppression de la réutilisation du cache.", + "action.compare_fresh_input": "Comparez la nouvelle entrée avec le tour précédent avant de continuer.", + "action.compare_subagent_calls": "Comparez le sous-agent attaché ou examinez les appels avant de modifier le flux de travail parent.", + "action.configure_pricing": "Configurez les prix avant de faire confiance aux totaux des coûts.", + "action.copied": "Copié", + "action.copy_failed": "Échec de la copie", + "action.expand_or_select_recommendations": "Développez les appels ou sélectionnez une ligne pour les recommandations au niveau des appels.", + "action.exported": "Exporté {count}", + "action.inspect_thread_timeline": "Inspectez la chronologie du fil de discussion et envisagez de créer un nouveau fil de discussion.", + "action.review_context_growth": "Examinez où commence la croissance du contexte et envisagez de créer un nouveau fil de discussion.", + "action.review_reasoning_effort": "Vérifiez si l’effort de raisonnement est approprié pour cette tâche.", + "action.run": "Courir", + "action.set_limits": "Fixer des limites", + "action.use_aggregate_first": "Utilisez d'abord les champs agrégés ; charger le contexte uniquement si le signal n’est toujours pas clair.", + "allowance.counted": "Crédits {value} comptabilisés dans les limites d'utilisation de Codex", + "allowance.cr_left": "{value} cr gauche", + "allowance.credit_coverage": "Couverture de crédit {ratio} des tokens chargés.", + "allowance.credit_rates": "Taux de crédit : {source}.", + "allowance.credits_remaining": "{value} crédits restants", + "allowance.init_hint": "Exécutez codex-usage-tracker init-allowance pour ajouter les fenêtres d'utilisation restantes.", + "allowance.of_allowance": "{ratio} d'allocation", + "allowance.of_total": "{used} sur {total} crédits", + "allowance.rate_card_error": "Erreur de carte tarifaire : {error}", + "allowance.remaining": "{value} restant", + "allowance.resets": "Réinitialisation : {resets}", + "allowance.row_no_rate": "Aucun taux de crédit Codex mappé", + "allowance.title_hint": "Ajoutez ~/.codex-usage-tracker/allowance.json pour afficher les 5 heures et l'utilisation hebdomadaire restante.", + "allowance.used_vs_remaining": "{used} utilisé contre {remaining} restant", + "allowance.window_configured": "{label} configuré", + "allowance.windows": "Fenêtres d'allocation : {windows}", + "aria.current_view_actions": "Actions de la vue actuelle", + "aria.dashboard_status": "Statut du tableau de bord", + "aria.dashboard_view": "Vue du tableau de bord", + "aria.history_title": "Les sessions actives uniquement sont la valeur par défaut. Tout l’historique analyse les journaux de session archivés lors de l’actualisation en direct.", + "aria.inspect_thread": "Inspecter l'utilisation de {thread}", + "aria.refresh_controls": "Commandes d'actualisation du tableau de bord", + "aria.table_pages": "Pages de tableau", + "badge.costs": "Coûts", + "badge.credits": "Crédits", + "badge.live": "En direct", + "badge.metadata_mode": "Métadonnées {mode}", + "badge.metadata_normal": "Métadonnées normales", + "badge.no_costs": "Pas de frais", + "badge.parser_warnings": "Avertissements de l'analyseur", + "badge.static": "Statique", + "badge.unofficial_project": "Projet non officiel", + "badge.unofficial_project_title": "Codex Usage Tracker est indépendant et n'est pas créé, affilié, approuvé, sponsorisé ou soutenu par OpenAI. OpenAI et Codex sont des marques commerciales de OpenAI.", + "button.back_to_dashboard": "Retour au tableau de bord", + "button.clear": "Effacer", + "button.copy_link": "Copier le lien", + "button.enable_context_loading": "Activer le chargement du contexte", + "button.export_csv": "Exporter CSV", + "button.full_serialized_analysis": "Exécuter une analyse sérialisée complète", + "button.hide_details": "Masquer les détails", + "button.hide_tool_output": "Masquer la sortie de l'outil", + "button.include_tool_output": "Inclure la sortie de l'outil", + "button.load_context": "Charger le contexte", + "button.load_more": "Charger plus", + "button.load_older_context": "Charger les anciennes entrées", + "button.next": "Suivant", + "button.next_call": "Prochain appel", + "button.no_char_limit": "Aucune limite de caractères", + "button.open_investigator": "Enquêteur ouvert", + "button.previous": "Précédent", + "button.previous_call": "Appel précédent", + "button.refresh": "Actualiser", + "button.show_compaction_history": "Afficher le remplacement compacté", + "button.show_tool_output": "Afficher la sortie de l'outil", + "button.show_turn_evidence": "Afficher les preuves du journal de bord", + "button.top": "Haut", + "call.cache_accounting_delta": "Delta cache/comptabilité", + "call.cache_cold": "Reprise à froid/cache obsolète", + "call.cache_diagnostics": "Diagnostic du cache", + "call.cache_partial": "Manque de cache partiel", + "call.cache_spike": "Pic non mis en cache", + "call.cache_steady": "Profil de cache stable", + "call.cache_warm": "Réutilisation du cache à chaud", + "call.compaction_diagnostics": "Diagnostic de compactage", + "call.compaction_hint": "Les preuves chargées peuvent montrer des événements de compactage explicites. L'historique de remplacement rédigé s'affiche uniquement après l'action de remplacement compactée.", + "call.context_estimate": "Estimation du changement de contexte", + "call.context_estimate_hint": "Comparez les entrées exactes non mises en cache avec les preuves de journal visibles comptées par le tokenizer. L’écart doit être traité comme une erreur cachée d’échafaudage, de sérialisation ou d’estimation du tokenizer.", + "call.derived_label": "Dérivé d'appels agrégés adjacents", + "call.estimated_label": "Estimé à partir du volume de journal visible", + "call.evidence_label": "Preuve d'exécution", + "call.exact_accounting": "Comptabilité exacte des tokens", + "call.exact_label": "Exactement à partir du rappel du token", + "call.hidden_estimate": "Estimation d'entrée masquée/sérialisée inexpliquée", + "call.no_previous": "Aucun appel précédent dans ce fil résolu.", + "call.not_found": "L'appel sélectionné est introuvable dans les lignes chargées du tableau de bord.", + "call.open_hint": "Cliquez sur une ligne d'appel pour des diagnostics approfondis.", + "call.position": "Appelez {position} dans ce fil de discussion résolu.", + "call.post_compaction": "Post-compactage possible", + "call.raw_evidence": "Preuve brute", + "call.remaining_after_serialized": "Restant après la liaison sérialisée", + "call.remaining_after_serialized_hint": "Entrée non mise en cache non couverte même par la limite supérieure sérialisée", + "call.serialized_bound_hint": "Structure JSONL locale de limite supérieure ; pas de texte d'invite exact.", + "call.serialized_breakdown": "Groupes de preuves sérialisées", + "call.serialized_bucket_detail": "Champs {count} · Caractères {chars}", + "call.serialized_candidate": "Frais généraux sérialisés possibles", + "call.serialized_candidate_hint": "Limite supérieure sérialisée moins estimation visible, plafonnée par une entrée exacte non mise en cache", + "call.serialized_deferred": "Estimation rapide chargée ; l'analyse complète par groupes sérialisés est différée.", + "call.serialized_quick_hint": "estimation rapide", + "call.serialized_upper_bound": "Limite supérieure locale sérialisée", + "call.visible_estimate": "Estimation du nouveau contexte visible", + "call.visible_gap": "Entrée non mise en cache moins estimation visible", + "caption.ascending": "ascendant", + "caption.call_investigator": "Appel d'enquête {record}.", + "caption.calls": "Affichage des appels de modèles individuels triés par {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "décroissant", + "caption.initial_calls": "Affichage des appels de modèles individuels.", + "caption.insights": "Classés par coût, crédits d'utilisation, réutilisation du cache, pression contextuelle et fiabilité des prix.", + "caption.loaded": "Appels {loaded} chargés", + "caption.loaded_capped": "{loaded} appels sur {available} chargés", + "caption.rows_loaded_progress": "Lignes chargées : {loaded} sur {total}", + "caption.rows_loading_background": "Les totaux du tableau de bord sont prêts. Les lignes se chargent en arrière-plan.", + "caption.rows_loading_progress": "Chargement des lignes : {loaded} sur {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", + "caption.threads": "Affichage des fils de discussion {threads} provenant des appels filtrés {calls}, triés par {sort}. {loaded}. Cliquez sur un fil de discussion pour développer ses appels.", + "context.api_http": "Le contexte API a renvoyé HTTP {status}.", + "context.api_unavailable": "Le contexte API n'est pas disponible ici. Exécutez codex-usage-tracker serve-dashboard --open pour le chargement du contexte à la demande.", + "context.auto_loading": "Chargement de preuves de virage sélectionné avec sortie d'outil incluse.", + "context.chars_omitted": "{count} caractères dépassant le budget omis.", + "context.compaction_detected": "Compactage détecté", + "context.compaction_replacement": "Contexte de remplacement compacté", + "context.compaction_replacement_count": "{count} entrées d'historique de remplacement disponibles.", + "context.disabled_hint": "Le chargement du contexte est désactivé pour ce serveur de tableau de bord. Activez-le ici pour charger le contexte JSONL local à la demande.", + "context.enabled_note": "Le chargement du contexte est activé. Appuyez sur Afficher les preuves du journal de virage pour lire cet appel à partir de la source JSONL locale.", + "context.file_hint": "Ouvrez ce tableau de bord avec codex-usage-tracker serve-dashboard pour charger le contexte brut à la demande.", + "context.line": "ligne {line}", + "context.loading": "Chargement du contexte local...", + "context.local_redacted": "Contexte local JSONL chargé à la demande. Les invites et les résultats de l'outil sont rédigés pour les modèles secrets courants et ne sont pas conservés dans SQLite ou dans le tableau de bord HTML.", + "context.no_char_limit_active": "Aucune limite de caractères appliquée.", + "context.no_record_id": "Cette ligne n'a aucun identifiant d'enregistrement pour la recherche contextuelle.", + "context.no_response": "Aucun corps de réponse", + "context.older_omitted": "{count} entrées plus anciennes omises.", + "context.ready_hint": "Le contexte n'est pas intégré dans ce tableau de bord. Appuyez sur un bouton pour lire cet appel depuis la source locale JSONL.", + "context.settings_http": "Les paramètres de contexte ont renvoyé HTTP {status}.", + "context.source": "Source : {file} :{line}", + "context.token_breakdown": "Répartition des tokens", + "context.token_cached": "En cache", + "context.token_input": "Entrée", + "context.token_output": "Sortie", + "context.token_reasoning": "Raisonnement", + "context.token_required": "Le chargement du contexte nécessite un token API du tableau de bord localhost.", + "context.token_scope_call": "Cet appel", + "context.token_scope_earlier": "Nombre de tokens antérieurs dans le même tour", + "context.token_scope_previous": "Nombre de tokens précédent dans le même tour", + "context.token_scope_selected": "Nombre de tokens d'appel sélectionnés", + "context.token_scope_session": "Session cumulée", "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "context.token_type": "Tapez", + "context.token_uncached": "Non mis en cache", + "context.tool_included": "Sortie de l'outil incluse avec les limites de rédaction et de taille.", + "context.tool_omitted": "Sortie de l'outil masquée pour cette vue.", + "credit.configured_rate": "Tarif configuré", + "credit.estimated_mapping": "Cartographie estimée", + "credit.inferred_mapping": "Mappage de modèle déduit", + "credit.no_mapped_rate": "Aucun tarif cartographié", + "credit.no_rate": "Pas de taux de crédit", + "credit.official_match": "Correspondance officielle de la grille tarifaire", + "credit.user_rate": "Taux de crédit fourni par l'utilisateur", + "credit.with_status": "{value} crédits · {status}", + "dashboard.call_details": "Détails de l'appel", + "dashboard.detail.empty": "Passez la souris ou cliquez sur une ligne pour inspecter les champs d'utilisation agrégés.", + "dashboard.eyebrow": "Analyses locales Codex", + "dashboard.local_storage_note": "L'en-tête du tableau de bord mémorise également votre choix de langue localement.", + "dashboard.model_calls": "Appels modèles", + "dashboard.title": "Tableau de bord d'utilisation", + "dashboard.top_threads_by_attention": "Principaux sujets par score d'attention", + "dashboard.view.call": "Appeler l'enquêteur", + "dashboard.view.calls": "Appels", + "dashboard.view.insights": "Aperçus", + "dashboard.view.threads": "Sujets", + "date.custom": "Personnalisé", + "date.invalid_range": "Plage de dates non valide", + "date.range_between": "{prefix} {start} à {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", - "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", + "date.range_from": "{prefix} de {start}", + "date.range_through": "{prefix} à {end}", + "detail.agent_nickname": "Pseudonyme de l'agent", + "detail.agent_role": "Rôle d'agent", + "detail.allowance_impact": "Impact des allocations", + "detail.attached_calls": "Appels joints", + "detail.auto_review_calls": "Examen automatique des appels", + "detail.cache_savings": "Économies de cache", + "detail.call_number": "appeler {number}", + "detail.calls": "Appels", + "detail.context_window": "Fenêtre contextuelle", + "detail.cost_usage_context": "Coût, utilisation et contexte", + "detail.credit_confidence": "Confiance du crédit", + "detail.credit_model": "Modèle de crédit", + "detail.credit_note": "Note de crédit", + "detail.credit_source": "Source de crédit", + "detail.credit_source_fetched": "Source de crédit récupérée", + "detail.credit_tier": "Niveau de crédit", + "detail.cwd": "CWD", + "detail.efficiency_signals": "Signaux d'efficacité", + "detail.first_expensive_turn": "Premier tour coûteux", + "detail.git_branch": "Branche Git", + "detail.largest_cumulative_jump": "Le plus grand saut cumulé", + "detail.latest_activity": "Dernière activité", + "detail.model_mix": "Mélange de modèles", + "detail.next_action": "Action suivante", + "detail.no_above_thresholds": "Aucun au-dessus des seuils", + "detail.no_aggregate_action": "Aucune action globale n’est signalée.", + "detail.parent_session": "Séance parents", + "detail.parent_thread": "Fil de discussion parent", + "detail.parent_updated": "Parent mis à jour", + "detail.pricing_model": "Modèle de tarification", + "detail.pricing_status": "Statut de tarification", + "detail.project_cwd": "Projet en cours", + "detail.project_tags": "Balises du projet", + "detail.raw_identifiers": "Identifiants agrégés bruts", + "detail.reasoning_mix": "Mélange de raisonnement", + "detail.relationships": "Relations", + "detail.remote_hash": "Hachage à distance", + "detail.remote_label": "Étiquette distante", + "detail.secondary_thread_fields": "Champs de thread secondaire", + "detail.source_file_line": "Fichier source et ligne", + "detail.source_line": "Ligne source", + "detail.spawned_child_calls": "Appels d'enfants générés", + "detail.spawned_from": "Apparu à partir de", + "detail.spawned_threads": "Discussions générées", + "detail.subagent_before_spike": "Sous-agent avant pic", + "detail.subagent_calls": "Appels de sous-agents", + "detail.subagent_type": "Type de sous-agent", + "detail.thread_attachment": "Attachement du fil", + "detail.thread_attention_summary": "Résumé de l'attention du sujet", + "detail.thread_lifecycle": "Cycle de vie des threads", + "detail.thread_narrative": "Récit du fil de discussion", + "detail.thread_source": "Source du fil de discussion", + "detail.thread_timeline": "Chronologie du sujet", + "detail.timeline_context": "contexte {value}", + "detail.timeline_empty": "Aucun appel dans ce fil.", + "detail.timeline_meta": "Tokens {tokens} · {cost} · {credits} · cache {cache}", + "detail.timestamp": "Horodatage", + "detail.token_pricing_breakdown": "Répartition des tokens et des prix", + "detail.tokens_at": "Tokens {tokens} à {time}", + "detail.turn": "Tourner", + "detail.why_flagged": "Pourquoi signalé", + "docs.dashboard_guide": "Guide du tableau de bord", + "effort.high": "haut", + "effort.low": "faible", + "effort.medium": "moyen", + "filter.confidence": "Confiance", + "filter.effort": "Raisonnement", + "filter.end": "Fin", + "filter.model": "Modèle", + "filter.project": "Projet", + "filter.reasoning": "Raisonnement", + "filter.search": "Rechercher", + "filter.search_placeholder": "Sujet, cwd, modèle", + "filter.session": "Séance", + "filter.sort": "Trier", + "filter.start": "Commencer", + "filter.thread": "Sujet", + "filter.time": "Temps", + "flag.elevated_context_use": "Utilisation du contexte élevée", + "flag.expensive_low_output_call": "Appel coûteux à faible débit", + "flag.high_context_use": "Utilisation contextuelle élevée", + "flag.high_estimated_cost": "Coût estimé élevé", + "flag.high_reasoning_share": "Part de raisonnement élevée", + "flag.low_cache_reuse": "Faible réutilisation du cache", + "history.active_hidden": "Sessions actives uniquement ; {count} appels archivés masqués", + "history.active_only": "Sessions actives uniquement", + "history.all_empty": "Tout l'historique sélectionné ; aucun appel archivé n'est encore indexé", + "history.all_includes": "Tout l'historique comprend {count} appels archivés", + "history.archived_scan_hint": "{detail}. Les sessions archivées sont analysées uniquement lorsque Tout l'historique est sélectionné lors de l'actualisation en direct.", + "insight.apply_cache_misses": "Appliquer le préréglage des échecs de cache", + "insight.apply_context_bloat": "Appliquer le préréglage de gonflement du contexte", + "insight.codex_allowance_usage": "Utilisation de l'allocation Codex", + "insight.context_bloat": "Gonflement du contexte", + "insight.context_bloat_body": "Les appels {calls} sont égaux ou supérieurs à l'utilisation du contexte {ratio}.", + "insight.costliest_thread": "Fil le plus coûteux", + "insight.costliest_thread_body": "{thread} a {calls} appels et {tokens} tokens.", + "insight.credit_coverage_body": "{ratio} de tokens visibles correspondent aux taux de crédit Codex.", + "insight.estimated_pricing": "Prix estimé", + "insight.estimated_pricing_body": "Les prix indicatifs indiqués sont inclus, mais doivent être examinés séparément.", + "insight.inspect_selected_call": "Inspecter l'appel sélectionné", + "insight.low_cache_reuse": "Faible réutilisation du cache", + "insight.low_cache_reuse_body": "Les appels {calls} sont sous réutilisation du cache {ratio}. Commencez par {thread}.", + "insight.open_thread_timeline": "Ouvrir la chronologie du fil de discussion", + "insight.reasoning_output_spike": "Pic de production de raisonnement", + "insight.reasoning_spike_body": "{thread} a le plus grand appel de sortie de raisonnement dans le filtre actuel.", + "insight.review_estimates": "Examiner les estimations", + "insight.review_highest_credit": "Examiner les appels ayant le crédit le plus élevé", + "insight.review_pricing_gaps": "Examiner les écarts de prix", + "insight.unpriced_usage": "Utilisation sans prix", + "insight.unpriced_usage_body": "Ces tokens sont omis des coûts totaux estimés jusqu'à ce que la tarification soit configurée.", + "language.english": "Anglais", + "language.label": "Langue", "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", + "live.checking_usage": "Vérification d'une nouvelle utilisation...", + "live.every": "Actualisation en direct tous les {seconds}s", + "live.history_static_hint": "Exécutez codex-usage-tracker serve-dashboard pour basculer entre les sessions actives et tout l'historique à partir du tableau de bord.", + "live.indexed": "Lignes agrégées {rows} indexées à partir des journaux {files}.", + "live.load_static_hint": "Exécutez codex-usage-tracker serve-dashboard pour charger une taille d'historique différente à partir du tableau de bord.", + "live.loading_rows": "Chargement des lignes en arrière-plan...", + "live.paused": "Actualisation en direct interrompue", + "live.refresh_suffix": ". Rechargez cette page après avoir régénéré un tableau de bord statique ou exécutez codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "Actualisation en direct indisponible : {message}{suffix}", + "live.refreshing_index": "Actualisation de l'index d'utilisation locale...", + "live.reloading_static": "Rechargement de l'instantané statique du tableau de bord...", + "live.skipped": "Événements de décompte de tokens mal formés {count} ignorés.", + "live.updated_detail": "{time} mis à jour. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "Score d'attention", + "metric.cache_ratio": "Taux de cache", + "metric.cache_trend": "Tendance du cache", + "metric.cached_input": "Entrée en cache", + "metric.codex_credits": "Codex crédits", + "metric.context_trend": "Tendance contextuelle", + "metric.context_use": "Utilisation du contexte", + "metric.estimated_cost": "Coût estimé", + "metric.input_tokens": "Tokens d'entrée", + "metric.last_call_input": "Saisie du dernier appel", + "metric.last_call_total": "Total du dernier appel", + "metric.max_context_use": "Utilisation maximale du contexte", + "metric.output": "Sortie", + "metric.output_tokens": "Tokens de sortie", + "metric.reasoning_output": "Sortie de raisonnement", + "metric.remaining_usage": "Utilisation restante", + "metric.session_cumulative": "Session cumulée", "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", - "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", + "metric.total_tokens": "Total des tokens", + "metric.uncached_input": "Entrée non mise en cache", + "metric.usage_credits": "Crédits d'utilisation", + "metric.usage_remaining": "Utilisation restante", + "metric.visible_calls": "Appels visibles", + "nav.history": "Histoire", + "nav.live": "En direct", + "nav.load": "Charger", + "option.active_sessions_only": "Sessions actives uniquement", + "option.all_confidence": "Toute confiance", + "option.all_efforts": "Tous les efforts", + "option.all_history": "Toute l'histoire", + "option.all_models": "Tous les modèles", + "option.all_time": "Tout le temps", + "option.custom_range": "Gamme personnalisée", + "option.estimated_cost": "Coût estimé", + "option.estimated_credit_mapping": "Cartographie de crédit estimée", + "option.exact_cost": "Coût exact", + "option.exact_credit_rate": "Taux de crédit exact", + "option.highest_codex_credits": "Crédits Codex les plus élevés", + "option.highest_context_use": "Utilisation du contexte le plus élevé", + "option.highest_estimated_cost": "Coût estimé le plus élevé", + "option.last_7_days": "7 derniers jours", + "option.load_10000": "10 000 appels", + "option.load_20000": "20 000 appels", + "option.load_5000": "5 000 appels", + "option.load_all": "Tous les appels", + "option.lowest_cache_ratio": "Taux de cache le plus bas", + "option.missing_credit_rate": "Taux de crédit manquant", + "option.most_signals": "La plupart des signaux", + "option.most_tokens": "La plupart des tokens", + "option.needs_attention": "A besoin d'attention", + "option.newest_calls": "Appels les plus récents", + "option.this_month": "Ce mois-ci", + "option.this_week": "Cette semaine", + "option.thread_name": "Nom du fil", + "option.today": "Aujourd'hui", + "option.unpriced_cost": "Coût non tarifé", + "option.user_credit_override": "Remplacement du crédit utilisateur", + "parser.warnings_title": "La dernière actualisation a signalé les diagnostics de l'analyseur {count} : {entries}. Exécutez codex-usage-tracker inspect-log pour étudier la dérive du schéma.", + "preset.cache_misses": "Le cache manque", + "preset.cache_misses_caption": "Le cache manque le préréglage", + "preset.cache_misses_desc": "Appels à faible taux de cache regroupés par cwd, modèle et thread.", + "preset.caption": "{caption} : {description}", + "preset.context_bloat": "Gonflement du contexte", + "preset.context_bloat_caption": "Préréglage de ballonnement de contexte", + "preset.context_bloat_desc": "Appels à plus de 60 % d'utilisation du contexte ou avec des tokens cumulés très élevés.", + "preset.description": "Points de départ en un clic pour les questions d'utilisation courantes.", + "preset.estimated_price_review": "Examen du prix estimé", + "preset.estimated_price_review_caption": "Préréglage d'évaluation du prix estimé", + "preset.estimated_price_review_desc": "Utilisation tarifée avec les meilleures estimations marquées.", + "preset.highest_codex_credits": "Crédits Codex les plus élevés", + "preset.highest_codex_credits_caption": "Préréglage de crédits Codex le plus élevé", + "preset.highest_codex_credits_desc": "Appels triés par impact estimé sur la limite d'utilisation de Codex.", + "preset.highest_cost_threads": "Fils les plus chers", + "preset.highest_cost_threads_caption": "Préréglage des threads les plus coûteux", + "preset.highest_cost_threads_desc": "Discussions triées par dépenses estimées, avec sous-agents attachés.", + "preset.investigation_presets": "Préréglages d'enquête", + "preset.no_preset": "Aucun préréglage appliqué.", + "preset.pricing_gaps": "Écarts de prix", + "preset.pricing_gaps_caption": "Écarts de prix prédéfinis", + "preset.pricing_gaps_desc": "Utilisation non tarifée qui rend les coûts totaux estimés incomplets.", + "pricing.configure_hint": "Exécutez codex-usage-tracker update-pricing pour configurer les coûts estimés.", + "pricing.fetched": "récupéré {time}", + "pricing.pinned": "instantané épinglé", + "pricing.source": "Source de prix", + "pricing.tier": "Niveau {tier}", + "pricing.title": "{parts}. Les étiquettes internes Codex peuvent utiliser des estimations de meilleure estimation marquées.{warning}", + "pricing.title_fetched": "{parts}. Récupéré de {url} à {time}. Les étiquettes internes Codex peuvent utiliser des estimations de meilleure estimation marquées.{warning}", + "privacy.aliases_preserved": "Les alias de projet configurés sont traités comme des opt-ins d'affichage explicites.", + "privacy.cwd_redacted": "Les chemins bruts cwd sont expurgés.", + "privacy.git_branch_hidden": "La branche Git est masquée.", + "privacy.git_remote_label_hidden": "Les étiquettes distantes Git sont masquées.", + "privacy.mode": "Mode de confidentialité des métadonnées du projet : {mode}.", + "privacy.normal_title": "Les métadonnées du projet sont affichées avec les étiquettes locales cwd, projet, branche et configurées.", + "privacy.project_names_redacted": "Les projets sans nom utilisent des étiquettes hachées stables.", + "privacy.relative_cwd_hidden": "Le cwd relatif est masqué.", + "privacy.tags_hidden": "Les balises du projet sont masquées.", + "recommendation.context_bloat.action": "Envisagez de démarrer un nouveau fil de discussion Codex si le contexte plus ancien n'est plus pertinent.", + "recommendation.context_bloat.title": "Forte pression contextuelle", + "recommendation.context_bloat.why": "Cet appel utilise une grande partie de la fenêtre contextuelle du modèle.", + "recommendation.elevated_context.action": "Vérifiez si le fil peut être rétréci avant d’ajouter du travail.", + "recommendation.elevated_context.title": "Pression contextuelle élevée", + "recommendation.elevated_context.why": "L'utilisation du contexte est élevée et peut devenir coûteuse dans les tours ultérieurs.", + "recommendation.estimated_pricing.action": "Vérifiez la couverture tarifaire et épinglez ou remplacez le tarif du modèle si cet appel est important.", + "recommendation.estimated_pricing.title": "Prix estimé", + "recommendation.estimated_pricing.why": "Ce coût utilise un mappage de modèle déduit plutôt qu'une ligne de tarification directe.", + "recommendation.high_cost.action": "Ouvrez la chronologie du fil et inspectez le tour précédent avant de continuer.", + "recommendation.high_cost.title": "Coût estimé élevé", + "recommendation.high_cost.why": "Cet appel a dépassé le seuil de coût élevé configuré.", + "recommendation.large_thread.action": "Préférez un nouveau fil de discussion pour des travaux de suivi sans rapport.", + "recommendation.large_thread.title": "Gros fil de discussion cumulatif", + "recommendation.large_thread.why": "Le total cumulé de la session est suffisamment élevé pour rendre les tours ultérieurs coûteux.", + "recommendation.low_cache.action": "Vérifiez si les fichiers, les résultats de l'outil ou le contexte général ont été réintroduits inutilement.", + "recommendation.low_cache.title": "Faible réutilisation du cache", + "recommendation.low_cache.why": "Les nouvelles entrées non mises en cache sont élevées tandis que la réutilisation du cache est faible.", + "recommendation.low_output.action": "Inspectez d’abord le contexte global ; charger le contexte brut uniquement si la cause n'est pas claire.", + "recommendation.low_output.title": "Grand appel à faible débit", + "recommendation.low_output.why": "L'appel a consommé de nombreux tokens mais a produit peu de résultats.", + "recommendation.none.action": "Aucune action globale n'est signalée ; continuer à surveiller les modèles d’utilisation.", + "recommendation.pricing_gap.action": "Mettez à jour les prix ou ajoutez un alias local avant de faire confiance aux totaux des coûts.", + "recommendation.pricing_gap.title": "Écart de prix", + "recommendation.pricing_gap.why": "Cet appel de modèle n'a pas de prix configuré, donc les coûts totaux sous-estiment l'utilisation visible.", + "recommendation.reasoning_spike.action": "Vérifiez si cette tâche nécessite l’effort de raisonnement sélectionné.", + "recommendation.reasoning_spike.title": "Part de raisonnement élevée", + "recommendation.reasoning_spike.why": "La sortie de raisonnement domine la sortie visible pour cet appel.", + "recommendation.subagent_attribution.action": "Comparez les appels directs avec le sous-agent attaché ou examinez les appels avant de modifier le flux de travail.", + "recommendation.subagent_attribution.title": "Attribution des sous-agents", + "recommendation.subagent_attribution.why": "Cet appel est attaché au travail délégué et peut expliquer la croissance du thread parent.", + "section.allowance": "Allocation", + "section.needs_attention": "A besoin d'attention", + "section.pricing": "Tarifs", + "section.recommendations": "Recommandations", + "severity.high": "Élevé", + "severity.medium": "Moyen", + "severity.review": "Examen", + "source.auto_review": "Révision automatique", + "source.codex_initiated": "Codex lancé", + "source.subagent": "Sous-agent", + "source.subagent_role": "Sous-agent : {role}", + "source.user": "Utilisateur", + "source.user_initiated": "Initié par l'utilisateur", + "state.allowance_config_error": "Erreur de configuration des allocations", + "state.allowance_configured": "Allocation configurée", + "state.best_guess_estimate": "Estimation la plus probable", + "state.configured": "Configuré", + "state.configured_price": "Prix configuré", + "state.error": "Erreur", + "state.estimated": "Estimé", + "state.loading": "Chargement", + "state.loading_rows": "Chargement des lignes", + "state.mixed": "Mixte", + "state.no": "Non", + "state.no_calls": "Aucun appel ne correspond aux filtres actuels.", + "state.no_configured_price": "Aucun prix configuré", + "state.no_context_entries": "Aucune entrée de contexte trouvée pour cet appel.", + "state.no_data": "Aucune donnée", + "state.no_mapped_rate": "Aucun tarif cartographié", + "state.no_price": "Pas de prix", + "state.no_rate": "Pas de tarif", + "state.no_rows": "Aucune ligne", + "state.no_threads": "Aucun sujet ne correspond aux filtres actuels.", + "state.none": "Aucun", + "state.not_configured": "Non configuré", + "state.requires_evidence": "Preuves nécessaires", + "state.unknown": "Inconnu", + "state.yes": "Oui", + "status.checking": "Vérification", + "status.paused": "En pause", + "status.refresh_error": "Erreur d'actualisation", + "status.refreshing": "Rafraîchissant", + "status.reloading": "Rechargement", + "status.static": "Statique", + "status.updated": "Mis à jour", "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", + "table.cached": "En cache", + "table.calls": "appels", + "table.cost": "Coût", "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", + "table.initiated": "Initié", + "table.last_call": "Dernier appel", + "table.model": "Modèle", "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", + "table.more_models": "Modèles {model} +{count}", + "table.output": "Sortie", + "table.page_status": "{start}-{end} de {total} {items} · page {page}/{pages}", + "table.rows": "lignes", + "table.signals": "Signaux", "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", + "table.thread": "Sujet", + "table.threads": "fils de discussion", + "table.time": "Temps", "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", + "table.uncached": "Non mis en cache", + "table.visible_status": "Affichage de {end} sur {total} {items}", + "thread.attached": "ci-joint", "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", + "thread.auto_review": "{count} révision automatique", + "thread.collapse": "Réduire", "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", + "thread.expand": "Développer", + "thread.expand_label": "{action} {thread} appelle. Score d'attention {score}.", + "thread.explicit_parent": "parent explicite", + "thread.explicit_parent_thread": "fil parent explicite", "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "thread.session": "séance", + "thread.spawned": "engendré", + "thread.spawned_from": "engendré à partir de {thread}", + "thread.spawned_threads": "{count} fils de discussion générés", + "thread.subagent": "Sous-agent {count}", + "thread.unknown": "Sujet inconnu", + "thread.unmatched_subagent": "sous-agent inégalé" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/it.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/it.json index ef2ecf4..9ff9907 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/it.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/it.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "Verificare la presenza di file reintrodotti o output dello strumento dopo l'interruzione del riutilizzo della cache.", + "action.compare_fresh_input": "Confronta il nuovo input con il turno precedente prima di continuare.", + "action.compare_subagent_calls": "Confronta il subagente collegato o esamina le chiamate prima di modificare il flusso di lavoro principale.", + "action.configure_pricing": "Configura i prezzi prima di fidarti dei totali dei costi.", + "action.copied": "Copiato", + "action.copy_failed": "Copia non riuscita", + "action.expand_or_select_recommendations": "Espandi le chiamate o seleziona una riga per i consigli a livello di chiamata.", + "action.exported": "Esportato {count}", + "action.inspect_thread_timeline": "Esamina la sequenza temporale del thread e valuta la possibilità di iniziare un nuovo thread.", + "action.review_context_growth": "Rivedi dove inizia la crescita del contesto e valuta la possibilità di avviare un nuovo thread.", + "action.review_reasoning_effort": "Verificare se lo sforzo di ragionamento è appropriato per questo compito.", + "action.run": "Corri", + "action.set_limits": "Stabilisci dei limiti", + "action.use_aggregate_first": "Utilizza prima i campi aggregati; caricare il contesto solo se il segnale non è ancora chiaro.", + "allowance.counted": "{value} crediti conteggiati ai fini dei limiti di utilizzo di Codex", + "allowance.cr_left": "{value} cr a sinistra", + "allowance.credit_coverage": "Copertura del credito {ratio} dei token caricati.", + "allowance.credit_rates": "Tassi di credito: {source}.", + "allowance.credits_remaining": "{value} crediti rimanenti", + "allowance.init_hint": "Esegui codex-usage-tracker init-allowance per aggiungere le finestre di utilizzo rimanenti.", + "allowance.of_allowance": "{ratio} di indennità", + "allowance.of_total": "{used} di {total} crediti", + "allowance.rate_card_error": "Errore tariffario: {error}", + "allowance.remaining": "{value} rimanente", + "allowance.resets": "Ripristina: {resets}", + "allowance.row_no_rate": "Nessun tasso di credito Codex mappato", + "allowance.title_hint": "Aggiungi ~/.codex-usage-tracker/allowance.json per mostrare 5 ore e l'utilizzo rimanente settimanale.", + "allowance.used_vs_remaining": "{used} utilizzato rispetto a {remaining} rimanente", + "allowance.window_configured": "{label} configurato", + "allowance.windows": "Finestre di indennità: {windows}", + "aria.current_view_actions": "Azioni di visualizzazione correnti", + "aria.dashboard_status": "Stato del dashboard", + "aria.dashboard_view": "Visualizzazione dashboard", + "aria.history_title": "Solo le sessioni attive è l'impostazione predefinita. Tutta la cronologia esegue la scansione dei registri delle sessioni archiviate durante l'aggiornamento in tempo reale.", + "aria.inspect_thread": "Controlla l'utilizzo di {thread}", + "aria.refresh_controls": "Controlli di aggiornamento del dashboard", + "aria.table_pages": "Pagine della tabella", + "badge.costs": "Costi", + "badge.credits": "Crediti", + "badge.live": "Vivi", + "badge.metadata_mode": "Metadati {mode}", + "badge.metadata_normal": "Metadati normali", + "badge.no_costs": "Nessun costo", + "badge.parser_warnings": "Avvisi del parser", + "badge.static": "Statico", + "badge.unofficial_project": "Progetto non ufficiale", + "badge.unofficial_project_title": "Codex Usage Tracker è indipendente e non è creato, affiliato, approvato, sponsorizzato o supportato da OpenAI. OpenAI e Codex sono marchi di OpenAI.", + "button.back_to_dashboard": "Torna al cruscotto", + "button.clear": "Chiaro", + "button.copy_link": "Copia collegamento", + "button.enable_context_loading": "Abilita il caricamento del contesto", + "button.export_csv": "Esporta CSV", + "button.full_serialized_analysis": "Esegui l'analisi serializzata completa", + "button.hide_details": "Nascondi dettagli", + "button.hide_tool_output": "Nascondi l'output dello strumento", + "button.include_tool_output": "Includi l'output dello strumento", + "button.load_context": "Carica contesto", + "button.load_more": "Carica di più", + "button.load_older_context": "Carica le voci più vecchie", + "button.next": "Avanti", + "button.next_call": "Prossima chiamata", + "button.no_char_limit": "Nessun limite di caratteri", + "button.open_investigator": "Investigatore aperto", + "button.previous": "Precedente", + "button.previous_call": "Chiamata precedente", + "button.refresh": "Aggiorna", + "button.show_compaction_history": "Mostra sostituzione compattata", + "button.show_tool_output": "Mostra l'output dello strumento", + "button.show_turn_evidence": "Mostra le prove del registro delle svolte", + "button.top": "In alto", + "call.cache_accounting_delta": "Delta cache/contabile", + "call.cache_cold": "Curriculum a freddo/cache obsoleta", + "call.cache_diagnostics": "Diagnostica della cache", + "call.cache_partial": "Mancata cache parziale", + "call.cache_spike": "Picco non memorizzato nella cache", + "call.cache_steady": "Profilo cache stabile", + "call.cache_warm": "Riutilizzo della cache a caldo", + "call.compaction_diagnostics": "Diagnostica della compattazione", + "call.compaction_hint": "Le prove caricate possono mostrare eventi di compattazione espliciti. La cronologia delle sostituzioni redatta viene visualizzata solo dopo l'azione di sostituzione compattata.", + "call.context_estimate": "Stima del cambiamento di contesto", + "call.context_estimate_hint": "Confronta l'esatto input non memorizzato nella cache con le prove di log visibili conteggiate tramite tokenizzatore. Il divario deve essere trattato come un'impalcatura nascosta, una serializzazione o un errore di stima del tokenizzatore.", + "call.derived_label": "Derivato da chiamate aggregate adiacenti", + "call.estimated_label": "Stima dal volume di log visibile", + "call.evidence_label": "Prove di runtime", + "call.exact_accounting": "Contabilità dei token esatta", + "call.exact_label": "Esatto dalla richiamata del token", + "call.hidden_estimate": "Stima dell'input nascosto/serializzato inspiegabile", + "call.no_previous": "Nessuna chiamata precedente in questo thread risolto.", + "call.not_found": "La chiamata selezionata non è stata trovata nelle righe del dashboard caricato.", + "call.open_hint": "Fare clic su una riga di chiamata per una diagnostica approfondita.", + "call.position": "Chiama {position} in questo thread risolto.", + "call.post_compaction": "Possibile postcompattazione", + "call.raw_evidence": "Prove crude", + "call.remaining_after_serialized": "Rimanente dopo il limite serializzato", + "call.remaining_after_serialized_hint": "Input non memorizzato nella cache non coperto nemmeno dal limite superiore serializzato", + "call.serialized_bound_hint": "Struttura JSONL locale con limite superiore; testo del prompt non esatto.", + "call.serialized_breakdown": "Gruppi di evidenza serializzata", + "call.serialized_bucket_detail": "{count} campi · {chars} car", + "call.serialized_candidate": "Possibile sovraccarico serializzato", + "call.serialized_candidate_hint": "Limite superiore serializzato meno stima visibile, limitato dall'input esatto non memorizzato nella cache", + "call.serialized_deferred": "Stima rapida caricata; l'analisi completa dei gruppi serializzati è rinviata.", + "call.serialized_quick_hint": "preventivo veloce", + "call.serialized_upper_bound": "Limite superiore locale serializzato", + "call.visible_estimate": "Visibile nuova stima del contesto", + "call.visible_gap": "Input non memorizzato nella cache meno stima visibile", + "caption.ascending": "ascendente", + "caption.call_investigator": "Indagando sulla chiamata {record}.", + "caption.calls": "Visualizzazione delle chiamate dei singoli modelli ordinate per {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "discendente", + "caption.initial_calls": "Visualizzazione delle chiamate dei singoli modelli.", + "caption.insights": "Classificato in base a costo, crediti di utilizzo, riutilizzo della cache, pressione del contesto e affidabilità dei prezzi.", + "caption.loaded": "{loaded} chiamate caricate", + "caption.loaded_capped": "{loaded} di {available} chiamate caricate", + "caption.rows_loaded_progress": "Righe caricate: {loaded} di {total}", + "caption.rows_loading_background": "I totali della dashboard sono pronti. Le righe vengono caricate in background.", + "caption.rows_loading_progress": "Caricamento righe: {loaded} di {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "Visualizzazione dei thread {threads} dalle chiamate filtrate {calls}, ordinati per {sort}. {loaded}. Fare clic su un thread per espanderne le chiamate.", + "context.api_http": "Il contesto API ha restituito HTTP {status}.", + "context.api_unavailable": "Il contesto API non è disponibile qui. Esegui codex-usage-tracker serve-dashboard --open per il caricamento del contesto su richiesta.", + "context.auto_loading": "Caricamento delle prove della svolta selezionata con output dello strumento incluso.", + "context.chars_omitted": "{count} caratteri oltre il budget omessi.", + "context.compaction_detected": "Rilevata compattazione", + "context.compaction_replacement": "Contesto di sostituzione compattato", + "context.compaction_replacement_count": "{count} voci della cronologia delle sostituzioni disponibili.", + "context.disabled_hint": "Il caricamento del contesto è disattivato per questo server dashboard. Abilitalo qui per caricare il contesto JSONL locale su richiesta.", + "context.enabled_note": "Il caricamento del contesto è abilitato. Premi Mostra prova registro svolte per leggere questa chiamata dalla sorgente JSONL locale.", + "context.file_hint": "Apri questa dashboard con codex-usage-tracker serve-dashboard per caricare il contesto non elaborato su richiesta.", + "context.line": "riga {line}", + "context.loading": "Caricamento contesto locale...", + "context.local_redacted": "Contesto locale JSONL caricato su richiesta. Le richieste e l'output dello strumento vengono oscurati in base ai modelli segreti comuni e non vengono mantenuti in SQLite o nel dashboard HTML.", + "context.no_char_limit_active": "Nessun limite di caratteri applicato.", + "context.no_record_id": "Questa riga non ha un ID record per la ricerca del contesto.", + "context.no_response": "Nessun corpo di risposta", + "context.older_omitted": "{count} voci precedenti omesse.", + "context.ready_hint": "Il contesto non è incorporato in questa dashboard. Premi un pulsante per leggere questa chiamata dalla sorgente JSONL locale.", + "context.settings_http": "Le impostazioni del contesto hanno restituito HTTP {status}.", + "context.source": "Fonte: {file}:{line}", + "context.token_breakdown": "Ripartizione dei token", + "context.token_cached": "Memorizzato nella cache", + "context.token_input": "Ingresso", + "context.token_output": "Uscita", + "context.token_reasoning": "Ragionamento", + "context.token_required": "Il caricamento del contesto richiede un token localhost dashboard API.", + "context.token_scope_call": "Questa chiamata", + "context.token_scope_earlier": "Conteggio dei token precedente nello stesso turno", + "context.token_scope_previous": "Conteggio dei token precedenti nello stesso turno", + "context.token_scope_selected": "Conteggio token chiamata selezionato", + "context.token_scope_session": "Sessione cumulativa", + "context.token_total": "Totale", + "context.token_type": "Digitare", + "context.token_uncached": "Non memorizzato nella cache", + "context.tool_included": "Output dello strumento incluso con limiti di redazione e dimensione.", + "context.tool_omitted": "Output dello strumento nascosto per questa vista.", + "credit.configured_rate": "Tariffa configurata", + "credit.estimated_mapping": "Mappatura stimata", + "credit.inferred_mapping": "Mappatura del modello dedotto", + "credit.no_mapped_rate": "Nessuna tariffa mappata", + "credit.no_rate": "Nessun tasso di credito", + "credit.official_match": "Partita ufficiale del tariffario", + "credit.user_rate": "Tasso di credito fornito dall'utente", + "credit.with_status": "{value} crediti · {status}", + "dashboard.call_details": "Dettagli chiamata", + "dashboard.detail.empty": "Passa il mouse o fai clic su una riga per esaminare i campi di utilizzo aggregato.", + "dashboard.eyebrow": "Analisi Codex locale", + "dashboard.local_storage_note": "L'intestazione della dashboard ricorda anche la lingua scelta a livello locale.", + "dashboard.model_calls": "Chiamate di modelli", + "dashboard.title": "Pannello di utilizzo", + "dashboard.top_threads_by_attention": "Discussioni principali per punteggio di attenzione", + "dashboard.view.call": "Chiama l'investigatore", + "dashboard.view.calls": "Chiamate", + "dashboard.view.insights": "Approfondimenti", + "dashboard.view.threads": "Discussioni", + "date.custom": "Personalizzato", + "date.invalid_range": "Intervallo di date non valido", + "date.range_between": "da {prefix} da {start} a {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", + "date.range_from": "{prefix} da {start}", + "date.range_through": "da {prefix} a {end}", + "detail.agent_nickname": "Soprannome dell'agente", + "detail.agent_role": "Ruolo dell'agente", + "detail.allowance_impact": "Impatto dell'indennità", + "detail.attached_calls": "Chiamate allegate", + "detail.auto_review_calls": "Chiamate con revisione automatica", + "detail.cache_savings": "Risparmio nella cache", + "detail.call_number": "chiama {number}", + "detail.calls": "Chiamate", + "detail.context_window": "Finestra di contesto", + "detail.cost_usage_context": "Costo, utilizzo e contesto", + "detail.credit_confidence": "Fiducia nel credito", + "detail.credit_model": "Modello di credito", + "detail.credit_note": "Nota di credito", + "detail.credit_source": "Fonte del credito", + "detail.credit_source_fetched": "Fonte del credito recuperata", + "detail.credit_tier": "Livello di credito", "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", + "detail.efficiency_signals": "Segnali di efficienza", + "detail.first_expensive_turn": "Primo turno costoso", + "detail.git_branch": "Ramo Git", + "detail.largest_cumulative_jump": "Il più grande salto cumulativo", + "detail.latest_activity": "Ultima attività", + "detail.model_mix": "Miscela di modelli", + "detail.next_action": "Azione successiva", + "detail.no_above_thresholds": "Nessuno sopra le soglie", + "detail.no_aggregate_action": "Nessuna azione aggregata è contrassegnata.", + "detail.parent_session": "Sessione genitoriale", + "detail.parent_thread": "Discussione principale", + "detail.parent_updated": "Genitore aggiornato", + "detail.pricing_model": "Modello di prezzo", + "detail.pricing_status": "Stato dei prezzi", + "detail.project_cwd": "Progetto CWD", + "detail.project_tags": "Tag del progetto", + "detail.raw_identifiers": "Identificatori aggregati grezzi", + "detail.reasoning_mix": "Miscela di ragionamenti", + "detail.relationships": "Relazioni", + "detail.remote_hash": "Hash remoto", + "detail.remote_label": "Etichetta remota", + "detail.secondary_thread_fields": "Campi del thread secondario", + "detail.source_file_line": "File e riga di origine", + "detail.source_line": "Linea di origine", + "detail.spawned_child_calls": "Chiamate dei bambini generati", + "detail.spawned_from": "Generato da", + "detail.spawned_threads": "Discussioni generate", + "detail.subagent_before_spike": "Subagente prima del picco", + "detail.subagent_calls": "Il subagente chiama", + "detail.subagent_type": "Tipo di agente secondario", + "detail.thread_attachment": "Allegato filo", + "detail.thread_attention_summary": "Riepilogo dell'attenzione del thread", + "detail.thread_lifecycle": "Ciclo di vita del thread", + "detail.thread_narrative": "Narrazione del filo", + "detail.thread_source": "Fonte della discussione", + "detail.thread_timeline": "Cronologia del thread", + "detail.timeline_context": "contesto {value}", + "detail.timeline_empty": "Nessuna chiamata in questo thread.", + "detail.timeline_meta": "{tokens} token · {cost} · {credits} · cache {cache}", "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", + "detail.token_pricing_breakdown": "Scomposizione dei token e dei prezzi", + "detail.tokens_at": "Token {tokens} presso {time}", + "detail.turn": "Girare", + "detail.why_flagged": "Perché segnalato", + "docs.dashboard_guide": "Guida al cruscotto", + "effort.high": "alto", + "effort.low": "basso", + "effort.medium": "medio", + "filter.confidence": "Fiducia", + "filter.effort": "Ragionamento", + "filter.end": "Fine", + "filter.model": "Modello", + "filter.project": "Progetto", + "filter.reasoning": "Ragionamento", + "filter.search": "Cerca", + "filter.search_placeholder": "Filo, cwd, modello", + "filter.session": "Sessione", + "filter.sort": "Ordina", + "filter.start": "Inizia", + "filter.thread": "Filo", + "filter.time": "Tempo", + "flag.elevated_context_use": "Utilizzo del contesto elevato", + "flag.expensive_low_output_call": "Chiamata costosa a basso rendimento", + "flag.high_context_use": "Utilizzo ad alto contesto", + "flag.high_estimated_cost": "Costo stimato elevato", + "flag.high_reasoning_share": "Quota di ragionamento elevata", + "flag.low_cache_reuse": "Riutilizzo della cache ridotto", + "history.active_hidden": "Solo sessioni attive; {count} chiamate archiviate nascoste", + "history.active_only": "Solo sessioni attive", + "history.all_empty": "Tutta la cronologia selezionata; nessuna chiamata archiviata è ancora indicizzata", + "history.all_includes": "Tutta la cronologia include {count} chiamate archiviate", + "history.archived_scan_hint": "{detail}. Le sessioni archiviate vengono scansionate solo quando tutta la cronologia è selezionata durante l'aggiornamento in tempo reale.", + "insight.apply_cache_misses": "Applica la preimpostazione degli errori di cache", + "insight.apply_context_bloat": "Applica il preset di gonfiamento del contesto", + "insight.codex_allowance_usage": "Codex utilizzo dell'indennità", + "insight.context_bloat": "Il contesto è gonfio", + "insight.context_bloat_body": "Le chiamate {calls} hanno un utilizzo del contesto pari o superiore a {ratio}.", + "insight.costliest_thread": "Discussione più costosa", + "insight.costliest_thread_body": "{thread} ha chiamate {calls} e token {tokens}.", + "insight.credit_coverage_body": "{ratio} dei token visibili corrisponde a Codex tassi di credito.", + "insight.estimated_pricing": "Prezzo stimato", + "insight.estimated_pricing_body": "I prezzi indicati come migliori sono inclusi, ma devono essere esaminati separatamente.", + "insight.inspect_selected_call": "Esamina la chiamata selezionata", + "insight.low_cache_reuse": "Riutilizzo della cache ridotto", + "insight.low_cache_reuse_body": "Le chiamate {calls} sono in fase di riutilizzo della cache {ratio}. Inizia con {thread}.", + "insight.open_thread_timeline": "Apri la sequenza temporale del thread", + "insight.reasoning_output_spike": "Picco di output del ragionamento", + "insight.reasoning_spike_body": "{thread} ha la chiamata di output del ragionamento più grande nel filtro corrente.", + "insight.review_estimates": "Rivedere le stime", + "insight.review_highest_credit": "Esamina le chiamate con il credito più elevato", + "insight.review_pricing_gaps": "Esaminare le lacune di prezzo", + "insight.unpriced_usage": "Utilizzo senza prezzo", + "insight.unpriced_usage_body": "Questi token vengono omessi dai totali dei costi stimati finché non viene configurato il prezzo.", + "language.english": "Inglese", + "language.label": "Lingua", "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "live.checking_usage": "Verifica del nuovo utilizzo in corso...", + "live.every": "Aggiornamento in tempo reale ogni {seconds}s", + "live.history_static_hint": "Esegui codex-usage-tracker serve-dashboard per passare tra le sessioni attive e tutta la cronologia dalla dashboard.", + "live.indexed": "Righe aggregate {rows} indicizzate dai log {files}.", + "live.load_static_hint": "Esegui codex-usage-tracker serve-dashboard per caricare una dimensione di cronologia diversa dalla dashboard.", + "live.loading_rows": "Caricamento righe in background...", + "live.paused": "Aggiornamento in tempo reale in pausa", + "live.refresh_suffix": ". Ricarica questa pagina dopo aver rigenerato un dashboard statico oppure esegui codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "Aggiornamento in tempo reale non disponibile: {message}{suffix}", + "live.refreshing_index": "Aggiornamento dell'indice di utilizzo locale in corso...", + "live.reloading_static": "Ricaricamento dell'istantanea della dashboard statica in corso...", + "live.skipped": "Eventi di conteggio token con formato errato {count} ignorati.", + "live.updated_detail": "Aggiornato {time}. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "Punteggio di attenzione", + "metric.cache_ratio": "Rapporto cache", + "metric.cache_trend": "Tendenza della cache", + "metric.cached_input": "Ingresso memorizzato nella cache", + "metric.codex_credits": "Codex crediti", + "metric.context_trend": "Andamento del contesto", + "metric.context_use": "Uso del contesto", + "metric.estimated_cost": "Costo stimato", + "metric.input_tokens": "Token di input", + "metric.last_call_input": "Ingresso dell'ultima chiamata", + "metric.last_call_total": "Totale ultima chiamata", + "metric.max_context_use": "Utilizzo massimo del contesto", + "metric.output": "Uscita", + "metric.output_tokens": "Token di uscita", + "metric.reasoning_output": "Uscita del ragionamento", + "metric.remaining_usage": "Utilizzo rimanente", + "metric.session_cumulative": "Sessione cumulativa", + "metric.total": "Totale", + "metric.total_tokens": "Token totali", + "metric.uncached_input": "Ingresso non memorizzato nella cache", + "metric.usage_credits": "Crediti di utilizzo", + "metric.usage_remaining": "Utilizzo rimanente", + "metric.visible_calls": "Chiamate visibili", + "nav.history": "Storia", + "nav.live": "Vivi", + "nav.load": "Caricare", + "option.active_sessions_only": "Solo sessioni attive", + "option.all_confidence": "Tutta fiducia", + "option.all_efforts": "Tutti gli sforzi", + "option.all_history": "Tutta la storia", + "option.all_models": "Tutti i modelli", + "option.all_time": "Tutto il tempo", + "option.custom_range": "Gamma personalizzata", + "option.estimated_cost": "Costo stimato", + "option.estimated_credit_mapping": "Mappatura del credito stimato", + "option.exact_cost": "Costo esatto", + "option.exact_credit_rate": "Tasso di credito esatto", + "option.highest_codex_credits": "Crediti Codex più alti", + "option.highest_context_use": "Utilizzo del contesto più elevato", + "option.highest_estimated_cost": "Costo stimato più alto", + "option.last_7_days": "Ultimi 7 giorni", + "option.load_10000": "10.000 chiamate", + "option.load_20000": "20.000 chiamate", + "option.load_5000": "5.000 chiamate", + "option.load_all": "Tutte le chiamate", + "option.lowest_cache_ratio": "Rapporto cache più basso", + "option.missing_credit_rate": "Tasso di credito mancante", + "option.most_signals": "La maggior parte dei segnali", + "option.most_tokens": "La maggior parte dei token", + "option.needs_attention": "Ha bisogno di attenzione", + "option.newest_calls": "Chiamate più recenti", + "option.this_month": "Questo mese", + "option.this_week": "Questa settimana", + "option.thread_name": "Nome del thread", + "option.today": "Oggi", + "option.unpriced_cost": "Costo impagabile", + "option.user_credit_override": "Sostituzione del credito utente", + "parser.warnings_title": "L'ultimo aggiornamento ha riportato la diagnostica del parser {count}: {entries}. Esegui codex-usage-tracker inspect-log per esaminare la deriva dello schema.", + "preset.cache_misses": "La cache manca", + "preset.cache_misses_caption": "La cache non contiene preimpostazioni", + "preset.cache_misses_desc": "Chiamate con rapporto cache basso raggruppate per cwd, modello e thread.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", + "preset.context_bloat": "Il contesto è gonfio", + "preset.context_bloat_caption": "Preimpostazione rigonfiamento del contesto", + "preset.context_bloat_desc": "Chiamate con utilizzo del contesto superiore al 60% o con token cumulativi molto elevati.", + "preset.description": "Punti di partenza con un clic per domande sull'utilizzo comune.", + "preset.estimated_price_review": "Revisione del prezzo stimato", + "preset.estimated_price_review_caption": "Preimpostazione revisione prezzo stimato", + "preset.estimated_price_review_desc": "Prezzo di utilizzo con stime approssimative contrassegnate.", + "preset.highest_codex_credits": "Crediti Codex più alti", + "preset.highest_codex_credits_caption": "Crediti Codex più alti preimpostati", + "preset.highest_codex_credits_desc": "Chiamate ordinate in base all'impatto stimato sul limite di utilizzo Codex.", + "preset.highest_cost_threads": "Discussioni più costose", + "preset.highest_cost_threads_caption": "Preimpostazione dei thread più costosi", + "preset.highest_cost_threads_desc": "Discussioni ordinate per spesa stimata, con agenti secondari allegati.", + "preset.investigation_presets": "Preimpostazioni di indagine", + "preset.no_preset": "Nessuna preimpostazione applicata.", + "preset.pricing_gaps": "Divari di prezzo", + "preset.pricing_gaps_caption": "Divari di prezzo preimpostati", + "preset.pricing_gaps_desc": "Utilizzo senza prezzo che rende incompleti i totali dei costi stimati.", + "pricing.configure_hint": "Esegui codex-usage-tracker update-pricing per configurare i costi stimati.", + "pricing.fetched": "recuperato {time}", + "pricing.pinned": "istantanea appuntata", + "pricing.source": "Fonte dei prezzi", + "pricing.tier": "{tier} livello", + "pricing.title": "{parts}. Le etichette interne Codex possono utilizzare le migliori stime contrassegnate.{warning}", + "pricing.title_fetched": "{parts}. Recuperato da {url} alle {time}. Le etichette interne Codex possono utilizzare le migliori stime contrassegnate.{warning}", + "privacy.aliases_preserved": "Gli alias di progetto configurati vengono trattati come opzioni di visualizzazione esplicite.", + "privacy.cwd_redacted": "I percorsi CWD non elaborati vengono oscurati.", + "privacy.git_branch_hidden": "Il ramo Git è nascosto.", + "privacy.git_remote_label_hidden": "Le etichette remote di Git sono nascoste.", + "privacy.mode": "Modalità privacy dei metadati del progetto: {mode}.", + "privacy.normal_title": "I metadati del progetto vengono visualizzati con etichette cwd, progetto, ramo e configurate locali.", + "privacy.project_names_redacted": "I progetti senza nome utilizzano etichette con hash stabili.", + "privacy.relative_cwd_hidden": "Il cwd relativo è nascosto.", + "privacy.tags_hidden": "I tag del progetto sono nascosti.", + "recommendation.context_bloat.action": "Considera l'idea di avviare un nuovo thread Codex se il contesto precedente non è più rilevante.", + "recommendation.context_bloat.title": "Alta pressione del contesto", + "recommendation.context_bloat.why": "Questa chiamata utilizza gran parte della finestra di contesto del modello.", + "recommendation.elevated_context.action": "Controlla se il thread può essere ristretto prima di aggiungere altro lavoro.", + "recommendation.elevated_context.title": "Elevata pressione del contesto", + "recommendation.elevated_context.why": "L’uso del contesto è elevato e potrebbe diventare costoso nei turni successivi.", + "recommendation.estimated_pricing.action": "Controlla la copertura dei prezzi e blocca o sostituisci la tariffa del modello se la chiamata è importante.", + "recommendation.estimated_pricing.title": "Prezzo stimato", + "recommendation.estimated_pricing.why": "Questo costo utilizza una mappatura del modello dedotto anziché una riga di prezzo diretta.", + "recommendation.high_cost.action": "Apri la sequenza temporale del thread e controlla la svolta precedente prima di continuare.", + "recommendation.high_cost.title": "Costo stimato elevato", + "recommendation.high_cost.why": "Questa chiamata ha superato la soglia di costo elevato configurata.", + "recommendation.large_thread.action": "Preferire un nuovo thread per lavori di follow-up non correlati.", + "recommendation.large_thread.title": "Thread cumulativo di grandi dimensioni", + "recommendation.large_thread.why": "Il totale cumulativo della sessione è sufficientemente elevato da rendere costosi i turni successivi.", + "recommendation.low_cache.action": "Controlla se i file, l'output dello strumento o il contesto ampio sono stati reintrodotti inutilmente.", + "recommendation.low_cache.title": "Riutilizzo della cache ridotto", + "recommendation.low_cache.why": "L'input fresco non memorizzato nella cache è elevato mentre il riutilizzo della cache è basso.", + "recommendation.low_output.action": "Ispezionare prima il contesto aggregato; caricare il contesto non elaborato solo se la causa non è chiara.", + "recommendation.low_output.title": "Chiamata estesa a basso rendimento", + "recommendation.low_output.why": "La chiamata ha consumato molti token ma ha prodotto poco output.", + "recommendation.none.action": "Nessuna azione aggregata è contrassegnata; continuare a monitorare i modelli di utilizzo.", + "recommendation.pricing_gap.action": "Aggiorna i prezzi o aggiungi un alias locale prima di fidarti dei totali dei costi.", + "recommendation.pricing_gap.title": "Divario di prezzo", + "recommendation.pricing_gap.why": "Questa chiamata modello non ha un prezzo configurato, quindi i costi totali sottostimano l'utilizzo visibile.", + "recommendation.reasoning_spike.action": "Verifica se questa attività richiede lo sforzo di ragionamento selezionato.", + "recommendation.reasoning_spike.title": "Quota di ragionamento elevata", + "recommendation.reasoning_spike.why": "L'output del ragionamento domina l'output visibile per questa chiamata.", + "recommendation.subagent_attribution.action": "Confronta le chiamate dirette con il subagente collegato o rivedi le chiamate prima di modificare il flusso di lavoro.", + "recommendation.subagent_attribution.title": "Attribuzione subagente", + "recommendation.subagent_attribution.why": "Questa chiamata è allegata al lavoro delegato e potrebbe spiegare la crescita del thread principale.", + "section.allowance": "Indennità", + "section.needs_attention": "Ha bisogno di attenzione", + "section.pricing": "Prezzi", + "section.recommendations": "Raccomandazioni", + "severity.high": "Alto", + "severity.medium": "Medio", + "severity.review": "Recensione", + "source.auto_review": "Revisione automatica", + "source.codex_initiated": "Codex avviato", + "source.subagent": "Subagente", + "source.subagent_role": "Subagente: {role}", + "source.user": "Utente", + "source.user_initiated": "Avviato dall'utente", + "state.allowance_config_error": "Errore di configurazione dell'indennità", + "state.allowance_configured": "Indennità configurata", + "state.best_guess_estimate": "Stima migliore", + "state.configured": "Configurato", + "state.configured_price": "Prezzo configurato", + "state.error": "Errore", + "state.estimated": "Stima", + "state.loading": "Caricamento", + "state.loading_rows": "Caricamento righe", + "state.mixed": "Misto", "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", + "state.no_calls": "Nessuna chiamata corrisponde ai filtri attuali.", + "state.no_configured_price": "Nessun prezzo configurato", + "state.no_context_entries": "Nessuna voce di contesto trovata per questa chiamata.", + "state.no_data": "Nessun dato", + "state.no_mapped_rate": "Nessuna tariffa mappata", + "state.no_price": "Nessun prezzo", + "state.no_rate": "Nessuna tariffa", + "state.no_rows": "Nessuna riga", + "state.no_threads": "Nessun thread corrisponde ai filtri attuali.", + "state.none": "Nessuno", + "state.not_configured": "Non configurato", + "state.requires_evidence": "Servono prove", + "state.unknown": "Sconosciuto", + "state.yes": "Sì", + "status.checking": "Controllo", + "status.paused": "In pausa", + "status.refresh_error": "Errore di aggiornamento", + "status.refreshing": "Rinfrescante", + "status.reloading": "Ricaricamento", + "status.static": "Statico", + "status.updated": "Aggiornato", "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "table.cached": "Memorizzato nella cache", + "table.calls": "chiamate", + "table.cost": "Costo", + "table.effort": "Sforzo", + "table.initiated": "Iniziato", + "table.last_call": "Ultima chiamata", + "table.model": "Modello", + "table.more_efforts": "{effort} +{count} sforzi", + "table.more_models": "{model} +{count} modelli", + "table.output": "Uscita", + "table.page_status": "{start}-{end} di {total} {items} · pagina {page}/{pages}", + "table.rows": "righe", + "table.signals": "Segnali", + "table.source": "Fonte", + "table.thread": "Filo", + "table.threads": "discussioni", + "table.time": "Tempo", + "table.tokens": "Token", + "table.uncached": "Non memorizzato nella cache", + "table.visible_status": "Mostra {end} di {total} {items}", + "thread.attached": "allegato", + "thread.attention": "attenzione {score}", + "thread.auto_review": "{count} revisione automatica", + "thread.collapse": "Crollo", + "thread.direct": "diretto", + "thread.expand": "Espandi", + "thread.expand_label": "{action} {thread} chiama. Punteggio di attenzione {score}.", + "thread.explicit_parent": "genitore esplicito", + "thread.explicit_parent_thread": "thread genitore esplicito", + "thread.parent": "Genitore {id}", + "thread.session": "sessione", + "thread.spawned": "generato", + "thread.spawned_from": "generato da {thread}", + "thread.spawned_threads": "{count} hanno generato discussioni", + "thread.subagent": "{count} subagente", + "thread.unknown": "Discussione sconosciuta", + "thread.unmatched_subagent": "subagente senza eguali" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/ja.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/ja.json index ef2ecf4..8fed77e 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/ja.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/ja.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "キャッシュの再利用が削除された後に再導入されたファイルまたはツールの出力を確認します。", + "action.compare_fresh_input": "続行する前に、新しい入力を前のターンと比較してください。", + "action.compare_subagent_calls": "親ワークフローを変更する前に、接続されたサブエージェントを比較するか、呼び出しを確認してください。", + "action.configure_pricing": "コストの合計を信頼する前に、価格を設定してください。", + "action.copied": "コピーされました", + "action.copy_failed": "コピーに失敗しました", + "action.expand_or_select_recommendations": "通話を展開するか、通話レベルの推奨事項の行を選択します。", + "action.exported": "{count} をエクスポートしました", + "action.inspect_thread_timeline": "スレッドのタイムラインを調べて、新しいスレッドを開始することを検討してください。", + "action.review_context_growth": "コンテキストの増加がどこから始まるかを確認し、新しいスレッドを開始することを検討してください。", + "action.review_reasoning_effort": "推論の労力がこのタスクに適切かどうかを検討してください。", + "action.run": "走る", + "action.set_limits": "制限を設定する", + "action.use_aggregate_first": "最初に集計フィールドを使用します。信号がまだ不明瞭な場合にのみコンテキストを読み込みます。", + "allowance.counted": "{value} クレジットは Codex の使用制限にカウントされます", + "allowance.cr_left": "{value} cr 残り", + "allowance.credit_coverage": "ロードされたトークンの信用範囲 {ratio}。", + "allowance.credit_rates": "信用レート: {source}。", + "allowance.credits_remaining": "{value} クレジットが残っています", + "allowance.init_hint": "codex-usage-tracker init-allowance を実行して、残りの使用期間を追加します。", + "allowance.of_allowance": "手当の{ratio}", + "allowance.of_total": "{used}/{total} クレジット", + "allowance.rate_card_error": "料金表エラー: {error}", + "allowance.remaining": "残り{value}", + "allowance.resets": "リセット: {resets}", + "allowance.row_no_rate": "マッピングされたCodex信用率がありません", + "allowance.title_hint": "~/.codex-usage-tracker/allowance.json を追加して、5 時間および週ごとの残りの使用量を表示します。", + "allowance.used_vs_remaining": "{used} 使用済み vs {remaining} 残り", + "allowance.window_configured": "{label} が構成されました", + "allowance.windows": "許容範囲: {windows}", + "aria.current_view_actions": "現在のビューのアクション", + "aria.dashboard_status": "ダッシュボードのステータス", + "aria.dashboard_view": "ダッシュボードビュー", + "aria.history_title": "デフォルトはアクティブなセッションのみです。すべての履歴は、ライブ更新中にアーカイブされたセッション ログをスキャンします。", + "aria.inspect_thread": "{thread} の使用状況を検査する", + "aria.refresh_controls": "ダッシュボードの更新コントロール", + "aria.table_pages": "表のページ", + "badge.costs": "コスト", + "badge.credits": "クレジット", + "badge.live": "ライブ", + "badge.metadata_mode": "メタデータ {mode}", + "badge.metadata_normal": "メタデータ通常", + "badge.no_costs": "費用はかかりません", + "badge.parser_warnings": "パーサーの警告", + "badge.static": "静的", + "badge.unofficial_project": "非公式プロジェクト", + "badge.unofficial_project_title": "Codex Usage Tracker は独立したものであり、OpenAI によって作成、提携、承認、後援、サポートされていません。 OpenAI および Codex は OpenAI の商標です。", + "button.back_to_dashboard": "ダッシュボードに戻る", + "button.clear": "クリア", + "button.copy_link": "リンクをコピー", + "button.enable_context_loading": "コンテキストの読み込みを有効にする", + "button.export_csv": "CSV をエクスポート", + "button.full_serialized_analysis": "完全なシリアル化された分析を実行する", + "button.hide_details": "詳細を隠す", + "button.hide_tool_output": "ツールの出力を非表示にする", + "button.include_tool_output": "ツール出力を含める", + "button.load_context": "コンテキストをロードする", + "button.load_more": "さらにロードする", + "button.load_older_context": "古いエントリをロードする", + "button.next": "次へ", + "button.next_call": "次の呼び出し", + "button.no_char_limit": "文字数制限なし", + "button.open_investigator": "調査員を公開する", + "button.previous": "前へ", + "button.previous_call": "前回の通話", + "button.refresh": "リフレッシュ", + "button.show_compaction_history": "圧縮された置換を表示", + "button.show_tool_output": "ツールの出力を表示", + "button.show_turn_evidence": "ターンログの証拠を表示", + "button.top": "トップへ", + "call.cache_accounting_delta": "キャッシュ/アカウンティングデルタ", + "call.cache_cold": "コールド レジューム/古いキャッシュ", + "call.cache_diagnostics": "キャッシュ診断", + "call.cache_partial": "部分的なキャッシュミス", + "call.cache_spike": "キャッシュされていないスパイク", + "call.cache_steady": "安定したキャッシュプロファイル", + "call.cache_warm": "ウォームキャッシュの再利用", + "call.compaction_diagnostics": "圧縮診断", + "call.compaction_hint": "読み込まれた証拠は、明示的な圧縮イベントを示す可能性があります。編集された置換履歴は、圧縮された置換アクションの後にのみ表示されます。", + "call.context_estimate": "コンテキスト変化の推定", + "call.context_estimate_hint": "キャッシュされていない正確な入力とトークナイザーでカウントされた可視ログ証拠を比較します。このギャップは、隠れたスキャフォールディング、シリアル化、またはトークナイザーの推定エラーとして扱う必要があります。", + "call.derived_label": "隣接する集約呼び出しから派生", + "call.estimated_label": "目に見えるログ量から推定", + "call.evidence_label": "実行時の証拠", + "call.exact_accounting": "正確なトークンアカウンティング", + "call.exact_label": "トークンコールバックからの正確な", + "call.hidden_estimate": "説明のない非表示/シリアル化された入力推定値", + "call.no_previous": "この解決されたスレッドには以前の呼び出しはありません。", + "call.not_found": "選択したコールがロードされたダッシュボード行に見つかりませんでした。", + "call.open_hint": "詳細な診断を行うには、コール行をクリックします。", + "call.position": "この解決されたスレッドで {position} を呼び出します。", + "call.post_compaction": "後圧縮が可能", + "call.raw_evidence": "生の証拠", + "call.remaining_after_serialized": "シリアル化バインド後の残り", + "call.remaining_after_serialized_hint": "キャッシュされていない入力はシリアル化された上限でもカバーされない", + "call.serialized_bound_hint": "上限ローカル JSONL 構造体。正確なプロンプトテキストではありません。", + "call.serialized_breakdown": "シリアル化された証拠グループ", + "call.serialized_bucket_detail": "{count} フィールド · {chars} 文字", + "call.serialized_candidate": "シリアル化されたオーバーヘッドの可能性", + "call.serialized_candidate_hint": "シリアル化された上限から表示される推定値を差し引いたもので、キャッシュされていない正確な入力によって制限されます", + "call.serialized_deferred": "高速見積もりを読み込みました。完全なシリアル化グループ分析は後回しです。", + "call.serialized_quick_hint": "早い見積もり", + "call.serialized_upper_bound": "シリアル化されたローカル上限", + "call.visible_estimate": "目に見える新しいコンテキスト推定", + "call.visible_gap": "キャッシュされていない入力から表示される推定値を差し引いたもの", + "caption.ascending": "昇順", + "caption.call_investigator": "{record} のコールを調査しています。", + "caption.calls": "個々のモデル呼び出しを {sort} でソートして表示します。 {loaded}。", + "caption.date_prefix": "{label}。", + "caption.descending": "降順", + "caption.initial_calls": "個々のモデル呼び出しを表示します。", + "caption.insights": "コスト、使用クレジット、キャッシュの再利用、コンテキストの圧力、および価格設定の信頼度によってランク付けされます。", + "caption.loaded": "{loaded} 呼び出しがロードされました", + "caption.loaded_capped": "{available} 件中 {loaded} 件の呼び出しが読み込まれました", + "caption.rows_loaded_progress": "行を読み込み済み: {loaded} / {total}", + "caption.rows_loading_background": "ダッシュボードの合計は準備できています。行はバックグラウンドで読み込んでいます。", + "caption.rows_loading_progress": "行を読み込み中: {loaded} / {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "{calls} でフィルタリングされた呼び出しからの {threads} スレッドを {sort} 順に並べて表示します。 {loaded}。スレッドをクリックしてその呼び出しを展開します。", + "context.api_http": "コンテキスト API は HTTP {status} を返しました。", + "context.api_unavailable": "コンテキスト API はここでは使用できません。オンデマンドのコンテキスト読み込みのために codex-usage-tracker serve-dashboard --open を実行します。", + "context.auto_loading": "ツール出力を含む選択したターンの証拠をロードします。", + "context.chars_omitted": "予算を超えた {count} 文字は省略されました。", + "context.compaction_detected": "圧縮が検出されました", + "context.compaction_replacement": "圧縮された置換コンテキスト", + "context.compaction_replacement_count": "{count} 交換履歴エントリが利用可能です。", + "context.disabled_hint": "このダッシュボード サーバーではコンテキストの読み込みがオフになっています。ここで有効にしてローカル JSONL コンテキストをオンデマンドでロードします。", + "context.enabled_note": "コンテキストの読み込みが有効になっています。 [ターンログ証拠の表示] を押して、ローカル JSONL ソースからこの呼び出しを読み取ります。", + "context.file_hint": "codex-usage-tracker serve-dashboard を使用してこのダッシュボードを開き、生のコンテキストをオンデマンドでロードします。", + "context.line": "{line}行目", + "context.loading": "ローカルコンテキストを読み込んでいます...", + "context.local_redacted": "ローカル JSONL コンテキストはオンデマンドでロードされます。プロンプトとツールの出力は一般的なシークレット パターンに合わせて編集されており、SQLite またはダッシュボード HTML には保持されません。", + "context.no_char_limit_active": "文字数制限は適用されません。", + "context.no_record_id": "この行にはコンテキスト検索用のレコード ID がありません。", + "context.no_response": "応答本文がありません", + "context.older_omitted": "{count} 古いエントリは省略されました。", + "context.ready_hint": "このダッシュボードにはコンテキストが埋め込まれていません。ボタンを押して、ローカル JSONL ソースからこの呼び出しを読み取ります。", + "context.settings_http": "コンテキスト設定は HTTP {status} を返しました。", + "context.source": "出典: {file}:{line}", + "context.token_breakdown": "トークンの内訳", + "context.token_cached": "キャッシュされた", + "context.token_input": "入力", + "context.token_output": "出力", + "context.token_reasoning": "推論", + "context.token_required": "コンテキストの読み込みには、localhost ダッシュボード API トークンが必要です。", + "context.token_scope_call": "この呼び出し", + "context.token_scope_earlier": "同じターン内でのトークン数の増加", + "context.token_scope_previous": "同じターン内の前回のトークン数", + "context.token_scope_selected": "選択されたコールトークン数", + "context.token_scope_session": "セッションの累積", + "context.token_total": "合計", + "context.token_type": "タイプ", + "context.token_uncached": "キャッシュされていない", + "context.tool_included": "ツール出力には墨消しとサイズ制限が含まれています。", + "context.tool_omitted": "このビューではツール出力が非表示になっています。", + "credit.configured_rate": "設定されたレート", + "credit.estimated_mapping": "推定マッピング", + "credit.inferred_mapping": "推論されたモデルのマッピング", + "credit.no_mapped_rate": "マッピングされたレートはありません", + "credit.no_rate": "信用率なし", + "credit.official_match": "公式料金表の一致", + "credit.user_rate": "ユーザー指定の信用レート", + "credit.with_status": "{value} クレジット · {status}", + "dashboard.call_details": "通話の詳細", + "dashboard.detail.empty": "行にマウスを置くかクリックして、集計使用状況フィールドを調べます。", + "dashboard.eyebrow": "ローカル Codex 分析", + "dashboard.local_storage_note": "ダッシュボードのヘッダーには、ローカルで選択した言語も記憶されます。", + "dashboard.model_calls": "モデルコール", + "dashboard.title": "使用状況ダッシュボード", + "dashboard.top_threads_by_attention": "アテンションスコア別の上位スレッド", + "dashboard.view.call": "調査員に呼び出しする", + "dashboard.view.calls": "呼び出し", + "dashboard.view.insights": "洞察", + "dashboard.view.threads": "スレッド", + "date.custom": "カスタム", + "date.invalid_range": "無効な日付範囲です", + "date.range_between": "{prefix} {start} ~ {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", - "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", - "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "date.range_from": "{prefix} から {start}", + "date.range_through": "{prefix} ~ {end}", + "detail.agent_nickname": "エージェントのニックネーム", + "detail.agent_role": "エージェントの役割", + "detail.allowance_impact": "手当の影響", + "detail.attached_calls": "接続された通話", + "detail.auto_review_calls": "コールの自動レビュー", + "detail.cache_savings": "キャッシュの節約", + "detail.call_number": "{number}に呼び出ししてください", + "detail.calls": "呼び出し", + "detail.context_window": "コンテキストウィンドウ", + "detail.cost_usage_context": "コスト、使用法、およびコンテキスト", + "detail.credit_confidence": "信用の信頼", + "detail.credit_model": "信用モデル", + "detail.credit_note": "クレジットノート", + "detail.credit_source": "クレジットソース", + "detail.credit_source_fetched": "クレジットソースを取得しました", + "detail.credit_tier": "信用ティア", + "detail.cwd": "CWD", + "detail.efficiency_signals": "効率シグナル", + "detail.first_expensive_turn": "最初の高価なターン", + "detail.git_branch": "Git ブランチ", + "detail.largest_cumulative_jump": "最大の累積ジャンプ", + "detail.latest_activity": "最新の活動", + "detail.model_mix": "モデルミックス", + "detail.next_action": "次のアクション", + "detail.no_above_thresholds": "しきい値を超えるものはありません", + "detail.no_aggregate_action": "フラグが設定されている集約アクションはありません。", + "detail.parent_session": "親セッション", + "detail.parent_thread": "親スレッド", + "detail.parent_updated": "親が更新されました", + "detail.pricing_model": "価格モデル", + "detail.pricing_status": "価格設定ステータス", + "detail.project_cwd": "プロジェクトcwd", + "detail.project_tags": "プロジェクトタグ", + "detail.raw_identifiers": "生の集合体識別子", + "detail.reasoning_mix": "推理ミックス", + "detail.relationships": "人間関係", + "detail.remote_hash": "リモートハッシュ", + "detail.remote_label": "リモートラベル", + "detail.secondary_thread_fields": "セカンダリ スレッド フィールド", + "detail.source_file_line": "ソースファイルと行", + "detail.source_line": "ソース行", + "detail.spawned_child_calls": "生成された子呼び出し", + "detail.spawned_from": "から生成されました", + "detail.spawned_threads": "生成されたスレッド", + "detail.subagent_before_spike": "スパイク前のサブエージェント", + "detail.subagent_calls": "サブエージェント呼び出し", + "detail.subagent_type": "サブエージェントの種類", + "detail.thread_attachment": "ネジの取り付け", + "detail.thread_attention_summary": "スレッド注目まとめ", + "detail.thread_lifecycle": "スレッドのライフサイクル", + "detail.thread_narrative": "スレッドの物語", + "detail.thread_source": "スレッドソース", + "detail.thread_timeline": "スレッドのタイムライン", + "detail.timeline_context": "コンテキスト {value}", + "detail.timeline_empty": "このスレッドには通話はありません。", + "detail.timeline_meta": "{tokens} トークン · {cost} · {credits} · キャッシュ {cache}", + "detail.timestamp": "タイムスタンプ", + "detail.token_pricing_breakdown": "トークンと価格の内訳", + "detail.tokens_at": "{tokens} トークン ({time})", + "detail.turn": "ターン", + "detail.why_flagged": "フラグが立てられた理由", + "docs.dashboard_guide": "ダッシュボードガイド", + "effort.high": "高い", + "effort.low": "低い", + "effort.medium": "中程度", + "filter.confidence": "自信", + "filter.effort": "推論", + "filter.end": "終了", + "filter.model": "モデル", + "filter.project": "プロジェクト", + "filter.reasoning": "推論", + "filter.search": "検索", + "filter.search_placeholder": "スレッド、CWD、モデル", + "filter.session": "セッション", + "filter.sort": "並べ替え", + "filter.start": "スタート", + "filter.thread": "スレッド", + "filter.time": "時間", + "flag.elevated_context_use": "高度なコンテキストの使用", + "flag.expensive_low_output_call": "高価な低出力通話", + "flag.high_context_use": "ハイコンテキストの使用", + "flag.high_estimated_cost": "推定コストが高い", + "flag.high_reasoning_share": "高い推論シェア", + "flag.low_cache_reuse": "キャッシュの再利用が少ない", + "history.active_hidden": "アクティブなセッションのみ。 {count} アーカイブされた通話が非表示になりました", + "history.active_only": "アクティブなセッションのみ", + "history.all_empty": "すべての履歴が選択されています。アーカイブされた通話はまだインデックス付けされていません", + "history.all_includes": "すべての履歴には、{count} アーカイブされた通話が含まれます", + "history.archived_scan_hint": "{detail}。アーカイブされたセッションは、ライブ更新中に [すべての履歴] が選択されている場合にのみスキャンされます。", + "insight.apply_cache_misses": "キャッシュミス プリセットを適用する", + "insight.apply_context_bloat": "context-bloat プリセットを適用する", + "insight.codex_allowance_usage": "Codex 割り当ての使用量", + "insight.context_bloat": "コンテキストの肥大化", + "insight.context_bloat_body": "{calls} 呼び出しは {ratio} コンテキスト以上で使用されます。", + "insight.costliest_thread": "最も高価なスレッド", + "insight.costliest_thread_body": "{thread} には {calls} 呼び出しと {tokens} トークンがあります。", + "insight.credit_coverage_body": "表示可能なトークンの {ratio} は、Codex 信用レートにマッピングされます。", + "insight.estimated_pricing": "推定価格", + "insight.estimated_pricing_body": "マークされた最良推定価格も含まれていますが、別途検討する必要があります。", + "insight.inspect_selected_call": "選択した通話を検査する", + "insight.low_cache_reuse": "キャッシュの再利用が少ない", + "insight.low_cache_reuse_body": "{calls} 呼び出しは {ratio} キャッシュの再利用の下にあります。 {thread} から始めます。", + "insight.open_thread_timeline": "スレッドのタイムラインを開く", + "insight.reasoning_output_spike": "推論出力のスパイク", + "insight.reasoning_spike_body": "{thread} には、現在のフィルター内で最大の推論出力呼び出しがあります。", + "insight.review_estimates": "見積もりを確認する", + "insight.review_highest_credit": "最もクレジットの高い通話を確認する", + "insight.review_pricing_gaps": "価格ギャップを確認する", + "insight.unpriced_usage": "無償使用", + "insight.unpriced_usage_body": "これらのトークンは、価格が設定されるまで推定コスト合計から除外されます。", + "language.english": "英語", + "language.label": "言語", + "language.vietnamese": "ティエン・ヴィエット", + "live.checking_usage": "新しい使用状況を確認しています...", + "live.every": "{seconds}秒ごとにライブ更新", + "live.history_static_hint": "codex-usage-tracker serve-dashboard を実行して、アクティブなセッションとダッシュボードのすべての履歴を切り替えます。", + "live.indexed": "インデックス付き {rows} は、{files} ログからの行を集約します。", + "live.load_static_hint": "codex-usage-tracker serve-dashboard を実行して、ダッシュボードから異なる履歴サイズをロードします。", + "live.loading_rows": "バックグラウンドで行を読み込み中...", + "live.paused": "ライブ更新が一時停止されました", + "live.refresh_suffix": "。静的ダッシュボードを再生成した後にこのページをリロードするか、codex-usage-tracker serve-dashboard を実行します。", + "live.refresh_unavailable": "ライブ更新は利用できません: {message}{suffix}", + "live.refreshing_index": "ローカル使用状況インデックスを更新しています...", + "live.reloading_static": "静的ダッシュボードのスナップショットを再ロードしています...", + "live.skipped": "{count} の不正な形式のトークンカウント イベントをスキップしました。", + "live.updated_detail": "{time} を更新しました。 {loaded}。 {history}.{indexed}{skipped}", + "metric.attention_score": "注意スコア", + "metric.cache_ratio": "キャッシュ率", + "metric.cache_trend": "キャッシュの傾向", + "metric.cached_input": "キャッシュされた入力", + "metric.codex_credits": "Codex クレジット", + "metric.context_trend": "コンテキストトレンド", + "metric.context_use": "コンテキストの使用", + "metric.estimated_cost": "推定コスト", + "metric.input_tokens": "入力トークン", + "metric.last_call_input": "最終通話入力", + "metric.last_call_total": "最終通話の合計", + "metric.max_context_use": "コンテキストの最大使用量", + "metric.output": "出力", + "metric.output_tokens": "出力トークン", + "metric.reasoning_output": "推論出力", + "metric.remaining_usage": "残りの使用量", + "metric.session_cumulative": "セッションの累積", + "metric.total": "合計", + "metric.total_tokens": "総トークン数", + "metric.uncached_input": "キャッシュされていない入力", + "metric.usage_credits": "使用クレジット", + "metric.usage_remaining": "残りの使用量", + "metric.visible_calls": "可視通話", + "nav.history": "歴史", + "nav.live": "ライブ", + "nav.load": "ロード", + "option.active_sessions_only": "アクティブなセッションのみ", + "option.all_confidence": "すべての自信", + "option.all_efforts": "あらゆる努力", + "option.all_history": "すべての歴史", + "option.all_models": "全モデル", + "option.all_time": "ずっと", + "option.custom_range": "カスタム範囲", + "option.estimated_cost": "推定コスト", + "option.estimated_credit_mapping": "推定クレジットマッピング", + "option.exact_cost": "正確なコスト", + "option.exact_credit_rate": "正確な信用率", + "option.highest_codex_credits": "最高のCodexクレジット", + "option.highest_context_use": "最高のコンテキストの使用", + "option.highest_estimated_cost": "推定最高コスト", + "option.last_7_days": "過去 7 日間", + "option.load_10000": "10,000回の通話", + "option.load_20000": "20,000 件の通話", + "option.load_5000": "5,000 件の通話", + "option.load_all": "すべての通話", + "option.lowest_cache_ratio": "最低のキャッシュ率", + "option.missing_credit_rate": "信用率がありません", + "option.most_signals": "ほとんどの信号", + "option.most_tokens": "ほとんどのトークン", + "option.needs_attention": "注意が必要です", + "option.newest_calls": "最新の通話", + "option.this_month": "今月", + "option.this_week": "今週", + "option.thread_name": "スレッド名", + "option.today": "今日", + "option.unpriced_cost": "値段がつけられていないコスト", + "option.user_credit_override": "ユーザークレジットの上書き", + "parser.warnings_title": "最新の更新では、{count} パーサー診断が報告されました: {entries}。 codex-usage-tracker inspect-log を実行して、スキーマのドリフトを調査します。", + "preset.cache_misses": "キャッシュミス", + "preset.cache_misses_caption": "キャッシュミスプリセット", + "preset.cache_misses_desc": "cwd、モデル、スレッドごとにグループ化された低キャッシュ率の呼び出し。", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", - "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "preset.context_bloat": "コンテキストの肥大化", + "preset.context_bloat_caption": "コンテキスト肥大化プリセット", + "preset.context_bloat_desc": "60% を超えるコンテキストを使用するか、非常に高い累積トークンを使用して呼び出します。", + "preset.description": "使用上のよくある質問をワンクリックで開始できます。", + "preset.estimated_price_review": "見積価格の見直し", + "preset.estimated_price_review_caption": "見積価格レビュープリセット", + "preset.estimated_price_review_desc": "使用量の価格は、マークされた最良の推定見積もりで設定されます。", + "preset.highest_codex_credits": "最高のCodexクレジット", + "preset.highest_codex_credits_caption": "最高のCodexクレジットがプリセットされています", + "preset.highest_codex_credits_desc": "Codex 使用量許容量への推定影響によってソートされた通話。", + "preset.highest_cost_threads": "最もコストの高いスレッド", + "preset.highest_cost_threads_caption": "最高コストのスレッドが事前に設定されている", + "preset.highest_cost_threads_desc": "スレッドは推定支出額順にソートされ、サブエージェントが接続されています。", + "preset.investigation_presets": "調査プリセット", + "preset.no_preset": "プリセットは適用されていません。", + "preset.pricing_gaps": "価格ギャップ", + "preset.pricing_gaps_caption": "プリセットされた価格ギャップ", + "preset.pricing_gaps_desc": "価格の設定されていない使用により、推定コスト合計が不完全になります。", + "pricing.configure_hint": "codex-usage-tracker update-pricing を実行して推定コストを構成します。", + "pricing.fetched": "{time} を取得しました", + "pricing.pinned": "固定されたスナップショット", + "pricing.source": "価格設定ソース", + "pricing.tier": "{tier}層", + "pricing.title": "{parts}。内部 Codex ラベルは、マークされた最良推測推定値を使用する場合があります。{warning}", + "pricing.title_fetched": "{parts}。 {time} で {url} から取得されました。内部 Codex ラベルは、マークされた最良推測推定値を使用する場合があります。{warning}", + "privacy.aliases_preserved": "構成されたプロジェクト エイリアスは、明示的な表示オプトインとして扱われます。", + "privacy.cwd_redacted": "生の CWD パスは編集されます。", + "privacy.git_branch_hidden": "Git ブランチは非表示になります。", + "privacy.git_remote_label_hidden": "Git リモート ラベルは非表示になります。", + "privacy.mode": "プロジェクト メタデータ プライバシー モード: {mode}。", + "privacy.normal_title": "プロジェクトのメタデータは、ローカル cwd、プロジェクト、ブランチ、および設定されたラベルとともに表示されます。", + "privacy.project_names_redacted": "名前のないプロジェクトでは、安定したハッシュ ラベルが使用されます。", + "privacy.relative_cwd_hidden": "相対 cwd は非表示になります。", + "privacy.tags_hidden": "プロジェクトタグは非表示になります。", + "recommendation.context_bloat.action": "古いコンテキストが関係なくなった場合は、新しい Codex スレッドを開始することを検討してください。", + "recommendation.context_bloat.title": "高いコンテキストプレッシャー", + "recommendation.context_bloat.why": "この呼び出しは、モデル コンテキスト ウィンドウの大部分を使用しています。", + "recommendation.elevated_context.action": "追加の作業を追加する前に、ねじ山を狭くできるかどうかを確認してください。", + "recommendation.elevated_context.title": "コンテキストのプレッシャーの高まり", + "recommendation.elevated_context.why": "コンテキストの使用が増加し、後のターンでコストが高くなる可能性があります。", + "recommendation.estimated_pricing.action": "この通話が重要な場合は、価格設定範囲を確認し、モデル料金を固定するか上書きします。", + "recommendation.estimated_pricing.title": "推定価格", + "recommendation.estimated_pricing.why": "このコストでは、直接の価格設定行ではなく、推論されたモデル マッピングが使用されます。", + "recommendation.high_cost.action": "スレッドのタイムラインを開いて、続行する前に前のターンを調べてください。", + "recommendation.high_cost.title": "推定コストが高い", + "recommendation.high_cost.why": "このコールは、設定された高コストしきい値を超えました。", + "recommendation.large_thread.action": "無関係なフォローアップ作業については、新しいスレッドを推奨します。", + "recommendation.large_thread.title": "大量の累積スレッド", + "recommendation.large_thread.why": "セッションの累計は十分に大きいため、後のターンが高価になります。", + "recommendation.low_cache.action": "ファイル、ツール出力、または広範なコンテキストが不必要に再導入されていないかどうかを確認してください。", + "recommendation.low_cache.title": "キャッシュの再利用が少ない", + "recommendation.low_cache.why": "キャッシュされていない新鮮な入力は高くなりますが、キャッシュの再利用は低くなります。", + "recommendation.low_output.action": "最初に集約コンテキストを検査します。原因が不明な場合にのみ生のコンテキストをロードします。", + "recommendation.low_output.title": "大規模な低出力通話", + "recommendation.low_output.why": "この呼び出しでは多くのトークンが消費されましたが、出力はほとんど生成されませんでした。", + "recommendation.none.action": "集計アクションにはフラグが立てられません。使用パターンの監視を継続します。", + "recommendation.pricing_gap.action": "コスト合計を信頼する前に、価格を更新するか、ローカル エイリアスを追加してください。", + "recommendation.pricing_gap.title": "価格差", + "recommendation.pricing_gap.why": "このモデル コールには価格が設定されていないため、コストの合計は目に見える使用量よりも少なく表示されます。", + "recommendation.reasoning_spike.action": "このタスクに選択した推論作業が必要かどうかを確認します。", + "recommendation.reasoning_spike.title": "高い推論シェア", + "recommendation.reasoning_spike.why": "推論出力は、この呼び出しの表示出力の大部分を占めます。", + "recommendation.subagent_attribution.action": "直接コールと接続されたサブエージェントを比較するか、ワークフローを変更する前にコールを確認してください。", + "recommendation.subagent_attribution.title": "副代理人の帰属", + "recommendation.subagent_attribution.why": "この呼び出しは委任された作業に関連付けられており、親スレッドの成長を説明する可能性があります。", + "section.allowance": "手当", + "section.needs_attention": "注意が必要です", + "section.pricing": "価格設定", + "section.recommendations": "推奨事項", + "severity.high": "高", + "severity.medium": "中", + "severity.review": "レビュー", + "source.auto_review": "自動レビュー", + "source.codex_initiated": "Codex が開始されました", + "source.subagent": "復代理人", + "source.subagent_role": "サブエージェント: {role}", + "source.user": "ユーザー", + "source.user_initiated": "ユーザーが開始", + "state.allowance_config_error": "許容設定エラー", + "state.allowance_configured": "設定された許容量", + "state.best_guess_estimate": "最善の推測による推定", + "state.configured": "設定済み", + "state.configured_price": "設定価格", + "state.error": "エラー", + "state.estimated": "推定", + "state.loading": "読み込み中", + "state.loading_rows": "行を読み込み中", + "state.mixed": "混合", + "state.no": "いいえ", + "state.no_calls": "現在のフィルターに一致する呼び出しはありません。", + "state.no_configured_price": "価格が設定されていません", + "state.no_context_entries": "この呼び出しのコンテキスト エントリが見つかりません。", + "state.no_data": "データなし", + "state.no_mapped_rate": "マッピングされたレートはありません", + "state.no_price": "価格なし", + "state.no_rate": "料金なし", + "state.no_rows": "行がありません", + "state.no_threads": "現在のフィルターに一致するスレッドはありません。", + "state.none": "なし", + "state.not_configured": "未設定", + "state.requires_evidence": "証拠が必要です", + "state.unknown": "不明", + "state.yes": "はい", + "status.checking": "チェック中", + "status.paused": "一時停止中", + "status.refresh_error": "リフレッシュエラー", + "status.refreshing": "さわやか", + "status.reloading": "リロード中", + "status.static": "静的", + "status.updated": "更新されました", + "table.cache": "キャッシュ", + "table.cached": "キャッシュされた", + "table.calls": "呼び出し", + "table.cost": "コスト", + "table.effort": "努力", + "table.initiated": "開始済み", + "table.last_call": "ラストコール", + "table.model": "モデル", + "table.more_efforts": "{effort} +{count} の取り組み", + "table.more_models": "{model} +{count} モデル", + "table.output": "出力", + "table.page_status": "{start}-{end} / {total} {items} · ページ {page}/{pages}", + "table.rows": "行", + "table.signals": "信号", + "table.source": "ソース", + "table.thread": "スレッド", + "table.threads": "スレッド", + "table.time": "時間", + "table.tokens": "トークン", + "table.uncached": "キャッシュされていない", + "table.visible_status": "{end}/{total} {items} を表示しています", + "thread.attached": "付属の", + "thread.attention": "注意 {score}", + "thread.auto_review": "{count} 自動レビュー", + "thread.collapse": "崩壊する", + "thread.direct": "直接", + "thread.expand": "拡大する", + "thread.expand_label": "{action} {thread} が呼び出します。注意スコア {score}。", + "thread.explicit_parent": "明示的な親", + "thread.explicit_parent_thread": "明示的な親スレッド", + "thread.parent": "親{id}", + "thread.session": "セッション", + "thread.spawned": "産まれた", + "thread.spawned_from": "{thread} から生成されました", + "thread.spawned_threads": "{count} 個のスレッドが生成されました", + "thread.subagent": "{count} サブエージェント", + "thread.unknown": "不明なスレッド", + "thread.unmatched_subagent": "一致しないサブエージェント" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/ko.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/ko.json index ef2ecf4..395abec 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/ko.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/ko.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "캐시 재사용이 중단된 후 다시 도입된 파일이나 도구 출력을 확인하세요.", + "action.compare_fresh_input": "계속하기 전에 새로운 입력을 이전 차례와 비교하십시오.", + "action.compare_subagent_calls": "상위 워크플로를 변경하기 전에 첨부된 하위 에이전트를 비교하거나 호출를 검토하세요.", + "action.configure_pricing": "총 비용을 신뢰하기 전에 가격을 구성하세요.", + "action.copied": "복사됨", + "action.copy_failed": "복사 실패", + "action.expand_or_select_recommendations": "호출를 확장하거나 호출 수준 권장 사항에 대한 행을 선택하세요.", + "action.exported": "{count} 내보냈습니다.", + "action.inspect_thread_timeline": "스레드 타임라인을 검사하고 새로운 스레드를 시작하는 것을 고려해보세요.", + "action.review_context_growth": "컨텍스트 성장이 시작되는 위치를 검토하고 새로운 스레드 시작을 고려하세요.", + "action.review_reasoning_effort": "이 작업에 추론 노력이 적절한지 검토하세요.", + "action.run": "실행", + "action.set_limits": "한도 설정", + "action.use_aggregate_first": "집계 필드를 먼저 사용하십시오. 신호가 여전히 명확하지 않은 경우에만 컨텍스트를 로드하세요.", + "allowance.counted": "{value} 크레딧은 Codex 사용 한도에 포함됩니다.", + "allowance.cr_left": "{value} cr 남음", + "allowance.credit_coverage": "로드된 토큰의 신용 한도는 {ratio}입니다.", + "allowance.credit_rates": "신용율: {source}.", + "allowance.credits_remaining": "{value} 크레딧 남음", + "allowance.init_hint": "codex-usage-tracker init-allowance을 실행하여 남은 사용 기간을 추가하세요.", + "allowance.of_allowance": "수당의 {ratio}", + "allowance.of_total": "{used}/{total} 크레딧", + "allowance.rate_card_error": "요율표 오류: {error}", + "allowance.remaining": "{value} 남음", + "allowance.resets": "재설정: {resets}", + "allowance.row_no_rate": "매핑된 Codex 신용율이 없습니다.", + "allowance.title_hint": "~/.codex-usage-tracker/allowance.json을 추가하여 5시간 및 주간 남은 사용량을 표시합니다.", + "allowance.used_vs_remaining": "{used} 사용 대 {remaining} 남음", + "allowance.window_configured": "{label} 구성됨", + "allowance.windows": "수당 창구: {windows}", + "aria.current_view_actions": "현재 보기 작업", + "aria.dashboard_status": "대시보드 상태", + "aria.dashboard_view": "대시보드 보기", + "aria.history_title": "활성 세션만 기본값입니다. 모든 기록은 실시간 새로 고침 중에 보관된 세션 로그를 검사합니다.", + "aria.inspect_thread": "{thread} 사용법 검사", + "aria.refresh_controls": "대시보드 새로 고침 제어", + "aria.table_pages": "테이블 페이지", + "badge.costs": "비용", + "badge.credits": "크레딧", + "badge.live": "라이브", + "badge.metadata_mode": "메타데이터 {mode}", + "badge.metadata_normal": "메타데이터 정상", + "badge.no_costs": "비용 없음", + "badge.parser_warnings": "파서 경고", + "badge.static": "정적", + "badge.unofficial_project": "비공식 프로젝트", + "badge.unofficial_project_title": "Codex Usage Tracker은(는) 독립적이며 OpenAI이 제작, 제휴, 보증, 후원 또는 지원하지 않습니다. OpenAI 및 Codex은 OpenAI의 상표입니다.", + "button.back_to_dashboard": "대시보드로 돌아가기", + "button.clear": "지우기", + "button.copy_link": "링크 복사", + "button.enable_context_loading": "컨텍스트 로딩 활성화", + "button.export_csv": "CSV 내보내기", + "button.full_serialized_analysis": "전체 직렬화된 분석 실행", + "button.hide_details": "세부정보 숨기기", + "button.hide_tool_output": "도구 출력 숨기기", + "button.include_tool_output": "도구 출력 포함", + "button.load_context": "컨텍스트 로드", + "button.load_more": "더 로드하기", + "button.load_older_context": "이전 항목 로드", + "button.next": "다음", + "button.next_call": "다음 호출", + "button.no_char_limit": "문자 제한 없음", + "button.open_investigator": "열린 조사관", + "button.previous": "이전", + "button.previous_call": "이전 호출", + "button.refresh": "새로고침", + "button.show_compaction_history": "압축된 교체 표시", + "button.show_tool_output": "도구 출력 표시", + "button.show_turn_evidence": "턴 로그 증거 표시", + "button.top": "탑", + "call.cache_accounting_delta": "캐시/회계 델타", + "call.cache_cold": "콜드 이력서 / 오래된 캐시", + "call.cache_diagnostics": "캐시 진단", + "call.cache_partial": "부분적인 캐시 누락", + "call.cache_spike": "캐시되지 않은 스파이크", + "call.cache_steady": "꾸준한 캐시 프로필", + "call.cache_warm": "웜 캐시 재사용", + "call.compaction_diagnostics": "다짐 진단", + "call.compaction_hint": "로드된 증거는 명시적인 압축 이벤트를 표시할 수 있습니다. 수정된 교체 내역은 압축된 교체 작업 이후에만 표시됩니다.", + "call.context_estimate": "상황 변화 추정", + "call.context_estimate_hint": "캐시되지 않은 정확한 입력을 토크나이저에서 계산된 가시적 로그 증거와 비교합니다. 격차는 숨겨진 비계, 직렬화 또는 토크나이저 추정 오류로 처리되어야 합니다.", + "call.derived_label": "인접한 집계 호출에서 파생됨", + "call.estimated_label": "표시되는 로그 볼륨으로 추정", + "call.evidence_label": "런타임 증거", + "call.exact_accounting": "정확한 토큰 회계", + "call.exact_label": "토큰 콜백에서 정확함", + "call.hidden_estimate": "설명되지 않은 숨겨진/직렬화된 입력 추정치", + "call.no_previous": "이 해결된 스레드에는 이전 호출이 없습니다.", + "call.not_found": "로드된 대시보드 행에서 선택한 호출를 찾을 수 없습니다.", + "call.open_hint": "심층 진단을 위해 호출 행을 클릭하세요.", + "call.position": "이 해결된 스레드에서 {position}을(를) 호출하세요.", + "call.post_compaction": "후압축 가능", + "call.raw_evidence": "원시 증거", + "call.remaining_after_serialized": "직렬화된 바인딩 후 남은 부분", + "call.remaining_after_serialized_hint": "직렬화된 상한으로도 처리되지 않는 캐시되지 않은 입력", + "call.serialized_bound_hint": "상한 지역 JSONL 구조; 정확한 프롬프트 텍스트가 아닙니다.", + "call.serialized_breakdown": "직렬화된 증거 그룹", + "call.serialized_bucket_detail": "{count} 필드 · {chars}자", + "call.serialized_candidate": "가능한 직렬화 오버헤드", + "call.serialized_candidate_hint": "직렬화된 상한에서 눈에 보이는 추정치를 뺀 값, 캐시되지 않은 정확한 입력으로 제한됨", + "call.serialized_deferred": "빠른 추정이 로드되었습니다. 전체 직렬화 그룹 분석은 지연됩니다.", + "call.serialized_quick_hint": "빠른 견적", + "call.serialized_upper_bound": "직렬화된 로컬 상한", + "call.visible_estimate": "표시되는 새 컨텍스트 추정", + "call.visible_gap": "캐시되지 않은 입력에서 눈에 보이는 추정치를 뺀 값", + "caption.ascending": "오름차순", + "caption.call_investigator": "{record} 호출를 조사 중입니다.", + "caption.calls": "{sort}별로 정렬된 개별 모델 호출을 표시합니다. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "내림차순", + "caption.initial_calls": "개별 모델 호출을 표시합니다.", + "caption.insights": "비용, 사용 크레딧, 캐시 재사용, 컨텍스트 압박, 가격 신뢰도를 기준으로 순위가 매겨집니다.", + "caption.loaded": "{loaded} 호출가 로드되었습니다.", + "caption.loaded_capped": "{available} 호출 중 {loaded}번 로드됨", + "caption.rows_loaded_progress": "행 로드됨: {loaded} / {total}", + "caption.rows_loading_background": "대시보드 합계가 준비되었습니다. 행은 백그라운드에서 로드 중입니다.", + "caption.rows_loading_progress": "행 로드 중: {loaded} / {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "{calls} 필터링된 호출의 {threads} 스레드를 {sort}별로 정렬하여 표시합니다. {loaded}. 호출을 확장하려면 스레드를 클릭하세요.", + "context.api_http": "컨텍스트 API이(가) HTTP {status}을(를) 반환했습니다.", + "context.api_unavailable": "여기서는 API 컨텍스트를 사용할 수 없습니다. 주문형 컨텍스트 로드를 위해 codex-usage-tracker serve-dashboard --open을 실행합니다.", + "context.auto_loading": "도구 출력이 포함된 선택 턴 증거 로드.", + "context.chars_omitted": "예산 초과 {count} 문자가 생략되었습니다.", + "context.compaction_detected": "압축이 감지되었습니다.", + "context.compaction_replacement": "압축된 대체 컨텍스트", + "context.compaction_replacement_count": "{count} 교체 내역 항목이 사용 가능합니다.", + "context.disabled_hint": "이 대시보드 서버에 대한 컨텍스트 로드가 꺼져 있습니다. 요청 시 로컬 JSONL 컨텍스트를 로드하려면 여기에서 활성화하세요.", + "context.enabled_note": "컨텍스트 로딩이 활성화되었습니다. 로컬 JSONL 소스에서 이 호출를 읽으려면 턴 로그 증거 표시를 누르세요.", + "context.file_hint": "필요에 따라 원시 컨텍스트를 로드하려면 codex-usage-tracker serve-dashboard로 이 대시보드를 엽니다.", + "context.line": "라인 {line}", + "context.loading": "로컬 컨텍스트 로드 중...", + "context.local_redacted": "요청 시 로컬 JSONL 컨텍스트가 로드됩니다. 프롬프트 및 도구 출력은 일반적인 비밀 패턴에 대해 수정되었으며 SQLite 또는 대시보드 HTML에 유지되지 않습니다.", + "context.no_char_limit_active": "글자 수 제한이 적용되지 않습니다.", + "context.no_record_id": "이 행에는 컨텍스트 조회를 위한 레코드 ID가 없습니다.", + "context.no_response": "응답 본문 없음", + "context.older_omitted": "{count} 이전 항목은 생략되었습니다.", + "context.ready_hint": "이 대시보드에는 컨텍스트가 포함되어 있지 않습니다. 로컬 JSONL 소스에서 이 호출을 읽으려면 버튼을 누르세요.", + "context.settings_http": "컨텍스트 설정이 HTTP {status}을(를) 반환했습니다.", + "context.source": "출처: {file}:{line}", + "context.token_breakdown": "토큰 분석", + "context.token_cached": "캐시됨", + "context.token_input": "입력", + "context.token_output": "출력", + "context.token_reasoning": "추론", + "context.token_required": "컨텍스트를 로드하려면 localhost 대시보드 API 토큰이 필요합니다.", + "context.token_scope_call": "이 호출", + "context.token_scope_earlier": "같은 턴의 이전 토큰 수", + "context.token_scope_previous": "같은 턴의 이전 토큰 수", + "context.token_scope_selected": "선택한 호출 토큰 수", + "context.token_scope_session": "세션 누적", + "context.token_total": "합계", + "context.token_type": "유형", + "context.token_uncached": "캐시되지 않음", + "context.tool_included": "수정 및 크기 제한이 포함된 도구 출력입니다.", + "context.tool_omitted": "이 보기에서는 도구 출력이 숨겨졌습니다.", + "credit.configured_rate": "구성된 요금", + "credit.estimated_mapping": "예상 매핑", + "credit.inferred_mapping": "추론된 모델 매핑", + "credit.no_mapped_rate": "매핑된 요금 없음", + "credit.no_rate": "신용등급 없음", + "credit.official_match": "공식 요율표 일치", + "credit.user_rate": "사용자 제공 신용등급", + "credit.with_status": "{value} 크레딧 · {status}", + "dashboard.call_details": "호출 세부정보", + "dashboard.detail.empty": "행을 마우스로 가리키거나 클릭하여 집계 사용량 필드를 검사합니다.", + "dashboard.eyebrow": "로컬 Codex 분석", + "dashboard.local_storage_note": "대시보드 헤더는 로컬에서 선택한 언어도 기억합니다.", + "dashboard.model_calls": "모델 호출", + "dashboard.title": "사용량 대시보드", + "dashboard.top_threads_by_attention": "관심 점수별 상위 스레드", + "dashboard.view.call": "조사관에게 전화하기", + "dashboard.view.calls": "호출", + "dashboard.view.insights": "통찰력", + "dashboard.view.threads": "스레드", + "date.custom": "맞춤", + "date.invalid_range": "잘못된 기간", + "date.range_between": "{prefix} {start} ~ {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", + "date.range_from": "{start}에서 {prefix}", + "date.range_through": "{prefix}부터 {end}까지", + "detail.agent_nickname": "상담원 닉네임", + "detail.agent_role": "상담원 역할", + "detail.allowance_impact": "수당 영향", + "detail.attached_calls": "첨부된 호출", + "detail.auto_review_calls": "호출 자동 검토", + "detail.cache_savings": "캐시 절약", + "detail.call_number": "{number}에 전화하세요", + "detail.calls": "호출", + "detail.context_window": "컨텍스트 창", + "detail.cost_usage_context": "비용, 사용량 및 컨텍스트", + "detail.credit_confidence": "신용신뢰", + "detail.credit_model": "신용 모델", + "detail.credit_note": "신용 메모", + "detail.credit_source": "신용 출처", + "detail.credit_source_fetched": "크레딧 소스를 가져왔습니다.", + "detail.credit_tier": "신용 등급", "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", - "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "detail.efficiency_signals": "효율성 신호", + "detail.first_expensive_turn": "첫 번째 비싼 차례", + "detail.git_branch": "Git 브랜치", + "detail.largest_cumulative_jump": "최대 누적 점프", + "detail.latest_activity": "최근 활동", + "detail.model_mix": "모델 믹스", + "detail.next_action": "다음 작업", + "detail.no_above_thresholds": "임계값을 초과하는 항목 없음", + "detail.no_aggregate_action": "집계 작업이 플래그 지정되지 않았습니다.", + "detail.parent_session": "학부모 세션", + "detail.parent_thread": "상위 스레드", + "detail.parent_updated": "부모가 업데이트됨", + "detail.pricing_model": "가격 모델", + "detail.pricing_status": "가격 상태", + "detail.project_cwd": "프로젝트 CWD", + "detail.project_tags": "프로젝트 태그", + "detail.raw_identifiers": "원시 집계 식별자", + "detail.reasoning_mix": "추론 믹스", + "detail.relationships": "관계", + "detail.remote_hash": "원격 해시", + "detail.remote_label": "원격 라벨", + "detail.secondary_thread_fields": "보조 스레드 필드", + "detail.source_file_line": "소스 파일 및 라인", + "detail.source_line": "소스 라인", + "detail.spawned_child_calls": "생성된 자식 호출", + "detail.spawned_from": "다음에서 생성됨", + "detail.spawned_threads": "생성된 스레드", + "detail.subagent_before_spike": "급증 전 하위 에이전트", + "detail.subagent_calls": "하위 에이전트 호출", + "detail.subagent_type": "하위 에이전트 유형", + "detail.thread_attachment": "실 부착", + "detail.thread_attention_summary": "스레드 관심 요약", + "detail.thread_lifecycle": "스레드 수명주기", + "detail.thread_narrative": "스레드 내러티브", + "detail.thread_source": "스레드 소스", + "detail.thread_timeline": "스레드 타임라인", + "detail.timeline_context": "컨텍스트 {value}", + "detail.timeline_empty": "이 스레드에는 호출가 없습니다.", + "detail.timeline_meta": "{tokens} 토큰 · {cost} · {credits} · 캐시 {cache}", + "detail.timestamp": "타임스탬프", + "detail.token_pricing_breakdown": "토큰 및 가격 분석", + "detail.tokens_at": "{time}의 {tokens} 토큰", + "detail.turn": "회전", + "detail.why_flagged": "신고된 이유", + "docs.dashboard_guide": "대시보드 가이드", + "effort.high": "높다", + "effort.low": "낮음", + "effort.medium": "중간", + "filter.confidence": "자신감", + "filter.effort": "추론", + "filter.end": "종료", + "filter.model": "모델", + "filter.project": "프로젝트", + "filter.reasoning": "추론", + "filter.search": "검색", + "filter.search_placeholder": "스레드, CWD, 모델", + "filter.session": "세션", + "filter.sort": "정렬", + "filter.start": "시작", + "filter.thread": "스레드", + "filter.time": "시간", + "flag.elevated_context_use": "높은 컨텍스트 사용", + "flag.expensive_low_output_call": "값비싼 저출력 호출", + "flag.high_context_use": "높은 상황별 사용", + "flag.high_estimated_cost": "예상 비용이 높음", + "flag.high_reasoning_share": "높은 추론 점유율", + "flag.low_cache_reuse": "낮은 캐시 재사용", + "history.active_hidden": "활성 세션만 해당. {count} 보관된 호출 숨김", + "history.active_only": "활성 세션만", + "history.all_empty": "모든 기록이 선택되었습니다. 아직 색인이 생성된 보관된 호출가 없습니다.", + "history.all_includes": "모든 기록에는 {count} 보관된 호출가 포함됩니다.", + "history.archived_scan_hint": "{detail}. 보관된 세션은 실시간 새로 고침 중에 모든 기록을 선택한 경우에만 검사됩니다.", + "insight.apply_cache_misses": "캐시 누락 사전 설정 적용", + "insight.apply_context_bloat": "컨텍스트 부풀림 사전 설정 적용", + "insight.codex_allowance_usage": "Codex 수당 사용", + "insight.context_bloat": "컨텍스트 팽창", + "insight.context_bloat_body": "{calls} 호출은 {ratio} 컨텍스트 사용 이상입니다.", + "insight.costliest_thread": "가장 비용이 많이 드는 스레드", + "insight.costliest_thread_body": "{thread}에는 {calls} 호출과 {tokens} 토큰이 있습니다.", + "insight.credit_coverage_body": "표시되는 토큰의 {ratio}는 Codex 신용율에 매핑됩니다.", + "insight.estimated_pricing": "예상 가격", + "insight.estimated_pricing_body": "표시된 최선의 추측 가격이 포함되어 있지만 별도로 검토해야 합니다.", + "insight.inspect_selected_call": "선택한 호출 검사", + "insight.low_cache_reuse": "낮은 캐시 재사용", + "insight.low_cache_reuse_body": "{calls} 호출은 {ratio} 캐시 재사용 상태에 있습니다. {thread}로 시작하세요.", + "insight.open_thread_timeline": "스레드 타임라인 열기", + "insight.reasoning_output_spike": "추론 출력 스파이크", + "insight.reasoning_spike_body": "{thread}에는 현재 필터에서 가장 큰 추론 출력 호출이 있습니다.", + "insight.review_estimates": "견적 검토", + "insight.review_highest_credit": "신용도가 가장 높은 호출 검토", + "insight.review_pricing_gaps": "가격 차이 검토", + "insight.unpriced_usage": "가격이 책정되지 않은 사용량", + "insight.unpriced_usage_body": "이러한 토큰은 가격이 구성될 때까지 예상 비용 총액에서 생략됩니다.", + "language.english": "영어", + "language.label": "언어", + "language.vietnamese": "티엥 비엣", + "live.checking_usage": "새로운 사용량을 확인하는 중...", + "live.every": "{seconds}s마다 실시간 새로 고침", + "live.history_static_hint": "대시보드의 활성 세션과 모든 기록 간에 전환하려면 codex-usage-tracker serve-dashboard을 실행하세요.", + "live.indexed": "색인이 생성된 {rows}은 {files} 로그의 행을 집계합니다.", + "live.load_static_hint": "대시보드에서 다른 기록 크기를 로드하려면 codex-usage-tracker serve-dashboard을 실행하세요.", + "live.loading_rows": "백그라운드에서 행 로드 중...", + "live.paused": "실시간 새로고침이 일시중지되었습니다.", + "live.refresh_suffix": ". 정적 대시보드를 다시 생성한 후 이 페이지를 다시 로드하거나 codex-usage-tracker serve-dashboard을 실행하세요.", + "live.refresh_unavailable": "실시간 새로고침을 사용할 수 없습니다: {message}{suffix}", + "live.refreshing_index": "로컬 사용 색인 새로고침 중...", + "live.reloading_static": "정적 대시보드 스냅샷을 다시 로드하는 중...", + "live.skipped": "{count} 잘못된 토큰 카운트 이벤트를 건너뛰었습니다.", + "live.updated_detail": "{time}을(를) 업데이트했습니다. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "주의점수", + "metric.cache_ratio": "캐시 비율", + "metric.cache_trend": "캐시 추세", + "metric.cached_input": "캐시된 입력", + "metric.codex_credits": "Codex 크레딧", + "metric.context_trend": "컨텍스트 추세", + "metric.context_use": "컨텍스트 사용", + "metric.estimated_cost": "예상 비용", + "metric.input_tokens": "입력 토큰", + "metric.last_call_input": "마지막 호출 입력", + "metric.last_call_total": "마지막 호출 합계", + "metric.max_context_use": "최대 컨텍스트 사용", + "metric.output": "출력", + "metric.output_tokens": "출력 토큰", + "metric.reasoning_output": "추론 출력", + "metric.remaining_usage": "남은 사용량", + "metric.session_cumulative": "세션 누적", + "metric.total": "합계", + "metric.total_tokens": "총 토큰", + "metric.uncached_input": "캐시되지 않은 입력", + "metric.usage_credits": "사용량 크레딧", + "metric.usage_remaining": "남은 사용량", + "metric.visible_calls": "보이는 호출", + "nav.history": "역사", + "nav.live": "라이브", + "nav.load": "로드", + "option.active_sessions_only": "활성 세션만", + "option.all_confidence": "모든 자신감", + "option.all_efforts": "모든 노력", + "option.all_history": "모든 역사", + "option.all_models": "모든 모델", + "option.all_time": "모든 시간", + "option.custom_range": "맞춤 범위", + "option.estimated_cost": "예상 비용", + "option.estimated_credit_mapping": "예상 신용 매핑", + "option.exact_cost": "정확한 비용", + "option.exact_credit_rate": "정확한 신용등급", + "option.highest_codex_credits": "최고 Codex 크레딧", + "option.highest_context_use": "가장 높은 컨텍스트 사용", + "option.highest_estimated_cost": "최고 예상 비용", + "option.last_7_days": "지난 7일", + "option.load_10000": "10,000 호출", + "option.load_20000": "20,000 호출", + "option.load_5000": "5,000 호출", + "option.load_all": "모든 호출", + "option.lowest_cache_ratio": "최저 캐시 비율", + "option.missing_credit_rate": "누락된 신용등급", + "option.most_signals": "대부분의 신호", + "option.most_tokens": "대부분의 토큰", + "option.needs_attention": "주의가 필요함", + "option.newest_calls": "최신 호출", + "option.this_month": "이번 달", + "option.this_week": "이번주", + "option.thread_name": "스레드 이름", + "option.today": "오늘", + "option.unpriced_cost": "가격이 정해지지 않은 비용", + "option.user_credit_override": "사용자 신용 재정의", + "parser.warnings_title": "최근 새로 고침으로 보고된 {count} 파서 진단: {entries}. 스키마 드리프트를 조사하려면 codex-usage-tracker inspect-log 를 실행하세요.", + "preset.cache_misses": "캐시 미스", + "preset.cache_misses_caption": "캐시 미스 사전 설정", + "preset.cache_misses_desc": "cwd, 모델 및 스레드별로 그룹화된 낮은 캐시 비율 호출입니다.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", - "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "preset.context_bloat": "컨텍스트 팽창", + "preset.context_bloat_caption": "컨텍스트 팽창 사전 설정", + "preset.context_bloat_desc": "60% 이상의 컨텍스트 사용 또는 매우 높은 누적 토큰을 호출합니다.", + "preset.description": "일반적인 사용 질문에 대한 원클릭 시작점입니다.", + "preset.estimated_price_review": "예상 가격 검토", + "preset.estimated_price_review_caption": "예상 가격 검토 사전 설정", + "preset.estimated_price_review_desc": "표시된 최선의 추정 추정치로 사용량 가격이 책정됩니다.", + "preset.highest_codex_credits": "최고 Codex 크레딧", + "preset.highest_codex_credits_caption": "최고 Codex 크레딧 사전 설정", + "preset.highest_codex_credits_desc": "Codex 사용 허용량에 대한 예상 영향을 기준으로 호출가 정렬되었습니다.", + "preset.highest_cost_threads": "최고 비용 스레드", + "preset.highest_cost_threads_caption": "최고 비용 스레드 사전 설정", + "preset.highest_cost_threads_desc": "하위 에이전트가 연결된 상태로 예상 지출을 기준으로 정렬된 스레드입니다.", + "preset.investigation_presets": "조사 사전 설정", + "preset.no_preset": "사전 설정이 적용되지 않았습니다.", + "preset.pricing_gaps": "가격 격차", + "preset.pricing_gaps_caption": "가격 격차 사전 설정", + "preset.pricing_gaps_desc": "예상 총 비용을 불완전하게 만드는 가격이 책정되지 않은 사용량입니다.", + "pricing.configure_hint": "codex-usage-tracker update-pricing을 실행하여 예상 비용을 구성하세요.", + "pricing.fetched": "{time}을(를) 가져왔습니다.", + "pricing.pinned": "고정된 스냅샷", + "pricing.source": "가격 소스", + "pricing.tier": "{tier} 등급", + "pricing.title": "{parts}. 내부 Codex 라벨은 표시된 최선의 추측을 사용할 수 있습니다.{warning}", + "pricing.title_fetched": "{parts}. {time}의 {url}에서 가져왔습니다. 내부 Codex 라벨은 표시된 최선의 추측을 사용할 수 있습니다.{warning}", + "privacy.aliases_preserved": "구성된 프로젝트 별칭은 명시적 표시 선택으로 처리됩니다.", + "privacy.cwd_redacted": "원시 cwd 경로가 수정되었습니다.", + "privacy.git_branch_hidden": "Git 분기가 숨겨져 있습니다.", + "privacy.git_remote_label_hidden": "Git 원격 레이블이 숨겨져 있습니다.", + "privacy.mode": "프로젝트 메타데이터 개인정보 보호 모드: {mode}.", + "privacy.normal_title": "프로젝트 메타데이터는 로컬 cwd, 프로젝트, 분기 및 구성된 라벨과 함께 표시됩니다.", + "privacy.project_names_redacted": "이름이 없는 프로젝트는 안정적인 해시 라벨을 사용합니다.", + "privacy.relative_cwd_hidden": "상대 cwd가 숨겨져 있습니다.", + "privacy.tags_hidden": "프로젝트 태그가 숨겨져 있습니다.", + "recommendation.context_bloat.action": "이전 컨텍스트가 더 이상 관련이 없으면 새로운 Codex 스레드를 시작하는 것이 좋습니다.", + "recommendation.context_bloat.title": "높은 맥락 압박", + "recommendation.context_bloat.why": "이 호출은 모델 컨텍스트 창의 많은 부분을 사용하고 있습니다.", + "recommendation.elevated_context.action": "작업을 더 추가하기 전에 스레드를 좁힐 수 있는지 확인하십시오.", + "recommendation.elevated_context.title": "상황에 대한 압박감 증가", + "recommendation.elevated_context.why": "컨텍스트 사용이 증가하고 나중에 비용이 많이 들 수 있습니다.", + "recommendation.estimated_pricing.action": "가격 범위를 검토하고 이 호출이 중요한 경우 모델 요율을 고정하거나 재정의합니다.", + "recommendation.estimated_pricing.title": "예상 가격", + "recommendation.estimated_pricing.why": "이 비용은 직접 가격 책정 행이 아닌 추론된 모델 매핑을 사용합니다.", + "recommendation.high_cost.action": "계속하기 전에 스레드 타임라인을 열고 이전 턴을 검사하세요.", + "recommendation.high_cost.title": "예상 비용이 높음", + "recommendation.high_cost.why": "이 호출는 구성된 높은 비용 임계값을 초과했습니다.", + "recommendation.large_thread.action": "관련 없는 후속 작업에는 새 스레드를 선호합니다.", + "recommendation.large_thread.title": "큰 누적 스레드", + "recommendation.large_thread.why": "세션 누적 합계는 나중에 비용이 많이 들 정도로 높습니다.", + "recommendation.low_cache.action": "파일, 도구 출력 또는 광범위한 컨텍스트가 불필요하게 다시 도입되었는지 확인하십시오.", + "recommendation.low_cache.title": "낮은 캐시 재사용", + "recommendation.low_cache.why": "캐시되지 않은 새로운 입력은 높지만 캐시 재사용은 낮습니다.", + "recommendation.low_output.action": "먼저 집계 컨텍스트를 검사하세요. 원인이 불분명한 경우에만 원시 컨텍스트를 로드하세요.", + "recommendation.low_output.title": "대규모 저출력 호출", + "recommendation.low_output.why": "호출은 많은 토큰을 소비했지만 출력은 거의 생성되지 않았습니다.", + "recommendation.none.action": "집계 작업에는 플래그가 지정되지 않습니다. 사용 패턴을 계속 모니터링하세요.", + "recommendation.pricing_gap.action": "총 비용을 신뢰하기 전에 가격을 업데이트하거나 로컬 별칭을 추가하세요.", + "recommendation.pricing_gap.title": "가격 격차", + "recommendation.pricing_gap.why": "이 모델 호출에는 구성된 가격이 없으므로 총 비용은 눈에 보이는 사용량을 과소평가합니다.", + "recommendation.reasoning_spike.action": "이 작업에 선택된 추론 노력이 필요한지 검토하세요.", + "recommendation.reasoning_spike.title": "높은 추론 점유율", + "recommendation.reasoning_spike.why": "추론 결과가 이 호출에 대한 시각적 출력을 지배합니다.", + "recommendation.subagent_attribution.action": "워크플로를 변경하기 전에 첨부된 하위 에이전트와 직접 호출를 비교하거나 호출를 검토하세요.", + "recommendation.subagent_attribution.title": "하위 에이전트 속성", + "recommendation.subagent_attribution.why": "이 호출은 위임된 작업에 첨부되며 상위 스레드 증가를 설명할 수 있습니다.", + "section.allowance": "수당", + "section.needs_attention": "주의가 필요함", + "section.pricing": "가격", + "section.recommendations": "권장사항", + "severity.high": "높음", + "severity.medium": "중간", + "severity.review": "검토", + "source.auto_review": "자동 검토", + "source.codex_initiated": "Codex 시작됨", + "source.subagent": "하위 에이전트", + "source.subagent_role": "하위 에이전트: {role}", + "source.user": "사용자", + "source.user_initiated": "사용자가 시작함", + "state.allowance_config_error": "허용량 구성 오류", + "state.allowance_configured": "허용량이 구성됨", + "state.best_guess_estimate": "최선의 추측", + "state.configured": "구성됨", + "state.configured_price": "구성된 가격", + "state.error": "오류", + "state.estimated": "추정", + "state.loading": "로드 중", + "state.loading_rows": "행 로드 중", + "state.mixed": "혼합", + "state.no": "아니요", + "state.no_calls": "현재 필터와 일치하는 호출가 없습니다.", + "state.no_configured_price": "구성된 가격 없음", + "state.no_context_entries": "이 호출에 대한 컨텍스트 항목을 찾을 수 없습니다.", + "state.no_data": "데이터 없음", + "state.no_mapped_rate": "매핑된 요금 없음", + "state.no_price": "가격 없음", + "state.no_rate": "요금 없음", + "state.no_rows": "행 없음", + "state.no_threads": "현재 필터와 일치하는 스레드가 없습니다.", + "state.none": "없음", + "state.not_configured": "구성되지 않음", + "state.requires_evidence": "증거가 필요함", + "state.unknown": "알 수 없음", + "state.yes": "예", + "status.checking": "확인 중", + "status.paused": "일시중지됨", + "status.refresh_error": "새로고침 오류", + "status.refreshing": "상쾌함", + "status.reloading": "다시 로드 중", + "status.static": "정적", + "status.updated": "업데이트됨", + "table.cache": "캐시", + "table.cached": "캐시됨", + "table.calls": "전화", + "table.cost": "비용", + "table.effort": "노력", + "table.initiated": "개시됨", + "table.last_call": "마지막 호출", + "table.model": "모델", + "table.more_efforts": "{effort} +{count} 노력", + "table.more_models": "{model} +{count} 모델", + "table.output": "출력", + "table.page_status": "{total} 중 {start}-{end} {items} · 페이지 {page}/{pages}", + "table.rows": "행", + "table.signals": "신호", + "table.source": "소스", + "table.thread": "스레드", + "table.threads": "스레드", + "table.time": "시간", + "table.tokens": "토큰", + "table.uncached": "캐시되지 않음", + "table.visible_status": "{total} {items} 중 {end} 표시 중", + "thread.attached": "첨부", + "thread.attention": "주의 {score}", + "thread.auto_review": "{count} 자동 검토", + "thread.collapse": "접기", + "thread.direct": "직접", + "thread.expand": "펼치기", + "thread.expand_label": "{action} {thread} 전화입니다. 관심 점수 {score}.", + "thread.explicit_parent": "명시적인 부모", + "thread.explicit_parent_thread": "명시적 상위 스레드", + "thread.parent": "상위 {id}", + "thread.session": "세션", + "thread.spawned": "산란", + "thread.spawned_from": "{thread}에서 생성됨", + "thread.spawned_threads": "{count} 생성된 스레드", + "thread.subagent": "{count} 하위 에이전트", + "thread.unknown": "알 수 없는 스레드", + "thread.unmatched_subagent": "일치하지 않는 하위 에이전트" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/pt.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/pt.json index ef2ecf4..c56d5ed 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/pt.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/pt.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "Verifique se há arquivos reintroduzidos ou saída de ferramenta após a reutilização do cache ser descartada.", + "action.compare_fresh_input": "Compare a nova entrada com o turno anterior antes de continuar.", + "action.compare_subagent_calls": "Compare o subagente anexado ou revise as chamadas antes de alterar o fluxo de trabalho pai.", + "action.configure_pricing": "Configure os preços antes de confiar nos totais de custos.", + "action.copied": "Copiado", + "action.copy_failed": "Falha na cópia", + "action.expand_or_select_recommendations": "Expanda as chamadas ou selecione uma linha para recomendações no nível da chamada.", + "action.exported": "Exportado {count}", + "action.inspect_thread_timeline": "Inspecione a linha do tempo do tópico e considere iniciar um novo tópico.", + "action.review_context_growth": "Revise onde começa o crescimento do contexto e considere iniciar um novo tópico.", + "action.review_reasoning_effort": "Revise se o esforço de raciocínio é apropriado para esta tarefa.", + "action.run": "Corre", + "action.set_limits": "Definir limites", + "action.use_aggregate_first": "Use os campos agregados primeiro; carregue o contexto apenas se o sinal ainda não estiver claro.", + "allowance.counted": "{value} créditos contados para os limites de uso de Codex", + "allowance.cr_left": "{value} cr esquerda", + "allowance.credit_coverage": "Cobertura de crédito {ratio} de tokens carregados.", + "allowance.credit_rates": "Taxas de crédito: {source}.", + "allowance.credits_remaining": "{value} créditos restantes", + "allowance.init_hint": "Execute codex-usage-tracker init-allowance para adicionar janelas de uso restantes.", + "allowance.of_allowance": "{ratio} de subsídio", + "allowance.of_total": "{used} de {total} créditos", + "allowance.rate_card_error": "Erro na tabela de preços: {error}", + "allowance.remaining": "{value} restante", + "allowance.resets": "Reinicializações: {resets}", + "allowance.row_no_rate": "Nenhuma taxa de crédito Codex mapeada", + "allowance.title_hint": "Adicione ~/.codex-usage-tracker/allowance.json para mostrar o uso restante semanal e de 5 horas.", + "allowance.used_vs_remaining": "{used} usado vs {remaining} restante", + "allowance.window_configured": "{label} configurado", + "allowance.windows": "Janelas de permissão: {windows}", + "aria.current_view_actions": "Ações de visualização atual", + "aria.dashboard_status": "Status do painel", + "aria.dashboard_view": "Visualização do painel", + "aria.history_title": "Somente sessões ativas é o padrão. Todo o histórico verifica os logs de sessão arquivados durante a atualização ao vivo.", + "aria.inspect_thread": "Inspecione o uso de {thread}", + "aria.refresh_controls": "Controles de atualização do painel", + "aria.table_pages": "Páginas de tabela", + "badge.costs": "Custos", + "badge.credits": "Créditos", + "badge.live": "Ao vivo", + "badge.metadata_mode": "Metadados {mode}", + "badge.metadata_normal": "Metadados normais", + "badge.no_costs": "Sem custos", + "badge.parser_warnings": "Avisos do analisador", + "badge.static": "Estático", + "badge.unofficial_project": "Projeto não oficial", + "badge.unofficial_project_title": "Codex Usage Tracker é independente e não é feito, afiliado, endossado, patrocinado ou apoiado por OpenAI. OpenAI e Codex são marcas registradas de OpenAI.", + "button.back_to_dashboard": "Voltar ao painel", + "button.clear": "Limpar", + "button.copy_link": "Copiar link", + "button.enable_context_loading": "Habilitar carregamento de contexto", + "button.export_csv": "Exportar CSV", + "button.full_serialized_analysis": "Execute uma análise serializada completa", + "button.hide_details": "Ocultar detalhes", + "button.hide_tool_output": "Ocultar saída da ferramenta", + "button.include_tool_output": "Incluir saída da ferramenta", + "button.load_context": "Carregar contexto", + "button.load_more": "Carregar mais", + "button.load_older_context": "Carregar entradas mais antigas", + "button.next": "Próximo", + "button.next_call": "Próxima chamada", + "button.no_char_limit": "Sem limite de caracteres", + "button.open_investigator": "Investigador aberto", + "button.previous": "Anterior", + "button.previous_call": "Chamada anterior", + "button.refresh": "Atualizar", + "button.show_compaction_history": "Mostrar substituição compactada", + "button.show_tool_output": "Mostrar saída da ferramenta", + "button.show_turn_evidence": "Mostrar evidências do registro de giro", + "button.top": "Superior", + "call.cache_accounting_delta": "Delta de cache/contabilidade", + "call.cache_cold": "Currículo frio/cache obsoleto", + "call.cache_diagnostics": "Diagnóstico de cache", + "call.cache_partial": "Falta parcial de cache", + "call.cache_spike": "Pico não armazenado em cache", + "call.cache_steady": "Perfil de cache estável", + "call.cache_warm": "Reutilização de cache quente", + "call.compaction_diagnostics": "Diagnóstico de compactação", + "call.compaction_hint": "A evidência carregada pode mostrar eventos de compactação explícitos. O histórico de substituição redigido é mostrado somente após a ação de substituição compactada.", + "call.context_estimate": "Estimativa de mudança de contexto", + "call.context_estimate_hint": "Compare a entrada exata não armazenada em cache com a evidência de log visível contada pelo tokenizador. A lacuna deve ser tratada como erro oculto de andaime, serialização ou estimativa do tokenizador.", + "call.derived_label": "Derivado de chamadas agregadas adjacentes", + "call.estimated_label": "Estimado a partir do volume de log visível", + "call.evidence_label": "Evidência de tempo de execução", + "call.exact_accounting": "Contabilidade de token exata", + "call.exact_label": "Exato do retorno de chamada do token", + "call.hidden_estimate": "Estimativa de entrada oculta/serializada inexplicável", + "call.no_previous": "Nenhuma chamada anterior neste tópico resolvido.", + "call.not_found": "A chamada selecionada não foi encontrada nas linhas carregadas do painel.", + "call.open_hint": "Clique em uma linha de chamada para diagnósticos aprofundados.", + "call.position": "Chame {position} neste tópico resolvido.", + "call.post_compaction": "Pós-compactação possível", + "call.raw_evidence": "Evidência bruta", + "call.remaining_after_serialized": "Restante após o limite serializado", + "call.remaining_after_serialized_hint": "Entrada não armazenada em cache não coberta nem mesmo pelo limite superior serializado", + "call.serialized_bound_hint": "Estrutura JSONL local de limite superior; texto de prompt não exato.", + "call.serialized_breakdown": "Grupos de evidência serializada", + "call.serialized_bucket_detail": "{count} campos · {chars} caracteres", + "call.serialized_candidate": "Possível sobrecarga serializada", + "call.serialized_candidate_hint": "Limite superior serializado menos estimativa visível, limitado pela entrada exata não armazenada em cache", + "call.serialized_deferred": "Estimativa rápida carregada; o agrupamento serializado completo fica adiado.", + "call.serialized_quick_hint": "estimativa rápida", + "call.serialized_upper_bound": "Limite superior local serializado", + "call.visible_estimate": "Nova estimativa de contexto visível", + "call.visible_gap": "Entrada não armazenada em cache menos estimativa visível", + "caption.ascending": "ascendente", + "caption.call_investigator": "Investigando ligue para {record}.", + "caption.calls": "Mostrando chamadas de modelo individuais classificadas por {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "descendente", + "caption.initial_calls": "Mostrando chamadas de modelo individuais.", + "caption.insights": "Classificado por custo, créditos de uso, reutilização de cache, pressão de contexto e confiança nos preços.", + "caption.loaded": "{loaded} chamadas carregadas", + "caption.loaded_capped": "{loaded} de {available} chamadas carregadas", + "caption.rows_loaded_progress": "Linhas carregadas: {loaded} de {total}", + "caption.rows_loading_background": "Os totais do dashboard estão prontos. As linhas estão carregando em segundo plano.", + "caption.rows_loading_progress": "Carregando linhas: {loaded} de {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", + "caption.threads": "Mostrando threads {threads} de chamadas filtradas {calls}, classificadas por {sort}. {loaded}. Clique em um tópico para expandir suas chamadas.", + "context.api_http": "O contexto API retornou HTTP {status}.", + "context.api_unavailable": "O contexto API não está disponível aqui. Execute codex-usage-tracker serve-dashboard --open para carregamento de contexto sob demanda.", + "context.auto_loading": "Carregando evidências de curva selecionada com saída de ferramenta incluída.", + "context.chars_omitted": "{count} caracteres acima do orçamento omitidos.", + "context.compaction_detected": "Compactação detectada", + "context.compaction_replacement": "Contexto de substituição compactado", + "context.compaction_replacement_count": "{count} entradas do histórico de substituição disponíveis.", + "context.disabled_hint": "O carregamento de contexto está desativado para este servidor de painel. Habilite-o aqui para carregar o contexto JSONL local sob demanda.", + "context.enabled_note": "O carregamento de contexto está ativado. Pressione Mostrar evidência de registro de turno para ler esta chamada da fonte JSONL local.", + "context.file_hint": "Abra este painel com codex-usage-tracker serve-dashboard para carregar o contexto bruto sob demanda.", + "context.line": "linha {line}", + "context.loading": "Carregando contexto local...", + "context.local_redacted": "Contexto JSONL local carregado sob demanda. Os prompts e a saída da ferramenta são redigidos para padrões secretos comuns e não são persistidos no SQLite ou no painel HTML.", + "context.no_char_limit_active": "Nenhum limite de caracteres aplicado.", + "context.no_record_id": "Esta linha não possui ID de registro para pesquisa de contexto.", + "context.no_response": "Nenhum corpo de resposta", + "context.older_omitted": "{count} entradas mais antigas omitidas.", + "context.ready_hint": "O contexto não está incorporado neste painel. Pressione um botão para ler esta chamada da fonte JSONL local.", + "context.settings_http": "As configurações de contexto retornaram HTTP {status}.", + "context.source": "Fonte: {file}:{line}", + "context.token_breakdown": "Detalhamento do token", + "context.token_cached": "Em cache", + "context.token_input": "Entrada", + "context.token_output": "Saída", + "context.token_reasoning": "Raciocínio", + "context.token_required": "O carregamento de contexto requer um token localhost do painel API.", + "context.token_scope_call": "Esta chamada", + "context.token_scope_earlier": "Contagem de tokens anterior no mesmo turno", + "context.token_scope_previous": "Contagem de tokens anterior no mesmo turno", + "context.token_scope_selected": "Contagem de tokens de chamada selecionados", + "context.token_scope_session": "Sessão cumulativa", "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "context.token_type": "Tipo", + "context.token_uncached": "Sem cache", + "context.tool_included": "Saída da ferramenta incluída com redação e limites de tamanho.", + "context.tool_omitted": "Saída da ferramenta oculta para esta visualização.", + "credit.configured_rate": "Taxa configurada", + "credit.estimated_mapping": "Mapeamento estimado", + "credit.inferred_mapping": "Mapeamento de modelo inferido", + "credit.no_mapped_rate": "Nenhuma taxa mapeada", + "credit.no_rate": "Sem taxa de crédito", + "credit.official_match": "Correspondência oficial da tabela de preços", + "credit.user_rate": "Taxa de crédito fornecida pelo usuário", + "credit.with_status": "{value} créditos · {status}", + "dashboard.call_details": "Detalhes da chamada", + "dashboard.detail.empty": "Passe o mouse ou clique em uma linha para inspecionar os campos de uso agregados.", + "dashboard.eyebrow": "Análise Codex local", + "dashboard.local_storage_note": "O cabeçalho do painel também lembra localmente sua escolha de idioma.", + "dashboard.model_calls": "Chamadas de modelo", + "dashboard.title": "Painel de uso", + "dashboard.top_threads_by_attention": "Principais tópicos por pontuação de atenção", + "dashboard.view.call": "Chamar investigador", + "dashboard.view.calls": "Chamadas", + "dashboard.view.insights": "Informações", + "dashboard.view.threads": "Tópicos", + "date.custom": "Personalizado", + "date.invalid_range": "Período inválido", + "date.range_between": "{prefix} {start} a {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", + "date.range_from": "{prefix} de {start}", + "date.range_through": "{prefix} até {end}", + "detail.agent_nickname": "Apelido do agente", + "detail.agent_role": "Função do agente", + "detail.allowance_impact": "Impacto do subsídio", + "detail.attached_calls": "Chamadas anexadas", + "detail.auto_review_calls": "Revisão automática de chamadas", + "detail.cache_savings": "Economia de cache", + "detail.call_number": "ligue para {number}", + "detail.calls": "Chamadas", + "detail.context_window": "Janela de contexto", + "detail.cost_usage_context": "Custo, uso e contexto", + "detail.credit_confidence": "Confiança de crédito", + "detail.credit_model": "Modelo de crédito", + "detail.credit_note": "Nota de crédito", + "detail.credit_source": "Fonte de crédito", + "detail.credit_source_fetched": "Fonte de crédito obtida", + "detail.credit_tier": "Nível de crédito", "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", + "detail.efficiency_signals": "Sinais de eficiência", + "detail.first_expensive_turn": "Primeira curva cara", + "detail.git_branch": "Ramo Git", + "detail.largest_cumulative_jump": "Maior salto cumulativo", + "detail.latest_activity": "Última atividade", + "detail.model_mix": "Mistura de modelos", + "detail.next_action": "Próxima ação", + "detail.no_above_thresholds": "Nenhum acima dos limites", + "detail.no_aggregate_action": "Nenhuma ação agregada é sinalizada.", + "detail.parent_session": "Sessão pai", + "detail.parent_thread": "Tópico pai", + "detail.parent_updated": "Pai atualizado", + "detail.pricing_model": "Modelo de preços", + "detail.pricing_status": "Status de preços", + "detail.project_cwd": "Projeto cwd", + "detail.project_tags": "Tags do projeto", + "detail.raw_identifiers": "Identificadores agregados brutos", + "detail.reasoning_mix": "Mistura de raciocínio", + "detail.relationships": "Relacionamentos", + "detail.remote_hash": "Hash remoto", + "detail.remote_label": "Etiqueta remota", + "detail.secondary_thread_fields": "Campos de thread secundários", + "detail.source_file_line": "Arquivo fonte e linha", + "detail.source_line": "Linha de origem", + "detail.spawned_child_calls": "Chamadas de crianças geradas", + "detail.spawned_from": "Gerado de", + "detail.spawned_threads": "Tópicos gerados", + "detail.subagent_before_spike": "Subagente antes do pico", + "detail.subagent_calls": "Chamadas de subagente", + "detail.subagent_type": "Tipo de subagente", + "detail.thread_attachment": "Anexo de linha", + "detail.thread_attention_summary": "Resumo de atenção do tópico", + "detail.thread_lifecycle": "Ciclo de vida do encadeamento", + "detail.thread_narrative": "Narrativa do tópico", + "detail.thread_source": "Fonte do tópico", + "detail.thread_timeline": "Linha do tempo do tópico", + "detail.timeline_context": "contexto {value}", + "detail.timeline_empty": "Nenhuma chamada neste tópico.", "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", + "detail.timestamp": "Carimbo de data e hora", + "detail.token_pricing_breakdown": "Detalhamento de token e preços", + "detail.tokens_at": "{tokens} tokens em {time}", + "detail.turn": "Vire", + "detail.why_flagged": "Por que sinalizado", + "docs.dashboard_guide": "Guia do painel", + "effort.high": "alto", + "effort.low": "baixo", + "effort.medium": "médio", + "filter.confidence": "Confiança", + "filter.effort": "Raciocínio", + "filter.end": "Fim", + "filter.model": "Modelo", + "filter.project": "Projeto", + "filter.reasoning": "Raciocínio", + "filter.search": "Pesquisar", + "filter.search_placeholder": "Tópico, cwd, modelo", + "filter.session": "Sessão", + "filter.sort": "Classificar", + "filter.start": "Começar", + "filter.thread": "Tópico", + "filter.time": "Hora", + "flag.elevated_context_use": "Uso elevado de contexto", + "flag.expensive_low_output_call": "Chamada cara de baixa saída", + "flag.high_context_use": "Uso de alto contexto", + "flag.high_estimated_cost": "Alto custo estimado", + "flag.high_reasoning_share": "Alto compartilhamento de raciocínio", + "flag.low_cache_reuse": "Baixa reutilização de cache", + "history.active_hidden": "Somente sessões ativas; {count} chamadas arquivadas ocultas", + "history.active_only": "Somente sessões ativas", + "history.all_empty": "Todo histórico selecionado; nenhuma chamada arquivada foi indexada ainda", + "history.all_includes": "Todo o histórico inclui chamadas arquivadas {count}", + "history.archived_scan_hint": "{detail}. As sessões arquivadas são verificadas somente quando Todo o histórico é selecionado durante a atualização ao vivo.", + "insight.apply_cache_misses": "Aplicar predefinição de erros de cache", + "insight.apply_context_bloat": "Aplicar predefinição de inchaço de contexto", + "insight.codex_allowance_usage": "Codex uso de permissão", + "insight.context_bloat": "Inchaço de contexto", + "insight.context_bloat_body": "As chamadas {calls} são iguais ou superiores ao uso do contexto {ratio}.", + "insight.costliest_thread": "Tópico mais caro", + "insight.costliest_thread_body": "{thread} tem chamadas {calls} e tokens {tokens}.", + "insight.credit_coverage_body": "{ratio} dos tokens visíveis são mapeados para taxas de crédito Codex.", + "insight.estimated_pricing": "Preço estimado", + "insight.estimated_pricing_body": "Os preços marcados como melhor estimativa estão incluídos, mas devem ser revisados separadamente.", + "insight.inspect_selected_call": "Inspecionar chamada selecionada", + "insight.low_cache_reuse": "Baixa reutilização de cache", + "insight.low_cache_reuse_body": "As chamadas {calls} estão sob reutilização de cache {ratio}. Comece com {thread}.", + "insight.open_thread_timeline": "Linha do tempo do tópico aberto", + "insight.reasoning_output_spike": "Pico de saída de raciocínio", + "insight.reasoning_spike_body": "{thread} possui a maior chamada de saída de raciocínio no filtro atual.", + "insight.review_estimates": "Revise as estimativas", + "insight.review_highest_credit": "Revise as chamadas de maior crédito", + "insight.review_pricing_gaps": "Revise as lacunas de preços", + "insight.unpriced_usage": "Uso sem preço", + "insight.unpriced_usage_body": "Esses tokens são omitidos dos totais de custos estimados até que o preço seja configurado.", + "language.english": "Inglês", + "language.label": "Idioma", "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", + "live.checking_usage": "Verificando novo uso...", + "live.every": "Atualização ao vivo a cada {seconds}s", + "live.history_static_hint": "Execute codex-usage-tracker serve-dashboard para alternar entre sessões ativas e todo o histórico do painel.", + "live.indexed": "Linhas agregadas {rows} indexadas de logs {files}.", + "live.load_static_hint": "Execute codex-usage-tracker serve-dashboard para carregar um tamanho de histórico diferente do painel.", + "live.loading_rows": "Carregando linhas em segundo plano...", + "live.paused": "Atualização ao vivo pausada", + "live.refresh_suffix": ". Recarregue esta página após regenerar um painel estático ou execute codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "Atualização ao vivo indisponível: {message}{suffix}", + "live.refreshing_index": "Atualizando índice de uso local...", + "live.reloading_static": "Recarregando instantâneo do painel estático...", + "live.skipped": "Ignorados {count} eventos de contagem de tokens malformados.", + "live.updated_detail": "Atualizado {time}. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "Pontuação de atenção", + "metric.cache_ratio": "Proporção de cache", + "metric.cache_trend": "Tendência de cache", + "metric.cached_input": "Entrada em cache", + "metric.codex_credits": "Codex créditos", + "metric.context_trend": "Tendência de contexto", + "metric.context_use": "Uso de contexto", + "metric.estimated_cost": "Custo estimado", + "metric.input_tokens": "Tokens de entrada", + "metric.last_call_input": "Entrada da última chamada", + "metric.last_call_total": "Total da última chamada", + "metric.max_context_use": "Uso máximo de contexto", + "metric.output": "Saída", + "metric.output_tokens": "Tokens de saída", + "metric.reasoning_output": "Saída de raciocínio", + "metric.remaining_usage": "Uso restante", + "metric.session_cumulative": "Sessão cumulativa", "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "metric.total_tokens": "Totais de tokens", + "metric.uncached_input": "Entrada sem cache", + "metric.usage_credits": "Créditos de uso", + "metric.usage_remaining": "Uso restante", + "metric.visible_calls": "Chamadas visíveis", + "nav.history": "História", + "nav.live": "Ao vivo", + "nav.load": "Carregar", + "option.active_sessions_only": "Somente sessões ativas", + "option.all_confidence": "Toda confiança", + "option.all_efforts": "Todos os esforços", + "option.all_history": "Toda a história", + "option.all_models": "Todos os modelos", + "option.all_time": "Todo o tempo", + "option.custom_range": "Intervalo personalizado", + "option.estimated_cost": "Custo estimado", + "option.estimated_credit_mapping": "Mapeamento de crédito estimado", + "option.exact_cost": "Custo exato", + "option.exact_credit_rate": "Taxa de crédito exata", + "option.highest_codex_credits": "Maiores créditos Codex", + "option.highest_context_use": "Maior uso de contexto", + "option.highest_estimated_cost": "Custo estimado mais alto", + "option.last_7_days": "Últimos 7 dias", + "option.load_10000": "10.000 chamadas", + "option.load_20000": "20.000 ligações", + "option.load_5000": "5.000 ligações", + "option.load_all": "Todas as chamadas", + "option.lowest_cache_ratio": "Taxa de cache mais baixa", + "option.missing_credit_rate": "Taxa de crédito ausente", + "option.most_signals": "A maioria dos sinais", + "option.most_tokens": "A maioria dos tokens", + "option.needs_attention": "Precisa de atenção", + "option.newest_calls": "Últimas chamadas", + "option.this_month": "Este mês", + "option.this_week": "Esta semana", + "option.thread_name": "Nome do tópico", + "option.today": "Hoje", + "option.unpriced_cost": "Custo sem preço", + "option.user_credit_override": "Substituição de crédito do usuário", + "parser.warnings_title": "Última atualização relatada diagnóstico do analisador {count}: {entries}. Execute codex-usage-tracker inspect-log para investigar desvios de esquema.", + "preset.cache_misses": "Faltas de cache", + "preset.cache_misses_caption": "Cache perde predefinição", + "preset.cache_misses_desc": "Chamadas com baixa taxa de cache agrupadas por cwd, modelo e thread.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", + "preset.context_bloat": "Inchaço de contexto", + "preset.context_bloat_caption": "Predefinição de inchaço de contexto", + "preset.context_bloat_desc": "Chamadas com mais de 60% de uso de contexto ou com tokens cumulativos muito altos.", + "preset.description": "Pontos de partida com um clique para perguntas comuns de uso.", + "preset.estimated_price_review": "Revisão de preço estimado", + "preset.estimated_price_review_caption": "Predefinição de revisão de preço estimado", + "preset.estimated_price_review_desc": "Preço de uso com estimativas de melhor estimativa marcadas.", + "preset.highest_codex_credits": "Maiores créditos Codex", + "preset.highest_codex_credits_caption": "Predefinição de créditos Codex mais altos", + "preset.highest_codex_credits_desc": "Chamadas classificadas por impacto estimado na permissão de uso de Codex.", + "preset.highest_cost_threads": "Threads de maior custo", + "preset.highest_cost_threads_caption": "Predefinição de threads de maior custo", + "preset.highest_cost_threads_desc": "Tópicos classificados por gasto estimado, com subagentes anexados.", + "preset.investigation_presets": "Predefinições de investigação", + "preset.no_preset": "Nenhuma predefinição aplicada.", + "preset.pricing_gaps": "Lacunas de preços", + "preset.pricing_gaps_caption": "Predefinição de lacunas de preços", + "preset.pricing_gaps_desc": "Uso sem preço que torna os totais de custos estimados incompletos.", + "pricing.configure_hint": "Execute codex-usage-tracker update-pricing para configurar os custos estimados.", + "pricing.fetched": "buscado {time}", + "pricing.pinned": "instantâneo fixado", + "pricing.source": "Fonte de preços", + "pricing.tier": "{tier} nível", + "pricing.title": "{parts}. Rótulos internos Codex podem usar estimativas de melhor estimativa marcadas.{warning}", + "pricing.title_fetched": "{parts}. Obtido de {url} em {time}. Rótulos internos Codex podem usar estimativas de melhor estimativa marcadas.{warning}", + "privacy.aliases_preserved": "Os aliases de projeto configurados são tratados como opções de exibição explícitas.", + "privacy.cwd_redacted": "Os caminhos cwd brutos são editados.", + "privacy.git_branch_hidden": "A ramificação Git está oculta.", + "privacy.git_remote_label_hidden": "Os rótulos remotos do Git estão ocultos.", + "privacy.mode": "Modo de privacidade de metadados do projeto: {mode}.", + "privacy.normal_title": "Os metadados do projeto são mostrados com cwd local, projeto, ramificação e rótulos configurados.", + "privacy.project_names_redacted": "Projetos sem nome usam rótulos com hash estáveis.", + "privacy.relative_cwd_hidden": "O cwd relativo está oculto.", + "privacy.tags_hidden": "As tags do projeto estão ocultas.", + "recommendation.context_bloat.action": "Considere iniciar um novo tópico Codex se o contexto antigo não for mais relevante.", + "recommendation.context_bloat.title": "Alta pressão de contexto", + "recommendation.context_bloat.why": "Esta chamada está usando uma grande parte da janela de contexto do modelo.", + "recommendation.elevated_context.action": "Verifique se o tópico pode ser reduzido antes de adicionar mais trabalho.", + "recommendation.elevated_context.title": "Pressão de contexto elevada", + "recommendation.elevated_context.why": "O uso do contexto é elevado e pode se tornar caro em turnos posteriores.", + "recommendation.estimated_pricing.action": "Revise a cobertura de preços e fixe ou substitua a taxa do modelo se esta chamada for importante.", + "recommendation.estimated_pricing.title": "Preço estimado", + "recommendation.estimated_pricing.why": "Este custo utiliza um mapeamento de modelo inferido em vez de uma linha de preços direta.", + "recommendation.high_cost.action": "Abra a linha do tempo do tópico e inspecione o turno anterior antes de continuar.", + "recommendation.high_cost.title": "Alto custo estimado", + "recommendation.high_cost.why": "Esta chamada ultrapassou o limite de alto custo configurado.", + "recommendation.large_thread.action": "Prefira um novo tópico para trabalhos de acompanhamento não relacionados.", + "recommendation.large_thread.title": "Grande thread cumulativo", + "recommendation.large_thread.why": "O total acumulado da sessão é alto o suficiente para tornar dispendiosas as trocas posteriores.", + "recommendation.low_cache.action": "Verifique se os arquivos, a saída da ferramenta ou o contexto amplo foram reintroduzidos desnecessariamente.", + "recommendation.low_cache.title": "Baixa reutilização de cache", + "recommendation.low_cache.why": "A entrada recente não armazenada em cache é alta, enquanto a reutilização do cache é baixa.", + "recommendation.low_output.action": "Inspecione primeiro o contexto agregado; carregue o contexto bruto somente se a causa não for clara.", + "recommendation.low_output.title": "Grande chamada de baixa saída", + "recommendation.low_output.why": "A chamada consumiu muitos tokens, mas produziu pouca saída.", + "recommendation.none.action": "Nenhuma ação agregada é sinalizada; continue monitorando os padrões de uso.", + "recommendation.pricing_gap.action": "Atualize os preços ou adicione um alias local antes de confiar nos totais de custos.", + "recommendation.pricing_gap.title": "Diferença de preços", + "recommendation.pricing_gap.why": "Esta chamada de modelo não tem preço configurado, portanto, os totais de custos subestimam o uso visível.", + "recommendation.reasoning_spike.action": "Revise se esta tarefa precisa do esforço de raciocínio selecionado.", + "recommendation.reasoning_spike.title": "Alto compartilhamento de raciocínio", + "recommendation.reasoning_spike.why": "A saída do raciocínio domina a saída visível para esta chamada.", + "recommendation.subagent_attribution.action": "Compare chamadas diretas com subagentes anexados ou revise chamadas antes de alterar o fluxo de trabalho.", + "recommendation.subagent_attribution.title": "Atribuição de subagente", + "recommendation.subagent_attribution.why": "Esta chamada está anexada ao trabalho delegado e pode explicar o crescimento do thread pai.", + "section.allowance": "Subsídio", + "section.needs_attention": "Precisa de atenção", + "section.pricing": "Preços", + "section.recommendations": "Recomendações", + "severity.high": "Alto", + "severity.medium": "Médio", + "severity.review": "Revisão", + "source.auto_review": "Revisão automática", + "source.codex_initiated": "Codex iniciado", + "source.subagent": "Subagente", + "source.subagent_role": "Subagente: {role}", + "source.user": "Usuário", + "source.user_initiated": "Iniciado pelo usuário", + "state.allowance_config_error": "Erro de configuração de permissão", + "state.allowance_configured": "Subsídio configurado", + "state.best_guess_estimate": "Estimativa de melhor palpite", + "state.configured": "Configurado", + "state.configured_price": "Preço configurado", + "state.error": "Erro", + "state.estimated": "Estimado", + "state.loading": "Carregando", + "state.loading_rows": "Carregando linhas", + "state.mixed": "Misto", + "state.no": "Não", + "state.no_calls": "Nenhuma chamada corresponde aos filtros atuais.", + "state.no_configured_price": "Nenhum preço configurado", + "state.no_context_entries": "Nenhuma entrada de contexto encontrada para esta chamada.", + "state.no_data": "Sem dados", + "state.no_mapped_rate": "Nenhuma taxa mapeada", + "state.no_price": "Sem preço", + "state.no_rate": "Sem taxa", + "state.no_rows": "Nenhuma linha", + "state.no_threads": "Nenhum thread corresponde aos filtros atuais.", + "state.none": "Nenhum", + "state.not_configured": "Não configurado", + "state.requires_evidence": "Evidências necessárias", + "state.unknown": "Desconhecido", + "state.yes": "Sim", + "status.checking": "Verificando", + "status.paused": "Pausado", + "status.refresh_error": "Erro de atualização", + "status.refreshing": "Refrescante", + "status.reloading": "Recarregando", + "status.static": "Estático", + "status.updated": "Atualizado", "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", + "table.cached": "Em cache", + "table.calls": "chamadas", + "table.cost": "Custo", + "table.effort": "Esforço", + "table.initiated": "Iniciado", + "table.last_call": "Última chamada", + "table.model": "Modelo", + "table.more_efforts": "{effort} +{count} esforços", + "table.more_models": "Modelos {model} +{count}", + "table.output": "Saída", + "table.page_status": "{start}-{end} de {total} {items} · página {page}/{pages}", + "table.rows": "linhas", + "table.signals": "Sinais", + "table.source": "Fonte", + "table.thread": "Tópico", + "table.threads": "tópicos", + "table.time": "Hora", "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "table.uncached": "Sem cache", + "table.visible_status": "Mostrando {end} de {total} {items}", + "thread.attached": "anexado", + "thread.attention": "atenção {score}", + "thread.auto_review": "{count} revisão automática", + "thread.collapse": "Recolher", + "thread.direct": "direto", + "thread.expand": "Expandir", + "thread.expand_label": "{action} {thread} chama. Pontuação de atenção {score}.", + "thread.explicit_parent": "pai explícito", + "thread.explicit_parent_thread": "thread pai explícito", + "thread.parent": "Pai {id}", + "thread.session": "sessão", + "thread.spawned": "gerado", + "thread.spawned_from": "gerado de {thread}", + "thread.spawned_threads": "{count} tópicos gerados", + "thread.subagent": "{count} subagente", + "thread.unknown": "Tópico desconhecido", + "thread.unmatched_subagent": "subagente incomparável" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/ru.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/ru.json index ef2ecf4..2f541f8 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/ru.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/ru.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "Проверьте наличие вновь появившихся файлов или результатов инструмента после прекращения повторного использования кэша.", + "action.compare_fresh_input": "Прежде чем продолжить, сравните новый ввод с предыдущим ходом.", + "action.compare_subagent_calls": "Сравните прикрепленный субагент или просмотрите вызовы, прежде чем менять родительский рабочий процесс.", + "action.configure_pricing": "Настройте цены, прежде чем доверять итоговым затратам.", + "action.copied": "Скопировано", + "action.copy_failed": "Не удалось скопировать", + "action.expand_or_select_recommendations": "Разверните звонки или выберите строку для рекомендаций на уровне звонков.", + "action.exported": "Экспортировано {count}", + "action.inspect_thread_timeline": "Просмотрите хронологию обсуждения и рассмотрите возможность создания новой темы.", + "action.review_context_growth": "Посмотрите, где начинается рост контекста, и подумайте о том, чтобы начать новую тему.", + "action.review_reasoning_effort": "Проверьте, подходят ли рассуждения для этой задачи.", + "action.run": "Беги", + "action.set_limits": "Установить ограничения", + "action.use_aggregate_first": "Сначала используйте агрегированные поля; загружайте контекст только в том случае, если сигнал все еще неясен.", + "allowance.counted": "Кредитов {value} засчитывается в лимиты использования Codex.", + "allowance.cr_left": "{value} cr осталось", + "allowance.credit_coverage": "Кредитное покрытие {ratio} загруженных токенов.", + "allowance.credit_rates": "Кредитные ставки: {source}.", + "allowance.credits_remaining": "Осталось {value} кредитов", + "allowance.init_hint": "Запустите codex-usage-tracker init-allowance, чтобы добавить оставшиеся окна использования.", + "allowance.of_allowance": "{ratio} пособия", + "allowance.of_total": "{used} из {total} кредитов", + "allowance.rate_card_error": "Ошибка прейскуранта: {error}.", + "allowance.remaining": "осталось {value}", + "allowance.resets": "Сбрасывает: {resets}", + "allowance.row_no_rate": "Нет сопоставленной кредитной ставки Codex", + "allowance.title_hint": "Добавьте ~/.codex-usage-tracker/allowance.json, чтобы показать оставшееся использование за 5 часов и за неделю.", + "allowance.used_vs_remaining": "Использовано {used} и осталось {remaining}", + "allowance.window_configured": "{label} настроен", + "allowance.windows": "Окна пособий: {windows}", + "aria.current_view_actions": "Текущие действия просмотра", + "aria.dashboard_status": "Статус информационной панели", + "aria.dashboard_view": "Вид панели мониторинга", + "aria.history_title": "По умолчанию используются только активные сеансы. Вся история сканирует архивные журналы сеансов во время живого обновления.", + "aria.inspect_thread": "Проверьте использование {thread}", + "aria.refresh_controls": "Элементы управления обновлением информационной панели", + "aria.table_pages": "Страницы таблицы", + "badge.costs": "Затраты", + "badge.credits": "Кредиты", + "badge.live": "Живи", + "badge.metadata_mode": "Метаданные {mode}", + "badge.metadata_normal": "Метаданные в норме", + "badge.no_costs": "Никаких затрат", + "badge.parser_warnings": "Предупреждения парсера", + "badge.static": "Статический", + "badge.unofficial_project": "Неофициальный проект", + "badge.unofficial_project_title": "Codex Usage Tracker является независимым и не создается, не связан, не одобрен, не спонсируется и не поддерживается OpenAI. OpenAI и Codex являются товарными знаками OpenAI.", + "button.back_to_dashboard": "Вернуться к панели управления", + "button.clear": "Очистить", + "button.copy_link": "Копировать ссылку", + "button.enable_context_loading": "Включить загрузку контекста", + "button.export_csv": "Экспортировать CSV", + "button.full_serialized_analysis": "Запустите полный сериализованный анализ", + "button.hide_details": "Скрыть детали", + "button.hide_tool_output": "Скрыть вывод инструмента", + "button.include_tool_output": "Включить выходные данные инструмента", + "button.load_context": "Загрузить контекст", + "button.load_more": "Загрузить больше", + "button.load_older_context": "Загрузить старые записи", + "button.next": "Далее", + "button.next_call": "Следующий звонок", + "button.no_char_limit": "Нет ограничения по количеству символов", + "button.open_investigator": "Открытый следователь", + "button.previous": "Предыдущий", + "button.previous_call": "Предыдущий звонок", + "button.refresh": "Обновить", + "button.show_compaction_history": "Показать сжатую замену", + "button.show_tool_output": "Показать выходные данные инструмента", + "button.show_turn_evidence": "Показать доказательства журнала поворотов", + "button.top": "Топ", + "call.cache_accounting_delta": "Разница в кэше/учётной записи", + "call.cache_cold": "Холодное возобновление/устаревший кэш", + "call.cache_diagnostics": "Диагностика кэша", + "call.cache_partial": "Частичный промах в кэше", + "call.cache_spike": "Некэшированный пик", + "call.cache_steady": "Стабильный профиль кэша", + "call.cache_warm": "Теплое повторное использование кэша", + "call.compaction_diagnostics": "Диагностика уплотнения", + "call.compaction_hint": "Загруженные данные могут указывать на явные события уплотнения. Отредактированная история замен отображается только после уплотненного действия по замене.", + "call.context_estimate": "Оценка изменения контекста", + "call.context_estimate_hint": "Сравните точные некэшированные входные данные с видимыми данными журнала, подсчитанными токенизатором. Разрыв следует рассматривать как скрытую ошибку, сериализацию или оценку токенизатора.", + "call.derived_label": "Получено из соседних агрегатных вызовов", + "call.estimated_label": "Оценка на основе видимого объема журнала.", + "call.evidence_label": "Доказательства во время выполнения", + "call.exact_accounting": "Точный учет токенов", + "call.exact_label": "Точно из обратного вызова токена", + "call.hidden_estimate": "Необъяснимая скрытая/сериализованная входная оценка", + "call.no_previous": "В этой решенной теме предыдущих звонков не было.", + "call.not_found": "Выбранный звонок не найден в загруженных строках панели.", + "call.open_hint": "Щелкните строку вызова для углубленной диагностики.", + "call.position": "Позвоните {position} в этой решенной теме.", + "call.post_compaction": "Возможно пост-уплотнение", + "call.raw_evidence": "Необработанные доказательства", + "call.remaining_after_serialized": "Оставшееся после сериализованной привязки", + "call.remaining_after_serialized_hint": "Некэшированный ввод не покрывается даже сериализованной верхней границей", + "call.serialized_bound_hint": "Локальная структура JSONL с верхней границей; не точный текст подсказки.", + "call.serialized_breakdown": "Группы сериализованных данных", + "call.serialized_bucket_detail": "{count} полей · {chars} символов", + "call.serialized_candidate": "Возможные сериализованные накладные расходы", + "call.serialized_candidate_hint": "Сериализованная верхняя граница минус видимая оценка, ограниченная точным некешированным вводом", + "call.serialized_deferred": "Быстрая оценка загружена; полный анализ сериализованных групп отложен.", + "call.serialized_quick_hint": "быстрая оценка", + "call.serialized_upper_bound": "Сериализованная локальная верхняя граница", + "call.visible_estimate": "Видимая новая оценка контекста", + "call.visible_gap": "Некэшированные входные данные минус видимая оценка", + "caption.ascending": "восходящий", + "caption.call_investigator": "Расследование звонка {record}.", + "caption.calls": "Показаны отдельные вызовы моделей, отсортированные по {sort}. {loaded}.", + "caption.date_prefix": "{label}.", + "caption.descending": "нисходящий", + "caption.initial_calls": "Показаны звонки отдельных моделей.", + "caption.insights": "Ранжирование по стоимости, кредитам за использование, повторному использованию кэша, контекстному давлению и достоверности цен.", + "caption.loaded": "{loaded} вызовов загружено", + "caption.loaded_capped": "Загружено {loaded} из {available} вызовов", + "caption.rows_loaded_progress": "Строки загружены: {loaded} из {total}", + "caption.rows_loading_background": "Итоги панели готовы. Строки загружаются в фоновом режиме.", + "caption.rows_loading_progress": "Загрузка строк: {loaded} из {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "Показаны потоки {threads} из отфильтрованных вызовов {calls}, отсортированные по {sort}. {loaded}. Щелкните цепочку, чтобы развернуть ее вызовы.", + "context.api_http": "Контекст API вернул HTTP {status}.", + "context.api_unavailable": "Контекст API здесь недоступен. Запустите codex-usage-tracker serve-dashboard --open для загрузки контекста по требованию.", + "context.auto_loading": "Загрузка данных выбранного поворота с включением выходных данных инструмента.", + "context.chars_omitted": "{count} символы превышения бюджета опущены.", + "context.compaction_detected": "Обнаружено уплотнение", + "context.compaction_replacement": "Сжатый контекст замены", + "context.compaction_replacement_count": "Доступны {count} записи истории замен.", + "context.disabled_hint": "Загрузка контекста отключена для этого сервера информационной панели. Включите его здесь, чтобы загружать локальный контекст JSONL по требованию.", + "context.enabled_note": "Загрузка контекста включена. Нажмите «Показать журнал поворотов», чтобы прочитать этот звонок из местного источника JSONL.", + "context.file_hint": "Откройте эту панель мониторинга с помощью codex-usage-tracker serve-dashboard, чтобы загрузить необработанный контекст по требованию.", + "context.line": "строка {line}", + "context.loading": "Загрузка локального контекста...", + "context.local_redacted": "Локальный контекст JSONL загружается по требованию. Подсказки и выходные данные инструмента редактируются с учетом общих секретных шаблонов и не сохраняются в SQLite или на информационной панели HTML.", + "context.no_char_limit_active": "Ограничение на количество символов не применяется.", + "context.no_record_id": "Эта строка не имеет идентификатора записи для контекстного поиска.", + "context.no_response": "Нет тела ответа", + "context.older_omitted": "{count} старые записи опущены.", + "context.ready_hint": "Контекст не встроен в эту панель мониторинга. Нажмите кнопку, чтобы прочитать этот вызов из локального источника JSONL.", + "context.settings_http": "Настройки контекста вернули HTTP {status}.", + "context.source": "Источник: {file}:{line}", + "context.token_breakdown": "Разбивка токенов", + "context.token_cached": "Кэшированный", + "context.token_input": "Ввод", + "context.token_output": "Выход", + "context.token_reasoning": "Рассуждение", + "context.token_required": "Для загрузки контекста требуется токен информационной панели localhost API.", + "context.token_scope_call": "Этот звонок", + "context.token_scope_earlier": "Более раннее количество жетонов в том же ходу", + "context.token_scope_previous": "Предыдущее количество жетонов в том же ходу", + "context.token_scope_selected": "Выбранное количество токенов вызова", + "context.token_scope_session": "Совокупный сеанс", + "context.token_total": "Итого", + "context.token_type": "Тип", + "context.token_uncached": "Некэшированный", + "context.tool_included": "Вывод инструмента включен с ограничениями на редактирование и размер.", + "context.tool_omitted": "Выходные данные инструмента скрыты для этого вида.", + "credit.configured_rate": "Настроенная ставка", + "credit.estimated_mapping": "Предполагаемое картографирование", + "credit.inferred_mapping": "Предполагаемое сопоставление модели", + "credit.no_mapped_rate": "Нет сопоставленной ставки", + "credit.no_rate": "Нет кредитной ставки", + "credit.official_match": "Официальное совпадение прейскурантов", + "credit.user_rate": "Кредитная ставка, предоставляемая пользователем", + "credit.with_status": "{value} кредитов · {status}", + "dashboard.call_details": "Детали звонка", + "dashboard.detail.empty": "Наведите указатель мыши или щелкните строку, чтобы просмотреть совокупные поля использования.", + "dashboard.eyebrow": "Локальная аналитика Codex", + "dashboard.local_storage_note": "Заголовок панели управления также запоминает ваш локальный выбор языка.", + "dashboard.model_calls": "Модельные звонки", + "dashboard.title": "Панель использования", + "dashboard.top_threads_by_attention": "Самые популярные темы по оценке внимания", + "dashboard.view.call": "Вызов следователя", + "dashboard.view.calls": "Звонки", + "dashboard.view.insights": "Информация", + "dashboard.view.threads": "Темы", + "date.custom": "Пользовательский", + "date.invalid_range": "Неверный диапазон дат.", + "date.range_between": "От {prefix} от {start} до {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", - "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", - "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", + "date.range_from": "{prefix} из {start}", + "date.range_through": "с {prefix} по {end}", + "detail.agent_nickname": "Ник агента", + "detail.agent_role": "Роль агента", + "detail.allowance_impact": "Влияние пособий", + "detail.attached_calls": "Прикрепленные звонки", + "detail.auto_review_calls": "Автоматический просмотр звонков", + "detail.cache_savings": "Экономия кэша", + "detail.call_number": "позвони {number}", + "detail.calls": "Звонки", + "detail.context_window": "Контекстное окно", + "detail.cost_usage_context": "Стоимость, использование и контекст", + "detail.credit_confidence": "Кредитная уверенность", + "detail.credit_model": "Кредитная модель", + "detail.credit_note": "Кредит-нота", + "detail.credit_source": "Источник кредита", + "detail.credit_source_fetched": "Источник кредита получен.", + "detail.credit_tier": "Кредитный уровень", + "detail.cwd": "Квд", + "detail.efficiency_signals": "Сигналы эффективности", + "detail.first_expensive_turn": "Первый дорогой поворот", + "detail.git_branch": "Git-ветвь", + "detail.largest_cumulative_jump": "Самый большой совокупный прыжок", + "detail.latest_activity": "Последняя активность", + "detail.model_mix": "Модельный микс", + "detail.next_action": "Следующее действие", + "detail.no_above_thresholds": "Нет превышения пороговых значений", + "detail.no_aggregate_action": "Никакие совокупные действия не отмечены.", + "detail.parent_session": "Родительская сессия", + "detail.parent_thread": "Родительский поток", + "detail.parent_updated": "Родительский обновлен", + "detail.pricing_model": "Модель ценообразования", + "detail.pricing_status": "Статус цен", + "detail.project_cwd": "Проект cwd", + "detail.project_tags": "Теги проекта", + "detail.raw_identifiers": "Необработанные совокупные идентификаторы", + "detail.reasoning_mix": "Смесь рассуждений", + "detail.relationships": "Отношения", + "detail.remote_hash": "Удаленный хеш", + "detail.remote_label": "Удаленная метка", + "detail.secondary_thread_fields": "Поля вторичного потока", + "detail.source_file_line": "Исходный файл и строка", + "detail.source_line": "Исходная строка", + "detail.spawned_child_calls": "Порожденные дочерние звонки", + "detail.spawned_from": "Порожден из", + "detail.spawned_threads": "Порожденные темы", + "detail.subagent_before_spike": "Субагент до всплеска", + "detail.subagent_calls": "Звонки субагента", + "detail.subagent_type": "Тип субагента", + "detail.thread_attachment": "Крепление резьбы", + "detail.thread_attention_summary": "Сводка внимания к теме", + "detail.thread_lifecycle": "Жизненный цикл потока", + "detail.thread_narrative": "Рассказ о теме", + "detail.thread_source": "Источник темы", + "detail.thread_timeline": "Хронология темы", + "detail.timeline_context": "контекст {value}", + "detail.timeline_empty": "В этой теме нет звонков.", + "detail.timeline_meta": "токены {tokens} · {cost} · {credits} · кэш {cache}", + "detail.timestamp": "Временная метка", + "detail.token_pricing_breakdown": "Токен и разбивка цен", + "detail.tokens_at": "{tokens} токенов на {time}", + "detail.turn": "Поворот", + "detail.why_flagged": "Почему отмечено", + "docs.dashboard_guide": "Руководство по информационной панели", + "effort.high": "высокий", + "effort.low": "низкий", + "effort.medium": "средний", + "filter.confidence": "Уверенность", + "filter.effort": "Рассуждение", + "filter.end": "Конец", + "filter.model": "Модель", + "filter.project": "Проект", + "filter.reasoning": "Рассуждение", + "filter.search": "Поиск", + "filter.search_placeholder": "Тема, cwd, модель", + "filter.session": "Сессия", + "filter.sort": "Сортировать", + "filter.start": "Старт", + "filter.thread": "Тема", + "filter.time": "Время", + "flag.elevated_context_use": "Использование повышенного контекста", + "flag.expensive_low_output_call": "Дорогой звонок с низкой производительностью", + "flag.high_context_use": "Высокое использование контекста", + "flag.high_estimated_cost": "Высокая ориентировочная стоимость", + "flag.high_reasoning_share": "Высокая доля аргументов", + "flag.low_cache_reuse": "Низкое повторное использование кэша", + "history.active_hidden": "Только активные сессии; {count} архивированные звонки скрыты", + "history.active_only": "Только активные сеансы", + "history.all_empty": "Выбрана вся история; ни один заархивированный звонок еще не проиндексирован", + "history.all_includes": "Вся история включает в себя {count} заархивированные звонки.", + "history.archived_scan_hint": "{detail}. Архивированные сеансы сканируются только в том случае, если во время оперативного обновления выбрана опция «Вся история».", + "insight.apply_cache_misses": "Применить настройки промахов в кэше", + "insight.apply_context_bloat": "Применить настройки раздувания контекста", + "insight.codex_allowance_usage": "Codex использование квоты", + "insight.context_bloat": "Раздувание контекста", + "insight.context_bloat_body": "Вызовы {calls} находятся на уровне использования контекста {ratio} или выше.", + "insight.costliest_thread": "Самая дорогая нить", + "insight.costliest_thread_body": "{thread} имеет вызовы {calls} и токены {tokens}.", + "insight.credit_coverage_body": "{ratio} видимых токенов соответствуют кредитным ставкам Codex.", + "insight.estimated_pricing": "Ориентировочная цена", + "insight.estimated_pricing_body": "Отмеченные наиболее вероятные цены включены, но их следует рассматривать отдельно.", + "insight.inspect_selected_call": "Проверить выбранный звонок", + "insight.low_cache_reuse": "Низкое повторное использование кэша", + "insight.low_cache_reuse_body": "Вызовы {calls} подлежат повторному использованию кэша {ratio}. Начните с {thread}.", + "insight.open_thread_timeline": "Открыть хронологию обсуждения", + "insight.reasoning_output_spike": "Обоснование скачка производительности", + "insight.reasoning_spike_body": "{thread} имеет самый большой вызов вывода рассуждений в текущем фильтре.", + "insight.review_estimates": "Просмотрите оценки", + "insight.review_highest_credit": "Просмотрите звонки с самой высокой оценкой", + "insight.review_pricing_gaps": "Анализ ценовых различий", + "insight.unpriced_usage": "Бесплатное использование", + "insight.unpriced_usage_body": "Эти токены не включаются в расчетную общую стоимость до тех пор, пока не будет настроена цена.", + "language.english": "английский", + "language.label": "Язык", + "language.vietnamese": "Тианг Вьет", + "live.checking_usage": "Проверка нового использования...", + "live.every": "Живое обновление каждые {seconds} с.", + "live.history_static_hint": "Запустите codex-usage-tracker serve-dashboard, чтобы переключаться между активными сеансами и всей историей с панели управления.", + "live.indexed": "Индексированные агрегированные строки {rows} из журналов {files}.", + "live.load_static_hint": "Запустите codex-usage-tracker serve-dashboard, чтобы загрузить историю другого размера с панели управления.", + "live.loading_rows": "Строки загружаются в фоновом режиме...", + "live.paused": "Обновление в реальном времени приостановлено", + "live.refresh_suffix": ". Перезагрузите эту страницу после повторного создания статической информационной панели или запустите codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "Обновление в реальном времени недоступно: {message}{suffix}.", + "live.refreshing_index": "Обновление локального индекса использования...", + "live.reloading_static": "Перезагрузка статического снимка информационной панели...", + "live.skipped": "Пропущены события неправильного подсчета токенов {count}.", + "live.updated_detail": "Обновлен {time}. {loaded}. {history}.{indexed}{skipped}", + "metric.attention_score": "Оценка внимания", + "metric.cache_ratio": "Коэффициент кэша", + "metric.cache_trend": "Тенденция кэширования", + "metric.cached_input": "Кэшированный ввод", + "metric.codex_credits": "Codex кредитов", + "metric.context_trend": "Контекстная тенденция", + "metric.context_use": "Использование контекста", + "metric.estimated_cost": "Ориентировочная стоимость", + "metric.input_tokens": "Входные токены", + "metric.last_call_input": "Ввод последнего звонка", + "metric.last_call_total": "Итого последнего звонка", + "metric.max_context_use": "Максимальное использование контекста", + "metric.output": "Выход", + "metric.output_tokens": "Выходные токены", + "metric.reasoning_output": "Вывод рассуждений", + "metric.remaining_usage": "Оставшееся использование", + "metric.session_cumulative": "Совокупный сеанс", + "metric.total": "Итого", + "metric.total_tokens": "Всего токенов", + "metric.uncached_input": "Некэшированный ввод", + "metric.usage_credits": "Кредиты за использование", + "metric.usage_remaining": "Оставшееся использование", + "metric.visible_calls": "Видимые звонки", + "nav.history": "История", + "nav.live": "Живи", + "nav.load": "Загрузить", + "option.active_sessions_only": "Только активные сеансы", + "option.all_confidence": "Вся уверенность", + "option.all_efforts": "Все усилия", + "option.all_history": "Вся история", + "option.all_models": "Все модели", + "option.all_time": "Все время", + "option.custom_range": "Пользовательский диапазон", + "option.estimated_cost": "Ориентировочная стоимость", + "option.estimated_credit_mapping": "Расчетное кредитное картирование", + "option.exact_cost": "Точная стоимость", + "option.exact_credit_rate": "Точная кредитная ставка", + "option.highest_codex_credits": "Наивысшие баллы Codex", + "option.highest_context_use": "Максимальное использование контекста", + "option.highest_estimated_cost": "Самая высокая ориентировочная стоимость", + "option.last_7_days": "Последние 7 дней", + "option.load_10000": "10 000 звонков", + "option.load_20000": "20 000 звонков", + "option.load_5000": "5000 звонков", + "option.load_all": "Все звонки", + "option.lowest_cache_ratio": "Самый низкий коэффициент кэширования", + "option.missing_credit_rate": "Отсутствует кредитная ставка", + "option.most_signals": "Большинство сигналов", + "option.most_tokens": "Большинство токенов", + "option.needs_attention": "Требует внимания", + "option.newest_calls": "Новейшие звонки", + "option.this_month": "В этом месяце", + "option.this_week": "На этой неделе", + "option.thread_name": "Название темы", + "option.today": "Сегодня", + "option.unpriced_cost": "Стоимость без цены", + "option.user_credit_override": "Переопределение кредита пользователя", + "parser.warnings_title": "Последнее обновление сообщило о диагностике парсера {count}: {entries}. Запустите codex-usage-tracker inspect-log , чтобы исследовать смещение схемы.", + "preset.cache_misses": "Кэш промахивается", + "preset.cache_misses_caption": "В кэше отсутствует предустановка", + "preset.cache_misses_desc": "Вызовы с низким коэффициентом кэширования, сгруппированные по cwd, модели и потоку.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", - "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "preset.context_bloat": "Раздувание контекста", + "preset.context_bloat_caption": "Предварительная настройка раздувания контекста", + "preset.context_bloat_desc": "Вызовы с использованием контекста более 60 % или с очень высоким совокупным количеством токенов.", + "preset.description": "Отправные точки для ответов на распространенные вопросы в один клик.", + "preset.estimated_price_review": "Обзор ориентировочной цены", + "preset.estimated_price_review_caption": "Предварительная настройка обзора ориентировочной цены", + "preset.estimated_price_review_desc": "Стоимость использования указана с пометкой «наилучшее предположение».", + "preset.highest_codex_credits": "Наивысшие баллы Codex", + "preset.highest_codex_credits_caption": "Предустановка максимального количества кредитов Codex", + "preset.highest_codex_credits_desc": "Звонки отсортированы по предполагаемому влиянию на лимит использования Codex.", + "preset.highest_cost_threads": "Самые дорогие темы", + "preset.highest_cost_threads_caption": "Предварительная настройка потоков с самой высокой стоимостью", + "preset.highest_cost_threads_desc": "Темы отсортированы по предполагаемым расходам с прикрепленными субагентами.", + "preset.investigation_presets": "Предварительные настройки расследования", + "preset.no_preset": "Никакая предустановка не применена.", + "preset.pricing_gaps": "Ценовые разрывы", + "preset.pricing_gaps_caption": "Предустановленные ценовые разрывы", + "preset.pricing_gaps_desc": "Использование без оплаты, что делает расчетные общие затраты неполными.", + "pricing.configure_hint": "Запустите codex-usage-tracker update-pricing, чтобы настроить предполагаемые затраты.", + "pricing.fetched": "получено {time}", + "pricing.pinned": "закрепленный снимок", + "pricing.source": "Источник цен", + "pricing.tier": "{tier} уровень", + "pricing.title": "{parts}. Внутренние метки Codex могут использовать отмеченные оценки наилучшего предположения.{warning}", + "pricing.title_fetched": "{parts}. Получено с {url} в {time}. Внутренние метки Codex могут использовать отмеченные оценки наилучшего предположения.{warning}", + "privacy.aliases_preserved": "Настроенные псевдонимы проектов рассматриваются как явные разрешения на отображение.", + "privacy.cwd_redacted": "Необработанные пути cwd отредактированы.", + "privacy.git_branch_hidden": "Ветка Git скрыта.", + "privacy.git_remote_label_hidden": "Удаленные метки Git скрыты.", + "privacy.mode": "Режим конфиденциальности метаданных проекта: {mode}.", + "privacy.normal_title": "Метаданные проекта отображаются с локальными метками cwd, проект, ветвь и настроенными.", + "privacy.project_names_redacted": "Безымянные проекты используют стабильные хэш-метки.", + "privacy.relative_cwd_hidden": "Относительный cwd скрыт.", + "privacy.tags_hidden": "Теги проекта скрыты.", + "recommendation.context_bloat.action": "Рассмотрите возможность создания новой темы Codex, если старый контекст больше не актуален.", + "recommendation.context_bloat.title": "Высокое контекстное давление", + "recommendation.context_bloat.why": "Этот вызов использует большую часть контекстного окна модели.", + "recommendation.elevated_context.action": "Прежде чем добавлять дополнительную работу, проверьте, можно ли сузить нить.", + "recommendation.elevated_context.title": "Повышенное контекстное давление", + "recommendation.elevated_context.why": "Использование контекста увеличивается и может стать дорогостоящим на последующих ходах.", + "recommendation.estimated_pricing.action": "Просмотрите ценовое покрытие и закрепите или отмените модельную ставку, если этот вызов имеет значение.", + "recommendation.estimated_pricing.title": "Ориентировочная цена", + "recommendation.estimated_pricing.why": "Эта стоимость использует предполагаемое сопоставление модели, а не строку прямой цены.", + "recommendation.high_cost.action": "Откройте временную шкалу потока и просмотрите предыдущий ход, прежде чем продолжить.", + "recommendation.high_cost.title": "Высокая ориентировочная стоимость", + "recommendation.high_cost.why": "Этот вызов превысил настроенный порог высокой стоимости.", + "recommendation.large_thread.action": "Предпочитайте новую тему для несвязанной последующей работы.", + "recommendation.large_thread.title": "Большая накопительная нить", + "recommendation.large_thread.why": "Совокупная сумма сеанса достаточно велика, чтобы сделать последующие повороты дорогостоящими.", + "recommendation.low_cache.action": "Проверьте, не были ли файлы, выходные данные инструмента или широкий контекст повторно введены без необходимости.", + "recommendation.low_cache.title": "Низкое повторное использование кэша", + "recommendation.low_cache.why": "Свежий некэшированный ввод имеет высокий уровень, а повторное использование кэша низкое.", + "recommendation.low_output.action": "Сначала проверьте совокупный контекст; загружать необработанный контекст только в том случае, если причина неясна.", + "recommendation.low_output.title": "Большой вызов с низкой производительностью", + "recommendation.low_output.why": "Вызов потреблял много токенов, но давал мало результатов.", + "recommendation.none.action": "Никакое совокупное действие не отмечено; продолжайте отслеживать модели использования.", + "recommendation.pricing_gap.action": "Обновите цены или добавьте локальный псевдоним, прежде чем доверять итоговым затратам.", + "recommendation.pricing_gap.title": "Ценовой разрыв", + "recommendation.pricing_gap.why": "Для этого вызова модели не настроена цена, поэтому общая стоимость занижает видимое использование.", + "recommendation.reasoning_spike.action": "Проверьте, требует ли эта задача выбранных рассуждений.", + "recommendation.reasoning_spike.title": "Высокая доля аргументов", + "recommendation.reasoning_spike.why": "Вывод рассуждения доминирует над видимым выводом для этого вызова.", + "recommendation.subagent_attribution.action": "Сравните прямые вызовы с прикрепленными субагентами или просмотрите вызовы, прежде чем менять рабочий процесс.", + "recommendation.subagent_attribution.title": "Атрибуция субагента", + "recommendation.subagent_attribution.why": "Этот вызов связан с делегированной работой и может объяснить рост родительского потока.", + "section.allowance": "Пособие", + "section.needs_attention": "Требует внимания", + "section.pricing": "Цены", + "section.recommendations": "Рекомендации", + "severity.high": "Высокий", + "severity.medium": "Средний", + "severity.review": "Обзор", + "source.auto_review": "Автоматическое рассмотрение", + "source.codex_initiated": "Codex запущено", + "source.subagent": "Субагент", + "source.subagent_role": "Субагент: {role}", + "source.user": "Пользователь", + "source.user_initiated": "Пользователь инициировал", + "state.allowance_config_error": "Ошибка конфигурации допусков", + "state.allowance_configured": "Допуск настроен", + "state.best_guess_estimate": "Оптимальная оценка", + "state.configured": "Настроен", + "state.configured_price": "Настроенная цена", + "state.error": "Ошибка", + "state.estimated": "Предполагаемый", + "state.loading": "Загрузка", + "state.loading_rows": "Загрузка строк", + "state.mixed": "Смешанный", + "state.no": "Нет", + "state.no_calls": "Нет вызовов, соответствующих текущим фильтрам.", + "state.no_configured_price": "Нет настроенной цены", + "state.no_context_entries": "Для этого вызова не найдено записей контекста.", + "state.no_data": "Нет данных", + "state.no_mapped_rate": "Нет сопоставленной ставки", + "state.no_price": "Нет цены", + "state.no_rate": "Нет ставки", + "state.no_rows": "Нет строк", + "state.no_threads": "Ни одна тема не соответствует текущим фильтрам.", + "state.none": "Нет", + "state.not_configured": "Не настроено", + "state.requires_evidence": "Необходимы доказательства", + "state.unknown": "Неизвестно", + "state.yes": "Да", + "status.checking": "Проверка", + "status.paused": "Приостановлено", + "status.refresh_error": "Ошибка обновления", + "status.refreshing": "Освежающий", + "status.reloading": "Перезарядка", + "status.static": "Статический", + "status.updated": "Обновлено", + "table.cache": "Кэш", + "table.cached": "Кэшированный", + "table.calls": "звонки", + "table.cost": "Стоимость", + "table.effort": "Усилие", + "table.initiated": "Инициировано", + "table.last_call": "Последний звонок", + "table.model": "Модель", + "table.more_efforts": "{effort} +{count} усилий", + "table.more_models": "Модели {model} +{count}", + "table.output": "Выход", + "table.page_status": "{start}-{end} из {total} {items} · страница {page}/{pages}", + "table.rows": "ряды", + "table.signals": "Сигналы", + "table.source": "Источник", + "table.thread": "Тема", + "table.threads": "нити", + "table.time": "Время", + "table.tokens": "Токены", + "table.uncached": "Некэшированный", + "table.visible_status": "Показаны {end} из {total} {items}", + "thread.attached": "прилагается", + "thread.attention": "внимание {score}", + "thread.auto_review": "{count} автопроверка", + "thread.collapse": "Свернуть", + "thread.direct": "прямой", + "thread.expand": "Развернуть", + "thread.expand_label": "{action} Звонит {thread}. Оценка внимания {score}.", + "thread.explicit_parent": "явный родительский элемент", + "thread.explicit_parent_thread": "явный родительский поток", + "thread.parent": "Родитель {id}", + "thread.session": "сессия", + "thread.spawned": "породил", + "thread.spawned_from": "порожден из {thread}", + "thread.spawned_threads": "{count} создал темы", + "thread.subagent": "{count} субагент", + "thread.unknown": "Неизвестная тема", + "thread.unmatched_subagent": "непревзойденный субагент" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/vi.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/vi.json index 3407274..e554244 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/vi.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/vi.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Kiểm tra file hoặc output công cụ được đưa lại sau khi cache giảm.", - "action.compare_fresh_input": "So sánh input mới với turn trước trước khi tiếp tục.", - "action.compare_subagent_calls": "So sánh subagent hoặc lượt gọi review đã gắn trước khi đổi workflow cha.", - "action.configure_pricing": "Cấu hình giá trước khi tin vào tổng chi phí.", + "action.check_cache_drop": "Kiểm tra các tệp được giới thiệu lại hoặc đầu ra của công cụ sau khi ngừng sử dụng lại bộ nhớ đệm.", + "action.compare_fresh_input": "So sánh đầu vào mới với lượt trước trước khi tiếp tục.", + "action.compare_subagent_calls": "So sánh đại lý phụ đính kèm hoặc xem xét các cuộc gọi trước khi thay đổi quy trình công việc chính.", + "action.configure_pricing": "Định cấu hình giá trước khi tin tưởng vào tổng chi phí.", "action.copied": "Đã sao chép", - "action.copy_failed": "Sao chép thất bại", - "action.expand_or_select_recommendations": "Mở các lượt gọi hoặc chọn một dòng để xem khuyến nghị cấp lượt gọi.", + "action.copy_failed": "Sao chép không thành công", + "action.expand_or_select_recommendations": "Mở rộng cuộc gọi hoặc chọn một hàng để xem đề xuất cấp độ cuộc gọi.", "action.exported": "Đã xuất {count}", - "action.inspect_thread_timeline": "Xem timeline cuộc trò chuyện và cân nhắc bắt đầu cuộc trò chuyện mới.", - "action.review_context_growth": "Rà soát nơi ngữ cảnh bắt đầu tăng và cân nhắc mở cuộc trò chuyện mới.", - "action.review_reasoning_effort": "Rà soát mức suy luận có phù hợp với tác vụ này không.", + "action.inspect_thread_timeline": "Kiểm tra dòng thời gian của chuỗi và cân nhắc việc bắt đầu một chuỗi mới.", + "action.review_context_growth": "Xem lại nơi bắt đầu phát triển bối cảnh và xem xét bắt đầu một chủ đề mới.", + "action.review_reasoning_effort": "Xem xét liệu nỗ lực lý luận có phù hợp với nhiệm vụ này hay không.", "action.run": "Chạy", - "action.set_limits": "Thiết lập giới hạn", - "action.use_aggregate_first": "Dùng các trường tổng hợp trước; chỉ tải ngữ cảnh nếu tín hiệu vẫn chưa rõ.", - "allowance.counted": "{value} tín dụng được tính vào giới hạn mức sử dụng Codex", - "allowance.cr_left": "còn {value} cr", - "allowance.credit_coverage": "Tỷ lệ bao phủ tín dụng {ratio} token đã tải.", - "allowance.credit_rates": "Tỷ lệ tín dụng: {source}.", - "allowance.credits_remaining": "còn {value} tín dụng", - "allowance.init_hint": "Chạy codex-usage-tracker init-allowance để thêm mức sử dụng còn lại.", - "allowance.of_allowance": "{ratio} hạn mức", - "allowance.of_total": "{used} trên {total} tín dụng", - "allowance.rate_card_error": "Lỗi bảng tỷ lệ: {error}", - "allowance.remaining": "còn {value}", - "allowance.resets": "Reset: {resets}", - "allowance.row_no_rate": "Chưa có tỷ lệ tín dụng Codex khớp", - "allowance.title_hint": "Thêm ~/.codex-usage-tracker/allowance.json để hiển thị mức sử dụng còn lại 5h và weekly.", - "allowance.used_vs_remaining": "đã dùng {used} so với còn {remaining}", - "allowance.window_configured": "{label} đã cấu hình", - "allowance.windows": "Cửa sổ hạn mức: {windows}", - "aria.current_view_actions": "Hành động cho chế độ xem hiện tại", - "aria.dashboard_status": "Trạng thái dashboard", - "aria.dashboard_view": "Chế độ xem dashboard", - "aria.history_title": "Mặc định chỉ dùng phiên đang hoạt động. Toàn bộ lịch sử sẽ quét log phiên đã lưu trữ khi live refresh.", - "aria.inspect_thread": "Xem chi tiết mức sử dụng của {thread}", - "aria.refresh_controls": "Điều khiển làm mới dashboard", + "action.set_limits": "Đặt giới hạn", + "action.use_aggregate_first": "Trước tiên hãy sử dụng các trường tổng hợp; chỉ tải bối cảnh nếu tín hiệu vẫn không rõ ràng.", + "allowance.counted": "{value} tín dụng được tính vào Codex giới hạn sử dụng", + "allowance.cr_left": "{value} cr còn lại", + "allowance.credit_coverage": "Bảo hiểm tín dụng {ratio} của token đã tải.", + "allowance.credit_rates": "Lãi suất tín dụng: {source}.", + "allowance.credits_remaining": "{value} tín dụng còn lại", + "allowance.init_hint": "Chạy codex-usage-tracker init-allowance để thêm các cửa sổ sử dụng còn lại.", + "allowance.of_allowance": "{ratio} phụ cấp", + "allowance.of_total": "{used} trong số {total} tín chỉ", + "allowance.rate_card_error": "Lỗi thẻ giá: {error}", + "allowance.remaining": "{value} còn lại", + "allowance.resets": "Đặt lại: {resets}", + "allowance.row_no_rate": "Không có tỷ lệ tín dụng Codex nào được ánh xạ", + "allowance.title_hint": "Thêm ~/.codex-usage-tracker/allowance.json để hiển thị mức sử dụng còn lại trong 5 giờ và hàng tuần.", + "allowance.used_vs_remaining": "{used} đã sử dụng so với {remaining} còn lại", + "allowance.window_configured": "{label} đã được định cấu hình", + "allowance.windows": "Khoảng thời gian phụ cấp: {windows}", + "aria.current_view_actions": "Hành động xem hiện tại", + "aria.dashboard_status": "Trạng thái trang tổng quan", + "aria.dashboard_view": "Chế độ xem trang tổng quan", + "aria.history_title": "Chỉ phiên hoạt động là mặc định. Tất cả lịch sử quét nhật ký phiên đã lưu trữ trong quá trình làm mới trực tiếp.", + "aria.inspect_thread": "Kiểm tra việc sử dụng {thread}", + "aria.refresh_controls": "Điều khiển làm mới trang tổng quan", "aria.table_pages": "Trang bảng", "badge.costs": "Chi phí", "badge.credits": "Tín dụng", "badge.live": "Trực tiếp", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata bình thường", - "badge.no_costs": "Chưa cấu hình chi phí", - "badge.parser_warnings": "Cảnh báo parser", + "badge.metadata_mode": "Siêu dữ liệu {mode}", + "badge.metadata_normal": "Siêu dữ liệu bình thường", + "badge.no_costs": "Không có chi phí", + "badge.parser_warnings": "Cảnh báo của trình phân tích cú pháp", "badge.static": "Tĩnh", "badge.unofficial_project": "Dự án không chính thức", - "badge.unofficial_project_title": "Codex Usage Tracker là dự án độc lập, không do OpenAI tạo ra, liên kết, chứng thực, tài trợ hoặc hỗ trợ. OpenAI và Codex là nhãn hiệu của OpenAI.", - "button.back_to_dashboard": "Back to dashboard", + "badge.unofficial_project_title": "Codex Usage Tracker độc lập và không được tạo ra, liên kết với, xác nhận, tài trợ hoặc hỗ trợ bởi OpenAI. OpenAI và Codex là thương hiệu của OpenAI.", + "button.back_to_dashboard": "Quay lại trang tổng quan", "button.clear": "Xóa", "button.copy_link": "Sao chép liên kết", "button.enable_context_loading": "Bật tải ngữ cảnh", "button.export_csv": "Xuất CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Gồm output công cụ", + "button.full_serialized_analysis": "Chạy phân tích tuần tự đầy đủ", + "button.hide_details": "Ẩn chi tiết", + "button.hide_tool_output": "Ẩn đầu ra công cụ", + "button.include_tool_output": "Bao gồm đầu ra công cụ", "button.load_context": "Tải ngữ cảnh", "button.load_more": "Tải thêm", - "button.load_older_context": "Tải entry cũ hơn", - "button.next": "Tiếp", - "button.next_call": "Next call", - "button.no_char_limit": "Không giới hạn ký tự", - "button.open_investigator": "Open investigator", - "button.previous": "Trước", - "button.previous_call": "Previous call", + "button.load_older_context": "Tải các mục cũ hơn", + "button.next": "Tiếp theo", + "button.next_call": "Cuộc gọi tiếp theo", + "button.no_char_limit": "Không có giới hạn char", + "button.open_investigator": "điều tra viên mở", + "button.previous": "trước đó", + "button.previous_call": "Cuộc gọi trước", "button.refresh": "Làm mới", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Hiển thị output công cụ", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Lên đầu", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", + "button.show_compaction_history": "Hiển thị thay thế nén", + "button.show_tool_output": "Hiển thị đầu ra công cụ", + "button.show_turn_evidence": "Hiển thị bằng chứng nhật ký lần lượt", + "button.top": "hàng đầu", + "call.cache_accounting_delta": "Đồng bằng bộ nhớ đệm/kế toán", + "call.cache_cold": "Sơ yếu lý lịch nguội/bộ nhớ đệm cũ", + "call.cache_diagnostics": "Chẩn đoán bộ đệm", + "call.cache_partial": "Thiếu bộ nhớ đệm một phần", + "call.cache_spike": "Tăng vọt không được lưu vào bộ nhớ đệm", + "call.cache_steady": "Cấu hình bộ đệm ổn định", + "call.cache_warm": "Tái sử dụng bộ đệm ấm", + "call.compaction_diagnostics": "Chẩn đoán nén", + "call.compaction_hint": "Bằng chứng được tải có thể hiển thị các sự kiện nén rõ ràng. Lịch sử thay thế được xử lý lại chỉ được hiển thị sau hành động thay thế được nén.", + "call.context_estimate": "Ước tính thay đổi bối cảnh", + "call.context_estimate_hint": "So sánh dữ liệu đầu vào không được lưu vào bộ nhớ đệm chính xác với bằng chứng nhật ký hiển thị được đếm bằng token. Khoảng trống phải được coi là lỗi ước tính giàn giáo ẩn, tuần tự hóa hoặc token.", + "call.derived_label": "Bắt nguồn từ các cuộc gọi tổng hợp liền kề", + "call.estimated_label": "Ước tính từ khối lượng nhật ký hiển thị", + "call.evidence_label": "Bằng chứng về thời gian chạy", + "call.exact_accounting": "Kế toán token chính xác", + "call.exact_label": "Chính xác từ cuộc gọi lại token", + "call.hidden_estimate": "Ước tính đầu vào ẩn/được tuần tự hóa không giải thích được", + "call.no_previous": "Không có cuộc gọi trước nào trong chuỗi đã giải quyết này.", + "call.not_found": "Không tìm thấy cuộc gọi đã chọn trong các hàng trên trang tổng quan đã tải.", + "call.open_hint": "Nhấp vào hàng cuộc gọi để chẩn đoán sâu.", + "call.position": "Gọi {position} trong chuỗi đã được giải quyết này.", + "call.post_compaction": "Có thể nén sau", + "call.raw_evidence": "Bằng chứng thô", + "call.remaining_after_serialized": "Còn lại sau khi được nối tiếp", + "call.remaining_after_serialized_hint": "Đầu vào không được lưu trong bộ nhớ đệm không được bao phủ ngay cả bởi giới hạn trên được tuần tự hóa", + "call.serialized_bound_hint": "Cấu trúc JSONL cục bộ giới hạn trên; văn bản nhắc nhở không chính xác.", + "call.serialized_breakdown": "Nhóm bằng chứng đã tuần tự hóa", + "call.serialized_bucket_detail": "{count} trường · {chars} ký tự", + "call.serialized_candidate": "Chi phí tuần tự có thể có", + "call.serialized_candidate_hint": "Giới hạn trên được tuần tự hóa trừ đi ước tính hiển thị, được giới hạn bởi đầu vào chính xác không được lưu vào bộ đệm", + "call.serialized_deferred": "Đã tải ước tính nhanh; phân nhóm tuần tự hóa đầy đủ được hoãn lại.", + "call.serialized_quick_hint": "ước tính nhanh", + "call.serialized_upper_bound": "Giới hạn trên cục bộ được tuần tự hóa", + "call.visible_estimate": "Ước tính bối cảnh mới có thể nhìn thấy", + "call.visible_gap": "Dữ liệu đầu vào không được lưu vào bộ nhớ đệm trừ đi ước tính hiển thị", "caption.ascending": "tăng dần", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Đang hiển thị từng lượt gọi mô hình, sắp xếp theo {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", + "caption.call_investigator": "Cuộc gọi điều tra {record}.", + "caption.calls": "Hiển thị các cuộc gọi mô hình riêng lẻ được sắp xếp theo {sort}. {loaded}.", + "caption.date_prefix": "{label}.", "caption.descending": "giảm dần", - "caption.initial_calls": "Đang hiển thị từng lượt gọi mô hình.", - "caption.insights": "Xếp hạng theo chi phí, tín dụng sử dụng, tái sử dụng cache, áp lực ngữ cảnh và độ tin cậy giá.", - "caption.loaded": "Đã tải {loaded} lượt gọi", - "caption.loaded_capped": "Đã tải {loaded} trên {available} lượt gọi", + "caption.initial_calls": "Hiển thị các cuộc gọi mô hình riêng lẻ.", + "caption.insights": "Được xếp hạng theo chi phí, tín dụng sử dụng, tái sử dụng bộ đệm, áp lực ngữ cảnh và độ tin cậy về giá.", + "caption.loaded": "{loaded} cuộc gọi đã được tải", + "caption.loaded_capped": "Đã tải {loaded} trong số {available} cuộc gọi", + "caption.rows_loaded_progress": "Đã tải hàng: {loaded} / {total}", + "caption.rows_loading_background": "Tổng quan dashboard đã sẵn sàng. Các hàng đang được tải ở nền.", + "caption.rows_loading_progress": "Đang tải hàng: {loaded} / {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Đang hiển thị {threads} cuộc trò chuyện từ {calls} lượt gọi đã lọc, sắp xếp theo {sort}. {loaded}. Bấm một cuộc trò chuyện để mở các lượt gọi.", - "context.api_http": "Ngữ cảnh API trả HTTP {status}.", - "context.api_unavailable": "Ngữ cảnh API không khả dụng ở đây. Chạy codex-usage-tracker serve-dashboard --open để tải ngữ cảnh theo yêu cầu.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "Đã bỏ qua {count} ký tự vượt ngân sách.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Tải ngữ cảnh đang tắt cho dashboard server này. Bật tại đây để tải ngữ cảnh JSONL cục bộ theo yêu cầu.", - "context.enabled_note": "Tải ngữ cảnh đã bật. Bấm Tải ngữ cảnh để đọc lượt gọi này từ nguồn JSONL cục bộ.", - "context.file_hint": "Mở dashboard bằng codex-usage-tracker serve-dashboard để tải ngữ cảnh thô theo yêu cầu.", + "caption.threads": "Hiển thị {threads} chuỗi từ {calls} cuộc gọi được lọc, được sắp xếp theo {sort}. {loaded}. Nhấp vào một chủ đề để mở rộng các cuộc gọi của nó.", + "context.api_http": "Ngữ cảnh API trả về HTTP {status}.", + "context.api_unavailable": "Ngữ cảnh API không có ở đây. Chạy codex-usage-tracker serve-dashboard --open để tải ngữ cảnh theo yêu cầu.", + "context.auto_loading": "Đang tải bằng chứng lượt đã chọn với đầu ra công cụ được bao gồm.", + "context.chars_omitted": "{count} ký tự vượt quá ngân sách bị bỏ qua.", + "context.compaction_detected": "Đã phát hiện nén", + "context.compaction_replacement": "Bối cảnh thay thế được nén", + "context.compaction_replacement_count": "{count} mục nhập lịch sử thay thế có sẵn.", + "context.disabled_hint": "Tính năng tải ngữ cảnh bị tắt đối với máy chủ bảng thông tin này. Kích hoạt nó ở đây để tải ngữ cảnh JSONL cục bộ theo yêu cầu.", + "context.enabled_note": "Tải ngữ cảnh được kích hoạt. Nhấn Hiển thị bằng chứng nhật ký lần lượt để đọc cuộc gọi này từ nguồn JSONL địa phương.", + "context.file_hint": "Mở trang tổng quan này bằng codex-usage-tracker serve-dashboard để tải ngữ cảnh thô theo yêu cầu.", "context.line": "dòng {line}", "context.loading": "Đang tải ngữ cảnh cục bộ...", - "context.local_redacted": "Ngữ cảnh JSONL cục bộ được tải theo yêu cầu. Prompt và output công cụ được che (redacted) các mẫu secret phổ biến và không lưu vào SQLite hoặc HTML dashboard.", + "context.local_redacted": "Ngữ cảnh JSONL cục bộ được tải theo yêu cầu. Lời nhắc và đầu ra công cụ được sắp xếp lại cho các mẫu bí mật phổ biến và không được lưu giữ ở SQLite hoặc bảng điều khiển HTML.", "context.no_char_limit_active": "Không áp dụng giới hạn ký tự.", - "context.no_record_id": "Dòng này không có record id để tra ngữ cảnh.", - "context.no_response": "Không có nội dung phản hồi", - "context.older_omitted": "Đã bỏ qua {count} entry cũ hơn.", - "context.ready_hint": "Ngữ cảnh không được nhúng trong dashboard. Bấm nút để đọc lượt gọi này từ JSONL cục bộ.", - "context.settings_http": "Thiết lập ngữ cảnh trả HTTP {status}.", + "context.no_record_id": "Hàng này không có id bản ghi để tra cứu ngữ cảnh.", + "context.no_response": "Không có cơ quan phản hồi", + "context.older_omitted": "{count} mục cũ hơn bị bỏ qua.", + "context.ready_hint": "Ngữ cảnh không được nhúng trong trang tổng quan này. Nhấn nút để đọc cuộc gọi này từ nguồn JSONL cục bộ.", + "context.settings_http": "Cài đặt ngữ cảnh được trả về HTTP {status}.", "context.source": "Nguồn: {file}:{line}", - "context.token_breakdown": "Phân rã token", - "context.token_cached": "Đã cache", - "context.token_input": "Đầu vào", - "context.token_output": "Đầu ra", - "context.token_reasoning": "Reasoning", - "context.token_required": "Tải ngữ cảnh cần token API dashboard localhost.", - "context.token_scope_call": "Lượt gọi này", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Lũy kế phiên", - "context.token_total": "Tổng", - "context.token_type": "Loại", - "context.token_uncached": "Chưa cache", - "context.tool_included": "Output công cụ được bao gồm với redaction và giới hạn kích thước.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Tỷ lệ đã cấu hình", + "context.token_breakdown": "Phân tích token", + "context.token_cached": "Đã lưu vào bộ nhớ đệm", + "context.token_input": "đầu vào", + "context.token_output": "đầu ra", + "context.token_reasoning": "Lý luận", + "context.token_required": "Việc tải ngữ cảnh yêu cầu bảng điều khiển localhost token API.", + "context.token_scope_call": "Cuộc gọi này", + "context.token_scope_earlier": "Số lượng token trước đó trong cùng một lượt", + "context.token_scope_previous": "Số lượng token trước đó trong cùng một lượt", + "context.token_scope_selected": "Số token cuộc gọi đã chọn", + "context.token_scope_session": "Phiên tích lũy", + "context.token_total": "Tổng cộng", + "context.token_type": "loại", + "context.token_uncached": "Không được lưu vào bộ nhớ đệm", + "context.tool_included": "Đầu ra công cụ bao gồm giới hạn kích thước và biên tập.", + "context.tool_omitted": "Đầu ra công cụ bị ẩn cho chế độ xem này.", + "credit.configured_rate": "Tỷ lệ được cấu hình", "credit.estimated_mapping": "Ánh xạ ước tính", "credit.inferred_mapping": "Ánh xạ mô hình suy luận", - "credit.no_mapped_rate": "Chưa có tỷ lệ khớp", - "credit.no_rate": "Chưa có tỷ lệ tín dụng", - "credit.official_match": "Khớp bảng tỷ lệ chính thức", + "credit.no_mapped_rate": "Không có tỷ lệ được ánh xạ", + "credit.no_rate": "Không có lãi suất tín dụng", + "credit.official_match": "Trận đấu thẻ giá chính thức", "credit.user_rate": "Tỷ lệ tín dụng do người dùng cung cấp", "credit.with_status": "{value} tín dụng · {status}", - "dashboard.call_details": "Chi tiết lượt gọi", - "dashboard.detail.empty": "Rê chuột hoặc bấm một dòng để xem các trường usage tổng hợp.", + "dashboard.call_details": "Chi tiết cuộc gọi", + "dashboard.detail.empty": "Di chuột hoặc nhấp vào một hàng để kiểm tra các trường sử dụng tổng hợp.", "dashboard.eyebrow": "Phân tích Codex cục bộ", - "dashboard.local_storage_note": "Dashboard ghi nhớ lựa chọn ngôn ngữ ngay trong trình duyệt.", - "dashboard.model_calls": "Lượt gọi mô hình", - "dashboard.title": "Theo dõi sử dụng Codex", - "dashboard.top_threads_by_attention": "Cuộc trò chuyện cần chú ý nhất", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Lượt gọi", - "dashboard.view.insights": "Tổng quan", - "dashboard.view.threads": "Cuộc trò chuyện", - "date.custom": "Tùy chỉnh", - "date.invalid_range": "Khoảng ngày không hợp lệ", - "date.range_between": "{prefix} từ {start} đến {end}", + "dashboard.local_storage_note": "Tiêu đề trang tổng quan cũng ghi nhớ cục bộ lựa chọn ngôn ngữ của bạn.", + "dashboard.model_calls": "Cuộc gọi mẫu", + "dashboard.title": "Trang tổng quan sử dụng", + "dashboard.top_threads_by_attention": "Chủ đề hàng đầu theo điểm chú ý", + "dashboard.view.call": "Gọi điều tra viên", + "dashboard.view.calls": "Cuộc gọi", + "dashboard.view.insights": "Thông tin chi tiết", + "dashboard.view.threads": "chủ đề", + "date.custom": "tùy chỉnh", + "date.invalid_range": "Phạm vi ngày không hợp lệ", + "date.range_between": "{prefix} {start} tới {end}", "date.range_exact": "{prefix} {date}", "date.range_from": "{prefix} từ {start}", - "date.range_through": "{prefix} đến hết {end}", - "detail.agent_nickname": "Tên agent", - "detail.agent_role": "Vai trò agent", - "detail.allowance_impact": "Tác động hạn mức", - "detail.attached_calls": "Lượt gọi đã gắn", - "detail.auto_review_calls": "Lượt gọi auto-review", - "detail.cache_savings": "Tiết kiệm nhờ cache", - "detail.call_number": "lượt gọi {number}", - "detail.calls": "Lượt gọi", + "date.range_through": "{prefix} đến {end}", + "detail.agent_nickname": "Biệt danh đại lý", + "detail.agent_role": "Vai trò đại lý", + "detail.allowance_impact": "Tác động phụ cấp", + "detail.attached_calls": "Cuộc gọi đính kèm", + "detail.auto_review_calls": "Tự động xem xét cuộc gọi", + "detail.cache_savings": "Tiết kiệm bộ nhớ đệm", + "detail.call_number": "gọi {number}", + "detail.calls": "Cuộc gọi", "detail.context_window": "Cửa sổ ngữ cảnh", - "detail.cost_usage_context": "Chi phí, mức sử dụng và ngữ cảnh", - "detail.credit_confidence": "Độ tin cậy tín dụng", + "detail.cost_usage_context": "Chi phí, cách sử dụng và bối cảnh", + "detail.credit_confidence": "Niềm tin tín dụng", "detail.credit_model": "Mô hình tín dụng", - "detail.credit_note": "Ghi chú tín dụng", + "detail.credit_note": "Giấy báo có", "detail.credit_source": "Nguồn tín dụng", - "detail.credit_source_fetched": "Lúc lấy nguồn tín dụng", - "detail.credit_tier": "Phân nhóm tín dụng", + "detail.credit_source_fetched": "Đã tìm nạp nguồn tín dụng", + "detail.credit_tier": "Cấp tín dụng", "detail.cwd": "Cwd", "detail.efficiency_signals": "Tín hiệu hiệu quả", - "detail.first_expensive_turn": "Turn tốn kém đầu tiên", - "detail.git_branch": "Nhánh git", - "detail.largest_cumulative_jump": "Bước nhảy lũy kế lớn nhất", + "detail.first_expensive_turn": "Lượt đắt đầu tiên", + "detail.git_branch": "Nhánh Git", + "detail.largest_cumulative_jump": "Bước nhảy tích lũy lớn nhất", "detail.latest_activity": "Hoạt động mới nhất", - "detail.model_mix": "Tổ hợp mô hình", + "detail.model_mix": "hỗn hợp mô hình", "detail.next_action": "Hành động tiếp theo", - "detail.no_above_thresholds": "Không có mục nào vượt ngưỡng", - "detail.no_aggregate_action": "Không có hành động tổng hợp nào bị gắn cờ.", - "detail.parent_session": "Phiên cha", - "detail.parent_thread": "Cuộc trò chuyện cha", - "detail.parent_updated": "Cha cập nhật", - "detail.pricing_model": "Mô hình tính giá", + "detail.no_above_thresholds": "Không có gì vượt quá ngưỡng", + "detail.no_aggregate_action": "Không có hành động tổng hợp nào được gắn cờ.", + "detail.parent_session": "Phiên họp phụ huynh", + "detail.parent_thread": "Chủ đề gốc", + "detail.parent_updated": "Đã cập nhật phụ huynh", + "detail.pricing_model": "Mô hình định giá", "detail.pricing_status": "Trạng thái giá", - "detail.project_cwd": "Cwd dự án", - "detail.project_tags": "Tag dự án", - "detail.raw_identifiers": "Định danh tổng hợp thô", - "detail.reasoning_mix": "Tổ hợp suy luận", - "detail.relationships": "Quan hệ", - "detail.remote_hash": "Hash remote", - "detail.remote_label": "Nhãn remote", - "detail.secondary_thread_fields": "Trường phụ của cuộc trò chuyện", - "detail.source_file_line": "File nguồn và dòng", + "detail.project_cwd": "dự án cwd", + "detail.project_tags": "Thẻ dự án", + "detail.raw_identifiers": "Giá trị nhận dạng tổng hợp thô", + "detail.reasoning_mix": "Lý luận hỗn hợp", + "detail.relationships": "Mối quan hệ", + "detail.remote_hash": "Băm từ xa", + "detail.remote_label": "Nhãn từ xa", + "detail.secondary_thread_fields": "Các trường chủ đề phụ", + "detail.source_file_line": "Tệp nguồn và dòng", "detail.source_line": "Dòng nguồn", - "detail.spawned_child_calls": "Lượt gọi con", - "detail.spawned_from": "Được tạo từ", - "detail.spawned_threads": "Cuộc trò chuyện con", - "detail.subagent_before_spike": "Subagent trước spike", - "detail.subagent_calls": "Lượt gọi subagent", - "detail.subagent_type": "Loại subagent", - "detail.thread_attachment": "Gắn cuộc trò chuyện", - "detail.thread_attention_summary": "Tóm tắt chú ý cuộc trò chuyện", - "detail.thread_lifecycle": "Vòng đời cuộc trò chuyện", - "detail.thread_narrative": "Diễn biến cuộc trò chuyện", - "detail.thread_source": "Nguồn cuộc trò chuyện", - "detail.thread_timeline": "Timeline cuộc trò chuyện", - "detail.timeline_context": "ngữ cảnh {value}", - "detail.timeline_empty": "Không có lượt gọi trong cuộc trò chuyện này.", - "detail.timeline_meta": "{tokens} token · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Thời điểm", - "detail.token_pricing_breakdown": "Phân rã token và giá", - "detail.tokens_at": "{tokens} token lúc {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Lý do bị gắn cờ", - "docs.dashboard_guide": "Hướng dẫn bảng điều khiển", + "detail.spawned_child_calls": "Cuộc gọi con sinh ra", + "detail.spawned_from": "Sinh ra từ", + "detail.spawned_threads": "Chủ đề sinh ra", + "detail.subagent_before_spike": "Chất phụ trước khi tăng đột biến", + "detail.subagent_calls": "Cuộc gọi đại lý phụ", + "detail.subagent_type": "Loại đại lý phụ", + "detail.thread_attachment": "Đính kèm chủ đề", + "detail.thread_attention_summary": "Tóm tắt sự chú ý của chủ đề", + "detail.thread_lifecycle": "Vòng đời của chủ đề", + "detail.thread_narrative": "Chủ đề tường thuật", + "detail.thread_source": "Nguồn chủ đề", + "detail.thread_timeline": "Dòng thời gian của chủ đề", + "detail.timeline_context": "bối cảnh {value}", + "detail.timeline_empty": "Không có cuộc gọi nào trong chủ đề này.", + "detail.timeline_meta": "{tokens} token · {cost} · {credits} · bộ nhớ đệm {cache}", + "detail.timestamp": "Dấu thời gian", + "detail.token_pricing_breakdown": "Phân tích token và giá cả", + "detail.tokens_at": "{tokens} token tại {time}", + "detail.turn": "rẽ", + "detail.why_flagged": "Tại sao bị gắn cờ", + "docs.dashboard_guide": "Hướng dẫn trang tổng quan", "effort.high": "cao", "effort.low": "thấp", "effort.medium": "trung bình", - "filter.confidence": "Độ tin cậy", - "filter.effort": "Suy luận", + "filter.confidence": "sự tự tin", + "filter.effort": "Lý luận", "filter.end": "Kết thúc", - "filter.model": "Mô hình", - "filter.project": "Dự án", - "filter.reasoning": "Suy luận", + "filter.model": "người mẫu", + "filter.project": "dự án", + "filter.reasoning": "Lý luận", "filter.search": "Tìm kiếm", - "filter.search_placeholder": "Cuộc trò chuyện, cwd, mô hình", + "filter.search_placeholder": "Chủ đề, cwd, mô hình", "filter.session": "Phiên", "filter.sort": "Sắp xếp", "filter.start": "Bắt đầu", - "filter.thread": "Cuộc trò chuyện", - "filter.time": "Thời gian", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Chỉ phiên đang hoạt động; ẩn {count} lượt gọi đã lưu trữ", - "history.active_only": "Chỉ phiên đang hoạt động", - "history.all_empty": "Đã chọn toàn bộ lịch sử; chưa index lượt gọi lưu trữ nào", - "history.all_includes": "Toàn bộ lịch sử gồm {count} lượt gọi đã lưu trữ", - "history.archived_scan_hint": "{detail}. Phiên lưu trữ chỉ được quét khi chọn Toàn bộ lịch sử trong live refresh.", - "insight.apply_cache_misses": "Áp dụng mẫu kiểm tra không trúng cache", - "insight.apply_context_bloat": "Áp dụng mẫu kiểm tra ngữ cảnh phình lớn", - "insight.codex_allowance_usage": "Hạn mức Codex đã dùng", - "insight.context_bloat": "Ngữ cảnh phình lớn", - "insight.context_bloat_body": "{calls} lượt gọi đạt hoặc vượt {ratio} mức dùng ngữ cảnh.", - "insight.costliest_thread": "Cuộc trò chuyện tốn chi phí nhất", - "insight.costliest_thread_body": "{thread} có {calls} lượt gọi và {tokens} token.", - "insight.credit_coverage_body": "{ratio} token đang hiển thị khớp tỷ lệ tín dụng Codex.", + "filter.thread": "chủ đề", + "filter.time": "thời gian", + "flag.elevated_context_use": "Sử dụng bối cảnh nâng cao", + "flag.expensive_low_output_call": "Cuộc gọi đầu ra tốn kém", + "flag.high_context_use": "Sử dụng ngữ cảnh cao", + "flag.high_estimated_cost": "Chi phí ước tính cao", + "flag.high_reasoning_share": "Chia sẻ lý luận cao", + "flag.low_cache_reuse": "Tái sử dụng bộ nhớ đệm thấp", + "history.active_hidden": "Chỉ các phiên hoạt động; {count} cuộc gọi đã lưu trữ bị ẩn", + "history.active_only": "Chỉ các phiên hoạt động", + "history.all_empty": "Tất cả lịch sử được chọn; chưa có cuộc gọi lưu trữ nào được lập chỉ mục", + "history.all_includes": "Tất cả lịch sử bao gồm {count} cuộc gọi được lưu trữ", + "history.archived_scan_hint": "{detail}. Các phiên đã lưu trữ chỉ được quét khi chọn Tất cả lịch sử trong khi làm mới trực tiếp.", + "insight.apply_cache_misses": "Áp dụng cài đặt trước nhớ nhớ cache", + "insight.apply_context_bloat": "Áp dụng cài đặt sẵn theo ngữ cảnh", + "insight.codex_allowance_usage": "Codex mức sử dụng trợ cấp", + "insight.context_bloat": "Bối cảnh phình to", + "insight.context_bloat_body": "{calls} lệnh gọi bằng hoặc cao hơn mức sử dụng ngữ cảnh {ratio}.", + "insight.costliest_thread": "Chủ đề đắt nhất", + "insight.costliest_thread_body": "{thread} có {calls} cuộc gọi và {tokens} token.", + "insight.credit_coverage_body": "{ratio} của token hiển thị ánh xạ tới tỷ lệ tín dụng Codex.", "insight.estimated_pricing": "Giá ước tính", - "insight.estimated_pricing_body": "Giá ước tính đã được tính vào tổng, nhưng nên rà soát riêng.", - "insight.inspect_selected_call": "Xem lượt gọi đã chọn", - "insight.low_cache_reuse": "Tái sử dụng cache thấp", - "insight.low_cache_reuse_body": "{calls} lượt gọi dưới {ratio} tái sử dụng cache. Bắt đầu với {thread}.", - "insight.open_thread_timeline": "Mở timeline cuộc trò chuyện", - "insight.reasoning_output_spike": "Token suy luận tăng đột biến", - "insight.reasoning_spike_body": "{thread} có lượt gọi reasoning-output lớn nhất trong bộ lọc hiện tại.", - "insight.review_estimates": "Rà soát ước tính", - "insight.review_highest_credit": "Rà soát lượt gọi dùng nhiều tín dụng", - "insight.review_pricing_gaps": "Rà soát phần thiếu giá", - "insight.unpriced_usage": "Mức sử dụng chưa có giá", - "insight.unpriced_usage_body": "Các token này chưa được tính vào tổng chi phí ước tính cho đến khi có cấu hình giá.", - "language.english": "English", + "insight.estimated_pricing_body": "Giá dự đoán tốt nhất được đánh dấu được bao gồm nhưng cần được xem xét riêng.", + "insight.inspect_selected_call": "Kiểm tra cuộc gọi đã chọn", + "insight.low_cache_reuse": "Tái sử dụng bộ nhớ đệm thấp", + "insight.low_cache_reuse_body": "{calls} cuộc gọi đang được {ratio} sử dụng lại bộ nhớ đệm. Bắt đầu với {thread}.", + "insight.open_thread_timeline": "Mở dòng thời gian của chủ đề", + "insight.reasoning_output_spike": "Lý luận sản lượng tăng đột biến", + "insight.reasoning_spike_body": "{thread} có lệnh gọi đầu ra lý luận lớn nhất trong bộ lọc hiện tại.", + "insight.review_estimates": "Xem lại ước tính", + "insight.review_highest_credit": "Xem lại các cuộc gọi có tín dụng cao nhất", + "insight.review_pricing_gaps": "Xem lại khoảng cách về giá", + "insight.unpriced_usage": "Mức sử dụng không tính phí", + "insight.unpriced_usage_body": "Các token này bị loại bỏ khỏi tổng chi phí ước tính cho đến khi định giá được định cấu hình.", + "language.english": "Tiếng Anh", "language.label": "Ngôn ngữ", "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Đang kiểm tra mức sử dụng mới...", - "live.every": "Live refresh mỗi {seconds}s", - "live.history_static_hint": "Chạy codex-usage-tracker serve-dashboard để đổi giữa phiên đang hoạt động và toàn bộ lịch sử từ dashboard.", - "live.indexed": " Đã index {rows} dòng tổng hợp từ {files} log.", - "live.load_static_hint": "Chạy codex-usage-tracker serve-dashboard để tải kích thước lịch sử khác từ dashboard.", - "live.paused": "Live refresh đã tạm dừng", - "live.refresh_suffix": ". Tải lại trang này sau khi tạo dashboard tĩnh, hoặc chạy codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh không khả dụng: {message}{suffix}", - "live.refreshing_index": "Đang làm mới index mức sử dụng cục bộ...", - "live.reloading_static": "Đang tải lại snapshot dashboard tĩnh...", - "live.skipped": " Bỏ qua {count} token-count event lỗi.", - "live.updated_detail": "Cập nhật {time}. {loaded}. {history}.{indexed}{skipped}", + "live.checking_usage": "Đang kiểm tra việc sử dụng mới...", + "live.every": "Làm mới trực tiếp mỗi {seconds} giây", + "live.history_static_hint": "Chạy codex-usage-tracker serve-dashboard để chuyển đổi giữa các phiên hoạt động và tất cả lịch sử từ trang tổng quan.", + "live.indexed": "Đã lập chỉ mục các hàng tổng hợp {rows} từ nhật ký {files}.", + "live.load_static_hint": "Chạy codex-usage-tracker serve-dashboard để tải kích thước lịch sử khác từ trang tổng quan.", + "live.loading_rows": "Đang tải hàng ở nền...", + "live.paused": "Đã tạm dừng làm mới trực tiếp", + "live.refresh_suffix": ". Tải lại trang này sau khi tạo lại trang tổng quan tĩnh hoặc chạy codex-usage-tracker serve-dashboard.", + "live.refresh_unavailable": "Không thể làm mới trực tiếp: {message}{suffix}", + "live.refreshing_index": "Đang làm mới chỉ mục sử dụng cục bộ...", + "live.reloading_static": "Đang tải lại ảnh chụp nhanh trang tổng quan tĩnh...", + "live.skipped": "Đã bỏ qua {count} sự kiện đếm token không đúng định dạng.", + "live.updated_detail": "Đã cập nhật {time}. {loaded}. {history}.{indexed}{skipped}", "metric.attention_score": "Điểm chú ý", - "metric.cache_ratio": "Tỷ lệ cache", - "metric.cache_trend": "Xu hướng cache", - "metric.cached_input": "Đầu vào đã cache", - "metric.codex_credits": "Tín dụng Codex", - "metric.context_trend": "Xu hướng ngữ cảnh", - "metric.context_use": "Mức dùng ngữ cảnh", + "metric.cache_ratio": "Tỷ lệ bộ đệm", + "metric.cache_trend": "Xu hướng bộ đệm", + "metric.cached_input": "Đầu vào được lưu vào bộ nhớ đệm", + "metric.codex_credits": "Codex tín dụng", + "metric.context_trend": "Xu hướng bối cảnh", + "metric.context_use": "Sử dụng bối cảnh", "metric.estimated_cost": "Chi phí ước tính", - "metric.input_tokens": "Token input", - "metric.last_call_input": "Input lượt gọi cuối", - "metric.last_call_total": "Tổng lượt gọi cuối", - "metric.max_context_use": "Ngữ cảnh cao nhất", - "metric.output": "Output", - "metric.output_tokens": "Token output", - "metric.reasoning_output": "Token suy luận", + "metric.input_tokens": "Token đầu vào", + "metric.last_call_input": "Đầu vào cuộc gọi cuối cùng", + "metric.last_call_total": "Tổng số cuộc gọi cuối cùng", + "metric.max_context_use": "Sử dụng bối cảnh tối đa", + "metric.output": "đầu ra", + "metric.output_tokens": "Token đầu ra", + "metric.reasoning_output": "Đầu ra lý luận", "metric.remaining_usage": "Mức sử dụng còn lại", - "metric.session_cumulative": "Lũy kế phiên", - "metric.total": "Tổng", - "metric.total_tokens": "Tổng token", - "metric.uncached_input": "Đầu vào chưa cache", - "metric.usage_credits": "Tín dụng mức sử dụng", + "metric.session_cumulative": "Phiên tích lũy", + "metric.total": "Tổng cộng", + "metric.total_tokens": "Tổng số token", + "metric.uncached_input": "Đầu vào không được lưu vào bộ nhớ đệm", + "metric.usage_credits": "tín dụng sử dụng", "metric.usage_remaining": "Mức sử dụng còn lại", - "metric.visible_calls": "Lượt gọi đang hiển thị", + "metric.visible_calls": "Cuộc gọi hiển thị", "nav.history": "Lịch sử", "nav.live": "Trực tiếp", "nav.load": "Tải", - "option.active_sessions_only": "Chỉ phiên đang hoạt động", - "option.all_confidence": "Tất cả độ tin cậy", - "option.all_efforts": "Tất cả mức suy luận", - "option.all_history": "Toàn bộ lịch sử", - "option.all_models": "Tất cả mô hình", - "option.all_time": "Toàn bộ thời gian", - "option.custom_range": "Khoảng tùy chỉnh", + "option.active_sessions_only": "Chỉ các phiên hoạt động", + "option.all_confidence": "Tất cả sự tự tin", + "option.all_efforts": "Mọi nỗ lực", + "option.all_history": "Tất cả lịch sử", + "option.all_models": "Tất cả các mô hình", + "option.all_time": "Mọi lúc", + "option.custom_range": "Phạm vi tùy chỉnh", "option.estimated_cost": "Chi phí ước tính", - "option.estimated_credit_mapping": "Ánh xạ tín dụng ước tính", + "option.estimated_credit_mapping": "Bản đồ tín dụng ước tính", "option.exact_cost": "Chi phí chính xác", - "option.exact_credit_rate": "Tỷ lệ tín dụng chính xác", + "option.exact_credit_rate": "Lãi suất tín dụng chính xác", "option.highest_codex_credits": "Tín dụng Codex cao nhất", - "option.highest_context_use": "Mức dùng ngữ cảnh cao nhất", + "option.highest_context_use": "Sử dụng bối cảnh cao nhất", "option.highest_estimated_cost": "Chi phí ước tính cao nhất", "option.last_7_days": "7 ngày qua", - "option.load_10000": "10.000 lượt gọi", - "option.load_20000": "20.000 lượt gọi", - "option.load_5000": "5.000 lượt gọi", - "option.load_all": "Tất cả lượt gọi", - "option.lowest_cache_ratio": "Tỷ lệ cache thấp nhất", + "option.load_10000": "10.000 cuộc gọi", + "option.load_20000": "20.000 cuộc gọi", + "option.load_5000": "5.000 cuộc gọi", + "option.load_all": "Tất cả cuộc gọi", + "option.lowest_cache_ratio": "Tỷ lệ bộ đệm thấp nhất", "option.missing_credit_rate": "Thiếu tỷ lệ tín dụng", - "option.most_signals": "Nhiều tín hiệu nhất", - "option.most_tokens": "Nhiều token nhất", + "option.most_signals": "Hầu hết các tín hiệu", + "option.most_tokens": "Hầu hết các token", "option.needs_attention": "Cần chú ý", - "option.newest_calls": "Lượt gọi mới nhất", + "option.newest_calls": "Cuộc gọi mới nhất", "option.this_month": "Tháng này", "option.this_week": "Tuần này", - "option.thread_name": "Tên cuộc trò chuyện", - "option.today": "Hôm nay", - "option.unpriced_cost": "Chưa có giá", - "option.user_credit_override": "Tỷ lệ tín dụng người dùng đặt", - "parser.warnings_title": "Lần refresh gần nhất báo {count} diagnostics parser: {entries}. Chạy codex-usage-tracker inspect-log để kiểm tra schema drift.", - "preset.cache_misses": "Không trúng cache", - "preset.cache_misses_caption": "Mẫu kiểm tra không trúng cache", - "preset.cache_misses_desc": "Các lượt gọi có tỷ lệ cache thấp, nhóm theo cwd, mô hình và cuộc trò chuyện.", + "option.thread_name": "Tên chủ đề", + "option.today": "hôm nay", + "option.unpriced_cost": "Chi phí chưa được định giá", + "option.user_credit_override": "Ghi đè tín dụng người dùng", + "parser.warnings_title": "Báo cáo làm mới mới nhất {count} chẩn đoán trình phân tích cú pháp: {entries}. Chạy codex-usage-tracker inspect-log để điều tra sự trôi dạt của lược đồ.", + "preset.cache_misses": "Thiếu bộ nhớ đệm", + "preset.cache_misses_caption": "Bộ nhớ đệm thiếu giá trị đặt trước", + "preset.cache_misses_desc": "Các lệnh gọi có tỷ lệ bộ nhớ đệm thấp được nhóm theo cwd, model và thread.", "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Ngữ cảnh phình lớn", - "preset.context_bloat_caption": "Mẫu kiểm tra ngữ cảnh phình lớn", - "preset.context_bloat_desc": "Lượt gọi dùng trên 60% ngữ cảnh hoặc có token lũy kế rất cao.", - "preset.description": "Điểm bắt đầu một lần bấm cho các câu hỏi mức sử dụng thường gặp.", - "preset.estimated_price_review": "Rà soát chi phí ước tính", - "preset.estimated_price_review_caption": "Mẫu kiểm tra rà soát chi phí ước tính", - "preset.estimated_price_review_desc": "Mức sử dụng đang dùng chi phí ước tính cần được rà soát.", + "preset.context_bloat": "Bối cảnh phình to", + "preset.context_bloat_caption": "Cài đặt sẵn bối cảnh", + "preset.context_bloat_desc": "Cuộc gọi sử dụng ngữ cảnh hơn 60% hoặc có token tích lũy rất cao.", + "preset.description": "Điểm bắt đầu bằng một cú nhấp chuột cho các câu hỏi sử dụng phổ biến.", + "preset.estimated_price_review": "Đánh giá ước tính giá", + "preset.estimated_price_review_caption": "Đánh giá giá ước tính đặt trước", + "preset.estimated_price_review_desc": "Giá sử dụng được đánh dấu bằng ước tính dự đoán tốt nhất.", "preset.highest_codex_credits": "Tín dụng Codex cao nhất", - "preset.highest_codex_credits_caption": "Mẫu kiểm tra Tín dụng Codex cao nhất", - "preset.highest_codex_credits_desc": "Sắp xếp theo tác động ước tính lên hạn mức Codex.", - "preset.highest_cost_threads": "Cuộc trò chuyện tốn chi phí nhất", - "preset.highest_cost_threads_caption": "Preset cuộc trò chuyện tốn chi phí nhất", - "preset.highest_cost_threads_desc": "Sắp xếp cuộc trò chuyện theo chi phí ước tính, kèm các subagent liên quan.", - "preset.investigation_presets": "Bộ lọc kiểm tra", - "preset.no_preset": "Chưa áp dụng mẫu kiểm tra.", - "preset.pricing_gaps": "Thiếu cấu hình giá", - "preset.pricing_gaps_caption": "Preset thiếu cấu hình giá", - "preset.pricing_gaps_desc": "Mức sử dụng chưa có giá khiến tổng chi phí ước tính chưa đầy đủ.", - "pricing.configure_hint": "Chạy codex-usage-tracker update-pricing để cấu hình chi phí ước tính.", - "pricing.fetched": "lấy lúc {time}", - "pricing.pinned": "snapshot đã pin", - "pricing.source": "Nguồn giá", - "pricing.tier": "tier {tier}", - "pricing.title": "{parts}. Nhãn Codex nội bộ có thể dùng ước tính gần đúng đã đánh dấu.{warning}", - "pricing.title_fetched": "{parts}. Lấy từ {url} lúc {time}. Nhãn Codex nội bộ có thể dùng ước tính gần đúng đã đánh dấu.{warning}", - "privacy.aliases_preserved": "Alias dự án đã cấu hình được xem là lựa chọn hiển thị rõ ràng.", - "privacy.cwd_redacted": "Đường dẫn cwd thô đã được ẩn.", - "privacy.git_branch_hidden": "Nhánh git đã được ẩn.", - "privacy.git_remote_label_hidden": "Nhãn git remote đã được ẩn.", - "privacy.mode": "Chế độ riêng tư metadata dự án: {mode}.", - "privacy.normal_title": "Metadata dự án hiển thị cwd cục bộ, dự án, nhánh và nhãn đã cấu hình.", - "privacy.project_names_redacted": "Dự án chưa đặt tên dùng nhãn hash ổn định.", - "privacy.relative_cwd_hidden": "Relative cwd đã được ẩn.", - "privacy.tags_hidden": "Tag dự án đã được ẩn.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Hạn mức", + "preset.highest_codex_credits_caption": "Đặt trước tín dụng Codex cao nhất", + "preset.highest_codex_credits_desc": "Các cuộc gọi được sắp xếp theo mức độ tác động ước tính đến mức sử dụng Codex.", + "preset.highest_cost_threads": "Chủ đề có chi phí cao nhất", + "preset.highest_cost_threads_caption": "Chủ đề có chi phí cao nhất được đặt trước", + "preset.highest_cost_threads_desc": "Các chủ đề được sắp xếp theo mức chi tiêu ước tính, có đính kèm các tác nhân phụ.", + "preset.investigation_presets": "Cài đặt trước điều tra", + "preset.no_preset": "Không áp dụng cài đặt trước.", + "preset.pricing_gaps": "Khoảng cách giá", + "preset.pricing_gaps_caption": "Khoảng cách giá đặt trước", + "preset.pricing_gaps_desc": "Mức sử dụng không được định giá khiến cho tổng chi phí ước tính không đầy đủ.", + "pricing.configure_hint": "Chạy codex-usage-tracker update-pricing để định cấu hình chi phí ước tính.", + "pricing.fetched": "đã tìm nạp {time}", + "pricing.pinned": "ảnh chụp nhanh được ghim", + "pricing.source": "Nguồn định giá", + "pricing.tier": "cấp độ {tier}", + "pricing.title": "{parts}. Nhãn Codex nội bộ có thể sử dụng ước tính đoán đúng nhất được đánh dấu.{warning}", + "pricing.title_fetched": "{parts}. Được tìm nạp từ {url} lúc {time}. Nhãn Codex nội bộ có thể sử dụng ước tính dự đoán tốt nhất được đánh dấu.{warning}", + "privacy.aliases_preserved": "Bí danh dự án đã định cấu hình được coi là tùy chọn hiển thị rõ ràng.", + "privacy.cwd_redacted": "Đường dẫn cwd thô được điều chỉnh lại.", + "privacy.git_branch_hidden": "Nhánh Git bị ẩn.", + "privacy.git_remote_label_hidden": "Nhãn từ xa Git bị ẩn.", + "privacy.mode": "Chế độ bảo mật siêu dữ liệu dự án: {mode}.", + "privacy.normal_title": "Siêu dữ liệu dự án được hiển thị với các nhãn cwd, dự án, nhánh và được định cấu hình cục bộ.", + "privacy.project_names_redacted": "Các dự án chưa được đặt tên sử dụng nhãn băm ổn định.", + "privacy.relative_cwd_hidden": "Cwd tương đối bị ẩn.", + "privacy.tags_hidden": "Thẻ dự án bị ẩn.", + "recommendation.context_bloat.action": "Hãy cân nhắc việc bắt đầu một chuỗi Codex mới nếu bối cảnh cũ hơn không còn phù hợp nữa.", + "recommendation.context_bloat.title": "Áp lực bối cảnh cao", + "recommendation.context_bloat.why": "Cuộc gọi này đang sử dụng một phần lớn cửa sổ ngữ cảnh mô hình.", + "recommendation.elevated_context.action": "Kiểm tra xem chủ đề có thể được thu hẹp hay không trước khi thêm công việc khác.", + "recommendation.elevated_context.title": "Áp lực bối cảnh nâng cao", + "recommendation.elevated_context.why": "Việc sử dụng bối cảnh được nâng cao và có thể trở nên tốn kém ở những lượt sau.", + "recommendation.estimated_pricing.action": "Xem lại phạm vi định giá và ghim hoặc ghi đè giá mô hình nếu cuộc gọi này quan trọng.", + "recommendation.estimated_pricing.title": "Giá ước tính", + "recommendation.estimated_pricing.why": "Chi phí này sử dụng ánh xạ mô hình được suy ra thay vì hàng định giá trực tiếp.", + "recommendation.high_cost.action": "Mở dòng thời gian của chuỗi và kiểm tra lượt trước trước khi tiếp tục.", + "recommendation.high_cost.title": "Chi phí ước tính cao", + "recommendation.high_cost.why": "Cuộc gọi này đã vượt qua ngưỡng chi phí cao đã định cấu hình.", + "recommendation.large_thread.action": "Thích một chủ đề mới cho công việc tiếp theo không liên quan.", + "recommendation.large_thread.title": "Chủ đề tích lũy lớn", + "recommendation.large_thread.why": "Tổng số phiên tích lũy đủ cao để khiến cho những lượt sau trở nên đắt đỏ.", + "recommendation.low_cache.action": "Kiểm tra xem các tệp, đầu ra công cụ hoặc ngữ cảnh rộng có được giới thiệu lại một cách không cần thiết hay không.", + "recommendation.low_cache.title": "Tái sử dụng bộ nhớ đệm thấp", + "recommendation.low_cache.why": "Dữ liệu đầu vào mới chưa được lưu vào bộ nhớ đệm ở mức cao trong khi khả năng sử dụng lại bộ nhớ đệm ở mức thấp.", + "recommendation.low_output.action": "Trước tiên hãy kiểm tra bối cảnh tổng hợp; chỉ tải bối cảnh thô nếu nguyên nhân không rõ ràng.", + "recommendation.low_output.title": "Cuộc gọi đầu ra lớn", + "recommendation.low_output.why": "Cuộc gọi tiêu tốn nhiều token nhưng tạo ra ít kết quả.", + "recommendation.none.action": "Không có hành động tổng hợp nào được gắn cờ; tiếp tục theo dõi các kiểu sử dụng.", + "recommendation.pricing_gap.action": "Cập nhật giá hoặc thêm bí danh địa phương trước khi tin tưởng vào tổng chi phí.", + "recommendation.pricing_gap.title": "Khoảng cách giá", + "recommendation.pricing_gap.why": "Cuộc gọi mô hình này không có mức giá được định cấu hình nên tổng chi phí được đánh giá thấp hơn mức sử dụng hiển thị.", + "recommendation.reasoning_spike.action": "Xem lại liệu nhiệm vụ này có cần nỗ lực lập luận đã chọn hay không.", + "recommendation.reasoning_spike.title": "Chia sẻ lý luận cao", + "recommendation.reasoning_spike.why": "Đầu ra lý luận chiếm ưu thế đầu ra hiển thị cho cuộc gọi này.", + "recommendation.subagent_attribution.action": "So sánh các cuộc gọi trực tiếp với đại lý phụ được đính kèm hoặc xem xét các cuộc gọi trước khi thay đổi quy trình làm việc.", + "recommendation.subagent_attribution.title": "Phân bổ đại lý phụ", + "recommendation.subagent_attribution.why": "Cuộc gọi này được đính kèm với công việc được ủy quyền và có thể giải thích sự phát triển của luồng gốc.", + "section.allowance": "Phụ cấp", "section.needs_attention": "Cần chú ý", - "section.pricing": "Giá cả", + "section.pricing": "giá cả", "section.recommendations": "Khuyến nghị", "severity.high": "Cao", "severity.medium": "Trung bình", - "severity.review": "Cần rà soát", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "Người dùng", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Lỗi cấu hình hạn mức", - "state.allowance_configured": "Đã cấu hình hạn mức", - "state.best_guess_estimate": "Ước tính gần đúng", - "state.configured": "Đã cấu hình", - "state.configured_price": "Giá đã cấu hình", + "severity.review": "Xem lại", + "source.auto_review": "Tự động xem xét", + "source.codex_initiated": "Codex đã bắt đầu", + "source.subagent": "chất phụ", + "source.subagent_role": "Đại lý phụ: {role}", + "source.user": "người dùng", + "source.user_initiated": "Người dùng bắt đầu", + "state.allowance_config_error": "Lỗi cấu hình phụ cấp", + "state.allowance_configured": "Đã định cấu hình phụ cấp", + "state.best_guess_estimate": "Ước tính dự đoán tốt nhất", + "state.configured": "Đã định cấu hình", + "state.configured_price": "Giá cấu hình", "state.error": "Lỗi", - "state.estimated": "Ước tính", + "state.estimated": "ước tính", "state.loading": "Đang tải", - "state.mixed": "Lẫn lộn", + "state.loading_rows": "Đang tải hàng", + "state.mixed": "Hỗn hợp", "state.no": "Không", - "state.no_calls": "Không có lượt gọi khớp bộ lọc hiện tại.", - "state.no_configured_price": "Chưa cấu hình giá", - "state.no_context_entries": "Không tìm thấy ngữ cảnh cho lượt gọi này.", + "state.no_calls": "Không có cuộc gọi nào phù hợp với bộ lọc hiện tại.", + "state.no_configured_price": "Không có giá cấu hình", + "state.no_context_entries": "Không tìm thấy mục ngữ cảnh nào cho cuộc gọi này.", "state.no_data": "Không có dữ liệu", - "state.no_mapped_rate": "Chưa có tỷ lệ khớp", - "state.no_price": "Chưa có giá", - "state.no_rate": "Chưa có tỷ lệ", - "state.no_rows": "Không có dòng", - "state.no_threads": "Không có cuộc trò chuyện khớp bộ lọc hiện tại.", - "state.none": "Không có", - "state.not_configured": "Chưa cấu hình", - "state.requires_evidence": "Load evidence", - "state.unknown": "Không rõ", + "state.no_mapped_rate": "Không có tỷ lệ được ánh xạ", + "state.no_price": "Không có giá", + "state.no_rate": "Không có giá", + "state.no_rows": "Không có hàng", + "state.no_threads": "Không có chủ đề nào phù hợp với bộ lọc hiện tại.", + "state.none": "không có", + "state.not_configured": "Chưa được định cấu hình", + "state.requires_evidence": "Bằng chứng cần thiết", + "state.unknown": "Không xác định", "state.yes": "Có", - "status.checking": "Đang chạy", - "status.paused": "Tạm dừng", + "status.checking": "Kiểm tra", + "status.paused": "Đã tạm dừng", "status.refresh_error": "Lỗi làm mới", - "status.refreshing": "Đang làm mới", + "status.refreshing": "Làm mới", "status.reloading": "Đang tải lại", "status.static": "Tĩnh", "status.updated": "Đã cập nhật", - "table.cache": "Cache", - "table.cached": "Đã cache", - "table.calls": "lượt gọi", + "table.cache": "Bộ nhớ đệm", + "table.cached": "Đã lưu vào bộ nhớ đệm", + "table.calls": "cuộc gọi", "table.cost": "Chi phí", - "table.effort": "Mức suy luận", - "table.initiated": "Initiated", - "table.last_call": "Lượt gọi cuối", - "table.model": "Mô hình", - "table.more_efforts": "{effort} +{count} mức suy luận", + "table.effort": "nỗ lực", + "table.initiated": "Đã bắt đầu", + "table.last_call": "Cuộc gọi cuối cùng", + "table.model": "người mẫu", + "table.more_efforts": "{effort} +{count} nỗ lực", "table.more_models": "{model} +{count} mô hình", - "table.output": "Đầu ra", - "table.page_status": "{start}-{end} trên {total} {items} · trang {page}/{pages}", - "table.rows": "dòng", + "table.output": "đầu ra", + "table.page_status": "{start}-{end} của {total} {items} · trang {page}/{pages}", + "table.rows": "hàng", "table.signals": "Tín hiệu", "table.source": "Nguồn", - "table.thread": "Cuộc trò chuyện", - "table.threads": "cuộc trò chuyện", - "table.time": "Thời gian", + "table.thread": "chủ đề", + "table.threads": "chủ đề", + "table.time": "thời gian", "table.tokens": "Token", - "table.uncached": "Chưa cache", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "đã gắn", - "thread.attention": "điểm chú ý {score}", - "thread.auto_review": "{count} auto-review", + "table.uncached": "Không được lưu vào bộ nhớ đệm", + "table.visible_status": "Đang hiển thị {end} trong số {total} {items}", + "thread.attached": "đính kèm", + "thread.attention": "chú ý {score}", + "thread.auto_review": "{count} tự động xem xét", "thread.collapse": "Thu gọn", "thread.direct": "trực tiếp", - "thread.expand": "Mở", - "thread.expand_label": "{action} lượt gọi của {thread}. Điểm chú ý {score}.", - "thread.explicit_parent": "cha rõ ràng", - "thread.explicit_parent_thread": "cuộc trò chuyện cha rõ ràng", - "thread.parent": "Parent {id}", + "thread.expand": "Mở rộng", + "thread.expand_label": "{action} {thread} cuộc gọi. Điểm chú ý {score}.", + "thread.explicit_parent": "cha mẹ rõ ràng", + "thread.explicit_parent_thread": "chủ đề gốc rõ ràng", + "thread.parent": "Phụ huynh {id}", "thread.session": "phiên", - "thread.spawned": "được tạo", - "thread.spawned_from": "được tạo từ {thread}", - "thread.spawned_threads": "{count} cuộc trò chuyện con", - "thread.subagent": "{count} subagent", - "thread.unknown": "Cuộc trò chuyện không rõ", - "thread.unmatched_subagent": "subagent chưa khớp" + "thread.spawned": "sinh ra", + "thread.spawned_from": "sinh ra từ {thread}", + "thread.spawned_threads": "{count} chủ đề được sinh ra", + "thread.subagent": "{count} đại lý phụ", + "thread.unknown": "Chủ đề không xác định", + "thread.unmatched_subagent": "chất phụ chưa từng có" } diff --git a/src/codex_usage_tracker/plugin_data/dashboard/locales/zh-Hans.json b/src/codex_usage_tracker/plugin_data/dashboard/locales/zh-Hans.json index ef2ecf4..ad3f1f9 100644 --- a/src/codex_usage_tracker/plugin_data/dashboard/locales/zh-Hans.json +++ b/src/codex_usage_tracker/plugin_data/dashboard/locales/zh-Hans.json @@ -1,510 +1,518 @@ { - "action.check_cache_drop": "Check for reintroduced files or tool output after cache reuse dropped.", - "action.compare_fresh_input": "Compare fresh input with the previous turn before continuing.", - "action.compare_subagent_calls": "Compare attached subagent or review calls before changing the parent workflow.", - "action.configure_pricing": "Configure pricing before trusting cost totals.", - "action.copied": "Copied", - "action.copy_failed": "Copy failed", - "action.expand_or_select_recommendations": "Expand calls or select a row for call-level recommendations.", - "action.exported": "Exported {count}", - "action.inspect_thread_timeline": "Inspect the thread timeline and consider starting a fresh thread.", - "action.review_context_growth": "Review where context growth begins and consider starting a fresh thread.", - "action.review_reasoning_effort": "Review whether reasoning effort is appropriate for this task.", - "action.run": "Run", - "action.set_limits": "Set limits", - "action.use_aggregate_first": "Use the aggregate fields first; load context only if the signal is still unclear.", - "allowance.counted": "{value} credits counted toward Codex usage limits", - "allowance.cr_left": "{value} cr left", - "allowance.credit_coverage": "Credit coverage {ratio} of loaded tokens.", - "allowance.credit_rates": "Credit rates: {source}.", - "allowance.credits_remaining": "{value} credits remaining", - "allowance.init_hint": "Run codex-usage-tracker init-allowance to add remaining usage windows.", - "allowance.of_allowance": "{ratio} of allowance", - "allowance.of_total": "{used} of {total} credits", - "allowance.rate_card_error": "Rate-card error: {error}", - "allowance.remaining": "{value} remaining", - "allowance.resets": "Resets: {resets}", - "allowance.row_no_rate": "No mapped Codex credit rate", - "allowance.title_hint": "Add ~/.codex-usage-tracker/allowance.json to show 5h and weekly remaining usage.", - "allowance.used_vs_remaining": "{used} used vs {remaining} remaining", - "allowance.window_configured": "{label} configured", - "allowance.windows": "Allowance windows: {windows}", - "aria.current_view_actions": "Current view actions", - "aria.dashboard_status": "Dashboard status", - "aria.dashboard_view": "Dashboard view", - "aria.history_title": "Active sessions only is the default. All history scans archived session logs during live refresh.", - "aria.inspect_thread": "Inspect {thread} usage", - "aria.refresh_controls": "Dashboard refresh controls", - "aria.table_pages": "Table pages", - "badge.costs": "Costs", - "badge.credits": "Credits", - "badge.live": "Live", - "badge.metadata_mode": "Metadata {mode}", - "badge.metadata_normal": "Metadata normal", - "badge.no_costs": "No costs", - "badge.parser_warnings": "Parser warnings", - "badge.static": "Static", - "badge.unofficial_project": "Unofficial project", - "badge.unofficial_project_title": "Codex Usage Tracker is independent and is not made by, affiliated with, endorsed by, sponsored by, or supported by OpenAI. OpenAI and Codex are trademarks of OpenAI.", - "button.back_to_dashboard": "Back to dashboard", - "button.clear": "Clear", - "button.copy_link": "Copy link", - "button.enable_context_loading": "Enable context loading", - "button.export_csv": "Export CSV", - "button.hide_details": "Hide details", - "button.hide_tool_output": "Hide tool output", - "button.include_tool_output": "Include tool output", - "button.load_context": "Load context", - "button.load_more": "Load more", - "button.load_older_context": "Load older entries", - "button.next": "Next", - "button.next_call": "Next call", - "button.no_char_limit": "No char limit", - "button.open_investigator": "Open investigator", - "button.previous": "Previous", - "button.previous_call": "Previous call", - "button.refresh": "Refresh", - "button.show_compaction_history": "Show compacted replacement", - "button.show_tool_output": "Show tool output", - "button.show_turn_evidence": "Show turn log evidence", - "button.top": "Top", - "call.cache_accounting_delta": "Cache/accounting delta", - "call.cache_cold": "Cold resume / stale cache", - "call.cache_diagnostics": "Cache diagnostics", - "call.cache_partial": "Partial cache miss", - "call.cache_spike": "Uncached spike", - "call.cache_steady": "Steady cache profile", - "call.cache_warm": "Warm cache reuse", - "call.compaction_diagnostics": "Compaction diagnostics", - "call.compaction_hint": "Loaded evidence can show explicit compaction events. Redacted replacement history is shown only after the compacted replacement action.", - "call.context_estimate": "Context change estimate", - "call.context_estimate_hint": "Load raw evidence to compare exact uncached input with visible log entries. The gap should be treated as hidden scaffolding, serialization, or tokenizer estimate error.", - "call.derived_label": "Derived from adjacent aggregate calls", - "call.estimated_label": "Estimated from visible log volume", - "call.evidence_label": "Runtime evidence", - "call.exact_accounting": "Exact token accounting", - "call.exact_label": "Exact from token callback", - "call.hidden_estimate": "Unexplained hidden/serialized input estimate", - "call.serialized_bound_hint": "Upper-bound local JSONL structure; not exact prompt text.", - "call.serialized_breakdown": "Serialized evidence buckets", - "call.serialized_bucket_detail": "{count} fields · {chars} chars", - "call.serialized_candidate": "Possible serialized overhead", - "call.serialized_candidate_hint": "Serialized upper bound minus visible estimate, capped by exact uncached input", - "call.serialized_upper_bound": "Serialized local upper bound", - "call.remaining_after_serialized": "Remaining after serialized bound", - "call.remaining_after_serialized_hint": "Uncached input not covered even by serialized upper bound", - "call.visible_gap": "Uncached input minus visible estimate", - "call.no_previous": "No previous call in this resolved thread.", - "call.not_found": "Selected call was not found in the loaded dashboard rows.", - "call.open_hint": "Click a call row for deep diagnostics.", - "call.position": "Call {position} in this resolved thread.", - "call.post_compaction": "Post-compaction possible", - "call.raw_evidence": "Raw evidence", - "call.visible_estimate": "Visible new context estimate", - "caption.ascending": "ascending", - "caption.call_investigator": "Investigating call {record}.", - "caption.calls": "Showing individual model calls sorted by {sort}. {loaded}.", - "caption.date_prefix": "{label}. ", - "caption.descending": "descending", - "caption.initial_calls": "Showing individual model calls.", - "caption.insights": "Ranked by cost, usage credits, cache reuse, context pressure, and pricing confidence.", - "caption.loaded": "{loaded} calls loaded", - "caption.loaded_capped": "{loaded} of {available} calls loaded", + "action.check_cache_drop": "检查缓存重用删除后重新引入的文件或工具输出。", + "action.compare_fresh_input": "在继续之前,将新输入与上一回合进行比较。", + "action.compare_subagent_calls": "在更改父工作流程之前比较附加的子代理或审查调用。", + "action.configure_pricing": "在信任成本总计之前配置定价。", + "action.copied": "已复制", + "action.copy_failed": "复制失败", + "action.expand_or_select_recommendations": "展开调用或选择一行以获取调用级别的建议。", + "action.exported": "导出 {count}", + "action.inspect_thread_timeline": "检查线程时间线并考虑启动一个新线程。", + "action.review_context_growth": "查看上下文增长的开始位置并考虑启动一个新线程。", + "action.review_reasoning_effort": "检查推理工作是否适合这项任务。", + "action.run": "运行", + "action.set_limits": "设定限制", + "action.use_aggregate_first": "先使用聚合字段;仅当信号仍不清楚时才加载上下文。", + "allowance.counted": "{value} 积分计入 Codex 使用限制", + "allowance.cr_left": "{value} cr 左", + "allowance.credit_coverage": "加载Token的信用覆盖率{ratio}。", + "allowance.credit_rates": "信用利率:{source}。", + "allowance.credits_remaining": "剩余 {value} 学分", + "allowance.init_hint": "运行 codex-usage-tracker init-allowance 以添加剩余的使用窗口。", + "allowance.of_allowance": "津贴的{ratio}", + "allowance.of_total": "{total} 学分中的 {used}", + "allowance.rate_card_error": "价目表错误:{error}", + "allowance.remaining": "剩余 {value}", + "allowance.resets": "重置:{resets}", + "allowance.row_no_rate": "没有映射 Codex 信用率", + "allowance.title_hint": "添加 ~/.codex-usage-tracker/allowance.json 以显示 5 小时和每周的剩余使用情况。", + "allowance.used_vs_remaining": "已用 {used} 与剩余 {remaining}", + "allowance.window_configured": "{label} 配置", + "allowance.windows": "津贴窗口:{windows}", + "aria.current_view_actions": "当前视图操作", + "aria.dashboard_status": "仪表板状态", + "aria.dashboard_view": "仪表板视图", + "aria.history_title": "默认情况下仅活动会话。所有历史记录都会在实时刷新期间扫描存档的会话日志。", + "aria.inspect_thread": "检查 {thread} 的使用情况", + "aria.refresh_controls": "仪表板刷新控件", + "aria.table_pages": "表页", + "badge.costs": "成本", + "badge.credits": "制作人员", + "badge.live": "直播", + "badge.metadata_mode": "元数据 {mode}", + "badge.metadata_normal": "元数据正常", + "badge.no_costs": "无成本", + "badge.parser_warnings": "解析器警告", + "badge.static": "静态", + "badge.unofficial_project": "非官方项目", + "badge.unofficial_project_title": "Codex Usage Tracker 是独立的,不由 OpenAI 制作、附属、认可、赞助或支持。 OpenAI 和 Codex 是 OpenAI 的商标。", + "button.back_to_dashboard": "返回仪表板", + "button.clear": "清除", + "button.copy_link": "复制链接", + "button.enable_context_loading": "启用上下文加载", + "button.export_csv": "导出CSV", + "button.full_serialized_analysis": "运行完整的序列化分析", + "button.hide_details": "隐藏详细信息", + "button.hide_tool_output": "隐藏工具输出", + "button.include_tool_output": "包括工具输出", + "button.load_context": "加载上下文", + "button.load_more": "加载更多", + "button.load_older_context": "加载旧条目", + "button.next": "下一步", + "button.next_call": "下次调用", + "button.no_char_limit": "无字符数限制", + "button.open_investigator": "开放调查员", + "button.previous": "上一页", + "button.previous_call": "上次调用", + "button.refresh": "刷新", + "button.show_compaction_history": "显示压缩替换", + "button.show_tool_output": "显示工具输出", + "button.show_turn_evidence": "显示转弯日志证据", + "button.top": "顶部", + "call.cache_accounting_delta": "缓存/记帐增量", + "call.cache_cold": "冷恢复/陈旧缓存", + "call.cache_diagnostics": "缓存诊断", + "call.cache_partial": "部分缓存未命中", + "call.cache_spike": "未缓存的峰值", + "call.cache_steady": "稳定的缓存配置文件", + "call.cache_warm": "热缓存重用", + "call.compaction_diagnostics": "压实诊断", + "call.compaction_hint": "加载的证据可以显示明确的压缩事件。仅在压缩替换操作之后才会显示编辑的替换历史记录。", + "call.context_estimate": "上下文变化估计", + "call.context_estimate_hint": "将精确的未缓存输入与分词器计数的可见日志证据进行比较。该差距应被视为隐藏的脚手架、序列化或分词器估计错误。", + "call.derived_label": "源自相邻的聚合调用", + "call.estimated_label": "根据可见日志量估算", + "call.evidence_label": "运行时证据", + "call.exact_accounting": "精确的Token记账", + "call.exact_label": "完全来自Token回调", + "call.hidden_estimate": "无法解释的隐藏/序列化输入估计", + "call.no_previous": "此已解析线程中没有先前的调用。", + "call.not_found": "在加载的仪表板行中找不到选定的调用。", + "call.open_hint": "单击调用行进行深度诊断。", + "call.position": "在此已解析线程中调用 {position}。", + "call.post_compaction": "可进行后压实", + "call.raw_evidence": "原始证据", + "call.remaining_after_serialized": "序列化绑定后剩余", + "call.remaining_after_serialized_hint": "即使序列化上限也未覆盖未缓存的输入", + "call.serialized_bound_hint": "上限局部 JSONL 结构;不准确的提示文本。", + "call.serialized_breakdown": "序列化证据分组", + "call.serialized_bucket_detail": "{count} 字段 · {chars} 字符", + "call.serialized_candidate": "可能的序列化开销", + "call.serialized_candidate_hint": "序列化上限减去可见估计,由精确的未缓存输入限制", + "call.serialized_deferred": "已加载快速估算;完整的序列化分组分析已推迟。", + "call.serialized_quick_hint": "快速估计", + "call.serialized_upper_bound": "序列化局部上限", + "call.visible_estimate": "可见的新上下文估计", + "call.visible_gap": "未缓存的输入减去可见的估计", + "caption.ascending": "上升", + "caption.call_investigator": "调查调用 {record}。", + "caption.calls": "显示按 {sort} 排序的各个模型调用。 {loaded}。", + "caption.date_prefix": "{label}。", + "caption.descending": "下降", + "caption.initial_calls": "显示单独的模型调用。", + "caption.insights": "按成本、使用积分、缓存重用、上下文压力和定价信心排名。", + "caption.loaded": "{loaded} 调用已加载", + "caption.loaded_capped": "已加载 {available} 调用中的 {loaded}", + "caption.rows_loaded_progress": "已加载行:{loaded} / {total}", + "caption.rows_loading_background": "仪表板汇总已就绪。行数据正在后台加载。", + "caption.rows_loading_progress": "正在加载行:{loaded} / {total}", "caption.sort_direction": "{label} {direction}", - "caption.threads": "Showing {threads} threads from {calls} filtered calls, sorted by {sort}. {loaded}. Click a thread to expand its calls.", - "context.api_http": "Context API returned HTTP {status}.", - "context.api_unavailable": "Context API is unavailable here. Run codex-usage-tracker serve-dashboard --open for on-demand context loading.", - "context.auto_loading": "Loading selected-turn evidence with tool output included.", - "context.chars_omitted": "{count} chars over budget omitted.", - "context.compaction_detected": "Compaction detected", - "context.compaction_replacement": "Compacted replacement context", - "context.compaction_replacement_count": "{count} replacement history entries available.", - "context.disabled_hint": "Context loading is off for this dashboard server. Enable it here to load local JSONL context on demand.", - "context.enabled_note": "Context loading is enabled. Press Show turn log evidence to read this call from the local JSONL source.", - "context.file_hint": "Open this dashboard with codex-usage-tracker serve-dashboard to load raw context on demand.", - "context.line": "line {line}", - "context.loading": "Loading local context...", - "context.local_redacted": "Local JSONL context loaded on demand. Prompts and tool output are redacted for common secret patterns and are not persisted to SQLite or dashboard HTML.", - "context.no_char_limit_active": "No character limit applied.", - "context.no_record_id": "This row has no record id for context lookup.", - "context.no_response": "No response body", - "context.older_omitted": "{count} older entries omitted.", - "context.ready_hint": "Context is not embedded in this dashboard. Press a button to read this call from the local JSONL source.", - "context.settings_http": "Context settings returned HTTP {status}.", - "context.source": "Source: {file}:{line}", - "context.token_breakdown": "Token breakdown", - "context.token_cached": "Cached", - "context.token_input": "Input", - "context.token_output": "Output", - "context.token_reasoning": "Reasoning", - "context.token_required": "Context loading requires a localhost dashboard API token.", - "context.token_scope_call": "This call", - "context.token_scope_earlier": "Earlier token count in same turn", - "context.token_scope_previous": "Previous token count in same turn", - "context.token_scope_selected": "Selected call token count", - "context.token_scope_session": "Session cumulative", - "context.token_total": "Total", - "context.token_type": "Type", - "context.token_uncached": "Uncached", - "context.tool_included": "Tool output included with redaction and size limits.", - "context.tool_omitted": "Tool output hidden for this view.", - "credit.configured_rate": "Configured rate", - "credit.estimated_mapping": "Estimated mapping", - "credit.inferred_mapping": "Inferred model mapping", - "credit.no_mapped_rate": "No mapped rate", - "credit.no_rate": "No credit rate", - "credit.official_match": "Official rate-card match", - "credit.user_rate": "User-provided credit rate", - "credit.with_status": "{value} credits · {status}", - "dashboard.call_details": "Call Details", - "dashboard.detail.empty": "Hover or click a row to inspect aggregate usage fields.", - "dashboard.eyebrow": "Local Codex analytics", - "dashboard.local_storage_note": "The dashboard header also remembers your language choice locally.", - "dashboard.model_calls": "Model Calls", - "dashboard.title": "Usage Dashboard", - "dashboard.top_threads_by_attention": "Top Threads by Attention Score", - "dashboard.view.call": "Call Investigator", - "dashboard.view.calls": "Calls", - "dashboard.view.insights": "Insights", - "dashboard.view.threads": "Threads", - "date.custom": "Custom", - "date.invalid_range": "Invalid date range", - "date.range_between": "{prefix} {start} to {end}", + "caption.threads": "显示来自 {calls} 过滤调用的 {threads} 线程,按 {sort} 排序。 {loaded}。单击一个线程可展开其调用。", + "context.api_http": "上下文 API 返回 HTTP {status}。", + "context.api_unavailable": "上下文 API 在这里不可用。运行 codex-usage-tracker serve-dashboard --open 进行按需上下文加载。", + "context.auto_loading": "加载包含工具输出的选定转弯证据。", + "context.chars_omitted": "省略了超出预算的 {count} 字符。", + "context.compaction_detected": "检测到压实", + "context.compaction_replacement": "压缩替换上下文", + "context.compaction_replacement_count": "{count} 替换历史记录条目可用。", + "context.disabled_hint": "此仪表板服务器的上下文加载已关闭。此处启用它以按需加载本地 JSONL 上下文。", + "context.enabled_note": "上下文加载已启用。按显示转向日志证据可从本地 JSONL 源读取此调用。", + "context.file_hint": "使用 codex-usage-tracker serve-dashboard 打开此仪表板以按需加载原始上下文。", + "context.line": "行 {line}", + "context.loading": "正在加载本地上下文...", + "context.local_redacted": "按需加载本地 JSONL 上下文。提示和工具输出针对常见秘密模式进行了编辑,并且不会保留到 SQLite 或仪表板 HTML。", + "context.no_char_limit_active": "没有字符数限制。", + "context.no_record_id": "该行没有用于上下文查找的记录 ID。", + "context.no_response": "无响应体", + "context.older_omitted": "{count} 旧条目被省略。", + "context.ready_hint": "上下文未嵌入此仪表板中。按按钮可从本地 JSONL 源读取此调用。", + "context.settings_http": "上下文设置返回 HTTP {status}。", + "context.source": "来源:{file}:{line}", + "context.token_breakdown": "Token细分", + "context.token_cached": "缓存", + "context.token_input": "输入", + "context.token_output": "输出", + "context.token_reasoning": "推理", + "context.token_required": "上下文加载需要 localhost 仪表板 API Token。", + "context.token_scope_call": "这个调用", + "context.token_scope_earlier": "同一回合较早的Token计数", + "context.token_scope_previous": "同一回合中之前的Token计数", + "context.token_scope_selected": "选定的调用Token计数", + "context.token_scope_session": "会话累计", + "context.token_total": "总计", + "context.token_type": "类型", + "context.token_uncached": "未缓存", + "context.tool_included": "工具输出包含修订和大小限制。", + "context.tool_omitted": "该视图的工具输出已隐藏。", + "credit.configured_rate": "配置速率", + "credit.estimated_mapping": "预计测绘", + "credit.inferred_mapping": "推断模型映射", + "credit.no_mapped_rate": "无映射速率", + "credit.no_rate": "无信用率", + "credit.official_match": "官方价目表匹配", + "credit.user_rate": "用户提供的信用率", + "credit.with_status": "{value} 学分 · {status}", + "dashboard.call_details": "调用详情", + "dashboard.detail.empty": "将鼠标悬停或单击一行可检查聚合使用字段。", + "dashboard.eyebrow": "本地 Codex 分析", + "dashboard.local_storage_note": "仪表板标题还会记住您在本地选择的语言。", + "dashboard.model_calls": "模型调用", + "dashboard.title": "使用情况仪表板", + "dashboard.top_threads_by_attention": "按注意力分数排列的热门话题", + "dashboard.view.call": "致电调查员", + "dashboard.view.calls": "调用", + "dashboard.view.insights": "见解", + "dashboard.view.threads": "线程数", + "date.custom": "定制", + "date.invalid_range": "日期范围无效", + "date.range_between": "{prefix} {start} 至 {end}", "date.range_exact": "{prefix} {date}", - "date.range_from": "{prefix} from {start}", - "date.range_through": "{prefix} through {end}", - "detail.agent_nickname": "Agent nickname", - "detail.agent_role": "Agent role", - "detail.allowance_impact": "Allowance impact", - "detail.attached_calls": "Attached calls", - "detail.auto_review_calls": "Auto-review calls", - "detail.cache_savings": "Cache savings", - "detail.call_number": "call {number}", - "detail.calls": "Calls", - "detail.context_window": "Context window", - "detail.cost_usage_context": "Cost, usage, and context", - "detail.credit_confidence": "Credit confidence", - "detail.credit_model": "Credit model", - "detail.credit_note": "Credit note", - "detail.credit_source": "Credit source", - "detail.credit_source_fetched": "Credit source fetched", - "detail.credit_tier": "Credit tier", - "detail.cwd": "Cwd", - "detail.efficiency_signals": "Efficiency signals", - "detail.first_expensive_turn": "First expensive turn", - "detail.git_branch": "Git branch", - "detail.largest_cumulative_jump": "Largest cumulative jump", - "detail.latest_activity": "Latest activity", - "detail.model_mix": "Model mix", - "detail.next_action": "Next action", - "detail.no_above_thresholds": "None above thresholds", - "detail.no_aggregate_action": "No aggregate action is flagged.", - "detail.parent_session": "Parent session", - "detail.parent_thread": "Parent thread", - "detail.parent_updated": "Parent updated", - "detail.pricing_model": "Pricing model", - "detail.pricing_status": "Pricing status", - "detail.project_cwd": "Project cwd", - "detail.project_tags": "Project tags", - "detail.raw_identifiers": "Raw aggregate identifiers", - "detail.reasoning_mix": "Reasoning mix", - "detail.relationships": "Relationships", - "detail.remote_hash": "Remote hash", - "detail.remote_label": "Remote label", - "detail.secondary_thread_fields": "Secondary thread fields", - "detail.source_file_line": "Source file and line", - "detail.source_line": "Source line", - "detail.spawned_child_calls": "Spawned child calls", - "detail.spawned_from": "Spawned from", - "detail.spawned_threads": "Spawned threads", - "detail.subagent_before_spike": "Subagent before spike", - "detail.subagent_calls": "Subagent calls", - "detail.subagent_type": "Subagent type", - "detail.thread_attachment": "Thread attachment", - "detail.thread_attention_summary": "Thread attention summary", - "detail.thread_lifecycle": "Thread lifecycle", - "detail.thread_narrative": "Thread narrative", - "detail.thread_source": "Thread source", - "detail.thread_timeline": "Thread timeline", - "detail.timeline_context": "context {value}", - "detail.timeline_empty": "No calls in this thread.", - "detail.timeline_meta": "{tokens} tokens · {cost} · {credits} · cache {cache}", - "detail.timestamp": "Timestamp", - "detail.token_pricing_breakdown": "Token and pricing breakdown", - "detail.tokens_at": "{tokens} tokens at {time}", - "detail.turn": "Turn", - "detail.why_flagged": "Why flagged", - "docs.dashboard_guide": "Dashboard guide", - "effort.high": "high", - "effort.low": "low", - "effort.medium": "medium", - "filter.confidence": "Confidence", - "filter.effort": "Reasoning", - "filter.end": "End", - "filter.model": "Model", - "filter.project": "Project", - "filter.reasoning": "Reasoning", - "filter.search": "Search", - "filter.search_placeholder": "Thread, cwd, model", - "filter.session": "Session", - "filter.sort": "Sort", - "filter.start": "Start", - "filter.thread": "Thread", - "filter.time": "Time", - "flag.elevated_context_use": "Elevated context use", - "flag.expensive_low_output_call": "Expensive low-output call", - "flag.high_context_use": "High context use", - "flag.high_estimated_cost": "High estimated cost", - "flag.high_reasoning_share": "High reasoning share", - "flag.low_cache_reuse": "Low cache reuse", - "history.active_hidden": "Active sessions only; {count} archived calls hidden", - "history.active_only": "Active sessions only", - "history.all_empty": "All history selected; no archived calls are indexed yet", - "history.all_includes": "All history includes {count} archived calls", - "history.archived_scan_hint": "{detail}. Archived sessions are scanned only when All history is selected during live refresh.", - "insight.apply_cache_misses": "Apply cache-misses preset", - "insight.apply_context_bloat": "Apply context-bloat preset", - "insight.codex_allowance_usage": "Codex allowance usage", - "insight.context_bloat": "Context bloat", - "insight.context_bloat_body": "{calls} calls are at or above {ratio} context use.", - "insight.costliest_thread": "Costliest thread", - "insight.costliest_thread_body": "{thread} has {calls} calls and {tokens} tokens.", - "insight.credit_coverage_body": "{ratio} of visible tokens map to Codex credit rates.", - "insight.estimated_pricing": "Estimated pricing", - "insight.estimated_pricing_body": "Marked best-guess prices are included, but should be reviewed separately.", - "insight.inspect_selected_call": "Inspect selected call", - "insight.low_cache_reuse": "Low cache reuse", - "insight.low_cache_reuse_body": "{calls} calls are under {ratio} cache reuse. Start with {thread}.", - "insight.open_thread_timeline": "Open thread timeline", - "insight.reasoning_output_spike": "Reasoning output spike", - "insight.reasoning_spike_body": "{thread} has the largest reasoning-output call in the current filter.", - "insight.review_estimates": "Review estimates", - "insight.review_highest_credit": "Review highest-credit calls", - "insight.review_pricing_gaps": "Review pricing gaps", - "insight.unpriced_usage": "Unpriced usage", - "insight.unpriced_usage_body": "These tokens are omitted from estimated cost totals until pricing is configured.", - "language.english": "English", - "language.label": "Language", - "language.vietnamese": "Tiếng Việt", - "live.checking_usage": "Checking for new usage...", - "live.every": "Live refresh every {seconds}s", - "live.history_static_hint": "Run codex-usage-tracker serve-dashboard to switch between active sessions and all history from the dashboard.", - "live.indexed": " Indexed {rows} aggregate rows from {files} logs.", - "live.load_static_hint": "Run codex-usage-tracker serve-dashboard to load a different history size from the dashboard.", - "live.paused": "Live refresh paused", - "live.refresh_suffix": ". Reload this page after regenerating a static dashboard, or run codex-usage-tracker serve-dashboard.", - "live.refresh_unavailable": "Live refresh unavailable: {message}{suffix}", - "live.refreshing_index": "Refreshing local usage index...", - "live.reloading_static": "Reloading static dashboard snapshot...", - "live.skipped": " Skipped {count} malformed token-count events.", - "live.updated_detail": "Updated {time}. {loaded}. {history}.{indexed}{skipped}", - "metric.attention_score": "Attention score", - "metric.cache_ratio": "Cache ratio", - "metric.cache_trend": "Cache trend", - "metric.cached_input": "Cached input", - "metric.codex_credits": "Codex credits", - "metric.context_trend": "Context trend", - "metric.context_use": "Context use", - "metric.estimated_cost": "Estimated cost", - "metric.input_tokens": "Input tokens", - "metric.last_call_input": "Last call input", - "metric.last_call_total": "Last call total", - "metric.max_context_use": "Max context use", - "metric.output": "Output", - "metric.output_tokens": "Output tokens", - "metric.reasoning_output": "Reasoning output", - "metric.remaining_usage": "Remaining usage", - "metric.session_cumulative": "Session cumulative", - "metric.total": "Total", - "metric.total_tokens": "Total tokens", - "metric.uncached_input": "Uncached input", - "metric.usage_credits": "Usage credits", - "metric.usage_remaining": "Remaining usage", - "metric.visible_calls": "Visible calls", - "nav.history": "History", - "nav.live": "Live", - "nav.load": "Load", - "option.active_sessions_only": "Active sessions only", - "option.all_confidence": "All confidence", - "option.all_efforts": "All efforts", - "option.all_history": "All history", - "option.all_models": "All models", - "option.all_time": "All time", - "option.custom_range": "Custom range", - "option.estimated_cost": "Estimated cost", - "option.estimated_credit_mapping": "Estimated credit mapping", - "option.exact_cost": "Exact cost", - "option.exact_credit_rate": "Exact credit rate", - "option.highest_codex_credits": "Highest Codex credits", - "option.highest_context_use": "Highest context use", - "option.highest_estimated_cost": "Highest estimated cost", - "option.last_7_days": "Last 7 days", - "option.load_10000": "10,000 calls", - "option.load_20000": "20,000 calls", - "option.load_5000": "5,000 calls", - "option.load_all": "All calls", - "option.lowest_cache_ratio": "Lowest cache ratio", - "option.missing_credit_rate": "Missing credit rate", - "option.most_signals": "Most signals", - "option.most_tokens": "Most tokens", - "option.needs_attention": "Needs attention", - "option.newest_calls": "Newest calls", - "option.this_month": "This month", - "option.this_week": "This week", - "option.thread_name": "Thread name", - "option.today": "Today", - "option.unpriced_cost": "Unpriced cost", - "option.user_credit_override": "User credit override", - "parser.warnings_title": "Latest refresh reported {count} parser diagnostics: {entries}. Run codex-usage-tracker inspect-log to investigate schema drift.", - "preset.cache_misses": "Cache misses", - "preset.cache_misses_caption": "Cache misses preset", - "preset.cache_misses_desc": "Low cache-ratio calls grouped by cwd, model, and thread.", - "preset.caption": "{caption}: {description}", - "preset.context_bloat": "Context bloat", - "preset.context_bloat_caption": "Context bloat preset", - "preset.context_bloat_desc": "Calls over 60% context use or with very high cumulative tokens.", - "preset.description": "One-click starting points for common usage questions.", - "preset.estimated_price_review": "Estimated-price review", - "preset.estimated_price_review_caption": "Estimated-price review preset", - "preset.estimated_price_review_desc": "Usage priced with marked best-guess estimates.", - "preset.highest_codex_credits": "Highest Codex credits", - "preset.highest_codex_credits_caption": "Highest Codex credits preset", - "preset.highest_codex_credits_desc": "Calls sorted by estimated impact on Codex usage allowance.", - "preset.highest_cost_threads": "Highest-cost threads", - "preset.highest_cost_threads_caption": "Highest-cost threads preset", - "preset.highest_cost_threads_desc": "Threads sorted by estimated spend, with subagents attached.", - "preset.investigation_presets": "Investigation presets", - "preset.no_preset": "No preset applied.", - "preset.pricing_gaps": "Pricing gaps", - "preset.pricing_gaps_caption": "Pricing gaps preset", - "preset.pricing_gaps_desc": "Unpriced usage that makes estimated cost totals incomplete.", - "pricing.configure_hint": "Run codex-usage-tracker update-pricing to configure estimated costs.", - "pricing.fetched": "fetched {time}", - "pricing.pinned": "pinned snapshot", - "pricing.source": "Pricing source", - "pricing.tier": "{tier} tier", - "pricing.title": "{parts}. Internal Codex labels may use marked best-guess estimates.{warning}", - "pricing.title_fetched": "{parts}. Fetched from {url} at {time}. Internal Codex labels may use marked best-guess estimates.{warning}", - "privacy.aliases_preserved": "Configured project aliases are treated as explicit display opt-ins.", - "privacy.cwd_redacted": "Raw cwd paths are redacted.", - "privacy.git_branch_hidden": "Git branch is hidden.", - "privacy.git_remote_label_hidden": "Git remote labels are hidden.", - "privacy.mode": "Project metadata privacy mode: {mode}.", - "privacy.normal_title": "Project metadata is shown with local cwd, project, branch, and configured labels.", - "privacy.project_names_redacted": "Unnamed projects use stable hashed labels.", - "privacy.relative_cwd_hidden": "Relative cwd is hidden.", - "privacy.tags_hidden": "Project tags are hidden.", - "recommendation.context_bloat.action": "Consider starting a fresh Codex thread if older context is no longer relevant.", - "recommendation.context_bloat.title": "High context pressure", - "recommendation.context_bloat.why": "This call is using a large share of the model context window.", - "recommendation.elevated_context.action": "Check whether the thread can be narrowed before adding more work.", - "recommendation.elevated_context.title": "Elevated context pressure", - "recommendation.elevated_context.why": "Context use is elevated and may become costly in later turns.", - "recommendation.estimated_pricing.action": "Review pricing coverage and pin or override the model rate if this call matters.", - "recommendation.estimated_pricing.title": "Estimated pricing", - "recommendation.estimated_pricing.why": "This cost uses an inferred model mapping rather than a direct pricing row.", - "recommendation.high_cost.action": "Open the thread timeline and inspect the preceding turn before continuing.", - "recommendation.high_cost.title": "High estimated cost", - "recommendation.high_cost.why": "This call crossed the configured high-cost threshold.", - "recommendation.large_thread.action": "Prefer a new thread for unrelated follow-up work.", - "recommendation.large_thread.title": "Large cumulative thread", - "recommendation.large_thread.why": "The session cumulative total is high enough to make later turns expensive.", - "recommendation.low_cache.action": "Check whether files, tool output, or broad context were reintroduced unnecessarily.", - "recommendation.low_cache.title": "Low cache reuse", - "recommendation.low_cache.why": "Fresh uncached input is high while cache reuse is low.", - "recommendation.low_output.action": "Inspect aggregate context first; load raw context only if the cause is unclear.", - "recommendation.low_output.title": "Large low-output call", - "recommendation.low_output.why": "The call consumed many tokens but produced little output.", - "recommendation.none.action": "No aggregate action is flagged; continue monitoring usage patterns.", - "recommendation.pricing_gap.action": "Update pricing or add a local alias before trusting cost totals.", - "recommendation.pricing_gap.title": "Pricing gap", - "recommendation.pricing_gap.why": "This model call has no configured price, so cost totals understate visible usage.", - "recommendation.reasoning_spike.action": "Review whether this task needs the selected reasoning effort.", - "recommendation.reasoning_spike.title": "High reasoning share", - "recommendation.reasoning_spike.why": "Reasoning output dominates visible output for this call.", - "recommendation.subagent_attribution.action": "Compare direct calls with attached subagent or review calls before changing workflow.", - "recommendation.subagent_attribution.title": "Subagent attribution", - "recommendation.subagent_attribution.why": "This call is attached to delegated work and may explain parent-thread growth.", - "section.allowance": "Allowance", - "section.needs_attention": "Needs Attention", - "section.pricing": "Pricing", - "section.recommendations": "Recommendations", - "severity.high": "High", - "severity.medium": "Medium", - "severity.review": "Review", - "source.auto_review": "Auto-review", - "source.codex_initiated": "Codex initiated", - "source.subagent": "Subagent", - "source.subagent_role": "Subagent: {role}", - "source.user": "User", - "source.user_initiated": "User initiated", - "state.allowance_config_error": "Allowance config error", - "state.allowance_configured": "Allowance configured", - "state.best_guess_estimate": "Best-guess estimate", - "state.configured": "Configured", - "state.configured_price": "Configured price", - "state.error": "Error", - "state.estimated": "Estimated", - "state.loading": "Loading", - "state.mixed": "Mixed", - "state.no": "No", - "state.no_calls": "No calls match the current filters.", - "state.no_configured_price": "No configured price", - "state.no_context_entries": "No context entries found for this call.", - "state.no_data": "No data", - "state.no_mapped_rate": "No mapped rate", - "state.no_price": "No price", - "state.no_rate": "No rate", - "state.no_rows": "No rows", - "state.no_threads": "No threads match the current filters.", - "state.none": "None", - "state.not_configured": "Not configured", - "state.requires_evidence": "Load evidence", - "state.unknown": "Unknown", - "state.yes": "Yes", - "status.checking": "Checking", - "status.paused": "Paused", - "status.refresh_error": "Refresh error", - "status.refreshing": "Refreshing", - "status.reloading": "Reloading", - "status.static": "Static", - "status.updated": "Updated", - "table.cache": "Cache", - "table.cached": "Cached", - "table.calls": "calls", - "table.cost": "Cost", - "table.effort": "Effort", - "table.initiated": "Initiated", - "table.last_call": "Last Call", - "table.model": "Model", - "table.more_efforts": "{effort} +{count} efforts", - "table.more_models": "{model} +{count} models", - "table.output": "Output", - "table.page_status": "{start}-{end} of {total} {items} · page {page}/{pages}", - "table.rows": "rows", - "table.signals": "Signals", - "table.source": "Source", - "table.thread": "Thread", - "table.threads": "threads", - "table.time": "Time", - "table.tokens": "Tokens", - "table.uncached": "Uncached", - "table.visible_status": "Showing {end} of {total} {items}", - "thread.attached": "attached", - "thread.attention": "attention {score}", - "thread.auto_review": "{count} auto-review", - "thread.collapse": "Collapse", - "thread.direct": "direct", - "thread.expand": "Expand", - "thread.expand_label": "{action} {thread} calls. Attention score {score}.", - "thread.explicit_parent": "explicit parent", - "thread.explicit_parent_thread": "explicit parent thread", - "thread.parent": "Parent {id}", - "thread.session": "session", - "thread.spawned": "spawned", - "thread.spawned_from": "spawned from {thread}", - "thread.spawned_threads": "{count} spawned threads", - "thread.subagent": "{count} subagent", - "thread.unknown": "Unknown thread", - "thread.unmatched_subagent": "unmatched subagent" + "date.range_from": "{prefix} 来自 {start}", + "date.range_through": "{prefix} 到 {end}", + "detail.agent_nickname": "代理昵称", + "detail.agent_role": "代理角色", + "detail.allowance_impact": "津贴影响", + "detail.attached_calls": "附加调用", + "detail.auto_review_calls": "自动审核调用", + "detail.cache_savings": "缓存节省", + "detail.call_number": "致电 {number}", + "detail.calls": "调用", + "detail.context_window": "上下文窗口", + "detail.cost_usage_context": "成本、使用情况和背景", + "detail.credit_confidence": "信用信心", + "detail.credit_model": "信用模式", + "detail.credit_note": "贷方票据", + "detail.credit_source": "信用来源", + "detail.credit_source_fetched": "已获取信用来源", + "detail.credit_tier": "信用等级", + "detail.cwd": "西德", + "detail.efficiency_signals": "效率信号", + "detail.first_expensive_turn": "第一次昂贵的转弯", + "detail.git_branch": "git分支", + "detail.largest_cumulative_jump": "最大累计跳跃", + "detail.latest_activity": "最新活动", + "detail.model_mix": "车型组合", + "detail.next_action": "下一步行动", + "detail.no_above_thresholds": "均未超过阈值", + "detail.no_aggregate_action": "没有标记聚合操作。", + "detail.parent_session": "家长会", + "detail.parent_thread": "父线程", + "detail.parent_updated": "家长已更新", + "detail.pricing_model": "定价模型", + "detail.pricing_status": "定价状态", + "detail.project_cwd": "项目cwd", + "detail.project_tags": "项目标签", + "detail.raw_identifiers": "原始聚合标识符", + "detail.reasoning_mix": "推理组合", + "detail.relationships": "人际关系", + "detail.remote_hash": "远程哈希", + "detail.remote_label": "远程标签", + "detail.secondary_thread_fields": "辅助线程字段", + "detail.source_file_line": "源文件和行", + "detail.source_line": "源线", + "detail.spawned_child_calls": "生成的孩子叫声", + "detail.spawned_from": "产生自", + "detail.spawned_threads": "生成的线程", + "detail.subagent_before_spike": "尖峰之前的子代理", + "detail.subagent_calls": "子代理调用", + "detail.subagent_type": "子代理类型", + "detail.thread_attachment": "螺纹附件", + "detail.thread_attention_summary": "话题关注总结", + "detail.thread_lifecycle": "线程生命周期", + "detail.thread_narrative": "线索叙事", + "detail.thread_source": "线程来源", + "detail.thread_timeline": "线程时间线", + "detail.timeline_context": "上下文{value}", + "detail.timeline_empty": "此线程中没有调用。", + "detail.timeline_meta": "{tokens} Token · {cost} · {credits} · 缓存 {cache}", + "detail.timestamp": "时间戳", + "detail.token_pricing_breakdown": "Token和定价细目", + "detail.tokens_at": "{tokens} Token位于 {time}", + "detail.turn": "转", + "detail.why_flagged": "为什么被标记", + "docs.dashboard_guide": "仪表板指南", + "effort.high": "高", + "effort.low": "低", + "effort.medium": "中等", + "filter.confidence": "信心", + "filter.effort": "推理", + "filter.end": "结束", + "filter.model": "型号", + "filter.project": "项目", + "filter.reasoning": "推理", + "filter.search": "搜索", + "filter.search_placeholder": "线程、cwd、模型", + "filter.session": "会议", + "filter.sort": "排序", + "filter.start": "开始", + "filter.thread": "线程", + "filter.time": "时间", + "flag.elevated_context_use": "提升上下文使用", + "flag.expensive_low_output_call": "昂贵的低输出调用", + "flag.high_context_use": "高上下文使用", + "flag.high_estimated_cost": "预估成本高", + "flag.high_reasoning_share": "高推理分享", + "flag.low_cache_reuse": "缓存重用率低", + "history.active_hidden": "仅活动会话; {count} 已存档调用已隐藏", + "history.active_only": "仅活动会话", + "history.all_empty": "选择所有历史记录;尚未对存档的调用建立索引", + "history.all_includes": "所有历史记录包括 {count} 存档的调用", + "history.archived_scan_hint": "{detail}。仅当在实时刷新期间选择“所有历史记录”时才会扫描已存档的会话。", + "insight.apply_cache_misses": "应用缓存未命中预设", + "insight.apply_context_bloat": "应用上下文膨胀预设", + "insight.codex_allowance_usage": "Codex 津贴使用情况", + "insight.context_bloat": "上下文膨胀", + "insight.context_bloat_body": "{calls} 调用等于或高于 {ratio} 上下文使用。", + "insight.costliest_thread": "最昂贵的线程", + "insight.costliest_thread_body": "{thread} 有 {calls} 调用和 {tokens} Token。", + "insight.credit_coverage_body": "可见Token的 {ratio} 映射到 Codex 信用率。", + "insight.estimated_pricing": "预计定价", + "insight.estimated_pricing_body": "包括标记的最佳猜测价格,但应单独审查。", + "insight.inspect_selected_call": "检查选定的调用", + "insight.low_cache_reuse": "缓存重用率低", + "insight.low_cache_reuse_body": "{calls} 调用在 {ratio} 缓存重用下。从 {thread} 开始。", + "insight.open_thread_timeline": "打开线程时间线", + "insight.reasoning_output_spike": "推理输出峰值", + "insight.reasoning_spike_body": "{thread} 在当前过滤器中具有最大的推理输出调用。", + "insight.review_estimates": "审查估计", + "insight.review_highest_credit": "审查最高信用调用", + "insight.review_pricing_gaps": "审查定价差距", + "insight.unpriced_usage": "无价使用", + "insight.unpriced_usage_body": "在配置定价之前,这些Token将从估计成本总额中省略。", + "language.english": "英语", + "language.label": "语言", + "language.vietnamese": "越南", + "live.checking_usage": "正在检查新用途...", + "live.every": "每 {seconds}s 实时刷新一次", + "live.history_static_hint": "运行 codex-usage-tracker serve-dashboard 在活动会话和仪表板中的所有历史记录之间切换。", + "live.indexed": "索引 {rows} 聚合 {files} 日志中的行。", + "live.load_static_hint": "运行 codex-usage-tracker serve-dashboard 从仪表板加载不同的历史记录大小。", + "live.loading_rows": "正在后台加载行...", + "live.paused": "实时刷新已暂停", + "live.refresh_suffix": "。重新生成静态仪表板后重新加载此页面,或运行 codex-usage-tracker serve-dashboard。", + "live.refresh_unavailable": "实时刷新不可用:{message}{suffix}", + "live.refreshing_index": "正在刷新本地使用指数...", + "live.reloading_static": "正在重新加载静态仪表板快照...", + "live.skipped": "跳过 {count} 格式错误的Token计数事件。", + "live.updated_detail": "更新了{time}。 {loaded}。 {history}.{indexed}{skipped}", + "metric.attention_score": "注意力分数", + "metric.cache_ratio": "缓存比率", + "metric.cache_trend": "缓存趋势", + "metric.cached_input": "缓存输入", + "metric.codex_credits": "Codex 学分", + "metric.context_trend": "背景趋势", + "metric.context_use": "上下文使用", + "metric.estimated_cost": "预计费用", + "metric.input_tokens": "输入Token", + "metric.last_call_input": "最后调用输入", + "metric.last_call_total": "最后调用总数", + "metric.max_context_use": "最大上下文使用", + "metric.output": "输出", + "metric.output_tokens": "输出Token", + "metric.reasoning_output": "推理输出", + "metric.remaining_usage": "剩余使用量", + "metric.session_cumulative": "会话累计", + "metric.total": "总计", + "metric.total_tokens": "Token总数", + "metric.uncached_input": "未缓存的输入", + "metric.usage_credits": "使用积分", + "metric.usage_remaining": "剩余使用量", + "metric.visible_calls": "可见调用", + "nav.history": "历史", + "nav.live": "直播", + "nav.load": "负载", + "option.active_sessions_only": "仅活动会话", + "option.all_confidence": "充满信心", + "option.all_efforts": "所有的努力", + "option.all_history": "所有历史", + "option.all_models": "所有型号", + "option.all_time": "所有时间", + "option.custom_range": "定制范围", + "option.estimated_cost": "预计费用", + "option.estimated_credit_mapping": "估计信用映射", + "option.exact_cost": "确切成本", + "option.exact_credit_rate": "准确的信用率", + "option.highest_codex_credits": "最高 Codex 学分", + "option.highest_context_use": "最高的上下文使用率", + "option.highest_estimated_cost": "最高估计成本", + "option.last_7_days": "过去 7 天", + "option.load_10000": "10,000 次调用", + "option.load_20000": "20,000 个调用", + "option.load_5000": "5,000 通调用", + "option.load_all": "所有调用", + "option.lowest_cache_ratio": "最低缓存比率", + "option.missing_credit_rate": "缺少信用率", + "option.most_signals": "大多数信号", + "option.most_tokens": "大多数Token", + "option.needs_attention": "需要注意", + "option.newest_calls": "最新调用", + "option.this_month": "这个月", + "option.this_week": "本周", + "option.thread_name": "线程名称", + "option.today": "今天", + "option.unpriced_cost": "未定价成本", + "option.user_credit_override": "用户信用覆盖", + "parser.warnings_title": "最新刷新报告了 {count} 解析器诊断:{entries}。运行 codex-usage-tracker inspect-log 以调查架构漂移。", + "preset.cache_misses": "缓存未命中", + "preset.cache_misses_caption": "缓存未命中预设", + "preset.cache_misses_desc": "按 cwd、模型和线程分组的低缓存比率调用。", + "preset.caption": "{caption}:{description}", + "preset.context_bloat": "上下文膨胀", + "preset.context_bloat_caption": "上下文膨胀预设", + "preset.context_bloat_desc": "调用超过 60% 的上下文使用或具有非常高的累积标记。", + "preset.description": "常见使用问题的一键起点。", + "preset.estimated_price_review": "预估价格审核", + "preset.estimated_price_review_caption": "预估价格审核预设", + "preset.estimated_price_review_desc": "使用量的定价带有标记的最佳猜测估计。", + "preset.highest_codex_credits": "最高 Codex 学分", + "preset.highest_codex_credits_caption": "预设最高 Codex 学分", + "preset.highest_codex_credits_desc": "调用按对 Codex 使用限额的估计影响排序。", + "preset.highest_cost_threads": "成本最高的线程", + "preset.highest_cost_threads_caption": "成本最高的线程预设", + "preset.highest_cost_threads_desc": "主题按预计支出排序,并附加子代理。", + "preset.investigation_presets": "调查预设", + "preset.no_preset": "未应用预设。", + "preset.pricing_gaps": "定价差距", + "preset.pricing_gaps_caption": "预设定价差距", + "preset.pricing_gaps_desc": "未定价的使用导致估计成本总计不完整。", + "pricing.configure_hint": "运行 codex-usage-tracker update-pricing 以配置估计成本。", + "pricing.fetched": "已获取 {time}", + "pricing.pinned": "固定快照", + "pricing.source": "定价来源", + "pricing.tier": "{tier} 层", + "pricing.title": "{parts}。内部 Codex 标签可以使用标记的最佳猜测估计。{warning}", + "pricing.title_fetched": "{parts}。从 {url} 的 {time} 获取。内部 Codex 标签可以使用标记的最佳猜测估计。{warning}", + "privacy.aliases_preserved": "配置的项目别名被视为显式显示选择。", + "privacy.cwd_redacted": "原始 cwd 路径已被编辑。", + "privacy.git_branch_hidden": "Git 分支被隐藏。", + "privacy.git_remote_label_hidden": "Git 远程标签被隐藏。", + "privacy.mode": "项目元数据隐私模式:{mode}。", + "privacy.normal_title": "项目元数据显示为本地 cwd、项目、分支和配置的标签。", + "privacy.project_names_redacted": "未命名的项目使用稳定的散列标签。", + "privacy.relative_cwd_hidden": "相对 cwd 被隐藏。", + "privacy.tags_hidden": "项目标签被隐藏。", + "recommendation.context_bloat.action": "如果旧的上下文不再相关,请考虑启动一个新的 Codex 线程。", + "recommendation.context_bloat.title": "高情境压力", + "recommendation.context_bloat.why": "此调用使用了模型上下文窗口的很大一部分。", + "recommendation.elevated_context.action": "在添加更多工作之前检查是否可以缩小线程。", + "recommendation.elevated_context.title": "环境压力升高", + "recommendation.elevated_context.why": "上下文的使用会增加,并且在以后的回合中可能会变得昂贵。", + "recommendation.estimated_pricing.action": "如果此调用很重要,请查看定价范围并固定或覆盖模型费率。", + "recommendation.estimated_pricing.title": "预计定价", + "recommendation.estimated_pricing.why": "此成本使用推断模型映射而不是直接定价行。", + "recommendation.high_cost.action": "打开线程时间线并检查前一回合,然后再继续。", + "recommendation.high_cost.title": "预估成本高", + "recommendation.high_cost.why": "此调用超出了配置的高成本阈值。", + "recommendation.large_thread.action": "更喜欢使用新线程来进行不相关的后续工作。", + "recommendation.large_thread.title": "大累积线程", + "recommendation.large_thread.why": "会话累计总数足够高,使得后面的轮次变得昂贵。", + "recommendation.low_cache.action": "检查是否不必要地重新引入了文件、工具输出或广泛上下文。", + "recommendation.low_cache.title": "缓存重用率低", + "recommendation.low_cache.why": "新鲜的未缓存输入较高,而缓存重用率较低。", + "recommendation.low_output.action": "首先检查聚合上下文;仅当原因不清楚时才加载原始上下文。", + "recommendation.low_output.title": "大低输出调用", + "recommendation.low_output.why": "该调用消耗了很多Token,但产生的输出很少。", + "recommendation.none.action": "没有标记聚合操作;继续监控使用模式。", + "recommendation.pricing_gap.action": "在信任成本总计之前更新定价或添加本地别名。", + "recommendation.pricing_gap.title": "定价差距", + "recommendation.pricing_gap.why": "此模型调用没有配置价格,因此成本总计低估了可见使用情况。", + "recommendation.reasoning_spike.action": "检查此任务是否需要所选的推理工作。", + "recommendation.reasoning_spike.title": "高推理分享", + "recommendation.reasoning_spike.why": "推理输出在该调用的可见输出中占主导地位。", + "recommendation.subagent_attribution.action": "将直接调用与附加的子座席进行比较,或在更改工作流程之前审核调用。", + "recommendation.subagent_attribution.title": "子代理归因", + "recommendation.subagent_attribution.why": "此调用附加到委派工作,并且可以解释父线程的增长。", + "section.allowance": "津贴", + "section.needs_attention": "需要注意", + "section.pricing": "定价", + "section.recommendations": "建议", + "severity.high": "高", + "severity.medium": "中等", + "severity.review": "评论", + "source.auto_review": "自动审核", + "source.codex_initiated": "Codex 发起", + "source.subagent": "子代理", + "source.subagent_role": "子代理:{role}", + "source.user": "用户", + "source.user_initiated": "用户发起", + "state.allowance_config_error": "津贴配置错误", + "state.allowance_configured": "已配置津贴", + "state.best_guess_estimate": "最佳猜测估计", + "state.configured": "已配置", + "state.configured_price": "配置价格", + "state.error": "错误", + "state.estimated": "估计", + "state.loading": "加载中", + "state.loading_rows": "正在加载行", + "state.mixed": "混合", + "state.no": "否", + "state.no_calls": "没有调用与当前过滤器匹配。", + "state.no_configured_price": "没有配置价格", + "state.no_context_entries": "找不到此调用的上下文条目。", + "state.no_data": "无数据", + "state.no_mapped_rate": "无映射速率", + "state.no_price": "无价格", + "state.no_rate": "无利率", + "state.no_rows": "没有行", + "state.no_threads": "没有线程与当前过滤器匹配。", + "state.none": "无", + "state.not_configured": "未配置", + "state.requires_evidence": "需要证据", + "state.unknown": "未知", + "state.yes": "是的", + "status.checking": "检查", + "status.paused": "已暂停", + "status.refresh_error": "刷新错误", + "status.refreshing": "清爽", + "status.reloading": "重新装弹", + "status.static": "静态", + "status.updated": "已更新", + "table.cache": "缓存", + "table.cached": "缓存", + "table.calls": "来电", + "table.cost": "成本", + "table.effort": "努力", + "table.initiated": "发起", + "table.last_call": "最后一次调用", + "table.model": "型号", + "table.more_efforts": "{effort} +{count} 努力", + "table.more_models": "{model} +{count} 型号", + "table.output": "输出", + "table.page_status": "{total} {items} 的 {start}-{end} · 页 {page}/{pages}", + "table.rows": "行", + "table.signals": "信号", + "table.source": "来源", + "table.thread": "线程", + "table.threads": "线程", + "table.time": "时间", + "table.tokens": "Token", + "table.uncached": "未缓存", + "table.visible_status": "显示 {total} {items} 的 {end}", + "thread.attached": "附上", + "thread.attention": "注意{score}", + "thread.auto_review": "{count} 自动审核", + "thread.collapse": "崩溃", + "thread.direct": "直接", + "thread.expand": "展开", + "thread.expand_label": "{action} {thread} 调用。注意力分数{score}。", + "thread.explicit_parent": "显式父级", + "thread.explicit_parent_thread": "显式父线程", + "thread.parent": "家长 {id}", + "thread.session": "会议", + "thread.spawned": "产生的", + "thread.spawned_from": "从 {thread} 生成", + "thread.spawned_threads": "{count} 生成线程", + "thread.subagent": "{count} 子代理", + "thread.unknown": "未知线程", + "thread.unmatched_subagent": "不匹配的子代理" } diff --git a/src/codex_usage_tracker/plugin_data/docs/dashboard-guide.html b/src/codex_usage_tracker/plugin_data/docs/dashboard-guide.html index 1efd12f..a050562 100644 --- a/src/codex_usage_tracker/plugin_data/docs/dashboard-guide.html +++ b/src/codex_usage_tracker/plugin_data/docs/dashboard-guide.html @@ -121,7 +121,7 @@

Threads View

Use Threads view to understand a work session as a group. Expand a thread to see calls newest first by default, then click an expanded-call header to change the per-thread sort. Subagents with logged parent session ids are shown under their parent thread; inferred auto-review attachments are marked in the details panel. Selected threads also show lifecycle signals such as first expensive turn, largest cumulative jump, cache trend, context trend, and whether subagent or auto-review work appeared before a usage spike.

Call Investigator

-

Clicking a Calls row opens dashboard.html?view=call&record=<record_id> for one model call. It separates exact callback counts, derived previous/next deltas, visible-context estimates, serialized local JSONL upper bounds, candidate serialized-overhead buckets, and redacted evidence loaded at runtime for the selected call. When the localhost context API is enabled, the investigator automatically loads a bounded turn-log evidence window with tool output included; use Hide tool output for a quieter evidence stream, and expand or load older surrounding evidence explicitly when needed. Visible evidence token estimates are calculated from the full selected-turn evidence set before display limiting, using tiktoken when available and a conservative character fallback only when the tokenizer is unavailable. The serialized upper bound tokenizes a redacted raw-JSON representation of the same selected-turn log slice. It can explain why visible text is much smaller than exact uncached input, but it can overcount because local JSONL includes client metadata that may not be prompt text. Bucket labels such as encrypted reasoning/state, local goal metadata, token callback metadata, and rate-limit metadata are counts only; raw text is not returned. encrypted_content is an opaque encrypted field found on some reasoning response items. The tracker cannot decrypt it and treats it as serialized state, not readable prompt, assistant, or tool text. The evidence view also shows call anchors: the nearest visible message before the selected call and the selected call's reasoning output summary when the local log exposes one. These anchors are redacted and loaded only at runtime through the context API. Previous and next buttons move chronologically within the same resolved thread. Cache diagnostics label warm cache reuse, cold resume or stale cache, partial cache miss, uncached spike, and post-compaction without claiming exact cached text spans.

+

Clicking a Calls row opens dashboard.html?view=call&record=<record_id> for one model call. It separates exact callback counts, derived previous/next deltas, visible-context estimates, serialized local JSONL upper bounds, candidate serialized-overhead buckets, and redacted evidence loaded at runtime for the selected call. When the localhost context API is enabled, the investigator automatically loads a bounded turn-log evidence window with tool output included; use Hide tool output for a quieter evidence stream, and expand or load older surrounding evidence explicitly when needed. Visible evidence token estimates are calculated from the full selected-turn evidence set before display limiting, using tiktoken when available and a conservative character fallback only when the tokenizer is unavailable. The serialized upper bound tokenizes a redacted raw-JSON representation of the same selected-turn log slice. It can explain why visible text is much smaller than exact uncached input, but it can overcount because local JSONL includes client metadata that may not be prompt text. Bucket labels such as encrypted reasoning/state, local goal metadata, token callback metadata, and rate-limit metadata are counts only; raw text is not returned. encrypted_content is an opaque encrypted field found on some reasoning response items. The tracker cannot decrypt it and treats it as serialized state, not readable prompt, assistant, or tool text. Previous and next buttons move chronologically within the same resolved thread. Cache diagnostics label warm cache reuse, cold resume or stale cache, partial cache miss, uncached spike, and post-compaction without claiming exact cached text spans.

Details And Context

Details panel showing aggregate usage fields for a selected call. diff --git a/src/codex_usage_tracker/schema.py b/src/codex_usage_tracker/schema.py index 5597e70..6a60cfd 100644 --- a/src/codex_usage_tracker/schema.py +++ b/src/codex_usage_tracker/schema.py @@ -31,6 +31,19 @@ class UsageColumn: UsageColumn("effort", "TEXT", "TEXT", repairable=True), UsageColumn("current_date", "TEXT", "TEXT", repairable=True), UsageColumn("timezone", "TEXT", "TEXT", repairable=True), + UsageColumn("call_initiator", "TEXT", "TEXT", repairable=True), + UsageColumn("call_initiator_reason", "TEXT", "TEXT", repairable=True), + UsageColumn("call_initiator_confidence", "TEXT", "TEXT", repairable=True), + UsageColumn( + "is_archived", + "INTEGER NOT NULL DEFAULT 0", + "INTEGER NOT NULL DEFAULT 0", + repairable=True, + ), + UsageColumn("thread_key", "TEXT", "TEXT", repairable=True), + UsageColumn("thread_call_index", "INTEGER", "INTEGER", repairable=True), + UsageColumn("previous_record_id", "TEXT", "TEXT", repairable=True), + UsageColumn("next_record_id", "TEXT", "TEXT", repairable=True), UsageColumn("thread_source", "TEXT", "TEXT", repairable=True), UsageColumn("subagent_type", "TEXT", "TEXT", repairable=True), UsageColumn("agent_role", "TEXT", "TEXT", repairable=True), diff --git a/src/codex_usage_tracker/server.py b/src/codex_usage_tracker/server.py index c2d3ba9..91e9f9d 100644 --- a/src/codex_usage_tracker/server.py +++ b/src/codex_usage_tracker/server.py @@ -14,14 +14,24 @@ from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from ipaddress import ip_address from pathlib import Path +from time import perf_counter +from typing import Any from urllib.parse import parse_qs, urlparse +from codex_usage_tracker.allowance import annotate_rows_with_allowance, load_allowance_config +from codex_usage_tracker.call_origin import ensure_call_origin from codex_usage_tracker.context import ( + CONTEXT_MODE_QUICK, + CONTEXT_MODES, DEFAULT_CONTEXT_CHARS, DEFAULT_CONTEXT_ENTRIES, load_call_context, ) -from codex_usage_tracker.dashboard import dashboard_payload, generate_dashboard +from codex_usage_tracker.dashboard import ( + dashboard_payload, + generate_dashboard, + render_dashboard_html, +) from codex_usage_tracker.i18n import normalize_language from codex_usage_tracker.paths import ( DEFAULT_ALLOWANCE_PATH, @@ -32,7 +42,32 @@ DEFAULT_RATE_CARD_PATH, DEFAULT_THRESHOLDS_PATH, ) -from codex_usage_tracker.store import refresh_usage_index +from codex_usage_tracker.pricing import annotate_rows_with_efficiency, load_pricing_config +from codex_usage_tracker.projects import ( + annotate_rows_with_project_identity, + apply_project_privacy_to_rows, + load_project_config, +) +from codex_usage_tracker.recommendations import ( + annotate_rows_with_recommendations, + load_threshold_config, +) +from codex_usage_tracker.reports import ( + QUERY_CREDIT_CONFIDENCE_CHOICES, + QUERY_PRICING_STATUS_CHOICES, + build_recommendations_report, + build_summary_report, +) +from codex_usage_tracker.store import ( + query_thread_summaries, + query_usage_api_event_count, + query_usage_api_events, + query_usage_record, + query_usage_status, + refresh_metadata, + refresh_usage_index, +) +from codex_usage_tracker.threads import annotate_thread_attachments class _ContextApiState: @@ -93,6 +128,7 @@ def serve_dashboard( privacy_mode=privacy_mode, include_archived=include_archived, language=selected_language, + include_rows=False, ) handler = partial( _UsageDashboardHandler, @@ -109,6 +145,7 @@ def serve_dashboard( codex_home=codex_home, include_archived=include_archived, dashboard_name=output.name, + dashboard_path=output, context_chars=context_chars, api_token=api_token, context_api_state=context_api_state, @@ -152,6 +189,7 @@ def __init__( context_chars: int, api_token: str, refresh_lock: threading.Lock, + dashboard_path: Path | None = None, context_api_enabled: bool = False, context_api_state: _ContextApiState | None = None, privacy_mode: str = "normal", @@ -172,6 +210,11 @@ def __init__( self._codex_home = codex_home self._include_archived = include_archived self._dashboard_name = dashboard_name + self._dashboard_path = ( + Path(dashboard_path) + if dashboard_path is not None + else Path(str(kwargs.get("directory", "."))) / dashboard_name + ) self._context_chars = context_chars self._api_token = api_token self._context_api_state = context_api_state or _ContextApiState(context_api_enabled) @@ -192,11 +235,36 @@ def do_GET(self) -> None: # noqa: N802 - stdlib hook name if parsed.path == "/api/open-investigator": self._handle_open_investigator(parsed.query) return + if parsed.path == "/api/status": + self._handle_status(parsed.query) + return + if parsed.path == "/api/calls": + self._handle_calls(parsed.query) + return + if parsed.path == "/api/call": + self._handle_call(parsed.query) + return + if parsed.path == "/api/threads": + self._handle_threads(parsed.query) + return + if parsed.path == "/api/thread-calls": + self._handle_thread_calls(parsed.query) + return + if parsed.path == "/api/summary": + self._handle_summary(parsed.query) + return + if parsed.path == "/api/recommendations": + self._handle_recommendations(parsed.query) + return if parsed.path == "/api/usage": self._handle_usage(parsed.query) return - if parsed.path == "/": - self.path = f"/{self._dashboard_name}" + if self._is_investigator_dashboard_request(parsed.path, parsed.query): + self._handle_investigator_dashboard(parsed.query) + return + if parsed.path in {"/", f"/{self._dashboard_name}"}: + self._handle_dashboard_shell(parsed.query) + return super().do_GET() def end_headers(self) -> None: @@ -216,6 +284,97 @@ def _is_dashboard_html_request(self) -> bool: path = urlparse(self.path).path return path in {"/", f"/{self._dashboard_name}"} + def _is_investigator_dashboard_request(self, path: str, query: str) -> bool: + if path != f"/{self._dashboard_name}": + return False + params = parse_qs(query) + return _first(params.get("view")) == "call" and bool(_first(params.get("record"))) + + def _handle_investigator_dashboard(self, query: str) -> None: + payload = self._dashboard_shell_payload(query) + if payload is None: + return + payload["investigator_boot"] = True + payload["pricing_snapshot_warning"] = "" + body = render_dashboard_html( + payload, + output_path=self._dashboard_path, + guide_href="codex-usage-tracker-guide/dashboard-guide.html", + body_attrs={ + "data-active-view": "call", + "data-investigator-boot": "true", + "data-dashboard-shell": "true", + }, + ).encode("utf-8") + self._send_html(body) + + def _handle_dashboard_shell(self, query: str) -> None: + payload = self._dashboard_shell_payload(query) + if payload is None: + return + payload["pricing_snapshot_warning"] = "" + body = render_dashboard_html( + payload, + output_path=self._dashboard_path, + guide_href="codex-usage-tracker-guide/dashboard-guide.html", + body_attrs={"data-dashboard-shell": "true"}, + ).encode("utf-8") + self._send_html(body) + + def _dashboard_shell_payload(self, query: str) -> dict[str, object] | None: + params = parse_qs(query) + include_archived = self._include_archived + history_scope = _first(params.get("history")) + if history_scope == "all": + include_archived = True + elif history_scope == "active": + include_archived = False + include_archived = _parse_bool( + _first(params.get("include_archived")), + include_archived, + ) + language = normalize_language(_first(params.get("lang")) or self._language) + try: + return dashboard_payload( + db_path=self._db_path, + limit=0, + offset=0, + pricing_path=self._pricing_path, + allowance_path=self._allowance_path, + rate_card_path=self._rate_card_path, + thresholds_path=self._thresholds_path, + projects_path=self._projects_path, + privacy_mode=self._privacy_mode, + since=self._since, + api_token=self._api_token, + context_api_enabled=self._context_api_state.enabled, + include_archived=include_archived, + language=language, + include_rows=False, + ) + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while preparing dashboard shell: {exc}"}, + ) + return None + except OSError as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Could not prepare dashboard shell: {exc}"}, + ) + return None + + def _send_html(self, body: bytes) -> None: + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + try: + self.wfile.write(body) + except (BrokenPipeError, ConnectionResetError): + return + def log_message(self, format: str, *args: object) -> None: if self.path.startswith("/api/usage"): return @@ -245,6 +404,16 @@ def _handle_context(self, query: str) -> None: return include_tool_output = _truthy(_first(params.get("include_tool_output"))) include_compaction_history = _truthy(_first(params.get("include_compaction_history"))) + diagnostics = _parse_bool(_first(params.get("diagnostics")), False) + context_mode = (_first(params.get("mode")) or CONTEXT_MODE_QUICK).strip().lower() + if context_mode not in CONTEXT_MODES: + self._send_json( + HTTPStatus.BAD_REQUEST, + { + "error": "mode must be one of: " + ", ".join(sorted(CONTEXT_MODES)), + }, + ) + return max_chars = _parse_context_limit(_first(params.get("max_chars")), self._context_chars) max_entries = _parse_context_limit( _first(params.get("max_entries")), DEFAULT_CONTEXT_ENTRIES @@ -257,6 +426,8 @@ def _handle_context(self, query: str) -> None: max_entries=max_entries, include_tool_output=include_tool_output, include_compaction_history=include_compaction_history, + diagnostics=diagnostics, + mode=context_mode, ) except sqlite3.Error as exc: self._send_json( @@ -337,13 +508,392 @@ def _handle_open_investigator(self, query: str) -> None: }, ) + def _handle_status(self, query: str) -> None: + params = parse_qs(query) + include_archived = _parse_bool(_first(params.get("include_archived")), self._include_archived) + try: + counts = query_usage_status( + db_path=self._db_path, + include_archived=include_archived, + ) + metadata = refresh_metadata(self._db_path) + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading status: {exc}"}, + ) + return + parser_diagnostics = { + key.removeprefix("parser_"): _safe_int(value) + for key, value in metadata.items() + if key.startswith("parser_") and _safe_int(value) + } + self._send_json( + HTTPStatus.OK, + { + "schema": "codex-usage-tracker-status-v1", + "payload_schema": "codex-usage-tracker-live-api-v1", + "latest_refresh_at": metadata.get("latest_refresh_at"), + "include_archived": include_archived, + "row_counts": counts, + "max_event_timestamp": counts.get("max_event_timestamp"), + "parser_adapter": metadata.get("parser_adapter"), + "parser_diagnostics": parser_diagnostics, + }, + ) + + def _handle_calls(self, query: str) -> None: + params = parse_qs(query) + try: + query_params = self._live_query_params(params) + pricing_status = _optional_filter( + _first(params.get("pricing_status")), + QUERY_PRICING_STATUS_CHOICES, + "pricing_status", + ) + credit_confidence = _optional_filter( + _first(params.get("credit_confidence")), + QUERY_CREDIT_CONFIDENCE_CHOICES, + "credit_confidence", + ) + rows, total_matched = self._live_call_rows( + query_params=query_params, + pricing_status=pricing_status, + credit_confidence=credit_confidence, + ) + except ValueError as exc: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)}) + return + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading calls: {exc}"}, + ) + return + self._send_json( + HTTPStatus.OK, + { + "schema": "codex-usage-tracker-calls-v1", + "rows": rows, + "row_count": len(rows), + "total_matched_rows": total_matched, + "limit": query_params["limit"], + "offset": query_params["offset"], + "has_more": _has_more(query_params["limit"], query_params["offset"], len(rows), total_matched), + "next_offset": _next_offset(query_params["limit"], query_params["offset"], len(rows), total_matched), + "filters": { + **query_params["filters"], + "pricing_status": pricing_status, + "credit_confidence": credit_confidence, + }, + "raw_context_included": False, + }, + ) + + def _handle_call(self, query: str) -> None: + params = parse_qs(query) + record_id = _first(params.get("record_id")) or _first(params.get("record")) + if not record_id: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": "record_id is required"}) + return + try: + row = query_usage_record(db_path=self._db_path, record_id=record_id) + if row is None: + self._send_json(HTTPStatus.NOT_FOUND, {"error": f"No usage record found: {record_id}"}) + return + adjacent_raw_rows = [ + query_usage_record(db_path=self._db_path, record_id=adjacent_id) + for adjacent_id in (row.get("previous_record_id"), row.get("next_record_id")) + if adjacent_id + ] + rows_by_id = { + candidate["record_id"]: candidate + for candidate in self._annotate_live_rows( + [candidate for candidate in [row, *adjacent_raw_rows] if candidate] + ) + if candidate.get("record_id") + } + selected_row = rows_by_id.get(record_id, row) + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading call: {exc}"}, + ) + return + previous_record = rows_by_id.get(str(row.get("previous_record_id") or "")) + next_record = rows_by_id.get(str(row.get("next_record_id") or "")) + self._send_json( + HTTPStatus.OK, + { + "schema": "codex-usage-tracker-call-v1", + "record": selected_row, + "previous_record": previous_record, + "next_record": next_record, + "adjacent_records": [ + candidate + for candidate in (previous_record, selected_row, next_record) + if candidate + ], + "previous_record_id": row.get("previous_record_id"), + "next_record_id": row.get("next_record_id"), + "raw_context_included": False, + }, + ) + + def _handle_threads(self, query: str) -> None: + params = parse_qs(query) + try: + limit = _parse_api_limit(_first(params.get("limit")), 100) + offset = _parse_api_offset(_first(params.get("offset"))) + include_archived = _parse_bool(_first(params.get("include_archived")), self._include_archived) + sort = _first(params.get("sort")) or "tokens" + direction = _first(params.get("direction")) or "desc" + rows = query_thread_summaries( + db_path=self._db_path, + limit=limit, + offset=offset, + search=_first(params.get("q")) or _first(params.get("search")), + include_archived=include_archived, + sort=sort, + direction=direction, + ) + except ValueError as exc: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)}) + return + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading threads: {exc}"}, + ) + return + self._send_json( + HTTPStatus.OK, + { + "schema": "codex-usage-tracker-threads-v1", + "rows": rows, + "row_count": len(rows), + "limit": limit, + "offset": offset, + "include_archived": include_archived, + "raw_context_included": False, + }, + ) + + def _handle_thread_calls(self, query: str) -> None: + params = parse_qs(query) + thread_key = _first(params.get("thread_key")) or _first(params.get("thread")) + if not thread_key: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": "thread_key is required"}) + return + try: + query_params = self._live_query_params(params, thread_key=thread_key) + rows, total_matched = self._live_call_rows( + query_params=query_params, + pricing_status=None, + credit_confidence=None, + ) + except ValueError as exc: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)}) + return + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading thread calls: {exc}"}, + ) + return + self._send_json( + HTTPStatus.OK, + { + "schema": "codex-usage-tracker-thread-calls-v1", + "thread_key": thread_key, + "rows": rows, + "row_count": len(rows), + "total_matched_rows": total_matched, + "limit": query_params["limit"], + "offset": query_params["offset"], + "has_more": _has_more(query_params["limit"], query_params["offset"], len(rows), total_matched), + "next_offset": _next_offset(query_params["limit"], query_params["offset"], len(rows), total_matched), + "raw_context_included": False, + }, + ) + + def _handle_summary(self, query: str) -> None: + params = parse_qs(query) + try: + report = build_summary_report( + db_path=self._db_path, + pricing_path=self._pricing_path, + group_by=_first(params.get("group_by")) or "thread", + limit=_parse_report_limit(_first(params.get("limit")), 20), + preset=_first(params.get("preset")), + since=_first(params.get("since")), + projects_path=self._projects_path, + privacy_mode=self._privacy_mode, + ) + except ValueError as exc: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)}) + return + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading summary: {exc}"}, + ) + return + payload = report.payload() + payload["raw_context_included"] = False + self._send_json(HTTPStatus.OK, payload) + + def _handle_recommendations(self, query: str) -> None: + params = parse_qs(query) + try: + report = build_recommendations_report( + db_path=self._db_path, + pricing_path=self._pricing_path, + allowance_path=self._allowance_path, + projects_path=self._projects_path, + since=_first(params.get("since")), + until=_first(params.get("until")), + model=_first(params.get("model")), + effort=_first(params.get("effort")), + thread=_first(params.get("thread")), + project=_first(params.get("project")), + min_score=_parse_optional_float(_first(params.get("min_score")), "min_score"), + limit=_parse_report_limit(_first(params.get("limit")), 20), + privacy_mode=self._privacy_mode, + ) + except ValueError as exc: + self._send_json(HTTPStatus.BAD_REQUEST, {"error": str(exc)}) + return + except sqlite3.Error as exc: + self._send_json( + HTTPStatus.INTERNAL_SERVER_ERROR, + {"error": f"Database error while reading recommendations: {exc}"}, + ) + return + payload = dict(report.payload) + payload["raw_context_included"] = False + self._send_json(HTTPStatus.OK, payload) + + def _live_query_params( + self, + params: dict[str, list[str]], + *, + thread_key: str | None = None, + ) -> dict[str, Any]: + include_archived = _parse_bool( + _first(params.get("include_archived")), + self._include_archived, + ) + limit = _parse_api_limit(_first(params.get("limit")), 100) + offset = _parse_api_offset(_first(params.get("offset"))) + return { + "limit": limit, + "offset": offset, + "search": _first(params.get("q")) or _first(params.get("search")), + "since": _first(params.get("since")), + "until": _first(params.get("until")), + "model": _first(params.get("model")), + "effort": _first(params.get("effort")), + "thread": _first(params.get("thread")) if thread_key is None else None, + "thread_key": thread_key, + "include_archived": include_archived, + "sort": _first(params.get("sort")) or "time", + "direction": _first(params.get("direction")) or "desc", + "filters": { + "q": _first(params.get("q")) or _first(params.get("search")), + "since": _first(params.get("since")), + "until": _first(params.get("until")), + "model": _first(params.get("model")), + "effort": _first(params.get("effort")), + "thread": _first(params.get("thread")) if thread_key is None else None, + "thread_key": thread_key, + "include_archived": include_archived, + "sort": _first(params.get("sort")) or "time", + "direction": _first(params.get("direction")) or "desc", + }, + } + + def _live_call_rows( + self, + *, + query_params: dict[str, Any], + pricing_status: str | None, + credit_confidence: str | None, + ) -> tuple[list[dict[str, Any]], int]: + derived_filter = bool(pricing_status or credit_confidence) + rows = query_usage_api_events( + db_path=self._db_path, + limit=None if derived_filter else query_params["limit"], + offset=0 if derived_filter else query_params["offset"], + search=query_params["search"], + since=query_params["since"], + until=query_params["until"], + model=query_params["model"], + effort=query_params["effort"], + thread=query_params["thread"], + thread_key=query_params["thread_key"], + include_archived=query_params["include_archived"], + sort=query_params["sort"], + direction=query_params["direction"], + ) + rows = self._annotate_live_rows(rows) + if derived_filter: + rows = [ + row + for row in rows + if _matches_live_derived_filters( + row, + pricing_status=pricing_status, + credit_confidence=credit_confidence, + ) + ] + total_matched = len(rows) + limit = query_params["limit"] + offset = query_params["offset"] + rows = rows[offset:] if limit is None else rows[offset : offset + limit] + return rows, total_matched + total_matched = query_usage_api_event_count( + db_path=self._db_path, + search=query_params["search"], + since=query_params["since"], + until=query_params["until"], + model=query_params["model"], + effort=query_params["effort"], + thread=query_params["thread"], + thread_key=query_params["thread_key"], + include_archived=query_params["include_archived"], + ) + return rows, total_matched + + def _annotate_live_rows(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: + if not rows: + return [] + rows = annotate_thread_attachments([ensure_call_origin(row) for row in rows]) + pricing = load_pricing_config(self._pricing_path) + allowance = load_allowance_config( + self._allowance_path, + rate_card_path=self._rate_card_path, + ) + thresholds = load_threshold_config(self._thresholds_path) + projects = load_project_config(self._projects_path) + rows = annotate_rows_with_allowance( + annotate_rows_with_efficiency(rows, pricing), + allowance, + ) + rows = annotate_rows_with_recommendations(rows, thresholds) + rows = annotate_rows_with_project_identity(rows, projects) + return apply_project_privacy_to_rows(rows, privacy_mode=self._privacy_mode) + def _handle_usage(self, query: str) -> None: params = parse_qs(query) limit = _parse_limit(_first(params.get("limit")), self._limit) offset = _parse_offset(_first(params.get("offset"))) include_archived = _parse_bool(_first(params.get("include_archived")), self._include_archived) language = normalize_language(_first(params.get("lang")) or self._language) + diagnostics_enabled = _parse_bool(_first(params.get("diagnostics")), False) + shell_only = _parse_bool(_first(params.get("shell")), False) refresh_result = None + refresh_ms: float | None = None try: if _truthy(_first(params.get("refresh"))): if not self._has_valid_api_token(params): @@ -352,12 +902,14 @@ def _handle_usage(self, query: str) -> None: {"error": "Valid API token is required for refresh"}, ) return + refresh_started = perf_counter() with self._refresh_lock: result = refresh_usage_index( codex_home=self._codex_home, db_path=self._db_path, include_archived=include_archived, ) + refresh_ms = _elapsed_ms(refresh_started) refresh_result = { "scanned_files": result.scanned_files, "parsed_events": result.parsed_events, @@ -367,6 +919,7 @@ def _handle_usage(self, query: str) -> None: "parser_diagnostics": result.parser_diagnostics, "include_archived": include_archived, } + payload_started = perf_counter() payload = dashboard_payload( db_path=self._db_path, limit=limit, @@ -382,7 +935,9 @@ def _handle_usage(self, query: str) -> None: context_api_enabled=self._context_api_state.enabled, include_archived=include_archived, language=language, + include_rows=not shell_only, ) + dashboard_payload_ms = _elapsed_ms(payload_started) except sqlite3.Error as exc: self._send_json( HTTPStatus.INTERNAL_SERVER_ERROR, @@ -397,6 +952,17 @@ def _handle_usage(self, query: str) -> None: return payload["refreshed_at"] = _utc_now() payload["refresh_result"] = refresh_result + if diagnostics_enabled: + diagnostic_payload: dict[str, object] = { + "dashboard_payload_ms": dashboard_payload_ms, + "rows_returned": len(payload.get("rows") or []), + "include_archived": include_archived, + "limit": limit, + "offset": offset, + } + if refresh_ms is not None: + diagnostic_payload["refresh_ms"] = refresh_ms + payload["diagnostics"] = diagnostic_payload self._send_json(HTTPStatus.OK, payload) def _request_origin_allowed(self) -> bool: @@ -417,13 +983,16 @@ def _has_valid_api_token(self, params: dict[str, list[str]]) -> bool: return hmac.compare_digest(str(provided), self._api_token) def _send_json(self, status: HTTPStatus, payload: dict[str, object]) -> None: - body = json.dumps(payload, ensure_ascii=True).encode("utf-8") + body = _json_response_body(payload) self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Cache-Control", "no-store") self.send_header("Content-Length", str(len(body))) self.end_headers() - self.wfile.write(body) + try: + self.wfile.write(body) + except (BrokenPipeError, ConnectionResetError): + return def _first(values: list[str] | None) -> str | None: @@ -469,6 +1038,86 @@ def _parse_offset(value: str | None) -> int: return max(offset, 0) +def _parse_api_limit(value: str | None, default: int) -> int | None: + if value is None or value == "": + return default + if value.lower() == "all": + return None + try: + limit = int(value) + except ValueError as exc: + raise ValueError("limit must be a positive integer or all") from exc + if limit <= 0: + raise ValueError("limit must be a positive integer or all") + return min(limit, 10_000) + + +def _parse_report_limit(value: str | None, default: int) -> int: + limit = _parse_api_limit(value, default) + return 10_000 if limit is None else limit + + +def _parse_api_offset(value: str | None) -> int: + if value is None or value == "": + return 0 + try: + offset = int(value) + except ValueError as exc: + raise ValueError("offset must be a non-negative integer") from exc + if offset < 0: + raise ValueError("offset must be a non-negative integer") + return offset + + +def _parse_optional_float(value: str | None, name: str) -> float | None: + if value is None or value == "": + return None + try: + return float(value) + except ValueError as exc: + raise ValueError(f"{name} must be a number") from exc + + +def _optional_filter( + value: str | None, + allowed: tuple[str, ...], + name: str, +) -> str | None: + if value is None or value == "": + return None + if value not in allowed: + raise ValueError(f"{name} must be one of: {', '.join(allowed)}") + return value + + +def _matches_live_derived_filters( + row: dict[str, Any], + *, + pricing_status: str | None, + credit_confidence: str | None, +) -> bool: + if pricing_status == "priced" and not row.get("pricing_model"): + return False + if pricing_status == "estimated" and not row.get("pricing_estimated"): + return False + if pricing_status == "unpriced" and row.get("pricing_model"): + return False + return not (credit_confidence and row.get("usage_credit_confidence") != credit_confidence) + + +def _has_more(limit: int | None, offset: int, row_count: int, total_matched: int) -> bool: + return limit is not None and offset + row_count < total_matched + + +def _next_offset( + limit: int | None, + offset: int, + row_count: int, + total_matched: int, +) -> int | None: + return offset + row_count if _has_more(limit, offset, row_count, total_matched) else None + + def _parse_context_limit(value: str | None, default: int) -> int: if value is None or value == "": return default @@ -481,6 +1130,32 @@ def _parse_context_limit(value: str | None, default: int) -> int: return max(limit, 0) +def _elapsed_ms(started_at: float) -> float: + return round((perf_counter() - started_at) * 1000, 3) + + +def _safe_int(value: object) -> int: + try: + return int(value) + except (TypeError, ValueError): + return 0 + + +def _json_response_body(payload: dict[str, object]) -> bytes: + diagnostics = payload.get("diagnostics") + if not isinstance(diagnostics, dict): + return json.dumps(payload, ensure_ascii=True).encode("utf-8") + + previous_size: int | None = None + while True: + body = json.dumps(payload, ensure_ascii=True).encode("utf-8") + current_size = len(body) + if current_size == previous_size: + return body + diagnostics["json_bytes"] = current_size + previous_size = current_size + + def _utc_now() -> str: return datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z") diff --git a/src/codex_usage_tracker/store.py b/src/codex_usage_tracker/store.py index 81447d6..586e990 100644 --- a/src/codex_usage_tracker/store.py +++ b/src/codex_usage_tracker/store.py @@ -3,9 +3,12 @@ from __future__ import annotations import csv +import hashlib +import json import sqlite3 from collections.abc import Iterable, Iterator from contextlib import contextmanager, suppress +from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path from typing import Any @@ -13,10 +16,13 @@ from codex_usage_tracker.models import RefreshResult, UsageEvent from codex_usage_tracker.parser import ( PARSER_DIAGNOSTIC_KEYS, + ParserState, compact_parser_diagnostics, find_session_logs, load_session_index, - parse_usage_events, + parse_usage_events_from_file_with_state, + parser_state_from_json, + parser_state_to_json, ) from codex_usage_tracker.paths import DEFAULT_CODEX_HOME, DEFAULT_DB_PATH from codex_usage_tracker.projects import apply_project_privacy_to_rows, validate_privacy_mode @@ -29,10 +35,27 @@ EVENT_COLUMNS = list(USAGE_EVENT_COLUMN_NAMES) -SCHEMA_VERSION = 2 +SCHEMA_VERSION = 7 MIGRATION_NAMES = { 1: "create usage_events aggregate fact table", 2: "track schema migration checksum metadata", + 3: "persist aggregate call-origin metadata", + 4: "persist dashboard query helper fields", + 5: "materialize thread summaries", + 6: "track source file refresh metadata", + 7: "persist source file parser cursors", +} +CALL_ORIGIN_REPAIR_COLUMNS = { + "call_initiator": "TEXT", + "call_initiator_reason": "TEXT", + "call_initiator_confidence": "TEXT", +} +DASHBOARD_HELPER_REPAIR_COLUMNS = { + "is_archived": "INTEGER NOT NULL DEFAULT 0", + "thread_key": "TEXT", + "thread_call_index": "INTEGER", + "previous_record_id": "TEXT", + "next_record_id": "TEXT", } _ARCHIVED_SOURCE_PATTERNS = ( "%/archived_sessions/%", @@ -40,12 +63,35 @@ "%\\archived_sessions\\%", "archived_sessions\\%", ) +API_USAGE_SORTS = { + "time": "usage_events.event_timestamp", + "tokens": "usage_events.total_tokens", + "input": "usage_events.input_tokens", + "cached": "usage_events.cached_input_tokens", + "uncached": "usage_events.uncached_input_tokens", + "output": "usage_events.output_tokens", + "reasoning": "usage_events.reasoning_output_tokens", + "cache": "usage_events.cache_ratio", + "model": "usage_events.model", + "effort": "usage_events.effort", + "thread": "coalesce(usage_events.thread_name, usage_events.parent_thread_name, usage_events.session_id)", + "initiator": "coalesce(usage_events.call_initiator, 'unknown')", +} class SchemaMigrationError(RuntimeError): """Raised when a persisted aggregate schema cannot be repaired safely.""" +@dataclass(frozen=True) +class SourceParsePlan: + path: Path + start_byte: int = 0 + start_line: int = 0 + initial_state: ParserState | None = None + replace_existing: bool = True + + def refresh_usage_index( codex_home: Path = DEFAULT_CODEX_HOME, db_path: Path = DEFAULT_DB_PATH, @@ -55,9 +101,31 @@ def refresh_usage_index( logs = find_session_logs(codex_home=codex_home, include_archived=include_archived) session_index = load_session_index(codex_home) + parse_plans = _source_logs_requiring_parse(logs, db_path=db_path) stats: dict[str, int] = {} - events = parse_usage_events(logs, session_index=session_index, stats=stats) - inserted = upsert_usage_events(events, db_path=db_path) + events: list[UsageEvent] = [] + parsed_files: list[tuple[Path, list[UsageEvent], dict[str, int], ParserState]] = [] + for plan in parse_plans: + file_stats: dict[str, int] = {} + parsed_file = parse_usage_events_from_file_with_state( + plan.path, + session_index=session_index, + stats=file_stats, + start_byte=plan.start_byte, + start_line=plan.start_line, + initial_state=plan.initial_state, + ) + file_events = parsed_file.events + events.extend(file_events) + parsed_files.append((plan.path, file_events, file_stats, parsed_file.state)) + for key, value in file_stats.items(): + stats[key] = stats.get(key, 0) + int(value) + inserted = upsert_usage_events( + events, + db_path=db_path, + replace_source_files=(plan.path for plan in parse_plans if plan.replace_existing), + ) + record_source_file_metadata(db_path=db_path, parsed_files=parsed_files) skipped_events = stats.get("skipped_events", 0) diagnostics = compact_parser_diagnostics(stats) record_refresh_metadata( @@ -67,6 +135,8 @@ def refresh_usage_index( skipped_events=skipped_events, inserted_or_updated_events=inserted, parser_diagnostics=diagnostics, + parsed_source_files=len(parse_plans), + skipped_source_files=len(logs) - len(parse_plans), ) return RefreshResult( scanned_files=len(logs), @@ -88,6 +158,8 @@ def rebuild_usage_index( with connect(db_path) as conn: init_db(conn) conn.execute("DELETE FROM usage_events") + conn.execute("DELETE FROM thread_summaries") + conn.execute("DELETE FROM source_files") conn.execute("DELETE FROM refresh_meta") return refresh_usage_index( codex_home=codex_home, @@ -104,6 +176,8 @@ def reset_usage_database(db_path: Path = DEFAULT_DB_PATH) -> dict[str, Any]: row = conn.execute("SELECT COUNT(*) AS count FROM usage_events").fetchone() deleted_rows = int(row["count"] if row is not None else 0) conn.execute("DELETE FROM usage_events") + conn.execute("DELETE FROM thread_summaries") + conn.execute("DELETE FROM source_files") conn.execute("DELETE FROM refresh_meta") return {"db_path": str(db_path), "deleted_usage_events": deleted_rows} @@ -141,6 +215,36 @@ def init_db(conn: sqlite3.Connection) -> None: else: _migrate_v2(conn) _record_migration_if_missing(conn, 2) + if user_version < 3: + _migrate_v3(conn) + _record_migration(conn, 3) + else: + _migrate_v3(conn) + _record_migration_if_missing(conn, 3) + if user_version < 4: + _migrate_v4(conn) + _record_migration(conn, 4) + else: + _migrate_v4(conn) + _record_migration_if_missing(conn, 4) + if user_version < 5: + _migrate_v5(conn) + _record_migration(conn, 5) + else: + _migrate_v5(conn) + _record_migration_if_missing(conn, 5) + if user_version < 6: + _migrate_v6(conn) + _record_migration(conn, 6) + else: + _migrate_v6(conn) + _record_migration_if_missing(conn, 6) + if user_version < 7: + _migrate_v7(conn) + _record_migration(conn, 7) + else: + _migrate_v7(conn) + _record_migration_if_missing(conn, 7) _validate_usage_events_schema(conn) conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") @@ -189,6 +293,96 @@ def _migrate_v2(conn: sqlite3.Connection) -> None: _ensure_migrations_table(conn) +def _migrate_v3(conn: sqlite3.Connection) -> None: + _ensure_columns(conn, CALL_ORIGIN_REPAIR_COLUMNS) + + +def _migrate_v4(conn: sqlite3.Connection) -> None: + _ensure_columns(conn, DASHBOARD_HELPER_REPAIR_COLUMNS) + conn.executescript( + """ + CREATE INDEX IF NOT EXISTS idx_usage_archived_timestamp + ON usage_events(is_archived, event_timestamp); + CREATE INDEX IF NOT EXISTS idx_usage_archived_model_effort + ON usage_events(is_archived, model, effort); + CREATE INDEX IF NOT EXISTS idx_usage_thread_key_timestamp + ON usage_events(thread_key, event_timestamp, cumulative_total_tokens); + """ + ) + + +def _migrate_v5(conn: sqlite3.Connection) -> None: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS thread_summaries ( + thread_key TEXT NOT NULL, + is_archived_scope TEXT NOT NULL, + thread_label TEXT, + first_event_timestamp TEXT, + latest_event_timestamp TEXT, + call_count INTEGER NOT NULL DEFAULT 0, + session_count INTEGER NOT NULL DEFAULT 0, + input_tokens INTEGER NOT NULL DEFAULT 0, + cached_input_tokens INTEGER NOT NULL DEFAULT 0, + uncached_input_tokens INTEGER NOT NULL DEFAULT 0, + output_tokens INTEGER NOT NULL DEFAULT 0, + reasoning_output_tokens INTEGER NOT NULL DEFAULT 0, + total_tokens INTEGER NOT NULL DEFAULT 0, + estimated_cost_usd REAL, + usage_credits REAL, + avg_cache_ratio REAL NOT NULL DEFAULT 0, + max_context_window_percent REAL NOT NULL DEFAULT 0, + max_recommendation_score REAL, + primary_recommendation TEXT, + call_initiator_summary TEXT, + archived_call_count INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL, + PRIMARY KEY (thread_key, is_archived_scope) + ); + + CREATE INDEX IF NOT EXISTS idx_thread_summaries_scope_tokens + ON thread_summaries(is_archived_scope, total_tokens); + CREATE INDEX IF NOT EXISTS idx_thread_summaries_scope_latest + ON thread_summaries(is_archived_scope, latest_event_timestamp); + """ + ) + + +def _migrate_v6(conn: sqlite3.Connection) -> None: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS source_files ( + source_file_id TEXT PRIMARY KEY, + source_file TEXT NOT NULL UNIQUE, + source_file_hash TEXT NOT NULL, + is_archived INTEGER NOT NULL DEFAULT 0, + size_bytes INTEGER NOT NULL DEFAULT 0, + mtime_ns INTEGER NOT NULL DEFAULT 0, + parsed_until_line INTEGER NOT NULL DEFAULT 0, + parsed_until_byte INTEGER NOT NULL DEFAULT 0, + latest_record_id TEXT, + latest_event_timestamp TEXT, + parser_adapter TEXT NOT NULL, + parser_diagnostics_json TEXT NOT NULL DEFAULT '{}', + last_indexed_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_source_files_archived + ON source_files(is_archived); + CREATE INDEX IF NOT EXISTS idx_source_files_mtime + ON source_files(mtime_ns, size_bytes); + """ + ) + + +def _migrate_v7(conn: sqlite3.Connection) -> None: + _ensure_table_columns( + conn, + "source_files", + {"parser_state_json": "TEXT NOT NULL DEFAULT ''"}, + ) + + def _record_migration(conn: sqlite3.Connection, version: int) -> None: conn.execute( """ @@ -224,6 +418,8 @@ def record_refresh_metadata( skipped_events: int, inserted_or_updated_events: int, parser_diagnostics: dict[str, int] | None = None, + parsed_source_files: int | None = None, + skipped_source_files: int | None = None, ) -> None: """Record the latest refresh counters in refresh_meta.""" @@ -237,6 +433,10 @@ def record_refresh_metadata( "schema_version": str(SCHEMA_VERSION), "usage_events_schema_checksum": USAGE_EVENT_SCHEMA_CHECKSUM, } + if parsed_source_files is not None: + values["parsed_source_files"] = str(parsed_source_files) + if skipped_source_files is not None: + values["skipped_source_files"] = str(skipped_source_files) diagnostics = parser_diagnostics or {} for key in PARSER_DIAGNOSTIC_KEYS: values[f"parser_{key}"] = str(int(diagnostics.get(key, 0))) @@ -299,14 +499,22 @@ def schema_state(db_path: Path = DEFAULT_DB_PATH) -> dict[str, Any]: def _ensure_columns(conn: sqlite3.Connection, columns: dict[str, str]) -> None: + _ensure_table_columns(conn, "usage_events", columns) + + +def _ensure_table_columns( + conn: sqlite3.Connection, + table_name: str, + columns: dict[str, str], +) -> None: existing = { str(row["name"]) - for row in conn.execute("PRAGMA table_info(usage_events)").fetchall() + for row in conn.execute(f"PRAGMA table_info({table_name})").fetchall() } for column, column_type in columns.items(): if column not in existing: try: - conn.execute(f"ALTER TABLE usage_events ADD COLUMN {column} {column_type}") + conn.execute(f"ALTER TABLE {table_name} ADD COLUMN {column} {column_type}") except sqlite3.OperationalError as exc: if "duplicate column name" not in str(exc).lower(): raise @@ -328,13 +536,204 @@ def _validate_usage_events_schema(conn: sqlite3.Connection) -> None: ) +def _source_logs_requiring_parse( + logs: Iterable[Path], + *, + db_path: Path, +) -> list[SourceParsePlan]: + paths = list(logs) + if not paths: + return [] + changed: list[SourceParsePlan] = [] + with connect(db_path) as conn: + init_db(conn) + for path in paths: + metadata = _source_file_metadata(path) + if metadata is None: + continue + row = conn.execute( + """ + SELECT size_bytes, mtime_ns, parsed_until_line + , parsed_until_byte, parser_state_json + FROM source_files + WHERE source_file = ? + """, + (str(path),), + ).fetchone() + if row is None: + changed.append(SourceParsePlan(path=path)) + continue + previous_size = int(row["size_bytes"]) + previous_mtime_ns = int(row["mtime_ns"]) + previous_byte = int(row["parsed_until_byte"]) + previous_line = int(row["parsed_until_line"]) + previous_state = parser_state_from_json(row["parser_state_json"]) + if previous_state is None: + changed.append(SourceParsePlan(path=path)) + continue + if ( + previous_size == metadata["size_bytes"] + and previous_mtime_ns == metadata["mtime_ns"] + ): + continue + if metadata["size_bytes"] > previous_size and 0 < previous_byte <= previous_size: + changed.append( + SourceParsePlan( + path=path, + start_byte=previous_byte, + start_line=previous_line, + initial_state=previous_state, + replace_existing=False, + ) + ) + continue + changed.append(SourceParsePlan(path=path)) + return changed + + +def record_source_file_metadata( + db_path: Path = DEFAULT_DB_PATH, + *, + parsed_files: Iterable[tuple[Path, list[UsageEvent], dict[str, int], ParserState]], +) -> None: + """Record metadata for source files parsed during refresh.""" + + parsed = list(parsed_files) + if not parsed: + return + indexed_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat() + rows: list[dict[str, Any]] = [] + for path, events, diagnostics, parser_state in parsed: + metadata = _source_file_metadata(path) + if metadata is None: + continue + latest_event = max( + events, + key=lambda event: ( + event.event_timestamp, + event.cumulative_total_tokens, + event.line_number, + event.record_id, + ), + default=None, + ) + rows.append( + { + "source_file_id": _source_file_id(path), + "source_file": str(path), + "source_file_hash": _source_file_hash(path), + "is_archived": int(metadata["is_archived"]), + "size_bytes": int(metadata["size_bytes"]), + "mtime_ns": int(metadata["mtime_ns"]), + "parsed_until_line": _count_lines(path), + "parsed_until_byte": int(metadata["size_bytes"]), + "latest_record_id": ( + latest_event.record_id + if latest_event + else parser_state.latest_record_id + ), + "latest_event_timestamp": ( + latest_event.event_timestamp + if latest_event + else parser_state.latest_event_timestamp + ), + "parser_adapter": "codex-jsonl-v1", + "parser_diagnostics_json": json.dumps( + compact_parser_diagnostics(diagnostics), + sort_keys=True, + ), + "parser_state_json": parser_state_to_json(parser_state), + "last_indexed_at": indexed_at, + } + ) + if not rows: + return + columns = [ + "source_file_id", + "source_file", + "source_file_hash", + "is_archived", + "size_bytes", + "mtime_ns", + "parsed_until_line", + "parsed_until_byte", + "latest_record_id", + "latest_event_timestamp", + "parser_adapter", + "parser_diagnostics_json", + "parser_state_json", + "last_indexed_at", + ] + placeholders = ", ".join("?" for _column in columns) + update_clause = ", ".join( + f"{column}=excluded.{column}" for column in columns if column != "source_file_id" + ) + with connect(db_path) as conn: + init_db(conn) + conn.executemany( + ( + f"INSERT INTO source_files ({', '.join(columns)}) " + f"VALUES ({placeholders}) " + f"ON CONFLICT(source_file_id) DO UPDATE SET {update_clause}" + ), + [[row[column] for column in columns] for row in rows], + ) + + +def _source_file_metadata(path: Path) -> dict[str, int] | None: + try: + stat = path.stat() + except OSError: + return None + return { + "size_bytes": int(stat.st_size), + "mtime_ns": int(stat.st_mtime_ns), + "is_archived": _is_archived_source_file(path), + } + + +def _is_archived_source_file(path: Path) -> int: + normalized = str(path).replace("\\", "/") + return int("/archived_sessions/" in normalized or normalized.startswith("archived_sessions/")) + + +def _source_file_id(path: Path) -> str: + return _source_file_hash(path) + + +def _source_file_hash(path: Path) -> str: + return hashlib.sha256(str(path).encode("utf-8")).hexdigest() + + +def _count_lines(path: Path) -> int: + try: + with path.open("rb") as handle: + return sum(1 for _line in handle) + except OSError: + return 0 + + def upsert_usage_events( - events: Iterable[UsageEvent], db_path: Path = DEFAULT_DB_PATH + events: Iterable[UsageEvent], + db_path: Path = DEFAULT_DB_PATH, + *, + refresh_links: bool = True, + replace_source_files: Iterable[Path] | None = None, ) -> int: rows = [event.to_row() for event in events] + source_files_to_replace = [str(path) for path in replace_source_files or []] with connect(db_path) as conn: init_db(conn) + if source_files_to_replace: + placeholders = ", ".join("?" for _source in source_files_to_replace) + conn.execute( + f"DELETE FROM usage_events WHERE source_file IN ({placeholders})", + source_files_to_replace, + ) if not rows: + if source_files_to_replace and refresh_links: + _refresh_usage_event_links(conn) + _refresh_thread_summaries(conn) return 0 placeholders = ", ".join("?" for _ in EVENT_COLUMNS) update_clause = ", ".join( @@ -348,9 +747,207 @@ def upsert_usage_events( f"ON CONFLICT(record_id) DO UPDATE SET {update_clause}" ) conn.executemany(sql, [[row[column] for column in EVENT_COLUMNS] for row in rows]) + if refresh_links: + _refresh_usage_event_links(conn) + _refresh_thread_summaries(conn) return len(rows) +def refresh_usage_event_links(db_path: Path = DEFAULT_DB_PATH) -> int: + """Recompute per-thread chronological adjacency for aggregate usage rows.""" + + with connect(db_path) as conn: + init_db(conn) + changed = _refresh_usage_event_links(conn) + _refresh_thread_summaries(conn) + return changed + + +def _refresh_usage_event_links(conn: sqlite3.Connection) -> int: + before = conn.total_changes + conn.execute("DROP TABLE IF EXISTS temp_usage_event_links") + conn.execute( + """ + CREATE TEMP TABLE temp_usage_event_links AS + SELECT + record_id, + ROW_NUMBER() OVER ( + PARTITION BY coalesce(nullif(thread_key, ''), 'session:' || session_id) + ORDER BY event_timestamp, cumulative_total_tokens, line_number, record_id + ) AS next_thread_call_index, + LAG(record_id) OVER ( + PARTITION BY coalesce(nullif(thread_key, ''), 'session:' || session_id) + ORDER BY event_timestamp, cumulative_total_tokens, line_number, record_id + ) AS previous_id, + LEAD(record_id) OVER ( + PARTITION BY coalesce(nullif(thread_key, ''), 'session:' || session_id) + ORDER BY event_timestamp, cumulative_total_tokens, line_number, record_id + ) AS next_id + FROM usage_events + """ + ) + conn.execute( + "CREATE UNIQUE INDEX temp_usage_event_links_record_id ON temp_usage_event_links(record_id)" + ) + conn.execute( + """ + UPDATE usage_events + SET + thread_call_index = ( + SELECT next_thread_call_index + FROM temp_usage_event_links + WHERE temp_usage_event_links.record_id = usage_events.record_id + ), + previous_record_id = ( + SELECT previous_id + FROM temp_usage_event_links + WHERE temp_usage_event_links.record_id = usage_events.record_id + ), + next_record_id = ( + SELECT next_id + FROM temp_usage_event_links + WHERE temp_usage_event_links.record_id = usage_events.record_id + ) + WHERE record_id IN ( + SELECT record_id + FROM temp_usage_event_links + ) + """ + ) + conn.execute("DROP TABLE IF EXISTS temp_usage_event_links") + return conn.total_changes - before + + +def refresh_thread_summaries(db_path: Path = DEFAULT_DB_PATH) -> int: + """Rebuild materialized per-thread aggregate summaries.""" + + with connect(db_path) as conn: + init_db(conn) + return _refresh_thread_summaries(conn) + + +def _refresh_thread_summaries(conn: sqlite3.Connection) -> int: + before = conn.total_changes + conn.execute("DELETE FROM thread_summaries") + updated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat() + _insert_thread_summary_scope( + conn, + scope="active", + include_archived=False, + updated_at=updated_at, + ) + _insert_thread_summary_scope( + conn, + scope="all-history", + include_archived=True, + updated_at=updated_at, + ) + return conn.total_changes - before + + +def _insert_thread_summary_scope( + conn: sqlite3.Connection, + *, + scope: str, + include_archived: bool, + updated_at: str, +) -> None: + where_clause, params = _usage_where_clause(include_archived=include_archived) + thread_key_expr = _thread_key_expression() + conn.execute( + f""" + INSERT INTO thread_summaries ( + thread_key, + is_archived_scope, + thread_label, + first_event_timestamp, + latest_event_timestamp, + call_count, + session_count, + input_tokens, + cached_input_tokens, + uncached_input_tokens, + output_tokens, + reasoning_output_tokens, + total_tokens, + estimated_cost_usd, + usage_credits, + avg_cache_ratio, + max_context_window_percent, + max_recommendation_score, + primary_recommendation, + call_initiator_summary, + archived_call_count, + updated_at + ) + SELECT + {thread_key_expr} AS thread_key, + ? AS is_archived_scope, + coalesce(max(thread_name), max(parent_thread_name), max(session_id)) AS thread_label, + MIN(event_timestamp) AS first_event_timestamp, + MAX(event_timestamp) AS latest_event_timestamp, + COUNT(*) AS call_count, + COUNT(DISTINCT session_id) AS session_count, + coalesce(SUM(input_tokens), 0) AS input_tokens, + coalesce(SUM(cached_input_tokens), 0) AS cached_input_tokens, + coalesce(SUM(uncached_input_tokens), 0) AS uncached_input_tokens, + coalesce(SUM(output_tokens), 0) AS output_tokens, + coalesce(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens, + coalesce(SUM(total_tokens), 0) AS total_tokens, + NULL AS estimated_cost_usd, + NULL AS usage_credits, + coalesce(AVG(cache_ratio), 0) AS avg_cache_ratio, + coalesce(MAX(context_window_percent), 0) AS max_context_window_percent, + coalesce(MAX( + CASE + WHEN context_window_percent >= 0.90 THEN 100 + WHEN cache_ratio < 0.20 AND input_tokens >= 50000 THEN 80 + WHEN total_tokens >= 100000 THEN 70 + ELSE 0 + END + ), 0) AS max_recommendation_score, + CASE + WHEN MAX(context_window_percent) >= 0.90 THEN 'high_context_use' + WHEN MIN(cache_ratio) < 0.20 AND MAX(input_tokens) >= 50000 + THEN 'low_cache_reuse' + WHEN MAX(total_tokens) >= 100000 THEN 'large_calls' + ELSE NULL + END AS primary_recommendation, + CASE + WHEN SUM( + CASE WHEN coalesce(call_initiator, 'unknown') = 'codex' + THEN 1 ELSE 0 END + ) > SUM( + CASE WHEN coalesce(call_initiator, 'unknown') = 'user' + THEN 1 ELSE 0 END + ) + THEN 'mostly_codex' + WHEN SUM( + CASE WHEN coalesce(call_initiator, 'unknown') = 'user' + THEN 1 ELSE 0 END + ) > SUM( + CASE WHEN coalesce(call_initiator, 'unknown') = 'codex' + THEN 1 ELSE 0 END + ) + THEN 'mostly_user' + WHEN SUM( + CASE WHEN coalesce(call_initiator, 'unknown') = 'unknown' + THEN 1 ELSE 0 END + ) = COUNT(*) + THEN 'unknown' + ELSE 'mixed' + END AS call_initiator_summary, + SUM(CASE WHEN coalesce(is_archived, 0) != 0 THEN 1 ELSE 0 END) + AS archived_call_count, + ? AS updated_at + FROM usage_events + {where_clause} + GROUP BY {thread_key_expr} + """, + [scope, updated_at, *params], + ) + + def query_summary( db_path: Path = DEFAULT_DB_PATH, group_by: str = "thread", @@ -552,6 +1149,278 @@ def query_dashboard_event_count( return int(row["row_count"] if row is not None else 0) +def query_dashboard_token_summary( + db_path: Path = DEFAULT_DB_PATH, + since: str | None = None, + include_archived: bool = True, +) -> dict[str, Any]: + """Return cheap aggregate token totals for the dashboard shell.""" + + where_clause, params = _usage_where_clause( + since=since, + include_archived=include_archived, + ) + with connect(db_path) as conn: + init_db(conn) + total_row = conn.execute( + f""" + SELECT + COUNT(*) AS row_count, + coalesce(SUM(input_tokens), 0) AS input_tokens, + coalesce(SUM(cached_input_tokens), 0) AS cached_input_tokens, + coalesce(SUM(uncached_input_tokens), 0) AS uncached_input_tokens, + coalesce(SUM(output_tokens), 0) AS output_tokens, + coalesce(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens, + coalesce(SUM(total_tokens), 0) AS total_tokens + FROM usage_events + {where_clause} + """, + params, + ).fetchone() + model_rows = [ + _row_to_dict(row) + for row in conn.execute( + f""" + SELECT + coalesce(model, 'Unknown model') AS model, + COUNT(*) AS row_count, + coalesce(SUM(input_tokens), 0) AS input_tokens, + coalesce(SUM(cached_input_tokens), 0) AS cached_input_tokens, + coalesce(SUM(uncached_input_tokens), 0) AS uncached_input_tokens, + coalesce(SUM(output_tokens), 0) AS output_tokens, + coalesce(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens, + coalesce(SUM(total_tokens), 0) AS total_tokens + FROM usage_events + {where_clause} + GROUP BY coalesce(model, 'Unknown model') + """, + params, + ) + ] + summary = _row_to_dict(total_row) if total_row is not None else {} + return { + "row_count": int(summary.get("row_count") or 0), + "input_tokens": int(summary.get("input_tokens") or 0), + "cached_input_tokens": int(summary.get("cached_input_tokens") or 0), + "uncached_input_tokens": int(summary.get("uncached_input_tokens") or 0), + "output_tokens": int(summary.get("output_tokens") or 0), + "reasoning_output_tokens": int(summary.get("reasoning_output_tokens") or 0), + "total_tokens": int(summary.get("total_tokens") or 0), + "model_rows": model_rows, + } + + +def query_usage_status( + db_path: Path = DEFAULT_DB_PATH, + *, + include_archived: bool = False, +) -> dict[str, Any]: + """Return cheap row-count metadata for live dashboard status checks.""" + + scoped_where, scoped_params = _usage_where_clause(include_archived=include_archived) + active_where, active_params = _usage_where_clause(include_archived=False) + with connect(db_path) as conn: + init_db(conn) + total_row = conn.execute("SELECT COUNT(*) AS count FROM usage_events").fetchone() + active_row = conn.execute( + f"SELECT COUNT(*) AS count FROM usage_events {active_where}", + active_params, + ).fetchone() + scoped_row = conn.execute( + f"SELECT COUNT(*) AS count FROM usage_events {scoped_where}", + scoped_params, + ).fetchone() + max_row = conn.execute( + f"SELECT MAX(event_timestamp) AS max_event_timestamp FROM usage_events {scoped_where}", + scoped_params, + ).fetchone() + return { + "total_rows": int(total_row["count"] if total_row is not None else 0), + "active_rows": int(active_row["count"] if active_row is not None else 0), + "scoped_rows": int(scoped_row["count"] if scoped_row is not None else 0), + "max_event_timestamp": ( + max_row["max_event_timestamp"] if max_row is not None else None + ), + } + + +def query_usage_api_events( + db_path: Path = DEFAULT_DB_PATH, + *, + limit: int | None = 100, + offset: int = 0, + search: str | None = None, + since: str | None = None, + until: str | None = None, + model: str | None = None, + effort: str | None = None, + thread: str | None = None, + thread_key: str | None = None, + include_archived: bool = False, + sort: str = "time", + direction: str = "desc", +) -> list[dict[str, Any]]: + """Return a SQL-backed slice for live dashboard call APIs.""" + + where_clause, params = _usage_api_where_clause( + search=search, + since=since, + until=until, + model=model, + effort=effort, + thread=thread, + thread_key=thread_key, + include_archived=include_archived, + table_alias="usage_events", + ) + order_expr = _usage_api_sort_expression(sort) + direction_sql = _normalize_sort_direction(direction) + normalized_limit = _normalize_limit(limit) + normalized_offset = _normalize_offset(offset) + limit_clause = "" + query_params = list(params) + if normalized_limit is not None: + limit_clause = "LIMIT ?" + query_params.append(normalized_limit) + if normalized_offset: + limit_clause += " OFFSET ?" + query_params.append(normalized_offset) + elif normalized_offset: + limit_clause = "LIMIT -1 OFFSET ?" + query_params.append(normalized_offset) + parent_where_clause, parent_params = _usage_where_clause(include_archived=include_archived) + parent_thread_filter = ( + f"{parent_where_clause} AND thread_name IS NOT NULL" + if parent_where_clause + else "WHERE thread_name IS NOT NULL" + ) + with connect(db_path) as conn: + init_db(conn) + rows = conn.execute( + f""" + SELECT + usage_events.*, + coalesce( + usage_events.parent_thread_name, + parent_threads.thread_name + ) AS resolved_parent_thread_name, + coalesce( + usage_events.parent_session_updated_at, + parent_threads.session_updated_at + ) AS resolved_parent_session_updated_at + FROM usage_events + LEFT JOIN ( + SELECT + session_id, + max(thread_name) AS thread_name, + max(session_updated_at) AS session_updated_at + FROM usage_events + {parent_thread_filter} + GROUP BY session_id + ) AS parent_threads + ON usage_events.parent_session_id = parent_threads.session_id + {where_clause} + ORDER BY {order_expr} {direction_sql}, + usage_events.event_timestamp DESC, + usage_events.cumulative_total_tokens DESC + {limit_clause} + """, + [*parent_params, *query_params], + ) + return [_row_to_dict(row) for row in rows] + + +def query_usage_api_event_count( + db_path: Path = DEFAULT_DB_PATH, + *, + search: str | None = None, + since: str | None = None, + until: str | None = None, + model: str | None = None, + effort: str | None = None, + thread: str | None = None, + thread_key: str | None = None, + include_archived: bool = False, +) -> int: + """Return count for SQL-backed live dashboard call APIs.""" + + where_clause, params = _usage_api_where_clause( + search=search, + since=since, + until=until, + model=model, + effort=effort, + thread=thread, + thread_key=thread_key, + include_archived=include_archived, + ) + with connect(db_path) as conn: + init_db(conn) + row = conn.execute( + f"SELECT COUNT(*) AS row_count FROM usage_events {where_clause}", + params, + ).fetchone() + return int(row["row_count"] if row is not None else 0) + + +def query_thread_summaries( + db_path: Path = DEFAULT_DB_PATH, + *, + limit: int | None = 100, + offset: int = 0, + search: str | None = None, + include_archived: bool = False, + sort: str = "tokens", + direction: str = "desc", +) -> list[dict[str, Any]]: + """Return materialized thread summaries for live dashboard APIs.""" + + clauses = ["is_archived_scope = ?"] + params: list[Any] = ["all-history" if include_archived else "active"] + if search: + like = f"%{search}%" + clauses.append("(thread_key LIKE ? OR thread_label LIKE ?)") + params.extend([like, like]) + where_clause = "WHERE " + " AND ".join(f"({clause})" for clause in clauses) + sort_map = { + "tokens": "total_tokens", + "time": "latest_event_timestamp", + "calls": "call_count", + "cache": "avg_cache_ratio", + "thread": "thread_label", + } + if sort not in sort_map: + allowed = ", ".join(sorted(sort_map)) + raise ValueError(f"sort must be one of: {allowed}") + direction_sql = _normalize_sort_direction(direction) + normalized_limit = _normalize_limit(limit) + normalized_offset = _normalize_offset(offset) + limit_clause = "" + query_params = list(params) + if normalized_limit is not None: + limit_clause = "LIMIT ?" + query_params.append(normalized_limit) + if normalized_offset: + limit_clause += " OFFSET ?" + query_params.append(normalized_offset) + elif normalized_offset: + limit_clause = "LIMIT -1 OFFSET ?" + query_params.append(normalized_offset) + with connect(db_path) as conn: + init_db(conn) + rows = conn.execute( + f""" + SELECT * + FROM thread_summaries + {where_clause} + ORDER BY {sort_map[sort]} {direction_sql}, latest_event_timestamp DESC + {limit_clause} + """, + query_params, + ) + return [_row_to_dict(row) for row in rows] + + def query_most_expensive_calls( db_path: Path = DEFAULT_DB_PATH, limit: int = 20, since: str | None = None ) -> list[dict[str, Any]]: @@ -625,6 +1494,15 @@ def _since_where_clause(since: str | None) -> tuple[str, list[Any]]: return _usage_where_clause(since=since) +def _thread_key_expression(prefix: str = "") -> str: + return ( + f"coalesce(nullif({prefix}thread_key, ''), " + f"CASE WHEN {prefix}thread_name IS NOT NULL " + f"THEN 'thread:' || {prefix}thread_name " + f"ELSE 'session:' || {prefix}session_id END)" + ) + + def _usage_where_clause( *, since: str | None = None, @@ -664,13 +1542,87 @@ def _usage_where_clause( clauses.append(f"{prefix}total_tokens >= ?") params.append(min_tokens) if not include_archived: - clauses.extend([f"{prefix}source_file NOT LIKE ?"] * len(_ARCHIVED_SOURCE_PATTERNS)) + archived_path_clause = " OR ".join( + f"{prefix}source_file LIKE ?" for _pattern in _ARCHIVED_SOURCE_PATTERNS + ) + clauses.append( + f"(coalesce({prefix}is_archived, 0) = 0 AND NOT ({archived_path_clause}))" + ) params.extend(_ARCHIVED_SOURCE_PATTERNS) if not clauses: return "", [] return "WHERE " + " AND ".join(clauses), params +def _usage_api_where_clause( + *, + search: str | None = None, + since: str | None = None, + until: str | None = None, + model: str | None = None, + effort: str | None = None, + thread: str | None = None, + thread_key: str | None = None, + min_tokens: int | None = None, + include_archived: bool = True, + table_alias: str | None = None, +) -> tuple[str, list[Any]]: + prefix = f"{table_alias}." if table_alias else "" + base_where, params = _usage_where_clause( + since=since, + until=until, + model=model, + effort=effort, + thread=thread, + min_tokens=min_tokens, + include_archived=include_archived, + table_alias=table_alias, + ) + clauses = [base_where.removeprefix("WHERE ")] if base_where else [] + if search: + like = f"%{search}%" + clauses.append( + "(" + f"{prefix}thread_name LIKE ? OR " + f"{prefix}parent_thread_name LIKE ? OR " + f"{prefix}cwd LIKE ? OR " + f"{prefix}model LIKE ? OR " + f"{prefix}session_id LIKE ?" + ")" + ) + params.extend([like, like, like, like, like]) + if thread_key: + clauses.append( + "(" + f"{prefix}thread_key = ? OR " + f"'thread:' || {prefix}thread_name = ? OR " + f"'session:' || {prefix}session_id = ? OR " + f"{prefix}session_id = ?" + ")" + ) + params.extend([thread_key, thread_key, thread_key, thread_key]) + if not clauses: + return "", [] + return "WHERE " + " AND ".join(f"({clause})" for clause in clauses), params + + +def _usage_api_sort_expression(sort: str) -> str: + try: + return API_USAGE_SORTS[sort] + except KeyError as exc: + allowed = ", ".join(sorted(API_USAGE_SORTS)) + raise ValueError(f"sort must be one of: {allowed}") from exc + + +def _normalize_sort_direction(direction: str) -> str: + normalized = direction.lower() + if normalized == "asc": + return "ASC" + if normalized == "desc": + return "DESC" + raise ValueError("direction must be one of: asc, desc") + + def _normalize_limit(limit: int | None) -> int | None: if limit is None or limit <= 0: return None diff --git a/tests/test_call_origin.py b/tests/test_call_origin.py index 9593d3b..0e9e856 100644 --- a/tests/test_call_origin.py +++ b/tests/test_call_origin.py @@ -1,91 +1,86 @@ from __future__ import annotations -import json -from pathlib import Path +from codex_usage_tracker.call_origin import ( + CallOriginFlags, + classify_call_origin, + event_flags_from_envelope, + fallback_call_origin, +) -from codex_usage_tracker.call_origin import annotate_rows_with_call_origin - -def test_call_origin_uses_event_metadata_between_token_counts(tmp_path: Path) -> None: - log_path = tmp_path / "session.jsonl" - rows = [ - _entry("session_meta", {"id": "session-a"}), - _entry("turn_context", {"turn_id": "turn-a"}), - _entry("response_item", {"type": "message", "role": "user", "content": []}), - _entry("event_msg", {"type": "user_message", "message": "raw prompt is ignored"}), - _entry("response_item", {"type": "message", "role": "assistant", "content": []}), - _token_event(), - _entry("response_item", {"type": "function_call_output", "output": "raw output is ignored"}), - _entry("response_item", {"type": "message", "role": "assistant", "content": []}), - _token_event(), - _entry("compacted", {"message": "", "replacement_history": []}), - _entry("turn_context", {"turn_id": "turn-b"}), - _token_event(), - _token_event(), - ] - _write_jsonl(log_path, rows) - - annotated = annotate_rows_with_call_origin( +def test_call_origin_classifies_metadata_segments_without_raw_text() -> None: + user_fields = classify_call_origin( [ - _row("user", log_path, 6), - _row("tool", log_path, 9), - _row("compaction", log_path, 12), - _row("unknown", log_path, 13), + CallOriginFlags(user_message=True), + CallOriginFlags(tool_result=True), ] ) - by_id = {row["record_id"]: row for row in annotated} - - assert by_id["user"]["call_initiator"] == "user" - assert by_id["user"]["call_initiator_reason"] == "user_message" - assert by_id["tool"]["call_initiator"] == "codex" - assert by_id["tool"]["call_initiator_reason"] == "tool_result" - assert by_id["compaction"]["call_initiator"] == "codex" - assert by_id["compaction"]["call_initiator_reason"] == "post_compaction" - assert by_id["unknown"]["call_initiator"] == "unknown" - assert by_id["unknown"]["call_initiator_reason"] == "no_signal" - - -def test_call_origin_falls_back_to_subagent_metadata_when_source_missing(tmp_path: Path) -> None: - annotated = annotate_rows_with_call_origin( - [ + compaction_fields = classify_call_origin([CallOriginFlags(compaction=True)]) + tool_fields = classify_call_origin([CallOriginFlags(tool_result=True)]) + continuation_fields = classify_call_origin([CallOriginFlags(codex_activity=True)]) + unknown_fields = classify_call_origin([]) + + assert user_fields == _origin("user", "user_message", "high") + assert compaction_fields == _origin("codex", "post_compaction", "high") + assert tool_fields == _origin("codex", "tool_result", "high") + assert continuation_fields == _origin("codex", "agent_continuation", "medium") + assert unknown_fields == _origin("unknown", "no_signal", "low") + + +def test_call_origin_reads_only_event_shape_metadata() -> None: + assert event_flags_from_envelope( + _entry( + "response_item", { - "record_id": "subagent", - "source_file": str(tmp_path / "missing.jsonl"), - "line_number": 1, - "thread_source": "subagent", + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "SECRET RAW PROMPT"}], }, + ) + ).user_message is True + assert event_flags_from_envelope( + _entry("response_item", {"type": "function_call_output", "output": "SECRET"}) + ).tool_result is True + assert event_flags_from_envelope( + _entry("event_msg", {"type": "context_compacted", "replacement_history": ["SECRET"]}) + ).compaction is True + assert event_flags_from_envelope( + _entry( + "response_item", { - "record_id": "normal", - "source_file": str(tmp_path / "missing.jsonl"), - "line_number": 2, - "thread_source": "user", + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "SECRET RAW ANSWER"}], }, - ] + ) + ).codex_activity is True + + +def test_call_origin_falls_back_to_subagent_metadata_for_migrated_rows() -> None: + subagent_fields = fallback_call_origin( + { + "record_id": "subagent", + "thread_source": "subagent", + } + ) + normal_fields = fallback_call_origin( + { + "record_id": "normal", + "thread_source": "user", + } ) - by_id = {row["record_id"]: row for row in annotated} - assert by_id["subagent"]["call_initiator"] == "codex" - assert by_id["subagent"]["call_initiator_reason"] == "thread_source" - assert by_id["normal"]["call_initiator"] == "unknown" - assert by_id["normal"]["call_initiator_reason"] == "source_unavailable" + assert subagent_fields == _origin("codex", "thread_source", "medium") + assert normal_fields == _origin("unknown", "missing_origin", "low") -def _row(record_id: str, source_file: Path, line_number: int) -> dict[str, object]: +def _origin(initiator: str, reason: str, confidence: str) -> dict[str, str]: return { - "record_id": record_id, - "source_file": str(source_file), - "line_number": line_number, - "thread_source": "user", + "call_initiator": initiator, + "call_initiator_reason": reason, + "call_initiator_confidence": confidence, } -def _token_event() -> dict[str, object]: - return _entry("event_msg", {"type": "token_count"}) - - def _entry(entry_type: str, payload: dict[str, object]) -> dict[str, object]: return {"type": entry_type, "payload": payload} - - -def _write_jsonl(path: Path, rows: list[dict[str, object]]) -> None: - path.write_text("".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8") diff --git a/tests/test_cli_release.py b/tests/test_cli_release.py index 4457b6a..7ed32c7 100644 --- a/tests/test_cli_release.py +++ b/tests/test_cli_release.py @@ -396,6 +396,52 @@ def test_synthetic_history_benchmark_script_smoke(tmp_path: Path) -> None: } <= set(payload["benchmarks"][0]["timings"]) +def test_synthetic_history_benchmark_with_source_logs_smoke(tmp_path: Path) -> None: + repo_root = Path(__file__).resolve().parents[1] + result = subprocess.run( + [ + sys.executable, + "scripts/benchmark_synthetic_history.py", + "--rows", + "100", + "--batch-size", + "25", + "--db-dir", + str(tmp_path), + "--with-source-logs", + "--json", + "--enforce-thresholds", + "--threshold-scale", + "5", + ], + check=True, + capture_output=True, + text=True, + cwd=repo_root, + env=_subprocess_env(), + ) + payload = json.loads(result.stdout) + benchmark = payload["benchmarks"][0] + + assert benchmark["threshold_status"] == "pass" + assert benchmark["threshold_failures"] == [] + assert benchmark["source_logs_generated"] > 0 + assert benchmark["source_log_bytes"] > 0 + assert benchmark["context_load_seconds"] is not None + assert benchmark["context_payload_json_bytes"] > 0 + assert benchmark["source_scan_ms"] >= 0 + assert benchmark["serialized_estimate_ms"] >= 0 + assert { + "dashboard_payload_with_source_logs_seconds", + "context_load_early_line_seconds", + "context_load_middle_line_seconds", + "context_load_late_line_seconds", + } <= set(benchmark["timings"]) + assert {"early", "middle", "late"} == set(benchmark["context_loads"]) + assert benchmark["context_loads"]["middle"]["context_payload_json_bytes"] > 0 + assert benchmark["context_loads"]["middle"]["source_scan_ms"] >= 0 + + def _subprocess_env() -> dict[str, str]: env = dict(os.environ) repo_root = Path(__file__).resolve().parents[1] diff --git a/tests/test_dashboard_data.py b/tests/test_dashboard_data.py index b17d534..2d3ae6c 100644 --- a/tests/test_dashboard_data.py +++ b/tests/test_dashboard_data.py @@ -112,7 +112,8 @@ def test_adjacent_thread_calls_are_chronological_and_scoped_to_resolved_thread() { record_id: 'last', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:02:00Z', cumulative_total_tokens: 50 }, ]; const selected = rows.find(row => row.record_id === 'middle'); -const adjacent = helpers.adjacentThreadCalls(rows, selected); +const index = helpers.buildCallAdjacencyIndex(rows); +const adjacent = helpers.adjacentThreadCalls(rows, selected, index); console.log(JSON.stringify({ order: adjacent.calls.map(row => row.record_id), index: adjacent.index, @@ -130,6 +131,54 @@ def test_adjacent_thread_calls_are_chronological_and_scoped_to_resolved_thread() } +def test_call_adjacency_index_prefers_persisted_neighbors_when_loaded() -> None: + payload = _run_dashboard_data_script( + """ +const rows = [ + { record_id: 'old-loaded', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:00:00Z', cumulative_total_tokens: 1 }, + { record_id: 'middle', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:02:00Z', cumulative_total_tokens: 30, previous_record_id: 'persisted-prev', next_record_id: 'persisted-next' }, + { record_id: 'persisted-next', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:03:00Z', cumulative_total_tokens: 40 }, + { record_id: 'persisted-prev', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:04:00Z', cumulative_total_tokens: 50 }, +]; +const index = helpers.buildCallAdjacencyIndex(rows); +const adjacent = index.get('middle'); +console.log(JSON.stringify({ + order: adjacent.calls.map(row => row.record_id), + previous: adjacent.previous.record_id, + next: adjacent.next.record_id, +})); +""" + ) + + assert payload == { + "order": ["old-loaded", "middle", "persisted-next", "persisted-prev"], + "previous": "persisted-prev", + "next": "persisted-next", + } + + +def test_call_adjacency_index_does_not_guess_when_persisted_neighbor_is_unloaded() -> None: + payload = _run_dashboard_data_script( + """ +const rows = [ + { record_id: 'old-loaded', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:00:00Z', cumulative_total_tokens: 1 }, + { record_id: 'middle', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:02:00Z', cumulative_total_tokens: 30, previous_record_id: 'not-loaded' }, + { record_id: 'next-loaded', thread_name: 'Thread A', event_timestamp: '2026-06-01T00:03:00Z', cumulative_total_tokens: 40 }, +]; +const adjacent = helpers.buildCallAdjacencyIndex(rows).get('middle'); +console.log(JSON.stringify({ + previous: adjacent.previous, + next: adjacent.next.record_id, +})); +""" + ) + + assert payload == { + "previous": None, + "next": "next-loaded", + } + + def test_call_accounting_delta_uses_token_counter_fields() -> None: payload = _run_dashboard_data_script( """ diff --git a/tests/test_i18n.py b/tests/test_i18n.py index 5a01ade..f129c49 100644 --- a/tests/test_i18n.py +++ b/tests/test_i18n.py @@ -147,6 +147,25 @@ def test_catalog_placeholders_match_english(language: str) -> None: assert placeholders(current[key]) == placeholders(english_value), key +@pytest.mark.parametrize("language", [lang for lang in SUPPORTED_LANGUAGES if lang != "en"]) +def test_non_english_catalogs_translate_core_visible_labels(language: str) -> None: + english = raw_catalog("en") + current = raw_catalog(language) + + core_labels = [ + "dashboard.title", + "button.refresh", + "filter.search", + "dashboard.view.calls", + "dashboard.view.threads", + "metric.total_tokens", + "table.cost", + ] + + untranslated = [key for key in core_labels if current[key] == english[key]] + assert not untranslated, f"{language} leaves core dashboard labels untranslated: {untranslated}" + + def test_catalog_keys_use_expected_namespaces() -> None: for key in raw_catalog("en"): assert key.startswith(EXPECTED_KEY_PREFIXES), key @@ -251,7 +270,7 @@ def test_arabic_direction_is_rtl() -> None: @pytest.mark.parametrize("language", [lang for lang in SUPPORTED_LANGUAGES if lang != "ar"]) -def test_non_arabic_starter_languages_are_ltr(language: str) -> None: +def test_non_arabic_supported_languages_are_ltr(language: str) -> None: assert language_direction(language) == "ltr" diff --git a/tests/test_parser.py b/tests/test_parser.py index 4d4cf0c..92e5864 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -161,9 +161,96 @@ def test_parser_ignores_known_non_token_context_compaction_event(tmp_path: Path) assert len(events) == 1 assert stats.get("unknown_event_shape", 0) == 0 + assert events[0].call_initiator == "codex" + assert events[0].call_initiator_reason == "post_compaction" + assert events[0].call_initiator_confidence == "high" assert "SECRET COMPACTION TEXT" not in json.dumps([event.to_row() for event in events]) +def test_parser_persists_call_origin_from_metadata_segments(tmp_path: Path) -> None: + log_path = tmp_path / f"rollout-2026-05-17T14-58-23-{SESSION_ID}.jsonl" + _write_jsonl( + log_path, + [ + _entry("session_meta", {"id": SESSION_ID}), + _entry("turn_context", {"turn_id": "turn-a", "model": "gpt-5.5"}), + _entry( + "response_item", + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "SECRET USER TEXT"}], + }, + ), + _token_event(100, 100), + _entry("response_item", {"type": "function_call_output", "output": "SECRET TOOL"}), + _token_event(150, 50), + _entry( + "response_item", + { + "type": "message", + "role": "assistant", + "content": [{"type": "output_text", "text": "SECRET ASSISTANT TEXT"}], + }, + ), + _token_event(210, 60), + _token_event(280, 70), + ], + ) + + events = parse_usage_events_from_file(log_path) + + assert [ + ( + event.call_initiator, + event.call_initiator_reason, + event.call_initiator_confidence, + ) + for event in events + ] == [ + ("user", "user_message", "high"), + ("codex", "tool_result", "high"), + ("codex", "agent_continuation", "medium"), + ("unknown", "no_signal", "low"), + ] + assert "SECRET" not in json.dumps([event.to_row() for event in events]) + + +def test_parser_persists_dashboard_helper_metadata(tmp_path: Path) -> None: + log_path = ( + tmp_path + / ".codex" + / "archived_sessions" + / f"rollout-2026-05-17T14-58-23-{SESSION_ID}.jsonl" + ) + _write_jsonl( + log_path, + [ + _entry("session_meta", {"id": SESSION_ID}), + _entry("turn_context", {"turn_id": "turn-a", "model": "gpt-5.5"}), + _token_event(100, 100), + ], + ) + + events = parse_usage_events_from_file( + log_path, + { + SESSION_ID: SessionInfo( + session_id=SESSION_ID, + thread_name="Archived tracker thread", + updated_at="2026-05-17T18:00:00Z", + ) + }, + ) + + assert len(events) == 1 + assert events[0].is_archived == 1 + assert events[0].thread_key == "thread:Archived tracker thread" + assert events[0].thread_call_index is None + assert events[0].previous_record_id is None + assert events[0].next_record_id is None + + def test_inspect_log_reports_aggregate_diagnostics_without_db_writes(tmp_path: Path) -> None: log_path = tmp_path / "unknown-name.jsonl" missing_counter = _token_event(100, 100) diff --git a/tests/test_privacy.py b/tests/test_privacy.py index 0b103e1..93ee2dc 100644 --- a/tests/test_privacy.py +++ b/tests/test_privacy.py @@ -97,6 +97,17 @@ def test_aggregate_outputs_exclude_raw_transcript_content(tmp_path: Path) -> Non assert strict_row["git_branch"] is None assert strict_row["git_remote_label"] is None assert strict_row["project_tags"] == [] + assert strict_row["call_initiator"] in {"user", "codex", "unknown"} + assert strict_row["call_initiator_reason"] in { + "user_message", + "post_compaction", + "tool_result", + "agent_continuation", + "no_signal", + "thread_source", + "missing_origin", + } + assert strict_row["call_initiator_confidence"] in {"high", "medium", "low"} assert csv_rows[0]["cwd"].startswith("[redacted cwd:") assert PRIVATE_BRANCH not in json.dumps(strict_payload) assert PRIVATE_TAG not in json.dumps(strict_payload) @@ -192,10 +203,12 @@ def test_context_loading_is_explicit_redacted_and_not_static_html(tmp_path: Path assert sentinel not in static_html assert default_context["loaded_on_demand"] is True assert default_context["raw_context_persisted"] is False + assert default_context["context_mode"] == "quick" + assert default_context["serialized_evidence"]["deferred_buckets"] is True assert PROMPT_SENTINEL in default_context_text assert ASSISTANT_SENTINEL in default_context_text - assert default_context["call_anchors"]["available"] is True - assert ASSISTANT_SENTINEL in json.dumps(default_context["call_anchors"]) + assert "call_anchors" not in default_context + assert "thread_anchors" not in default_context assert PROMPT_SENTINEL not in static_html assert TOOL_OUTPUT_SENTINEL not in default_context_text assert "Tool output hidden for this request" in default_context_text @@ -283,6 +296,7 @@ def test_context_server_requires_loopback_origin_token_and_enablement(tmp_path: assert settings_payload["raw_context_persisted"] is False assert context_payload["loaded_on_demand"] is True assert context_payload["raw_context_persisted"] is False + assert context_payload["context_mode"] == "quick" class _PrivacyFixture: diff --git a/tests/test_schema.py b/tests/test_schema.py index 4b3e93a..a11815b 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -21,6 +21,14 @@ def test_usage_event_schema_matches_persisted_row_shape() -> None: effort="high", current_date="2026-05-17", timezone="America/New_York", + call_initiator="user", + call_initiator_reason="user_message", + call_initiator_confidence="high", + is_archived=0, + thread_key="thread:Thread", + thread_call_index=None, + previous_record_id=None, + next_record_id=None, thread_source="user", subagent_type=None, agent_role=None, diff --git a/tests/test_store_dashboard_mcp.py b/tests/test_store_dashboard_mcp.py index 8614838..3eaefc0 100644 --- a/tests/test_store_dashboard_mcp.py +++ b/tests/test_store_dashboard_mcp.py @@ -10,7 +10,11 @@ from functools import partial from http.server import ThreadingHTTPServer from pathlib import Path +from typing import Any +import pytest + +from codex_usage_tracker import store as store_module from codex_usage_tracker.context import load_call_context from codex_usage_tracker.dashboard import dashboard_payload, generate_dashboard from codex_usage_tracker.diagnostics import run_doctor @@ -31,6 +35,7 @@ query_most_expensive_calls, query_session_usage, query_summary, + query_thread_summaries, rebuild_usage_index, refresh_metadata, refresh_usage_index, @@ -74,7 +79,8 @@ def test_refresh_is_idempotent_and_summary_works(tmp_path: Path) -> None: subagent_rows = query_session_usage(db_path=db_path, session_id=SECOND_SESSION_ID) assert first.parsed_events == 4 - assert second.parsed_events == 4 + assert second.parsed_events == 0 + assert second.inserted_or_updated_events == 0 assert first.skipped_events == 0 assert len(session_rows) == 2 assert summary[0]["group_key"] == "gpt-5.5" @@ -93,16 +99,37 @@ def test_refresh_is_idempotent_and_summary_works(tmp_path: Path) -> None: row["key"]: row["value"] for row in conn.execute("SELECT key, value FROM refresh_meta").fetchall() } - assert meta["parsed_events"] == "4" + assert meta["parsed_events"] == "0" assert meta["skipped_events"] == "0" - assert meta["inserted_or_updated_events"] == "4" + assert meta["inserted_or_updated_events"] == "0" + assert meta["parsed_source_files"] == "0" + assert meta["skipped_source_files"] == "3" assert meta["parser_adapter"] == "codex-jsonl-v1" - assert meta["schema_version"] == "2" + assert meta["schema_version"] == "7" assert meta["parser_skipped_events"] == "0" state = schema_state(db_path) - assert state["schema_version"] == 2 + assert state["schema_version"] == 7 assert state["checksum_matches"] is True - assert [row["version"] for row in state["migrations"]] == [1, 2] + assert [row["version"] for row in state["migrations"]] == [1, 2, 3, 4, 5, 6, 7] + with connect(db_path) as conn: + init_db(conn) + source_rows = [ + dict(row) + for row in conn.execute( + """ + SELECT source_file, size_bytes, parsed_until_line, latest_record_id, + parser_diagnostics_json, parser_state_json + FROM source_files + ORDER BY source_file + """ + ).fetchall() + ] + assert len(source_rows) == 3 + assert all(row["size_bytes"] > 0 for row in source_rows) + assert all(row["parsed_until_line"] > 0 for row in source_rows) + assert any(row["latest_record_id"] for row in source_rows) + assert all(row["parser_state_json"] for row in source_rows) + assert "SECRET RAW PROMPT" not in json.dumps(source_rows) def test_refresh_reports_skipped_corrupt_token_events(tmp_path: Path) -> None: @@ -126,6 +153,137 @@ def test_refresh_reports_skipped_corrupt_token_events(tmp_path: Path) -> None: assert [row["cumulative_total_tokens"] for row in rows] == [100, 300, 650] +def test_refresh_indexes_only_appended_token_events_when_source_grows( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + codex_home = _make_codex_home(tmp_path) + db_path = tmp_path / "usage.sqlite3" + log_path = next( + path for path in (codex_home / "sessions").glob("**/*.jsonl") if SESSION_ID in path.name + ) + + first = refresh_usage_index(codex_home=codex_home, db_path=db_path) + with connect(db_path) as conn: + init_db(conn) + source_before = conn.execute( + """ + SELECT parsed_until_byte, parsed_until_line, parser_state_json + FROM source_files + WHERE source_file = ? + """, + (str(log_path),), + ).fetchone() + with log_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(_token_event(650, 350)) + "\n") + parse_calls: list[dict[str, Any]] = [] + original_parse = store_module.parse_usage_events_from_file_with_state + + def tracking_parse(*args: Any, **kwargs: Any): + parse_calls.append( + { + "path": args[0], + "start_byte": kwargs.get("start_byte"), + "start_line": kwargs.get("start_line"), + "initial_state": kwargs.get("initial_state"), + } + ) + return original_parse(*args, **kwargs) + + monkeypatch.setattr( + store_module, + "parse_usage_events_from_file_with_state", + tracking_parse, + ) + second = refresh_usage_index(codex_home=codex_home, db_path=db_path) + third = refresh_usage_index(codex_home=codex_home, db_path=db_path) + rows = query_session_usage(db_path=db_path, session_id=SESSION_ID) + metadata = refresh_metadata(db_path) + + assert first.parsed_events == 4 + assert source_before is not None + assert source_before["parser_state_json"] + assert len(parse_calls) == 1 + assert parse_calls[0]["path"] == log_path + assert parse_calls[0]["start_byte"] == source_before["parsed_until_byte"] + assert parse_calls[0]["start_line"] == source_before["parsed_until_line"] + assert parse_calls[0]["start_byte"] > 0 + assert parse_calls[0]["initial_state"] is not None + assert second.parsed_events == 1 + assert second.inserted_or_updated_events == 1 + assert third.parsed_events == 0 + assert [row["cumulative_total_tokens"] for row in rows] == [100, 300, 650] + assert metadata["parsed_source_files"] == "0" + assert metadata["skipped_source_files"] == "3" + + +def test_append_cursor_preserves_pending_call_origin_between_refreshes( + tmp_path: Path, +) -> None: + session_id = "019e37d5-f19f-7e4d-84cb-50894143c001" + codex_home = tmp_path / ".codex" + log_path = ( + codex_home + / "sessions" + / "2026" + / "05" + / "17" + / f"rollout-2026-05-17T18-58-27-{session_id}.jsonl" + ) + _write_jsonl( + codex_home / "session_index.jsonl", + [ + { + "id": session_id, + "thread_name": "Append cursor origin", + "updated_at": "2026-05-17T19:00:00Z", + } + ], + ) + _write_jsonl( + log_path, + [ + _entry("session_meta", {"id": session_id}), + _entry("turn_context", {"turn_id": "turn-a", "model": "gpt-5.5"}), + _token_event(100, 100), + _entry( + "response_item", + { + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "SECRET PENDING USER TEXT"}], + }, + ), + ], + ) + db_path = tmp_path / "usage.sqlite3" + + first = refresh_usage_index(codex_home=codex_home, db_path=db_path) + with log_path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(_token_event(150, 50)) + "\n") + second = refresh_usage_index(codex_home=codex_home, db_path=db_path) + rows = query_session_usage(db_path=db_path, session_id=session_id) + source_rows_text = "" + with connect(db_path) as conn: + init_db(conn) + source_rows_text = json.dumps( + [ + dict(row) + for row in conn.execute( + "SELECT parser_state_json FROM source_files WHERE source_file = ?", + (str(log_path),), + ).fetchall() + ] + ) + + assert first.parsed_events == 1 + assert second.parsed_events == 1 + assert [row["cumulative_total_tokens"] for row in rows] == [100, 150] + assert rows[-1]["call_initiator"] == "user" + assert rows[-1]["call_initiator_reason"] == "user_message" + assert "SECRET PENDING USER TEXT" not in source_rows_text + + def test_connect_sets_sqlite_concurrency_pragmas(tmp_path: Path) -> None: db_path = tmp_path / "usage.sqlite3" with connect(db_path) as conn: @@ -136,7 +294,7 @@ def test_connect_sets_sqlite_concurrency_pragmas(tmp_path: Path) -> None: assert busy_timeout == 5000 assert str(journal_mode).lower() == "wal" - assert user_version == 2 + assert user_version == 7 def test_init_db_repairs_version_zero_schema(tmp_path: Path) -> None: @@ -190,12 +348,24 @@ def test_init_db_repairs_version_zero_schema(tmp_path: Path) -> None: ).fetchall() ] - assert {"thread_source", "parent_thread_name", "parent_session_updated_at"} <= columns + assert { + "thread_source", + "parent_thread_name", + "parent_session_updated_at", + "call_initiator", + "call_initiator_reason", + "call_initiator_confidence", + "is_archived", + "thread_key", + "thread_call_index", + "previous_record_id", + "next_record_id", + } <= columns assert "idx_usage_timestamp" in indexes assert "idx_usage_parent_thread" in indexes assert "idx_usage_total_tokens" in indexes - assert user_version == 2 - assert [row["version"] for row in migrations] == [1, 2] + assert user_version == 7 + assert [row["version"] for row in migrations] == [1, 2, 3, 4, 5, 6, 7] def test_rebuild_index_clears_aggregate_rows_before_rescan(tmp_path: Path) -> None: @@ -264,6 +434,14 @@ def test_large_history_query_prefilter_uses_sql_indexes(tmp_path: Path) -> None: effort="high" if index % 3 == 0 else "low", current_date="2026-05-17", timezone="UTC", + call_initiator="user", + call_initiator_reason="user_message", + call_initiator_confidence="high", + is_archived=0, + thread_key=f"thread:Thread {index % 25}", + thread_call_index=None, + previous_record_id=None, + next_record_id=None, thread_source="user", subagent_type=None, agent_role=None, @@ -316,6 +494,127 @@ def test_large_history_query_prefilter_uses_sql_indexes(tmp_path: Path) -> None: assert "idx_usage_model_effort" in plan +def test_upsert_refreshes_thread_adjacency_fields(tmp_path: Path) -> None: + db_path = tmp_path / "usage.sqlite3" + events = [ + _usage_event( + record_id="a1", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T12:00:00Z", + cumulative_total_tokens=100, + ), + _usage_event( + record_id="b1", + session_id="session-b", + thread_key="thread:Beta", + event_timestamp="2026-05-17T12:00:01Z", + cumulative_total_tokens=50, + ), + _usage_event( + record_id="a2", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T12:00:02Z", + cumulative_total_tokens=200, + ), + _usage_event( + record_id="a3", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T12:00:03Z", + cumulative_total_tokens=300, + ), + ] + + upsert_usage_events(events, db_path=db_path) + rows = query_dashboard_events(db_path=db_path, limit=0, include_archived=True) + by_id = {row["record_id"]: row for row in rows} + + assert by_id["a1"]["thread_call_index"] == 1 + assert by_id["a1"]["previous_record_id"] is None + assert by_id["a1"]["next_record_id"] == "a2" + assert by_id["a2"]["thread_call_index"] == 2 + assert by_id["a2"]["previous_record_id"] == "a1" + assert by_id["a2"]["next_record_id"] == "a3" + assert by_id["a3"]["thread_call_index"] == 3 + assert by_id["a3"]["previous_record_id"] == "a2" + assert by_id["a3"]["next_record_id"] is None + assert by_id["b1"]["thread_call_index"] == 1 + assert by_id["b1"]["previous_record_id"] is None + assert by_id["b1"]["next_record_id"] is None + + +def test_upsert_materializes_thread_summaries(tmp_path: Path) -> None: + db_path = tmp_path / "usage.sqlite3" + events = [ + _usage_event( + record_id="a1", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T12:00:00Z", + cumulative_total_tokens=100, + ), + _usage_event( + record_id="a2", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T12:00:02Z", + cumulative_total_tokens=220, + ), + _usage_event( + record_id="b1", + session_id="session-b", + thread_key="thread:Beta", + event_timestamp="2026-05-17T12:00:01Z", + cumulative_total_tokens=90, + ), + ] + + upsert_usage_events(events, db_path=db_path) + summaries = query_thread_summaries(db_path=db_path, limit=0, include_archived=False) + by_key = {row["thread_key"]: row for row in summaries} + + assert set(by_key) == {"thread:Alpha", "thread:Beta"} + assert by_key["thread:Alpha"]["call_count"] == 2 + assert by_key["thread:Alpha"]["session_count"] == 1 + assert by_key["thread:Alpha"]["total_tokens"] == 220 + assert by_key["thread:Alpha"]["cached_input_tokens"] == 40 + assert by_key["thread:Alpha"]["call_initiator_summary"] == "mostly_user" + assert by_key["thread:Alpha"]["is_archived_scope"] == "active" + assert by_key["thread:Alpha"]["estimated_cost_usd"] is None + assert by_key["thread:Alpha"]["usage_credits"] is None + + with connect(db_path) as conn: + init_db(conn) + persisted = conn.execute("SELECT COUNT(*) AS count FROM thread_summaries").fetchone() + assert persisted is not None + assert persisted["count"] == 4 + + +def test_thread_summaries_keep_active_and_all_history_scopes_separate( + tmp_path: Path, +) -> None: + codex_home = _make_codex_home(tmp_path) + _write_archived_log(codex_home) + db_path = tmp_path / "usage.sqlite3" + + refresh_usage_index(codex_home=codex_home, db_path=db_path, include_archived=True) + active_summaries = query_thread_summaries(db_path=db_path, limit=0) + all_summaries = query_thread_summaries( + db_path=db_path, + limit=0, + include_archived=True, + ) + + assert {row["is_archived_scope"] for row in active_summaries} == {"active"} + assert {row["is_archived_scope"] for row in all_summaries} == {"all-history"} + assert sum(row["call_count"] for row in active_summaries) == 4 + assert sum(row["call_count"] for row in all_summaries) == 5 + assert sum(row["archived_call_count"] for row in active_summaries) == 0 + assert sum(row["archived_call_count"] for row in all_summaries) == 1 + + def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: codex_home = _make_codex_home(tmp_path) db_path = tmp_path / "usage.sqlite3" @@ -475,6 +774,10 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert "selectRow(row);" not in render_calls_js assert "dashboard.view.call" in dashboard_js assert "renderCallInvestigator" in dashboard_js + assert "fetchCallRecord" in dashboard_js + assert "fetchCallRecord" in dashboard_call_js + assert "/api/call?" in dashboard_js + assert "supplementalRowsByRecordId" in dashboard_js assert 'body[data-active-view="call"] .detail-section' in dashboard_css assert 'body[data-active-view="call"] .table-tools' in dashboard_css assert ".call-diagnostic-section.exact" in dashboard_css @@ -483,10 +786,13 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert "renderInvestigationReadout" in dashboard_call_js assert "contextStateRecord(row)" in dashboard_call_js assert "defaultContextRequest" in dashboard_call_js - assert "includeToolOutput: true" in dashboard_call_js - assert "maxChars: 0" in dashboard_call_js + assert "mode: 'quick'" in dashboard_call_js + assert "mode: 'full'" in dashboard_call_js + assert "includeToolOutput: false" in dashboard_call_js + assert "maxChars: null" in dashboard_call_js assert "maxEntries: defaultContextEntries" in dashboard_call_js assert "data-context-toggle-tool-output" in dashboard_call_js + assert "data-context-full-analysis" in dashboard_call_js assert "button.hide_tool_output" in dashboard_call_js assert "data-context-autoload-toggle" not in dashboard_call_js assert "renderCacheVerdict" in dashboard_call_js @@ -517,8 +823,10 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert "data-context-entry-key" in dashboard_call_js assert "button.show_tool_output" in dashboard_call_js assert "data-context-entry-load-output" in dashboard_call_js + assert "button.full_serialized_analysis" in dashboard_call_js assert ".grid > section:not(.detail-section)" in dashboard_css - assert "overflow: visible" in dashboard_css + assert "overflow-x: auto" in dashboard_css + assert "overscroll-behavior-x: contain" in dashboard_css assert "position: sticky" in dashboard_css assert ".grid > section:first-child > table > thead" in dashboard_css assert "${callInitiatorPuck(row)}" in dashboard_js @@ -529,11 +837,13 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert "data-context-no-budget" not in dashboard_call_js assert "renderContextTokenUsage" in dashboard_call_js assert "renderContextCompaction" in dashboard_call_js - assert "renderThreadAnchors" in dashboard_call_js + assert "renderThreadAnchors" not in dashboard_call_js + assert "payload.call_anchors" not in dashboard_call_js + assert "payload.thread_anchors" not in dashboard_call_js assert "context-entry-collapsed" in dashboard_call_js assert "Evidence analyzed:" in dashboard_call_js assert "total_entries" in dashboard_call_js - assert ".context-anchor-panel" in dashboard_css + assert ".context-anchor-panel" not in dashboard_css assert ".context-entry-summary" in dashboard_css assert "data-context-compaction-history" in dashboard_call_js assert "context-token-breakdown" in dashboard_css @@ -566,6 +876,7 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert en_trans["dashboard.view.call"] == "Call Investigator" assert en_trans["button.show_tool_output"] == "Show tool output" assert en_trans["button.hide_tool_output"] == "Hide tool output" + assert en_trans["button.full_serialized_analysis"] == "Run full serialized analysis" assert en_trans["button.hide_details"] == "Hide details" assert en_trans["table.initiated"] == "Initiated" assert en_trans["source.user_initiated"] == "User initiated" @@ -585,6 +896,11 @@ def test_dashboard_and_csv_are_aggregate_only(tmp_path: Path) -> None: assert "github.com/douglasmonsky/codex-usage-tracker/blob/main/docs/dashboard-guide.md" not in dashboard assert "codex-usage-tracker-guide/dashboard-guide.html" in dashboard assert (tmp_path / "codex-usage-tracker-guide" / "dashboard-guide.html").exists() + dashboard_guide = (tmp_path / "codex-usage-tracker-guide" / "dashboard-guide.html").read_text( + encoding="utf-8" + ) + assert "call anchors" not in dashboard_guide.lower() + assert "nearest visible message" not in dashboard_guide assert (tmp_path / "codex-usage-tracker-guide" / "assets" / "dashboard-calls.png").exists() assert (asset_dir / "dashboard.js").exists() assert (asset_dir / "dashboard_call_investigator.js").exists() @@ -699,6 +1015,42 @@ def test_dashboard_payload_contract_includes_analysis_metadata(tmp_path: Path) - } <= set(row) +def test_dashboard_payload_uses_persisted_call_origin_without_source_scan( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + codex_home = _make_codex_home(tmp_path) + db_path = tmp_path / "usage.sqlite3" + refresh_usage_index(codex_home=codex_home, db_path=db_path) + poison_source = tmp_path / "poison-source.jsonl" + poison_source.write_text("{this is not valid json}\n" * 1000, encoding="utf-8") + with connect(db_path) as conn: + conn.execute( + """ + UPDATE usage_events + SET source_file = ? + WHERE call_initiator = 'user' + """, + (str(poison_source),), + ) + + original_open = Path.open + + def fail_source_open(self: Path, *args: object, **kwargs: object) -> object: + if self == poison_source: + raise AssertionError("dashboard_payload must not read source JSONL") + return original_open(self, *args, **kwargs) + + monkeypatch.setattr(Path, "open", fail_source_open) + + payload = dashboard_payload(db_path=db_path) + rows = payload["rows"] + by_initiator = {row["call_initiator"]: row for row in rows} + + assert by_initiator["user"]["call_initiator_reason"] == "user_message" + assert by_initiator["user"]["call_initiator_confidence"] == "high" + + def test_dashboard_payload_and_csv_privacy_mode_redact_project_metadata(tmp_path: Path) -> None: codex_home = _make_codex_home(tmp_path) db_path = tmp_path / "usage.sqlite3" @@ -778,6 +1130,7 @@ def test_dashboard_server_usage_api_refreshes_aggregate_rows(tmp_path: Path) -> timeout=5, ) as response: dashboard_cache_control = response.headers.get("Cache-Control") + dashboard_html = response.read().decode("utf-8") refresh_without_token = _http_error_json( f"http://127.0.0.1:{server.server_port}/api/usage?refresh=1&limit=2" ) @@ -801,6 +1154,11 @@ def test_dashboard_server_usage_api_refreshes_aggregate_rows(tmp_path: Path) -> timeout=5, ) as response: offset_payload = json.loads(response.read().decode("utf-8")) + with urllib.request.urlopen( # noqa: S310 - local test server only + f"http://127.0.0.1:{server.server_port}/api/usage?shell=1&limit=all", + timeout=5, + ) as response: + shell_payload = json.loads(response.read().decode("utf-8")) forbidden_origin = _http_error_json( f"http://127.0.0.1:{server.server_port}/api/usage", headers={"Origin": "http://example.test"}, @@ -812,6 +1170,15 @@ def test_dashboard_server_usage_api_refreshes_aggregate_rows(tmp_path: Path) -> assert refresh_without_token["status"] == 403 assert dashboard_cache_control == "no-store" + shell_raw_payload = dashboard_html.split( + '", 1)[0] + dashboard_shell_payload = json.loads(shell_raw_payload) + assert 'data-dashboard-shell="true"' in dashboard_html + assert dashboard_shell_payload["shell_boot"] is True + assert dashboard_shell_payload["rows"] == [] + assert dashboard_shell_payload["summary"]["visible_calls"] == 0 assert limited_payload["refresh_result"]["parsed_events"] == 4 assert limited_payload["refresh_result"]["skipped_events"] == 0 assert limited_payload["refresh_result"]["parser_diagnostics"] == {} @@ -834,6 +1201,14 @@ def test_dashboard_server_usage_api_refreshes_aggregate_rows(tmp_path: Path) -> assert all_payload["has_more"] is False assert all_payload["limit_label"] == "All" assert len(offset_payload["rows"]) == 2 + assert shell_payload["shell_boot"] is True + assert shell_payload["rows"] == [] + assert shell_payload["summary"]["visible_calls"] == 4 + assert shell_payload["summary"]["total_tokens"] == sum( + row["total_tokens"] for row in all_payload["rows"] + ) + assert shell_payload["limit"] is None + assert shell_payload["loaded_row_count"] == 0 assert offset_payload["loaded_row_count"] == 2 assert offset_payload["total_available_rows"] == 4 assert offset_payload["limit"] == 2 @@ -869,6 +1244,7 @@ def test_dashboard_history_scope_excludes_archived_rows_by_default(tmp_path: Pat all_history_payload = dashboard_payload(db_path=db_path, limit=0, include_archived=True) active_rows = query_dashboard_events(db_path=db_path, limit=0, include_archived=False) all_rows = query_dashboard_events(db_path=db_path, limit=0, include_archived=True) + archived_rows = [row for row in all_rows if "/archived_sessions/" in row["source_file"]] assert refresh_result.parsed_events == 5 assert active_payload["include_archived"] is False @@ -885,7 +1261,22 @@ def test_dashboard_history_scope_excludes_archived_rows_by_default(tmp_path: Pat assert len(active_rows) == 4 assert len(all_rows) == 5 assert not any("/archived_sessions/" in row["source_file"] for row in active_rows) - assert any("/archived_sessions/" in row["source_file"] for row in all_rows) + assert len(archived_rows) == 1 + assert archived_rows[0]["is_archived"] == 1 + assert all(row["is_archived"] == 0 for row in active_rows) + + with connect(db_path) as conn: + conn.execute("UPDATE usage_events SET is_archived = 0") + active_rows_after_migrated_flag_reset = query_dashboard_events( + db_path=db_path, + limit=0, + include_archived=False, + ) + assert len(active_rows_after_migrated_flag_reset) == 4 + assert not any( + "/archived_sessions/" in row["source_file"] + for row in active_rows_after_migrated_flag_reset + ) def test_dashboard_server_usage_api_switches_history_scope(tmp_path: Path) -> None: @@ -960,6 +1351,377 @@ def test_dashboard_server_usage_api_switches_history_scope(tmp_path: Path) -> No ) +def test_dashboard_server_api_timing_diagnostics_are_opt_in_and_technical( + tmp_path: Path, +) -> None: + from codex_usage_tracker.server import _UsageDashboardHandler + + codex_home = _make_codex_home(tmp_path) + db_path = tmp_path / "usage.sqlite3" + pricing_path = _write_pricing(tmp_path / "pricing.json") + handler = partial( + _UsageDashboardHandler, + directory=str(tmp_path), + db_path=db_path, + pricing_path=pricing_path, + allowance_path=tmp_path / "allowance.json", + thresholds_path=tmp_path / "thresholds.json", + projects_path=tmp_path / "projects.json", + limit=5000, + since=None, + codex_home=codex_home, + include_archived=False, + dashboard_name="dashboard.html", + context_chars=2000, + api_token="test-token", + context_api_enabled=True, + refresh_lock=threading.Lock(), + ) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + base_url = f"http://127.0.0.1:{server.server_port}" + with urllib.request.urlopen( # noqa: S310 - local test server only + urllib.request.Request( + f"{base_url}/api/usage?refresh=1&limit=2&diagnostics=true", + headers={"X-Codex-Usage-Token": "test-token"}, + ), + timeout=5, + ) as response: + usage_diagnostics_payload = json.loads(response.read().decode("utf-8")) + with urllib.request.urlopen( # noqa: S310 - local test server only + f"{base_url}/api/usage?limit=2", + timeout=5, + ) as response: + usage_default_payload = json.loads(response.read().decode("utf-8")) + with urllib.request.urlopen( # noqa: S310 - local test server only + f"{base_url}/api/usage?limit=2&diagnostics=false", + timeout=5, + ) as response: + usage_false_payload = json.loads(response.read().decode("utf-8")) + + record_id = usage_diagnostics_payload["rows"][0]["record_id"] + with urllib.request.urlopen( # noqa: S310 - local test server only + urllib.request.Request( + f"{base_url}/api/context?record_id={record_id}&diagnostics=true", + headers={"X-Codex-Usage-Token": "test-token"}, + ), + timeout=5, + ) as response: + context_diagnostics_payload = json.loads(response.read().decode("utf-8")) + with urllib.request.urlopen( # noqa: S310 - local test server only + urllib.request.Request( + f"{base_url}/api/context?record_id={record_id}", + headers={"X-Codex-Usage-Token": "test-token"}, + ), + timeout=5, + ) as response: + context_default_payload = json.loads(response.read().decode("utf-8")) + with urllib.request.urlopen( # noqa: S310 - local test server only + urllib.request.Request( + f"{base_url}/api/context?record_id={record_id}&diagnostics=false", + headers={"X-Codex-Usage-Token": "test-token"}, + ), + timeout=5, + ) as response: + context_false_payload = json.loads(response.read().decode("utf-8")) + with urllib.request.urlopen( # noqa: S310 - local test server only + urllib.request.Request( + f"{base_url}/api/context?record_id={record_id}&mode=full", + headers={"X-Codex-Usage-Token": "test-token"}, + ), + timeout=5, + ) as response: + context_full_payload = json.loads(response.read().decode("utf-8")) + invalid_context_mode = _http_error_json( + f"{base_url}/api/context?record_id={record_id}&mode=slow", + headers={"X-Codex-Usage-Token": "test-token"}, + ) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + assert "diagnostics" not in usage_default_payload + assert "diagnostics" not in usage_false_payload + usage_diagnostics = usage_diagnostics_payload["diagnostics"] + assert usage_diagnostics["refresh_ms"] >= 0 + assert usage_diagnostics["dashboard_payload_ms"] >= 0 + assert usage_diagnostics["json_bytes"] > 0 + assert usage_diagnostics["rows_returned"] == 2 + assert usage_diagnostics["include_archived"] is False + assert usage_diagnostics["limit"] == 2 + assert usage_diagnostics["offset"] == 0 + usage_diagnostics_text = json.dumps(usage_diagnostics) + assert "SECRET RAW PROMPT" not in usage_diagnostics_text + assert "source_file" not in usage_diagnostics_text + + assert "diagnostics" not in context_default_payload + assert "diagnostics" not in context_false_payload + assert context_default_payload["context_mode"] == "quick" + assert context_default_payload["serialized_evidence"]["deferred_buckets"] is True + assert context_full_payload["context_mode"] == "full" + assert context_full_payload["serialized_evidence"]["deferred_buckets"] is False + assert invalid_context_mode["status"] == 400 + context_diagnostics = context_diagnostics_payload["diagnostics"] + assert context_diagnostics["db_lookup_ms"] >= 0 + assert context_diagnostics["source_scan_ms"] >= 0 + assert context_diagnostics["serialized_estimate_ms"] >= 0 + assert context_diagnostics["source_file_bytes"] > 0 + assert context_diagnostics["source_line_number"] > 0 + assert context_diagnostics["entries_before_limit"] >= context_diagnostics["entries_returned"] + assert context_diagnostics["json_bytes"] > 0 + context_diagnostics_text = json.dumps(context_diagnostics) + assert "SECRET RAW PROMPT" not in context_diagnostics_text + assert str(codex_home) not in context_diagnostics_text + assert ".jsonl" not in context_diagnostics_text + + +def test_dashboard_server_live_sql_api_slices_are_aggregate_only(tmp_path: Path) -> None: + from codex_usage_tracker.server import _UsageDashboardHandler + + codex_home = _make_codex_home(tmp_path) + db_path = tmp_path / "usage.sqlite3" + pricing_path = _write_pricing(tmp_path / "pricing.json") + refresh_usage_index(codex_home=codex_home, db_path=db_path) + handler = partial( + _UsageDashboardHandler, + directory=str(tmp_path), + db_path=db_path, + pricing_path=pricing_path, + allowance_path=tmp_path / "allowance.json", + thresholds_path=tmp_path / "thresholds.json", + projects_path=tmp_path / "projects.json", + limit=5000, + since=None, + codex_home=codex_home, + include_archived=False, + dashboard_name="dashboard.html", + context_chars=2000, + api_token="test-token", + context_api_enabled=True, + refresh_lock=threading.Lock(), + ) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + base_url = f"http://127.0.0.1:{server.server_port}" + + status_payload = _read_json(f"{base_url}/api/status") + calls_payload = _read_json( + f"{base_url}/api/calls?limit=2&sort=tokens&direction=desc&q=Codex" + ) + offset_payload = _read_json( + f"{base_url}/api/calls?limit=1&offset=1&sort=tokens&direction=desc&q=Codex" + ) + record_id = calls_payload["rows"][0]["record_id"] + call_payload = _read_json(f"{base_url}/api/call?record_id={record_id}") + threads_payload = _read_json(f"{base_url}/api/threads?limit=2&sort=tokens") + thread_key = threads_payload["rows"][0]["thread_key"] + thread_calls_payload = _read_json( + f"{base_url}/api/thread-calls?thread_key={urllib.parse.quote(thread_key)}&limit=2" + ) + summary_payload = _read_json(f"{base_url}/api/summary?group_by=model&limit=5") + recommendations_payload = _read_json(f"{base_url}/api/recommendations?limit=5") + invalid_sort = _http_error_json(f"{base_url}/api/calls?sort=not-a-sort") + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + assert status_payload["schema"] == "codex-usage-tracker-status-v1" + _assert_contract(status_payload) + assert "rows" not in status_payload + assert status_payload["row_counts"]["scoped_rows"] == 4 + assert status_payload["max_event_timestamp"] + + assert calls_payload["schema"] == "codex-usage-tracker-calls-v1" + _assert_contract(calls_payload) + assert calls_payload["row_count"] == 2 + assert calls_payload["total_matched_rows"] >= 2 + assert calls_payload["has_more"] is True + assert calls_payload["next_offset"] == 2 + assert calls_payload["rows"][0]["total_tokens"] >= calls_payload["rows"][1]["total_tokens"] + assert offset_payload["rows"][0]["record_id"] != calls_payload["rows"][0]["record_id"] + + assert call_payload["schema"] == "codex-usage-tracker-call-v1" + _assert_contract(call_payload) + assert call_payload["record"]["record_id"] == record_id + assert call_payload["raw_context_included"] is False + + assert threads_payload["schema"] == "codex-usage-tracker-threads-v1" + _assert_contract(threads_payload) + assert threads_payload["row_count"] >= 1 + assert "total_tokens" in threads_payload["rows"][0] + + assert thread_calls_payload["schema"] == "codex-usage-tracker-thread-calls-v1" + _assert_contract(thread_calls_payload) + assert thread_calls_payload["thread_key"] == thread_key + assert thread_calls_payload["row_count"] >= 1 + + assert summary_payload["schema"] == "codex-usage-tracker-summary-v1" + _assert_contract(summary_payload) + assert summary_payload["group_by"] == "model" + assert recommendations_payload["schema"] == "codex-usage-tracker-recommendations-v1" + _assert_contract(recommendations_payload) + assert invalid_sort["status"] == 400 + assert "sort must be one of" in invalid_sort["payload"]["error"] + + combined_payload = json.dumps( + [ + status_payload, + calls_payload, + call_payload, + threads_payload, + thread_calls_payload, + summary_payload, + recommendations_payload, + ] + ) + assert "SECRET RAW PROMPT" not in combined_payload + assert "raw_context_included\": true" not in combined_payload + + +def test_dashboard_server_serves_lightweight_call_investigator_boot_html( + tmp_path: Path, +) -> None: + from codex_usage_tracker.server import _UsageDashboardHandler + + codex_home = _make_codex_home(tmp_path) + db_path = tmp_path / "usage.sqlite3" + pricing_path = _write_pricing(tmp_path / "pricing.json") + refresh_usage_index(codex_home=codex_home, db_path=db_path) + dashboard_path = tmp_path / "dashboard.html" + generate_dashboard( + db_path=db_path, + output_path=dashboard_path, + pricing_path=pricing_path, + api_token="test-token", + context_api_enabled=True, + include_archived=True, + ) + record_id = query_dashboard_events(db_path=db_path, include_archived=True)[0]["record_id"] + static_html = dashboard_path.read_text(encoding="utf-8") + handler = partial( + _UsageDashboardHandler, + directory=str(tmp_path), + db_path=db_path, + pricing_path=pricing_path, + allowance_path=tmp_path / "allowance.json", + thresholds_path=tmp_path / "thresholds.json", + projects_path=tmp_path / "projects.json", + limit=5000, + since=None, + codex_home=codex_home, + include_archived=False, + dashboard_name="dashboard.html", + dashboard_path=dashboard_path, + context_chars=2000, + api_token="test-token", + context_api_enabled=True, + refresh_lock=threading.Lock(), + ) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + url = ( + f"http://127.0.0.1:{server.server_port}/dashboard.html" + f"?view=call&record={urllib.parse.quote(record_id)}&history=all" + ) + with urllib.request.urlopen(url, timeout=5) as response: # noqa: S310 - local test server only + html = response.read().decode("utf-8") + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + raw_payload = html.split('", + 1, + )[0] + payload = json.loads(raw_payload) + assert 'data-active-view="call"' in html + assert 'data-investigator-boot="true"' in html + assert 'data-dashboard-shell="true"' in html + assert payload["investigator_boot"] is True + assert payload["rows"] == [] + assert payload["include_archived"] is True + assert payload["api_token"] == "test-token" + assert len(html) < len(static_html) + + +def test_dashboard_call_api_returns_adjacent_records_for_investigator( + tmp_path: Path, +) -> None: + from codex_usage_tracker.server import _UsageDashboardHandler + + db_path = tmp_path / "usage.sqlite3" + events = [ + _usage_event( + record_id="a1", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T18:58:20Z", + cumulative_total_tokens=100, + ), + _usage_event( + record_id="a2", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T18:58:30Z", + cumulative_total_tokens=250, + ), + _usage_event( + record_id="a3", + session_id="session-a", + thread_key="thread:Alpha", + event_timestamp="2026-05-17T18:58:40Z", + cumulative_total_tokens=400, + ), + ] + upsert_usage_events(events, db_path=db_path) + handler = partial( + _UsageDashboardHandler, + directory=str(tmp_path), + db_path=db_path, + pricing_path=_write_pricing(tmp_path / "pricing.json"), + allowance_path=tmp_path / "allowance.json", + thresholds_path=tmp_path / "thresholds.json", + projects_path=tmp_path / "projects.json", + limit=5000, + since=None, + codex_home=tmp_path / ".codex", + include_archived=False, + dashboard_name="dashboard.html", + context_chars=2000, + api_token="test-token", + context_api_enabled=True, + refresh_lock=threading.Lock(), + ) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + payload = _read_json( + f"http://127.0.0.1:{server.server_port}/api/call?record_id=a2", + headers={"X-Codex-Usage-Token": "test-token"}, + ) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + assert payload["schema"] == "codex-usage-tracker-call-v1" + assert payload["record"]["record_id"] == "a2" + assert payload["previous_record"]["record_id"] == "a1" + assert payload["next_record"]["record_id"] == "a3" + assert [row["record_id"] for row in payload["adjacent_records"]] == ["a1", "a2", "a3"] + assert payload["raw_context_included"] is False + + def test_dashboard_server_opens_only_token_protected_investigator_urls( tmp_path: Path, monkeypatch ) -> None: @@ -1165,6 +1927,7 @@ def test_dashboard_server_can_enable_context_api_at_runtime(tmp_path: Path) -> N assert usage_payload["context_api_enabled"] is True assert context_payload["loaded_on_demand"] is True assert context_payload["raw_context_persisted"] is False + assert context_payload["context_mode"] == "quick" assert unlimited_context_payload["omitted"]["max_chars"] == 0 assert unlimited_context_payload["omitted"]["max_entries"] == 0 assert unlimited_context_payload["omitted"]["older_entries"] == 0 @@ -1191,6 +1954,7 @@ def test_context_loads_raw_log_only_on_demand(tmp_path: Path) -> None: assert context["loaded_on_demand"] is True assert context["raw_context_persisted"] is False + assert context["context_mode"] == "quick" assert context["visible_char_count"] > 0 assert context["visible_token_estimate"] > 0 assert context["visible_token_estimator"] in { @@ -1200,30 +1964,33 @@ def test_context_loads_raw_log_only_on_demand(tmp_path: Path) -> None: } serialized = context["serialized_evidence"] assert serialized["available"] is True - assert serialized["scope"] == "selected_turn_raw_jsonl" + assert serialized["scope"] == "selected_turn_raw_jsonl_fast_estimate" assert serialized["upper_bound"] is True assert serialized["raw_text_returned"] is False + assert serialized["deferred"] is True + assert serialized["deferred_buckets"] is True + assert serialized["reason"] == "full_serialized_analysis_not_requested" assert serialized["raw_line_count"] >= len(context["entries"]) assert serialized["raw_json_char_count"] > context["visible_char_count"] assert serialized["raw_json_token_estimate"] >= context["visible_token_estimate"] - assert serialized["token_estimator"] in { + assert serialized["token_estimator"] == "chars_per_4_fallback" + assert serialized["buckets"] == [] + full_context = load_call_context(rows[0]["record_id"], db_path=db_path, mode="full") + full_serialized = full_context["serialized_evidence"] + assert full_context["context_mode"] == "full" + assert full_serialized["scope"] == "selected_turn_raw_jsonl" + assert full_serialized["deferred"] is False + assert full_serialized["deferred_buckets"] is False + assert full_serialized["token_estimator"] in { "chars_per_4_fallback", "tiktoken:o200k_base", "tiktoken:cl100k_base", } - serialized_bucket_keys = {bucket["key"] for bucket in serialized["buckets"]} + serialized_bucket_keys = {bucket["key"] for bucket in full_serialized["buckets"]} assert "encrypted_reasoning_state" in serialized_bucket_keys assert "local_goal_metadata" in serialized_bucket_keys - anchors = context["call_anchors"] - assert anchors["available"] is True - assert anchors["scope"] == "selected_call_reasoning_output" - assert anchors["before_message"]["role"] == "user" - assert anchors["reasoning_output"]["role"] == "reasoning" - assert "SECRET RAW PROMPT" in anchors["before_message"]["text"] - assert "Reasoning summary" in anchors["reasoning_output"]["text"] - assert "AFTER SELECTED CALL" not in json.dumps(anchors) - assert "AGENTS.md instructions" not in anchors["before_message"]["text"] - assert "sk" + "-proj-" not in anchors["before_message"]["text"] + assert "call_anchors" not in context + assert "thread_anchors" not in context assert context["omitted"]["total_entries"] >= context["omitted"]["returned_entries"] assert "SECRET RAW PROMPT" in context_text assert "sk" + "-proj-" not in context_text @@ -1298,6 +2065,40 @@ def test_context_loads_raw_log_only_on_demand(tmp_path: Path) -> None: assert len(unlimited_context["entries"]) > len(limited_context["entries"]) +def test_context_loading_uses_one_source_scan_for_evidence_and_serialized_estimate( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + codex_home = _make_codex_home(tmp_path) + db_path = tmp_path / "usage.sqlite3" + refresh_usage_index(codex_home=codex_home, db_path=db_path) + row = query_session_usage(db_path=db_path, session_id=SESSION_ID)[0] + source_file = Path(str(row["source_file"])) + open_count = 0 + real_open = Path.open + + def counting_open(path: Path, *args: object, **kwargs: object): + nonlocal open_count + if path == source_file: + open_count += 1 + return real_open(path, *args, **kwargs) + + monkeypatch.setattr(Path, "open", counting_open) + + context = load_call_context(row["record_id"], db_path=db_path, diagnostics=True) + + assert open_count == 1 + assert any(entry["label"] == "message / user" for entry in context["entries"]) + assert context["include_tool_output"] is False + assert context["context_mode"] == "quick" + assert context["serialized_evidence"]["available"] is True + assert context["serialized_evidence"]["deferred_buckets"] is True + assert "call_anchors" not in context + assert "thread_anchors" not in context + assert context["diagnostics"]["source_scan_ms"] >= 0 + assert context["diagnostics"]["serialized_estimate_ms"] >= 0 + + def test_context_carries_incoming_compaction_history_into_selected_turn(tmp_path: Path) -> None: session_id = "019e37d5-f19f-7e4d-84cb-508941431111" codex_home = tmp_path / ".codex" @@ -1832,6 +2633,58 @@ def _write_archived_log(codex_home: Path) -> Path: return archived_log_path +def _usage_event( + *, + record_id: str, + session_id: str, + thread_key: str, + event_timestamp: str, + cumulative_total_tokens: int, +) -> UsageEvent: + return UsageEvent( + record_id=record_id, + session_id=session_id, + thread_name=thread_key.removeprefix("thread:"), + session_updated_at="2026-05-17T18:58:27Z", + event_timestamp=event_timestamp, + source_file=f"/tmp/synthetic/{record_id}.jsonl", + line_number=1, + turn_id=f"turn-{record_id}", + turn_timestamp=event_timestamp, + cwd="/tmp/project", + model="gpt-5.5", + effort="high", + current_date="2026-05-17", + timezone="UTC", + call_initiator="user", + call_initiator_reason="user_message", + call_initiator_confidence="high", + is_archived=0, + thread_key=thread_key, + thread_call_index=None, + previous_record_id=None, + next_record_id=None, + thread_source="user", + subagent_type=None, + agent_role=None, + agent_nickname=None, + parent_session_id=None, + parent_thread_name=None, + parent_session_updated_at=None, + model_context_window=200000, + input_tokens=100, + cached_input_tokens=20, + output_tokens=10, + reasoning_output_tokens=5, + total_tokens=110, + cumulative_input_tokens=cumulative_total_tokens - 10, + cumulative_cached_input_tokens=20, + cumulative_output_tokens=10, + cumulative_reasoning_output_tokens=5, + cumulative_total_tokens=cumulative_total_tokens, + ) + + def _write_pricing(path: Path) -> Path: path.write_text( json.dumps( @@ -1854,6 +2707,12 @@ def _assert_contract(payload: object) -> None: assert validate_json_payload_contract(payload) == [] +def _read_json(url: str, headers: dict[str, str] | None = None) -> dict[str, object]: + request = urllib.request.Request(url, headers=headers or {}) + with urllib.request.urlopen(request, timeout=5) as response: # noqa: S310 - local test server only + return json.loads(response.read().decode("utf-8")) + + def _http_error_json(url: str, headers: dict[str, str] | None = None) -> dict[str, object]: request = urllib.request.Request(url, headers=headers or {}) try: diff --git a/tests/test_store_migrations.py b/tests/test_store_migrations.py index aab05ef..b751712 100644 --- a/tests/test_store_migrations.py +++ b/tests/test_store_migrations.py @@ -39,14 +39,22 @@ def test_init_db_migrates_legacy_aggregate_table_without_data_loss(tmp_path: Pat assert len(rows) == 1 assert rows[0]["record_id"] == "legacy-record" assert rows[0]["source_file"] == "/tmp/synthetic-session.jsonl" + assert rows[0]["call_initiator"] is None + assert rows[0]["call_initiator_reason"] is None + assert rows[0]["call_initiator_confidence"] is None + assert rows[0]["is_archived"] == 0 + assert rows[0]["thread_key"] is None + assert rows[0]["thread_call_index"] is None + assert rows[0]["previous_record_id"] is None + assert rows[0]["next_record_id"] is None assert rows[0]["thread_source"] is None assert rows[0]["parent_thread_name"] is None assert rows[0]["model_context_window"] is None assert metadata["parsed_events"] == "legacy" assert metadata["parser_invalid_integer"] == "2" - assert state["schema_version"] == 2 + assert state["schema_version"] == 7 assert state["checksum_matches"] is True - assert [row["version"] for row in state["migrations"]] == [1, 2] + assert [row["version"] for row in state["migrations"]] == [1, 2, 3, 4, 5, 6, 7] def test_refresh_is_idempotent_after_legacy_migration(tmp_path: Path) -> None: @@ -63,14 +71,17 @@ def test_refresh_is_idempotent_after_legacy_migration(tmp_path: Path) -> None: metadata = refresh_metadata(db_path) assert first.parsed_events == 1 - assert second.parsed_events == 1 + assert second.parsed_events == 0 + assert second.inserted_or_updated_events == 0 assert first_count == 2 assert second_count == 2 assert legacy_rows[0]["record_id"] == "legacy-record" assert new_rows[0]["thread_name"] == "Synthetic migration thread" - assert metadata["schema_version"] == "2" - assert metadata["parsed_events"] == "1" - assert metadata["inserted_or_updated_events"] == "1" + assert metadata["schema_version"] == "7" + assert metadata["parsed_events"] == "0" + assert metadata["inserted_or_updated_events"] == "0" + assert metadata["parsed_source_files"] == "0" + assert metadata["skipped_source_files"] == "1" def test_csv_export_keeps_current_columns_after_legacy_migration(tmp_path: Path) -> None: @@ -85,6 +96,14 @@ def test_csv_export_keeps_current_columns_after_legacy_migration(tmp_path: Path) assert exported == 1 assert rows[0]["record_id"] == "legacy-record" + assert rows[0]["call_initiator"] == "" + assert rows[0]["call_initiator_reason"] == "" + assert rows[0]["call_initiator_confidence"] == "" + assert rows[0]["is_archived"] == "0" + assert rows[0]["thread_key"] == "" + assert rows[0]["thread_call_index"] == "" + assert rows[0]["previous_record_id"] == "" + assert rows[0]["next_record_id"] == "" assert list(rows[0]) == EVENT_COLUMNS