Commit Graph

155 Commits

Author SHA1 Message Date
agessaman 9d24e4824c feat(config, docs): add JWT configuration options for MQTT authentication
- Introduced `jwt_ttl_seconds` and `jwt_renewal_interval` settings in `config.ini.example` for global JWT management, allowing for better control over token expiration and renewal intervals.
- Updated documentation in `packet-capture.md` to clarify the usage of global and per-broker JWT settings, enhancing user understanding of authentication configurations.
- Refactored `PacketCaptureService` to incorporate new JWT settings, ensuring consistent handling of token lifetimes and renewal processes.
2026-05-15 20:46:05 -07:00
agessaman c91baf1fdf refactor(message_handler, repeater_manager): enhance contact advertisement tracking
- Updated the `track_contact_advertisement` method to return a structured result indicating success and duplicate packet status, improving clarity in handling contact advertisements.
- Adjusted the `MessageHandler` to utilize the new result structure, allowing for better decision-making based on tracking outcomes.
- Enhanced unit tests to cover new behaviors and ensure correct handling of duplicate packets and advertisement tracking results.
2026-05-15 20:03:04 -07:00
agessaman 4a0b989f5a feat(scheduler): optional flood scope for scheduled channel messages
Parse channel:#scope:body in [Scheduled_Messages], pass scope to
send_channel_message; extend schedule list and docs.
2026-05-14 11:32:20 -07:00
agessaman d4eba9ad11 fix(scheduler): stagger scheduled sends and skip global rate limit
Scheduled cron jobs now pass the cron key into the send path, apply a
deterministic [0, N) second delay from scheduled_message_max_stagger_seconds
(default 1.5), and call send_channel_message with skip_user_rate_limit=True
so simultaneous jobs are not dropped by rate_limit_seconds. Per-channel and
bot_tx limits still apply. Document the new Bot key in config.ini.example;
tests assert kwargs, skip flag, and stagger behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 11:18:25 -07:00
agessaman 70b2256752 feat(test): Test_Command response_format with piped path filters
Add format_piped_template (pathbytes_min, prefix_if_nonempty), utils
bytes-per-hop helpers, and [Test_Command] format priority over Keywords.
2026-05-12 19:30:42 -07:00
agessaman 13f22e4d7b feat(path_command): add reply prefix and minimum path bytes configuration
- Introduced `reply_prefix` to prepend a customizable string to path command replies, supporting placeholders for dynamic content.
- Added `minimum_path_bytes` setting to control the resolution of repeater names based on the number of bytes per hop, enhancing path command behavior.
- Updated relevant documentation and translations to reflect these new configuration options.
- Implemented unit tests to ensure correct functionality of the new features.
2026-05-12 17:23:22 -07:00
agessaman 5879838d8b feat(command_manager): Fix flood scope on keyword and RandomLine channel replies
Route process_message keyword/RandomLine sends through send_response so
reply_scope reaches set_flood_scope; add optional command_id to
send_response for transmission tracking.
2026-05-10 19:20:22 -07:00
agessaman 4d35e103a2 refactor(scheduler): enhance scheduled message configuration and parsing
- Updated the scheduled message format in `config.ini.example` to support 5-field cron expressions and preset aliases, replacing the deprecated HHMM format.
- Improved the `MessageScheduler` class to parse and validate new schedule formats, logging warnings for deprecated usage.
- Adjusted the `ScheduleCommand` to display scheduled messages with their respective cron or preset labels.
- Added unit tests to ensure correct parsing and handling of various schedule formats, including legacy HHMM and cron expressions.
2026-05-05 14:12:02 -07:00
agessaman 6d496934d9 refactor(advert_parsing): improve flag handling in message and map uploader services
- Replaced the use of AdvertFlags enum for flag parsing with bitwise operations to handle invalid values correctly.
- Updated the parsing logic in both MessageHandler and MapUploaderService to use boolean flags for better clarity and performance.
- Adjusted error logging to use warnings instead of errors for parsing issues, enhancing the logging strategy.
- Added unit tests to ensure correct parsing behavior for edge cases with invalid flag values.
2026-05-04 15:45:38 -07:00
agessaman dfa5dc2e01 feat(services): add Discord/Telegram outbound helpers and repeater discovery alerts
Add bridge_outbound posting, BaseServicePlugin.send_external_notifications,
and RepeaterPrefixCollision discovery vs collision routing with silence_mesh_output.
2026-05-02 21:50:03 -07:00
agessaman 263c73c2e2 feat(packet_capture): enhance MQTT packet publishing controls and path length decoding
- Added configuration options `mqtt_skip_unparseable_packets` and `advert_require_valid_signature` to control MQTT publishing behavior based on packet validity.
- Updated `decode_path_len_byte` function to return `None` for reserved size codes, improving path length validation.
- Implemented logic in `PacketCaptureService` to skip publishing unparseable packets and ADVERT packets with invalid signatures.
- Introduced `verify_meshcore_advert_ed25519` function for signature verification of ADVERT packets, with corresponding unit tests to ensure functionality.
- Enhanced documentation to reflect new configuration options and their effects on packet processing.
2026-05-01 21:32:40 -07:00
agessaman 5a484d1db2 feat(web_viewer): add channel_name validation and update feed functionality
- Implemented validation for the `channel_name` field to ensure it is not empty when updating feeds, raising a ValueError if validation fails.
- Added test cases to verify that updates to `channel_name` persist correctly and that attempts to set an empty `channel_name` are rejected with an appropriate error response.
2026-04-25 21:34:17 -07:00
agessaman d000e9858e fix(repeater_manager): update purging log insertion to include public_key and name
- Modified the SQL insert statement in the RepeaterManager class to include `public_key` and `name` fields, ensuring compatibility with the new schema.
- Updated corresponding test cases to reflect changes in the database operation, maintaining the integrity of the purging log functionality.
2026-04-25 21:25:43 -07:00
agessaman d34ad24e9b feat(weather): add default city configuration for wx commands
- Introduced a new configuration option `default_city` in `config.ini.example` for fallback city names when no location is provided.
- Updated `wx_command.py` and `wx_international.py` to utilize the `default_city` for location disambiguation, enhancing user experience when companion location is unavailable.
- Improved logging to reflect the use of default city or bot location as fallback options.
2026-04-25 21:15:51 -07:00
agessaman 0f3ecea42e refactor(darc_mowas_service): improve imports and type hints
- Replaced direct imports of `pytz` with dynamic imports for better compatibility.
- Updated type hints for server and loop attributes in the DARC MoWaS service.
- Enhanced XML handling by specifying `xml.dom.minidom.Element` in method signatures.
- Improved message processing logic to handle missing channels and ensure valid area descriptions.
- Refactored `_child_text` function to accept both `Document` and `Element` types for better flexibility.
- Adjusted unit tests to reflect changes in the DARC MoWaS service structure.
2026-04-25 20:37:47 -07:00
Adam Gessaman a5f3cb008a Merge pull request #165 from fmoessbauer/fm/darc-mowas-api
feat: add implementation of the DARC MoWaS gateway
2026-04-25 20:29:34 -07:00
agessaman 6d80ad03da feat(trace_command): enhance path parsing for multibyte nodes and update documentation
- Updated the TraceCommand class to support parsing of 1-byte, 2-byte, and 3-byte hex node paths.
- Improved the _extract_path_from_message and _parse_path_arg methods to handle multibyte node formats and enforce consistent length checks.
- Enhanced help text and usage examples to reflect new path formats.
- Added unit tests for multibyte path parsing and reciprocal path building to ensure correct functionality.
2026-04-25 20:28:41 -07:00
agessaman 63592bdd65 feat(commands, config): implement path byte length requirements for commands
- Added configuration options to specify minimum path byte length requirements for the path, test, and multitest commands.
- Introduced methods to enforce these requirements and handle failure responses.
- Updated the packet capture service to correctly calculate packet hashes using the raw wire byte length.
- Enhanced unit tests to verify the correct behavior of the new path byte length logic.
2026-04-25 09:05:17 -07:00
Felix Moessbauer ffd93edd96 feat: add implementation of the DARC MoWaS gateway
We implement support for the DARC MoWaS service that distributes
emergency messages from the German BBK via the "Warnmultiplikator"
interface to Meshcore channels. This service is push-based and receives
alerts on /api/alert, which are then formatted similar to cell-broadcast
messages (without modification of the text) and re-distributed on
Meshcore.

Signed-off-by: Felix Moessbauer <felix.moessbauer@gmail.com>
2026-04-24 16:16:43 +02:00
agessaman 5f40c45f07 feat(PacketCapture): align RX/RAW correlation with meshcore-packet-capture
Deduplicate RAW_DATA against RX_LOG_DATA hex, cache RF metadata by packet
prefix, extend RAW payload extraction, fix payload_len for multi-byte paths.
2026-04-21 19:26:09 -07:00
agessaman 23cdce03e8 feat(db_migrations, message_handler, repeater_manager): enhance purging log functionality
- Added a new migration to include a 'details' column in the 'purging_log' table for improved logging of actions.
- Refactored the `log_purging_action` method in `RepeaterManager` to handle both new and legacy schemas, ensuring compatibility with the updated database structure.
- Updated `MessageHandler` to utilize the new logging method, replacing direct database updates with a unified logging approach.
- Added tests to verify the presence of the new column and the correct functionality of the logging mechanism across different scenarios.
2026-04-21 17:04:51 -07:00
agessaman 8e71a11bd1 feat(command_manager): enhance DM recipient resolution with public key prefix lookup
- Updated `send_dm` method to first attempt contact resolution by name, and if that fails, fallback to resolving by public key prefix.
- Improved logging to indicate the method of recipient resolution used.
- Added tests to verify the new recipient resolution logic, ensuring correct behavior when looking up contacts by public key prefix and handling failures appropriately.
2026-04-21 10:07:40 -07:00
agessaman 0e5daadd04 feat(config, message_handler, repeater_manager, scheduler): implement device mode for auto-managing contacts
- Enhanced configuration options for `auto_manage_contacts` to support 'device' mode, allowing firmware to handle companion auto-addition and favourite hygiene.
- Updated `MessageHandler` to track new companions based on the `auto_manage_contacts` setting, with distinct behaviors for 'false', 'device', and 'bot' modes.
- Introduced scheduled jobs in `MessageScheduler` for device mode to manage firmware preferences and favourite keys with specified delays.
- Modified `RepeaterManager` to skip companion auto-purge in device mode, ensuring firmware manages contact slots effectively.
- Added tests to validate new behaviors and configurations, ensuring robust handling of contact management across different modes.
2026-04-20 21:26:17 -07:00
agessaman f061df391e revert: back out #80 find_recent_rf_data changes due to regression 2026-04-18 11:14:34 -07:00
agessaman 879a86bd80 fix(message_handler): keep #80 correlation strictly observation-level
Simplifies find_recent_rf_data so the sample it returns is always a single
wire observation — snr, rssi, raw_hex, and routing_info all belong to the
same decoded copy. The #80 fix stays intact via the two earlier invariants:
unmatched correlation_key returns None (no fallback to an unrelated recent
packet), and the no-key DM path is constrained to a narrow
rf_fallback_window.

An earlier iteration tried to enrich routing_info with the longest observed
same-packet_hash forward, but that would have left message_stats rows with
hops sourced from the matched observation while path was a different
forward's — an internal skew visible in the web viewer and !path output.
The enrichment is documented in TODO.md as a v0.9.1+ option-C follow-up
that must also sync message.hops for consistency.

Regression tests in tests/test_message_handler.py::TestFindRecentRfData
cover: return-None on unmatched key, narrow-fallback-window rejection of
stale samples, and the no-cross-wiring assertion for same-packet_hash
forwards.

Made-with: Cursor
2026-04-17 14:37:21 -07:00
agessaman bf6bc14de7 release(v0.9.0): readiness — silent-subscribe tests, py310, RF correlation fix
- Rewrite test_subscribe_packets/messages_emits_status_ack to match the
  silent subscription UX from 1ee84f2.
- Reconcile Python version: requires-python>=3.10, ruff target py310, CI
  matrix adds 3.13, pyupgrade UP0xx ignored pending a separate typing-rewrite
  PR; fix two B905 zip(strict=...) lints.
- Issue #80 fix in find_recent_rf_data: return None when correlation_key is
  provided but unmatched; prefer the longest observed path among samples
  sharing a packet_hash; narrow the no-key fallback to a configurable
  rf_fallback_window (default 2s).
- Issue #161: lower shipped max_response_hops default 10 -> 7.
- Add CHANGELOG.md, restructure BUGS.md around a ## v0.9.0 Fixed Bugs
  table, prune crossed-out duplicate outstanding rows, and add a
  Deferred-from-v0.9.0 triage section to TODO.md.
- Untrack coverage.json and add it to .gitignore.

Made-with: Cursor
2026-04-17 13:53:13 -07:00
agessaman b5cce55604 feat(config): add configurable timeouts for web viewer integration
Introduced optional timeout settings in the configuration for various web viewer operations, including edge and node post timeouts, SQLite connection timeout, and requeue timeout. Updated the web viewer integration to utilize these settings, enhancing flexibility and reliability. Added commands to inspect the resolved configuration with sensitive keys redacted, and updated documentation accordingly.
2026-04-16 19:56:47 -07:00
agessaman 329905dd08 feat(core, scheduler): implement timeout handling for advert sending
Added asyncio timeout handling for sending startup and interval-based adverts, improving reliability by recording failures on timeout. Updated relevant tests to ensure coverage for timeout scenarios and increment failure counts appropriately. Introduced a new Radio Reliability panel in the web viewer for monitoring radio status.
2026-04-16 19:29:28 -07:00
agessaman ad09e8ba8a fix(web_viewer): update config item retrieval to prevent interpolation issues
Modified the config item retrieval in BotDataViewer to use raw=True, ensuring that literal '%' characters in configuration values are not subject to interpolation. Additionally, removed the breadcrumb navigation from the admin configuration template for a cleaner UI. Added a test to verify that '%' in values does not cause server errors.
2026-04-16 19:09:23 -07:00
agessaman 550a15e0d2 feat(web_viewer): bound packet_stream queue and requeue on flush failure
Add Web_Viewer.packet_stream_write_queue_max (default 1000), timed put
with flush retry on Full, flush lock, and re-queue rows after failed
batch insert. Document deferred PR #147 items. Reduce pytest cov
verbosity to term-only. Extend BotIntegration queue tests.
2026-04-16 19:02:33 -07:00
agessaman e04cc587cb test: add tests for admin config page loading and password redaction
Introduced a new test class, TestAdminConfig, to verify the loading of the admin configuration page and ensure sensitive information like passwords is redacted in the response. This enhances test coverage for the admin configuration functionality.
2026-04-16 18:41:27 -07:00
Stacy Olivas db0e1c6539 fix: resolve pr/web-viewer-ux CI failures
- Add allow_private param to validate_external_url (alias for
  allow_localhost) to unblock web viewer SSRF guard using allow_private=
- Block non-globally-routable IPs (RFC 6598 100.64.0.0/10 CGN) on
  Python 3.10 which does not classify them as private or reserved
- Remove tests for greeter DB tables and admin_config template that
  depend on features not present on this branch
2026-04-16 18:35:14 -07:00
Stacy Olivas a15827be8f usability: API Explorer tab, actionable error messages (USE-05, USE-06)
- USE-05: Add /api-explorer page listing all ~65 API endpoints in 9
  categories (System, Contacts, Mesh, Channels, Feeds, Radio, Admin,
  Maintenance, Config, Greeter) with method badges, descriptions, and
  curl example modal. Filter bar and collapse per section. Nav item
  added to base.html.

- USE-06: Three targeted error-message improvements:
  1. 500 handler now returns user-friendly HTML page (error.html) for
     browser requests and sanitized JSON for API/JSON requests instead
     of a bare string.
  2. Feed processed-items query failures promoted from logger.debug to
     logger.warning so operators see them in normal log output.
  3. Global JS fetch interceptor in base.html redirects to /login?next=
     on any 401 response, handling session expiry mid-page.

- Fix pre-existing test bug: test_reload_endpoint_success mock return
  value did not match actual code message from reload_config.
2026-04-16 18:33:40 -07:00
agessaman 572652e712 refactor: improve database connection handling in web viewer and tests
Updated the BotDataViewer class to utilize a context manager for database connections, enhancing resource management. Additionally, refactored test files to implement a centralized approach for managing SQLite connections, ensuring proper cleanup after tests. This change improves code maintainability and reliability across the application.
2026-04-16 11:18:59 -07:00
agessaman a4d5f5478b feat: add RandomLine command support for website generation
Introduced a new command structure for RandomLine entries in the website generation process, allowing for dynamic rendering of random lines based on configuration. Updated the configuration file to include a new category option for commands and enhanced documentation to reflect these changes. This addition improves the command reference organization and user experience on the website.
2026-04-15 09:40:39 -07:00
agessaman 90fdd0c77a feat: add cmd_reference_url option for Cmd_Command
Introduced a new configuration option `cmd_reference_url` in the Cmd_Command section, allowing users to override the default `cmd` output with a link to a full documentation page. Updated the CmdCommand class to utilize this new setting and modified related documentation and tests to ensure proper functionality.
2026-04-15 09:26:19 -07:00
agessaman e058da4968 fix: harden shutdown (single stop, viewer, MQTT logs, scheduler)
- Make MeshCoreBot.stop idempotent; remove duplicate stop from start().
- Avoid web viewer restart during shutdown; gate main-loop restarts.
- Fix packet capture MQTT callbacks to log each broker’s host.
- Only shutdown APScheduler when running; silence double-shutdown noise.
- Add regression tests in tests/test_shutdown_reliability.py.
2026-04-14 12:46:01 -07:00
agessaman 313dfccf75 fix: Align send suppression and security compatibility behavior
Make outbound send suppression consistent by honoring both radio-offline and zombie states across command-manager and scheduler paths. Preserve strict SSRF defaults while adding explicit private-feed URL opt-ins, persist allow_local_smtp from notifications config writes, reconcile zombie alert setting precedence, and replace deprecated UTC timestamp calls with timezone-aware UTC usage.
2026-04-14 12:18:02 -07:00
agessaman 52227ae6ac refactor: replace fortune command with random lines fortunes command
Removed the fortune command and its associated files, transitioning to existing new random lines command that retrieves fortunes from a different file structure. Updated configuration settings in config.ini.example to reflect this change, including the new file path for fortunes. Adjusted the .gitignore to accommodate the new directory structure for random lines. This refactor enhances the organization of fortune-related content and improves maintainability.
2026-04-14 11:27:40 -07:00
agessaman 6e8204c1ec fix: enhance path validation for security
Updated the path validation logic in security_utils.py and web_viewer/app.py to include additional dangerous prefixes, specifically targeting private directories. This change aims to strengthen security by preventing access to sensitive system paths. Additionally, modified the test for posting feeds to mock the external URL validation, ensuring consistent behavior during tests.
2026-04-14 10:25:18 -07:00
Stacy Olivas 13c10fd1a5 feat: add fortune command reading BSD fortune file format
Adds a new !fortune command that picks a random fortune from a
configured BSD-format fortune file (entries separated by a line
containing only %). The parser handles: % as separator or terminator,
multi-line fortunes, trailing whitespace, and % embedded mid-line.

Path validation via validate_safe_path() blocks path traversal before
any file open. Command is auto-discovered by the plugin loader.

Config section [Fortune_Command] added to config.ini.example with
enabled and file keys. Default file: data/fortune/fortunes.txt.

22 tests cover: parsing edge cases, security path rejection,
can_execute enable/disable, execute happy path, empty file fallback,
and plugin metadata.

Fortune file: BSD V8-fortunes (fortunes-freebsd-classic)
Source: https://github.com/HubTou/fortunes-freebsd-classic
1,903 fortunes drawn from the classic BSD UNIX fortune database.

Content note -- data/fortune/fortunes.txt (1,903 fortunes):
No racist, lewd, or pornographic content found. Six lines flagged
during review as mildly coarse; retained but noted here for reference
in case a future maintainer wishes to remove them:

  L1763: Some days chicken salad -- some days chicken shit.
  L2289: We retard what we cannot repel, we palliate what we cannot
         cure. -Johnson
  L3009: In New York they signal the wrong way just to fuck you up.
         -David Yost
  L3161: Sure [Somoza]'s a son-of-a-bitch, but he's OUR
         son-of-a-bitch.  -F.D. Roosevelt
  L3305: When better machines are built, jks will break them, and td
         will bitch about it.
  L3589: It's the thought, if any, that counts! -Dick Grantges
         ("Dick" is a proper name, not a slur)
2026-04-14 10:07:35 -07:00
Stacy Olivas 773b80f6ae feat: bot admin HTTP server + reload_config.sh CLI
- core.py: add _BotAdminServer daemon thread (Flask, 127.0.0.1 only,
  bearer token auth); POST /api/admin/reload calls reload_config() and
  returns JSON {success, message}; GET /api/admin/health; started from
  start() when [Admin] enabled = true and token is set
- scripts/reload_config.sh: curl wrapper for the reload API; reads
  port/token from config.ini [Admin] section; exits 1 on rejection
- tests/test_core.py: TestBotAdminServer — 7 tests covering server
  creation, missing token guard, reload success/failure/auth, health
2026-04-14 10:07:04 -07:00
agessaman 887068faa2 fix: resolve merge-marker cleanup and concise config docs
Clean up residual cherry-pick conflict markers and keep SMTP guidance in config templates brief while preserving full behavior in code and tests.

Made-with: Cursor
2026-04-14 10:06:44 -07:00
Stacy Olivas 7679eb7e2c fix: ruff and mypy compliance across utility scripts and tests
Fix ruff violations (import sort, unused imports, whitespace) in
backup_database.py, generate_website.py, meshcore_bot.py,
migrate_webviewer_db.py, packet_capture_service.py, version_info.py,
scripts/config_tui.py, scripts/update_todos.py, and validate_config.py.

Fix mypy stale type-ignore and remove unused imports in test files.
Add is_radio_zombie/is_radio_offline mock attributes to command_manager
test fixtures to prevent truthy Mock early returns.
2026-04-14 10:02:36 -07:00
Stacy Olivas 54aeb28bf0 security: SSRF hardening, log injection sanitization, and allow_local_smtp
Add SSRF host validation to maintenance.py send_nightly_email and
scheduler.py send_zombie_alert_email using validate_external_url().
New allow_local_smtp config key permits private-IP SMTP for local
relay setups.

Add sanitize_name() to security_utils and apply it to all log calls
in message_handler, repeater_manager, path_command, solarforecast_command,
command_manager, and discord_bridge_service to prevent log injection.

Move nightly email logic from duplicate scheduler._send_nightly_email()
into the canonical maintenance.py implementation, removing the duplicate.
Update tests to call maintenance.send_nightly_email() directly.

Add validate_external_url allow_private parameter with support for
loopback, RFC1918, CGN, and link-local address ranges.
2026-04-14 10:02:36 -07:00
Stacy Olivas c7fa0ba3d2 fix: resolve ruff F401 violations in command_manager and upstream test files
- Add noqa: F401 to PUBLIC_CHANNEL_KEY_HEX import (re-exported for core.py)
- Remove unused `call` import from test_flood_scope_reply.py
- Remove unused `asyncio` import from test_log_data_scope_fields.py
- Remove unused `patch` and `validate_config` imports from
  test_public_channel_guard.py; fix import sort order
2026-04-14 10:01:51 -07:00
Stacy Olivas 68fb1ef080 fix: stub is_radio_zombie in flood scope test mock
The send_channel_message test was leaving bot.is_radio_zombie as an
unset MagicMock attribute, which evaluates truthy and triggers the
zombie-state early return before flood scope logic runs.

Add bot.is_radio_zombie = False to the mock setup so the function
reaches the set_flood_scope call as intended.
2026-04-14 10:01:51 -07:00
Stacy Olivas eaf5324f0d fix: restore MaintenanceRunner patch targets in TestMaybeRunDbBackup
ff7917e incorrectly changed all test patches from
`scheduler.maintenance.run_db_backup` to `scheduler._run_db_backup`,
and rewrote `_setup` to bypass the delegation architecture upstream uses.

- Restore `_setup` to use `scheduler.maintenance.get_maint` side_effect
- Restore all 6 tests to patch `scheduler.maintenance.run_db_backup`
- Add `patch.object(scheduler.maintenance, "_get_current_time")` to
  `test_weekly_on_wrong_day_does_not_run` to prevent Monday flakiness
  in UTC CI environments (maintenance captures `_get_current_time` as a
  bound method, so patching only `scheduler.get_current_time` is not
  sufficient)
2026-04-14 10:01:51 -07:00
Stacy Olivas b56fa83fe3 fix: remove cross-branch security test classes from radio-reliability PR 2026-04-14 10:01:51 -07:00
Stacy Olivas fd0875611d fix: restore MaintenanceRunner arch and correct zombie-detection tests 2026-04-14 10:01:51 -07:00