@stacksjs/defaults 0.74.32 → 0.74.34
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/ai/skills/stacks-analytics/SKILL.md +104 -26
- package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
- package/ai/skills/stacks-dashboard/SKILL.md +60 -0
- package/ai/skills/stacks-orm/SKILL.md +1 -1
- package/ai/skills/stacks-storage/SKILL.md +58 -4
- package/ai/skills/stacks-technical-diagrams/SKILL.md +1 -1
- package/app/Actions/Dashboard/Content/FileDuplicateAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileFavoriteAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileRenameAction.ts +25 -0
- package/app/Actions/Dashboard/Content/FileReprocessAction.ts +31 -0
- package/app/Actions/Dashboard/Content/FileTagsAction.ts +27 -0
- package/app/Actions/Dashboard/Content/FileVisibilityAction.ts +25 -0
- package/app/Actions/Dashboard/Content/file-manager.test.ts +227 -9
- package/app/Actions/Dashboard/Content/file-manager.ts +544 -11
- package/app/Actions/Dashboard/Content/file-metadata-store.ts +432 -0
- package/app/Actions/Dashboard/Content/file-metadata.test.ts +357 -0
- package/app/Actions/Dashboard/Content/file-metadata.ts +550 -0
- package/app/Actions/Dashboard/Content/file-pipeline.test.ts +344 -0
- package/app/Jobs/OptimizeStorageImageJob.ts +74 -0
- package/app/Jobs/TagStorageMediaJob.ts +122 -0
- package/app/Jobs/TranscodeStorageVideoJob.ts +88 -0
- package/app/Models/StorageItem.ts +123 -0
- package/app/Models/StorageItemTask.ts +134 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/routes/dashboard-api.ts +13 -0
- package/vcs/github/workflows/buddy-bot.yml +109 -0
- package/vcs/github/workflows/ci.yml +3 -3
- package/vcs/github/workflows/release.yml +1 -1
- package/resources/assets/fonts/Monaco.ttf +0 -0
- package/vcs/github/renovate.json +0 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-analytics
|
|
3
|
-
description: Use when adding analytics to a Stacks application - configuring Fathom or self-hosted analytics, generating tracking scripts, privacy-friendly analytics setup, or the analytics configuration. Covers @stacksjs/analytics and config/analytics.ts.
|
|
3
|
+
description: Use when adding analytics to a Stacks application - configuring Fathom, Plausible, Google Analytics or self-hosted analytics, generating tracking scripts, privacy-friendly analytics setup, or the analytics configuration. Covers @stacksjs/analytics and config/analytics.ts.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -8,24 +8,84 @@ allowed-tools: Read Edit Write Bash Grep Glob
|
|
|
8
8
|
|
|
9
9
|
# Stacks Analytics
|
|
10
10
|
|
|
11
|
-
Privacy-friendly analytics with Fathom
|
|
11
|
+
Privacy-friendly analytics with four drivers: Fathom, Plausible, Google Analytics
|
|
12
|
+
and self-hosted.
|
|
12
13
|
|
|
13
14
|
## Key Paths
|
|
14
15
|
- Core package: `storage/framework/core/analytics/src/`
|
|
16
|
+
- Drivers: `storage/framework/core/analytics/src/drivers/`
|
|
17
|
+
- Registry (dispatch on the configured driver): `storage/framework/core/analytics/src/registry.ts`
|
|
15
18
|
- Configuration: `config/analytics.ts`
|
|
16
19
|
|
|
20
|
+
## Driver registry
|
|
21
|
+
|
|
22
|
+
Read `config/analytics.ts` and render the driver it names. This is the entry
|
|
23
|
+
point; reach for an individual driver only when you need one specific script
|
|
24
|
+
regardless of config.
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { generateAnalyticsScript, getAnalyticsHead } from '@stacksjs/analytics'
|
|
28
|
+
import { analytics } from '@stacksjs/config'
|
|
29
|
+
|
|
30
|
+
// HTML you can drop into a layout <head>
|
|
31
|
+
const script = generateAnalyticsScript(analytics)
|
|
32
|
+
|
|
33
|
+
// Or the [tag, attributes] pairs a docs `head` array takes
|
|
34
|
+
const head = getAnalyticsHead(analytics)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The registry is loud on purpose. A `driver` value with no implementation throws,
|
|
38
|
+
and so does a driver that is selected but missing its config (the message names
|
|
39
|
+
the exact key to set). Only an app with no `driver` at all gets an empty result.
|
|
40
|
+
|
|
41
|
+
Nothing injects the script for you - a framework that silently posted pageviews
|
|
42
|
+
to a third party would be the wrong default. Render it yourself in the layout
|
|
43
|
+
that should carry it.
|
|
44
|
+
|
|
17
45
|
## Drivers
|
|
18
46
|
|
|
19
|
-
### Fathom
|
|
20
|
-
|
|
47
|
+
### Fathom
|
|
48
|
+
Hosted, cookie-free, GDPR-compliant. Requires a Fathom account.
|
|
49
|
+
`scriptUrl` points the tag at your own origin so a content blocker does not drop it.
|
|
21
50
|
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
```typescript
|
|
52
|
+
import { generateFathomScript } from '@stacksjs/analytics'
|
|
53
|
+
|
|
54
|
+
generateFathomScript({
|
|
55
|
+
siteId: 'ABCDEFGH',
|
|
56
|
+
honorDnt: true, // -> data-honor-dnt="true"
|
|
57
|
+
spa: true, // -> data-spa="auto"
|
|
58
|
+
scriptUrl: undefined, // defaults to Fathom's CDN
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Plausible
|
|
63
|
+
Cookie-free, hosted or self-hosted. `hashMode` and `trackLocalhost` pick the
|
|
64
|
+
script variant (`script.hash.js`, `script.local.js`); `scriptUrl` overrides the
|
|
65
|
+
computed URL entirely, which is how you point at a self-hosted install.
|
|
24
66
|
|
|
25
67
|
```typescript
|
|
26
|
-
import {
|
|
68
|
+
import { generatePlausibleScript } from '@stacksjs/analytics'
|
|
69
|
+
|
|
70
|
+
generatePlausibleScript({ domain: 'example.com', hashMode: true })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Google Analytics
|
|
74
|
+
GA4 via gtag.js. Emits the loader plus the inline `js` / `config` bootstrap;
|
|
75
|
+
`debug: true` sets `debug_mode` so the property shows up in DebugView.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { generateGoogleAnalyticsScript } from '@stacksjs/analytics'
|
|
79
|
+
|
|
80
|
+
generateGoogleAnalyticsScript({ trackingId: 'G-XXXXXXXXXX', debug: false })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Self-Hosted
|
|
84
|
+
A small first-party tracker that POSTs to your own endpoint.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { generateSelfHostedScript, getSelfHostedAnalyticsHead } from '@stacksjs/analytics'
|
|
27
88
|
|
|
28
|
-
// Generate tracking script tag
|
|
29
89
|
const script = generateSelfHostedScript({
|
|
30
90
|
siteId: 'ABCDEF',
|
|
31
91
|
apiEndpoint: 'https://analytics.myapp.com/api/event',
|
|
@@ -34,19 +94,34 @@ const script = generateSelfHostedScript({
|
|
|
34
94
|
trackOutboundLinks: true // track external link clicks
|
|
35
95
|
})
|
|
36
96
|
|
|
37
|
-
// Generate head configuration for STX
|
|
38
97
|
const headConfig = getSelfHostedAnalyticsHead({
|
|
39
98
|
siteId: 'ABCDEF',
|
|
40
99
|
apiEndpoint: 'https://analytics.myapp.com/api/event'
|
|
41
100
|
})
|
|
42
|
-
|
|
43
|
-
// Generate inline script (no external JS file)
|
|
44
|
-
const inline = generateInlineScript(config)
|
|
45
101
|
```
|
|
46
102
|
|
|
47
|
-
##
|
|
103
|
+
## Driver config interfaces
|
|
48
104
|
|
|
49
105
|
```typescript
|
|
106
|
+
interface FathomConfig {
|
|
107
|
+
siteId: string // Fathom site ID
|
|
108
|
+
scriptUrl?: string // serve the script first-party
|
|
109
|
+
honorDnt?: boolean // respect Do Not Track
|
|
110
|
+
spa?: boolean // re-record pageviews on client routing
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface PlausibleConfig {
|
|
114
|
+
domain: string // the site's domain
|
|
115
|
+
scriptUrl?: string // self-hosted Plausible or a proxy
|
|
116
|
+
trackLocalhost?: boolean // keep localhost pageviews
|
|
117
|
+
hashMode?: boolean // count hash routes
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface GoogleAnalyticsConfig {
|
|
121
|
+
trackingId: string // GA4 measurement ID
|
|
122
|
+
debug?: boolean // debug_mode -> DebugView
|
|
123
|
+
}
|
|
124
|
+
|
|
50
125
|
interface SelfHostedConfig {
|
|
51
126
|
siteId: string // unique site identifier
|
|
52
127
|
apiEndpoint: string // analytics API URL
|
|
@@ -60,18 +135,22 @@ interface SelfHostedConfig {
|
|
|
60
135
|
|
|
61
136
|
```typescript
|
|
62
137
|
{
|
|
63
|
-
driver: 'fathom', // 'fathom' | 'google-analytics'
|
|
138
|
+
driver: 'fathom', // 'fathom' | 'plausible' | 'google-analytics' | 'self-hosted'
|
|
64
139
|
drivers: {
|
|
65
|
-
googleAnalytics: {
|
|
66
|
-
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
siteId: '' // Fathom site ID
|
|
70
|
-
}
|
|
140
|
+
googleAnalytics: { trackingId: '' },
|
|
141
|
+
fathom: { siteId: '' },
|
|
142
|
+
plausible: { domain: '' },
|
|
143
|
+
selfHosted: { siteId: '', apiEndpoint: '' },
|
|
71
144
|
}
|
|
72
145
|
}
|
|
73
146
|
```
|
|
74
147
|
|
|
148
|
+
## First-party pageview capture
|
|
149
|
+
|
|
150
|
+
Separate from the drivers, and no script at all: `capturePageviews: true` has the
|
|
151
|
+
stx servers record page GETs into `analytics_events`, which is what feeds the
|
|
152
|
+
native `/analytics/pages`, `/referrers` and `/devices` dashboards.
|
|
153
|
+
|
|
75
154
|
## Dashboard Integration
|
|
76
155
|
|
|
77
156
|
Analytics dashboard at `/dashboard/analytics` displays:
|
|
@@ -81,11 +160,10 @@ Analytics dashboard at `/dashboard/analytics` displays:
|
|
|
81
160
|
- Engagement metrics
|
|
82
161
|
|
|
83
162
|
## Gotchas
|
|
84
|
-
-
|
|
85
|
-
-
|
|
163
|
+
- Selecting a driver does not inject anything - call `generateAnalyticsScript()` in your layout
|
|
164
|
+
- A misconfigured driver throws rather than emitting nothing; the message names the config key
|
|
86
165
|
- `honorDnt: true` respects browser Do Not Track settings
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
- Analytics scripts are generated server-side and injected into page head
|
|
90
|
-
- Fathom is a paid service — self-hosted is free but requires infrastructure
|
|
166
|
+
- Attribute values are escaped, and inline script values are JS-escaped, to prevent XSS
|
|
167
|
+
- Fathom is a paid service - self-hosted is free but requires infrastructure
|
|
91
168
|
- `trackOutboundLinks` adds click handlers to external `<a>` tags
|
|
169
|
+
- `capturePageviews` is server-side and independent of `driver`
|
|
@@ -66,7 +66,7 @@ globalThis.toggleDark = toggleDark
|
|
|
66
66
|
- **Custom Functions**: From `resources/functions/` (counter, dark mode, GPX, geo utilities)
|
|
67
67
|
|
|
68
68
|
### Server Auto-Imports (100+)
|
|
69
|
-
- **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (
|
|
69
|
+
- **All ORM Models**: User, Post, Author, Product, Order, Payment, Customer, etc. (102 models)
|
|
70
70
|
- **Request Models**: UserRequest, PostRequest, OrderRequest, etc.
|
|
71
71
|
- **Actions**: Action types and helpers
|
|
72
72
|
- **Schema**: validation schema builder
|
|
@@ -61,6 +61,66 @@ dashboard data Actions.
|
|
|
61
61
|
- `/content/comments` - comment moderation
|
|
62
62
|
- `/content/files`, `/content/blog`, `/content/seo` - files, blog operations, and SEO
|
|
63
63
|
|
|
64
|
+
### The file manager's two layers (stacksjs/stacks#2577)
|
|
65
|
+
|
|
66
|
+
Worth knowing before adding anything to it, because the split is not obvious
|
|
67
|
+
from the endpoints:
|
|
68
|
+
|
|
69
|
+
- **Storage operations** map to a `StorageAdapter` method and go straight to the
|
|
70
|
+
disk: list, upload, create folder, rename, visibility, duplicate, delete.
|
|
71
|
+
- **Metadata** - favourites and tags - has nowhere to live on a disk (extended
|
|
72
|
+
attributes do not survive a copy; S3 object metadata is set at write time, so
|
|
73
|
+
starring a 2 GB video would rewrite 2 GB). It lives in `storage_items`, keyed
|
|
74
|
+
by `(disk, path)`, written by `PUT /files/favorite` and `PUT /files/tags`.
|
|
75
|
+
|
|
76
|
+
**The disk is authoritative and the table is advisory.** The listing comes from
|
|
77
|
+
the disk and rows are joined onto it, so a path with no row is a file with
|
|
78
|
+
nothing recorded - which is most files. Renames and deletes made THROUGH the
|
|
79
|
+
dashboard reconcile eagerly (a folder is a prefix update, because moving a
|
|
80
|
+
folder moves everything under it); a completed listing sweeps rows for paths it
|
|
81
|
+
did not see, which is free because the walk already enumerated them. A TRUNCATED
|
|
82
|
+
listing sweeps nothing - it has not proved a path is absent.
|
|
83
|
+
|
|
84
|
+
A file renamed outside the dashboard loses its metadata, and that is by design:
|
|
85
|
+
a rename and a copy-then-delete are the same two events to a bucket listing, so
|
|
86
|
+
reconciling would be guessing.
|
|
87
|
+
|
|
88
|
+
### The media pipeline (stacksjs/stacks#2578)
|
|
89
|
+
|
|
90
|
+
None of the three things an upload might need can happen inside the request: a
|
|
91
|
+
transcode is minutes, a vision call is a round trip to a third party. So an
|
|
92
|
+
upload dispatches and the dashboard shows state.
|
|
93
|
+
|
|
94
|
+
- `storage_item_tasks`, one row per `(disk, path, kind)`, kind being
|
|
95
|
+
`optimize` (images, via `ts-images`), `transcode` (video, via `ts-videos`) or
|
|
96
|
+
`tag` (a vision model). They succeed and fail independently, which is why this
|
|
97
|
+
is not a column on `storage_items` - a video whose transcode finished and
|
|
98
|
+
whose tagging failed is a normal state.
|
|
99
|
+
- `dispatchDashboardFileTasks` decides from the CONTENT TYPE what a file needs.
|
|
100
|
+
Most uploads are documents and get nothing. A transcode waits for a video
|
|
101
|
+
profile, because the ladder is derived from the source dimensions.
|
|
102
|
+
- A dispatch failure is RECORDED, not thrown: a queue that is down leaves a
|
|
103
|
+
visible failure rather than an upload that fails or a file that is silently
|
|
104
|
+
never processed.
|
|
105
|
+
- `runTask` owns the queued -> running -> done/failed transitions so the three
|
|
106
|
+
jobs cannot disagree about them. It rethrows after recording, because the row
|
|
107
|
+
and the queue answer different questions - the queue decides whether to retry,
|
|
108
|
+
the row is what somebody looking at the file sees.
|
|
109
|
+
- `POST /files/reprocess` re-runs everything, or the kinds you name.
|
|
110
|
+
|
|
111
|
+
Derivatives are written back to the same disk under `.variants/<path>/`. The
|
|
112
|
+
leading dot keeps them out of the listing, which skips hidden components - a
|
|
113
|
+
folder of thirty derivatives beside every photo makes the browser useless.
|
|
114
|
+
|
|
115
|
+
**There is no ffmpeg.** #2578 asked whether video was in scope given the
|
|
116
|
+
external binary, its licensing and its provisioning; `@stacksjs/video` is built
|
|
117
|
+
on `ts-videos`, which encodes itself, so that question was already answered.
|
|
118
|
+
|
|
119
|
+
Tags go through `taggables` + `taggable_models` with `taggable_type =
|
|
120
|
+
'storage_items'` - the trait the CMS already uses. Do NOT declare a
|
|
121
|
+
`belongsToMany` to the `Tag` model for this: `taggable_models.tag_id` resolves
|
|
122
|
+
against `taggables`, which is a different table from `tags`.
|
|
123
|
+
|
|
64
124
|
### Data Management
|
|
65
125
|
- `/data/dashboard` - data overview
|
|
66
126
|
- `/data/users` - user management
|
|
@@ -11,7 +11,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
|
|
|
11
11
|
## Key Paths
|
|
12
12
|
- Core ORM package: `storage/framework/core/orm/src/`
|
|
13
13
|
- ORM implementation: `storage/framework/orm/`
|
|
14
|
-
- Model definitions: `storage/framework/defaults/app/Models/` (
|
|
14
|
+
- Model definitions: `storage/framework/defaults/app/Models/` (102 models)
|
|
15
15
|
- Application models: `app/Models/`
|
|
16
16
|
- Default model templates: `storage/framework/defaults/app/Models/`
|
|
17
17
|
- ORM type globals: `storage/framework/types/orm-globals.d.ts`
|
|
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
|
|
|
8
8
|
|
|
9
9
|
# Stacks Storage
|
|
10
10
|
|
|
11
|
-
File system abstraction with a Laravel-style Storage facade, local/S3 adapters, file upload handling, and low-level file utilities.
|
|
11
|
+
File system abstraction with a Laravel-style Storage facade, local/S3/Azure adapters, file upload handling, and low-level file utilities.
|
|
12
12
|
|
|
13
13
|
## Key Paths
|
|
14
14
|
- Core package: `storage/framework/core/storage/src/`
|
|
@@ -18,6 +18,8 @@ File system abstraction with a Laravel-style Storage facade, local/S3 adapters,
|
|
|
18
18
|
- Filesystem config types: `storage/framework/core/storage/src/types/filesystem.ts`
|
|
19
19
|
- Local adapter: `storage/framework/core/storage/src/adapters/local.ts`
|
|
20
20
|
- S3 adapter: `storage/framework/core/storage/src/adapters/s3.ts`
|
|
21
|
+
- Azure adapter: `storage/framework/core/storage/src/adapters/azure.ts`
|
|
22
|
+
- Azure request signing: `storage/framework/core/storage/src/azure-signing.ts`
|
|
21
23
|
- Memory adapter: `storage/framework/core/storage/src/adapters/memory.ts`
|
|
22
24
|
- Bun adapter: `storage/framework/core/storage/src/adapters/bun.ts`
|
|
23
25
|
- File utilities: `storage/framework/core/storage/src/files.ts`
|
|
@@ -45,7 +47,7 @@ import { createLocalStorage, LocalStorageAdapter } from '@stacksjs/storage'
|
|
|
45
47
|
import { createS3Storage, S3StorageAdapter } from '@stacksjs/storage'
|
|
46
48
|
|
|
47
49
|
// Config helpers
|
|
48
|
-
import { localDisk, s3Disk, configFromEnv } from '@stacksjs/storage'
|
|
50
|
+
import { localDisk, s3Disk, azureDisk, r2Disk, gcsDisk, filebaseDisk, backblazeDisk, hetznerDisk, configFromEnv } from '@stacksjs/storage'
|
|
49
51
|
|
|
50
52
|
// Types
|
|
51
53
|
import type { StorageAdapter, FileContents, StatEntry, DirectoryEntry, DirectoryListing } from '@stacksjs/storage'
|
|
@@ -233,6 +235,46 @@ const s3 = new S3StorageAdapter(client, { bucket: 'my-bucket', region: 'us-east-
|
|
|
233
235
|
- `list()` supports pagination via continuation tokens; `deep: true` uses `listAllObjects()`
|
|
234
236
|
- `fileExists()` uses `headObject()` and catches 404/NoSuchKey/NotFound errors
|
|
235
237
|
|
|
238
|
+
Every S3-COMPATIBLE provider is an `s3` disk with a different endpoint, and has
|
|
239
|
+
a preset that fills it in: `r2Disk(bucket, accountId)`, `gcsDisk(bucket)`,
|
|
240
|
+
`filebaseDisk(bucket)`, `backblazeDisk(bucket, region)`,
|
|
241
|
+
`hetznerDisk(bucket, location)`. R2 additionally needs `url` set, because its
|
|
242
|
+
API host never serves public objects.
|
|
243
|
+
|
|
244
|
+
### Azure Adapter (`AzureBlobStorageAdapter`)
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { azureDisk, AzureBlobStorageAdapter } from '@stacksjs/storage'
|
|
248
|
+
|
|
249
|
+
const azure = new AzureBlobStorageAdapter({
|
|
250
|
+
account: 'mystorageaccount',
|
|
251
|
+
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
|
|
252
|
+
container: 'uploads',
|
|
253
|
+
prefix: 'tenant-7',
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Its own driver rather than an `s3` disk with an endpoint, because Azure is the
|
|
258
|
+
one provider with no S3-compatible API.
|
|
259
|
+
|
|
260
|
+
- Speaks the blob REST API over `fetch`; no SDK, and no lazy-client dance
|
|
261
|
+
- Shared Key authorization for server-side calls, service SAS for signed URLs,
|
|
262
|
+
both in `azure-signing.ts` and tested there directly
|
|
263
|
+
- `changeVisibility()` THROWS -- Azure has no per-blob ACL. Public access is a
|
|
264
|
+
container property, so `visibility()` reports the container's level, and a
|
|
265
|
+
per-object grant is `signedUrl()`
|
|
266
|
+
- `putStream()` stages blocks (Put Block) and commits one Put Block List, so the
|
|
267
|
+
write is atomic; block ids are zero-padded to a fixed width because Azure
|
|
268
|
+
rejects unequal-length ids
|
|
269
|
+
- `getStream()` is genuinely incremental, unlike the S3 adapter's buffered one
|
|
270
|
+
- `createDirectory()` is a no-op and `deleteDirectory()` deletes by prefix, same
|
|
271
|
+
as S3
|
|
272
|
+
- `endpoint` defaults to `https://<account>.blob.core.windows.net`; point it at
|
|
273
|
+
`http://127.0.0.1:10000/devstoreaccount1` for Azurite
|
|
274
|
+
- A `sasToken` disk can read and write but cannot sign URLs -- minting a SAS
|
|
275
|
+
needs the account key, and re-serving the configured token would hand out its
|
|
276
|
+
full grant
|
|
277
|
+
|
|
236
278
|
## File Uploads (UploadedFile)
|
|
237
279
|
|
|
238
280
|
```typescript
|
|
@@ -490,8 +532,8 @@ interface ChecksumOptions {
|
|
|
490
532
|
}
|
|
491
533
|
|
|
492
534
|
// Filesystem config types
|
|
493
|
-
type FilesystemDriver = 'local' | 's3'
|
|
494
|
-
type DiskConfig = LocalDiskConfig | S3DiskConfig
|
|
535
|
+
type FilesystemDriver = 'local' | 's3' | 'azure'
|
|
536
|
+
type DiskConfig = LocalDiskConfig | S3DiskConfig | AzureDiskConfig
|
|
495
537
|
|
|
496
538
|
interface LocalDiskConfig {
|
|
497
539
|
driver: 'local'
|
|
@@ -512,6 +554,18 @@ interface S3DiskConfig {
|
|
|
512
554
|
visibility?: 'public' | 'private'
|
|
513
555
|
}
|
|
514
556
|
|
|
557
|
+
interface AzureDiskConfig {
|
|
558
|
+
driver: 'azure'
|
|
559
|
+
account: string
|
|
560
|
+
container: string
|
|
561
|
+
accountKey?: string
|
|
562
|
+
sasToken?: string
|
|
563
|
+
prefix?: string
|
|
564
|
+
url?: string
|
|
565
|
+
endpoint?: string
|
|
566
|
+
visibility?: 'public' | 'private'
|
|
567
|
+
}
|
|
568
|
+
|
|
515
569
|
interface FilesystemConfig {
|
|
516
570
|
default: string
|
|
517
571
|
disks: Record<string, DiskConfig>
|
|
@@ -30,7 +30,7 @@ Run `bun --config=storage/framework/defaults/ai/skills/stacks-technical-diagrams
|
|
|
30
30
|
1. Inspect the implementation before drawing. Treat `app/` as overrides and `storage/framework/defaults/app/` as fallbacks. Follow registrations in `app/Routes.ts`, route files, actions, jobs, listeners, middleware, models, config, resources, and framework entry points that matter to the requested view.
|
|
31
31
|
2. Read the relevant Stacks domain skill before mapping an unfamiliar subsystem. Common companions include `stacks-router`, `stacks-actions`, `stacks-models`, `stacks-database`, `stacks-jobs`, `stacks-events`, `stacks-realtime`, and `stacks-cloud`.
|
|
32
32
|
3. Draw one question per diagram. For a runtime overview, prefer browser or client -> stx/router -> action or service -> model/query -> database, then add only the external systems and trust boundaries needed for that story.
|
|
33
|
-
4. Write output to the user's requested path. When no path is given, use `docs/diagrams/<descriptive-name
|
|
33
|
+
4. Write output to the user's requested path. When no path is given, use `docs/public/diagrams/<descriptive-name>/index.html` and keep the source JSON beside it. That path matters: BunPress only renders `docs/**/*.md` and copies `docs/public/**`, so a diagram written anywhere else under `docs/` never reaches the built site. The directory-plus-`index.html` shape is what the deployed docs host serves at the clean URL `/docs/diagrams/<descriptive-name>` - a bare `<name>.html` is redirected to an extensionless path that does not exist. Link to it from a docs page as `/diagrams/<descriptive-name>` (BunPress adds the `/docs` base).
|
|
34
34
|
|
|
35
35
|
If Bun cannot run, fall back to architecture mode: copy `assets/template.html`, hand-place SVG using the design system below, and run the self-review checklist before delivering.
|
|
36
36
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { DashboardFileError, duplicateDashboardFile } from './file-manager'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'FileDuplicateAction',
|
|
8
|
+
description: 'Copies a file or directory beside the original on a configured storage disk.',
|
|
9
|
+
method: 'POST',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const duplicated = await duplicateDashboardFile({
|
|
13
|
+
disk: String(request.get('disk', 'public')),
|
|
14
|
+
path: request.get('path'),
|
|
15
|
+
name: request.get('name'),
|
|
16
|
+
})
|
|
17
|
+
return response.json(duplicated)
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof DashboardFileError)
|
|
21
|
+
return response.json({ message: error.message, fields: error.fields }, error.status)
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { DashboardFileError, setDashboardFileFavorite } from './file-manager'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'FileFavoriteAction',
|
|
8
|
+
description: 'Stars or unstars a file or directory on a configured storage disk.',
|
|
9
|
+
method: 'PUT',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await setDashboardFileFavorite({
|
|
13
|
+
disk: String(request.get('disk', 'public')),
|
|
14
|
+
path: request.get('path'),
|
|
15
|
+
favorite: request.get('favorite'),
|
|
16
|
+
})
|
|
17
|
+
return response.json(result)
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof DashboardFileError)
|
|
21
|
+
return response.json({ message: error.message, fields: error.fields }, error.status)
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { DashboardFileError, renameDashboardFile } from './file-manager'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'FileRenameAction',
|
|
8
|
+
description: 'Renames a file or directory in place on a configured storage disk.',
|
|
9
|
+
method: 'PATCH',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const renamed = await renameDashboardFile({
|
|
13
|
+
disk: String(request.get('disk', 'public')),
|
|
14
|
+
path: request.get('path'),
|
|
15
|
+
name: request.get('name'),
|
|
16
|
+
})
|
|
17
|
+
return response.json(renamed)
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof DashboardFileError)
|
|
21
|
+
return response.json({ message: error.message, fields: error.fields }, error.status)
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { DashboardFileError, reprocessDashboardFile } from './file-manager'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'FileReprocessAction',
|
|
8
|
+
description: 'Re-runs the background processing for a file: image variants, video renditions, AI tags.',
|
|
9
|
+
method: 'POST',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await reprocessDashboardFile({
|
|
13
|
+
disk: String(request.get('disk', 'public')),
|
|
14
|
+
path: request.get('path'),
|
|
15
|
+
// Omit to run every kind the file's content type calls for; name kinds
|
|
16
|
+
// to re-run just one, which is what a failed transcode beside a
|
|
17
|
+
// successful tagging wants.
|
|
18
|
+
kinds: request.get('kinds'),
|
|
19
|
+
// A transcode derives its ladder from the source dimensions, so it only
|
|
20
|
+
// runs when the caller supplies them.
|
|
21
|
+
videoProfile: request.get('profile'),
|
|
22
|
+
})
|
|
23
|
+
return response.json(result)
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error instanceof DashboardFileError)
|
|
27
|
+
return response.json({ message: error.message, fields: error.fields }, error.status)
|
|
28
|
+
throw error
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { DashboardFileError, setDashboardFileTags } from './file-manager'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'FileTagsAction',
|
|
8
|
+
description: 'Replaces the tags on a file or directory on a configured storage disk.',
|
|
9
|
+
method: 'PUT',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const result = await setDashboardFileTags({
|
|
13
|
+
disk: String(request.get('disk', 'public')),
|
|
14
|
+
path: request.get('path'),
|
|
15
|
+
// The whole set, not a delta: a UI that can add a tag can also remove
|
|
16
|
+
// one, and there is no separate signal for a removal.
|
|
17
|
+
tags: request.get('tags'),
|
|
18
|
+
})
|
|
19
|
+
return response.json(result)
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error instanceof DashboardFileError)
|
|
23
|
+
return response.json({ message: error.message, fields: error.fields }, error.status)
|
|
24
|
+
throw error
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RequestInstance } from '@stacksjs/types'
|
|
2
|
+
import { Action } from '@stacksjs/actions'
|
|
3
|
+
import { response } from '@stacksjs/router'
|
|
4
|
+
import { DashboardFileError, setDashboardFileVisibility } from './file-manager'
|
|
5
|
+
|
|
6
|
+
export default new Action({
|
|
7
|
+
name: 'FileVisibilityAction',
|
|
8
|
+
description: 'Sets a file or directory public or private on a configured storage disk.',
|
|
9
|
+
method: 'PUT',
|
|
10
|
+
async handle(request: RequestInstance) {
|
|
11
|
+
try {
|
|
12
|
+
const updated = await setDashboardFileVisibility({
|
|
13
|
+
disk: String(request.get('disk', 'public')),
|
|
14
|
+
path: request.get('path'),
|
|
15
|
+
visibility: request.get('visibility'),
|
|
16
|
+
})
|
|
17
|
+
return response.json(updated)
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof DashboardFileError)
|
|
21
|
+
return response.json({ message: error.message, fields: error.fields }, error.status)
|
|
22
|
+
throw error
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
})
|