appmanager-server 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appmanager/__init__.py +52 -0
- appmanager/admin/__init__.py +3 -0
- appmanager/admin/app_installer.py +739 -0
- appmanager/admin/members.py +95 -0
- appmanager/admin/registry.py +372 -0
- appmanager/admin/routes.py +900 -0
- appmanager/api/__init__.py +5 -0
- appmanager/api/routes.py +373 -0
- appmanager/app_config.py +101 -0
- appmanager/auth/__init__.py +3 -0
- appmanager/auth/oauth.py +77 -0
- appmanager/auth/routes.py +224 -0
- appmanager/auth/utils.py +175 -0
- appmanager/bridge.py +90 -0
- appmanager/cli.py +1171 -0
- appmanager/config.py +58 -0
- appmanager/database.py +120 -0
- appmanager/extension.py +198 -0
- appmanager/extensions.py +225 -0
- appmanager/health.py +133 -0
- appmanager/hooks.py +187 -0
- appmanager/middleware.py +256 -0
- appmanager/models.py +307 -0
- appmanager/py.typed +1 -0
- appmanager/sdk.py +30 -0
- appmanager/security.py +227 -0
- appmanager/security_scanner.py +582 -0
- appmanager/signals.py +21 -0
- appmanager/static/css/app.css +808 -0
- appmanager/static/vendor/fonts/fonts.css +288 -0
- appmanager/static/vendor/fonts/jetbrainsmono-0.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-1.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-10.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-11.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-12.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-13.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-14.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-15.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-16.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-17.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-18.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-19.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-2.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-20.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-21.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-22.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-23.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-3.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-4.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-5.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-6.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-7.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-8.woff2 +0 -0
- appmanager/static/vendor/fonts/jetbrainsmono-9.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-0.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-1.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-10.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-11.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-2.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-3.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-4.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-5.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-6.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-7.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-8.woff2 +0 -0
- appmanager/static/vendor/fonts/spacegrotesk-9.woff2 +0 -0
- appmanager/static/vendor/htmx/htmx.min.js +1 -0
- appmanager/templates/admin/_member_drawer.html +98 -0
- appmanager/templates/admin/_members_table.html +108 -0
- appmanager/templates/admin/app_detail.html +136 -0
- appmanager/templates/admin/dashboard.html +902 -0
- appmanager/templates/admin/flairs.html +90 -0
- appmanager/templates/admin/permissions.html +88 -0
- appmanager/templates/admin/roles.html +119 -0
- appmanager/templates/admin/users.html +156 -0
- appmanager/templates/auth/login.html +58 -0
- appmanager/templates/auth/profile.html +44 -0
- appmanager/templates/base.html +76 -0
- appmanager/templates/errors/401.html +19 -0
- appmanager/templates/errors/403.html +19 -0
- appmanager/templates/errors/404.html +18 -0
- appmanager/templates/errors/500.html +19 -0
- appmanager/templates/index.html +54 -0
- appmanager_server-0.2.0.dist-info/METADATA +320 -0
- appmanager_server-0.2.0.dist-info/RECORD +89 -0
- appmanager_server-0.2.0.dist-info/WHEEL +5 -0
- appmanager_server-0.2.0.dist-info/entry_points.txt +5 -0
- appmanager_server-0.2.0.dist-info/licenses/LICENSE +21 -0
- appmanager_server-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{# Paginated member table fragment — swapped in place by htmx #}
|
|
2
|
+
<div class="members-toolbar">
|
|
3
|
+
<form class="members-search" hx-get="/admin/members" hx-target="#members-table-wrap" hx-trigger="keyup changed delay:300ms, search" hx-include="[name='per_page'],[name='sort']" hx-push-url="true">
|
|
4
|
+
<span class="search-icon">⌕</span>
|
|
5
|
+
<input type="search" name="q" value="{{ q }}" placeholder="Search name or email…" class="form-input" autocomplete="off">
|
|
6
|
+
</form>
|
|
7
|
+
<div class="members-toolbar-right">
|
|
8
|
+
<select name="role" class="form-select" hx-get="/admin/members" hx-target="#members-table-wrap" hx-include="[name='q'],[name='per_page'],[name='sort']" hx-push-url="true" hx-trigger="change">
|
|
9
|
+
<option value="">All roles</option>
|
|
10
|
+
{% for r in roles %}
|
|
11
|
+
<option value="{{ r.slug }}" {% if role == r.slug %}selected{% endif %}>{{ r.name }}</option>
|
|
12
|
+
{% endfor %}
|
|
13
|
+
</select>
|
|
14
|
+
<span class="result-count mono">{{ pagination.total }} MEMBERS</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<div class="table-wrap">
|
|
19
|
+
<table class="table table-members">
|
|
20
|
+
<thead>
|
|
21
|
+
<tr>
|
|
22
|
+
<th class="col-check"><input type="checkbox" class="row-check-all" title="Select all on this page"></th>
|
|
23
|
+
<th>Member</th>
|
|
24
|
+
<th>Role</th>
|
|
25
|
+
<th>Status</th>
|
|
26
|
+
<th class="num">Apps</th>
|
|
27
|
+
<th>Last Login</th>
|
|
28
|
+
<th class="actions-col">Actions</th>
|
|
29
|
+
</tr>
|
|
30
|
+
</thead>
|
|
31
|
+
<tbody id="member-rows">
|
|
32
|
+
{% for u in users %}
|
|
33
|
+
<tr data-user-id="{{ u.id }}">
|
|
34
|
+
<td class="col-check"><input type="checkbox" class="row-check" value="{{ u.id }}"></td>
|
|
35
|
+
<td class="member-cell">
|
|
36
|
+
<div class="member-name-row">
|
|
37
|
+
<strong>{{ u.name or u.email }}</strong>
|
|
38
|
+
{{ render_user_flair_badge(u.id) }}
|
|
39
|
+
</div>
|
|
40
|
+
<div class="member-email mono">{{ u.email }}</div>
|
|
41
|
+
</td>
|
|
42
|
+
<td>
|
|
43
|
+
{% if u.role == 'admin' %}
|
|
44
|
+
<span class="badge badge-accent">Admin</span>
|
|
45
|
+
{% else %}
|
|
46
|
+
<span class="badge badge-primary">{{ u.role }}</span>
|
|
47
|
+
{% endif %}
|
|
48
|
+
</td>
|
|
49
|
+
<td>
|
|
50
|
+
{% if u.is_online() %}
|
|
51
|
+
<span class="badge badge-success badge-dot">Online</span>
|
|
52
|
+
{% else %}
|
|
53
|
+
<span class="badge badge-muted badge-dot">Offline</span>
|
|
54
|
+
{% endif %}
|
|
55
|
+
{% if not u.is_active %}
|
|
56
|
+
<span class="badge badge-danger">Disabled</span>
|
|
57
|
+
{% endif %}
|
|
58
|
+
</td>
|
|
59
|
+
<td class="num">{{ access_counts.get(u.id, 0) }}<span class="text-faint">/{{ total_apps }}</span></td>
|
|
60
|
+
<td class="mono">{{ u.last_login_at.strftime('%Y-%m-%d %H:%M') if u.last_login_at else 'Never' }}</td>
|
|
61
|
+
<td class="actions-col">
|
|
62
|
+
<button type="button" class="btn btn-ghost btn-sm" onclick="openMemberDrawer({{ u.id }})" title="Edit member">Edit</button>
|
|
63
|
+
</td>
|
|
64
|
+
</tr>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="members-footer">
|
|
71
|
+
<div class="bulk-bar" id="bulk-bar">
|
|
72
|
+
<span class="bulk-count mono" id="bulk-count">0 selected</span>
|
|
73
|
+
<form class="bulk-form" action="/admin/members/bulk" method="POST" id="bulk-form">
|
|
74
|
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
75
|
+
<div id="bulk-ids"></div>
|
|
76
|
+
<select name="action" class="form-select">
|
|
77
|
+
<option value="set_role">Set role…</option>
|
|
78
|
+
<option value="deactivate">Deactivate</option>
|
|
79
|
+
<option value="activate">Activate</option>
|
|
80
|
+
<option value="delete">Delete</option>
|
|
81
|
+
</select>
|
|
82
|
+
<select name="role" class="form-select" id="bulk-role" style="display:none;">
|
|
83
|
+
{% for r in roles %}
|
|
84
|
+
<option value="{{ r.slug }}">{{ r.name }}</option>
|
|
85
|
+
{% endfor %}
|
|
86
|
+
</select>
|
|
87
|
+
<button type="submit" class="btn btn-primary btn-sm">Apply</button>
|
|
88
|
+
</form>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="pagination mono">
|
|
91
|
+
{% if pagination.has_prev %}
|
|
92
|
+
<a href="#" hx-get="/admin/members?page={{ pagination.prev_num }}&q={{ q }}&role={{ role }}&sort={{ sort }}&per_page={{ per_page }}" hx-target="#members-table-wrap" hx-push-url="true">‹ prev</a>
|
|
93
|
+
{% else %}
|
|
94
|
+
<span class="text-faint">‹ prev</span>
|
|
95
|
+
{% endif %}
|
|
96
|
+
<span class="page-indicator">{{ pagination.page }} / {{ pagination.pages or 1 }}</span>
|
|
97
|
+
{% if pagination.has_next %}
|
|
98
|
+
<a href="#" hx-get="/admin/members?page={{ pagination.next_num }}&q={{ q }}&role={{ role }}&sort={{ sort }}&per_page={{ per_page }}" hx-target="#members-table-wrap" hx-push-url="true">next ›</a>
|
|
99
|
+
{% else %}
|
|
100
|
+
<span class="text-faint">next ›</span>
|
|
101
|
+
{% endif %}
|
|
102
|
+
<select name="per_page" class="form-select page-size" hx-get="/admin/members" hx-target="#members-table-wrap" hx-include="[name='q'],[name='role'],[name='sort']" hx-push-url="true" hx-trigger="change">
|
|
103
|
+
{% for n in per_page_choices %}
|
|
104
|
+
<option value="{{ n }}" {% if per_page == n %}selected{% endif %}>{{ n }}</option>
|
|
105
|
+
{% endfor %}
|
|
106
|
+
</select>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
{% block title %}{{ app.name }} — Admin{% endblock %}
|
|
3
|
+
|
|
4
|
+
{% block content %}
|
|
5
|
+
<div class="page-head">
|
|
6
|
+
<div class="breadcrumb mono">admin / apps / {{ app.slug }}</div>
|
|
7
|
+
<h1 class="page-title">{{ app.name }}</h1>
|
|
8
|
+
<div class="page-subtitle mono">
|
|
9
|
+
{{ app.app_type|upper }} · {{ app.slug }} · v{{ manifest.get('version', '—') }}
|
|
10
|
+
{% if app.is_active %}<span class="badge badge-success badge-dot">active</span>{% else %}<span class="badge badge-danger">inactive</span>{% endif %}
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="admin-layout">
|
|
15
|
+
<aside class="admin-sidebar">
|
|
16
|
+
<div class="admin-nav">
|
|
17
|
+
<a href="/admin" class="admin-nav-link" data-tab="apps">← Back to Admin</a>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="admin-nav-group">
|
|
20
|
+
<div class="admin-nav-group-title">APP PANELS</div>
|
|
21
|
+
{% for p in panels %}
|
|
22
|
+
<a href="#panel-{{ p.panel_id }}" class="admin-nav-link" data-panel="{{ p.panel_id }}">{{ p.label }}</a>
|
|
23
|
+
{% else %}
|
|
24
|
+
<div class="text-faint" style="font-size:0.8rem; padding:0.5rem 0.75rem;">No panels declared</div>
|
|
25
|
+
{% endfor %}
|
|
26
|
+
</div>
|
|
27
|
+
</aside>
|
|
28
|
+
|
|
29
|
+
<div class="admin-content">
|
|
30
|
+
{% if not panels and not schema %}
|
|
31
|
+
<div class="card">
|
|
32
|
+
<div class="empty-state">
|
|
33
|
+
<h2>No admin panels declared</h2>
|
|
34
|
+
<p>This app's manifest does not declare any <code>admin_sections</code> or <code>settings_schema</code>.</p>
|
|
35
|
+
<p class="text-faint mono" style="font-size:0.75rem;">Add them to <code>manifest.json</code> and reinstall to enable per-app admin settings.</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
{% endif %}
|
|
39
|
+
|
|
40
|
+
{% for item in rendered %}
|
|
41
|
+
<section class="tab-panel app-panel" id="panel-{{ item.panel.panel_id }}">
|
|
42
|
+
<div class="card">
|
|
43
|
+
<div class="card-header">
|
|
44
|
+
<h2 class="card-title mb-0">{{ item.panel.label }}</h2>
|
|
45
|
+
{% if item.panel.endpoint %}
|
|
46
|
+
<span class="badge badge-accent">custom panel</span>
|
|
47
|
+
{% else %}
|
|
48
|
+
<span class="badge badge-primary">generated form</span>
|
|
49
|
+
{% endif %}
|
|
50
|
+
</div>
|
|
51
|
+
<div class="card-body">
|
|
52
|
+
{{ item.html|safe }}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
{% endfor %}
|
|
57
|
+
|
|
58
|
+
{% if schema %}
|
|
59
|
+
<section class="tab-panel app-panel" id="panel-settings">
|
|
60
|
+
<div class="card">
|
|
61
|
+
<div class="card-header">
|
|
62
|
+
<h2 class="card-title mb-0">Settings</h2>
|
|
63
|
+
<span class="badge badge-primary">generated form</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div class="card-body">
|
|
66
|
+
<form method="POST" action="/admin/apps/{{ app.slug }}/settings" class="schema-form">
|
|
67
|
+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
68
|
+
{% for desc in schema %}
|
|
69
|
+
{% set key = desc.key %}
|
|
70
|
+
{% set label = desc.label or key %}
|
|
71
|
+
{% set vtype = desc.type or 'json' %}
|
|
72
|
+
{% set value = configs.get(key, desc.default) %}
|
|
73
|
+
<div class="form-group">
|
|
74
|
+
<label class="form-label">{{ label }}</label>
|
|
75
|
+
{% if vtype == 'boolean' %}
|
|
76
|
+
<label class="flex-center" style="gap:0.5rem;cursor:pointer;">
|
|
77
|
+
<input type="checkbox" name="cfg_{{ key }}" value="1" {% if value %}checked{% endif %} class="perm-toggle">
|
|
78
|
+
<span class="text-muted" style="font-size:0.8rem;">enabled</span>
|
|
79
|
+
</label>
|
|
80
|
+
{% elif vtype == 'color' %}
|
|
81
|
+
<input type="color" name="cfg_{{ key }}" value="{{ value or '#F5A524' }}" style="height:36px;width:60px;padding:0;border:none;background:none;cursor:pointer;">
|
|
82
|
+
{% elif vtype == 'integer' %}
|
|
83
|
+
<input type="number" name="cfg_{{ key }}" value="{{ value if value is not none else '' }}" class="form-input">
|
|
84
|
+
{% elif vtype == 'textarea' %}
|
|
85
|
+
<textarea name="cfg_{{ key }}" class="form-textarea" rows="3">{{ value if value is not none else '' }}</textarea>
|
|
86
|
+
{% else %}
|
|
87
|
+
<input type="text" name="cfg_{{ key }}" value="{{ value if value is not none else '' }}" class="form-input">
|
|
88
|
+
{% endif %}
|
|
89
|
+
</div>
|
|
90
|
+
{% endfor %}
|
|
91
|
+
<div class="action-group mt-2">
|
|
92
|
+
<button type="submit" class="btn btn-primary btn-sm">Save Settings</button>
|
|
93
|
+
</div>
|
|
94
|
+
</form>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</section>
|
|
98
|
+
{% endif %}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
{% endblock %}
|
|
102
|
+
|
|
103
|
+
{% block extra_js %}
|
|
104
|
+
<script>
|
|
105
|
+
// Panel sub-nav switching
|
|
106
|
+
document.querySelectorAll('.admin-nav-link[data-panel]').forEach(function (link) {
|
|
107
|
+
link.addEventListener('click', function (e) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
var pid = link.getAttribute('data-panel');
|
|
110
|
+
document.querySelectorAll('.admin-nav-link[data-panel]').forEach(function (a) {
|
|
111
|
+
a.classList.toggle('active', a.getAttribute('data-panel') === pid);
|
|
112
|
+
});
|
|
113
|
+
document.querySelectorAll('.app-panel').forEach(function (p) {
|
|
114
|
+
p.classList.toggle('active', p.id === 'panel-' + pid);
|
|
115
|
+
});
|
|
116
|
+
history.replaceState(null, '', '#' + pid);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
(function () {
|
|
120
|
+
var hash = window.location.hash.replace('#', '');
|
|
121
|
+
if (hash && document.getElementById('panel-' + hash)) {
|
|
122
|
+
document.querySelectorAll('.admin-nav-link[data-panel]').forEach(function (a) {
|
|
123
|
+
a.classList.toggle('active', a.getAttribute('data-panel') === hash);
|
|
124
|
+
});
|
|
125
|
+
document.querySelectorAll('.app-panel').forEach(function (p) {
|
|
126
|
+
p.classList.toggle('active', p.id === 'panel-' + hash);
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
var first = document.querySelector('.app-panel');
|
|
130
|
+
if (first) first.classList.add('active');
|
|
131
|
+
var firstLink = document.querySelector('.admin-nav-link[data-panel]');
|
|
132
|
+
if (firstLink) firstLink.classList.add('active');
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
</script>
|
|
136
|
+
{% endblock %}
|