diff --git a/app/Http/Controllers/Admin/DownloadProductController.php b/app/Http/Controllers/Admin/DownloadProductController.php
new file mode 100644
index 0000000..e8e7701
--- /dev/null
+++ b/app/Http/Controllers/Admin/DownloadProductController.php
@@ -0,0 +1,25 @@
+DownloadProduct::latest()->get()]); }
+ public function store(Request $request) {
+ $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'required|file|max:512000','is_published'=>'sometimes|boolean']);
+ $file=$request->file('package');
+ $path=$file->store('download-products','local');
+ DownloadProduct::create(['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'is_published'=>$request->boolean('is_published')]);
+ return back()->with('status','Product added.');
+ }
+ public function update(Request $request, DownloadProduct $product) {
+ $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'nullable|file|max:512000','is_published'=>'sometimes|boolean']);
+ $changes=['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'is_published'=>$request->boolean('is_published')];
+ if ($request->hasFile('package')) { $file=$request->file('package'); $path=$file->store('download-products','local'); $old=$product->file_path; $changes+=['file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize()]; $product->update($changes); if($old) Storage::disk('local')->delete($old); }
+ else $product->update($changes);
+ return back()->with('status','Product updated.');
+ }
+ public function destroy(DownloadProduct $product) { if($product->file_path) Storage::disk('local')->delete($product->file_path); $product->delete(); return back()->with('status','Product deleted.'); }
+}
diff --git a/app/Http/Controllers/ReleaseController.php b/app/Http/Controllers/ReleaseController.php
index f8246af..b961246 100644
--- a/app/Http/Controllers/ReleaseController.php
+++ b/app/Http/Controllers/ReleaseController.php
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\SiteSetting;
+use App\Models\DownloadProduct;
use App\Models\SonarDeskRelease;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
@@ -16,8 +17,9 @@ final class ReleaseController extends Controller
?? SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->orderByDesc('id')->first();
$releases = SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);
$settings = $this->settings();
+ $products = DownloadProduct::where('is_published', true)->whereNotNull('file_path')->orderBy('name')->get();
- return view('downloads.index', compact('latest', 'releases', 'settings'));
+ return view('downloads.index', compact('latest', 'releases', 'settings', 'products'));
}
public function latest(): BinaryFileResponse
@@ -32,6 +34,13 @@ final class ReleaseController extends Controller
return $this->send($release);
}
+ public function productDownload(DownloadProduct $product): BinaryFileResponse
+ {
+ abort_unless($product->is_published && $product->file_path && Storage::disk('local')->exists($product->file_path), 404);
+ $product->increment('download_count');
+ return response()->download(Storage::disk('local')->path($product->file_path), $product->original_filename);
+ }
+
public function appcast(): Response
{
$releases = SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->limit(20)->get();
diff --git a/app/Models/DownloadProduct.php b/app/Models/DownloadProduct.php
new file mode 100644
index 0000000..431156c
--- /dev/null
+++ b/app/Models/DownloadProduct.php
@@ -0,0 +1,8 @@
+'boolean','file_size'=>'integer','download_count'=>'integer']; }
+ public function getFormattedSizeAttribute(): string { return $this->file_size >= 1048576 ? number_format($this->file_size/1048576,1).' MB' : number_format($this->file_size/1024,1).' KB'; }
+}
diff --git a/database/migrations/2026_10_12_100000_create_download_products_table.php b/database/migrations/2026_10_12_100000_create_download_products_table.php
new file mode 100644
index 0000000..8368227
--- /dev/null
+++ b/database/migrations/2026_10_12_100000_create_download_products_table.php
@@ -0,0 +1,18 @@
+id(); $table->string('name'); $table->text('description')->nullable();
+ $table->string('version')->nullable(); $table->string('platform')->nullable();
+ $table->string('file_path')->nullable(); $table->string('original_filename')->nullable();
+ $table->unsignedBigInteger('file_size')->default(0); $table->unsignedBigInteger('download_count')->default(0);
+ $table->boolean('is_published')->default(false); $table->timestamps();
+ });
+ }
+ public function down(): void { Schema::dropIfExists('download_products'); }
+};
diff --git a/resources/views/admin/download-products/index.blade.php b/resources/views/admin/download-products/index.blade.php
new file mode 100644
index 0000000..3ab2973
--- /dev/null
+++ b/resources/views/admin/download-products/index.blade.php
@@ -0,0 +1,12 @@
+@extends('layouts.app', ['title' => 'Download products — Admin'])
+@section('content')
+
+@if(session('status'))
@endif
+@if($errors->any())@foreach($errors->all() as $error)- {{ $error }}
@endforeach
@endif
+
+@foreach($products as $product)
+
+@endforeach
+@endsection
diff --git a/resources/views/admin/releases/index.blade.php b/resources/views/admin/releases/index.blade.php
index fa07a4b..a8caa71 100644
--- a/resources/views/admin/releases/index.blade.php
+++ b/resources/views/admin/releases/index.blade.php
@@ -1,6 +1,6 @@
@extends('layouts.app', ['title' => 'Releases — SonarDesk Admin'])
@section('content')
-
+
{{ $releases->total() }}Total releases
{{ $publishedCount }}Published
{{ number_format($downloadCount) }}Total downloads
SonarDesk builds
Only published builds with an uploaded package are available publicly.
View public page
{{ $releases->links() }}
diff --git a/resources/views/downloads/index.blade.php b/resources/views/downloads/index.blade.php
index 0d66597..166b5b5 100644
--- a/resources/views/downloads/index.blade.php
+++ b/resources/views/downloads/index.blade.php
@@ -3,6 +3,9 @@
{{ $settings['eyebrow'] }}{{ $settings['title'] }}
{{ $settings['intro'] }}
@if($latest)
✓ Signed distribution✓ SHA-256 verified✓ Secure licence activation
@else
The next SonarDesk release is being prepared.Check back shortly or contact support.
@endif
SonarDesk
Project healthAll systems monitored
NativeBuilt for macOS
Native macOSFast, focused desktop workflow
SonarQubeYour server remains the source of truth
SecurityTokens protected by macOS Keychain
UpdatesProfessional in-app release delivery
@if($latest)Latest stable releaseSonarDesk {{ $latest->version }}
Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}
Download {{ $latest->version }}{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}
SHA-256{{ $latest->sha256 }}
@endif
+@if($products->isNotEmpty())
+More downloadsOther products
Explore our other available software and downloads.
@foreach($products as $product)
{{ $product->name }}@if($product->version) {{ $product->version }}@endif{{ $product->description }}@if($product->platform){{ $product->platform }}@endif
@endforeach
+@endif
Release archivePrevious versions
Published SonarDesk builds remain available here for customers who need a previous release.
@forelse($releases as $release)
SonarDesk {{ $release->version }}Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}
@empty
No published releases are available yet.
@endforelse
{{ $releases->links() }}
@endsection
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
index 99d2e8f..32426c2 100755
--- a/resources/views/layouts/app.blade.php
+++ b/resources/views/layouts/app.blade.php
@@ -231,6 +231,7 @@
Admin
Users
Releases
+ Download products
Support
@if ($supportUnread > 0)
diff --git a/routes/web.php b/routes/web.php
index a78dee0..f1ae5c0 100755
--- a/routes/web.php
+++ b/routes/web.php
@@ -13,6 +13,7 @@ use App\Http\Controllers\Auth\EmailVerificationController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\ReleaseController;
+use App\Http\Controllers\Admin\DownloadProductController;
use App\Http\Controllers\Support\SupportController;
use App\Http\Middleware\AdminOnly;
use Illuminate\Support\Facades\Route;
@@ -25,6 +26,7 @@ Route::get('/', fn () => view('portal.welcome', ['latestRelease' => \App\Models\
Route::view('/features', 'features')->name('features');
Route::get('/release', [ReleaseController::class, 'index'])->name('release');
Route::get('/download', [ReleaseController::class, 'latest'])->name('download.latest');
+Route::get('/downloads/products/{product}', [ReleaseController::class, 'productDownload'])->name('download.product');
Route::get('/downloads/{release}', [ReleaseController::class, 'download'])->name('download.release');
Route::get('/updates/appcast.xml', [ReleaseController::class, 'appcast'])->name('updates.appcast');
Route::get('/support', [SupportController::class, 'create'])->name('support');
@@ -74,6 +76,10 @@ Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name
Route::get('/cms/{page}/edit', [AdminCmsPageController::class, 'edit'])->name('cms.edit');
Route::put('/cms/{page}', [AdminCmsPageController::class, 'update'])->name('cms.update');
Route::delete('/cms/{page}', [AdminCmsPageController::class, 'destroy'])->name('cms.destroy');
+ Route::get('/download-products', [DownloadProductController::class, 'index'])->name('download-products.index');
+ Route::post('/download-products', [DownloadProductController::class, 'store'])->name('download-products.store');
+ Route::put('/download-products/{product}', [DownloadProductController::class, 'update'])->name('download-products.update');
+ Route::delete('/download-products/{product}', [DownloadProductController::class, 'destroy'])->name('download-products.destroy');
Route::get('/releases', [AdminReleaseController::class, 'index'])->name('releases.index');
Route::get('/releases/create', [AdminReleaseController::class, 'create'])->name('releases.create');
Route::post('/releases', [AdminReleaseController::class, 'store'])->name('releases.store');