You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by xi...@apache.org on 2026/04/28 05:42:43 UTC

(pinot) branch master updated: [Controller UI] Add Minion Task Manager sidebar tab and wire additional task APIs (#18336)

This is an automated email from the ASF dual-hosted git repository.

xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new c0c31b8f2eb [Controller UI] Add Minion Task Manager sidebar tab and wire additional task APIs (#18336)
c0c31b8f2eb is described below

commit c0c31b8f2ebade4348e04f1cf142f4431f3c3b3a
Author: Xiang Fu <xi...@apache.org>
AuthorDate: Mon Apr 27 22:42:37 2026 -0700

    [Controller UI] Add Minion Task Manager sidebar tab and wire additional task APIs (#18336)
    
    * [Controller UI] Add Minion Task Manager sidebar tab and wire additional task APIs
    
    Promote the existing /minion-task-manager page from a buried homepage card to
    a first-class sidebar entry, and surface a richer view on top of existing
    controller REST APIs so operators can manage minion tasks without dropping to
    Swagger.
    
    Sidebar
    - New "Minion Task Manager" entry between Query Console and Swagger,
      backed by a new SvgIcons/MinionTaskIcon.
    
    MinionTaskManager page (/minion-task-manager)
    - 4 summary tiles: task types, minion instances, periodic tasks,
      cron scheduler state.
    - Cron Scheduler Information panel reading /tasks/scheduler/information
      (scheduler name, instance id, version, thread pool size, jobs executed,
      running since, scheduled job count). Tolerates both `getThreadPoolSize`
      (current controller key) and `threadPoolSize` for forward compatibility.
    - "Run Periodic Task" dialog driving GET /periodictask/run, with optional
      table name + table type filters.
    - Independent fetches via Promise.allSettled so a single failed endpoint
      does not blank the dashboard.
    
    SubTaskDetail page
    - New "Minion Log Files" panel that lists log files from the minion that
      ran the subtask via GET /loggers/instances/{instanceName}, with per-file
      download. Downloads go through axios (Blob + createObjectURL + anchor)
      so the configured Authorization header is preserved on auth-enabled
      clusters; plain anchor href would have skipped it.
    
    TaskDetail page
    - Operations accordion with a Delete Task action backed by
      DELETE /tasks/task/{taskName}; success path inspects embedded
      `data.error`, defers history.push to let the toast render, and falls
      back to window.location.hash when react-router history is unavailable.
    
    Wiring
    - requests/index.ts: getTasksSummary, getTaskCounts, getTaskStates,
      getCronSchedulerInformation, deleteSingleTask, getInstanceLogFiles,
      downloadInstanceLogFile.
    - PinotMethodUtils.ts: corresponding *Data / *Op / *Action wrappers
      plus downloadInstanceLogFileToBrowser helper.
    
    No backend changes; all endpoints already exist on the controller.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
    
    * [Controller UI] Fix Minion Task Manager render crash, log-file dev proxy, and silent error swallow
    
    Three bugs caught by an end-to-end UI walkthrough on a local quickstart:
    
    1. MinionTaskManager periodic-tasks accordion crashed the page.
       <CustomizedTables data={periodicTaskNames} ...> was rendered without a `title`
       prop. Table.tsx forwards that to SimpleAccordion's `headerTitle` and
       TableToolbar's `name`, both of which call `.toUpperCase()` unconditionally,
       so undefined → "Cannot read properties of undefined (reading 'toUpperCase')"
       → React unmounted the entire app, leaving a blank screen on every route.
       Fix: pass `title="Registered Periodic Tasks"`.
    
    2. Vite dev-server proxy did not route /loggers/... to the controller.
       `BACKEND_API_PREFIXES` listed every other Pinot path family but not
       `loggers`, so the new SubTaskDetail "Minion Log Files" panel hit the
       SPA fallback in development and received the index.html as its
       "response data". Added `loggers` to the prefix list.
    
    3. getInstanceLogFilesData silently swallowed non-array responses.
       The previous defensive `return []` rendered "No log files reported by
       minion." for both the legitimate empty case AND for proxy
       misconfiguration / controller errors. Now we throw so the panel shows
       the actual error message via setLogFilesError, e.g.
       "Failed to load log files: Failed to fetch logs from instance ...".
    
    Verified: tsc --noEmit clean for changed files, license:check pass,
    all four minion-task routes (/minion-task-manager, /task-queue/{type},
    .../tables/{table}, .../task/{taskID}, .../sub-task/{subID}) load
    without console errors against a quickstart-batch cluster, and the
    log-files panel surfaces the controller's 500 error message verbatim.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
    
    * [Controller UI] Move cron scheduler + periodic tasks to Cluster Manager; trim sidebar label
    
    The Quartz cron scheduler and the registered periodic-task names are
    controller-level concerns (RetentionManager, SegmentRelocator, the
    PinotTaskManager periodic that triggers minion task scheduling, etc.).
    They are not minion task queues, so they belong on the Cluster Manager
    home page rather than buried inside the Minion Task Manager page.
    
    Changes
    - New components/Homepage/CronSchedulerInformation.tsx wraps
      /tasks/scheduler/information into a collapsible accordion with
      scheduler name, instance id, version, thread pool size, jobs
      executed, running-since timestamp (timezone-aware), state, and
      scheduled job count. Tolerates both the legacy `getThreadPoolSize`
      controller key and a future `threadPoolSize` rename.
    - New components/Homepage/PeriodicTasks.tsx wraps /periodictask/names
      into a collapsible accordion with a "Run Periodic Task" dialog that
      POSTs to /periodictask/run with optional table-name + table-type
      filters.
    - HomePage.tsx mounts both new sections between Task Queues and
      Cluster Configuration.
    - MinionTaskManager.tsx is trimmed to truly minion-only concerns:
      Task Types / Minion Instances / Running Tasks / Waiting Tasks tiles
      (Running/Waiting come from /tasks/summary) plus the Task Queues
      table. Cron scheduler accordion and periodic tasks section moved
      out.
    
    Sidebar fix
    - Renamed the sidebar entry from "Minion Task Manager" to "Minion
      Tasks". The longer label was wrapping to two lines in the open
      sidebar (icon above text), and the existing
      `.menu-item:hover { display: inline-block }` rule (intended for the
      collapsed-sidebar popup) un-wrapped it on hover, making the icon
      appear to jump. The shorter label fits on one line in both states
      and matches the user's original request ("add a tab of minion
      tasks on the left side bars"). The page heading remains
      "Minion Task Manager".
    
    Verified
    - tsc --noEmit clean for all touched files
    - npm run build-dev succeeds
    - ./mvnw license:check -pl pinot-controller passes
    - Live UI walk on a quickstart-batch cluster:
      - Cluster Manager shows: Tenants, Instances, Task Queues,
        Cron Scheduler Information, Periodic Tasks (13), Cluster
        Configuration, Package Versions
      - Minion Tasks page shows: 4 minion-relevant tiles + Task Queues
      - Sidebar item height is 44px (single-line ListItem); icon no
        longer shifts on hover
      - Zero console errors across all routes
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
    
    * [Controller UI] Route new admin/status helpers through baseApiWithErrors
    
    Address @xiangfu0 review comment on PR #18336.
    
    `baseApi`'s response error interceptor returns `error.response || error`
    instead of throwing, so promises returned by `baseApi.get/delete/...`
    resolve with the error body for any 4xx/5xx. The new admin and status
    helpers (`getTasksSummary`, `getTaskCounts`, `getTaskStates`,
    `getCronSchedulerInformation`, `deleteSingleTask`, `getInstanceLogFiles`,
    `downloadInstanceLogFile`) and the `runPeriodicTask` wrapper all relied
    on `.catch()` or embedded `data.error` to detect failure, which means a
    disabled scheduler could render as "Running", a failed delete could toast
    "Successfully deleted task ...", and a permission-denied periodic-task
    trigger could show success.
    
    Switch every new helper to `baseApiWithErrors` so 4xx/5xx actually
    reject. The legacy `runPeriodicTask` request stays on `baseApi` to
    preserve existing callers (e.g., the realtime segment validation flow);
    a new `runPeriodicTaskWithErrors` is added for the new Run Periodic Task
    dialog path.
    
    Component updates:
    - PinotMethodUtils.runPeriodicTaskAction now uses runPeriodicTaskWithErrors.
    - PinotMethodUtils.getCronSchedulerInformationData distinguishes
      "scheduler disabled" (404 / 500 with "Task scheduler is disabled" body)
      → returns null (UI: "Cron scheduler is disabled on this controller.")
      from any other failure → propagates so caller can render an error.
    - CronSchedulerInformation component catches the propagated error and
      surfaces it via a colorError caption.
    - PeriodicTasks component catches list-fetch failures and routes them
      through the existing NotificationContext error toast.
    
    Verified live (quickstart + dev server):
    - runPeriodicTaskAction('DoesNotExist') REJECTS 404 with embedded body.
    - deleteSingleTaskOp('Task_NoSuchTask') REJECTS 500 with embedded body.
    - getInstanceLogFilesData(missing) REJECTS 500 with embedded body.
    - getCronSchedulerInformationData on a healthy quickstart RESOLVES with
      scheduler metadata; the existing TaskDetail / SubTaskDetail try/catch
      blocks already convert rejections into error toasts.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <no...@anthropic.com>
---
 .../Homepage/CronSchedulerInformation.tsx          | 126 +++++++++++++
 .../app/components/Homepage/PeriodicTasks.tsx      | 203 +++++++++++++++++++++
 .../src/main/resources/app/components/Layout.tsx   |   2 +
 .../SvgIcons/MinionTaskIcon.tsx}                   |  39 ++--
 .../src/main/resources/app/pages/HomePage.tsx      |   4 +
 .../main/resources/app/pages/MinionTaskManager.tsx |  96 +++++++++-
 .../src/main/resources/app/pages/SubTaskDetail.tsx | 121 +++++++++++-
 .../src/main/resources/app/pages/TaskDetail.tsx    |  62 ++++++-
 .../src/main/resources/app/requests/index.ts       |  51 ++++++
 .../main/resources/app/utils/PinotMethodUtils.ts   |  96 ++++++++++
 pinot-controller/src/main/resources/vite.config.ts |   2 +-
 11 files changed, 765 insertions(+), 37 deletions(-)

diff --git a/pinot-controller/src/main/resources/app/components/Homepage/CronSchedulerInformation.tsx b/pinot-controller/src/main/resources/app/components/Homepage/CronSchedulerInformation.tsx
new file mode 100644
index 00000000000..127c3a81876
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/CronSchedulerInformation.tsx
@@ -0,0 +1,126 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { get } from 'lodash';
+import { Grid, makeStyles, Typography } from '@material-ui/core';
+import SimpleAccordion from '../SimpleAccordion';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
+import { formatTimeInTimezone } from '../../utils/TimezoneUtils';
+import { useTimezone } from '../../contexts/TimezoneContext';
+
+const useStyles = makeStyles(() => ({
+  box: {
+    border: '1px #BDCCD9 solid',
+    borderRadius: 4,
+    marginBottom: 20,
+  },
+  body: {
+    padding: '12px 16px',
+    fontSize: 14,
+    lineHeight: '2rem',
+  },
+}));
+
+// Shape returned by /tasks/scheduler/information. The controller currently
+// emits the leaked Java method name `getThreadPoolSize`; we tolerate both that
+// and a future cleaner `threadPoolSize` key for forward compatibility.
+type SchedulerInfo = {
+  Version?: string;
+  SchedulerName?: string;
+  SchedulerInstanceId?: string;
+  InStandbyMode?: boolean;
+  RunningSince?: number;
+  NumberOfJobsExecuted?: number;
+  JobDetails?: Array<unknown>;
+  getThreadPoolSize?: number;
+  threadPoolSize?: number;
+};
+
+const CronSchedulerInformation = () => {
+  const classes = useStyles();
+  const { currentTimezone } = useTimezone();
+  const [schedulerInfo, setSchedulerInfo] = useState<SchedulerInfo | null>(null);
+  const [error, setError] = useState<string | null>(null);
+
+  useEffect(() => {
+    let isMounted = true;
+    PinotMethodUtils.getCronSchedulerInformationData()
+      .then((res) => {
+        if (!isMounted) {
+          return;
+        }
+        setSchedulerInfo(res as SchedulerInfo | null);
+        setError(null);
+      })
+      .catch((err) => {
+        if (!isMounted) {
+          return;
+        }
+        setError(
+          String(get(err, 'response.data.error')
+            || get(err, 'response.data.message')
+            || (err && err.message)
+            || 'Unable to load cron scheduler information')
+        );
+      });
+    return () => { isMounted = false; };
+  }, []);
+
+  const status = error
+    ? 'Unknown'
+    : (schedulerInfo
+      ? (schedulerInfo.InStandbyMode ? 'Standby' : 'Running')
+      : 'Disabled');
+  const runningSince = schedulerInfo?.RunningSince
+    ? formatTimeInTimezone(schedulerInfo.RunningSince, 'YYYY-MM-DD HH:mm:ss z', currentTimezone)
+    : '-';
+  const threadPoolSize = schedulerInfo?.getThreadPoolSize ?? schedulerInfo?.threadPoolSize ?? '-';
+
+  return (
+    <div className={classes.box}>
+      <SimpleAccordion headerTitle="Cron Scheduler Information" showSearchBox={false}>
+        <Grid container className={classes.body}>
+          <Grid item xs={6}><strong>Scheduler Name:</strong> {schedulerInfo?.SchedulerName || '-'}</Grid>
+          <Grid item xs={6}><strong>Instance Id:</strong> {schedulerInfo?.SchedulerInstanceId || '-'}</Grid>
+          <Grid item xs={6}><strong>Version:</strong> {schedulerInfo?.Version || '-'}</Grid>
+          <Grid item xs={6}><strong>Thread Pool Size:</strong> {threadPoolSize}</Grid>
+          <Grid item xs={6}><strong>Jobs Executed:</strong> {schedulerInfo?.NumberOfJobsExecuted ?? '-'}</Grid>
+          <Grid item xs={6}><strong>Running Since:</strong> {runningSince}</Grid>
+          <Grid item xs={6}><strong>State:</strong> {status}</Grid>
+          <Grid item xs={6}><strong>Scheduled Jobs:</strong> {schedulerInfo?.JobDetails?.length ?? '-'}</Grid>
+          {error && (
+            <Grid item xs={12}>
+              <Typography variant='caption' color='error'>
+                Failed to load cron scheduler information: {error}
+              </Typography>
+            </Grid>
+          )}
+          {!error && !schedulerInfo && (
+            <Grid item xs={12}>
+              <Typography variant='caption'>Cron scheduler is disabled on this controller.</Typography>
+            </Grid>
+          )}
+        </Grid>
+      </SimpleAccordion>
+    </div>
+  );
+};
+
+export default CronSchedulerInformation;
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/PeriodicTasks.tsx b/pinot-controller/src/main/resources/app/components/Homepage/PeriodicTasks.tsx
new file mode 100644
index 00000000000..4789629bc1b
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/PeriodicTasks.tsx
@@ -0,0 +1,203 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useContext, useEffect, useState } from 'react';
+import {
+  Box,
+  FormControl,
+  InputLabel,
+  MenuItem,
+  Select,
+  TextField,
+  Typography,
+  makeStyles
+} from '@material-ui/core';
+import { get } from 'lodash';
+import { TableData } from 'Models';
+import SimpleAccordion from '../SimpleAccordion';
+import CustomButton from '../CustomButton';
+import CustomDialog from '../CustomDialog';
+import CustomizedTables from '../Table';
+import PinotMethodUtils from '../../utils/PinotMethodUtils';
+import { NotificationContext } from '../Notification/NotificationContext';
+
+const useStyles = makeStyles(() => ({
+  box: {
+    border: '1px #BDCCD9 solid',
+    borderRadius: 4,
+    marginBottom: 20,
+  },
+  formField: {
+    width: '100%',
+  },
+}));
+
+const PeriodicTasks = () => {
+  const classes = useStyles();
+  const { dispatch } = useContext(NotificationContext);
+
+  const [periodicTaskNames, setPeriodicTaskNames] = useState<TableData>({ records: [], columns: ['Periodic Task Name'] });
+  const [runDialogOpen, setRunDialogOpen] = useState(false);
+  const [selectedPeriodicTask, setSelectedPeriodicTask] = useState<string>('');
+  const [periodicTaskTableName, setPeriodicTaskTableName] = useState<string>('');
+  const [periodicTaskTableType, setPeriodicTaskTableType] = useState<string>('');
+  const [running, setRunning] = useState(false);
+
+  useEffect(() => {
+    let isMounted = true;
+    PinotMethodUtils.getAllPeriodicTaskNames()
+      .then((res) => {
+        if (!isMounted) {
+          return;
+        }
+        setPeriodicTaskNames(res);
+      })
+      .catch((err) => {
+        if (!isMounted) {
+          return;
+        }
+        dispatch({
+          type: 'error',
+          message: `Failed to load periodic tasks: ${get(err, 'response.data.error') || (err as Error).message || 'unknown error'}`,
+          show: true,
+        });
+      });
+    return () => { isMounted = false; };
+  }, [dispatch]);
+
+  const handleRunPeriodicTask = async () => {
+    if (!selectedPeriodicTask) {
+      return;
+    }
+    setRunning(true);
+    try {
+      const response = await PinotMethodUtils.runPeriodicTaskAction(
+        selectedPeriodicTask,
+        periodicTaskTableName.trim() || undefined,
+        periodicTaskTableType.trim() || undefined
+      );
+      dispatch({
+        type: 'success',
+        message: get(response, 'description') || `Periodic task ${selectedPeriodicTask} triggered`,
+        show: true,
+      });
+      setRunDialogOpen(false);
+      setPeriodicTaskTableName('');
+      setPeriodicTaskTableType('');
+      setSelectedPeriodicTask('');
+    } catch (e) {
+      dispatch({
+        type: 'error',
+        message: `Failed to trigger periodic task: ${get(e, 'response.data.error') || (e as Error).message}`,
+        show: true,
+      });
+    } finally {
+      setRunning(false);
+    }
+  };
+
+  const periodicTaskCount = periodicTaskNames?.records?.length || 0;
+
+  return (
+    <>
+      <div className={classes.box}>
+        <SimpleAccordion
+          headerTitle={`Periodic Tasks${periodicTaskCount ? ` (${periodicTaskCount})` : ''}`}
+          showSearchBox={false}
+        >
+          <Box p={2}>
+            <CustomButton
+              onClick={() => setRunDialogOpen(true)}
+              tooltipTitle="Manually trigger a controller periodic task. Optionally scope to a single table."
+              enableTooltip={true}
+              isDisabled={periodicTaskCount === 0}
+            >
+              Run Periodic Task
+            </CustomButton>
+            {periodicTaskCount > 0 && (
+              <Box mt={2}>
+                <CustomizedTables
+                  title="Registered Periodic Tasks"
+                  data={periodicTaskNames}
+                  showSearchBox={true}
+                  inAccordionFormat={false}
+                />
+              </Box>
+            )}
+            {periodicTaskCount === 0 && (
+              <Typography variant='body2'>No periodic tasks registered.</Typography>
+            )}
+          </Box>
+        </SimpleAccordion>
+      </div>
+
+      <CustomDialog
+        open={runDialogOpen}
+        title="Run Periodic Task"
+        handleClose={() => setRunDialogOpen(false)}
+        handleSave={handleRunPeriodicTask}
+        btnOkText={running ? 'Running...' : 'Run'}
+        okBtnDisabled={running || !selectedPeriodicTask}
+      >
+        <Box display="flex" flexDirection="column" pt={1} pb={1}>
+          <FormControl variant="outlined" size="small" className={classes.formField} fullWidth>
+            <InputLabel>Periodic Task</InputLabel>
+            <Select
+              label="Periodic Task"
+              value={selectedPeriodicTask}
+              onChange={(e) => setSelectedPeriodicTask(e.target.value as string)}
+            >
+              {periodicTaskNames.records.map((row) => {
+                const name = Array.isArray(row) ? row[0] : row;
+                return <MenuItem key={String(name)} value={String(name)}>{String(name)}</MenuItem>;
+              })}
+            </Select>
+          </FormControl>
+          <Box mt={2}>
+            <TextField
+              label="Table name (optional, raw or with type suffix)"
+              variant="outlined"
+              size="small"
+              fullWidth
+              value={periodicTaskTableName}
+              onChange={(e) => setPeriodicTaskTableName(e.target.value)}
+            />
+          </Box>
+          <Box mt={2}>
+            <TextField
+              label="Table type (optional: OFFLINE / REALTIME)"
+              variant="outlined"
+              size="small"
+              fullWidth
+              value={periodicTaskTableType}
+              onChange={(e) => setPeriodicTaskTableType(e.target.value)}
+            />
+          </Box>
+          <Box mt={1}>
+            <Typography variant='caption'>
+              Leaving the table name empty runs the periodic task across all tables.
+            </Typography>
+          </Box>
+        </Box>
+      </CustomDialog>
+    </>
+  );
+};
+
+export default PeriodicTasks;
diff --git a/pinot-controller/src/main/resources/app/components/Layout.tsx b/pinot-controller/src/main/resources/app/components/Layout.tsx
index 534ae0e7d43..a2b213b3fb0 100644
--- a/pinot-controller/src/main/resources/app/components/Layout.tsx
+++ b/pinot-controller/src/main/resources/app/components/Layout.tsx
@@ -25,12 +25,14 @@ import QueryConsoleIcon from './SvgIcons/QueryConsoleIcon';
 import SwaggerIcon from './SvgIcons/SwaggerIcon';
 import ClusterManagerIcon from './SvgIcons/ClusterManagerIcon';
 import ZookeeperIcon from './SvgIcons/ZookeeperIcon';
+import MinionTaskIcon from './SvgIcons/MinionTaskIcon';
 import app_state from '../app_state';
 import AccountCircleOutlinedIcon from '@material-ui/icons/AccountCircleOutlined';
 
 const BASE_NAVIGATION_ITEMS = [
   { id: 1, name: 'Cluster Manager', link: '/', icon: <ClusterManagerIcon /> },
   { id: 2, name: 'Query Console', link: '/query', icon: <QueryConsoleIcon /> },
+  { id: 6, name: 'Minion Tasks', link: '/minion-task-manager', icon: <MinionTaskIcon /> },
   { id: 4, name: 'Swagger REST API', link: 'help', target: '_blank', icon: <SwaggerIcon /> }
 ];
 
diff --git a/pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx b/pinot-controller/src/main/resources/app/components/SvgIcons/MinionTaskIcon.tsx
similarity index 50%
copy from pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx
copy to pinot-controller/src/main/resources/app/components/SvgIcons/MinionTaskIcon.tsx
index 7f921acbc70..9c0b478db0e 100644
--- a/pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx
+++ b/pinot-controller/src/main/resources/app/components/SvgIcons/MinionTaskIcon.tsx
@@ -17,32 +17,15 @@
  * under the License.
  */
 
-import React from 'react';
-import { Grid, makeStyles, Typography, Box } from '@material-ui/core';
-import useTaskTypesTable from '../components/Homepage/useTaskTypesTable';
+import * as React from 'react';
+import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
 
-const useStyles = makeStyles(() => ({
-  gridContainer: {
-    padding: 20,
-    backgroundColor: 'white',
-    maxHeight: 'calc(100vh - 70px)',
-    overflowY: 'auto'
-  }
-}));
-
-const MinionTaskManager = () => {
-  const classes = useStyles();
-  const { taskTypesTable } = useTaskTypesTable();
-
-  return (
-    <Grid item xs className={classes.gridContainer}>
-      <Box mb={3}>
-        <Typography variant='h5'>Minion Task Manager</Typography>
-        <Typography variant='caption'>Manage the minion tasks queues and tasks</Typography>
-      </Box>
-      {taskTypesTable}
-    </Grid>
-  );
-};
-
-export default MinionTaskManager;
+export default (props: SvgIconProps) => (
+  <SvgIcon
+    style={{ width: 24, height: 24, verticalAlign: 'middle' }}
+    viewBox="0 0 24 24"
+    {...props}
+  >
+    <path d="M9 2h6l1 3h3a2 2 0 0 1 2 2v3l-3 1v3l3 1v3a2 2 0 0 1-2 2h-3l-1 3H9l-1-3H5a2 2 0 0 1-2-2v-3l3-1v-3L3 10V7a2 2 0 0 1 2-2h3l1-3zm3 6.5A3.5 3.5 0 0 0 8.5 12 3.5 3.5 0 0 0 12 15.5 3.5 3.5 0 0 0 15.5 12 3.5 3.5 0 0 0 12 8.5zm0 2A1.5 1.5 0 0 1 13.5 12 1.5 1.5 0 0 1 12 13.5 1.5 1.5 0 0 1 10.5 12 1.5 1.5 0 0 1 12 10.5z" />
+  </SvgIcon>
+);
diff --git a/pinot-controller/src/main/resources/app/pages/HomePage.tsx b/pinot-controller/src/main/resources/app/pages/HomePage.tsx
index 3ab4700d9cb..e39a452337f 100644
--- a/pinot-controller/src/main/resources/app/pages/HomePage.tsx
+++ b/pinot-controller/src/main/resources/app/pages/HomePage.tsx
@@ -27,6 +27,8 @@ import Instances from '../components/Homepage/InstancesTables';
 import ClusterConfig from '../components/Homepage/ClusterConfig';
 import PackageVersions from '../components/Homepage/PackageVersions';
 import useTaskTypesTable from '../components/Homepage/useTaskTypesTable';
+import CronSchedulerInformation from '../components/Homepage/CronSchedulerInformation';
+import PeriodicTasks from '../components/Homepage/PeriodicTasks';
 import Skeleton from '@material-ui/lab/Skeleton';
 import { getTenants } from '../requests';
 import { DataTable, InstanceType } from 'Models';
@@ -232,6 +234,8 @@ const HomePage = () => {
         clusterName={clusterName}
       />
       {taskTypesTable}
+      <CronSchedulerInformation />
+      <PeriodicTasks />
       <ClusterConfig />
       <PackageVersions />
     </Grid>
diff --git a/pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx b/pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx
index 7f921acbc70..c7cecc774d0 100644
--- a/pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx
+++ b/pinot-controller/src/main/resources/app/pages/MinionTaskManager.tsx
@@ -17,9 +17,11 @@
  * under the License.
  */
 
-import React from 'react';
-import { Grid, makeStyles, Typography, Box } from '@material-ui/core';
+import React, { useEffect, useState } from 'react';
+import { Grid, makeStyles, Typography, Box, Paper } from '@material-ui/core';
+import { get } from 'lodash';
 import useTaskTypesTable from '../components/Homepage/useTaskTypesTable';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
 
 const useStyles = makeStyles(() => ({
   gridContainer: {
@@ -27,19 +29,101 @@ const useStyles = makeStyles(() => ({
     backgroundColor: 'white',
     maxHeight: 'calc(100vh - 70px)',
     overflowY: 'auto'
-  }
+  },
+  summaryPaper: {
+    padding: '12px 0',
+    textAlign: 'center',
+    color: '#4285f4',
+    backgroundColor: 'rgba(66, 133, 244, 0.08)',
+    border: '1px solid rgba(66, 133, 244, 0.4)',
+    borderRadius: 4,
+    '& h2, h4': {
+      margin: 0,
+    },
+    '& h4': {
+      textTransform: 'uppercase',
+      letterSpacing: 1,
+      fontWeight: 600,
+      fontSize: 12,
+    },
+    '& h2': {
+      fontSize: 28,
+    },
+  },
 }));
 
+// Subset of /tasks/summary fields used by the dashboard tiles.
+type TasksSummary = {
+  totalRunningTasks?: number;
+  totalWaitingTasks?: number;
+};
+
 const MinionTaskManager = () => {
   const classes = useStyles();
-  const { taskTypesTable } = useTaskTypesTable();
+  const { taskTypes, taskTypesTable } = useTaskTypesTable();
+
+  const [minionCount, setMinionCount] = useState<number | null>(null);
+  const [tasksSummary, setTasksSummary] = useState<TasksSummary | null>(null);
+
+  useEffect(() => {
+    let isMounted = true;
+    Promise.allSettled([
+      PinotMethodUtils.getAllInstances(),
+      PinotMethodUtils.getTasksSummaryData()
+    ]).then(([instancesRes, summaryRes]) => {
+      if (!isMounted) {
+        return;
+      }
+      if (instancesRes.status === 'fulfilled') {
+        setMinionCount(get(instancesRes.value, 'MINION', []).length);
+      }
+      if (summaryRes.status === 'fulfilled') {
+        setTasksSummary(summaryRes.value as TasksSummary);
+      }
+    });
+    return () => { isMounted = false; };
+  }, []);
+
+  const taskTypeCount = taskTypes?.records?.length || 0;
+  const runningCount = tasksSummary?.totalRunningTasks ?? '-';
+  const waitingCount = tasksSummary?.totalWaitingTasks ?? '-';
 
   return (
     <Grid item xs className={classes.gridContainer}>
-      <Box mb={3}>
+      <Box mb={2}>
         <Typography variant='h5'>Minion Task Manager</Typography>
-        <Typography variant='caption'>Manage the minion tasks queues and tasks</Typography>
+        <Typography variant='caption'>Manage minion task queues, drill into tasks and sub-tasks, and view per-minion log files</Typography>
       </Box>
+
+      <Grid container spacing={2}>
+        <Grid item xs={3}>
+          <Paper className={classes.summaryPaper} elevation={0}>
+            <h4>Task Types</h4>
+            <h2>{taskTypeCount}</h2>
+          </Paper>
+        </Grid>
+        <Grid item xs={3}>
+          <Paper className={classes.summaryPaper} elevation={0}>
+            <h4>Minion Instances</h4>
+            <h2>{minionCount === null ? '-' : minionCount}</h2>
+          </Paper>
+        </Grid>
+        <Grid item xs={3}>
+          <Paper className={classes.summaryPaper} elevation={0}>
+            <h4>Running Tasks</h4>
+            <h2>{runningCount}</h2>
+          </Paper>
+        </Grid>
+        <Grid item xs={3}>
+          <Paper className={classes.summaryPaper} elevation={0}>
+            <h4>Waiting Tasks</h4>
+            <h2>{waitingCount}</h2>
+          </Paper>
+        </Grid>
+      </Grid>
+
+      <Box mt={3} />
+
       {taskTypesTable}
     </Grid>
   );
diff --git a/pinot-controller/src/main/resources/app/pages/SubTaskDetail.tsx b/pinot-controller/src/main/resources/app/pages/SubTaskDetail.tsx
index 39f94b24aed..71523e0fa46 100644
--- a/pinot-controller/src/main/resources/app/pages/SubTaskDetail.tsx
+++ b/pinot-controller/src/main/resources/app/pages/SubTaskDetail.tsx
@@ -17,11 +17,13 @@
  * under the License.
  */
 
-import React, { useEffect, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
 import { get, find } from 'lodash';
 import { UnControlled as CodeMirror } from 'react-codemirror2';
 import { Grid, makeStyles, Box, List, ListItem, ListItemText, Typography, Divider } from '@material-ui/core';
 import SimpleAccordion from '../components/SimpleAccordion';
+import CustomButton from '../components/CustomButton';
+import { NotificationContext } from '../components/Notification/NotificationContext';
 import PinotMethodUtils from '../utils/PinotMethodUtils';
 import { TaskProgressStatus } from 'Models';
 import moment from 'moment';
@@ -80,9 +82,14 @@ const useStyles = makeStyles(() => ({
 const TaskDetail = (props) => {
   const classes = useStyles();
   const { currentTimezone } = useTimezone();
+  const { dispatch } = useContext(NotificationContext);
   const { subTaskID, taskID, queueTableName } = props.match.params;
   const [taskDebugData, setTaskDebugData] = useState({});
   const [taskProgressData, setTaskProgressData] = useState<TaskProgressStatus[] | string>("");
+  const [logFiles, setLogFiles] = useState<string[]>([]);
+  const [logFilesError, setLogFilesError] = useState<string | null>(null);
+  const [logFilesLoading, setLogFilesLoading] = useState<boolean>(false);
+  const [downloadingFile, setDownloadingFile] = useState<string | null>(null);
 
   const fetchTaskDebugData = async () => {
     const debugRes = await PinotMethodUtils.getTaskDebugData(taskID, queueTableName);
@@ -95,11 +102,56 @@ const TaskDetail = (props) => {
       setTaskProgressData(get(taskProgressData, subTaskID, "No Status"));
   }
 
+  const fetchLogFiles = async (participant: string) => {
+    if (!participant) {
+      return;
+    }
+    setLogFilesLoading(true);
+    setLogFilesError(null);
+    try {
+      const files = await PinotMethodUtils.getInstanceLogFilesData(participant);
+      setLogFiles(files);
+    } catch (e) {
+      setLogFilesError(
+        get(e, 'response.data.error')
+          || get(e, 'response.data.message')
+          || (e as Error).message
+          || 'Unable to fetch logs'
+      );
+      setLogFiles([]);
+    } finally {
+      setLogFilesLoading(false);
+    }
+  };
+
+  const handleDownloadLog = async (participant: string, filePath: string) => {
+    setDownloadingFile(filePath);
+    try {
+      await PinotMethodUtils.downloadInstanceLogFileToBrowser(participant, filePath);
+    } catch (e) {
+      dispatch({
+        type: 'error',
+        message: `Failed to download ${filePath}: ${get(e, 'response.data.error') || (e as Error).message || 'unknown error'}`,
+        show: true,
+      });
+    } finally {
+      setDownloadingFile(null);
+    }
+  };
+
   useEffect(() => {
     fetchTaskDebugData();
     fetchTaskProgressData();
   }, [currentTimezone]);
 
+  const participant: string = get(taskDebugData, 'participant', '');
+
+  useEffect(() => {
+    if (participant) {
+      fetchLogFiles(participant);
+    }
+  }, [participant]);
+
   return (
     <Grid item xs className={classes.gridContainer}>
       <div className={classes.highlightBackground}>
@@ -185,6 +237,73 @@ const TaskDetail = (props) => {
             </SimpleAccordion>
           </div>
         </Grid>
+        <Grid item xs={12}>
+          <div className={classes.sqlDiv}>
+            <SimpleAccordion
+              headerTitle={`Minion Log Files${participant ? ` — ${participant}` : ''}`}
+              showSearchBox={false}
+            >
+              <Box p={2}>
+                {!participant && (
+                  <Typography variant='body2'>This subtask has not been assigned to a minion yet.</Typography>
+                )}
+                {participant && (
+                  <>
+                    <Box mb={1}>
+                      <CustomButton
+                        onClick={() => fetchLogFiles(participant)}
+                        tooltipTitle="Re-fetch log file list from the minion"
+                        enableTooltip={true}
+                        isDisabled={logFilesLoading}
+                      >
+                        {logFilesLoading ? 'Refreshing...' : 'Refresh'}
+                      </CustomButton>
+                    </Box>
+                    {logFilesError && (
+                      <Typography variant='body2' color='error'>
+                        Failed to load log files: {logFilesError}
+                      </Typography>
+                    )}
+                    {!logFilesError && logFiles.length === 0 && !logFilesLoading && (
+                      <Typography variant='body2'>No log files reported by minion.</Typography>
+                    )}
+                    {logFiles.length > 0 && (
+                      <List dense>
+                        {logFiles.map((filePath) => {
+                          const isDownloading = downloadingFile === filePath;
+                          return (
+                            <ListItem key={filePath} disableGutters>
+                              <ListItemText
+                                primary={
+                                  <a
+                                    href="#"
+                                    onClick={(e: React.MouseEvent<HTMLAnchorElement>) => {
+                                      e.preventDefault();
+                                      if (!isDownloading) {
+                                        handleDownloadLog(participant, filePath);
+                                      }
+                                    }}
+                                    style={{
+                                      color: isDownloading ? '#9e9e9e' : '#4285f4',
+                                      cursor: isDownloading ? 'default' : 'pointer',
+                                      textDecoration: 'underline',
+                                    }}
+                                  >
+                                    {filePath}{isDownloading ? ' (downloading...)' : ''}
+                                  </a>
+                                }
+                              />
+                            </ListItem>
+                          );
+                        })}
+                      </List>
+                    )}
+                  </>
+                )}
+              </Box>
+            </SimpleAccordion>
+          </div>
+        </Grid>
       </Grid>
     </Grid>
   );
diff --git a/pinot-controller/src/main/resources/app/pages/TaskDetail.tsx b/pinot-controller/src/main/resources/app/pages/TaskDetail.tsx
index a35005ab2c7..7e481dcb7f6 100644
--- a/pinot-controller/src/main/resources/app/pages/TaskDetail.tsx
+++ b/pinot-controller/src/main/resources/app/pages/TaskDetail.tsx
@@ -17,16 +17,19 @@
  * under the License.
  */
 
-import React, { useEffect, useState, useMemo, useCallback } from 'react';
+import React, { useContext, useEffect, useState, useMemo, useCallback } from 'react';
 import { get, each } from 'lodash';
 import { Grid, makeStyles, Box } from '@material-ui/core';
 import PinotMethodUtils from '../utils/PinotMethodUtils';
 import CustomizedTables from '../components/Table';
+import CustomButton from '../components/CustomButton';
+import { useConfirm } from '../components/Confirm';
 import TaskStatusFilter, { TaskStatus } from '../components/TaskStatusFilter';
 import { TaskRuntimeConfig } from 'Models';
 import AppLoader from '../components/AppLoader';
 import SimpleAccordion from '../components/SimpleAccordion';
 import CustomCodemirror from '../components/CustomCodemirror';
+import { NotificationContext } from '../components/Notification/NotificationContext';
 import { formatTimeInTimezone } from '../utils/TimezoneUtils';
 import { useTimezone } from '../contexts/TimezoneContext';
 
@@ -72,7 +75,9 @@ const useStyles = makeStyles(() => ({
 const TaskDetail = (props) => {
   const classes = useStyles();
   const { currentTimezone } = useTimezone();
+  const { history } = props;
   const { taskID, taskType, queueTableName } = props.match.params;
+  const { dispatch } = useContext(NotificationContext);
 
   const [fetching, setFetching] = useState(true);
   const [taskDebugData, setTaskDebugData] = useState({});
@@ -148,12 +153,66 @@ const TaskDetail = (props) => {
     />
   );
 
+  const handleDeleteTask = async () => {
+    deleteTaskConfirm.setConfirmDialog(false);
+    try {
+      // Some Pinot endpoints embed errors in a 200 body (`{error: "..."}`).
+      // Treat that as a failure rather than a silent success.
+      const result = await PinotMethodUtils.deleteSingleTaskOp(taskID);
+      const embeddedError = get(result, 'error');
+      if (embeddedError) {
+        throw new Error(String(embeddedError));
+      }
+      dispatch({
+        type: 'success',
+        message: `Successfully deleted task ${taskID}`,
+        show: true,
+      });
+      const target = `/task-queue/${taskType}/tables/${queueTableName}`;
+      // Defer navigation by a tick so the success toast has time to mount before
+      // this page unmounts. Fall back to window.location when react-router is
+      // unavailable so the user does not stay on a stale (deleted) URL.
+      setTimeout(() => {
+        if (history && typeof history.push === 'function') {
+          history.push(target);
+        } else if (typeof window !== 'undefined') {
+          window.location.hash = `#${target}`;
+        }
+      }, 0);
+    } catch (e) {
+      dispatch({
+        type: 'error',
+        message: `Failed to delete task: ${get(e, 'response.data.error') || (e as Error).message || 'unknown error'}`,
+        show: true,
+      });
+    }
+  };
+
+  const deleteTaskConfirm = useConfirm({
+    dialogTitle: 'Delete task',
+    dialogContent: `Delete task ${taskID}? Sub-tasks and their state will be removed from ZooKeeper.`,
+    successCallback: handleDeleteTask,
+  });
+
   if(fetching) {
     return <AppLoader />
   }
 
   return (
     <Grid item xs className={classes.gridContainer}>
+      <div className={classes.operationDiv}>
+        <SimpleAccordion headerTitle="Operations" showSearchBox={false}>
+          <Box p={1}>
+            <CustomButton
+              onClick={() => deleteTaskConfirm.setConfirmDialog(true)}
+              tooltipTitle="Delete this task and remove its sub-tasks from the task queue"
+              enableTooltip={true}
+            >
+              Delete Task
+            </CustomButton>
+          </Box>
+        </SimpleAccordion>
+      </div>
       <div className={classes.highlightBackground}>
         <Grid container className={classes.body}>
           <Grid item xs={12}>
@@ -207,6 +266,7 @@ const TaskDetail = (props) => {
           />
         </Grid>
       </Grid>
+      {deleteTaskConfirm.confirmComponent}
     </Grid>
   );
 };
diff --git a/pinot-controller/src/main/resources/app/requests/index.ts b/pinot-controller/src/main/resources/app/requests/index.ts
index d3dae2b8bd1..27152f2e476 100644
--- a/pinot-controller/src/main/resources/app/requests/index.ts
+++ b/pinot-controller/src/main/resources/app/requests/index.ts
@@ -221,6 +221,57 @@ export const getTaskGeneratorDebug = (taskName: string, taskType: string): Promi
 export const getTaskTypeDebug = (taskType: string): Promise<AxiosResponse<OperationResponse>> =>
   baseApi.get(`/tasks/${taskType}/debug?verbosity=1`, { headers: { ...headers, Accept: 'application/json' } });
 
+// All admin / status helpers below intentionally use `baseApiWithErrors` so that
+// 4xx/5xx responses actually reject. `baseApi` swallows error responses (see
+// axios-config.ts: "control will never go to catch block"), which would otherwise
+// let the UI render "Running"/"Successfully deleted task" on top of a failed call.
+export const getTasksSummary = (tenant?: string): Promise<AxiosResponse<OperationResponse>> =>
+  baseApiWithErrors.get(`/tasks/summary`, { headers: { ...headers, Accept: 'application/json' }, params: tenant ? { tenant } : {} });
+
+export const getTaskCounts = (taskType: string): Promise<AxiosResponse<OperationResponse>> =>
+  baseApiWithErrors.get(`/tasks/${taskType}/taskcounts`, { headers: { ...headers, Accept: 'application/json' } });
+
+export const getTaskStates = (taskType: string): Promise<AxiosResponse<OperationResponse>> =>
+  baseApiWithErrors.get(`/tasks/${taskType}/taskstates`, { headers: { ...headers, Accept: 'application/json' } });
+
+export const getCronSchedulerInformation = (): Promise<AxiosResponse<OperationResponse>> =>
+  baseApiWithErrors.get(`/tasks/scheduler/information`, { headers: { ...headers, Accept: 'application/json' } });
+
+export const deleteSingleTask = (taskName: string, forceDelete = false): Promise<AxiosResponse<OperationResponse>> =>
+  baseApiWithErrors.delete(`/tasks/task/${taskName}`, {
+    headers: { ...headers, Accept: 'application/json' },
+    params: { forceDelete }
+  });
+
+export const getInstanceLogFiles = (instanceName: string): Promise<AxiosResponse<string[]>> =>
+  baseApiWithErrors.get(`/loggers/instances/${instanceName}`, { headers: { ...headers, Accept: 'application/json' } });
+
+// Fetches the log file as a Blob through the authenticated axios client so that
+// auth headers (basic / bearer) attached by the request interceptor are preserved.
+// Plain anchor downloads cannot reuse those headers, so we route through axios
+// and trigger a download via createObjectURL on the client side.
+export const downloadInstanceLogFile = (
+  instanceName: string,
+  filePath: string
+): Promise<AxiosResponse<Blob>> =>
+  baseApiWithErrors.get(`/loggers/instances/${instanceName}/download`, {
+    params: { filePath },
+    responseType: 'blob',
+  });
+
+// `runPeriodicTask` (above) is shared with legacy callers and stays on baseApi
+// to preserve their behavior. The error-aware variant is used by the new Run
+// Periodic Task dialog so a failed invocation surfaces an error toast rather
+// than a misleading success.
+export const runPeriodicTaskWithErrors = (
+  taskName: string,
+  tableName?: string,
+  tableType?: string
+): Promise<AxiosResponse<OperationResponse>> =>
+  baseApiWithErrors.get(`/periodictask/run`, {
+    params: { taskname: taskName, tableName, type: tableType },
+  });
+
 export const getTables = (params): Promise<AxiosResponse<OperationResponse>> =>
   baseApi.get(`/tables`, { params, headers: { ...headers, Accept: 'application/json' } });
 
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index 2b9c93c0605..b4b8ed8c7b2 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -43,6 +43,7 @@ import {
   dropInstance,
   getPeriodicTaskNames,
   runPeriodicTask,
+  runPeriodicTaskWithErrors,
   getTaskTypes,
   getTaskTypeDebug,
   getTables,
@@ -59,6 +60,13 @@ import {
   getTasks,
   getTaskDebug,
   getTaskGeneratorDebug,
+  getTasksSummary,
+  getTaskCounts,
+  getTaskStates,
+  getCronSchedulerInformation,
+  deleteSingleTask,
+  getInstanceLogFiles,
+  downloadInstanceLogFile,
   updateInstanceTags,
   getClusterConfig,
   getQueryTables,
@@ -1375,6 +1383,86 @@ const getScheduleJobDetail = (tableName, taskType)=>{
   })
 };
 
+// Returns a TaskSummaryResponse covering all task types across all tenants.
+// Shape: { tasks: { tenant: { taskType: TaskCount } } } depending on backend version.
+const getTasksSummaryData = (tenant?: string) => {
+  return getTasksSummary(tenant).then(response => response.data);
+};
+
+// Returns a map from task name to TaskCount {total, running, waiting, error, completed, ...}
+const getTaskCountsData = (taskType: string) => {
+  return getTaskCounts(taskType).then(response => response.data);
+};
+
+// Returns a map from task name to TaskState (NOT_STARTED, IN_PROGRESS, COMPLETED, FAILED, ABORTED, ...).
+const getTaskStatesData = (taskType: string) => {
+  return getTaskStates(taskType).then(response => response.data);
+};
+
+// Returns null only when the controller has the scheduler disabled (404 or
+// "Task scheduler is disabled" 500). Other errors propagate so the caller can
+// distinguish "scheduler off" from "controller unreachable" / "auth failed".
+const getCronSchedulerInformationData = () => {
+  return getCronSchedulerInformation()
+    .then(response => response.data)
+    .catch((err) => {
+      const status = get(err, 'response.status');
+      const message = String(get(err, 'response.data.error') || get(err, 'response.data.message') || '');
+      if (status === 404 || (status === 500 && /scheduler is disabled/i.test(message))) {
+        return null;
+      }
+      throw err;
+    });
+};
+
+const deleteSingleTaskOp = (taskName: string, forceDelete = false) => {
+  return deleteSingleTask(taskName, forceDelete).then(response => response.data);
+};
+
+const runPeriodicTaskAction = (taskName: string, tableName?: string, tableType?: string) => {
+  return runPeriodicTaskWithErrors(taskName, tableName || undefined, tableType || undefined).then(response => response.data);
+};
+
+const getInstanceLogFilesData = (instanceName: string): Promise<string[]> => {
+  return getInstanceLogFiles(instanceName).then(response => {
+    const data = response.data;
+    if (Array.isArray(data)) {
+      return data as string[];
+    }
+    // Anything that isn't a JSON array is treated as a backend error. Returning
+    // [] here silently would render "No log files reported by minion." on the UI,
+    // which masks proxy misconfiguration or controller errors. Surface it instead.
+    const embeddedError = (data && typeof data === 'object' && 'error' in (data as Record<string, unknown>))
+      ? String((data as Record<string, unknown>).error)
+      : null;
+    throw new Error(embeddedError || 'Unexpected log-file response shape');
+  });
+};
+
+// Fetches the requested minion log file as a Blob through the authenticated axios
+// client and triggers a browser download via a temporary anchor + createObjectURL.
+// This avoids the auth header gap that a plain `<a href>` open would have hit on
+// the controller's `/loggers/instances/{name}/download` endpoint.
+const downloadInstanceLogFileToBrowser = async (instanceName: string, filePath: string): Promise<void> => {
+  const response = await downloadInstanceLogFile(instanceName, filePath);
+  const blob: Blob = response.data instanceof Blob ? response.data : new Blob([response.data]);
+  const objectUrl = window.URL.createObjectURL(blob);
+  try {
+    const a = document.createElement('a');
+    a.href = objectUrl;
+    // Last path component for filename; controller serves the whole file path so
+    // we slash-strip locally for a sane saved name.
+    const nameParts = filePath.split('/');
+    a.download = nameParts[nameParts.length - 1] || 'minion.log';
+    document.body.appendChild(a);
+    a.click();
+    a.parentNode?.removeChild(a);
+  } finally {
+    // Defer revoke to next tick so the browser has time to start the download.
+    setTimeout(() => window.URL.revokeObjectURL(objectUrl), 0);
+  }
+};
+
 const getUserList = ()=>{
   return requestUserList().then(response=>{
     return response.data;
@@ -1544,6 +1632,14 @@ export default {
   scheduleTaskAction,
   executeTaskAction,
   getScheduleJobDetail,
+  getTasksSummaryData,
+  getTaskCountsData,
+  getTaskStatesData,
+  getCronSchedulerInformationData,
+  deleteSingleTaskOp,
+  runPeriodicTaskAction,
+  getInstanceLogFilesData,
+  downloadInstanceLogFileToBrowser,
   getMinionMetaData,
   getElapsedTime,
   getTasksList,
diff --git a/pinot-controller/src/main/resources/vite.config.ts b/pinot-controller/src/main/resources/vite.config.ts
index e7e460ac0d0..ff6bc6912b8 100644
--- a/pinot-controller/src/main/resources/vite.config.ts
+++ b/pinot-controller/src/main/resources/vite.config.ts
@@ -28,7 +28,7 @@ const BACKEND_API_PREFIXES = [
   'tenants', 'tables', 'schemas', 'segments', 'instances',
   'cluster', 'periodictask', 'tasks', 'zk', 'sql', 'timeseries',
   'brokers', 'table', 'auth', 'users', 'health', 'appconfigs',
-  'query', 'version', 'debug',
+  'query', 'version', 'debug', 'loggers',
 ];
 
 // Match the prefix then require either '/', '?', or end-of-path.


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org