mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-17 09:37:55 -05:00
Add v0.2 pillars: chaos inbox, identity center, command palette
This commit is contained in:
150
backend/app/routers/capture.py
Normal file
150
backend/app/routers/capture.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""Routes for Chaos Inbox capture items and clustering"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.capture import CaptureItem, CaptureCluster
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/capture", tags=["Chaos Inbox"])
|
||||
|
||||
|
||||
class CaptureItemCreate(BaseModel):
|
||||
type: str = "note"
|
||||
raw_content: Optional[str] = None
|
||||
source: Optional[str] = "manual"
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
related_to: List[str] = Field(default_factory=list)
|
||||
status: str = "inbox"
|
||||
|
||||
|
||||
class CaptureItemResponse(BaseModel):
|
||||
id: int
|
||||
type: str
|
||||
raw_content: Optional[str]
|
||||
source: Optional[str]
|
||||
tags: List[str]
|
||||
related_to: List[str]
|
||||
status: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TagUpdate(BaseModel):
|
||||
tags: List[str]
|
||||
|
||||
|
||||
class StatusUpdate(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
class ClusterCreate(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
item_ids: List[int] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ClusterResponse(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: Optional[str]
|
||||
item_ids: List[int]
|
||||
last_refreshed_at: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.post("/items", response_model=CaptureItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_capture_item(
|
||||
item: CaptureItemCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""Create a new capture item."""
|
||||
capture_item = CaptureItem(**item.model_dump())
|
||||
db.add(capture_item)
|
||||
await db.commit()
|
||||
await db.refresh(capture_item)
|
||||
return capture_item
|
||||
|
||||
|
||||
@router.get("/items", response_model=List[CaptureItemResponse])
|
||||
async def list_capture_items(
|
||||
status_filter: Optional[str] = None,
|
||||
type_filter: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""List capture items with optional filters."""
|
||||
query = select(CaptureItem)
|
||||
if status_filter:
|
||||
query = query.where(CaptureItem.status == status_filter)
|
||||
if type_filter:
|
||||
query = query.where(CaptureItem.type == type_filter)
|
||||
|
||||
result = await db.execute(query.order_by(CaptureItem.created_at.desc()))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/items/{item_id}/tag", response_model=CaptureItemResponse)
|
||||
async def update_tags(
|
||||
item_id: int,
|
||||
payload: TagUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CaptureItem).where(CaptureItem.id == item_id))
|
||||
item = result.scalar_one_or_none()
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
|
||||
|
||||
item.tags = payload.tags
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/items/{item_id}/status", response_model=CaptureItemResponse)
|
||||
async def update_status(
|
||||
item_id: int,
|
||||
payload: StatusUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CaptureItem).where(CaptureItem.id == item_id))
|
||||
item = result.scalar_one_or_none()
|
||||
if not item:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Item not found")
|
||||
|
||||
item.status = payload.status
|
||||
await db.commit()
|
||||
await db.refresh(item)
|
||||
return item
|
||||
|
||||
|
||||
@router.post("/clusters", response_model=ClusterResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_cluster(
|
||||
payload: ClusterCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
cluster = CaptureCluster(**payload.model_dump())
|
||||
db.add(cluster)
|
||||
await db.commit()
|
||||
await db.refresh(cluster)
|
||||
return cluster
|
||||
|
||||
|
||||
@router.get("/clusters", response_model=List[ClusterResponse])
|
||||
async def list_clusters(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CaptureCluster).order_by(CaptureCluster.last_refreshed_at.desc()))
|
||||
return result.scalars().all()
|
||||
35
backend/app/routers/compliance_ops.py
Normal file
35
backend/app/routers/compliance_ops.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Compliance and operations visibility routes"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.compliance_event import ComplianceEvent
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/compliance", tags=["Compliance"])
|
||||
|
||||
|
||||
class ComplianceEventResponse(BaseModel):
|
||||
id: int
|
||||
actor: str
|
||||
action: str
|
||||
resource: str
|
||||
severity: str
|
||||
metadata: dict | None = None
|
||||
timestamp: str | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/events", response_model=List[ComplianceEventResponse])
|
||||
async def list_events(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(ComplianceEvent).order_by(ComplianceEvent.timestamp.desc()))
|
||||
return result.scalars().all()
|
||||
85
backend/app/routers/creator.py
Normal file
85
backend/app/routers/creator.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Creator Studio routes"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.creator import CreativeProject
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/creator", tags=["Creator"])
|
||||
|
||||
|
||||
class CreativeProjectPayload(BaseModel):
|
||||
title: str
|
||||
type: str = "mixed"
|
||||
description: Optional[str] = None
|
||||
links_to_assets: List[str] = Field(default_factory=list)
|
||||
status: str = "idea"
|
||||
revenue_streams: dict = Field(default_factory=dict)
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class CreativeProjectResponse(CreativeProjectPayload):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.get("/projects", response_model=List[CreativeProjectResponse])
|
||||
async def list_projects(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CreativeProject).order_by(CreativeProject.created_at.desc()))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/projects", response_model=CreativeProjectResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_project(
|
||||
payload: CreativeProjectPayload,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
project = CreativeProject(**payload.model_dump())
|
||||
db.add(project)
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
return project
|
||||
|
||||
|
||||
@router.get("/projects/{project_id}", response_model=CreativeProjectResponse)
|
||||
async def get_project(
|
||||
project_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CreativeProject).where(CreativeProject.id == project_id))
|
||||
project = result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
return project
|
||||
|
||||
|
||||
@router.put("/projects/{project_id}", response_model=CreativeProjectResponse)
|
||||
async def update_project(
|
||||
project_id: int,
|
||||
payload: CreativeProjectPayload,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(CreativeProject).where(CreativeProject.id == project_id))
|
||||
project = result.scalar_one_or_none()
|
||||
if not project:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Project not found")
|
||||
|
||||
for field, value in payload.model_dump().items():
|
||||
setattr(project, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(project)
|
||||
return project
|
||||
123
backend/app/routers/identity_center.py
Normal file
123
backend/app/routers/identity_center.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Identity Center routes"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.user import User
|
||||
from app.models.identity_profile import UserProfile
|
||||
|
||||
router = APIRouter(prefix="/api/identity", tags=["Identity"])
|
||||
|
||||
|
||||
class UserProfilePayload(BaseModel):
|
||||
name: Optional[str] = None
|
||||
legal_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
secondary_emails: List[str] = Field(default_factory=list)
|
||||
phone: Optional[str] = None
|
||||
secondary_phones: List[str] = Field(default_factory=list)
|
||||
address: Optional[str] = None
|
||||
timezone: Optional[str] = None
|
||||
pronouns: Optional[str] = None
|
||||
avatar_url: Optional[str] = None
|
||||
external_ids: Dict[str, str] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class UserProfileResponse(UserProfilePayload):
|
||||
completeness: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
def calculate_completeness(profile: UserProfile) -> int:
|
||||
filled = [
|
||||
profile.name,
|
||||
profile.email,
|
||||
profile.phone,
|
||||
profile.address,
|
||||
profile.timezone,
|
||||
profile.pronouns,
|
||||
]
|
||||
score = int((len([f for f in filled if f]) / len(filled)) * 100)
|
||||
return min(score + (10 if profile.avatar_url else 0), 100)
|
||||
|
||||
|
||||
@router.get("/profile", response_model=UserProfileResponse)
|
||||
async def get_profile(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id, email=current_user.email, name=current_user.full_name)
|
||||
db.add(profile)
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
|
||||
response = UserProfileResponse.model_validate(profile)
|
||||
response.completeness = calculate_completeness(profile)
|
||||
return response
|
||||
|
||||
|
||||
@router.put("/profile", response_model=UserProfileResponse)
|
||||
async def update_profile(
|
||||
payload: UserProfilePayload,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id)
|
||||
db.add(profile)
|
||||
|
||||
for field, value in payload.model_dump().items():
|
||||
setattr(profile, field, value)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
|
||||
response = UserProfileResponse.model_validate(profile)
|
||||
response.completeness = calculate_completeness(profile)
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/linked", response_model=dict)
|
||||
async def list_linked_accounts(
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
# Placeholder for linked services registry
|
||||
return {
|
||||
"github": bool(current_user.wallet_address),
|
||||
"wallet": bool(current_user.wallet_address),
|
||||
"discord": False,
|
||||
"railway": False,
|
||||
}
|
||||
|
||||
|
||||
@router.post("/link_external", response_model=dict)
|
||||
async def link_external(
|
||||
provider: str,
|
||||
external_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = result.scalar_one_or_none()
|
||||
if not profile:
|
||||
profile = UserProfile(user_id=current_user.id)
|
||||
db.add(profile)
|
||||
|
||||
external = profile.external_ids or {}
|
||||
external[provider] = external_id
|
||||
profile.external_ids = external
|
||||
await db.commit()
|
||||
await db.refresh(profile)
|
||||
|
||||
return {"provider": provider, "external_id": external_id}
|
||||
81
backend/app/routers/notifications_center.py
Normal file
81
backend/app/routers/notifications_center.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Notification center routes"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.notification import Notification
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/notifications", tags=["Notifications"])
|
||||
|
||||
|
||||
class NotificationCreate(BaseModel):
|
||||
type: str = "info"
|
||||
source_app_id: Optional[str] = None
|
||||
title: str
|
||||
body: str
|
||||
importance: str = "normal"
|
||||
delivery_mode: str = "immediate"
|
||||
|
||||
|
||||
class NotificationResponse(BaseModel):
|
||||
id: int
|
||||
type: str
|
||||
source_app_id: Optional[str]
|
||||
title: str
|
||||
body: str
|
||||
importance: str
|
||||
delivery_mode: str
|
||||
read_at: Optional[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
@router.post("", response_model=NotificationResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_notification(
|
||||
payload: NotificationCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
notification = Notification(**payload.model_dump())
|
||||
db.add(notification)
|
||||
await db.commit()
|
||||
await db.refresh(notification)
|
||||
return notification
|
||||
|
||||
|
||||
@router.get("", response_model=List[NotificationResponse])
|
||||
async def list_notifications(
|
||||
importance: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
query = select(Notification)
|
||||
if importance:
|
||||
query = query.where(Notification.importance == importance)
|
||||
result = await db.execute(query.order_by(Notification.created_at.desc()))
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
@router.post("/{notification_id}/read", response_model=NotificationResponse)
|
||||
async def mark_read(
|
||||
notification_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
result = await db.execute(select(Notification).where(Notification.id == notification_id))
|
||||
notification = result.scalar_one_or_none()
|
||||
if not notification:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notification not found")
|
||||
|
||||
if not notification.read_at:
|
||||
notification.read_at = datetime.utcnow()
|
||||
await db.commit()
|
||||
await db.refresh(notification)
|
||||
return notification
|
||||
88
backend/app/routers/search.py
Normal file
88
backend/app/routers/search.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Unified search endpoints"""
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.auth import get_current_active_user
|
||||
from app.database import get_db
|
||||
from app.models.capture import CaptureItem
|
||||
from app.models.identity_profile import UserProfile
|
||||
from app.models.creator import CreativeProject
|
||||
from app.models.user import User
|
||||
|
||||
router = APIRouter(prefix="/api/search", tags=["Search"])
|
||||
|
||||
|
||||
class SearchResult(BaseModel):
|
||||
app_id: str
|
||||
type: str
|
||||
title: str
|
||||
snippet: str | None = None
|
||||
ref_id: str | None = None
|
||||
|
||||
|
||||
@router.get("", response_model=List[SearchResult])
|
||||
async def unified_search(
|
||||
q: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
results: List[SearchResult] = []
|
||||
|
||||
# Capture items search
|
||||
capture_result = await db.execute(
|
||||
select(CaptureItem).where(CaptureItem.raw_content.ilike(f"%{q}%"))
|
||||
)
|
||||
for item in capture_result.scalars().all():
|
||||
results.append(
|
||||
SearchResult(
|
||||
app_id="chaos-inbox",
|
||||
type=item.type,
|
||||
title=item.raw_content[:60] if item.raw_content else item.type,
|
||||
snippet=item.status,
|
||||
ref_id=str(item.id)
|
||||
)
|
||||
)
|
||||
|
||||
# Creative projects
|
||||
project_result = await db.execute(
|
||||
select(CreativeProject).where(CreativeProject.title.ilike(f"%{q}%"))
|
||||
)
|
||||
for project in project_result.scalars().all():
|
||||
results.append(
|
||||
SearchResult(
|
||||
app_id="creator-studio",
|
||||
type=project.type,
|
||||
title=project.title,
|
||||
snippet=project.description[:80] if project.description else None,
|
||||
ref_id=str(project.id)
|
||||
)
|
||||
)
|
||||
|
||||
# Identity profile
|
||||
profile_result = await db.execute(select(UserProfile).where(UserProfile.user_id == current_user.id))
|
||||
profile = profile_result.scalar_one_or_none()
|
||||
if profile and q.lower() in (profile.name or "").lower():
|
||||
results.append(
|
||||
SearchResult(
|
||||
app_id="identity-center",
|
||||
type="profile",
|
||||
title=profile.name or "Profile",
|
||||
snippet=profile.email,
|
||||
ref_id=str(profile.id)
|
||||
)
|
||||
)
|
||||
|
||||
# App discovery (front-end uses registry; include sample results)
|
||||
if "identity" in q.lower():
|
||||
results.append(SearchResult(app_id="identity-center", type="app", title="Identity Center"))
|
||||
if "chaos" in q.lower() or "note" in q.lower():
|
||||
results.append(SearchResult(app_id="chaos-inbox", type="app", title="Chaos Inbox"))
|
||||
if "creator" in q.lower():
|
||||
results.append(SearchResult(app_id="creator-studio", type="app", title="Creator Studio"))
|
||||
if "compliance" in q.lower():
|
||||
results.append(SearchResult(app_id="compliance-ops", type="app", title="Compliance & Ops"))
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user