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
3 changes: 2 additions & 1 deletion internal/server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func decode(r *http.Request, v any) error {

type overviewJSON struct {
Lang string `json:"lang"`
Terminal bool `json:"terminal"` // the dashboard has a web terminal (v2)
Active *activeJSON `json:"active"`
Starting *startingJSON `json:"starting"` // a start in progress; null if none
Next *nextJSON `json:"next"`
Expand Down Expand Up @@ -148,7 +149,7 @@ type statsJSON struct {
func (s *Server) handleOverview(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
lang := s.lang(ctx)
out := overviewJSON{Lang: lang, Levels: []levelJSON{}}
out := overviewJSON{Lang: lang, Terminal: s.cfg.Engine.HasTerminal(), Levels: []levelJSON{}}
s.startMu.Lock()
if s.starting != nil {
st := *s.starting
Expand Down
2 changes: 2 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@
"web.intro.inbox.body": "Each ticket is a Linux server with a fault. The one at the top is picked for you: open \"Why this ticket\" to see why.",
"web.intro.session.title": "Ticket left, terminal right",
"web.intro.session.body": "Opening a ticket starts a sandbox. Read the ticket, find and fix the fault in the terminal, then run the check (Ctrl+Enter).",
"web.intro.session_shell.title": "Ticket left, shell beside it",
"web.intro.session_shell.body": "Opening a ticket starts a sandbox. Read the ticket, run rootcause shell in a terminal to get onto the machine, find and fix the fault, then run the check (Ctrl+Enter).",
"web.intro.hints.title": "Hints and stars",
"web.intro.hints.body": "Stuck? Reveal a hint. A solve with no hints earns 3 stars, with one or two hints 2, and with all three 1. After the third hint you can give up and read the walkthrough.",
"web.intro.levels.title": "Levels and stats",
Expand Down
2 changes: 2 additions & 0 deletions locales/no.json
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,8 @@
"web.intro.inbox.body": "Hver sak er en Linux-server med en feil. Den øverste er valgt for deg: åpne «Hvorfor denne saken» for å se hvorfor.",
"web.intro.session.title": "Saken til venstre, terminalen til høyre",
"web.intro.session.body": "Når du åpner en sak, starter en sandkasse. Les saken, finn og rett feilen i terminalen, og kjør sjekken (Ctrl+Enter).",
"web.intro.session_shell.title": "Saken her, skallet ved siden av",
"web.intro.session_shell.body": "Når du åpner en sak, starter en sandkasse. Les saken, kjør rootcause shell i en terminal for å komme inn på maskinen, finn og rett feilen, og kjør sjekken (Ctrl+Enter).",
"web.intro.hints.title": "Hint og stjerner",
"web.intro.hints.body": "Står du fast? Vis et hint. Løser du uten hint, får du 3 stjerner, med ett eller to hint 2 og med alle tre 1. Etter det tredje hintet kan du gi opp og lese gjennomgangen.",
"web.intro.levels.title": "Nivåer og statistikk",
Expand Down
2 changes: 1 addition & 1 deletion web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export function App() {
setPrefs({ introDone: false });
}}
/>
<Intro open={!prefs.introDone && !help} onDone={() => setPrefs({ introDone: true })} />
<Intro open={!prefs.introDone && !help} terminal={overview.data?.terminal ?? true} onDone={() => setPrefs({ introDone: true })} />
<Palette open={palette} onClose={() => setPalette(false)} overview={overview.data} actions={paletteActions} />
</LiveRegionProvider>
</I18nContext.Provider>
Expand Down
1 change: 1 addition & 0 deletions web/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Strings = Record<string, string>;

export interface Overview {
lang: Lang;
terminal: boolean; // the dashboard has a web terminal (v2)
active: { id: string; exercise: string } | null;
// A session start in progress: the engine's generic step, nothing more.
starting: { exercise: string; step: number; steps: number } | null;
Expand Down
10 changes: 6 additions & 4 deletions web/src/shell/Intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ type Step = (typeof steps)[number];
// Intro is the guided first-run tour: four steps in a modal dialog, with
// Back, Next and Skip. It opens on a browser's first visit (no stored
// preferences) and again from the help dialog. Esc, Skip and the last step's
// button all end it, and end calls onDone.
export function Intro({ open, onDone }: { open: boolean; onDone: () => void }) {
// button all end it, and end calls onDone. Without a web terminal (v1) the
// session step points to rootcause shell instead.
export function Intro({ open, terminal, onDone }: { open: boolean; terminal: boolean; onDone: () => void }) {
const { t } = useI18n();
const [index, setIndex] = useState(0);
const next = useRef<HTMLButtonElement>(null);
Expand All @@ -29,6 +30,7 @@ export function Intro({ open, onDone }: { open: boolean; onDone: () => void }) {
moved.current = false;
}, [index]);
const step = steps[index] ?? "inbox";
const text = step === "session" && !terminal ? "session_shell" : step;
const last = index === steps.length - 1;
const go = (i: number) => {
moved.current = true;
Expand All @@ -44,8 +46,8 @@ export function Intro({ open, onDone }: { open: boolean; onDone: () => void }) {
<p className="label" aria-live="polite">
{t("web.intro.step", { n: index + 1, total: steps.length })}
</p>
<h2 id="intro-title">{t(`web.intro.${step}.title`)}</h2>
<p>{t(`web.intro.${step}.body`)}</p>
<h2 id="intro-title">{t(`web.intro.${text}.title`)}</h2>
<p>{t(`web.intro.${text}.body`)}</p>
</div>
<div className="intro-dots" aria-hidden="true">
{steps.map((s, i) => (
Expand Down
25 changes: 17 additions & 8 deletions web/src/views/Inbox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo } from "react";
import { useMemo, useRef } from "react";
import { api, type ExerciseRow, type Overview } from "../api";
import { sessionHref, usePoll } from "../hooks";
import { useI18n } from "../i18n";
Expand Down Expand Up @@ -29,10 +29,19 @@ export function Inbox({ overview, offline, reloadOverview, onStarting }: Props)
const list = usePoll(api.exercises, 15000, "inbox");
const { start, busy, error } = useStart(reloadOverview, onStarting);

const rows = useMemo(() => (overview && list.data ? inboxRows(overview, list.data) : null), [overview, list.data]);
const active = overview?.active ?? null;
const live = useMemo(() => (overview && list.data ? inboxRows(overview, list.data) : null), [overview, list.data]);
const starting = overview?.starting ?? null;
const locked = active !== null || starting !== null || busy !== null || offline;
const inStart = busy !== null || starting !== null;
// A start creates the session part way through, which moves the
// recommendation and the tags; the list holds still until the page opens.
const held = useRef<Row[] | null>(null);
if (!inStart || !held.current) held.current = live;
const rows = inStart ? held.current : live;
// The session exists before its machine is ready; it is not "in progress"
// until the start is done.
const active = inStart ? null : (overview?.active ?? null);
const startingId = starting?.exercise ?? busy;
const locked = overview?.active != null || inStart || offline;

return (
<div className="page page-inbox">
Expand Down Expand Up @@ -71,9 +80,9 @@ export function Inbox({ overview, offline, reloadOverview, onStarting }: Props)
overview={overview!}
locked={locked}
lockedReason={offline ? t("web.error.load") : active ? t("web.inbox.locked_active") : undefined}
busy={busy === r.ex.id || (busy === "" && r.tag === "next")}
onStart={() => start(r.tag === "next" ? "" : r.ex.id)}
starting={starting && starting.exercise === r.ex.id ? starting : busy === r.ex.id || (busy === "" && r.tag === "next") ? (starting ?? { exercise: r.ex.id, step: 0, steps: 4 }) : null}
busy={busy === r.ex.id}
onStart={() => start(r.tag === "next" ? "" : r.ex.id, r.ex.id)}
starting={startingId === r.ex.id ? (starting ?? { exercise: r.ex.id, step: 0, steps: 4 }) : null}
/>
))}
</ul>
Expand Down Expand Up @@ -147,7 +156,7 @@ function TicketRow({
</Button>
</div>
</div>
{next && <WhyPicked overview={overview} ex={ex} />}
{next && !starting && <WhyPicked overview={overview} ex={ex} />}
{starting && <StartProgress starting={starting} />}
</li>
);
Expand Down
12 changes: 7 additions & 5 deletions web/src/views/start.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import { ProgressSteps, useAnnounce } from "../ui/kit";
// stepKeys name the engine's start steps (session.StartStep), in order.
const stepKeys = ["web.start.step.image", "web.start.step.machine", "web.start.step.setup", "web.start.step.fault"];

// useStart starts a session ("" = the recommended one) and opens it. While
// it runs, onStarting(true) makes the app poll the overview fast, so the
// start steps tick off.
// useStart starts a session ("" = the recommended one) and opens it. busy is
// the row that was clicked (row, or the exercise), never re-derived from the
// recommendation, which moves once the start creates the session. While it
// runs, onStarting(true) makes the app poll the overview fast, so the start
// steps tick off.
export function useStart(reloadOverview: () => void, onStarting: (on: boolean) => void) {
const announce = useAnnounce();
const { t } = useI18n();
const [busy, setBusy] = useState<string | null>(null);
const [error, setError] = useState("");
const start = (exercise: string) => {
setBusy(exercise);
const start = (exercise: string, row = exercise) => {
setBusy(row);
setError("");
onStarting(true);
announce(t("web.start.announce"));
Expand Down