Commit Graph

507 Commits

Author SHA1 Message Date
red person
15a3b71802 Require runnable dispatcher subcommands (#1585)
* Require runnable dispatcher subcommands

* Use modern dispatcher test loader
2026-06-03 08:56:56 +09:00
red person
e68d0448b8 Parse all AMD GPU check args (#1586) 2026-06-03 08:56:48 +09:00
red person
db3a5c17b0 Reject backup output inside data dir (#1587) 2026-06-03 08:38:27 +09:00
red person
f39c87561b Save only string personal doc paths (#1566) 2026-06-03 08:37:29 +09:00
Mahdi Salmanzade
f7df069ca1 fix(ui): stop welcome-screen tip from clipping on narrow phones (#1612)
The empty-state tip ("Add an AI endpoint from Settings...") shares a 60px
max-height ceiling with the one-line .welcome-sub / .welcome-version. On
narrow phones the welcome block shrink-wraps and the tip wraps to 4-5 lines
(~67px), so the shared ceiling clipped its last line ("...key into the
chat.") - the only setup hint a first-run user gets.

Give .welcome-tip its own taller max-height (120px), placed above the
@media (max-height: 650px) block so that rule's max-height:0 still collapses
the tip on short viewports. .welcome-sub / .welcome-version are untouched,
and desktop is unchanged (the tip is ~50px there, well under the ceiling).
2026-06-03 08:37:23 +09:00
.bulat
eacb99f963 docs: clarify host Ollama with Docker (#1594) 2026-06-03 08:37:17 +09:00
Wes Huber
fb1341b629 fix(cookbook): set UTF-8 encoding for detached download/serve subprocesses (#1599)
On Windows, Python defaults to the active code page (cp1252) for
subprocess I/O. HuggingFace CLI outputs U+2713 (✓) when validating
tokens, which cp1252 cannot encode, crashing the download process.

Set PYTHONUTF8=1 and PYTHONIOENCODING=utf-8 in the subprocess
environment so Unicode output from hf/pip/llama-server is handled
correctly.

Fixes #1543

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-03 08:37:11 +09:00
Afonso Coutinho
19b6cbac12 fix: skills CLI summary crashes on a non-string description (#1595) 2026-06-03 08:37:05 +09:00
Afonso Coutinho
258fe455eb fix: research CLI summary crashes on a non-string query (#1596) 2026-06-03 08:36:57 +09:00
Afonso Coutinho
667c663668 fix: gallery CLI image serialization crashes on a non-string prompt (#1598) 2026-06-03 08:36:51 +09:00
Afonso Coutinho
0283216e67 fix: rag_server add/remove_directory crashes on a non-string directory arg (#1614) 2026-06-03 08:36:45 +09:00
Afonso Coutinho
77313170c6 fix: search query helpers crash on a non-string query (#1604) 2026-06-03 08:36:01 +09:00
Afonso Coutinho
0d25dfb5f4 fix: shared MCP truncate() crashes on None/non-string tool output (#1605) 2026-06-03 08:35:54 +09:00
Afonso Coutinho
6df0f5e6df fix: _sanitize_export_filename crashes on a non-string session name (#1607) 2026-06-03 08:35:47 +09:00
Afonso Coutinho
382d49d887 fix: validate_caldav_url crashes with TypeError on a non-string URL (#1608) 2026-06-03 08:35:16 +09:00
Afonso Coutinho
0d88c9989e fix: mcp CLI _serialize crashes when stored env JSON is a list (#1609) 2026-06-03 08:35:09 +09:00
Zarl-prog
5462e36d10 fix(ui): add missing Escape key handlers for email-lib-modal, model-picker-menu, and sort dropdowns (#1487)
CONTEXT: Several interactive elements lacked Escape key handlers: the email library modal was not in dynamicModals, the model-picker popup had no Escape close, and the session/model sort dropdowns only closed on outside click.

CHANGE: Adds email-lib-modal to the dynamicModals array in the Escape handler so it gets dismissed via dismissModal. Adds a check for model-picker-menu.open before the modal chain to close the dropdown on Escape. Adds checks for session-sort-dropdown and model-sort-dropdown display=block before the document panel minimize fallback.

WHY: Users expect consistent Escape-to-close behavior across all modals, overlays, and popups. These four were the only interactive containers in the app that ignored the Escape key entirely.

IMPACT: Pressing Escape now closes the email library modal, model picker popup, session sort dropdown, and model sort dropdown -- matching user expectations and the behavior of every other modal in the app.
2026-06-03 08:14:27 +09:00
Paulo Victor Cordeiro
5b0c7442ee fix: rename local url-quote import to avoid shadowing module-level _q (#1471)
The 'from urllib.parse import quote as _q' at line 734 shadows the
module-level _q (istrstrstrstrstrstrIMAPutility) imported from email_helpers, causing
UnboundLocalError at lines 191 and 278 where _q is used before the
local import executes. This silently breaks the entire auto-summarize
pass.
2026-06-03 08:14:19 +09:00
Paulo Victor Cordeiro
441905bc13 fix: guard AI tidy verdict against non-string LLM output (#1486)
The AI document-tidy endpoint parses verdicts from LLM JSON output
and calls .lower().strip() directly. If the model returns null or a
non-string element, this crashes with AttributeError. Coerce to str
so malformed output is treated as 'keep' instead of crashing.
2026-06-03 08:14:10 +09:00
Paulo Victor Cordeiro
24be3f3ca8 fix: guard uid.decode() in auto-classify warning log against str UIDs (#1472)
Every other uid.decode() call in this function uses
'uid.decode() if isinstance(uid, bytes) else str(uid)' but the
warning at line 832 does bare uid.decode(), crashing with
AttributeError when uid is already a string.
2026-06-03 08:13:01 +09:00
Paulo Victor Cordeiro
cf60a14d74 fix: capture download exit code before test consumes it (#1497)
The shell pattern 'if [ $? -eq 0 ]; ... else ... echo DOWNLOAD_FAILED (exit $?)' always reports 'exit 1' because $? inside the else branch is the exit code of the [ test command, not the download. Capture into _ec first.
2026-06-03 08:12:54 +09:00
Paulo Victor Cordeiro
bd0845e081 fix: guard sp.destroy() in _loadScheduled against null spinner (#1495)
When the scheduled folder is opened with cached data, sp is null
(the loading spinner is skipped). _loadScheduled receives null and
calls sp.destroy() unconditionally, crashing with TypeError.
2026-06-03 08:12:47 +09:00
Paulo Victor Cordeiro
dc3421c34e fix: return sorted model list on first call in group chat (#1484)
Both _getModels() and getAllModels() store the sorted copy in a cache
variable but return the original unsorted array on first invocation.
Subsequent calls return the cache (sorted), causing inconsistent
model picker ordering on first render.
2026-06-03 08:12:37 +09:00
lekt8
1f743970dd Don't lose deep-research findings when synthesis times out (#1551) (#1562)
Two problems made deep research report "No information could be gathered" even
after it had extracted findings, on slow local models (reporter served a 20B
via LM Studio):

- _synthesize hard-capped its LLM call at timeout=60, while extraction uses the
  user's extraction_timeout (300s here) and the final report uses 180s. The slow
  model needed >60s to synthesize the round's findings, so synthesis timed out
  after 3 attempts. Raised it to 180s to match the final-report call.

- When synthesis produced no report (it returns the unchanged, still-empty
  report on failure during round 1), the run hit
  `if not report: return "No information could be gathered…"` and discarded the
  findings it had already gathered. Now it falls back to a compiled report built
  from those findings (_fallback_report) so the user keeps the gathered material.

Tests stub the LLM (no live model/DB), pin the synthesis timeout >= 180, that the
fallback surfaces the findings rather than the give-up message, and that a failed
synthesis preserves the previous report.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 08:11:44 +09:00
Afonso Coutinho
c9361262df fix: APIKeyManager.load crashes app startup on a corrupt/wrong-shape api_keys.json (#1565) 2026-06-03 08:11:37 +09:00
lekt8
0e6cbd8315 Drop GPU-only flags from the CPU-only (-ngl 0) serve command (#1433)
A CPU-only llama.cpp serve config still emitted --flash-attn on and exported
GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 (independent toggles, often left on by an Auto
profile), so the command mixed "zero GPU layers" with CUDA/flash-attn and failed
to start (issue #1291). Gate both on a _cpuOnly check (ngl == 0). GPU serving is
unchanged — the gate only affects the ngl=0 path.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:26:15 +09:00
Zeus-Deus
5c6bd0fc2b Fix Edge/Chromium sidebar section-title clipping (#1420)
Sidebar section titles were vertically clipped in Chromium/Edge (fine in
Firefox). Raise line-height 1 → 1.3, mirroring the existing .list-item fix.
The titles are flex-centred in a fixed-height (29px) header, so this adds
glyph headroom without any reflow.
2026-06-03 04:24:29 +09:00
lekt8
57abe69173 Let the output "x" delete work when no model/session exists (#1431)
deleteMessage() bailed at `if (!sessionId) return;`, so the "x" on an output
shown before a model/API was selected did nothing — there's no session yet
(issue #1428). The session id is only needed for the server-side delete; without
one (or with no persisted message ids) we now fall through to removing the DOM,
so the "x" always at least dismisses the bubble.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:20:48 +09:00
lekt8
583df3dd6a Recognize gemma3/llama4/mistral-small3.1+/multimodal as vision models (#1430)
is_vision_model() classified several genuinely multimodal families as text-only
because their names contain neither "vision" nor "vl": Gemma 3 (4b+), Llama 4,
Mistral Small 3.1/3.2, and *-multimodal models (e.g. phi-4-multimodal). For those
the attached image was stripped before the request, so the model never saw it —
a "can't read the image" report (issue #1274), common with Ollama tags like
gemma3:4b.

Add those keywords (plus a generic "multimodal"). Per the file's err-toward-True
policy (#124), a rare text-only tag treated as vision is the safer failure than
dropping a real image. Guard tests confirm the text-only siblings (gemma2, plain
gemma, mistral-small, phi-3) are not over-matched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:17:40 +09:00
Isharak
70103d8719 fix(email): no-op IMAP connection leak in _auto_summarize_pass_single on exception (#1423)
`_auto_summarize_pass_single` in `routes/email_pollers.py` opens a
long-lived IMAP connection at line 172 and then performs ~700 lines of
work — IMAP `select`/`FETCH`/`SEARCH`, network POSTs to the LLM
endpoint, SQLite writes, and per-uid awaits. The only `conn.logout()`
calls were on three safe paths (early `"No recent emails"`, early
`"No model configured"`, and the happy path at the very end). If any
exception fired between `conn` being created and the final happy path,
the outer `except` block at line 921 caught it, logged, and returned —
without ever calling `conn.logout()`. The IMAP socket leaked until
the server's idle timeout killed it.

This is the same shape as the just-merged upstream fixes #1325
(`_imap_move` in `routes/email_helpers.py`) and #1330 (`_list_emails_sync`
in `routes/email_routes.py`), but in the *background* poller path —
`_auto_summarize_poller` invokes it every 30 min, so the leak
accumulates on every crashed pass instead of being a transient
request-path leak.

The fix is the exact try/finally pattern from #1330:
  1. initialize `conn = None` before the try
  2. let the try-block assign `conn = _imap_connect(...)`
  3. drop the three explicit `conn.logout()` calls on safe paths
  4. add a `finally:` block that calls `conn.logout()` if `conn` was set

Tests in `tests/test_email_polly_imap_leak.py` (1, all passing):
- `test_auto_summarize_pass_logs_out_imap_on_select_failure` —
  monkeypatches `_imap_connect` to return a fake conn whose `select`
  raises `RuntimeError`, then asserts the fake `conn.logout` was
  called exactly once and the function returned an `Error: ...`
  string. Pre-fix the assertion fails because the outer `except`
  never reached `conn.logout`; post-fix the `finally` block
  guarantees it on every exit path.

Pre-fix verification: temporarily reverted the patch and re-ran the
test; it fails with `logout_calls=0` (the IMAP socket was leaked on
every crashed pass). Post-fix: `logout_calls=1`.

Uniqueness:
- `git log --all --oneline -S 'conn.logout' -- routes/email_pollers.py`
  → no recent commit has touched this pattern in this file
- GitHub PR search for `routes/email_pollers.py` open PRs → 0
- Function has no existing test file (`grep _auto_summarize_pass_single
  tests/` → no results)

---

**@pewdiepie-archdaemon — gentle bump on a sibling PR that's also stuck
in your queue from the same author:** PR #1306
(`fix(caldav): no-op prune when date_search returns 0 events`) is on
its 4th rebase, isolated to 2 files, 2/2 tests passing, with one
independent approval from `lalalune` already on record. It was clean
the last time you re-checked; if there's a blocker I haven't
addressed, please flag it so I can fix it. Otherwise, both #1306 and
this one are ready to merge.

Co-authored-by: isharak7m <192635824+isharak7m@users.noreply.github.com>
2026-06-03 04:13:52 +09:00
lekt8
8450cee02a Surface upload failures instead of silently dropping the files (#1425)
uploadPending() read `data.files` from /api/upload without checking `res.ok`, so
a non-OK response (429 rate limit, 413 too large, …) was swallowed: the pending
files vanished and the chat sent with no attachments and no feedback — part of
why the model "didn't even see them" in #1346.

Check res.ok; on failure show the server's reason via a toast and keep the
pending files so the attach strip re-renders for a retry (matching the existing
"restored on error" comment that the code never actually honored).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:12:23 +09:00
red person
1ecd113808 Keep presets loading with bad local state (#1417) 2026-06-03 04:09:28 +09:00
lekt8
4d1829add0 Clear the composer draft when entering the New Chat / welcome state (#1408)
Clicking "New chat" (the brand/welcome navigation path) left the previous
session's unsent draft in the composer (issue #1343). The direct model-picker
path (createDirectChat) already cleared it, but the welcome path did not.

Clear `#message` in chatRenderer.showWelcomeScreen() — the shared entry point
for that state — resetting its autosized height and dispatching an `input` event
so the send button / autosize listeners update. Switching between existing
sessions loads them directly and does not call showWelcomeScreen, so genuine
drafts are not erased.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:07:31 +09:00
red person
5fd71f68e8 Keep group chat session cache loading (#1418) 2026-06-03 04:05:40 +09:00
lekt8
0ec8415f0e Fix multi-file uploads tripping the per-IP concurrency guard (#1346) (#1362)
* Stop multi-file uploads from tripping the per-IP concurrency guard

The /api/upload concurrency check summed its condition over `files`, but the
condition didn't reference the loop variable — so it collapsed to len(files)
whenever the IP had any recent upload. A single multi-file batch sent right
after another upload therefore counted itself as N concurrent uploads and hit
max_concurrent_uploads (3), returning 429. The browser swallows the 429 (no
`files` in the body) and sends the chat with no attachments, so the model
"doesn't even see" them (issue #1346).

Count genuine recent upload events instead, via a pure count_recent_uploads()
helper, independent of the current batch's file count. save_upload still
enforces the per-minute sliding-window rate limit per file, so throttling is
preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Also reconcile the per-minute upload rate limit with the batch cap

Follow-up within #1346: even after the concurrency-guard fix, a 6+ file batch
still failed because save_upload() counts each file against upload_rate_limit
(was 5/min) while the composer allows MAX_FILES=10 per batch — the reporter saw
"5 attachments work, 6 fail". Raise the per-minute file cap to 60 so a single
full batch (and a few of them) isn't self-rejected; burst abuse stays bounded by
max_concurrent_uploads. Add a real 6-file regression + a config guard that the
cap exceeds the frontend MAX_FILES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 04:04:19 +09:00
red person
fd37ccebae Ignore invalid personal docs state (#1401) 2026-06-03 04:02:16 +09:00
red person
abbc073429 Reject invalid preset CLI stores (#1395) 2026-06-03 03:59:05 +09:00
lekt8
a5282e9748 Pin the SearXNG image so a broken :latest can't block startup (#1419)
odysseus waits on searxng's healthcheck (depends_on: condition: service_healthy),
so when the upstream `searxng:latest` tag is broken the whole app never starts.
The 2026.6.2 image crashes on boot with `KeyError: 'default_doi_resolver'`,
failing the healthcheck and blocking fresh Docker installs (issue #1414).

Pin to the last known-good tag (2026.5.31-7159b8aed) instead of :latest, with a
comment to bump it deliberately after verifying a newer tag boots clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:56:54 +09:00
red person
35c40bce75 Fall back from invalid settings stores (#1416) 2026-06-03 03:53:05 +09:00
lekt8
77b63ed942 Keep Cookbook download-failure toasts visible long enough to read (#1412)
The Cookbook download path showed its error toasts with the default ~1.2s
duration, so an actionable message like "tmux is required for Cookbook
background downloads/serves … install it with your OS package manager" vanished
before it could be read (issue #1355). The serve path already uses multi-second
durations.

Give the three "Download failed" toasts a 9s duration to match.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:48:25 +09:00
Paulo Victor Cordeiro
1f2a06facd fix: MCP reconnect via tool passes only server_id to connect_server (#1385)
* fix: MCP reconnect via tool passes only server_id to connect_server

connect_server requires name, transport, command, args, env, and url
but the reconnect path in do_manage_mcp only passed the server_id,
causing a TypeError on every reconnect attempt. Mirror the pattern
used in mcp_routes.py reconnect_server.

* test: verify MCP reconnect passes full server config to connect_server

Mocks the MCP manager and DB to assert that do_manage_mcp reconnect
passes name, transport, command, args, env, and url — not just the
server_id.
2026-06-03 03:46:07 +09:00
lekt8
69d6fe44b3 Wrap the README banner in a code fence so it renders as typed (#1403)
The decorative banner under the title wasn't in a fenced code block, so GitHub's
markdown collapsed its leading whitespace and joined the box-drawing rules,
rendering the ASCII art misaligned instead of monospace-as-typed (issue #1390).
Fence it; the H1 title stays a real heading.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:42:01 +09:00
Afonso Coutinho
7af168fa59 fix: rag add_directory records the dir so list/remove can see it (#1369) 2026-06-03 03:37:33 +09:00
CorVous
7ce2db2771 fix: prevent iOS focus-zoom on form fields (touch only) (#1323)
iOS Safari auto-zooms when a focused input has font-size < 16px. Bump
text-entry controls to 16px under (hover: none) and (pointer: coarse) so
desktop sizing is untouched. Date/time inputs and selects are excluded —
they open native pickers and never zoom.

Doc-editor tiers keep their size hierarchy: Large lands at 18px (above the
16px threshold) instead of collapsing onto Medium, and the email rich-body
Large (17px) is left alone since it was already zoom-safe. All three editor
layers (textarea, highlight overlay, line numbers) move together so the
syntax overlay stays metrically aligned.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 03:34:25 +09:00
lekt8
cb114d6e87 Remove stray PR screenshots accidentally committed under docs/ (#1351)
PRs #738 and #644 committed their before/after review screenshots into the
repo (docs/a11y/focus-*.png, docs/a11y/login-*.png, docs/gallery-314-*.png).
Nothing references these files, so they only showed up as "random images" in
the doc folder (issue #1335). The README hero image and the feature preview
clips are referenced and are left untouched.

Add tests/test_docs_no_orphan_images.py to guard against recurrence: it fails
if any image under docs/ is referenced by no tracked text file.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:31:09 +09:00
LittleLlama
50a486b608 fix(cookbook): add NVFP4 to quantization picker dropdown (#1378)
Fixes #1328
2026-06-03 03:26:43 +09:00
lekt8
b6843c7621 Route "read that report" to manage_research instead of the HTML render (#1375)
After a deep-research job completes, a follow-up like "check it out" / "read
that report" had the agent web_fetch the /api/research/report/{id} HTML render
(and then drift into unrelated searches) instead of reading the saved report
(issue #1363). The report text is already available via the manage_research
tool (action read), and action list returns ids most-recent-first, so the
agent can resolve "the recent report" itself.

Strengthen the manage_research instructions: read a finished report via
action list -> action read; do NOT web_fetch/app_api the report URL (it renders
HTML, not clean text) and do NOT start a fresh web_search just to read an
existing report. Annotate the app_api endpoint list to say the same.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 03:24:09 +09:00
Shaw
b54468291e fix(hwfit): detect unified-memory NVIDIA (Grace Blackwell GB10 / DGX Spark) instead of 'No GPU' (#1340) (#1372)
_detect_nvidia parsed nvidia-smi --query-gpu=memory.total,name and did
float(memory.total) per row, dropping the row on ValueError. Grace Blackwell
GB10 (DGX Spark, sm_121) reports memory.total as '[N/A]'/'Not Supported'
because the GPU shares the system LPDDR pool rather than carrying discrete VRAM
— so the only GPU row was dropped and a real GB10 (even with vLLM running on it)
was reported as 'No GPU', breaking Cookbook recommendations and model switching.

Keep a named device whose memory.total is non-numeric: when there are no
discrete-VRAM rows but such unified devices exist, report a unified-memory CUDA
GPU backed by the system RAM pool (has_gpu, name, backend=cuda, count,
unified_memory=True) — mirroring how Apple Silicon and AMD APUs are already
handled. Discrete GPUs are unchanged, and a box with a real discrete GPU keeps
the discrete path.

Adds tests/test_hwfit_unified_nvidia.py with a GB10 nvidia-smi fixture: the
device is detected (not dropped), surfaces through detect_system with
unified_memory propagated, discrete GPUs stay non-unified, and a discrete GPU
takes precedence over an N/A-memory row.

Co-authored-by: NubsCarson <nubs@nubs.site>
2026-06-03 03:19:39 +09:00
Shaw
66c9349ee3 fix(skills): markdown save must not rename the skill, so delete keeps working (#1333) (#1365)
POST /api/skills/{id}/markdown set sk.name = slugify(sk.name or match['name']),
taking the name parsed from the edited markdown frontmatter. A changed name
makes update_skill() move the skill directory on disk and re-key its usage
sidecar, orphaning the original id. The UI still holds that original id, so the
next DELETE /api/skills/{id} fails the name/id lookup and 404s — 'can't delete
them now'.

The audit save path (_apply_skill_md) already guards against exactly this with
sk.name = name and an explicit 'must NEVER rename the skill' comment. Apply the
same pin here: keep the stored name on markdown save (content edits still take
effect; only the rename is suppressed). Drops the now-unused slugify import.

Adds tests/test_skill_save_no_rename.py: saving markdown whose frontmatter
renames the skill keeps the original name and applies the edit, and a
subsequent delete-by-original-id succeeds. Pure unit test — calls the route
handlers directly with a mock Request (no server/network), like
test_skills_delete_owner.py.

Co-authored-by: lalalune <shawgotbags@gmail.com>
2026-06-03 03:16:11 +09:00
Paulo Victor Cordeiro
c3fd969965 fix: once-schedule comparison uses local time against UTC date (#1349)
When a timezone is configured, `now` is tz-aware local time.
The comparison stripped tzinfo with `.replace(tzinfo=None)`,
producing naive local time, but `scheduled_date` is stored as
naive UTC. For users east of UTC this causes tasks to appear
expired prematurely; for users west they linger past due time.

Use `_to_utc_naive(now)` to convert to the same reference frame.
2026-06-03 03:07:00 +09:00