One combined Discourse plugin. Bundles seven previously-separate plugins under a single registration and a single master site setting (jtech_enabled). Each sub-plugin keeps its own settings, locales, and Ruby namespace.
| Sub-plugin | Ruby namespace | Settings prefix | Master switch |
|---|---|---|---|
| Dislike (phantom reactions) | DiscourseNoLikes |
dislike_*, discourse_no_likes_*, no_reactions_*, purge_phantom_likes_now |
discourse_no_likes_enabled |
| Another SMTP | — | discourse_another_email_* |
discourse_another_email_enabled |
| Mini-mod | DiscourseMiniMod |
mini_mod_*, tl4_* |
mini_mod_enabled |
| Mod-categories | DiscourseModCategories |
mod_*, precheck_*, topic_footer_*, topic_reply_prompt_* |
mod_categories_enabled |
| Dumbcourse | DiscourseDumbcourse |
dumbcourse_* |
dumbcourse_enabled |
| Translator-tweaks | (patches DiscourseTranslator) |
(none — gated by translator's own settings) | translator_enabled (upstream) |
| Smart search | DiscourseSmartSearch |
smart_search_* |
smart_search_enabled |
The bundle is gated by jtech_enabled; each sub-plugin is independently gated by its own setting above.
Mod-categories ships a notification fan-out for five staff-event streams in addition to its original topic-level moderator notes. Whenever a moderator performs one of the actions below, every OTHER staff member gets a high-priority bell notification + live MessageBus pop-up alert, AND the event surfaces in the shield-tab user menu alongside topic notes.
| Stream | Event hook | URL on click |
|---|---|---|
| Post deleted by staff | on(:post_destroyed) (skips self-deletes + system user) |
topic + post number |
| Queued post approved | on(:reviewable_transitioned_to) (status=:approved, ReviewableQueuedPost) |
/review/:id |
| Queued post rejected | on(:reviewable_transitioned_to) (status=:rejected, ReviewableQueuedPost) |
/review/:id |
| User note added | wraps ::DiscourseUserNotes.add_note (bundled plugin fires no DiscourseEvent) |
/u/:username/notes |
| Flag note added on a reviewable | ::ReviewableNote.after_create callback |
/review/:id |
All five are gated by independent site settings (mod_notify_staff_on_post_actions, mod_notify_staff_on_user_notes, mod_notify_staff_on_flag_notes) so streams can be disabled individually. The fan-out itself lives in lib/discourse_mod_categories/staff_notifier.rb and is wrapped in two layers of rescue StandardError so a notifier failure can never 500 the underlying moderator action. A 30-second per-user dedup check in StaffNotifier.recent_duplicate? protects against an event hook firing twice in quick succession.
The shield-tab /discourse-mod-categories/notes-feed returns a UNION of topic-attached notes (legacy behavior — what TopicCustomField writes surface as) plus the non-topic event notifications above, so the tab mirrors what the bell shows for every mod-note-kind notification.
Synonym query expansion using WordNet (~117K-word English lexical DB, bundled via the rwordnet gem) for general English, with a small tech-jargon YAML overlay (~70 entries in config/dictionaries/smart_search_synonyms.yml) for the abbreviations and brand names WordNet doesn't know (js ↔ javascript, k8s ↔ kubernetes, pg ↔ postgres, etc.). When smart_search_enabled is on:
- The user's original search runs first via Discourse's vanilla
Search#execute. - If the original returns fewer than
smart_search_minimum_resultsposts (default 5), up tosmart_search_variant_limit(default 2, max 5) synonym-substituted variant searches run and their results are merged in. - Every smart-search path (dictionary load, variant generation, inner variant search, merge) is wrapped in
rescue StandardError→ log and return the vanilla result. The fallback contract is documented at the top oflib/discourse_smart_search/search_extension.rb.
No external services, no API keys, no embedding models — both backends (WordNet via SQLite DB shipped in-gem, plus the YAML overlay) run in-process. This is deliberate: the previous semantic-search attempt (Discourse AI embeddings) was disabled after every query started returning 500 when the embedding backend went down. Smart search's failure mode is "results identical to vanilla," never "search broken."
Editing the overlay: only ADD entries WordNet doesn't already cover — abbreviations, brand names, protocol initialisms. Don't curate general English (WordNet handles it for free). Lowercase ASCII rows, each row is a symmetric synonym group. Reloaded at boot (or via DiscourseSmartSearch::Synonyms.reload! in a Rails console). See docs/smart_search.md for the full architecture: two-backend lookup order, request-flow diagram, fallback contract, performance notes, and a console-recipe for diagnostics.
plugin.rb master plugin file — instance_eval's each file under sub_plugins/
about.json
sub_plugins/
dislike.rb body of original Dislike/plugin.rb
another_smtp.rb body of original discourse-another-smtp/plugin.rb
mini_mod.rb body of original discourse-mini-mod/plugin.rb
mod_categories.rb body of original discourse-mod/plugin.rb + staff-event notifications
dumbcourse.rb body of original dumbcourse/plugin.rb
translator_tweaks.rb runtime patches for upstream discourse/discourse-translator
(alltechdev's two-commit fork ported as in-process tweaks
so we can track upstream and apply our overrides on top)
smart_search.rb synonym query expansion (in-process, no external services)
scripts/
translator_backfill_foreign_detection.rb
one-shot rails runner; enqueues the upstream translator's
detect job for legacy foreign-script posts
config/
settings.yml all settings.yml files merged into seven jtech_* admin tabs
dictionaries/
smart_search_synonyms.yml
symmetric synonym groups for smart_search; lowercase ASCII
locales/
server.en.yml deep-merged server locale + categories.jtech_* translations
client.en.yml deep-merged client locale
lib/
discourse_no_likes/ from Dislike
discourse_mini_mod/ from discourse-mini-mod
discourse_mod_categories/ from discourse-mod + staff_notifier.rb (fan-out helper)
discourse_dumbcourse/ from dumbcourse
discourse_smart_search/ synonyms / query_expander / Search prepend module
app/
controllers/{discourse_mod_categories,discourse_dumbcourse}/
models/{discourse_no_likes,*_site_setting.rb}
jobs/regular/
db/migrate/ phantom-reactions table migration from Dislike
assets/ merged JS/CSS — no filename collisions across sources
public/ Dumbcourse SPA bundle (index.html, dumbcourse.{js,css}, emoji_map.json)
The merged config/settings.yml exposes one admin tab per sub-plugin: Jtech, Jtech — Dislike, Jtech — Alternate SMTP, Jtech — Mini-mod, Jtech — Mod, Jtech — Dumbcourse, Jtech — Smart search. TL4 settings remain in Discourse's core Trust Level 4 tab.
Two GitHub Actions workflows render visual fixtures of the plugin's UI surface:
-
Feature Screenshots— ~25 hand-picked scenarios capturing the actively-developed features. Runs on push tomain, PRs, and manual dispatch. Artifact:feature-screenshots. -
Comprehensive Screenshots— parameterized matrix across kinds × lengths × read-states × roles × ordinals, ~1180 scenarios attempted. Dispatch-only (gated byENV["JTECH_COMPREHENSIVE_SHOTS"]so it never slows ordinary CI). Run via:gh workflow run "Comprehensive Screenshots" --ref <branch> --repo Shalom-Karr/JtechTools
Spec files:
spec/system/comprehensive_screenshots_spec.rbplus_part2,_part3,_part4. Empirical success rate ~75% across the full matrix (the fast-path P6 section alone hits 100%). Section-prefix convention so the artifact zip sorts navigably:A1xxbell rows,B2xxshield tab,C3xxmod-note panel,D4xxbell stacking,E5xx/K1xxsmart search,G7xxtime-ago variants,H8xxdensity 1→100, etc.
Discourse plugins can only register a single enabled_site_setting at load time. The bundle's master gate is jtech_enabled. Every sub-plugin's logic still checks its own master switch internally (Guardian overrides, event hooks, controllers, etc. all early-return when their sub-feature is disabled), so you keep per-feature on/off control through admin settings.
cd /var/discourse/plugins
git clone https://github.com/JTech-Forums/JtechTools.git jtech-tools
cd /var/discourse
./launcher rebuild app