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.
Files changed (89) hide show
  1. appmanager/__init__.py +52 -0
  2. appmanager/admin/__init__.py +3 -0
  3. appmanager/admin/app_installer.py +739 -0
  4. appmanager/admin/members.py +95 -0
  5. appmanager/admin/registry.py +372 -0
  6. appmanager/admin/routes.py +900 -0
  7. appmanager/api/__init__.py +5 -0
  8. appmanager/api/routes.py +373 -0
  9. appmanager/app_config.py +101 -0
  10. appmanager/auth/__init__.py +3 -0
  11. appmanager/auth/oauth.py +77 -0
  12. appmanager/auth/routes.py +224 -0
  13. appmanager/auth/utils.py +175 -0
  14. appmanager/bridge.py +90 -0
  15. appmanager/cli.py +1171 -0
  16. appmanager/config.py +58 -0
  17. appmanager/database.py +120 -0
  18. appmanager/extension.py +198 -0
  19. appmanager/extensions.py +225 -0
  20. appmanager/health.py +133 -0
  21. appmanager/hooks.py +187 -0
  22. appmanager/middleware.py +256 -0
  23. appmanager/models.py +307 -0
  24. appmanager/py.typed +1 -0
  25. appmanager/sdk.py +30 -0
  26. appmanager/security.py +227 -0
  27. appmanager/security_scanner.py +582 -0
  28. appmanager/signals.py +21 -0
  29. appmanager/static/css/app.css +808 -0
  30. appmanager/static/vendor/fonts/fonts.css +288 -0
  31. appmanager/static/vendor/fonts/jetbrainsmono-0.woff2 +0 -0
  32. appmanager/static/vendor/fonts/jetbrainsmono-1.woff2 +0 -0
  33. appmanager/static/vendor/fonts/jetbrainsmono-10.woff2 +0 -0
  34. appmanager/static/vendor/fonts/jetbrainsmono-11.woff2 +0 -0
  35. appmanager/static/vendor/fonts/jetbrainsmono-12.woff2 +0 -0
  36. appmanager/static/vendor/fonts/jetbrainsmono-13.woff2 +0 -0
  37. appmanager/static/vendor/fonts/jetbrainsmono-14.woff2 +0 -0
  38. appmanager/static/vendor/fonts/jetbrainsmono-15.woff2 +0 -0
  39. appmanager/static/vendor/fonts/jetbrainsmono-16.woff2 +0 -0
  40. appmanager/static/vendor/fonts/jetbrainsmono-17.woff2 +0 -0
  41. appmanager/static/vendor/fonts/jetbrainsmono-18.woff2 +0 -0
  42. appmanager/static/vendor/fonts/jetbrainsmono-19.woff2 +0 -0
  43. appmanager/static/vendor/fonts/jetbrainsmono-2.woff2 +0 -0
  44. appmanager/static/vendor/fonts/jetbrainsmono-20.woff2 +0 -0
  45. appmanager/static/vendor/fonts/jetbrainsmono-21.woff2 +0 -0
  46. appmanager/static/vendor/fonts/jetbrainsmono-22.woff2 +0 -0
  47. appmanager/static/vendor/fonts/jetbrainsmono-23.woff2 +0 -0
  48. appmanager/static/vendor/fonts/jetbrainsmono-3.woff2 +0 -0
  49. appmanager/static/vendor/fonts/jetbrainsmono-4.woff2 +0 -0
  50. appmanager/static/vendor/fonts/jetbrainsmono-5.woff2 +0 -0
  51. appmanager/static/vendor/fonts/jetbrainsmono-6.woff2 +0 -0
  52. appmanager/static/vendor/fonts/jetbrainsmono-7.woff2 +0 -0
  53. appmanager/static/vendor/fonts/jetbrainsmono-8.woff2 +0 -0
  54. appmanager/static/vendor/fonts/jetbrainsmono-9.woff2 +0 -0
  55. appmanager/static/vendor/fonts/spacegrotesk-0.woff2 +0 -0
  56. appmanager/static/vendor/fonts/spacegrotesk-1.woff2 +0 -0
  57. appmanager/static/vendor/fonts/spacegrotesk-10.woff2 +0 -0
  58. appmanager/static/vendor/fonts/spacegrotesk-11.woff2 +0 -0
  59. appmanager/static/vendor/fonts/spacegrotesk-2.woff2 +0 -0
  60. appmanager/static/vendor/fonts/spacegrotesk-3.woff2 +0 -0
  61. appmanager/static/vendor/fonts/spacegrotesk-4.woff2 +0 -0
  62. appmanager/static/vendor/fonts/spacegrotesk-5.woff2 +0 -0
  63. appmanager/static/vendor/fonts/spacegrotesk-6.woff2 +0 -0
  64. appmanager/static/vendor/fonts/spacegrotesk-7.woff2 +0 -0
  65. appmanager/static/vendor/fonts/spacegrotesk-8.woff2 +0 -0
  66. appmanager/static/vendor/fonts/spacegrotesk-9.woff2 +0 -0
  67. appmanager/static/vendor/htmx/htmx.min.js +1 -0
  68. appmanager/templates/admin/_member_drawer.html +98 -0
  69. appmanager/templates/admin/_members_table.html +108 -0
  70. appmanager/templates/admin/app_detail.html +136 -0
  71. appmanager/templates/admin/dashboard.html +902 -0
  72. appmanager/templates/admin/flairs.html +90 -0
  73. appmanager/templates/admin/permissions.html +88 -0
  74. appmanager/templates/admin/roles.html +119 -0
  75. appmanager/templates/admin/users.html +156 -0
  76. appmanager/templates/auth/login.html +58 -0
  77. appmanager/templates/auth/profile.html +44 -0
  78. appmanager/templates/base.html +76 -0
  79. appmanager/templates/errors/401.html +19 -0
  80. appmanager/templates/errors/403.html +19 -0
  81. appmanager/templates/errors/404.html +18 -0
  82. appmanager/templates/errors/500.html +19 -0
  83. appmanager/templates/index.html +54 -0
  84. appmanager_server-0.2.0.dist-info/METADATA +320 -0
  85. appmanager_server-0.2.0.dist-info/RECORD +89 -0
  86. appmanager_server-0.2.0.dist-info/WHEEL +5 -0
  87. appmanager_server-0.2.0.dist-info/entry_points.txt +5 -0
  88. appmanager_server-0.2.0.dist-info/licenses/LICENSE +21 -0
  89. appmanager_server-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,90 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Manage User Flairs - AppManager{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <style>
7
+ .flair-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 1rem; }
8
+ .table-container { overflow-x: auto; margin-top: 1rem; }
9
+ table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
10
+ th, td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color); }
11
+ th { background: rgba(15, 23, 42, 0.8); color: var(--text-muted); font-weight: 600; text-transform: uppercase; font-size: 0.75rem; }
12
+ .flair-badge-preview { display: inline-block; padding: 3px 10px; border-radius: 9999px; font-size: 0.8rem; font-weight: 600; }
13
+ .preset-btn { cursor: pointer; border: 1px solid transparent; padding: 0.4rem 0.8rem; border-radius: 6px; font-size: 0.8rem; font-weight: 500; transition: transform 0.1s; }
14
+ .preset-btn:hover { transform: translateY(-1px); }
15
+ </style>
16
+ {% endblock %}
17
+
18
+ {% block content %}
19
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
20
+ <div>
21
+ <h1 style="font-size: 1.8rem; font-weight: 700;">🏷️ Manage User Flairs & Titles</h1>
22
+ <p style="color: var(--text-muted);">Assign custom title badges and colors to platform members</p>
23
+ </div>
24
+ <div style="display: flex; gap: 0.5rem;">
25
+ <a href="/admin/users" class="btn btn-secondary">👥 Members List</a>
26
+ <a href="/admin" class="btn btn-secondary">↩ Admin Dashboard</a>
27
+ </div>
28
+ </div>
29
+
30
+ <div class="flair-grid">
31
+ <!-- Presets Card -->
32
+ <div class="card">
33
+ <h3 style="font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; color: #a855f7;">Preset Flair Badges</h3>
34
+ <p style="font-size: 0.85rem; color: var(--text-muted); margin-bottom: 1rem;">Click any preset flair to quickly apply it in the user assignment table on the right.</p>
35
+ <div style="display: flex; flex-direction: column; gap: 0.6rem;">
36
+ {% for preset in presets %}
37
+ <div style="display: flex; justify-content: space-between; align-items: center; background: rgba(15, 23, 42, 0.6); padding: 0.6rem 0.8rem; border-radius: 8px; border: 1px solid var(--border-color);">
38
+ <span class="flair-badge-preview" style="background: {{ preset.color }}22; color: {{ preset.color }}; border: 1px solid {{ preset.color }}44;">{{ preset.title }}</span>
39
+ <span style="font-size: 0.75rem; color: var(--text-muted);">Color: <code>{{ preset.color }}</code></span>
40
+ </div>
41
+ {% endfor %}
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Assigned Flairs Card -->
46
+ <div class="card">
47
+ <h3 style="font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem;">Member Flair Assignments</h3>
48
+ <div class="table-container">
49
+ <table>
50
+ <thead>
51
+ <tr>
52
+ <th>Member</th>
53
+ <th>Current Flair</th>
54
+ <th>Assign / Update</th>
55
+ </tr>
56
+ </thead>
57
+ <tbody>
58
+ {% for u in users %}
59
+ {% set f = user_flairs.get(u.id) %}
60
+ <tr>
61
+ <td>
62
+ <strong>{{ u.name or u.email }}</strong>
63
+ <div style="font-size: 0.75rem; color: var(--text-muted);">{{ u.email }}</div>
64
+ </td>
65
+ <td>
66
+ {% if f and f.title %}
67
+ <span class="flair-badge-preview" style="background: {{ f.color }}22; color: {{ f.color }}; border: 1px solid {{ f.color }}44;">{{ f.title }}</span>
68
+ {% else %}
69
+ <span style="font-size: 0.75rem; color: var(--text-muted);">No Flair</span>
70
+ {% endif %}
71
+ </td>
72
+ <td>
73
+ <form action="/admin/flairs" method="POST" style="display: flex; gap: 0.4rem; align-items: center;">
74
+ <input type="hidden" name="user_id" value="{{ u.id }}">
75
+ <input type="text" name="flair_title" placeholder="Flair title..." value="{{ f.title if f else '' }}" class="form-input" style="width: 130px; padding: 0.3rem; font-size: 0.8rem; background: #0f172a; border: 1px solid #334155; color: white; border-radius: 4px;">
76
+ <input type="color" name="flair_color" value="{{ f.color if f else '#a855f7' }}" style="height: 28px; width: 32px; padding: 0; border: none; background: none; cursor: pointer;">
77
+ <button type="submit" name="action" value="save" class="btn btn-primary" style="padding: 0.3rem 0.6rem; font-size: 0.75rem;">Save</button>
78
+ {% if f and f.title %}
79
+ <button type="submit" name="action" value="clear" class="btn btn-secondary" style="padding: 0.3rem 0.5rem; font-size: 0.75rem; color: #f87171;" title="Clear Flair">✕</button>
80
+ {% endif %}
81
+ </form>
82
+ </td>
83
+ </tr>
84
+ {% endfor %}
85
+ </tbody>
86
+ </table>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ {% endblock %}
@@ -0,0 +1,88 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}App Access Permissions - AppManager{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <style>
7
+ .matrix-table {
8
+ width: 100%;
9
+ border-collapse: collapse;
10
+ text-align: center;
11
+ font-size: 0.9rem;
12
+ }
13
+ .matrix-table th, .matrix-table td {
14
+ padding: 0.75rem 1rem;
15
+ border: 1px solid var(--border-color);
16
+ }
17
+ .matrix-table th {
18
+ background: rgba(15, 23, 42, 0.8);
19
+ }
20
+ .matrix-table th.user-col {
21
+ text-align: left;
22
+ }
23
+ .checkbox-cell {
24
+ transform: scale(1.2);
25
+ cursor: pointer;
26
+ }
27
+ </style>
28
+ {% endblock %}
29
+
30
+ {% block content %}
31
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
32
+ <div>
33
+ <h1 style="font-size: 1.8rem; font-weight: 700;">App Access Permissions Matrix</h1>
34
+ <p style="color: var(--text-muted);">Manage which users have access to specific installed Flask sub-apps</p>
35
+ </div>
36
+ <a href="/admin" class="btn btn-secondary">&larr; Back to Admin Dashboard</a>
37
+ </div>
38
+
39
+ <div class="card">
40
+ {% if users and apps %}
41
+ <form action="/admin/permissions" method="POST">
42
+ <div style="overflow-x: auto; margin-bottom: 1.5rem;">
43
+ <table class="matrix-table">
44
+ <thead>
45
+ <tr>
46
+ <th class="user-col">User / Email</th>
47
+ <th class="user-col">Role</th>
48
+ {% for app in apps %}
49
+ <th>
50
+ <div>{{ app.name }}</div>
51
+ <div style="font-size: 0.75rem; color: var(--text-muted); font-weight: normal;">/apps/{{ app.slug }}</div>
52
+ </th>
53
+ {% endfor %}
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ {% for u in users %}
58
+ <tr>
59
+ <td class="user-col">
60
+ <strong>{{ u.name or u.email }}</strong><br>
61
+ <span style="font-size: 0.8rem; color: var(--text-muted);">{{ u.email }}</span>
62
+ </td>
63
+ <td class="user-col">
64
+ <span style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; background: rgba(99, 102, 241, 0.2); color: #818cf8;">{{ u.role }}</span>
65
+ </td>
66
+ {% for app in apps %}
67
+ {% set is_checked = perms_map.get((u.id, app.id), True) %}
68
+ <td>
69
+ {% if u.is_admin() %}
70
+ <span style="color: #4ade80; font-size: 0.8rem;">Full Access (Admin)</span>
71
+ {% else %}
72
+ <input type="checkbox" name="perm_{{ u.id }}_{{ app.id }}" value="1" class="checkbox-cell" {% if is_checked %}checked{% endif %}>
73
+ {% endif %}
74
+ </td>
75
+ {% endfor %}
76
+ </tr>
77
+ {% endfor %}
78
+ </tbody>
79
+ </table>
80
+ </div>
81
+
82
+ <button type="submit" class="btn btn-primary">Save Permission Changes</button>
83
+ </form>
84
+ {% else %}
85
+ <p style="color: var(--text-muted);">You need both registered users and installed sub-apps to manage permissions.</p>
86
+ {% endif %}
87
+ </div>
88
+ {% endblock %}
@@ -0,0 +1,119 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Role Management - AppManager{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <style>
7
+ .roles-grid {
8
+ display: grid;
9
+ grid-template-columns: 1fr 2fr;
10
+ gap: 1.5rem;
11
+ margin-bottom: 2rem;
12
+ }
13
+ @media (max-width: 900px) {
14
+ .roles-grid { grid-template-columns: 1fr; }
15
+ }
16
+ .table-container { overflow-x: auto; margin-top: 0.5rem; }
17
+ table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
18
+ th, td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color); }
19
+ th { background: rgba(15, 23, 42, 0.8); color: var(--text-muted); font-weight: 600; text-transform: uppercase; font-size: 0.75rem; }
20
+ .role-badge { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
21
+ .badge-system { background: rgba(168, 85, 247, 0.2); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
22
+ .badge-custom { background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.3); }
23
+ .form-group { margin-bottom: 1.2rem; }
24
+ .form-label { display: block; font-size: 0.85rem; font-weight: 500; margin-bottom: 0.4rem; color: var(--text-main); }
25
+ .form-input { width: 100%; padding: 0.6rem 0.8rem; border-radius: 6px; border: 1px solid var(--border-color); background-color: rgba(15, 23, 42, 0.8); color: var(--text-main); font-size: 0.9rem; }
26
+ .form-input:focus { outline: none; border-color: var(--primary); }
27
+ </style>
28
+ {% endblock %}
29
+
30
+ {% block content %}
31
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
32
+ <div>
33
+ <h1 style="font-size: 1.8rem; font-weight: 700;">Role Management</h1>
34
+ <p style="color: var(--text-muted);">Create custom member roles, assign permissions, and organize portal access levels</p>
35
+ </div>
36
+ <div style="display: flex; gap: 0.5rem;">
37
+ <a href="/admin/users" class="btn btn-secondary">👥 Member List</a>
38
+ <a href="/admin/permissions" class="btn btn-secondary">📊 Permissions Matrix</a>
39
+ <a href="/admin" class="btn btn-secondary">↩ Admin Dashboard</a>
40
+ </div>
41
+ </div>
42
+
43
+ <div class="roles-grid">
44
+ <!-- Create Role Card -->
45
+ <div class="card">
46
+ <h2 style="font-size: 1.2rem; font-weight: 600; margin-bottom: 1rem;">➕ Create New Role</h2>
47
+ <form action="/admin/roles/create" method="POST">
48
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
49
+ <div class="form-group">
50
+ <label class="form-label">Role Display Name</label>
51
+ <input type="text" name="name" class="form-input" placeholder="e.g. Content Editor" required>
52
+ </div>
53
+ <div class="form-group">
54
+ <label class="form-label">Role Slug (identifier)</label>
55
+ <input type="text" name="slug" class="form-input" placeholder="e.g. content-editor (optional)">
56
+ </div>
57
+ <div class="form-group">
58
+ <label class="form-label">Description</label>
59
+ <textarea name="description" class="form-input" style="height: 80px; resize: vertical;" placeholder="Describe what responsibilities or access this role grants..."></textarea>
60
+ </div>
61
+ <button type="submit" class="btn btn-primary" style="width: 100%;">Create Custom Role</button>
62
+ </form>
63
+ </div>
64
+
65
+ <!-- Defined Roles Table -->
66
+ <div class="card">
67
+ <h2 style="font-size: 1.2rem; font-weight: 600; margin-bottom: 1rem;">🛡️ Configured Roles ({{ roles|length }})</h2>
68
+ <div class="table-container">
69
+ <table>
70
+ <thead>
71
+ <tr>
72
+ <th>Role Name</th>
73
+ <th>Slug</th>
74
+ <th>Type</th>
75
+ <th>Members</th>
76
+ <th>Description</th>
77
+ <th>Actions</th>
78
+ </tr>
79
+ </thead>
80
+ <tbody>
81
+ {% for r in roles %}
82
+ <tr>
83
+ <td>
84
+ <strong>{{ r.name }}</strong>
85
+ </td>
86
+ <td><code>{{ r.slug }}</code></td>
87
+ <td>
88
+ {% if r.is_system %}
89
+ <span class="role-badge badge-system">System Core</span>
90
+ {% else %}
91
+ <span class="role-badge badge-custom">Custom</span>
92
+ {% endif %}
93
+ </td>
94
+ <td>
95
+ <span style="font-weight: 600; color: #38bdf8;">{{ role_counts.get(r.slug, 0) }}</span> user(s)
96
+ </td>
97
+ <td style="color: var(--text-muted); font-size: 0.85rem; max-width: 200px;">
98
+ {{ r.description or 'No description provided.' }}
99
+ </td>
100
+ <td>
101
+ <div style="display: flex; gap: 0.4rem; align-items: center;">
102
+ {% if not r.is_system %}
103
+ <form action="/admin/roles/{{ r.id }}/delete" method="POST" onsubmit="return confirm('Delete custom role {{ r.name }}? Members assigned this role will be reassigned to standard user.');" style="margin:0;">
104
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
105
+ <button type="submit" class="btn btn-danger" style="padding: 0.25rem 0.5rem; font-size: 0.75rem;">Delete</button>
106
+ </form>
107
+ {% else %}
108
+ <span style="font-size: 0.75rem; color: var(--text-muted); opacity: 0.6;">Protected</span>
109
+ {% endif %}
110
+ </div>
111
+ </td>
112
+ </tr>
113
+ {% endfor %}
114
+ </tbody>
115
+ </table>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ {% endblock %}
@@ -0,0 +1,156 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}User Management & Activity Tracking - AppManager{% endblock %}
4
+
5
+ {% block extra_css %}
6
+ <style>
7
+ .table-container { overflow-x: auto; margin-top: 1rem; }
8
+ table { width: 100%; border-collapse: collapse; text-align: left; font-size: 0.9rem; }
9
+ th, td { padding: 0.75rem 1rem; border-bottom: 1px solid var(--border-color); }
10
+ th { background: rgba(15, 23, 42, 0.8); color: var(--text-muted); font-weight: 600; text-transform: uppercase; font-size: 0.75rem; }
11
+ .status-badge { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
12
+ .badge-online { background: rgba(34, 197, 94, 0.2); color: #4ade80; }
13
+ .badge-offline { background: rgba(148, 163, 184, 0.2); color: #94a3b8; }
14
+ .badge-admin { background: rgba(168, 85, 247, 0.2); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
15
+ .badge-user { background: rgba(99, 102, 241, 0.2); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.3); }
16
+ .edit-box { background: rgba(15, 23, 42, 0.6); padding: 1rem; border-radius: 8px; border: 1px solid var(--border-color); margin-top: 0.5rem; }
17
+ </style>
18
+ {% endblock %}
19
+
20
+ {% block content %}
21
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
22
+ <div>
23
+ <h1 style="font-size: 1.8rem; font-weight: 700;">User Management & Member Tracking</h1>
24
+ <p style="color: var(--text-muted);">Monitor real-time online status, login metrics, and manage user roles and permissions</p>
25
+ </div>
26
+ <div style="display: flex; gap: 0.5rem;">
27
+ <a href="/admin/roles" class="btn btn-secondary">🛡️ Manage Roles</a>
28
+ <a href="/admin/flairs" class="btn btn-secondary">🏷️ Manage Flairs</a>
29
+ <a href="/admin/permissions" class="btn btn-secondary">📊 Matrix View</a>
30
+ <a href="/admin" class="btn btn-secondary">↩ Admin Dashboard</a>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="card">
35
+ <h2 style="font-size: 1.2rem; font-weight: 600; margin-bottom: 1rem;">Registered Members</h2>
36
+ <div class="table-container">
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ <th>User / Email</th>
41
+ <th>Status</th>
42
+ <th>Role</th>
43
+ <th>Registration Date</th>
44
+ <th>Last Login</th>
45
+ <th>Logins</th>
46
+ <th>Last IP</th>
47
+ <th>Actions</th>
48
+ </tr>
49
+ </thead>
50
+ <tbody>
51
+ {% for u in users %}
52
+ <tr>
53
+ <td>
54
+ <strong>{{ u.name or u.email }}</strong> {{ render_user_flair_badge(u.id) }}
55
+ <div style="font-size: 0.8rem; color: var(--text-muted);">{{ u.email }}</div>
56
+ </td>
57
+ <td>
58
+ {% if u.is_online() %}
59
+ <span class="status-badge badge-online">🟢 Online</span>
60
+ {% else %}
61
+ <span class="status-badge badge-offline">⚪ Offline</span>
62
+ {% endif %}
63
+ {% if not u.is_active %}
64
+ <span class="status-badge" style="background: rgba(239, 68, 68, 0.2); color: #f87171;">Disabled</span>
65
+ {% endif %}
66
+ </td>
67
+ <td>
68
+ {% if u.role == 'admin' %}
69
+ <span class="status-badge badge-admin">Admin</span>
70
+ {% else %}
71
+ <span class="status-badge badge-user">User</span>
72
+ {% endif %}
73
+ </td>
74
+ <td>{{ u.created_at.strftime('%Y-%m-%d') if u.created_at else 'N/A' }}</td>
75
+ <td>{{ u.last_login_at.strftime('%Y-%m-%d %H:%M') if u.last_login_at else 'Never' }}</td>
76
+ <td><code>{{ u.login_count }}</code></td>
77
+ <td><code>{{ u.last_ip or 'N/A' }}</code></td>
78
+ <td>
79
+ <button onclick="document.getElementById('edit-user-{{ u.id }}').style.display = (document.getElementById('edit-user-{{ u.id }}').style.display === 'none' ? 'block' : 'none')" class="btn btn-secondary" style="padding: 0.3rem 0.6rem; font-size: 0.8rem;">Edit User</button>
80
+ </td>
81
+ </tr>
82
+ <tr id="edit-user-{{ u.id }}" style="display: none; background: rgba(30, 41, 59, 0.4);">
83
+ <td colspan="8">
84
+ <div class="edit-box">
85
+ <h4 style="margin-bottom: 0.75rem; font-size: 0.95rem; color: #38bdf8;">Edit Member Details & App Access: {{ u.email }}</h4>
86
+ {% set curr_flair = get_user_flair(u.id) %}
87
+ <form action="/admin/users/{{ u.id }}/edit" method="POST">
88
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
89
+ <div>
90
+ <label style="display:block; font-size:0.8rem; margin-bottom:0.3rem;">Display Name</label>
91
+ <input type="text" name="name" value="{{ u.name or '' }}" class="form-input" style="width:100%; padding:0.4rem; background:#0f172a; border:1px solid #334155; color:white; border-radius:4px;">
92
+ </div>
93
+ <div>
94
+ <label style="display:block; font-size:0.8rem; margin-bottom:0.3rem;">Assigned Role</label>
95
+ <select name="role" style="width:100%; padding:0.4rem; background:#0f172a; border:1px solid #334155; color:white; border-radius:4px;">
96
+ {% if roles %}
97
+ {% for r in roles %}
98
+ <option value="{{ r.slug }}" {% if u.role == r.slug %}selected{% endif %}>{{ r.name }}</option>
99
+ {% endfor %}
100
+ {% else %}
101
+ <option value="user" {% if u.role == 'user' %}selected{% endif %}>User</option>
102
+ <option value="admin" {% if u.role == 'admin' %}selected{% endif %}>Admin</option>
103
+ {% endif %}
104
+ </select>
105
+ </div>
106
+ <div>
107
+ <label style="display:block; font-size:0.8rem; margin-bottom:0.3rem;">Account Status</label>
108
+ <select name="is_active" style="width:100%; padding:0.4rem; background:#0f172a; border:1px solid #334155; color:white; border-radius:4px;">
109
+ <option value="1" {% if u.is_active %}selected{% endif %}>Active</option>
110
+ <option value="0" {% if not u.is_active %}selected{% endif %}>Disabled</option>
111
+ </select>
112
+ </div>
113
+ </div>
114
+
115
+ <div style="background: rgba(15, 23, 42, 0.7); padding: 0.75rem; border-radius: 6px; border: 1px solid #334155; margin-bottom: 1rem;">
116
+ <label style="display:block; font-size:0.85rem; font-weight:600; margin-bottom:0.5rem; color: #a855f7;">🏷️ User Flair Title & Badge Color:</label>
117
+ <div style="display: flex; gap: 0.75rem; align-items: center; flex-wrap: wrap;">
118
+ <input type="text" id="flair_title_{{ u.id }}" name="flair_title" value="{{ curr_flair.title if curr_flair else '' }}" placeholder="e.g. 🚀 Lead Developer" style="padding: 0.4rem; background: #0f172a; border: 1px solid #334155; color: white; border-radius: 4px; font-size: 0.85rem; flex: 1; min-width: 160px;">
119
+ <input type="color" id="flair_color_{{ u.id }}" name="flair_color" value="{{ curr_flair.color if curr_flair else '#a855f7' }}" style="height: 32px; width: 40px; padding:0; border:none; background:none; cursor:pointer;" title="Pick Badge Color">
120
+ <div style="display: flex; gap: 0.3rem; align-items: center;">
121
+ {% for preset in DEFAULT_PRESET_FLAIRS %}
122
+ <button type="button" onclick="document.getElementById('flair_title_{{ u.id }}').value='{{ preset.title }}'; document.getElementById('flair_color_{{ u.id }}').value='{{ preset.color }}';" class="status-badge" style="background: {{ preset.color }}22; color: {{ preset.color }}; border: 1px solid {{ preset.color }}44; cursor: pointer; font-size: 0.7rem;">{{ preset.title }}</button>
123
+ {% endfor %}
124
+ <button type="button" onclick="document.getElementById('flair_title_{{ u.id }}').value='';" class="status-badge" style="background: rgba(148,163,184,0.2); color: #94a3b8; border: 1px solid rgba(148,163,184,0.3); cursor: pointer; font-size: 0.7rem;">Clear</button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+
130
+ <div style="margin-bottom: 1rem;">
131
+ <label style="display:block; font-size:0.85rem; font-weight:600; margin-bottom:0.5rem; color: #94a3b8;">Accessible Sub-Applications:</label>
132
+ <div style="display: flex; flex-wrap: wrap; gap: 1rem;">
133
+ {% for app in apps %}
134
+ {% set has_perm = perms_map.get((u.id, app.id), True) %}
135
+ <label style="display: flex; align-items: center; gap: 0.4rem; font-size: 0.85rem; cursor: pointer;">
136
+ <input type="checkbox" name="perm_{{ app.id }}" value="1" {% if has_perm or u.is_admin() %}checked{% endif %} {% if u.is_admin() %}disabled{% endif %}>
137
+ <span>{{ app.name }}</span>
138
+ </label>
139
+ {% endfor %}
140
+ </div>
141
+ </div>
142
+
143
+ <div style="display: flex; gap: 0.5rem;">
144
+ <button type="submit" class="btn btn-primary" style="padding: 0.4rem 1rem; font-size: 0.85rem;">Save User Changes</button>
145
+ <button type="button" onclick="document.getElementById('edit-user-{{ u.id }}').style.display='none'" class="btn btn-secondary" style="padding: 0.4rem 1rem; font-size: 0.85rem;">Cancel</button>
146
+ </div>
147
+ </form>
148
+ </div>
149
+ </td>
150
+ </tr>
151
+ {% endfor %}
152
+ </tbody>
153
+ </table>
154
+ </div>
155
+ </div>
156
+ {% endblock %}
@@ -0,0 +1,58 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Log In — AppManager{% endblock %}
4
+
5
+ {% block content %}
6
+ <div style="max-width:440px; margin:3rem auto;">
7
+ <div class="card boot-item" style="padding:2.5rem; border-radius:var(--radius-lg);">
8
+ <h1 class="page-title" style="text-align:center; font-size:1.5rem;">Welcome to AppManager</h1>
9
+ <p class="text-muted" style="text-align:center; font-size:0.9rem; margin-bottom:2rem;">Sign in or register with Email Magic Link or Google</p>
10
+
11
+ {% if magic_sent %}
12
+ <div class="alert alert-success">
13
+ <strong>Magic Link Sent!</strong> Check <code>{{ email }}</code> for your one-time login link.
14
+ </div>
15
+
16
+ {% if dev_magic_url %}
17
+ <div class="alert alert-warning" style="margin-top:1rem;">
18
+ <div>
19
+ <strong>Developer Mode Preview:</strong><br>
20
+ <span style="font-size:0.85rem;">SMTP is not configured. Use this magic link directly to log in:</span>
21
+ </div>
22
+ <a href="{{ dev_magic_url }}" class="btn btn-primary btn-block" style="margin-top:0.5rem;">Click to Log In Instantly &rarr;</a>
23
+ </div>
24
+ {% endif %}
25
+
26
+ <p style="text-align:center; margin-top:1.5rem;">
27
+ <a href="/auth/login" style="color: var(--accent); text-decoration:none;">&larr; Back to sign in</a>
28
+ </p>
29
+ {% else %}
30
+ {% if google_configured %}
31
+ <a href="/auth/google" class="btn btn-block" style="background:#fff; color:#1f2937; font-weight:600; margin-bottom:1rem;">
32
+ <svg width="18" height="18" viewBox="0 0 24 24" style="margin-right:8px;">
33
+ <path fill="#4285F4" d="M23.745 12.27c0-.7-.06-1.4-.19-2.07H12v4.51h6.6c-.29 1.52-1.14 2.82-2.4 3.68v3.05h3.88c2.27-2.09 3.665-5.17 3.665-9.17z"/>
34
+ <path fill="#34A853" d="M12 24c3.24 0 5.95-1.08 7.93-2.91l-3.88-3.05c-1.08.72-2.45 1.16-4.05 1.16-3.12 0-5.77-2.1-6.72-4.93H1.28v3.15C3.31 21.39 7.37 24 12 24z"/>
35
+ <path fill="#FBBC05" d="M5.28 14.27c-.25-.72-.38-1.49-.38-2.27s.13-1.55.38-2.27V6.58H1.28C.46 8.21 0 10.05 0 12s.46 3.79 1.28 5.42l4-3.15z"/>
36
+ <path fill="#EA4335" d="M12 4.75c1.77 0 3.35.61 4.6 1.8l3.42-3.42C17.95 1.19 15.24 0 12 0 7.37 0 3.31 2.61 1.28 6.58l4 3.15c.95-2.83 3.6-4.98 6.72-4.98z"/>
37
+ </svg>
38
+ Continue with Google
39
+ </a>
40
+ <div class="divider" style="margin:1.5rem 0;">
41
+ <span style="flex:1; border-bottom:1px solid var(--border);"></span>
42
+ <span style="padding:0 0.75rem;">OR EMAIL MAGIC LINK</span>
43
+ <span style="flex:1; border-bottom:1px solid var(--border);"></span>
44
+ </div>
45
+ {% endif %}
46
+
47
+ <form action="/auth/magic-link" method="POST">
48
+ <input type="hidden" name="next" value="{{ next }}">
49
+ <div class="form-group">
50
+ <label class="form-label" for="email">Email Address</label>
51
+ <input type="email" id="email" name="email" class="form-input" placeholder="you@example.com" required autofocus>
52
+ </div>
53
+ <button type="submit" class="btn btn-primary btn-block btn-lg">Send Magic Login Link &rarr;</button>
54
+ </form>
55
+ {% endif %}
56
+ </div>
57
+ </div>
58
+ {% endblock %}
@@ -0,0 +1,44 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Profile — AppManager{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="card mb-4 boot-item">
7
+ <div class="flex" style="align-items:center; gap:1.5rem;">
8
+ <div class="avatar" style="width:56px; height:56px; border-radius:var(--radius); background:var(--accent); display:flex; align-items:center; justify-content:center; font-family:var(--mono); font-size:1.5rem; font-weight:700; color:var(--accent-contrast); flex-shrink:0;">
9
+ {{ user.name[0].upper() if user.name else user.email[0].upper() }}
10
+ </div>
11
+ <div>
12
+ <h1 class="page-title" style="font-size:1.4rem;">{{ user.name or user.email }} {{ render_user_flair_badge(user.id) }}</h1>
13
+ <p class="text-muted mono" style="font-size:0.85rem;">{{ user.email }}</p>
14
+ <span class="badge badge-accent mt-1">role: {{ user.role }}</span>
15
+ </div>
16
+ </div>
17
+ </div>
18
+
19
+ <h2 class="card-title mb-3">Your Accessible Applications</h2>
20
+ {% if apps %}
21
+ <div class="grid grid-apps">
22
+ {% for app in apps %}
23
+ <div class="app-card boot-item" style="animation-delay: {{ loop.index0 * 60 }}ms;">
24
+ <div class="app-body">
25
+ <h2 class="app-title">{{ app.name }}</h2>
26
+ <p class="app-meta">/apps/{{ app.slug }}</p>
27
+ </div>
28
+ <div class="app-footer">
29
+ {% if app.has_web_ui %}
30
+ <a href="/apps/{{ app.slug }}/" class="btn btn-secondary btn-sm">Launch &rarr;</a>
31
+ {% else %}
32
+ <span class="text-faint" style="font-size:0.75rem;">Extension</span>
33
+ {% endif %}
34
+ </div>
35
+ </div>
36
+ {% endfor %}
37
+ </div>
38
+ {% else %}
39
+ <div class="empty-state">
40
+ <h2>No Accessible Applications</h2>
41
+ <p>You do not currently have access to any installed apps.</p>
42
+ </div>
43
+ {% endif %}
44
+ {% endblock %}
@@ -0,0 +1,76 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}AppManager{% endblock %}</title>
7
+ <link rel="stylesheet" href="{{ url_for('static', filename='vendor/fonts/fonts.css') }}">
8
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}">
9
+ {% block extra_css %}{% endblock %}
10
+ {{ render_slot('head_assets') if render_slot is defined else '' }}
11
+ </head>
12
+ <body>
13
+ <header class="site-header">
14
+ <div class="nav-container">
15
+ <a href="/" class="brand">
16
+ <div class="brand-icon">AM</div>
17
+ <span>AppManager</span>
18
+ </a>
19
+ <nav>
20
+ <ul class="nav-links">
21
+ {% if g.current_user %}
22
+ <li><a href="/" class="nav-link">Applications</a></li>
23
+ <li><a href="/auth/profile" class="nav-link">Profile</a></li>
24
+ {% if g.current_user.is_admin() %}
25
+ <li><a href="/admin" class="nav-link">Admin</a></li>
26
+ {% endif %}
27
+ {{ render_slot('nav_item', user=g.current_user) if render_slot is defined else '' }}
28
+ <li><a href="/auth/logout" class="btn btn-secondary btn-sm">Log Out</a></li>
29
+ {% else %}
30
+ {{ render_slot('nav_item', user=None) if render_slot is defined else '' }}
31
+ <li><a href="/auth/login" class="btn btn-primary btn-sm">Log In / Register</a></li>
32
+ {% endif %}
33
+ </ul>
34
+ </nav>
35
+ <div class="topbar-instruments">
36
+ <span class="instrument" title="UTC time"><span class="blink">▮</span><span id="utc-clock">--:--:--</span></span>
37
+ </div>
38
+ </div>
39
+ </header>
40
+
41
+ <main class="container">
42
+ <div class="flash-messages">
43
+ {% with messages = get_flashed_messages(with_categories=true) %}
44
+ {% if messages %}
45
+ {% for category, message in messages %}
46
+ <div class="alert alert-{{ category }}">{{ message }}</div>
47
+ {% endfor %}
48
+ {% endif %}
49
+ {% endwith %}
50
+ </div>
51
+
52
+ {% block content %}{% endblock %}
53
+ </main>
54
+
55
+ <footer class="site-footer">
56
+ <p>&copy; 2026 AppManager &bull; Flask Sub-App Container Framework</p>
57
+ </footer>
58
+
59
+ <script src="{{ url_for('static', filename='vendor/htmx/htmx.min.js') }}"></script>
60
+ <script>
61
+ // Live UTC clock — the page feels "powered on"
62
+ (function () {
63
+ var el = document.getElementById('utc-clock');
64
+ if (!el) return;
65
+ function tick() {
66
+ var d = new Date();
67
+ var p = function (n) { return String(n).padStart(2, '0'); };
68
+ el.textContent = p(d.getUTCHours()) + ':' + p(d.getUTCMinutes()) + ':' + p(d.getUTCSeconds()) + ' UTC';
69
+ }
70
+ tick();
71
+ setInterval(tick, 1000);
72
+ })();
73
+ </script>
74
+ {% block extra_js %}{% endblock %}
75
+ </body>
76
+ </html>