Compare commits
4 Commits
feat/B6-se
...
feat/B7-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
452d7d9b3d
|
|||
|
21eb211d6a
|
|||
|
0de99a8cc7
|
|||
|
1bf3348c8c
|
@@ -66,6 +66,7 @@ jobs:
|
||||
build_cortex: ${{ steps.changes.outputs.build_cortex }}
|
||||
build_neuron: ${{ steps.changes.outputs.build_neuron }}
|
||||
build_bench: ${{ steps.changes.outputs.build_bench }}
|
||||
build_upstream: ${{ steps.changes.outputs.build_upstream }}
|
||||
check_rust: ${{ steps.changes.outputs.check_rust }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -104,6 +105,7 @@ jobs:
|
||||
BUILD_CORTEX=true
|
||||
BUILD_NEURON=true
|
||||
BUILD_BENCH=true
|
||||
BUILD_UPSTREAM=true
|
||||
CHECK_RUST=true
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
|
||||
@@ -149,6 +151,7 @@ jobs:
|
||||
NEURON_RE='^crates/neuron/|^crates/cortex-core/|^Cargo\.toml$|^Cargo\.lock$|^rpm/helexa-neuron-prerelease\.spec$|^data/neuron|^neuron\.example\.toml$|^\.gitea/workflows/build-prerelease\.yml$'
|
||||
CORTEX_RE='^crates/cortex-gateway/|^crates/cortex-cli/|^crates/cortex-core/|^Cargo\.toml$|^Cargo\.lock$|^rpm/cortex-prerelease\.spec$|^data/cortex|^cortex\.example\.toml$|^models\.example\.toml$|^\.gitea/workflows/build-prerelease\.yml$'
|
||||
BENCH_RE='^crates/helexa-bench/|^crates/cortex-core/|^Cargo\.toml$|^Cargo\.lock$|^rpm/helexa-bench-prerelease\.spec$|^data/helexa-bench|^helexa-bench\.example\.toml$|^\.gitea/workflows/build-prerelease\.yml$'
|
||||
UPSTREAM_RE='^crates/helexa-upstream/|^crates/cortex-core/|^Cargo\.toml$|^Cargo\.lock$|^rpm/helexa-upstream-prerelease\.spec$|^data/helexa-upstream|^helexa-upstream\.example\.toml$|^\.gitea/workflows/build-prerelease\.yml$'
|
||||
# Any Rust change (incl. crates not packaged here, e.g.
|
||||
# helexa-acp) still needs lint+test on main.
|
||||
RUST_RE='\.rs$|^crates/|Cargo\.toml$|^Cargo\.lock$'
|
||||
@@ -156,10 +159,12 @@ jobs:
|
||||
CORTEX_BASE=$(base_for cortex)
|
||||
NEURON_BASE=$(base_for helexa-neuron-blackwell)
|
||||
BENCH_BASE=$(base_for helexa-bench)
|
||||
UPSTREAM_BASE=$(base_for helexa-upstream)
|
||||
BUILD_CORTEX=$(decide "$CORTEX_BASE" "$CORTEX_RE")
|
||||
BUILD_NEURON=$(decide "$NEURON_BASE" "$NEURON_RE")
|
||||
BUILD_BENCH=$(decide "$BENCH_BASE" "$BENCH_RE")
|
||||
if [ "$BUILD_CORTEX" = "true" ] || [ "$BUILD_NEURON" = "true" ] || [ "$BUILD_BENCH" = "true" ]; then
|
||||
BUILD_UPSTREAM=$(decide "$UPSTREAM_BASE" "$UPSTREAM_RE")
|
||||
if [ "$BUILD_CORTEX" = "true" ] || [ "$BUILD_NEURON" = "true" ] || [ "$BUILD_BENCH" = "true" ] || [ "$BUILD_UPSTREAM" = "true" ]; then
|
||||
CHECK_RUST=true
|
||||
else
|
||||
CHECK_RUST=$(decide "$CORTEX_BASE" "$RUST_RE")
|
||||
@@ -170,8 +175,9 @@ jobs:
|
||||
echo "build_cortex=${BUILD_CORTEX}" >> "$GITHUB_OUTPUT"
|
||||
echo "build_neuron=${BUILD_NEURON}" >> "$GITHUB_OUTPUT"
|
||||
echo "build_bench=${BUILD_BENCH}" >> "$GITHUB_OUTPUT"
|
||||
echo "build_upstream=${BUILD_UPSTREAM}" >> "$GITHUB_OUTPUT"
|
||||
echo "check_rust=${CHECK_RUST}" >> "$GITHUB_OUTPUT"
|
||||
echo "### change detection: build_cortex=${BUILD_CORTEX} build_neuron=${BUILD_NEURON} build_bench=${BUILD_BENCH} check_rust=${CHECK_RUST}"
|
||||
echo "### change detection: build_cortex=${BUILD_CORTEX} build_neuron=${BUILD_NEURON} build_bench=${BUILD_BENCH} build_upstream=${BUILD_UPSTREAM} check_rust=${CHECK_RUST}"
|
||||
|
||||
# fmt + clippy + test moved here from ci.yml for main pushes so the
|
||||
# two workflows stop queueing against each other (ci.yml's checks
|
||||
@@ -303,6 +309,45 @@ jobs:
|
||||
path: artifacts/helexa-bench
|
||||
retention-days: 1
|
||||
|
||||
build-upstream:
|
||||
name: Build helexa-upstream binary
|
||||
timeout-minutes: 25
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.build_upstream == 'true'
|
||||
# Pure-Rust, non-CUDA binary — same runner as cortex/bench.
|
||||
runs-on: rust
|
||||
env:
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_BUCKET: sccache
|
||||
SCCACHE_ENDPOINT: http://caveman.kosherinata.internal:9000
|
||||
SCCACHE_REGION: auto
|
||||
SCCACHE_S3_USE_SSL: "false"
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.SCCACHE_S3_ACCESS_KEY }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SCCACHE_S3_SECRET_KEY }}
|
||||
# helexa-upstream uses the sqlx runtime query API (no compile-time
|
||||
# query macros), so it builds without a database or a .sqlx cache.
|
||||
# Set OFFLINE defensively so a stray macro can never reach for a DB.
|
||||
SQLX_OFFLINE: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Build helexa-upstream (release, sccache escalation)
|
||||
run: script/ci-cargo-escalate.sh cargo build --release -p helexa-upstream
|
||||
|
||||
- name: Stage binary
|
||||
run: |
|
||||
mkdir --parents artifacts
|
||||
cp target/release/helexa-upstream artifacts/helexa-upstream
|
||||
./artifacts/helexa-upstream --version || true
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: upstream-fc43
|
||||
path: artifacts/helexa-upstream
|
||||
retention-days: 1
|
||||
|
||||
build-neuron:
|
||||
name: Build neuron-${{ matrix.flavour }}
|
||||
timeout-minutes: 35
|
||||
@@ -459,6 +504,44 @@ jobs:
|
||||
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||
retention-days: 7
|
||||
|
||||
package-upstream:
|
||||
name: Package helexa-upstream RPM
|
||||
timeout-minutes: 20
|
||||
needs: [prepare, build-upstream]
|
||||
runs-on: rpm
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: upstream-fc43
|
||||
path: artifacts/
|
||||
|
||||
- name: Build RPM
|
||||
run: |
|
||||
set -eux
|
||||
rm -f ~/.rpmmacros
|
||||
rpmdev-setuptree
|
||||
cp artifacts/helexa-upstream ~/rpmbuild/SOURCES/
|
||||
cp data/helexa-upstream.service ~/rpmbuild/SOURCES/
|
||||
cp data/helexa-upstream-sysusers.conf ~/rpmbuild/SOURCES/
|
||||
cp data/helexa-upstream-firewalld.xml ~/rpmbuild/SOURCES/
|
||||
cp helexa-upstream.example.toml ~/rpmbuild/SOURCES/
|
||||
cp LICENSE ~/rpmbuild/SOURCES/
|
||||
rpmbuild -bb rpm/helexa-upstream-prerelease.spec \
|
||||
--define "upstream_version ${{ needs.prepare.outputs.version }}" \
|
||||
--define "upstream_prerelease ${{ needs.prepare.outputs.release }}" \
|
||||
--undefine dist \
|
||||
--define "dist .fc43"
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: rpm-upstream-fc43
|
||||
path: ~/rpmbuild/RPMS/x86_64/*.rpm
|
||||
retention-days: 7
|
||||
|
||||
package-neuron:
|
||||
name: Package helexa-neuron-${{ matrix.flavour }} RPM
|
||||
timeout-minutes: 20
|
||||
@@ -508,7 +591,7 @@ jobs:
|
||||
publish:
|
||||
name: Publish to rpm.lair.cafe (unstable)
|
||||
timeout-minutes: 25
|
||||
needs: [lint, test, package-cortex, package-neuron, package-bench]
|
||||
needs: [lint, test, package-cortex, package-neuron, package-bench, package-upstream]
|
||||
# Runs when at least one package was built and nothing failed.
|
||||
# lint/test may be skipped (docs-only refs never get here because
|
||||
# no packages build), but a real failure in any blocks the
|
||||
@@ -518,10 +601,11 @@ jobs:
|
||||
!cancelled()
|
||||
&& (needs.lint.result == 'success' || needs.lint.result == 'skipped')
|
||||
&& (needs.test.result == 'success' || needs.test.result == 'skipped')
|
||||
&& (needs.package-cortex.result == 'success' || needs.package-neuron.result == 'success' || needs.package-bench.result == 'success')
|
||||
&& (needs.package-cortex.result == 'success' || needs.package-neuron.result == 'success' || needs.package-bench.result == 'success' || needs.package-upstream.result == 'success')
|
||||
&& needs.package-cortex.result != 'failure'
|
||||
&& needs.package-neuron.result != 'failure'
|
||||
&& needs.package-bench.result != 'failure'
|
||||
&& needs.package-upstream.result != 'failure'
|
||||
}}
|
||||
runs-on: rpm
|
||||
concurrency:
|
||||
|
||||
6
data/helexa-upstream-firewalld.xml
Normal file
6
data/helexa-upstream-firewalld.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<service>
|
||||
<short>helexa-upstream</short>
|
||||
<description>helexa-upstream — mesh account + budget authority API (/authz/v1 for cortex, /web/v1 for the frontend)</description>
|
||||
<port protocol="tcp" port="8090"/>
|
||||
</service>
|
||||
3
data/helexa-upstream-sysusers.conf
Normal file
3
data/helexa-upstream-sysusers.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
g helexa-upstream - -
|
||||
u helexa-upstream - "helexa-upstream authority" /var/lib/helexa-upstream /sbin/nologin
|
||||
m helexa-upstream helexa-upstream
|
||||
22
data/helexa-upstream.service
Normal file
22
data/helexa-upstream.service
Normal file
@@ -0,0 +1,22 @@
|
||||
[Unit]
|
||||
Description=helexa-upstream — mesh account + budget authority (accounts, API keys, allocation ledger, top-up codes)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/helexa-upstream serve --config /etc/helexa-upstream/helexa-upstream.toml
|
||||
# HTTP authority for cortex (/authz/v1) and the frontend (/web/v1); restart
|
||||
# unconditionally if it ever exits. It connects out to PostgreSQL (the
|
||||
# system of record) and runs schema migrations on startup.
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
User=helexa-upstream
|
||||
Group=helexa-upstream
|
||||
# Service user home; no local state (PostgreSQL holds everything), but
|
||||
# StateDirectory gives the user a writable, correctly-owned home.
|
||||
StateDirectory=helexa-upstream
|
||||
StateDirectoryMode=0755
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -23,7 +23,7 @@ const ROOT = path.resolve(
|
||||
const RESOURCES_DIR = path.join(ROOT, "src", "i18n", "resources");
|
||||
|
||||
// Namespaces to validate.
|
||||
const NAMESPACES = ["common", "mission", "chat"];
|
||||
const NAMESPACES = ["common", "mission", "chat", "account"];
|
||||
|
||||
// Languages to validate should track SUPPORTED_LANGUAGES in src/i18n/languages.ts.
|
||||
// NOTE: This list is intentionally narrower than SUPPORTED_LANGUAGES and does not
|
||||
|
||||
@@ -1,26 +1,58 @@
|
||||
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
||||
import ThemeProvider from "./layout/ThemeProvider";
|
||||
import AuthProvider from "./auth/AuthProvider";
|
||||
import RequireAuth from "./auth/RequireAuth";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Mission from "./pages/Mission";
|
||||
import Chat from "./pages/Chat";
|
||||
import Login from "./pages/auth/Login";
|
||||
import Register from "./pages/auth/Register";
|
||||
import VerifyEmail from "./pages/auth/VerifyEmail";
|
||||
import RequestReset from "./pages/auth/RequestReset";
|
||||
import ResetPassword from "./pages/auth/ResetPassword";
|
||||
import Dashboard from "./pages/account/Dashboard";
|
||||
import ApiKeys from "./pages/account/ApiKeys";
|
||||
import "./App.css";
|
||||
|
||||
// Composition root: theme + router + layout shell. `/` is the chat
|
||||
// workspace (F3, anonymous for now); `/mission` (F2) is the EU-sovereignty
|
||||
// narrative; the auth/account routes (F4) land next.
|
||||
// Composition root: theme → router → auth → layout shell. `/` is the chat
|
||||
// workspace (F3); `/mission` the EU-sovereignty narrative (F2); the auth +
|
||||
// account routes (F4) follow, with /account guarded.
|
||||
export default function App() {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<BrowserRouter>
|
||||
<div className="d-flex flex-column min-vh-100">
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<Chat />} />
|
||||
<Route path="/mission" element={<Mission />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</div>
|
||||
<AuthProvider>
|
||||
<div className="d-flex flex-column min-vh-100">
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<Chat />} />
|
||||
<Route path="/mission" element={<Mission />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/register" element={<Register />} />
|
||||
<Route path="/verify" element={<VerifyEmail />} />
|
||||
<Route path="/forgot" element={<RequestReset />} />
|
||||
<Route path="/reset" element={<ResetPassword />} />
|
||||
<Route
|
||||
path="/account"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Dashboard />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/account/keys"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<ApiKeys />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Footer />
|
||||
</div>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
213
helexa.ai/src/api/account.ts
Normal file
213
helexa.ai/src/api/account.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
// Account API client over helexa-upstream's /web/v1 (B4/B5). The browser
|
||||
// calls a same-origin `/api` prefix (vite-proxied in dev, nginx-routed in
|
||||
// prod). A MockAccountApi behind VITE_USE_MOCK_ACCOUNT_API lets the
|
||||
// dashboard be built/demoed before the upstream service is reachable.
|
||||
|
||||
import {
|
||||
ApiError,
|
||||
type AccountBalance,
|
||||
type ApiKeySummary,
|
||||
type CreatedKey,
|
||||
type Session,
|
||||
} from "./types";
|
||||
|
||||
export interface AccountApi {
|
||||
register(email: string, password: string, fingerprint?: string): Promise<void>;
|
||||
verify(token: string): Promise<void>;
|
||||
login(email: string, password: string): Promise<Session>;
|
||||
requestReset(email: string): Promise<void>;
|
||||
confirmReset(token: string, newPassword: string): Promise<void>;
|
||||
account(token: string): Promise<AccountBalance>;
|
||||
listKeys(token: string): Promise<ApiKeySummary[]>;
|
||||
createKey(
|
||||
token: string,
|
||||
label: string,
|
||||
limitKind: "percent" | "hardcap",
|
||||
limitValue: number,
|
||||
): Promise<CreatedKey>;
|
||||
archiveKey(token: string, id: string): Promise<void>;
|
||||
updateKeyLimit(
|
||||
token: string,
|
||||
id: string,
|
||||
limitKind: "percent" | "hardcap",
|
||||
limitValue: number,
|
||||
): Promise<void>;
|
||||
redeem(token: string, code: string): Promise<AccountBalance>;
|
||||
}
|
||||
|
||||
const BASE = (import.meta.env.VITE_ACCOUNT_BASE_URL || "/api").replace(/\/$/, "");
|
||||
|
||||
async function call<T>(
|
||||
path: string,
|
||||
init: RequestInit & { token?: string } = {},
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = { "content-type": "application/json" };
|
||||
if (init.token) headers.authorization = `Bearer ${init.token}`;
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(`${BASE}${path}`, { ...init, headers });
|
||||
} catch {
|
||||
throw new ApiError(0, "network_error", "Could not reach the account service.");
|
||||
}
|
||||
if (resp.status === 204) return undefined as T;
|
||||
let body: unknown = null;
|
||||
try {
|
||||
body = await resp.json();
|
||||
} catch {
|
||||
/* empty body */
|
||||
}
|
||||
if (!resp.ok) {
|
||||
const err = (body as { error?: { code?: string; message?: string } })?.error;
|
||||
throw new ApiError(resp.status, err?.code ?? "error", err?.message ?? "Request failed.");
|
||||
}
|
||||
return body as T;
|
||||
}
|
||||
|
||||
class RealAccountApi implements AccountApi {
|
||||
async register(email: string, password: string, fingerprint?: string) {
|
||||
await call("/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password, fingerprint }),
|
||||
});
|
||||
}
|
||||
async verify(token: string) {
|
||||
await call("/verify", { method: "POST", body: JSON.stringify({ token }) });
|
||||
}
|
||||
login(email: string, password: string) {
|
||||
return call<Session>("/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
}
|
||||
async requestReset(email: string) {
|
||||
await call("/password-reset/request", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
}
|
||||
async confirmReset(token: string, newPassword: string) {
|
||||
await call("/password-reset/confirm", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ token, new_password: newPassword }),
|
||||
});
|
||||
}
|
||||
account(token: string) {
|
||||
return call<AccountBalance>("/account", { token });
|
||||
}
|
||||
listKeys(token: string) {
|
||||
return call<{ keys: ApiKeySummary[] }>("/keys", { token }).then((r) => r.keys);
|
||||
}
|
||||
createKey(token: string, label: string, limit_kind: "percent" | "hardcap", limit_value: number) {
|
||||
return call<CreatedKey>("/keys", {
|
||||
method: "POST",
|
||||
token,
|
||||
body: JSON.stringify({ label, limit_kind, limit_value }),
|
||||
});
|
||||
}
|
||||
async archiveKey(token: string, id: string) {
|
||||
await call(`/keys/${id}/archive`, { method: "POST", token, body: "{}" });
|
||||
}
|
||||
async updateKeyLimit(
|
||||
token: string,
|
||||
id: string,
|
||||
limit_kind: "percent" | "hardcap",
|
||||
limit_value: number,
|
||||
) {
|
||||
await call(`/keys/${id}/limit`, {
|
||||
method: "PATCH",
|
||||
token,
|
||||
body: JSON.stringify({ limit_kind, limit_value }),
|
||||
});
|
||||
}
|
||||
redeem(token: string, code: string) {
|
||||
return call<AccountBalance>("/redeem", {
|
||||
method: "POST",
|
||||
token,
|
||||
body: JSON.stringify({ code }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mock (VITE_USE_MOCK_ACCOUNT_API) ────────────────────────────────
|
||||
// Minimal in-memory account so the dashboard is fully developable offline.
|
||||
|
||||
class MockAccountApi implements AccountApi {
|
||||
private total = 1_000_000;
|
||||
private spent = 0;
|
||||
private reserved = 0;
|
||||
private keys: ApiKeySummary[] = [];
|
||||
private seq = 1;
|
||||
|
||||
async register() {}
|
||||
async verify() {}
|
||||
async login(): Promise<Session> {
|
||||
return { token: "mock-token", expires_in: 604800 };
|
||||
}
|
||||
async requestReset() {}
|
||||
async confirmReset() {}
|
||||
async account(): Promise<AccountBalance> {
|
||||
return {
|
||||
account_id: "mock-account",
|
||||
allocation_total: this.total,
|
||||
allocation_spent: this.spent,
|
||||
allocation_reserved: this.reserved,
|
||||
};
|
||||
}
|
||||
async listKeys(): Promise<ApiKeySummary[]> {
|
||||
return [...this.keys];
|
||||
}
|
||||
async createKey(
|
||||
_t: string,
|
||||
label: string,
|
||||
limit_kind: "percent" | "hardcap",
|
||||
limit_value: number,
|
||||
): Promise<CreatedKey> {
|
||||
const id = `mock-${this.seq++}`;
|
||||
const prefix = `sk-helexa-mock${this.seq}`;
|
||||
this.keys.push({
|
||||
id,
|
||||
prefix,
|
||||
label,
|
||||
status: "active",
|
||||
limit_kind,
|
||||
limit_value,
|
||||
spent: 0,
|
||||
reserved: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
return { id, key: `${prefix}-RAWSECRETSHOWNONCE`, prefix, limit_kind, limit_value };
|
||||
}
|
||||
async archiveKey(_t: string, id: string) {
|
||||
const k = this.keys.find((x) => x.id === id);
|
||||
if (k) k.status = "archived";
|
||||
}
|
||||
async updateKeyLimit(
|
||||
_t: string,
|
||||
id: string,
|
||||
limit_kind: "percent" | "hardcap",
|
||||
limit_value: number,
|
||||
) {
|
||||
const k = this.keys.find((x) => x.id === id);
|
||||
if (k) {
|
||||
k.limit_kind = limit_kind;
|
||||
k.limit_value = limit_value;
|
||||
}
|
||||
}
|
||||
async redeem(_t: string, code: string): Promise<AccountBalance> {
|
||||
if (!code.startsWith("helexa-topup-")) {
|
||||
throw new ApiError(400, "bad_request", "invalid or already-redeemed code");
|
||||
}
|
||||
this.total += 500_000;
|
||||
return this.account();
|
||||
}
|
||||
}
|
||||
|
||||
let instance: AccountApi | null = null;
|
||||
export function accountApi(): AccountApi {
|
||||
if (!instance) {
|
||||
instance = import.meta.env.VITE_USE_MOCK_ACCOUNT_API
|
||||
? new MockAccountApi()
|
||||
: new RealAccountApi();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
45
helexa.ai/src/api/types.ts
Normal file
45
helexa.ai/src/api/types.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// Wire types for the helexa-upstream /web/v1 account API (B4/B5).
|
||||
|
||||
export interface ApiKeySummary {
|
||||
id: string;
|
||||
prefix: string;
|
||||
label: string;
|
||||
status: "active" | "archived";
|
||||
limit_kind: "percent" | "hardcap";
|
||||
limit_value: number;
|
||||
spent: number;
|
||||
reserved: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreatedKey {
|
||||
id: string;
|
||||
/** Raw secret — shown exactly once at creation. */
|
||||
key: string;
|
||||
prefix: string;
|
||||
limit_kind: "percent" | "hardcap";
|
||||
limit_value: number;
|
||||
}
|
||||
|
||||
export interface AccountBalance {
|
||||
account_id: string;
|
||||
allocation_total: number;
|
||||
allocation_spent: number;
|
||||
allocation_reserved: number;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
token: string;
|
||||
expires_in: number;
|
||||
}
|
||||
|
||||
/** Typed error carrying the backend's machine-readable code. */
|
||||
export class ApiError extends Error {
|
||||
code: string;
|
||||
status: number;
|
||||
constructor(status: number, code: string, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
53
helexa.ai/src/auth/AuthProvider.tsx
Normal file
53
helexa.ai/src/auth/AuthProvider.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { accountApi } from "../api/account";
|
||||
import { claimAnonymousData } from "../data/repositories";
|
||||
import { getFingerprint } from "../lib/fingerprint";
|
||||
import { AuthContext } from "./context";
|
||||
|
||||
const TOKEN_KEY = "helexa.token";
|
||||
const EMAIL_KEY = "helexa.email";
|
||||
|
||||
export default function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [token, setToken] = useState<string | null>(() =>
|
||||
localStorage.getItem(TOKEN_KEY),
|
||||
);
|
||||
const [email, setEmail] = useState<string | null>(() =>
|
||||
localStorage.getItem(EMAIL_KEY),
|
||||
);
|
||||
|
||||
async function login(em: string, password: string): Promise<void> {
|
||||
const api = accountApi();
|
||||
const session = await api.login(em, password);
|
||||
localStorage.setItem(TOKEN_KEY, session.token);
|
||||
localStorage.setItem(EMAIL_KEY, em);
|
||||
setToken(session.token);
|
||||
setEmail(em);
|
||||
// Claim anonymous local history into the account (stays client-side).
|
||||
try {
|
||||
const acct = await api.account(session.token);
|
||||
await claimAnonymousData(acct.account_id);
|
||||
} catch {
|
||||
/* non-fatal */
|
||||
}
|
||||
}
|
||||
|
||||
async function register(em: string, password: string): Promise<void> {
|
||||
const fingerprint = await getFingerprint();
|
||||
await accountApi().register(em, password, fingerprint);
|
||||
}
|
||||
|
||||
function logout(): void {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(EMAIL_KEY);
|
||||
setToken(null);
|
||||
setEmail(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{ token, email, status: token ? "authed" : "anon", login, register, logout }}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
}
|
||||
14
helexa.ai/src/auth/RequireAuth.tsx
Normal file
14
helexa.ai/src/auth/RequireAuth.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type ReactNode } from "react";
|
||||
import { Navigate, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "./context";
|
||||
|
||||
/** Route guard: redirect unauthenticated users to /login?next=…. */
|
||||
export default function RequireAuth({ children }: { children: ReactNode }) {
|
||||
const { status } = useAuth();
|
||||
const location = useLocation();
|
||||
if (status !== "authed") {
|
||||
const next = encodeURIComponent(location.pathname + location.search);
|
||||
return <Navigate to={`/login?next=${next}`} replace />;
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
23
helexa.ai/src/auth/context.ts
Normal file
23
helexa.ai/src/auth/context.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export interface AuthContextValue {
|
||||
token: string | null;
|
||||
email: string | null;
|
||||
status: "anon" | "authed";
|
||||
login: (email: string, password: string) => Promise<void>;
|
||||
register: (email: string, password: string) => Promise<void>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue>({
|
||||
token: null,
|
||||
email: null,
|
||||
status: "anon",
|
||||
login: async () => {},
|
||||
register: async () => {},
|
||||
logout: () => {},
|
||||
});
|
||||
|
||||
export function useAuth(): AuthContextValue {
|
||||
return useContext(AuthContext);
|
||||
}
|
||||
@@ -6,11 +6,12 @@ import { useTheme } from "../layout/theme";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AUTONYM_MAP, type LanguageCode, isRtlLanguage } from "../i18n/languages";
|
||||
import { getLanguageOptionsByUsage } from "../i18n/translation-priority";
|
||||
import { useAuth } from "../auth/context";
|
||||
|
||||
/**
|
||||
* Top navigation: brand, primary routes (chat at `/`, `/mission`), an
|
||||
* auth-aware cluster (stubbed until F4 wires sessions), the theme toggle,
|
||||
* and the language selector.
|
||||
* auth-aware cluster (Account/Sign out when signed in, else Sign in/up),
|
||||
* the theme toggle, and the language selector.
|
||||
*
|
||||
* The language picker is ordered by **estimated usage**
|
||||
* (getLanguageOptionsByUsage), not alphabetically — a deliberate choice that
|
||||
@@ -21,6 +22,7 @@ import { getLanguageOptionsByUsage } from "../i18n/translation-priority";
|
||||
const Header: React.FC = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const { t, i18n } = useTranslation("common");
|
||||
const { status, logout } = useAuth();
|
||||
|
||||
const currentLanguage: LanguageCode = (i18n.language.split("-")[0] ||
|
||||
"en") as LanguageCode;
|
||||
@@ -75,13 +77,31 @@ const Header: React.FC = () => {
|
||||
</Nav>
|
||||
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{/* Auth cluster — plain links until F4 wires session state. */}
|
||||
<NavLink to="/login" className="nav-link">
|
||||
{t("nav.login")}
|
||||
</NavLink>
|
||||
<NavLink to="/register" className="nav-link">
|
||||
{t("nav.register")}
|
||||
</NavLink>
|
||||
{/* Auth-aware cluster. */}
|
||||
{status === "authed" ? (
|
||||
<>
|
||||
<NavLink to="/account" className="nav-link">
|
||||
{t("nav.account")}
|
||||
</NavLink>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline-secondary"
|
||||
onClick={logout}
|
||||
className="me-1"
|
||||
>
|
||||
{t("nav.logout")}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NavLink to="/login" className="nav-link">
|
||||
{t("nav.login")}
|
||||
</NavLink>
|
||||
<NavLink to="/register" className="nav-link">
|
||||
{t("nav.register")}
|
||||
</NavLink>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -13,131 +13,163 @@ import ruCommon from "./resources/ru/common.json";
|
||||
import enMission from "./resources/en/mission.json";
|
||||
import ruMission from "./resources/ru/mission.json";
|
||||
import enChat from "./resources/en/chat.json";
|
||||
import enAccount from "./resources/en/account.json";
|
||||
import ruChat from "./resources/ru/chat.json";
|
||||
import ruAccount from "./resources/ru/account.json";
|
||||
|
||||
// Scandinavian & Nordic languages
|
||||
import daCommon from "./resources/da/common.json";
|
||||
import daMission from "./resources/da/mission.json";
|
||||
import daChat from "./resources/da/chat.json";
|
||||
import daAccount from "./resources/da/account.json";
|
||||
|
||||
import fiCommon from "./resources/fi/common.json";
|
||||
import fiMission from "./resources/fi/mission.json";
|
||||
import fiChat from "./resources/fi/chat.json";
|
||||
import fiAccount from "./resources/fi/account.json";
|
||||
|
||||
import noCommon from "./resources/no/common.json";
|
||||
import noMission from "./resources/no/mission.json";
|
||||
import noChat from "./resources/no/chat.json";
|
||||
import noAccount from "./resources/no/account.json";
|
||||
|
||||
import svCommon from "./resources/sv/common.json";
|
||||
import svMission from "./resources/sv/mission.json";
|
||||
import svChat from "./resources/sv/chat.json";
|
||||
import svAccount from "./resources/sv/account.json";
|
||||
|
||||
import bgCommon from "./resources/bg/common.json";
|
||||
import bgMission from "./resources/bg/mission.json";
|
||||
import bgChat from "./resources/bg/chat.json";
|
||||
import bgAccount from "./resources/bg/account.json";
|
||||
|
||||
import etCommon from "./resources/et/common.json";
|
||||
import etMission from "./resources/et/mission.json";
|
||||
import etChat from "./resources/et/chat.json";
|
||||
import etAccount from "./resources/et/account.json";
|
||||
|
||||
// African & MENA languages
|
||||
import swCommon from "./resources/sw/common.json";
|
||||
import swMission from "./resources/sw/mission.json";
|
||||
import swChat from "./resources/sw/chat.json";
|
||||
import swAccount from "./resources/sw/account.json";
|
||||
|
||||
import arCommon from "./resources/ar/common.json";
|
||||
import arMission from "./resources/ar/mission.json";
|
||||
import arChat from "./resources/ar/chat.json";
|
||||
import arAccount from "./resources/ar/account.json";
|
||||
|
||||
import faCommon from "./resources/fa/common.json";
|
||||
import faMission from "./resources/fa/mission.json";
|
||||
import faChat from "./resources/fa/chat.json";
|
||||
import faAccount from "./resources/fa/account.json";
|
||||
|
||||
import haCommon from "./resources/ha/common.json";
|
||||
import haMission from "./resources/ha/mission.json";
|
||||
import haChat from "./resources/ha/chat.json";
|
||||
import haAccount from "./resources/ha/account.json";
|
||||
|
||||
import amCommon from "./resources/am/common.json";
|
||||
import amMission from "./resources/am/mission.json";
|
||||
import amChat from "./resources/am/chat.json";
|
||||
import amAccount from "./resources/am/account.json";
|
||||
|
||||
import yoCommon from "./resources/yo/common.json";
|
||||
import yoMission from "./resources/yo/mission.json";
|
||||
import yoChat from "./resources/yo/chat.json";
|
||||
import yoAccount from "./resources/yo/account.json";
|
||||
|
||||
import zuCommon from "./resources/zu/common.json";
|
||||
import zuMission from "./resources/zu/mission.json";
|
||||
import zuChat from "./resources/zu/chat.json";
|
||||
import zuAccount from "./resources/zu/account.json";
|
||||
|
||||
// Darija (Moroccan Arabic)
|
||||
import maCommon from "./resources/ma/common.json";
|
||||
import maMission from "./resources/ma/mission.json";
|
||||
import maChat from "./resources/ma/chat.json";
|
||||
import maAccount from "./resources/ma/account.json";
|
||||
|
||||
// European / other languages
|
||||
import esCommon from "./resources/es/common.json";
|
||||
import esMission from "./resources/es/mission.json";
|
||||
import esChat from "./resources/es/chat.json";
|
||||
import esAccount from "./resources/es/account.json";
|
||||
|
||||
import frCommon from "./resources/fr/common.json";
|
||||
import frMission from "./resources/fr/mission.json";
|
||||
import frChat from "./resources/fr/chat.json";
|
||||
import frAccount from "./resources/fr/account.json";
|
||||
|
||||
import deCommon from "./resources/de/common.json";
|
||||
import deMission from "./resources/de/mission.json";
|
||||
import deChat from "./resources/de/chat.json";
|
||||
import deAccount from "./resources/de/account.json";
|
||||
|
||||
import elCommon from "./resources/el/common.json";
|
||||
import elMission from "./resources/el/mission.json";
|
||||
import elChat from "./resources/el/chat.json";
|
||||
import elAccount from "./resources/el/account.json";
|
||||
|
||||
import itCommon from "./resources/it/common.json";
|
||||
import itMission from "./resources/it/mission.json";
|
||||
import itChat from "./resources/it/chat.json";
|
||||
import itAccount from "./resources/it/account.json";
|
||||
|
||||
import heCommon from "./resources/he/common.json";
|
||||
import heMission from "./resources/he/mission.json";
|
||||
import heChat from "./resources/he/chat.json";
|
||||
import heAccount from "./resources/he/account.json";
|
||||
|
||||
import ptCommon from "./resources/pt/common.json";
|
||||
import ptMission from "./resources/pt/mission.json";
|
||||
import ptChat from "./resources/pt/chat.json";
|
||||
import ptAccount from "./resources/pt/account.json";
|
||||
|
||||
import roCommon from "./resources/ro/common.json";
|
||||
import roMission from "./resources/ro/mission.json";
|
||||
import roChat from "./resources/ro/chat.json";
|
||||
import roAccount from "./resources/ro/account.json";
|
||||
|
||||
import kaCommon from "./resources/ka/common.json";
|
||||
import kaMission from "./resources/ka/mission.json";
|
||||
import kaChat from "./resources/ka/chat.json";
|
||||
import kaAccount from "./resources/ka/account.json";
|
||||
|
||||
import trCommon from "./resources/tr/common.json";
|
||||
import trMission from "./resources/tr/mission.json";
|
||||
import trChat from "./resources/tr/chat.json";
|
||||
import trAccount from "./resources/tr/account.json";
|
||||
|
||||
import plCommon from "./resources/pl/common.json";
|
||||
import plMission from "./resources/pl/mission.json";
|
||||
import plChat from "./resources/pl/chat.json";
|
||||
import plAccount from "./resources/pl/account.json";
|
||||
|
||||
import ukCommon from "./resources/uk/common.json";
|
||||
import ukMission from "./resources/uk/mission.json";
|
||||
import ukChat from "./resources/uk/chat.json";
|
||||
import ukAccount from "./resources/uk/account.json";
|
||||
|
||||
import nlCommon from "./resources/nl/common.json";
|
||||
import nlMission from "./resources/nl/mission.json";
|
||||
import nlChat from "./resources/nl/chat.json";
|
||||
import nlAccount from "./resources/nl/account.json";
|
||||
|
||||
import srCommon from "./resources/sr/common.json";
|
||||
import srMission from "./resources/sr/mission.json";
|
||||
import srChat from "./resources/sr/chat.json";
|
||||
import srAccount from "./resources/sr/account.json";
|
||||
|
||||
import kkCommon from "./resources/kk/common.json";
|
||||
import kkMission from "./resources/kk/mission.json";
|
||||
import kkChat from "./resources/kk/chat.json";
|
||||
import kkAccount from "./resources/kk/account.json";
|
||||
|
||||
import uzCommon from "./resources/uz/common.json";
|
||||
import uzMission from "./resources/uz/mission.json";
|
||||
import uzChat from "./resources/uz/chat.json";
|
||||
import uzAccount from "./resources/uz/account.json";
|
||||
|
||||
/**
|
||||
* Application translation resources, split by language and namespace.
|
||||
@@ -151,41 +183,49 @@ const resources: Resource = {
|
||||
common: enCommon,
|
||||
mission: enMission,
|
||||
chat: enChat,
|
||||
account: enAccount,
|
||||
},
|
||||
ru: {
|
||||
common: ruCommon,
|
||||
mission: ruMission,
|
||||
chat: ruChat,
|
||||
account: ruAccount,
|
||||
},
|
||||
bg: {
|
||||
common: bgCommon,
|
||||
mission: bgMission,
|
||||
chat: bgChat,
|
||||
account: bgAccount,
|
||||
},
|
||||
da: {
|
||||
common: daCommon,
|
||||
mission: daMission,
|
||||
chat: daChat,
|
||||
account: daAccount,
|
||||
},
|
||||
et: {
|
||||
common: etCommon,
|
||||
mission: etMission,
|
||||
chat: etChat,
|
||||
account: etAccount,
|
||||
},
|
||||
fi: {
|
||||
common: fiCommon,
|
||||
mission: fiMission,
|
||||
chat: fiChat,
|
||||
account: fiAccount,
|
||||
},
|
||||
kk: {
|
||||
common: kkCommon,
|
||||
mission: kkMission,
|
||||
chat: kkChat,
|
||||
account: kkAccount,
|
||||
},
|
||||
uz: {
|
||||
common: uzCommon,
|
||||
mission: uzMission,
|
||||
chat: uzChat,
|
||||
account: uzAccount,
|
||||
},
|
||||
|
||||
// African & MENA languages (LTR unless marked RTL via isRtlLanguage)
|
||||
@@ -193,41 +233,49 @@ const resources: Resource = {
|
||||
common: swCommon,
|
||||
mission: swMission,
|
||||
chat: swChat,
|
||||
account: swAccount,
|
||||
},
|
||||
ar: {
|
||||
common: arCommon,
|
||||
mission: arMission,
|
||||
chat: arChat,
|
||||
account: arAccount,
|
||||
},
|
||||
fa: {
|
||||
common: faCommon,
|
||||
mission: faMission,
|
||||
chat: faChat,
|
||||
account: faAccount,
|
||||
},
|
||||
ha: {
|
||||
common: haCommon,
|
||||
mission: haMission,
|
||||
chat: haChat,
|
||||
account: haAccount,
|
||||
},
|
||||
am: {
|
||||
common: amCommon,
|
||||
mission: amMission,
|
||||
chat: amChat,
|
||||
account: amAccount,
|
||||
},
|
||||
yo: {
|
||||
common: yoCommon,
|
||||
mission: yoMission,
|
||||
chat: yoChat,
|
||||
account: yoAccount,
|
||||
},
|
||||
zu: {
|
||||
common: zuCommon,
|
||||
mission: zuMission,
|
||||
chat: zuChat,
|
||||
account: zuAccount,
|
||||
},
|
||||
ma: {
|
||||
common: maCommon,
|
||||
mission: maMission,
|
||||
chat: maChat,
|
||||
account: maAccount,
|
||||
},
|
||||
|
||||
// European & other languages
|
||||
@@ -235,81 +283,97 @@ const resources: Resource = {
|
||||
common: esCommon,
|
||||
mission: esMission,
|
||||
chat: esChat,
|
||||
account: esAccount,
|
||||
},
|
||||
fr: {
|
||||
common: frCommon,
|
||||
mission: frMission,
|
||||
chat: frChat,
|
||||
account: frAccount,
|
||||
},
|
||||
de: {
|
||||
common: deCommon,
|
||||
mission: deMission,
|
||||
chat: deChat,
|
||||
account: deAccount,
|
||||
},
|
||||
el: {
|
||||
common: elCommon,
|
||||
mission: elMission,
|
||||
chat: elChat,
|
||||
account: elAccount,
|
||||
},
|
||||
it: {
|
||||
common: itCommon,
|
||||
mission: itMission,
|
||||
chat: itChat,
|
||||
account: itAccount,
|
||||
},
|
||||
he: {
|
||||
common: heCommon,
|
||||
mission: heMission,
|
||||
chat: heChat,
|
||||
account: heAccount,
|
||||
},
|
||||
pt: {
|
||||
common: ptCommon,
|
||||
mission: ptMission,
|
||||
chat: ptChat,
|
||||
account: ptAccount,
|
||||
},
|
||||
ro: {
|
||||
common: roCommon,
|
||||
mission: roMission,
|
||||
chat: roChat,
|
||||
account: roAccount,
|
||||
},
|
||||
ka: {
|
||||
common: kaCommon,
|
||||
mission: kaMission,
|
||||
chat: kaChat,
|
||||
account: kaAccount,
|
||||
},
|
||||
tr: {
|
||||
common: trCommon,
|
||||
mission: trMission,
|
||||
chat: trChat,
|
||||
account: trAccount,
|
||||
},
|
||||
pl: {
|
||||
common: plCommon,
|
||||
mission: plMission,
|
||||
chat: plChat,
|
||||
account: plAccount,
|
||||
},
|
||||
uk: {
|
||||
common: ukCommon,
|
||||
mission: ukMission,
|
||||
chat: ukChat,
|
||||
account: ukAccount,
|
||||
},
|
||||
nl: {
|
||||
common: nlCommon,
|
||||
mission: nlMission,
|
||||
chat: nlChat,
|
||||
account: nlAccount,
|
||||
},
|
||||
sr: {
|
||||
common: srCommon,
|
||||
mission: srMission,
|
||||
chat: srChat,
|
||||
account: srAccount,
|
||||
},
|
||||
no: {
|
||||
common: noCommon,
|
||||
mission: noMission,
|
||||
chat: noChat,
|
||||
account: noAccount,
|
||||
},
|
||||
sv: {
|
||||
common: svCommon,
|
||||
mission: svMission,
|
||||
chat: svChat,
|
||||
account: svAccount,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -335,7 +399,7 @@ i18n.use(initReactI18next).init({
|
||||
lng: browserLang,
|
||||
fallbackLng: "en",
|
||||
supportedLngs: SUPPORTED_LANGUAGES,
|
||||
ns: ["common", "mission", "chat"],
|
||||
ns: ["common", "mission", "chat", "account"],
|
||||
defaultNS: "common",
|
||||
// Because we control the keys and interpolate only simple values.
|
||||
interpolation: {
|
||||
|
||||
69
helexa.ai/src/i18n/resources/am/account.json
Normal file
69
helexa.ai/src/i18n/resources/am/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/ar/account.json
Normal file
69
helexa.ai/src/i18n/resources/ar/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/bg/account.json
Normal file
69
helexa.ai/src/i18n/resources/bg/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/da/account.json
Normal file
69
helexa.ai/src/i18n/resources/da/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/de/account.json
Normal file
69
helexa.ai/src/i18n/resources/de/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/el/account.json
Normal file
69
helexa.ai/src/i18n/resources/el/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/en/account.json
Normal file
69
helexa.ai/src/i18n/resources/en/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/es/account.json
Normal file
69
helexa.ai/src/i18n/resources/es/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/et/account.json
Normal file
69
helexa.ai/src/i18n/resources/et/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/fa/account.json
Normal file
69
helexa.ai/src/i18n/resources/fa/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/fi/account.json
Normal file
69
helexa.ai/src/i18n/resources/fi/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/fr/account.json
Normal file
69
helexa.ai/src/i18n/resources/fr/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/ha/account.json
Normal file
69
helexa.ai/src/i18n/resources/ha/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/he/account.json
Normal file
69
helexa.ai/src/i18n/resources/he/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/it/account.json
Normal file
69
helexa.ai/src/i18n/resources/it/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/ka/account.json
Normal file
69
helexa.ai/src/i18n/resources/ka/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/kk/account.json
Normal file
69
helexa.ai/src/i18n/resources/kk/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/ma/account.json
Normal file
69
helexa.ai/src/i18n/resources/ma/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/nl/account.json
Normal file
69
helexa.ai/src/i18n/resources/nl/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/no/account.json
Normal file
69
helexa.ai/src/i18n/resources/no/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/pl/account.json
Normal file
69
helexa.ai/src/i18n/resources/pl/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/pt/account.json
Normal file
69
helexa.ai/src/i18n/resources/pt/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/ro/account.json
Normal file
69
helexa.ai/src/i18n/resources/ro/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/ru/account.json
Normal file
69
helexa.ai/src/i18n/resources/ru/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/sr/account.json
Normal file
69
helexa.ai/src/i18n/resources/sr/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/sv/account.json
Normal file
69
helexa.ai/src/i18n/resources/sv/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/sw/account.json
Normal file
69
helexa.ai/src/i18n/resources/sw/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/tr/account.json
Normal file
69
helexa.ai/src/i18n/resources/tr/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/uk/account.json
Normal file
69
helexa.ai/src/i18n/resources/uk/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/uz/account.json
Normal file
69
helexa.ai/src/i18n/resources/uz/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/yo/account.json
Normal file
69
helexa.ai/src/i18n/resources/yo/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
69
helexa.ai/src/i18n/resources/zu/account.json
Normal file
69
helexa.ai/src/i18n/resources/zu/account.json
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"login": {
|
||||
"title": "Sign in",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign in",
|
||||
"noAccount": "No account? Sign up"
|
||||
},
|
||||
"register": {
|
||||
"title": "Create your account",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"submit": "Sign up",
|
||||
"haveAccount": "Already have an account? Sign in",
|
||||
"checkEmail": "Almost there — check your email to verify your account."
|
||||
},
|
||||
"verify": {
|
||||
"verifying": "Verifying…",
|
||||
"ok": "Email verified. You can now sign in.",
|
||||
"failed": "This verification link is invalid or has expired.",
|
||||
"toLogin": "Go to sign in"
|
||||
},
|
||||
"reset": {
|
||||
"requestTitle": "Reset your password",
|
||||
"email": "Email",
|
||||
"requestSubmit": "Send reset link",
|
||||
"requestDone": "If that email has an account, a reset link is on its way.",
|
||||
"confirmTitle": "Choose a new password",
|
||||
"newPassword": "New password",
|
||||
"confirmSubmit": "Set password",
|
||||
"ok": "Password updated. You can now sign in."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Account",
|
||||
"balance": "Allocation",
|
||||
"total": "Total",
|
||||
"spent": "Spent",
|
||||
"reserved": "Reserved",
|
||||
"remaining": "Remaining",
|
||||
"manageKeys": "Manage API keys",
|
||||
"redeemTitle": "Redeem a top-up code",
|
||||
"redeemPlaceholder": "helexa-topup-…",
|
||||
"redeem": "Redeem",
|
||||
"redeemed": "Code redeemed.",
|
||||
"logout": "Sign out"
|
||||
},
|
||||
"keys": {
|
||||
"title": "API keys",
|
||||
"create": "Create key",
|
||||
"label": "Label",
|
||||
"limitKind": "Limit",
|
||||
"percent": "% of allocation",
|
||||
"hardcap": "Hard cap (tokens)",
|
||||
"value": "Value",
|
||||
"none": "No keys yet.",
|
||||
"createdTitle": "Your new API key",
|
||||
"createdWarn": "Copy it now — you won't see it again.",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"archive": "Archive",
|
||||
"save": "Save",
|
||||
"status": "Status",
|
||||
"usage": "Used"
|
||||
},
|
||||
"error": {
|
||||
"generic": "Something went wrong.",
|
||||
"unauthorized": "Please sign in again."
|
||||
}
|
||||
}
|
||||
159
helexa.ai/src/pages/account/ApiKeys.tsx
Normal file
159
helexa.ai/src/pages/account/ApiKeys.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Alert, Badge, Button, Container, Form, Modal, Table } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../../auth/context";
|
||||
import { accountApi } from "../../api/account";
|
||||
import { ApiError, type ApiKeySummary, type CreatedKey } from "../../api/types";
|
||||
|
||||
type LimitKind = "percent" | "hardcap";
|
||||
|
||||
export default function ApiKeys() {
|
||||
const { t } = useTranslation("account");
|
||||
const { token, logout } = useAuth();
|
||||
const [keys, setKeys] = useState<ApiKeySummary[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Create-key form state.
|
||||
const [label, setLabel] = useState("");
|
||||
const [limitKind, setLimitKind] = useState<LimitKind>("percent");
|
||||
const [limitValue, setLimitValue] = useState(100);
|
||||
const [created, setCreated] = useState<CreatedKey | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
setKeys(await accountApi().listKeys(token));
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) logout();
|
||||
else setError(t("error.generic"));
|
||||
}
|
||||
}, [token, logout, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// load() is async; setState happens after await, not synchronously.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function create(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!token) return;
|
||||
setError(null);
|
||||
try {
|
||||
const key = await accountApi().createKey(token, label, limitKind, limitValue);
|
||||
setCreated(key);
|
||||
setLabel("");
|
||||
await load();
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : t("error.generic"));
|
||||
}
|
||||
}
|
||||
|
||||
async function archive(id: string) {
|
||||
if (!token) return;
|
||||
await accountApi().archiveKey(token, id);
|
||||
await load();
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 860 }}>
|
||||
<h1 className="h3 mb-4">{t("keys.title")}</h1>
|
||||
{error && <Alert variant="warning">{error}</Alert>}
|
||||
|
||||
<Form onSubmit={create} className="surface-elevated p-3 rounded-3 mb-4">
|
||||
<div className="row g-2 align-items-end">
|
||||
<div className="col">
|
||||
<Form.Label className="small">{t("keys.label")}</Form.Label>
|
||||
<Form.Control value={label} onChange={(e) => setLabel(e.target.value)} />
|
||||
</div>
|
||||
<div className="col">
|
||||
<Form.Label className="small">{t("keys.limitKind")}</Form.Label>
|
||||
<Form.Select
|
||||
value={limitKind}
|
||||
onChange={(e) => setLimitKind(e.target.value as LimitKind)}
|
||||
>
|
||||
<option value="percent">{t("keys.percent")}</option>
|
||||
<option value="hardcap">{t("keys.hardcap")}</option>
|
||||
</Form.Select>
|
||||
</div>
|
||||
<div className="col">
|
||||
<Form.Label className="small">{t("keys.value")}</Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min={0}
|
||||
value={limitValue}
|
||||
onChange={(e) => setLimitValue(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
<Button type="submit">{t("keys.create")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
{keys.length === 0 ? (
|
||||
<p className="text-muted">{t("keys.none")}</p>
|
||||
) : (
|
||||
<Table responsive hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("keys.label")}</th>
|
||||
<th>Prefix</th>
|
||||
<th>{t("keys.limitKind")}</th>
|
||||
<th>{t("keys.usage")}</th>
|
||||
<th>{t("keys.status")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{keys.map((k) => (
|
||||
<tr key={k.id}>
|
||||
<td>{k.label || "—"}</td>
|
||||
<td>
|
||||
<code>{k.prefix}…</code>
|
||||
</td>
|
||||
<td>
|
||||
{k.limit_kind === "percent" ? `${k.limit_value}%` : k.limit_value.toLocaleString()}
|
||||
</td>
|
||||
<td>{k.spent.toLocaleString()}</td>
|
||||
<td>
|
||||
<Badge bg={k.status === "active" ? "success" : "secondary"}>{k.status}</Badge>
|
||||
</td>
|
||||
<td className="text-end">
|
||||
{k.status === "active" && (
|
||||
<Button size="sm" variant="outline-danger" onClick={() => void archive(k.id)}>
|
||||
{t("keys.archive")}
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
{/* The raw key is shown exactly once. */}
|
||||
<Modal show={!!created} onHide={() => setCreated(null)} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title className="h6">{t("keys.createdTitle")}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Alert variant="warning" className="py-2">{t("keys.createdWarn")}</Alert>
|
||||
<div className="d-flex gap-2">
|
||||
<Form.Control readOnly value={created?.key ?? ""} />
|
||||
<Button
|
||||
variant="outline-secondary"
|
||||
onClick={() => {
|
||||
if (created) void navigator.clipboard.writeText(created.key);
|
||||
setCopied(true);
|
||||
}}
|
||||
>
|
||||
{copied ? t("keys.copied") : t("keys.copy")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
104
helexa.ai/src/pages/account/Dashboard.tsx
Normal file
104
helexa.ai/src/pages/account/Dashboard.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Alert, Button, Card, Container, Form, ProgressBar } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../../auth/context";
|
||||
import { accountApi } from "../../api/account";
|
||||
import { ApiError, type AccountBalance } from "../../api/types";
|
||||
|
||||
export default function Dashboard() {
|
||||
const { t } = useTranslation("account");
|
||||
const { token, logout } = useAuth();
|
||||
const [balance, setBalance] = useState<AccountBalance | null>(null);
|
||||
const [code, setCode] = useState("");
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!token) return;
|
||||
try {
|
||||
setBalance(await accountApi().account(token));
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError && err.status === 401) logout();
|
||||
else setError(t("error.generic"));
|
||||
}
|
||||
}, [token, logout, t]);
|
||||
|
||||
useEffect(() => {
|
||||
// load() is async; setState happens after await, not synchronously.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
async function redeem(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!token) return;
|
||||
setError(null);
|
||||
setMsg(null);
|
||||
try {
|
||||
setBalance(await accountApi().redeem(token, code.trim()));
|
||||
setCode("");
|
||||
setMsg(t("dashboard.redeemed"));
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : t("error.generic"));
|
||||
}
|
||||
}
|
||||
|
||||
const remaining = balance
|
||||
? balance.allocation_total - balance.allocation_spent - balance.allocation_reserved
|
||||
: 0;
|
||||
const pct = balance && balance.allocation_total > 0
|
||||
? Math.round(((balance.allocation_spent + balance.allocation_reserved) / balance.allocation_total) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 720 }}>
|
||||
<div className="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 className="h3 mb-0">{t("dashboard.title")}</h1>
|
||||
<Button variant="outline-secondary" size="sm" onClick={logout}>
|
||||
{t("dashboard.logout")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="surface-elevated mb-4">
|
||||
<Card.Body>
|
||||
<Card.Title className="h6 text-uppercase text-muted">
|
||||
{t("dashboard.balance")}
|
||||
</Card.Title>
|
||||
{balance && (
|
||||
<>
|
||||
<ProgressBar now={pct} className="my-3" />
|
||||
<div className="d-flex justify-content-between small">
|
||||
<span>{t("dashboard.total")}: {balance.allocation_total.toLocaleString()}</span>
|
||||
<span>{t("dashboard.spent")}: {balance.allocation_spent.toLocaleString()}</span>
|
||||
<span>{t("dashboard.reserved")}: {balance.allocation_reserved.toLocaleString()}</span>
|
||||
<span>{t("dashboard.remaining")}: {remaining.toLocaleString()}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Link to="/account/keys" className="btn btn-primary btn-sm mt-3">
|
||||
{t("dashboard.manageKeys")}
|
||||
</Link>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
<Card className="surface-elevated">
|
||||
<Card.Body>
|
||||
<Card.Title className="h6">{t("dashboard.redeemTitle")}</Card.Title>
|
||||
{msg && <Alert variant="success" className="py-2">{msg}</Alert>}
|
||||
{error && <Alert variant="warning" className="py-2">{error}</Alert>}
|
||||
<Form onSubmit={redeem} className="d-flex gap-2">
|
||||
<Form.Control
|
||||
value={code}
|
||||
placeholder={t("dashboard.redeemPlaceholder")}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
/>
|
||||
<Button type="submit" disabled={!code.trim()}>
|
||||
{t("dashboard.redeem")}
|
||||
</Button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
64
helexa.ai/src/pages/auth/Login.tsx
Normal file
64
helexa.ai/src/pages/auth/Login.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Alert, Button, Container, Form } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../../auth/context";
|
||||
import { ApiError } from "../../api/types";
|
||||
|
||||
export default function Login() {
|
||||
const { t } = useTranslation("account");
|
||||
const { login } = useAuth();
|
||||
const nav = useNavigate();
|
||||
const [params] = useSearchParams();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await login(email, password);
|
||||
nav(params.get("next") || "/account", { replace: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : t("error.generic"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 420 }}>
|
||||
<h1 className="h3 mb-4">{t("login.title")}</h1>
|
||||
{error && <Alert variant="warning">{error}</Alert>}
|
||||
<Form onSubmit={submit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t("login.email")}</Form.Label>
|
||||
<Form.Control
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t("login.password")}</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button type="submit" disabled={busy} className="w-100">
|
||||
{t("login.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
<p className="mt-3 small">
|
||||
<Link to="/register">{t("login.noAccount")}</Link>
|
||||
</p>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
70
helexa.ai/src/pages/auth/Register.tsx
Normal file
70
helexa.ai/src/pages/auth/Register.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Alert, Button, Container, Form } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAuth } from "../../auth/context";
|
||||
import { ApiError } from "../../api/types";
|
||||
|
||||
export default function Register() {
|
||||
const { t } = useTranslation("account");
|
||||
const { register } = useAuth();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await register(email, password);
|
||||
setDone(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : t("error.generic"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 420 }}>
|
||||
<h1 className="h3 mb-4">{t("register.title")}</h1>
|
||||
{done ? (
|
||||
<Alert variant="success">{t("register.checkEmail")}</Alert>
|
||||
) : (
|
||||
<>
|
||||
{error && <Alert variant="warning">{error}</Alert>}
|
||||
<Form onSubmit={submit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t("register.email")}</Form.Label>
|
||||
<Form.Control
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t("register.password")}</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
minLength={8}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button type="submit" disabled={busy} className="w-100">
|
||||
{t("register.submit")}
|
||||
</Button>
|
||||
</Form>
|
||||
<p className="mt-3 small">
|
||||
<Link to="/login">{t("register.haveAccount")}</Link>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
48
helexa.ai/src/pages/auth/RequestReset.tsx
Normal file
48
helexa.ai/src/pages/auth/RequestReset.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState } from "react";
|
||||
import { Alert, Button, Container, Form } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { accountApi } from "../../api/account";
|
||||
|
||||
export default function RequestReset() {
|
||||
const { t } = useTranslation("account");
|
||||
const [email, setEmail] = useState("");
|
||||
const [done, setDone] = useState(false);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setBusy(true);
|
||||
// Always succeeds from the UI's view (no account enumeration).
|
||||
try {
|
||||
await accountApi().requestReset(email);
|
||||
} catch {
|
||||
/* swallow */
|
||||
}
|
||||
setDone(true);
|
||||
setBusy(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 420 }}>
|
||||
<h1 className="h3 mb-4">{t("reset.requestTitle")}</h1>
|
||||
{done ? (
|
||||
<Alert variant="info">{t("reset.requestDone")}</Alert>
|
||||
) : (
|
||||
<Form onSubmit={submit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t("reset.email")}</Form.Label>
|
||||
<Form.Control
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button type="submit" disabled={busy} className="w-100">
|
||||
{t("reset.requestSubmit")}
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
64
helexa.ai/src/pages/auth/ResetPassword.tsx
Normal file
64
helexa.ai/src/pages/auth/ResetPassword.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { Alert, Button, Container, Form } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { accountApi } from "../../api/account";
|
||||
import { ApiError } from "../../api/types";
|
||||
|
||||
export default function ResetPassword() {
|
||||
const { t } = useTranslation("account");
|
||||
const [params] = useSearchParams();
|
||||
const [password, setPassword] = useState("");
|
||||
const [done, setDone] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const token = params.get("token");
|
||||
if (!token) {
|
||||
setError(t("verify.failed"));
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await accountApi().confirmReset(token, password);
|
||||
setDone(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : t("error.generic"));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 420 }}>
|
||||
<h1 className="h3 mb-4">{t("reset.confirmTitle")}</h1>
|
||||
{done ? (
|
||||
<Alert variant="success">
|
||||
{t("reset.ok")} <Link to="/login">{t("verify.toLogin")}</Link>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
{error && <Alert variant="warning">{error}</Alert>}
|
||||
<Form onSubmit={submit}>
|
||||
<Form.Group className="mb-3">
|
||||
<Form.Label>{t("reset.newPassword")}</Form.Label>
|
||||
<Form.Control
|
||||
type="password"
|
||||
minLength={8}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button type="submit" disabled={busy} className="w-100">
|
||||
{t("reset.confirmSubmit")}
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
40
helexa.ai/src/pages/auth/VerifyEmail.tsx
Normal file
40
helexa.ai/src/pages/auth/VerifyEmail.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import { Alert, Container, Spinner } from "react-bootstrap";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { accountApi } from "../../api/account";
|
||||
|
||||
export default function VerifyEmail() {
|
||||
const { t } = useTranslation("account");
|
||||
const [params] = useSearchParams();
|
||||
const [state, setState] = useState<"verifying" | "ok" | "failed">("verifying");
|
||||
|
||||
useEffect(() => {
|
||||
const token = params.get("token");
|
||||
// Keep all setState in async callbacks (no synchronous setState in the
|
||||
// effect body): a missing token resolves to a rejected promise.
|
||||
const run = token ? accountApi().verify(token) : Promise.reject(new Error("no token"));
|
||||
run.then(() => setState("ok")).catch(() => setState("failed"));
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<Container className="py-5 flex-grow-1" style={{ maxWidth: 480 }}>
|
||||
{state === "verifying" && (
|
||||
<p>
|
||||
<Spinner size="sm" className="me-2" />
|
||||
{t("verify.verifying")}
|
||||
</p>
|
||||
)}
|
||||
{state === "ok" && (
|
||||
<Alert variant="success">
|
||||
{t("verify.ok")} <Link to="/login">{t("verify.toLogin")}</Link>
|
||||
</Alert>
|
||||
)}
|
||||
{state === "failed" && (
|
||||
<Alert variant="warning">
|
||||
{t("verify.failed")} <Link to="/login">{t("verify.toLogin")}</Link>
|
||||
</Alert>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
109
rpm/helexa-upstream-prerelease.spec
Normal file
109
rpm/helexa-upstream-prerelease.spec
Normal file
@@ -0,0 +1,109 @@
|
||||
# Prebuilt-binary spec for helexa-upstream.
|
||||
#
|
||||
# Wraps a pre-built `helexa-upstream` binary produced by an upstream CI
|
||||
# job and packages it for rpm.lair.cafe. The %build phase is a no-op.
|
||||
# helexa-upstream is a pure-Rust, non-CUDA daemon: the mesh account +
|
||||
# budget authority. It serves an inbound HTTP API on tcp/8090 (/authz/v1
|
||||
# for cortex, /web/v1 for the frontend) — hence the firewalld service —
|
||||
# and connects out to PostgreSQL (the system of record), running schema
|
||||
# migrations on startup.
|
||||
#
|
||||
# Required defines at rpmbuild time:
|
||||
# upstream_version e.g. "0.1.16"
|
||||
# upstream_prerelease e.g. "0.1.20260518140530.gitabcdef0"
|
||||
# ^^^^^^^^^^^^^^^^^^ ^^^^^^^^
|
||||
# commit time (sec) commit sha
|
||||
# (used as Release; the timestamp prefix
|
||||
# keeps same-day builds strictly ordered.)
|
||||
|
||||
%global _build_id_links none
|
||||
%global debug_package %{nil}
|
||||
%global __strip /usr/bin/true
|
||||
|
||||
%{!?upstream_version: %global upstream_version 0.0.0}
|
||||
%if 0%{?upstream_prerelease:1}
|
||||
%global upstream_release %{upstream_prerelease}
|
||||
%else
|
||||
%global upstream_release 1
|
||||
%endif
|
||||
|
||||
Name: helexa-upstream
|
||||
Version: %{upstream_version}
|
||||
Release: %{upstream_release}%{?dist}
|
||||
Summary: Mesh account + budget authority for helexa (prebuilt)
|
||||
|
||||
License: GPL-3.0-or-later
|
||||
URL: https://git.lair.cafe/helexa/helexa
|
||||
|
||||
Source0: helexa-upstream
|
||||
Source1: helexa-upstream.service
|
||||
Source2: helexa-upstream-sysusers.conf
|
||||
Source3: helexa-upstream.example.toml
|
||||
Source4: LICENSE
|
||||
Source5: helexa-upstream-firewalld.xml
|
||||
|
||||
Requires: firewalld-filesystem
|
||||
|
||||
ExclusiveArch: x86_64
|
||||
|
||||
Requires(pre): shadow-utils
|
||||
Requires: systemd
|
||||
|
||||
Provides: user(helexa-upstream)
|
||||
|
||||
%description
|
||||
helexa-upstream is the mesh-level authority: it issues user accounts and
|
||||
API keys, holds the allocation ledger (free grant + redeemable top-up
|
||||
codes), enforces per-key budgets via a transactional reserve/settle
|
||||
contract, and reconciles served usage for operator compensation. cortex
|
||||
validates locally-unrecognised keys against its /authz/v1 surface
|
||||
(fail-closed); the helexa.ai frontend drives account self-service via
|
||||
/web/v1. PostgreSQL is the system of record; the schema is migrated on
|
||||
startup.
|
||||
|
||||
%prep
|
||||
cp %{SOURCE0} ./helexa-upstream
|
||||
cp %{SOURCE1} .
|
||||
cp %{SOURCE2} .
|
||||
cp %{SOURCE3} .
|
||||
cp %{SOURCE4} .
|
||||
cp %{SOURCE5} .
|
||||
|
||||
%build
|
||||
# Already built in the upstream CI build job.
|
||||
|
||||
%install
|
||||
install -Dm755 helexa-upstream %{buildroot}%{_bindir}/helexa-upstream
|
||||
install -Dm644 helexa-upstream.service %{buildroot}%{_unitdir}/helexa-upstream.service
|
||||
install -Dm644 helexa-upstream-sysusers.conf %{buildroot}%{_sysusersdir}/helexa-upstream.conf
|
||||
install -Dm644 helexa-upstream-firewalld.xml %{buildroot}%{_prefix}/lib/firewalld/services/helexa-upstream.xml
|
||||
install -dm755 %{buildroot}%{_sysconfdir}/helexa-upstream
|
||||
install -Dm644 helexa-upstream.example.toml %{buildroot}%{_sysconfdir}/helexa-upstream/helexa-upstream.toml
|
||||
|
||||
%pre
|
||||
getent group helexa-upstream >/dev/null || groupadd -r helexa-upstream
|
||||
getent passwd helexa-upstream >/dev/null || \
|
||||
useradd -r -g helexa-upstream -d /var/lib/helexa-upstream -s /sbin/nologin \
|
||||
-c "helexa-upstream authority" helexa-upstream
|
||||
|
||||
%post
|
||||
%systemd_post helexa-upstream.service
|
||||
|
||||
%preun
|
||||
%systemd_preun helexa-upstream.service
|
||||
|
||||
%postun
|
||||
%systemd_postun_with_restart helexa-upstream.service
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%{_bindir}/helexa-upstream
|
||||
%{_unitdir}/helexa-upstream.service
|
||||
%{_sysusersdir}/helexa-upstream.conf
|
||||
%{_prefix}/lib/firewalld/services/helexa-upstream.xml
|
||||
%dir %{_sysconfdir}/helexa-upstream
|
||||
%config(noreplace) %{_sysconfdir}/helexa-upstream/helexa-upstream.toml
|
||||
|
||||
%changelog
|
||||
* Mon Jun 23 2026 Gitea Actions <actions@git.lair.cafe> - %{upstream_version}-%{upstream_release}
|
||||
- Prerelease build from upstream CI binary.
|
||||
Reference in New Issue
Block a user