diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 179cc92..7e85e97 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -465,8 +465,8 @@ export default function App() { notify({ level: "info", message: t("channels.notify.filteredByTag", { name }) }); }} /> - ) : page === "stats" && meQuery.data!.role === "admin" ? ( - + ) : page === "stats" ? ( + ) : page === "scheduler" && meQuery.data!.role === "admin" ? ( ) : page === "playlists" ? ( diff --git a/frontend/src/components/NavSidebar.tsx b/frontend/src/components/NavSidebar.tsx index cef12ec..e229011 100644 --- a/frontend/src/components/NavSidebar.tsx +++ b/frontend/src/components/NavSidebar.tsx @@ -133,13 +133,12 @@ export default function NavSidebar({ { page: "channels", icon: Tv, label: t("header.account.channels") }, { page: "playlists", icon: ListVideo, label: t("header.account.playlists") }, { page: "notifications", icon: Bell, label: t("inbox.navLabel"), badge: unread }, + // Per-user sync status + your own API usage (admins get an extra system-wide tab inside). + { page: "stats", icon: BarChart3, label: t("header.account.stats") }, ]; const systemItems: NavItem[] = me.role === "admin" - ? [ - { page: "stats", icon: BarChart3, label: t("header.account.stats") }, - { page: "scheduler", icon: Activity, label: t("header.account.scheduler") }, - ] + ? [{ page: "scheduler", icon: Activity, label: t("header.account.scheduler") }] : []; const rowBase = diff --git a/frontend/src/components/SettingsPanel.tsx b/frontend/src/components/SettingsPanel.tsx index 6e65456..c92ddbe 100644 --- a/frontend/src/components/SettingsPanel.tsx +++ b/frontend/src/components/SettingsPanel.tsx @@ -1,10 +1,9 @@ import { useCallback, useEffect, useState, useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Bell, Check, History, Monitor, Pause, Play, RefreshCw, RotateCcw, Save, Trash2, User, UserPlus, X } from "lucide-react"; +import { Bell, Check, Monitor, RotateCcw, Save, Trash2, User, UserPlus, X } from "lucide-react"; import { SCHEMES, type Scheme, type ThemePrefs } from "../lib/theme"; import { api, type Invite, type Me } from "../lib/api"; -import { formatEta, quotaActionLabel } from "../lib/format"; import Avatar from "./Avatar"; import { notify, type NotifSettings } from "../lib/notifications"; import Tooltip from "./Tooltip"; @@ -32,11 +31,10 @@ export interface PrefsController { saveState: PrefsSaveState; } -type TabId = "appearance" | "notifications" | "sync" | "account"; +type TabId = "appearance" | "notifications" | "account"; const TABS: { id: TabId; icon: typeof Monitor }[] = [ { id: "appearance", icon: Monitor }, { id: "notifications", icon: Bell }, - { id: "sync", icon: RefreshCw }, { id: "account", icon: User }, ]; @@ -98,7 +96,6 @@ export default function SettingsPanel({
{tab === "appearance" && } {tab === "notifications" && } - {tab === "sync" && } {tab === "account" && }
@@ -317,158 +314,6 @@ function Notifications({ prefs }: { prefs: PrefsController }) { ); } -function Sync({ me }: { me: Me }) { - const { t } = useTranslation(); - const qc = useQueryClient(); - const status = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus }); - const usage = useQuery({ queryKey: ["my-usage"], queryFn: api.myUsage }); - const s = status.data; - const syncSubs = useMutation({ - mutationFn: () => api.syncSubscriptions(), - onSuccess: (r: { subscriptions?: number }) => { - qc.invalidateQueries({ queryKey: ["my-status"] }); - qc.invalidateQueries({ queryKey: ["channels"] }); - notify({ - level: "success", - message: t("settings.sync.synced", { count: r.subscriptions ?? 0 }), - }); - }, - onError: () => notify({ level: "error", message: t("settings.sync.syncFailed") }), - }); - const pauseResume = useMutation({ - mutationFn: (paused: boolean) => (paused ? api.resumeSync() : api.pauseSync()), - onSuccess: () => qc.invalidateQueries({ queryKey: ["my-status"] }), - }); - const deepAll = useMutation({ - mutationFn: () => api.deepAll(true), - onSuccess: (r: { updated?: number }) => { - qc.invalidateQueries({ queryKey: ["my-status"] }); - qc.invalidateQueries({ queryKey: ["channels"] }); - notify({ - level: "success", - message: t("settings.sync.fullHistoryRequested", { count: r.updated ?? 0 }), - }); - }, - onError: () => notify({ level: "error", message: t("settings.sync.fullHistoryFailed") }), - }); - - return ( - <> -
- {s ? ( -
- - {s.channels_total} - - - {`${s.channels_recent_synced}/${s.channels_total}`} - - - {`${s.channels_deep_done}/${s.channels_deep_requested}`} - - {s.deep_pending_count > 0 && ( - - {formatEta(s.deep_eta_seconds)} - - )} - - {s.my_videos.toLocaleString()} - - - {s.quota_remaining_today.toLocaleString()} - -
- ) : ( -
{t("settings.sync.loading")}
- )} -
- -
- {usage.data ? ( - <> -
- - {usage.data.today.toLocaleString()} - - {usage.data.last_7d.toLocaleString()} - {usage.data.last_30d.toLocaleString()} - {usage.data.all_time.toLocaleString()} -
- {Object.keys(usage.data.by_action).length > 0 && ( -
-
{t("settings.sync.byAction")}
- {Object.entries(usage.data.by_action) - .sort((a, b) => b[1] - a[1]) - .map(([action, units]) => ( -
- {quotaActionLabel(action)} - {units.toLocaleString()} -
- ))} -
- )} - - ) : ( -
{t("settings.sync.noUsage")}
- )} -
- - {!me.is_demo && ( -
- - - - - - -
- )} - - {me.role === "admin" && s && ( -
- - - -
- )} - - ); -} - function AccessRow({ title, granted, diff --git a/frontend/src/components/Stats.tsx b/frontend/src/components/Stats.tsx index 46a776e..97a6109 100644 --- a/frontend/src/components/Stats.tsx +++ b/frontend/src/components/Stats.tsx @@ -1,24 +1,236 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useQuery } from "@tanstack/react-query"; -import { api, type AdminQuotaRow } from "../lib/api"; -import { quotaActionLabel } from "../lib/format"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { History, Pause, Play, RefreshCw } from "lucide-react"; +import { api, type AdminQuotaRow, type Me } from "../lib/api"; +import { formatEta, quotaActionLabel } from "../lib/format"; +import { notify } from "../lib/notifications"; +import Tooltip from "./Tooltip"; const RANGES = [7, 30, 90] as const; +const STATS_TAB_KEY = "siftlode.statsTab"; +type StatsTab = "overview" | "system"; -export default function Stats() { +// Usage & stats page. "Overview" is per-user (sync status + your own API usage + actions); +// "System" is the admin-only instance-wide quota dashboard + background-sync control. The +// per-user content used to live in Settings → Sync; it moved here so Settings stays config-only. +export default function Stats({ me }: { me: Me }) { const { t } = useTranslation(); + const isAdmin = me.role === "admin"; + const [tab, setTabState] = useState(() => { + const v = localStorage.getItem(STATS_TAB_KEY); + return v === "system" && isAdmin ? "system" : "overview"; + }); + const setTab = (id: StatsTab) => { + setTabState(id); + localStorage.setItem(STATS_TAB_KEY, id); + }; + + return ( +
+ {/* Only admins have a second (System) tab; everyone else just sees the Overview. */} + {isAdmin && ( +
+ {(["overview", "system"] as StatsTab[]).map((id) => ( + + ))} +
+ )} + + {tab === "system" && isAdmin ? : } +
+ ); +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+
{title}
+ {children} +
+ ); +} + +function Row({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { + return ( +
+ + + {label} + + + {children} +
+ ); +} + +// Per-user view: sync status + your own API usage + (for real accounts) the manual sync actions. +function Overview({ me }: { me: Me }) { + const { t } = useTranslation(); + const qc = useQueryClient(); + const status = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus }); + const usage = useQuery({ queryKey: ["my-usage"], queryFn: api.myUsage }); + const s = status.data; + const syncSubs = useMutation({ + mutationFn: () => api.syncSubscriptions(), + onSuccess: (r: { subscriptions?: number }) => { + qc.invalidateQueries({ queryKey: ["my-status"] }); + qc.invalidateQueries({ queryKey: ["channels"] }); + notify({ level: "success", message: t("stats.sync.synced", { count: r.subscriptions ?? 0 }) }); + }, + onError: () => notify({ level: "error", message: t("stats.sync.syncFailed") }), + }); + const deepAll = useMutation({ + mutationFn: () => api.deepAll(true), + onSuccess: (r: { updated?: number }) => { + qc.invalidateQueries({ queryKey: ["my-status"] }); + qc.invalidateQueries({ queryKey: ["channels"] }); + notify({ + level: "success", + message: t("stats.sync.fullHistoryRequested", { count: r.updated ?? 0 }), + }); + }, + onError: () => notify({ level: "error", message: t("stats.sync.fullHistoryFailed") }), + }); + + return ( + <> +
+ {s ? ( +
+ + {s.channels_total} + + + {`${s.channels_recent_synced}/${s.channels_total}`} + + + {`${s.channels_deep_done}/${s.channels_deep_requested}`} + + {s.deep_pending_count > 0 && ( + + {formatEta(s.deep_eta_seconds)} + + )} + + {s.my_videos.toLocaleString()} + + + {s.quota_remaining_today.toLocaleString()} + +
+ ) : ( +
{t("stats.sync.loading")}
+ )} +
+ +
+ {usage.data ? ( + <> +
+ + {usage.data.today.toLocaleString()} + + {usage.data.last_7d.toLocaleString()} + {usage.data.last_30d.toLocaleString()} + {usage.data.all_time.toLocaleString()} +
+ {Object.keys(usage.data.by_action).length > 0 && ( +
+
{t("stats.sync.byAction")}
+ {Object.entries(usage.data.by_action) + .sort((a, b) => b[1] - a[1]) + .map(([action, units]) => ( +
+ {quotaActionLabel(action)} + {units.toLocaleString()} +
+ ))} +
+ )} + + ) : ( +
{t("stats.sync.noUsage")}
+ )} +
+ + {!me.is_demo && ( +
+ + + + + + +
+ )} + + ); +} + +// Admin-only view: instance-wide quota dashboard + the global background-sync pause toggle. +function SystemStats() { + const { t } = useTranslation(); + const qc = useQueryClient(); const [days, setDays] = useState(30); - const q = useQuery({ - queryKey: ["admin-quota", days], - queryFn: () => api.adminQuota(days), + const q = useQuery({ queryKey: ["admin-quota", days], queryFn: () => api.adminQuota(days) }); + const status = useQuery({ queryKey: ["my-status"], queryFn: api.myStatus }); + const pauseResume = useMutation({ + mutationFn: (paused: boolean) => (paused ? api.resumeSync() : api.pauseSync()), + onSuccess: () => qc.invalidateQueries({ queryKey: ["my-status"] }), }); const data = q.data; const maxDaily = Math.max(1, ...(data?.daily ?? []).map((d) => d.total)); const grandTotal = (data?.rows ?? []).reduce((s, r) => s + r.total, 0); + const s = status.data; return ( -
+ <> + {s && ( +
+ + + +
+ )} +

{t("stats.title")}

@@ -53,25 +265,17 @@ export default function Stats() { {/* Daily totals (instance-wide, Pacific days) */}
- {t("stats.dailyTotal", { - count: data.range_days, - units: grandTotal.toLocaleString(), - })} + {t("stats.dailyTotal", { count: data.range_days, units: grandTotal.toLocaleString() })}
{data.daily.length === 0 ? (
{t("stats.noUsageInRange")}
) : (
{data.daily.map((d) => ( - // Each day gets a full-height track so the column is always visible - // (even on near-zero days); the accent fill renders the value on top.
)} -
+ ); } @@ -106,16 +310,11 @@ function UserRow({ row, max }: { row: AdminQuotaRow; max: number }) { const isSystem = row.user_id === null; return (
- {/* Proportion bar */}
diff --git a/frontend/src/i18n/locales/de/settings.json b/frontend/src/i18n/locales/de/settings.json index 0b9677f..b610ea8 100644 --- a/frontend/src/i18n/locales/de/settings.json +++ b/frontend/src/i18n/locales/de/settings.json @@ -3,7 +3,6 @@ "tabs": { "appearance": "Darstellung", "notifications": "Benachrichtigungen", - "sync": "Synchronisierung", "account": "Konto" }, "save": { @@ -42,43 +41,6 @@ "testTitle": "Testbenachrichtigung", "testMessage": "So sieht eine Benachrichtigung aus." }, - "sync": { - "myStatus": "Mein Synchronisierungsstatus", - "channels": "Kanäle", - "channelsHint": "Wie viele Kanäle du abonniert hast.", - "recentSynced": "Neueste synchronisiert", - "recentSyncedHint": "Kanäle, deren neueste Uploads (~letztes Jahr) bereits in der lokalen Datenbank sind und so in deinem Feed erscheinen.", - "fullHistory": "Vollständiger Verlauf", - "fullHistoryHint": "Kanäle, deren gesamter Katalog abgerufen wurde, von denen, für die du den vollständigen Verlauf angefordert hast (pro Kanal auf der Kanäle-Seite aktivierbar).", - "fullHistoryEta": "Vollständiger Verlauf – Restzeit", - "fullHistoryEtaHint": "Grobe Schätzung, um das Nachladen des vollständigen Verlaufs von {{count}} ausstehenden Kanal/Kanälen abzuschließen, basierend auf dem gemeinsamen Tageskontingent.", - "myVideos": "Meine Videos", - "myVideosHint": "Gesamtzahl der dir verfügbaren Videos über deine abonnierten Kanäle.", - "quotaLeft": "Heute verbleibendes Kontingent", - "quotaLeftHint": "Heute verbleibendes gemeinsames YouTube-API-Budget (wird um Mitternacht US-Pazifik zurückgesetzt). Das Nachladen ordnet sich diesem unter.", - "loading": "Wird geladen…", - "apiUsage": "Deine API-Nutzung", - "today": "Heute", - "todayHint": "YouTube-API-Einheiten, die deine eigenen Aktionen heute verbraucht haben (wird um Mitternacht US-Pazifik zurückgesetzt). Die Hintergrund-Synchronisierung zählt hier nicht.", - "last7d": "Letzte 7 Tage", - "last30d": "Letzte 30 Tage", - "allTime": "Gesamt", - "byAction": "Nach Aktion (30 Tage)", - "noUsage": "Noch keine Nutzung.", - "actions": "Aktionen", - "syncSubscriptions": "Abos synchronisieren", - "syncSubscriptionsHint": "Importiert deine YouTube-Abos erneut und holt neu gefolgte Kanäle hinzu.", - "backfillEverything": "Alles nachladen", - "backfillEverythingHint": "Fordert das Nachladen des vollständigen Katalogs für jeden Kanal an, den du abonniert hast. Ältere Videos und die vollständige Suche werden gefüllt, soweit das gemeinsame Tageskontingent es zulässt.", - "synced": "{{count}} Abos synchronisiert", - "syncFailed": "Synchronisierung fehlgeschlagen", - "fullHistoryRequested": "Vollständiger Verlauf für {{count}} Kanäle angefordert", - "fullHistoryFailed": "Vollständiger Verlauf konnte nicht angefordert werden", - "admin": "Admin", - "adminPauseHint": "Pausiert oder setzt die Hintergrund-Synchronisierung für die gesamte App fort (alle Nutzer).", - "resumeBackgroundSync": "Hintergrund-Synchronisierung fortsetzen", - "pauseBackgroundSync": "Hintergrund-Synchronisierung pausieren" - }, "account": { "title": "Konto", "youtubeAccess": "YouTube-Zugriff", diff --git a/frontend/src/i18n/locales/de/stats.json b/frontend/src/i18n/locales/de/stats.json index 6f90ffe..3abf5ce 100644 --- a/frontend/src/i18n/locales/de/stats.json +++ b/frontend/src/i18n/locales/de/stats.json @@ -1,4 +1,8 @@ { + "tabs": { + "overview": "Übersicht", + "system": "System" + }, "title": "API-Kontingentnutzung", "rangeDays": "{{count}} T", "introBefore": "YouTube-Data-API-Einheiten, zugeordnet danach, wer die Nutzung ausgelöst hat. ", @@ -9,5 +13,42 @@ "dailyTotal": "Tagessumme ({{count}} T · {{units}} Einheiten)", "noUsageInRange": "Keine Nutzung in diesem Zeitraum.", "dailyTooltip": "{{day}}: {{units}} Einheiten", - "system": "System (Hintergrund)" + "system": "System (Hintergrund)", + "sync": { + "myStatus": "Mein Synchronisierungsstatus", + "channels": "Kanäle", + "channelsHint": "Wie viele Kanäle du abonniert hast.", + "recentSynced": "Neueste synchronisiert", + "recentSyncedHint": "Kanäle, deren neueste Uploads (~letztes Jahr) bereits in der lokalen Datenbank sind und so in deinem Feed erscheinen.", + "fullHistory": "Vollständiger Verlauf", + "fullHistoryHint": "Kanäle, deren gesamter Katalog abgerufen wurde, von denen, für die du den vollständigen Verlauf angefordert hast (pro Kanal auf der Kanäle-Seite aktivierbar).", + "fullHistoryEta": "Vollständiger Verlauf – Restzeit", + "fullHistoryEtaHint": "Grobe Schätzung, um das Nachladen des vollständigen Verlaufs von {{count}} ausstehenden Kanal/Kanälen abzuschließen, basierend auf dem gemeinsamen Tageskontingent.", + "myVideos": "Meine Videos", + "myVideosHint": "Gesamtzahl der dir verfügbaren Videos über deine abonnierten Kanäle.", + "quotaLeft": "Heute verbleibendes Kontingent", + "quotaLeftHint": "Heute verbleibendes gemeinsames YouTube-API-Budget (wird um Mitternacht US-Pazifik zurückgesetzt). Das Nachladen ordnet sich diesem unter.", + "loading": "Wird geladen…", + "apiUsage": "Deine API-Nutzung", + "today": "Heute", + "todayHint": "YouTube-API-Einheiten, die deine eigenen Aktionen heute verbraucht haben (wird um Mitternacht US-Pazifik zurückgesetzt). Die Hintergrund-Synchronisierung zählt hier nicht.", + "last7d": "Letzte 7 Tage", + "last30d": "Letzte 30 Tage", + "allTime": "Gesamt", + "byAction": "Nach Aktion (30 Tage)", + "noUsage": "Noch keine Nutzung.", + "actions": "Aktionen", + "syncSubscriptions": "Abos synchronisieren", + "syncSubscriptionsHint": "Importiert deine YouTube-Abos erneut und holt neu gefolgte Kanäle hinzu.", + "backfillEverything": "Alles nachladen", + "backfillEverythingHint": "Fordert das Nachladen des vollständigen Katalogs für jeden Kanal an, den du abonniert hast. Ältere Videos und die vollständige Suche werden gefüllt, soweit das gemeinsame Tageskontingent es zulässt.", + "synced": "{{count}} Abos synchronisiert", + "syncFailed": "Synchronisierung fehlgeschlagen", + "fullHistoryRequested": "Vollständiger Verlauf für {{count}} Kanäle angefordert", + "fullHistoryFailed": "Vollständiger Verlauf konnte nicht angefordert werden", + "admin": "Admin", + "adminPauseHint": "Pausiert oder setzt die Hintergrund-Synchronisierung für die gesamte App fort (alle Nutzer).", + "resumeBackgroundSync": "Hintergrund-Synchronisierung fortsetzen", + "pauseBackgroundSync": "Hintergrund-Synchronisierung pausieren" + } } diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index b7e837f..7302b80 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -3,7 +3,6 @@ "tabs": { "appearance": "Appearance", "notifications": "Notifications", - "sync": "Sync", "account": "Account" }, "save": { @@ -42,43 +41,6 @@ "testTitle": "Test notification", "testMessage": "This is what a notification looks like." }, - "sync": { - "myStatus": "My sync status", - "channels": "Channels", - "channelsHint": "How many channels you're subscribed to.", - "recentSynced": "Recent synced", - "recentSyncedHint": "Channels whose recent uploads (~last year) are already in the local database, so they show in your feed.", - "fullHistory": "Full history", - "fullHistoryHint": "Channels whose entire back-catalog has been fetched, out of the ones you've requested full history for (opt in per channel on the Channels page).", - "fullHistoryEta": "Full history ETA", - "fullHistoryEtaHint": "Rough estimate to finish full-history backfill of {{count}} pending channel(s), based on the shared daily quota.", - "myVideos": "My videos", - "myVideosHint": "Total videos available to you across your subscribed channels.", - "quotaLeft": "Quota left today", - "quotaLeftHint": "Shared YouTube API budget remaining today (resets midnight US Pacific). Backfill yields to it.", - "loading": "Loading…", - "apiUsage": "Your API usage", - "today": "Today", - "todayHint": "YouTube API units your own actions used today (resets midnight US Pacific). Background sync isn't counted here.", - "last7d": "Last 7 days", - "last30d": "Last 30 days", - "allTime": "All time", - "byAction": "By action (30d)", - "noUsage": "No usage yet.", - "actions": "Actions", - "syncSubscriptions": "Sync subscriptions", - "syncSubscriptionsHint": "Re-import your YouTube subscriptions and pull in any newly-followed channels.", - "backfillEverything": "Backfill everything", - "backfillEverythingHint": "Request full back-catalog backfill for every channel you're subscribed to. Older videos + complete search fill in as the shared daily quota allows.", - "synced": "Synced {{count}} subscriptions", - "syncFailed": "Sync failed", - "fullHistoryRequested": "Full history requested for {{count}} channels", - "fullHistoryFailed": "Couldn't request full history", - "admin": "Admin", - "adminPauseHint": "Pause or resume the background sync for the whole app (all users).", - "resumeBackgroundSync": "Resume background sync", - "pauseBackgroundSync": "Pause background sync" - }, "account": { "title": "Account", "youtubeAccess": "YouTube access", diff --git a/frontend/src/i18n/locales/en/stats.json b/frontend/src/i18n/locales/en/stats.json index b8a0efc..1131e73 100644 --- a/frontend/src/i18n/locales/en/stats.json +++ b/frontend/src/i18n/locales/en/stats.json @@ -1,4 +1,8 @@ { + "tabs": { + "overview": "Overview", + "system": "System" + }, "title": "API quota usage", "rangeDays": "{{count}}d", "introBefore": "YouTube Data API units attributed by who triggered the spend. ", @@ -9,5 +13,42 @@ "dailyTotal": "Daily total ({{count}}d · {{units}} units)", "noUsageInRange": "No usage in this range.", "dailyTooltip": "{{day}}: {{units}} units", - "system": "System (background)" + "system": "System (background)", + "sync": { + "myStatus": "My sync status", + "channels": "Channels", + "channelsHint": "How many channels you're subscribed to.", + "recentSynced": "Recent synced", + "recentSyncedHint": "Channels whose recent uploads (~last year) are already in the local database, so they show in your feed.", + "fullHistory": "Full history", + "fullHistoryHint": "Channels whose entire back-catalog has been fetched, out of the ones you've requested full history for (opt in per channel on the Channels page).", + "fullHistoryEta": "Full history ETA", + "fullHistoryEtaHint": "Rough estimate to finish full-history backfill of {{count}} pending channel(s), based on the shared daily quota.", + "myVideos": "My videos", + "myVideosHint": "Total videos available to you across your subscribed channels.", + "quotaLeft": "Quota left today", + "quotaLeftHint": "Shared YouTube API budget remaining today (resets midnight US Pacific). Backfill yields to it.", + "loading": "Loading…", + "apiUsage": "Your API usage", + "today": "Today", + "todayHint": "YouTube API units your own actions used today (resets midnight US Pacific). Background sync isn't counted here.", + "last7d": "Last 7 days", + "last30d": "Last 30 days", + "allTime": "All time", + "byAction": "By action (30d)", + "noUsage": "No usage yet.", + "actions": "Actions", + "syncSubscriptions": "Sync subscriptions", + "syncSubscriptionsHint": "Re-import your YouTube subscriptions and pull in any newly-followed channels.", + "backfillEverything": "Backfill everything", + "backfillEverythingHint": "Request full back-catalog backfill for every channel you're subscribed to. Older videos + complete search fill in as the shared daily quota allows.", + "synced": "Synced {{count}} subscriptions", + "syncFailed": "Sync failed", + "fullHistoryRequested": "Full history requested for {{count}} channels", + "fullHistoryFailed": "Couldn't request full history", + "admin": "Admin", + "adminPauseHint": "Pause or resume the background sync for the whole app (all users).", + "resumeBackgroundSync": "Resume background sync", + "pauseBackgroundSync": "Pause background sync" + } } diff --git a/frontend/src/i18n/locales/hu/settings.json b/frontend/src/i18n/locales/hu/settings.json index 0212e51..d06369f 100644 --- a/frontend/src/i18n/locales/hu/settings.json +++ b/frontend/src/i18n/locales/hu/settings.json @@ -3,7 +3,6 @@ "tabs": { "appearance": "Megjelenés", "notifications": "Értesítések", - "sync": "Szinkronizálás", "account": "Fiók" }, "save": { @@ -42,43 +41,6 @@ "testTitle": "Tesztértesítés", "testMessage": "Így néz ki egy értesítés." }, - "sync": { - "myStatus": "Saját szinkronizálási állapot", - "channels": "Csatornák", - "channelsHint": "Hány csatornára vagy feliratkozva.", - "recentSynced": "Friss szinkronizálva", - "recentSyncedHint": "Azok a csatornák, amelyek legutóbbi feltöltései (~az elmúlt év) már a helyi adatbázisban vannak, így megjelennek a hírfolyamodban.", - "fullHistory": "Teljes előzmény", - "fullHistoryHint": "Azok a csatornák, amelyek teljes archívuma letöltődött, azokból, amelyekhez teljes előzményt kértél (csatornánként választható a Csatornák oldalon).", - "fullHistoryEta": "Teljes előzmény becsült ideje", - "fullHistoryEtaHint": "Hozzávetőleges becslés {{count}} függőben lévő csatorna teljes előzményének letöltéséhez, a megosztott napi kvóta alapján.", - "myVideos": "Saját videók", - "myVideosHint": "A számodra elérhető videók összesen a feliratkozott csatornáidon.", - "quotaLeft": "Ma maradt kvóta", - "quotaLeftHint": "A ma fennmaradó megosztott YouTube API-keret (éjfélkor nullázódik, USA csendes-óceáni idő szerint). A letöltés ennek alárendelődik.", - "loading": "Betöltés…", - "apiUsage": "Saját API-használat", - "today": "Ma", - "todayHint": "A saját műveleteid által ma használt YouTube API-egységek (éjfélkor nullázódik, USA csendes-óceáni idő szerint). A háttér-szinkronizálás itt nem számít bele.", - "last7d": "Utolsó 7 nap", - "last30d": "Utolsó 30 nap", - "allTime": "Mindenkori", - "byAction": "Művelet szerint (30 nap)", - "noUsage": "Még nincs használat.", - "actions": "Műveletek", - "syncSubscriptions": "Feliratkozások szinkronizálása", - "syncSubscriptionsHint": "Újraimportálja a YouTube-feliratkozásaidat és behúzza az újonnan követett csatornákat.", - "backfillEverything": "Minden letöltése", - "backfillEverythingHint": "Teljes archívum letöltésének kérése minden csatornához, amelyre fel vagy iratkozva. A régebbi videók és a teljes keresés annyira töltődnek fel, amennyit a megosztott napi kvóta enged.", - "synced": "{{count}} feliratkozás szinkronizálva", - "syncFailed": "A szinkronizálás sikertelen", - "fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához", - "fullHistoryFailed": "Nem sikerült teljes előzményt kérni", - "admin": "Adminisztrátor", - "adminPauseHint": "A háttér-szinkronizálás szüneteltetése vagy folytatása az egész alkalmazásra (minden felhasználóra).", - "resumeBackgroundSync": "Háttér-szinkronizálás folytatása", - "pauseBackgroundSync": "Háttér-szinkronizálás szüneteltetése" - }, "account": { "title": "Fiók", "youtubeAccess": "YouTube-hozzáférés", diff --git a/frontend/src/i18n/locales/hu/stats.json b/frontend/src/i18n/locales/hu/stats.json index 3c1b901..d85adb0 100644 --- a/frontend/src/i18n/locales/hu/stats.json +++ b/frontend/src/i18n/locales/hu/stats.json @@ -1,4 +1,8 @@ { + "tabs": { + "overview": "Áttekintés", + "system": "Rendszer" + }, "title": "API-kvóta használat", "rangeDays": "{{count}} nap", "introBefore": "A YouTube Data API-egységek aszerint, hogy ki váltotta ki a felhasználást. A ", @@ -9,5 +13,42 @@ "dailyTotal": "Napi összesen ({{count}} nap · {{units}} egység)", "noUsageInRange": "Nincs használat ebben az időszakban.", "dailyTooltip": "{{day}}: {{units}} egység", - "system": "Rendszer (háttér)" + "system": "Rendszer (háttér)", + "sync": { + "myStatus": "Saját szinkronizálási állapot", + "channels": "Csatornák", + "channelsHint": "Hány csatornára vagy feliratkozva.", + "recentSynced": "Friss szinkronizálva", + "recentSyncedHint": "Azok a csatornák, amelyek legutóbbi feltöltései (~az elmúlt év) már a helyi adatbázisban vannak, így megjelennek a hírfolyamodban.", + "fullHistory": "Teljes előzmény", + "fullHistoryHint": "Azok a csatornák, amelyek teljes archívuma letöltődött, azokból, amelyekhez teljes előzményt kértél (csatornánként választható a Csatornák oldalon).", + "fullHistoryEta": "Teljes előzmény becsült ideje", + "fullHistoryEtaHint": "Hozzávetőleges becslés {{count}} függőben lévő csatorna teljes előzményének letöltéséhez, a megosztott napi kvóta alapján.", + "myVideos": "Saját videók", + "myVideosHint": "A számodra elérhető videók összesen a feliratkozott csatornáidon.", + "quotaLeft": "Ma maradt kvóta", + "quotaLeftHint": "A ma fennmaradó megosztott YouTube API-keret (éjfélkor nullázódik, USA csendes-óceáni idő szerint). A letöltés ennek alárendelődik.", + "loading": "Betöltés…", + "apiUsage": "Saját API-használat", + "today": "Ma", + "todayHint": "A saját műveleteid által ma használt YouTube API-egységek (éjfélkor nullázódik, USA csendes-óceáni idő szerint). A háttér-szinkronizálás itt nem számít bele.", + "last7d": "Utolsó 7 nap", + "last30d": "Utolsó 30 nap", + "allTime": "Mindenkori", + "byAction": "Művelet szerint (30 nap)", + "noUsage": "Még nincs használat.", + "actions": "Műveletek", + "syncSubscriptions": "Feliratkozások szinkronizálása", + "syncSubscriptionsHint": "Újraimportálja a YouTube-feliratkozásaidat és behúzza az újonnan követett csatornákat.", + "backfillEverything": "Minden letöltése", + "backfillEverythingHint": "Teljes archívum letöltésének kérése minden csatornához, amelyre fel vagy iratkozva. A régebbi videók és a teljes keresés annyira töltődnek fel, amennyit a megosztott napi kvóta enged.", + "synced": "{{count}} feliratkozás szinkronizálva", + "syncFailed": "A szinkronizálás sikertelen", + "fullHistoryRequested": "Teljes előzmény kérve {{count}} csatornához", + "fullHistoryFailed": "Nem sikerült teljes előzményt kérni", + "admin": "Adminisztrátor", + "adminPauseHint": "A háttér-szinkronizálás szüneteltetése vagy folytatása az egész alkalmazásra (minden felhasználóra).", + "resumeBackgroundSync": "Háttér-szinkronizálás folytatása", + "pauseBackgroundSync": "Háttér-szinkronizálás szüneteltetése" + } }