Skip to content

Commit ee879d2

Browse files
Phase 8 Batch 5b: Mobile Native APIs + Multi-Tenant + Customer Portal (1,250 lines, 39 endpoints, 3 features)
1 parent 8ac722e commit ee879d2

7 files changed

Lines changed: 1800 additions & 0 deletions

File tree

app/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ def _register_blueprints(app):
229229
from app.routes.compliance_routes import compliance_bp
230230
from app.routes.knowledge_base_routes import kb_bp
231231
from app.routes.team_collaboration_routes import team_bp
232+
from app.routes.mobile_routes import mobile_bp
233+
from app.routes.tenant_routes import tenant_bp
234+
from app.routes.customer_portal_routes import portal_bp
232235
from app.admin_secure.routes import create_secure_admin_blueprint
233236
import secrets
234237

@@ -248,6 +251,9 @@ def _register_blueprints(app):
248251
app.register_blueprint(compliance_bp)
249252
app.register_blueprint(kb_bp)
250253
app.register_blueprint(team_bp)
254+
app.register_blueprint(mobile_bp)
255+
app.register_blueprint(tenant_bp)
256+
app.register_blueprint(portal_bp)
251257
app.register_blueprint(phase6_bp)
252258
app.register_blueprint(projects_bp, url_prefix='/project')
253259

app/mobile/native_app.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# app/mobile/native_app.py
2+
"""
3+
Mobile Native App APIs
4+
Support for iOS/Android clients with offline sync, push notifications, and device management.
5+
"""
6+
7+
from dataclasses import dataclass, field
8+
from datetime import datetime
9+
from typing import Dict, List, Optional
10+
from enum import Enum
11+
import uuid
12+
13+
14+
class DevicePlatform(Enum):
15+
"""Mobile device platforms."""
16+
IOS = "ios"
17+
ANDROID = "android"
18+
WINDOWS = "windows"
19+
20+
21+
class SyncStatus(Enum):
22+
"""Data sync status."""
23+
PENDING = "pending"
24+
IN_PROGRESS = "in_progress"
25+
SYNCED = "synced"
26+
FAILED = "failed"
27+
28+
29+
@dataclass
30+
class MobileDevice:
31+
"""Registered mobile device."""
32+
device_id: str = field(default_factory=lambda: str(uuid.uuid4()))
33+
user_id: str = ""
34+
platform: DevicePlatform = DevicePlatform.ANDROID
35+
app_version: str = ""
36+
os_version: str = ""
37+
device_name: str = ""
38+
push_token: str = ""
39+
registered_at: datetime = field(default_factory=datetime.utcnow)
40+
last_sync: Optional[datetime] = None
41+
is_active: bool = True
42+
43+
def to_dict(self) -> Dict:
44+
return {
45+
'device_id': self.device_id,
46+
'platform': self.platform.value,
47+
'app_version': self.app_version,
48+
'os_version': self.os_version,
49+
'device_name': self.device_name,
50+
'registered_at': self.registered_at.isoformat(),
51+
'last_sync': self.last_sync.isoformat() if self.last_sync else None,
52+
'is_active': self.is_active
53+
}
54+
55+
56+
@dataclass
57+
class SyncJob:
58+
"""Data synchronization job."""
59+
sync_id: str = field(default_factory=lambda: str(uuid.uuid4()))
60+
device_id: str = ""
61+
sync_type: str = "" # pull, push, bidirectional
62+
status: SyncStatus = SyncStatus.PENDING
63+
records_count: int = 0
64+
synced_count: int = 0
65+
failed_count: int = 0
66+
started_at: datetime = field(default_factory=datetime.utcnow)
67+
completed_at: Optional[datetime] = None
68+
69+
def to_dict(self) -> Dict:
70+
return {
71+
'sync_id': self.sync_id,
72+
'device_id': self.device_id,
73+
'sync_type': self.sync_type,
74+
'status': self.status.value,
75+
'progress': round((self.synced_count / max(self.records_count, 1)) * 100, 1),
76+
'started_at': self.started_at.isoformat(),
77+
'completed_at': self.completed_at.isoformat() if self.completed_at else None
78+
}
79+
80+
81+
@dataclass
82+
class OfflineData:
83+
"""Offline data cached on device."""
84+
data_id: str = field(default_factory=lambda: str(uuid.uuid4()))
85+
device_id: str = ""
86+
entity_type: str = "" # projects, tasks, etc
87+
entity_id: str = ""
88+
data: Dict = field(default_factory=dict)
89+
cached_at: datetime = field(default_factory=datetime.utcnow)
90+
synced: bool = False
91+
92+
def to_dict(self) -> Dict:
93+
return {
94+
'data_id': self.data_id,
95+
'entity_type': self.entity_type,
96+
'entity_id': self.entity_id,
97+
'cached_at': self.cached_at.isoformat(),
98+
'synced': self.synced
99+
}
100+
101+
102+
class MobileNativeAppManager:
103+
"""
104+
Manages mobile native app functionality for iOS and Android.
105+
"""
106+
107+
def __init__(self):
108+
"""Initialize mobile app manager."""
109+
self.devices: Dict[str, MobileDevice] = {}
110+
self.sync_jobs: Dict[str, SyncJob] = {}
111+
self.offline_data: Dict[str, OfflineData] = {}
112+
self.device_users: Dict[str, List[str]] = {} # user_id -> device_ids
113+
self.stats = {
114+
'total_devices': 0,
115+
'active_devices': 0,
116+
'total_syncs': 0,
117+
'successful_syncs': 0
118+
}
119+
120+
def register_device(self, user_id: str, platform: str, app_version: str,
121+
os_version: str, device_name: str,
122+
push_token: str = "") -> MobileDevice:
123+
"""Register mobile device."""
124+
try:
125+
platform_enum = DevicePlatform[platform.upper()]
126+
except KeyError:
127+
platform_enum = DevicePlatform.ANDROID
128+
129+
device = MobileDevice(
130+
user_id=user_id,
131+
platform=platform_enum,
132+
app_version=app_version,
133+
os_version=os_version,
134+
device_name=device_name,
135+
push_token=push_token
136+
)
137+
138+
self.devices[device.device_id] = device
139+
140+
if user_id not in self.device_users:
141+
self.device_users[user_id] = []
142+
self.device_users[user_id].append(device.device_id)
143+
144+
self.stats['total_devices'] += 1
145+
self.stats['active_devices'] += 1
146+
147+
return device
148+
149+
def get_user_devices(self, user_id: str) -> List[Dict]:
150+
"""Get all devices for user."""
151+
if user_id not in self.device_users:
152+
return []
153+
154+
return [self.devices[did].to_dict() for did in self.device_users[user_id]
155+
if did in self.devices]
156+
157+
def create_sync_job(self, device_id: str, sync_type: str,
158+
records_count: int = 0) -> SyncJob:
159+
"""Create synchronization job."""
160+
sync_job = SyncJob(
161+
device_id=device_id,
162+
sync_type=sync_type,
163+
records_count=records_count,
164+
status=SyncStatus.IN_PROGRESS
165+
)
166+
167+
self.sync_jobs[sync_job.sync_id] = sync_job
168+
self.stats['total_syncs'] += 1
169+
170+
return sync_job
171+
172+
def update_sync_progress(self, sync_id: str, synced_count: int,
173+
failed_count: int = 0) -> bool:
174+
"""Update sync progress."""
175+
if sync_id not in self.sync_jobs:
176+
return False
177+
178+
sync_job = self.sync_jobs[sync_id]
179+
sync_job.synced_count = synced_count
180+
sync_job.failed_count = failed_count
181+
182+
# Mark complete if all synced
183+
if synced_count + failed_count >= sync_job.records_count:
184+
sync_job.status = SyncStatus.SYNCED if failed_count == 0 else SyncStatus.FAILED
185+
sync_job.completed_at = datetime.utcnow()
186+
187+
if failed_count == 0:
188+
self.stats['successful_syncs'] += 1
189+
190+
# Update device last sync
191+
if sync_job.device_id in self.devices:
192+
self.devices[sync_job.device_id].last_sync = datetime.utcnow()
193+
194+
return True
195+
196+
def cache_offline_data(self, device_id: str, entity_type: str,
197+
entity_id: str, data: Dict) -> OfflineData:
198+
"""Cache data for offline access."""
199+
offline_data = OfflineData(
200+
device_id=device_id,
201+
entity_type=entity_type,
202+
entity_id=entity_id,
203+
data=data
204+
)
205+
206+
self.offline_data[offline_data.data_id] = offline_data
207+
return offline_data
208+
209+
def get_offline_data(self, device_id: str, entity_type: str = None) -> List[Dict]:
210+
"""Get offline cached data."""
211+
results = []
212+
213+
for data in self.offline_data.values():
214+
if data.device_id == device_id:
215+
if entity_type is None or data.entity_type == entity_type:
216+
results.append(data.to_dict())
217+
218+
return results
219+
220+
def mark_offline_data_synced(self, data_ids: List[str]) -> int:
221+
"""Mark offline data as synced."""
222+
count = 0
223+
for data_id in data_ids:
224+
if data_id in self.offline_data:
225+
self.offline_data[data_id].synced = True
226+
count += 1
227+
228+
return count
229+
230+
def check_app_update(self, platform: str, current_version: str) -> Dict:
231+
"""Check if app update is available."""
232+
# Simulate version checking
233+
latest_versions = {
234+
'ios': '2.5.0',
235+
'android': '2.5.0'
236+
}
237+
238+
latest = latest_versions.get(platform.lower(), current_version)
239+
needs_update = latest > current_version
240+
241+
return {
242+
'current_version': current_version,
243+
'latest_version': latest,
244+
'update_available': needs_update,
245+
'force_update': False,
246+
'update_url': f'https://app.store/{platform}/v{latest}' if needs_update else None
247+
}
248+
249+
def get_device_analytics(self, device_id: str) -> Dict:
250+
"""Get analytics for device."""
251+
if device_id not in self.devices:
252+
return {'error': 'Device not found'}
253+
254+
device = self.devices[device_id]
255+
device_syncs = [s for s in self.sync_jobs.values() if s.device_id == device_id]
256+
257+
return {
258+
'device_id': device_id,
259+
'platform': device.platform.value,
260+
'total_syncs': len(device_syncs),
261+
'successful_syncs': sum(1 for s in device_syncs if s.status == SyncStatus.SYNCED),
262+
'failed_syncs': sum(1 for s in device_syncs if s.status == SyncStatus.FAILED),
263+
'cached_items': len([d for d in self.offline_data.values() if d.device_id == device_id]),
264+
'last_sync': device.last_sync.isoformat() if device.last_sync else None
265+
}
266+
267+
def get_stats(self) -> Dict:
268+
"""Get mobile app statistics."""
269+
active = sum(1 for d in self.devices.values() if d.is_active)
270+
271+
return {
272+
'total_devices': self.stats['total_devices'],
273+
'active_devices': active,
274+
'ios_devices': sum(1 for d in self.devices.values() if d.platform == DevicePlatform.IOS),
275+
'android_devices': sum(1 for d in self.devices.values() if d.platform == DevicePlatform.ANDROID),
276+
'total_syncs': self.stats['total_syncs'],
277+
'successful_syncs': self.stats['successful_syncs'],
278+
'sync_success_rate': round(
279+
self.stats['successful_syncs'] / max(self.stats['total_syncs'], 1) * 100, 1
280+
),
281+
'cached_items': len(self.offline_data)
282+
}
283+
284+
285+
# Global mobile app manager
286+
mobile_manager = MobileNativeAppManager()

0 commit comments

Comments
 (0)