feat: add backlinking feature flag following blog_writer pattern
- 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
This commit is contained in:
@@ -58,6 +58,10 @@ FEATURE_GROUPS: Dict[str, FeatureGroup] = {
|
|||||||
"api.blog_writer.seo_analysis:router",
|
"api.blog_writer.seo_analysis:router",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
"backlinking": FeatureGroup(
|
||||||
|
features=("backlinking",),
|
||||||
|
routers=("routers.backlink_outreach:router",),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -67,5 +71,6 @@ PROFILE_GROUP_MAP: Dict[str, Tuple[str, ...]] = {
|
|||||||
"podcast": ("core", "podcast"),
|
"podcast": ("core", "podcast"),
|
||||||
"youtube": ("core", "youtube"),
|
"youtube": ("core", "youtube"),
|
||||||
"blog_writer": ("core", "blog_writer"),
|
"blog_writer": ("core", "blog_writer"),
|
||||||
|
"backlinking": ("core", "backlinking"),
|
||||||
"planning": ("core", "content_planning"),
|
"planning": ("core", "content_planning"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ OPTIONAL_ROUTER_REGISTRY = [
|
|||||||
{"name": "oauth_token_monitoring", "module": "api.oauth_token_monitoring_routes", "attr": "router", "features": {"all", "core"}},
|
{"name": "oauth_token_monitoring", "module": "api.oauth_token_monitoring_routes", "attr": "router", "features": {"all", "core"}},
|
||||||
{"name": "agents", "module": "api.agents_api", "attr": "router", "features": {"all"}},
|
{"name": "agents", "module": "api.agents_api", "attr": "router", "features": {"all"}},
|
||||||
{"name": "today_workflow", "module": "api.today_workflow", "attr": "router", "features": {"all"}},
|
{"name": "today_workflow", "module": "api.today_workflow", "attr": "router", "features": {"all"}},
|
||||||
|
{"name": "backlink_outreach", "module": "routers.backlink_outreach", "attr": "router", "features": {"all", "backlinking"}},
|
||||||
]
|
]
|
||||||
|
|
||||||
OPTIONAL_MODULE_MATRIX = {
|
OPTIONAL_MODULE_MATRIX = {
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ if _is_full_mode():
|
|||||||
from routers.image_studio import router as image_studio_router
|
from routers.image_studio import router as image_studio_router
|
||||||
from routers.product_marketing import router as product_marketing_router
|
from routers.product_marketing import router as product_marketing_router
|
||||||
from routers.campaign_creator import router as campaign_creator_router
|
from routers.campaign_creator import router as campaign_creator_router
|
||||||
from routers.backlink_outreach import router as backlink_outreach_router
|
|
||||||
else:
|
else:
|
||||||
# In feature-only modes, only load essential assets router
|
# In feature-only modes, only load essential assets router
|
||||||
from api.assets_serving import router as assets_serving_router
|
from api.assets_serving import router as assets_serving_router
|
||||||
@@ -147,7 +146,6 @@ else:
|
|||||||
image_studio_router = None
|
image_studio_router = None
|
||||||
product_marketing_router = None
|
product_marketing_router = None
|
||||||
campaign_creator_router = None
|
campaign_creator_router = None
|
||||||
backlink_outreach_router = None
|
|
||||||
|
|
||||||
# Import hallucination detector router
|
# Import hallucination detector router
|
||||||
try:
|
try:
|
||||||
@@ -683,8 +681,6 @@ if _is_full_mode():
|
|||||||
app.include_router(product_marketing_router)
|
app.include_router(product_marketing_router)
|
||||||
if campaign_creator_router:
|
if campaign_creator_router:
|
||||||
app.include_router(campaign_creator_router)
|
app.include_router(campaign_creator_router)
|
||||||
if backlink_outreach_router:
|
|
||||||
app.include_router(backlink_outreach_router)
|
|
||||||
|
|
||||||
router_group_status["platform_extensions"] = {
|
router_group_status["platform_extensions"] = {
|
||||||
"mounted": True,
|
"mounted": True,
|
||||||
@@ -799,6 +795,24 @@ async def startup_event():
|
|||||||
else:
|
else:
|
||||||
logger.info(f"[FEATURE-MODE] Skipping scheduler startup (features: {enabled_features})")
|
logger.info(f"[FEATURE-MODE] Skipping scheduler startup (features: {enabled_features})")
|
||||||
|
|
||||||
|
# Recover stale YouTube tasks on startup
|
||||||
|
if _is_feature_enabled("youtube"):
|
||||||
|
try:
|
||||||
|
from api.youtube.task_manager import task_manager
|
||||||
|
from services.database import get_all_user_ids
|
||||||
|
user_ids = get_all_user_ids()
|
||||||
|
recovered = 0
|
||||||
|
for uid in user_ids:
|
||||||
|
try:
|
||||||
|
count = task_manager.recover_stale_tasks(uid)
|
||||||
|
recovered += count
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if recovered > 0:
|
||||||
|
logger.info(f"[STARTUP] Recovered {recovered} stale YouTube tasks across {len(user_ids)} users")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"[STARTUP] YouTube task recovery skipped: {e}")
|
||||||
|
|
||||||
# Check Wix configuration (OAuth-based, API key optional)
|
# Check Wix configuration (OAuth-based, API key optional)
|
||||||
wix_api_key = os.getenv('WIX_API_KEY')
|
wix_api_key = os.getenv('WIX_API_KEY')
|
||||||
if wix_api_key:
|
if wix_api_key:
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ const App: React.FC = () => {
|
|||||||
<Route path="/dashboard" element={<ProtectedRoute><MainDashboard /></ProtectedRoute>} />
|
<Route path="/dashboard" element={<ProtectedRoute><MainDashboard /></ProtectedRoute>} />
|
||||||
<Route path="/seo" element={<ProtectedRoute><FeatureRoute feature="seo"><SEODashboard /></FeatureRoute></ProtectedRoute>} />
|
<Route path="/seo" element={<ProtectedRoute><FeatureRoute feature="seo"><SEODashboard /></FeatureRoute></ProtectedRoute>} />
|
||||||
<Route path="/seo-dashboard" element={<ProtectedRoute><FeatureRoute feature="seo"><SEODashboard /></FeatureRoute></ProtectedRoute>} />
|
<Route path="/seo-dashboard" element={<ProtectedRoute><FeatureRoute feature="seo"><SEODashboard /></FeatureRoute></ProtectedRoute>} />
|
||||||
<Route path="/backlink-outreach" element={<ProtectedRoute><FeatureRoute feature="seo"><BacklinkOutreachDashboard /></FeatureRoute></ProtectedRoute>} />
|
<Route path="/backlink-outreach" element={<ProtectedRoute><FeatureRoute feature="backlinking"><BacklinkOutreachDashboard /></FeatureRoute></ProtectedRoute>} />
|
||||||
<Route path="/content-planning" element={<ProtectedRoute><FeatureRoute feature="content-planning"><ContentPlanningDashboard /></FeatureRoute></ProtectedRoute>} />
|
<Route path="/content-planning" element={<ProtectedRoute><FeatureRoute feature="content-planning"><ContentPlanningDashboard /></FeatureRoute></ProtectedRoute>} />
|
||||||
<Route path="/facebook-writer" element={<ProtectedRoute><FeatureRoute feature="social"><FacebookWriter /></FeatureRoute></ProtectedRoute>} />
|
<Route path="/facebook-writer" element={<ProtectedRoute><FeatureRoute feature="social"><FacebookWriter /></FeatureRoute></ProtectedRoute>} />
|
||||||
<Route path="/linkedin-writer" element={<ProtectedRoute><FeatureRoute feature="social"><LinkedInWriter /></FeatureRoute></ProtectedRoute>} />
|
<Route path="/linkedin-writer" element={<ProtectedRoute><FeatureRoute feature="social"><LinkedInWriter /></FeatureRoute></ProtectedRoute>} />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const FEATURE_KEYS = {
|
|||||||
WIX: 'wix',
|
WIX: 'wix',
|
||||||
BING: 'bing',
|
BING: 'bing',
|
||||||
ASSET_LIBRARY: 'asset-library',
|
ASSET_LIBRARY: 'asset-library',
|
||||||
|
BACKLINKING: 'backlinking',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FeatureKey = typeof FEATURE_KEYS[keyof typeof FEATURE_KEYS];
|
export type FeatureKey = typeof FEATURE_KEYS[keyof typeof FEATURE_KEYS];
|
||||||
@@ -124,6 +125,7 @@ export function getSingleFeature(): string | null {
|
|||||||
const FEATURE_ROUTE_PRIORITY: [string, string][] = [
|
const FEATURE_ROUTE_PRIORITY: [string, string][] = [
|
||||||
['podcast', '/podcast-maker'],
|
['podcast', '/podcast-maker'],
|
||||||
['blog_writer', '/blog-writer'],
|
['blog_writer', '/blog-writer'],
|
||||||
|
['backlinking', '/backlink-outreach'],
|
||||||
['story', '/story-writer'],
|
['story', '/story-writer'],
|
||||||
['image', '/image-studio'],
|
['image', '/image-studio'],
|
||||||
['video', '/video-studio'],
|
['video', '/video-studio'],
|
||||||
|
|||||||
Reference in New Issue
Block a user