54 changed files with 523 additions and 126 deletions
+28 -7
View File
@@ -57,13 +57,34 @@ jobs:
docker push mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG & docker push mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG &
wait wait
- name: Deploy to production - name: Install kubectl
run: |
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
curl -sLO "https://dl.k8s.io/release/v1.35.0/bin/linux/${ARCH}/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/kubectl
- name: Configure kubectl
run: |
kubectl config set-cluster buzz --server=https://100.75.220.33:6443 --insecure-skip-tls-verify=true
kubectl config set-credentials ci-deployer --token=${{ secrets.KUBE_TOKEN }}
kubectl config set-context buzz --cluster=buzz --user=ci-deployer --namespace=buzz
kubectl config use-context buzz
- name: Deploy to k3s
run: | run: |
TAG=${{ gitea.sha }} TAG=${{ gitea.sha }}
docker service update \ # DB migration
--image mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG \ kubectl delete job db-migrate -n buzz --ignore-not-found
buzz_app kubectl create job db-migrate \
docker service update \ --image=mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG \
--image mts.dgnr.us:5000/${{ gitea.repository }}:backend-$TAG \ -n buzz \
buzz_backend -- bun dr push
kubectl wait --for=condition=complete job/db-migrate -n buzz --timeout=180s
# Roll app & backend
kubectl set image deployment/app app=mts.dgnr.us:5000/${{ gitea.repository }}:frontend-$TAG -n buzz
kubectl set image deployment/backend backend=mts.dgnr.us:5000/${{ gitea.repository }}:backend-$TAG -n buzz
kubectl rollout status deployment/app -n buzz --timeout=180s
kubectl rollout status deployment/backend -n buzz --timeout=180s
+1 -15
View File
@@ -21,21 +21,6 @@ COPY patches ./patches
# Install into cache, then persist into real node_modules # Install into cache, then persist into real node_modules
RUN --mount=type=cache,target=/root/.bun bun install --frozen-lockfile RUN --mount=type=cache,target=/root/.bun bun install --frozen-lockfile
### Stage N: drizzle ###
FROM oven/bun:canary-alpine as drizzle
# Isolation
RUN adduser -Du 1001 container
USER container
WORKDIR /home/container
COPY --from=deps /home/container/node_modules ./node_modules
COPY --from=deps /home/container/package*.json ./
COPY --from=deps /home/container/bun.lock ./
COPY --chown=1001 ./drizzle.config.ts ./.env ./
COPY --chown=1001 ./lib/db ./lib/db
RUN bun dr push
### Stage 2: builder ### ### Stage 2: builder ###
FROM oven/bun:canary-alpine AS builder FROM oven/bun:canary-alpine AS builder
@@ -86,5 +71,6 @@ COPY --from=builder /home/container/util ./util
COPY --from=builder /home/container/lib ./lib COPY --from=builder /home/container/lib ./lib
COPY --from=builder /home/container/tsconfig.json ./tsconfig.json COPY --from=builder /home/container/tsconfig.json ./tsconfig.json
COPY --from=builder /home/container/.version ./.version COPY --from=builder /home/container/.version ./.version
COPY --from=builder /home/container/drizzle.config.ts ./drizzle.config.ts
CMD ["bun", "start"] CMD ["bun", "start"]
+6 -6
View File
@@ -6,6 +6,7 @@ import Link from "next/link";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { MaybeWrap } from "@/components/action-button";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { import {
@@ -23,6 +24,11 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { import {
SidebarMenu, SidebarMenu,
@@ -40,12 +46,6 @@ import { setLimit, toggleCheck, toggleLock } from "@/lib/api";
import { shared } from "@/lib/comms"; import { shared } from "@/lib/comms";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { MaybeWrap } from "@/components/action-button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
export function SidebarLink({ export function SidebarLink({
submission, submission,
+2 -2
View File
@@ -1,3 +1,4 @@
import { isNotNull, sql } from "drizzle-orm";
import { Dice3, Trash2 } from "lucide-react"; import { Dice3, Trash2 } from "lucide-react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { import {
@@ -27,9 +28,8 @@ import {
import { getArtifactConfig, random, wipe } from "@/lib/api"; import { getArtifactConfig, random, wipe } from "@/lib/api";
import { adminCheck } from "@/lib/auth"; import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { LimitManager, SubmissionList, Watcher } from "./client";
import { isNotNull, sql } from "drizzle-orm";
import { submissions } from "@/lib/db/schema"; import { submissions } from "@/lib/db/schema";
import { LimitManager, SubmissionList, Watcher } from "./client";
export const metadata = { export const metadata = {
title: "เสือกไอดีชาวบ้าน (แอดมิน)", title: "เสือกไอดีชาวบ้าน (แอดมิน)",
+2 -2
View File
@@ -2,7 +2,9 @@ import { PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import z from "zod"; import z from "zod";
import { ActionButton } from "@/components/action-button";
import { FormAction, FormInput, FormProvider } from "@/components/form"; import { FormAction, FormInput, FormProvider } from "@/components/form";
import { formParse } from "@/components/form-submit";
import { ModalBase } from "@/components/modal"; import { ModalBase } from "@/components/modal";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { DialogClose, DialogFooter } from "@/components/ui/dialog"; import { DialogClose, DialogFooter } from "@/components/ui/dialog";
@@ -12,9 +14,7 @@ import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth"; import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { settings } from "@/lib/db/schema"; import { settings } from "@/lib/db/schema";
import { formParse } from "@/components/form-submit";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { ActionButton } from "@/components/action-button";
import { resetGoal } from "../../api"; import { resetGoal } from "../../api";
const Schema = z.object({ const Schema = z.object({
+2 -2
View File
@@ -1,12 +1,12 @@
"use server"; "use server";
import { eq } from "drizzle-orm";
import { adminCheck } from "@/lib/auth"; import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { donations, settings } from "@/lib/db/schema"; import { donations, settings } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { fileToDataUrl } from "@/lib/utils";
import { eq } from "drizzle-orm";
import { getPostHogClient } from "@/lib/posthog-server"; import { getPostHogClient } from "@/lib/posthog-server";
import { fileToDataUrl } from "@/lib/utils";
export async function resetGoal() { export async function resetGoal() {
if (!(await adminCheck())) throw new Error("Unauthorized"); if (!(await adminCheck())) throw new Error("Unauthorized");
+14 -14
View File
@@ -1,13 +1,5 @@
"use client"; "use client";
import { DataTable } from "@/components/tantable";
import { SimpleTooltip } from "@/components/tooltip";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import type { donations } from "@/lib/db/schema";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import { import {
BitcoinIcon, BitcoinIcon,
@@ -18,15 +10,23 @@ import {
ImageIcon, ImageIcon,
MessageCircleWarning, MessageCircleWarning,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import posthog from "posthog-js";
import { useEffect } from "react";
import { useEllipsisVisible } from "react-hook-text-overflow"; import { useEllipsisVisible } from "react-hook-text-overflow";
import { DataTable } from "@/components/tantable";
import { SimpleTooltip } from "@/components/tooltip";
import { Button } from "@/components/ui/button";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import type { donations } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints";
import { ActionButton } from "../../../../components/action-button"; import { ActionButton } from "../../../../components/action-button";
import { getImage, reloadWidget, resendPopup, testPopup } from "./api"; import { getImage, reloadWidget, resendPopup, testPopup } from "./api";
import { useEffect } from "react";
import posthog from "posthog-js";
import { sse } from "@/lib/db/sse-endpoints";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import Link from "next/link";
const columns: ColumnDef<typeof donations.$inferSelect>[] = [ const columns: ColumnDef<typeof donations.$inferSelect>[] = [
{ accessorKey: "name", header: "Name", meta: { className: "w-50 truncate" } }, { accessorKey: "name", header: "Name", meta: { className: "w-50 truncate" } },
+1 -1
View File
@@ -1,6 +1,6 @@
import { ErrorModal } from "@/components/error";
import { ErrorBoundary } from "next/dist/client/components/error-boundary"; import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { ErrorModal } from "@/components/error";
export default async function DonateAdminLayout({ export default async function DonateAdminLayout({
children, children,
+5 -5
View File
@@ -1,10 +1,10 @@
import { db } from "@/lib/db";
import { DonateAdminPage } from "./client";
import { donations } from "@/lib/db/schema";
import { desc, getTableColumns, sql } from "drizzle-orm"; import { desc, getTableColumns, sql } from "drizzle-orm";
import { adminCheck } from "@/lib/auth";
import { redirect } from "next/navigation";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { redirect } from "next/navigation";
import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db";
import { donations } from "@/lib/db/schema";
import { DonateAdminPage } from "./client";
export default async function () { export default async function () {
if (!(await adminCheck())) if (!(await adminCheck()))
+3 -3
View File
@@ -1,9 +1,11 @@
import { sql } from "drizzle-orm";
import { QrCodeIcon, SendIcon } from "lucide-react"; import { QrCodeIcon, SendIcon } from "lucide-react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import z from "zod"; import z from "zod";
import { th } from "zod/v4/locales"; import { th } from "zod/v4/locales";
import PromptpayImage from "#/assets/promptpay.jpg"; import PromptpayImage from "#/assets/promptpay.jpg";
import TruemoneyIcon from "#/assets/tmn.webp"; import TruemoneyIcon from "#/assets/tmn.webp";
import DonateLogo from "#/logos/donate.webp";
import Cropper from "@/components/cropper"; import Cropper from "@/components/cropper";
import { import {
FormAction, FormAction,
@@ -27,15 +29,13 @@ import { db } from "@/lib/db";
import { donations, endgameSlips, submissions } from "@/lib/db/schema"; import { donations, endgameSlips, submissions } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { checkSlip } from "@/lib/payment"; import { checkSlip } from "@/lib/payment";
import { getPostHogClient } from "@/lib/posthog-server";
import { fileToDataUrl } from "@/lib/utils"; import { fileToDataUrl } from "@/lib/utils";
import { import {
CurrencyInput, CurrencyInput,
SlipUpload, SlipUpload,
} from "../rubgram/admin/@modal/manual/client"; } from "../rubgram/admin/@modal/manual/client";
import { DownloadButton } from "../rubgram/client"; import { DownloadButton } from "../rubgram/client";
import { getPostHogClient } from "@/lib/posthog-server";
import { sql } from "drizzle-orm";
import DonateLogo from "#/logos/donate.webp";
const { TMN_DEST_PHONE_NUM, SASTIFY_API_PRIVKEY } = process.env as Record< const { TMN_DEST_PHONE_NUM, SASTIFY_API_PRIVKEY } = process.env as Record<
string, string,
+1 -1
View File
@@ -1,5 +1,6 @@
import type { SQL } from "drizzle-orm"; import type { SQL } from "drizzle-orm";
import { asc, desc, max, sql, sum } from "drizzle-orm"; import { asc, desc, max, sql, sum } from "drizzle-orm";
import type { Metadata } from "next";
import { Suspense } from "react"; import { Suspense } from "react";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
@@ -7,7 +8,6 @@ import { donations } from "@/lib/db/schema";
import { fileToDataUrl } from "@/lib/utils"; import { fileToDataUrl } from "@/lib/utils";
import { Podium } from "./podium"; import { Podium } from "./podium";
import { TopDonateTable } from "./table"; import { TopDonateTable } from "./table";
import type { Metadata } from "next";
type NumberString = `${number}`; type NumberString = `${number}`;
type NumberLike = number | NumberString; type NumberLike = number | NumberString;
+1 -1
View File
@@ -1,3 +1,4 @@
import { Anuphan } from "next/font/google";
import { ViewTransition } from "react"; import { ViewTransition } from "react";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import Background from "#/bg.webp"; import Background from "#/bg.webp";
@@ -5,7 +6,6 @@ import Image from "@/components/image";
import { StarsRenderer } from "@/components/stars"; import { StarsRenderer } from "@/components/stars";
import Providers, { VersionCheck } from "../client"; import Providers, { VersionCheck } from "../client";
import { Navbar } from "./navbar"; import { Navbar } from "./navbar";
import { Anuphan } from "next/font/google";
const anuphan = Anuphan({ const anuphan = Anuphan({
variable: "--font-anuphan", variable: "--font-anuphan",
+1 -1
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { Loader2, UserLock } from "lucide-react"; import { Loader2, UserLock } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { type FormEvent, use, useEffect, useState } from "react"; import { type FormEvent, use, useEffect, useState } from "react";
@@ -11,7 +12,6 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client"; import { authClient } from "@/lib/auth-client";
import { Brash } from "./brash"; import { Brash } from "./brash";
import Link from "next/link";
export default function LoginPage({ export default function LoginPage({
searchParams, searchParams,
+1 -1
View File
@@ -3,10 +3,10 @@ import Link from "next/link";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import Banner from "#/banner_t.webp"; import Banner from "#/banner_t.webp";
import ArtifactLogo from "#/logos/artifact.webp"; import ArtifactLogo from "#/logos/artifact.webp";
import DonateLogo from "#/logos/donate.webp";
import GuideLogo from "#/logos/guide.webp"; import GuideLogo from "#/logos/guide.webp";
import RubgramLogo from "#/logos/rubgram.webp"; import RubgramLogo from "#/logos/rubgram.webp";
import TierlistLogo from "#/logos/tierlist.webp"; import TierlistLogo from "#/logos/tierlist.webp";
import DonateLogo from "#/logos/donate.webp";
import Image from "@/components/image"; import Image from "@/components/image";
import { SimpleTooltip } from "@/components/tooltip"; import { SimpleTooltip } from "@/components/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -4,15 +4,16 @@
import { FileSearch2, RefreshCw, X } from "lucide-react"; import { FileSearch2, RefreshCw, X } from "lucide-react";
import { type ComponentProps, useEffect, useRef, useState } from "react"; import { type ComponentProps, useEffect, useRef, useState } from "react";
import { b2sClient } from "@/app/(ui)/admin/cdn/table"; import { b2sClient } from "@/app/(ui)/admin/cdn/table";
import { VirtualizedComboBox } from "@/components/combobox";
import { useFormContext } from "@/components/form"; import { useFormContext } from "@/components/form";
import { SimpleTooltip } from "@/components/tooltip"; import { SimpleTooltip } from "@/components/tooltip";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { Input } from "@/components/ui/input";
import { import {
InputGroup, InputGroup,
InputGroupAddon, InputGroupAddon,
InputGroupInput, InputGroupInput,
} from "@/components/ui/input-group"; } from "@/components/ui/input-group";
import type { Input } from "@/components/ui/input";
import { Kbd } from "@/components/ui/kbd"; import { Kbd } from "@/components/ui/kbd";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -23,9 +24,8 @@ import {
MultiSelectTrigger, MultiSelectTrigger,
MultiSelectValue, MultiSelectValue,
} from "@/components/ui/multi-select"; } from "@/components/ui/multi-select";
import { cn } from "@/lib/utils";
import { VirtualizedComboBox } from "@/components/combobox";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
export function CurrencyInput(props: ComponentProps<typeof Input>) { export function CurrencyInput(props: ComponentProps<typeof Input>) {
return ( return (
+12 -12
View File
@@ -2,6 +2,8 @@ import { PlusIcon } from "lucide-react";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import z from "zod"; import z from "zod";
import { th } from "zod/v4/locales";
import { getDiscordUsers } from "@/app/api/discord/users/api";
import { import {
FormAction, FormAction,
FormInput, FormInput,
@@ -12,6 +14,15 @@ import { ModalBase } from "@/components/modal";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { DialogClose, DialogFooter } from "@/components/ui/dialog"; import { DialogClose, DialogFooter } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Spinner } from "@/components/ui/spinner"; import { Spinner } from "@/components/ui/spinner";
import { actionLog } from "@/lib/api"; import { actionLog } from "@/lib/api";
import { adminCheck } from "@/lib/auth"; import { adminCheck } from "@/lib/auth";
@@ -22,19 +33,8 @@ import {
endgameSubmissions, endgameSubmissions,
endgameTypes, endgameTypes,
} from "@/lib/db/schema"; } from "@/lib/db/schema";
import { CurrencyInput, ServiceSelect, SlipUpload, UserSelect } from "./client";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { SlipokResponse } from "../../../api"; import type { SlipokResponse } from "../../../api";
import { getDiscordUsers } from "@/app/api/discord/users/api"; import { CurrencyInput, ServiceSelect, SlipUpload, UserSelect } from "./client";
import { th } from "zod/v4/locales";
const Schema = z.object({ const Schema = z.object({
name: z.string(), name: z.string(),
+2 -1
View File
@@ -16,7 +16,6 @@ import Link from "next/link";
import { useMemo, useTransition } from "react"; import { useMemo, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { SimpleTooltip } from "@/components/tooltip"; import { SimpleTooltip } from "@/components/tooltip";
import { toggleMonth } from "../../api";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -34,6 +33,8 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { toggleMonth } from "../../api";
type Row = { type Row = {
id: string; id: string;
name: string; name: string;
+1 -1
View File
@@ -1,5 +1,5 @@
import { endOfMonth, startOfMonth } from "date-fns";
import { and, between, eq, getTableColumns } from "drizzle-orm"; import { and, between, eq, getTableColumns } from "drizzle-orm";
import { startOfMonth, endOfMonth } from "date-fns";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { adminCheck } from "@/lib/auth"; import { adminCheck } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
+4 -4
View File
@@ -1,5 +1,9 @@
import { sql } from "drizzle-orm";
import { Dice5, PlusIcon, Trash2 } from "lucide-react"; import { Dice5, PlusIcon, Trash2 } from "lucide-react";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { ErrorModal } from "@/components/error";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -34,10 +38,6 @@ import {
SubmissionList, SubmissionList,
Watcher, Watcher,
} from "./client"; } from "./client";
import { sql } from "drizzle-orm";
import Link from "next/link";
import { ErrorBoundary } from "next/dist/client/components/error-boundary";
import { ErrorModal } from "@/components/error";
export default async function AdminLayout({ export default async function AdminLayout({
modal, modal,
+2 -1
View File
@@ -17,8 +17,9 @@ import {
settings, settings,
} from "@/lib/db/schema"; } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { getPostHogClient } from "@/lib/posthog-server";
import { checkSlip } from "@/lib/payment"; import { checkSlip } from "@/lib/payment";
import { getPostHogClient } from "@/lib/posthog-server";
const { DISCORD_WEBHOOK_URL, DISCORD_CLIENT_ID, BASE_URL } = const { DISCORD_WEBHOOK_URL, DISCORD_CLIENT_ID, BASE_URL } =
process.env as Record<string, string>; process.env as Record<string, string>;
+1 -2
View File
@@ -449,8 +449,7 @@ export function TierList({
> >
<span> <span>
{type.name} เวอร์ชั่น {version.name} ระดับ {type.name} เวอร์ชั่น {version.name} ระดับ
<span className="text-yellow-400">{type.mode}</span>{" "} <span className="text-yellow-400">{type.mode}</span> ใช้ได้ถึง{" "}
ใช้ได้ถึง{" "}
</span> </span>
<span className="text-green-400">{version.deprecates}</span> <span className="text-green-400">{version.deprecates}</span>
{deleteMode && ( {deleteMode && (
-2
View File
@@ -1,7 +1,5 @@
import { getAmberVh } from "@/lib/api"; import { getAmberVh } from "@/lib/api";
export const revalidate = 3600;
export async function GET() { export async function GET() {
return new Response( return new Response(
await fetch( await fetch(
-2
View File
@@ -1,7 +1,5 @@
import { getAmberVh } from "@/lib/api"; import { getAmberVh } from "@/lib/api";
export const revalidate = 3600;
export async function GET() { export async function GET() {
return new Response( return new Response(
await fetch( await fetch(
+2 -2
View File
@@ -2,9 +2,9 @@
import { env } from "node:process"; import { env } from "node:process";
import Snowflake, { GatewayIntents } from "@sfjs/snowflake"; import Snowflake, { GatewayIntents } from "@sfjs/snowflake";
import { redis } from "@/lib/db/redis";
import { adminCheck } from "@/lib/auth";
import { forbidden } from "next/navigation"; import { forbidden } from "next/navigation";
import { adminCheck } from "@/lib/auth";
import { redis } from "@/lib/db/redis";
const bot = new Snowflake({ const bot = new Snowflake({
token: env.DISCORD_BOT_TOKEN || "", token: env.DISCORD_BOT_TOKEN || "",
+3 -3
View File
@@ -1,10 +1,10 @@
import { and, asc, not, sql } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { donations } from "@/lib/db/schema"; import { donations } from "@/lib/db/schema";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { fileToDataUrl } from "@/lib/utils";
import { and, asc, not, sql } from "drizzle-orm";
import type { NextRequest } from "next/server";
import { getPostHogClient } from "@/lib/posthog-server"; import { getPostHogClient } from "@/lib/posthog-server";
import { fileToDataUrl } from "@/lib/utils";
async function runResume() { async function runResume() {
const [havent] = await db const [havent] = await db
+3 -2
View File
@@ -1,9 +1,10 @@
import { GoogleGenAI } from "@google/genai"; import { GoogleGenAI } from "@google/genai";
import { hash } from "bun";
import { PassThrough, Readable } from "stream";
import wav from "wav"; import wav from "wav";
import { Readable, PassThrough } from "stream";
import z from "zod"; import z from "zod";
import { redis } from "@/lib/db/redis"; import { redis } from "@/lib/db/redis";
import { hash } from "bun";
const { DONATE_WIDGET_KEY, GEMINI_TTS_API_KEY } = process.env as Record< const { DONATE_WIDGET_KEY, GEMINI_TTS_API_KEY } = process.env as Record<
string, string,
string string
+1 -5
View File
@@ -3,11 +3,7 @@ import { notFound } from "next/navigation";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { cdn } from "@/lib/db/schema"; import { cdn } from "@/lib/db/schema";
export const revalidate = 86400; // 24 hours export const dynamic = "force-dynamic";
export async function generateStaticParams() {
return await db.select({ id: cdn.id }).from(cdn);
}
export async function GET( export async function GET(
_req: Request, _req: Request,
+2 -1
View File
@@ -1,4 +1,6 @@
// oxlint-disable tailwindcss/no-unknown-classes // oxlint-disable tailwindcss/no-unknown-classes
import { isNotNull, sql } from "drizzle-orm";
import { Kanit } from "next/font/google"; import { Kanit } from "next/font/google";
import { Watcher } from "@/app/(ui)/artifact/admin/client"; import { Watcher } from "@/app/(ui)/artifact/admin/client";
import { VersionCheck } from "@/app/client"; import { VersionCheck } from "@/app/client";
@@ -6,7 +8,6 @@ import AutoFitText from "@/components/fit";
import { getArtifactConfig } from "@/lib/api"; import { getArtifactConfig } from "@/lib/api";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { submissions } from "@/lib/db/schema"; import { submissions } from "@/lib/db/schema";
import { isNotNull, sql } from "drizzle-orm";
const kanit = Kanit({ const kanit = Kanit({
variable: "--font-kanit", variable: "--font-kanit",
+1 -1
View File
@@ -1,8 +1,8 @@
"use server"; "use server";
import { eq } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { donations } from "@/lib/db/schema"; import { donations } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
import { getPostHogClient } from "@/lib/posthog-server"; import { getPostHogClient } from "@/lib/posthog-server";
export async function markRunning(id: string) { export async function markRunning(id: string) {
+1 -1
View File
@@ -1,9 +1,9 @@
"use server"; "use server";
import { gt, sum } from "drizzle-orm";
import { getArtifactConfig } from "@/lib/api"; import { getArtifactConfig } from "@/lib/api";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { donations, settings } from "@/lib/db/schema"; import { donations, settings } from "@/lib/db/schema";
import { gt, sum } from "drizzle-orm";
export async function getDonateBar() { export async function getDonateBar() {
const { donateGoalStarting } = await getArtifactConfig(); const { donateGoalStarting } = await getArtifactConfig();
+3 -3
View File
@@ -1,10 +1,10 @@
"use client"; "use client";
import { sse } from "@/lib/db/sse-endpoints";
import { useEffect, useRef, useState } from "react";
import { getDonateBar } from "./api";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { useEffect, useRef, useState } from "react";
import { sse } from "@/lib/db/sse-endpoints";
import { pausePass } from "@/lib/utils"; import { pausePass } from "@/lib/utils";
import { getDonateBar } from "./api";
type Data = Awaited<ReturnType<typeof getDonateBar>>; type Data = Awaited<ReturnType<typeof getDonateBar>>;
+4 -4
View File
@@ -1,13 +1,13 @@
"use client"; "use client";
import Image from "next/image";
import { AnimatePresence, motion } from "motion/react"; import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { VersionCheck } from "@/app/client";
import { cn } from "@/lib/utils";
import { sse } from "@/lib/db/sse-endpoints";
import DefaultImage from "#/favicon.webp"; import DefaultImage from "#/favicon.webp";
import { VersionCheck } from "@/app/client";
import { sse } from "@/lib/db/sse-endpoints";
import { cn } from "@/lib/utils";
import { markDone, markRunning } from "./api"; import { markDone, markRunning } from "./api";
type DonateData = { type DonateData = {
+1 -1
View File
@@ -1,8 +1,8 @@
"use server"; "use server";
import { asc, desc, max, sum } from "drizzle-orm";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { donations } from "@/lib/db/schema"; import { donations } from "@/lib/db/schema";
import { asc, desc, max, sum } from "drizzle-orm";
export async function getTopDonate() { export async function getTopDonate() {
const [top] = await db const [top] = await db
+1 -1
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import { sse } from "@/lib/db/sse-endpoints";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { sse } from "@/lib/db/sse-endpoints";
import { getTopDonate } from "./api"; import { getTopDonate } from "./api";
type Data = Awaited<ReturnType<typeof getTopDonate>>; type Data = Awaited<ReturnType<typeof getTopDonate>>;
+2 -2
View File
@@ -1,12 +1,12 @@
"use client"; "use client";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import type { MaybePromise } from "bun"; import type { MaybePromise } from "bun";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import type { ComponentProps, ReactNode } from "react"; import type { ComponentProps, ReactNode } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export function ActionButton({ export function ActionButton({
action, action,
+1 -1
View File
@@ -1,8 +1,8 @@
"use client"; "use client";
import { useVirtualizer } from "@tanstack/react-virtual";
import { Check, ChevronsUpDown } from "lucide-react"; import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Command, Command,
+1 -2
View File
@@ -9,8 +9,6 @@ import {
ZoomOutIcon, ZoomOutIcon,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useFileUpload } from "@/hooks/use-file-upload";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Cropper, Cropper,
@@ -27,6 +25,7 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Slider } from "@/components/ui/slider"; import { Slider } from "@/components/ui/slider";
import { useFileUpload } from "@/hooks/use-file-upload";
// Define type for pixel crop area // Define type for pixel crop area
type Area = { x: number; y: number; width: number; height: number }; type Area = { x: number; y: number; width: number; height: number };
+2 -2
View File
@@ -1,14 +1,14 @@
// oxlint-disable react-hooks/exhaustive-deps // oxlint-disable react-hooks/exhaustive-deps
"use client"; "use client";
import { Divide } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import type { FormSubmitResult } from "./form-submit";
import { DialogClose } from "./ui/dialog"; import { DialogClose } from "./ui/dialog";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
import { Divide } from "lucide-react";
import type { FormSubmitResult } from "./form-submit";
const AUTOSAVE_TTL_MS = 10 * 60 * 1000; const AUTOSAVE_TTL_MS = 10 * 60 * 1000;
const STORAGE_PREFIX = "form:autosave:"; const STORAGE_PREFIX = "form:autosave:";
+137
View File
@@ -0,0 +1,137 @@
# Buzz Events — k3s Deployment
## Architecture Decisions
### Ingress
- **k3s built-in Traefik**, not nginx.
- Annotations `traefik.ingress.kubernetes.io/response-buffering: "false"` + `read-timeout: "200"` preserve SSE (Server-Sent Events).
- Traefik Middleware `sse-optimizer` caps `maxRequestBodyBytes` at 100 MB (for slip image uploads).
- Standard `networking.k8s.io/v1` Ingress resource (not Traefik CRD IngressRoute) for portability.
- External port: **80/443** (standard). `m.dgnr.us` DNS should point at k3s node IP(s). Old port 8604 is retired.
### Namespace
- Everything lives in namespace `buzz`.
- The namespace is created once manually (`kubectl create ns buzz`) and listed in kustomization.yaml.
### Secrets
- **`buzz-env`** Secret created once from `.env`:
```
kubectl create secret generic buzz-env --from-env-file=.env -n buzz
```
- All Deployments and the migration Job reference it via `envFrom.secretRef`.
- Update by deleting and recreating (or editing in place):
```
kubectl delete secret buzz-env -n buzz && kubectl create secret generic buzz-env --from-env-file=.env -n buzz
```
### Database Migration
- **k8s Job** (deploy-time), not in Dockerfile.
- The `drizzle` stage was removed from `Dockerfile`. The build no longer runs `bun dr push`.
- CI creates the `db-migrate` Job imperatively, waits for completion, then updates deployments.
- The Job uses the frontend image (has drizzle config) and `command: ["bun", "dr", "push"]`.
- Old job is deleted before creating the new one (`kubectl delete job ... --ignore-not-found`).
### Service Discovery
- Frontend Service: `app` (ClusterIP, port 3000).
- Backend reaches frontend at `http://app:3000` (same namespace, k8s DNS resolves `<service>`).
- Backend references in `backend/index.ts:231,279` using `http://app:3000` remain unchanged.
- `lib/auth.ts` `trustedOrigins` includes `http://app.buzz.svc.cluster.local:3000` for completeness.
### CI/CD
- Gitea Actions builds and pushes images as before (same tags: `:frontend`, `:backend`, `:frontend-$SHA`, `:backend-$SHA`).
- Deploy step in `build.yml`:
1. Delete old migration Job, create fresh one with `:frontend-$SHA`, wait for completion.
2. `kubectl set image deployment/app` and `deployment/backend` with the new SHA tags.
- Kustomize is used for initial cluster bootstrap (`kubectl apply -k k8s/`). Subsequent updates use imperative `kubectl set image` because Kustomize's `images` field cannot differentiate two images that share the same image name (different tags). If image naming changes to separate repos (e.g., `buzz-frontend:tag` / `buzz-backend:tag`), switch to `kustomize edit set image` + `kubectl apply -k .`.
### CI Authentication
- **ServiceAccount `ci-deployer`** in namespace `buzz`.
- RBAC Role grants: Deployments (get/list/watch/update/patch), Pods (get/list/watch), Jobs (full CRUD), Services/Ingresses/Middlewares (get/list/watch/create/update/patch).
- Token for the runner: `kubectl create token ci-deployer -n buzz` (or use a long-lived token from a Secret tied to the SA).
- The runner also needs `docker` login access to `mts.dgnr.us:5000` (same as today).
### Replicated Stateful Services (external to buzz namespace)
- **PostgreSQL**: CloudNativePG operator. `DATABASE_URL` in Secret points to the CNPG cluster's read-write service (e.g. `postgres://...@<cluster>-rw.<cnpg-ns>.svc:5432/buzz`).
- **Redis**: redis-operator. `REDIS_URL` in Secret points to the Redis operator's endpoint.
### Container Registry
- **Zot** running in-cluster (namespace `registry`), exposed via ServiceLB on host port 5000.
- All k3s nodes have `/etc/rancher/k3s/registries.yaml` mirroring `mts.dgnr.us:5000` → `http://localhost:5000` (svclb binds host port 5000). k3s must be restarted after writing this file.
- CI runner pushes to `mts.dgnr.us:5000` (Tailscale → `100.75.220.33` → svclb → zot pod).
### Resource Limits
- **app**: 2 replicas, limits 1 CPU / 1 Gi memory, requests 500m / 512 Mi. Liveness probe on `/api/health` (60s interval). Readiness probe same.
- **backend**: 1 replica, limits 1 CPU / 512 Mi memory, requests 250m / 256 Mi. No probes (no HTTP endpoint).
- **db-migrate Job**: limits 1 CPU / 512 Mi, requests 250m / 256 Mi.
### HPA / PDB
- HPA scales `app` on CPU > 70%, min 2 / max 6 replicas (replaces `swarm.autoscale` label).
- PDB ensures min 1 pod always available during voluntary disruptions (node drains, rollouts).
### TLS
- Not configured in manifests. Currently HTTP-only behind Traefik. Add `spec.tls` + cert-manager or k3s Traefik's built-in Let's Encrypt when ready.
## First Deploy (manual bootstrap)
```bash
kubectl create ns buzz
kubectl create secret generic buzz-env --from-env-file=.env -n buzz
kubectl apply -k k8s/
```
## Subsequent Deploys (CI)
```bash
# In .github/workflows/build.yml
TAG=${{ gitea.sha }}
# Install kubectl (arch-detected)
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
curl -sLO "https://dl.k8s.io/release/v1.35.0/bin/linux/${ARCH}/kubectl"
chmod +x kubectl && mv kubectl /usr/local/bin/kubectl
# Wire up kubeconfig
kubectl config set-cluster buzz --server=https://100.75.220.33:6443 --insecure-skip-tls-verify=true
kubectl config set-credentials ci-deployer --token=${{ secrets.KUBE_TOKEN }}
kubectl config set-context buzz --cluster=buzz --user=ci-deployer --namespace=buzz
kubectl config use-context buzz
# DB migration
kubectl delete job db-migrate -n buzz --ignore-not-found
kubectl create job db-migrate \
--image=mts.dgnr.us:5000/astral/buzz:frontend-$TAG \
-n buzz \
--command -- bun dr push
kubectl wait --for=condition=complete job/db-migrate -n buzz --timeout=120s
# Roll app & backend
kubectl set image deployment/app app=mts.dgnr.us:5000/astral/buzz:frontend-$TAG -n buzz
kubectl set image deployment/backend backend=mts.dgnr.us:5000/astral/buzz:backend-$TAG -n buzz
```
CI runner needs Gitea secret `KUBE_TOKEN` containing the `ci-deployer` SA token. Rotate via:
```bash
kubectl delete secret ci-deployer-token -n buzz && \
kubectl apply -f k8s/service-account.yaml && \
kubectl create secret generic ci-deployer-token \
--namespace buzz \
--type=kubernetes.io/service-account-token \
--annotation=kubernetes.io/service-account.name=ci-deployer
# then get new token and update Gitea secret
TOKEN=$(kubectl get secret ci-deployer-token -n buzz -o jsonpath='{.data.token}' | base64 -d)
```
## Files Changed from Swarm
| File | Change |
|------|--------|
| `Dockerfile` | Removed `drizzle` stage (migration moved to k8s Job) |
| `lib/auth.ts` | Added `app.buzz.svc.cluster.local:3000` to `trustedOrigins` |
| `.github/workflows/build.yml` | Replaced `docker service update` with k8s deploy commands |
| `nginx/nginx.conf` | Archived — replaced by Traefik Ingress |
| `docker-compose.yml` | Archived — replaced by k8s manifests |
## Key Distinctions from Swarm
- No `overlay` network (k8s CNI handles multi-node networking).
- No host port `8604` (Traefik on 80/443).
- Service discovery via k8s DNS, not Swarm DNS.
- Rolling updates via `kubectl set image`, not `docker service update`.
- Health checks via k8s probes, not Docker HEALTHCHECK.
- Autoscaling via HPA, not `swarm.autoscale` label.
+56
View File
@@ -0,0 +1,56 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
namespace: buzz
labels:
app: buzz
component: app
spec:
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: buzz
component: app
template:
metadata:
labels:
app: buzz
component: app
spec:
terminationGracePeriodSeconds: 5
containers:
- name: app
image: mts.dgnr.us:5000/astral/buzz:frontend
ports:
- containerPort: 3000
protocol: TCP
envFrom:
- secretRef:
name: buzz-env
resources:
limits:
cpu: "1"
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 10
periodSeconds: 60
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
+42
View File
@@ -0,0 +1,42 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: buzz
labels:
app: buzz
component: backend
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: buzz
component: backend
template:
metadata:
labels:
app: buzz
component: backend
spec:
terminationGracePeriodSeconds: 5
containers:
- name: backend
image: mts.dgnr.us:5000/astral/buzz:backend
env:
- name: NO_AUTH_CHECK
value: "true"
envFrom:
- secretRef:
name: buzz-env
resources:
limits:
cpu: "1"
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
+19
View File
@@ -0,0 +1,19 @@
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app
namespace: buzz
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
+30
View File
@@ -0,0 +1,30 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app
namespace: buzz
annotations:
traefik.ingress.kubernetes.io/read-timeout: "200"
spec:
ingressClassName: traefik
rules:
- host: m.dgnr.us
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 3000
- host: buzz.sudloh.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app
port:
number: 3000
+17
View File
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: buzz
resources:
- namespace.yaml
- deployment-app.yaml
- service-app.yaml
- deployment-backend.yaml
- ingress.yaml
- middleware.yaml
- hpa.yaml
- pdb.yaml
- service-account.yaml
- role.yaml
- role-binding.yaml
+9
View File
@@ -0,0 +1,9 @@
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: sse-optimizer
namespace: buzz
spec:
buffering:
maxRequestBodyBytes: 104857600
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: buzz
+11
View File
@@ -0,0 +1,11 @@
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: app
namespace: buzz
spec:
minAvailable: 1
selector:
matchLabels:
app: buzz
component: app
+13
View File
@@ -0,0 +1,13 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer
namespace: buzz
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ci-deployer
subjects:
- kind: ServiceAccount
name: ci-deployer
namespace: buzz
+30
View File
@@ -0,0 +1,30 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deployer
namespace: buzz
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["traefik.io"]
resources: ["middlewares"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
+5
View File
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-deployer
namespace: buzz
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: app
namespace: buzz
labels:
app: buzz
component: app
spec:
selector:
app: buzz
component: app
ports:
- port: 3000
targetPort: 3000
protocol: TCP
name: http
type: ClusterIP
+8 -6
View File
@@ -1,6 +1,6 @@
"use server"; "use server";
import { randomUUIDv7 } from "bun"; import { fetch, randomUUIDv7 } from "bun";
import { and, eq, inArray, isNotNull, lt, not, or, sql } from "drizzle-orm"; import { and, eq, inArray, isNotNull, lt, not, or, sql } from "drizzle-orm";
import type { PgDatabase } from "drizzle-orm/pg-core"; import type { PgDatabase } from "drizzle-orm/pg-core";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
@@ -9,6 +9,7 @@ import z from "zod";
import { adminCheck } from "./auth"; import { adminCheck } from "./auth";
import { uidRegex } from "./const"; import { uidRegex } from "./const";
import { db } from "./db"; import { db } from "./db";
import { redis } from "./db/redis";
import { cdnReferences } from "./db/references"; import { cdnReferences } from "./db/references";
import { import {
artifactSettings, artifactSettings,
@@ -23,8 +24,6 @@ import {
} from "./db/schema"; } from "./db/schema";
import { sse, tlSse } from "./db/sse-endpoints"; import { sse, tlSse } from "./db/sse-endpoints";
import { b2s } from "./utils"; import { b2s } from "./utils";
import { redis } from "./db/redis";
import { fetch } from "bun";
export async function getCharacters(chars: string[]) { export async function getCharacters(chars: string[]) {
return await db return await db
@@ -410,8 +409,11 @@ export async function actionLog(text: string, details?: unknown) {
} }
export async function getAmberVh() { export async function getAmberVh() {
const cached = await redis!.get("amber:vh"); try {
if (cached) return cached; const cached = await redis?.get("amber:vh");
if (cached) return cached;
} catch {}
const Schema = z.object({ const Schema = z.object({
response: z.number().positive(), response: z.number().positive(),
data: z.object({ data: z.object({
@@ -425,6 +427,6 @@ export async function getAmberVh() {
e.json(), e.json(),
), ),
); );
queueMicrotask(() => redis!.setex("amber:vh", 86400, vh)); queueMicrotask(() => redis?.setex("amber:vh", 86400, vh));
return vh; return vh;
} }
+2
View File
@@ -19,6 +19,8 @@ export const auth = betterAuth({
"http://localhost:3000", "http://localhost:3000",
"https://dev3000.astrxl.dev", "https://dev3000.astrxl.dev",
"http://m.dgnr.us", "http://m.dgnr.us",
"http://buzz.sudloh.com",
"http://app.buzz.svc.cluster.local:3000",
], ],
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: "pg", // or "mysql", "sqlite" provider: "pg", // or "mysql", "sqlite"
File diff suppressed because one or more lines are too long