feat: add scoped device catalog and unlimited custom area hierarchy

This commit is contained in:
Codex
2026-10-04 11:19:14 +08:00
parent 72a3252e25
commit 5544780bf0
16 changed files with 349 additions and 66 deletions
+22 -5
View File
@@ -32,6 +32,7 @@ import playback
import identity
import genealogy
import households
import inventory
import ptz
ROOT = Path(__file__).resolve().parent
@@ -75,6 +76,7 @@ class Problem(Exception):
ACCOUNTS = identity.Accounts(sys.modules[__name__])
FAMILY = genealogy.Family(sys.modules[__name__])
HOUSEHOLDS = households.Households(sys.modules[__name__])
INVENTORY = inventory.Inventory(sys.modules[__name__])
def setting(key, default=None):
@@ -165,13 +167,21 @@ def object_key(table, body):
@serialized_save
def save_site(body, actor=None):
family = HOUSEHOLDS.prepare(actor, 'sites', body)
family = HOUSEHOLDS.prepare(actor, 'sites', body, (('sites', 'parentId'),))
key = object_key('sites', body)
parent = clean_text(body.get('parentId', ''), 32)
ancestor, seen = parent, {key}
while ancestor:
if ancestor in seen:
raise Problem('区域不能放到自己或自己的下级区域中')
seen.add(ancestor)
ancestor = (get_object('sites', ancestor) or {}).get('parentId', '')
item = {k: clean_text(body.get(k, ''), 240 if k in ('address', 'note') else 100,
required=k == 'name') for k in SITE_FIELDS}
item.update(id=key, kind=body.get('kind', 'home'), familyId=family)
if item['kind'] not in ('home', 'company', 'other'):
raise Problem('请选择家庭、公司或其他用途')
item.update(id=key, kind=body.get('kind', 'home'), familyId=family, parentId=parent,
category=clean_text(body.get('category', ''), 40))
if item['kind'] not in ('home', 'company', 'office', 'factory', 'other'):
raise Problem('请选择有效区域用途')
save_object('sites', item)
audit('保存空间', item['name'])
return item
@@ -238,6 +248,7 @@ def save_asset(body, actor=None):
item = {k: clean_text(body.get(k, ''), 120, required=k == 'name')
for k in ('name', 'brand', 'model', 'serial', 'point', 'note')}
item.update(id=key, siteId=site, lensCount=integer(body.get('lensCount', 2), 1, 16), familyId=family)
item.update(INVENTORY.metadata(body, get_object('assets', key)))
attached = [c for c in objects('cameras') if c.get('assetId') == key]
if len(attached) > item['lensCount']:
raise Problem('镜头数不能小于已经建立的通道数')
@@ -269,6 +280,7 @@ def save_recorder(body, actor=None):
if not isinstance(password, str) or len(password) > 256 or any(ord(c) < 32 for c in password):
raise Problem('密码格式不正确')
item['password'] = password or old.get('password', '')
item.update(INVENTORY.metadata(body, old))
save_object('recorders', item)
write_media_config()
audit('保存录像机', item['name'])
@@ -411,7 +423,7 @@ def status():
storage = dict(STORAGE)
scan = dict(SCAN)
return {'cameras': cameras, 'assets': objects('assets'), 'recorders': [public_camera(r) for r in objects('recorders')],
'sites': objects('sites'), 'mediaOnline': media,
'sites': objects('sites'), 'devices': objects('devices'), 'mediaOnline': media,
'storage': {**setting('storage'), **storage}, 'scan': scan,
'version': (ROOT / 'VERSION').read_text().strip(), 'uptime': round(time.time() - STARTED)}
@@ -660,6 +672,10 @@ class Handler(BaseHTTPRequestHandler):
self.answer(HOUSEHOLDS.save(data, self.user))
elif path == '/api/households/leadership':
self.answer(HOUSEHOLDS.leadership(data, self.user))
elif path == '/api/device-view':
self.answer(HOUSEHOLDS.save_view(data, self.user))
elif path == '/api/devices':
self.answer(INVENTORY.save(data, self.user))
elif path in ('/api/family/person', '/api/family/link', '/api/family/event'):
self.answer(FAMILY.save(path.rsplit('/', 1)[-1], data, self.user))
elif path == '/api/sites':
@@ -900,6 +916,7 @@ class Handler(BaseHTTPRequestHandler):
else:
files = {'/': 'index.html', '/app.js': 'app.js', '/live-player.js': 'live-player.js', '/style.css': 'style.css',
'/family.js': 'family.js', '/kinship.js': 'kinship.js', '/households.js': 'households.js',
'/device-model.js': 'device-model.js', '/devices.js': 'devices.js',
'/family-graph.js': 'family-graph.js', '/family-map.js': 'family-map.js',
'/family-map.css': 'family-map.css', '/chinese-theme.css': 'chinese-theme.css',
'/family-events.js': 'family-events.js',