IKHSAN
EN Read in EnglishID

homeio

Publik

Fork dari doctor-io/homeio

Branch
main
Commit terakhir
958d74616 Oktober 2026

controllers.ts

Buka raw
"use client";

import { useEffect, useRef, useState } from "react";
import type {
  BackupSettingsDraft,
  DesktopPreferencesApi,
  GeneralPreferencesDraft,
  NotificationSettingsDraft,
  SecuritySettingsDraft,
  SettingsBackend,
  SettingsSectionId,
} from "@/modules/settings/components/panel/types";
import {
  areNotificationThresholdsValid,
  areSecurityNumbersValid,
  getBackupSaveTitle,
  getGeneralSaveTitle,
  getNotificationsSaveTitle,
  getSecuritySaveTitle,
  isBackupRetentionCountValid,
  isBackupTimeValid,
  isValidHostname,
  normalizeHostnameDraft,
  parseBackupRetentionCount,
  parseNotificationThresholds,
  parseSecurityDraft,
} from "@/modules/settings/components/panel/save-policies";

export function useActiveSettingsSection(
  selectedSection: string | null | undefined,
  availableSectionIds: SettingsSectionId[],
) {
  const [activeSection, setActiveSection] = useState<SettingsSectionId>(
    selectedSection && availableSectionIds.includes(selectedSection as SettingsSectionId)
      ? (selectedSection as SettingsSectionId)
      : "general",
  );

  useEffect(() => {
    if (!selectedSection) return;
    if (!availableSectionIds.includes(selectedSection as SettingsSectionId)) return;
    setActiveSection(selectedSection as SettingsSectionId);
  }, [availableSectionIds, selectedSection]);

  return {
    activeSection,
    setActiveSection,
  };
}

export function useGeneralSettingsController(
  generalPreferences: SettingsBackend["generalPreferences"],
  saveGeneralPreferences: SettingsBackend["actions"]["saveGeneralPreferences"],
) {
  const [draft, setDraft] = useState<GeneralPreferencesDraft>({
    hostname: generalPreferences.hostname,
    timezone: generalPreferences.timezone,
  });

  useEffect(() => {
    setDraft({
      hostname: generalPreferences.hostname,
      timezone: generalPreferences.timezone,
    });
  }, [generalPreferences.hostname, generalPreferences.timezone]);

  const normalizedHostname = normalizeHostnameDraft(draft.hostname);
  const draftChanged =
    normalizedHostname !== generalPreferences.hostname ||
    draft.timezone !== generalPreferences.timezone;
  const canSave =
    draftChanged &&
    isValidHostname(normalizedHostname) &&
    !generalPreferences.isLoading &&
    !generalPreferences.isSaving;

  return {
    draft,
    setHostname(hostname: string) {
      setDraft((current) => ({
        ...current,
        hostname,
      }));
    },
    setTimezone(timezone: string) {
      setDraft((current) => ({
        ...current,
        timezone,
      }));
    },
    saveState: {
      canSave,
      title: getGeneralSaveTitle({ draft, data: generalPreferences }),
      pending: generalPreferences.isSaving,
      label: generalPreferences.isSaving ? "Saving..." : "Save Changes",
    },
    async save() {
      if (!canSave) return;

      await saveGeneralPreferences({
        hostname: normalizedHostname || "homeio",
        timezone: draft.timezone,
      });
    },
  };
}

export function useSecuritySettingsController(
  security: SettingsBackend["security"],
  saveSecuritySettings: SettingsBackend["actions"]["saveSecuritySettings"],
) {
  const [draft, setDraft] = useState<SecuritySettingsDraft>({
    firewallEnabled: security.firewall.enabled,
    firewallIncomingPolicy: security.firewall.incomingPolicy,
    firewallOutgoingPolicy: security.firewall.outgoingPolicy,
    fail2banEnabled: security.fail2ban.enabled,
    fail2banMaxRetries: String(security.fail2ban.maxRetries),
    fail2banBanDurationSeconds: String(security.fail2ban.banDuration),
  });

  useEffect(() => {
    setDraft({
      firewallEnabled: security.firewall.enabled,
      firewallIncomingPolicy: security.firewall.incomingPolicy,
      firewallOutgoingPolicy: security.firewall.outgoingPolicy,
      fail2banEnabled: security.fail2ban.enabled,
      fail2banMaxRetries: String(security.fail2ban.maxRetries),
      fail2banBanDurationSeconds: String(security.fail2ban.banDuration),
    });
  }, [
    security.fail2ban.banDuration,
    security.fail2ban.enabled,
    security.fail2ban.maxRetries,
    security.firewall.enabled,
    security.firewall.incomingPolicy,
    security.firewall.outgoingPolicy,
  ]);

  const parsedDraft = parseSecurityDraft(draft);
  const draftChanged =
    draft.firewallEnabled !== security.firewall.enabled ||
    draft.firewallIncomingPolicy !== security.firewall.incomingPolicy ||
    draft.firewallOutgoingPolicy !== security.firewall.outgoingPolicy ||
    draft.fail2banEnabled !== security.fail2ban.enabled ||
    parsedDraft.maxRetries !== security.fail2ban.maxRetries ||
    parsedDraft.banDuration !== security.fail2ban.banDuration;
  const canSave =
    draftChanged &&
    areSecurityNumbersValid(draft) &&
    !security.isLoading &&
    !security.isSaving &&
    !security.unavailable;

  return {
    draft,
    patchDraft(patch: Partial<SecuritySettingsDraft>) {
      setDraft((current) => ({
        ...current,
        ...patch,
      }));
    },
    saveState: {
      canSave,
      title: getSecuritySaveTitle({ draft, data: security }),
      pending: security.isSaving,
      label: security.isSaving ? "Saving..." : "Save Changes",
    },
    async save() {
      if (!canSave) return;

      await saveSecuritySettings({
        firewallEnabled: draft.firewallEnabled,
        firewallIncomingPolicy: draft.firewallIncomingPolicy,
        firewallOutgoingPolicy: draft.firewallOutgoingPolicy,
        fail2banEnabled: draft.fail2banEnabled,
        fail2banMaxRetries: parsedDraft.maxRetries,
        fail2banBanDurationSeconds: parsedDraft.banDuration,
      });
    },
  };
}

export function useBackupSettingsController(
  backupSettings: SettingsBackend["backup"]["settings"],
  saveBackupSettings: SettingsBackend["actions"]["saveBackupSettings"],
) {
  const [draft, setDraft] = useState<BackupSettingsDraft>({
    enabled: backupSettings.enabled,
    frequency: backupSettings.frequency,
    dayOfWeek: backupSettings.dayOfWeek,
    time: backupSettings.time,
    retentionCount: String(backupSettings.retentionCount),
  });

  useEffect(() => {
    setDraft({
      enabled: backupSettings.enabled,
      frequency: backupSettings.frequency,
      dayOfWeek: backupSettings.dayOfWeek,
      time: backupSettings.time,
      retentionCount: String(backupSettings.retentionCount),
    });
  }, [
    backupSettings.dayOfWeek,
    backupSettings.enabled,
    backupSettings.frequency,
    backupSettings.retentionCount,
    backupSettings.time,
  ]);

  const retentionCount = parseBackupRetentionCount(draft);
  const draftChanged =
    draft.enabled !== backupSettings.enabled ||
    draft.frequency !== backupSettings.frequency ||
    draft.dayOfWeek !== backupSettings.dayOfWeek ||
    draft.time !== backupSettings.time ||
    retentionCount !== backupSettings.retentionCount;
  const canSave =
    draftChanged &&
    isBackupTimeValid(draft.time) &&
    isBackupRetentionCountValid(retentionCount) &&
    !backupSettings.isLoading &&
    !backupSettings.isSaving;

  return {
    draft,
    patchDraft(patch: Partial<BackupSettingsDraft>) {
      setDraft((current) => ({
        ...current,
        ...patch,
      }));
    },
    saveState: {
      canSave,
      title: getBackupSaveTitle({ draft, data: backupSettings }),
      pending: backupSettings.isSaving,
      label: backupSettings.isSaving ? "Saving..." : "Save Changes",
    },
    async save() {
      if (!canSave) return;

      await saveBackupSettings({
        enabled: draft.enabled,
        frequency: draft.frequency,
        dayOfWeek: draft.dayOfWeek,
        time: draft.time,
        retentionCount,
      });
    },
  };
}

export function useNotificationSettingsController(
  desktopPreferences: DesktopPreferencesApi,
) {
  const [draft, setDraft] = useState<NotificationSettingsDraft>({
    systemAlertsEnabled:
      desktopPreferences.notificationPreferences.systemAlertsEnabled,
    updateNotificationsEnabled:
      desktopPreferences.notificationPreferences.updateNotificationsEnabled,
    backupReportsEnabled:
      desktopPreferences.notificationPreferences.backupReportsEnabled,
    securityEventsEnabled:
      desktopPreferences.notificationPreferences.securityEventsEnabled,
    cpuAlertThresholdPercent: String(
      desktopPreferences.notificationPreferences.cpuAlertThresholdPercent,
    ),
    memoryAlertThresholdPercent: String(
      desktopPreferences.notificationPreferences.memoryAlertThresholdPercent,
    ),
    diskAlertThresholdPercent: String(
      desktopPreferences.notificationPreferences.diskAlertThresholdPercent,
    ),
    temperatureAlertThresholdCelsius: String(
      desktopPreferences.notificationPreferences.temperatureAlertThresholdCelsius,
    ),
  });

  useEffect(() => {
    setDraft({
      systemAlertsEnabled:
        desktopPreferences.notificationPreferences.systemAlertsEnabled,
      updateNotificationsEnabled:
        desktopPreferences.notificationPreferences.updateNotificationsEnabled,
      backupReportsEnabled:
        desktopPreferences.notificationPreferences.backupReportsEnabled,
      securityEventsEnabled:
        desktopPreferences.notificationPreferences.securityEventsEnabled,
      cpuAlertThresholdPercent: String(
        desktopPreferences.notificationPreferences.cpuAlertThresholdPercent,
      ),
      memoryAlertThresholdPercent: String(
        desktopPreferences.notificationPreferences.memoryAlertThresholdPercent,
      ),
      diskAlertThresholdPercent: String(
        desktopPreferences.notificationPreferences.diskAlertThresholdPercent,
      ),
      temperatureAlertThresholdCelsius: String(
        desktopPreferences.notificationPreferences.temperatureAlertThresholdCelsius,
      ),
    });
  }, [desktopPreferences.notificationPreferences]);

  const thresholds = parseNotificationThresholds(draft);
  const draftChanged =
    draft.systemAlertsEnabled !==
      desktopPreferences.notificationPreferences.systemAlertsEnabled ||
    draft.updateNotificationsEnabled !==
      desktopPreferences.notificationPreferences.updateNotificationsEnabled ||
    draft.backupReportsEnabled !==
      desktopPreferences.notificationPreferences.backupReportsEnabled ||
    thresholds.cpu !==
      desktopPreferences.notificationPreferences.cpuAlertThresholdPercent ||
    thresholds.memory !==
      desktopPreferences.notificationPreferences.memoryAlertThresholdPercent ||
    thresholds.disk !==
      desktopPreferences.notificationPreferences.diskAlertThresholdPercent ||
    thresholds.temperature !==
      desktopPreferences.notificationPreferences.temperatureAlertThresholdCelsius;
  const canSave =
    desktopPreferences.isHydrated &&
    draftChanged &&
    areNotificationThresholdsValid(draft);

  return {
    draft,
    patchDraft(patch: Partial<NotificationSettingsDraft>) {
      setDraft((current) => ({
        ...current,
        ...patch,
      }));
    },
    saveState: {
      canSave,
      title: getNotificationsSaveTitle(draft),
      pending: false,
      label: "Save Changes",
    },
    async save() {
      if (!canSave) return;

      desktopPreferences.setNotificationPreferences((current) => ({
        ...current,
        systemAlertsEnabled: draft.systemAlertsEnabled,
        updateNotificationsEnabled: draft.updateNotificationsEnabled,
        backupReportsEnabled: draft.backupReportsEnabled,
        securityEventsEnabled: current.securityEventsEnabled,
        cpuAlertThresholdPercent: thresholds.cpu,
        memoryAlertThresholdPercent: thresholds.memory,
        diskAlertThresholdPercent: thresholds.disk,
        temperatureAlertThresholdCelsius: thresholds.temperature,
      }));
    },
  };
}

export function useUpdatesAutoCheckEffect(args: {
  activeSection: SettingsSectionId;
  isHydrated: boolean;
  autoCheckEnabled: boolean;
  onCheckForUpdates: () => void;
}) {
  const { activeSection, isHydrated, autoCheckEnabled, onCheckForUpdates } =
    args;
  const updatesAutoCheckTriggeredRef = useRef(false);

  useEffect(() => {
    if (activeSection !== "updates" || !isHydrated) {
      updatesAutoCheckTriggeredRef.current = false;
      return;
    }

    if (!autoCheckEnabled) {
      updatesAutoCheckTriggeredRef.current = false;
      return;
    }

    if (updatesAutoCheckTriggeredRef.current) {
      return;
    }

    updatesAutoCheckTriggeredRef.current = true;
    onCheckForUpdates();
  }, [activeSection, autoCheckEnabled, isHydrated, onCheckForUpdates]);
}
byte
12.451
baris
386
Jenis
ts
Commit terakhir
16 Oktober 2026