Skip to content

Commit 2d46af7

Browse files
feat: Add standalone Android app with embedded Python backend via Chaquopy
- Integrate Chaquopy Gradle plugin to embed Python 3.8 runtime on Android - Add MeshSyncService foreground service to run Python backend in background - Configure pip packages: FastAPI, Uvicorn, Pydantic v1, aiosqlite, PyNaCl - Create android_main.py entry point for Chaquopy integration - Add Gradle task to copy DTN and ValueFlows backend source at build time - Configure proper task dependencies for Gradle 8.x strict validation - Add Android permissions for WiFi Direct, Bluetooth, and foreground service - Add node configuration page and guards for first-time setup - APK includes Python runtime for arm64-v8a, armeabi-v7a, x86, x86_64 The app runs completely offline with FastAPI backends on localhost:8000/8001. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5704e76 commit 2d46af7

298 files changed

Lines changed: 66575 additions & 48 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/api/node_config.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Node Configuration API
3+
4+
Handles initial node setup and configuration management.
5+
"""
6+
7+
from fastapi import APIRouter, HTTPException
8+
from app.models.node_config import NodeConfig, NodeConfigUpdate
9+
from app.services.node_config_service import get_node_config_service
10+
import structlog
11+
12+
logger = structlog.get_logger()
13+
14+
router = APIRouter(prefix="/node", tags=["node"])
15+
16+
17+
@router.get("/config")
18+
async def get_node_config():
19+
"""
20+
Get current node configuration.
21+
22+
Returns:
23+
NodeConfig if configured, None if not yet configured
24+
"""
25+
service = get_node_config_service()
26+
config = service.get_config()
27+
28+
if not config:
29+
return None
30+
31+
return config
32+
33+
34+
@router.get("/config/status")
35+
async def get_config_status():
36+
"""Check if node has been configured"""
37+
service = get_node_config_service()
38+
return {
39+
"configured": service.is_configured()
40+
}
41+
42+
43+
@router.post("/config")
44+
async def create_node_config(config: NodeConfig):
45+
"""
46+
Create initial node configuration.
47+
48+
This should only be called once during first-run setup.
49+
50+
Raises:
51+
400: If node is already configured
52+
422: If validation fails
53+
"""
54+
service = get_node_config_service()
55+
56+
try:
57+
created_config = service.create_config(config)
58+
59+
logger.info(
60+
"node_configured_via_api",
61+
mesh_name=created_config.mesh_fqdn,
62+
ai_enabled=created_config.enable_ai_inference,
63+
bridge_enabled=created_config.enable_bridge_mode,
64+
)
65+
66+
return created_config
67+
68+
except ValueError as e:
69+
raise HTTPException(status_code=400, detail=str(e))
70+
except Exception as e:
71+
logger.error("config_creation_failed", error=str(e))
72+
raise HTTPException(status_code=500, detail="Failed to create configuration")
73+
74+
75+
@router.put("/config")
76+
async def update_node_config(updates: NodeConfigUpdate):
77+
"""
78+
Update node configuration.
79+
80+
Only provided fields will be updated.
81+
82+
Raises:
83+
400: If node is not configured yet
84+
422: If validation fails
85+
"""
86+
service = get_node_config_service()
87+
88+
try:
89+
updated_config = service.update_config(updates)
90+
91+
logger.info(
92+
"node_config_updated_via_api",
93+
mesh_name=updated_config.mesh_fqdn,
94+
)
95+
96+
return updated_config
97+
98+
except ValueError as e:
99+
raise HTTPException(status_code=400, detail=str(e))
100+
except Exception as e:
101+
logger.error("config_update_failed", error=str(e))
102+
raise HTTPException(status_code=500, detail="Failed to update configuration")

app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class Settings(BaseSettings):
4141

4242
# CORS configuration
4343
allowed_origins: List[str] = Field(
44-
default=["http://localhost:3000", "http://localhost:5173", "http://127.0.0.1:3000", "http://127.0.0.1:5173"],
44+
default=["http://localhost:3000", "http://localhost:5173", "http://localhost:4444", "http://127.0.0.1:3000", "http://127.0.0.1:5173", "http://127.0.0.1:4444"],
4545
description="Allowed CORS origins"
4646
)
4747

app/main.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,14 @@
6868
from .api.fork_rights import router as fork_rights_router
6969
from .api.security_status import router as security_status_router
7070
from .api.mourning import router as mourning_router
71+
from .api.node_config import router as node_config_router
7172
from .services import TTLService, CryptoService, CacheService
73+
from .services.node_config_service import get_node_config_service
7274
from .middleware import CSRFMiddleware, PrometheusMetricsMiddleware, metrics_endpoint, init_metrics
7375
from .middleware.correlation_id import CorrelationIdMiddleware
76+
import sys
77+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
78+
from mesh_naming.mesh_dns import get_mesh_naming
7479

7580
# Configure structured logging
7681
configure_logging(log_level=settings.log_level, json_logs=settings.json_logs)
@@ -144,6 +149,35 @@ async def lifespan(app: FastAPI):
144149
init_metrics(version="1.0.0", node_id=fingerprint)
145150
logger.info("Prometheus metrics initialized")
146151

152+
# Initialize mesh naming if node is configured
153+
try:
154+
node_config_service = get_node_config_service()
155+
node_config = node_config_service.get_config()
156+
157+
if node_config:
158+
# Initialize mesh naming with node config
159+
mesh_naming = get_mesh_naming(
160+
node_name=node_config.mesh_name,
161+
community_name=node_config.community_name,
162+
)
163+
164+
# Announce on the network if AI inference or bridge mode enabled
165+
if node_config.enable_ai_inference or node_config.enable_bridge_mode:
166+
mesh_naming.announce(
167+
dtn_port=settings.port,
168+
valueflows_port=8001,
169+
ai_port=8005,
170+
)
171+
logger.info(
172+
"mesh_announced",
173+
mesh_name=node_config.mesh_fqdn,
174+
ai_enabled=node_config.enable_ai_inference,
175+
bridge_enabled=node_config.enable_bridge_mode,
176+
)
177+
except Exception as e:
178+
logger.warning("mesh_naming_initialization_failed", error=str(e))
179+
# Continue startup even if mesh naming fails
180+
147181
logger.info("DTN Bundle System started successfully")
148182
logger.info("=" * 60)
149183
logger.info(f"API available at http://{settings.host}:{settings.port}")
@@ -170,6 +204,15 @@ async def lifespan(app: FastAPI):
170204
await ttl_service.stop()
171205
logger.info("TTL service stopped")
172206

207+
# Shutdown mesh naming if active
208+
try:
209+
mesh_naming = get_mesh_naming()
210+
if mesh_naming:
211+
mesh_naming.shutdown()
212+
logger.info("Mesh naming service stopped")
213+
except:
214+
pass
215+
173216
# Close database connections
174217
logger.info("Closing database connections...")
175218
await close_db()
@@ -216,6 +259,8 @@ async def lifespan(app: FastAPI):
216259
"/openapi.json",
217260
"/health",
218261
"/node/info",
262+
"/node/config", # Node configuration must be accessible before users exist
263+
"/node/config/status",
219264
"/auth/csrf-token", # CSRF token endpoint must be exempt
220265
"/auth/register", # Registration should be exempt
221266
"/auth/login", # Login should be exempt
@@ -253,6 +298,7 @@ async def lifespan(app: FastAPI):
253298
app.include_router(fork_rights_router)
254299
app.include_router(security_status_router)
255300
app.include_router(mourning_router)
301+
app.include_router(node_config_router)
256302

257303

258304
@app.get("/")

app/models/node_config.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""
2+
Node Configuration Model
3+
4+
Stores configuration for this specific node/device.
5+
This is set once during initial setup, before any users log in.
6+
"""
7+
8+
from pydantic import BaseModel, Field
9+
from typing import Optional
10+
from datetime import datetime
11+
12+
13+
class NodeConfig(BaseModel):
14+
"""Configuration for this mesh node"""
15+
16+
# Identity
17+
mesh_name: str = Field(
18+
...,
19+
description="Node name on .multiversemesh (e.g., 'alice', 'food-coop')"
20+
)
21+
community_name: Optional[str] = Field(
22+
None,
23+
description="Community subdomain (e.g., 'mycommunity' → alice.mycommunity.multiversemesh)"
24+
)
25+
node_description: Optional[str] = Field(
26+
None,
27+
description="What is this node for? (e.g., 'Personal device', 'Community hub')"
28+
)
29+
30+
# Services
31+
enable_ai_inference: bool = Field(
32+
default=False,
33+
description="Share AI compute with the mesh"
34+
)
35+
enable_bridge_mode: bool = Field(
36+
default=False,
37+
description="Act as bridge between mesh islands"
38+
)
39+
40+
# Contact
41+
admin_contact: Optional[str] = Field(
42+
None,
43+
description="Contact info for node operator (optional)"
44+
)
45+
46+
# Metadata
47+
configured_at: datetime = Field(
48+
default_factory=datetime.utcnow,
49+
description="When this node was configured"
50+
)
51+
node_id: Optional[str] = Field(
52+
None,
53+
description="Auto-generated unique node ID"
54+
)
55+
56+
@property
57+
def mesh_fqdn(self) -> str:
58+
"""Get full mesh name"""
59+
if self.community_name:
60+
return f"{self.mesh_name}.{self.community_name}.multiversemesh"
61+
return f"{self.mesh_name}.multiversemesh"
62+
63+
64+
class NodeConfigUpdate(BaseModel):
65+
"""Update node configuration"""
66+
67+
mesh_name: Optional[str] = None
68+
community_name: Optional[str] = None
69+
node_description: Optional[str] = None
70+
enable_ai_inference: Optional[bool] = None
71+
enable_bridge_mode: Optional[bool] = None
72+
admin_contact: Optional[str] = None

0 commit comments

Comments
 (0)