IKHSAN
ENID Baca dalam Bahasa Indonesia

homeio

Public

Forked from doctor-io/homeio

Branch
main
Latest commit
958d74616 October 2026

power-section.tsx

Open raw
"use client";

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import {
  DAY_OF_WEEK_OPTIONS,
  SCHEDULE_TIME_OPTIONS,
  type ScheduledRebootInput,
  type SettingsBackend,
} from "@/modules/settings/components/panel/types";
import { InfoBanner, SectionDivider, Toggle } from "@/modules/settings/components/panel/controls";
import { SETTINGS_PANEL_INSET } from "@/modules/settings/components/panel/surface";
import { AlertTriangle, Loader2, Power, RefreshCw } from "@/components/icons/platform-icons";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

type PowerSectionProps = {
  power: SettingsBackend["power"];
  onRebootNow: () => Promise<void>;
  onShutdownNow: () => Promise<void>;
  onSaveScheduledReboot: (input: ScheduledRebootInput) => Promise<void>;
  onFactoryReset: () => Promise<void>;
};

const selectCls =
  "h-8 w-36 cursor-pointer appearance-none rounded-lg border border-glass-border bg-background/55 px-3 text-xs text-foreground focus:border-primary/40 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50";

function PreferenceRow({
  label,
  description,
  children,
}: {
  label: string;
  description?: string;
  children: React.ReactNode;
}) {
  return (
    <div className={cn(SETTINGS_PANEL_INSET, "flex items-center justify-between gap-4 px-4 py-3")}>
      <div className="min-w-0">
        <div className="text-sm text-foreground">{label}</div>
        {description && (
          <div className="mt-0.5 text-[11px] text-muted-foreground/70">{description}</div>
        )}
      </div>
      {children}
    </div>
  );
}

export function PowerSection({
  power,
  onRebootNow,
  onShutdownNow,
  onSaveScheduledReboot,
  onFactoryReset,
}: PowerSectionProps) {
  const rebooting = power.reboot.isPending;
  const shuttingDown = power.shutdown.isPending;
  const resetting = power.factoryReset.isPending;
  const scheduledReboot = power.scheduledReboot;

  const [scheduleEnabled, setScheduleEnabled] = useState(scheduledReboot.enabled);
  const [scheduleFrequency, setScheduleFrequency] = useState(scheduledReboot.frequency);
  const [scheduleDayOfWeek, setScheduleDayOfWeek] = useState(scheduledReboot.dayOfWeek);
  const [scheduleTime, setScheduleTime] = useState(scheduledReboot.time);

  useEffect(() => {
    setScheduleEnabled(scheduledReboot.enabled);
    setScheduleFrequency(scheduledReboot.frequency);
    setScheduleDayOfWeek(scheduledReboot.dayOfWeek);
    setScheduleTime(scheduledReboot.time);
  }, [scheduledReboot.dayOfWeek, scheduledReboot.enabled, scheduledReboot.frequency, scheduledReboot.time]);

  const scheduleDirty =
    scheduleEnabled !== scheduledReboot.enabled ||
    scheduleFrequency !== scheduledReboot.frequency ||
    scheduleDayOfWeek !== scheduledReboot.dayOfWeek ||
    scheduleTime !== scheduledReboot.time;

  const scheduleDisabled = scheduledReboot.isLoading || scheduledReboot.isPending;

  return (
    <div className="flex flex-col gap-1">
      {/* ── Power Management ── */}
      <SectionDivider title="Power Management" />
      <InfoBanner
        text="These actions will affect all running services. Make sure to save your work before proceeding."
        variant="warning"
      />
      {power.reboot.error && <InfoBanner text={power.reboot.error} variant="warning" />}
      {power.shutdown.error && <InfoBanner text={power.shutdown.error} variant="warning" />}

      <div className="grid grid-cols-2 gap-2">
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button
              disabled={rebooting || !power.reboot.available}
              className={cn(
                SETTINGS_PANEL_INSET,
                "group flex flex-col items-center gap-2 px-4 py-5 transition-colors hover:bg-background/70 disabled:cursor-not-allowed disabled:opacity-60",
              )}
            >
              <RefreshCw className="size-5 text-status-amber group-hover:animate-spin" />
              <span className="text-xs font-medium text-foreground">Reboot</span>
              <span className="text-[11px] text-muted-foreground/60">Restart all services</span>
            </button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Reboot server?</AlertDialogTitle>
              <AlertDialogDescription>
                All running Docker containers and services will be temporarily stopped and restarted. This takes about 30–60 seconds.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction
                className="bg-status-amber text-white hover:bg-status-amber/90"
                onClick={() => void onRebootNow()}
                disabled={rebooting}
              >
                {rebooting ? <><Loader2 className="size-4 animate-spin" />Rebooting…</> : "Reboot now"}
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>

        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button
              disabled={shuttingDown || !power.shutdown.available}
              className={cn(
                SETTINGS_PANEL_INSET,
                "group flex flex-col items-center gap-2 px-4 py-5 transition-colors hover:bg-background/70 disabled:cursor-not-allowed disabled:opacity-60",
              )}
            >
              <Power className="size-5 text-status-red" />
              <span className="text-xs font-medium text-foreground">Shutdown</span>
              <span className="text-[11px] text-muted-foreground/60">Power off the machine</span>
            </button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Shut down server?</AlertDialogTitle>
              <AlertDialogDescription>
                This powers the machine off completely. You will need to start it again manually before Homeio becomes available.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction
                className="bg-destructive text-white hover:bg-destructive/90"
                onClick={() => void onShutdownNow()}
                disabled={shuttingDown}
              >
                {shuttingDown ? <><Loader2 className="size-4 animate-spin" />Shutting down…</> : "Shutdown now"}
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </div>

      {/* ── Scheduled Reboot ── */}
      <SectionDivider title="Scheduled Reboot" />
      {scheduledReboot.error && <InfoBanner text={scheduledReboot.error} variant="warning" />}
      <div className="flex flex-col gap-1.5">
        <div className={cn(SETTINGS_PANEL_INSET, "px-4 py-1")}>
          <Toggle
            label="Scheduled reboot"
            description="Automatically reboot the server on a set schedule"
            enabled={scheduleEnabled}
            onToggle={() => setScheduleEnabled((v) => !v)}
            disabled={scheduleDisabled}
          />
        </div>

        <PreferenceRow label="Frequency">
          <select
            value={scheduleFrequency}
            onChange={(e) => setScheduleFrequency(e.target.value as "daily" | "weekly")}
            disabled={!scheduleEnabled || scheduleDisabled}
            className={selectCls}
          >
            <option value="daily">Daily</option>
            <option value="weekly">Weekly</option>
          </select>
        </PreferenceRow>

        {scheduleFrequency === "weekly" && (
          <PreferenceRow label="Day of week">
            <select
              value={scheduleDayOfWeek}
              onChange={(e) => setScheduleDayOfWeek(e.target.value as ScheduledRebootInput["dayOfWeek"])}
              disabled={!scheduleEnabled || scheduleDisabled}
              className={selectCls}
            >
              {DAY_OF_WEEK_OPTIONS.map((d) => (
                <option key={d} value={d}>{d.charAt(0).toUpperCase() + d.slice(1)}</option>
              ))}
            </select>
          </PreferenceRow>
        )}

        <PreferenceRow label="Time" description="24-hour server local time">
          <select
            value={scheduleTime}
            onChange={(e) => setScheduleTime(e.target.value)}
            disabled={!scheduleEnabled || scheduleDisabled}
            className={cn(selectCls, "font-mono")}
          >
            {SCHEDULE_TIME_OPTIONS.map((t) => (
              <option key={t} value={t}>{t}</option>
            ))}
          </select>
        </PreferenceRow>

        <div className={cn(SETTINGS_PANEL_INSET, "flex items-center justify-between gap-4 px-4 py-3")}>
          <span className="text-[11px] text-muted-foreground/70">
            {scheduleEnabled
              ? `Reboot will run ${scheduleFrequency === "daily" ? "daily" : `every ${scheduleDayOfWeek}`} at ${scheduleTime}.`
              : "Scheduled reboot is disabled."}
          </span>
          <button
            disabled={!scheduleDirty || scheduleDisabled}
            onClick={() => void onSaveScheduledReboot({ enabled: scheduleEnabled, frequency: scheduleFrequency, dayOfWeek: scheduleDayOfWeek, time: scheduleTime })}
            className="inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-primary/15 px-3 py-1.5 text-xs font-medium text-primary transition-colors hover:bg-primary/25 disabled:cursor-not-allowed disabled:opacity-50"
          >
            {scheduledReboot.isPending ? <><Loader2 className="size-3 animate-spin" />Saving…</> : "Save schedule"}
          </button>
        </div>
      </div>

      {/* ── Factory Reset ── */}
      <SectionDivider title="Factory Reset" />
      {power.factoryReset.error && <InfoBanner text={power.factoryReset.error} variant="warning" />}
      <InfoBanner
        text="Factory reset will stop all containers, remove Docker images, custom networks, and volumes, reset the database, delete everything under /DATA, recreate the default directories, and then reboot the machine."
        variant="warning"
      />
      <div className={cn(SETTINGS_PANEL_INSET, "flex items-center justify-between gap-4 px-4 py-3")}>
        <div className="min-w-0">
          <div className="text-sm text-foreground">Factory reset server</div>
          <div className="mt-0.5 text-[11px] text-muted-foreground/70">Wipes all data and returns Homeio to a clean state</div>
        </div>
        <AlertDialog>
          <AlertDialogTrigger asChild>
            <button
              disabled={resetting || !power.factoryReset.available}
              className="inline-flex shrink-0 items-center gap-1.5 rounded-lg bg-destructive/15 px-3 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/25 disabled:cursor-not-allowed disabled:opacity-50"
            >
              <AlertTriangle className="size-3" />
              {resetting ? "Resetting…" : "Factory Reset"}
            </button>
          </AlertDialogTrigger>
          <AlertDialogContent>
            <AlertDialogHeader>
              <AlertDialogTitle>Factory reset server?</AlertDialogTitle>
              <AlertDialogDescription>
                This is destructive. Homeio will remove Docker containers, images, volumes, custom networks, reset the database, wipe /DATA, recreate a clean data root, and reboot. After reset, you will need to register a new user.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction
                className="bg-destructive text-white hover:bg-destructive/90"
                onClick={() => void onFactoryReset()}
                disabled={resetting}
              >
                {resetting ? <><Loader2 className="size-4 animate-spin" />Resetting…</> : "Factory reset"}
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        </AlertDialog>
      </div>
    </div>
  );
}
bytes
12,484
lines
287
Type
tsx
Latest commit
16 October 2026