Commit graph

328 commits

Author SHA1 Message Date
3053704247
twine-venv: pin twine<6 — classic ~/.pypirc auth on build runner
twine 6 (Sep 2025) auto-detects GitLab CI and refuses to fall back to
~/.pypirc, requiring PYPI_ID_TOKEN (Trusted Publishing OIDC). Pin <6
to keep the runner's ~/.pypirc fallback working until we migrate all
python/* repos to Trusted Publishing as a coordinated change.
2026-06-16 14:12:00 -04:00
892e6a859e
twine-upload: validate metadata with twine check, non-interactive
twine check validates the built artifacts' metadata (long_description
rendering, required fields) so a broken render hits CI logs instead of
landing on PyPI as a malformed project page.

--non-interactive guards against a wedged prompt hanging the pipeline.
2026-06-16 13:55:43 -04:00
50b9d1ee8e
feat: source_format auto-detect with expandable detector registry
The create-form selector forced authors to remember to switch from
markdown to rst before typing. Auto-detect runs on every preview
keystroke + at form submit time, picking the right format from
content shape so authors just write.

SOURCE_FORMAT_DETECTORS in custom.js carries the registry — first
match wins, ordered most-specific first. Today: html, latex,
mediawiki, rst, with markdown as the catch-all default.

Adding a new format (e.g. asciidoc, org, ipynb):
  1. Push one entry to SOURCE_FORMAT_DETECTORS in custom.js
  2. Add one <option> to source_format_select in create.j2
  3. Confirm pandoc accepts the format name

No Python changes needed for new formats — the existing
preview_post + new_thread paths pass source_format through to
pandoc untouched.

UX:
  - Default selector value: 'auto' (was 'markdown')
  - "detected: rst" label appears next to selector when auto picks
  - Manual override options stay for ambiguous content
  - Form onsubmit resolves 'auto' to a concrete format before POST
    so node.source_format never persists as the literal 'auto'

Defense in depth: new_thread.py + preview_post both treat 'auto'
as a no-op (falls to markdown default) for no-JS clients or racy
submits.
2026-06-05 21:03:59 -04:00
d1145510e2
feat: source_format selector on new-thread create form
The preview fix (64a15b6) honored source_format only when the textarea
already declared one — which works for editing an existing node but
not for creating a new one. Authors writing RST in the create form
saw their preview rendered as markdown, then saved into a node whose
source_format defaulted to markdown — so even the final saved page
came back wrong.

Three pieces:

- create.j2: <select> for source_format alongside the textarea.
  Default markdown; rst, html, mediawiki, latex, org, asciidoc,
  textile available. onchange syncs the chosen value into
  textarea.dataset.sourceFormat and re-runs preview, so the
  existing custom.js sendPreview picks it up unchanged.

- new_thread.py: read source_format from request.params, pass to
  set_data() so the created node's persistent source_format matches
  what the author saw in preview.

- No JS change needed — custom.js already reads
  textarea.dataset.sourceFormat.
2026-06-05 20:58:11 -04:00
5942f6b2b3
fix: eliminate pandoc subprocess thrash under pytest-xdist -n auto
Root cause of recurring 35929 / 38536 CI flake — `-n auto` on a 32-core
CI runner spawns 32 pytest workers. test_pandoc.py contains ~14 tests
that each fork a `pandoc` subprocess. When several land on parallel
workers at once, pandoc's cold-start cost (GHC runtime + filter loading)
plus runner CPU contention pushes wall time past the 5s metadata timeout
and 30s convert timeout. Subprocess gets SIGKILL'd (returncode -9),
tests fail, pipeline reruns the same flake.

Three-part root cause fix:

1. Cache get_available_input_formats / get_available_output_formats with
   functools.lru_cache(maxsize=1). Pandoc's format list is static for
   a given binary; we only need one subprocess per Python process,
   not one per call site. Also bump the metadata-query timeout from 5s
   to 30s — the first cold-start under contention still has to succeed.

2. Mark TestConvert + TestAvailableFormats with
   @pytest.mark.xdist_group("pandoc-subprocess") so every pandoc-forking
   test pins to one xdist worker. Other 31 workers continue parallel-
   processing the rest of the suite; pandoc tests run sequentially on
   their single worker, each pandoc cold-start completes before the
   next starts.

3. Update Makefile test target to pass --dist=loadgroup so pytest-xdist
   honors the xdist_group marker.

Other test classes in test_pandoc.py (TestNodeTreeToMarkdown,
TestNamespaceToMarkdown, TestGetAuthorName) use MockNode objects
without data_html, so node_tree_to_markdown's pandoc path stays cold —
those classes don't need the marker.

Replaces 1527ef7 (which just retriggered the same flake).
2026-06-05 20:47:30 -04:00
64a15b6a86
fix: live preview honors source_format — RST/HTML/mediawiki now render correctly
The /preview-post endpoint hardcoded markdown_to_html(), ignoring the
node's source_format. RST pages showed `..` comments verbatim, literal
`name_` references, and unrendered `.. _target: url` definitions, even
though Node.set_data dispatched correctly on save. Preview lied; save
told the truth.

Mirror set_data's dispatch in preview_post:
  - markdown → markdown_to_html (unchanged)
  - html     → pandoc clean → markdown_to_html (matches set_data)
  - any other → pandoc + namespace sanitizer

Plumb source_format from the edit textarea via data-source-format,
read by sendPreview in custom.js, sent as a form param.
2026-06-05 20:26:39 -04:00
1527ef7d64
ci: retrigger pipeline (flaky pandoc subprocess timeouts in 35929) 2026-05-24 11:06:00 -04:00
112d3fd3b1
fix: vendor minimal pkg_resources shim so pyramid survives setuptools>=81
setuptools 81 removed pkg_resources from its distribution. pyramid 2.0.x
(and 2.1) still does `import pkg_resources`. When setuptools 82.0.1 landed
in /opt/remarkbox/env, pyramid failed to import, all three uwsgi services
on origin crash-looped, and my.remarkbox.com / meta.remarkbox.com /
foxhop.net / westworld2.com served 502.

remarkbox/_vendor/pkg_resources/__init__.py is a 115-line shim backed by
importlib.resources (stdlib only — no setuptools, no jaraco.text, no
platformdirs). Exposes exactly the surface pyramid uses:
resource_filename / resource_stream / resource_string / resource_exists /
resource_isdir / resource_listdir / DefaultProvider / register_loader_type.

remarkbox/__init__.py prepends our _vendor dir to sys.path before the
first pyramid import so `import pkg_resources` always finds our shim,
regardless of which setuptools is installed. Bleeding-edge friendly:
upstream setuptools removals can no longer break us.

558 tests pass.
2026-05-24 10:44:05 -04:00
05f51ff965
supply-chain: hash-pin prod PyPI dependencies (requirements-prod.lock)
Prod previously installed via 'pip install .' (unbounded requirements.py3.txt) plus
'pip install --upgrade -r requirements-prod.txt' — every deploy re-resolved external
PyPI deps to whatever was latest, unverified.

- requirements-prod.in: source (runtime + prod-server deps)
- requirements-prod.lock: 55 PyPI pkgs pinned to exact versions + SHA256 (622 hashes)
- scripts/strip-vcs-from-lock.py: removes first-party git theme deps (pip can't hash
  a git repo; themes are integrity-pinned by their own commit SHAs)
- install-source-prod: pip install --require-hashes -r requirements-prod.lock, then
  pip install . to resolve the first-party git themes without re-resolving the
  hash-pinned PyPI deps
- make pins-lock regenerates the lock deliberately

Validated locally: stripped lock installs under --require-hashes, app imports under
resolved versions (SQLAlchemy 2.0, Pyramid latest).
2026-05-21 09:22:21 -04:00
99d27c8483
fix: remove per-comment export menu; keep export only at thread top 2026-05-17 06:15:58 -04:00
10e7a2bdb2
docs: T16 — themes self-contained; common.css is the built-in (embed) stylesheet 2026-05-12 08:39:30 -04:00
89679fd784
fix: restore neutral Remarkbox palette in common.css
68ca0ef swapped common.css :root from the Remarkbox light palette to the
chaostheory dark palette (teal background, ice-blue primary) and dark-skinned
.alert-*, section.well, .focused, etc. common.css ships in every theme, so
chaostheory colors leaked into meta/default pages (e.g. the teal well box on
my.remarkbox.com). Reverted those rules to their pre-68ca0ef values; the
chaostheory palette now lives in remarkbox-theme-chaostheory/theme.css. Kept
the .export-menu styles added in 9e22c0d.
2026-05-12 08:37:25 -04:00
763cacba57
fix: prefer X-Forwarded-Host over rewritten Host
Edge proxy rewrites Host: www.foxhop.net → foxhop.net before forwarding
to origin (see proxy.unturf.com Caddyfile). request.host on origin is
therefore always the apex, stripping any www. prefix even when the user
fetched from www. Caddy's reverse_proxy preserves the original host in
X-Forwarded-Host, so consult that first and fall back to request.host
when the proxy isn't in the path.
2026-04-27 11:34:41 -04:00
97a1fffeba
fix: use request.host for canonical URI (preserves www. prefix)
Provenance helpers were using namespace.name as the host. Foxhop's
namespace is named 'foxhop.net' but the public URI lives at
'www.foxhop.net' — exports were emitting 'https://foxhop.net/...' which
works (apex 301-redirects to www) but reads wrong on a printed page.

Pass request.host through into the provenance bundle so canonical URIs,
chapter titles, and per-reply permalinks all preserve whatever host
prefix the export was actually fetched from. Falls back to namespace.name
when no host is supplied (callers outside the request context).
2026-04-26 16:33:50 -04:00
81344da09a
fix: PDF body fills page width (override pandoc CSS max-width)
Pandoc's --standalone HTML5 template applies max-width: 36em + margin: 0
auto + padding: 50px to body, centering content in a narrow column on
any page size. With wkhtmltopdf at 15mm page margins, effective content
margins were measuring ~55mm because pandoc's CSS added ~40mm of inner
padding on top.

Inject a header-includes <style> block that strips body's max-width,
margin, and padding so content fills the page minus wkhtmltopdf's own
page margins. Measured: 55mm → 15mm left margin.
2026-04-26 15:55:55 -04:00
d7fa7d1b93
feat: tighter PDF margins + shorter QR caption
wkhtmltopdf default top margin is ~25mm — leaves a half-page of empty
space above the provenance header on every PDF. Set explicit 12mm
top/bottom and 15mm left/right via pandoc -V margin-* options.

Shorten QR caption from 'Scan to visit the living source' (six words,
wrapped onto four lines in the 160px cell) to 'Scan for living source'
(four words). Widen the QR cell from 160px to 200px so the caption
sits on one or two lines without crowding the QR.
2026-04-26 15:16:47 -04:00
63a5a939c0
ci: retry pipeline (245c5c7 test stage failed flakily)
Local pytest run shows 584 pass on the same code; previous CI test
job hit a transient failure. Retry to validate the auto-restart fix in
foxhop-states 591cb4f end-to-end through CI.
2026-04-26 15:03:38 -04:00
245c5c7d8c
ci: rebuild to test foxhop-states 591cb4f auto-restart in caddy_sites
Empty commit triggers a salt highstate so the new auto-restart cmd.run
in uwsgi.caddy_sites lands and (assuming akuma's clone of foxhop-states
has been pulled to 591cb4f) fires the catch-up restart of foxhop.net
and westworld2.com onto whatever commit hash this build produces.
2026-04-26 10:39:25 -04:00
b35c136f7f
feat: side-by-side provenance header (text left, QR right)
Wraps the provenance header in an HTML table so the source/snapshot/
generator block sits left and the QR sits right at the same horizontal
level. Pandoc converts the table cleanly into native table cells across
HTML, PDF (wkhtmltopdf), DOCX, ODT, EPUB.

Without a QR (qr_data_uri=None) we fall back to the plain blockquote.
2026-04-25 16:26:21 -04:00
37350324ea
ci: rebuild to pick up foxhop-states ce4edcc
Triggers a salt highstate so the content-based skip_deploy auto-restart
state lands on origin and finally drives the catch-up restart of
westworld2 (:6002) and foxhop.net (:6003) onto the current commit.
2026-04-25 13:39:58 -04:00
38475eceeb
feat: provenance + QR codes on every export
Every exported document (PDF, EPUB, DOCX, HTML, markdown, plain, ...)
now points back to its living source on Remarkbox so a printed snapshot
remains traceable years later.

What's injected:
  - Top banner: source URI, snapshot ISO timestamp, generator commit hash,
    plus a notice the document is a snapshot of a living source.
  - QR code (PNG, embedded as data URI) linking back to the canonical URI.
    Scannable from print, survives the pandoc image pipeline across
    HTML, PDF (via wkhtmltopdf), DOCX, EPUB, ODT.
  - Per-reply permalinks: each reply heading hyperlinks its date to the
    deep-link permalink (thread-uri#node-id), academic-citation style.
  - Footer repeating the source URI + commit hash.
  - Namespace exports also hyperlink each chapter title to the live
    thread.

Canonical URI = https://{namespace.name}{node.path} — namespace name is
its host on the public web.

New module: remarkbox/lib/provenance.py — header_md, footer_md,
reply_heading_md, qr_png_data_uri, canonical_uri helpers, and a one-shot
build() bundler.

Dep added: segno (pure-Python QR generator, BSD, zero deps).
2026-04-25 06:14:39 -04:00
9e22c0d768
fix: always-visible export menu + wiki history rev creation
Export menu: dropped the details/summary toggle so format options are
visible at all times; added flex layout with gap for breathing room.

Wiki history: the browser edit form path was calling node.edit() instead
of node.wiki_edit() so revisions were never recorded — /api/v1/nodes/.../revisions
returned empty after every edit. Also tightened the access gate to
can_wiki_edit so non-owner authenticated users can edit wiki-mode root
nodes through the UI (matching the gate already used by the edit-button
macro).
2026-04-25 06:14:27 -04:00
dded1622e8 fix: export markdown from data_html, not data+source_format
Observed defect: a node's source_format column can disagree with the
actual bytes in data. A Collatz thread on foxhop.net has data filled
with RST (======, ---, :: blocks) yet source_format="markdown". The
earlier helper trusted source_format and returned the RST untouched,
so GET /api/v1/export/.../thread.md served raw RST to anyone who asked
for markdown.

data_html is the canonical rendered form — set at every write via
set_data() regardless of input syntax — so converting data_html → markdown
via pandoc sidesteps the label mismatch entirely. Falls back to raw
data if data_html is missing or pandoc fails.
2026-04-24 15:09:31 -04:00
f5e453d6ba ci: rebuild to exercise foxhop-states 04ad310 auto-restart rule 2026-04-24 14:55:08 -04:00
8b6dac4dd7 fix: stop tripling thread titles in exports; convert non-markdown node data
Three fixes to thread/namespace export:

1. Nodes authored in non-markdown source_format (rst, mediawiki, latex, ...)
   had their raw source dumped directly into the markdown renderer. A RST
   thread exported as .md therefore yielded RST, not markdown. New
   _node_data_as_markdown helper converts node.data through pandoc when
   source_format != markdown, with a raw-source fallback on pandoc failure.

2. node_tree_to_markdown prepended '# {root_node.title}' on top of data
   that already carries its own H1 (either native markdown or a converted
   RST underline heading). Drop the prepend — the data owns the title.

3. convert() passed --metadata title=X which makes pandoc render a visible
   title-block in HTML/PDF above the body. Combined with (2) and the data's
   own H1 this showed the title three times in exported HTML/PDF. For
   html-family outputs (and pdf via wkhtmltopdf) switch to -V pagetitle=X
   so only the <title> tag gets populated; other formats still use the
   proper --metadata title=X for real document metadata.
2026-04-24 14:38:26 -04:00
2fd71e9029 feat: map user-facing file extensions to pandoc format names in export URIs
Users hitting /api/v1/export/threads/{id}.md got 'Unsupported format: md'
because pandoc's internal name is 'markdown'. Add EXTENSION_ALIASES +
resolve_format() so common extensions map to the right pandoc format:
  .md    -> markdown
  .html  -> html5
  .tex   -> latex
  .txt   -> plain
  .epub  -> epub3
  .wiki  -> mediawiki
  .adoc  -> asciidoc
  ...
Pandoc's own format names pass through unchanged (commonmark_x, docbook5,
fb2, etc.) via fallback.
2026-04-24 12:53:53 -04:00
586f2366df ci: rebuild to pull remarkbox-theme-chaostheory fed3eea (call_to_action in wikibar-right) 2026-04-24 07:47:29 -04:00
1bf5111638 ci: rebuild to pull remarkbox-theme-chaostheory df04c0d (call_to_action block fix) 2026-04-23 19:17:51 -04:00
9db2407395 feat: wiki revision history — HTML tabs, diff view, export links
- New route /{node_id}/revisions rendered by revision-history.j2
- Show content/history tabs on wiki root nodes
- Export dropdown includes history (json + html) links on wiki namespaces
- Node action buttons now gate on can_wiki_edit (not can_alter_node)
  so wiki-mode members can edit their own root nodes
- Suppress Topic link when URI matches current domain (self-reference)
- Tests covering revision timestamps, wiki_edit(), can_wiki_edit(),
  /revisions endpoints, CSRF trusted origins, export menu gating,
  pandoc rst→html rendering
2026-04-22 11:37:06 -04:00
a5c3e36fce fix: Namespace.moderators returned all members; owners now imply mod
moderators property returned self.enabled_users (every enabled member),
so every user who had ever posted got moderator powers: spam
approve/deny, edit/delete other users' comments, moderator Slack
notifications. Bug was universal across all namespaces — any site where
users post was affected.

Fix: moderators now returns only users with role='moderator'.
Owners now implicitly pass is_moderator() checks, since NamespaceUser
is single-role per user per namespace; this keeps owners functional
without needing a data migration.
2026-04-21 19:59:40 -04:00
e1952cb1c4 ci: fix chaostheory requirement name (pip distribution is remarkbox-chaostheory) 2026-04-21 18:31:49 -04:00
ddbcdfa094 ci: install and bundle remarkbox-theme-chaostheory
Adds chaostheory theme to the shared env.tar.gz build so foxhop.net
(which sets app.theme = chaostheory and shares /opt/remarkbox/env with
my.remarkbox.com) has the theme's templates + static assets available
after deploy.
2026-04-21 18:27:49 -04:00
68ca0ef6e7 feat: foxhop.net support — /attachment/ static view, pandoc standalone fix, figure/figcaption tags, avatar null guard, html5lib body fragment, chaostheory palette in common.css 2026-04-08 15:05:22 -04:00
176be89319 docs: use make migration — never hand-write revision IDs 2026-04-06 14:23:16 -04:00
604c8407ee style: avoid "the", use "our" — writing style rule + sweep 2026-03-31 13:20:22 -04:00
f3815eb0ce fix: CWE-407 content length cap on browser form paths; tighten bleach pin; add tests
- views/__init__.py: add MAX_CONTENT_LENGTH = 500_000 (shared constant with comment
  explaining the CWE-407 / bleach ReDoS rationale)
- reply_node.py: reject oversized content before set_data() / clean_raw_html()
- modify_node.py: same guard on edit path
- new_thread.py: same guard on new thread path
- requirements.py3.txt: tighten bleach>=2.1.4 -> bleach>=6.0.0 with CVE note
- test_render.py: unit tests for bleach version contract, API stability,
  sanitization correctness, and ReDoS resistance timing
- test_views.py: functional tests for content length enforcement on all three
  browser form paths (reply, new thread)
2026-03-30 09:27:59 -04:00
d37c60481d docs: CWE-407 audit — bleach sanitization pipeline and build tools 2026-03-30 08:25:27 -04:00
67483f2b98 ops: operation voyeur — credential opsec protocol 2026-03-29 15:46:07 -04:00
b51cf4574d chore: trigger CI to pull updated meta theme 2026-03-29 09:47:07 -04:00
06d70b9e92 docs: never access credentials without explicit instruction 2026-03-28 15:44:13 -04:00
c87fb0bd1f fix: cap search keywords and query results to prevent CWE-407 amplification
- list_nodes.py: strip empty tokens, cap keywords to 10 before passing to model
- node.py: add .limit(200) per keyword query — was unbounded .all()
- namespace.py: add .limit(500) on root.children in dict_dump — was unbounded iteration
- __init__.py: cap page_number to 1000 — large offsets force full table scans
2026-03-28 15:12:39 -04:00
2061097e13 Skip PDF tests when wkhtmltopdf unavailable (fixes CI). 2026-03-10 11:22:01 -04:00
46bdf3a217 Add revision diff endpoint and progressive enhancement export/wiki UI.
Diff: GET /api/v1/revisions/{id}/diff/{other_id} — unified diff between two revisions.
UI: <details>-based export menus on thread and node views (works without JS).
UI: Revision history link for wiki-mode namespaces.
2026-03-10 11:13:39 -04:00
75bec564b2 Replace ASCII diagrams with Graphviz DOT in architecture and ticket docs. 2026-03-10 09:29:36 -04:00
b63277e4d1 Update docs for Operation Undigg: API reference, testing guide, architecture diagram. 2026-03-10 09:07:32 -04:00
6d1cfff91d Operation Undigg: multi-syntax input, pandoc export, wiki mode, auto-themes.
Phase 1: Pandoc export pipeline — 67 output formats for threads, namespaces, nodes.
Phase 2: Multi-syntax input — accept markdown, HTML, RST, MediaWiki, LaTeX, etc.
Phase 3: Wiki mode — per-namespace toggle, revision tracking, wiki-edit endpoint.
Phase 4: Auto-generated themes — deterministic CSS per namespace, light + dark mode.

Includes unit, integration, and functional tests (95 new, 544 total).
2026-03-10 00:35:51 -04:00
f45c42d343 modified: remarkbox/__init__.py
modified:   requirements.py3.txt
2026-03-09 19:50:36 -04:00
5bcd7df585 Document proxy architecture and domain routing in CLAUDE.md.
Maps which domains go through the edge proxy (142.93.73.64) vs direct to
origin (162.243.167.224). Prevents repeat of the 5-day meta/faq SSL outage
caused by missing proxy blocks.
2026-03-02 15:51:56 -05:00
a57187672c Update postmortem with actual root cause and add T14.
The 5-day meta/faq SSL outage was caused by missing proxy blocks in
proxy.unturf.com, not a Caddy cold-start issue. Fixed in proxy commit f12a56d.
2026-03-02 15:22:39 -05:00
4287aeee2c Add postmortem for 2026-02-25 SSL/TLS outage on meta and faq subdomains. 2026-02-26 07:06:48 -05:00