Commit graph

98 commits

Author SHA1 Message Date
59b34a780e feat: show recognition photo on photo management page
Some checks are pending
CI / test (push) Waiting to run
Displays the original pipeline search photo (storedPhotoPath from
AIPipelineJob input_data) as a read-only card above the editable photos.
The photo is fetched only if the file still exists on disk.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:00:53 +00:00
f1d3ee6b1e feat: seed eBay platform row via migration
Some checks are pending
CI / test (push) Waiting to run
INSERT INTO app.platforms WHERE NOT EXISTS — idempotent, safe to run
on existing installations. Fixes "eBay-Platform nicht konfiguriert" error
when opening the eBay category config CRUD.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 15:47:19 +00:00
40834a8bd6 fix: eBay config CRUD — override createEntity() for required constructor args
Some checks are pending
CI / test (push) Waiting to run
EasyAdmin calls new ArticleTypePlatformConfig() with no args. Fix:
- Add setArticleType() setter to the entity
- Override createEntity() in the CRUD controller to inject the eBay Platform
  and first available ArticleType as placeholder (form overwrites both)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 13:58:48 +00:00
36e7d02539 docs: add Docker layout and service interaction documentation
Some checks are pending
CI / test (push) Waiting to run
Covers all 8 services, image/Dockerfile, PHP config, FPM pool tuning,
volume layout, network topology, startup order, local dev override,
operational commands, and Gitea Actions CI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:22:53 +00:00
c98f53c6e5 docs: update README to reflect current state (Plan 8)
Some checks are pending
CI / test (push) Waiting to run
- AI backend: Mistral Cloud API (primary, not Ollama)
- Features: photo management, eBay business policies, aspect import
- Architecture section: updated source tree, Messenger transport table
- eBay setup section: step-by-step config guide
- Remove outdated Ollama switch instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 11:19:05 +00:00
a79791a972 style: apply CS Fixer formatting across codebase
Some checks are pending
CI / test (push) Waiting to run
Consistent brace style, spacing, and method expansion throughout
domain, infrastructure, and test files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 10:56:37 +00:00
fc18958e0e feat: article photo management + eBay image URLs
Some checks are pending
CI / test (push) Waiting to run
- Public photo endpoint at /photos/{filename} (no auth, UUID-based filenames)
- Admin photo management page per article: upload multiple, delete, set main, drag-reorder
- "Fotos verwalten" action button on article index + detail pages
- EbayAdapter.publishListing() now includes imageUrls (main photo first, max 24)
- APP_PUBLIC_URL env var for absolute URL generation in Messenger workers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 10:51:03 +00:00
237b0e6d8e docs: add comprehensive architecture reference
Some checks are pending
CI / test (push) Waiting to run
Covers layer structure, domain model, all Messenger flows (AI pipeline A+B,
order processing, stock sync), channel adapter pattern, eBay integration details,
auth, EasyAdmin layout, Doctrine gotchas, and a file-location index.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 07:39:04 +00:00
6bd8e0bec8 feat: eBay business policies + per-adapter admin navigation
Some checks are pending
CI / test (push) Waiting to run
- ArticleTypePlatformConfig: fulfillmentPolicyId, paymentPolicyId,
  returnPolicyId, merchantLocationKey (all nullable)
- EbayAccountApiClient: fetches Fulfillment/Payment/Return policies
  from eBay Account API (/sell/account/v1)
- EbayInventoryApiClient: adds getLocations()
- EbayPolicyProvider: aggregates choices with 5 min cache; returns
  empty array on API failure so the form degrades to TextField
- EbayAdapter: reads real ArticleTypePlatformConfig (category ID no
  longer hardcoded), passes listingPolicies + merchantLocationKey
  into createOffer() when set
- EbayArticleTypePlatformConfigCrudController: live policy dropdowns
  from EbayPolicyProvider; fallback to TextField with help text
- DashboardController: eBay subMenu with Kategorie-Konfigurationen
- 7 new unit tests for EbayAdapter policy scenarios

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 07:13:51 +00:00
31c5116a1b docs: add CLAUDE.md and Plan 7 (eBay admin navigation + business policies)
Plan 7 covers: ArticleTypePlatformConfig policy fields, EbayAccountApiClient,
EbayPolicyProvider with live dropdown choices, EbayAdapter reading real config,
and per-adapter admin navigation section.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 06:25:14 +00:00
371213dbbb docs: update design doc + add infrastructure runbook
- Remove Ollama as AI backend (never used); Mistral is primary throughout
- Add infrastructure.md: full Docker setup, queue architecture, prod
  deployment steps, env vars — prod has no staging ERP container

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 05:59:21 +00:00
bf1af0a0bf feat: replace JSON ebay mappings with ArticleTypeEbayMapping entity
Introduces a proper key-value table (article_type_ebay_mappings) that
explicitly maps each eBay aspect name to either an Article field
(manufacturer, modelNumber, …) or an AttributeDefinition, with a
required flag per mapping entry.

- New entity ArticleTypeEbayMapping with SOURCE_ARTICLE_FIELD / SOURCE_ATTRIBUTE
- ArticleType gains OneToMany ebayMappings collection with upsertEbayMapping()
- EbayAdapter.buildAspects() reads from the mapping table instead of implicit name-matching
- Import controller persists mappings via upsertEbayMapping() and syncs required attribute assignments
- Template shows active mappings card and article_field action option
- Migration 20260520100000 creates the new table, drops old JSON column

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:52:25 +00:00
9259b99e7d fix: allow null for ebayAspectFieldMappings on existing rows
Doctrine sets nullable JSON columns to null when the DB value is NULL.
Typed array property cannot hold null — changing to ?array and coercing
to [] in the getter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:42:30 +00:00
ed2b83ba9f feat: map eBay 'Produktart' aspect to ArticleType name
Adds articleTypeName as a mappable article field — Article.getArticleTypeName()
proxies to articleType.getName(). 'Produktart' auto-detects via alias, so the
import UI pre-selects this mapping and the eBay listing gets the type name as
the Produktart aspect value automatically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:40:28 +00:00
61ce94bc6f feat: map eBay aspects to Article fields (Marke→manufacturer, PN→modelNumber)
Adds an 'Artikelfeld' action in the aspect import UI alongside skip/match/create.
Aspects like 'Marke' and 'Herstellernummer' auto-detect to manufacturer/modelNumber
via ARTICLE_FIELD_ALIASES. Mappings are persisted as a JSON column on ArticleType.
EbayAdapter.buildAspects() now reads these mappings and populates them from the
article's direct fields when building eBay listing aspects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:34:49 +00:00
929f5a0b2d test: add category suggestions integration test
Verifies getCategorySuggestions() returns id/name/path shaped results
from the eBay sandbox Taxonomy API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:29:47 +00:00
0fdb4979c1 fix: route EbayAspectImportController redirects through AdminUrlGenerator
Plain redirectToRoute() bypasses EasyAdmin's AdminRouterSubscriber, so
the admin context (ea.i18n etc.) was never set and the EasyAdmin layout
threw "Impossible to access i18n on null". Using AdminUrlGenerator wraps
the redirect URL in the EasyAdmin routing layer, keeping context alive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:15:31 +00:00
53e2d36574 fix: replace unknown Twig toString filter with toRfc4122() call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:12:17 +00:00
818c1ec8f7 feat: eBay category typeahead search for aspect import
Adds live category search so users don't need to know eBay category IDs.
Typing in the search box hits /admin/ebay/category-search, shows name +
breadcrumb path, and auto-saves the selection to the ArticleType on pick.
Aspect import table now only renders after a category is set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:10:07 +00:00
d26c534c34 feat: eBay aspect import — match/create attributes from eBay taxonomy
ArticleType gains ebayCategoryId (migration 20260520080000).

New admin action "Import eBay Aspects" on ArticleType list and detail:
  - Fetches aspects via EbayTaxonomyService (cached 7d)
  - Sorts: Required → Recommended → Optional
  - Auto-matches by case-insensitive name to existing AttributeDefinitions
  - Pre-selects "Create new" for required/recommended with no match
  - Pre-selects "Skip" for optional with no match
  - Already-assigned definitions highlighted green
  - Per-row: override to Skip / Match existing / Create new
  - Type auto-detected: Select (≤30 eBay values) or String (freetext)
  - User can override type in create form
  - Required checkbox pre-checked for eBay-required aspects
  - "All → Create" / "All → Skip" bulk buttons
  - On submit: creates new AttributeDefinitions, links all to ArticleType,
    deduplicates, calls applyAttributeAssignments(), flushes

PHPStan level 9 clean throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:30:45 +00:00
7f2ec21c64 feat: expose eBay aspect usage tier (RECOMMENDED vs OPTIONAL)
EbayTaxonomyService.getCategoryAspects() now returns 'usage' alongside
'required'. eBay has three effective tiers for category 177/Notebooks:
  required=true  + usage=RECOMMENDED → hard gate (3 aspects)
  required=false + usage=RECOMMENDED → search ranking signal (17 aspects)
  required=false + usage=OPTIONAL    → truly optional (11 aspects)

Integration test covers all three tiers explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:21:31 +00:00
68a9f0094e feat: eBay sandbox integration — env config + taxonomy/adapter tests
Add sandbox credentials to .env.test and .env.local (sandbox URLs).
Pass EBAY_* vars through bin/test-integration.

EbayTaxonomyIntegrationTest: 6 tests against sandbox Taxonomy API using
app token (client_credentials) — verifies OAuth, aspects for notebooks
(cat 177) and RAM (cat 170083), required flags, value lists, caching.

EbayAdapterIntegrationTest: listing publish/update/deactivate tests skip
gracefully when EBAY_USER_TOKEN not set (Inventory API requires
Authorization Code user token). Noop-deactivate test always runs.

All 6 taxonomy tests pass against live sandbox.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:02:49 +00:00
f0d9f374e6 docs: add README and update design doc for post-plan features
New README.md covers quick start, tech stack, running tests, AI backend
switching, admin features, and architecture overview.

Design doc updated (2026-05-18): Tavily replaces SerpAPI, specs_text
field, pipeline model cache step, findExistingCustomer in order flow,
required-attribute UX, user permissions UI, article detail-first nav,
updated open items.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 17:41:33 +00:00
376171303e fix: vision agent serial-bleed regex + fix broken agent unit tests
OllamaVisionAgent.extractField() now handles field labels at the start
of a value (e.g. MODEL_NUMBER: "SERIAL: 1005NK677594" -> "") not just
mid-value bleed. Both agent test files updated to mock
PromptTemplateRepositoryInterface and construct a real
PromptTemplateService, since the service is final and unmockable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:57:01 +00:00
c19637465b feat: Frappe ERP matching, pipeline model cache, ACL, stock field, specs by type
Frappe ERP:
- findExistingCustomer() on FrappeErpAdapter — two-step name+address lookup
- FrappeHttpClient: add put() method; switch invoice submit to PUT docstatus=1 (Frappe v16)
- buildItemDescription() uses specsText + inventory number + serial number
- Integration tests: find Simon Kühn, create real 1337€ invoice, cancel+delete in tearDown
- FRAPPE_GENERIC_ITEM_CODE=SKU002 added to .env.local and bin/test-integration

Pipeline — model cache:
- PhotoUploadHandler: after vision, check DB for existing article with same modelNumber
- On match: copy ebayTitle/ebayDescription/specsText/attributes, skip specs+JSON+eBay steps
- DraftArticleHandler: apply model_match data and mark job complete directly
- ArticleRepository: findCompletedByModelNumber() query

Pipeline — specs by article type:
- SpecsResearchAgent: accept attributeFields list, format as bullet list in {{fields}} var
- SpecsResearchHandler: derive attribute names from ArticleType, pass to agent
- SpecsResearchMessage: add attributeFields param
- Prompt migration: replace hardcoded laptop spec list with {{fields}} placeholder

Article:
- specsText field (nullable text column + migration)
- stock field visible on index and editable in CRUD form
- addAttributeValue()/removeAttributeValue() adder-remover pair for Symfony form binding
- AttributeValue::getArticle() getter
- AttributeValueFormType: detect required attributes from ArticleType assignments, set required=true
- ManualIngestType: add stock/quantity field (default 1, min 1)

Users / ACL:
- PermissionVoter: define named permission constants + allPermissions()
- User: getGrantedPermissions()/setGrantedPermissions() helpers
- UserCrudController: permissions checkbox group on edit form

UI / assets:
- public/css/admin/custom.css: red asterisk for required fields
- DashboardController: register custom CSS

Infra:
- PipelineJobFailureListener: mark job failed (with real error) when Messenger exhausts retries
- doctrine.yaml: exclude app.inventory_seq from schema diff
- ErpAdapterInterface: add findExistingCustomer()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 16:42:15 +00:00
cba8ebcf5e feat: Frappe customer integration tests + FrappeHttpClient get/delete
Adds GET and DELETE methods to FrappeHttpClient. Integration tests cover
create, find, not-found (wrong name), and delete against the live staging
ERPNext instance. Run with: bin/test-integration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 14:47:20 +00:00
25cc47e7d6 feat: add erpstaging.schaunwama.de reverse proxy to frappe docker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:39:14 +00:00
5927fa97c4 fix: article index row click goes to edit instead of detail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:27:23 +00:00
945f0479ca feat: collapsible attribute list on article detail via <details> element
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:22:07 +00:00
9f64b2c125 fix: render ebay description HTML via field.value|raw, drop scrollbar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:20:33 +00:00
ad9cb279c9 fix: article detail — photos on top, no ID, eBay description without scrollbar
Photos field moved to first position. ID field removed entirely. eBay
description on detail uses a custom template that renders raw HTML in a
plain div (no span/title wrapper, no overflow constraints). Form view
keeps the textarea editor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:19:42 +00:00
a381ee6531 feat: strip --- delimiters from eBay text output, commercial listing prompt
Agent now extracts content between --- markers so LLM preamble/postamble
is discarded. ebay_description prompt updated for commercial listings:
no private-sale language, condition explicitly "gebraucht", 1 year
gesetzliche Gewährleistung. Device label now includes modelName.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:18:40 +00:00
1df6b7f0c6 fix: use entity.instance instead of ea.entity.instance in field template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:16:30 +00:00
14eab1ab5c fix: use Field for photos in article detail view (Collection can't be TextField)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:15:53 +00:00
ed0caea344 fix: use Field for attributeValues detail view, render ebayDescription as HTML
TextField rejects Collection values before formatValue runs. Switching to
the generic Field avoids the type check. ebayDescription now renders its
HTML tags in the detail view instead of showing raw markup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:15:32 +00:00
b908e44a6e fix: cascade delete attribute values when attribute definition is removed
FK on attribute_values.attribute_definition_id now uses ON DELETE CASCADE
so deleting an AttributeDefinition also clears its values from all
articles. Admin delete action shows a confirmation warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:02:46 +00:00
5b2a200fc2 feat: specs research agent reports corrected model number
When Tavily search results reveal that the OCR'd model number has a
character error, the specs_research prompt asks the LLM to output a
CORRECTED_MODEL_NUMBER line. The agent parses it out, stores it in the
job output, and DraftArticleHandler applies it to the article in
preference to the raw vision value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 11:00:41 +00:00
4515911b27 fix: combine modelName + modelNumber for specs search query
Model numbers from OCR often have character errors (G→6, O→0 etc).
Using both fields together lets the human-readable name anchor the
search even when the model number is slightly wrong.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:33:00 +00:00
b8dd64eeba chore: migration to update vision_analyze prompt in DB
Captures the current working vision prompt (MODEL_NAME/MODEL_NUMBER
split) as a versioned migration so fresh installs get the correct prompt
without manual intervention.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:22:40 +00:00
32da9bb48f refactor: make prompt templates DB-only system prompts
Remove all hardcoded defaults from PromptTemplateService — the DB record
is now mandatory and render() throws if a key is missing. Admin UI
disables new/delete and makes the key field read-only so system prompts
cannot be renamed or removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:20:17 +00:00
974bd239a5 fix: prevent serial bleed into MODEL_NUMBER in vision output
Parser now strips any embedded field labels (e.g. "SERIAL: x") that the
LLM mistakenly appends to a field value. Prompt updated with a concrete
example showing MODEL_NUMBER as blank to reinforce leaving it empty when
no separate part code is visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:19:17 +00:00
fc628df42b fix: block pipeline at needs_review when no model detected
Without a model name/number specs research is pointless. Stays at
needs_review so the user can enter the model manually, then re-run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:17:25 +00:00
4d223c37c1 fix: relax vision prompt to prefer MODEL_NAME over strict name/number split
Splitting MODEL into MODEL_NAME and MODEL_NUMBER confused the vision
model into returning empty for both. Now MODEL_NAME accepts any visible
model identifier; MODEL_NUMBER is only filled when a separate part/
product code is explicitly shown on the label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:15:04 +00:00
321f6aaa05 fix: only block vision pipeline if nothing at all was readable
Previously blocked on needs_review when model name/number were empty,
even if manufacturer or serial were detected. Now proceeds to specs
research whenever any useful field was extracted, only blocking when the
nameplate was completely unreadable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:11:30 +00:00
525424a6a1 feat: extract modelName and modelNumber separately in vision pipeline
Vision prompt now distinguishes MODEL_NAME (human-readable product name,
e.g. "ThinkPad T490s") from MODEL_NUMBER (part/product code, e.g.
"20NXS0BA00"). Both fields flow through the pipeline and are written to
the article. Specs research uses model number as search subject when
available, falling back to model name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:09:26 +00:00
c10c306a5a feat: add optional modelName field to Article
Adds model_name VARCHAR(255) column to app.articles, exposes it in the
admin CRUD form (optional, hidden on index), and adds translations for
both EN and DE.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:06:36 +00:00
6e17fb82a0 fix: mark pipeline complete only after eBay text is generated
DraftArticleHandler was calling markCompleted() before dispatching
EbayTextMessage, causing the SSE to fire "completed" while the article
title was still null. EbayTextHandler had no job tracking at all.

- DraftArticleHandler: recordStep('draft_article') instead of markCompleted()
- EbayTextHandler: call markCompleted() after setEbayTexts() so the job
  is only marked done once the title and description are actually written

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 10:01:54 +00:00
0453d0542c fix: file picker and drop zone on ingest page
- Use <label for="..."> with form.image.vars.id instead of .click() on
  hidden input — display:none blocks programmatic click in some browsers
- Add drag-and-drop to the search photo drop zone (dragover/drop)
- Make extra photos input opacity:0/absolute so label trigger works too
- Camera fallback references correct searchInput variable via closure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:58:36 +00:00
6241398390 feat: camera capture and multi-photo upload at article ingest
- Ingest page redesigned: camera modal (getUserMedia + capture fallback
  for mobile), mandatory search photo with client-side validation,
  optional extra photos grid with per-photo remove button
- Camera modal: live video preview, capture → preview → confirm/retake
  flow; stops stream on modal close; falls back to native file picker
  if getUserMedia is unavailable
- ManualIngestController: uploads extra photos via PhotoService::uploadRaw(),
  stores [{storagePathId, filename}] in job inputData as extraPhotos
- PhotoService::attachExtra(): attach already-stored file to an article
  by StoragePath ID + filename
- DraftArticleHandler: after creating the article, attaches extra photos
  in sort order; errors are best-effort (pipeline not aborted)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:16:04 +00:00
693e458e07 feat: photo gallery on article detail with upload, sort and set-main
- Add secured GET /admin/photos/{filename} to serve files from var/uploads/
- Add POST /api/articles/{id}/photos/sort for drag-and-drop reordering
- Add PhotoService::reorder() to persist new sort positions
- Add photo gallery field (onlyOnDetail) using custom Twig template:
  - Drag-and-drop reorder via SortableJS CDN
  - Click or drop to upload new photos
  - Set main (★) and delete per photo
  - Main photo highlighted with blue border + badge
- Add field.photos translation key (EN/DE)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 09:10:59 +00:00