@mp-consulting/homebridge-unifi-access 1.0.7 → 1.0.8
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.
- package/homebridge-ui/public/index.html +231 -212
- package/homebridge-ui/public/lib/kit.css +253 -0
- package/homebridge-ui/public/lib/kit.js +133 -0
- package/homebridge-ui/public/modules/constants.js +5 -7
- package/homebridge-ui/public/modules/controllers.js +10 -8
- package/homebridge-ui/public/modules/discovery.js +4 -4
- package/homebridge-ui/public/modules/dom-helpers.js +1 -1
- package/homebridge-ui/public/modules/feature-options/renderer.js +7 -7
- package/homebridge-ui/public/styles.css +0 -63
- package/package.json +4 -2
|
@@ -1,267 +1,286 @@
|
|
|
1
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" data-bs-theme="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>UniFi Access</title>
|
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
|
9
|
+
<link rel="stylesheet" href="lib/kit.css">
|
|
10
|
+
<link rel="stylesheet" href="styles.css">
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div class="container py-3">
|
|
2
14
|
|
|
3
|
-
<div
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
15
|
+
<div class="mp-header mb-3">
|
|
16
|
+
<span class="mp-header-icon" style="color: #0099ff;"><i class="bi bi-door-open-fill"></i></span>
|
|
17
|
+
<div>
|
|
18
|
+
<h1 class="mp-header-title">UniFi Access</h1>
|
|
19
|
+
<p class="mp-header-subtitle">Manage your UniFi Access controllers and devices in HomeKit</p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
8
22
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
<!-- Screen: Discovery -->
|
|
24
|
+
<div id="discoveryScreen">
|
|
25
|
+
<h5><i class="bi bi-search"></i> Discover Your Controller</h5>
|
|
26
|
+
<p class="text-muted">Scan your network for UniFi Access controllers, or enter the address manually.</p>
|
|
13
27
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
<div class="alert alert-info mb-3">
|
|
29
|
+
<i class="bi bi-info-circle"></i> Ensure your UniFi console is powered on and connected to the same network.
|
|
30
|
+
</div>
|
|
17
31
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
<div class="row g-2 mb-3">
|
|
33
|
+
<div class="col-12">
|
|
34
|
+
<button id="discoverBtn" class="btn btn-secondary">
|
|
35
|
+
<span id="discoverSpinner" class="spinner-border spinner-border-sm" style="display: none;"></span>
|
|
36
|
+
<span id="discoverBtnText"><i class="bi bi-search"></i> Discover Controllers</span>
|
|
37
|
+
</button>
|
|
38
|
+
<button id="manualEntryBtn" class="btn btn-outline-primary">
|
|
39
|
+
<i class="bi bi-keyboard"></i> Manual Entry
|
|
40
|
+
</button>
|
|
41
|
+
<button id="cancelDiscoveryBtn" class="btn btn-primary" style="display: none;">Cancel</button>
|
|
42
|
+
</div>
|
|
28
43
|
</div>
|
|
29
|
-
</div>
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
<div id="deviceList" style="display: none;">
|
|
46
|
+
<div id="deviceListContainer"></div>
|
|
47
|
+
</div>
|
|
33
48
|
</div>
|
|
34
|
-
</div>
|
|
35
49
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
<!-- Screen: Setup Controller (Add/Edit) -->
|
|
51
|
+
<div id="setupScreen">
|
|
52
|
+
<h5><i class="bi bi-shield-lock"></i> <span id="setupTitle">Add UniFi Access Controller</span></h5>
|
|
53
|
+
<p class="text-muted" id="setupSubtitle">Enter your UniFi Access controller details and login credentials.</p>
|
|
40
54
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
<form id="setupForm" novalidate>
|
|
56
|
+
<div class="mb-3 row">
|
|
57
|
+
<label for="inputAddress" class="col-sm-3 col-form-label"><i class="bi bi-server"></i> Controller</label>
|
|
58
|
+
<div class="col-sm-9">
|
|
59
|
+
<input type="text" class="form-control" id="inputAddress" placeholder="Hostname or IP (e.g. unvr.local)" required>
|
|
60
|
+
<small class="form-text text-muted">The address of your UniFi Access controller.</small>
|
|
61
|
+
</div>
|
|
47
62
|
</div>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
63
|
+
<div class="mb-3 row">
|
|
64
|
+
<label for="inputUsername" class="col-sm-3 col-form-label"><i class="bi bi-person"></i> Username</label>
|
|
65
|
+
<div class="col-sm-9">
|
|
66
|
+
<input type="text" class="form-control" id="inputUsername" autocomplete="username" placeholder="Local user account username" required>
|
|
67
|
+
</div>
|
|
53
68
|
</div>
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
<div class="mb-3 row">
|
|
70
|
+
<label for="inputPassword" class="col-sm-3 col-form-label"><i class="bi bi-lock"></i> Password</label>
|
|
71
|
+
<div class="col-sm-9">
|
|
72
|
+
<input type="password" class="form-control" id="inputPassword" autocomplete="current-password" placeholder="Local user account password" required>
|
|
73
|
+
</div>
|
|
59
74
|
</div>
|
|
60
|
-
</div>
|
|
61
75
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
<div id="setupError" class="alert alert-danger" style="display: none;">
|
|
77
|
+
<i class="bi bi-exclamation-circle"></i> <span id="setupErrorText"></span>
|
|
78
|
+
</div>
|
|
65
79
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
<div class="alert alert-info">
|
|
81
|
+
<i class="bi bi-info-circle"></i>
|
|
82
|
+
<strong>Local account required.</strong> Ubiquiti.com/UI.com accounts are not supported. Create one via the <em>Admins & Users</em> tab in your UniFi console.
|
|
83
|
+
</div>
|
|
70
84
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
<hr>
|
|
86
|
+
<div class="col-12">
|
|
87
|
+
<button id="saveControllerBtn" class="btn btn-success" type="submit">
|
|
88
|
+
<i class="bi bi-floppy"></i> Save Controller
|
|
89
|
+
</button>
|
|
90
|
+
<button id="cancelSetupBtn" class="btn btn-secondary" type="button">Cancel</button>
|
|
91
|
+
</div>
|
|
92
|
+
</form>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<!-- Screen: Controllers List (Main) -->
|
|
96
|
+
<div id="controllersScreen">
|
|
97
|
+
<h5><i class="bi bi-hdd-network"></i> Configured Controllers</h5>
|
|
98
|
+
<div id="noControllersMessage" class="alert alert-info mb-3">
|
|
99
|
+
<i class="bi bi-info-circle"></i> No controllers configured yet. Add one to get started.
|
|
100
|
+
</div>
|
|
101
|
+
<ul id="controllersList" class="list-group mb-3"></ul>
|
|
102
|
+
<div class="d-flex gap-2">
|
|
103
|
+
<button id="addControllerBtn" class="btn btn-primary">
|
|
104
|
+
<i class="bi bi-plus"></i> Add Controller
|
|
105
|
+
</button>
|
|
106
|
+
<button id="supportBtn" class="btn btn-outline-secondary">
|
|
107
|
+
<i class="bi bi-life-preserver"></i> Support
|
|
75
108
|
</button>
|
|
76
|
-
<button id="cancelSetupBtn" class="btn btn-secondary" type="button">Cancel</button>
|
|
77
109
|
</div>
|
|
78
|
-
</form>
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<!-- Screen: Controllers List (Main) -->
|
|
82
|
-
<div id="controllersScreen">
|
|
83
|
-
<h5><i class="fas fa-network-wired"></i> Configured Controllers</h5>
|
|
84
|
-
<div id="noControllersMessage" class="alert alert-info mb-3">
|
|
85
|
-
<i class="fas fa-info-circle"></i> No controllers configured yet. Add one to get started.
|
|
86
|
-
</div>
|
|
87
|
-
<ul id="controllersList" class="list-group mb-3"></ul>
|
|
88
|
-
<div class="d-flex gap-2">
|
|
89
|
-
<button id="addControllerBtn" class="btn btn-primary">
|
|
90
|
-
<i class="fas fa-plus"></i> Add Controller
|
|
91
|
-
</button>
|
|
92
|
-
<button id="supportBtn" class="btn btn-outline-secondary">
|
|
93
|
-
<i class="fas fa-life-ring"></i> Support
|
|
94
|
-
</button>
|
|
95
110
|
</div>
|
|
96
|
-
</div>
|
|
97
111
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
<!-- Screen: Feature Options (split layout) -->
|
|
113
|
+
<div id="featureOptionsScreen">
|
|
114
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
115
|
+
<h5 class="mb-0"><i class="bi bi-sliders"></i> Feature Options</h5>
|
|
116
|
+
<button id="backFromOptionsBtn" class="btn btn-secondary btn-sm">
|
|
117
|
+
<i class="bi bi-arrow-left"></i> Back
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
<div class="row">
|
|
122
|
+
<!-- Left sidebar: scope -->
|
|
123
|
+
<div class="col-sm-4">
|
|
124
|
+
<div class="scope-sidebar">
|
|
125
|
+
<!-- Scope selector -->
|
|
126
|
+
<div class="card mb-3">
|
|
127
|
+
<div class="card-body py-2">
|
|
128
|
+
<label class="form-label mb-1 text-muted"><small><i class="bi bi-layers"></i> Scope</small></label>
|
|
129
|
+
<select class="form-select form-select-sm" id="scopeSelect"></select>
|
|
130
|
+
</div>
|
|
116
131
|
</div>
|
|
117
|
-
</div>
|
|
118
132
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
<!-- Visual cascade -->
|
|
134
|
+
<div class="card mb-3">
|
|
135
|
+
<div class="card-body py-3" id="scopeCascade">
|
|
136
|
+
<div class="scope-cascade">
|
|
137
|
+
<div class="scope-level" data-scope="global">
|
|
138
|
+
<div class="scope-dot"><i class="bi bi-globe"></i></div>
|
|
139
|
+
<div class="scope-text">
|
|
140
|
+
<div class="scope-label">Global</div>
|
|
141
|
+
<div class="scope-hint">All devices</div>
|
|
142
|
+
</div>
|
|
128
143
|
</div>
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
<div class="scope-connector"><div class="scope-connector-line"></div></div>
|
|
145
|
+
<div class="scope-level" data-scope="controller">
|
|
146
|
+
<div class="scope-dot"><i class="bi bi-server"></i></div>
|
|
147
|
+
<div class="scope-text">
|
|
148
|
+
<div class="scope-label">Controller</div>
|
|
149
|
+
<div class="scope-hint">Overrides global</div>
|
|
150
|
+
</div>
|
|
136
151
|
</div>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
152
|
+
<div class="scope-connector"><div class="scope-connector-line"></div></div>
|
|
153
|
+
<div class="scope-level" data-scope="device">
|
|
154
|
+
<div class="scope-dot"><i class="bi bi-cpu"></i></div>
|
|
155
|
+
<div class="scope-text">
|
|
156
|
+
<div class="scope-label">Device</div>
|
|
157
|
+
<div class="scope-hint">Final value</div>
|
|
158
|
+
</div>
|
|
144
159
|
</div>
|
|
145
160
|
</div>
|
|
146
161
|
</div>
|
|
147
162
|
</div>
|
|
148
|
-
</div>
|
|
149
163
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
164
|
+
<!-- Device info panel -->
|
|
165
|
+
<div id="deviceInfoPanel" class="card mb-3" style="display: none;">
|
|
166
|
+
<div class="card-body py-2">
|
|
167
|
+
<div class="mb-2"><small class="text-muted d-block">Model</small><strong id="infoModel"></strong></div>
|
|
168
|
+
<div class="mb-2"><small class="text-muted d-block">MAC</small><code id="infoMac" style="font-size:0.75rem"></code></div>
|
|
169
|
+
<div class="mb-2"><small class="text-muted d-block">IP</small><code id="infoIp"></code></div>
|
|
170
|
+
<div><small class="text-muted d-block">Status</small><span id="infoStatus"></span></div>
|
|
171
|
+
</div>
|
|
157
172
|
</div>
|
|
158
|
-
</div>
|
|
159
173
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
<!-- Legend -->
|
|
175
|
+
<div class="card mb-3">
|
|
176
|
+
<div class="card-body py-2">
|
|
177
|
+
<small class="text-muted d-block mb-2"><i class="bi bi-palette"></i> Legend</small>
|
|
178
|
+
<div class="d-flex flex-column gap-1" style="font-size: 0.75rem;">
|
|
179
|
+
<div class="d-flex align-items-center gap-2">
|
|
180
|
+
<span class="scope-legend-bar" style="background: #ffc107;"></span> Set at global
|
|
181
|
+
</div>
|
|
182
|
+
<div class="d-flex align-items-center gap-2">
|
|
183
|
+
<span class="scope-legend-bar" style="background: #198754;"></span> Set at controller
|
|
184
|
+
</div>
|
|
185
|
+
<div class="d-flex align-items-center gap-2">
|
|
186
|
+
<span class="scope-legend-bar" style="background: #0dcaf0;"></span> Set at device
|
|
187
|
+
</div>
|
|
173
188
|
</div>
|
|
174
189
|
</div>
|
|
175
190
|
</div>
|
|
176
|
-
</div>
|
|
177
191
|
|
|
192
|
+
</div>
|
|
178
193
|
</div>
|
|
179
|
-
</div>
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
<!-- Right main: options -->
|
|
196
|
+
<div class="col-sm-8">
|
|
197
|
+
<!-- Search & filter -->
|
|
198
|
+
<div class="d-flex align-items-center gap-2 mb-3">
|
|
199
|
+
<div class="input-group flex-grow-1">
|
|
200
|
+
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
201
|
+
<input type="text" class="form-control" placeholder="Search options..." id="optionsSearch">
|
|
202
|
+
<button class="btn btn-outline-secondary" type="button" id="clearSearchBtn"><i class="bi bi-x"></i></button>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="form-check form-switch mb-0 flex-shrink-0" style="min-width: 110px;">
|
|
205
|
+
<input class="form-check-input" type="checkbox" id="modifiedOnlyToggle">
|
|
206
|
+
<label class="form-check-label text-muted text-nowrap" for="modifiedOnlyToggle"><small>Modified <span id="modifiedSummary"></span></small></label>
|
|
207
|
+
</div>
|
|
193
208
|
</div>
|
|
194
|
-
</div>
|
|
195
209
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
210
|
+
<!-- Loading -->
|
|
211
|
+
<div id="optionsLoading" class="text-center my-4">
|
|
212
|
+
<div class="spinner-border text-primary" role="status">
|
|
213
|
+
<span class="visually-hidden">Loading...</span>
|
|
214
|
+
</div>
|
|
215
|
+
<p class="mt-2 text-muted">Loading options...</p>
|
|
200
216
|
</div>
|
|
201
|
-
<p class="mt-2 text-muted">Loading options...</p>
|
|
202
|
-
</div>
|
|
203
217
|
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
<!-- Options container -->
|
|
219
|
+
<div id="optionsContainer" class="mb-3"></div>
|
|
206
220
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
<!-- Unsaved changes notice -->
|
|
222
|
+
<div class="alert alert-warning mb-3" id="unsavedChanges" style="display: none;">
|
|
223
|
+
<i class="bi bi-exclamation-triangle"></i> You have unsaved changes. Click <strong>Save</strong> in Homebridge to apply.
|
|
224
|
+
</div>
|
|
210
225
|
</div>
|
|
211
226
|
</div>
|
|
212
227
|
</div>
|
|
213
|
-
</div>
|
|
214
228
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
<!-- Screen: Support -->
|
|
230
|
+
<div id="supportScreen">
|
|
231
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
232
|
+
<h5 class="mb-0"><i class="bi bi-life-preserver"></i> Support</h5>
|
|
233
|
+
<button id="backFromSupportBtn" class="btn btn-secondary btn-sm">
|
|
234
|
+
<i class="bi bi-arrow-left"></i> Back
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
223
237
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
238
|
+
<div class="card mb-3">
|
|
239
|
+
<div class="card-header bg-transparent"><i class="bi bi-heart-fill"></i> About This Plugin</div>
|
|
240
|
+
<div class="card-body">
|
|
241
|
+
<p class="card-text mb-0">If you find this plugin useful, please consider <a target="_blank" href="https://github.com/mp-consulting/homebridge-unifi-access">starring the project on GitHub</a>.</p>
|
|
242
|
+
</div>
|
|
228
243
|
</div>
|
|
229
|
-
</div>
|
|
230
244
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
<div class="card mb-3">
|
|
246
|
+
<div class="card-header bg-transparent"><i class="bi bi-book"></i> Getting Started</div>
|
|
247
|
+
<div class="list-group list-group-flush">
|
|
248
|
+
<a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/README.md#installation" target="_blank" class="list-group-item list-group-item-action">
|
|
249
|
+
<i class="bi bi-download text-primary"></i> <strong>Installation</strong>
|
|
250
|
+
<small class="text-muted d-block">Installing this plugin, including system requirements.</small>
|
|
251
|
+
</a>
|
|
252
|
+
<a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/docs/feature-options.md" target="_blank" class="list-group-item list-group-item-action">
|
|
253
|
+
<i class="bi bi-sliders text-primary"></i> <strong>Feature Options</strong>
|
|
254
|
+
<small class="text-muted d-block">Granular options to configure at a controller or device level.</small>
|
|
255
|
+
</a>
|
|
256
|
+
<a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/docs/mqtt.md" target="_blank" class="list-group-item list-group-item-action">
|
|
257
|
+
<i class="bi bi-broadcast text-primary"></i> <strong>MQTT</strong>
|
|
258
|
+
<small class="text-muted d-block">How to configure MQTT support.</small>
|
|
259
|
+
</a>
|
|
260
|
+
<a href="https://github.com/mp-consulting/homebridge-unifi-access/blob/main/CHANGELOG.md" target="_blank" class="list-group-item list-group-item-action">
|
|
261
|
+
<i class="bi bi-clock-history text-primary"></i> <strong>Changelog</strong>
|
|
262
|
+
<small class="text-muted d-block">Changes and release history.</small>
|
|
263
|
+
</a>
|
|
264
|
+
</div>
|
|
250
265
|
</div>
|
|
251
|
-
</div>
|
|
252
266
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
267
|
+
<div class="card mb-3">
|
|
268
|
+
<div class="card-header bg-transparent"><i class="bi bi-life-preserver"></i> Get Help</div>
|
|
269
|
+
<div class="list-group list-group-flush">
|
|
270
|
+
<a href="https://discord.gg/QXqfHEW" target="_blank" class="list-group-item list-group-item-action">
|
|
271
|
+
<i class="bi bi-discord text-primary"></i> <strong>Discord Support Channel</strong>
|
|
272
|
+
</a>
|
|
273
|
+
<a href="https://github.com/mp-consulting/homebridge-unifi-access/issues/new/choose" target="_blank" class="list-group-item list-group-item-action">
|
|
274
|
+
<i class="bi bi-github text-primary"></i> <strong>Create a Developer Support Request</strong>
|
|
275
|
+
</a>
|
|
276
|
+
</div>
|
|
262
277
|
</div>
|
|
263
278
|
</div>
|
|
279
|
+
|
|
264
280
|
</div>
|
|
265
|
-
</div>
|
|
266
281
|
|
|
267
|
-
<script
|
|
282
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
|
283
|
+
<script src="lib/kit.js"></script>
|
|
284
|
+
<script type="module" src="app.js"></script>
|
|
285
|
+
</body>
|
|
286
|
+
</html>
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/* @mp-consulting/homebridge-ui-kit v1.0.0 */
|
|
2
|
+
|
|
3
|
+
/* ── Design tokens ──────────────────────────────────────────────────────────
|
|
4
|
+
CSS custom properties used throughout the design system.
|
|
5
|
+
Override these in your plugin's styles.css to customize per-plugin.
|
|
6
|
+
────────────────────────────────────────────────────────────────────────── */
|
|
7
|
+
|
|
8
|
+
:root {
|
|
9
|
+
/* Brand */
|
|
10
|
+
--mp-primary: #4f46e5;
|
|
11
|
+
--mp-primary-rgb: 79, 70, 229;
|
|
12
|
+
--mp-primary-hover: #4338ca;
|
|
13
|
+
--mp-primary-subtle: rgba(79, 70, 229, 0.12);
|
|
14
|
+
|
|
15
|
+
/* Status */
|
|
16
|
+
--mp-status-online: #10b981;
|
|
17
|
+
--mp-status-online-glow: rgba(16, 185, 129, 0.35);
|
|
18
|
+
--mp-status-offline: #ef4444;
|
|
19
|
+
--mp-status-checking: #94a3b8;
|
|
20
|
+
|
|
21
|
+
/* Surfaces */
|
|
22
|
+
--mp-surface: rgba(255, 255, 255, 0.03);
|
|
23
|
+
--mp-border: rgba(255, 255, 255, 0.08);
|
|
24
|
+
|
|
25
|
+
/* Shape */
|
|
26
|
+
--mp-radius: 0.5rem;
|
|
27
|
+
--mp-shadow-hover: 0 0.5rem 1rem rgba(0, 0, 0, 0.3);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* ── Bootstrap 5 overrides ──────────────────────────────────────────────────
|
|
31
|
+
Remap Bootstrap components to use brand tokens.
|
|
32
|
+
────────────────────────────────────────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
/* Primary button */
|
|
35
|
+
.btn-primary {
|
|
36
|
+
--bs-btn-bg: var(--mp-primary);
|
|
37
|
+
--bs-btn-border-color: var(--mp-primary);
|
|
38
|
+
--bs-btn-hover-bg: var(--mp-primary-hover);
|
|
39
|
+
--bs-btn-hover-border-color: var(--mp-primary-hover);
|
|
40
|
+
--bs-btn-active-bg: var(--mp-primary-hover);
|
|
41
|
+
--bs-btn-active-border-color: var(--mp-primary-hover);
|
|
42
|
+
--bs-btn-focus-shadow-rgb: var(--mp-primary-rgb);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Primary spinner */
|
|
46
|
+
.spinner-border.text-primary,
|
|
47
|
+
.spinner-grow.text-primary {
|
|
48
|
+
color: var(--mp-primary) !important;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* ── Components ─────────────────────────────────────────────────────────────
|
|
52
|
+
Reusable UI components used across all Homebridge plugins.
|
|
53
|
+
────────────────────────────────────────────────────────────────────────── */
|
|
54
|
+
|
|
55
|
+
/* ── Plugin header ──────────────────────────────────────── */
|
|
56
|
+
|
|
57
|
+
.mp-header {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 0.875rem;
|
|
61
|
+
margin-bottom: 0.5rem;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.mp-header-icon {
|
|
65
|
+
font-size: 2rem;
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
line-height: 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.mp-header-title {
|
|
71
|
+
font-size: 1.375rem;
|
|
72
|
+
font-weight: 600;
|
|
73
|
+
margin: 0;
|
|
74
|
+
line-height: 1.3;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.mp-header-subtitle {
|
|
78
|
+
font-size: 0.875rem;
|
|
79
|
+
color: var(--bs-secondary-color);
|
|
80
|
+
margin: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ── Status indicator dot ───────────────────────────────── */
|
|
84
|
+
|
|
85
|
+
.mp-status {
|
|
86
|
+
width: 8px;
|
|
87
|
+
height: 8px;
|
|
88
|
+
border-radius: 50%;
|
|
89
|
+
display: inline-block;
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
vertical-align: middle;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.mp-status-online {
|
|
95
|
+
background-color: var(--mp-status-online);
|
|
96
|
+
box-shadow: 0 0 5px var(--mp-status-online-glow);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.mp-status-offline {
|
|
100
|
+
background-color: var(--mp-status-offline);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.mp-status-checking {
|
|
104
|
+
background-color: var(--mp-status-checking);
|
|
105
|
+
animation: mp-pulse 1.5s ease-in-out infinite;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@keyframes mp-pulse {
|
|
109
|
+
0%, 100% { opacity: 1; }
|
|
110
|
+
50% { opacity: 0.35; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ── Device card ────────────────────────────────────────── */
|
|
114
|
+
|
|
115
|
+
.mp-device-card {
|
|
116
|
+
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.mp-device-card:hover {
|
|
121
|
+
transform: translateY(-1px);
|
|
122
|
+
box-shadow: var(--mp-shadow-hover) !important;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ── Settings surface card ──────────────────────────────── */
|
|
126
|
+
|
|
127
|
+
.mp-settings-card {
|
|
128
|
+
background: var(--mp-surface);
|
|
129
|
+
border: 1px solid var(--mp-border) !important;
|
|
130
|
+
border-radius: var(--mp-radius);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ── Empty state ────────────────────────────────────────── */
|
|
134
|
+
|
|
135
|
+
.mp-empty-state {
|
|
136
|
+
display: flex;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
padding: 3rem 1.5rem;
|
|
141
|
+
text-align: center;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.mp-empty-state-icon {
|
|
145
|
+
font-size: 2.5rem;
|
|
146
|
+
color: var(--bs-secondary-color);
|
|
147
|
+
margin-bottom: 0.75rem;
|
|
148
|
+
line-height: 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.mp-empty-state-title {
|
|
152
|
+
font-size: 0.9375rem;
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
color: var(--bs-secondary-color);
|
|
155
|
+
margin: 0 0 0.25rem;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.mp-empty-state-hint {
|
|
159
|
+
font-size: 0.8125rem;
|
|
160
|
+
color: var(--bs-secondary-color);
|
|
161
|
+
margin: 0;
|
|
162
|
+
opacity: 0.7;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/* ── Loading state ──────────────────────────────────────── */
|
|
166
|
+
|
|
167
|
+
.mp-loading {
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
padding: 2.5rem 1.5rem;
|
|
173
|
+
gap: 0.75rem;
|
|
174
|
+
color: var(--bs-secondary-color);
|
|
175
|
+
font-size: 0.875rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── Nav tabs ───────────────────────────────────────────── */
|
|
179
|
+
|
|
180
|
+
.mp-tabs {
|
|
181
|
+
border-bottom: 1px solid var(--mp-border);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.mp-tabs .nav-link {
|
|
185
|
+
color: var(--bs-secondary-color);
|
|
186
|
+
border: none;
|
|
187
|
+
padding: 0.75rem 1.25rem;
|
|
188
|
+
font-weight: 500;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.mp-tabs .nav-link:hover {
|
|
192
|
+
color: var(--bs-body-color);
|
|
193
|
+
border: none;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.mp-tabs .nav-link.active {
|
|
197
|
+
color: var(--mp-primary);
|
|
198
|
+
background: transparent;
|
|
199
|
+
border: none;
|
|
200
|
+
border-bottom: 2px solid var(--mp-primary);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ── Metadata label ─────────────────────────────────────── */
|
|
204
|
+
|
|
205
|
+
.mp-label {
|
|
206
|
+
font-size: 0.75rem;
|
|
207
|
+
text-transform: uppercase;
|
|
208
|
+
letter-spacing: 0.5px;
|
|
209
|
+
color: var(--bs-secondary-color);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* ── View toggling ──────────────────────────────────────── */
|
|
213
|
+
|
|
214
|
+
.mp-view {
|
|
215
|
+
display: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.mp-view.active {
|
|
219
|
+
display: block;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* ── Support footer ─────────────────────────────────────── */
|
|
223
|
+
|
|
224
|
+
.mp-footer {
|
|
225
|
+
display: flex;
|
|
226
|
+
align-items: center;
|
|
227
|
+
justify-content: center;
|
|
228
|
+
flex-wrap: wrap;
|
|
229
|
+
gap: 0.75rem 1rem;
|
|
230
|
+
padding-top: 1.5rem;
|
|
231
|
+
margin-top: 2rem;
|
|
232
|
+
border-top: 1px solid var(--mp-border);
|
|
233
|
+
font-size: 0.8125rem;
|
|
234
|
+
color: var(--bs-secondary-color);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.mp-footer a {
|
|
238
|
+
color: var(--bs-secondary-color);
|
|
239
|
+
text-decoration: none;
|
|
240
|
+
display: inline-flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 0.3rem;
|
|
243
|
+
transition: color 0.15s ease;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.mp-footer a:hover {
|
|
247
|
+
color: var(--mp-primary);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.mp-footer-sep {
|
|
251
|
+
color: var(--mp-border);
|
|
252
|
+
user-select: none;
|
|
253
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/* @mp-consulting/homebridge-ui-kit v1.0.0
|
|
2
|
+
Brand design system for Homebridge plugins
|
|
3
|
+
https://github.com/mp-consulting/homebridge-ui-kit */
|
|
4
|
+
|
|
5
|
+
(function (global) {
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
var MpKit = {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* StatusBadge — returns inline HTML for device status badges.
|
|
12
|
+
*
|
|
13
|
+
* MpKit.StatusBadge.online() → green dot + "Online"
|
|
14
|
+
* MpKit.StatusBadge.offline() → red dot + "Offline"
|
|
15
|
+
* MpKit.StatusBadge.checking() → animated dot + "Checking..."
|
|
16
|
+
* MpKit.StatusBadge.disabled() → grey badge + "Disabled"
|
|
17
|
+
*/
|
|
18
|
+
StatusBadge: {
|
|
19
|
+
online: function (label) {
|
|
20
|
+
label = label || 'Online';
|
|
21
|
+
return '<span class="badge bg-success-subtle text-success">'
|
|
22
|
+
+ '<span class="mp-status mp-status-online me-1"></span>'
|
|
23
|
+
+ label + '</span>';
|
|
24
|
+
},
|
|
25
|
+
offline: function (label) {
|
|
26
|
+
label = label || 'Offline';
|
|
27
|
+
return '<span class="badge bg-danger-subtle text-danger">'
|
|
28
|
+
+ '<span class="mp-status mp-status-offline me-1"></span>'
|
|
29
|
+
+ label + '</span>';
|
|
30
|
+
},
|
|
31
|
+
checking: function (label) {
|
|
32
|
+
label = label || 'Checking...';
|
|
33
|
+
return '<span class="badge bg-secondary-subtle text-secondary">'
|
|
34
|
+
+ '<span class="mp-status mp-status-checking me-1"></span>'
|
|
35
|
+
+ label + '</span>';
|
|
36
|
+
},
|
|
37
|
+
disabled: function (label) {
|
|
38
|
+
label = label || 'Disabled';
|
|
39
|
+
return '<span class="badge bg-secondary">' + label + '</span>';
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* EmptyState — returns HTML for an empty list placeholder.
|
|
45
|
+
*
|
|
46
|
+
* MpKit.EmptyState.render({
|
|
47
|
+
* iconClass: 'bi bi-lightbulb',
|
|
48
|
+
* title: 'No devices found',
|
|
49
|
+
* hint: 'Click Discover to scan your network',
|
|
50
|
+
* })
|
|
51
|
+
*/
|
|
52
|
+
EmptyState: {
|
|
53
|
+
render: function (opts) {
|
|
54
|
+
opts = opts || {};
|
|
55
|
+
var icon = opts.iconClass || 'bi bi-inbox';
|
|
56
|
+
var title = opts.title || 'No items';
|
|
57
|
+
var hint = opts.hint || '';
|
|
58
|
+
return '<div class="mp-empty-state">'
|
|
59
|
+
+ '<i class="' + icon + ' mp-empty-state-icon"></i>'
|
|
60
|
+
+ '<p class="mp-empty-state-title">' + title + '</p>'
|
|
61
|
+
+ (hint ? '<p class="mp-empty-state-hint">' + hint + '</p>' : '')
|
|
62
|
+
+ '</div>';
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Loading — returns HTML for a centred loading spinner with message.
|
|
68
|
+
*
|
|
69
|
+
* MpKit.Loading.render('Loading device information...')
|
|
70
|
+
*/
|
|
71
|
+
Loading: {
|
|
72
|
+
render: function (message) {
|
|
73
|
+
message = message || 'Loading...';
|
|
74
|
+
return '<div class="mp-loading">'
|
|
75
|
+
+ '<div class="spinner-border spinner-border-sm text-secondary" role="status" aria-hidden="true"></div>'
|
|
76
|
+
+ '<span>' + message + '</span>'
|
|
77
|
+
+ '</div>';
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* View — controls which .mp-view element is visible.
|
|
83
|
+
*
|
|
84
|
+
* MpKit.View.show('listView') → shows #listView, hides all others
|
|
85
|
+
* MpKit.View.show('settingsView') → shows #settingsView, hides all others
|
|
86
|
+
*/
|
|
87
|
+
View: {
|
|
88
|
+
show: function (id) {
|
|
89
|
+
document.querySelectorAll('.mp-view').forEach(function (v) {
|
|
90
|
+
v.classList.remove('active');
|
|
91
|
+
});
|
|
92
|
+
var el = document.getElementById(id);
|
|
93
|
+
if (el) { el.classList.add('active'); }
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Footer — renders support links into a .mp-footer element.
|
|
99
|
+
*
|
|
100
|
+
* MpKit.Footer.render({
|
|
101
|
+
* github: 'https://github.com/mp-consulting/homebridge-...',
|
|
102
|
+
* npm: 'https://www.npmjs.com/package/@mp-consulting/...',
|
|
103
|
+
* changelog: 'https://github.com/.../blob/main/CHANGELOG.md',
|
|
104
|
+
* })
|
|
105
|
+
*/
|
|
106
|
+
Footer: {
|
|
107
|
+
render: function (opts) {
|
|
108
|
+
opts = opts || {};
|
|
109
|
+
var target = opts.target || '.mp-footer';
|
|
110
|
+
var el = typeof target === 'string' ? document.querySelector(target) : target;
|
|
111
|
+
if (!el) { return; }
|
|
112
|
+
var links = [];
|
|
113
|
+
if (opts.github) {
|
|
114
|
+
links.push('<a href="' + opts.github + '" target="_blank" rel="noopener">'
|
|
115
|
+
+ '<i class="bi bi-github"></i>GitHub</a>');
|
|
116
|
+
}
|
|
117
|
+
if (opts.npm) {
|
|
118
|
+
links.push('<a href="' + opts.npm + '" target="_blank" rel="noopener">'
|
|
119
|
+
+ '<i class="bi bi-box-seam"></i>npm</a>');
|
|
120
|
+
}
|
|
121
|
+
if (opts.changelog) {
|
|
122
|
+
links.push('<a href="' + opts.changelog + '" target="_blank" rel="noopener">'
|
|
123
|
+
+ '<i class="bi bi-clock-history"></i>Changelog</a>');
|
|
124
|
+
}
|
|
125
|
+
el.innerHTML = links.join('<span class="mp-footer-sep">|</span>');
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
global.MpKit = MpKit;
|
|
132
|
+
|
|
133
|
+
})(window);
|
|
@@ -2,13 +2,11 @@ export const SCREENS = [ 'discoveryScreen', 'setupScreen', 'controllersScreen',
|
|
|
2
2
|
export const PLUGIN_NAME = 'UniFi Access';
|
|
3
3
|
|
|
4
4
|
export const CATEGORY_ICONS = {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Hub: 'fa-door-open',
|
|
11
|
-
Log: 'fa-file-alt',
|
|
5
|
+
AccessMethod: 'fingerprint',
|
|
6
|
+
Controller: 'server',
|
|
7
|
+
Device: 'cpu',
|
|
8
|
+
Hub: 'door-open',
|
|
9
|
+
Log: 'file-text',
|
|
12
10
|
};
|
|
13
11
|
|
|
14
12
|
export const CATEGORY_COLORS = {
|
|
@@ -33,15 +33,17 @@ export const renderControllers = () => {
|
|
|
33
33
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
|
34
34
|
<div>
|
|
35
35
|
<h6 class="mb-1">
|
|
36
|
-
<i class="
|
|
37
|
-
<span class="status-badge badge rounded-pill bg-secondary ms-2" style="font-size: 0.65rem;"
|
|
36
|
+
<i class="bi bi-server me-2"></i> ${escapeHtml(ctrl.name || ctrl.address)}
|
|
37
|
+
<span class="status-badge badge rounded-pill bg-secondary ms-2" style="font-size: 0.65rem;">
|
|
38
|
+
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
|
39
|
+
</span>
|
|
38
40
|
</h6>
|
|
39
|
-
<small class="text-muted"><i class="
|
|
41
|
+
<small class="text-muted"><i class="bi bi-hdd-network me-1"></i> ${escapeHtml(ctrl.address)}</small>
|
|
40
42
|
</div>
|
|
41
43
|
<div class="d-flex gap-1">
|
|
42
|
-
<button class="btn btn-sm btn-primary feature-options-btn"><i class="
|
|
43
|
-
<button class="btn btn-sm btn-secondary edit-ctrl-btn"><i class="
|
|
44
|
-
<button class="btn btn-sm btn-danger delete-ctrl-btn"><i class="
|
|
44
|
+
<button class="btn btn-sm btn-primary feature-options-btn"><i class="bi bi-sliders"></i> Options</button>
|
|
45
|
+
<button class="btn btn-sm btn-secondary edit-ctrl-btn"><i class="bi bi-pencil"></i> Edit</button>
|
|
46
|
+
<button class="btn btn-sm btn-danger delete-ctrl-btn"><i class="bi bi-trash"></i></button>
|
|
45
47
|
</div>
|
|
46
48
|
</div>
|
|
47
49
|
`;
|
|
@@ -70,13 +72,13 @@ export const renderControllers = () => {
|
|
|
70
72
|
badge.className = 'status-badge badge rounded-pill bg-' + colorClass + ' ms-2';
|
|
71
73
|
badge.style.fontSize = '0.65rem';
|
|
72
74
|
badge.textContent = '';
|
|
73
|
-
badge.appendChild(el('i', { className: '
|
|
75
|
+
badge.appendChild(el('i', { className: 'bi bi-' + icon }));
|
|
74
76
|
badge.appendChild(document.createTextNode(' ' + label));
|
|
75
77
|
};
|
|
76
78
|
|
|
77
79
|
homebridge.request('/checkStatus', { address: ctrl.address }).then((result) => {
|
|
78
80
|
|
|
79
|
-
updateBadge(result?.online ? 'success' : 'danger', result?.online ? 'check-circle' : '
|
|
81
|
+
updateBadge(result?.online ? 'success' : 'danger', result?.online ? 'check-circle' : 'x-circle', result?.online ? 'Online' : 'Offline');
|
|
80
82
|
}).catch(() => {
|
|
81
83
|
|
|
82
84
|
updateBadge('warning', 'question-circle', 'Unknown');
|
|
@@ -53,7 +53,7 @@ export const handleDiscover = async () => {
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
btn.disabled = false;
|
|
56
|
-
btnText.innerHTML = '<i class="
|
|
56
|
+
btnText.innerHTML = '<i class="bi bi-search"></i> Discover Controllers';
|
|
57
57
|
spinner.style.display = 'none';
|
|
58
58
|
}
|
|
59
59
|
};
|
|
@@ -69,10 +69,10 @@ const createDiscoveredDeviceItem = (device) => {
|
|
|
69
69
|
li.innerHTML = `
|
|
70
70
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
|
71
71
|
<div>
|
|
72
|
-
<h6 class="mb-1"><i class="
|
|
73
|
-
<small class="text-muted"><i class="
|
|
72
|
+
<h6 class="mb-1"><i class="bi bi-server me-2"></i> ${escapeHtml(device.name)}</h6>
|
|
73
|
+
<small class="text-muted"><i class="bi bi-hdd-network me-1"></i> ${escapeHtml(device.ip)}</small>
|
|
74
74
|
${device.model ? `<span class="badge bg-secondary ms-2">${escapeHtml(device.model)}</span>` : ''}
|
|
75
|
-
${device.mac ? `<small class="text-muted ms-2"><i class="
|
|
75
|
+
${device.mac ? `<small class="text-muted ms-2"><i class="bi bi-upc me-1"></i> ${escapeHtml(device.mac)}</small>` : ''}
|
|
76
76
|
</div>
|
|
77
77
|
<span class="badge bg-primary rounded-pill">Select</span>
|
|
78
78
|
</div>
|
|
@@ -21,7 +21,7 @@ export const setButtonLoading = (btn, loading, loadingText = 'Loading...') => {
|
|
|
21
21
|
btn.dataset.originalContent = btn.innerHTML;
|
|
22
22
|
btn.disabled = true;
|
|
23
23
|
|
|
24
|
-
btn.innerHTML = `<
|
|
24
|
+
btn.innerHTML = `<span class="spinner-border spinner-border-sm" aria-hidden="true"></span> ${loadingText}`;
|
|
25
25
|
} else {
|
|
26
26
|
|
|
27
27
|
|
|
@@ -188,7 +188,7 @@ export const renderOptions = () => {
|
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
const icon = CATEGORY_ICONS[category.name] || '
|
|
191
|
+
const icon = CATEGORY_ICONS[category.name] || 'gear';
|
|
192
192
|
const color = CATEGORY_COLORS[category.name] || '#6c757d';
|
|
193
193
|
const isOpen = state.openCategories.has(category.name) || ((modifiedCount > 0) && !state.openCategories.size) || !!searchTerm;
|
|
194
194
|
|
|
@@ -209,7 +209,7 @@ export const renderOptions = () => {
|
|
|
209
209
|
|
|
210
210
|
header.innerHTML = `
|
|
211
211
|
<div class="d-flex align-items-center">
|
|
212
|
-
<span class="category-icon" style="background-color: ${color}"><i class="
|
|
212
|
+
<span class="category-icon" style="background-color: ${color}"><i class="bi bi-${icon}"></i></span>
|
|
213
213
|
<span class="ms-2">${escapeHtml(category.description.replace(/ feature options\.?/i, ''))}</span>
|
|
214
214
|
</div>
|
|
215
215
|
<div class="d-flex align-items-center gap-2">
|
|
@@ -218,7 +218,7 @@ export const renderOptions = () => {
|
|
|
218
218
|
<span class="category-progress"><span class="category-progress-fill" style="width: ${progressPct}%"></span></span>
|
|
219
219
|
</span>
|
|
220
220
|
${modifiedCount ? `<span class="badge bg-warning text-dark">${modifiedCount}</span>` : ''}
|
|
221
|
-
<i class="
|
|
221
|
+
<i class="bi bi-chevron-${isOpen ? 'up' : 'down'} toggle-icon"></i>
|
|
222
222
|
</div>
|
|
223
223
|
`;
|
|
224
224
|
|
|
@@ -252,7 +252,7 @@ export const renderOptions = () => {
|
|
|
252
252
|
const wasOpen = body.classList.contains('open');
|
|
253
253
|
|
|
254
254
|
body.classList.toggle('open');
|
|
255
|
-
header.querySelector('.toggle-icon').className = '
|
|
255
|
+
header.querySelector('.toggle-icon').className = 'bi bi-chevron-' + (wasOpen ? 'down' : 'up') + ' toggle-icon';
|
|
256
256
|
|
|
257
257
|
if(wasOpen) {
|
|
258
258
|
|
|
@@ -319,11 +319,11 @@ const createOptionItem = (optionKey, opt, scope, category) => {
|
|
|
319
319
|
} else if(optState.scope === 'controller') {
|
|
320
320
|
|
|
321
321
|
|
|
322
|
-
scopeIndicator = '<span class="badge bg-success bg-opacity-50 ms-1"><i class="
|
|
322
|
+
scopeIndicator = '<span class="badge bg-success bg-opacity-50 ms-1"><i class="bi bi-arrow-down" style="font-size:0.55rem"></i> controller</span>';
|
|
323
323
|
} else if((optState.scope === 'global') && (scope.type !== 'global')) {
|
|
324
324
|
|
|
325
325
|
|
|
326
|
-
scopeIndicator = '<span class="badge bg-secondary bg-opacity-50 ms-1"><i class="
|
|
326
|
+
scopeIndicator = '<span class="badge bg-secondary bg-opacity-50 ms-1"><i class="bi bi-arrow-down" style="font-size:0.55rem"></i> global</span>';
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
// Display name: use option name or category name for the unnamed device option.
|
|
@@ -336,7 +336,7 @@ const createOptionItem = (optionKey, opt, scope, category) => {
|
|
|
336
336
|
|
|
337
337
|
const resetButton = optState.explicit ?
|
|
338
338
|
'<button class="btn btn-outline-secondary btn-sm reset-option-btn flex-shrink-0 mt-1" title="Reset to inherited value">' +
|
|
339
|
-
'<i class="
|
|
339
|
+
'<i class="bi bi-arrow-counterclockwise"></i></button>' :
|
|
340
340
|
'';
|
|
341
341
|
|
|
342
342
|
// Value input for options with configurable numeric values.
|
|
@@ -419,66 +419,3 @@
|
|
|
419
419
|
font-size: 0.85rem;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
-
/* ============================================================================
|
|
423
|
-
DARK MODE SUPPORT
|
|
424
|
-
============================================================================ */
|
|
425
|
-
|
|
426
|
-
@media (prefers-color-scheme: dark) {
|
|
427
|
-
:root {
|
|
428
|
-
--muted-color: #a0a0a0;
|
|
429
|
-
--card-bg: rgba(255, 255, 255, 0.05);
|
|
430
|
-
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
431
|
-
--card-hover-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
432
|
-
--card-header-bg: rgba(255, 255, 255, 0.05);
|
|
433
|
-
--card-header-border: rgba(255, 255, 255, 0.1);
|
|
434
|
-
--card-header-hover-bg: rgba(255, 255, 255, 0.08);
|
|
435
|
-
--list-item-border: rgba(255, 255, 255, 0.08);
|
|
436
|
-
--progress-bg: rgba(255, 255, 255, 0.08);
|
|
437
|
-
--scope-device-bg: rgba(13, 202, 240, 0.08);
|
|
438
|
-
--scope-controller-bg: rgba(25, 135, 84, 0.08);
|
|
439
|
-
--scope-global-bg: rgba(255, 193, 7, 0.08);
|
|
440
|
-
--grouped-bar: rgba(255, 255, 255, 0.1);
|
|
441
|
-
--default-on-bg: rgba(25, 135, 84, 0.2);
|
|
442
|
-
--default-off-bg: rgba(108, 117, 125, 0.2);
|
|
443
|
-
--scope-hint-color: #a0a0a0;
|
|
444
|
-
--connector-line: rgba(255, 255, 255, 0.1);
|
|
445
|
-
--connector-active: rgba(255, 255, 255, 0.3);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
.text-muted {
|
|
449
|
-
color: #a0a0a0 !important;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
[data-theme="dark"],
|
|
454
|
-
.dark-mode {
|
|
455
|
-
--muted-color: #a0a0a0;
|
|
456
|
-
--card-bg: rgba(255, 255, 255, 0.05);
|
|
457
|
-
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
458
|
-
--card-hover-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
459
|
-
--card-header-bg: rgba(255, 255, 255, 0.05);
|
|
460
|
-
--card-header-border: rgba(255, 255, 255, 0.1);
|
|
461
|
-
--card-header-hover-bg: rgba(255, 255, 255, 0.08);
|
|
462
|
-
--list-item-border: rgba(255, 255, 255, 0.08);
|
|
463
|
-
--progress-bg: rgba(255, 255, 255, 0.08);
|
|
464
|
-
--scope-device-bg: rgba(13, 202, 240, 0.08);
|
|
465
|
-
--scope-controller-bg: rgba(25, 135, 84, 0.08);
|
|
466
|
-
--scope-global-bg: rgba(255, 193, 7, 0.08);
|
|
467
|
-
--grouped-bar: rgba(255, 255, 255, 0.1);
|
|
468
|
-
--default-on-bg: rgba(25, 135, 84, 0.2);
|
|
469
|
-
--default-off-bg: rgba(108, 117, 125, 0.2);
|
|
470
|
-
--scope-hint-color: #a0a0a0;
|
|
471
|
-
--connector-line: rgba(255, 255, 255, 0.1);
|
|
472
|
-
--connector-active: rgba(255, 255, 255, 0.3);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
[data-theme="dark"] .text-muted,
|
|
476
|
-
.dark-mode .text-muted {
|
|
477
|
-
color: #a0a0a0 !important;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
[data-theme="dark"] .badge.bg-secondary,
|
|
481
|
-
.dark-mode .badge.bg-secondary {
|
|
482
|
-
background-color: #6c757d !important;
|
|
483
|
-
color: #fff !important;
|
|
484
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mp-consulting/homebridge-unifi-access",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"displayName": "UniFi Access",
|
|
5
5
|
"description": "Complete HomeKit support for UniFi Access devices — locks, doorbells, sensors, and readers — with automatic device discovery and realtime events.",
|
|
6
6
|
"author": "Mickael Palma",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
],
|
|
47
47
|
"scripts": {
|
|
48
48
|
"prebuild": "npm run clean",
|
|
49
|
-
"
|
|
49
|
+
"copy:ui-kit": "mkdir -p homebridge-ui/public/lib && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/kit.css homebridge-ui/public/lib/ && cp node_modules/@mp-consulting/homebridge-ui-kit/dist/kit.js homebridge-ui/public/lib/",
|
|
50
|
+
"build": "npm run copy:ui-kit && tsc",
|
|
50
51
|
"clean": "shx rm -rf dist",
|
|
51
52
|
"lint": "eslint . --max-warnings=0",
|
|
52
53
|
"postpublish": "npm run clean",
|
|
@@ -70,6 +71,7 @@
|
|
|
70
71
|
"unifi-access": "1.5.3"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
74
|
+
"@mp-consulting/homebridge-ui-kit": "^1.0.0",
|
|
73
75
|
"@eslint/js": "^9.39.2",
|
|
74
76
|
"@types/node": "^24.0.0",
|
|
75
77
|
"@vitest/coverage-v8": "^4.0.18",
|