Skip to content

build(deps-dev): bump js-yaml from 3.14.2 to 3.15.0#888

Open
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/npm_and_yarn/js-yaml-3.15.0
Open

build(deps-dev): bump js-yaml from 3.14.2 to 3.15.0#888
dependabot[bot] wants to merge 1 commit into
masterfrom
dependabot/npm_and_yarn/js-yaml-3.15.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Jun 30, 2026

Copy link
Copy Markdown
Contributor

Bumps js-yaml from 3.14.2 to 3.15.0.

Changelog

Sourced from js-yaml's changelog.

4.3.0, 3.15.0 - 2026-06-27

Security

  • Backported maxTotalMergeKeys option.

[5.2.0] - 2026-06-26

Added

  • Added maxTotalMergeKeys (10000) loader option to limit the total number of keys processed by YAML merge (<<) across one load() / loadAll() call.
  • Added maxAliases (-1) loader option to limit the number of YAML aliases per document.

Removed

  • maxMergeSeqLength replaced with maxTotalMergeKeys for limiting YAML merge processing.

Fixed

  • Round-trip of integers with exponential form (>= 1e21)

[5.1.0] - 2026-06-23

Added

  • Collection tags can finalize an incrementally populated carrier into a different result value.

Changed

  • [breaking] quoteStyle now selects the preferred quote style; use the restored forceQuotes option to force quoting non-key strings.

[5.0.0] - 2026-06-20

Added

  • Added named exports for schemas, tags, parser events and AST utilities.
  • Reworked JSON_SCHEMA and CORE_SCHEMA with spec-compliant scalar resolution rules, and added YAML11_SCHEMA.
  • Added realMapTag for lossless mappings with non-string and complex keys. Object-based mappings now reject complex keys instead of stringifying them.
  • Added dump() transform option for changing the generated AST before rendering.
  • Added dump() options seqInlineFirst, flowBracketPadding, flowSkipCommaSpace, flowSkipColonSpace, quoteFlowKeys, quoteStyle and tagBeforeAnchor.
  • Added formal data layers (events and AST) for modular data pipelines.
    • Added low-level parser (to events), presenter and visitor APIs.
  • Added the YAML Test Suite to the test set.

Changed

  • See the migration guide for upgrade notes.
  • Rewritten in TypeScript and reorganized the public API around flat named exports.

... (truncated)

Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.2 to 3.15.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](nodeca/js-yaml@3.14.2...3.15.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.15.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update Javascript code labels Jun 30, 2026
TimeToBuildBob pushed a commit to TimeToBuildBob/aw-webui that referenced this pull request Jul 3, 2026
…revamps (ActivityWatch#857)

* feat(ui): pre-release polish pass across views, with bug fixes and tests

Phase A — runtime bug fixes + regression tests
- visualizations/VisTimeline: guard queriedInterval access and redraw when
  the vis Timeline instance hasn't been constructed yet. Fixes:
  - "TypeError: can't access property 0, this.queriedInterval is undefined"
    on the Bucket detail view (Bucket.vue doesn't pass a queried interval).
  - "TypeError: can't access property setData, this.timeline is null"
    when revisiting another bucket before mounted's nextTick completes.
- views/WorkReport: aw-query's flood() takes one argument. Passing
  breakTime as a second argument made aw-server respond with 400
  "Tried to call function flood with invalid amount of arguments" and
  broke the entire view. Drop the second arg and implement break-time
  gap bridging client-side so the slider still works.
  Also fix thisWeek/thisMonth: they previously returned "N days ending
  today". They now resolve to the actual ISO-week / calendar-month
  boundaries.
- views/Trends: call activityStore.ensure_loaded() before
  query_category_time_by_period() so buckets.window/afk are populated.
  Without this the query referenced bid_window=undefined and threw
  "TypeError: can't access property endsWith, bid is undefined" on every
  /trends visit. Also fall back to the first available host when :host
  is absent from the route.
- views/Timeline: drop the duplicate "Events shown" block, move the
  misplaced Duration filter into the filter details table (it was added
  outside the filter table by an indentation slip in PR ActivityWatch#679), and gate
  "No events match selected criteria" on buckets!==null so it no longer
  flashes during loading.
- views/Alerts: filter the hostname list to hosts that actually have both
  window AND AFK buckets so Check stops failing with "There's no bucket
  named 'aw-watcher-afk_<host>'" when a stale legacy hostname is present.
- stores/buckets: sort hosts by (matches-server-hostname, has window+afk,
  recency) instead of recency alone, so default host picks across views
  prefer the device the webui is actually running on and skip stale
  hostnames whose only bucket has never had an event.
- views/Buckets: replace misleading "Last updated 0s ago" / empty Updated
  column (which was moment(undefined).fromNow()) with explicit "No events
  recorded yet" / "no events" copy when a bucket has never been written
  to. Also fix the dismissable→dismissible typo on the import error alert.
- views/Report: copy fix — events.slice(0, 500) on a desc-sorted list is
  the most recent 500, not the last 500.
- views/activity/Activity: anchor period-switch (day/week/month/year) on
  today when today falls inside the source period, so clicking year →
  month → week stays on the current month/week instead of jumping into
  the previous year.
- views/activity/ActivityView: replace the no-confirmation "Remove" button
  with a b-modal confirmation; add an explicit "this view doesn't exist"
  fallback when the route's view_id isn't found.
- views/Stopwatch: remove unreachable v-else "No history to show" branch;
  fix horizontal overflow on narrow widths; add Enter-to-start; drop the
  dev-internal "Using bucket" line; better empty-state copy.
- views/Home: remove invalid <p><ul> nesting; resolve API browser link
  relative to document path so it works behind a reverse proxy.
- views/settings/CategoryBuilder: stuck "Loading..." replaced with an
  explicit empty-state when no host with window/AFK buckets is available.
- Update stale activitywatch.readthedocs.io URLs to docs.activitywatch.net
  across Home / Buckets / SelectableVisualization / CategoryBuilder /
  Alerts / TimespiralView.

Tests
- test/unit/workReport: snapshot the generated aw-query string and assert
  flood() is always single-arg, so a future "flood(events, x)" regression
  fails locally instead of producing an HTTP 400 in production.
- test/unit/Trends: assert host fallback and that refresh() calls
  ensure_loaded (the missing call was the root of the endsWith crash).

Phase B — visual consistency pass
- Fix b-alert(style="warning") → variant="warning" in Search, Trends,
  Graph, Report, Alerts. The prop typo meant every "early development"
  banner rendered as the default info-blue instead of yellow.
- Standardize page titles on h3 (Timeline, Buckets, Stopwatch, Timespiral
  were h2/h1); demote Buckets' "Import and export" to h4 to match.
- Replace "Danger!" modal titles with descriptive ones ("Delete bucket?",
  "Delete all buckets for <host>?").
- Replace inline color: #XXX with .text-success / .text-danger /
  .text-muted / .text-warning across CategoryEditTree, CategoryEditModal,
  ActivePatternSettings, Alerts, Buckets, Search, Trends, Graph, Report,
  CategoryBuilder, SelectableVisualization. Theme/dark-mode safe.
- Add empty states to Score (Top productive/distracting), Alerts (no
  viable host), Timespiral (no AFK bucket on host).
- Give Timespiral a sensible default date range (last 7 days instead of
  hard-coded 2022-08-08).
- Fix the low-contrast #aaa selected-chip background in
  InputTimeInterval (failed WCAG AA); use #495057/white instead.
- Add .mr-1 icon spacing in Search/Graph/Report/Buckets icon+label
  buttons.

Phase C — view-level revamps
- views/Timeline: rebuild the toolbar as a flex row instead of the prior
  float/inline-block hack that overlapped at 800px. Filter dropdown is a
  styled popover with shadow; Duration sits with the other filters;
  keyboard-hint is right-aligned and clears on wrap.
- components/InputTimeInterval: collapse the three near-duplicate
  "Show last" controls into a single Mode pill (Last duration / Date
  range) plus the existing quick-range chips.
- views/Buckets: card header is a 2-column key/value grid with a clear
  "this device" badge; uniform column widths across all device tables
  via fields.thStyle; row actions are right-aligned with ghost-styled
  kebab dropdowns that appear borderless until hover; "Delete host"
  returned to the kebab menu. Drop the b-table responsive wrapper
  (table-layout: fixed makes it unnecessary, and removing it stops the
  kebab popovers from being clipped by the overflow-x scroll container).
- views/activity/Activity: replace the period select with a pill
  button-group (day / week / month / 7 days / 30 days); add tooltips
  + aria-labels for icon-only Filters/Refresh on narrow widths.
- visualizations/summary: truncate long window-title labels at 80
  characters so they don't visually run past their bar.
- visualizations/Score: replace hand-rolled "Top distracting:" label
  with proper empty-state copy; add a (?) tooltip explaining how the
  score is computed; promote the score number into a styled element.
- views/settings/Settings: replace the flat list of 10+ hr-separated
  rows with a vertical pill navigation: General / Appearance /
  Notifications / Categorization / Privacy / Developer. Sticky on wide
  screens; collapses to a horizontal scrollable pill row on <768px.

* fix(ui): TS unit cast in Activity period anchor + Settings spacing + snapshot stability

- views/activity/Activity: moment.add() doesn't accept "isoWeek" as a
  DurationConstructor (startOf accepts both spellings, add doesn't).
  Pull the unit into a local and cast it explicitly so tsc stops
  failing the build on the period-switch anchoring helper.
- views/settings/Settings: tighter vertical rhythm — each child gets
  consistent 1.25rem top spacing, b-form-groups stop accumulating
  bottom-margin, hr/section padding refined, mobile grid gap tuned.
- util/workReport: strip per-line trailing whitespace from the
  generated aw-query string so the snapshot test stays stable under
  the trailing-whitespace pre-commit hook. aw-query is whitespace-
  tolerant so no runtime effect.

* ui(settings): Theme as button-group, rename Notifications → Updates

- Theme: replace the 3-option dropdown with an outline-dark button
  group (Auto / Light / Dark). One click instead of dropdown-then-pick,
  and clearly shows the current choice.
- Rename the "Notifications" group → "Updates" (the only setting under
  it was the new-release check, which isn't really a notification).
- Relabel "New release notification" → "Check for new releases" and
  reword the help text so it reads as a hint, not a push notification.

* ui(settings): add icons to the Theme buttons (Auto/Light/Dark)

* fix(ui): dark mode patches + Buckets kebab regressions

- Buckets: dropdown menus were invisible because the bucket-table td
  inherited overflow:hidden from my earlier ellipsis attempt. Apply
  truncation only to the .bucket-id span (where it actually belongs)
  and leave the td unclipped so the dropdown popover is visible.
- Buckets: replace variant="danger" on b-dropdown-item-button with
  button-class="text-danger" so the danger color is applied as an
  explicit class and survives dark-mode dropdown-item overrides.
- Theme: rename "Auto" → "System". More precise (GitHub/VS Code/iOS/
  macOS/Slack/Discord all use "System"); "Auto" can read as vague.
- static/dark.css: add overrides for the new components introduced by
  this PR — Timeline toolbar chips and filter popover, InputTimeInterval
  quick-range chips, Buckets ghost kebab and "unknown" device card,
  Settings vertical pill nav, Activity period button-group. Also keep
  destructive dropdown items obviously red (#ff6b6b) in dark mode
  instead of getting wiped by the global .dropdown-item color override.

* fix(ui): dark-mode hover legibility for non-responsive b-tables

The original .table-responsive hover-row rule only fired inside the
table-responsive-* wrapper. Dropping that wrapper from Buckets meant
the default Bootstrap tr:hover (color: #212529) made dark-mode row
text effectively invisible. Generalize to .table-hover tbody tr:hover
so both responsive and non-responsive tables stay legible.

* ui(activity,timeline): unified date input + filter icon

- views/activity/Activity: use the same native date picker for every
  period mode (day/week/month/year/7d/30d) instead of switching to a
  read-only b-input-group-text that displayed a long "YYYY-MM-DD —
  YYYY-MM-DD" range and broke alignment with the period button-group.
  Fixed width (9.5rem) keeps the control aligned. Full range still
  shown in the page heading and on hover via title attribute.
- views/Timeline: add a filter icon next to "Filters:" so the Timeline
  filter affordance matches the Activity view's Filters button.

* ui(activity): rolling ranges as primary pills, calendar periods in kebab

- Primary pills: day / 7 days / 30 days. Rolling windows always
  represent a full N days of data, whereas calendar periods show only
  a partial range until the boundary (e.g. "this week" on a Tuesday
  has 2 days).
- Kebab dropdown holds week / month / year for week-over-week style
  comparisons. Custom range can slot in here later without disturbing
  the primary row.
- Import ellipsis-v icon explicitly so the kebab renders (vue-awesome
  raises "this.icon is undefined" otherwise).

* ui(timeline,activity): reorder toolbars + restore pill rounding

- views/Timeline: reorder toolbar to Filters → Display kebab (Swimlanes
  options) → event count → keyboard hint. Filters is the most-used
  control so it leads; Swimlanes was getting outsized visual weight
  for what's a tertiary display option.
- views/activity/Activity: move the period kebab out of the
  b-button-group so the last primary pill ("30 days") keeps its
  rounded right corner. ml-1 gap keeps them visually adjacent.
- Import ellipsis-v in Timeline.vue.

* ui(activity,timeline): tighten toolbar vertical rhythm with row-gap

The mb-2 piled on every flex-wrap child compounded into ~1rem of dead
space below the toolbar row at full width when nothing actually wrapped.
Replace it with row-gap on the flex container, which only inserts
spacing when items wrap. Container keeps a single mb of 0.5rem instead
of children + container both adding margin.

Also drops the redundant .mt-2 above aw-periodusage in the Activity
view so the periodusage strip sits flush against the toolbar.

* ui(timeline): fix InputTimeInterval Mode/Range alignment

The secondary label was switching between "Quick range" and "Range"
depending on mode, and since the labels had different widths the
controls shifted horizontally on every toggle. Both rows are now in a
2-column grid (4rem label column + flexible control column), and the
secondary label is "Range" in both modes — the Mode toggle already
disambiguates which kind of range is being picked.

* ui(work-report): clearer category picker + auto subcategory inclusion

The native <select multiple> listbox was the least-discoverable shape
for multi-select (Cmd/Ctrl-click is not obvious), and the filter only
matched exact category arrays, so selecting "Work" missed "Work >
Programming" — the opposite of what most users expect.

- Replace the listbox with the Timeline-style add-as-badge picker:
  a dropdown to add categories and removable badges showing the
  current selection.
- Expand the selection to all descendants of each selected category
  before submitting the query. aw-query's filter_keyvals does exact
  array matches on $category, so this expansion is necessary to make
  parent-category selection behave intuitively.
- Inline hint clarifies the behavior.

* ui(activity): surface active extended period as a pressed pill

When week/month/year is selected from the kebab, the primary row
previously showed nothing pressed — leaving users unsure what was
active. Render the active extended period as an extra pressed pill
inside the button-group ("day | 7 days | 30 days | year"), and keep
the kebab visible so other extended ranges remain reachable. Clicking
the pressed extended pill goes back to "day".

* ui(stopwatch): refresh banner copy — top_stopwatches vis already exists

The "Data entered here is not shown in the Activity view, yet" warning
is stale: stopwatch events are already queryable (canonicalEvents'
bid_stopwatch flag unions them with window events) and the
"Top Stopwatch Events" visualization has shipped — it just isn't in
the default Summary view because adding it would surface a noisy
"missing data" banner for the (many) users without stopwatch buckets.

Replace the deprecation-flavored copy with a usage hint pointing
people at "+ New view" / "Add visualization" to surface their
stopwatch totals.

* ui(stopwatch): tighten banner copy — exact path is Edit view → Add visualization

* fix(activity): default include_stopwatch to true so the vis shows data

The "Top Stopwatch Events" visualization queries top_stopwatches in
the activity store, which is only populated when query_desktop_full's
include_stopwatch path runs (it sets bid_stopwatch and the query then
returns data[0].stopwatch). That flag defaulted to false and was only
toggleable from a dev-only filter, so a user who started a stopwatch
and added the vis correctly saw "No data" until they manually flipped
a hidden dev-mode switch.

Default it to true. canonicalEvents only sets bid_stopwatch when a
stopwatch bucket exists, so users without stopwatch buckets are
unaffected. The dev-mode UI toggle stays — it still controls whether
stopwatch events override window events during overlap.

* feat(trends): real trend stats — current vs previous period comparison

Trends used to share the activity store's by_period output and showed
only the per-day stacked barchart from the Activity Summary, which is
redundant. Now the view runs its own pair of queries:

- queryByDay over the selected window (7/30/90 days), one query per day
- queryTotals over the equal-length previous window

and renders summary cards (Active time, Daily average, Most-active
day) with deltas vs the previous window, the per-day barchart, and a
"Top changes by category" table sorted by absolute change.

Also fixes the stuck-loading bug: byPeriod entries are now stored as
{ cat_events: [...] } so buildBarchartDataset sees the shape it
expects (Object.values(data).map(result => result.cat_events.map(...))
used to throw "cannot read properties of undefined (reading 'map')").

Loading is gated on an explicit flag; the barchart's null/empty
handling now reflects "no data" vs "loading" correctly.

* fix(search): variant=warning (was style=warning, a no-op typo)

The "early development" alert on Search rendered as default info-blue
in light mode and as default info-teal in dark mode instead of the
intended warning yellow. Same prop typo I fixed in Trends/Graph/
Report/Alerts earlier but Search slipped through one of the earlier
edits because of a concurrent file rewrite.

* ui(dark): polish — alert links, card elevation, Timespiral labels

- Alert links across all variants (.alert-success/info/warning/danger
  in dark mode) inherited Bootstrap's darker shade and ended up nearly
  iso-luminant with the alert background — e.g. the "aw-webui#365"
  link on Graph's yellow warning was unreadable. Pin them to white +
  underline; hover dips opacity to 0.85.
- Card backgrounds bumped from #1a1d24 (same as aw-container) to
  #21252c so cards on Trends/Activity/Buckets read as elevated
  surfaces instead of blending into the page. Border tone bumped to
  match.
- Timespiral clock labels and date rings (ActivityWatch#888 / #ccc inline styles)
  were dim against the dark canvas. Add a #timespiral-scoped svg text
  override that brightens them to #d8dbe1 at 95% opacity, leaving the
  generic svg text rule alone so other charts aren't affected.

* fix(dark): Categorization "Category set:" panel respects dark theme

The category-set switcher box used an inline
  style="background: var(--bs-light, #f8f9fa); border-radius: 4px"
which beat dark.css since inline styles win over stylesheet rules.
Result: white block with white labels in dark mode.

Replace with the .bg-light + .rounded utility classes (dark.css already
overrides .bg-light to #1a1d24) and swap the inline var(--bs-secondary)
help text for the .text-muted utility for the same reason.

* ui(settings): URL-routed active group + wrap nav instead of scroll

- /settings/:group now persists which panel is open. Reloading
  /settings/categorization stays on Categorization; the nav uses
  router-link with replace so back/forward stays sane.
- The settings store falls back to /settings/general for unknown
  groups (and for the bare /settings path), avoiding broken state.

- At <768px the horizontal pill row used overflow-x: auto, which
  left pills like "Developer" off-screen and let the inner scrollbar
  push extra horizontal scroll space onto xs viewports. Switch to
  flex-wrap so pills break onto multiple rows, and constrain the
  nav with min-width: 0 / max-width: 100% so it can never push the
  layout wider than the viewport.

* feat(settings): embed Category Builder + lighter Stopwatch help

Embed:
- CategorizationSettings adds a "Category builder" subsection with a
  b-collapse + "Open builder" toggle. The CategoryBuilder component
  loads lazily (async import + v-if guard on builderMounted) so users
  who just want to edit rules don't pay the words-query cost.
- CategoryBuilder accepts an `embedded` prop. When true it skips the
  standalone page chrome (h3 + intro paragraphs + "back to Settings"
  link) so the embed reads as a subsection of its parent.
- /settings/category-builder redirects to /settings/categorization so
  external bookmarks still land in the right place.
- Dedupe the stray "name: 'aw-category-builder'" + empty props block
  that ESLint flagged as duplicate keys.

Stopwatch help:
- The "Track manually-logged sessions…" info banner was heavy for a
  one-time explainer. Replace with a (?) icon next to the title that
  opens a b-popover on hover/focus/click — works on both desktop and
  mobile, takes zero vertical space when not in use, and the help
  text stays available for users who need it.

* fix(dark): popover and tooltip colors

BootstrapVue's b-popover renders as a .popover with .popover-header /
.popover-body. In dark mode these inherited the default Bootstrap
backgrounds (white-ish) and ended up white-on-white. Same story for
.tooltip-inner.

Add dark.css rules for the popover/tooltip surfaces, including the
arrow ::before/::after pseudo-elements so the pointer tracks the
themed background instead of flashing a white triangle.

* ui(timeline): shorten bucket labels, disambiguate only on collision

- util/timelineLabels: add shortenBucketLabel() which strips the
  aw-watcher- / aw- prefix and the _<hostname> tail so a bucket id
  like aw-watcher-window_erb-m2.localdomain reads as "window".
  formatTimelineBucketLabelHtml uses the short form by default; the
  full id stays in the <title> tooltip for unambiguity.
- VisTimeline computes a labelCounts map and only appends the
  hostname suffix ("window @ host") when two buckets would otherwise
  share the same short label — single-host setups stay clean, and
  multi-host or sync setups disambiguate per-row only where needed.
- Replace the blocking alert("Changes won't be reflected…") on first
  event edit with a one-time b-toast (auto-hides after 6s, dismissal
  persisted in localStorage). The alert fired on top of the editor
  and interrupted the edit flow.

Updated tests cover both the helper and the format function.

* ui(timeline): consistent host suffix + hostnameless watcher exception

Previous pass inconsistently applied the "@ host" suffix per-row based
on whether each row's short label collided. With one window bucket on
two hosts and one afk bucket on a single host, that produced
  stopwatch
  afk
  window @ erb-m2.localdomain
which reads as random.

- Now all-or-nothing: if ANY two host-attributed buckets share a short
  label, every host-attributed row gets the "@ host" suffix. Single-
  host setups keep clean labels across the board.
- Stopwatch and aw-watcher-web-* don't currently carry a real
  hostname (they sit under "unknown"). They're excepted so they never
  get a noisy "@ unknown" suffix — left a TODO for the future
  migration that gives those watchers per-host bucket ids.

* ui(category-builder): paginate the words list with "Show more"

A typical week of uncategorized activity produces 30-80 word rows, each
with four action buttons. Rendering them all at once turned the embedded
Category Builder into a 2+ screen scroll-wall that buried the rules tree
above it.

Show 10 at a time (page_size=10) with a footer that reports
"Showing N of M words" and a "Show more" button that reveals the next
page_size. fetchWords() resets visible_count on every requery so the
user sees the top of the new ranking after changing options.

* ui(categorization): move Category Builder above the rules tree

Burying the builder collapse below a multi-screen list of categories
made it invisible — users had to know it was there. Since it stays
collapsed by default (and only mounts the inner component when
expanded), there's no cost to surfacing it near the top. New layout:

  Intro / Category set switcher
  → Category builder collapse (closed by default)
  → Rules tree
  → Add category / Save

Rules editing still happens in-place, and the builder's "New rule" /
"Append rule" actions still reflect into the tree right below it.

* ui(categorization): restructure — drop dup heading, move builder to end

- Drop the top-of-page "Categorization" h5 — it duplicated the
  Settings page heading that wraps the panel. Action toolbar
  (Restore defaults / Import / Export) moves down next to a new
  "Categories" h5 that sits right above the rules tree, so the
  heading actually labels the list it precedes.
- Move ActivePatternSettings to the bottom of the Categorization
  group (Settings.vue) — it's an edge-case AFK override, not the
  thing most users come here for.
- Move Category Builder back to the end of the categories list. Now
  that ActivePatternSettings sits below CategorizationSettings, the
  builder is no longer the very last thing on the page so it's not
  buried; meanwhile having it right above the rules list felt weirdly
  squeezed between unrelated content (set switcher → builder →
  rules).

* ui(categorization): UncategorizedNotification link opens + scrolls to builder

Clicking the "Category Builder" link in the uncategorized-time alert
used to redirect to /settings/categorization (since the standalone
route now redirects there) and dump the user at the top of a long
rules tree with the builder still collapsed and out of view.

- Link now targets /settings/categorization?builder=open.
- CategorizationSettings reads the query on mount, sets builderOpen
  (which also flips builderMounted via the existing watcher so the
  inner component lazy-loads), then scrolls the builder section into
  view on next tick.

ref="builderSection" added to the wrapping div as the scroll target.

* ui(settings): add breathing room at the bottom of each panel

Long subviews (Categorization in particular) ended flush against the
card border, and short ones stopped just above the sticky nav's last
item — both felt abrupt. Give each .settings-section a 3rem
padding-bottom so the content always has air below it.

* ui(settings): fold "Check for new releases" into General, drop Updates panel

A one-setting nav entry didn't earn its own panel. Append
ReleaseNotificationSettings to the General group's component list
(skipped on Android, same as before), drop the Updates group + its
route param.

* ui(settings): trim panel bottom padding 3rem → 1rem (pb-3 in Bootstrap)

* ui(settings): standardize help text on .text-muted

Some sub-option descriptions were bare <small> (inherits body color
→ white in dark mode) while others were <small.text-muted> (grey).
Theme/ReleaseNotificationSettings already had it; bring the rest in
line: ColorSettings, DaystartSettings (both descriptions), Landing
Page, TimelineDuration, ActivePattern.

* ui(settings): drop redundant 'Developer settings' heading

The panel header is already 'Developer' (from the Settings page nav
group). The inner h4 duplicated it. Also flip the warning alert from
the default info-blue to variant=warning since it's a caution, not
informational chatter.

* ui(settings): keep example regex on one line

The example pattern (Zoom Meeting|Google Meet|Microsoft Teams) used to
wrap mid-expression at narrow widths, which made the pipe-separated
alternation hard to read. inline-block + white-space: nowrap keeps it
contiguous (it'll overflow visually on very narrow viewports but stay
parseable).

* ui(settings): bind 'Example expression:' label to its code as one no-wrap unit

Wrap the label + code in a single span.text-nowrap so they always wrap
together — the label sits beside the code if the line has room, or
the whole label-plus-code unit moves to a new line if it doesn't. The
label no longer dangles alone above the code.

* fix(settings): privacy filter '[]' pug parsing escaped the closing bracket

Pug interprets ] as the end of an inline #[…] expansion, so
'#[code []]' rendered as '<code>[</code>]' — the open bracket got
the code/white color, the close bracket fell out as plain text. Swap
to a raw <code>[]</code> tag so both brackets are inside the code
element.

* fix(dark): outline-* buttons consistent across <button> and <label.btn>

The generic 'button { color: !important }' rule near the top of
dark.css wipes Bootstrap's variant text colors on <button> elements
but doesn't touch <label class='btn'> (which is how the Categorization
'Import' file-input control is rendered). That made Import blue and
Export white even though both used btn-outline-primary.

Add explicit overrides for outline-primary / outline-warning /
outline-danger that target both .btn-outline-* and label.btn-outline-*
so they look the same regardless of underlying element.

* feat(settings): make the uncategorized-time hint configurable + dismissible

The "High uncategorized time" banner had two TODOs in code: no way to
disable it and the thresholds were hard-coded. Both fixed.

- Add settings store key uncategorizedNotificationData with isEnabled,
  minTotalSeconds, minRatio — defaults preserve the previous behavior
  (1h + 30%).
- UncategorizedHintSettings.vue panel surfaces them under Settings →
  General: enable toggle, "Minimum total tracked time" in minutes,
  "Minimum uncategorized share" in percent.
- UncategorizedNotification reads the config, becomes dismissible
  (clicking × disables the hint outright and persists via settings).
  Inline link to Settings under the banner so the path back is
  discoverable.

* fix(ui): drop .text-muted on the uncategorized hint footer link

.text-muted forces a grey color that reads as low-contrast against
both the light and dark variants of the .alert-info background.
Inherit the alert's text color instead and use a small opacity dip
(0.85) to keep the line visually subordinate without sacrificing
readability.

* ui(hint): replace footer link with small ⚙ icon next to title

The 'You can hide or adjust this hint in Settings.' line made the
alert taller than it needed to be. Put a small cog icon (vue-awesome
'cog' at scale 0.85) next to the title — same destination, inherits
the alert's text color, fades to 70% opacity by default so it doesn't
compete with the title, full opacity on hover/focus.

* ui(hint): dim the settings cog further (0.7 → 0.45)

* ui(score): move help (?) to top-right corner of the widget

The (?) icon was inline at the end of 'Your total score for today is',
which (a) made an already-wordy heading even longer and (b) sat at an
odd vertical baseline next to the running text. Move it to the
top-right corner of the centered score block as a small ghost button
(0.45 opacity, brightens on hover/focus) and trim the heading to
'Score for today'.

* test(trends): drop stale ensure_loaded assertion

The Trends rebuild moved off activityStore.ensure_loaded() — refresh
now queries aw-server directly via getClient(). The 'calls
ensure_loaded' test was asserting an implementation detail that no
longer exists and was failing in CI. Its regression scope (the
bid.endsWith crash from missing host buckets) is still covered by the
host-fallback + no-host-bail-out tests.

* lint(route): drop unused CategoryBuilder import

CategoryBuilder is no longer a standalone route (the path now redirects
into CategorizationSettings, which embeds the component directly). The
const reference left over from the old route declaration triggered
@typescript-eslint/no-unused-vars and CI runs with --max-warnings=0.
TimeToBuildBob pushed a commit to TimeToBuildBob/aw-webui that referenced this pull request Jul 3, 2026
…revamps (ActivityWatch#857)

* feat(ui): pre-release polish pass across views, with bug fixes and tests

Phase A — runtime bug fixes + regression tests
- visualizations/VisTimeline: guard queriedInterval access and redraw when
  the vis Timeline instance hasn't been constructed yet. Fixes:
  - "TypeError: can't access property 0, this.queriedInterval is undefined"
    on the Bucket detail view (Bucket.vue doesn't pass a queried interval).
  - "TypeError: can't access property setData, this.timeline is null"
    when revisiting another bucket before mounted's nextTick completes.
- views/WorkReport: aw-query's flood() takes one argument. Passing
  breakTime as a second argument made aw-server respond with 400
  "Tried to call function flood with invalid amount of arguments" and
  broke the entire view. Drop the second arg and implement break-time
  gap bridging client-side so the slider still works.
  Also fix thisWeek/thisMonth: they previously returned "N days ending
  today". They now resolve to the actual ISO-week / calendar-month
  boundaries.
- views/Trends: call activityStore.ensure_loaded() before
  query_category_time_by_period() so buckets.window/afk are populated.
  Without this the query referenced bid_window=undefined and threw
  "TypeError: can't access property endsWith, bid is undefined" on every
  /trends visit. Also fall back to the first available host when :host
  is absent from the route.
- views/Timeline: drop the duplicate "Events shown" block, move the
  misplaced Duration filter into the filter details table (it was added
  outside the filter table by an indentation slip in PR ActivityWatch#679), and gate
  "No events match selected criteria" on buckets!==null so it no longer
  flashes during loading.
- views/Alerts: filter the hostname list to hosts that actually have both
  window AND AFK buckets so Check stops failing with "There's no bucket
  named 'aw-watcher-afk_<host>'" when a stale legacy hostname is present.
- stores/buckets: sort hosts by (matches-server-hostname, has window+afk,
  recency) instead of recency alone, so default host picks across views
  prefer the device the webui is actually running on and skip stale
  hostnames whose only bucket has never had an event.
- views/Buckets: replace misleading "Last updated 0s ago" / empty Updated
  column (which was moment(undefined).fromNow()) with explicit "No events
  recorded yet" / "no events" copy when a bucket has never been written
  to. Also fix the dismissable→dismissible typo on the import error alert.
- views/Report: copy fix — events.slice(0, 500) on a desc-sorted list is
  the most recent 500, not the last 500.
- views/activity/Activity: anchor period-switch (day/week/month/year) on
  today when today falls inside the source period, so clicking year →
  month → week stays on the current month/week instead of jumping into
  the previous year.
- views/activity/ActivityView: replace the no-confirmation "Remove" button
  with a b-modal confirmation; add an explicit "this view doesn't exist"
  fallback when the route's view_id isn't found.
- views/Stopwatch: remove unreachable v-else "No history to show" branch;
  fix horizontal overflow on narrow widths; add Enter-to-start; drop the
  dev-internal "Using bucket" line; better empty-state copy.
- views/Home: remove invalid <p><ul> nesting; resolve API browser link
  relative to document path so it works behind a reverse proxy.
- views/settings/CategoryBuilder: stuck "Loading..." replaced with an
  explicit empty-state when no host with window/AFK buckets is available.
- Update stale activitywatch.readthedocs.io URLs to docs.activitywatch.net
  across Home / Buckets / SelectableVisualization / CategoryBuilder /
  Alerts / TimespiralView.

Tests
- test/unit/workReport: snapshot the generated aw-query string and assert
  flood() is always single-arg, so a future "flood(events, x)" regression
  fails locally instead of producing an HTTP 400 in production.
- test/unit/Trends: assert host fallback and that refresh() calls
  ensure_loaded (the missing call was the root of the endsWith crash).

Phase B — visual consistency pass
- Fix b-alert(style="warning") → variant="warning" in Search, Trends,
  Graph, Report, Alerts. The prop typo meant every "early development"
  banner rendered as the default info-blue instead of yellow.
- Standardize page titles on h3 (Timeline, Buckets, Stopwatch, Timespiral
  were h2/h1); demote Buckets' "Import and export" to h4 to match.
- Replace "Danger!" modal titles with descriptive ones ("Delete bucket?",
  "Delete all buckets for <host>?").
- Replace inline color: #XXX with .text-success / .text-danger /
  .text-muted / .text-warning across CategoryEditTree, CategoryEditModal,
  ActivePatternSettings, Alerts, Buckets, Search, Trends, Graph, Report,
  CategoryBuilder, SelectableVisualization. Theme/dark-mode safe.
- Add empty states to Score (Top productive/distracting), Alerts (no
  viable host), Timespiral (no AFK bucket on host).
- Give Timespiral a sensible default date range (last 7 days instead of
  hard-coded 2022-08-08).
- Fix the low-contrast #aaa selected-chip background in
  InputTimeInterval (failed WCAG AA); use #495057/white instead.
- Add .mr-1 icon spacing in Search/Graph/Report/Buckets icon+label
  buttons.

Phase C — view-level revamps
- views/Timeline: rebuild the toolbar as a flex row instead of the prior
  float/inline-block hack that overlapped at 800px. Filter dropdown is a
  styled popover with shadow; Duration sits with the other filters;
  keyboard-hint is right-aligned and clears on wrap.
- components/InputTimeInterval: collapse the three near-duplicate
  "Show last" controls into a single Mode pill (Last duration / Date
  range) plus the existing quick-range chips.
- views/Buckets: card header is a 2-column key/value grid with a clear
  "this device" badge; uniform column widths across all device tables
  via fields.thStyle; row actions are right-aligned with ghost-styled
  kebab dropdowns that appear borderless until hover; "Delete host"
  returned to the kebab menu. Drop the b-table responsive wrapper
  (table-layout: fixed makes it unnecessary, and removing it stops the
  kebab popovers from being clipped by the overflow-x scroll container).
- views/activity/Activity: replace the period select with a pill
  button-group (day / week / month / 7 days / 30 days); add tooltips
  + aria-labels for icon-only Filters/Refresh on narrow widths.
- visualizations/summary: truncate long window-title labels at 80
  characters so they don't visually run past their bar.
- visualizations/Score: replace hand-rolled "Top distracting:" label
  with proper empty-state copy; add a (?) tooltip explaining how the
  score is computed; promote the score number into a styled element.
- views/settings/Settings: replace the flat list of 10+ hr-separated
  rows with a vertical pill navigation: General / Appearance /
  Notifications / Categorization / Privacy / Developer. Sticky on wide
  screens; collapses to a horizontal scrollable pill row on <768px.

* fix(ui): TS unit cast in Activity period anchor + Settings spacing + snapshot stability

- views/activity/Activity: moment.add() doesn't accept "isoWeek" as a
  DurationConstructor (startOf accepts both spellings, add doesn't).
  Pull the unit into a local and cast it explicitly so tsc stops
  failing the build on the period-switch anchoring helper.
- views/settings/Settings: tighter vertical rhythm — each child gets
  consistent 1.25rem top spacing, b-form-groups stop accumulating
  bottom-margin, hr/section padding refined, mobile grid gap tuned.
- util/workReport: strip per-line trailing whitespace from the
  generated aw-query string so the snapshot test stays stable under
  the trailing-whitespace pre-commit hook. aw-query is whitespace-
  tolerant so no runtime effect.

* ui(settings): Theme as button-group, rename Notifications → Updates

- Theme: replace the 3-option dropdown with an outline-dark button
  group (Auto / Light / Dark). One click instead of dropdown-then-pick,
  and clearly shows the current choice.
- Rename the "Notifications" group → "Updates" (the only setting under
  it was the new-release check, which isn't really a notification).
- Relabel "New release notification" → "Check for new releases" and
  reword the help text so it reads as a hint, not a push notification.

* ui(settings): add icons to the Theme buttons (Auto/Light/Dark)

* fix(ui): dark mode patches + Buckets kebab regressions

- Buckets: dropdown menus were invisible because the bucket-table td
  inherited overflow:hidden from my earlier ellipsis attempt. Apply
  truncation only to the .bucket-id span (where it actually belongs)
  and leave the td unclipped so the dropdown popover is visible.
- Buckets: replace variant="danger" on b-dropdown-item-button with
  button-class="text-danger" so the danger color is applied as an
  explicit class and survives dark-mode dropdown-item overrides.
- Theme: rename "Auto" → "System". More precise (GitHub/VS Code/iOS/
  macOS/Slack/Discord all use "System"); "Auto" can read as vague.
- static/dark.css: add overrides for the new components introduced by
  this PR — Timeline toolbar chips and filter popover, InputTimeInterval
  quick-range chips, Buckets ghost kebab and "unknown" device card,
  Settings vertical pill nav, Activity period button-group. Also keep
  destructive dropdown items obviously red (#ff6b6b) in dark mode
  instead of getting wiped by the global .dropdown-item color override.

* fix(ui): dark-mode hover legibility for non-responsive b-tables

The original .table-responsive hover-row rule only fired inside the
table-responsive-* wrapper. Dropping that wrapper from Buckets meant
the default Bootstrap tr:hover (color: #212529) made dark-mode row
text effectively invisible. Generalize to .table-hover tbody tr:hover
so both responsive and non-responsive tables stay legible.

* ui(activity,timeline): unified date input + filter icon

- views/activity/Activity: use the same native date picker for every
  period mode (day/week/month/year/7d/30d) instead of switching to a
  read-only b-input-group-text that displayed a long "YYYY-MM-DD —
  YYYY-MM-DD" range and broke alignment with the period button-group.
  Fixed width (9.5rem) keeps the control aligned. Full range still
  shown in the page heading and on hover via title attribute.
- views/Timeline: add a filter icon next to "Filters:" so the Timeline
  filter affordance matches the Activity view's Filters button.

* ui(activity): rolling ranges as primary pills, calendar periods in kebab

- Primary pills: day / 7 days / 30 days. Rolling windows always
  represent a full N days of data, whereas calendar periods show only
  a partial range until the boundary (e.g. "this week" on a Tuesday
  has 2 days).
- Kebab dropdown holds week / month / year for week-over-week style
  comparisons. Custom range can slot in here later without disturbing
  the primary row.
- Import ellipsis-v icon explicitly so the kebab renders (vue-awesome
  raises "this.icon is undefined" otherwise).

* ui(timeline,activity): reorder toolbars + restore pill rounding

- views/Timeline: reorder toolbar to Filters → Display kebab (Swimlanes
  options) → event count → keyboard hint. Filters is the most-used
  control so it leads; Swimlanes was getting outsized visual weight
  for what's a tertiary display option.
- views/activity/Activity: move the period kebab out of the
  b-button-group so the last primary pill ("30 days") keeps its
  rounded right corner. ml-1 gap keeps them visually adjacent.
- Import ellipsis-v in Timeline.vue.

* ui(activity,timeline): tighten toolbar vertical rhythm with row-gap

The mb-2 piled on every flex-wrap child compounded into ~1rem of dead
space below the toolbar row at full width when nothing actually wrapped.
Replace it with row-gap on the flex container, which only inserts
spacing when items wrap. Container keeps a single mb of 0.5rem instead
of children + container both adding margin.

Also drops the redundant .mt-2 above aw-periodusage in the Activity
view so the periodusage strip sits flush against the toolbar.

* ui(timeline): fix InputTimeInterval Mode/Range alignment

The secondary label was switching between "Quick range" and "Range"
depending on mode, and since the labels had different widths the
controls shifted horizontally on every toggle. Both rows are now in a
2-column grid (4rem label column + flexible control column), and the
secondary label is "Range" in both modes — the Mode toggle already
disambiguates which kind of range is being picked.

* ui(work-report): clearer category picker + auto subcategory inclusion

The native <select multiple> listbox was the least-discoverable shape
for multi-select (Cmd/Ctrl-click is not obvious), and the filter only
matched exact category arrays, so selecting "Work" missed "Work >
Programming" — the opposite of what most users expect.

- Replace the listbox with the Timeline-style add-as-badge picker:
  a dropdown to add categories and removable badges showing the
  current selection.
- Expand the selection to all descendants of each selected category
  before submitting the query. aw-query's filter_keyvals does exact
  array matches on $category, so this expansion is necessary to make
  parent-category selection behave intuitively.
- Inline hint clarifies the behavior.

* ui(activity): surface active extended period as a pressed pill

When week/month/year is selected from the kebab, the primary row
previously showed nothing pressed — leaving users unsure what was
active. Render the active extended period as an extra pressed pill
inside the button-group ("day | 7 days | 30 days | year"), and keep
the kebab visible so other extended ranges remain reachable. Clicking
the pressed extended pill goes back to "day".

* ui(stopwatch): refresh banner copy — top_stopwatches vis already exists

The "Data entered here is not shown in the Activity view, yet" warning
is stale: stopwatch events are already queryable (canonicalEvents'
bid_stopwatch flag unions them with window events) and the
"Top Stopwatch Events" visualization has shipped — it just isn't in
the default Summary view because adding it would surface a noisy
"missing data" banner for the (many) users without stopwatch buckets.

Replace the deprecation-flavored copy with a usage hint pointing
people at "+ New view" / "Add visualization" to surface their
stopwatch totals.

* ui(stopwatch): tighten banner copy — exact path is Edit view → Add visualization

* fix(activity): default include_stopwatch to true so the vis shows data

The "Top Stopwatch Events" visualization queries top_stopwatches in
the activity store, which is only populated when query_desktop_full's
include_stopwatch path runs (it sets bid_stopwatch and the query then
returns data[0].stopwatch). That flag defaulted to false and was only
toggleable from a dev-only filter, so a user who started a stopwatch
and added the vis correctly saw "No data" until they manually flipped
a hidden dev-mode switch.

Default it to true. canonicalEvents only sets bid_stopwatch when a
stopwatch bucket exists, so users without stopwatch buckets are
unaffected. The dev-mode UI toggle stays — it still controls whether
stopwatch events override window events during overlap.

* feat(trends): real trend stats — current vs previous period comparison

Trends used to share the activity store's by_period output and showed
only the per-day stacked barchart from the Activity Summary, which is
redundant. Now the view runs its own pair of queries:

- queryByDay over the selected window (7/30/90 days), one query per day
- queryTotals over the equal-length previous window

and renders summary cards (Active time, Daily average, Most-active
day) with deltas vs the previous window, the per-day barchart, and a
"Top changes by category" table sorted by absolute change.

Also fixes the stuck-loading bug: byPeriod entries are now stored as
{ cat_events: [...] } so buildBarchartDataset sees the shape it
expects (Object.values(data).map(result => result.cat_events.map(...))
used to throw "cannot read properties of undefined (reading 'map')").

Loading is gated on an explicit flag; the barchart's null/empty
handling now reflects "no data" vs "loading" correctly.

* fix(search): variant=warning (was style=warning, a no-op typo)

The "early development" alert on Search rendered as default info-blue
in light mode and as default info-teal in dark mode instead of the
intended warning yellow. Same prop typo I fixed in Trends/Graph/
Report/Alerts earlier but Search slipped through one of the earlier
edits because of a concurrent file rewrite.

* ui(dark): polish — alert links, card elevation, Timespiral labels

- Alert links across all variants (.alert-success/info/warning/danger
  in dark mode) inherited Bootstrap's darker shade and ended up nearly
  iso-luminant with the alert background — e.g. the "aw-webui#365"
  link on Graph's yellow warning was unreadable. Pin them to white +
  underline; hover dips opacity to 0.85.
- Card backgrounds bumped from #1a1d24 (same as aw-container) to
  #21252c so cards on Trends/Activity/Buckets read as elevated
  surfaces instead of blending into the page. Border tone bumped to
  match.
- Timespiral clock labels and date rings (ActivityWatch#888 / #ccc inline styles)
  were dim against the dark canvas. Add a #timespiral-scoped svg text
  override that brightens them to #d8dbe1 at 95% opacity, leaving the
  generic svg text rule alone so other charts aren't affected.

* fix(dark): Categorization "Category set:" panel respects dark theme

The category-set switcher box used an inline
  style="background: var(--bs-light, #f8f9fa); border-radius: 4px"
which beat dark.css since inline styles win over stylesheet rules.
Result: white block with white labels in dark mode.

Replace with the .bg-light + .rounded utility classes (dark.css already
overrides .bg-light to #1a1d24) and swap the inline var(--bs-secondary)
help text for the .text-muted utility for the same reason.

* ui(settings): URL-routed active group + wrap nav instead of scroll

- /settings/:group now persists which panel is open. Reloading
  /settings/categorization stays on Categorization; the nav uses
  router-link with replace so back/forward stays sane.
- The settings store falls back to /settings/general for unknown
  groups (and for the bare /settings path), avoiding broken state.

- At <768px the horizontal pill row used overflow-x: auto, which
  left pills like "Developer" off-screen and let the inner scrollbar
  push extra horizontal scroll space onto xs viewports. Switch to
  flex-wrap so pills break onto multiple rows, and constrain the
  nav with min-width: 0 / max-width: 100% so it can never push the
  layout wider than the viewport.

* feat(settings): embed Category Builder + lighter Stopwatch help

Embed:
- CategorizationSettings adds a "Category builder" subsection with a
  b-collapse + "Open builder" toggle. The CategoryBuilder component
  loads lazily (async import + v-if guard on builderMounted) so users
  who just want to edit rules don't pay the words-query cost.
- CategoryBuilder accepts an `embedded` prop. When true it skips the
  standalone page chrome (h3 + intro paragraphs + "back to Settings"
  link) so the embed reads as a subsection of its parent.
- /settings/category-builder redirects to /settings/categorization so
  external bookmarks still land in the right place.
- Dedupe the stray "name: 'aw-category-builder'" + empty props block
  that ESLint flagged as duplicate keys.

Stopwatch help:
- The "Track manually-logged sessions…" info banner was heavy for a
  one-time explainer. Replace with a (?) icon next to the title that
  opens a b-popover on hover/focus/click — works on both desktop and
  mobile, takes zero vertical space when not in use, and the help
  text stays available for users who need it.

* fix(dark): popover and tooltip colors

BootstrapVue's b-popover renders as a .popover with .popover-header /
.popover-body. In dark mode these inherited the default Bootstrap
backgrounds (white-ish) and ended up white-on-white. Same story for
.tooltip-inner.

Add dark.css rules for the popover/tooltip surfaces, including the
arrow ::before/::after pseudo-elements so the pointer tracks the
themed background instead of flashing a white triangle.

* ui(timeline): shorten bucket labels, disambiguate only on collision

- util/timelineLabels: add shortenBucketLabel() which strips the
  aw-watcher- / aw- prefix and the _<hostname> tail so a bucket id
  like aw-watcher-window_erb-m2.localdomain reads as "window".
  formatTimelineBucketLabelHtml uses the short form by default; the
  full id stays in the <title> tooltip for unambiguity.
- VisTimeline computes a labelCounts map and only appends the
  hostname suffix ("window @ host") when two buckets would otherwise
  share the same short label — single-host setups stay clean, and
  multi-host or sync setups disambiguate per-row only where needed.
- Replace the blocking alert("Changes won't be reflected…") on first
  event edit with a one-time b-toast (auto-hides after 6s, dismissal
  persisted in localStorage). The alert fired on top of the editor
  and interrupted the edit flow.

Updated tests cover both the helper and the format function.

* ui(timeline): consistent host suffix + hostnameless watcher exception

Previous pass inconsistently applied the "@ host" suffix per-row based
on whether each row's short label collided. With one window bucket on
two hosts and one afk bucket on a single host, that produced
  stopwatch
  afk
  window @ erb-m2.localdomain
which reads as random.

- Now all-or-nothing: if ANY two host-attributed buckets share a short
  label, every host-attributed row gets the "@ host" suffix. Single-
  host setups keep clean labels across the board.
- Stopwatch and aw-watcher-web-* don't currently carry a real
  hostname (they sit under "unknown"). They're excepted so they never
  get a noisy "@ unknown" suffix — left a TODO for the future
  migration that gives those watchers per-host bucket ids.

* ui(category-builder): paginate the words list with "Show more"

A typical week of uncategorized activity produces 30-80 word rows, each
with four action buttons. Rendering them all at once turned the embedded
Category Builder into a 2+ screen scroll-wall that buried the rules tree
above it.

Show 10 at a time (page_size=10) with a footer that reports
"Showing N of M words" and a "Show more" button that reveals the next
page_size. fetchWords() resets visible_count on every requery so the
user sees the top of the new ranking after changing options.

* ui(categorization): move Category Builder above the rules tree

Burying the builder collapse below a multi-screen list of categories
made it invisible — users had to know it was there. Since it stays
collapsed by default (and only mounts the inner component when
expanded), there's no cost to surfacing it near the top. New layout:

  Intro / Category set switcher
  → Category builder collapse (closed by default)
  → Rules tree
  → Add category / Save

Rules editing still happens in-place, and the builder's "New rule" /
"Append rule" actions still reflect into the tree right below it.

* ui(categorization): restructure — drop dup heading, move builder to end

- Drop the top-of-page "Categorization" h5 — it duplicated the
  Settings page heading that wraps the panel. Action toolbar
  (Restore defaults / Import / Export) moves down next to a new
  "Categories" h5 that sits right above the rules tree, so the
  heading actually labels the list it precedes.
- Move ActivePatternSettings to the bottom of the Categorization
  group (Settings.vue) — it's an edge-case AFK override, not the
  thing most users come here for.
- Move Category Builder back to the end of the categories list. Now
  that ActivePatternSettings sits below CategorizationSettings, the
  builder is no longer the very last thing on the page so it's not
  buried; meanwhile having it right above the rules list felt weirdly
  squeezed between unrelated content (set switcher → builder →
  rules).

* ui(categorization): UncategorizedNotification link opens + scrolls to builder

Clicking the "Category Builder" link in the uncategorized-time alert
used to redirect to /settings/categorization (since the standalone
route now redirects there) and dump the user at the top of a long
rules tree with the builder still collapsed and out of view.

- Link now targets /settings/categorization?builder=open.
- CategorizationSettings reads the query on mount, sets builderOpen
  (which also flips builderMounted via the existing watcher so the
  inner component lazy-loads), then scrolls the builder section into
  view on next tick.

ref="builderSection" added to the wrapping div as the scroll target.

* ui(settings): add breathing room at the bottom of each panel

Long subviews (Categorization in particular) ended flush against the
card border, and short ones stopped just above the sticky nav's last
item — both felt abrupt. Give each .settings-section a 3rem
padding-bottom so the content always has air below it.

* ui(settings): fold "Check for new releases" into General, drop Updates panel

A one-setting nav entry didn't earn its own panel. Append
ReleaseNotificationSettings to the General group's component list
(skipped on Android, same as before), drop the Updates group + its
route param.

* ui(settings): trim panel bottom padding 3rem → 1rem (pb-3 in Bootstrap)

* ui(settings): standardize help text on .text-muted

Some sub-option descriptions were bare <small> (inherits body color
→ white in dark mode) while others were <small.text-muted> (grey).
Theme/ReleaseNotificationSettings already had it; bring the rest in
line: ColorSettings, DaystartSettings (both descriptions), Landing
Page, TimelineDuration, ActivePattern.

* ui(settings): drop redundant 'Developer settings' heading

The panel header is already 'Developer' (from the Settings page nav
group). The inner h4 duplicated it. Also flip the warning alert from
the default info-blue to variant=warning since it's a caution, not
informational chatter.

* ui(settings): keep example regex on one line

The example pattern (Zoom Meeting|Google Meet|Microsoft Teams) used to
wrap mid-expression at narrow widths, which made the pipe-separated
alternation hard to read. inline-block + white-space: nowrap keeps it
contiguous (it'll overflow visually on very narrow viewports but stay
parseable).

* ui(settings): bind 'Example expression:' label to its code as one no-wrap unit

Wrap the label + code in a single span.text-nowrap so they always wrap
together — the label sits beside the code if the line has room, or
the whole label-plus-code unit moves to a new line if it doesn't. The
label no longer dangles alone above the code.

* fix(settings): privacy filter '[]' pug parsing escaped the closing bracket

Pug interprets ] as the end of an inline #[…] expansion, so
'#[code []]' rendered as '<code>[</code>]' — the open bracket got
the code/white color, the close bracket fell out as plain text. Swap
to a raw <code>[]</code> tag so both brackets are inside the code
element.

* fix(dark): outline-* buttons consistent across <button> and <label.btn>

The generic 'button { color: !important }' rule near the top of
dark.css wipes Bootstrap's variant text colors on <button> elements
but doesn't touch <label class='btn'> (which is how the Categorization
'Import' file-input control is rendered). That made Import blue and
Export white even though both used btn-outline-primary.

Add explicit overrides for outline-primary / outline-warning /
outline-danger that target both .btn-outline-* and label.btn-outline-*
so they look the same regardless of underlying element.

* feat(settings): make the uncategorized-time hint configurable + dismissible

The "High uncategorized time" banner had two TODOs in code: no way to
disable it and the thresholds were hard-coded. Both fixed.

- Add settings store key uncategorizedNotificationData with isEnabled,
  minTotalSeconds, minRatio — defaults preserve the previous behavior
  (1h + 30%).
- UncategorizedHintSettings.vue panel surfaces them under Settings →
  General: enable toggle, "Minimum total tracked time" in minutes,
  "Minimum uncategorized share" in percent.
- UncategorizedNotification reads the config, becomes dismissible
  (clicking × disables the hint outright and persists via settings).
  Inline link to Settings under the banner so the path back is
  discoverable.

* fix(ui): drop .text-muted on the uncategorized hint footer link

.text-muted forces a grey color that reads as low-contrast against
both the light and dark variants of the .alert-info background.
Inherit the alert's text color instead and use a small opacity dip
(0.85) to keep the line visually subordinate without sacrificing
readability.

* ui(hint): replace footer link with small ⚙ icon next to title

The 'You can hide or adjust this hint in Settings.' line made the
alert taller than it needed to be. Put a small cog icon (vue-awesome
'cog' at scale 0.85) next to the title — same destination, inherits
the alert's text color, fades to 70% opacity by default so it doesn't
compete with the title, full opacity on hover/focus.

* ui(hint): dim the settings cog further (0.7 → 0.45)

* ui(score): move help (?) to top-right corner of the widget

The (?) icon was inline at the end of 'Your total score for today is',
which (a) made an already-wordy heading even longer and (b) sat at an
odd vertical baseline next to the running text. Move it to the
top-right corner of the centered score block as a small ghost button
(0.45 opacity, brightens on hover/focus) and trim the heading to
'Score for today'.

* test(trends): drop stale ensure_loaded assertion

The Trends rebuild moved off activityStore.ensure_loaded() — refresh
now queries aw-server directly via getClient(). The 'calls
ensure_loaded' test was asserting an implementation detail that no
longer exists and was failing in CI. Its regression scope (the
bid.endsWith crash from missing host buckets) is still covered by the
host-fallback + no-host-bail-out tests.

* lint(route): drop unused CategoryBuilder import

CategoryBuilder is no longer a standalone route (the path now redirects
into CategorizationSettings, which embeds the component directly). The
const reference left over from the old route declaration triggered
@typescript-eslint/no-unused-vars and CI runs with --max-warnings=0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update Javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants