Compare commits
3 Commits
codex/anal
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 524de66ad3 | |||
| 63349a17ed | |||
| ff5b92986f |
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from "hono";
|
import { Hono } from "hono";
|
||||||
import { desc, eq, sql, inArray } from "drizzle-orm";
|
import { desc, eq, sql } from "drizzle-orm";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useDrizzle } from "@/lib/db.js";
|
import { useDrizzle } from "@/lib/db.js";
|
||||||
import { chargePoint, connector } from "@/db/schema.js";
|
import { chargePoint, connector } from "@/db/schema.js";
|
||||||
@@ -81,12 +81,12 @@ app.post("/", async (c) => {
|
|||||||
if (body.feePerKwh !== undefined && (!Number.isInteger(body.feePerKwh) || body.feePerKwh < 0)) {
|
if (body.feePerKwh !== undefined && (!Number.isInteger(body.feePerKwh) || body.feePerKwh < 0)) {
|
||||||
return c.json({ error: "feePerKwh must be a non-negative integer" }, 400);
|
return c.json({ error: "feePerKwh must be a non-negative integer" }, 400);
|
||||||
}
|
}
|
||||||
if (body.pricingMode !== undefined && !['fixed', 'tou'].includes(body.pricingMode)) {
|
if (body.pricingMode !== undefined && !["fixed", "tou"].includes(body.pricingMode)) {
|
||||||
return c.json({ error: "pricingMode must be 'fixed' or 'tou'" }, 400);
|
return c.json({ error: "pricingMode must be 'fixed' or 'tou'" }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const plainPassword = generateOcppPassword()
|
const plainPassword = generateOcppPassword();
|
||||||
const passwordHash = await hashOcppPassword(plainPassword)
|
const passwordHash = await hashOcppPassword(plainPassword);
|
||||||
|
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(chargePoint)
|
.insert(chargePoint)
|
||||||
@@ -175,21 +175,19 @@ app.patch("/:id", async (c) => {
|
|||||||
}
|
}
|
||||||
set.registrationStatus = body.registrationStatus as "Accepted" | "Pending" | "Rejected";
|
set.registrationStatus = body.registrationStatus as "Accepted" | "Pending" | "Rejected";
|
||||||
}
|
}
|
||||||
if (body.chargePointVendor !== undefined) set.chargePointVendor = body.chargePointVendor.trim() || "Unknown";
|
if (body.chargePointVendor !== undefined)
|
||||||
if (body.chargePointModel !== undefined) set.chargePointModel = body.chargePointModel.trim() || "Unknown";
|
set.chargePointVendor = body.chargePointVendor.trim() || "Unknown";
|
||||||
|
if (body.chargePointModel !== undefined)
|
||||||
|
set.chargePointModel = body.chargePointModel.trim() || "Unknown";
|
||||||
if ("deviceName" in body) set.deviceName = body.deviceName?.trim() || null;
|
if ("deviceName" in body) set.deviceName = body.deviceName?.trim() || null;
|
||||||
if (body.pricingMode !== undefined) {
|
if (body.pricingMode !== undefined) {
|
||||||
if (!['fixed', 'tou'].includes(body.pricingMode)) {
|
if (!["fixed", "tou"].includes(body.pricingMode)) {
|
||||||
return c.json({ error: "pricingMode must be 'fixed' or 'tou'" }, 400);
|
return c.json({ error: "pricingMode must be 'fixed' or 'tou'" }, 400);
|
||||||
}
|
}
|
||||||
set.pricingMode = body.pricingMode;
|
set.pricingMode = body.pricingMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [updated] = await db
|
const [updated] = await db.update(chargePoint).set(set).where(eq(chargePoint.id, id)).returning();
|
||||||
.update(chargePoint)
|
|
||||||
.set(set)
|
|
||||||
.where(eq(chargePoint.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!updated) return c.json({ error: "Not found" }, 404);
|
if (!updated) return c.json({ error: "Not found" }, 404);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { desc, eq } from "drizzle-orm";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { useDrizzle } from "@/lib/db.js";
|
import { useDrizzle } from "@/lib/db.js";
|
||||||
import { idTag } from "@/db/schema.js";
|
import { idTag } from "@/db/schema.js";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { HonoEnv } from "@/types/hono.ts";
|
import type { HonoEnv } from "@/types/hono.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
|||||||
dayjs().diff(dayjs(cp.lastHeartbeatAt), "second") < (cp.heartbeatInterval ?? 60) * 3;
|
dayjs().diff(dayjs(cp.lastHeartbeatAt), "second") < (cp.heartbeatInterval ?? 60) * 3;
|
||||||
const commandChannelUnavailable = cp?.transportStatus === "unavailable";
|
const commandChannelUnavailable = cp?.transportStatus === "unavailable";
|
||||||
const statusLabel = isOnline ? "在线" : commandChannelUnavailable ? "通道异常" : "离线";
|
const statusLabel = isOnline ? "在线" : commandChannelUnavailable ? "通道异常" : "离线";
|
||||||
const statusDotClass = isOnline
|
const transportStatusDotClass = isOnline
|
||||||
? "bg-success animate-pulse"
|
? "bg-success animate-pulse"
|
||||||
: commandChannelUnavailable
|
: commandChannelUnavailable
|
||||||
? "bg-warning"
|
? "bg-warning"
|
||||||
@@ -253,7 +253,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
|||||||
</Chip>
|
</Chip>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span
|
<span
|
||||||
className={`size-2 rounded-full ${statusDotClass}`}
|
className={`size-2 rounded-full ${transportStatusDotClass}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-muted">{statusLabel}</span>
|
<span className="text-xs text-muted">{statusLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -445,7 +445,7 @@ export default function ChargePointDetailPage({ params }: { params: Promise<{ id
|
|||||||
<dd>
|
<dd>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span
|
<span
|
||||||
className={`size-2 rounded-full ${statusDotClass}`}
|
className={`size-2 rounded-full ${transportStatusDotClass}`}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm text-foreground">{statusLabel}</span>
|
<span className="text-sm text-foreground">{statusLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -184,7 +184,6 @@ export default function PricingPage() {
|
|||||||
const [activeTier, setActiveTier] = useState<PriceTier>("peak");
|
const [activeTier, setActiveTier] = useState<PriceTier>("peak");
|
||||||
const [isDirty, setIsDirty] = useState(false);
|
const [isDirty, setIsDirty] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [showPayload, setShowPayload] = useState(false);
|
|
||||||
|
|
||||||
// Populate state once remote tariff loads
|
// Populate state once remote tariff loads
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -286,7 +285,6 @@ export default function PricingPage() {
|
|||||||
|
|
||||||
// ── Derived values ───────────────────────────────────────────────────────
|
// ── Derived values ───────────────────────────────────────────────────────
|
||||||
const slots = scheduleToSlots(schedule);
|
const slots = scheduleToSlots(schedule);
|
||||||
const apiPayload: TariffConfig = { slots, prices };
|
|
||||||
|
|
||||||
// ── Admin gate ───────────────────────────────────────────────────────────
|
// ── Admin gate ───────────────────────────────────────────────────────────
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Controls,
|
Controls,
|
||||||
Handle,
|
Handle,
|
||||||
MiniMap,
|
MiniMap,
|
||||||
Panel,
|
|
||||||
Position,
|
Position,
|
||||||
type Node,
|
type Node,
|
||||||
type Edge,
|
type Edge,
|
||||||
@@ -27,7 +26,8 @@ type ConnectionStatus = "online" | "stale" | "offline";
|
|||||||
|
|
||||||
function getStatus(cp: ChargePoint, connected: string[]): ConnectionStatus {
|
function getStatus(cp: ChargePoint, connected: string[]): ConnectionStatus {
|
||||||
if (cp.transportStatus === "unavailable") return "stale";
|
if (cp.transportStatus === "unavailable") return "stale";
|
||||||
if (cp.transportStatus !== "online" || !connected.includes(cp.chargePointIdentifier)) return "offline";
|
if (cp.transportStatus !== "online" || !connected.includes(cp.chargePointIdentifier))
|
||||||
|
return "offline";
|
||||||
if (!cp.lastHeartbeatAt) return "stale";
|
if (!cp.lastHeartbeatAt) return "stale";
|
||||||
return dayjs().diff(dayjs(cp.lastHeartbeatAt), "minute") < 5 ? "online" : "stale";
|
return dayjs().diff(dayjs(cp.lastHeartbeatAt), "minute") < 5 ? "online" : "stale";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import {
|
import { CreditCard, Gear, TagDollar, Thunderbolt, Xmark, Bars } from "@gravity-ui/icons";
|
||||||
CreditCard,
|
|
||||||
Gear,
|
|
||||||
ListCheck,
|
|
||||||
Person,
|
|
||||||
PlugConnection,
|
|
||||||
TagDollar,
|
|
||||||
Thunderbolt,
|
|
||||||
ThunderboltFill,
|
|
||||||
Xmark,
|
|
||||||
Bars,
|
|
||||||
} from "@gravity-ui/icons";
|
|
||||||
import SidebarFooter from "@/components/sidebar-footer";
|
import SidebarFooter from "@/components/sidebar-footer";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import { useSession } from "@/lib/auth-client";
|
||||||
import { EvCharger, Gauge, Network, ReceiptText, UserCog, Users } from "lucide-react";
|
import { EvCharger, Gauge, Network, ReceiptText, UserCog, Users } from "lucide-react";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const CSMS_INTERNAL_URL =
|
|||||||
process.env.CSMS_INTERNAL_URL ?? process.env.NEXT_PUBLIC_CSMS_URL ?? "http://localhost:3001";
|
process.env.CSMS_INTERNAL_URL ?? process.env.NEXT_PUBLIC_CSMS_URL ?? "http://localhost:3001";
|
||||||
|
|
||||||
/** 检查 CSMS 是否已完成初始化(有用户存在)。 */
|
/** 检查 CSMS 是否已完成初始化(有用户存在)。 */
|
||||||
async function isInitialized(request: NextRequest): Promise<boolean> {
|
async function isInitialized(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${CSMS_INTERNAL_URL}/api/setup`, {
|
const res = await fetch(`${CSMS_INTERNAL_URL}/api/setup`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -25,7 +25,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
|
|
||||||
// /setup 页面:已初始化则跳转登录
|
// /setup 页面:已初始化则跳转登录
|
||||||
if (pathname === "/setup") {
|
if (pathname === "/setup") {
|
||||||
if (await isInitialized(request)) {
|
if (await isInitialized()) {
|
||||||
return NextResponse.redirect(new URL("/login", request.url));
|
return NextResponse.redirect(new URL("/login", request.url));
|
||||||
}
|
}
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
@@ -33,7 +33,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
|
|
||||||
// /dashboard 路由:检查 session,未登录跳转 /login
|
// /dashboard 路由:检查 session,未登录跳转 /login
|
||||||
if (pathname.startsWith("/dashboard")) {
|
if (pathname.startsWith("/dashboard")) {
|
||||||
if (!(await isInitialized(request))) {
|
if (!(await isInitialized())) {
|
||||||
return NextResponse.redirect(new URL("/setup", request.url));
|
return NextResponse.redirect(new URL("/setup", request.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export async function proxy(request: NextRequest) {
|
|||||||
|
|
||||||
// /login 路由:未初始化则跳转 /setup
|
// /login 路由:未初始化则跳转 /setup
|
||||||
if (pathname === "/login") {
|
if (pathname === "/login") {
|
||||||
if (!(await isInitialized(request))) {
|
if (!(await isInitialized())) {
|
||||||
return NextResponse.redirect(new URL("/setup", request.url));
|
return NextResponse.redirect(new URL("/setup", request.url));
|
||||||
}
|
}
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
|
|||||||
Reference in New Issue
Block a user