Connect a channel by issuing a one-time code the user posts as /connect <code>
in their channel. A secret-token-guarded webhook matches the code, links the
channel as a SocialAccount (chat_id in meta), and records it on the request so
the connect endpoint can poll for completion. Adds the TelegramConnectRequest
model + migration, the connect/status endpoints, the public webhook route (CSRF
exempt), a ConnectionVerifier branch (getChat liveness), and a telegram:set-webhook
command. Tests cover the code issue, webhook link, secret rejection, expired/
unknown codes, status polling, and the command.
Register Telegram as a platform: Platform/ContentType enum cases, a
platforms.telegram config block (shared bot token via env), TelegramPublisher
(sendMessage / sendPhoto|Video|Document / sendMediaGroup over the Bot API, HTML
parse mode, 4096 limit with long text split off a 1024 caption), wired into the
publisher dispatch. Add a Telegram ContentSanitizer branch (Telegram-allowed
HTML + ampersand escaping), MediaOptimizer/profile-url/factory support, and the
TelegramPublishException. Tests cover text, single media, album, long-text split,
overflow, API errors, private-channel URLs, and sanitization.
Add a test that a network error during handle resolution degrades the mention
to plain text instead of failing the post (exercises the resolveHandleToDid
try/catch), and a test asserting the post URL is built from the configured
web_app host (a revert to a hardcoded host would now fail).
Add a unit test asserting each BlueskyLexicon constant equals its AT Protocol
NSID — a wrong value (not just a typo'd name) would otherwise fail silently at
runtime. Add a test that a URL with a matching '(' keeps its trailing ')'
(Wikipedia-style), covering the previously-untested keep branch of the trim.
The Bluesky lexicon identifiers (createRecord, createSession, feed.post, facet
types, etc.) were repeated as magic strings across BlueskyPublisher,
BlueskyAnalytics, ConnectionVerifier and BlueskyController, where a typo fails
silently at runtime as "Invalid request". Define them once as named constants
so a typo is an undefined-constant error instead. Tests keep the literal NSIDs
as the independent contract.
The image tests only checked that createRecord was sent — the faked empty
download meant no blob was ever attached, so they never verified the embed.
Drive a real (mocked) optimize + upload and assert the createRecord record
carries an app.bsky.embed.images embed, and that six images upload exactly four.
Import Exception instead of referencing it inline, read the blob via data_get,
source the web-app host from config (new bluesky.web_app key) instead of
hardcoding bsky.app, and drop the getUtf8ByteOffset no-op — the PREG /u offset
is already in bytes, so the call sites use it directly.
The URL regex captured trailing sentence punctuation and an unmatched closing
paren ("see https://x.com).") into the link facet, so Bluesky rendered the
punctuation as part of a malformed link. Trim it like the official atproto
tokenizer. Add tests for the trim and for byte-offset correctness after
multibyte characters (emoji/accents).
resolveHandle is a public read served by the AppView, so drop the PDS-first
auth, the multi-endpoint fallback loop and the threaded service/account args in
favor of one unauthenticated GET against config public_appview — matching how
BlueskyAnalytics already reads from the AppView. The try/catch stays so a
network error degrades the mention to plain text instead of failing the post.
Remove the now-obsolete fallback test.
Memoize by key (array_key_exists) so a handle that fails to resolve is tried
once per post instead of on every repeat occurrence, and add a test that a
repeated handle resolves once. Cast the PREG_OFFSET_CAPTURE offsets to int to
match getUtf8ByteOffset's signature, and reword the resolver docblock to say
"configured default service" instead of naming bsky.social.
Add tests for the two untested branches of the DID resolution: falling back to
the public AppView when the account's PDS can't resolve a handle, and a post
mixing a resolvable mention (kept as a facet) with an unresolvable one (a 200
without a DID, left as plain text). The prior tests only hit the happy path and
the all-endpoints-fail path via a wildcard that masked the fallback.
Remove the final Log::warning (and the lastError/lastEndpoint tracking that only
fed it) — the per-endpoint debug log already covers diagnosis. Fake the
createRecord endpoint from config('trypost.platforms.bluesky.default_service')
in the new tests instead of hardcoding the host.
Use the existing config('trypost.platforms.bluesky.public_appview') and
default_service keys for the handle-resolution fallbacks instead of hardcoding
the hosts, so a self-hosted override (BLUESKY_PUBLIC_APPVIEW) is honored here
like it already is in BlueskyAnalytics. Import Throwable instead of referencing
it inline.
Add NodeRunStatus and RunStatus consts mirroring the backend enums, and route
the test panel and invocations list through them (plus the existing NodeType)
instead of bare 'completed'/'failed'/'fetch_rss' string checks. Type the run/
node-run interfaces with the value unions, extract the status tile classes into
a helper, and render the node type via i18n like the invocations list already does.
The test-run and invocation-detail requests used raw fetch with a hand-rolled
CSRF header. Switch to Inertia v3's useHttp (the standalone XHR client already
used across the app), which handles CSRF, headers and error payloads — dropping
the manual plumbing and reading the 422 message from error.response.data.
Rewrite inferScheduleFromCron: bail once on a pinned month, group the branches
by cadence (sub-hourly / hourly / daily / weekly / monthly), and share the
hour+minute via one `clock` object instead of repeating the same guards in
eight stacked conditionals. Behavior-preserving — verified the generate↔infer
round-trip across every preset.
The required method/unit/operator/mode props were typed as plain string after
dropping their defaults. Use the matching *Value union types (HttpMethodValue,
DelayUnitValue, ConditionOperatorValue, PublishModeValue) so the renderers are
constrained to real enum values, matching the config panels.
Webhook and HTTP nodes are always created with a method, so the ?? default was
dead. Make the prop required and remove the now-unused HttpMethod import,
matching the unit/operator/mode cleanup.
The Generate/Delay/Condition/Publish node summaries rendered English prose and
raw enum values (account/accounts, single, hours, contains, scheduled · +N min).
Route them through i18n: pluralize the account count, and reuse the existing
unit/operator/mode/format labels in all three locales. Drop the redundant
?? defaults where the node is always created with the value (unit, operator,
mode) and tighten those prop types to required.
Add date.getTimezoneAbbr() and route every "GMT-x" abbreviation through it,
removing the three inline dayjs().format('z') copies (schedule-summary,
TriggerNodeConfig, PickTimePopover) and the duplicate userTimezone helper.
Drop the pointless computed wrapper for a value that never changes, and rename
snap5 to snapToFiveMinutes.
- Move the automation-node and json-viewer styles out of app.css into their
own imported stylesheets.
- Replace raw <label> elements across the node config panels and the test
panel with the shared ui/label component.
- Add AutomationRun::durationInMilliseconds() as the single source of truth
for the Invocations list and metrics, replacing the duplicated inline diff.
- Fold the variables and root_run_id columns into their create migrations
(this branch isn't in production) and drop the standalone alters.
- Import Illuminate\Http\Response (aliased) instead of referencing it inline.
A webhook node parses its payload template as JSON before resolving
placeholders, so a template with unquoted {{ }} placeholders or any malformed
JSON could be saved, tested, and activated — only to fail midway through a run.
Reject it up front instead: AutomationConfigValidator is the single source of
truth for per-node config issues (keyed to the field the editor surfaces them
under), enforced on save (field errors), on activate, and before a test run.
The editor mirrors the check to disable Test/Activate with a clear reason, and
the test panel now surfaces the server's message instead of a generic toast.
Show the user's timezone inline next to clock-pinned schedules
(Runs every day at 09:00 (GMT-3)) instead of on a separate hint line, and
only when the schedule pins a time of day — minute/hour intervals omit it.
The scheduler command ran every minute and loaded all active automations,
then filtered by trigger_type in PHP because that value lived buried in the
nodes JSON array — effectively a full-table scan plus a JSON decode per row
each minute, discarding every non-schedule automation.
Derive trigger_type into a real, indexed column on save (recomputed in the
existing saving() hook so it can never drift from nodes) and filter on it in
SQL. Applies to both the schedule firer and the post-trigger dispatcher.
Replace magic strings across the automation domain with backed PHP enums
(HttpMethod, AuthType, DelayUnit, ScheduleField) and mirrored TS consts
(http-method, auth-type, delay-unit, schedule-field, condition-operator,
publish-mode), plus the existing Condition\Handle / Operator / Publish\Mode.
Also:
- require scheduled_offset via concrete-index required_if instead of
defaulting to 60 when the publish mode is scheduled
- fail the webhook node explicitly when the resolved url is empty
- localize node failure messages (fetch_rss/http/webhook)
- cast resolver/strtoupper inputs to string so a present-null config value
degrades gracefully instead of crashing
- list automations with config('app.pagination.default'), drop the perPage param
- Pagination: drop the perPage override from ListAutomations and the
hardcoded page size from GetAutomationInvocations; both use
config('app.pagination.default'). Document the rule in CLAUDE.md.
- Imports: import DomainException / InvalidArgumentException / Throwable
instead of inline backslash references across the automation actions.
- i18n: move the hardcoded Fetch RSS and HTTP Request failure strings to
automations.errors.* in all three locales.
- Publish: require scheduled_offset when mode is scheduled (validation)
and drop the magic 60-minute default in the node. Fixes the required_if
rules to reference the concrete node index instead of a wildcard that
never resolved (also repairs the trigger cron rule).
- Condition handles: back the yes/no output handles with a shared
Condition\Handle enum (PHP) and ConditionHandle const (TS).
resolveVariable() declares a string return type, but json_encode returns
false on malformed UTF-8 (plausible for scraped feed/HTTP payloads),
which throws a TypeError under strict_types and fails the node. Encode
with JSON_PARTIAL_OUTPUT_ON_ERROR and fall back to an empty string.
Remove the "Custom (Cron)" schedule field — too technical for the editor.
The remaining presets (minutes/hours/days/weeks/months) cover the need and
still build the cron string under the hood.
Removed at the root: the ScheduleField.Custom enum case, the custom-cron
input and select option, the schedule_custom_cron type field and its
schedule-summary handling, the backend validation (Rule::in and the
schedule_custom_cron rule), the i18n keys, and the custom round-trip test
case. Existing automations keep firing — the scheduler runs off the stored
cron string, not schedule_field.
Wrap the webhook HTTP send in a try/catch so a connection error returns
a clean failed result (reason: request_failed) instead of bubbling up as
a job failure — matching the HTTP request node.
Add tests for the gaps in node-run coverage:
- HTTP request: basic auth, PUT/PATCH/DELETE, non-2xx responses,
connection exceptions, and an items_path that doesn't resolve to a list.
- Webhook: every HTTP method, header expression resolution, and the new
connection-failure path.
- Fetch RSS: non-2xx feed responses, malformed XML, items without a
publish date (skipped), and the link fallback when an item has no guid.
- Delay: unknown unit throws.
- Publish: dry runs don't publish or queue.
- Updated the 'guide' label in English, Spanish, and Portuguese translations to provide clearer context: changed from 'Guide' to 'Learn how it works' and its translations.
- Removed the EditorGuide component from the automation form, replacing it with a tooltip that links to the documentation.
- Adjusted the icon used for the help button from IconHelp to IconLifebuoy for better visual representation.
- Enhanced the tooltip functionality to provide guidance on automation actions directly within the UI.
Split the automation detail screen into four route-based tabs behind a
shared AutomationHeader:
- Workflow: the existing editor canvas.
- Invocations: a paginated, filterable run log with expandable per-node
detail, a refresh control, and a loading state.
- Metrics: KPI cards, a runs-over-time @unovis chart with locale-aware
date labels, and a posts-by-platform breakdown over a date range.
- Settings: rename, an activate/pause switch, and a danger-zone delete.
Invocations and Metrics report only real executions via a new
productionRuns scope, so manual test runs (dry or with real data) never
leak into the log or the charts. The now-unused excludingDryRuns scope
is removed.
Generated copy now flows the most-restrictive platform context through
the humanizer too, and the editor guide documents every available
expression grouped by source node.
Replace free-text brand_tone/brand_voice_notes with a single structured
brand_voice_traits JSON column backed by the BrandVoiceTrait enum, exposed
as choice-chip pills in the brand settings UI and autofillable from a site.
Brand voice and visuals become per-automation toggles on the Generate node.
Unify the image controls into one 0-10 picker (0 = text-only, 1 = single,
2+ = carousel) and feed the generator the most restrictive selected network
so copy fits every platform. Pass that same platform context through the
humanizer pass — extracted into a shared ResolvesPlatformCopyBudget trait —
so the rewrite can no longer drift past the character cap the generator
respected, in both the automation and manual creation flows.
Persist the trigger node's schedule editor fields on save (they were
silently dropped by validated() for lacking validation rules).
Automations editor:
- {{ }} expression autocomplete in CodeMirror, scoped to the braces and
graph-aware (suggests only what upstream nodes provide + variables + now);
migrate the Generate prompt to CodeMirror so it shares the same completions
- Expandable editors: an expand button slides out a side-by-side panel
(matching the sidebar card), with a minimize control; the inline field
collapses to a hint while editing in the panel
- Hover-revealed editor toolbar (expand/copy) with styled tooltips so the
buttons no longer obscure the text while reading
- Beta badge on the Automations sidebar item
- Delete a single connection with Backspace/Delete (edge selection)
- Re-key node config so switching between same-type nodes refreshes the form
HTTP fetch node — cover every JSON response shape:
- Top-level array, object map (items_path=*), array of primitives, and NDJSON
- Key-based dedup via item_key_path (seen-set, FIFO-capped) for feeds without
dates; first poll records a baseline and emits nothing (date path too)
Fan-out test visibility:
- root_run_id links every forked branch back to the run that started a test,
so the test panel aggregates all branches instead of one
Fix a few pre-existing type issues (ScheduleData import, padded minute,
optional created_at).
- Added support for workflow variables in automations, allowing users to define reusable values.
- Implemented validation for Generate nodes to ensure intended image counts align with selected accounts.
- Updated automation models and requests to handle new variables, including encryption for sensitive data.
- Enhanced UI to display variables and their management within the automation editor.
- Improved error handling for webhook and HTTP nodes to prevent requests to invalid URLs.
- Refactored various components for better context resolution during automation runs.
- resolveHandleToDid: rtrim trailing slash on the service URL before building
the request URL and before the `=== $service` auth check (avoids double-slash
URLs and mis-detected auth).
- Log per-endpoint resolution attempts at debug; emit a single warning only
after all endpoints are exhausted (less noise during transient outages).
- Tests: fake resolveHandle with a wildcard so the case is isolated from the
configured service URL; read request payload via $request->data().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>