Commit graph

511 commits

Author SHA1 Message Date
Dustin Keeton
920e853c83
Merge pull request #481 from dustinkeeton/fix/issue-292-banner-font-outline
fix: outline banner wordmark so GitHub README renders true letterforms
2026-07-15 17:47:47 -07:00
Dustin Keeton
0b90bcd9a1
Merge pull request #480 from dustinkeeton/fix/issue-479-tiktok-slideshow-false-positive
fix: don't misreport real TikTok videos as photo slideshows
2026-07-15 17:47:00 -07:00
bot
7e31faaea5
fix: outline banner wordmark so GitHub README renders true letterforms
The README hero (banner-animated.svg, and its static twin banner.svg)
rendered the wordmark and tagline as live <text> in Space Grotesk /
Inter. GitHub serves README images through a sandboxed <img>/camo
context that can't load webfonts, so nearly every visitor saw the
wordmark in Helvetica/Arial fallback — a name-treatment violation on
the most visible surface (issue #292).

Outline both <text> elements to vector <path>s (both fonts are OFL, so
outlining is permitted): the wordmark in Space Grotesk Medium 500 at
-0.02em tracking and the tagline in Inter Medium 500, generated via
HarfBuzz shaping + fontTools so the geometry is identical to a
font-correct render. Only the two text elements changed — the mark,
motif, gradient, and SMIL animate nodes are byte-identical. Fills stay
Ion (#F3F0FF) / Lilac (#B7A8FF). Both banners now carry zero font
dependency and render true letterforms on every consumer.

Documented the outlining + regeneration recipe in assets/brand/README.md.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-15 17:43:10 -07:00
bot
bc36c72e61
fix: don't misreport real TikTok videos as photo slideshows
The reactive "unable to obtain file audio codec" ffprobe stderr was
unconditionally mapped to NoAudioError, overriding positive audio
evidence from --dump-json. A real HEVC/h265 TikTok video whose local
ffprobe can't introspect the stream was wrongly declared a slideshow.

Gate the reactive slideshow verdict on dump evidence: when the dump
proves audio exists (top-level acodec or any format acodec other than
'none'), a NO_AUDIO_STDERR match now becomes a new terminal
AudioCodecReadError pointing at the ffmpeg-path setting / yt-dlp update
instead of the slideshow message. The reactive NoAudioError is only
trusted when the dump was null/inconclusive.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-15 17:39:26 -07:00
Dustin Keeton
d4e0747a80
Merge pull request #470 from dustinkeeton/chore/release-1.0.13
chore: release 1.0.13
2026-07-15 02:46:09 -07:00
bot
787d075a0f
chore: release 1.0.13
Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 02:39:26 -07:00
Dustin Keeton
f6b0f0314d
Merge pull request #469 from dustinkeeton/feat/caption-deterministic-format
feat: deterministic caption formatting — speaker turns, chapters, pauses
2026-07-15 02:32:41 -07:00
Dustin Keeton
2ff96c3695
Merge pull request #468 from dustinkeeton/fix/postprocess-token-guard
fix: post-processing token guard + filler-removal off by default
2026-07-15 02:32:17 -07:00
Dustin Keeton
849a0ff448
Merge pull request #464 from dustinkeeton/fix/time-range-toast-design
feat: first-class time-range modal replaces the slider toast
2026-07-15 02:31:22 -07:00
Dustin Keeton
4e5da1a567
Merge pull request #463 from dustinkeeton/fix/batch-caption-routing
fix: route batch note-media transcription through the caption tier
2026-07-15 02:30:42 -07:00
bot
1d02770c75
feat: deterministic caption formatting — speaker turns, chapters, pauses
Caption transcripts rendered as one wall of text. The caption stream
carries its own structure, no AI needed: `>>` is the broadcast-captioning
convention for a speaker change (each turn becomes a paragraph), creators
declare chapters as `MM:SS Title` description lines (each becomes a ###
heading linked to that timestamp in the video), and cue timing exposes
real pauses (paragraph breaks for marker-less ASR, with a force-break so
no paragraph grows unreadable).

The json3 parser now keeps per-cue timing; strongly structured output
(speaker turns or chapters) skips the AI post-processing pass entirely —
a rewrite would only flatten structure the stream itself declared — while
plain ASR keeps the existing post-processing path.

Verified live: a 70-minute podcast episode formats into 7 linked chapter
headings and 260 dialogue paragraphs in under a second.

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 02:22:34 -07:00
bot
2988d8e003
feat: default "Remove filler words" off and explain its trade-off
Stripping filler is a per-content-type call — right for voice memos,
wrong for interviews, talks, and video transcripts the user wants
verbatim — and an always-on default is exactly the kind of forgotten
setting that silently rewords content. Off by default for new installs
(existing vaults keep their saved value); the toggle now explains when
to use it. Whether this belongs in tidy instead is part of the #465
UX review.

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 02:10:54 -07:00
bot
6b53f7cde1
fix: skip post-processing when the transcript exceeds the token budget
Post-processing rewrites the transcript, so the response needs roughly
as many tokens as the input — but every provider dispatch caps output at
ai.maxTokens (default 2048), silently truncating anything past ~1,500
words, and the cut-off text landed in `processed`, which every consumer
prefers over `raw`. Caption-first transcription (#184) made this common
by unlocking long videos Whisper's upload cap used to keep out.

Guard: estimate the needed budget (chars/4) and keep the raw transcript
with a console diagnostic when it exceeds maxTokens — raw-but-complete
beats clean-but-truncated. Chunked processing for long transcripts is
tracked in #467.

Closes #466

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 02:08:33 -07:00
bot
31ddcf6037
feat: promote the time-range choice from a toast to a modal
A blocking follow-up decision deserves a first-class surface: the
trim-range picker is now a TimeRangeModal (front and center, can't be
lost to a stray click) replacing both the slider toast and the fallback
text-input toast. Dismissing it resolves 'cancelled' — do nothing —
instead of silently transcribing the whole file.

The trim bar's handles now actually overlay the bar: Obsidian styles
input[type='range'] itself at higher specificity than a bare class, so
every slider rule is prefixed with the attribute selector. The duplicate
range readout above the bar and the redundant "Duration:" label are
gone — the live start/end endpoint labels say it all.

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 01:38:58 -07:00
bot
8ac0fe7200
fix: redesign the time-range toast as a proper trim bar
The toast clipped its own labels and buttons: the min-width sat on the
inner .notice-message (the #361 lesson), so the outer .notice never
grew. Size now lands on the .notice via :has(), keyed on the shared
title class so the fallback text-input toast is covered too.

Visual redesign: the slider reads as a video trim bar — vertical pill
in/out handles and a solid gold selection fill (--synapse-gold, the one
brand gesture) on a quiet rail, replacing the overlapping accent-colored
blobs. Eyebrow + two-line-clamped title replace the wrapping "Synapse:"
prefix; endpoint labels get tabular numerals; actions right-align with
the primary button last; handles get grab cursors and a visible
keyboard-focus ring. Verified with headless-Chromium renders of the
notice DOM in dark and light themes.

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 01:20:36 -07:00
bot
c062527cee
fix: route batch note-media transcription through the caption tier
The "Transcribe media" note flow (VideoModule.transcribeAndInsert) still
called processUrl directly, bypassing the tier router — a captioned
YouTube video was forced through yt-dlp + Whisper and long videos failed
on the provider size limit (413). main.ts now injects the router into
VideoModule so the batch path is caption-first like every other URL
path, with direct extraction kept as a defensive fallback.

Also removes the now-dead VideoModule.transcribeUrl and
transcribeUrlToActiveNote — both had no callers since the router took
over the summarize and modal paths.

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-15 00:45:54 -07:00
Dustin Keeton
81a9c16ca1
Merge pull request #461 from dustinkeeton/feat/mobile-url-transcription
feat: mobile URL transcription via caption-first tier routing
2026-07-15 00:34:16 -07:00
bot
d4609fb908
feat: mobile URL transcription via caption-first tier routing
YouTube URLs now transcribe on every platform, including mobile, through
a tiered UrlTranscriptionRouter: a zero-dependency caption fetcher
(Innertube ANDROID primary, watch-page fallback — web track URLs are
POT-gated and fetch empty) tries first, and the desktop yt-dlp/ffmpeg
pipeline covers TikTok/Instagram and caption-less videos. The intake
media-URL stub (#112) is wired to the router and now runs the full
pipeline on the transcript-bearing note; failures leave the note
un-stamped so a synced desktop vault finishes what mobile can't. The
opt-in "adopt shared captures" intake setting moves root-level bare-URL
notes (Android share-sheet captures) into the intake folder (#455).

The transcript-level strategy seam deliberately replaces #180's
audio-returning MediaExtractor design — captions never produce audio,
and the Phase 2 server extractor (#181) slots in as a third strategy.
See the 2026-07-14 DECISIONS.md entry.

Verified live against real YouTube (caption fetch end-to-end) plus 40+
new unit tests across the fetcher, router, strategies, intake adoption,
and modal.

Closes #184, closes #112, closes #455

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_012HVfTic7eKWvFiEYkoJe6m
2026-07-14 22:59:22 -07:00
Dustin Keeton
86f27e7991
Merge pull request #460 from dustinkeeton/chore/release-1.0.12
chore: release 1.0.12
2026-07-03 15:02:16 -07:00
bot
92456d1365
chore: release 1.0.12
Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-03 14:54:03 -07:00
Dustin Keeton
3921ec8b7b
Merge pull request #459 from dustinkeeton/chore/audit-2026-07-03
chore: codebase audit 2026-07-03
2026-07-03 14:49:15 -07:00
bot
0e6f50ba3f
docs: update human docs for 1.0.11 and the redaction lint gate
STATUS.md current-focus and counts refreshed; DECISIONS.md gains the
no-unredacted-console gate (#418) and automated-review triage (#454)
entries; ARCHITECTURE.md security layers and audit status extended.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-03 14:41:26 -07:00
bot
5cd534b5e0
docs: reground machine docs after PRs #452-#458
Document the no-unredacted-console lint gate (root + shared AGENTS.md)
and fix stale file:line references across 9 feature AGENTS.md files.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-03 14:35:16 -07:00
bot
2348add781
chore: capitalize notification copy to match sentence-case convention
Obsidian-compliance audit pass: three notification strings started
lowercase against the repo-wide sentence-case convention; test fixture
updated to mirror the corrected copy.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-03 14:24:12 -07:00
Dustin Keeton
8f11b417b0
Merge pull request #458 from dustinkeeton/chore/issue-418-redact-sink-guard
chore: enforce redactError contract on console error sinks
2026-07-03 13:57:48 -07:00
Dustin Keeton
6afa8c1607
Merge pull request #457 from dustinkeeton/chore/issue-454-review-triage
chore: triage Obsidian v1.0.11 automated review findings
2026-07-03 13:56:54 -07:00
Synapse Bot
53a927b969
chore: enforce redactError contract on console sinks via lint rule
The 8 raw-error console sinks listed in #418 were already routed
through redactError() by the 2026-07-02 audit (PR #451: fd56a30,
ec7e4e0). This adds the remaining deliverable: an enforcement guard so
the "every raw-error console sink goes through redactError()" contract
cannot silently regress.

New type-aware ESLint rule `synapse/no-unredacted-console`
(scripts/eslint-rules/no-unredacted-console.mjs), enabled in
eslint.config.mjs for shipped code (tests/mocks excluded, mirroring the
obsidianmd scope): every value reaching a console.* sink must be
statically string-like — checked at the argument top level, inside
template substitutions, and across `+` concatenation. redactError()/
redactSecrets() return string, so sanctioned sites pass with no
function-name allowlist. String()/JSON.stringify() of non-string values
and direct .message/.stack access are flagged as stringification-is-
not-redaction bypasses. The rule fails closed (throws) if type info is
unavailable. Picked up automatically by `npm run lint` in CI.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-03 13:48:14 -07:00
Synapse Bot
4c1ed940c5
chore: triage Obsidian v1.0.11 automated review findings
Triage the v1.0.11 automated plugin-review dump into fix /
document-as-intended / rebut / untyped-analysis-artifact.

- summarize: replace deprecated querySelectorAll + Array.from().find()
  with Obsidian's typed containerEl.findAll(); feature-detect findAll so
  the vitest env no-ops cleanly. Add a recursive findAll stub to the
  obsidian mock and a test that exercises the scroll-reveal path.
- docs: add docs/automated-review-notes.md, a reviewer-facing companion
  declaring desktop-only Node usage (temp-file fs under os.tmpdir with
  finally cleanup, execFile-only subprocesses with arg arrays + sanitized
  args, the shellEnv() env allowlist, write-only clipboard), the wontfix
  rationale for the node-loader require pattern and the :has() toast
  selectors, and the false-positive rebuttals (no system-identity reads;
  test-only globalThis). References in-code comments rather than
  duplicating them.
- README: declare clipboard is write-only and link the reviewer doc.

The no-unsafe-* cascade is an artifact of untyped analysis: npm run lint
(type-aware) exits 0 on main. Full harness reproduction and per-rule
enumeration posted to issue #454.

Refs #454, #389

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-03 13:38:28 -07:00
Dustin Keeton
69cdca473a
Merge pull request #453 from dustinkeeton/chore/wafflestack-0.8.0
chore: upgrade wafflestack 0.6.0 → 0.8.0
2026-07-02 22:18:54 -07:00
bot
828d749da9
chore: upgrade wafflestack 0.6.0 → 0.8.0
- 0.8.0 migration: consumer config trio moves to .waffle/ (waffle.yaml,
  waffle.local.yaml, waffle.lock.json); .gitignore paths updated
- 0.7.0 bundle reorg: design bundle dissolved — designer stays as a
  project-local agent; architect/security replaced by per-item includes
  of engineering-team's lead-engineer/security-engineer; security-audit
  skill renamed electron-security-audit (obsidian-dev)
- product-manager is now toolkit-rendered from pm.* config, replacing
  the stale local PRD/roadmap variant
- doctor CI workflow re-managed as waffle-doctor.yml via the new
  doctor.flags: --allow-missing (previously ejected + hand-edited);
  audit compliance agent pinned to plugin-architect
- new label-hook workflow + skill render inert (no trigger labels or
  ANTHROPIC_API_KEY secret configured)

Claude-Session: https://claude.ai/code/session_01CCfj4i42BWiaHHWMHQGeKR
Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 21:59:19 -07:00
Dustin Keeton
ff17b80f10
Merge pull request #452 from dustinkeeton/chore/release-1.0.11
chore: release 1.0.11
2026-07-02 14:33:39 -07:00
bot
73207e433b
chore: release 1.0.11
Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 14:27:27 -07:00
Dustin Keeton
3500a775eb
Merge pull request #451 from dustinkeeton/chore/audit-2026-07-02
chore: codebase audit 2026-07-02
2026-07-02 14:23:16 -07:00
bot
46a94d1d63
docs: record the three pass-2 redaction sinks in machine + human docs
Keeps the redactError coverage enumerations exact after security
pass 2 routed image/preprocess, notifications, and video
settings-section clipboard catches through the scrub.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 14:16:56 -07:00
bot
ec7e4e0184
fix: redact three raw-error console sinks found in pass 2
Security audit pass 2 (step 6/6): the image-downscale fallback warn
and the two clipboard-copy error catches (notifications, video
settings) logged raw error objects; route them through redactError.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 14:15:31 -07:00
bot
6548268f61
docs: update human docs for 2026-07-02 audit pass
Docs-human pass (step 5/6): refresh STATUS.md snapshot (v1.0.10,
1871 tests / 138 files, current branch and recent feature work),
add the 2026-07-02 DECISIONS.md entry, and extend ARCHITECTURE.md's
redaction coverage table and audit history.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 14:07:34 -07:00
bot
878cebe867
docs: update machine docs for audit-chain changes
Docs-agent pass (step 4/6): record the expanded redactSecrets/
redactError consumer lists (main.ts, credential-field, update-checker)
and the title module's barrel re-export of isUntitled.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 13:59:37 -07:00
bot
fd56a302c8
fix: redact secrets in remaining error-log call sites
Security audit pass 1 (step 2/6): five console.warn/error sites and the
credential Test-button error path logged raw error objects that could
leak API keys embedded in provider error messages. Route them through
the existing redactError/redactSecrets helpers.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 13:48:23 -07:00
bot
33a9a58db9
refactor: import isUntitled through the shared barrel
Architecture audit (step 1/6): the title module re-exported isUntitled
from the internal shared/title-detector file instead of the ../shared
barrel, violating the shared-import rule.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 13:38:17 -07:00
Dustin Keeton
95ebfafd6e
Merge pull request #450 from dustinkeeton/chore/adopt-wafflestack-v0.6.0
chore: adopt wafflestack v0.6.0 (dotfile rename + doctor CI)
2026-07-02 13:23:57 -07:00
Synapse Bot
596da20842
chore: adopt wafflestack v0.6.0 (dotfile rename + doctor CI)
Upgrade the toolkit render from 0.3.0 to 0.6.0. The 0.6.0 migration
renames the consumer dotfiles .wafflestack.* -> .waffle.*; .gitignore
is updated to match (the CLI does not edit it).

The merged .waffle.yaml keeps the git identity block main gained in
PR #448 — the upgrade migrated a stale pre-#448 local copy, so the
block was re-added and the tree re-rendered before committing.

The new wafflestack-doctor CI workflow is ejected from toolkit
management and hand-pinned to v0.6.0 with --allow-missing, because
this repo deliberately gitignores some renders (.agents/, .codex/,
and the .claude skills that embed the local overlay); the stock
render would fail on every CI checkout. Verified against a simulated
tracked-files-only checkout: 24 renders absent and tolerated,
everything else matches the lock.

Co-Authored-By: Claude <bot@wafflenet.io>
Claude-Session: https://claude.ai/code/session_01FG1dCMAp225RdVmp8GsN2a
2026-07-02 13:17:58 -07:00
Dustin Keeton
148f904c3c
Merge pull request #449 from dustinkeeton/feat/brand-refresh
feat: brand refresh — Iris + Gold (2026)
2026-07-02 12:42:05 -07:00
bot
0831fc4b37
feat: brand refresh — Iris + Gold (2026)
Adopt the replaced assets/brand kit across the plugin and repo:

- Regenerate src/brand-icons.ts from the new assets: glyph bodies stay
  currentColor, each may carry ONE gold gesture via
  var(--synapse-gold, #FFD23F); synapse-actions is now the S-Signal at
  glyph weight (brain retired), synapse-main stays impulse-free.
- Relax the brand-icons test color rule: the gold var's canonical
  fallback is the only hex a glyph may bake.
- Theme the gold in styles.css (#FFD23F, deepened to #E8B419 on
  .theme-light for contrast on white).
- README: hero uses banner-animated.svg (SMIL spike draw-on); sponsor
  badges move from the retired violets to Iris/Gold. Same palette note
  updated in the donation plan doc.
- Restore the SMIL animations that were stripped from the delivered
  banner-animated.svg and accept-flash.svg (reconstructed from the
  accept-flash.md spec; scaffolding was intact, animate nodes missing).
- Re-rasterize social-preview.png (1280x640, 679 KB) from the new SVG.
- Sync package-lock version to 1.0.10 (validated with npm@10 ci).

Log the palette/exception decision in DECISIONS.md.

Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-02 12:05:37 -07:00
Dustin Keeton
1fbbbbab5e
Merge pull request #448 from dustinkeeton/chore/adopt-wafflestack-v0.3.0
chore: adopt wafflestack v0.3.0 (opt back into bot identity)
2026-07-01 21:24:00 -07:00
bot
a9be56febf
chore: adopt wafflestack v0.3.0 (opt back into bot identity)
Upstream (wafflestack PR #1, from the plotto consumer) made bot identity
optional: git.cmd / git.coAuthorTrailer / git.identitySection now default
to the developer's own git config. Synapse opts back in via committed
config that references {{git.botEmail}}/{{git.botName}} through nested
substitution, keeping the literal identity in the gitignored local
overlay exactly as before.

Also adopted from upstream: `tools:` frontmatter (was allowed-tools),
issue-skill label-taxonomy guardrails + native sub-issue linking, new
task-planner agent and clean-up skill, delegate worktree-bug workaround.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5rA4nHBQrjk9cPCHEwFCk
2026-07-01 21:18:03 -07:00
Dustin Keeton
01b2dc8472
Merge pull request #446 from dustinkeeton/feat/issue-445-agent-toolkit-consumer
feat: consume shared wafflestack toolkit for agent/skill definitions
2026-07-01 18:56:54 -07:00
bot
3f44c9ab44
refactor: rename agent-toolkit to wafflestack (v0.2.0)
Config contract renames with the toolkit: .wafflestack.yaml,
.wafflestack.local.yaml (gitignored), .wafflestack.lock.json.
Rendered output unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5rA4nHBQrjk9cPCHEwFCk
2026-07-01 18:26:15 -07:00
bot
100e1cce8b
feat: consume shared agent-toolkit for agent/skill definitions
Agent and skill definitions previously hand-maintained (and hand-mirrored
into .codex/ and .agents/) are now rendered from the private
dustinkeeton/agent-toolkit repo (v0.1.0). The committed
.agent-toolkit.yaml carries the project config (roster, module maps,
threat model, tdd tiers, plugin manifest values); account-specific bot
identity lives in the gitignored .agent-toolkit.local.yaml overlay.
Rendered output is byte-identical to the previous hand-maintained files.

Newly tracked: designer + project-manager agents and the
github-project-management skill (rendered clean of account refs, so the
PR #290-era gitignore entries no longer apply). delegate and
git-workflow renders embed the bot identity and stay ignored, as do the
project-local brand-manager/product-manager/brand-guidelines files.

closes #445

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P5rA4nHBQrjk9cPCHEwFCk
2026-07-01 18:17:23 -07:00
Dustin Keeton
ccb1d4f0a4
Merge pull request #444 from dustinkeeton/chore/release-1.0.10
chore: release 1.0.10
2026-07-01 14:05:59 -07:00
bot
f5722b483f
chore: release 1.0.10
Co-Authored-By: Claude <bot@wafflenet.io>
2026-07-01 14:01:26 -07:00