refactor: chia nốt theme.py, i18n.py, usage_tracker.py — Gamma hết file vượt 400 dòng

Ba file dữ liệu cuối cùng của Gamma còn trên ngưỡng CASAN Check 2.

i18n.py  3075 -> 94
    Dict STRINGS 3.000 dòng cắt thành 10 cụm theo đúng mốc phân đoạn có sẵn
    trong file (mỗi mốc là một màn/hộp thoại), cụm nào quá dài thì cắt tiếp ở
    ranh giới khoá. i18n.py giờ chỉ gộp lại và giữ 4 hàm set_language/
    get_language/tr/on_language_changed.

    Kiểm bằng cách so với bản gốc lấy từ git: 1437 mục / 1431 khoá duy nhất
    (bản gốc vốn có 6 khoá lặp), sau khi chia vẫn 1431, KHÔNG thiếu khoá nào,
    KHÔNG thừa khoá nào, KHÔNG giá trị nào lệch. Thứ tự gộp giữ nguyên nên
    quy tắc "khoá trùng thì bản sau thắng" không đổi.

theme.py  907 -> 130
    theme_palettes.py 328  hai bảng màu Tối/Sáng + lớp Palette
    theme_qss.py      198  nửa vỏ (reset + shell)
    theme_qss_controls.py 307  nửa điều khiển (nút, ô nhập, tab, badge)

    Khuôn QSS 470 dòng cắt đôi đúng mốc `/* ---- surfaces */` của chính nó.
    Đã đối chiếu: stylesheet('dark') ra đúng 24762 ký tự y như trước — khớp
    từng byte, không phải "trông có vẻ giống".

core/usage_tracker.py  536 -> 307
    usage_cost.py      101  bảng giá, quy đổi token sang tiền, định dạng
    usage_periods.py   144  gộp theo ngày/tuần/tháng/quý, chuỗi vẽ biểu đồ
    usage_ai_report.py  56  dựng câu nhắc cho AI phân tích

HAI LẦN TỰ CẮT HỎNG, ĐỀU CÙNG MỘT GỐC
--------------------------------------
1. Cắt theo m.lineno mà quên dòng @decorator phía trên -> @dataclass của
   Palette bị bỏ lại mồ côi, "Palette() takes no arguments".
2. Đọc số dòng từ AST GỐC trong khi danh sách dòng đã bị cắt -> lần bóc thứ
   hai dùng toạ độ cũ và cắt vào giữa một chữ ký hàm.

Cả hai lộ ngay vì mỗi script tự parse lại sau khi ghi. Bài học đã áp vào cả
ba lần chia: parse lại sau mỗi lần cắt, và luôn tính cả decorator.

KẾT QUẢ CASAN CHECK 2
---------------------
    Nam       0 file vượt 400   (trước: 4, tổng 5.898 dòng)
    Hiệp      0                 (trước: 1)
    Lâm       0                 (trước: 1)
    file mới  0                 (61 file dưới presentation/ application/
                                 domain/ infrastructure/ — chưa cái nào vượt)

Gamma sạch. 23 file còn vượt đều thuộc team khác (chat_panel.py 1802,
folder_tab.py 1589, structure_graph_view.py 1034...) — cần báo lên sớm chứ
đừng để tới hạn 30/08 mới lộ.

714 test xanh. 24/24 checker qua. CASAN Check 1 sạch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-26 10:57:01 +09:00
co-authored by Claude Opus 5
parent 0e00bf3c2f
commit 6c68417103
19 changed files with 4296 additions and 4026 deletions
+101
View File
@@ -0,0 +1,101 @@
"""Bảng giá và quy đổi token thành tiền — R09-T02.
Tách khỏi ``usage_tracker.py``: ghi nhận mức dùng và tính tiền là hai việc
khác nhau. Bảng giá đổi theo nhà cung cấp, cách ghi nhận thì không.
"""
from __future__ import annotations
import json
import threading
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from ..config import CONFIG_DIR
from . import model_pricing as mp
DEFAULT_PRICING = {
"price_per_mtok_in_usd": 0.5, # USD per 1M input tokens (flat fallback rate)
"price_per_mtok_out_usd": 1.5, # USD per 1M output tokens
"price_per_mtok_cache_usd": 0.1, # USD per 1M cached tokens
"currency": "USD", # display currency: USD | VND | JPY
"usd_to_vnd": 25000.0,
"usd_to_jpy": 150.0,
# Per-model price table (USD / 1M tokens): {model: {"in","out","cache"}}.
# Events whose model has an entry are costed with ITS rates; everything
# else falls back to the flat price_per_mtok_* rates above. Edited in the
# Monitoring Overview's pricing table.
"model_prices": {},
# Reference URL of the price list the table was filled from (set in
# Settings; shown as a link beside the table — informational only, the
# app never scrapes it).
"pricing_url": "",
}
_CURRENCY_FMT = {"USD": ("$", 4), "VND": ("₫", 0), "JPY": ("¥", 1)}
SUPPORTED_CURRENCIES = tuple(_CURRENCY_FMT)
def cost_usd(summary: Dict[str, Any], pricing: Dict[str, Any]) -> Dict[str, float]:
p = {**DEFAULT_PRICING, **(pricing or {})}
return {
"in": summary.get("in", 0) / 1e6 * float(p["price_per_mtok_in_usd"]),
"out": summary.get("out", 0) / 1e6 * float(p["price_per_mtok_out_usd"]),
"cache": summary.get("cache", 0) / 1e6 * float(p["price_per_mtok_cache_usd"]),
}
def cost_usd_events(events: List[Dict[str, Any]], pricing: Dict[str, Any]) -> Dict[str, float]:
"""Per-bucket USD cost computed EVENT BY EVENT so the per-model price
table applies: an event whose ``model`` has an entry in
``pricing["model_prices"]`` is costed with that model's own rates; any
other event uses the flat ``price_per_mtok_*`` rates. With an empty
table this equals ``cost_usd(summarize(events), pricing)`` exactly."""
p = {**DEFAULT_PRICING, **(pricing or {})}
table = p.get("model_prices") or {}
flat = {"in": float(p["price_per_mtok_in_usd"]),
"out": float(p["price_per_mtok_out_usd"]),
"cache": float(p["price_per_mtok_cache_usd"])}
out = {"in": 0.0, "out": 0.0, "cache": 0.0}
for e in events:
rates = table.get(e.get("model", "")) or {}
for bucket in ("in", "out", "cache"):
try:
rate = float(rates.get(bucket, flat[bucket]))
except (TypeError, ValueError):
rate = flat[bucket]
out[bucket] += e.get(bucket, 0) / 1e6 * rate
return out
def format_cost(usd: float, pricing: Dict[str, Any], digits: Optional[int] = None) -> str:
"""Format a USD amount in the display currency. ``digits`` caps the number
of decimal places (e.g. ``digits=2`` for the Total cost / Budget cards, so
USD shows $1.23 not the default up-to-4 $1.2345) — never ADDS decimals to a
currency that uses fewer (VND stays whole, JPY one place)."""
p = {**DEFAULT_PRICING, **(pricing or {})}
cur = p.get("currency", "USD")
rate = {"USD": 1.0, "VND": float(p["usd_to_vnd"]), "JPY": float(p["usd_to_jpy"])}.get(cur, 1.0)
symbol, cur_digits = _CURRENCY_FMT.get(cur, ("$", 2))
if digits is not None:
cur_digits = min(cur_digits, digits)
value = usd * rate
return f"{symbol}{value:,.{cur_digits}f}"
def format_cost_compact(usd: float, pricing: Dict[str, Any]) -> str:
"""Compact cost format for the Dashboard chart's y-axis/endpoint labels —
always 2 decimals (not format_cost's up-to-4 for USD) and abbreviated with
K/M above 1,000/1,000,000, same convention as ``fmt_tokens``. The chart's
y-axis label box is narrow; the longer full-precision string used to
overflow it, visually clipping/obscuring the leading currency symbol."""
p = {**DEFAULT_PRICING, **(pricing or {})}
cur = p.get("currency", "USD")
rate = {"USD": 1.0, "VND": float(p["usd_to_vnd"]), "JPY": float(p["usd_to_jpy"])}.get(cur, 1.0)
symbol, _digits = _CURRENCY_FMT.get(cur, ("$", 2))
value = usd * rate
sign = "-" if value < 0 else ""
value = abs(value)
if value >= 1_000_000:
body = f"{value / 1_000_000:,.2f}M"
elif value >= 1_000:
body = f"{value / 1_000:,.2f}K"
else:
body = f"{value:,.2f}"
return f"{sign}{symbol}{body}"