Files
altdock/app/lib/dashboard.ts
T
amania-jailbreak 54cb335d66 api: フロントエンドのAPI URLを本番ドメインにハードコード
シングルユーザー運用のため env フォールバックを廃止し、
APIオリジンを https://api.altdock.app.amania.jp に固定した。
- dashboard.ts / PublicSourceView.tsx の localhost:4000 フォールバックを削除
- API側 config.ts の PUBLIC_BASE_URL/WEB_ORIGIN デフォルトも本番ドメインに
- compose/web の NEXT_PUBLIC_API_BASE_URL 環境変数を削除(コード固定のため)
2026-08-05 14:07:18 +09:00

107 lines
3.7 KiB
TypeScript

import type { AppRecord, ReleaseRecord, SourceRecord } from "@/packages/core/src/index";
export type DashboardSource = {
source: SourceRecord;
apps: AppRecord[];
releases: ReleaseRecord[];
};
export type DashboardData = {
workspace: {
name: string;
slug: string;
};
sources: DashboardSource[];
usage: {
storageBytes: number;
sourceCount: number;
};
limits: {
maxSources: number;
maxAppsPerSource: number;
maxStorageBytes: number;
maxUploadBytes: number;
};
};
export type Notice = {
tone: "success" | "error" | "info";
text: string;
};
export type Visibility = SourceRecord["visibility"];
// Single-user deployment: hard-code the public API origin. The API is served
// at api.altdock.app.amania.jp; there is no per-environment fallback anymore.
export const API_BASE = "https://api.altdock.app.amania.jp";
export const AUTH_MODE = process.env.NEXT_PUBLIC_AUTH_MODE || "demo";
export const DEMO_HEADERS = { "x-demo-user": "demo@altdock.local" };
export async function api<T>(path: string, init: RequestInit = {}) {
const headers = new Headers(init.headers);
headers.set("accept", "application/json");
if (init.body) headers.set("content-type", "application/json");
Object.entries(DEMO_HEADERS).forEach(([key, value]) => headers.set(key, value));
const response = await fetch(`${API_BASE}${path}`, { ...init, headers, credentials: "include" });
const body = (await response.json().catch(() => ({}))) as T & { error?: string; detail?: string };
if (!response.ok) throw new Error(body.detail || body.error || `Request failed (${response.status})`);
return body;
}
export function formatBytes(value: number) {
if (!value) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const index = Math.min(Math.floor(Math.log(value) / Math.log(1024)), units.length - 1);
return `${(value / 1024 ** index).toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
}
export function formatDate(value: string) {
return new Intl.DateTimeFormat("ja-JP", { year: "numeric", month: "short", day: "numeric" }).format(new Date(value));
}
export function formatDateTime(value: string) {
return new Intl.DateTimeFormat("ja-JP", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }).format(new Date(value));
}
export function statusLabel(status: ReleaseRecord["status"]) {
return {
uploaded: "アップロード済み",
processing: "処理中",
ready: "公開準備完了",
rejected: "要修正",
published: "公開中",
archived: "アーカイブ",
}[status];
}
export function statusTone(status: ReleaseRecord["status"]) {
return {
uploaded: "neutral",
processing: "info",
ready: "warning",
rejected: "danger",
published: "success",
archived: "neutral",
}[status] as "neutral" | "info" | "warning" | "danger" | "success";
}
export function visibilityLabel(visibility: Visibility) {
return { draft: "下書き", unlisted: "限定公開", public: "公開" }[visibility];
}
export function visibilityTone(visibility: Visibility) {
return { draft: "neutral", unlisted: "warning", public: "success" }[visibility] as "neutral" | "warning" | "success";
}
export function flattenSources(dashboard: DashboardData) {
return dashboard.sources;
}
export function flattenApps(dashboard: DashboardData) {
return dashboard.sources.flatMap((entry) => entry.apps.map((app) => ({ app, source: entry.source, releases: entry.releases.filter((release) => release.appId === app.id) })));
}
export function flattenReleases(dashboard: DashboardData) {
return dashboard.sources.flatMap((entry) => entry.releases.map((release) => ({ release, source: entry.source, app: entry.apps.find((app) => app.id === release.appId) })));
}