uniweb 0.36.0 → 0.37.0
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/package.json +4 -4
- package/src/commands/publish.js +59 -25
- package/src/commands/update.js +38 -1
- package/src/framework-index.json +3 -3
- package/src/utils/dep-survey.js +53 -1
- package/src/utils/site-data-upload.js +101 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
+
"@uniweb/core": "^0.16.0",
|
|
44
45
|
"@uniweb/kit": "^0.15.2",
|
|
45
|
-
"@uniweb/semantic-parser": "^1.4.0",
|
|
46
46
|
"@uniweb/runtime": "^0.13.5",
|
|
47
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/semantic-parser": "^1.4.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "^0.
|
|
50
|
+
"@uniweb/build": "^0.33.0",
|
|
51
51
|
"@uniweb/content-reader": "^1.2.4",
|
|
52
52
|
"@uniweb/semantic-parser": "^1.4.0"
|
|
53
53
|
},
|
package/src/commands/publish.js
CHANGED
|
@@ -95,7 +95,10 @@ import {
|
|
|
95
95
|
reportPaymentRefusal
|
|
96
96
|
} from '../backend/payment-handoff.js'
|
|
97
97
|
import { reportSchemalessQueries } from '../utils/schemaless-report.js'
|
|
98
|
-
import {
|
|
98
|
+
import {
|
|
99
|
+
uploadSiteData,
|
|
100
|
+
describeDataRefusal
|
|
101
|
+
} from '../utils/site-data-upload.js'
|
|
99
102
|
|
|
100
103
|
const c = {
|
|
101
104
|
reset: '\x1b[0m',
|
|
@@ -456,7 +459,12 @@ export async function publish(args = []) {
|
|
|
456
459
|
// destructive — a placeholder file, created meaning to fill it in — so the count
|
|
457
460
|
// is reported and confirmed before anything is sent.
|
|
458
461
|
{
|
|
459
|
-
const guard = await guardEmptyRecords({
|
|
462
|
+
const guard = await guardEmptyRecords({
|
|
463
|
+
siteDir,
|
|
464
|
+
args,
|
|
465
|
+
warn: say.warn,
|
|
466
|
+
note: say.dim
|
|
467
|
+
})
|
|
460
468
|
if (!guard.ok) return { exitCode: 1 }
|
|
461
469
|
}
|
|
462
470
|
|
|
@@ -602,28 +610,56 @@ export async function publish(args = []) {
|
|
|
602
610
|
// does not exist, which is the failure this work kept catching in others.
|
|
603
611
|
// `client.discover()` is the mechanism if that changes — `DISCOVERY_DEFAULTS`
|
|
604
612
|
// makes an absent key non-breaking by construction.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
613
|
+
//
|
|
614
|
+
// ⛔ UNCONDITIONAL — `if (ball)` was here until 2026-09-01 and it was the bug.
|
|
615
|
+
//
|
|
616
|
+
// `collectSchemalessData` returns null for an empty set, so a publish that
|
|
617
|
+
// carried no schema-less data sent NO PLAN AT ALL. The backend reconciles a
|
|
618
|
+
// site's data usage against this manifest, and a request that never arrives
|
|
619
|
+
// is not a manifest saying "none" — it is silence, indistinguishable from a
|
|
620
|
+
// publish that never happened. So deleting your LAST schema-less collection
|
|
621
|
+
// — the exact operation the reconcile exists to make free — was the one
|
|
622
|
+
// operation that could not be expressed, and the site kept paying for it
|
|
623
|
+
// until the whole site was deleted.
|
|
624
|
+
//
|
|
625
|
+
// ⭐ The general shape, worth more than the fix: an EMPTY set and NO set are
|
|
626
|
+
// different statements, and an `if (x)` guard collapses them into one. The
|
|
627
|
+
// cost is always paid by whoever is downstream trying to tell them apart.
|
|
628
|
+
//
|
|
629
|
+
// Both halves agreed in channel backend-framework-82f2 (2026-09-01); the
|
|
630
|
+
// backend's route accepts an empty `files` array as of the same exchange.
|
|
631
|
+
say.info('Uploading schema-less record data…')
|
|
632
|
+
try {
|
|
633
|
+
const r = await uploadSiteData({
|
|
634
|
+
apiBase: client.origin,
|
|
635
|
+
token: await client.token(),
|
|
636
|
+
siteUuid: site.uuid,
|
|
637
|
+
ball,
|
|
638
|
+
onProgress: (m) => say.dim(` ${m}`)
|
|
639
|
+
})
|
|
640
|
+
if (r.failed.length) {
|
|
641
|
+
// A file whose bytes did not land must not be published: the site would
|
|
642
|
+
// serve a stale copy or 404, and the only trace would be a warning.
|
|
643
|
+
say.err(
|
|
644
|
+
`${r.failed.length} data file(s) failed to upload — not publishing.`
|
|
645
|
+
)
|
|
646
|
+
for (const f of r.failed) say.dim(` ${f.path} (HTTP ${f.status})`)
|
|
625
647
|
return { exitCode: 1 }
|
|
626
648
|
}
|
|
649
|
+
say.dim(`Record data : ${r.uploaded.length} file(s) [${r.mode}]`)
|
|
650
|
+
} catch (err) {
|
|
651
|
+
// A typed plan refusal gets its own account (quota, or whatever else the
|
|
652
|
+
// backend names); anything else falls through to the raw message. Same
|
|
653
|
+
// treatment the asset plan and the site create already get — this was the
|
|
654
|
+
// last door still printing the problem document at the user verbatim.
|
|
655
|
+
const refusal = describeDataRefusal(err)
|
|
656
|
+
if (refusal) {
|
|
657
|
+
say.err(refusal.headline)
|
|
658
|
+
for (const line of refusal.notes) say.dim(line)
|
|
659
|
+
} else {
|
|
660
|
+
say.err(`Record data upload failed: ${err.message}`)
|
|
661
|
+
}
|
|
662
|
+
return { exitCode: 1 }
|
|
627
663
|
}
|
|
628
664
|
|
|
629
665
|
// 5. Push the site (content + folder) over the send-only-changed cache —
|
|
@@ -680,9 +716,7 @@ export async function publish(args = []) {
|
|
|
680
716
|
? { baseVersions, itemBaseVersions: readItemBaseVersions(siteDir) }
|
|
681
717
|
: {}),
|
|
682
718
|
...(Object.keys(injectInfo).length ? { injectInfo } : {}),
|
|
683
|
-
...(Object.keys(ext.pins).length
|
|
684
|
-
? { injectExtensions: ext.pins }
|
|
685
|
-
: {}),
|
|
719
|
+
...(Object.keys(ext.pins).length ? { injectExtensions: ext.pins } : {}),
|
|
686
720
|
...(assetRewrite ? { assetRewrite } : {}),
|
|
687
721
|
...(assetIds ? { assetIds } : {})
|
|
688
722
|
})
|
package/src/commands/update.js
CHANGED
|
@@ -257,7 +257,15 @@ export function printSurvey(
|
|
|
257
257
|
statusText = `${colors.dim}aligned${colors.reset}`
|
|
258
258
|
} else if (row.status === 'behind') {
|
|
259
259
|
icon = `${colors.yellow}✗${colors.reset}`
|
|
260
|
-
|
|
260
|
+
// ⛔ `behind` alone was the whole message, in one colour, for every
|
|
261
|
+
// distance. In 0.x the minor slot is where our breaking changes live
|
|
262
|
+
// (`publish.js` derives it from a breaking marker and nothing else), so
|
|
263
|
+
// `^0.14.1 → ^0.16.0` is two releases a consumer must act on and
|
|
264
|
+
// `^0.15.0 → ^0.15.2` is not — and the table said the same thing about
|
|
265
|
+
// both. Naming the class is the difference between a list and a signal.
|
|
266
|
+
statusText = row.breaking
|
|
267
|
+
? `${colors.red}${row.bump} · BREAKING${colors.reset}`
|
|
268
|
+
: `${colors.yellow}${row.bump}${colors.reset}`
|
|
261
269
|
} else {
|
|
262
270
|
icon = `${colors.cyan}↑${colors.reset}`
|
|
263
271
|
statusText = `${colors.cyan}ahead of CLI${colors.reset}`
|
|
@@ -272,6 +280,35 @@ export function printSurvey(
|
|
|
272
280
|
` ${colors.dim}(${alignedCount} other${alignedCount === 1 ? '' : 's'} already aligned — ${colors.reset}${colors.cyan}--verbose${colors.reset}${colors.dim} to list)${colors.reset}`
|
|
273
281
|
)
|
|
274
282
|
}
|
|
283
|
+
|
|
284
|
+
// ⭐ **A summary, because the table scrolls and `--yes` does not stop.** The
|
|
285
|
+
// per-row label above is invisible to the case that matters most: a CI or an
|
|
286
|
+
// agent running `update --yes`, where nobody reads a table and the only
|
|
287
|
+
// artifact is a log. This block names the packages, so "what did that
|
|
288
|
+
// upgrade cross?" is answerable afterwards from the log alone.
|
|
289
|
+
//
|
|
290
|
+
// ⚖️ It reports; it does not gate. Crossing a 0.x minor IS the ordinary way
|
|
291
|
+
// to take a Uniweb update — gating it would gate nearly every real upgrade
|
|
292
|
+
// and make the verb useless. The decision stays the operator's; what changed
|
|
293
|
+
// is that they can now make it knowingly.
|
|
294
|
+
const breakingRows = report.rows.filter((r) => r.breaking)
|
|
295
|
+
if (breakingRows.length > 0) {
|
|
296
|
+
const names = [...new Set(breakingRows.map((r) => r.name))]
|
|
297
|
+
log('')
|
|
298
|
+
log(
|
|
299
|
+
`${colors.red}⚠${colors.reset} ${colors.bright}${names.length} package${names.length === 1 ? '' : 's'} cross${names.length === 1 ? 'es' : ''} a breaking boundary:${colors.reset}`
|
|
300
|
+
)
|
|
301
|
+
for (const name of names) {
|
|
302
|
+
const r = breakingRows.find((x) => x.name === name)
|
|
303
|
+
log(` ${colors.red}${name}${colors.reset} ${r.current} → ${r.target} ${colors.dim}(${r.bump})${colors.reset}`)
|
|
304
|
+
}
|
|
305
|
+
log(
|
|
306
|
+
` ${colors.dim}In 0.x the minor slot is where breaking changes go, so these are releases${colors.reset}`
|
|
307
|
+
)
|
|
308
|
+
log(
|
|
309
|
+
` ${colors.dim}you may need to act on. Read their changelogs before shipping.${colors.reset}`
|
|
310
|
+
)
|
|
311
|
+
}
|
|
275
312
|
log('')
|
|
276
313
|
}
|
|
277
314
|
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-02T16:36:26.382Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.2.2",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.33.0",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"deps": []
|
|
120
120
|
},
|
|
121
121
|
"@uniweb/unipress": {
|
|
122
|
-
"version": "0.8.
|
|
122
|
+
"version": "0.8.19",
|
|
123
123
|
"path": "framework/unipress",
|
|
124
124
|
"deps": [
|
|
125
125
|
"@uniweb/build",
|
package/src/utils/dep-survey.js
CHANGED
|
@@ -33,6 +33,53 @@ export function stripVersionRange(spec) {
|
|
|
33
33
|
)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* How far a bump moves, in the terms our versioning actually uses.
|
|
38
|
+
*
|
|
39
|
+
* ⭐ **In 0.x the MINOR slot is the breaking one, and it is the only channel we
|
|
40
|
+
* have.** `scripts/framework/publish.js` derives a package's bump from its own
|
|
41
|
+
* commits: a breaking marker means minor, everything else patch. So
|
|
42
|
+
* `^0.14.1 → ^0.16.0` is two breaking releases and `^0.15.0 → ^0.15.2` is not —
|
|
43
|
+
* and until 2026-09-02 `update` printed both as `behind`, in the same colour,
|
|
44
|
+
* and `--yes` applied them without a word.
|
|
45
|
+
*
|
|
46
|
+
* That is the one signal the version scheme exists to send, discarded by the
|
|
47
|
+
* command we tell every project to run — `AGENTS.md` ships that instruction
|
|
48
|
+
* into every scaffold. The `flows` lane crossed `@uniweb/core` `^0.14.1 →
|
|
49
|
+
* ^0.15.0` this way and learned it afterwards, from a changelog.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} from - the currently declared range or version
|
|
52
|
+
* @param {string} to - the version the matrix carries
|
|
53
|
+
* @returns {'patch'|'minor'|'major'|'none'}
|
|
54
|
+
*/
|
|
55
|
+
export function bumpClass(from, to) {
|
|
56
|
+
const [aMaj = 0, aMin = 0, aPat = 0] = stripVersionRange(from).split('.').map(Number)
|
|
57
|
+
const [bMaj = 0, bMin = 0, bPat = 0] = stripVersionRange(to).split('.').map(Number)
|
|
58
|
+
if (bMaj !== aMaj) return 'major'
|
|
59
|
+
if (bMin !== aMin) return 'minor'
|
|
60
|
+
if (bPat !== aPat) return 'patch'
|
|
61
|
+
return 'none'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Is this crossing one a consumer must act on?
|
|
66
|
+
*
|
|
67
|
+
* A major always is. A minor is **when the major is 0**, because that is where
|
|
68
|
+
* our scheme puts breaking changes — and npm agrees, which is the check that
|
|
69
|
+
* makes this more than our own convention: `^0.14.1` admits `0.14.x` and
|
|
70
|
+
* refuses `0.15.0`, so the range itself already treats the slot as a wall.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} from
|
|
73
|
+
* @param {string} to
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
export function isBreakingBump(from, to) {
|
|
77
|
+
const cls = bumpClass(from, to)
|
|
78
|
+
if (cls === 'major') return true
|
|
79
|
+
if (cls !== 'minor') return false
|
|
80
|
+
return Number(stripVersionRange(to).split('.')[0]) === 0
|
|
81
|
+
}
|
|
82
|
+
|
|
36
83
|
/**
|
|
37
84
|
* Compare two version specs (range prefix tolerated). Returns 1 / -1 / 0.
|
|
38
85
|
* @param {string} a
|
|
@@ -115,7 +162,12 @@ export async function surveyWorkspaceDeps(workspaceDir) {
|
|
|
115
162
|
name,
|
|
116
163
|
current,
|
|
117
164
|
target,
|
|
118
|
-
status
|
|
165
|
+
status,
|
|
166
|
+
// Classified here rather than at print time so every consumer of a
|
|
167
|
+
// survey row gets the same answer — the report, the summary, and any
|
|
168
|
+
// gate a caller applies.
|
|
169
|
+
bump: status === 'behind' ? bumpClass(current, target) : 'none',
|
|
170
|
+
breaking: status === 'behind' && isBreakingBump(current, target)
|
|
119
171
|
})
|
|
120
172
|
}
|
|
121
173
|
}
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
*/
|
|
52
52
|
|
|
53
53
|
import { createHash } from 'node:crypto'
|
|
54
|
+
import { humanBytes } from './bytes.js'
|
|
54
55
|
|
|
55
56
|
/**
|
|
56
57
|
* Plan + upload a site's static collection data files.
|
|
@@ -72,10 +73,23 @@ export async function uploadSiteData({
|
|
|
72
73
|
ball,
|
|
73
74
|
onProgress = () => {}
|
|
74
75
|
}) {
|
|
76
|
+
// ⛔ An EMPTY set still posts a plan, and that is the whole point of this lane
|
|
77
|
+
// being a manifest rather than a stream of files.
|
|
78
|
+
//
|
|
79
|
+
// The backend reconciles a site's data usage against what this plan declares:
|
|
80
|
+
// whatever is not in the manifest is gone, so deleting a collection stops
|
|
81
|
+
// costing on the next publish. ⭐ A plan with zero files is a STATEMENT that
|
|
82
|
+
// there are none; the ABSENCE of a request says nothing at all. Returning
|
|
83
|
+
// early here — which this did until 2026-09-01 — made "the user deleted their
|
|
84
|
+
// last schema-less collection" unexpressible on the wire, so that site kept
|
|
85
|
+
// paying for bytes it no longer served until the whole site was deleted.
|
|
86
|
+
// Nothing looked wrong at either end: no error, no warning, just a request
|
|
87
|
+
// that was never sent.
|
|
88
|
+
//
|
|
89
|
+
// Agreed both sides in channel backend-framework-82f2; the backend's plan
|
|
90
|
+
// route accepted an empty `files` array in the same exchange (it was a 400
|
|
91
|
+
// before, which is what made the omission look like the only option).
|
|
75
92
|
const entries = Object.entries(ball?.data || {})
|
|
76
|
-
if (!entries.length) {
|
|
77
|
-
return { mode: 'none', uploaded: [], failed: [], serveBase: null }
|
|
78
|
-
}
|
|
79
93
|
|
|
80
94
|
// One plan for the whole set. The per-request file cap counts a plan, so
|
|
81
95
|
// splitting would evade it rather than respect it; if a set ever exceeds it,
|
|
@@ -129,9 +143,25 @@ export async function uploadSiteData({
|
|
|
129
143
|
)
|
|
130
144
|
if (!planRes.ok) {
|
|
131
145
|
const body = await planRes.text().catch(() => '')
|
|
132
|
-
throw
|
|
146
|
+
// The PARSED problem document has to survive the throw. Flattening it into
|
|
147
|
+
// the message is what left the other two doors unable to branch on `reason`
|
|
148
|
+
// and printing raw JSON at users; this lane was the last one still doing it.
|
|
149
|
+
// Callers read `err.problem`; `describeDataRefusal` turns it into lines.
|
|
150
|
+
let problem = null
|
|
151
|
+
if (body) {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(body)
|
|
154
|
+
if (parsed && typeof parsed === 'object') problem = parsed
|
|
155
|
+
} catch {
|
|
156
|
+
/* not a problem document — prose refusal, or an upstream error page */
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const err = new Error(
|
|
133
160
|
`site data-uploads plan rejected: HTTP ${planRes.status}${body ? ` — ${body.slice(0, 300)}` : ''}`
|
|
134
161
|
)
|
|
162
|
+
err.status = planRes.status
|
|
163
|
+
err.problem = problem
|
|
164
|
+
throw err
|
|
135
165
|
}
|
|
136
166
|
|
|
137
167
|
const plan = await planRes.json()
|
|
@@ -149,7 +179,11 @@ export async function uploadSiteData({
|
|
|
149
179
|
if (!target) {
|
|
150
180
|
// A file the plan did not answer for is unaddressable. Report it; never
|
|
151
181
|
// invent a location for it.
|
|
152
|
-
failed.push({
|
|
182
|
+
failed.push({
|
|
183
|
+
path: f.path,
|
|
184
|
+
status: 0,
|
|
185
|
+
detail: 'no upload target in plan'
|
|
186
|
+
})
|
|
153
187
|
continue
|
|
154
188
|
}
|
|
155
189
|
try {
|
|
@@ -182,3 +216,65 @@ export async function uploadSiteData({
|
|
|
182
216
|
serveBase: plan.serve_base || null
|
|
183
217
|
}
|
|
184
218
|
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Turn a typed data-uploads refusal into user-facing lines, or null when there is
|
|
222
|
+
* no typed `reason` (⇒ fall through to the generic message, degrading rather than
|
|
223
|
+
* swallowing).
|
|
224
|
+
*
|
|
225
|
+
* ⭐ The THIRD door, and the last to get one. `/dev/assets` has
|
|
226
|
+
* `describeAssetRefusal`, `POST /dev/site` has `describeCreateRefusal`, and this
|
|
227
|
+
* lane threw prose with the JSON inlined until 2026-09-01 — which is the failure
|
|
228
|
+
* the other two describers exist to prevent, so leaving it was just an untreated
|
|
229
|
+
* instance of a solved problem.
|
|
230
|
+
*
|
|
231
|
+
* ⛔ Branch on `reason`, never the status: `507` alone cannot be told from any
|
|
232
|
+
* other `507` and carries none of the numbers, and `detail` is prose the backend
|
|
233
|
+
* may reword.
|
|
234
|
+
*
|
|
235
|
+
* ⚖️ **The advice DIVERGES from the asset lane's, and that divergence is the whole
|
|
236
|
+
* reason this is a separate function rather than a reused one.** On the asset lane
|
|
237
|
+
* removing an image frees nothing — freeing is entity-deletion-granular. Here the
|
|
238
|
+
* publish declares the COMPLETE set of schema-less data files every time, so
|
|
239
|
+
* dropping a collection and re-publishing is a real way to stop paying for it.
|
|
240
|
+
* Telling a data user "editing content frees nothing" would be false, and telling
|
|
241
|
+
* an asset user "just remove it" would be worse.
|
|
242
|
+
*
|
|
243
|
+
* @param {Error & { problem?: object|null }} err
|
|
244
|
+
* @returns {{ headline: string, notes: string[] } | null}
|
|
245
|
+
*/
|
|
246
|
+
export function describeDataRefusal(err) {
|
|
247
|
+
const p = err?.problem
|
|
248
|
+
const reason = p?.reason
|
|
249
|
+
if (typeof reason !== 'string') return null
|
|
250
|
+
|
|
251
|
+
if (reason === 'storage_quota_exceeded') {
|
|
252
|
+
const notes = []
|
|
253
|
+
const used = humanBytes(p.used_bytes)
|
|
254
|
+
const limit = humanBytes(p.limit_bytes)
|
|
255
|
+
const needed = humanBytes(p.needed_bytes)
|
|
256
|
+
if (used) notes.push(` Used: ${used}`)
|
|
257
|
+
if (limit) notes.push(` Limit: ${limit}`)
|
|
258
|
+
if (needed) notes.push(` This publish adds: ${needed}`)
|
|
259
|
+
notes.push(
|
|
260
|
+
'Every publish declares the full set of schema-less data files, so removing'
|
|
261
|
+
)
|
|
262
|
+
notes.push(
|
|
263
|
+
'a collection and re-publishing stops it counting. Deleting a site or entity'
|
|
264
|
+
)
|
|
265
|
+
notes.push('frees space too.')
|
|
266
|
+
return {
|
|
267
|
+
headline:
|
|
268
|
+
"Storage quota reached — the site owner's workspace cannot take on more record data.",
|
|
269
|
+
notes
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// An unrecognised reason still beats a status dump: name it, and let the
|
|
274
|
+
// backend's own prose follow when it sent any.
|
|
275
|
+
const detail = typeof p.detail === 'string' ? p.detail : ''
|
|
276
|
+
return {
|
|
277
|
+
headline: `Record data upload refused by the backend (${reason}).`,
|
|
278
|
+
notes: detail ? [` ${detail}`] : []
|
|
279
|
+
}
|
|
280
|
+
}
|