@escalated-dev/escalated 0.6.0 → 0.7.1

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 (187) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +125 -2
  3. package/package.json +40 -5
  4. package/src/components/ActiveChatsPanel.vue +246 -0
  5. package/src/components/ActivityTimeline.stories.js +55 -0
  6. package/src/components/ActivityTimeline.vue +83 -66
  7. package/src/components/AdminDashboard.stories.js +1423 -0
  8. package/src/components/AgentLoadIndicator.stories.js +37 -0
  9. package/src/components/AgentLoadIndicator.vue +49 -0
  10. package/src/components/AgentPerformanceCard.vue +44 -0
  11. package/src/components/ArticleEditor.vue +82 -0
  12. package/src/components/AssigneeSelect.vue +37 -23
  13. package/src/components/AttachmentList.vue +65 -37
  14. package/src/components/AuditLogEntry.vue +95 -0
  15. package/src/components/BulkActionBar.vue +166 -97
  16. package/src/components/CategoryTree.vue +89 -0
  17. package/src/components/ChartWidget.vue +46 -0
  18. package/src/components/ChatActionBar.vue +226 -0
  19. package/src/components/ChatBubble.vue +162 -0
  20. package/src/components/ChatComposer.vue +308 -0
  21. package/src/components/ChatQueue.vue +222 -0
  22. package/src/components/ChatThread.vue +197 -0
  23. package/src/components/CollisionWarning.stories.js +30 -0
  24. package/src/components/CollisionWarning.vue +68 -0
  25. package/src/components/ConditionalFieldRules.vue +135 -0
  26. package/src/components/ContextPanel.vue +66 -0
  27. package/src/components/ContextPanelSection.vue +65 -0
  28. package/src/components/CustomFieldRenderer.vue +159 -0
  29. package/src/components/EscalatedLayout.vue +424 -70
  30. package/src/components/FileDropzone.vue +57 -41
  31. package/src/components/FollowButton.stories.js +30 -0
  32. package/src/components/FollowButton.vue +88 -63
  33. package/src/components/KeyboardShortcutHelp.vue +213 -123
  34. package/src/components/KnowledgePanel.vue +122 -0
  35. package/src/components/KpiCard.stories.js +32 -0
  36. package/src/components/KpiCard.vue +88 -0
  37. package/src/components/MacroDropdown.vue +150 -90
  38. package/src/components/MentionDropdown.vue +154 -0
  39. package/src/components/MetricComparison.vue +89 -0
  40. package/src/components/PercentileChart.vue +67 -0
  41. package/src/components/PeriodSelector.vue +98 -0
  42. package/src/components/PermissionMatrix.vue +107 -0
  43. package/src/components/PinnedNotes.vue +129 -90
  44. package/src/components/PluginSlot.vue +68 -62
  45. package/src/components/PresenceIndicator.vue +138 -28
  46. package/src/components/PriorityBadge.stories.js +33 -0
  47. package/src/components/PriorityBadge.vue +46 -39
  48. package/src/components/QuickFilters.vue +65 -49
  49. package/src/components/ReplyComposer.vue +207 -30
  50. package/src/components/ReplyThread.vue +111 -21
  51. package/src/components/ReportChart.vue +395 -0
  52. package/src/components/ReportExportButton.vue +108 -0
  53. package/src/components/SatisfactionRating.vue +180 -128
  54. package/src/components/SaveViewDialog.vue +172 -0
  55. package/src/components/SavedViewSidebar.vue +208 -0
  56. package/src/components/ScheduleEditor.vue +104 -0
  57. package/src/components/SideConversation.vue +165 -0
  58. package/src/components/SideConversationList.vue +147 -0
  59. package/src/components/SkillTagManager.vue +79 -0
  60. package/src/components/SlaComplianceChart.vue +46 -0
  61. package/src/components/SlaTimer.stories.js +35 -0
  62. package/src/components/SlaTimer.vue +62 -49
  63. package/src/components/SnoozeButton.vue +247 -0
  64. package/src/components/StatsCard.stories.js +24 -0
  65. package/src/components/StatsCard.vue +18 -6
  66. package/src/components/StatusBadge.stories.js +53 -0
  67. package/src/components/StatusBadge.vue +78 -46
  68. package/src/components/TagSelect.vue +84 -53
  69. package/src/components/TicketFilters.vue +247 -46
  70. package/src/components/TicketLinkPanel.vue +193 -0
  71. package/src/components/TicketList.vue +536 -74
  72. package/src/components/TicketMergeDialog.vue +157 -0
  73. package/src/components/TicketSidebar.vue +167 -99
  74. package/src/components/TicketSplitDialog.vue +98 -0
  75. package/src/components/TicketTypeSelector.vue +21 -0
  76. package/src/components/TotpInput.stories.js +9 -0
  77. package/src/components/TotpInput.vue +81 -0
  78. package/src/components/TwoFactorSetup.vue +113 -0
  79. package/src/components/VariableTokenInput.vue +111 -0
  80. package/src/components/WorkflowActionConfig.vue +287 -0
  81. package/src/components/WorkflowActionList.vue +350 -0
  82. package/src/components/WorkflowConditionBuilder.vue +265 -0
  83. package/src/components/WorkflowTriggerSelector.vue +125 -0
  84. package/src/composables/useChat.js +187 -0
  85. package/src/composables/useDebouncedSearch.js +38 -0
  86. package/src/composables/useI18n.js +80 -80
  87. package/src/composables/useKeyboardShortcuts.js +46 -46
  88. package/src/composables/useMentions.js +134 -0
  89. package/src/composables/usePluginExtensions.js +159 -67
  90. package/src/composables/usePluginHooks.js +65 -0
  91. package/src/composables/useRealtime.js +182 -0
  92. package/src/defineEscalatedPlugin.js +76 -50
  93. package/src/index.js +77 -28
  94. package/src/locales/ar.json +727 -0
  95. package/src/locales/de.json +678 -430
  96. package/src/locales/en.json +717 -430
  97. package/src/locales/es.json +679 -430
  98. package/src/locales/fr.json +680 -431
  99. package/src/locales/index.js +12 -2
  100. package/src/locales/it.json +727 -0
  101. package/src/locales/ja.json +727 -0
  102. package/src/locales/ko.json +727 -0
  103. package/src/locales/nl.json +727 -0
  104. package/src/locales/pl.json +727 -0
  105. package/src/locales/pt-BR.json +727 -0
  106. package/src/locales/ru.json +727 -0
  107. package/src/locales/tr.json +727 -0
  108. package/src/locales/zh-CN.json +727 -0
  109. package/src/pages/Admin/ApiTokens/Index.vue +381 -217
  110. package/src/pages/Admin/AuditLog/Index.vue +154 -0
  111. package/src/pages/Admin/BusinessHours/Form.vue +172 -0
  112. package/src/pages/Admin/BusinessHours/Index.vue +113 -0
  113. package/src/pages/Admin/CannedResponses/Index.vue +82 -25
  114. package/src/pages/Admin/Capacity/Index.vue +134 -0
  115. package/src/pages/Admin/CustomFields/Form.vue +195 -0
  116. package/src/pages/Admin/CustomFields/Index.vue +134 -0
  117. package/src/pages/Admin/CustomObjects/Form.vue +202 -0
  118. package/src/pages/Admin/CustomObjects/Index.vue +127 -0
  119. package/src/pages/Admin/CustomObjects/Records.vue +228 -0
  120. package/src/pages/Admin/Departments/Form.vue +35 -11
  121. package/src/pages/Admin/Departments/Index.vue +74 -21
  122. package/src/pages/Admin/EscalationRules/Form.vue +86 -25
  123. package/src/pages/Admin/EscalationRules/Index.vue +158 -37
  124. package/src/pages/Admin/Import/Connect.vue +95 -0
  125. package/src/pages/Admin/Import/Index.vue +102 -0
  126. package/src/pages/Admin/Import/Mapping.vue +110 -0
  127. package/src/pages/Admin/Import/Progress.vue +153 -0
  128. package/src/pages/Admin/Import/Review.vue +76 -0
  129. package/src/pages/Admin/KnowledgeBase/Articles/Form.vue +142 -0
  130. package/src/pages/Admin/KnowledgeBase/Articles/Index.vue +195 -0
  131. package/src/pages/Admin/KnowledgeBase/Categories/Index.vue +193 -0
  132. package/src/pages/Admin/Macros/Index.vue +448 -287
  133. package/src/pages/Admin/Plugins/Index.vue +327 -197
  134. package/src/pages/Admin/Reports/AgentDetail.vue +132 -0
  135. package/src/pages/Admin/Reports/AgentMetrics.vue +115 -0
  136. package/src/pages/Admin/Reports/AgentRanking.vue +246 -0
  137. package/src/pages/Admin/Reports/Cohorts.vue +180 -0
  138. package/src/pages/Admin/Reports/Comparison.vue +141 -0
  139. package/src/pages/Admin/Reports/CsatReport.vue +127 -0
  140. package/src/pages/Admin/Reports/Dashboard.vue +226 -0
  141. package/src/pages/Admin/Reports/ResolutionTimes.vue +164 -0
  142. package/src/pages/Admin/Reports/ResponseTimes.vue +164 -0
  143. package/src/pages/Admin/Reports/SlaReport.vue +162 -0
  144. package/src/pages/Admin/Reports/SlaTrends.vue +183 -0
  145. package/src/pages/Admin/Reports.vue +206 -101
  146. package/src/pages/Admin/Roles/Form.vue +71 -0
  147. package/src/pages/Admin/Roles/Index.vue +122 -0
  148. package/src/pages/Admin/Settings/CsatSettings.vue +165 -0
  149. package/src/pages/Admin/Settings/DataRetention.vue +182 -0
  150. package/src/pages/Admin/Settings/EmailSettings.vue +243 -0
  151. package/src/pages/Admin/Settings/Sandbox.vue +135 -0
  152. package/src/pages/Admin/Settings/SsoSettings.vue +218 -0
  153. package/src/pages/Admin/Settings/TwoFactor.vue +110 -0
  154. package/src/pages/Admin/Settings.vue +847 -99
  155. package/src/pages/Admin/Skills/Form.vue +53 -0
  156. package/src/pages/Admin/Skills/Index.vue +94 -0
  157. package/src/pages/Admin/SlaPolicies/Form.vue +62 -19
  158. package/src/pages/Admin/SlaPolicies/Index.vue +85 -23
  159. package/src/pages/Admin/Statuses/Form.vue +114 -0
  160. package/src/pages/Admin/Statuses/Index.vue +153 -0
  161. package/src/pages/Admin/Tags/Index.vue +121 -35
  162. package/src/pages/Admin/Tickets/Index.vue +96 -9
  163. package/src/pages/Admin/Tickets/Show.vue +130 -37
  164. package/src/pages/Admin/Webhooks/DeliveryLog.vue +120 -0
  165. package/src/pages/Admin/Webhooks/Form.vue +110 -0
  166. package/src/pages/Admin/Webhooks/Index.vue +136 -0
  167. package/src/pages/Admin/Workflows/Builder.vue +207 -0
  168. package/src/pages/Admin/Workflows/Index.vue +235 -0
  169. package/src/pages/Admin/Workflows/Logs.vue +286 -0
  170. package/src/pages/Agent/Dashboard.vue +311 -186
  171. package/src/pages/Agent/TicketIndex.vue +80 -6
  172. package/src/pages/Agent/TicketShow.vue +399 -60
  173. package/src/pages/Customer/Create.vue +37 -10
  174. package/src/pages/Customer/Index.vue +9 -2
  175. package/src/pages/Customer/KnowledgeBase/Article.vue +129 -0
  176. package/src/pages/Customer/KnowledgeBase/Index.vue +151 -0
  177. package/src/pages/Customer/Show.vue +69 -63
  178. package/src/pages/Guest/Create.vue +40 -15
  179. package/src/pages/Guest/Show.vue +124 -93
  180. package/src/plugin.js +111 -1
  181. package/src/utils/constants.js +30 -0
  182. package/src/utils/formatting.js +35 -0
  183. package/src/utils/mentionHighlight.js +24 -0
  184. package/src/utils/sanitizeHtml.js +78 -64
  185. package/src/widget/EscalatedWidget.vue +804 -0
  186. package/src/widget/index.js +254 -0
  187. package/src/pages/Admin/Plugins.vue +0 -129
@@ -1,217 +1,381 @@
1
- <script setup>
2
- import { ref, computed, inject } from 'vue';
3
- import { useForm, usePage } from '@inertiajs/vue3';
4
- import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
-
6
- const props = defineProps({
7
- tokens: Array,
8
- users: Array,
9
- api_enabled: Boolean,
10
- });
11
-
12
- const dark = inject('esc-dark', computed(() => true));
13
- const page = usePage();
14
- const prefix = computed(() => {
15
- const p = page.props.escalated?.prefix || 'support';
16
- return p.startsWith('/') ? p : `/${p}`;
17
- });
18
-
19
- const showCreate = ref(false);
20
- const copiedToken = ref(null);
21
-
22
- const form = useForm({
23
- name: '',
24
- user_id: '',
25
- abilities: ['agent'],
26
- expires_in_days: null,
27
- });
28
-
29
- function createToken() {
30
- form.post(`${prefix.value}/admin/api-tokens`, {
31
- preserveScroll: true,
32
- onSuccess: (page) => {
33
- const flash = page.props.flash || {};
34
- if (flash.plain_text_token) {
35
- copiedToken.value = flash.plain_text_token;
36
- }
37
- form.reset();
38
- showCreate.value = false;
39
- },
40
- });
41
- }
42
-
43
- function revokeToken(id) {
44
- if (!confirm('Are you sure you want to revoke this token? This cannot be undone.')) return;
45
- useForm({}).delete(`${prefix.value}/admin/api-tokens/${id}`, { preserveScroll: true });
46
- }
47
-
48
- function copyToken() {
49
- if (copiedToken.value) {
50
- navigator.clipboard.writeText(copiedToken.value);
51
- }
52
- }
53
-
54
- function dismissToken() {
55
- copiedToken.value = null;
56
- }
57
-
58
- function toggleAbility(ability) {
59
- const idx = form.abilities.indexOf(ability);
60
- if (idx >= 0) {
61
- form.abilities.splice(idx, 1);
62
- } else {
63
- form.abilities.push(ability);
64
- }
65
- }
66
-
67
- function formatDate(iso) {
68
- if (!iso) return '—';
69
- return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
70
- }
71
- </script>
72
-
73
- <template>
74
- <EscalatedLayout title="API Tokens">
75
- <div class="space-y-6">
76
- <!-- Header -->
77
- <div class="flex items-center justify-between">
78
- <div>
79
- <h2 :class="dark ? 'text-white' : 'text-gray-900'" class="text-lg font-semibold">API Tokens</h2>
80
- <p :class="dark ? 'text-neutral-500' : 'text-gray-500'" class="mt-1 text-sm">Manage API tokens for desktop apps and external integrations.</p>
81
- </div>
82
- <button @click="showCreate = true"
83
- :class="dark ? 'bg-white text-black hover:bg-neutral-200' : 'bg-gray-900 text-white hover:bg-gray-800'"
84
- class="rounded-lg px-4 py-2 text-sm font-medium transition-colors">
85
- Create Token
86
- </button>
87
- </div>
88
-
89
- <!-- Not enabled warning -->
90
- <div v-if="!api_enabled"
91
- :class="dark ? 'border-amber-500/30 bg-amber-500/10 text-amber-300' : 'border-amber-300 bg-amber-50 text-amber-800'"
92
- class="rounded-lg border p-4 text-sm">
93
- The REST API is currently disabled. Set <code class="font-mono">ESCALATED_API_ENABLED=true</code> in your .env to enable it.
94
- </div>
95
-
96
- <!-- Copied token modal -->
97
- <div v-if="copiedToken"
98
- :class="dark ? 'border-emerald-500/30 bg-emerald-500/10' : 'border-emerald-300 bg-emerald-50'"
99
- class="rounded-lg border p-4">
100
- <p :class="dark ? 'text-emerald-300' : 'text-emerald-800'" class="text-sm font-medium">Token created! Copy it now — it won't be shown again.</p>
101
- <div class="mt-2 flex items-center gap-2">
102
- <code :class="dark ? 'bg-black text-emerald-400' : 'bg-white text-emerald-700'"
103
- class="flex-1 rounded border px-3 py-2 text-xs font-mono break-all"
104
- :style="dark ? 'border-color: rgba(255,255,255,0.1)' : ''">{{ copiedToken }}</code>
105
- <button @click="copyToken"
106
- :class="dark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'"
107
- class="rounded-lg px-3 py-2 text-sm font-medium transition-colors">Copy</button>
108
- <button @click="dismissToken"
109
- :class="dark ? 'text-neutral-500 hover:text-white' : 'text-gray-400 hover:text-gray-600'"
110
- class="px-2 py-2 text-sm transition-colors">Dismiss</button>
111
- </div>
112
- </div>
113
-
114
- <!-- Create form -->
115
- <div v-if="showCreate"
116
- :class="dark ? 'border-white/[0.06] bg-neutral-950' : 'border-gray-200 bg-white'"
117
- class="rounded-lg border p-5 space-y-4">
118
- <h3 :class="dark ? 'text-white' : 'text-gray-900'" class="text-sm font-semibold">New API Token</h3>
119
-
120
- <div class="grid grid-cols-2 gap-4">
121
- <div>
122
- <label :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">Token Name</label>
123
- <input v-model="form.name" type="text" placeholder="e.g. Desktop App - Work Laptop"
124
- :class="dark ? 'border-white/[0.08] bg-white/[0.04] text-white' : 'border-gray-300 bg-white text-gray-900'"
125
- class="w-full rounded-lg border px-3 py-2 text-sm" />
126
- </div>
127
- <div>
128
- <label :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">User</label>
129
- <select v-model="form.user_id"
130
- :class="dark ? 'border-white/[0.08] bg-white/[0.04] text-white' : 'border-gray-300 bg-white text-gray-900'"
131
- class="w-full rounded-lg border px-3 py-2 text-sm">
132
- <option value="">Select user...</option>
133
- <option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
134
- </select>
135
- </div>
136
- </div>
137
-
138
- <div>
139
- <label :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">Abilities</label>
140
- <div class="flex items-center gap-3">
141
- <label v-for="ability in ['agent', 'admin']" :key="ability"
142
- :class="dark ? 'text-neutral-300' : 'text-gray-700'"
143
- class="flex items-center gap-1.5 text-sm cursor-pointer">
144
- <input type="checkbox" :checked="form.abilities.includes(ability)" @change="toggleAbility(ability)"
145
- class="rounded border-neutral-600" />
146
- {{ ability }}
147
- </label>
148
- </div>
149
- </div>
150
-
151
- <div>
152
- <label :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="mb-1 block text-xs font-medium">Expires In (days)</label>
153
- <input v-model.number="form.expires_in_days" type="number" min="1" max="365" placeholder="Leave empty for no expiration"
154
- :class="dark ? 'border-white/[0.08] bg-white/[0.04] text-white' : 'border-gray-300 bg-white text-gray-900'"
155
- class="w-48 rounded-lg border px-3 py-2 text-sm" />
156
- </div>
157
-
158
- <div class="flex items-center gap-3 pt-2">
159
- <button @click="createToken" :disabled="form.processing || !form.name || !form.user_id"
160
- :class="dark ? 'bg-white text-black hover:bg-neutral-200 disabled:bg-neutral-700 disabled:text-neutral-500' : 'bg-gray-900 text-white hover:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500'"
161
- class="rounded-lg px-4 py-2 text-sm font-medium transition-colors">
162
- {{ form.processing ? 'Creating...' : 'Create Token' }}
163
- </button>
164
- <button @click="showCreate = false"
165
- :class="dark ? 'text-neutral-500 hover:text-white' : 'text-gray-500 hover:text-gray-900'"
166
- class="text-sm transition-colors">Cancel</button>
167
- </div>
168
- </div>
169
-
170
- <!-- Tokens table -->
171
- <div :class="dark ? 'border-white/[0.06]' : 'border-gray-200'" class="overflow-hidden rounded-lg border">
172
- <table class="w-full text-sm">
173
- <thead :class="dark ? 'bg-white/[0.03] text-neutral-500' : 'bg-gray-50 text-gray-500'">
174
- <tr>
175
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
176
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
177
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Abilities</th>
178
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Last Used</th>
179
- <th class="px-4 py-3 text-left text-xs font-medium uppercase">Expires</th>
180
- <th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
181
- </tr>
182
- </thead>
183
- <tbody :class="dark ? 'divide-white/[0.04]' : 'divide-gray-100'" class="divide-y">
184
- <tr v-for="token in tokens" :key="token.id"
185
- :class="[dark ? 'hover:bg-white/[0.02]' : 'hover:bg-gray-50', token.is_expired ? 'opacity-50' : '']">
186
- <td :class="dark ? 'text-white' : 'text-gray-900'" class="px-4 py-3 font-medium">{{ token.name }}</td>
187
- <td :class="dark ? 'text-neutral-400' : 'text-gray-600'" class="px-4 py-3">{{ token.user_name }}</td>
188
- <td class="px-4 py-3">
189
- <span v-for="a in (token.abilities || [])" :key="a"
190
- :class="dark ? 'bg-white/[0.06] text-neutral-300' : 'bg-gray-100 text-gray-700'"
191
- class="mr-1 rounded px-1.5 py-0.5 text-xs">{{ a }}</span>
192
- </td>
193
- <td :class="dark ? 'text-neutral-500' : 'text-gray-500'" class="px-4 py-3 text-xs">
194
- <template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
195
- <template v-else>Never</template>
196
- </td>
197
- <td :class="dark ? 'text-neutral-500' : 'text-gray-500'" class="px-4 py-3 text-xs">
198
- <span v-if="token.is_expired" class="text-rose-400">Expired</span>
199
- <template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
200
- <template v-else>Never</template>
201
- </td>
202
- <td class="px-4 py-3 text-right">
203
- <button @click="revokeToken(token.id)"
204
- class="text-xs text-rose-400 hover:text-rose-300 transition-colors">Revoke</button>
205
- </td>
206
- </tr>
207
- <tr v-if="!tokens.length">
208
- <td colspan="6" :class="dark ? 'text-neutral-500' : 'text-gray-400'" class="px-4 py-8 text-center text-sm">
209
- No API tokens created yet.
210
- </td>
211
- </tr>
212
- </tbody>
213
- </table>
214
- </div>
215
- </div>
216
- </EscalatedLayout>
217
- </template>
1
+ <script setup>
2
+ import { ref, computed, inject } from 'vue';
3
+ import { useForm, usePage } from '@inertiajs/vue3';
4
+ import EscalatedLayout from '../../../components/EscalatedLayout.vue';
5
+
6
+ defineProps({
7
+ tokens: Array,
8
+ users: Array,
9
+ api_enabled: Boolean,
10
+ });
11
+
12
+ const dark = inject(
13
+ 'esc-dark',
14
+ computed(() => true),
15
+ );
16
+ const page = usePage();
17
+ const prefix = computed(() => {
18
+ const p = page.props.escalated?.prefix || 'support';
19
+ return p.startsWith('/') ? p : `/${p}`;
20
+ });
21
+
22
+ const showCreate = ref(false);
23
+ const copiedToken = ref(null);
24
+
25
+ const form = useForm({
26
+ name: '',
27
+ user_id: '',
28
+ abilities: ['agent'],
29
+ expires_in_days: null,
30
+ });
31
+
32
+ function createToken() {
33
+ form.post(`${prefix.value}/admin/api-tokens`, {
34
+ preserveScroll: true,
35
+ onSuccess: (page) => {
36
+ const flash = page.props.flash || {};
37
+ if (flash.plain_text_token) {
38
+ copiedToken.value = flash.plain_text_token;
39
+ }
40
+ form.reset();
41
+ showCreate.value = false;
42
+ },
43
+ });
44
+ }
45
+
46
+ function revokeToken(id) {
47
+ if (!confirm('Are you sure you want to revoke this token? This cannot be undone.')) return;
48
+ useForm({}).delete(`${prefix.value}/admin/api-tokens/${id}`, { preserveScroll: true });
49
+ }
50
+
51
+ function copyToken() {
52
+ if (copiedToken.value) {
53
+ navigator.clipboard.writeText(copiedToken.value);
54
+ }
55
+ }
56
+
57
+ function dismissToken() {
58
+ copiedToken.value = null;
59
+ }
60
+
61
+ function toggleAbility(ability) {
62
+ const idx = form.abilities.indexOf(ability);
63
+ if (idx >= 0) {
64
+ form.abilities.splice(idx, 1);
65
+ } else {
66
+ form.abilities.push(ability);
67
+ }
68
+ }
69
+
70
+ function formatDate(iso) {
71
+ if (!iso) return '—';
72
+ return new Date(iso).toLocaleDateString(undefined, {
73
+ year: 'numeric',
74
+ month: 'short',
75
+ day: 'numeric',
76
+ hour: '2-digit',
77
+ minute: '2-digit',
78
+ });
79
+ }
80
+ </script>
81
+
82
+ <template>
83
+ <EscalatedLayout title="API Tokens">
84
+ <div class="space-y-6">
85
+ <!-- Header -->
86
+ <div class="flex items-center justify-between">
87
+ <div>
88
+ <h2 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-lg font-semibold">
89
+ API Tokens
90
+ </h2>
91
+ <p :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'" class="mt-1 text-sm">
92
+ Manage API tokens for desktop apps and external integrations.
93
+ </p>
94
+ </div>
95
+ <button
96
+ :class="
97
+ dark ? 'bg-white text-black hover:bg-neutral-200' : 'bg-gray-900 text-white hover:bg-gray-800'
98
+ "
99
+ class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
100
+ @click="showCreate = true"
101
+ >
102
+ Create Token
103
+ </button>
104
+ </div>
105
+
106
+ <!-- Not enabled warning -->
107
+ <div
108
+ v-if="!api_enabled"
109
+ :class="
110
+ dark
111
+ ? 'border-amber-500/30 bg-amber-500/10 text-amber-300'
112
+ : 'border-amber-300 bg-amber-50 text-amber-800'
113
+ "
114
+ class="rounded-lg border p-4 text-sm"
115
+ >
116
+ The REST API is currently disabled. Set <code class="font-mono">ESCALATED_API_ENABLED=true</code> in
117
+ your .env to enable it.
118
+ </div>
119
+
120
+ <!-- Copied token modal -->
121
+ <div
122
+ v-if="copiedToken"
123
+ :class="dark ? 'border-emerald-500/30 bg-emerald-500/10' : 'border-emerald-300 bg-emerald-50'"
124
+ class="rounded-lg border p-4"
125
+ >
126
+ <p :class="dark ? 'text-emerald-300' : 'text-emerald-800'" class="text-sm font-medium">
127
+ Token created! Copy it now — it won't be shown again.
128
+ </p>
129
+ <div class="mt-2 flex items-center gap-2">
130
+ <code
131
+ :class="dark ? 'bg-black text-emerald-400' : 'bg-white text-emerald-700'"
132
+ class="flex-1 rounded border px-3 py-2 text-xs font-mono break-all"
133
+ :style="dark ? 'border-color: rgba(255,255,255,0.1)' : ''"
134
+ >{{ copiedToken }}</code
135
+ >
136
+ <button
137
+ :class="
138
+ dark
139
+ ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)] hover:bg-[var(--esc-panel-active)]'
140
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
141
+ "
142
+ class="rounded-lg px-3 py-2 text-sm font-medium transition-colors"
143
+ @click="copyToken"
144
+ >
145
+ Copy
146
+ </button>
147
+ <button
148
+ :class="
149
+ dark
150
+ ? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
151
+ : 'text-gray-400 hover:text-gray-600'
152
+ "
153
+ class="px-2 py-2 text-sm transition-colors"
154
+ @click="dismissToken"
155
+ >
156
+ Dismiss
157
+ </button>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- Create form -->
162
+ <div
163
+ v-if="showCreate"
164
+ :class="
165
+ dark
166
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-surface-alt)]'
167
+ : 'border-gray-200 bg-white'
168
+ "
169
+ class="rounded-lg border p-5 space-y-4"
170
+ >
171
+ <h3 :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'" class="text-sm font-semibold">
172
+ New API Token
173
+ </h3>
174
+
175
+ <div class="grid grid-cols-2 gap-4">
176
+ <div>
177
+ <label
178
+ :class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
179
+ class="mb-1 block text-xs font-medium"
180
+ >Token Name</label
181
+ >
182
+ <input
183
+ v-model="form.name"
184
+ type="text"
185
+ placeholder="e.g. Desktop App - Work Laptop"
186
+ :class="
187
+ dark
188
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
189
+ : 'border-gray-300 bg-white text-gray-900'
190
+ "
191
+ class="w-full rounded-lg border px-3 py-2 text-sm"
192
+ />
193
+ </div>
194
+ <div>
195
+ <label
196
+ :class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
197
+ class="mb-1 block text-xs font-medium"
198
+ >User</label
199
+ >
200
+ <select
201
+ v-model="form.user_id"
202
+ :class="
203
+ dark
204
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
205
+ : 'border-gray-300 bg-white text-gray-900'
206
+ "
207
+ class="w-full rounded-lg border px-3 py-2 text-sm"
208
+ >
209
+ <option value="">Select user...</option>
210
+ <option v-for="u in users" :key="u.id" :value="u.id">{{ u.name }} ({{ u.email }})</option>
211
+ </select>
212
+ </div>
213
+ </div>
214
+
215
+ <div>
216
+ <label
217
+ :class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
218
+ class="mb-1 block text-xs font-medium"
219
+ >Abilities</label
220
+ >
221
+ <div class="flex items-center gap-3">
222
+ <label
223
+ v-for="ability in ['agent', 'admin']"
224
+ :key="ability"
225
+ :class="dark ? 'text-[var(--esc-panel-text-secondary)]' : 'text-gray-700'"
226
+ class="flex items-center gap-1.5 text-sm cursor-pointer"
227
+ >
228
+ <input
229
+ type="checkbox"
230
+ :checked="form.abilities.includes(ability)"
231
+ class="rounded border-neutral-600"
232
+ @change="toggleAbility(ability)"
233
+ />
234
+ {{ ability }}
235
+ </label>
236
+ </div>
237
+ </div>
238
+
239
+ <div>
240
+ <label
241
+ :class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
242
+ class="mb-1 block text-xs font-medium"
243
+ >Expires In (days)</label
244
+ >
245
+ <input
246
+ v-model.number="form.expires_in_days"
247
+ type="number"
248
+ min="1"
249
+ max="365"
250
+ placeholder="Leave empty for no expiration"
251
+ :class="
252
+ dark
253
+ ? 'border-[var(--esc-panel-border)] bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text)]'
254
+ : 'border-gray-300 bg-white text-gray-900'
255
+ "
256
+ class="w-48 rounded-lg border px-3 py-2 text-sm"
257
+ />
258
+ </div>
259
+
260
+ <div class="flex items-center gap-3 pt-2">
261
+ <button
262
+ :disabled="form.processing || !form.name || !form.user_id"
263
+ :class="
264
+ dark
265
+ ? 'bg-white text-black hover:bg-neutral-200 disabled:bg-[var(--esc-panel-text-muted)] disabled:text-[var(--esc-panel-text-muted)]'
266
+ : 'bg-gray-900 text-white hover:bg-gray-800 disabled:bg-gray-300 disabled:text-gray-500'
267
+ "
268
+ class="rounded-lg px-4 py-2 text-sm font-medium transition-colors"
269
+ @click="createToken"
270
+ >
271
+ {{ form.processing ? 'Creating...' : 'Create Token' }}
272
+ </button>
273
+ <button
274
+ :class="
275
+ dark
276
+ ? 'text-[var(--esc-panel-text-muted)] hover:text-[var(--esc-panel-text)]'
277
+ : 'text-gray-500 hover:text-gray-900'
278
+ "
279
+ class="text-sm transition-colors"
280
+ @click="showCreate = false"
281
+ >
282
+ Cancel
283
+ </button>
284
+ </div>
285
+ </div>
286
+
287
+ <!-- Tokens table -->
288
+ <div
289
+ :class="dark ? 'border-[var(--esc-panel-border)]' : 'border-gray-200'"
290
+ class="overflow-hidden rounded-lg border"
291
+ >
292
+ <table class="w-full text-sm">
293
+ <thead
294
+ :class="
295
+ dark
296
+ ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-muted)]'
297
+ : 'bg-gray-50 text-gray-500'
298
+ "
299
+ >
300
+ <tr>
301
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Name</th>
302
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">User</th>
303
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Abilities</th>
304
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Last Used</th>
305
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase">Expires</th>
306
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase">Actions</th>
307
+ </tr>
308
+ </thead>
309
+ <tbody :class="dark ? 'divide-[var(--esc-panel-border)]' : 'divide-gray-100'" class="divide-y">
310
+ <tr
311
+ v-for="token in tokens"
312
+ :key="token.id"
313
+ :class="[
314
+ dark ? 'hover:bg-[var(--esc-panel-hover)]' : 'hover:bg-gray-50',
315
+ token.is_expired ? 'opacity-50' : '',
316
+ ]"
317
+ >
318
+ <td
319
+ :class="dark ? 'text-[var(--esc-panel-text)]' : 'text-gray-900'"
320
+ class="px-4 py-3 font-medium"
321
+ >
322
+ {{ token.name }}
323
+ </td>
324
+ <td
325
+ :class="dark ? 'text-[var(--esc-panel-text-tertiary)]' : 'text-gray-600'"
326
+ class="px-4 py-3"
327
+ >
328
+ {{ token.user_name }}
329
+ </td>
330
+ <td class="px-4 py-3">
331
+ <span
332
+ v-for="a in token.abilities || []"
333
+ :key="a"
334
+ :class="
335
+ dark
336
+ ? 'bg-[var(--esc-panel-hover)] text-[var(--esc-panel-text-secondary)]'
337
+ : 'bg-gray-100 text-gray-700'
338
+ "
339
+ class="mr-1 rounded px-1.5 py-0.5 text-xs"
340
+ >{{ a }}</span
341
+ >
342
+ </td>
343
+ <td
344
+ :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
345
+ class="px-4 py-3 text-xs"
346
+ >
347
+ <template v-if="token.last_used_at">{{ formatDate(token.last_used_at) }}</template>
348
+ <template v-else>Never</template>
349
+ </td>
350
+ <td
351
+ :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-500'"
352
+ class="px-4 py-3 text-xs"
353
+ >
354
+ <span v-if="token.is_expired" class="text-rose-400">Expired</span>
355
+ <template v-else-if="token.expires_at">{{ formatDate(token.expires_at) }}</template>
356
+ <template v-else>Never</template>
357
+ </td>
358
+ <td class="px-4 py-3 text-right">
359
+ <button
360
+ class="text-xs text-rose-400 hover:text-rose-300 transition-colors"
361
+ @click="revokeToken(token.id)"
362
+ >
363
+ Revoke
364
+ </button>
365
+ </td>
366
+ </tr>
367
+ <tr v-if="!tokens.length">
368
+ <td
369
+ colspan="6"
370
+ :class="dark ? 'text-[var(--esc-panel-text-muted)]' : 'text-gray-400'"
371
+ class="px-4 py-8 text-center text-sm"
372
+ >
373
+ No API tokens created yet.
374
+ </td>
375
+ </tr>
376
+ </tbody>
377
+ </table>
378
+ </div>
379
+ </div>
380
+ </EscalatedLayout>
381
+ </template>