Commit Graph
510 Commits
Author SHA1 Message Date
nesquena-hermesandneaucode-bot 58af505cc6 fix(sessions): clear stale sidebar unread indicator on session visit (salvage of #4946)
Salvaged from #4946 by @neaucode-bot. Rebuilt fresh on master (repro-confirmed),
without regressing hidden-tab completion state or lineage-parent unread dots.

Co-authored-by: neaucode-bot <neaucode-bot@users.noreply.github.com>
2026-07-11 03:32:26 +00:00
nesquena-hermesandMicoRobot. 75153f5b79 fix(share): always-on redaction boundary + rebase/UX fixes (gate findings)
Adversarial security re-gate on the rebased head found 4 issues; fixed:

1. [CORE] api/shares.py public snapshot could leak credentials/paths/tool
   payloads. The sanitizer relied on redact_session_data (gated on the
   user-toggleable api_redact_enabled) — disabling it exposed API keys, a dict
   assistant content was stringified as raw tool data, and session.workspace
   survived in transcript text. Now the per-message sanitizer applies ALWAYS-ON
   credential redaction (_redact_fn_cached, force=True, ignores the setting) +
   scrubs known session/workspace/home paths, and refuses to stringify non-text
   structured content. Title is force-redacted too. Regression test proves the
   boundary holds with api_redact_enabled=False.
2. [SILENT] api/models.py: my rebase wrongly kept anchor_activity_scenes in
   METADATA_FIELDS, moving 250-480KB scene bodies before messages and undoing
   master's metadata fast-path (#5854). Removed; 17 #5854 tests pass again.
3. [SILENT] static/index.html: the Share/StopSharing/ExportHTML button triplet
   was double-applied by the rebase (duplicate IDs → only the first wired).
   Deduped to one each + a count assertion test.
4. tests: fixed the stale Session.load-after-/api/session/new setup (master
   keeps new sessions memory-only until first message).

Also (UX, found in the visual flow sweep): a clipboard-copy failure after a
SUCCESSFUL share showed a red 'Share failed' toast and skipped opening the page.
The copy is now isolated — the share succeeds, the page opens, and the toast
shows the link if the clipboard write fails, never a false failure.

Co-authored-by: MicoRobot.
2026-07-11 02:06:34 +00:00
nesquena-hermes e061e8534c Rebase #5337 (public read-only conversation sharing) onto master
Rebased 363 commits forward (branch-point 2026-07-05). Applied the PR's code
diff cleanly across 16 files; resolved api/models.py additively (both master's
process_wakeup_pause/anchor_scene_index fields AND the PR's share_token/
share_created_at fields kept in the Session constructor, metadata list, and
to_dict). CI workflow (.github/workflows/tests.yml) intentionally NOT taken from
the stale branch — master owns it (the branch's apparent removal of Playwright/
Office steps was a PAT-workflow-scope revert; rebase restores master's CI).

Co-authored-by: MicoRobot. Concept approved by maintainer (opt-in public share,
2026-07-11). Note: 4 dynamic share tests fail identically on the original
unrebased head (pre-existing test-env Session.load fragility, not rebase-caused);
to be resolved during the security re-gate in a proper sandbox.
2026-07-11 01:39:42 +00:00
nesquena-hermesandstarship-s 07b3f411d7 fix: preserve same-session load ownership on redundant reload + guard hint cleanup
Gate review (Codex) reproduced two SILENT regressions in the generation-token
ownership change, both validated against PR head + merge base:

1. sessions.js:1428 — the no-op early-return did NOT fire for a redundant
   same-session load (currentSid===sid, !_loadingSessionId was false because a
   keep-stale refresh was in flight). The redundant loadSession() then bumped
   _loadSessionGeneration and CANCELLED the authoritative in-flight refresh —
   stale rows remained while message_count advanced, so later polling could not
   detect them. Restore the pre-existing behavior: also return when the in-flight
   load is for the SAME sid (!_loadingSessionId || _loadingSessionId===sid).

2. sessions.js:2781 — _ensureMessagesLoaded's finally cleared the
   transcript-width reload hint unconditionally, so a stale same-session response
   collapsed a long session to the default tail window. Guard the cleanup with
   _ownsLoad() so only the owning load clears the hint.

Both fixes retain the PR's cross-session, same-session-rejection, and INFLIGHT
race behavior (47 tests pass incl. the new isolation suite + the 4 edited files).

Co-authored-by: starship-s <starship-s@users.noreply.github.com>
2026-07-10 16:52:37 +00:00
Hermes Agent 47c3fcf387 Fix stale session load ownership races 2026-07-10 09:26:36 -06:00
46720a39dd Release: Copy conversation link copies an openable URL (#5478) (#5851)
Was copying the internal session:// reference (not browser-openable); now copies
an absolute origin + /session/<id> URL (verified single-origin, no doubling —
_sessionUrlForSid returns a relative path, the caller prepends origin once),
tracking params stripped. Product/UX parity with major chat apps (Anchor C).

Nathan-approved. 17 copy-link regression tests green.

Co-authored-by: t <a@b>
Co-authored-by: djsavvy <djsavvy@users.noreply.github.com>
2026-07-09 13:24:45 -07:00
sheldon-im 06e0df4656 Announce New Chat workspace transiently 2026-07-09 02:40:36 +00:00
Rod Boev 745e0be0df fix(#5699): keep child streams from moving parent sidebar buckets 2026-07-09 02:35:52 +00:00
Daniel Harcekandnesquena-hermes 407451977e fix: auto-switch profiles for session links (#5419)
A valid session:// deep link to a session owned by a DIFFERENT Hermes
profile used to look identical to a deleted session (404 -> frontend
self-heals away). Now GET /api/session returns a structured 409
session_profile_mismatch envelope (error/code/session_id/profile ONLY,
no transcript) ONLY when the owning profile is KNOWN, and loadSession()
catches it, switches to the owning profile, and retries once. Truly
missing/deleted or legacy None-profile sessions keep the 404 self-heal.

Gate fixes applied (Codex + Fable):
- Codex CORE: added a post-await stale-load guard after
  _switchProfileForSessionLoad so a navigation during the switch can't
  hijack the UI back to the old session.
- Fable Finding 1: only emit 409 when _session_profile is truthy;
  a None-profile (missing/legacy) session under a non-default active
  profile now keeps 404 instead of a useless profile=null 409 (which
  skipped self-heal + spun the SSE reconnect against a dead sid). Both
  detail branches. + 2 regression tests.
- Fable Finding 2: _switchProfileForSessionLoad now clears the sidebar
  skeleton + re-renders from cache on switch-POST failure (mirrors the
  #4671 canonical-switch catch), then rethrows, so a failed switch can't
  strand the sidebar on the skeleton.

Reconciled tests/test_issue1611_session_profile_filtering.py (4 tests) to
the 409 contract while preserving the no-leak boundary assertion.

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-07-07 02:35:02 +00:00
Rod Boev 80ee386827 Preserve lineage authority through serializer and refresh pass 2026-07-06 05:08:23 -04:00
Rod Boev 409be9bcd4 fix(#5598): stabilize lineage grouping across live refreshes 2026-07-06 04:49:24 -04:00
nesquena-hermes db87fd3a07 Merge PR #5630 into visible-batch stage 2026-07-06 05:37:45 +00:00
nesquena-hermes f10cb2a35c review: merge #5555 (11eed2af) 2026-07-06 01:44:21 +00:00
Anthony Ruiz 5901ed68e1 fix(steer): scope upload UX to owner session 2026-07-05 16:02:54 +00:00
nankingjingandClaude 8fc3179a8a fix(#3874): auto-refresh sidebar session list after new session creation
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-05 18:43:20 +08:00
nesquena-hermes 4559ca85f0 fix(#5409): iOS PWA session-switch — close sidebar instantly + prevent _loadingSessionId race
Two iOS-PWA bugs when tapping an old/large session:
1) closeMobileSidebar() ran AFTER the slow `await _openSidebarSession()` (3-15s
   for large sessions on WKWebView), so the sidebar stayed open with only a tiny
   spinner. Moved closeMobileSidebar() to run synchronously BEFORE the await in
   the tap handler + lineage/child open paths for instant feedback.
2) _loadingSessionId race: an SSE-triggered idle reload
   (_scheduleActiveSessionIdleReload / refreshActiveSessionIfExternallyUpdated,
   fed by idle-reconcile/poll/visibility/focus) could overwrite _loadingSessionId
   and silently cancel an in-flight session switch. Both paths now skip while a
   different session's loadSession() is in flight.

Contributor stage; gate-pass. Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
2026-07-05 05:42:51 +00:00
nesquena-hermesandrodboev f6cc48b834 fix(#5501): clear pending retry state on render invalidation (Codex gate)
Codex re-gate SILENT: a retry whose fetch is invalidated mid-flight (e.g. a
profile switch) left the button stuck as an inert 'Retrying…' with no request in
flight — the stale catch returns before _showSessionListLoadError and the
.finally() bails when the skeleton removed the old button. _invalidateSessionListRenders()
now clears the pending retry markers (retrying, _retryFailedFocus) so the next
repaint shows an actionable idle Retry. Regression test added (fails without the
fix); test harness extracts _invalidateSessionListRenders + declares its render-gen
globals.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 04:48:50 +00:00
nesquena-hermesandrodboev 9be4048c72 fix(#5501): fold in Fable a11y hardening — live region, aria-disabled, focus restore, ellipsis
Fable UX gate SHIP-WITH-UX-FIXES (non-blocking). Applied all: (1) role=status +
aria-live=polite on the error note so retry failure/restore is announced to
screen readers; (2) pending state uses aria-disabled (not the disabled property)
so the button retains keyboard focus, with click/keydown guards keeping it inert;
(3) on a failed retry, the fresh rebuilt Retry button reclaims keyboard focus
(_retryFailedFocus flag, rAF-guarded) so keyboard users aren't dropped to <body>;
(4) label uses a true ellipsis 'Retrying…' matching the app's other progress
string. CSS styles [aria-disabled=true] alongside [aria-busy]. Tests updated +
new a11y assertions; 8/8 pass, node --check clean.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 04:39:14 +00:00
nesquena-hermes 7349a9434f fix(#5501): improve session-list retry feedback
The sidebar's load-error Retry button behaved like a raw idle browser button while
the next slow fetch was pending, so it looked broken/unresponsive. Retry now paints
an immediate pending state (label 'Retrying...', disabled, aria-busy), keeps the
retry state local to the error block via _renderSessionListLoadErrorNote(), and
lets the existing success/failure paths replace the error object when the request
settles (restoring 'Retry' on failure). Also adds proper styling.

Contributor stage; gate-pass. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-05 04:39:14 +00:00
nesquena-hermesandrodboev b36bd789ce fix(#5551): refresh sidebar recency after missed session events
A reactivated older conversation wasn't bumped to the top / into Today until a
manual refresh when the viewing tab was backgrounded/unfocused: refreshSessionList
early-returns on document.hidden unless force=true, and the sidebar SSE closes on
blur — but the in-flight/pending coalescing only preserved the refresh REASON
string, dropping the opts (force/refreshActive). So a coalesced 'sessions_changed'
refresh lost its force flag and got skipped on a hidden tab. Now the pending
request preserves merged opts (force/refreshActive OR-merged), and a dedicated
resume-refresh path forces a catch-up when the sidebar regains focus, so
cross-device recency re-sorts land without a manual refresh.

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-04 22:34:28 +00:00
Rod Boev 11eed2af54 fix(session): harden cron branch source gate (#5477) 2026-07-04 16:41:15 -04:00
Rod Boev cbc164de0c feat(#5477): fork read-only cron sessions 2026-07-04 16:38:27 -04:00
nesquena-hermesandruizanthony 6baf501dce harden #5471: dual-signature suppression (Opus root fix) — covers stale prefix + attachment
Opus found the exact-match signature fails open in the COMMON case: the server's
last persisted draft is often a PREFIX of the submitted text (Enter-to-send within
400ms cancels the pending debounced save), so matching only the submitted text
misses it and the prefix restores (the literal 'tail of the already-submitted
message' symptom). Root fix (Opus-recommended): capture BOTH signatures at send —
the remembered SERVER-draft signature (_composerDraftPayloadSignatureForSid, read
before the reset) AND the submitted-payload signature — and suppress a restore
matching EITHER. The prefix IS the remembered server draft; the remembered files
went through the same lossy serialization so they self-match; a genuinely new
cross-tab draft matches neither and restores. Subsumes the file-canonicalization
fix (kept as belt-and-suspenders). Suppression stores signatures[] not signature.

Regression tests: stale-prefix suppressed (Opus Finding 1), file round-trip
self-matches (Codex Finding 2), cross-tab-different still restores. Test harness
updated to model send-time server draft vs later restore draft as distinct.

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-04 08:51:13 +00:00
nesquena-hermesandruizanthony 5a8dc91bbb harden #5471: canonicalize draft files before persist+sign (attachment round-trip)
Codex found a SILENT edge in the payload-signature fix: a live browser File
JSON-serializes to {} when the draft is POSTed to the server, so a text+attachment
send's suppression signature (computed from the rich File) never matched the
restored server draft (files:[{}]) — suppression cleared and the just-sent tail
could repopulate for the attachment case. Fix: _composerDraftFilesForPersist()
canonicalizes files to a stable serializable shape (name/path/size/type/lastModified)
used at BOTH _saveComposerDraft/_saveComposerDraftNow (persist) AND
_composerDraftPayloadSignature (sign), so the sent signature matches the
round-tripped server draft. Regression test self-demonstrates lossy-mismatch vs
canon-match.

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-04 08:37:15 +00:00
Agent Zero 68a692a8e0 fix(composer): scope draft restore suppression to payload 2026-07-04 08:06:33 +00:00
Anthony Ruiz b1a200f166 fix(composer): suppress stale draft restore after send 2026-07-04 08:06:33 +00:00
nesquena-hermes fcd3de7d8f Merge PR #5487 from webtecnica: prevent unintended session switch on stray pointerup (#5462) 2026-07-04 03:39:18 +00:00
nesquena-hermes dbf3c9d3c0 Merge PR #5460 from ruizanthony: replace cross-profile empty sessions on profile switch 2026-07-04 03:05:45 +00:00
nesquena-hermesandwebtecnica 329e911327 fix: prevent unintended session switch on stray pointerup (#5462)
Clean rebase of webtecnica's #5487 (rebase-first).

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-07-03 23:34:37 +00:00
nesquena-hermes b438976507 gate-fix #5467 r2: force render when recovering from skeleton/error-banner DOM
Codex re-gate found 2 more false-skips (DOM state OUTSIDE the signature path):
- CORE: profile-switch skeleton stays visible if the new payload signature
  matches the last one (empty/same-shaped profile) — skip fires over the skeleton.
- SILENT: stale 'Could not load conversations' banner persists when a transient
  fetch failure heals with identical rows.
Fix: capture _hadSessionListSkeleton + _hadSessionListLoadError BEFORE clearing
them, and disallow the identical-signature skip when either was set. + regression
test. (messages.js:1462 finding is a base-comparison artifact for #5472/#5479/#5484,
not in #5467's diff.)
2026-07-03 23:04:41 +00:00
nesquena-hermes 3999418b0e gate-fix #5467: serialize full rows in render-skip signature (close false-skips)
Codex gate found the render-skip signature omitted fields the sidebar renders:
- CORE: pending/running (active_stream_id/has_pending_user_message/pending_started_at)
- SILENT: attention dots
- SILENT: source/read-only/worktree/lineage/child/model/profile + _sidebarReferenceSessions
Any omitted field = a false-skip = stale sidebar.

Root-cause fix (most reliable): serialize the FULL applied row objects +
_sidebarReferenceSessions instead of a hand-picked allowlist — inherently
complete, can't rot when a new rendered field is added. Rewrote the
'ignores non-display fields' test into positive full-serialization + no-strip
regression assertions.
2026-07-03 22:58:21 +00:00
nesquena-hermes 10e5e941d3 Merge PR #5467 from ai-ag2026: skip full session-list rebuild when payload unchanged (#5455) 2026-07-03 22:47:40 +00:00
nesquena-hermesandruizanthony 5e5b44e733 fix(webui): replace cross-profile empty sessions on profile switch + open all-profile rows under owning profile
Clean rebase of ruizanthony's #5460 (rebase-first).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-03 20:49:51 +00:00
TARS 3f4ebf18a8 fix(sidebar): refresh timestamp labels on tab return
Add a dedicated visibilitychange hook for the 60s relative-time refresh poll so skipped hidden-tab ticks repaint immediately when the tab becomes visible, even when no streaming poll is active.
2026-07-03 20:13:24 +02:00
TARS 5973d2b0e0 fix: narrow sidebar render-skip signature 2026-07-03 20:10:44 +02:00
TARSandClaude Opus 4.8 fcf3e23643 perf(sidebar): pause background session polling while the tab is hidden (#5455)
The 30s streaming poll (renderSessionList -> GET /api/sessions + full sidebar
rebuild) and the 60s relative-time refresh (renderSessionListFromCache -> full
DOM rebuild) ran unconditionally, including in hidden background tabs, so a
backgrounded window kept fetching and rebuilding the sidebar it could not show.

Guard both interval callbacks with document.hidden and refresh the list on
visibilitychange when the tab is shown again, mirroring the existing gateway
poll. Visually neutral while the tab is visible; a hidden tab simply stops
burning network and DOM churn and re-syncs the moment it is shown.

Verified: node --check + ESLint runtime guard clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:41:49 +02:00
TARSandClaude Opus 4.8 a53f3bdaba perf(sidebar): skip full session-list rebuild when the payload is unchanged (#5455)
The 30s streaming poll and the gateway poll re-applied /api/sessions and ran a
full sidebar DOM rebuild (list.innerHTML='' + one node tree per row) on every
tick, even when the payload was byte-identical to what was already on screen —
the common case on an idle sidebar.

Add a signature over the applied rows + projects + coarse display state in
_applySessionListPayload and skip renderSessionListFromCache() when it matches
the last rendered signature and no entrance animation is pending. A streaming
row's fields advance each poll so its signature changes and it still repaints;
client-only display toggles (search, active, density, select, lineage
expansion) render through their own paths. The per-refresh INFLIGHT cleanup is
still run on the skip path. The 60s relative-time refresh and all other render
triggers call renderSessionListFromCache directly and are unaffected.

Output-neutral: a skip only happens when the render would produce the same DOM.
node --check + ESLint runtime guard clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:41:49 +02:00
nesquena-hermesandnankingjing 791523dffa fix(ux): keep sidebar action menu open during chat scroll + close on anchor detach (#5347)
Clean rebase of nankingjing's #5402 (rebase-first).

Co-authored-by: nankingjing <nankingjing@users.noreply.github.com>
2026-07-02 10:09:55 +00:00
nesquena-hermesandweidzhou 3fde698232 fix(sessions): always retry sidebar session-list GET on 502/503/504 (#5394)
The sidebar session-list GET had 502/503/504 retry logic, but it was gated
to cold boot only. Once `_sessionListHasLoadedOnce` flipped true, every later
refresh (profile switch, focus/visible/reconnect) shipped no retryStatuses, so
a transient 502 during an nginx->backend restart window failed on the first
attempt and left the sidebar stale until a hard reload (Ctrl+F5).

The session-list GET is idempotent, so retrying it is safe unconditionally.
This moves `retries:1` + `retryStatuses:[502,503,504]` into the base request
options so they apply to every refresh, while keeping the larger boot timeout
(`_SESSION_LIST_BOOT_TIMEOUT_MS`) and `retryTimeouts` boot-only. The api()
wrapper in static/workspace.js already retries when the error status is in
retryStatuses, so no other change is needed.

Extends the existing source-string regression test to assert the retry
options are now always present (declared before the boot-only gate) while the
boot path still carries the timeout + timeout retry.

Reported and root-caused by @weidzhou, who traced the boot-only retry gate.

Co-authored-by: weidzhou <weidzhou@users.noreply.github.com>
2026-07-02 05:13:59 +00:00
nesquena-hermes f50b1d84e1 feat(sessions): move Export-to-HTML into the sidebar conversation menu
Follow-up to v0.51.819 which removed the export button from the composer footer
(it tripped the footer overflow-collapse, hiding model/workspace labels). Per
design consult (Fable) + ChatGPT/Open-WebUI convention, export now lives in the
per-conversation sidebar three-dot action menu, right after Duplicate:
- exportSessionHTML(session) parameterized (was active-session-only); Settings
  button now wired ()=>exportSessionHTML() and still exports the active session
- new _appendSessionExportHtmlAction() added after Duplicate + in the read-only
  early-return branch (export is non-mutating; imported sessions re-exportable)
- exports THAT row's conversation, not just the active one
- download icon added to ICONS; session_export_html[_desc] added to 14 locales
- Settings HTML button retained as secondary data-management entry
2026-07-02 03:21:18 +00:00
nesquena-hermes ffad5e4acd stage #5374 2026-07-01 22:57:37 +00:00
Stacey2911 6044eaa0cc fix: respect auxiliary title timeout for manual regenerate 2026-07-02 08:48:47 +10:00
nesquena-hermes ebdd48079a fix(sidebar): keep active-parent delegate children stably visible (flicker) (#5306)
#5306 (flicker): while a parent WebUI session is the active/streaming session,
a linked delegate subagent child that transiently reports message_count===0
between /api/sessions polls was dropped by _sidebarRowHasVisibleMessages BEFORE
_attachChildSessionsToSidebarRows could stack it under its parent. It never
entered sessionsRaw, so the row vanished, then reappeared on the next refresh
once its list metadata caught up — the flicker. Extend the visibility predicate
with an active-parent exception (mirroring the existing active-session
exception): a child_session whose parent_session_id is the active sidebar
session stays visible even at message_count 0. Scoped to the active parent so
truly-empty unrelated sessions are still hidden.

#5305 (orphan): a delegated subagent child whose WebUI parent is filtered out of
the current render (project/profile/source scope) was promoted to a contextless
top-level "Subagent Session" orphan. Suppress cross-surface child_session rows
whose parent row is absent from the render instead of orphaning them, mirroring
the existing archived-hidden-parent suppression (#4293). The genuinely-external
parent case (messaging/CLI) still orphans via the parentIsExternal branch.

Tests: tests/test_5306_subagent_sidebar_flicker.py (7 tests) locks both
invariants and the no-regression guards, executing the real sessions.js helper
regions under node like the existing lineage tests.
2026-07-01 06:36:02 +00:00
nesquena-hermes 373c3e4b63 Merge #4969 into stage 2026-06-30 23:07:53 +00:00
nesquena-hermes f579ed83dc fix(#5002): repaint sidebar after quick-create newSession (Codex: newSession doesn't render; callers must) 2026-06-30 21:12:14 +00:00
nesquena-hermes b098e5c658 feat(#4676): gate per-project quick-create buttons behind opt-in setting
Add a default-OFF 'project_quick_create_buttons' setting that gates the
per-project '+' quick-create buttons on sidebar project chips. The chip
render site only calls _attachProjectQuickCreateButton when the new
window._projectQuickCreate runtime flag is enabled, so the buttons do
not appear unless the user opts in via Settings.

Mirrors the large_text_paste_as_attachment opt-in plumbing (defaults,
allowlist, settings collect/restore/save, window hydration at both sites,
checkbox row, i18n across all locales).
2026-06-30 19:49:07 +00:00
Rod Boev 66fc8722c1 fix(#4676): expose quick-create button purpose to screen readers 2026-06-30 19:28:28 +00:00
Rod Boev 80dbbc1e93 fix(#4676): swallow duplicate quick-create inflight rejections 2026-06-30 19:28:27 +00:00
Rod Boev 0ea9e72a9a docs(#4676): clarify quick-create rollback ownership 2026-06-30 19:28:27 +00:00
Rod Boev a109f86946 Keep project quick-create filter changes in sync with session creation outcomes 2026-06-30 19:28:27 +00:00