Commit Graph
332 Commits
Author SHA1 Message Date
pnoker 170c19e2ef chore: add project governance and license files 2026-05-08 23:25:36 +08:00
pnoker 8a5ffc1af2 build: expand Makefile with structured targets and help descriptions 2026-05-08 18:58:18 +08:00
pnoker 3c3496b9b2 chore(husky): drop deprecated hook bootstrap 2026-05-08 18:54:21 +08:00
pnoker 177bf70910 chore: update CLAUDE.md formatting and refresh Cargo.lock 2026-05-08 18:21:54 +08:00
pnoker 60df60e5cd refactor(views): inline management forms and lists 2026-05-08 18:19:30 +08:00
pnoker 1636dfd1be refactor(core): type dashboard and auth views 2026-05-08 18:19:05 +08:00
pnoker 6bb83f204e chore(tooling): tighten lint and format defaults 2026-05-08 18:18:18 +08:00
pnoker b768230ac9 chore: bump version to 2026.5.5 across project files 2026-05-05 00:50:56 +08:00
pnoker f9161ecc2c style(event-overview): lift tabs header onto its own surface
el-tabs renders flat by default, expecting to live inside a card.
Since Overview is a board with tabs at the top level, the header was
floating on the page background and the full-width nav underline
doubled up with the cards below. Give the header a bg surface +
radius + padding and hide the nav-wrap::after line so the active-tab
underline stands alone.

Also: lint-driven attribute reordering across the same file (imports,
el-button / stat-card / el-tab-pane props) — pure formatting.
2026-05-05 00:40:30 +08:00
pnoker 2e99a72cf4 fix(home): restore 8px rhythm between SlaBadge and stat grid
SlaBadge is rendered conditionally between the banner row and the
stat strip. Neither neighbour sets a top margin, so the badge was
sitting flush against the stats and reading as a banner appendage
instead of its own strip. Give it the page's 8px rhythm.
2026-05-05 00:40:15 +08:00
pnoker f301515324 chore: add Apache 2.0 license headers to source files 2026-05-04 22:26:22 +08:00
pnoker 7f85faf752 refactor(dashboard): migrate api and cards to shared entity / composables / utils
Second half of the shared-infra refactor. Points every consumer at the
freshly-extracted modules, deleting ~400 lines of duplicated boilerplate
across home and event-overview.

- api/dashboard.ts: re-import all payload interfaces from
  config/entity/dashboard; endpoints now share AlertSource / TimeRangeParams
  / Granularity / TopDimension unions.
- home/index.ts: statsTimeseries call switches from legacy rangeHours:24
  to rangeKey:'24h' — server prefers rangeKey anyway.
- SlaBadge / TopologySankey: types now come from config/entity; SlaBadge
  adopts useAsyncLoader.
- 9 event-overview cards (AgingBacklog, AlertCorrelation, ChangeImpact,
  FlappingSources, MttaMttr, PeerDeviation, PointCoverageGap,
  ProtocolHealth, SilentSources): use useAsyncLoader instead of ad-hoc
  loading flag + try/catch, useEntityNames instead of per-card resolveNames
  + nameMap, jumpToEntity / jumpToSourceEvents instead of inline
  router.push, formatDateTime / formatMs / humanDuration instead of
  per-card time helpers, resolveDashboardColour / DASHBOARD_PALETTE
  instead of hard-coded hex, and the @clickable-rows SCSS mixin instead
  of repeated :deep(.el-table__row){cursor:pointer}.
- Every card also exposes refresh() via defineExpose so parent panels
  can drive a coordinated reload.
2026-05-04 22:17:52 +08:00
pnoker f69808b76a refactor(dashboard): extract shared entity types, composables, utils and palette tokens
Every dashboard / event-overview card had been re-declaring the same
loading flag + try/catch, its own resolveNames + nameMap, its own
formatTime, and its own router.push handler. Consolidated those seams
into reusable modules so future cards (and brand tweaks) are a one-line
change instead of a 30-file sweep.

- config/entity/dashboard.ts: payload interfaces + shared primitives
  (AlertSource, RangeKey, TimeRangeParams) — interface-only module per
  the verbatimModuleSyntax convention.
- composables/useAsyncLoader: folds loading flag + global-error swallow
  into one run() helper.
- composables/useEntityNames: process-wide reactive cache with
  inflight dedup for device/driver/profile/point name lookups.
- utils/time: formatDateTime / formatClock / formatShortDateTime /
  formatMs / humanDuration — normalised zh-CN, 24-hour.
- utils/jump: jumpToEntity / jumpToSourceEvents for the consistent
  detail-page + event-page routing convention.
- config/constant/palette.ts + styles/palette.scss: driver/device/
  profile/point tone tokens (TS for JS consumers, SCSS for components).
2026-05-04 22:17:31 +08:00
pnoker e2700efd1c feat(event-overview): split into 4 tabs with 9 Phase-2 insight cards
Overview is now tabbed (situation / noise / availability / sla), synced
to ?tab= so SLA badge deeplinks land correctly. Situation keeps the
existing quick-actions + stat cards + charts + heatmap layout; the
three new tabs host:

- Noise: FlappingSources, PeerDeviation, AlertCorrelation
- Availability: SilentSources, PointCoverageGap, ProtocolHealth
- SLA: AgingBacklog, MttaMttr, ChangeImpact

Adds the 9 matching dashboard API endpoints and i18n strings.
2026-05-04 21:05:13 +08:00
pnoker ca91040a9f feat(home): add SLA badge strip and tenant topology Sankey
- SLA badge above row 1 surfaces unack >24h + silent-device counts and
  deeplinks to the matching event-overview tab; hides when clean.
- New bottom row renders a Driver → Device → Profile → Point Sankey
  over the /dashboard/topology endpoint. Row 2/3 swapped so realtime
  sits above analytics.
- Add @antv/g6 dependency for the Sankey layout, plus home.topology /
  home.sla i18n strings (zh + en).
2026-05-04 21:04:19 +08:00
pnoker 16f669b9a9 feat: introduce reusable DashboardCard component and refactor card-based views
- Created a modular `DashboardCard` component to standardize card design and functionality across the application.
- Updated `AlertStormSources`, `ActivityHeatmap`, `LatencyChart`, `AlertList`, `LiveDataFeed`, and `AnalyticsTabs` to use `DashboardCard`, reducing duplicated styles and logic.
- Improved maintainability and consistency by replacing individual card implementations with the unified component.
2026-05-04 14:45:47 +08:00
pnoker 64fcd9d226 fix(home-row3): replace el-scrollbar with native overflow:auto — same pattern as RecentUnconfirmed
The previous fix (body as flex column + el-scrollbar flex:1) didn't
actually hold the 300px baseline. el-scrollbar is a simulated
scrollbar whose height="100%" passes through multiple internal
wrappers; in our flex sizing chain it silently degraded to auto,
leaving content free to push the card past its min-height.

RecentUnconfirmed on the event-overview page has never had this
problem — it puts `overflow: auto` directly on the card body and
renders the list straight into it, no el-scrollbar. Copy that exact
pattern here:

- AlertList: remove <el-scrollbar>, render the timeline groups
  directly into the body, body gains `overflow: auto`. Native
  browser overflow respects the flex:1 + min-height:0 sizing and
  stops exactly at the card's computed height.
- LiveDataFeed: replace <el-scrollbar> with a plain <div class=
  "live-feed__scroll"> so the footer can stay pinned at the bottom
  of the body (can't put overflow directly on the body or the
  footer would scroll too). The div carries flex:1 + min-height:0
  + overflow:auto.

Row 3 now honours the 300px baseline on all three cards, and
el-row's stretch no longer lifts the whole row to match a runaway
sibling.
2026-05-04 14:10:29 +08:00
pnoker 862f98254e fix(home-row3): LiveFeed / AlertList bodies must be flex column to respect 300 baseline
Root cause of row-3 cards rendering at 800+px despite min-height:300:
  el-card__body was flex-child (flex:1, min-height:0) but NOT itself
  display:flex. The inner <el-scrollbar height="100%"> therefore had
  no concrete parent height to resolve against — height:100% degraded
  to auto, and the 20-row timeline / 10-row alarm list grew the body
  as far as its content wanted. el-row's align-items:stretch then
  lifted TrendChart alongside them, so all three looked "way too
  tall" together.

Fix is narrow: make the body itself a flex column and give each
el-scrollbar a class that claims flex:1 + min-height:0. Now the
scrollbar has a finite height (= body height = card height − header),
content scrolls inside it, and the card stays at 300.

Chart cards (TrendChart etc.) already had display:flex on the body
from the earlier refactor, so they're unaffected.
2026-05-04 14:05:35 +08:00
pnoker 949bb34ad7 refactor(dashboard): baseline 360 → 300 so narrow row-3 siblings stop looking tall
The 4:1:1 row (TrendChart / LiveFeed / Alerts) pairs a wide 12/24
chart with two 6/24 side columns (~320px wide each on 1280 screens).
At min-height 360 the two narrow cards rendered ~320 × 360 — visibly
taller than they are wide, and el-row's align-items:stretch meant
the whole row couldn't go below whatever the tallest sibling claimed.

Drop the family baseline to 300. Widths stay the same but aspect
ratios read better:
  TrendChart   ~640 × 300  (2:1, good for 30-day x-axis)
  LiveFeed     ~320 × 300  (~1:1, no longer tall)
  Alerts       ~320 × 300  (same)
  Row 4 cards  ~630 × 300  (2:1, latency + activity heatmap)
  Event diag   ~640 × 300  (2:1)

Content still fits: 300 − 48 header ≈ 250 in chart canvas / scroll
area, enough for 4-5 timeline items, a 30-day sparkline, or a 7×24
heatmap. Flex:1 + min-height:0 on the inner slot means each widget
still expands if its row stretches beyond 300 (which won't happen
on its own anymore, but remains safe when external code pins a row
height).
2026-05-04 14:01:43 +08:00
pnoker 8989141695 refactor(dashboard): lower family baseline 440 → 360
440 was inherited from LiveDataFeed's old min-height and left every
card feeling oversized — two-row grids pushed Data Ingestion Trend
taller than the useful chart content inside it. Rebase the family
baseline to 360, which matches the median of the previous per-card
heights (TrendChart 340, AlertList 370, the event-overview chart row
340, diag cards 320) without anything actually shrinking below its
content.

All twelve dashboard cards (home row 3/4 + event overview chart +
diag) now share min-height: 360. Chart/scrollbar/list slots still
take flex:1 inside the flex column so content expands to whatever
the row stretches them to — no card ever drops below 360, but rows
that set a larger stretched height (when one sibling card demands it)
still line up.
2026-05-04 13:57:27 +08:00
pnoker 947884f393 refactor(dashboard): unify all cards onto a 440px family baseline
The home page and event-overview page together had four different card
heights floating around (440 / 340 / 320 / 280 / 240), making the grid
feel bumpy as rows didn't line up. Standardise on one baseline so
every dashboard card reads as part of the same family.

Every chart / list card now follows the same skeleton:
  min-height: 440px
  height: 100%
  display: flex; flex-direction: column
  ::v-deep(.el-card__body) { flex: 1; min-height: 0; flex column }
  chart / scrollbar / list slot: flex: 1; min-height: 0
  G2 Chart constructor: autoFit: true (no explicit height)

Cards converted:
  Home row 4: LatencyChart, ActivityHeatmap (was 280 → 440)
  Event overview row 2: EventTrendChart, TopSourcesChart (was 340 → 440)
  Event overview diag grid: AlertActivityHeatmap, AlertTypePie,
    AlertStormSources, RecentUnconfirmed (was 240/320 → 440)

Overview.vue's grid-item min-height overrides (340 / 320) are now
redundant — dropped so the height lives entirely on the card. An
AlertTypePie empty state and AlertStormSources/RecentUnconfirmed
list slots pick up flex:1 so empty / short content still fills the
card instead of leaving a gap.
2026-05-04 13:52:58 +08:00
pnoker 0dcbdadadb fix(home-row3): align trend / live feed / alerts on a common 440 baseline
Each widget was sizing itself independently:
  TrendChart:    fixed 280px canvas + header ≈ 340px total
  LiveDataFeed:  min-height 440px + scrollbar height fixed 360px
  AlertList:     height 100% + scrollbar prop default 320px ≈ 370px

In a single flex row el-col does stretch children to the tallest col,
but the stretched space was being ignored by each card's internals,
so the three still rendered at different heights.

Unify:
- All three cards set min-height: 440px + display:flex column.
- el-card__body becomes flex:1 with min-height:0 so overflow children
  (chart / scrollbar) collapse properly into the available space.
- TrendChart canvas is flex:1 instead of a fixed 280px, and Chart()
  loses its height option so autoFit owns both axes.
- LiveDataFeed / AlertList el-scrollbars switch to height="100%" so
  they fill whatever the body gives them; AlertList drops its now-
  redundant scrollHeight prop.
2026-05-04 13:39:03 +08:00
pnoker 0cba40e845 refactor(home): adjust row-3 ratio 4:1:1 → 3:1.5:1.5
The 4:1:1 split squeezed the side columns to 4/24 (≈ 200px on 1280
screens), where timestamps and message text in Recent Alarms /
LiveFeed rows started wrapping awkwardly. Loosen to 12/6/6 so the
two side widgets each get ~300px of breathing room while TrendChart
still claims the dominant half.
2026-05-04 13:32:15 +08:00
pnoker 4242263d88 refactor(home): pack trend + live feed + alarms into a 4:1:1 row
Collapse the previous two-row layout (TrendChart alone on its own
full-width row, LiveFeed + Alerts split 12:12 above) into a single
row at 4:1:1 — TrendChart keeps 16/24 of the width for readable
30-day axis, LiveFeed and Alerts take 4/24 each as narrow side
columns. Saves a full vertical slot on the home page without losing
any of the three widgets.

Narrow breakpoints (md/sm/xs) still stack each col to full width so
the 4/24 side columns don't squeeze below usable size.
2026-05-04 13:28:57 +08:00
pnoker 9b7f1c16b2 refactor(home): split analytics card + rebalance row widths
Rework the middle of the home page into 12/12 columns so related
widgets read as equal siblings instead of a dominant-chart +
side-rail pattern:

- AnalyticsTabs takes a new `group` prop (structural | top). Each
  instance renders only its three tabs — structural breakdowns
  (protocol / deviceStatus / profile) on one card, top-N activity
  (topDriver / topPoint / topDevice) on the other. Default activeTab
  within each group is its first entry.
- Row 2: structural analytics + top-N analytics, 12 + 12.
- Row 3: LiveDataFeed + AlertList, 12 + 12 — the two "live now"
  widgets pair more naturally than the old trend+alert combo did.
- Row 4: TrendChart claims its own full-width row. A 30-day time
  series benefits from the extra x-axis room, and nothing else
  competes for attention alongside it.
- Row 5 (latency + activity heatmap) unchanged.
2026-05-04 13:26:38 +08:00
pnoker 15fab29753 fix(home-analytics): drop hardcoded chart height so autoFit respects caption row
When the caption row was added between the tab bar and the chart,
body height shrank from 360 → 340, but Chart constructor still
specified height: 360 explicitly — G2's autoFit only adopts container
width in that case, keeping the 360 height as-is. The chart then
painted 20px below the visible area, clipping the x-axis labels and
last row of ticks.

Remove the height option so autoFit sizes both axes from the
container's clientHeight. The .analytics-tabs__chart div is
height:100% inside the 340px body, so every chart now fits exactly.
2026-05-04 13:21:16 +08:00
pnoker 6c941cc5ac refactor(home-analytics): reorder tabs by driver → point → device → profile
Both tab groups now follow the same entity precedence so the strip
reads consistently left-to-right:

  Structural:  Drivers by Protocol → Device Enable Status → Devices by Profile
  Top-N:       Most Active Drivers → Most Active Points   → Most Active Devices

Driver-level views lead each group (drivers are the entry point for
diagnosing most DC3 fleets — "which protocols are we running" / "which
drivers are busiest" is the first question operators ask). Default
active tab switches from deviceStatus → protocol to match.
2026-05-04 13:19:47 +08:00
pnoker c6b7ee01dd fix(home-analytics): point protocol tab at byService (actual protocol mix)
The "Drivers by Protocol" tab was sourced from byType, which is
client vs server mode — not what the label promises. Switch to the
new byService bucket the manager backend now emits, and strip the
dc3-driver- prefix from service names so the chart axis reads
modbus-tcp / mqtt / opc-ua / ... instead of the verbose form.

Caption updated to match: "grouped by service_name (modbus-tcp /
mqtt / opc-ua, …)" so operators know exactly which column of the
driver table backs the visualisation.
2026-05-04 13:17:07 +08:00
pnoker f5c6bb9c7c refactor(home-analytics): drop tab icons — text-only reads cleaner
Per-tab icons (PieChart / Connection / Collection / Trophy×3) added
visual noise without carrying information the labels don't already
convey. The six-tab strip now reads calmer as plain text, and the
caption row below still tells operators exactly what each chart
measures.

Tab labels and captions are unchanged — only the icon column is gone.
2026-05-04 13:06:17 +08:00
pnoker a4c117a4af refactor(home-analytics): rewrite tab labels + add captions + icon grouping
The six analytics tabs were named by "what" (Device Status, Protocols,
TOP Devices, …) but never said "by which dimension" or "ranked how",
so operators couldn't tell what the chart was measuring. Rework along
three axes:

1. Tab labels now carry the subject AND the dimension:
     Device Status → Device Enable Status
     Protocols     → Drivers by Protocol
     Profiles      → Devices by Profile (TOP 10)
     TOP Devices   → Most Active Devices
     (plus TOP Points / Drivers analogously)

2. A caption line between the tab bar and the chart spells out the
   exact bucketing or ranking rule. Structural tabs get a fixed
   phrase; top-N tabs interpolate the active range segmented value so
   "ranked by write volume over Last 7 days" stays accurate when the
   user flips the control.

3. Tab icons split the grid into two visual groups:
   - Structural (deviceStatus / protocol / profile) → PieChart /
     Connection / Collection — each hints at the chart shape
   - Top-N (topDevice / topPoint / topDriver) → all share Trophy so
     the "ranking" concept reads at a glance; the subject is
     disambiguated by the label text.

Body height dropped 360 → 340 to leave room for the caption row; card
min-height stays 440 so the whole strip lines up with LiveDataFeed
next door.
2026-05-04 13:02:56 +08:00
pnoker 0cf1a47829 fix(settings): override el-main overflow so only Layout scrollbar drives the page
Element Plus ships <el-main> with overflow:auto — it's meant for the
"aside doesn't scroll, main scrolls" layout. In our stack the outer
Layout already wraps router-view in an el-scrollbar, so main's auto
overflow is redundant at best. At worst, flex-row sub-pixel rounding
lets main believe its content is 0.5px taller than itself and pops a
ghost scrollbar, which then eats wheel events over every settings
card. Reset main to overflow:visible so it never owns a scroll axis
of its own.

Drop the Settings-scoped <el-backtop target=".settings-main"> along
with it: with main no longer scrollable, that backtop had nothing
valid to scroll anyway — the Layout-level backtop on
.body .el-scrollbar__wrap already covers every settings sub-page.
2026-05-04 12:45:38 +08:00
pnoker 00cc1c380f fix(event-overview): drop nested scrollbar in RecentUnconfirmed + cap size to 5
The inner <el-scrollbar :max-height="320px"> turned RecentUnconfirmed
into an independent scroll container. Whenever the cursor hovered the
card and the list actually exceeded 320px (≥ 4-5 items with messages),
wheel events were consumed by the inner wrapper and the outer Layout
scrollbar couldn't advance — the page felt frozen until the mouse
moved onto a chart card.

Two-pronged fix:

1. Remove the el-scrollbar entirely so the timeline flows naturally
   inside the card. No nested scrollable means wheel events always
   reach the Layout scrollbar.

2. Drop alertPage size from 10 → 5 so five timeline items fit inside
   the diag-grid's existing min-height: 320px without overflowing.
   Five is enough for "recent unconfirmed at a glance"; operators who
   need more follow the Unconfirmed quick-action chip into the event
   list page (already the affordance this card links toward).
2026-05-04 00:00:49 +08:00
pnoker 7095ac9048 refactor(home): add subtitles to the 6 stat cards
All home cards now carry a second data point in their subtitle, mirroring
the new event-overview rhythm where no primary number stands alone:

- Driver / Device cards → "{n} cumulative alarms" — surfaces fleet
  health at a glance next to the fleet size.
- Point card → "across {n} profiles" — shows how the points are
  organised (via the already-loaded profileCount).
- Today's data card → unchanged ("Total {n}"), that subtitle was
  already doing the right job.
- Today's driver/device alarm cards → always show unconfirmed count
  (including 0) so card heights stay uniform across the grid instead
  of jumping when the backlog clears.
2026-05-03 23:46:44 +08:00
pnoker ca1270c1db refactor(event-overview): clarify card titles + unify subtitles
- Titles on the total/unconfirmed cards now carry an explicit 累计 /
  Total prefix so "Driver unconfirmed" is no longer ambiguous with the
  today cards — it reads as the cumulative backlog up to now, not the
  portion from today.
- Subtitles are unified so every card shows a second data point tied
  to the primary metric:
    - Total cards → how many of that total are still unhandled
      (e.g. "未确认 12/340 · 3%")
    - Unconfirmed cards → how large the backlog is against cumulative
      (e.g. "累计 340 条 · 占 3%")
    - Today cards → today's open count pulled from the already-returned
      todayDriverUnconfirmed / todayDeviceUnconfirmed fields
  The bare "→ open driver events" navigation hint is gone; a click
  anywhere on the card already handles that.

Drops three i18n keys (unconfirmedRatio, goToDriver, goToDevice) that
nothing references after this change.
2026-05-03 23:42:19 +08:00
pnoker ef6a81de06 fix(event-overview): restore trend indicators on all 6 cards
Two things were keeping the trend arrow + delta from showing up the way
Home's cards do:

1. sparkTrend returned null whenever the previous bucket was 0, which
   in a quiet event stream meant most cards had no indicator at all.
   Switch to Home's diff-style trend ("+5" / "-3" / "0") — we always
   have something to show as long as there are two buckets.

2. All six cards were sharing one sparkline pulled from alertStats'
   sparkline24h — and that series is the aggregate hourly count, not
   per-source. Driver and device cards ended up with identical shapes.
   Pull alertTrend(7) for per-day driver/device buckets and feed
   total/unconfirmed cards from whichever source they represent, so
   each card has its own shape and day-over-day delta. Today cards
   keep the 24h hourly bucket since "today" is intra-day.
2026-05-03 23:36:01 +08:00
pnoker dc4b5002cb feat(event-overview): reorder stats, add today cards, time-range chips, chart refresh
Event overview rework based on three separate asks — landed in one
commit because they all touch the same page:

- Reorder stat cards driver-first (driver-total → device-total →
  driver-unconfirmed → device-unconfirmed), matching the way tenants
  actually read alerts: driver-lane issues are the more common
  starting point for diagnosis.
- Add two "Today" cards (today-driver, today-device) sourced from
  alertStats.todayDriverAlarms / todayDeviceAlarms — backend already
  surfaces these for Home's today widgets, so no extra round-trip.
  Grid widens from 4 → 6 columns; breakpoint shifts to 3 cols at
  ≤1280px so cards don't squeeze below usable width.
- Quick-actions chips: drop "Last 24h" and surface "Today / 7d / 30d"
  (mapping to rangeKey values EventTable already handles). Thin
  vertical divider between confirmation-state chips (All, Unconfirmed)
  and time-range chips so the two groups read as groups.
- EventTrendChart and TopSourcesChart headers get a circular refresh
  button with its own loading spinner, consistent with how
  RecentUnconfirmed already does it.
2026-05-03 23:05:19 +08:00
pnoker 2539e1ece4 fix(settings): group parent-resource picker by type + cascade role-assign checks
Two UX fixes noticed while exercising the new settings forms:

1. ResourceEditForm's Parent Resource tree-select mixed all seven
   resource types into one flat tree — you couldn't tell MENU from
   API from DATA while picking a parent. Regroup the same way the
   role-assign dialog does: one disabled group node per type, real
   resources nested underneath. Group nodes can't be picked (prefixed
   id, disabled flag surfaced via `props.disabled`) so the model only
   ever carries a real resource id. Default-expanded so everything
   shows on first click rather than hiding behind type folders.

2. RoleAssignResources' trees had `check-strictly` so ticking a parent
   didn't tick its children — surprised users who expected
   "grant the whole folder" to Just Work. Drop the flag. el-tree's
   default cascade matches the mental model: ticking a parent selects
   all descendants, and `getCheckedKeys` returns them all on submit.
2026-05-03 22:54:21 +08:00
pnoker d54a146a1c feat(settings): resource detail tabs + menu detail parent name
Two resolved readability issues on the settings detail pages:

- ResourceDetail now mirrors UserDetail/RoleDetail with two extra
  tabs: "Assigned Roles" (backed by the new listRoleByResourceId
  backend endpoint) and "Child Resources" (walks the resource tree to
  locate the current node and renders its children as a tree-table).
  Loads lazily on first open with ?active= URL persistence, same as
  the other detail pages.

- MenuDetail's parentMenuId used to render a raw id. Resolve it via
  menuStore.findById (cached tree already loaded by Layout) so the
  descriptions row shows "Settings" instead of "1047". onMounted
  primes the tree in case the page is reached via a bookmark that
  skips Layout's initial fetch.
2026-05-03 22:46:00 +08:00
pnoker c3cd72a219 feat(settings-resource): tree-select parent + resolve entity names for API/MENU
Two fixes to the Resource page's readability:

1. Parent Resource was a free-text id input; reuse the existing
   /resource/tree endpoint to feed an el-tree-select with a synthetic
   Root node (top-level becomes a real choice). Same pattern as
   Role/Menu parent pickers.

2. Entity column rendered as a raw id for API and MENU rows because
   resolveEntityNames only covered DRIVER/DEVICE/POINT/PROFILE. Pull
   the API list (capped 1000, one-shot) into a local id→apiName map
   and resolve MENU rows via the already-cached menuStore (add a
   findById getter). Column header renames from "Entity ID" to
   "Entity" now that the raw id is gone in favor of the name.

menuStore.fetchTree is idempotent so we await it before name
resolution — Layout usually primes it on startup, but a hard reload
onto /settings/resource needs it before MENU entries render.
2026-05-03 22:41:51 +08:00
pnoker 26348bf3c4 refactor(settings-api): drop edit/delete/add — APIs are auto-registered
APIs are registered by each service at startup; editing them from the
UI would drift from the real routes the next time anything restarts.
Keep the list page read-only: no Add button, no row-level Edit or
Delete — just Detail.

ApiEditForm and its index.ts are deleted (nothing references them).
The tool bar loses its `#actions` slot (+ Plus icon import) and the
`@add` emit; the action column trims from 240px → 120px now that it
only carries one button.
2026-05-03 22:38:01 +08:00
pnoker 2dcf5413bb refactor(settings-role): drop assigned-list pane — the tree is the source of truth
The right-hand "Assigned Resources" table re-stated information already
visible on the left: checkboxes on the tree show what's selected, and
each tab label carries (selected/total). Rendering both made the dialog
wider than it needed to be and forced mental reconciliation between
two views of the same state.

Drop the pane. Dialog narrows to 640px, the tree stretches full-width,
and a terse "{count} selected" label in the header gives the global
count that used to justify the right pane.

Removed the now-unused assignedList / removeOne / typeTagType helpers
and the type-tag column — the per-tab label already shows the type
context.
2026-05-03 22:33:50 +08:00
pnoker 020fc91f43 refactor(settings-role): split resource tree into per-type tabs
One giant tree mixing MENU/API/DATA/DEVICE/... was visually noisy — you
couldn't tell at a glance which branch was which type, and cross-type
parent/child links (e.g. a DATA node under a MENU parent) made the
hierarchy misleading.

Left pane now shows a tab per resource type, each hosting its own
el-tree with only that type's nodes. Parent/child links are rebuilt
per-type so a DATA node whose source-tree parent is a MENU becomes a
root inside the DATA tab. Each tab label carries `(selected/total)`
so you can see at a glance where picks are sitting without flipping
through.

One el-tree per type with v-show + a ref map — each tree owns its own
checked state, onCheckChange merges across all trees into selectedIds.
The right pane now also carries a resourceTypeFlag tag on each row so
the type context survives even after the user moves off the tab.

Default tab lands on MENU when present, else first available.
2026-05-03 22:30:22 +08:00
pnoker 69727c1079 refactor(settings-role): rebuild resource assignment as tree + table
The role-resource transfer inherited the same problem the user-role
transfer had — flat columns, single-line labels, no horizontal scroll
— but it was also semantically wrong: resources form a tree and
el-transfer collapsed that structure into a flat list. Rebuild around
the actual shape of the data.

- Left pane: el-tree fed from /resource/tree, show-checkbox with
  check-strictly (a parent grant does not imply granting children —
  pick exactly what you mean). Has its own search box via filter-node.
- Right pane: el-table driven by a flattened node map from the tree,
  one row per selected resource with name/code/type and a per-row
  Remove button. Matches the visual rhythm of the new
  UserAssignRoles dual-table layout.

emit('save', roleId, addIds, removeBindIds, done) stays intact, so
Role.vue's onAssignResources needs no changes. assign/index.ts
deleted — the component is now single-file <script setup>.
2026-05-03 22:21:50 +08:00
pnoker 44a4b84cb2 feat(settings-role): add user and resource tabs to RoleDetail
Mirror UserDetail's pattern: 'detail' tab keeps the descriptions pane,
'user' tab shows assigned users via the new listUserByRoleId endpoint,
'resource' tab shows granted resources via the already-existing
listResourceByRoleId. Each tab loads on first open and writes
`?active=` to the URL so a reload stays on the selected tab.

Note RoleDetail.parentRoleId still displays as a raw id — the parent
role name would need a separate hop to render. Keep for a later pass;
not blocking this UX improvement.
2026-05-03 22:19:24 +08:00
pnoker 70687f255d feat(settings-role): tree-select parent role + load tree alongside list
Parent Role was a free-text id input — unusable since nobody knows the
numeric ids. Swap it for el-tree-select sourced from the new
POST /role/tree endpoint, rendered the same way MenuEditForm does its
parent picker.

- Synthesize a "Root" node (id=0) so top-level roles stay pickable.
- Load the tree alongside the paged list (initial + after
  add/update/delete) so edits see fresh parent choices.
- RoleEditForm now takes a treeData prop and resets parentRoleId to 0
  (not empty string) so el-tree-select's controlled value is valid
  from the start.
2026-05-03 22:18:08 +08:00
pnoker 5455ab57b0 feat(settings-user): add role and resource tabs to UserDetail
Mirror the DriverDetail / DeviceDetail pattern: keep the existing
descriptions pane on the 'detail' tab, add 'role' and 'resource' tabs
for the relations that the user is reached through. Each tab loads on
first open (rolesLoaded / resourcesLoaded guards) and writes
`?active=` to the URL so a reload stays on the selected tab.

Roles come from listRoleByUserId (already existed). Resources come from
the new listResourceByUserId backend endpoint — user → role →
resource is two hops, doing it in one call is cleaner than fanning out
per-role on the client.
2026-05-03 22:07:18 +08:00
pnoker 8a1fd133a7 refactor(settings-user): rebuild role assignment as a dual-table UI
el-transfer wedged every role onto a single truncated line inside a
cramped two-column layout that wouldn't scroll horizontally, so you
couldn't actually read the role name + code + remark together. Drop it
for two independent el-tables with their own search boxes, multi-select
checkboxes, and a center column of Assign/Remove buttons.

Assigned roles land in the right pane seeded from listRoleByUserId; the
left pane holds everything else (All - Assigned). Moving rows between
panes is purely client-side state; the save() diff + emit signature
against the parent are unchanged, so onAssignRoles keeps working with
no changes.

assign/index.ts is deleted — the component is now a single-file
<script setup>.
2026-05-03 22:04:24 +08:00
pnoker b76ee0b117 feat(settings): show creator/modifier columns in user/role/resource/menu lists
Pair with the backend change that starts populating creatorName/operatorName.
Two new columns land between the enable-tag and the time columns, with
a '-' fallback so legacy rows (written before the backend change) don't
show as blanks.
2026-05-03 21:51:20 +08:00
pnoker 9b7cc6cc83 refactor(ui): widen popconfirm + drop Language label from user dropdown
- global popconfirm styling: min-width 240px on main, vertical spacing
  between title and actions, button gap — fixes the cramped "delete?"
  bubble that felt like it was squeezed into a corner.
- Layout avatar dropdown: remove the standalone "Language" caption so
  the EN/中 segmented control sits cleanly on its own, stretched full
  width of the menu row. Unused layout.language i18n key deleted.
2026-05-03 21:43:06 +08:00
pnoker a5b9e04fbd fix(event-overview): cap RecentUnconfirmed height without stealing wheel events
Restore the inner el-scrollbar but use max-height so when the list is
short the wrapper collapses to fit content and wheel events bubble to
the outer el-main; once rows exceed the cap the scrollbar takes over.
2026-05-03 20:20:19 +08:00