@bobfrankston/rmfmail 1.0.634 → 1.0.643

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/client/index.html CHANGED
@@ -56,23 +56,279 @@
56
56
  include because nothing else imports those specifiers on the
57
57
  desktop side. Single HTML for both shells; CSS handles narrow vs
58
58
  wide layout, runtime detect handles bootstrap, no html fork. -->
59
+ <!-- TODO: regenerate via importgen once it scans dynamic `await import()`
60
+ calls (currently it only follows static imports, so it drops every
61
+ @bobfrankston/* entry our index.html and editor.ts dynamic-import).
62
+ Until then this map is hand-restored (entries from prior importgen run
63
+ + the @bobfrankston ones it missed). See programming.md for the rule. -->
64
+ <!-- Generated by importgen 0.1.35 on May 10, 2026, 1:26:13 AM EDT -->
59
65
  <script type="importmap">
60
66
  {
61
- "imports": {
62
- "@bobfrankston/mailx-store-web": "../packages/mailx-store-web/index.js",
63
- "@bobfrankston/mailx-store-web/": "../packages/mailx-store-web/",
64
- "@bobfrankston/mailx-types": "../packages/mailx-types/index.js",
65
- "@bobfrankston/iflow-direct": "../node_modules/@bobfrankston/iflow-direct/index.js",
66
- "@bobfrankston/iflow-direct/": "../node_modules/@bobfrankston/iflow-direct/",
67
- "@bobfrankston/smtp-direct": "../node_modules/@bobfrankston/smtp-direct/index.js",
68
- "@bobfrankston/smtp-direct/": "../node_modules/@bobfrankston/smtp-direct/",
69
- "@bobfrankston/tcp-transport": "../node_modules/@bobfrankston/tcp-transport/index.js",
70
- "@bobfrankston/tcp-transport/": "../node_modules/@bobfrankston/tcp-transport/",
71
- "@bobfrankston/mailx-sync": "../node_modules/@bobfrankston/mailx-sync/index.js",
72
- "@bobfrankston/mailx-sync/": "../node_modules/@bobfrankston/mailx-sync/",
73
- "sql.js": "../packages/mailx-store-web/sql-wasm-esm.js"
74
- }
75
- }
67
+ "imports": {
68
+ "@bobfrankston/iflow-direct": "../node_modules/@bobfrankston/iflow-direct/index.js",
69
+ "@bobfrankston/tcp-transport": "../node_modules/@bobfrankston/tcp-transport/index.js",
70
+ "@bobfrankston/mailx-host": "../node_modules/@bobfrankston/mailx-host/index.js",
71
+ "@bobfrankston/msger": "../node_modules/@bobfrankston/msger/index.js",
72
+ "@bobfrankston/msgcommon": "../node_modules/@bobfrankston/msger/node_modules/@bobfrankston/msgcommon/index.js",
73
+ "json5": "../node_modules/@bobfrankston/msger/node_modules/@bobfrankston/msgcommon/node_modules/json5/dist/index.mjs",
74
+ "marked": "../node_modules/@bobfrankston/msger/node_modules/@bobfrankston/msgcommon/node_modules/marked/lib/marked.esm.js",
75
+ "windows-shortcuts": "../node_modules/@bobfrankston/msger/node_modules/@bobfrankston/msgcommon/node_modules/windows-shortcuts/lib/windows-shortcuts",
76
+ "@bobfrankston/rust-builder": "../node_modules/@bobfrankston/msger/node_modules/@bobfrankston/rust-builder/index.js",
77
+ "ansi-to-html": "../node_modules/@bobfrankston/msger/node_modules/ansi-to-html/lib/ansi_to_html.js",
78
+ "entities": "../node_modules/entities/lib/index.js",
79
+ "@bobfrankston/mailx-imap": "../node_modules/@bobfrankston/mailx-imap/index.js",
80
+ "@bobfrankston/mailx-types": "../node_modules/@bobfrankston/mailx-types/index.js",
81
+ "@bobfrankston/mailx-settings": "../node_modules/@bobfrankston/mailx-settings/index.js",
82
+ "jsonc-parser": "../node_modules/jsonc-parser/lib/esm/main.js",
83
+ "@bobfrankston/mailx-store": "../node_modules/@bobfrankston/mailx-store/index.js",
84
+ "@bobfrankston/smtp-direct": "../node_modules/@bobfrankston/smtp-direct/index.js",
85
+ "@bobfrankston/mailx-sync": "../node_modules/@bobfrankston/mailx-sync/index.js",
86
+ "@bobfrankston/oauthsupport": "../node_modules/@bobfrankston/oauthsupport/index.js",
87
+ "@bobfrankston/mailx-store-web": "../node_modules/@bobfrankston/mailx-store-web/index.js",
88
+ "sql.js": "../node_modules/sql.js/dist/sql-wasm-browser.js",
89
+ "@bobfrankston/miscinfo": "../node_modules/@bobfrankston/miscinfo/index.js",
90
+ "@bobfrankston/node-tcp-transport": "../node_modules/@bobfrankston/node-tcp-transport/index.js",
91
+ "@bobfrankston/rmf-tiny": "../node_modules/@bobfrankston/rmf-tiny/src/adapter.js",
92
+ "@capacitor/android": "../node_modules/@capacitor/android/index.js",
93
+ "@capacitor/cli": "../node_modules/@capacitor/cli/dist/index.js",
94
+ "@ionic/cli-framework-output": "../node_modules/@ionic/cli-framework-output/dist/index.js",
95
+ "@ionic/utils-terminal": "../node_modules/@ionic/utils-terminal/dist/index.js",
96
+ "@types/slice-ansi": "../node_modules/@types/slice-ansi/",
97
+ "debug": "../node_modules/debug/src/index.js",
98
+ "ms": "../node_modules/ms/index",
99
+ "signal-exit": "../node_modules/signal-exit/index.js",
100
+ "slice-ansi": "../node_modules/slice-ansi/index.js",
101
+ "ansi-styles": "../node_modules/ansi-styles/index.js",
102
+ "color-convert": "../node_modules/color-convert/index.js",
103
+ "color-name": "../node_modules/color-name/index.js",
104
+ "astral-regex": "../node_modules/astral-regex/index.js",
105
+ "is-fullwidth-code-point": "../node_modules/is-fullwidth-code-point/index.js",
106
+ "string-width": "../node_modules/string-width/index.js",
107
+ "emoji-regex": "../node_modules/emoji-regex/index.js",
108
+ "strip-ansi": "../node_modules/strip-ansi/index.js",
109
+ "ansi-regex": "../node_modules/ansi-regex/index.js",
110
+ "tslib": "../node_modules/tslib/tslib.es6.mjs",
111
+ "untildify": "../node_modules/untildify/index.js",
112
+ "wrap-ansi": "../node_modules/wrap-ansi/index.js",
113
+ "@ionic/utils-subprocess": "../node_modules/@ionic/utils-subprocess/dist/index.js",
114
+ "@ionic/utils-array": "../node_modules/@ionic/utils-array/dist/index.js",
115
+ "@ionic/utils-fs": "../node_modules/@ionic/utils-fs/dist/index.js",
116
+ "@types/fs-extra": "../node_modules/@types/fs-extra/",
117
+ "@types/node": "../node_modules/@types/node/",
118
+ "undici-types": "../node_modules/undici-types/index.js",
119
+ "fs-extra": "../node_modules/fs-extra/lib/index.js",
120
+ "at-least-node": "../node_modules/at-least-node/index.js",
121
+ "graceful-fs": "../node_modules/graceful-fs/graceful-fs.js",
122
+ "jsonfile": "../node_modules/jsonfile/index.js",
123
+ "universalify": "../node_modules/universalify/index.js",
124
+ "@ionic/utils-process": "../node_modules/@ionic/utils-process/dist/index.js",
125
+ "@ionic/utils-object": "../node_modules/@ionic/utils-object/dist/index.js",
126
+ "tree-kill": "../node_modules/tree-kill/index.js",
127
+ "@ionic/utils-stream": "../node_modules/@ionic/utils-stream/dist/index.js",
128
+ "cross-spawn": "../node_modules/cross-spawn/index.js",
129
+ "path-key": "../node_modules/path-key/index.js",
130
+ "shebang-command": "../node_modules/shebang-command/index.js",
131
+ "shebang-regex": "../node_modules/shebang-regex/index.js",
132
+ "which": "../node_modules/which/which.js",
133
+ "isexe": "../node_modules/isexe/index.js",
134
+ "commander": "../node_modules/commander/esm.mjs",
135
+ "env-paths": "../node_modules/env-paths/index.js",
136
+ "kleur": "../node_modules/kleur/index.mjs",
137
+ "native-run": "../node_modules/native-run/dist/index.js",
138
+ "bplist-parser": "../node_modules/bplist-parser/bplistParser.js",
139
+ "big-integer": "../node_modules/big-integer/BigInteger",
140
+ "elementtree": "../node_modules/elementtree/lib/elementtree.js",
141
+ "sax": "../node_modules/sax/lib/sax.js",
142
+ "ini": "../node_modules/ini/lib/ini.js",
143
+ "plist": "../node_modules/plist/index.js",
144
+ "@xmldom/xmldom": "../node_modules/@xmldom/xmldom/lib/index.js",
145
+ "base64-js": "../node_modules/base64-js/index.js",
146
+ "xmlbuilder": "../node_modules/xmlbuilder/lib/index",
147
+ "split2": "../node_modules/split2/index.js",
148
+ "through2": "../node_modules/through2/through2.js",
149
+ "readable-stream": "../node_modules/readable-stream/readable.js",
150
+ "inherits": "../node_modules/inherits/inherits.js",
151
+ "string_decoder": "../node_modules/string_decoder/lib/string_decoder.js",
152
+ "safe-buffer": "../node_modules/safe-buffer/index.js",
153
+ "util-deprecate": "../node_modules/util-deprecate/node.js",
154
+ "yauzl": "../node_modules/yauzl/index.js",
155
+ "fd-slicer": "../node_modules/fd-slicer/index.js",
156
+ "pend": "../node_modules/pend/index.js",
157
+ "buffer-crc32": "../node_modules/buffer-crc32/index.js",
158
+ "open": "../node_modules/open/index.js",
159
+ "define-lazy-prop": "../node_modules/define-lazy-prop/index.js",
160
+ "is-docker": "../node_modules/is-docker/index.js",
161
+ "is-wsl": "../node_modules/is-wsl/index.js",
162
+ "prompts": "../node_modules/prompts/index.js",
163
+ "sisteransi": "../node_modules/sisteransi/src/index.js",
164
+ "rimraf": "../node_modules/rimraf/dist/esm/index.js",
165
+ "glob": "../node_modules/glob/dist/esm/index.min.js",
166
+ "minimatch": "../node_modules/minimatch/dist/esm/index.js",
167
+ "brace-expansion": "../node_modules/brace-expansion/dist/esm/index.js",
168
+ "balanced-match": "../node_modules/balanced-match/dist/esm/index.js",
169
+ "minipass": "../node_modules/minipass/dist/esm/index.js",
170
+ "path-scurry": "../node_modules/path-scurry/dist/esm/index.js",
171
+ "lru-cache": "../node_modules/lru-cache/dist/esm/browser/index.min.js",
172
+ "package-json-from-dist": "../node_modules/package-json-from-dist/dist/esm/index.js",
173
+ "semver": "../node_modules/semver/index.js",
174
+ "tar": "../node_modules/tar/dist/esm/index.min.js",
175
+ "@isaacs/fs-minipass": "../node_modules/@isaacs/fs-minipass/dist/esm/index.js",
176
+ "chownr": "../node_modules/chownr/dist/esm/index.js",
177
+ "minizlib": "../node_modules/minizlib/dist/esm/index.js",
178
+ "yallist": "../node_modules/yallist/dist/esm/index.js",
179
+ "xml2js": "../node_modules/xml2js/lib/xml2js",
180
+ "@capacitor/core": "../node_modules/@capacitor/core/dist/index.js",
181
+ "express": "../node_modules/express/index.js",
182
+ "accepts": "../node_modules/accepts/index.js",
183
+ "mime-types": "../node_modules/mime-types/index.js",
184
+ "mime-db": "../node_modules/mime-db/index.js",
185
+ "negotiator": "../node_modules/negotiator/index.js",
186
+ "array-flatten": "../node_modules/array-flatten/array-flatten.js",
187
+ "body-parser": "../node_modules/body-parser/index.js",
188
+ "bytes": "../node_modules/bytes/index.js",
189
+ "content-type": "../node_modules/content-type/index.js",
190
+ "depd": "../node_modules/depd/index.js",
191
+ "destroy": "../node_modules/destroy/index.js",
192
+ "http-errors": "../node_modules/http-errors/index.js",
193
+ "setprototypeof": "../node_modules/setprototypeof/index.js",
194
+ "statuses": "../node_modules/statuses/index.js",
195
+ "toidentifier": "../node_modules/toidentifier/index.js",
196
+ "iconv-lite": "../node_modules/iconv-lite/lib/index.js",
197
+ "safer-buffer": "../node_modules/safer-buffer/safer.js",
198
+ "on-finished": "../node_modules/on-finished/index.js",
199
+ "ee-first": "../node_modules/ee-first/index.js",
200
+ "qs": "../node_modules/qs/lib/index.js",
201
+ "side-channel": "../node_modules/side-channel/index.js",
202
+ "es-errors": "../node_modules/es-errors/index.js",
203
+ "object-inspect": "../node_modules/object-inspect/index.js",
204
+ "side-channel-list": "../node_modules/side-channel-list/index.js",
205
+ "side-channel-map": "../node_modules/side-channel-map/index.js",
206
+ "call-bound": "../node_modules/call-bound/index.js",
207
+ "call-bind-apply-helpers": "../node_modules/call-bind-apply-helpers/index.js",
208
+ "function-bind": "../node_modules/function-bind/index",
209
+ "get-intrinsic": "../node_modules/get-intrinsic/index.js",
210
+ "es-define-property": "../node_modules/es-define-property/index.js",
211
+ "es-object-atoms": "../node_modules/es-object-atoms/index.js",
212
+ "get-proto": "../node_modules/get-proto/index.js",
213
+ "dunder-proto": "../node_modules/dunder-proto/get.js",
214
+ "gopd": "../node_modules/gopd/index.js",
215
+ "has-symbols": "../node_modules/has-symbols/index.js",
216
+ "hasown": "../node_modules/hasown/index.js",
217
+ "math-intrinsics": "../node_modules/math-intrinsics/abs.js",
218
+ "side-channel-weakmap": "../node_modules/side-channel-weakmap/index.js",
219
+ "raw-body": "../node_modules/raw-body/index.js",
220
+ "unpipe": "../node_modules/unpipe/index.js",
221
+ "type-is": "../node_modules/type-is/index.js",
222
+ "media-typer": "../node_modules/media-typer/index.js",
223
+ "content-disposition": "../node_modules/content-disposition/index.js",
224
+ "cookie": "../node_modules/cookie/index.js",
225
+ "cookie-signature": "../node_modules/cookie-signature/index",
226
+ "encodeurl": "../node_modules/encodeurl/index.js",
227
+ "escape-html": "../node_modules/escape-html/index.js",
228
+ "etag": "../node_modules/etag/index.js",
229
+ "finalhandler": "../node_modules/finalhandler/index.js",
230
+ "parseurl": "../node_modules/parseurl/index.js",
231
+ "fresh": "../node_modules/fresh/index.js",
232
+ "merge-descriptors": "../node_modules/merge-descriptors/index.js",
233
+ "methods": "../node_modules/methods/index.js",
234
+ "path-to-regexp": "../node_modules/path-to-regexp/index.js",
235
+ "proxy-addr": "../node_modules/proxy-addr/index.js",
236
+ "forwarded": "../node_modules/forwarded/index.js",
237
+ "ipaddr.js": "../node_modules/ipaddr.js/lib/ipaddr.js",
238
+ "range-parser": "../node_modules/range-parser/index.js",
239
+ "send": "../node_modules/send/index.js",
240
+ "mime": "../node_modules/mime/mime.js",
241
+ "serve-static": "../node_modules/serve-static/index.js",
242
+ "utils-merge": "../node_modules/utils-merge/index",
243
+ "vary": "../node_modules/vary/index.js",
244
+ "html-to-docx": "../node_modules/html-to-docx/dist/html-to-docx.esm.js",
245
+ "@oozcitak/util": "../node_modules/@oozcitak/util/lib/index",
246
+ "@oozcitak/dom": "../node_modules/@oozcitak/dom/lib/index",
247
+ "@oozcitak/infra": "../node_modules/@oozcitak/infra/lib/index",
248
+ "@oozcitak/url": "../node_modules/@oozcitak/url/lib/index",
249
+ "html-entities": "../node_modules/html-entities/dist/esm/index.js",
250
+ "html-to-vdom": "../node_modules/html-to-vdom/index.js",
251
+ "ent": "../node_modules/ent/index.js",
252
+ "punycode": "../node_modules/punycode/punycode.js",
253
+ "safe-regex-test": "../node_modules/safe-regex-test/index.js",
254
+ "is-regex": "../node_modules/is-regex/index.js",
255
+ "has-tostringtag": "../node_modules/has-tostringtag/index.js",
256
+ "htmlparser2": "../node_modules/htmlparser2/lib/index.js",
257
+ "domelementtype": "../node_modules/domelementtype/index.js",
258
+ "domhandler": "../node_modules/domhandler/index.js",
259
+ "domutils": "../node_modules/domutils/index.js",
260
+ "dom-serializer": "../node_modules/dom-serializer/index.js",
261
+ "image-size": "../node_modules/image-size/dist/index.js",
262
+ "queue": "../node_modules/queue/index.js",
263
+ "image-to-base64": "../node_modules/image-to-base64/image-to-base64.min.js",
264
+ "node-fetch": "../node_modules/node-fetch/lib/index.mjs",
265
+ "whatwg-url": "../node_modules/whatwg-url/lib/public-api.js",
266
+ "tr46": "../node_modules/tr46/index.js",
267
+ "webidl-conversions": "../node_modules/webidl-conversions/lib/index.js",
268
+ "jszip": "../node_modules/jszip/lib/index",
269
+ "lie": "../node_modules/lie/lib/index.js",
270
+ "immediate": "../node_modules/immediate/lib/index.js",
271
+ "pako": "../node_modules/pako/index.js",
272
+ "setimmediate": "../node_modules/setimmediate/setImmediate.js",
273
+ "lodash": "../node_modules/lodash/lodash.js",
274
+ "nanoid": "../node_modules/nanoid/index.js",
275
+ "virtual-dom": "../node_modules/virtual-dom/index",
276
+ "browser-split": "../node_modules/browser-split/index.js",
277
+ "error": "../node_modules/error/index",
278
+ "camelize": "../node_modules/camelize/index.js",
279
+ "string-template": "../node_modules/string-template/index",
280
+ "xtend": "../node_modules/xtend/immutable",
281
+ "ev-store": "../node_modules/ev-store/index",
282
+ "individual": "../node_modules/individual/index",
283
+ "global": "../node_modules/global/window.js",
284
+ "min-document": "../node_modules/min-document/index.js",
285
+ "dom-walk": "../node_modules/dom-walk/index",
286
+ "process": "../node_modules/process/index.js",
287
+ "is-object": "../node_modules/is-object/index",
288
+ "next-tick": "../node_modules/next-tick/index.js",
289
+ "x-is-array": "../node_modules/x-is-array/index",
290
+ "x-is-string": "../node_modules/x-is-string/index",
291
+ "xmlbuilder2": "../node_modules/xmlbuilder2/lib/index",
292
+ "mailparser": "../node_modules/mailparser/index.js",
293
+ "encoding-japanese": "../node_modules/encoding-japanese/src/index.js",
294
+ "he": "../node_modules/he/he.js",
295
+ "html-to-text": "../node_modules/html-to-text/lib/html-to-text.mjs",
296
+ "@selderee/plugin-htmlparser2": "../node_modules/@selderee/plugin-htmlparser2/lib/hp2-builder.mjs",
297
+ "selderee": "../node_modules/selderee/lib/selderee.mjs",
298
+ "parseley": "../node_modules/parseley/lib/parseley.mjs",
299
+ "leac": "../node_modules/leac/lib/leac.mjs",
300
+ "peberminta": "../node_modules/peberminta/lib/core.mjs",
301
+ "deepmerge": "../node_modules/deepmerge/dist/cjs.js",
302
+ "libmime": "../node_modules/libmime/lib/libmime.js",
303
+ "libbase64": "../node_modules/libbase64/lib/libbase64.js",
304
+ "libqp": "../node_modules/libqp/lib/libqp.js",
305
+ "linkify-it": "../node_modules/linkify-it/index.mjs",
306
+ "uc.micro": "../node_modules/uc.micro/index.mjs",
307
+ "@zone-eu/mailsplit": "../node_modules/@zone-eu/mailsplit/index.js",
308
+ "nodemailer": "../node_modules/nodemailer/lib/nodemailer.js",
309
+ "punycode.js": "../node_modules/punycode.js/punycode.es6.js",
310
+ "tlds": "../node_modules/tlds/index.json",
311
+ "mammoth": "../node_modules/mammoth/lib/index.js",
312
+ "argparse": "../node_modules/argparse/index.js",
313
+ "sprintf-js": "../node_modules/sprintf-js/src/sprintf.js",
314
+ "bluebird": "../node_modules/bluebird/js/release/bluebird.js",
315
+ "dingbat-to-unicode": "../node_modules/dingbat-to-unicode/dist/index.js",
316
+ "lop": "../node_modules/lop/index.js",
317
+ "duck": "../node_modules/duck/index.js",
318
+ "underscore": "../node_modules/underscore/underscore-esm.js",
319
+ "option": "../node_modules/option/index.js",
320
+ "path-is-absolute": "../node_modules/path-is-absolute/index.js",
321
+ "quill": "../node_modules/quill/quill.js",
322
+ "eventemitter3": "../node_modules/eventemitter3/index.mjs",
323
+ "lodash-es": "../node_modules/lodash-es/lodash.js",
324
+ "parchment": "../node_modules/parchment/dist/parchment.js",
325
+ "quill-delta": "../node_modules/quill-delta/dist/Delta.js",
326
+ "fast-diff": "../node_modules/fast-diff/diff.js",
327
+ "lodash.clonedeep": "../node_modules/lodash.clonedeep/index.js",
328
+ "lodash.isequal": "../node_modules/lodash.isequal/index.js",
329
+ "ws": "../node_modules/ws/wrapper.mjs"
330
+ }
331
+ }
76
332
  </script>
77
333
  <!-- Early error handlers — ship problems to logit when running in
78
334
  a context that can reach the network (Android shell). msger
@@ -169,6 +425,8 @@
169
425
  <span class="tb-menu-label">Editor</span>
170
426
  <label class="tb-menu-item"><input type="radio" name="opt-editor" value="quill" id="opt-editor-quill" checked> Quill</label>
171
427
  <label class="tb-menu-item"><input type="radio" name="opt-editor" value="tiptap" id="opt-editor-tiptap"> tiptap</label>
428
+ <label class="tb-menu-item" title="Best Word-paste fidelity. TinyMCE itself is fetched at runtime from the CDN URL below."><input type="radio" name="opt-editor" value="tinymce" id="opt-editor-tinymce"> TinyMCE</label>
429
+ <label class="tb-menu-item tb-menu-subitem" title="TinyMCE CDN URL. Default (jsDelivr) needs no API key. Override for Tiny Cloud or self-hosted."><input type="text" id="opt-tinymce-cdn" placeholder="https://cdn.jsdelivr.net/npm/tinymce@6/tinymce.min.js" style="width:90%;font-size:0.85em"></label>
172
430
  <hr class="tb-menu-sep">
173
431
  <span class="tb-menu-label" title="Which app the compose 'Edit in Word' button hands the body off to. Auto = Word first, then LibreOffice, then OS default.">External editor</span>
174
432
  <label class="tb-menu-item"><input type="radio" name="opt-extedit" value="auto" id="opt-extedit-auto" checked> Auto (Word → LibreOffice → default)</label>
@@ -2186,3 +2186,17 @@ body.calendar-sidebar-on .calendar-sidebar { display: flex; }
2186
2186
  transition: transform 200ms ease-out;
2187
2187
  }
2188
2188
  .ml-body { position: relative; overflow-anchor: none; }
2189
+
2190
+ /* Quill snow theme zeros out paragraph margins, so a Word paste with
2191
+ distinct paragraphs LOOKS like line breaks while composing — even
2192
+ though q.root.innerHTML has proper <p> tags and the recipient
2193
+ renders them with their own UA defaults. Restore visual spacing in
2194
+ the editor body so the sender sees the paragraphs they pasted.
2195
+ :first-child / :last-child trim the outer margin so the editor
2196
+ doesn't get phantom whitespace at the boundaries. */
2197
+ .ql-editor p {
2198
+ margin: 0 0 0.6em 0;
2199
+ }
2200
+ .ql-editor p:last-child {
2201
+ margin-bottom: 0;
2202
+ }
@@ -1,102 +1,88 @@
1
1
  # prod / dev release workflow — Android (MAUI APK)
2
2
 
3
- Status: design only. Mirror of `prod.md` (npm side); same intent, different mechanism because Android isn't on npm.
3
+ Status: design only. Same intent as `prod.md` (npm side); the APK is a complete, self-contained binary so the unit of promotion is a *file*, not a dependency or dist-tag.
4
4
 
5
- ## Where the desktop pattern doesn't transfer
5
+ ## Simpler approach (Bob 2026-05-09)
6
6
 
7
- npm has built-in dist-tags: a single package, multiple movable pointers. Android side-loads have nothing equivalent an APK is a file at a URL, picked up by the in-app updater. To get the same prod-vs-dev shape we have to build it.
7
+ No separate MAUI project. Same source tree produces both flavors; the difference is purely in the OUTPUT filename, and "what's currently prod" is decided by which file lives at the canonical prod URL.
8
8
 
9
- Today (current state):
9
+ ### Today
10
10
 
11
- ```
12
- https://rmf39.aaz.lt/mailx/mailx-maui.apk ← single APK
13
- https://rmf39.aaz.lt/mailx/versions.json ← version manifest
14
- ```
11
+ - One build target: `mailx-maui.apk` at `https://rmf39.aaz.lt/mailx/mailx-maui.apk`.
12
+ - `versions.json` carries the version string; updater compares to running.
13
+ - Every build overwrites the single APK. No history, no channels.
15
14
 
16
- `AppUpdater` (in `MailxApp/Services/`) polls `versions.json`, compares to running `ApplicationVersion`, and prompts a download if newer. There's no channel concept — every install gets every build.
15
+ ### Phase 1 start producing dev.apk too
17
16
 
18
- ## Goal
17
+ While there's no formal "production release" yet, every build produces:
19
18
 
20
- Same as desktop: `rebuild` publishes builds freely; promotion to "users get this" is a separate step. On Android the unit isn't a tag pointer, it's the APK file the updater downloads.
19
+ - `rmfmaildev.apk` (or `dev.apk`) the just-built, latest dev tip. Updater on dev devices points here.
20
+ - `mailx-maui.apk` — unchanged for now (current production behavior). Updater on prod devices points here.
21
21
 
22
- ## Mechanism
22
+ The build script writes both. Same APK contents initially — the dual output is just renamed copies. Devices on different channels read different filenames.
23
23
 
24
- **Two URL prefixes** (or one prefix with channel paths). Manifest carries which build is current per channel. APK files are versioned filenames, never overwritten.
24
+ ### Phase 2 once a release is declared
25
25
 
26
- ```
27
- https://rmf39.aaz.lt/mailx/dev/manifest.json
28
- https://rmf39.aaz.lt/mailx/dev/mailx-maui-1.0.NNN.apk
29
- https://rmf39.aaz.lt/mailx/prod/manifest.json
30
- https://rmf39.aaz.lt/mailx/prod/mailx-maui-1.0.MMM.apk
31
- ```
26
+ Build outputs:
32
27
 
33
- `manifest.json` shape:
28
+ - `rmfmaildev.apk` — every build. Always the freshest.
29
+ - `rmfmail-<version>.apk` — versioned filename, never overwritten. One file per build.
34
30
 
35
- ```json
36
- { "version": "1.0.NNN", "apk": "mailx-maui-1.0.NNN.apk", "released": "2026-05-09T17:00:00Z", "minDesktop": "1.0.500" }
37
- ```
31
+ "Promoting" a known-good dev build to production = `cp rmfmail-1.0.NNN.apk rmfmail.apk` (or web-server-side: update a redirect / symlink / manifest pointer at `rmfmail.apk` to name the chosen versioned file).
38
32
 
39
- The "promotion" step writes a new `prod/manifest.json` pointing at an existing dev APK file. The dev APK doesn't move or recompile — same bytes, just referenced under prod's manifest.
33
+ The canonical URLs:
40
34
 
41
- ## Required pieces
42
-
43
- ### 1. `build-apk.cmd` / `build-apk.ts` build + upload to dev channel
44
-
45
- Currently builds and copies to `download/apks/mailx-maui.apk` (single name, overwrites every time). Change to:
46
-
47
- - Read version from `package.json` (already does).
48
- - Build APK, copy to `download/apks/mailx-maui-<version>.apk` (versioned filename, never overwritten).
49
- - Update local `download/dev/manifest.json` to point at the new APK.
50
- - Sync `download/dev/` to the rmf39.aaz.lt webroot.
51
-
52
- The dev channel always tracks the most recent build.
53
-
54
- ### 2. `prod-android.cmd` — promote dev → prod
55
-
56
- Mirror of `prod.cmd`. Reads `download/dev/manifest.json`, copies it to `download/prod/manifest.json`, syncs prod/ to webroot. No new APK file uploaded — the prod-channel manifest now points at the same APK that's already in `dev/` (or copy it under `prod/` for symmetry; cheap).
35
+ ```
36
+ https://rmf39.aaz.lt/mailx/rmfmail.apk ← prod pointer (chosen versioned APK)
37
+ https://rmf39.aaz.lt/mailx/rmfmaildev.apk latest dev build
38
+ https://rmf39.aaz.lt/mailx/rmfmail-1.0.NNN.apk ← every released version, immutable
39
+ ```
57
40
 
58
- ### 3. AppUpdater channel awareness
41
+ Roll back = re-point `rmfmail.apk` to a prior versioned APK.
59
42
 
60
- `AppUpdater` currently reads `https://rmf39.aaz.lt/mailx/versions.json`. Change to read `<base>/<channel>/manifest.json`. Channel default is `prod`; users on the dev channel set a flag (manual install, or a Settings toggle, or a build-time constant in a separate `MailxAppDev` package).
43
+ ## What changes from the current build
61
44
 
62
- Two ways to put a user on the dev channel:
45
+ `build-apk.cmd` / `build-apk.ts`:
63
46
 
64
- - **Flavor build**: build a separate APK (`MailxApp.Dev`) where `AppUpdater._channel = "dev"`. Different package id (`com.frankston.mailx.dev`), different AUMID, side-by-side install. This is the desktop `RMFMAIL_PROFILE` analog — full coexistence.
65
- - **Settings toggle**: single APK, channel stored in app settings, defaults to prod. User flips a switch, next updater poll hits the dev manifest. Same APK, just different update source.
47
+ - Read version from `package.json` (already does).
48
+ - Build APK once.
49
+ - Phase 1: write to `rmfmaildev.apk` AND `mailx-maui.apk` (mirror).
50
+ - Phase 2: write to `rmfmail-<version>.apk` (versioned, never overwritten) AND `rmfmaildev.apk` (latest dev mirror).
51
+ - Sync to web root.
66
52
 
67
- Settings toggle is simpler for a one-user dev/prod workflow; flavor build is necessary if you want both versions installed at once on the same phone (different package ids).
53
+ Promotion script (`prod-android.ps1`?) Phase 2 only:
68
54
 
69
- ### 4. APK version semantics
55
+ - Take a version arg (or default to current dev build).
56
+ - Copy / point `rmfmail.apk` at that version's APK.
57
+ - Update `versions.json` so the updater on prod devices sees a newer version available.
70
58
 
71
- Android's `ApplicationVersion` is a monotonically-increasing integer. Promoting a dev APK to prod does NOT bump the integer it's the same APK. The manifest's "version" field stays in sync with the APK's actual `ApplicationDisplayVersion`. Rolling back is just rewriting the manifest to point at an older APK.
59
+ `AppUpdater.cs` — channel awareness:
72
60
 
73
- ## Workflow once wired
61
+ - Reads from configurable URL based on channel setting.
62
+ - Default: prod URL (`rmfmail.apk` / corresponding `versions.json`).
63
+ - Dev mode: dev URL.
64
+ - Channel toggle in Settings (or build-time constant in a separate flavor — but per Bob's preference, NO separate MAUI project; channel is a runtime setting).
74
65
 
75
- 1. `build-apk.cmd` builds, uploads to `dev/`, updater on dev-channel devices picks it up.
76
- 2. Daily testing on the dev phone/tablet.
77
- 3. `prod-android.cmd` — copies the current dev manifest to prod. Devices on prod-channel pick up the new build on next poll.
78
- 4. Rolling back prod: re-run `prod-android.cmd <specific-version>` (need a version arg to override "current dev").
66
+ ## What this does NOT need
79
67
 
80
- ## Notes / open questions
68
+ - **No separate MAUI project**. The APK is the same binary regardless of which channel it serves; users with the dev URL configured get latest builds, users with the prod URL get whatever was last promoted. Source tree is single.
69
+ - **No dist-tag analog**. The "tag" is just the filename / URL the updater polls.
70
+ - **No flavor build**. Same package id, same signing key, same APK bytes — just two distribution endpoints (or, with Phase 2, two distinct files referenced from the manifests).
81
71
 
82
- - **Coexistence on a single device** — only possible with flavor build (different package id). Settings toggle is single-install. Pick based on how often you need both at once.
83
- - **Force-update / minimum-version** — manifest already supports a `minDesktop` field; an Android equivalent (`minSupported`: refuse to run older client) can refuse to run a stale APK after a breaking server change. Not needed today; cheap to add later.
84
- - **APK signing** — same key for dev and prod, otherwise users can't upgrade between them without uninstall. (Different package id flavor build IS a separate app and can have a separate key — but that's the coexistence model anyway.)
85
- - **`download/apks/mailx-maui.apk` (current single filename)** — keep as-is for back-compat until devices move off it; or symlink it to the current prod APK.
72
+ ## Coexistence on a single device
86
73
 
87
- ## Effort estimate
74
+ Single MAUI project means a single Android package id. Two builds CAN'T be installed side-by-side without diverging package ids (Android refuses). If side-by-side becomes a real need later, reintroduce the separate-MAUI-project idea — but that's the orthogonal "I want both versions on the same phone" design, not the prod/dev release flow.
88
75
 
89
- - `build-apk.ts` — read version, versioned filename, manifest write, sync. ~30 lines.
90
- - `prod-android.cmd` — manifest copy + sync. ~10 lines.
91
- - `AppUpdater.cs` channel awareness + Settings toggle UI — ~50 lines including the storage pref.
92
- - Webroot layout setup on rmf39.aaz.lt — one-time directory creation.
93
- - Test cycle: build dev, verify prod APK on phone unchanged. Promote, verify next poll picks up new prod APK.
76
+ ## Order
94
77
 
95
- Maybe 2 hours wall-clock total. The hardest part is the Settings toggle, not the channel logic.
78
+ 1. **Phase 1 first**: build script outputs `rmfmaildev.apk` alongside today's `mailx-maui.apk`. Zero behavior change for prod users; dev users (on the dev channel) get latest. ~10 lines in `build-apk.ts`.
79
+ 2. **AppUpdater channel toggle**: Settings checkbox + persisted preference + URL switch. ~50 lines.
80
+ 3. **Phase 2 when ready**: switch to versioned filenames + `rmfmail.apk` pointer + promotion script. Done when there's a real first "production release" to designate.
96
81
 
97
- ## Order
82
+ ## Effort
98
83
 
99
- 1. `prod.md` (desktop) ships first — proves the dist-tag flow and gives you the workflow you'd want here.
100
- 2. Android channels: `build-apk.ts` rewrite → AppUpdater channel param Settings toggle (or flavor build) `prod-android.cmd`.
84
+ - Phase 1: ~30 minutes (build script + manifest output).
85
+ - AppUpdater channel: ~1 hour (Settings UI + storage + URL plumbing).
86
+ - Phase 2: ~1 hour when activated (promotion script + versioned filenames + pointer mechanism on web root).
101
87
 
102
- No urgency until desktop side is settled and you actually want both flavors in use on the phone.
88
+ Total ~2.5 hours wall-clock when fully wired. Phase 1 alone gets you the "always-have-a-fresh-dev-build" benefit without any commitment to the formal release flow.