Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 47 additions & 9 deletions gui/src/components/MainAccountHardLockSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { useT } from "../i18n/shared";
import { hardLockThresholds, type MainAccountHardLockStatus } from "../hooks/useCodexAccountPool";

type Props = { apiBase: string; onSaved: () => Promise<boolean> };
type Snapshot = { codexMainAccountHardLock: boolean; mainAccountHardLock: MainAccountHardLockStatus };
type Field = "codexMainAccountHardLock" | "codexMainAccountCreditsOverrideHardLock";
type Snapshot = {
codexMainAccountHardLock: boolean;
codexMainAccountCreditsOverrideHardLock: boolean;
mainAccountHardLock: MainAccountHardLockStatus;
};

function readSnapshot(value: unknown): Snapshot {
if (!value || typeof value !== "object") throw new Error("settings shape");
Expand All @@ -17,7 +22,14 @@ function readSnapshot(value: unknown): Snapshot {
|| (policy.resetAt !== undefined && (typeof policy.resetAt !== "number" || !Number.isFinite(policy.resetAt)))) {
throw new Error("settings shape");
}
return payload as Snapshot;
// Absent means off: an older proxy that predates the key renders the switch off rather than
// erroring, which is also the server default.
return {
codexMainAccountHardLock: payload.codexMainAccountHardLock,
codexMainAccountCreditsOverrideHardLock: typeof payload.codexMainAccountCreditsOverrideHardLock === "boolean"
? payload.codexMainAccountCreditsOverrideHardLock : false,
mainAccountHardLock: policy,
};
}

function HardLockConfirmation({ pending, thresholds, onCancel, onConfirm }: {
Expand Down Expand Up @@ -78,13 +90,16 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
const [saveError, setSaveError] = useState(false);
const [refreshError, setRefreshError] = useState(false);
const [saved, setSaved] = useState<boolean | null>(null);
const [overrideSaved, setOverrideSaved] = useState<boolean | null>(null);
const [saving, setSaving] = useState(false);
const [confirming, setConfirming] = useState(false);
const busyRef = useRef(false);
const mountedRef = useRef(false);
const generationRef = useRef(0);
const readAbortRef = useRef<AbortController | null>(null);
const toggleRef = useRef<HTMLButtonElement>(null);
const overrideToggleRef = useRef<HTMLButtonElement>(null);
const focusTargetRef = useRef<HTMLButtonElement | null>(null);
const sectionRef = useRef<HTMLElement>(null);
const restoreFocusRef = useRef(false);

Expand All @@ -102,6 +117,7 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
setSnapshot(next);
setLoadError(false);
setSaved(null);
setOverrideSaved(null);
} catch {
if (mountedRef.current && generation === generationRef.current) setLoadError(true);
} finally {
Expand All @@ -124,8 +140,9 @@ function HardLockSetting({ apiBase, onSaved }: Props) {

useEffect(() => {
if (confirming || saving || !restoreFocusRef.current) return;
if (toggleRef.current && !toggleRef.current.disabled) {
toggleRef.current.focus();
const target = focusTargetRef.current ?? toggleRef.current;
if (target && !target.disabled) {
target.focus();
restoreFocusRef.current = false;
} else {
// Keep the intent through failed/pending authoritative reads: the section is
Expand All @@ -140,7 +157,7 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
if (mountedRef.current) setRefreshError(!confirmed);
};

const save = async (requested: boolean) => {
const save = async (field: Field, requested: boolean) => {
// The toggle requires a successful read before opening confirmation. A later poll
// failure must not silently turn an already-open confirmation into a no-op.
if (busyRef.current || !snapshot) return;
Expand All @@ -151,12 +168,13 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
setSaveError(false);
setRefreshError(false);
setSaved(null);
setOverrideSaved(null);
const bounded = createBoundedFetch(15_000);
let acknowledged = false;
try {
const response = await fetch(`${apiBase}/api/settings`, {
method: "PUT", headers: { "content-type": "application/json" },
body: JSON.stringify({ codexMainAccountHardLock: requested }), signal: bounded.signal,
body: JSON.stringify({ [field]: requested }), signal: bounded.signal,
});
if (!response.ok) throw new Error("save");
const payload: unknown = await response.json();
Expand All @@ -168,7 +186,8 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
if (mountedRef.current) {
setSnapshot(next);
setLoadError(false);
setSaved(next.codexMainAccountHardLock);
if (field === "codexMainAccountHardLock") setSaved(next.codexMainAccountHardLock);
else setOverrideSaved(next.codexMainAccountCreditsOverrideHardLock);
}
} catch {
if (mountedRef.current) {
Expand Down Expand Up @@ -200,6 +219,7 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
if (mountedRef.current) setSaving(false);
};
const enabled = snapshot?.codexMainAccountHardLock;
const overrideEnabled = snapshot?.codexMainAccountCreditsOverrideHardLock;
const thresholds = hardLockThresholds(snapshot?.mainAccountHardLock.thresholds);
return (
<section ref={sectionRef} id="codex-main-hard-lock-setting" tabIndex={-1}
Expand All @@ -221,7 +241,8 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
onClick={() => {
if (busyRef.current || enabled === undefined || loadError) return;
restoreFocusRef.current = true;
if (enabled) void save(false);
focusTargetRef.current = toggleRef.current;
if (enabled) void save("codexMainAccountHardLock", false);
else setConfirming(true);
}}><span className="toggle-knob" /></button>
<div className="codex-main-hard-lock-feedback">
Expand All @@ -234,7 +255,24 @@ function HardLockSetting({ apiBase, onSaved }: Props) {
<button type="button" className="link-btn" disabled={saving} onClick={() => { void retryRefresh(); }}>{t("common.retry")}</button>
</p>}
</div>
{confirming && <HardLockConfirmation pending={saving} thresholds={thresholds} onCancel={cancel} onConfirm={() => { void save(true); }} />}
<div className="codex-main-hard-lock-break" aria-hidden="true" />
<div className="codex-main-hard-lock-copy">
<strong id={`${id}-override-title`}>{t("codexAuth.mainCreditsOverrideTitle")}</strong>
<div id={`${id}-override-desc`} className="card-sub">{t("codexAuth.mainCreditsOverrideDesc")}</div>
</div>
<button ref={overrideToggleRef} type="button" className={`toggle ${overrideEnabled === true ? "on" : ""}`}
disabled={saving || overrideEnabled === undefined || loadError} aria-pressed={overrideEnabled === true}
aria-label={t("codexAuth.mainCreditsOverrideTitle")} aria-describedby={`${id}-override-desc`}
onClick={() => {
if (busyRef.current || overrideEnabled === undefined || loadError) return;
restoreFocusRef.current = true;
focusTargetRef.current = overrideToggleRef.current;
void save("codexMainAccountCreditsOverrideHardLock", !overrideEnabled);
}}><span className="toggle-knob" /></button>
<div className="codex-main-hard-lock-feedback">
{overrideSaved !== null && !saveError && <p role="status">{t(overrideSaved ? "codexAuth.mainCreditsOverrideOn" : "codexAuth.mainCreditsOverrideOff")}</p>}
</div>
{confirming && <HardLockConfirmation pending={saving} thresholds={thresholds} onCancel={cancel} onConfirm={() => { void save("codexMainAccountHardLock", true); }} />}
</section>
);
}
4 changes: 4 additions & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1837,6 +1837,10 @@ export const de: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "Schutz aktiv · Überwachung",
"codexAuth.mainExternalUsageWarning": "Mögliche Nutzung außerhalb von opencodex. Die Sperre verhindert möglicherweise keine Erschöpfung.",
"codexAuth.mainHardLockManage": "Schutzeinstellung anzeigen",
"codexAuth.mainCreditsOverrideTitle": "Credits zum Übergehen des Hauptkonto-Schutzes verwenden",
"codexAuth.mainCreditsOverrideDesc": "Wenn der Schutz das Hauptkonto blockiert, ein frisches verfügbares Credit-Guthaben weiter bedienen lassen. Standardmäßig aus; die Sperre greift wieder, sobald das Guthaben verbraucht, abgelehnt oder veraltet ist.",
"codexAuth.mainCreditsOverrideOn": "Credit-Übergehung ist an. Verfügbare Credits heben die Hauptkonto-Sperre auf.",
"codexAuth.mainCreditsOverrideOff": "Credit-Übergehung ist aus.",
"codexAuth.ultraFastDesc": "Verhindert, dass ein selbst konfigurierter ultrafast-Diensttarif beim Neuaufbau des Katalogs entfernt wird, und benennt ihn in den Anfrageprotokollen. Ultra Fast wird nicht in die Modellauswahl aufgenommen: Upstream kündigt nur Fast an, ein Eintrag würde also eine Geschwindigkeit anbieten, die die Leitung nicht liefern kann.",
"codexAuth.ultraFastLoadFailed": "Die Ultra-Fast-Einstellung konnte nicht gelesen werden.",
"codexAuth.ultraFastEnabled": "Ultra-Fast-Tarif aktiviert",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2448,6 +2448,10 @@ export const en = {
"codexAuth.mainHardLockMonitoring": "Protection on · monitoring",
"codexAuth.mainExternalUsageWarning": "Possible usage outside opencodex. The lock may not prevent exhaustion.",
"codexAuth.mainHardLockManage": "View protection setting",
"codexAuth.mainCreditsOverrideTitle": "Use credits to override main-account protection",
"codexAuth.mainCreditsOverrideDesc": "When protection blocks the main account, let a fresh spendable credit balance keep it serving. Off by default; the lock re-arms when the balance is spent, refused, or stale.",
"codexAuth.mainCreditsOverrideOn": "Credit override is on. Spendable credits release the main-account lock.",
"codexAuth.mainCreditsOverrideOff": "Credit override is off.",
"codexAuth.ultraFastDesc": "Keeps an ultrafast service tier you configured yourself from being stripped when the catalog is regenerated, and names it in the request logs. It does not add Ultra Fast to the model picker: upstream advertises only Fast, so a picker row would offer a speed the wire cannot deliver.",
"codexAuth.ultraFastLoadFailed": "Could not read the Ultra Fast setting.",
"codexAuth.ultraFastEnabled": "Ultra Fast tier enabled",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2375,6 +2375,10 @@ export const fr: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "Protection active · surveillance",
"codexAuth.mainExternalUsageWarning": "Utilisation possible hors opencodex. Le blocage peut ne pas empêcher l’épuisement du quota.",
"codexAuth.mainHardLockManage": "Voir le réglage de protection",
"codexAuth.mainCreditsOverrideTitle": "Utiliser les crédits pour contourner la protection du compte principal",
"codexAuth.mainCreditsOverrideDesc": "Lorsque la protection bloque le compte principal, laisser un solde de crédits disponible et récent continuer à le servir. Désactivé par défaut ; le verrou se réarme dès que le solde est épuisé, refusé ou obsolète.",
"codexAuth.mainCreditsOverrideOn": "Le contournement par crédits est activé. Les crédits disponibles lèvent le verrou du compte principal.",
"codexAuth.mainCreditsOverrideOff": "Le contournement par crédits est désactivé.",
"codexAuth.ultraFastDesc": "Empêche la suppression d’un niveau de service ultrafast que vous avez configuré vous-même lors de la régénération du catalogue, et le nomme dans les journaux de requêtes. Ultra Fast n’est pas ajouté au sélecteur de modèles : l’amont n’annonce que Fast, une entrée proposerait donc une vitesse que le transport ne peut pas fournir.",
"codexAuth.ultraFastLoadFailed": "Impossible de lire le réglage Ultra Fast.",
"codexAuth.ultraFastEnabled": "Niveau Ultra Fast activé",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2270,6 +2270,10 @@ export const ja: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "保護有効・監視中",
"codexAuth.mainExternalUsageWarning": "opencodex以外で使用量が増えた可能性があります。制限しても上限に達する場合があります。",
"codexAuth.mainHardLockManage": "保護設定を表示",
"codexAuth.mainCreditsOverrideTitle": "クレジットでメインアカウント保護を上書き",
"codexAuth.mainCreditsOverrideDesc": "保護がメインアカウントをブロックしたとき、新しく確認された利用可能なクレジット残高で処理を続行させます。既定はオフで、残高が使い切られる・拒否される・古くなるとロックが再び有効になります。",
"codexAuth.mainCreditsOverrideOn": "クレジット上書きはオンです。利用可能なクレジットがメインアカウントのロックを解除します。",
"codexAuth.mainCreditsOverrideOff": "クレジット上書きはオフです。",
"codexAuth.ultraFastDesc": "自分で設定した ultrafast サービスティアがカタログ再生成時に削除されないようにし、リクエストログにそのティア名を記録します。モデルピッカーに Ultra Fast は追加しません。アップストリームは Fast しか公開しておらず、ピッカーに項目を出すと実際には出せない速度を選ばせることになるためです。",
"codexAuth.ultraFastLoadFailed": "Ultra Fast 設定を読み取れませんでした。",
"codexAuth.ultraFastEnabled": "Ultra Fast ティアを有効にしました",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,10 @@ export const ko: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "보호 켜짐",
"codexAuth.mainExternalUsageWarning": "opencodex 밖에서 사용량이 늘었을 수 있습니다. 차단해도 한도를 소진할 수 있습니다.",
"codexAuth.mainHardLockManage": "차단 설정 보기",
"codexAuth.mainCreditsOverrideTitle": "크레딧으로 기본 계정 보호 재정의",
"codexAuth.mainCreditsOverrideDesc": "보호 기능이 기본 계정을 차단할 때, 새로 확인된 사용 가능한 크레딧 잔액으로 계속 처리하도록 허용합니다. 기본값은 꺼짐이며, 잔액이 소진·거부·만료되면 잠금이 다시 활성화됩니다.",
"codexAuth.mainCreditsOverrideOn": "크레딧 재정의가 켜져 있습니다. 사용 가능한 크레딧이 기본 계정 잠금을 해제합니다.",
"codexAuth.mainCreditsOverrideOff": "크레딧 재정의가 꺼져 있습니다.",
"codexAuth.ultraFastDesc": "직접 설정한 ultrafast 서비스 티어가 카탈로그를 다시 만들 때 지워지지 않게 하고, 요청 로그에 그 티어 이름을 남깁니다. 모델 피커에 Ultra Fast를 추가하지는 않습니다. 업스트림은 Fast만 알리기 때문에, 피커에 칸을 만들면 실제로 낼 수 없는 속도를 고르게 하는 셈입니다.",
"codexAuth.ultraFastLoadFailed": "Ultra Fast 설정을 읽지 못했습니다.",
"codexAuth.ultraFastEnabled": "Ultra Fast 티어를 켰습니다",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2422,6 +2422,10 @@ export const pt: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "Proteção ativa · monitorando",
"codexAuth.mainExternalUsageWarning": "Possível uso fora do opencodex. O bloqueio pode não impedir o esgotamento.",
"codexAuth.mainHardLockManage": "Ver configuração de proteção",
"codexAuth.mainCreditsOverrideTitle": "Usar créditos para sobrepor a proteção da conta principal",
"codexAuth.mainCreditsOverrideDesc": "Quando a proteção bloqueia a conta principal, permitir que um saldo de créditos recente e disponível continue a atendê-la. Desativado por padrão; o bloqueio volta a ativar-se quando o saldo é gasto, recusado ou fica desatualizado.",
"codexAuth.mainCreditsOverrideOn": "A sobreposição por créditos está ativada. Créditos disponíveis liberam o bloqueio da conta principal.",
"codexAuth.mainCreditsOverrideOff": "A sobreposição por créditos está desativada.",
"codexAuth.ultraFastDesc": "Impede que um nível de serviço ultrarrápido configurado por você seja removido quando o catálogo é regenerado e o nomeia nos logs de requisição. Não adiciona o Ultra Fast ao seletor de modelos: o upstream anuncia apenas o Fast, então uma linha no seletor ofereceria uma velocidade que a conexão não consegue entregar.",
"codexAuth.ultraFastLoadFailed": "Não foi possível ler a configuração do Ultra Fast.",
"codexAuth.ultraFastEnabled": "Nível Ultra Fast ativado",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2361,6 +2361,10 @@ export const ru: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "Защита включена · наблюдение",
"codexAuth.mainExternalUsageWarning": "Возможно использование вне opencodex. Блокировка может не предотвратить исчерпание квоты.",
"codexAuth.mainHardLockManage": "Открыть настройку защиты",
"codexAuth.mainCreditsOverrideTitle": "Использовать кредиты в обход защиты основной учётной записи",
"codexAuth.mainCreditsOverrideDesc": "Когда защита блокирует основную учётную запись, разрешить свежему доступному балансу кредитов продолжать обслуживание. По умолчанию выключено; блокировка снова срабатывает, когда баланс израсходован, отклонён или устарел.",
"codexAuth.mainCreditsOverrideOn": "Обход через кредиты включён. Доступные кредиты снимают блокировку основной учётной записи.",
"codexAuth.mainCreditsOverrideOff": "Обход через кредиты выключен.",
"codexAuth.ultraFastDesc": "Не даёт удалить настроенный вами уровень ultrafast при перегенерации каталога и записывает его имя в журналы запросов. Ultra Fast не добавляется в выбор моделей: вышестоящий сервис объявляет только Fast, поэтому пункт в списке предлагал бы скорость, которую канал не может обеспечить.",
"codexAuth.ultraFastLoadFailed": "Не удалось прочитать настройку Ultra Fast.",
"codexAuth.ultraFastEnabled": "Уровень Ultra Fast включён",
Expand Down
4 changes: 4 additions & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2399,6 +2399,10 @@ export const tr: Record<TKey, string> = {
"codexAuth.mainHardLockMonitoring": "Koruma açık · izleniyor",
"codexAuth.mainExternalUsageWarning": "opencodex dışında kullanım olabilir. Engel, kotanın tükenmesini önlemeyebilir.",
"codexAuth.mainHardLockManage": "Koruma ayarını göster",
"codexAuth.mainCreditsOverrideTitle": "Ana hesap korumasını kredilerle geçersiz kıl",
"codexAuth.mainCreditsOverrideDesc": "Koruma ana hesabı engellediğinde, yeni ve harcanabilir bir kredi bakiyesinin hizmet vermeye devam etmesine izin ver. Varsayılan olarak kapalı; bakiye harcandığında, reddedildiğinde veya eskidiğinde kilit yeniden devreye girer.",
"codexAuth.mainCreditsOverrideOn": "Kredi geçersiz kılma açık. Harcanabilir krediler ana hesap kilidini kaldırır.",
"codexAuth.mainCreditsOverrideOff": "Kredi geçersiz kılma kapalı.",
"codexAuth.ultraFastDesc": "Kendi yapılandırdığınız ultrafast hizmet katmanının katalog yeniden oluşturulurken silinmesini önler ve istek günlüklerinde bu katmanın adını yazar. Ultra Fast’i model seçicisine eklemez: üst kaynak yalnızca Fast duyurur, bu yüzden bir satır eklemek hattın veremeyeceği bir hızı seçtirmek olurdu.",
"codexAuth.ultraFastLoadFailed": "Ultra Fast ayarı okunamadı.",
"codexAuth.ultraFastEnabled": "Ultra Fast katmanı etkinleştirildi",
Expand Down
Loading
Loading