From b48b2294e1e0196c9cc06e659fe3334e7ec27bb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:19:41 +0000 Subject: [PATCH 001/118] Bump vite from 7.3.1 to 7.3.2 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.3.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- yarn.lock | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92c1fa0d..9d3777cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "devDependencies": { "@types/node": "^25.3.1", "prettier": "^3.8.1", - "vite": "^7.3.1", + "vite": "^7.3.2", "vite-plugin-ruby": "^5.1.2" } }, @@ -4391,9 +4391,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", "dependencies": { "esbuild": "^0.27.0", diff --git a/package.json b/package.json index 7c76b4f0..58dc3c75 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "devDependencies": { "@types/node": "^25.3.1", "prettier": "^3.8.1", - "vite": "^7.3.1", + "vite": "^7.3.2", "vite-plugin-ruby": "^5.1.2" }, "dependencies": { diff --git a/yarn.lock b/yarn.lock index 505a5e7f..9e03d759 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2087,10 +2087,10 @@ vite-plugin-ruby@^5.1.2: obug "^2.0" tinyglobby "^0.2.12" -vite@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" - integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== +vite@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.2.tgz#cb041794d4c1395e28baea98198fd6e8f4b96b5c" + integrity sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg== dependencies: esbuild "^0.27.0" fdir "^6.5.0" From bdaef7b12b4bb077e96222672da7102ea50049bc Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:20:26 +1000 Subject: [PATCH 002/118] Update docs/sourcing-parts.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/sourcing-parts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sourcing-parts.md b/docs/sourcing-parts.md index 9e2a00f4..99f555e6 100755 --- a/docs/sourcing-parts.md +++ b/docs/sourcing-parts.md @@ -89,7 +89,7 @@ Arduinos are fairly outdated by todays standards - [3Ding](https://www.3ding.in/) - -> confirmed more vendors? wanna add specific notes for your country? contribute [here](https://github.com/hackclub/forge/edit/main/docs/part-sourcing.md)!! You'd be helping a ton of people! +> confirmed more vendors? wanna add specific notes for your country? contribute [here](https://github.com/hackclub/forge/edit/main/docs/sourcing-parts.md)!! You'd be helping a ton of people! ## Tips for specific vendors From 34a057549cacb0e894bcc56bd9dc8acbbaf60718 Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:20:56 +1000 Subject: [PATCH 003/118] Update docs/index.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 80c766ab..89da3a0b 100755 --- a/docs/index.md +++ b/docs/index.md @@ -10,8 +10,8 @@ Forge is a [Hack Club](https://hackclub.com) program where teens like yourself c - [Pitching Your Project](/docs/pitching) - How to write a great pitch - [JOURNAL.md Format](/docs/journal) - How to document your build - [About Cost](/docs/cost) - How to effectively source parts and use grant funds -- [Sourcing Parts](docs/sourcing-parts) - where to find the best (and cheapest) parts -- [Shipping](docs/shipping) - How to structure and ship a project +- [Sourcing Parts](/docs/sourcing-parts) - where to find the best (and cheapest) parts +- [Shipping](/docs/shipping) - How to structure and ship a project - [FAQ](/docs/faq) - Frequently Asked Questions More coming soon! From c9904d6c06aa8913260f1e9d79cf0dee8085a937 Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:21:39 +1000 Subject: [PATCH 004/118] Update docs/cost.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/cost.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cost.md b/docs/cost.md index c7b4fc0a..530d1661 100755 --- a/docs/cost.md +++ b/docs/cost.md @@ -12,11 +12,11 @@ If you do not know what Hack Club is, we are a 501c3 non-profit based in Shelbur **We do not have infinite money!** -Even though you can receieve theoretically infiinite funding for your project, every dollar we spend, we need to fundraise ourselves. We would like to make it last to create as many cool projects as possible. +Even though you can receive theoretically infinite funding for your project, every dollar we spend, we need to fundraise ourselves. We would like to make it last to create as many cool projects as possible. That said, please always do what you can to select the cheaper option while making a project. This could include the following -- Not making projects that are simply expensive for the sake of it, maybe order your parts seperately and learn how to solder! +- Not making projects that are simply expensive for the sake of it, maybe order your parts separately and learn how to solder! - Always choosing the cheapest option, such as GSDR, even when it comes at the cost of your time (start a new project in the meantime) - Selecting the cheapest part for whatever you are making. (You don’t need a raspberry pi 5 to blink an LED) From 32797eb8ad1a063f2810499eb914a0844ea5968c Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:22:47 +1000 Subject: [PATCH 005/118] Update docs/faq.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 5d37d3e3..ba9919d4 100755 --- a/docs/faq.md +++ b/docs/faq.md @@ -30,7 +30,7 @@ You can edit and resubmit your pitch! Read the feedback and try again, just by e ## How do I get my money? -Once your project is approved, you'll receive funding through a virtual card on [HCB](https://hcb.hackclub.com). Please keep and upload all of your reciepts, since it is a requirement from the IRS! +Once your project is approved, you'll receive funding through a virtual card on [HCB](https://hcb.hackclub.com). Please keep and upload all of your receipts, since it is a requirement from the IRS! > **We monitor your transactions.** Misuse of your funds will lead to a ban from Forge and potentially Hack Club as a whole. If you're not sure if something is an appropriate use of your funds, ask us before buying it! From 2a8c0dcee702fc3ab688ba95711fe57e39655069 Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:23:07 +1000 Subject: [PATCH 006/118] Update docs/sourcing-parts.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/sourcing-parts.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/sourcing-parts.md b/docs/sourcing-parts.md index 99f555e6..524afec3 100755 --- a/docs/sourcing-parts.md +++ b/docs/sourcing-parts.md @@ -87,7 +87,6 @@ Arduinos are fairly outdated by todays standards - [Novo3D](https://novo3d.in/) - 3D printed parts (3D printing as a Service, JLC3DP-alike) - [3Ding](https://www.3ding.in/) - - > confirmed more vendors? wanna add specific notes for your country? contribute [here](https://github.com/hackclub/forge/edit/main/docs/sourcing-parts.md)!! You'd be helping a ton of people! From a866a34c8e2dfbb905f2c471a6f103464acdd17b Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:23:30 +1000 Subject: [PATCH 007/118] Update docs/sourcing-parts.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/sourcing-parts.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sourcing-parts.md b/docs/sourcing-parts.md index 524afec3..594bc35a 100755 --- a/docs/sourcing-parts.md +++ b/docs/sourcing-parts.md @@ -60,6 +60,7 @@ Arduinos are fairly outdated by todays standards ### Batteries (Li-Po, CR2032, AA, etc) +Coming soon. ## Country Specific Notes ### Egypt: From cca2a4c3c640a7bd0b4fa147d69cc9f8d56d762a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:22:39 +0000 Subject: [PATCH 008/118] Apply PR review feedback to docs Agent-Logs-Url: https://github.com/hackclub/forge/sessions/f4358f75-f42d-43e9-8f10-6f9d8f2c7682 Co-authored-by: Cybdo <119400800+Cybdo@users.noreply.github.com> --- docs/cost.md | 2 +- docs/faq.md | 2 +- docs/shipping.md | 6 +++--- docs/sourcing-parts.md | 7 +++++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/cost.md b/docs/cost.md index 530d1661..df318077 100755 --- a/docs/cost.md +++ b/docs/cost.md @@ -12,7 +12,7 @@ If you do not know what Hack Club is, we are a 501c3 non-profit based in Shelbur **We do not have infinite money!** -Even though you can receive theoretically infinite funding for your project, every dollar we spend, we need to fundraise ourselves. We would like to make it last to create as many cool projects as possible. +Even though you can receive up to $1,000 in funding for your project, every dollar we spend, we need to fundraise ourselves. We would like to make it last to create as many cool projects as possible. That said, please always do what you can to select the cheaper option while making a project. This could include the following diff --git a/docs/faq.md b/docs/faq.md index ba9919d4..9db74ae3 100755 --- a/docs/faq.md +++ b/docs/faq.md @@ -10,7 +10,7 @@ Forge is open to builders aged 13 to 18. You must be a member of the Hack Club S ## How much funding can I get? -Infinite! (within reason) The amount depends on the scope of your project and the quality of your documentation. Still try to save cost where you can! +Up to $1,000. The amount depends on the scope of your project and the quality of your documentation. Still try to save cost where you can! ## Do I need to know how to code? diff --git a/docs/shipping.md b/docs/shipping.md index 222b74f3..44330601 100644 --- a/docs/shipping.md +++ b/docs/shipping.md @@ -11,11 +11,11 @@ This was originally made by [@alexren](https://github.com/qcoral), adapted from Hi there! This page is my best attempt at explaining the age old question of, _"what on earth is shipping??"_ -In short, **shipping is process of actually making your project sharable**. It's _the_ most important part of your project, almost as important as the entire project itself +In short, **shipping is the process of actually making your project sharable**. It's _the_ most important part of your project, almost as important as the entire project itself When you first make something, usually it just lives as a file on your computer. This is bad because only YOU can access it\! Nobody else can see it. Not only that, but when you look back at the project a few years from now it’ll be very, _very_ difficult to remember anything about it\! _It’s not real_ -Shipping (at least in this context context) involves publishing your design out there for the world to see. Making it very real. This involves a couple steps: +Shipping (at least in this context) involves publishing your design out there for the world to see. Making it very real. This involves a couple steps: - **Documenting what your project actually is:** - A quick story/motivation on how the project came to be @@ -23,7 +23,7 @@ Shipping (at least in this context context) involves publishing your design out - A quick brief on how it all fits together - Some pictures of the design - **Making all files & resources easily accessible & organized** -- **Putting it on a platform that's easily shareable (i.e github, printables, etc)** +- **Putting it on a platform that's easily shareable (i.e GitHub, printables, etc)** --- diff --git a/docs/sourcing-parts.md b/docs/sourcing-parts.md index 594bc35a..e9c14c78 100755 --- a/docs/sourcing-parts.md +++ b/docs/sourcing-parts.md @@ -1,3 +1,6 @@ + | title | Sourcing Parts | +| description | Where to find the best (and cheapest) parts for your project | +| --- | --- | # Sourcing parts originally made by [@alexren](https://github.com/qcoral), adapted from hwdocs.hackclub.dev @@ -37,8 +40,7 @@ If you're in a pinch, Amazon actually has OK prices all things considered. I wou ### SBCs (Raspberry Pi, Orange Pi, etc) -Raspberry Pi -You can get Orange Pis from aliexpress. +Raspberry Pis are often harder to source cheaply, but you can get Orange Pis from AliExpress. Depending on your application though, you can also repurpose old laptops/desktops for your specific application if it's not space-sensitive @@ -61,6 +63,7 @@ Arduinos are fairly outdated by todays standards ### Batteries (Li-Po, CR2032, AA, etc) Coming soon. + ## Country Specific Notes ### Egypt: From 522506bb7ff169ac2edc3282d515207737d48acd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 00:26:59 +0000 Subject: [PATCH 009/118] Fix line endings in sourcing-parts.md Co-authored-by: Cybdo <119400800+Cybdo@users.noreply.github.com> --- docs/sourcing-parts.md | 230 ++++++++++++++++++++--------------------- 1 file changed, 115 insertions(+), 115 deletions(-) diff --git a/docs/sourcing-parts.md b/docs/sourcing-parts.md index e9c14c78..888850e4 100755 --- a/docs/sourcing-parts.md +++ b/docs/sourcing-parts.md @@ -1,116 +1,116 @@ - | title | Sourcing Parts | -| description | Where to find the best (and cheapest) parts for your project | -| --- | --- | - -# Sourcing parts -originally made by [@alexren](https://github.com/qcoral), adapted from hwdocs.hackclub.dev - -Welcome to part sourcing! Here's a ton of tips for you to optimize your part selection & costs! - -If you're ever unsure of your BOM, send it in #highway! People can definitely help you pick better parts and make better decisions. - -## Overview - -When picking parts, PLEASE be resourceful! Make the most out of every dollar and don't take the entire budget just because you can - -#### Do's - -- Lots of research into what parts to use! -- Ask in slack for advice on picking parts - -#### Don'ts - -- Buy a $100 Oscilloscope -- Buy extra parts for "later projects" -- Buy a $40 module on amazon instead of waiting 2 weeks to get it for $5 instead - -At the end, everything should be in a BOM with links to specific parts. - -## Specific parts - -### General - -Generally speaking, try to reference parts off of other projects since you'll eliminate more unknowns with the hardware. Adafruit projects are a great source for this! - -### Modules (TP4056, LED matrices, etc) - -Almost always the cheapest option is to get it from AliExpress; the only downside is that shipping _may_ take awhile - usually a 2-3 week buffer is safe. - -If you're in a pinch, Amazon actually has OK prices all things considered. I would avoid it though, unless you're going to miss Undercity if you don't use them - -### SBCs (Raspberry Pi, Orange Pi, etc) - -Raspberry Pis are often harder to source cheaply, but you can get Orange Pis from AliExpress. - -Depending on your application though, you can also repurpose old laptops/desktops for your specific application if it's not space-sensitive - -### Microcontrollers (arduino, RP2040, etc) - -AliExpress is still usually your go-to for this! - -Arduinos are fairly outdated by todays standards - -### PCBs - -- If you're in India, use [SEEED Studio's Fusion Service](https://www.seeedstudio.com/fusion_pcb.html). They take HCB cards and are by far the cheapest option -- If you're from another country, use JLCPCB, they offer PCBs for under $6 shipped as long as its under 100x100mm -- If you're from the US specifically, use PCBWAY. They're roughly half the cost of JLCPCB because they're able to avoid tariffs somehow - - Read here: [link](https://www.pcbway.com/blog/News/Impact_of_the_New_U_S_Tariff_Policy_on_Customs_Clearance_51dff4fd.html) -- EING is not allowed unless you have golden fingers (on-PCB USB-C contacts etc. and this is also subject to a case by case approval, please also mention it to your reviewer). -- Edge plating isn't allowed if it is only for the looks. -- You should just choose the cheapest options that gives you a working pcb - -### Batteries (Li-Po, CR2032, AA, etc) - -Coming soon. - -## Country Specific Notes - -### Egypt: - -#### Parts - -- Egypt has a store called RAM Electronics, which holds a certificate from Cytron, and it offers products at a significantly lower price. ([link](https://www.ram-e-shop.com/)) -- Additionally, there is Flux Electronics, which offers a wide range of 3D printer Parts and a vast selection of electronics. ([link](https://fluxelectronix.com/)) - -#### 3D printing service - -- There is Printfy3d, it accepts HCB cards, and it is a good choice. ([link](https://www.printfy3d.net/3d-printing)) - -### India: - -- Most vendors do not take international cards [cards issued outside India], which unfortunately, HCB falls into the category of. Contact your local vendors to try to get that changed! -- Here are some vendors that are documented to work with HCB cards currently, segregated into categories - - Electronic parts (RPIs, modules, power supplies and the like) - - [Silverline Electronics](https://www.silverlineelectronics.in/) - - [RoboSap](https://robosap.in/) - - [RoboticsDNA](https://roboticsdna.in/) - - [EBhoot](https://ebhoot.in/) - - [Electronicscomp](https://www.electronicscomp.com/) - - [Sharvi Electronics](https://sharvielectronics.com/) - - [Novo3D](https://novo3d.in/) - - 3D printed parts (3D printing as a Service, JLC3DP-alike) - - [3Ding](https://www.3ding.in/) - -> confirmed more vendors? wanna add specific notes for your country? contribute [here](https://github.com/hackclub/forge/edit/main/docs/sourcing-parts.md)!! You'd be helping a ton of people! - -## Tips for specific vendors - -### AliExpress - -Aliexpress is one of the highest skill ceiling stores out there - there's a ton of optimisation you can do. Here's some tips: - -- The welcome deal only works once when you make your account, so don't budget off of that - you will be hit by significantly higher prices afterwards. -- Pay attention to the estimate shipment date. This will vary by region, but generally speaking it's actually fairly reliable -- Also pay attention to the shipping cost! Usually items that don't have free shipping but a lower initial price actually end up being more expensive when you tally the costs -- There's specific vendors that are useful: - - AITXEM ROBOT: Any microcontroller you can think of - - Trianglelab: 3D printer parts! - -## Other resources - -COMING SOON! - - \ No newline at end of file From fbbd64be726476396efd92668d7de71027ff32ea Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 7 Apr 2026 10:32:46 +1000 Subject: [PATCH 010/118] remvoe deleted projects from pending count and view --- app/controllers/admin/projects_controller.rb | 13 ++++++++----- app/controllers/admin/static_pages_controller.rb | 2 +- app/javascript/pages/Admin/Projects/Index.tsx | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 04733001..15ca8414 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -4,10 +4,11 @@ class Admin::ProjectsController < Admin::ApplicationController def index scope = policy_scope(Project).includes(:user, :ships) + scope = params[:status] == "deleted" ? scope.discarded : scope.kept scope = scope.search(params[:query]) if params[:query].present? if params[:status] == "pending" scope = scope.where(status: [ :pending, :build_pending ]) - elsif params[:status].present? + elsif params[:status].present? && params[:status] != "deleted" scope = scope.where(status: params[:status]) end @pagy, @projects = pagy(scope.order(created_at: :desc)) @@ -23,7 +24,7 @@ def index end def pitches - scope = policy_scope(Project).includes(:user, :ships).where(status: :pending) + scope = policy_scope(Project).includes(:user, :ships).kept.where(status: :pending) scope = scope.search(params[:query]) if params[:query].present? @pagy, @projects = pagy(scope.order(created_at: :desc)) @@ -39,7 +40,7 @@ def pitches end def reviews - scope = policy_scope(Project).includes(:user, :ships).where(status: :build_pending) + scope = policy_scope(Project).includes(:user, :ships).kept.where(status: :build_pending) scope = scope.search(params[:query]) if params[:query].present? @pagy, @projects = pagy(scope.order(created_at: :desc)) @@ -181,7 +182,8 @@ def notify_slack_decision(project, decision, feedback) end def status_counts - raw = policy_scope(Project).group(:status).count + kept_scope = policy_scope(Project).kept + raw = kept_scope.group(:status).count pending_count = (raw["pending"] || 0) + (raw["build_pending"] || 0) { all: raw.values.sum, @@ -190,7 +192,8 @@ def status_counts returned: raw["returned"] || 0, rejected: raw["rejected"] || 0, draft: raw["draft"] || 0, - build_approved: raw["build_approved"] || 0 + build_approved: raw["build_approved"] || 0, + deleted: policy_scope(Project).discarded.count } end diff --git a/app/controllers/admin/static_pages_controller.rb b/app/controllers/admin/static_pages_controller.rb index 4b2803f3..d03f5a95 100644 --- a/app/controllers/admin/static_pages_controller.rb +++ b/app/controllers/admin/static_pages_controller.rb @@ -2,7 +2,7 @@ class Admin::StaticPagesController < Admin::ApplicationController def index counts = Rails.cache.fetch("admin/dashboard_counts", expires_in: 1.minute) do { - pending_reviews: Project.pending.count, + pending_reviews: Project.kept.pending.count, projects: Project.kept.count, users: User.kept.count, ships: Ship.count, diff --git a/app/javascript/pages/Admin/Projects/Index.tsx b/app/javascript/pages/Admin/Projects/Index.tsx index b9c72417..fb2c0a12 100644 --- a/app/javascript/pages/Admin/Projects/Index.tsx +++ b/app/javascript/pages/Admin/Projects/Index.tsx @@ -21,6 +21,7 @@ const filters = [ { key: 'returned', label: 'Returned' }, { key: 'rejected', label: 'Rejected' }, { key: 'draft', label: 'Draft' }, + { key: 'deleted', label: 'Deleted' }, ] export default function AdminProjectsIndex({ From 2d1d796dfcf210d1d99a52b06e08e5141543e7a7 Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:47:00 +1000 Subject: [PATCH 011/118] Render pitch text as markdown with Slack link conversion --- app/javascript/pages/Admin/Projects/Show.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/pages/Admin/Projects/Show.tsx b/app/javascript/pages/Admin/Projects/Show.tsx index 028497f5..76eb79ed 100644 --- a/app/javascript/pages/Admin/Projects/Show.tsx +++ b/app/javascript/pages/Admin/Projects/Show.tsx @@ -121,7 +121,11 @@ export default function AdminProjectsShow({ {project.pitch_text && (

Original Pitch

-
{project.pitch_text}
+
+ + {project.pitch_text.replace(/<(https?:\/\/[^|>]+)\|([^>]+)>/g, '[$2]($1)').replace(/<(https?:\/\/[^>]+)>/g, '$1')} + +
)} From 2e537e62f568f6f7ccc6722bc4a9c90cd76de40a Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:22:42 +1000 Subject: [PATCH 012/118] Add hidden toggle for projects Allow admins to hide projects from the explore page and public API while keeping them accessible to their owners via direct URL. --- app/controllers/admin/projects_controller.rb | 10 +- app/controllers/api/v1/projects_controller.rb | 2 +- app/controllers/explore_controller.rb | 2 +- app/javascript/pages/Admin/Projects/Show.tsx | 16 +++ app/javascript/types/index.ts | 1 + app/models/project.rb | 1 + config/routes.rb | 1 + .../20260407031232_add_hidden_to_projects.rb | 5 + db/schema.rb | 130 +++++++++++++++++- test/fixtures/projects.yml | 1 + test/models/project_test.rb | 1 + 11 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20260407031232_add_hidden_to_projects.rb diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 15ca8414..2abd74a2 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,6 +1,6 @@ class Admin::ProjectsController < Admin::ApplicationController before_action :require_admin!, only: [ :index ] - before_action :set_project, only: [ :show, :review, :destroy, :restore ] + before_action :set_project, only: [ :show, :review, :destroy, :restore, :toggle_hidden ] def index scope = policy_scope(Project).includes(:user, :ships) @@ -66,6 +66,13 @@ def show } end + def toggle_hidden + authorize @project, :update? + @project.update!(hidden: !@project.hidden) + status = @project.hidden? ? "hidden" : "visible" + redirect_to admin_project_path(@project), notice: "Project is now #{status} on explore." + end + def restore authorize @project @project.undiscard @@ -241,6 +248,7 @@ def serialize_project_detail(project) readme_fetched_at: project.readme_fetched_at&.strftime("%b %d, %Y %H:%M"), total_hours: project.total_hours, devlogs: project.devlogs.order(created_at: :desc).map { |d| serialize_devlog(d) }, + hidden: project.hidden, user_id: project.user_id, user_display_name: project.user.display_name, user_email: project.user.email, diff --git a/app/controllers/api/v1/projects_controller.rb b/app/controllers/api/v1/projects_controller.rb index 9c1dc4dc..6b7148db 100644 --- a/app/controllers/api/v1/projects_controller.rb +++ b/app/controllers/api/v1/projects_controller.rb @@ -2,7 +2,7 @@ class Api::V1::ProjectsController < Api::V1::BaseController include Pagy::Method def index - scope = Project.kept.order(created_at: :desc) + scope = Project.kept.where(hidden: false).order(created_at: :desc) scope = scope.search(params[:query]) if params[:query].present? @pagy, @projects = pagy(scope) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 4a95548f..74246678 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -2,7 +2,7 @@ class ExploreController < ApplicationController allow_unauthenticated_access def index - scope = Project.kept.includes(:user, :ships).order(created_at: :desc) + scope = Project.kept.where(hidden: false).includes(:user, :ships).order(created_at: :desc) scope = scope.search(params[:query]) if params[:query].present? @pagy, @projects = pagy(scope) diff --git a/app/javascript/pages/Admin/Projects/Show.tsx b/app/javascript/pages/Admin/Projects/Show.tsx index 028497f5..8867a787 100644 --- a/app/javascript/pages/Admin/Projects/Show.tsx +++ b/app/javascript/pages/Admin/Projects/Show.tsx @@ -427,6 +427,22 @@ export default function AdminProjectsShow({ )} +
+

Visibility

+
+ Hidden from explore + +
+ {project.hidden && ( +

This project won't appear on the explore page or public API.

+ )} +
+

Details

diff --git a/app/javascript/types/index.ts b/app/javascript/types/index.ts index e40e216c..45c9f828 100644 --- a/app/javascript/types/index.ts +++ b/app/javascript/types/index.ts @@ -149,6 +149,7 @@ export interface AdminProjectDetail { readme_fetched_at: string | null total_hours: number devlogs: AdminDevlog[] + hidden: boolean is_discarded: boolean discarded_at: string | null user_id: number diff --git a/app/models/project.rb b/app/models/project.rb index db3a6985..409ee7d1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -9,6 +9,7 @@ # devlog_mode :string # discarded_at :datetime # hcb_grant_link :string +# hidden :boolean default(FALSE), not null # name :string not null # override_hours :decimal(, ) # override_hours_justification :text diff --git a/config/routes.rb b/config/routes.rb index ac66ceb7..61a18c50 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -110,6 +110,7 @@ member do post :review post :restore + post :toggle_hidden end end resources :users, only: [ :index, :show, :destroy ] do diff --git a/db/migrate/20260407031232_add_hidden_to_projects.rb b/db/migrate/20260407031232_add_hidden_to_projects.rb new file mode 100644 index 00000000..36fc98f7 --- /dev/null +++ b/db/migrate/20260407031232_add_hidden_to_projects.rb @@ -0,0 +1,5 @@ +class AddHiddenToProjects < ActiveRecord::Migration[8.1] + def change + add_column :projects, :hidden, :boolean, default: false, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index c93c153e..21b1b929 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_04_05_012325) do +ActiveRecord::Schema[8.1].define(version: 2026_04_07_031232) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -112,6 +112,7 @@ t.string "devlog_mode" t.datetime "discarded_at" t.string "hcb_grant_link" + t.boolean "hidden", default: false, null: false t.string "name", null: false t.decimal "override_hours" t.text "override_hours_justification" @@ -161,6 +162,127 @@ t.index ["status"], name: "index_ships_on_status" end + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.string "concurrency_key", null: false + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.bigint "process_id" + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "error" + t.bigint "job_id", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "active_job_id" + t.text "arguments" + t.string "class_name", null: false + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "finished_at" + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at" + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "queue_name", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "hostname" + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.text "metadata" + t.string "name", null: false + t.integer "pid", null: false + t.bigint "supervisor_id" + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.datetime "run_at", null: false + t.string "task_key", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.text "arguments" + t.string "class_name" + t.string "command", limit: 2048 + t.datetime "created_at", null: false + t.text "description" + t.string "key", null: false + t.integer "priority", default: 0 + t.string "queue_name" + t.string "schedule", null: false + t.boolean "static", default: true, null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.string "key", null: false + t.datetime "updated_at", null: false + t.integer "value", default: 1, null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true + end + create_table "users", force: :cascade do |t| t.string "address_line1" t.string "address_line2" @@ -208,4 +330,10 @@ add_foreign_key "projects", "users", column: "reviewer_id" add_foreign_key "ships", "projects" add_foreign_key "ships", "users", column: "reviewer_id" + add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_claimed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_failed_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_ready_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_recurring_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade + add_foreign_key "solid_queue_scheduled_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade end diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml index 1013428a..a303f847 100644 --- a/test/fixtures/projects.yml +++ b/test/fixtures/projects.yml @@ -11,6 +11,7 @@ # devlog_mode :string # discarded_at :datetime # hcb_grant_link :string +# hidden :boolean default(FALSE), not null # name :string not null # override_hours :decimal(, ) # override_hours_justification :text diff --git a/test/models/project_test.rb b/test/models/project_test.rb index dbd944dc..c4894f77 100644 --- a/test/models/project_test.rb +++ b/test/models/project_test.rb @@ -9,6 +9,7 @@ # devlog_mode :string # discarded_at :datetime # hcb_grant_link :string +# hidden :boolean default(FALSE), not null # name :string not null # override_hours :decimal(, ) # override_hours_justification :text From a69e5596c79b706124369cecc50e4a56c0ea9272 Mon Sep 17 00:00:00 2001 From: Ryan <119400800+Cybdo@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:12:39 +1000 Subject: [PATCH 013/118] Revise funding details and project steps Updated the funding description and clarified project support. --- docs/how-it-works.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-it-works.md b/docs/how-it-works.md index 0d2d59b3..642018ee 100755 --- a/docs/how-it-works.md +++ b/docs/how-it-works.md @@ -4,7 +4,7 @@ # How It Works -Forge gives you up to $1,000 in grants to build hardware projects. Here's the process: +Forge will fund as much as you need to build any hardware project! Here's how: ## Step 1: Pitch Your Project From 0d976aa8aec91c469ca71f735cd70b77fe0960a3 Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:22:57 +1000 Subject: [PATCH 014/118] Show public-facing message for approved builds to non-owners Co-Authored-By: Claude Opus 4.6 (1M context) --- app/javascript/pages/Projects/Show.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/javascript/pages/Projects/Show.tsx b/app/javascript/pages/Projects/Show.tsx index 226340ff..b6d22668 100644 --- a/app/javascript/pages/Projects/Show.tsx +++ b/app/javascript/pages/Projects/Show.tsx @@ -619,7 +619,11 @@ export default function ProjectsShow({ verified

Build Approved!

-

Your build has been approved and your grant is ready.

+

+ {can.update + ? 'Your build has been approved and your grant is ready.' + : 'This build has been approved and has been funded!'} +

{project.hcb_grant_link && ( Date: Tue, 7 Apr 2026 18:13:53 +1000 Subject: [PATCH 015/118] Rework permissions with role-based defaults and add user role filter - Add support and fulfillment roles with auto-granted default permissions - Gate admin pages on permissions instead of admin-only role checks - Filter dashboard links by user permissions - Add role filter buttons to admin users index - RSVPs and database remain admin-only --- .../admin/application_controller.rb | 4 ++ app/controllers/admin/audit_log_controller.rb | 6 +- .../admin/feature_flags_controller.rb | 6 +- app/controllers/admin/projects_controller.rb | 6 +- .../admin/static_pages_controller.rb | 4 +- app/controllers/admin/users_controller.rb | 19 ++++-- app/controllers/application_controller.rb | 2 +- app/javascript/components/Nav.tsx | 2 +- .../pages/Admin/StaticPages/Index.tsx | 58 +++++++++++-------- app/javascript/pages/Admin/Users/Index.tsx | 29 +++++++++- app/javascript/pages/Admin/Users/Show.tsx | 22 +++++-- app/javascript/pages/Projects/Show.tsx | 6 +- app/models/user.rb | 23 +++++++- app/policies/project_policy.rb | 4 +- app/policies/user_policy.rb | 12 +++- config/routes.rb | 10 ++-- 16 files changed, 160 insertions(+), 53 deletions(-) diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index 11f3eef2..373149f8 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -10,4 +10,8 @@ def require_staff! def require_admin! raise ActionController::RoutingError, "Not Found" unless current_user&.admin? end + + def require_permission!(perm) + raise ActionController::RoutingError, "Not Found" unless current_user&.has_permission?(perm) + end end diff --git a/app/controllers/admin/audit_log_controller.rb b/app/controllers/admin/audit_log_controller.rb index 65c1f456..b86b7d6e 100644 --- a/app/controllers/admin/audit_log_controller.rb +++ b/app/controllers/admin/audit_log_controller.rb @@ -1,5 +1,5 @@ class Admin::AuditLogController < Admin::ApplicationController - before_action :require_admin! + before_action :require_audit_log_permission! def index scope = PaperTrail::Version.order(created_at: :desc) @@ -50,6 +50,10 @@ def show private + def require_audit_log_permission! + require_permission!("audit_log") + end + def serialize_version(version) actor = version.whodunnit.present? ? User.find_by(id: version.whodunnit) : nil diff --git a/app/controllers/admin/feature_flags_controller.rb b/app/controllers/admin/feature_flags_controller.rb index e1c73c1d..c3e55538 100644 --- a/app/controllers/admin/feature_flags_controller.rb +++ b/app/controllers/admin/feature_flags_controller.rb @@ -1,5 +1,5 @@ class Admin::FeatureFlagsController < Admin::ApplicationController - before_action :require_admin! + before_action :require_feature_flags_permission! def index @flags = FeatureFlag.order(:name) @@ -33,6 +33,10 @@ def destroy private + def require_feature_flags_permission! + require_permission!("feature_flags") + end + def flag_params params.expect(feature_flag: [ :name, :description, :enabled ]) end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 2abd74a2..f8a03bab 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,5 +1,5 @@ class Admin::ProjectsController < Admin::ApplicationController - before_action :require_admin!, only: [ :index ] + before_action :require_projects_permission! before_action :set_project, only: [ :show, :review, :destroy, :restore, :toggle_hidden ] def index @@ -151,6 +151,10 @@ def review private + def require_projects_permission! + require_permission!("projects") + end + def notify_slack_decision(project, decision, feedback) return unless project.slack_channel_id.present? && project.slack_message_ts.present? diff --git a/app/controllers/admin/static_pages_controller.rb b/app/controllers/admin/static_pages_controller.rb index d03f5a95..4820203a 100644 --- a/app/controllers/admin/static_pages_controller.rb +++ b/app/controllers/admin/static_pages_controller.rb @@ -12,7 +12,9 @@ def index render inertia: "Admin/StaticPages/Index", props: { user_name: current_user.display_name, - counts: counts + counts: counts, + permissions: User::AVAILABLE_PERMISSIONS.index_with { |p| current_user.has_permission?(p) }, + is_admin: current_user.admin? } end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 6c14218a..651636ac 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,15 +1,18 @@ class Admin::UsersController < Admin::ApplicationController - before_action :require_admin! + before_action :require_users_permission! def index scope = policy_scope(User).includes(:projects) scope = scope.search(params[:query]) if params[:query].present? + scope = scope.where("? = ANY(roles)", params[:role]) if params[:role].present? @pagy, @users = pagy(scope.order(created_at: :desc)) render inertia: "Admin/Users/Index", props: { users: @users.map { |u| serialize_user_row(u) }, pagy: pagy_props(@pagy), - query: params[:query].to_s + query: params[:query].to_s, + role_filter: params[:role].to_s, + available_roles: %w[user admin reviewer support fulfillment] } end @@ -22,7 +25,7 @@ def show user: serialize_user_detail(@user), projects: @projects.map { |p| serialize_project_row(p) }, can: { destroy: policy(@user).destroy?, restore: policy(@user).restore? }, - available_roles: %w[user admin reviewer], + available_roles: %w[user admin reviewer support fulfillment], available_permissions: User::AVAILABLE_PERMISSIONS } end @@ -69,7 +72,11 @@ def restore def update_roles @user = User.find(params[:id]) authorize @user, :update? - @user.update!(roles: params[:roles].select(&:present?)) + new_roles = params[:roles].select(&:present?) + added_roles = new_roles - @user.roles + @user.roles = new_roles + added_roles.each { |role| @user.apply_default_permissions_for_role(role) } + @user.save! redirect_to admin_user_path(@user), notice: "Roles updated." end @@ -89,6 +96,10 @@ def toggle_beta_approval private + def require_users_permission! + require_permission!("users") + end + def serialize_user_row(user) { id: user.id, diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1930e257..779b15c4 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -37,7 +37,7 @@ class ApplicationController < ActionController::Base private def gate_beta_access - return if current_user&.admin? + return if current_user&.staff? return if current_user&.is_beta_approved return if request.path == "/rsvp" || request.path.start_with?("/auth") || request.path.start_with?("/slack") || request.path.start_with?("/up") || request.path.start_with?("/rails/active_storage") diff --git a/app/javascript/components/Nav.tsx b/app/javascript/components/Nav.tsx index baad6e6b..9d336140 100644 --- a/app/javascript/components/Nav.tsx +++ b/app/javascript/components/Nav.tsx @@ -53,7 +53,7 @@ export default function Nav() { ) })} - {shared.auth.user?.is_admin && ( + {shared.auth.user?.is_staff && ( + is_admin: boolean } function DashboardLink({ href, label, external }: { href: string; label: string; external?: boolean }) { @@ -20,7 +22,9 @@ function DashboardLink({ href, label, external }: { href: string; label: string; return {label} } -export default function AdminStaticPagesIndex({ user_name, counts }: AdminDashboardProps) { +export default function AdminStaticPagesIndex({ user_name, counts, permissions, is_admin }: AdminDashboardProps) { + const can = (perm: string) => permissions[perm] + return (
@@ -31,34 +35,40 @@ export default function AdminStaticPagesIndex({ user_name, counts }: AdminDashbo

What are we forging today {user_name}?

-
-

Tasks

-
- - - + {(can('pending_reviews') || can('projects')) && ( +
+

Tasks

+
+ {can('projects') && } + {can('pending_reviews') && } + {can('pending_reviews') && } +
-
+ )} -
-

Dashboards

-
- - - - - - - + {(can('projects') || can('users') || can('feature_flags') || can('audit_log') || can('jobs')) && ( +
+

Dashboards

+
+ {can('projects') && } + {can('users') && } + {is_admin && } + {can('feature_flags') && } + {can('audit_log') && } + {is_admin && } + {can('jobs') && } +
-
+ )} -
-

3rd Party

-
- + {can('third_party') && ( +
+

3rd Party

+
+ +
-
+ )}
) } diff --git a/app/javascript/pages/Admin/Users/Index.tsx b/app/javascript/pages/Admin/Users/Index.tsx index c35c7291..78191233 100644 --- a/app/javascript/pages/Admin/Users/Index.tsx +++ b/app/javascript/pages/Admin/Users/Index.tsx @@ -7,23 +7,32 @@ export default function AdminUsersIndex({ users, pagy, query, + role_filter, + available_roles, }: { users: AdminUserRow[] pagy: PagyProps query: string + role_filter: string + available_roles: string[] }) { const [searchQuery, setSearchQuery] = useState(query) function search(e: React.FormEvent) { e.preventDefault() - router.get('/admin/users', { query: searchQuery }, { preserveState: true }) + router.get('/admin/users', { query: searchQuery, role: role_filter || undefined }, { preserveState: true }) + } + + function filterByRole(role: string) { + const newRole = role === role_filter ? undefined : role + router.get('/admin/users', { query: searchQuery || undefined, role: newRole }, { preserveState: true }) } return (

Users

-
+
+
+ {available_roles.map((role) => ( + + ))} +
+
Name diff --git a/app/javascript/pages/Admin/Users/Show.tsx b/app/javascript/pages/Admin/Users/Show.tsx index 778cc51d..254b6973 100644 --- a/app/javascript/pages/Admin/Users/Show.tsx +++ b/app/javascript/pages/Admin/Users/Show.tsx @@ -13,6 +13,14 @@ const permissionLabels: Record = { third_party: '3rd Party', } +const roleDescriptions: Record = { + user: 'Basic user account', + admin: 'Full access to everything', + reviewer: 'Reviews pitches and builds', + support: 'Manages users and projects', + fulfillment: 'Handles project fulfillment', +} + export default function AdminUsersShow({ user, projects, @@ -134,14 +142,15 @@ export default function AdminUsersShow({

Roles

-
+

Assigning a staff role auto-grants its default permissions. You can still manually adjust permissions below.

+
{available_roles.map((role) => { const active = user.roles.includes(role) return ( ) })} @@ -159,7 +173,7 @@ export default function AdminUsersShow({

Permissions

-

Admins have all permissions by default. These only apply to non-admin staff.

+

Admins have all permissions by default. These apply to non-admin staff roles.

{available_permissions.map((perm) => { const active = user.permissions.includes(perm) diff --git a/app/javascript/pages/Projects/Show.tsx b/app/javascript/pages/Projects/Show.tsx index 226340ff..b6d22668 100644 --- a/app/javascript/pages/Projects/Show.tsx +++ b/app/javascript/pages/Projects/Show.tsx @@ -619,7 +619,11 @@ export default function ProjectsShow({ verified

Build Approved!

-

Your build has been approved and your grant is ready.

+

+ {can.update + ? 'Your build has been approved and your grant is ready.' + : 'This build has been approved and has been funded!'} +

{project.hcb_grant_link && ( %w[pending_reviews projects ships], + "support" => %w[projects users], + "fulfillment" => %w[projects ships] + }.freeze + def has_permission?(perm) admin? || permissions.include?(perm.to_s) end @@ -115,6 +129,13 @@ def revoke_permission(perm) permissions.delete(perm.to_s) end + def apply_default_permissions_for_role(role) + defaults = ROLE_DEFAULT_PERMISSIONS[role.to_s] + return unless defaults + + self.permissions |= defaults + end + def self.exchange_hca_token(code, redirect_uri) token_data = HcaService.exchange_code_for_token(code, redirect_uri) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index c1b1a08c..452b8065 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -34,12 +34,12 @@ def restore? end def review? - user&.staff? + user&.has_permission?("projects") end class Scope < ApplicationPolicy::Scope def resolve - if user&.admin? + if user&.has_permission?("projects") scope.all else scope.kept diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 9c2e4f2b..80b81a82 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,10 +1,10 @@ class UserPolicy < ApplicationPolicy def index? - admin? + has_users_permission? end def show? - admin? || record == user + has_users_permission? || record == user end def update? @@ -21,11 +21,17 @@ def restore? class Scope < ApplicationPolicy::Scope def resolve - if user&.admin? + if user&.has_permission?("users") scope.all else scope.kept.where(id: user&.id) end end end + + private + + def has_users_permission? + user&.has_permission?("users") + end end diff --git a/config/routes.rb b/config/routes.rb index 61a18c50..f7737025 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -99,13 +99,7 @@ get "/" => "static_pages#index", as: :root get "pitches" => "projects#pitches", as: :pitches get "reviews" => "projects#reviews", as: :reviews - end - end - - constraints AdminConstraint.new do - mount MissionControl::Jobs::Engine, at: "/admin/jobs" - namespace :admin do resources :projects, only: [ :index, :show, :destroy ] do member do post :review @@ -141,6 +135,10 @@ end end + constraints AdminConstraint.new do + mount MissionControl::Jobs::Engine, at: "/admin/jobs" + end + post "slack/events" => "slack/events#create" get "up" => "rails/health#show", as: :rails_health_check From 7ee5411fddc75d2edba168d6c3d064bb524b4f77 Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:44:28 +1000 Subject: [PATCH 016/118] Update favicon and fix Rack::Attack throttle crash - Replace old SVG favicon with new forge blacksmith icon - Add ICO (16x16, 32x32), PNG (192x192), and apple-touch-icon (180x180) - Fix NoMethodError in throttled_responder when match_data is nil --- app/views/layouts/application.html.erb | 5 ++--- app/views/layouts/inertia.html.erb | 5 ++--- config/initializers/rack_attack.rb | 3 ++- public/apple-touch-icon.png | Bin 0 -> 55330 bytes public/favicon.ico | Bin 1666 -> 2994 bytes public/icon-192.png | Bin 0 -> 62155 bytes 6 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 public/apple-touch-icon.png create mode 100644 public/icon-192.png diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b7d7973d..b5a2b796 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,9 +9,8 @@ <%= csp_meta_tag %> - - - + + diff --git a/app/views/layouts/inertia.html.erb b/app/views/layouts/inertia.html.erb index 94f7d2c4..993ace70 100644 --- a/app/views/layouts/inertia.html.erb +++ b/app/views/layouts/inertia.html.erb @@ -9,9 +9,8 @@ <%= csp_meta_tag %> - - - + + diff --git a/config/initializers/rack_attack.rb b/config/initializers/rack_attack.rb index 905434a7..5356932e 100644 --- a/config/initializers/rack_attack.rb +++ b/config/initializers/rack_attack.rb @@ -35,7 +35,8 @@ class Rack::Attack # Custom throttle response self.throttled_responder = lambda do |env| - retry_after = env["rack.attack.match_data"][:period] + match_data = env["rack.attack.match_data"] || {} + retry_after = match_data[:period] || 60 [ 429, { diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5a61607063534239c0aff2c0cfa8dc2e4396025d GIT binary patch literal 55330 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 bPxt==DbI7VLeacy00000NkvXXu0mjfBBZTe literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico index 9cbd7e1d052ccf768080c13908b3dfa9e6305ab5..8004ca7d86743cbb86ab53988c4ac30f2b35c31b 100644 GIT binary patch delta 2989 zcmV;e3sUrg4YC)1000310ss&Y00000AOOn(001Tc001B$00000AOOJ#008j<004xBc76ciQVg+d=72txdS3PJY;-FX>xYe+_V6QYS; z=rA(lj3W)BGoJT3XP>>+l4CqCSI=|z!@I|*#K7) z-)|w#9+Cqkm`qeAUl2LO=4VB@7W}lg{qXZ`fBA=h&5DCwN-cqtA6BpZ`Vn?F?A`(_ zL|N8;d&p+stN7E+!RLGY2Q%Ae_LSD!P3{*L&X&&4)orr{NKp0=Ge$>a%BScOR`CpM3en zdzHn1J1q%EThAw&i~Y41qzb0Sl1-9TJ9=K#^aq5tGk6SZtLE z7LF(cc1+BafcX+571tLyK{R7&D^$5gaa9!n1Z0_W4>!*!qo#=9lt3}{(1u}Xxd(6M zhK$LPW3E^I>vL(}fzCiy?AwuFe}33L=5;<9Oan|QX;i9tEr4W^l2zyw!mN@MhN7L$ znX`X)n|X51Zeps`T(uF%@%U)AZ&&5*C4B!-@%ZtQN16J;>xhQP^^t2jH{xBOtiqw< z@}_r=yyq-q$M%vPBhTU4S$#ytlaMjng7*UsB{!=ySTFCsq4MP`@>)>sQ*2Pns;cM$ zfe?UY!e54_yE;{$uQ~8(626 z1lE&Ih{&mWO&}}6&!qju^sXTUOiY~q z-EXhShPP;~0eJ%;<(L7r%&!!Yokb@?5)3rMPrkG56CYczWwNC+$8djqMppw? zJ*G*|rr4HtJ0=zO!Qip3FO*i4x{5laR(^KhhQ}YdaFo5m9h;k5x4RQ(+Vy`be}3->OQ~^5NS#-c9aHO6(lZi0*>Cr+S z`djztVxp#-eXn@vS0`_~`O2~=5B&W|rzm%9*)V&q7#?ze`1!VHez|SW2m5rjM5jb2 zQXxG(h+64&pF(@0Bm&NJr(lhQMPP}Rx_b8`twXQHhktqUBiCKEYv+I1!g4F~ZtJG? z?QSux|Zqn5*V%6@{4<5s!VnNY@X%kTdXmxMJ)G%|Q?z&Ch z{n+x$Q{{P8U-5o)!@8(4`QH33H;wPP@scBNzIM^*^pBoguJ>{>G_WuLytnTL0c%OYdv6uHM-4+)o3SkoLQ_b7xO8`dKrE4FVHg(lUY;Zq1!@ zA#*-+RCR6$`;W&vpJ-L9`P{;abEsEyBAQ?BTz{ea#qITjvjcx41KGNo?_XCR+I2d3#=5nZT*qUHa~TEs@)Cs+s4berFd{MUfS=zdwD=t zlc@9L%P2lLQJa6C5G_~q={GuKLmSpLs|9fD?#pLp7v7qj9vZ0bzIMAy@rx@HjVfL_ zDfhqFdg)kt`B=*a8@FfXe9=j_T;RVx?o(S*5)s4QgH0NUNQ4m8d0aFQ-V|9_SYb`i zPrP;S7jFFMP2*$D>h{e8lc!csymW$Fvx6x}$%?RqFSMq6Aa+EMu`mG;@5e)I)RP zp;xV^dZ2S%Jv^tGqsy8k$>0$|Dy2vgOQosZUPXf^=h>ONLkOHZn@c^o_ zD0P2DlIiFgCSA+h!inLc-Dpi<`3Src$`WZU5vOK7fM}_Ut@vK<*7a&xu4#Z86g|<$ zT2l652=-)D5Y3*Ml%@#Z?f&1GMmyiQ#9+q>=2r||0wRgA+;TK4*$WOJ zCOfx`Yp~f|LQ{U4`XRfq<1SK_bG2bKRH5G=6>6)fw jj4)dls9QiuU5)<-bA0oNm*$`h00000NkvXXu0mjf?fk|l delta 1637 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%0ACI`nqDg z;Q#s~FTQfWp#ymh=!~Y`ZEbE#3;21@D+$golHlS0=8wMe!Dp_1;v24)1$J?k0G38U z)c5)DcqYq30uJ6c?d~rO&`O{4Jqy!C8M)0&8$sW|rsWY=6Q`xtX?npx!SU$A8rJ7G z3!C}LCKV)j+N*#1FIt(&qB+mMlB+p4c{sZ8!e4bP3uNm>c)h6RKxE+y&*t(PS3n{pJ94Wy6MT59{U2XFvPtvmgKbdGHzyzWWxnVA)5NP=Doc z@f*Iu+>iYU&rb&*$y1Jbh*4+Ild6(o0`S&r)E^g}oC}Fu|IMHO^dI{ZKY91)v`3;Q zl)3Ob?*V4ieCN#hfs;crscIC-;0gC%`G5SKkA3XwCq8*eDq24?CGXUpi9zlB3M0&E zZ+hfwTgKadfeh{RnCHV5uUS+U;-xHPes1Naf_9!T>^nS30_@6?GFx`F^>*mny*BNo zqy^WTJRIwfh$K6IEF7w|*5GldzyL;Ba2n1>*ebU89*we^-TwUibpRfFVTW%bRjE>Y zZQ6RRC;-wn{?+xYX1)bPYA`QD^TQ3I0gxpyfB%bL_}+i{$L=5bwXk7=)%uFp zsPyp6dByXu4~updlY@p*qMNr*{>*>yDJC}hgY20k?=uneXG*I;{~h-oxrDhw`(2&;@MI9R8v2zBFC@$Zr1yl;iK6 z^6xnqBA(kwRqwpUF^79;{|El-&;8Lq`IEz!zN_1>Eced0aZpEJ@fvN7{zu0naP+gt zKlwj>`PG*myzhPc639e=`id4;0^2z~Y2_io8E^jK(ucF)?dN)KjU6CVGAGC~9*X}! zbL;IHlHzFV=GpFZmT^)HJ?ASjX=&3~iWae8$c_U9+RD;~Mu|-KdbtTY^KR+@n6ly7 zdcu~7f)JoiE=K;PC8DuY-FTm�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 literal 0 HcmV?d00001 From 9f5b4865b57c48152ceaa00e3ee6552759d6eabe Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:49:24 +1000 Subject: [PATCH 017/118] Bust favicon cache with version query param --- app/views/layouts/application.html.erb | 6 +++--- app/views/layouts/inertia.html.erb | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b5a2b796..07903de3 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -8,9 +8,9 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - - - + + + diff --git a/app/views/layouts/inertia.html.erb b/app/views/layouts/inertia.html.erb index 993ace70..e90fb863 100644 --- a/app/views/layouts/inertia.html.erb +++ b/app/views/layouts/inertia.html.erb @@ -8,9 +8,9 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - - - + + + From a575f937813e29976b18d98c444716ddd2954127 Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:21:18 +1000 Subject: [PATCH 018/118] Add Slack support bot with ticket tracking and admin page - Bot auto-responds in public support channel with forge-themed message - Posts ticket to BTS channel with user info, Claim/Resolve buttons - BTS thread replies relayed to public thread with staff name/avatar - New interactivity endpoint handles Block Kit button clicks - SupportTicket model tracks status (open/claimed/resolved) - Admin page at /admin/support with stats, leaderboard, ticket list - New "support" permission added to AVAILABLE_PERMISSIONS --- .../admin/support_tickets_controller.rb | 54 +++++++ app/controllers/slack/events_controller.rb | 43 ++++-- .../slack/interactivity_controller.rb | 132 ++++++++++++++++ .../pages/Admin/StaticPages/Index.tsx | 3 +- .../pages/Admin/SupportTickets/Index.tsx | 144 ++++++++++++++++++ app/javascript/pages/Admin/Users/Show.tsx | 1 + app/javascript/types/index.ts | 21 +++ app/jobs/support_relay_job.rb | 29 ++++ app/jobs/support_ticket_job.rb | 129 ++++++++++++++++ app/models/support_ticket.rb | 19 +++ app/models/user.rb | 3 +- app/policies/support_ticket_policy.rb | 11 ++ config/routes.rb | 2 + .../20260407091606_create_support_tickets.rb | 32 ++++ 14 files changed, 612 insertions(+), 11 deletions(-) create mode 100644 app/controllers/admin/support_tickets_controller.rb create mode 100644 app/controllers/slack/interactivity_controller.rb create mode 100644 app/javascript/pages/Admin/SupportTickets/Index.tsx create mode 100644 app/jobs/support_relay_job.rb create mode 100644 app/jobs/support_ticket_job.rb create mode 100644 app/models/support_ticket.rb create mode 100644 app/policies/support_ticket_policy.rb create mode 100644 db/migrate/20260407091606_create_support_tickets.rb diff --git a/app/controllers/admin/support_tickets_controller.rb b/app/controllers/admin/support_tickets_controller.rb new file mode 100644 index 00000000..f5d9c022 --- /dev/null +++ b/app/controllers/admin/support_tickets_controller.rb @@ -0,0 +1,54 @@ +class Admin::SupportTicketsController < Admin::ApplicationController + before_action :require_support_permission! + + def index + scope = policy_scope(SupportTicket) + scope = scope.where(status: params[:status]) if params[:status].present? + @pagy, @tickets = pagy(scope.order(created_at: :desc)) + + render inertia: "Admin/SupportTickets/Index", props: { + tickets: @tickets.map { |t| serialize_ticket(t) }, + pagy: pagy_props(@pagy), + status_filter: params[:status].to_s, + counts: { + all: SupportTicket.count, + open: SupportTicket.open.count, + claimed: SupportTicket.claimed.count, + resolved: SupportTicket.resolved.count + }, + leaderboard: build_leaderboard + } + end + + private + + def require_support_permission! + require_permission!("support") + end + + def serialize_ticket(ticket) + { + id: ticket.id, + slack_display_name: ticket.slack_display_name, + slack_avatar_url: ticket.slack_avatar_url, + original_text: ticket.original_text.truncate(200), + status: ticket.status, + claimed_by_name: ticket.claimed_by_name, + resolved_by_name: ticket.resolved_by_name, + slack_thread_url: ticket.slack_thread_url, + bts_thread_url: ticket.bts_thread_url, + created_at: ticket.created_at.strftime("%b %d, %Y %H:%M"), + resolved_at: ticket.resolved_at&.strftime("%b %d, %Y %H:%M") + } + end + + def build_leaderboard + SupportTicket.resolved + .where.not(resolved_by_name: nil) + .group(:resolved_by_name) + .order(Arel.sql("count(*) DESC")) + .limit(10) + .count + .map { |name, count| { name: name, count: count } } + end +end diff --git a/app/controllers/slack/events_controller.rb b/app/controllers/slack/events_controller.rb index 68ccaadc..16996ca4 100644 --- a/app/controllers/slack/events_controller.rb +++ b/app/controllers/slack/events_controller.rb @@ -27,21 +27,46 @@ def handle_event(event) return unless event["type"] == "message" return if event["subtype"].present? return if event["bot_id"].present? - return if event["thread_ts"].present? - return unless event["channel"] == forge_channel_id - - SlackPitchJob.perform_later( - slack_user_id: event["user"], - channel_id: event["channel"], - message_ts: event["ts"], - text: event["text"] - ) + + channel = event["channel"] + is_thread = event["thread_ts"].present? + + if !is_thread && channel == forge_channel_id + SlackPitchJob.perform_later( + slack_user_id: event["user"], + channel_id: channel, + message_ts: event["ts"], + text: event["text"] + ) + elsif !is_thread && channel == support_channel_id + SupportTicketJob.perform_later( + slack_user_id: event["user"], + channel_id: channel, + message_ts: event["ts"], + text: event["text"] + ) + elsif is_thread && channel == bts_channel_id + SupportRelayJob.perform_later( + slack_user_id: event["user"], + channel_id: channel, + thread_ts: event["thread_ts"], + text: event["text"] + ) + end end def forge_channel_id ENV.fetch("SLACK_FORGE_CHANNEL_ID", "") end + def support_channel_id + ENV.fetch("SLACK_SUPPORT_CHANNEL_ID", "") + end + + def bts_channel_id + ENV.fetch("SLACK_BTS_CHANNEL_ID", "") + end + def verify_slack_request signing_secret = ENV.fetch("SLACK_SIGNING_SECRET", "") return head(:unauthorized) if signing_secret.blank? diff --git a/app/controllers/slack/interactivity_controller.rb b/app/controllers/slack/interactivity_controller.rb new file mode 100644 index 00000000..e7033ecc --- /dev/null +++ b/app/controllers/slack/interactivity_controller.rb @@ -0,0 +1,132 @@ +class Slack::InteractivityController < ApplicationController + skip_before_action :verify_authenticity_token + allow_unauthenticated_access + + before_action :verify_slack_request + + def create + payload = JSON.parse(params[:payload]) + + case payload["type"] + when "block_actions" + handle_block_actions(payload) + end + + head :ok + end + + private + + def handle_block_actions(payload) + action = payload.dig("actions", 0) + return unless action + + slack_user_id = payload.dig("user", "id") + display_name = resolve_display_name(slack_user_id, payload.dig("user", "username")) + + case action["action_id"] + when "support_claim" + handle_claim(action["value"], slack_user_id, display_name) + when "support_resolve" + handle_resolve(action["value"], slack_user_id, display_name) + when "support_mark_answered" + handle_mark_answered(action["value"], slack_user_id, display_name) + end + end + + def handle_claim(ticket_id, slack_user_id, display_name) + ticket = SupportTicket.find_by(id: ticket_id) + return unless ticket + return if ticket.resolved? + + ticket.update!( + status: :claimed, + claimed_by_slack_id: slack_user_id, + claimed_by_name: display_name, + claimed_at: Time.current + ) + + update_bts_message(ticket) + end + + def handle_resolve(ticket_id, slack_user_id, display_name) + ticket = SupportTicket.find_by(id: ticket_id) + return unless ticket + + ticket.update!( + status: :resolved, + resolved_by_slack_id: slack_user_id, + resolved_by_name: display_name, + resolved_at: Time.current + ) + + update_bts_message(ticket) + end + + def handle_mark_answered(ticket_id, slack_user_id, display_name) + ticket = SupportTicket.find_by(id: ticket_id) + return unless ticket + + ticket.update!( + status: :resolved, + resolved_by_slack_id: slack_user_id, + resolved_by_name: display_name, + resolved_at: Time.current + ) + + slack_client.chat_postMessage( + channel: ticket.channel_id, + thread_ts: ticket.thread_ts, + text: ":white_check_mark: This question has been marked as answered!" + ) + + update_bts_message(ticket) + rescue StandardError => e + Rails.logger.error("Failed to post answered message: #{e.message}") + end + + def update_bts_message(ticket) + return unless ticket.bts_message_ts.present? + + blocks = SupportTicketJob.bts_blocks(ticket) + slack_client.chat_update( + channel: ticket.bts_channel_id, + ts: ticket.bts_message_ts, + blocks: blocks, + text: "Support ticket from #{ticket.slack_display_name}" + ) + rescue StandardError => e + Rails.logger.error("Failed to update BTS message: #{e.message}") + end + + def resolve_display_name(slack_user_id, fallback) + result = slack_client.users_info(user: slack_user_id) + profile = result&.user&.profile + profile&.display_name.presence || profile&.real_name.presence || fallback + rescue StandardError + fallback + end + + def slack_client + @slack_client ||= Slack::Web::Client.new(token: ENV.fetch("SLACK_BOT_TOKEN", nil)) + end + + def verify_slack_request + signing_secret = ENV.fetch("SLACK_SIGNING_SECRET", "") + return head(:unauthorized) if signing_secret.blank? + + timestamp = request.headers["X-Slack-Request-Timestamp"] + return head(:unauthorized) if timestamp.blank? + return head(:unauthorized) if (Time.now.to_i - timestamp.to_i).abs > 300 + + request.body.rewind + body = request.body.read + request.body.rewind + + sig_basestring = "v0:#{timestamp}:#{body}" + my_signature = "v0=#{OpenSSL::HMAC.hexdigest("SHA256", signing_secret, sig_basestring)}" + slack_signature = request.headers["X-Slack-Signature"] + + head(:unauthorized) unless ActiveSupport::SecurityUtils.secure_compare(my_signature, slack_signature.to_s) + end +end diff --git a/app/javascript/pages/Admin/StaticPages/Index.tsx b/app/javascript/pages/Admin/StaticPages/Index.tsx index c2591d7a..d355a347 100644 --- a/app/javascript/pages/Admin/StaticPages/Index.tsx +++ b/app/javascript/pages/Admin/StaticPages/Index.tsx @@ -46,12 +46,13 @@ export default function AdminStaticPagesIndex({ user_name, counts, permissions,
)} - {(can('projects') || can('users') || can('feature_flags') || can('audit_log') || can('jobs')) && ( + {(can('projects') || can('users') || can('support') || can('feature_flags') || can('audit_log') || can('jobs')) && (

Dashboards

{can('projects') && } {can('users') && } + {can('support') && } {is_admin && } {can('feature_flags') && } {can('audit_log') && } diff --git a/app/javascript/pages/Admin/SupportTickets/Index.tsx b/app/javascript/pages/Admin/SupportTickets/Index.tsx new file mode 100644 index 00000000..7bd63d65 --- /dev/null +++ b/app/javascript/pages/Admin/SupportTickets/Index.tsx @@ -0,0 +1,144 @@ +import { router } from '@inertiajs/react' +import Pagination from '@/components/Pagination' +import type { AdminSupportTicketRow, PagyProps, SupportLeaderboardEntry, SupportTicketStatus } from '@/types' + +const statusConfig: Record = { + open: { label: 'Open', bg: 'bg-amber-500/10', text: 'text-amber-400' }, + claimed: { label: 'Claimed', bg: 'bg-blue-500/10', text: 'text-blue-400' }, + resolved: { label: 'Resolved', bg: 'bg-emerald-500/10', text: 'text-emerald-400' }, +} + +const filters = [ + { key: '', label: 'All' }, + { key: 'open', label: 'Open' }, + { key: 'claimed', label: 'Claimed' }, + { key: 'resolved', label: 'Resolved' }, +] + +export default function AdminSupportTicketsIndex({ + tickets, + pagy, + status_filter, + counts, + leaderboard, +}: { + tickets: AdminSupportTicketRow[] + pagy: PagyProps + status_filter: string + counts: { all: number; open: number; claimed: number; resolved: number } + leaderboard: SupportLeaderboardEntry[] +}) { + function filterByStatus(status: string) { + router.get('/admin/support', { status: status || undefined }, { preserveState: true }) + } + + return ( +
+

Support Tickets

+ +
+ {[ + { label: 'Total', value: counts.all, color: 'text-stone-300' }, + { label: 'Open', value: counts.open, color: 'text-amber-400' }, + { label: 'Claimed', value: counts.claimed, color: 'text-blue-400' }, + { label: 'Resolved', value: counts.resolved, color: 'text-emerald-400' }, + ].map((stat) => ( +
+ {stat.label} +

{stat.value}

+
+ ))} +
+ +
+ + +
+
+

Leaderboard

+ {leaderboard.length > 0 ? ( +
+ {leaderboard.map((entry, i) => ( +
+
+ + #{i + 1} + + {entry.name} +
+ {entry.count} +
+ ))} +
+ ) : ( +

No resolved tickets yet.

+ )} +
+
+
+
+ ) +} diff --git a/app/javascript/pages/Admin/Users/Show.tsx b/app/javascript/pages/Admin/Users/Show.tsx index 254b6973..b3f8447e 100644 --- a/app/javascript/pages/Admin/Users/Show.tsx +++ b/app/javascript/pages/Admin/Users/Show.tsx @@ -11,6 +11,7 @@ const permissionLabels: Record = { audit_log: 'Audit Log', jobs: 'Jobs', third_party: '3rd Party', + support: 'Support Tickets', } const roleDescriptions: Record = { diff --git a/app/javascript/types/index.ts b/app/javascript/types/index.ts index 45c9f828..2de1e664 100644 --- a/app/javascript/types/index.ts +++ b/app/javascript/types/index.ts @@ -191,3 +191,24 @@ export interface ShipForm { project_name: string user_display_name: string } + +export type SupportTicketStatus = 'open' | 'claimed' | 'resolved' + +export interface AdminSupportTicketRow { + id: number + slack_display_name: string + slack_avatar_url: string | null + original_text: string + status: SupportTicketStatus + claimed_by_name: string | null + resolved_by_name: string | null + slack_thread_url: string + bts_thread_url: string | null + created_at: string + resolved_at: string | null +} + +export interface SupportLeaderboardEntry { + name: string + count: number +} diff --git a/app/jobs/support_relay_job.rb b/app/jobs/support_relay_job.rb new file mode 100644 index 00000000..f51cc791 --- /dev/null +++ b/app/jobs/support_relay_job.rb @@ -0,0 +1,29 @@ +class SupportRelayJob < ApplicationJob + queue_as :default + + def perform(slack_user_id:, channel_id:, thread_ts:, text:) + ticket = SupportTicket.find_by(bts_message_ts: thread_ts) + return unless ticket + + user_info = slack_client.users_info(user: slack_user_id) + profile = user_info&.user&.profile + display_name = profile&.display_name.presence || profile&.real_name.presence || "Support" + avatar_url = profile&.image_48.presence || profile&.image_72.presence + + slack_client.chat_postMessage( + channel: ticket.channel_id, + thread_ts: ticket.thread_ts, + text: text, + username: display_name, + icon_url: avatar_url + ) + rescue StandardError => e + Rails.logger.error("SupportRelayJob failed: #{e.class}: #{e.message}\n#{e.backtrace&.first(5)&.join("\n")}") + end + + private + + def slack_client + @slack_client ||= Slack::Web::Client.new(token: ENV.fetch("SLACK_BOT_TOKEN", nil)) + end +end diff --git a/app/jobs/support_ticket_job.rb b/app/jobs/support_ticket_job.rb new file mode 100644 index 00000000..87b4f280 --- /dev/null +++ b/app/jobs/support_ticket_job.rb @@ -0,0 +1,129 @@ +class SupportTicketJob < ApplicationJob + queue_as :default + + def perform(slack_user_id:, channel_id:, message_ts:, text:) + return if SupportTicket.exists?(thread_ts: message_ts) + + user_info = slack_client.users_info(user: slack_user_id) + profile = user_info&.user&.profile + display_name = profile&.display_name.presence || profile&.real_name.presence || "Unknown" + avatar_url = profile&.image_192.presence || profile&.image_72.presence + + bts_channel = ENV.fetch("SLACK_BTS_CHANNEL_ID", "") + + ticket = SupportTicket.create!( + slack_user_id: slack_user_id, + slack_display_name: display_name, + slack_avatar_url: avatar_url, + channel_id: channel_id, + thread_ts: message_ts, + bts_channel_id: bts_channel, + original_text: text.truncate(4000), + status: :open + ) + + public_blocks = [ + { + type: "section", + text: { + type: "mrkdwn", + text: ":hammer_and_wrench: *Hey builder!* We've seen your question and someone from the team will be with you soon.\n\nIn the meantime, check out — your answer might already be there!" + } + }, + { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "Mark Answered", emoji: true }, + style: "primary", + action_id: "support_mark_answered", + value: ticket.id.to_s + } + ] + } + ] + + slack_client.chat_postMessage( + channel: channel_id, + thread_ts: message_ts, + blocks: public_blocks, + text: "Hey builder! We've seen your question and someone from the team will be with you soon." + ) + + bts_blocks = self.class.bts_blocks(ticket) + + result = slack_client.chat_postMessage( + channel: bts_channel, + blocks: bts_blocks, + text: "New support question from #{display_name}" + ) + + ticket.update!(bts_message_ts: result["ts"]) if result["ok"] + rescue StandardError => e + Rails.logger.error("SupportTicketJob failed: #{e.class}: #{e.message}\n#{e.backtrace&.first(10)&.join("\n")}") + end + + def self.bts_blocks(ticket) + status_emoji = case ticket.status + when "open" then ":large_yellow_circle:" + when "claimed" then ":large_blue_circle:" + when "resolved" then ":white_check_mark:" + end + + status_text = ticket.status.capitalize + status_text += " by #{ticket.claimed_by_name}" if ticket.claimed? && ticket.claimed_by_name.present? + status_text += " by #{ticket.resolved_by_name}" if ticket.resolved? && ticket.resolved_by_name.present? + + thread_url = ticket.slack_thread_url + + blocks = [ + { + type: "context", + elements: [ + ticket.slack_avatar_url.present? ? { type: "image", image_url: ticket.slack_avatar_url, alt_text: ticket.slack_display_name.to_s } : nil, + { type: "mrkdwn", text: "*#{ticket.slack_display_name}*" } + ].compact + }, + { + type: "section", + text: { type: "mrkdwn", text: ticket.original_text.truncate(2900) } + }, + { + type: "context", + elements: [ + { type: "mrkdwn", text: "#{status_emoji} *#{status_text}* | <#{thread_url}|View thread>" } + ] + } + ] + + unless ticket.resolved? + blocks << { + type: "actions", + elements: [ + { + type: "button", + text: { type: "plain_text", text: "Claim", emoji: true }, + action_id: "support_claim", + value: ticket.id.to_s + }, + { + type: "button", + text: { type: "plain_text", text: "Mark Resolved", emoji: true }, + style: "primary", + action_id: "support_resolve", + value: ticket.id.to_s + } + ] + } + end + + blocks + end + + private + + def slack_client + @slack_client ||= Slack::Web::Client.new(token: ENV.fetch("SLACK_BOT_TOKEN", nil)) + end +end diff --git a/app/models/support_ticket.rb b/app/models/support_ticket.rb new file mode 100644 index 00000000..ef9cfa2d --- /dev/null +++ b/app/models/support_ticket.rb @@ -0,0 +1,19 @@ +class SupportTicket < ApplicationRecord + has_paper_trail + + enum :status, { open: 0, claimed: 1, resolved: 2 } + + validates :slack_user_id, :channel_id, :thread_ts, :bts_channel_id, :original_text, presence: true + validates :thread_ts, uniqueness: true + + scope :recent, -> { order(created_at: :desc) } + + def slack_thread_url + "https://hackclub.slack.com/archives/#{channel_id}/p#{thread_ts.to_s.delete('.')}" + end + + def bts_thread_url + return nil unless bts_message_ts.present? + "https://hackclub.slack.com/archives/#{bts_channel_id}/p#{bts_message_ts.to_s.delete('.')}" + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 25b032b0..b2a1f873 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -109,11 +109,12 @@ def staff? audit_log jobs third_party + support ].freeze ROLE_DEFAULT_PERMISSIONS = { "reviewer" => %w[pending_reviews projects ships], - "support" => %w[projects users], + "support" => %w[projects users support], "fulfillment" => %w[projects ships] }.freeze diff --git a/app/policies/support_ticket_policy.rb b/app/policies/support_ticket_policy.rb new file mode 100644 index 00000000..fe67a9a0 --- /dev/null +++ b/app/policies/support_ticket_policy.rb @@ -0,0 +1,11 @@ +class SupportTicketPolicy < ApplicationPolicy + def index? + user&.has_permission?("support") + end + + class Scope < ApplicationPolicy::Scope + def resolve + scope.all + end + end +end diff --git a/config/routes.rb b/config/routes.rb index f7737025..7b42b64d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -132,6 +132,7 @@ get "database" => "database#index", as: :database post "database/query" => "database#query" post "database/execute" => "database#execute" + resources :support_tickets, only: [ :index ], path: "support" end end @@ -140,6 +141,7 @@ end post "slack/events" => "slack/events#create" + post "slack/interactivity" => "slack/interactivity#create" get "up" => "rails/health#show", as: :rails_health_check diff --git a/db/migrate/20260407091606_create_support_tickets.rb b/db/migrate/20260407091606_create_support_tickets.rb new file mode 100644 index 00000000..1ee31d5a --- /dev/null +++ b/db/migrate/20260407091606_create_support_tickets.rb @@ -0,0 +1,32 @@ +class CreateSupportTickets < ActiveRecord::Migration[8.1] + def change + create_table :support_tickets do |t| + t.string :slack_user_id, null: false + t.string :slack_display_name + t.string :slack_avatar_url + + t.string :channel_id, null: false + t.string :thread_ts, null: false + + t.string :bts_channel_id, null: false + t.string :bts_message_ts + + t.text :original_text, null: false + + t.integer :status, default: 0, null: false + + t.string :claimed_by_slack_id + t.string :claimed_by_name + t.string :resolved_by_slack_id + t.string :resolved_by_name + t.datetime :claimed_at + t.datetime :resolved_at + + t.timestamps + end + + add_index :support_tickets, :thread_ts, unique: true + add_index :support_tickets, :bts_message_ts + add_index :support_tickets, :status + end +end From 8511a400b01368b1c2a64d770fc79e32d3dfe46f Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:32:07 +1000 Subject: [PATCH 019/118] Post resolved message to public thread when staff resolves from BTS --- app/controllers/slack/interactivity_controller.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/controllers/slack/interactivity_controller.rb b/app/controllers/slack/interactivity_controller.rb index e7033ecc..efe5b429 100644 --- a/app/controllers/slack/interactivity_controller.rb +++ b/app/controllers/slack/interactivity_controller.rb @@ -60,6 +60,7 @@ def handle_resolve(ticket_id, slack_user_id, display_name) resolved_at: Time.current ) + notify_public_resolved(ticket) update_bts_message(ticket) end @@ -85,6 +86,16 @@ def handle_mark_answered(ticket_id, slack_user_id, display_name) Rails.logger.error("Failed to post answered message: #{e.message}") end + def notify_public_resolved(ticket) + slack_client.chat_postMessage( + channel: ticket.channel_id, + thread_ts: ticket.thread_ts, + text: ":white_check_mark: This question has been marked as resolved!" + ) + rescue StandardError => e + Rails.logger.error("Failed to post resolved message: #{e.message}") + end + def update_bts_message(ticket) return unless ticket.bts_message_ts.present? From 1c7ab2d9115a4839db9307959bb080a80d397899 Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:35:50 +1000 Subject: [PATCH 020/118] Add support ticket detail page with reply, claim, and resolve from web --- .../admin/support_tickets_controller.rb | 148 +++++++++++++++++ .../pages/Admin/SupportTickets/Index.tsx | 10 +- .../pages/Admin/SupportTickets/Show.tsx | 151 ++++++++++++++++++ app/javascript/types/index.ts | 10 ++ app/policies/support_ticket_policy.rb | 16 ++ config/routes.rb | 8 +- 6 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 app/javascript/pages/Admin/SupportTickets/Show.tsx diff --git a/app/controllers/admin/support_tickets_controller.rb b/app/controllers/admin/support_tickets_controller.rb index f5d9c022..08f7c132 100644 --- a/app/controllers/admin/support_tickets_controller.rb +++ b/app/controllers/admin/support_tickets_controller.rb @@ -1,5 +1,6 @@ class Admin::SupportTicketsController < Admin::ApplicationController before_action :require_support_permission! + before_action :set_ticket, only: [ :show, :reply, :claim, :resolve ] def index scope = policy_scope(SupportTicket) @@ -20,12 +21,143 @@ def index } end + def show + authorize @ticket + + render inertia: "Admin/SupportTickets/Show", props: { + ticket: serialize_ticket_detail(@ticket), + messages: fetch_thread_messages(@ticket), + can: { + reply: policy(@ticket).reply?, + claim: policy(@ticket).claim?, + resolve: policy(@ticket).resolve? + } + } + end + + def reply + authorize @ticket + text = params[:message].to_s.strip + return redirect_to admin_support_ticket_path(@ticket), alert: "Message cannot be blank." if text.blank? + + slack_client.chat_postMessage( + channel: @ticket.channel_id, + thread_ts: @ticket.thread_ts, + text: text, + username: current_user.display_name, + icon_url: current_user.avatar + ) + + redirect_to admin_support_ticket_path(@ticket), notice: "Reply sent." + rescue StandardError => e + Rails.logger.error("Support reply failed: #{e.message}") + redirect_to admin_support_ticket_path(@ticket), alert: "Failed to send reply." + end + + def claim + authorize @ticket + return redirect_to admin_support_ticket_path(@ticket) if @ticket.resolved? + + @ticket.update!( + status: :claimed, + claimed_by_slack_id: current_user.slack_id, + claimed_by_name: current_user.display_name, + claimed_at: Time.current + ) + + update_bts_message(@ticket) + redirect_to admin_support_ticket_path(@ticket), notice: "Ticket claimed." + end + + def resolve + authorize @ticket + + @ticket.update!( + status: :resolved, + resolved_by_slack_id: current_user.slack_id, + resolved_by_name: current_user.display_name, + resolved_at: Time.current + ) + + slack_client.chat_postMessage( + channel: @ticket.channel_id, + thread_ts: @ticket.thread_ts, + text: ":white_check_mark: This question has been marked as resolved!" + ) + update_bts_message(@ticket) + redirect_to admin_support_ticket_path(@ticket), notice: "Ticket resolved." + rescue StandardError => e + Rails.logger.error("Support resolve failed: #{e.message}") + redirect_to admin_support_ticket_path(@ticket), alert: "Failed to resolve ticket." + end + private + def set_ticket + @ticket = SupportTicket.find(params[:id]) + end + def require_support_permission! require_permission!("support") end + def fetch_thread_messages(ticket) + result = slack_client.conversations_replies( + channel: ticket.channel_id, + ts: ticket.thread_ts, + limit: 100 + ) + + return [] unless result["ok"] + + result["messages"].map do |msg| + user_info = fetch_slack_profile(msg["user"]) + { + text: msg["text"], + user: msg["user"], + display_name: user_info[:display_name], + avatar_url: user_info[:avatar_url], + ts: msg["ts"], + is_bot: msg["bot_id"].present?, + created_at: Time.at(msg["ts"].to_f).strftime("%b %d, %Y %H:%M") + } + end + rescue StandardError => e + Rails.logger.error("Failed to fetch thread messages: #{e.message}") + [] + end + + def fetch_slack_profile(slack_user_id) + return { display_name: "Bot", avatar_url: nil } if slack_user_id.blank? + + result = slack_client.users_info(user: slack_user_id) + profile = result&.user&.profile + { + display_name: profile&.display_name.presence || profile&.real_name.presence || "Unknown", + avatar_url: profile&.image_72.presence + } + rescue StandardError + { display_name: "Unknown", avatar_url: nil } + end + + def update_bts_message(ticket) + return unless ticket.bts_message_ts.present? + + blocks = SupportTicketJob.bts_blocks(ticket) + slack_client.chat_update( + channel: ticket.bts_channel_id, + ts: ticket.bts_message_ts, + blocks: blocks, + text: "Support ticket from #{ticket.slack_display_name}" + ) + rescue StandardError => e + Rails.logger.error("Failed to update BTS message: #{e.message}") + end + + def slack_client + @slack_client ||= Slack::Web::Client.new(token: ENV.fetch("SLACK_BOT_TOKEN", nil)) + end + def serialize_ticket(ticket) { id: ticket.id, @@ -42,6 +174,22 @@ def serialize_ticket(ticket) } end + def serialize_ticket_detail(ticket) + { + id: ticket.id, + slack_display_name: ticket.slack_display_name, + slack_avatar_url: ticket.slack_avatar_url, + original_text: ticket.original_text, + status: ticket.status, + claimed_by_name: ticket.claimed_by_name, + resolved_by_name: ticket.resolved_by_name, + slack_thread_url: ticket.slack_thread_url, + bts_thread_url: ticket.bts_thread_url, + created_at: ticket.created_at.strftime("%b %d, %Y %H:%M"), + resolved_at: ticket.resolved_at&.strftime("%b %d, %Y %H:%M") + } + end + def build_leaderboard SupportTicket.resolved .where.not(resolved_by_name: nil) diff --git a/app/javascript/pages/Admin/SupportTickets/Index.tsx b/app/javascript/pages/Admin/SupportTickets/Index.tsx index 7bd63d65..d59edc4c 100644 --- a/app/javascript/pages/Admin/SupportTickets/Index.tsx +++ b/app/javascript/pages/Admin/SupportTickets/Index.tsx @@ -1,4 +1,4 @@ -import { router } from '@inertiajs/react' +import { router, Link } from '@inertiajs/react' import Pagination from '@/components/Pagination' import type { AdminSupportTicketRow, PagyProps, SupportLeaderboardEntry, SupportTicketStatus } from '@/types' @@ -77,11 +77,9 @@ export default function AdminSupportTicketsIndex({ Created
{tickets.map((ticket) => ( -
@@ -106,7 +104,7 @@ export default function AdminSupportTicketsIndex({ {ticket.claimed_by_name || ticket.resolved_by_name || '—'} {ticket.created_at} - + ))} {tickets.length === 0 && (

No tickets found.

diff --git a/app/javascript/pages/Admin/SupportTickets/Show.tsx b/app/javascript/pages/Admin/SupportTickets/Show.tsx new file mode 100644 index 00000000..a5fa90a2 --- /dev/null +++ b/app/javascript/pages/Admin/SupportTickets/Show.tsx @@ -0,0 +1,151 @@ +import { useState } from 'react' +import { Link, router } from '@inertiajs/react' +import type { AdminSupportTicketRow, SupportThreadMessage, SupportTicketStatus } from '@/types' + +const statusConfig: Record = { + open: { label: 'Open', bg: 'bg-amber-500/10', text: 'text-amber-400' }, + claimed: { label: 'Claimed', bg: 'bg-blue-500/10', text: 'text-blue-400' }, + resolved: { label: 'Resolved', bg: 'bg-emerald-500/10', text: 'text-emerald-400' }, +} + +export default function AdminSupportTicketsShow({ + ticket, + messages, + can, +}: { + ticket: AdminSupportTicketRow + messages: SupportThreadMessage[] + can: { reply: boolean; claim: boolean; resolve: boolean } +}) { + const [replyText, setReplyText] = useState('') + const [sending, setSending] = useState(false) + + function handleReply(e: React.FormEvent) { + e.preventDefault() + if (!replyText.trim() || sending) return + setSending(true) + router.post(`/admin/support/${ticket.id}/reply`, { message: replyText }, { + onFinish: () => { + setReplyText('') + setSending(false) + }, + }) + } + + const status = statusConfig[ticket.status] + + return ( +
+ + arrow_back + Back to Support + + +
+ {ticket.slack_avatar_url ? ( + {ticket.slack_display_name} + ) : ( +
+ {ticket.slack_display_name?.[0] || '?'} +
+ )} +
+
+

{ticket.slack_display_name}

+ + {status.label} + +
+

{ticket.created_at}

+ {ticket.claimed_by_name && ( +

Claimed by {ticket.claimed_by_name}

+ )} + {ticket.resolved_by_name && ( +

Resolved by {ticket.resolved_by_name} {ticket.resolved_at ? `at ${ticket.resolved_at}` : ''}

+ )} +
+
+ +
+ {ticket.slack_thread_url && ( + + open_in_new + View in Slack + + )} + {can.claim && !ticket.resolved_by_name && ticket.status !== 'claimed' && ( + + )} + {can.resolve && ticket.status !== 'resolved' && ( + + )} +
+ +
+ {messages.map((msg) => ( +
+
+ {msg.avatar_url ? ( + {msg.display_name} + ) : ( +
+ {msg.display_name?.[0] || '?'} +
+ )} +
+
+
+ {msg.display_name} + {msg.created_at} +
+

{msg.text}

+
+
+ ))} + {messages.length === 0 && ( +

No messages yet.

+ )} +
+ + {can.reply && ( +
+ setReplyText(e.target.value)} + placeholder="Type a reply..." + className="bg-[#0e0e0e] border-none px-4 py-3 text-sm text-[#e5e2e1] focus:ring-1 focus:ring-[#ee671c]/30 placeholder:text-stone-600 flex-1" + /> + +
+ )} +
+ ) +} diff --git a/app/javascript/types/index.ts b/app/javascript/types/index.ts index 2de1e664..959a7980 100644 --- a/app/javascript/types/index.ts +++ b/app/javascript/types/index.ts @@ -212,3 +212,13 @@ export interface SupportLeaderboardEntry { name: string count: number } + +export interface SupportThreadMessage { + text: string + user: string + display_name: string + avatar_url: string | null + ts: string + is_bot: boolean + created_at: string +} diff --git a/app/policies/support_ticket_policy.rb b/app/policies/support_ticket_policy.rb index fe67a9a0..64355755 100644 --- a/app/policies/support_ticket_policy.rb +++ b/app/policies/support_ticket_policy.rb @@ -3,6 +3,22 @@ def index? user&.has_permission?("support") end + def show? + user&.has_permission?("support") + end + + def reply? + user&.has_permission?("support") + end + + def claim? + user&.has_permission?("support") + end + + def resolve? + user&.has_permission?("support") + end + class Scope < ApplicationPolicy::Scope def resolve scope.all diff --git a/config/routes.rb b/config/routes.rb index 7b42b64d..63e6320b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -132,7 +132,13 @@ get "database" => "database#index", as: :database post "database/query" => "database#query" post "database/execute" => "database#execute" - resources :support_tickets, only: [ :index ], path: "support" + resources :support_tickets, only: [ :index, :show ], path: "support" do + member do + post :reply + post :claim + post :resolve + end + end end end From a29a3b6a853884858963d4467a359d12ca720e3b Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:45:26 +1000 Subject: [PATCH 021/118] React with checkmark on original message when ticket resolved --- app/controllers/admin/support_tickets_controller.rb | 5 +++++ app/controllers/slack/interactivity_controller.rb | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/app/controllers/admin/support_tickets_controller.rb b/app/controllers/admin/support_tickets_controller.rb index 08f7c132..f2484b2e 100644 --- a/app/controllers/admin/support_tickets_controller.rb +++ b/app/controllers/admin/support_tickets_controller.rb @@ -84,6 +84,11 @@ def resolve thread_ts: @ticket.thread_ts, text: ":white_check_mark: This question has been marked as resolved!" ) + slack_client.reactions_add( + channel: @ticket.channel_id, + timestamp: @ticket.thread_ts, + name: "white_check_mark" + ) update_bts_message(@ticket) redirect_to admin_support_ticket_path(@ticket), notice: "Ticket resolved." rescue StandardError => e diff --git a/app/controllers/slack/interactivity_controller.rb b/app/controllers/slack/interactivity_controller.rb index efe5b429..770b3870 100644 --- a/app/controllers/slack/interactivity_controller.rb +++ b/app/controllers/slack/interactivity_controller.rb @@ -92,6 +92,11 @@ def notify_public_resolved(ticket) thread_ts: ticket.thread_ts, text: ":white_check_mark: This question has been marked as resolved!" ) + slack_client.reactions_add( + channel: ticket.channel_id, + timestamp: ticket.thread_ts, + name: "white_check_mark" + ) rescue StandardError => e Rails.logger.error("Failed to post resolved message: #{e.message}") end From 24ab1c117c2457fa0dd601525d615590ee900e7b Mon Sep 17 00:00:00 2001 From: TheUnknownHacker <128781393+The-UnknownHacker@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:47:08 +1000 Subject: [PATCH 022/118] Allow admins to permanently delete support tickets --- app/controllers/admin/support_tickets_controller.rb | 11 +++++++++-- app/javascript/pages/Admin/SupportTickets/Show.tsx | 11 ++++++++++- app/policies/support_ticket_policy.rb | 4 ++++ config/routes.rb | 2 +- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/support_tickets_controller.rb b/app/controllers/admin/support_tickets_controller.rb index f2484b2e..d7284f7d 100644 --- a/app/controllers/admin/support_tickets_controller.rb +++ b/app/controllers/admin/support_tickets_controller.rb @@ -1,6 +1,6 @@ class Admin::SupportTicketsController < Admin::ApplicationController before_action :require_support_permission! - before_action :set_ticket, only: [ :show, :reply, :claim, :resolve ] + before_action :set_ticket, only: [ :show, :reply, :claim, :resolve, :destroy ] def index scope = policy_scope(SupportTicket) @@ -30,7 +30,8 @@ def show can: { reply: policy(@ticket).reply?, claim: policy(@ticket).claim?, - resolve: policy(@ticket).resolve? + resolve: policy(@ticket).resolve?, + destroy: policy(@ticket).destroy? } } end @@ -96,6 +97,12 @@ def resolve redirect_to admin_support_ticket_path(@ticket), alert: "Failed to resolve ticket." end + def destroy + authorize @ticket + @ticket.destroy + redirect_to admin_support_tickets_path, notice: "Ticket permanently deleted." + end + private def set_ticket diff --git a/app/javascript/pages/Admin/SupportTickets/Show.tsx b/app/javascript/pages/Admin/SupportTickets/Show.tsx index a5fa90a2..9c6b64d7 100644 --- a/app/javascript/pages/Admin/SupportTickets/Show.tsx +++ b/app/javascript/pages/Admin/SupportTickets/Show.tsx @@ -15,7 +15,7 @@ export default function AdminSupportTicketsShow({ }: { ticket: AdminSupportTicketRow messages: SupportThreadMessage[] - can: { reply: boolean; claim: boolean; resolve: boolean } + can: { reply: boolean; claim: boolean; resolve: boolean; destroy: boolean } }) { const [replyText, setReplyText] = useState('') const [sending, setSending] = useState(false) @@ -99,6 +99,15 @@ export default function AdminSupportTicketsShow({ Resolve )} + {can.destroy && ( + + )}
diff --git a/app/policies/support_ticket_policy.rb b/app/policies/support_ticket_policy.rb index 64355755..e29abc91 100644 --- a/app/policies/support_ticket_policy.rb +++ b/app/policies/support_ticket_policy.rb @@ -19,6 +19,10 @@ def resolve? user&.has_permission?("support") end + def destroy? + admin? + end + class Scope < ApplicationPolicy::Scope def resolve scope.all diff --git a/config/routes.rb b/config/routes.rb index 63e6320b..1ddbc7fb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -132,7 +132,7 @@ get "database" => "database#index", as: :database post "database/query" => "database#query" post "database/execute" => "database#execute" - resources :support_tickets, only: [ :index, :show ], path: "support" do + resources :support_tickets, only: [ :index, :show, :destroy ], path: "support" do member do post :reply post :claim From 0fb35c0207fa99ff43da38b7fe3e5f07c5c172b7 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 7 Apr 2026 20:03:04 +1000 Subject: [PATCH 023/118] remove db execute route; sets transactions as read only from admin/database ui --- app/controllers/admin/database_controller.rb | 23 ++---- app/javascript/pages/Admin/Database/Index.tsx | 74 +------------------ config/routes.rb | 1 - 3 files changed, 8 insertions(+), 90 deletions(-) diff --git a/app/controllers/admin/database_controller.rb b/app/controllers/admin/database_controller.rb index 02454dfa..4141a4ff 100644 --- a/app/controllers/admin/database_controller.rb +++ b/app/controllers/admin/database_controller.rb @@ -17,7 +17,13 @@ def query end begin - result = ActiveRecord::Base.connection.exec_query(sql) + result = nil + + ActiveRecord::Base.connection.transaction do + ActiveRecord::Base.connection.execute("SET TRANSACTION READ ONLY") + result = ActiveRecord::Base.connection.exec_query(sql) + end + render json: { columns: result.columns, rows: result.rows, @@ -27,19 +33,4 @@ def query render json: { error: e.message }, status: :unprocessable_entity end end - - def execute - sql = params[:sql].to_s.strip - - if sql.blank? - return render json: { error: "SQL statement cannot be blank" }, status: :unprocessable_entity - end - - begin - result = ActiveRecord::Base.connection.execute(sql) - render json: { message: "Executed successfully", affected_rows: result.cmd_tuples } - rescue => e - render json: { error: e.message }, status: :unprocessable_entity - end - end end diff --git a/app/javascript/pages/Admin/Database/Index.tsx b/app/javascript/pages/Admin/Database/Index.tsx index 619546a5..ba942fe5 100644 --- a/app/javascript/pages/Admin/Database/Index.tsx +++ b/app/javascript/pages/Admin/Database/Index.tsx @@ -1,4 +1,4 @@ -import { useState, useRef } from 'react' +import { useState } from 'react' interface QueryResult { columns: string[] @@ -6,11 +6,6 @@ interface QueryResult { row_count: number } -interface ExecuteResult { - message: string - affected_rows: number -} - interface ErrorResult { error: string } @@ -18,11 +13,9 @@ interface ErrorResult { export default function AdminDatabaseIndex({ tables }: { tables: string[] }) { const [sql, setSql] = useState('') const [result, setResult] = useState(null) - const [executeMsg, setExecuteMsg] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [selectedTable, setSelectedTable] = useState(null) - const textareaRef = useRef(null) const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '' @@ -32,7 +25,6 @@ export default function AdminDatabaseIndex({ tables }: { tables: string[] }) { setLoading(true) setError(null) - setExecuteMsg(null) setResult(null) try { @@ -54,34 +46,6 @@ export default function AdminDatabaseIndex({ tables }: { tables: string[] }) { } } - async function runExecute() { - if (!sql.trim()) return - if (!confirm('This will execute a write operation. Continue?')) return - - setLoading(true) - setError(null) - setExecuteMsg(null) - setResult(null) - - try { - const res = await fetch('/admin/database/execute', { - method: 'POST', - headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, - body: JSON.stringify({ sql }), - }) - const data: ExecuteResult | ErrorResult = await res.json() - if ('error' in data) { - setError(data.error) - } else { - setExecuteMsg(`${data.message} (${data.affected_rows} rows affected)`) - } - } catch (e) { - setError('Request failed') - } finally { - setLoading(false) - } - } - function selectTable(table: string) { setSelectedTable(table) const query = `SELECT * FROM ${table} LIMIT 50` @@ -89,13 +53,6 @@ export default function AdminDatabaseIndex({ tables }: { tables: string[] }) { runQuery(query) } - function deleteRow(table: string, primaryKey: string, id: string | number | boolean | null) { - if (!confirm(`Delete row with ${primaryKey} = ${id} from ${table}?`)) return - const query = `DELETE FROM ${table} WHERE ${primaryKey} = ${typeof id === 'string' ? `'${id.replace(/'/g, "''")}'` : id}` - setSql(query) - runExecute() - } - function handleKeyDown(e: React.KeyboardEvent) { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') { e.preventDefault() @@ -103,8 +60,6 @@ export default function AdminDatabaseIndex({ tables }: { tables: string[] }) { } } - const idColumnIndex = result?.columns.findIndex(c => c === 'id') ?? -1 - return (
@@ -134,7 +89,6 @@ export default function AdminDatabaseIndex({ tables }: { tables: string[] }) {