Compare commits
3 Commits
v1.6.3
...
a220494bcf
| Author | SHA1 | Date | |
|---|---|---|---|
| a220494bcf | |||
| 5bda1dd616 | |||
| 3e333291c6 |
@@ -56,7 +56,7 @@ LLM_API_VERSION=
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# UI default page size (number of events shown per page)
|
||||
DEFAULT_PAGE_SIZE=25
|
||||
DEFAULT_PAGE_SIZE=24
|
||||
|
||||
# Optional: privacy / access control
|
||||
# Hide entire services from users without PRIVACY_SERVICE_ROLES
|
||||
|
||||
29
ROADMAP.md
29
ROADMAP.md
@@ -72,3 +72,32 @@ Goal: add AI-powered analysis and external tool integration.
|
||||
## Completed in this PR
|
||||
All Phase 5 items marked done were implemented in v1.3.0–v1.5.0.
|
||||
Redis caching + async queue implemented in v1.6.0, switched to Valkey.
|
||||
UI polish (topbar, footer, clickable pills) in v1.6.1–v1.6.4.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Multi-Tenancy (Premium) ⏸️
|
||||
Goal: allow MSPs to manage multiple client tenants from a single deployment.
|
||||
|
||||
Status: **Planned — not started**. Architecture designed, pending validation of core features (SIEM export, alerting) in production first.
|
||||
|
||||
### Architecture
|
||||
- Row-level isolation: `tenant_id` field on every MongoDB document
|
||||
- Each tenant has their own Microsoft Entra tenant + app registration credentials
|
||||
- Auth: user's JWT `tid` claim maps to tenant config automatically
|
||||
- Super-admin role for MSP staff to access all tenants
|
||||
|
||||
### Implementation phases
|
||||
- **Phase 6.1** (2–3 days): Tenant model & registry, tenant-aware data layer, per-tenant Graph API auth
|
||||
- **Phase 6.2** (1 day): Tenant-scoped API routes, tenant-specific config endpoints
|
||||
- **Phase 6.3** (2 days): Frontend tenant switcher, tenant name display, admin page
|
||||
- **Phase 6.4** (1 day): License gating — signed JWT `LICENSE_KEY` gates multi-tenant mode
|
||||
|
||||
### Licensing model
|
||||
- Single-tenant: remains MIT/free
|
||||
- Multi-tenant: premium feature requiring a signed license key
|
||||
- License key is a JWT with claims: `plan`, `max_tenants`, `exp`, `features`
|
||||
- Offline license generation tool included
|
||||
|
||||
### Effort estimate
|
||||
~7–9 days total. Deferred until SIEM export and alerting are battle-tested.
|
||||
|
||||
@@ -61,7 +61,7 @@ class Settings(BaseSettings):
|
||||
REDIS_URL: str = "redis://localhost:6379/0"
|
||||
|
||||
# UI defaults
|
||||
DEFAULT_PAGE_SIZE: int = 25
|
||||
DEFAULT_PAGE_SIZE: int = 24
|
||||
|
||||
|
||||
_settings = Settings()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Operations Center</title>
|
||||
<link rel="stylesheet" href="/style.css?v=11" />
|
||||
<link rel="stylesheet" href="/style.css?v=12" />
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://alcdn.msauth.net/browser/2.37.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
@@ -184,11 +184,7 @@
|
||||
<template x-for="(evt, idx) in askEvents" :key="evt.id || idx">
|
||||
<article class="event event--compact">
|
||||
<div class="event__meta">
|
||||
<span class="pill">
|
||||
<span x-text="evt.display_category || evt.service || '—'"></span>
|
||||
<span class="pill__action" @click.stop="addServiceFilter(evt.service || evt.display_category)" title="Include this service">+</span>
|
||||
<span class="pill__action" @click.stop="removeServiceFilter(evt.service || evt.display_category)" title="Exclude this service">−</span>
|
||||
</span>
|
||||
<span class="pill pill--clickable" x-text="evt.display_category || evt.service || '—'" @click="filterByService(evt.service || evt.display_category)" title="Filter by this service"></span>
|
||||
<span class="pill pill--clickable" :class="['success','succeeded','ok','passed','true'].includes((evt.result || '').toLowerCase()) ? 'pill--ok' : 'pill--warn'" x-text="evt.result || '—'" @click="filterByResult(evt.result)" title="Filter by this result"></span>
|
||||
</div>
|
||||
<h3 x-text="evt.operation || '—'"></h3>
|
||||
@@ -309,7 +305,7 @@
|
||||
accessToken: null,
|
||||
authScopes: [],
|
||||
filters: {
|
||||
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 25, includeTags: '', excludeTags: '',
|
||||
actor: '', selectedServices: [], search: '', operation: '', result: '', start: '', end: '', limit: 24, includeTags: '', excludeTags: '',
|
||||
},
|
||||
options: { actors: [], services: [], operations: [], results: [] },
|
||||
savedSearches: [],
|
||||
@@ -402,6 +398,8 @@
|
||||
this.aiFeaturesEnabled = featBody.ai_features_enabled !== false;
|
||||
if (featBody.default_page_size) {
|
||||
this.filters.limit = featBody.default_page_size;
|
||||
} else {
|
||||
this.filters.limit = 24;
|
||||
}
|
||||
} else {
|
||||
this.aiFeaturesEnabled = true;
|
||||
@@ -571,9 +569,8 @@
|
||||
|
||||
const saved = localStorage.getItem('aoc_filters');
|
||||
if (!saved && this.options.services.length) {
|
||||
// Default: exclude noisy high-volume services
|
||||
const noisy = ['Exchange', 'SharePoint', 'Teams'];
|
||||
this.filters.selectedServices = this.options.services.filter((s) => !noisy.includes(s));
|
||||
// Default: show all services (privacy controls handle exclusions server-side)
|
||||
this.filters.selectedServices = [...this.options.services];
|
||||
} else if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
@@ -667,26 +664,15 @@
|
||||
},
|
||||
|
||||
clearFilters() {
|
||||
const noisy = ['Exchange', 'SharePoint', 'Teams'];
|
||||
this.filters = { actor: '', selectedServices: this.options.services.filter((s) => !noisy.includes(s)), search: '', operation: '', result: '', start: '', end: '', limit: 25, includeTags: '', excludeTags: '' };
|
||||
this.filters = { actor: '', selectedServices: [...this.options.services], search: '', operation: '', result: '', start: '', end: '', limit: 24, includeTags: '', excludeTags: '' };
|
||||
this.saveFilters();
|
||||
this.resetPagination();
|
||||
this.loadEvents();
|
||||
},
|
||||
|
||||
addServiceFilter(service) {
|
||||
filterByService(service) {
|
||||
if (!service) return;
|
||||
if (!this.filters.selectedServices.includes(service)) {
|
||||
this.filters.selectedServices.push(service);
|
||||
this.saveFilters();
|
||||
this.resetPagination();
|
||||
this.loadEvents();
|
||||
}
|
||||
},
|
||||
|
||||
removeServiceFilter(service) {
|
||||
if (!service) return;
|
||||
this.filters.selectedServices = this.filters.selectedServices.filter((s) => s !== service);
|
||||
this.filters.selectedServices = [service];
|
||||
this.saveFilters();
|
||||
this.resetPagination();
|
||||
this.loadEvents();
|
||||
|
||||
@@ -376,31 +376,6 @@ input {
|
||||
background: rgba(249, 115, 22, 0.25);
|
||||
}
|
||||
|
||||
.pill__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
margin-left: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.12s ease;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.pill__action:hover {
|
||||
color: var(--text);
|
||||
background: rgba(125, 211, 252, 0.2);
|
||||
border-color: rgba(125, 211, 252, 0.3);
|
||||
}
|
||||
|
||||
.event h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 17px;
|
||||
|
||||
Reference in New Issue
Block a user