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

@@ -12,6 +12,15 @@ A comprehensive FastAPI backend for the BlackRoad Operating System, a Windows 95
- **File Storage** - File explorer with folder management and sharing - **File Storage** - File explorer with folder management and sharing
- **RoadCoin Blockchain** - Cryptocurrency with mining, transactions, and wallet management - **RoadCoin Blockchain** - Cryptocurrency with mining, transactions, and wallet management
- **AI Chat** - Conversational AI assistant with conversation history - **AI Chat** - Conversational AI assistant with conversation history
- **Chaos Inbox / Identity / Notifications / Creator / Compliance** - New v0.2 APIs for capture, profiles, alerts, creative projects, and audit visibility
### New v0.2 endpoints
- `/api/capture/*` — capture items, clustering, status and tagging
- `/api/identity/profile` — canonical user profile for OS apps
- `/api/notifications` — create/list/mark notifications
- `/api/creator/projects` — manage creator projects and assets
- `/api/compliance/events` — surface audit events
- `/api/search?q=` — unified search scaffold
### Technology Stack ### Technology Stack
- **FastAPI** - Modern, fast Python web framework - **FastAPI** - Modern, fast Python web framework

View File

@@ -13,7 +13,8 @@ from app.redis_client import close_redis
from app.routers import ( from app.routers import (
auth, email, social, video, files, blockchain, ai_chat, devices, miner, auth, email, social, video, files, blockchain, ai_chat, devices, miner,
digitalocean, github, huggingface, vscode, games, browser, dashboard, 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 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(slack.router)
app.include_router(discord.router) app.include_router(discord.router)
app.include_router(sentry.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 # API health monitoring
app.include_router(api_health.router) 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.file import File, Folder
from app.models.device import Device, DeviceMetric, DeviceLog from app.models.device import Device, DeviceMetric, DeviceLog
from app.models.blockchain import Block, Transaction, Wallet 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.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__ = [ __all__ = [
"User", "User",
@@ -29,10 +32,12 @@ __all__ = [
"Block", "Block",
"Transaction", "Transaction",
"Wallet", "Wallet",
"Device",
"Conversation", "Conversation",
"Message", "Message",
"Device", "CaptureItem",
"DeviceMetric", "CaptureCluster",
"DeviceLog", "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

View File

@@ -1,4 +1,4 @@
# BlackRoad OS v0.1.1 # BlackRoad OS v0.2
**The Living Portal** — A complete front-end operating system for the BlackRoad ecosystem. **The Living Portal** — A complete front-end operating system for the BlackRoad ecosystem.
@@ -12,12 +12,14 @@
BlackRoad OS is a **production-ready**, fully-accessible desktop operating system built entirely with vanilla JavaScript, HTML, and CSS. No frameworks, no build tools, no dependencies - just clean, maintainable code. BlackRoad OS is a **production-ready**, fully-accessible desktop operating system built entirely with vanilla JavaScript, HTML, and CSS. No frameworks, no build tools, no dependencies - just clean, maintainable code.
**New in v0.1.1:** **New in v0.2:**
- **Accessibility-first** - Full keyboard navigation, ARIA attributes throughout - 🌀 **Chaos Inbox** for neurodivergent-friendly capture and clustering
- 🎯 **Lifecycle hooks** - Apps can listen to window events - 🪪 **Identity Center** to kill duplication across apps
- 🔧 **Config layer** - Feature flags and API endpoint management - 🔔 **Notification Center focus modes** to tame alert noise
- 📚 **Component library** - 15 polished, accessible UI primitives - 🎨 **Creator Studio** baseline workspace for creators
- 📖 **Comprehensive docs** - ARCHITECTURE.md + EXTENDING.md guides - 🧭 **Compliance & Ops** surface for audits/workflows
- ⌨️ **Global command palette** (Ctrl/Cmd + K) unified search
- 🎨 **High contrast theme** added to theme cycle
It provides a complete enterprise portal for managing all BlackRoad operations including: It provides a complete enterprise portal for managing all BlackRoad operations including:

View File

@@ -515,3 +515,24 @@
.tab-content { .tab-content {
padding: 20px 0; padding: 20px 0;
} }
/* Chaos Inbox */
.chaos-inbox .two-column-layout { display: grid; grid-template-columns: 2fr 1fr; gap: 12px; }
.chaos-meta { display: flex; justify-content: space-between; color: var(--text-secondary); font-size: 12px; }
.chaos-body { margin: 8px 0; color: var(--text-primary); }
.chaos-tags { color: var(--text-secondary); font-size: 12px; }
.chaos-filters { margin-bottom: 12px; }
.chaos-clusters .pill { margin-right: 6px; display: inline-block; padding: 4px 6px; }
/* Identity Center */
.identity-center .identity-row { display: grid; grid-template-columns: 1fr 2fr; padding: 6px 0; }
.identity-center .label { color: var(--text-secondary); }
.identity-howto { padding-left: 18px; line-height: 1.5; }
/* Creator Studio */
.creator-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.creator-body p { margin: 0 0 8px 0; }
.creator-assets ul { padding-left: 18px; }
/* Compliance & Ops */
.compliance-ops { display: flex; flex-direction: column; gap: 12px; }

View File

@@ -37,6 +37,21 @@ body[data-theme="nightOS"] {
--taskbar-bg: rgba(10, 0, 20, 0.98); --taskbar-bg: rgba(10, 0, 20, 0.98);
} }
body[data-theme="contrastOS"] {
--primary: #FFD700;
--primary-dark: #C0A000;
--primary-light: #FFE85A;
--bg-desktop: #000;
--bg-surface: #111;
--bg-surface-hover: #191919;
--bg-window: #0c0c0c;
--text-primary: #fff;
--text-secondary: #e0e0e0;
--text-dim: #ccc;
--border-color: #FFD700;
--taskbar-bg: #000;
}
/* Desktop */ /* Desktop */
.desktop { .desktop {
width: 100%; width: 100%;
@@ -415,6 +430,37 @@ body[data-theme="nightOS"] {
border-left: 3px solid var(--info); border-left: 3px solid var(--info);
} }
/* Command palette */
.command-palette {
position: fixed;
top: 20%;
left: 50%;
transform: translateX(-50%);
width: 600px;
background: var(--bg-window);
border: 1px solid var(--border-color);
box-shadow: 0 8px 32px var(--shadow);
padding: 12px;
border-radius: 12px;
display: none;
z-index: 11000;
}
.command-palette.open { display: block; }
.command-palette input {
width: 100%;
padding: 10px;
margin-bottom: 10px;
background: var(--bg-surface);
border: 1px solid var(--border-color);
color: var(--text-primary);
}
.command-results { max-height: 320px; overflow: auto; display: flex; flex-direction: column; gap: 8px; }
.command-group-title { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.command-row { padding: 8px; border-radius: 8px; cursor: pointer; background: var(--bg-surface); }
.command-row:hover, .command-row:focus { background: var(--bg-surface-hover); outline: 1px solid var(--primary); }
/* Scrollbar Styling */ /* Scrollbar Styling */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;

View File

@@ -91,10 +91,14 @@
<script src="js/apps/compliance.js"></script> <script src="js/apps/compliance.js"></script>
<script src="js/apps/finance.js"></script> <script src="js/apps/finance.js"></script>
<script src="js/apps/identity.js"></script> <script src="js/apps/identity.js"></script>
<script src="js/apps/identity_center.js"></script>
<script src="js/apps/research.js"></script> <script src="js/apps/research.js"></script>
<script src="js/apps/engineering.js"></script> <script src="js/apps/engineering.js"></script>
<script src="js/apps/settings.js"></script> <script src="js/apps/settings.js"></script>
<script src="js/apps/notifications.js"></script> <script src="js/apps/notifications.js"></script>
<script src="js/apps/chaos_inbox.js"></script>
<script src="js/apps/creator_studio.js"></script>
<script src="js/apps/compliance_ops.js"></script>
<script src="js/apps/corporate.js"></script> <script src="js/apps/corporate.js"></script>
<!-- Registry and Bootloader (load last) --> <!-- Registry and Bootloader (load last) -->

View File

@@ -0,0 +1,163 @@
/**
* Chaos Inbox
* Neurodivergent-friendly capture surface for scraps, links, and ideas
*/
window.ChaosInboxApp = function() {
const appId = 'chaos-inbox';
const toolbar = document.createElement('div');
toolbar.className = 'window-toolbar';
const quickCaptureInput = document.createElement('input');
quickCaptureInput.type = 'text';
quickCaptureInput.placeholder = 'Quick capture a note...';
quickCaptureInput.setAttribute('aria-label', 'Quick capture note');
quickCaptureInput.className = 'input';
const captureButton = Components.Button('Save', {
onClick: () => {
if (!quickCaptureInput.value) return;
const newItem = {
id: Date.now(),
type: 'note',
raw_content: quickCaptureInput.value,
status: 'inbox',
created_at: new Date().toISOString()
};
MockData.captureItems.unshift(newItem);
quickCaptureInput.value = '';
renderContent();
window.OS.showNotification({
type: 'success',
title: 'Captured',
message: 'Added to Chaos Inbox',
duration: 1500
});
}
});
toolbar.appendChild(quickCaptureInput);
toolbar.appendChild(captureButton);
const container = document.createElement('div');
container.className = 'chaos-inbox';
function renderItemCard(item) {
const tags = item.tags?.length ? item.tags.join(', ') : 'No tags';
const statusBadge = Components.Badge(item.status, item.status === 'inbox' ? 'warning' : 'info');
const content = document.createElement('div');
content.innerHTML = `
<div class="chaos-meta">
<span>${item.type.toUpperCase()}${item.source || 'manual'}</span>
<span>${item.created_at}</span>
</div>
<div class="chaos-body">${item.raw_content || 'Empty'}</div>
<div class="chaos-tags">${tags}</div>
`;
const footer = document.createElement('div');
footer.style.display = 'flex';
footer.style.justifyContent = 'space-between';
footer.appendChild(statusBadge);
const actions = document.createElement('div');
const archiveBtn = Components.Button('Archive', {
size: 'small',
onClick: () => {
item.status = 'archived';
renderContent();
}
});
const resurfaceBtn = Components.Button('Resurface', {
size: 'small',
onClick: () => {
item.status = 'resurfaced';
renderContent();
}
});
actions.appendChild(archiveBtn);
actions.appendChild(resurfaceBtn);
footer.appendChild(actions);
return Components.Card({
title: item.raw_content?.slice(0, 40) || item.type,
subtitle: `Status: ${item.status}`,
content,
footer
});
}
function renderClusters() {
const clusterWrap = document.createElement('div');
clusterWrap.className = 'chaos-clusters';
const heading = document.createElement('h3');
heading.textContent = 'Suggested clusters';
clusterWrap.appendChild(heading);
MockData.captureClusters.forEach(cluster => {
const linked = MockData.captureItems.filter(item => cluster.item_ids.includes(item.id));
const body = document.createElement('div');
body.innerHTML = `<div class="cluster-desc">${cluster.description}</div>`;
linked.forEach(item => {
const pill = Components.Badge(item.type, 'info');
pill.textContent = `${item.type}: ${item.raw_content?.slice(0, 24)}`;
pill.classList.add('pill');
body.appendChild(pill);
});
clusterWrap.appendChild(Components.Card({ title: cluster.name, content: body }));
});
return clusterWrap;
}
function renderResurface() {
const older = MockData.captureItems.filter(item => item.status === 'inbox');
if (!older.length) return document.createElement('div');
const list = Components.List(older.map(item => ({
title: item.raw_content || item.type,
subtitle: `Captured ${item.created_at}`,
icon: '🔄'
})));
return Components.Card({ title: 'Resurface', subtitle: 'Things that need love', content: list });
}
function renderContent() {
container.innerHTML = '';
const filters = document.createElement('div');
filters.className = 'chaos-filters';
const statusFilter = document.createElement('select');
statusFilter.innerHTML = `<option value="all">All</option><option value="inbox">Inbox</option><option value="clustered">Clustered</option><option value="resurfaced">Resurfaced</option>`;
statusFilter.onchange = () => renderContent();
filters.appendChild(statusFilter);
const columns = document.createElement('div');
columns.className = 'two-column-layout';
const left = document.createElement('div');
const right = document.createElement('div');
const selectedStatus = statusFilter.value;
const items = MockData.captureItems.filter(item => selectedStatus === 'all' || item.status === selectedStatus);
if (!items.length) {
left.appendChild(Components.EmptyState({ icon: '🌀', title: 'Nothing captured yet', text: 'Quick add something above.' }));
} else {
items.forEach(item => left.appendChild(renderItemCard(item)));
}
right.appendChild(renderClusters());
right.appendChild(renderResurface());
columns.appendChild(left);
columns.appendChild(right);
container.appendChild(filters);
container.appendChild(columns);
}
renderContent();
window.OS.createWindow({
id: appId,
title: 'Chaos Inbox',
icon: '🌀',
toolbar,
content: container,
width: '1100px',
height: '720px'
});
};

View File

@@ -0,0 +1,49 @@
/**
* Compliance & Ops console
*/
window.ComplianceOpsApp = function() {
const appId = 'compliance-ops';
const container = document.createElement('div');
container.className = 'compliance-ops';
const eventsTable = Components.Table(
[
{ key: 'timestamp', label: 'When' },
{ key: 'actor', label: 'Actor' },
{ key: 'action', label: 'Action' },
{ key: 'resource', label: 'Resource' },
{ key: 'severity', label: 'Severity', render: (val) => Components.Badge(val, val === 'critical' ? 'error' : 'info') }
],
MockData.auditLogs.map(log => ({
timestamp: log.timestamp,
actor: log.user,
action: log.event,
resource: log.ip,
severity: log.result
})),
{ caption: 'Recent events' }
);
const workflows = Components.Card({
title: 'Workflows',
subtitle: 'Make compliance visible instead of hidden',
content: Components.List([
{ icon: '✅', title: 'Marketing review', subtitle: 'Queue: 5 items' },
{ icon: '🛡️', title: 'Security review', subtitle: 'Auto checks nightly' },
{ icon: '📜', title: 'Policy updates', subtitle: 'Notify teams automatically' }
])
});
container.appendChild(Components.Card({ title: 'Events', content: eventsTable }));
container.appendChild(workflows);
window.OS.createWindow({
id: appId,
title: 'Compliance & Ops',
icon: '🧭',
content: container,
width: '900px',
height: '650px'
});
};

View File

@@ -0,0 +1,60 @@
/**
* Creator Studio - minimal hub for creative projects
*/
window.CreatorStudioApp = function() {
const appId = 'creator-studio';
const container = document.createElement('div');
container.className = 'creator-studio';
const header = document.createElement('div');
header.className = 'creator-header';
header.innerHTML = '<div><h2>Creator Studio</h2><p>Track creative work without a dozen tabs.</p></div>';
const newBtn = Components.Button('New idea', {
onClick: () => {
const title = prompt('Project title');
if (!title) return;
MockData.creativeProjects.unshift({ id: Date.now(), title, type: 'mixed', status: 'idea', description: '' });
renderProjects();
}
});
header.appendChild(newBtn);
container.appendChild(header);
function renderProjects() {
const listContainer = document.createElement('div');
listContainer.className = 'creator-list';
MockData.creativeProjects.forEach(project => {
const assets = (project.links_to_assets || []).map(link => `<li><a href="${link}">${link}</a></li>`).join('');
const revenue = Object.entries(project.revenue_streams || {}).map(([k,v]) => `${k}: $${v}`).join(', ') || 'No revenue data';
const card = Components.Card({
title: project.title,
subtitle: `${project.type}${project.status}`,
content: `<div class="creator-body">
<p>${project.description || 'No description yet'}</p>
<div class="creator-assets"><strong>Assets</strong><ul>${assets}</ul></div>
<div class="creator-revenue"><strong>Revenue</strong>: ${revenue}</div>
<div class="creator-notes">${project.notes || ''}</div>
</div>`,
footer: Components.Button('Mark published', {
size: 'small',
onClick: () => { project.status = 'published'; renderProjects(); }
})
});
listContainer.appendChild(card);
});
return listContainer;
}
container.appendChild(renderProjects());
window.OS.createWindow({
id: appId,
title: 'Creator Studio',
icon: '🎨',
content: container,
width: '1000px',
height: '700px'
});
};

View File

@@ -0,0 +1,78 @@
/**
* Identity Center - unified profile
*/
window.IdentityCenterApp = function() {
const appId = 'identity-center';
const profile = MockData.identityProfile;
const toolbar = document.createElement('div');
toolbar.className = 'window-toolbar';
const completeness = calculateCompleteness(profile);
const completenessBadge = Components.Badge(`${completeness}% complete`, completeness > 80 ? 'success' : 'warning');
toolbar.appendChild(completenessBadge);
const container = document.createElement('div');
container.className = 'identity-center';
function field(label, value) {
const row = document.createElement('div');
row.className = 'identity-row';
row.innerHTML = `<div class="label">${label}</div><div class="value">${value || 'Not set'}</div>`;
return row;
}
const profileCard = Components.Card({
title: 'Profile',
subtitle: 'One canonical record',
content: (() => {
const wrap = document.createElement('div');
wrap.appendChild(field('Name', profile.name));
wrap.appendChild(field('Legal name', profile.legal_name));
wrap.appendChild(field('Email', profile.email));
wrap.appendChild(field('Phone', profile.phone));
wrap.appendChild(field('Address', profile.address));
wrap.appendChild(field('Timezone', profile.timezone));
wrap.appendChild(field('Pronouns', profile.pronouns));
return wrap;
})()
});
const externalList = Components.List(Object.entries(profile.external_ids || {}).map(([key, val]) => ({
icon: '🔗',
title: key,
subtitle: val
})));
const connections = Components.Card({ title: 'Connected services', content: externalList || 'No connections yet' });
const howTo = Components.Card({
title: 'Reuse identity in apps',
content: `<ul class="identity-howto">
<li>Apps read from this profile instead of re-asking basic info.</li>
<li>Developers can call /api/identity/profile via the SDK.</li>
<li>External IDs keep GitHub/Railway/Discord linked without re-auth prompts.</li>
</ul>`
});
container.appendChild(profileCard);
container.appendChild(connections);
container.appendChild(howTo);
window.OS.createWindow({
id: appId,
title: 'Identity Center',
icon: '🪪',
toolbar,
content: container,
width: '800px',
height: '650px'
});
};
function calculateCompleteness(profile) {
const filled = ['name', 'email', 'phone', 'address', 'timezone', 'pronouns'].filter(key => profile[key]);
let score = Math.round((filled.length / 6) * 100);
if (profile.avatar_url) score += 10;
return Math.min(score, 100);
}

View File

@@ -22,29 +22,36 @@ window.NotificationsApp = function() {
message: 'All notifications marked as read', message: 'All notifications marked as read',
duration: 2000 duration: 2000
}); });
// Refresh would go here renderContent();
} }
}); });
const clearAllBtn = Components.Button('Clear All', { const focusSelect = document.createElement('select');
type: 'danger', focusSelect.innerHTML = `<option value="normal">Normal</option><option value="deep">Deep Work</option><option value="offline">Offline</option>`;
onClick: () => { focusSelect.onchange = () => {
if (confirm('Clear all notifications?')) { const mode = focusSelect.value;
window.OS.eventBus.emit('notifications:focus', { mode });
window.OS.showNotification({ window.OS.showNotification({
type: 'info', type: 'info',
title: 'Notifications Cleared', title: 'Focus mode',
message: 'All notifications have been removed', message: mode === 'deep' ? 'Only high-importance alerts will interrupt you' : mode === 'offline' ? 'Notifications will quietly queue' : 'All notifications enabled',
duration: 2000 duration: 2000
}); });
} };
} focusSelect.setAttribute('aria-label', 'Focus mode');
});
toolbar.appendChild(markAllReadBtn); toolbar.appendChild(markAllReadBtn);
toolbar.appendChild(clearAllBtn); toolbar.appendChild(focusSelect);
// Create content // Create content
const content = createNotificationsContent(); const content = document.createElement('div');
const renderContent = () => {
content.innerHTML = '';
content.appendChild(createNotificationsContent());
};
renderContent();
// Create window // Create window
window.OS.createWindow({ window.OS.createWindow({

View File

@@ -126,6 +126,38 @@ const MockData = {
{ id: 'notif_004', type: 'success', title: 'Compliance Approved', message: 'Fee Schedule Update Email has been approved', timestamp: '2025-11-15 16:52:00', read: true } { id: 'notif_004', type: 'success', title: 'Compliance Approved', message: 'Fee Schedule Update Email has been approved', timestamp: '2025-11-15 16:52:00', read: true }
], ],
// Chaos Inbox capture items
captureItems: [
{ id: 1, type: 'note', raw_content: 'Call back Jamie re: brand refresh', source: 'mobile', tags: ['marketing'], status: 'inbox', created_at: '2025-11-12' },
{ id: 2, type: 'link', raw_content: 'https://example.com/roadchain-deck', source: 'web_capture', tags: ['roadchain'], status: 'clustered', created_at: '2025-11-10' },
{ id: 3, type: 'idea', raw_content: 'Course outline: GPU confidence bootcamp', source: 'manual', tags: ['education', 'hardware'], status: 'resurfaced', created_at: '2025-11-01' },
{ id: 4, type: 'screenshot', raw_content: 'Screenshot: confusing AWS invoice UI', source: 'desktop', tags: ['compliance'], status: 'inbox', created_at: '2025-10-28' }
],
captureClusters: [
{ id: 1, name: 'Hardware & PiOps', description: 'Troubleshooting notes and hardware tasks', item_ids: [3, 4] },
{ id: 2, name: 'Marketing & Brand', description: 'Content drafts and approvals', item_ids: [1, 2] }
],
// Unified identity profile
identityProfile: {
name: 'BlackRoad Pilot',
legal_name: 'BlackRoad Pilot',
email: 'pilot@blackroad.io',
phone: '+1-555-123-4567',
address: '1 Infinite Road, Neo City',
timezone: 'UTC',
pronouns: 'they/them',
avatar_url: '',
external_ids: { github: 'pilot', discord: 'pilot#0001' }
},
// Creator workspace
creativeProjects: [
{ id: 1, title: 'RoadStudio Lite Launch Video', type: 'video', status: 'in_production', description: '3 minute walkthrough for creators', links_to_assets: ['https://drive.example.com/video'], revenue_streams: { youtube: 200 }, notes: 'Need new b-roll of OS desktop' },
{ id: 2, title: 'GPU Confidence Course', type: 'course', status: 'drafting', description: 'Micro-course to make GPUs less scary', links_to_assets: ['notion://gpu-course-outline'], revenue_streams: { preorders: 12 }, notes: 'Pair with PiOps demo' }
],
// Corporate Departments // Corporate Departments
departments: [ departments: [
{ id: 'dept_hr', name: 'Human Resources', icon: '👥', color: '#5AF' }, { id: 'dept_hr', name: 'Human Resources', icon: '👥', color: '#5AF' },

View File

@@ -29,6 +29,7 @@ class BlackRoadOS {
this.eventBus = new EventEmitter(); this.eventBus = new EventEmitter();
this.windowsContainer = null; this.windowsContainer = null;
this.taskbarWindows = null; this.taskbarWindows = null;
this.commandPalette = null;
// App lifecycle hooks registry // App lifecycle hooks registry
this.lifecycleHooks = { this.lifecycleHooks = {
@@ -72,9 +73,7 @@ class BlackRoadOS {
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') { if (e.ctrlKey && e.key === 'k') {
e.preventDefault(); e.preventDefault();
// TODO v0.2.0: Implement command palette this.toggleCommandPalette();
// Should show searchable list of all apps, commands, and recent windows
console.log('⌨️ Command palette - coming in v0.2.0');
} }
}); });
@@ -591,6 +590,108 @@ class BlackRoadOS {
return Array.from(this.windows.values()); return Array.from(this.windows.values());
} }
/**
* Toggle global command palette for unified search
*/
toggleCommandPalette() {
if (!this.commandPalette) {
this.buildCommandPalette();
}
const isVisible = this.commandPalette.classList.contains('open');
if (isVisible) {
this.commandPalette.classList.remove('open');
} else {
this.commandPalette.classList.add('open');
const input = this.commandPalette.querySelector('input');
input.value = '';
input.focus();
this.populatePaletteResults('');
}
}
buildCommandPalette() {
this.commandPalette = document.createElement('div');
this.commandPalette.className = 'command-palette';
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Search apps, notes, and knowledge (Ctrl/Cmd + K)';
input.setAttribute('aria-label', 'Global search');
this.commandPalette.appendChild(input);
const results = document.createElement('div');
results.className = 'command-results';
this.commandPalette.appendChild(results);
input.addEventListener('input', (e) => this.populatePaletteResults(e.target.value));
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') this.toggleCommandPalette();
});
document.body.appendChild(this.commandPalette);
this.populatePaletteResults('');
}
populatePaletteResults(query) {
if (!this.commandPalette) return;
const resultsContainer = this.commandPalette.querySelector('.command-results');
resultsContainer.innerHTML = '';
const lower = query.toLowerCase();
const appMatches = Object.values(window.AppRegistry).filter(app =>
app.name.toLowerCase().includes(lower) || app.description.toLowerCase().includes(lower)
);
const captureMatches = (window.MockData?.captureItems || []).filter(item =>
!query || (item.raw_content || '').toLowerCase().includes(lower)
).slice(0, 5);
const projectMatches = (window.MockData?.creativeProjects || []).filter(project =>
!query || project.title.toLowerCase().includes(lower)
).slice(0, 5);
const renderGroup = (title, items, onClick) => {
if (!items.length) return;
const group = document.createElement('div');
group.className = 'command-group';
const heading = document.createElement('div');
heading.className = 'command-group-title';
heading.textContent = title;
group.appendChild(heading);
items.forEach(item => {
const row = document.createElement('div');
row.className = 'command-row';
row.textContent = item.label;
row.tabIndex = 0;
row.addEventListener('click', () => onClick(item));
row.addEventListener('keydown', (e) => {
if (e.key === 'Enter') onClick(item);
});
group.appendChild(row);
});
resultsContainer.appendChild(group);
};
renderGroup('Apps', appMatches.map(app => ({ label: `${app.icon} ${app.name}`, id: app.id })), (item) => {
window.launchApp(item.id);
this.toggleCommandPalette();
});
renderGroup('Chaos Inbox', captureMatches.map(c => ({ label: `🌀 ${c.raw_content || c.type}`, id: 'chaos-inbox' })), (item) => {
window.launchApp(item.id);
this.toggleCommandPalette();
});
renderGroup('Creator projects', projectMatches.map(p => ({ label: `🎨 ${p.title}`, id: 'creator-studio' })), (item) => {
window.launchApp(item.id);
this.toggleCommandPalette();
});
if (!resultsContainer.childElementCount) {
resultsContainer.textContent = 'No matches yet. Try searching for an app or project.';
}
}
/** /**
* Show a toast notification * Show a toast notification
* @param {Object} options - Notification options * @param {Object} options - Notification options

View File

@@ -127,6 +127,46 @@ const AppRegistry = {
category: 'Corporate', category: 'Corporate',
entry: window.CorporateApp, entry: window.CorporateApp,
defaultSize: { width: '800px', height: '600px' } defaultSize: { width: '800px', height: '600px' }
},
'chaos-inbox': {
id: 'chaos-inbox',
name: 'Chaos Inbox',
icon: '🌀',
description: 'All your scraps in one forgiving place',
category: 'Focus',
entry: window.ChaosInboxApp,
defaultSize: { width: '1100px', height: '720px' }
},
'identity-center': {
id: 'identity-center',
name: 'Identity Center',
icon: '🪪',
description: 'Your info once, used everywhere',
category: 'System',
entry: window.IdentityCenterApp,
defaultSize: { width: '800px', height: '650px' }
},
'creator-studio': {
id: 'creator-studio',
name: 'Creator Studio',
icon: '🎨',
description: 'Home base for creative work',
category: 'Creators',
entry: window.CreatorStudioApp,
defaultSize: { width: '1000px', height: '700px' }
},
'compliance-ops': {
id: 'compliance-ops',
name: 'Compliance & Ops',
icon: '🧭',
description: 'Transparent logs & workflows',
category: 'Compliance',
entry: window.ComplianceOpsApp,
defaultSize: { width: '900px', height: '650px' }
} }
}; };

View File

@@ -27,7 +27,7 @@
class ThemeManager { class ThemeManager {
constructor() { constructor() {
this.currentTheme = 'tealOS'; this.currentTheme = 'tealOS';
this.availableThemes = ['tealOS', 'nightOS']; // Extensible list this.availableThemes = ['tealOS', 'nightOS', 'contrastOS']; // Extensible list
// TODO v0.2.0: Load available themes dynamically from CSS // TODO v0.2.0: Load available themes dynamically from CSS
this.init(); this.init();
} }
@@ -80,8 +80,10 @@ class ThemeManager {
* TODO v0.2.0: Support more than 2 themes with dropdown or cycle logic * TODO v0.2.0: Support more than 2 themes with dropdown or cycle logic
*/ */
toggleTheme() { toggleTheme() {
// Simple toggle for now (2 themes) // Cycle through available themes
this.currentTheme = this.currentTheme === 'tealOS' ? 'nightOS' : 'tealOS'; const currentIndex = this.availableThemes.indexOf(this.currentTheme);
const nextIndex = (currentIndex + 1) % this.availableThemes.length;
this.currentTheme = this.availableThemes[nextIndex];
this.applyTheme(this.currentTheme); this.applyTheme(this.currentTheme);
this.saveTheme(); this.saveTheme();
@@ -143,13 +145,13 @@ class ThemeManager {
const icon = toggleBtn.querySelector('.icon'); const icon = toggleBtn.querySelector('.icon');
if (icon) { if (icon) {
// Sun for dark theme (clicking will go to light) const iconMap = { tealOS: '🌙', nightOS: '☀️', contrastOS: '⚡️' };
// Moon for light theme (clicking will go to dark) icon.textContent = iconMap[this.currentTheme] || '🎨';
icon.textContent = this.currentTheme === 'tealOS' ? '🌙' : '☀️';
} }
// Update aria-label for clarity // Update aria-label for clarity
const nextTheme = this.currentTheme === 'tealOS' ? 'Night OS' : 'Teal OS'; const currentIndex = this.availableThemes.indexOf(this.currentTheme);
const nextTheme = this.availableThemes[(currentIndex + 1) % this.availableThemes.length] || 'Teal OS';
toggleBtn.setAttribute('aria-label', `Switch to ${nextTheme}`); toggleBtn.setAttribute('aria-label', `Switch to ${nextTheme}`);
} }

View File

@@ -0,0 +1,7 @@
# Accessibility Notes (v0.2)
- Added **contrastOS** theme for high-contrast users; theme manager cycles through three presets.
- Global command palette (Ctrl/Cmd + K) is keyboard navigable and labeled for screen readers.
- Desktop icons/start menu already expose `aria-label`/keyboard handlers; palette and new apps keep focusable controls.
- Notification Center shows focus modes to minimize interruption; quick batch actions reduce cognitive load.
- Remaining gaps: full keyboard traversal inside every app, color contrast audit across legacy apps, and screen reader labels for dynamic tables.

35
docs/DESIGN_V0_2.md Normal file
View File

@@ -0,0 +1,35 @@
# BlackRoad OS v0.2 Design Note
## Pillars
- **Chaos & Neurodivergent Support:** generic CaptureItem model + Chaos Inbox UI to hold loose scraps, clusters, and resurfacing.
- **Unified Identity & Duplication Killer:** UserProfile model + Identity Center app exposes a single canonical record for apps.
- **Attention & Notification Engine:** Notification model/API plus Notification Center focus modes.
- **Unified Search & Knowledge:** lightweight command palette (Ctrl/Cmd+K) that searches apps, captured items, and creator projects; backend search endpoint for future plumbing.
- **Creator Workspace Baseline:** CreativeProject model + Creator Studio to centralize creative work and assets.
- **Enterprise & Compliance Surface:** ComplianceEvent model + Compliance & Ops UI for audits/workflows.
- **Hardware & Pi Ops Visibility:** Pi Ops kept in registry; Chaos clusters track hardware notes; hooks for energy/compute tagging documented.
- **Accessibility & UX:** High-contrast theme, keyboard-friendly palette, ARIA labels for launcher/palette.
## Data Models
- `CaptureItem` + `CaptureCluster` (capture.py) for multi-modal scraps with tags/status.
- `UserProfile` (identity_profile.py) canonical identity, external IDs.
- `Notification` (notification.py) app-level alerts with importance/delivery.
- `CreativeProject` (creator.py) type/status/assets/revenue/notes.
- `ComplianceEvent` (compliance_event.py) actor/action/resource/severity metadata.
## APIs
- Capture: `POST/GET /api/capture/items`, tagging, status, clusters.
- Identity: `GET/PUT /api/identity/profile`, linked accounts, link external IDs.
- Notifications: `POST/GET /api/notifications`, mark read.
- Creator: CRUD under `/api/creator/projects`.
- Compliance: `/api/compliance/events` list.
- Search: `/api/search?q=` unified lookup scaffold.
## Frontend Surfaces
- New apps: Chaos Inbox, Identity Center, Creator Studio, Compliance & Ops.
- Notification Center adds focus modes; command palette overlays globally.
- High-contrast theme added to theme cycle; new CSS for command palette and apps.
## Safety & Next Steps
- All models auto-migrate via SQLAlchemy create_all; endpoints gated by `get_current_active_user`.
- Future work: agent-powered clustering, real notifications toasts->backend, Pi energy telemetry, app SDK hook for identity fetch.

View File

@@ -0,0 +1,15 @@
# BlackRoad OS v0.2 Pain Mapping
This release connects PRD pain clusters from **THE NEW AGE** to concrete OS surfaces.
| Pain Cluster | OS Response | Paths/Modules |
| --- | --- | --- |
| Fragmentation & duplication | Identity Center centralizes profile + external IDs; unified search surfaces apps/data. | `backend/app/routers/identity_center.py`, `backend/app/models/identity_profile.py`, `blackroad-os/js/apps/identity_center.js`, `blackroad-os/js/os.js` |
| Neurodivergent hostility / chaos | Chaos Inbox collects notes, links, screenshots, resurfacing suggestions. | `backend/app/models/capture.py`, `backend/app/routers/capture.py`, `blackroad-os/js/apps/chaos_inbox.js`, `blackroad-os/js/assets/apps.css` |
| Notification apocalypse | Notification Center focus modes + centralized API. | `backend/app/models/notification.py`, `backend/app/routers/notifications_center.py`, `blackroad-os/js/apps/notifications.js` |
| Creator extraction | Creator Studio organizes creative projects and assets. | `backend/app/models/creator.py`, `backend/app/routers/creator.py`, `blackroad-os/js/apps/creator_studio.js` |
| Legacy enterprise & compliance | Compliance & Ops surface for audits/workflows. | `backend/app/models/compliance_event.py`, `backend/app/routers/compliance_ops.py`, `blackroad-os/js/apps/compliance_ops.js` |
| Attention management & knowledge | Command palette / unified search entry point. | `backend/app/routers/search.py`, `blackroad-os/js/os.js`, `blackroad-os/assets/styles.css` |
| Hardware fear & Pi visibility | Devices remain in PiOps app; Chaos clusters resurface hardware notes. | `blackroad-os/js/apps/pi_ops.js`, `blackroad-os/js/apps/chaos_inbox.js` |
| Accessibility crisis | Added high-contrast theme + keyboard palette + ARIA prompts. | `blackroad-os/js/theme.js`, `blackroad-os/assets/styles.css` |