- Simulate progress step advancement at 1.5s intervals during API calls
so users see incremental progress instead of all-at-once bursts
- PersonaChip skips API calls entirely in feature-only mode (no console spam)
- getUserPersonas/getPlatformPersona return null on 404 instead of throwing
- PersonaChip shows neutral gray state when no persona data exists
- Back button now clears draft to return to LinkedIn writer home screen
- Article title extracted from markdown content (fixes KeyError)
- InitialRouteHandler: demo mode subscribes getDefaultLandingRoute()
- Header: back button shown when draft exists, navigates to home screen
Phase 1: Dead Code Cleanup
- Remove GeminiGroundedProvider import and property from linkedin_service.py
- Remove fallback_provider property (gemini_provider imports)
- Fix routers/linkedin.py edit endpoint to use llm_text_gen
- Delete dead LinkedInImageEditor class
- Remove dead _transform_gemini_sources from content_generator.py
Phase 2: Research Infrastructure Alignment
- Add user_id to _conduct_research() for pre-flight validation
- Add validate_exa_research_operations() before Exa/Tavily calls
- Pass user_id to provider.simple_search() for usage tracking
- Inject research content into LLM prompts via _build_research_context()
- Fix Google engine path to fallback to Exa
- Add Exa → Tavily fallback on research failure
Phase 3: Cosmetic Cleanup
- Rename _generate_prompts_with_gemini → _generate_prompts_with_llm
- Rename _build_gemini_prompt → _build_image_prompt
- Rename _parse_gemini_response → _parse_llm_response
- Remove all Gemini references from LinkedIn code (0 remaining)
- Update docstrings and log messages
Additional:
- Research caching using existing ResearchCache
- Shared ExaContentResearchProvider in services/research/
- Persona service uses llm_text_gen instead of gemini_structured_json_response
- LinkedInWriter.tsx ChatMessage → ChatMsg type mapping fix
- RegisterLinkedInActionsEnhanced.tsx content_format_rules typing fix
Pin issuer and JWKS URL at startup from CLERK_PUBLISHABLE_KEY.
Validate token iss claim before any JWKS fetch.
Add issuer= to jwt.decode() with verify_iss=True.
- Register 'linkedin' FeatureGroup with routers.linkedin and
api.linkedin_image_generation routers
- Register 'facebook' FeatureGroup with
api.facebook_writer.routers:facebook_router
- Add 'linkedin' and 'facebook' profiles to PROFILE_GROUP_MAP
- Remove dead imports of linkedin_router, linkedin_image_router,
and facebook_router from app.py (router manager handles via
CORE_ROUTER_REGISTRY)
- Add LINKEDIN and FACEBOOK keys to frontend FEATURE_KEYS
- Add route priorities for /linkedin-writer and /facebook-writer
- Change route gates from feature='social' to feature='linkedin'
and feature='facebook' respectively
- Register 'backlinking' FeatureGroup in feature_registry.py with
routers=routers.backlink_outreach:router
- Add 'backlinking' profile to PROFILE_GROUP_MAP (core + backlinking)
- Add backlink_outreach to OPTIONAL_ROUTER_REGISTRY with
features={'all', 'backlinking'}
- Remove direct import/include of backlink_outreach from app.py
(router manager handles both 'all' and 'backlinking' modes)
- Add BACKLINKING key to FEATURE_KEYS and route priority in
frontend demoMode.ts
- Change frontend route gate from feature='seo' to feature='backlinking'
so ALWRITY_ENABLED_FEATURES=backlinking enables the route
#3 — Duplicate prospect handling: add_lead now checks (campaign_id, url)
before insert; bulk_add_leads skips existing URLs.
#8 — Atomic rate limiting: try_increment_* methods atomically check cap
and increment in a single session; router uses these before send.
#10 — Reply matching via Message-ID: sender generates Message-ID header,
stored on OutreachAttempt; reply monitor parses In-Reply-To/References;
poll_replies matches by message_id first, falls back to from_email.
#11 — Save-to-campaign uses existing store results instead of
re-running expensive deepDiscover.
#12 — Lead status Literal type: Pydantic models enforce valid status
values; backend validates via LEAD_VALID_STATUSES frozenset;
frontend API typed as LeadStatus union.
- Add BacklinkOutreachScraper (Exa + DuckDuckGo deep scraping)
- Extend DB and Pydantic models for lead enrichment columns
- Add StorageService methods for lead CRUD with auto-migration
- Add backend endpoints: deep discover, campaign detail, lead management
- Extend frontend API client and store with discovery + lead actions
- Create BacklinkOutreachDashboard component with campaigns/discover/leads tabs
- Register route at /backlink-outreach under SEO feature flag
- Add nav entry under Enterprise & Advanced in tool categories