Akoda35 commited on
Commit
1749e4f
·
verified ·
1 Parent(s): 3b0a9a2

Upload 6 files

Browse files
templates/cpanel/datastores/create.html ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Create Data Store - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:datastore_list' %}" class="hover:text-code-accent">Data Stores</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <span class="text-code-text">Create</span>
18
+ </nav>
19
+ <h1 class="text-2xl font-bold text-code-text">Create Data Store</h1>
20
+ <p class="text-code-muted">Create a new key-value store for your scripts</p>
21
+ </div>
22
+
23
+ <!-- Form -->
24
+ <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl">
25
+ <form method="post">
26
+ {% csrf_token %}
27
+
28
+ <!-- Store Name -->
29
+ <div class="mb-6">
30
+ <label for="id_name" class="block text-sm font-medium text-code-text mb-2">
31
+ {{ form.name.label }}
32
+ </label>
33
+ {{ form.name }}
34
+ {% if form.name.help_text %}
35
+ <p class="mt-1 text-sm text-code-muted">{{ form.name.help_text }}</p>
36
+ {% endif %}
37
+ {% if form.name.errors %}
38
+ <p class="mt-1 text-sm text-code-red">{{ form.name.errors.0 }}</p>
39
+ {% endif %}
40
+ </div>
41
+
42
+ <!-- Description -->
43
+ <div class="mb-6">
44
+ <label for="id_description" class="block text-sm font-medium text-code-text mb-2">
45
+ {{ form.description.label }}
46
+ </label>
47
+ {{ form.description }}
48
+ {% if form.description.help_text %}
49
+ <p class="mt-1 text-sm text-code-muted">{{ form.description.help_text }}</p>
50
+ {% endif %}
51
+ {% if form.description.errors %}
52
+ <p class="mt-1 text-sm text-code-red">{{ form.description.errors.0 }}</p>
53
+ {% endif %}
54
+ </div>
55
+
56
+ <!-- Info Box -->
57
+ <div class="mb-6 p-4 bg-code-bg border border-code-border rounded-lg">
58
+ <div class="flex items-start space-x-3">
59
+ <svg class="w-5 h-5 text-code-accent mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
60
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
61
+ </svg>
62
+ <div class="text-sm text-code-muted">
63
+ <p class="font-medium text-code-text">How it works</p>
64
+ <ul class="mt-1 space-y-1 list-disc list-inside">
65
+ <li>Data stores provide simple key-value storage</li>
66
+ <li>Values can be any JSON-serializable data</li>
67
+ <li>Data persists between script runs</li>
68
+ <li>Access from scripts using <code class="text-code-accent">DataStore("name")</code></li>
69
+ </ul>
70
+ </div>
71
+ </div>
72
+ </div>
73
+
74
+ <!-- Actions -->
75
+ <div class="flex items-center justify-end space-x-4 pt-6 border-t border-code-border">
76
+ <a href="{% url 'cpanel:datastore_list' %}"
77
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors">
78
+ Cancel
79
+ </a>
80
+ <button type="submit"
81
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors">
82
+ Create Data Store
83
+ </button>
84
+ </div>
85
+ </form>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ {% endblock %}
templates/cpanel/datastores/detail.html ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}{{ datastore.name }} - Data Stores - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:datastore_list' %}" class="hover:text-code-accent">Data Stores</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <span class="text-code-text">{{ datastore.name }}</span>
18
+ </nav>
19
+ <div class="flex items-start justify-between">
20
+ <div>
21
+ <h1 class="text-2xl font-bold text-code-text font-mono">{{ datastore.name }}</h1>
22
+ <p class="text-code-muted">{{ datastore.description|default:"No description" }}</p>
23
+ </div>
24
+ <div class="flex items-center space-x-2">
25
+ <a href="{% url 'cpanel:datastore_entry_create' pk=datastore.pk %}"
26
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors inline-flex items-center">
27
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
29
+ </svg>
30
+ Add Entry
31
+ </a>
32
+ <a href="{% url 'cpanel:datastore_edit' pk=datastore.pk %}"
33
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors">
34
+ Edit
35
+ </a>
36
+ </div>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- Messages -->
41
+ {% if messages %}
42
+ <div class="mb-6 space-y-2">
43
+ {% for message in messages %}
44
+ <div class="p-4 rounded-lg {% if message.tags == 'error' %}bg-code-red/10 border border-code-red/30 text-code-red{% elif message.tags == 'success' %}bg-code-green/10 border border-code-green/30 text-code-green{% else %}bg-code-accent/10 border border-code-accent/30 text-code-accent{% endif %}">
45
+ {{ message }}
46
+ </div>
47
+ {% endfor %}
48
+ </div>
49
+ {% endif %}
50
+
51
+ <!-- Stats -->
52
+ <div class="grid grid-cols-3 gap-4 mb-6">
53
+ <div class="bg-code-surface border border-code-border rounded-lg p-4">
54
+ <p class="text-sm text-code-muted">Total Entries</p>
55
+ <p class="text-2xl font-bold text-code-text">{{ entries.count }}</p>
56
+ </div>
57
+ <div class="bg-code-surface border border-code-border rounded-lg p-4">
58
+ <p class="text-sm text-code-muted">Created</p>
59
+ <p class="text-lg text-code-text">{{ datastore.created_at|date:"M d, Y" }}</p>
60
+ </div>
61
+ <div class="bg-code-surface border border-code-border rounded-lg p-4">
62
+ <p class="text-sm text-code-muted">Last Updated</p>
63
+ <p class="text-lg text-code-text">{{ datastore.updated_at|timesince }} ago</p>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Entries Table -->
68
+ {% if entries %}
69
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
70
+ <div class="bg-code-bg border-b border-code-border px-6 py-3 flex items-center justify-between">
71
+ <h2 class="text-sm font-semibold text-code-text">Entries</h2>
72
+ {% if entries.count > 0 %}
73
+ <form method="post" action="{% url 'cpanel:datastore_clear' pk=datastore.pk %}"
74
+ onsubmit="return confirm('Are you sure you want to delete all {{ entries.count }} entries? This cannot be undone.');"
75
+ class="inline">
76
+ {% csrf_token %}
77
+ <button type="submit"
78
+ class="px-3 py-1 text-sm bg-code-red/10 border border-code-red/30 hover:bg-code-red/20 text-code-red rounded transition-colors">
79
+ Clear All
80
+ </button>
81
+ </form>
82
+ {% endif %}
83
+ </div>
84
+ <table class="w-full">
85
+ <thead class="bg-code-bg border-b border-code-border">
86
+ <tr>
87
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider w-1/4">Key</th>
88
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Value</th>
89
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider w-32">Updated</th>
90
+ <th class="px-6 py-3 text-right text-xs font-semibold text-code-muted uppercase tracking-wider w-40">Actions</th>
91
+ </tr>
92
+ </thead>
93
+ <tbody class="divide-y divide-code-border">
94
+ {% for entry in entries %}
95
+ <tr class="hover:bg-code-bg/50 transition-colors">
96
+ <td class="px-6 py-4">
97
+ <span class="font-mono text-code-accent">{{ entry.key }}</span>
98
+ </td>
99
+ <td class="px-6 py-4">
100
+ <code class="text-sm text-code-text bg-code-bg px-2 py-1 rounded font-mono break-all">{{ entry.get_display_value }}</code>
101
+ </td>
102
+ <td class="px-6 py-4 text-code-muted text-sm">
103
+ {{ entry.updated_at|timesince }} ago
104
+ </td>
105
+ <td class="px-6 py-4 text-right">
106
+ <div class="flex items-center justify-end space-x-2">
107
+ <a href="{% url 'cpanel:datastore_entry_edit' pk=datastore.pk entry_pk=entry.pk %}"
108
+ class="px-3 py-1.5 text-sm bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors">
109
+ Edit
110
+ </a>
111
+ <form method="post" action="{% url 'cpanel:datastore_entry_delete' pk=datastore.pk entry_pk=entry.pk %}"
112
+ onsubmit="return confirm('Delete entry &quot;{{ entry.key }}&quot;?');"
113
+ class="inline">
114
+ {% csrf_token %}
115
+ <button type="submit"
116
+ class="px-3 py-1.5 text-sm bg-code-red/10 border border-code-red/30 hover:bg-code-red/20 text-code-red rounded transition-colors">
117
+ Delete
118
+ </button>
119
+ </form>
120
+ </div>
121
+ </td>
122
+ </tr>
123
+ {% endfor %}
124
+ </tbody>
125
+ </table>
126
+ </div>
127
+ {% else %}
128
+ <div class="bg-code-surface border border-code-border rounded-xl p-12 text-center">
129
+ <div class="w-16 h-16 bg-code-accent/20 rounded-full flex items-center justify-center mx-auto mb-4">
130
+ <svg class="w-8 h-8 text-code-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
131
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/>
132
+ </svg>
133
+ </div>
134
+ <h3 class="text-lg font-semibold text-code-text mb-2">No entries yet</h3>
135
+ <p class="text-code-muted mb-6">Add entries manually or let your scripts populate this data store.</p>
136
+ <a href="{% url 'cpanel:datastore_entry_create' pk=datastore.pk %}"
137
+ class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors inline-flex items-center">
138
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
140
+ </svg>
141
+ Add Entry
142
+ </a>
143
+ </div>
144
+ {% endif %}
145
+
146
+ <!-- Usage Info -->
147
+ <div class="mt-6 p-4 bg-code-bg border border-code-border rounded-lg">
148
+ <h3 class="text-sm font-medium text-code-text mb-2">Using this Data Store in Scripts</h3>
149
+ <code class="block p-3 bg-code-surface rounded text-sm font-mono text-code-text whitespace-pre">from pyrunner_datastore import DataStore
150
+
151
+ store = DataStore("{{ datastore.name }}")
152
+
153
+ # Set values
154
+ store["my_key"] = {"any": "json value"}
155
+
156
+ # Get values
157
+ value = store["my_key"]
158
+ value = store.get("my_key", default=None)
159
+
160
+ # Check if key exists
161
+ if "my_key" in store:
162
+ print(store["my_key"])
163
+
164
+ # Delete a key
165
+ del store["my_key"]
166
+
167
+ # List all keys
168
+ for key in store.keys():
169
+ print(key, store[key])</code>
170
+ </div>
171
+
172
+ <!-- REST API Access -->
173
+ <div class="mt-6 p-4 bg-code-bg border border-code-border rounded-lg">
174
+ <div class="flex items-center justify-between mb-2">
175
+ <h3 class="text-sm font-medium text-code-text">REST API Access</h3>
176
+ <button type="button"
177
+ id="api-toggle-btn"
178
+ onclick="toggleApiExamples()"
179
+ class="text-xs text-code-accent hover:underline">
180
+ Show curl examples
181
+ </button>
182
+ </div>
183
+
184
+ <p class="text-sm text-code-muted">
185
+ Access this datastore via the REST API using an
186
+ <a href="{% url 'cpanel:api_token_list' %}" class="text-code-accent hover:underline">API token</a>.
187
+ </p>
188
+
189
+ <div id="api-examples" class="hidden mt-4 space-y-4">
190
+ <!-- Authentication Methods -->
191
+ <div>
192
+ <h4 class="text-xs text-code-muted uppercase tracking-wider mb-2">Authentication</h4>
193
+ <p class="text-sm text-code-muted mb-2">Include your token in the request header:</p>
194
+ <code class="block p-2 bg-code-surface rounded text-xs font-mono text-code-text whitespace-pre">Authorization: Bearer YOUR_API_TOKEN
195
+ # or
196
+ X-API-Key: YOUR_API_TOKEN</code>
197
+ </div>
198
+
199
+ <!-- Endpoint 1: Get Datastore Metadata -->
200
+ <div>
201
+ <div class="flex items-center justify-between mb-1">
202
+ <h4 class="text-xs text-code-muted uppercase tracking-wider">Get Datastore Info</h4>
203
+ <button type="button" onclick="copyCode('curl-ds-info')"
204
+ class="text-xs text-code-accent hover:underline">Copy</button>
205
+ </div>
206
+ <code id="curl-ds-info" class="block p-2 bg-code-surface rounded text-xs font-mono text-code-text overflow-x-auto whitespace-pre">curl -H "Authorization: Bearer YOUR_TOKEN" \
207
+ "{{ request.scheme }}://{{ request.get_host }}/api/v1/datastores/{{ datastore.name }}/"</code>
208
+ <details class="mt-2">
209
+ <summary class="text-xs text-code-muted cursor-pointer hover:text-code-accent">Sample response</summary>
210
+ <code class="block mt-1 p-2 bg-code-surface rounded text-xs font-mono text-code-muted overflow-x-auto whitespace-pre">{
211
+ "name": "{{ datastore.name }}",
212
+ "description": "{{ datastore.description|default:"" }}",
213
+ "entry_count": {{ entries.count }},
214
+ "created_at": "{{ datastore.created_at|date:"c" }}",
215
+ "updated_at": "{{ datastore.updated_at|date:"c" }}"
216
+ }</code>
217
+ </details>
218
+ </div>
219
+
220
+ <!-- Endpoint 2: List All Entries -->
221
+ <div>
222
+ <div class="flex items-center justify-between mb-1">
223
+ <h4 class="text-xs text-code-muted uppercase tracking-wider">List Entries (paginated)</h4>
224
+ <button type="button" onclick="copyCode('curl-entries')"
225
+ class="text-xs text-code-accent hover:underline">Copy</button>
226
+ </div>
227
+ <code id="curl-entries" class="block p-2 bg-code-surface rounded text-xs font-mono text-code-text overflow-x-auto whitespace-pre">curl -H "Authorization: Bearer YOUR_TOKEN" \
228
+ "{{ request.scheme }}://{{ request.get_host }}/api/v1/datastores/{{ datastore.name }}/entries/?page=1&page_size=50"</code>
229
+ <details class="mt-2">
230
+ <summary class="text-xs text-code-muted cursor-pointer hover:text-code-accent">Sample response</summary>
231
+ <code class="block mt-1 p-2 bg-code-surface rounded text-xs font-mono text-code-muted overflow-x-auto whitespace-pre">{
232
+ "entries": [
233
+ {
234
+ "key": "example_key",
235
+ "value": {"any": "json data"},
236
+ "created_at": "2026-01-20T08:00:00Z",
237
+ "updated_at": "2026-02-01T12:00:00Z"
238
+ }
239
+ ],
240
+ "count": {{ entries.count }},
241
+ "page": 1,
242
+ "page_size": 50,
243
+ "total_pages": 1
244
+ }</code>
245
+ </details>
246
+ </div>
247
+
248
+ <!-- Endpoint 3: Get Single Entry -->
249
+ <div>
250
+ <div class="flex items-center justify-between mb-1">
251
+ <h4 class="text-xs text-code-muted uppercase tracking-wider">Get Single Entry</h4>
252
+ <button type="button" onclick="copyCode('curl-entry')"
253
+ class="text-xs text-code-accent hover:underline">Copy</button>
254
+ </div>
255
+ <code id="curl-entry" class="block p-2 bg-code-surface rounded text-xs font-mono text-code-text overflow-x-auto whitespace-pre">curl -H "Authorization: Bearer YOUR_TOKEN" \
256
+ "{{ request.scheme }}://{{ request.get_host }}/api/v1/datastores/{{ datastore.name }}/entries/YOUR_KEY/"</code>
257
+ <details class="mt-2">
258
+ <summary class="text-xs text-code-muted cursor-pointer hover:text-code-accent">Sample response</summary>
259
+ <code class="block mt-1 p-2 bg-code-surface rounded text-xs font-mono text-code-muted overflow-x-auto whitespace-pre">{
260
+ "key": "example_key",
261
+ "value": {"any": "json data"},
262
+ "created_at": "2026-01-20T08:00:00Z",
263
+ "updated_at": "2026-02-01T12:00:00Z"
264
+ }</code>
265
+ </details>
266
+ </div>
267
+
268
+ <!-- Endpoint 4: List All Datastores -->
269
+ <div>
270
+ <div class="flex items-center justify-between mb-1">
271
+ <h4 class="text-xs text-code-muted uppercase tracking-wider">List All Datastores</h4>
272
+ <button type="button" onclick="copyCode('curl-list')"
273
+ class="text-xs text-code-accent hover:underline">Copy</button>
274
+ </div>
275
+ <code id="curl-list" class="block p-2 bg-code-surface rounded text-xs font-mono text-code-text overflow-x-auto whitespace-pre">curl -H "Authorization: Bearer YOUR_TOKEN" \
276
+ "{{ request.scheme }}://{{ request.get_host }}/api/v1/datastores/"</code>
277
+ <details class="mt-2">
278
+ <summary class="text-xs text-code-muted cursor-pointer hover:text-code-accent">Sample response</summary>
279
+ <code class="block mt-1 p-2 bg-code-surface rounded text-xs font-mono text-code-muted overflow-x-auto whitespace-pre">{
280
+ "datastores": [
281
+ {
282
+ "name": "{{ datastore.name }}",
283
+ "description": "{{ datastore.description|default:"" }}",
284
+ "entry_count": {{ entries.count }}
285
+ }
286
+ ],
287
+ "count": 1
288
+ }</code>
289
+ </details>
290
+ </div>
291
+
292
+ <!-- Rate Limiting Info -->
293
+ <div class="pt-3 border-t border-code-border">
294
+ <h4 class="text-xs text-code-muted uppercase tracking-wider mb-2">Rate Limits & Pagination</h4>
295
+ <ul class="text-xs text-code-muted space-y-1">
296
+ <li>• Rate limit: 60 requests per minute per token</li>
297
+ <li>• Default page size: 50 entries (max: 100)</li>
298
+ <li>• Pagination params: <code class="px-1 bg-code-surface rounded">page</code>, <code class="px-1 bg-code-surface rounded">page_size</code></li>
299
+ </ul>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+
306
+ <script>
307
+ function toggleApiExamples() {
308
+ const examples = document.getElementById('api-examples');
309
+ const toggleBtn = document.getElementById('api-toggle-btn');
310
+ if (examples.classList.contains('hidden')) {
311
+ examples.classList.remove('hidden');
312
+ toggleBtn.textContent = 'Hide curl examples';
313
+ } else {
314
+ examples.classList.add('hidden');
315
+ toggleBtn.textContent = 'Show curl examples';
316
+ }
317
+ }
318
+
319
+ function copyCode(elementId) {
320
+ const codeElement = document.getElementById(elementId);
321
+ if (codeElement) {
322
+ navigator.clipboard.writeText(codeElement.textContent.trim()).then(() => {
323
+ const btn = event.currentTarget;
324
+ const original = btn.textContent;
325
+ btn.textContent = 'Copied!';
326
+ setTimeout(() => { btn.textContent = original; }, 1500);
327
+ });
328
+ }
329
+ }
330
+ </script>
331
+ {% endblock %}
templates/cpanel/datastores/edit.html ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit {{ datastore.name }} - Data Stores - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:datastore_list' %}" class="hover:text-code-accent">Data Stores</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <a href="{% url 'cpanel:datastore_detail' pk=datastore.pk %}" class="hover:text-code-accent">{{ datastore.name }}</a>
18
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
20
+ </svg>
21
+ <span class="text-code-text">Edit</span>
22
+ </nav>
23
+ <h1 class="text-2xl font-bold text-code-text">Edit Data Store</h1>
24
+ <p class="text-code-muted">Update the data store name and description</p>
25
+ </div>
26
+
27
+ <!-- Form -->
28
+ <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl">
29
+ <form method="post">
30
+ {% csrf_token %}
31
+
32
+ <!-- Store Name -->
33
+ <div class="mb-6">
34
+ <label for="id_name" class="block text-sm font-medium text-code-text mb-2">
35
+ {{ form.name.label }}
36
+ </label>
37
+ {{ form.name }}
38
+ {% if form.name.help_text %}
39
+ <p class="mt-1 text-sm text-code-muted">{{ form.name.help_text }}</p>
40
+ {% endif %}
41
+ {% if form.name.errors %}
42
+ <p class="mt-1 text-sm text-code-red">{{ form.name.errors.0 }}</p>
43
+ {% endif %}
44
+ </div>
45
+
46
+ <!-- Description -->
47
+ <div class="mb-6">
48
+ <label for="id_description" class="block text-sm font-medium text-code-text mb-2">
49
+ {{ form.description.label }}
50
+ </label>
51
+ {{ form.description }}
52
+ {% if form.description.help_text %}
53
+ <p class="mt-1 text-sm text-code-muted">{{ form.description.help_text }}</p>
54
+ {% endif %}
55
+ {% if form.description.errors %}
56
+ <p class="mt-1 text-sm text-code-red">{{ form.description.errors.0 }}</p>
57
+ {% endif %}
58
+ </div>
59
+
60
+ <!-- Warning -->
61
+ <div class="mb-6 p-4 bg-code-yellow/10 border border-code-yellow/30 rounded-lg">
62
+ <div class="flex items-start space-x-3">
63
+ <svg class="w-5 h-5 text-code-yellow mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
64
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
65
+ </svg>
66
+ <div class="text-sm text-code-yellow">
67
+ <p class="font-medium">Changing the name?</p>
68
+ <p class="mt-1">If you change the name, scripts using the old name will fail. Update your scripts to use the new name.</p>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Actions -->
74
+ <div class="flex items-center justify-end space-x-4 pt-6 border-t border-code-border">
75
+ <a href="{% url 'cpanel:datastore_detail' pk=datastore.pk %}"
76
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors">
77
+ Cancel
78
+ </a>
79
+ <button type="submit"
80
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors">
81
+ Save Changes
82
+ </button>
83
+ </div>
84
+ </form>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ {% endblock %}
templates/cpanel/datastores/entry_create.html ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Add Entry - {{ datastore.name }} - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:datastore_list' %}" class="hover:text-code-accent">Data Stores</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <a href="{% url 'cpanel:datastore_detail' pk=datastore.pk %}" class="hover:text-code-accent">{{ datastore.name }}</a>
18
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
20
+ </svg>
21
+ <span class="text-code-text">Add Entry</span>
22
+ </nav>
23
+ <h1 class="text-2xl font-bold text-code-text">Add Entry</h1>
24
+ <p class="text-code-muted">Add a new key-value entry to <span class="font-mono text-code-accent">{{ datastore.name }}</span></p>
25
+ </div>
26
+
27
+ <!-- Form -->
28
+ <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl">
29
+ <form method="post">
30
+ {% csrf_token %}
31
+
32
+ <!-- Key -->
33
+ <div class="mb-6">
34
+ <label for="id_key" class="block text-sm font-medium text-code-text mb-2">
35
+ {{ form.key.label }}
36
+ </label>
37
+ {{ form.key }}
38
+ {% if form.key.help_text %}
39
+ <p class="mt-1 text-sm text-code-muted">{{ form.key.help_text }}</p>
40
+ {% endif %}
41
+ {% if form.key.errors %}
42
+ <p class="mt-1 text-sm text-code-red">{{ form.key.errors.0 }}</p>
43
+ {% endif %}
44
+ </div>
45
+
46
+ <!-- Value -->
47
+ <div class="mb-6">
48
+ <label for="id_value" class="block text-sm font-medium text-code-text mb-2">
49
+ {{ form.value.label }}
50
+ </label>
51
+ {{ form.value }}
52
+ {% if form.value.help_text %}
53
+ <p class="mt-1 text-sm text-code-muted">{{ form.value.help_text }}</p>
54
+ {% endif %}
55
+ {% if form.value.errors %}
56
+ <p class="mt-1 text-sm text-code-red">{{ form.value.errors.0 }}</p>
57
+ {% endif %}
58
+ </div>
59
+
60
+ <!-- JSON Examples -->
61
+ <div class="mb-6 p-4 bg-code-bg border border-code-border rounded-lg">
62
+ <h4 class="text-sm font-medium text-code-text mb-2">Valid JSON Examples</h4>
63
+ <div class="grid grid-cols-2 gap-4 text-sm font-mono">
64
+ <div>
65
+ <p class="text-code-muted mb-1">String:</p>
66
+ <code class="text-code-accent">"hello world"</code>
67
+ </div>
68
+ <div>
69
+ <p class="text-code-muted mb-1">Number:</p>
70
+ <code class="text-code-accent">42</code> or <code class="text-code-accent">3.14</code>
71
+ </div>
72
+ <div>
73
+ <p class="text-code-muted mb-1">Boolean:</p>
74
+ <code class="text-code-accent">true</code> or <code class="text-code-accent">false</code>
75
+ </div>
76
+ <div>
77
+ <p class="text-code-muted mb-1">Null:</p>
78
+ <code class="text-code-accent">null</code>
79
+ </div>
80
+ <div>
81
+ <p class="text-code-muted mb-1">Array:</p>
82
+ <code class="text-code-accent">[1, 2, 3]</code>
83
+ </div>
84
+ <div>
85
+ <p class="text-code-muted mb-1">Object:</p>
86
+ <code class="text-code-accent">{"key": "value"}</code>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Actions -->
92
+ <div class="flex items-center justify-end space-x-4 pt-6 border-t border-code-border">
93
+ <a href="{% url 'cpanel:datastore_detail' pk=datastore.pk %}"
94
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors">
95
+ Cancel
96
+ </a>
97
+ <button type="submit"
98
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors">
99
+ Add Entry
100
+ </button>
101
+ </div>
102
+ </form>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ {% endblock %}
templates/cpanel/datastores/entry_edit.html ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Edit Entry - {{ datastore.name }} - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="mb-8">
12
+ <nav class="flex items-center space-x-2 text-sm text-code-muted mb-4">
13
+ <a href="{% url 'cpanel:datastore_list' %}" class="hover:text-code-accent">Data Stores</a>
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
16
+ </svg>
17
+ <a href="{% url 'cpanel:datastore_detail' pk=datastore.pk %}" class="hover:text-code-accent">{{ datastore.name }}</a>
18
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
20
+ </svg>
21
+ <span class="text-code-text">Edit Entry</span>
22
+ </nav>
23
+ <h1 class="text-2xl font-bold text-code-text">Edit Entry</h1>
24
+ <p class="text-code-muted">Editing <span class="font-mono text-code-accent">{{ entry.key }}</span> in <span class="font-mono text-code-accent">{{ datastore.name }}</span></p>
25
+ </div>
26
+
27
+ <!-- Form -->
28
+ <div class="bg-code-surface border border-code-border rounded-xl p-6 max-w-2xl">
29
+ <form method="post">
30
+ {% csrf_token %}
31
+
32
+ <!-- Key -->
33
+ <div class="mb-6">
34
+ <label for="id_key" class="block text-sm font-medium text-code-text mb-2">
35
+ {{ form.key.label }}
36
+ </label>
37
+ {{ form.key }}
38
+ {% if form.key.help_text %}
39
+ <p class="mt-1 text-sm text-code-muted">{{ form.key.help_text }}</p>
40
+ {% endif %}
41
+ {% if form.key.errors %}
42
+ <p class="mt-1 text-sm text-code-red">{{ form.key.errors.0 }}</p>
43
+ {% endif %}
44
+ </div>
45
+
46
+ <!-- Value -->
47
+ <div class="mb-6">
48
+ <label for="id_value" class="block text-sm font-medium text-code-text mb-2">
49
+ {{ form.value.label }}
50
+ </label>
51
+ {{ form.value }}
52
+ {% if form.value.help_text %}
53
+ <p class="mt-1 text-sm text-code-muted">{{ form.value.help_text }}</p>
54
+ {% endif %}
55
+ {% if form.value.errors %}
56
+ <p class="mt-1 text-sm text-code-red">{{ form.value.errors.0 }}</p>
57
+ {% endif %}
58
+ </div>
59
+
60
+ <!-- JSON Examples -->
61
+ <div class="mb-6 p-4 bg-code-bg border border-code-border rounded-lg">
62
+ <h4 class="text-sm font-medium text-code-text mb-2">Valid JSON Examples</h4>
63
+ <div class="grid grid-cols-2 gap-4 text-sm font-mono">
64
+ <div>
65
+ <p class="text-code-muted mb-1">String:</p>
66
+ <code class="text-code-accent">"hello world"</code>
67
+ </div>
68
+ <div>
69
+ <p class="text-code-muted mb-1">Number:</p>
70
+ <code class="text-code-accent">42</code> or <code class="text-code-accent">3.14</code>
71
+ </div>
72
+ <div>
73
+ <p class="text-code-muted mb-1">Boolean:</p>
74
+ <code class="text-code-accent">true</code> or <code class="text-code-accent">false</code>
75
+ </div>
76
+ <div>
77
+ <p class="text-code-muted mb-1">Null:</p>
78
+ <code class="text-code-accent">null</code>
79
+ </div>
80
+ <div>
81
+ <p class="text-code-muted mb-1">Array:</p>
82
+ <code class="text-code-accent">[1, 2, 3]</code>
83
+ </div>
84
+ <div>
85
+ <p class="text-code-muted mb-1">Object:</p>
86
+ <code class="text-code-accent">{"key": "value"}</code>
87
+ </div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Actions -->
92
+ <div class="flex items-center justify-end space-x-4 pt-6 border-t border-code-border">
93
+ <a href="{% url 'cpanel:datastore_detail' pk=datastore.pk %}"
94
+ class="px-4 py-2 bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded-lg transition-colors">
95
+ Cancel
96
+ </a>
97
+ <button type="submit"
98
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors">
99
+ Save Changes
100
+ </button>
101
+ </div>
102
+ </form>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ {% endblock %}
templates/cpanel/datastores/list.html ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Data Stores - PyRunner{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="flex">
7
+ {% include "cpanel/_sidebar.html" %}
8
+
9
+ <div class="flex-1 p-8">
10
+ <!-- Header -->
11
+ <div class="flex items-start justify-between mb-8">
12
+ <div>
13
+ <h1 class="text-2xl font-bold text-code-text">Data Stores</h1>
14
+ <p class="text-code-muted">Simple key-value storage for scripts backed by the local database</p>
15
+ </div>
16
+ <a href="{% url 'cpanel:datastore_create' %}"
17
+ class="px-4 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors inline-flex items-center">
18
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
20
+ </svg>
21
+ New Data Store
22
+ </a>
23
+ </div>
24
+
25
+ <!-- Messages -->
26
+ {% if messages %}
27
+ <div class="mb-6 space-y-2">
28
+ {% for message in messages %}
29
+ <div class="p-4 rounded-lg {% if message.tags == 'error' %}bg-code-red/10 border border-code-red/30 text-code-red{% elif message.tags == 'success' %}bg-code-green/10 border border-code-green/30 text-code-green{% else %}bg-code-accent/10 border border-code-accent/30 text-code-accent{% endif %}">
30
+ {{ message }}
31
+ </div>
32
+ {% endfor %}
33
+ </div>
34
+ {% endif %}
35
+
36
+ <!-- Overall Storage Usage -->
37
+ {% if datastores %}
38
+ <div class="mb-6 p-4 bg-code-surface border border-code-border rounded-lg flex items-center justify-between">
39
+ <div class="flex items-center">
40
+ <div class="w-10 h-10 bg-code-accent/20 rounded-lg flex items-center justify-center mr-3">
41
+ <svg class="w-5 h-5 text-code-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
43
+ </svg>
44
+ </div>
45
+ <div>
46
+ <p class="text-sm text-code-muted">Total Storage Used</p>
47
+ <p class="text-lg font-semibold text-code-text">{{ total_size_display }}</p>
48
+ </div>
49
+ </div>
50
+ <span class="text-code-muted text-sm">{{ datastore_count }} data store{{ datastore_count|pluralize }}</span>
51
+ </div>
52
+ {% endif %}
53
+
54
+ <!-- Data Stores Table -->
55
+ {% if datastores %}
56
+ <div class="bg-code-surface border border-code-border rounded-xl overflow-hidden">
57
+ <table class="w-full">
58
+ <thead class="bg-code-bg border-b border-code-border">
59
+ <tr>
60
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Name</th>
61
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Description</th>
62
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Entries</th>
63
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Size</th>
64
+ <th class="px-6 py-3 text-left text-xs font-semibold text-code-muted uppercase tracking-wider">Updated</th>
65
+ <th class="px-6 py-3 text-right text-xs font-semibold text-code-muted uppercase tracking-wider">Actions</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody class="divide-y divide-code-border">
69
+ {% for store in datastores %}
70
+ <tr class="hover:bg-code-bg/50 transition-colors">
71
+ <td class="px-6 py-4">
72
+ <a href="{% url 'cpanel:datastore_detail' pk=store.pk %}" class="font-mono text-code-accent hover:underline">
73
+ {{ store.name }}
74
+ </a>
75
+ </td>
76
+ <td class="px-6 py-4 text-code-text text-sm max-w-xs truncate">
77
+ {{ store.description|default:"-" }}
78
+ </td>
79
+ <td class="px-6 py-4 text-code-muted text-sm">
80
+ {{ store.entry_count }}
81
+ </td>
82
+ <td class="px-6 py-4 text-code-muted text-sm">
83
+ {{ store.size_display }}
84
+ </td>
85
+ <td class="px-6 py-4 text-code-muted text-sm">
86
+ {{ store.updated_at|timesince }} ago
87
+ </td>
88
+ <td class="px-6 py-4 text-right">
89
+ <div class="flex items-center justify-end space-x-2">
90
+ <a href="{% url 'cpanel:datastore_detail' pk=store.pk %}"
91
+ class="px-3 py-1.5 text-sm bg-code-surface border border-code-border hover:border-code-accent text-code-text rounded transition-colors">
92
+ View
93
+ </a>
94
+ <form method="post" action="{% url 'cpanel:datastore_delete' pk=store.pk %}"
95
+ onsubmit="return confirm('Are you sure you want to delete the data store &quot;{{ store.name }}&quot;? All entries will be permanently deleted.');"
96
+ class="inline">
97
+ {% csrf_token %}
98
+ <button type="submit"
99
+ class="px-3 py-1.5 text-sm bg-code-red/10 border border-code-red/30 hover:bg-code-red/20 text-code-red rounded transition-colors">
100
+ Delete
101
+ </button>
102
+ </form>
103
+ </div>
104
+ </td>
105
+ </tr>
106
+ {% endfor %}
107
+ </tbody>
108
+ </table>
109
+ </div>
110
+
111
+ <!-- Usage Info -->
112
+ <div class="mt-6 p-4 bg-code-bg border border-code-border rounded-lg">
113
+ <h3 class="text-sm font-medium text-code-text mb-2">Using Data Stores in Scripts</h3>
114
+ <p class="text-sm text-code-muted mb-2">
115
+ Data stores provide simple key-value storage that persists between script runs.
116
+ </p>
117
+ <code class="block p-3 bg-code-surface rounded text-sm font-mono text-code-text whitespace-pre">from pyrunner_datastore import DataStore
118
+
119
+ store = DataStore("{{ datastores.0.name|default:'my_store' }}")
120
+ store["key"] = {"any": "json value"}
121
+ value = store.get("key", default=None)</code>
122
+ </div>
123
+ {% else %}
124
+ <div class="bg-code-surface border border-code-border rounded-xl p-12 text-center">
125
+ <div class="w-16 h-16 bg-code-accent/20 rounded-full flex items-center justify-center mx-auto mb-4">
126
+ <svg class="w-8 h-8 text-code-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/>
128
+ </svg>
129
+ </div>
130
+ <h3 class="text-lg font-semibold text-code-text mb-2">No data stores yet</h3>
131
+ <p class="text-code-muted mb-6">Create a data store to enable simple persistent storage for your scripts.</p>
132
+ <a href="{% url 'cpanel:datastore_create' %}"
133
+ class="px-6 py-2 bg-code-accent hover:bg-code-accent/90 text-white font-semibold rounded-lg transition-colors inline-flex items-center">
134
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
135
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
136
+ </svg>
137
+ Create Data Store
138
+ </a>
139
+ </div>
140
+ {% endif %}
141
+ </div>
142
+ </div>
143
+ {% endblock %}