Add v0.2 pillars: chaos inbox, identity center, command palette

This commit is contained in:
Alexa Amundson
2025-11-16 18:12:33 -06:00
parent d8c5b073be
commit 785f5f6dd3
30 changed files with 1425 additions and 40 deletions

View File

@@ -13,7 +13,8 @@ from app.redis_client import close_redis
from app.routers import (
auth, email, social, video, files, blockchain, ai_chat, devices, miner,
digitalocean, github, huggingface, vscode, games, browser, dashboard,
railway, vercel, stripe, twilio, slack, discord, sentry, api_health, agents
railway, vercel, stripe, twilio, slack, discord, sentry, api_health, agents,
capture, identity_center, notifications_center, creator, compliance_ops, search
)
from app.services.crypto import rotate_plaintext_wallet_keys
@@ -138,6 +139,12 @@ app.include_router(twilio.router)
app.include_router(slack.router)
app.include_router(discord.router)
app.include_router(sentry.router)
app.include_router(capture.router)
app.include_router(identity_center.router)
app.include_router(notifications_center.router)
app.include_router(creator.router)
app.include_router(compliance_ops.router)
app.include_router(search.router)
# API health monitoring
app.include_router(api_health.router)

View File

@@ -6,9 +6,12 @@ from app.models.video import Video, VideoView, VideoLike
from app.models.file import File, Folder
from app.models.device import Device, DeviceMetric, DeviceLog
from app.models.blockchain import Block, Transaction, Wallet
from app.models.device import Device
from app.models.ai_chat import Conversation, Message
from app.models.device import Device, DeviceMetric, DeviceLog
from app.models.capture import CaptureItem, CaptureCluster
from app.models.identity_profile import UserProfile
from app.models.notification import Notification
from app.models.creator import CreativeProject
from app.models.compliance_event import ComplianceEvent
__all__ = [
"User",
@@ -29,10 +32,12 @@ __all__ = [
"Block",
"Transaction",
"Wallet",
"Device",
"Conversation",
"Message",
"Device",
"DeviceMetric",
"DeviceLog",
"CaptureItem",
"CaptureCluster",
"UserProfile",
"Notification",
"CreativeProject",
"ComplianceEvent",
]

View File

@@ -0,0 +1,39 @@
"""Capture and clustering models for chaotic inputs"""
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.database import Base
class CaptureItem(Base):
"""Generic capture item that can represent notes, links, screenshots, etc."""
__tablename__ = "capture_items"
id = Column(Integer, primary_key=True, index=True)
type = Column(String(50), default="note", index=True)
raw_content = Column(Text)
source = Column(String(100), default="manual", index=True)
tags = Column(JSON, default=list)
related_to = Column(JSON, default=list)
status = Column(String(50), default="inbox", index=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
def __repr__(self):
return f"<CaptureItem {self.id}:{self.type}>"
class CaptureCluster(Base):
"""Lightweight clustering container for captured items."""
__tablename__ = "capture_clusters"
id = Column(Integer, primary_key=True, index=True)
name = Column(String(255), nullable=False)
description = Column(Text)
item_ids = Column(JSON, default=list)
last_refreshed_at = Column(DateTime(timezone=True), server_default=func.now())
def __repr__(self):
return f"<CaptureCluster {self.name}>"

View File

@@ -0,0 +1,21 @@
"""Lightweight compliance and ops events"""
from sqlalchemy import Column, Integer, String, DateTime, JSON
from sqlalchemy.sql import func
from app.database import Base
class ComplianceEvent(Base):
"""Event log for compliance and ops visibility."""
__tablename__ = "compliance_events"
id = Column(Integer, primary_key=True, index=True)
actor = Column(String(255))
action = Column(String(255))
resource = Column(String(255))
metadata = Column(JSON, default=dict)
severity = Column(String(50), default="info")
timestamp = Column(DateTime(timezone=True), server_default=func.now())
def __repr__(self):
return f"<ComplianceEvent {self.action} by {self.actor}>"

View File

@@ -0,0 +1,24 @@
"""Creator workspace models"""
from sqlalchemy import Column, Integer, String, Text, JSON, DateTime
from sqlalchemy.sql import func
from app.database import Base
class CreativeProject(Base):
"""Creative project container for creators"""
__tablename__ = "creative_projects"
id = Column(Integer, primary_key=True, index=True)
title = Column(String(255), nullable=False)
type = Column(String(100), default="mixed")
description = Column(Text)
links_to_assets = Column(JSON, default=list)
status = Column(String(50), default="idea")
revenue_streams = Column(JSON, default=dict)
notes = Column(Text)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
def __repr__(self):
return f"<CreativeProject {self.title}>"

View File

@@ -0,0 +1,33 @@
"""Identity profile model centralizing user fields"""
from sqlalchemy import Column, Integer, String, ForeignKey, JSON
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from sqlalchemy import DateTime
from app.database import Base
class UserProfile(Base):
"""Canonical user profile stored once per account."""
__tablename__ = "user_profiles"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(Integer, ForeignKey("users.id"), unique=True)
name = Column(String(255))
legal_name = Column(String(255))
email = Column(String(255))
secondary_emails = Column(JSON, default=list)
phone = Column(String(50))
secondary_phones = Column(JSON, default=list)
address = Column(String(500))
timezone = Column(String(100))
pronouns = Column(String(100))
avatar_url = Column(String(500))
external_ids = Column(JSON, default=dict)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
user = relationship("User", backref="profile", uselist=False)
def __repr__(self):
return f"<UserProfile {self.user_id}>"

View File

@@ -0,0 +1,23 @@
"""Notification model for OS-level alerts"""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.sql import func
from app.database import Base
class Notification(Base):
"""Stores notifications across apps"""
__tablename__ = "notifications"
id = Column(Integer, primary_key=True, index=True)
type = Column(String(50), default="info")
source_app_id = Column(String(100))
title = Column(String(255))
body = Column(String(1000))
importance = Column(String(50), default="normal")
delivery_mode = Column(String(50), default="immediate")
read_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), server_default=func.now())
def __repr__(self):
return f"<Notification {self.id}:{self.title}>"

View 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()

View 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()

View 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

View 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}

View 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

View 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