Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ const realApi = {
installJob: () => request<{ job: InstallJob }>("/api/wizard/job"),
wizardComplete: () =>
request<{ status: string }>("/api/wizard/complete", { method: "POST" }),
wizardReset: () =>
request<import("./types").WizardState>("/api/wizard/reset", { method: "POST" }),
drift: () =>
request<import("./types").DriftProbe>("/api/drift"),
vlans: () =>
Expand Down
56 changes: 56 additions & 0 deletions app/src/components/system/WizardRelaunchCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { ListRestart } from "lucide-react";
import { api, isDemo } from "../../api";
import { Button, Card, ConfirmDialog, useToast } from "../ui";

/**
* #415: relaunch the first-run wizard on demand. Resetting the flag has no
* side effects on its own: the panel reloads and the wizard gates on
* wizardState().completed again. Steps read live probes, so an already
* configured router shows its current state (no false "not installed").
* In demo it reuses the ?wizard=1 preview path (App.tsx).
*/
export function WizardRelaunchCard({ index = 1 }: { index?: number }) {
const { t } = useTranslation();
const { push } = useToast();
const [confirm, setConfirm] = useState(false);
const [busy, setBusy] = useState(false);

const relaunch = async () => {
setConfirm(false);
setBusy(true);
try {
if (isDemo()) {
const url = new URL(window.location.href);
url.searchParams.set("wizard", "1");
window.location.href = url.toString();
return;
}
await api.wizardReset();
window.location.reload();
} catch (e) {
push({ tone: "danger", text: e instanceof Error ? e.message : t("system.wizardFailed") });
setBusy(false);
}
};

return (
<Card index={index} title={t("system.wizardTitle")} icon={ListRestart}>
<p className="text-small text-muted">{t("system.wizardDesc")}</p>
<div className="mt-4">
<Button variant="secondary" icon={ListRestart} loading={busy} onClick={() => setConfirm(true)}>
{t("system.wizardRelaunch")}
</Button>
</div>
<ConfirmDialog
open={confirm}
onClose={() => setConfirm(false)}
onConfirm={relaunch}
title={t("system.wizardConfirmTitle")}
consequence={t("system.wizardConsequence")}
confirmLabel={t("system.wizardConfirmBtn")}
/>
</Card>
);
}
1 change: 1 addition & 0 deletions app/src/demo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const demoApi: typeof api = {
wizardSetup: () => get(D.demoWizardSetup),
installWizardSetup: async () => { await wait(800, 1500); return { job: { phase: "done", total: 1, done: 1, installed: ["ethtool-full"] } }; },
wizardComplete: async () => { await wait(400, 800); return { status: "ok" }; },
wizardReset: async () => { await wait(400, 800); return { ...D.demoWizard, completed: false }; },

// núcleo
board: () => get(D.demoBoard),
Expand Down
7 changes: 7 additions & 0 deletions app/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,13 @@ export default {
rebootBanner: "The router is rebooting. The page will reload when it's back.",
rebootFailed: "Could not reboot",
waitBackground: "Keep waiting in the background",
wizardTitle: "Setup wizard",
wizardDesc: "Run the initial setup again (mode, packages, NetPulse, guest wifi) starting from the router's current state.",
wizardRelaunch: "Run the setup wizard",
wizardConfirmTitle: "Back to the setup wizard",
wizardConsequence: "The panel will reload showing the setup wizard. Nothing changes on the router until you act on each step.",
wizardConfirmBtn: "Open the wizard",
wizardFailed: "Could not open the wizard",
},
wan: {
title: "Internet connection",
Expand Down
7 changes: 7 additions & 0 deletions app/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,13 @@ export default {
rebootBanner: "El router se está reiniciando. La página se recargará sola cuando vuelva.",
rebootFailed: "No se pudo reiniciar",
waitBackground: "Seguir esperando en segundo plano",
wizardTitle: "Asistente de configuración",
wizardDesc: "Vuelve a ejecutar el asistente inicial (modo, paquetes, NetPulse, wifi de invitados) partiendo del estado actual del router.",
wizardRelaunch: "Ejecutar el asistente",
wizardConfirmTitle: "Volver al asistente inicial",
wizardConsequence: "El panel se recargará mostrando el asistente. Nada cambia en el router hasta que actúes en cada paso.",
wizardConfirmBtn: "Abrir el asistente",
wizardFailed: "No se pudo abrir el asistente",
},
wan: {
title: "Conexión a Internet",
Expand Down
5 changes: 4 additions & 1 deletion app/src/pages/System.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MQTTCard } from "../components/system/MQTTCard";
import { ConfigBackupCard } from "../components/ConfigBackupCard";
import { UpdateCard } from "../components/system/UpdateCard";
import { SelfUpdateCard } from "../components/system/SelfUpdateCard";
import { WizardRelaunchCard } from "../components/system/WizardRelaunchCard";
import { OptionsCard } from "../components/system/OptionsCard";
import { NetPulseStandaloneBanner, NetPulseStatusChip } from "../components/system/NetPulseStatus";
import { useTranslation } from "react-i18next";
Expand Down Expand Up @@ -60,14 +61,16 @@ export function System({ board, update, onUpdateChange, onLogout }: {

{/* Mantenimiento (#155): actualización de la imagen OpenWrt. La rama
de paquetes se oculta por diseño: la paquetería se gestiona desde
LuCI o CLI (`apk upgrade`), no desde esta tarjeta. */}
LuCI o CLI (`apk upgrade`), no desde esta tarjeta. #415: relanzar
el asistente inicial bajo demanda. */}
<section className="flex flex-col gap-[var(--card-gap)]">
<GroupLabel>{t("system.groupMaintenance")}</GroupLabel>
<UpdateCard
board={board}
update={update}
onChange={onUpdateChange}
/>
<WizardRelaunchCard index={1} />
</section>

{/* Opciones (#158): preferencias de la interfaz (idioma, densidad,
Expand Down
48 changes: 33 additions & 15 deletions internal/modules/wizard.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,43 @@ func ProbeWizard() *WizardState {
return &WizardState{Completed: completed, Mode: mode}
}

func CompleteWizard() error {
if !uciSectionExists("netgrip.wizard") {
if !uciSectionExists("netgrip.main") {
cmd := exec.Command("uci", "import", "netgrip")
cmd.Stdin = strings.NewReader("config panel 'main'\n\toption panel 'panel'\nconfig wizard 'wizard'\n\toption completed '0'\n")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("init netgrip wizard config: %s", strings.TrimSpace(string(out)))
}
} else {
cmd := exec.Command("uci", "import", "netgrip")
cmd.Stdin = strings.NewReader("config wizard 'wizard'\n\toption completed '0'\n")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("add wizard section: %s", strings.TrimSpace(string(out)))
}
func ensureWizardSection() error {
if uciSectionExists("netgrip.wizard") {
return nil
}
if !uciSectionExists("netgrip.main") {
cmd := exec.Command("uci", "import", "netgrip")
cmd.Stdin = strings.NewReader("config panel 'main'\n\toption panel 'panel'\nconfig wizard 'wizard'\n\toption completed '0'\n")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("init netgrip wizard config: %s", strings.TrimSpace(string(out)))
}
return nil
}
cmd := exec.Command("uci", "import", "netgrip")
cmd.Stdin = strings.NewReader("config wizard 'wizard'\n\toption completed '0'\n")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("add wizard section: %s", strings.TrimSpace(string(out)))
}
return nil
}

func setWizardCompleted(v string) error {
if err := ensureWizardSection(); err != nil {
return err
}
out, err := exec.Command("uci", "set", "netgrip.wizard.completed=1").CombinedOutput()
out, err := exec.Command("uci", "set", "netgrip.wizard.completed="+v).CombinedOutput()
if err != nil {
return fmt.Errorf("set wizard completed: %s", strings.TrimSpace(string(out)))
}
return exec.Command("uci", "commit", "netgrip").Run()
}

func CompleteWizard() error {
return setWizardCompleted("1")
}

// ResetWizard marks the first-run wizard as pending again so it can be
// relaunched from the panel (#415). No other setting is touched.
func ResetWizard() error {
return setWizardCompleted("0")
}
9 changes: 9 additions & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func New(rpcdURL, version string) *Server {
s.mux.HandleFunc("POST /api/wizard/packages", s.requireAuth(s.handleWizardPackages))
s.mux.HandleFunc("GET /api/wizard/job", s.requireAuth(s.handleInstallJobGet))
s.mux.HandleFunc("POST /api/wizard/complete", s.requireAuth(s.handleWizardComplete))
s.mux.HandleFunc("POST /api/wizard/reset", s.requireAuth(s.handleWizardReset))
s.mux.HandleFunc("GET /api/drift", s.requireAuth(s.handleDriftGet))
s.mux.HandleFunc("GET /api/telegram", s.requireAuth(s.handleTelegramGet))
s.mux.HandleFunc("POST /api/telegram", s.requireAuth(s.handleTelegramSet))
Expand Down Expand Up @@ -1934,6 +1935,14 @@ func (s *Server) handleWizardComplete(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, map[string]string{"status": "completed"})
}

func (s *Server) handleWizardReset(w http.ResponseWriter, _ *http.Request) {
if err := modules.ResetWizard(); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, modules.ProbeWizard())
}

func (s *Server) handleDriftGet(w http.ResponseWriter, _ *http.Request) {
probe, _ := modules.CachedRead("drift", probeCacheTTL, func() (*modules.DriftProbe, error) {
return modules.ProbeDrift(), nil
Expand Down
Loading