refactor(quota): canonical <entity>_<action> taxonomy for quota events
The quota-attribution action keys were inconsistent (mixed verbs, ad-hoc names, English-only labels). Introduce a QuotaAction constants holder (one source of truth), rename every attribution call site to it, and add migration 0020 to rename historical quota_events.action values. The display label now resolves from i18n (quotaActions.<key>, EN/HU/DE) instead of a hard-coded English map. Scheduler job ids and progress phase labels are a separate namespace and are left untouched.
This commit is contained in:
parent
e3d3596c7c
commit
dc70a978ed
12 changed files with 151 additions and 33 deletions
51
backend/alembic/versions/0020_rename_quota_actions.py
Normal file
51
backend/alembic/versions/0020_rename_quota_actions.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""rename quota_events.action to the canonical <entity>_<action> taxonomy
|
||||||
|
|
||||||
|
Revision ID: 0020_rename_quota_actions
|
||||||
|
Revises: 0019_user_yt_channel_id
|
||||||
|
Create Date: 2026-06-19
|
||||||
|
|
||||||
|
The quota-attribution action keys were inconsistent (mixed verbs, ad-hoc names). Rename the
|
||||||
|
stored historical values to the canonical scheme defined in app.quota.QuotaAction:
|
||||||
|
``<entity>_<action>``, snake_case, explicit pull/push. Pure data migration on quota_events;
|
||||||
|
fully reversible. (Scheduler job ids and progress phase labels are a separate namespace and
|
||||||
|
are NOT touched.)
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0020_rename_quota_actions"
|
||||||
|
down_revision: Union[str, None] = "0019_user_yt_channel_id"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
# old action value -> new canonical key (mirrors app.quota.QuotaAction).
|
||||||
|
RENAMES: list[tuple[str, str]] = [
|
||||||
|
("sync_subscriptions", "subscriptions_pull"),
|
||||||
|
("subscription_resync", "subscriptions_resync"),
|
||||||
|
("playlist_sync", "playlists_pull"),
|
||||||
|
("playlist_push", "playlists_push"),
|
||||||
|
("playlist_delete", "playlists_delete"),
|
||||||
|
("backfill_recent", "videos_backfill_recent"),
|
||||||
|
("backfill_deep", "videos_backfill_full"),
|
||||||
|
("enrich", "videos_enrich"),
|
||||||
|
("video_lookup", "videos_lookup"),
|
||||||
|
("discovery", "channels_discover"),
|
||||||
|
("subscribe", "channels_subscribe"),
|
||||||
|
("unsubscribe", "channels_unsubscribe"),
|
||||||
|
("maintenance", "maintenance_revalidate"),
|
||||||
|
("api", "other"),
|
||||||
|
]
|
||||||
|
|
||||||
|
_SQL = sa.text("UPDATE quota_events SET action = :to WHERE action = :frm")
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
for frm, to in RENAMES:
|
||||||
|
op.execute(_SQL.bindparams(frm=frm, to=to))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
for frm, to in RENAMES:
|
||||||
|
op.execute(_SQL.bindparams(frm=to, to=frm))
|
||||||
|
|
@ -17,6 +17,32 @@ from app.models import ApiQuotaUsage, QuotaEvent
|
||||||
|
|
||||||
_PACIFIC = ZoneInfo("America/Los_Angeles")
|
_PACIFIC = ZoneInfo("America/Los_Angeles")
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaAction:
|
||||||
|
"""Canonical quota-attribution action keys (one source of truth).
|
||||||
|
|
||||||
|
Naming scheme: ``<entity>_<action>``, all snake_case, explicit pull/push direction.
|
||||||
|
The stored value is this machine key; the user-facing label is resolved from i18n
|
||||||
|
(frontend ``quotaActions.<key>``). Distinct from scheduler job ids and progress phase
|
||||||
|
labels — do not conflate.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SUBSCRIPTIONS_PULL = "subscriptions_pull"
|
||||||
|
SUBSCRIPTIONS_RESYNC = "subscriptions_resync"
|
||||||
|
PLAYLISTS_PULL = "playlists_pull"
|
||||||
|
PLAYLISTS_PUSH = "playlists_push"
|
||||||
|
PLAYLISTS_DELETE = "playlists_delete"
|
||||||
|
VIDEOS_BACKFILL_RECENT = "videos_backfill_recent"
|
||||||
|
VIDEOS_BACKFILL_FULL = "videos_backfill_full"
|
||||||
|
VIDEOS_ENRICH = "videos_enrich"
|
||||||
|
VIDEOS_LOOKUP = "videos_lookup"
|
||||||
|
CHANNELS_DISCOVER = "channels_discover"
|
||||||
|
CHANNELS_SUBSCRIBE = "channels_subscribe"
|
||||||
|
CHANNELS_UNSUBSCRIBE = "channels_unsubscribe"
|
||||||
|
MAINTENANCE_REVALIDATE = "maintenance_revalidate"
|
||||||
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
# Request/job-scoped attribution for quota spend: who triggered it and what kind of work.
|
# Request/job-scoped attribution for quota spend: who triggered it and what kind of work.
|
||||||
# Set at entry points (route handlers, scheduler jobs) via attribute(); read by
|
# Set at entry points (route handlers, scheduler jobs) via attribute(); read by
|
||||||
# record_usage. Default = background/system, generic action.
|
# record_usage. Default = background/system, generic action.
|
||||||
|
|
@ -24,7 +50,7 @@ _actor_id: contextvars.ContextVar[int | None] = contextvars.ContextVar(
|
||||||
"quota_actor_id", default=None
|
"quota_actor_id", default=None
|
||||||
)
|
)
|
||||||
_action: contextvars.ContextVar[str] = contextvars.ContextVar(
|
_action: contextvars.ContextVar[str] = contextvars.ContextVar(
|
||||||
"quota_action", default="api"
|
"quota_action", default=QuotaAction.OTHER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ def discover_channels(
|
||||||
# to yourself. Lazy + cached; needs OAuth, so demo / token-less users just skip it.
|
# to yourself. Lazy + cached; needs OAuth, so demo / token-less users just skip it.
|
||||||
if user.yt_channel_id is None and user.token is not None and not user.is_demo:
|
if user.yt_channel_id is None and user.token is not None and not user.is_demo:
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "discovery"), YouTubeClient(db, user) as yt:
|
with quota.attribute(user.id, quota.QuotaAction.CHANNELS_DISCOVER), YouTubeClient(db, user) as yt:
|
||||||
own = yt.get_my_channel_id()
|
own = yt.get_my_channel_id()
|
||||||
if own:
|
if own:
|
||||||
user.yt_channel_id = own
|
user.yt_channel_id = own
|
||||||
|
|
@ -181,7 +181,7 @@ def discover_channels(
|
||||||
need = [ch.id for ch, _, _ in rows if ch.details_synced_at is None][:DISCOVERY_ENRICH_CAP]
|
need = [ch.id for ch, _, _ in rows if ch.details_synced_at is None][:DISCOVERY_ENRICH_CAP]
|
||||||
if need:
|
if need:
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "discovery"), YouTubeClient(db, user) as yt:
|
with quota.attribute(user.id, quota.QuotaAction.CHANNELS_DISCOVER), YouTubeClient(db, user) as yt:
|
||||||
apply_channel_details(db, yt.get_channels(need))
|
apply_channel_details(db, yt.get_channels(need))
|
||||||
db.commit()
|
db.commit()
|
||||||
rows = _discovery_rows(db, user)
|
rows = _discovery_rows(db, user)
|
||||||
|
|
@ -243,7 +243,7 @@ def update_channel(
|
||||||
# fetched its recent uploads yet, do that one channel now (cheap) instead of waiting for
|
# fetched its recent uploads yet, do that one channel now (cheap) instead of waiting for
|
||||||
# the scheduler. Deep paging still follows on the scheduler's next run (recent-then-deep).
|
# the scheduler. Deep paging still follows on the scheduler's next run (recent-then-deep).
|
||||||
if deep_turned_on and channel is not None and channel.recent_synced_at is None:
|
if deep_turned_on and channel is not None and channel.recent_synced_at is None:
|
||||||
with quota.attribute(user.id, "backfill_recent"):
|
with quota.attribute(user.id, quota.QuotaAction.VIDEOS_BACKFILL_RECENT):
|
||||||
run_recent_backfill(db, [channel], max_channels=1)
|
run_recent_backfill(db, [channel], max_channels=1)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
@ -274,7 +274,7 @@ def reset_backfill(
|
||||||
channel.recent_synced_at = None
|
channel.recent_synced_at = None
|
||||||
sub.deep_requested = True
|
sub.deep_requested = True
|
||||||
db.commit()
|
db.commit()
|
||||||
with quota.attribute(user.id, "backfill_recent"):
|
with quota.attribute(user.id, quota.QuotaAction.VIDEOS_BACKFILL_RECENT):
|
||||||
run_recent_backfill(db, [channel], max_channels=1)
|
run_recent_backfill(db, [channel], max_channels=1)
|
||||||
return {"id": channel_id, "reset": True}
|
return {"id": channel_id, "reset": True}
|
||||||
|
|
||||||
|
|
@ -306,7 +306,7 @@ def subscribe(
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
raise HTTPException(status_code=409, detail="Already subscribed to this channel.")
|
raise HTTPException(status_code=409, detail="Already subscribed to this channel.")
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "subscribe"), YouTubeClient(db, user) as yt:
|
with quota.attribute(user.id, quota.QuotaAction.CHANNELS_SUBSCRIBE), YouTubeClient(db, user) as yt:
|
||||||
yt_sub_id = yt.insert_subscription(channel_id)
|
yt_sub_id = yt.insert_subscription(channel_id)
|
||||||
# Enrich a stub channel (title/thumbnail/subscriber count) so it shows up
|
# Enrich a stub channel (title/thumbnail/subscriber count) so it shows up
|
||||||
# properly right away; no video pull here.
|
# properly right away; no video pull here.
|
||||||
|
|
@ -356,7 +356,7 @@ def unsubscribe(
|
||||||
detail="No YouTube subscription id on record — sync subscriptions first.",
|
detail="No YouTube subscription id on record — sync subscriptions first.",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "unsubscribe"), YouTubeClient(db, user) as yt:
|
with quota.attribute(user.id, quota.QuotaAction.CHANNELS_UNSUBSCRIBE), YouTubeClient(db, user) as yt:
|
||||||
yt.delete_subscription(sub.yt_subscription_id)
|
yt.delete_subscription(sub.yt_subscription_id)
|
||||||
except YouTubeError as exc:
|
except YouTubeError as exc:
|
||||||
# 422 (a real, explainable error → speaking modal), not 502 which the client treats as
|
# 422 (a real, explainable error → speaking modal), not 502 which the client treats as
|
||||||
|
|
|
||||||
|
|
@ -549,7 +549,7 @@ def get_video_detail(
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "video_lookup"), YouTubeClient(db, user) as yt:
|
with quota.attribute(user.id, quota.QuotaAction.VIDEOS_LOOKUP), YouTubeClient(db, user) as yt:
|
||||||
items = yt.get_videos([video_id])
|
items = yt.get_videos([video_id])
|
||||||
except YouTubeError as exc:
|
except YouTubeError as exc:
|
||||||
raise HTTPException(status_code=422, detail=f"YouTube lookup failed: {exc}")
|
raise HTTPException(status_code=422, detail=f"YouTube lookup failed: {exc}")
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ def sync_youtube(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Connect YouTube (read access) to sync your playlists.",
|
detail="Connect YouTube (read access) to sync your playlists.",
|
||||||
)
|
)
|
||||||
with quota.attribute(user.id, "playlist_sync"):
|
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PULL):
|
||||||
return sync_user_playlists(db, user)
|
return sync_user_playlists(db, user)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -266,7 +266,7 @@ def revert_youtube(
|
||||||
status_code=403, detail="Connect YouTube (read access) to sync your playlists."
|
status_code=403, detail="Connect YouTube (read access) to sync your playlists."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "playlist_sync"):
|
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PULL):
|
||||||
return repull_playlist(db, user, pl)
|
return repull_playlist(db, user, pl)
|
||||||
except YouTubeError:
|
except YouTubeError:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
|
|
@ -298,7 +298,7 @@ def push_plan(
|
||||||
status_code=403, detail="Enable YouTube editing in Settings first."
|
status_code=403, detail="Enable YouTube editing in Settings first."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "playlist_push"):
|
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PUSH):
|
||||||
plan = plan_push(db, user, pl)
|
plan = plan_push(db, user, pl)
|
||||||
except YouTubeError:
|
except YouTubeError:
|
||||||
raise HTTPException(status_code=422, detail="Couldn't read the playlist on YouTube.")
|
raise HTTPException(status_code=422, detail="Couldn't read the playlist on YouTube.")
|
||||||
|
|
@ -322,7 +322,7 @@ def push(
|
||||||
status_code=403, detail="Enable YouTube editing in Settings first."
|
status_code=403, detail="Enable YouTube editing in Settings first."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "playlist_push"):
|
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PUSH):
|
||||||
plan = plan_push(db, user, pl)
|
plan = plan_push(db, user, pl)
|
||||||
if plan["units_estimate"] > quota.remaining_today(db):
|
if plan["units_estimate"] > quota.remaining_today(db):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|
@ -418,7 +418,7 @@ def delete_playlist(
|
||||||
status_code=403, detail="Enable YouTube editing in Settings first."
|
status_code=403, detail="Enable YouTube editing in Settings first."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "playlist_delete"):
|
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_DELETE):
|
||||||
with YouTubeClient(db, user) as yt:
|
with YouTubeClient(db, user) as yt:
|
||||||
yt.delete_playlist(pl.yt_playlist_id)
|
yt.delete_playlist(pl.yt_playlist_id)
|
||||||
except YouTubeError:
|
except YouTubeError:
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ def sync_subscriptions(
|
||||||
detail="Connect your YouTube account (read access) to import subscriptions.",
|
detail="Connect your YouTube account (read access) to import subscriptions.",
|
||||||
)
|
)
|
||||||
before = quota.units_used_today(db)
|
before = quota.units_used_today(db)
|
||||||
with quota.attribute(user.id, "sync_subscriptions"):
|
with quota.attribute(user.id, quota.QuotaAction.SUBSCRIPTIONS_PULL):
|
||||||
result = import_subscriptions(db, user)
|
result = import_subscriptions(db, user)
|
||||||
result["quota_used_estimate"] = quota.units_used_today(db) - before
|
result["quota_used_estimate"] = quota.units_used_today(db) - before
|
||||||
result["quota_remaining_today"] = quota.remaining_today(db)
|
result["quota_remaining_today"] = quota.remaining_today(db)
|
||||||
|
|
@ -63,7 +63,7 @@ def sync_backfill(
|
||||||
max_channels: int = 25,
|
max_channels: int = 25,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
before = quota.units_used_today(db)
|
before = quota.units_used_today(db)
|
||||||
with quota.attribute(user.id, "backfill_recent"):
|
with quota.attribute(user.id, quota.QuotaAction.VIDEOS_BACKFILL_RECENT):
|
||||||
result = run_recent_backfill(db, _user_channels(db, user), max_channels=max_channels)
|
result = run_recent_backfill(db, _user_channels(db, user), max_channels=max_channels)
|
||||||
result["quota_used_estimate"] = quota.units_used_today(db) - before
|
result["quota_used_estimate"] = quota.units_used_today(db) - before
|
||||||
return result
|
return result
|
||||||
|
|
@ -74,7 +74,7 @@ def sync_enrich(
|
||||||
user: User = Depends(require_human), db: Session = Depends(get_db)
|
user: User = Depends(require_human), db: Session = Depends(get_db)
|
||||||
) -> dict:
|
) -> dict:
|
||||||
before = quota.units_used_today(db)
|
before = quota.units_used_today(db)
|
||||||
with quota.attribute(user.id, "enrich"):
|
with quota.attribute(user.id, quota.QuotaAction.VIDEOS_ENRICH):
|
||||||
enriched = run_enrich(db)
|
enriched = run_enrich(db)
|
||||||
return {
|
return {
|
||||||
"enriched": enriched,
|
"enriched": enriched,
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ def _rss_job() -> None:
|
||||||
|
|
||||||
def _enrich_job() -> None:
|
def _enrich_job() -> None:
|
||||||
def work(db):
|
def work(db):
|
||||||
with quota.attribute(None, "enrich"):
|
with quota.attribute(None, quota.QuotaAction.VIDEOS_ENRICH):
|
||||||
return run_enrich(db)
|
return run_enrich(db)
|
||||||
|
|
||||||
_job("enrich", work)
|
_job("enrich", work)
|
||||||
|
|
@ -214,9 +214,9 @@ def _backfill_job() -> None:
|
||||||
# Recent-first for not-yet-synced channels, then deep backfill for the rest. All
|
# Recent-first for not-yet-synced channels, then deep backfill for the rest. All
|
||||||
# background spend is attributed to the system (no actor), split by action.
|
# background spend is attributed to the system (no actor), split by action.
|
||||||
def work(db):
|
def work(db):
|
||||||
with quota.attribute(None, "backfill_recent"):
|
with quota.attribute(None, quota.QuotaAction.VIDEOS_BACKFILL_RECENT):
|
||||||
recent = run_recent_backfill(db, max_channels=25)
|
recent = run_recent_backfill(db, max_channels=25)
|
||||||
with quota.attribute(None, "backfill_deep"):
|
with quota.attribute(None, quota.QuotaAction.VIDEOS_BACKFILL_FULL):
|
||||||
deep = run_deep_backfill(db, max_channels=10)
|
deep = run_deep_backfill(db, max_channels=10)
|
||||||
return {"recent": recent, "deep": deep}
|
return {"recent": recent, "deep": deep}
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ def _shorts_job() -> None:
|
||||||
|
|
||||||
def _subscriptions_job() -> None:
|
def _subscriptions_job() -> None:
|
||||||
def work(db):
|
def work(db):
|
||||||
with quota.attribute(None, "subscription_resync"):
|
with quota.attribute(None, quota.QuotaAction.SUBSCRIPTIONS_RESYNC):
|
||||||
return run_subscription_resync(db)
|
return run_subscription_resync(db)
|
||||||
|
|
||||||
_job("subscriptions", work)
|
_job("subscriptions", work)
|
||||||
|
|
@ -247,7 +247,7 @@ def _playlist_sync_job() -> None:
|
||||||
|
|
||||||
def _maintenance_job() -> None:
|
def _maintenance_job() -> None:
|
||||||
def work(db):
|
def work(db):
|
||||||
with quota.attribute(None, "maintenance"):
|
with quota.attribute(None, quota.QuotaAction.MAINTENANCE_REVALIDATE):
|
||||||
return run_maintenance(db)
|
return run_maintenance(db)
|
||||||
|
|
||||||
_job("maintenance", work)
|
_job("maintenance", work)
|
||||||
|
|
|
||||||
|
|
@ -387,7 +387,7 @@ def sync_all_playlists(db: Session) -> dict:
|
||||||
progress.report(i, len(users), "playlist_sync")
|
progress.report(i, len(users), "playlist_sync")
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
with quota.attribute(user.id, "playlist_sync"):
|
with quota.attribute(user.id, quota.QuotaAction.PLAYLISTS_PULL):
|
||||||
total += sync_user_playlists(db, user).get("synced", 0)
|
total += sync_user_playlists(db, user).get("synced", 0)
|
||||||
except YouTubeError:
|
except YouTubeError:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
|
|
|
||||||
16
frontend/src/i18n/locales/de/quotaActions.json
Normal file
16
frontend/src/i18n/locales/de/quotaActions.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"subscriptions_pull": "Abonnements einlesen",
|
||||||
|
"subscriptions_resync": "Automatische Abo-Resynchronisierung",
|
||||||
|
"playlists_pull": "Playlists einlesen",
|
||||||
|
"playlists_push": "Playlists hochladen",
|
||||||
|
"playlists_delete": "Playlist löschen",
|
||||||
|
"videos_backfill_recent": "Aktuelle Uploads laden",
|
||||||
|
"videos_backfill_full": "Vollständigen Verlauf laden",
|
||||||
|
"videos_enrich": "Video-Anreicherung",
|
||||||
|
"videos_lookup": "Video-Abfrage",
|
||||||
|
"channels_discover": "Kanal-Entdeckung",
|
||||||
|
"channels_subscribe": "Abonnieren",
|
||||||
|
"channels_unsubscribe": "Abo beenden",
|
||||||
|
"maintenance_revalidate": "Wartungs-Neuvalidierung",
|
||||||
|
"other": "Sonstiges"
|
||||||
|
}
|
||||||
16
frontend/src/i18n/locales/en/quotaActions.json
Normal file
16
frontend/src/i18n/locales/en/quotaActions.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"subscriptions_pull": "Read subscriptions",
|
||||||
|
"subscriptions_resync": "Auto subscription resync",
|
||||||
|
"playlists_pull": "Read playlists",
|
||||||
|
"playlists_push": "Push playlists",
|
||||||
|
"playlists_delete": "Delete playlist",
|
||||||
|
"videos_backfill_recent": "Recent backfill",
|
||||||
|
"videos_backfill_full": "Full-history backfill",
|
||||||
|
"videos_enrich": "Video enrichment",
|
||||||
|
"videos_lookup": "Video lookup",
|
||||||
|
"channels_discover": "Channel discovery",
|
||||||
|
"channels_subscribe": "Subscribe",
|
||||||
|
"channels_unsubscribe": "Unsubscribe",
|
||||||
|
"maintenance_revalidate": "Maintenance re-validation",
|
||||||
|
"other": "Other"
|
||||||
|
}
|
||||||
16
frontend/src/i18n/locales/hu/quotaActions.json
Normal file
16
frontend/src/i18n/locales/hu/quotaActions.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"subscriptions_pull": "Feliratkozások beolvasása",
|
||||||
|
"subscriptions_resync": "Automatikus feliratkozás-újraszinkron",
|
||||||
|
"playlists_pull": "Lejátszási listák beolvasása",
|
||||||
|
"playlists_push": "Lejátszási listák feltöltése",
|
||||||
|
"playlists_delete": "Lejátszási lista törlése",
|
||||||
|
"videos_backfill_recent": "Friss feltöltések betöltése",
|
||||||
|
"videos_backfill_full": "Teljes előzmény betöltése",
|
||||||
|
"videos_enrich": "Videó-gazdagítás",
|
||||||
|
"videos_lookup": "Videó-lekérdezés",
|
||||||
|
"channels_discover": "Csatorna-felfedezés",
|
||||||
|
"channels_subscribe": "Feliratkozás",
|
||||||
|
"channels_unsubscribe": "Leiratkozás",
|
||||||
|
"maintenance_revalidate": "Karbantartó újraellenőrzés",
|
||||||
|
"other": "Egyéb"
|
||||||
|
}
|
||||||
|
|
@ -44,18 +44,11 @@ export function formatDuration(sec: number | null): string {
|
||||||
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
|
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUOTA_ACTION_LABELS: Record<string, string> = {
|
// Human label for a canonical quota-action key (see backend app.quota.QuotaAction). Resolved
|
||||||
sync_subscriptions: "Sync subscriptions",
|
// from i18n (quotaActions.<key>) so it's translated in all UI languages; falls back to the raw
|
||||||
backfill_recent: "Recent backfill",
|
// key for any unmapped/legacy value.
|
||||||
backfill_deep: "Full-history backfill",
|
|
||||||
enrich: "Enrichment",
|
|
||||||
unsubscribe: "Unsubscribe",
|
|
||||||
subscription_resync: "Auto subscription resync",
|
|
||||||
api: "Other",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function quotaActionLabel(action: string): string {
|
export function quotaActionLabel(action: string): string {
|
||||||
return QUOTA_ACTION_LABELS[action] ?? action;
|
return i18n.t(`quotaActions.${action}`, { defaultValue: action });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Coarse, human ETA for a remaining duration in seconds ("~3 hours", "~2 days"). */
|
/** Coarse, human ETA for a remaining duration in seconds ("~3 hours", "~2 days"). */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue