Compare commits

...
7 Commits
Author SHA1 Message Date
Mark Gregory 04c1f9fb26 move into sonardesk 2026-10-12 16:35:14 +01:00
Mark Gregory 663e311724 move into sonardesk 2026-10-12 16:29:40 +01:00
Mark Gregory dd1f144908 move into sonardesk 2026-10-12 16:23:27 +01:00
Mark Gregory 27af94d8ca move into sonardesk 2026-10-12 16:06:40 +01:00
Mark Gregory 7ed8c6ef58 move into sonardesk 2026-10-12 15:59:55 +01:00
Mark Gregory 27b4e53caf move into sonardesk 2026-10-12 15:55:14 +01:00
Mark Gregory cd1d3186f0 Sonardesk updates 2026-10-12 14:52:53 +00:00
24 changed files with 507 additions and 53 deletions
@@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
if (!Schema::hasTable('download_products')) return;
foreach (['release_notes' => 'longText', 'minimum_system' => 'text', 'sort_order' => 'integer'] as $column => $type) {
if (!Schema::hasColumn('download_products', $column)) {
Schema::table('download_products', function (Blueprint $table) use ($column, $type) {
$definition = $table->{$type}($column);
if ($column === 'sort_order') $definition->default(0); else $definition->nullable();
});
}
}
}
public function down(): void { /* Preserve existing download metadata. */ }
};
@@ -0,0 +1,15 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
if (!Schema::hasTable('download_products')) return;
foreach (['release_notes'=>'longText','minimum_system'=>'text','sort_order'=>'integer'] as $column=>$type) {
if (!Schema::hasColumn('download_products',$column)) Schema::table('download_products',function(Blueprint $table) use ($column,$type) {
$c=$table->{$type}($column); if ($column==='sort_order') $c->default(0); else $c->nullable();
});
}
}
public function down(): void { /* Keep uploaded download metadata intact. */ }
};
@@ -6,8 +6,16 @@ use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\Rule;
use Modules\Licensing\Models\{SonarDeskRelease,DownloadProduct};
final class DistributionAdminController extends Controller {
public function index(){return view('licensing::distribution.admin', ['releases'=>SonarDeskRelease::orderByDesc('created_at')->paginate(20),'products'=>DownloadProduct::orderBy('name')->get()]);}
public function release(Request $request){$data=$request->validate(['version'=>'required|string|max:40','build_number'=>'required|integer|min:1','channel'=>['required',Rule::in(['stable','beta'])],'minimum_macos'=>'nullable|string|max:30','release_notes'=>'nullable|string','sparkle_signature'=>'nullable|string|max:2048','is_published'=>'nullable|boolean','is_featured'=>'nullable|boolean','file'=>'required|file|max:512000|mimes:dmg,zip,pkg']);$file=$request->file('file');$path=$file->store('sonardesk-distribution/releases','local');$release=SonarDeskRelease::create(['version'=>$data['version'],'build_number'=>$data['build_number'],'channel'=>$data['channel'],'minimum_macos'=>$data['minimum_macos']??null,'release_notes'=>$data['release_notes']??null,'sparkle_signature'=>$data['sparkle_signature']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'sha256'=>hash_file('sha256',$file->getRealPath()),'is_published'=>(bool)($data['is_published']??false),'is_featured'=>(bool)($data['is_featured']??false),'published_at'=>!empty($data['is_published'])?now():null]);return redirect()->route('administration.sonardesk.distribution')->with('status','Release '.$release->version.' created');}
public function product(Request $request){$data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string','version'=>'nullable|string|max:60','platform'=>'nullable|string|max:60','is_published'=>'nullable|boolean','file'=>'required|file|max:512000']);$file=$request->file('file');$path=$file->store('sonardesk-distribution/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'=>(bool)($data['is_published']??false)]);return redirect()->route('administration.sonardesk.distribution')->with('status','Download created');}
public function index(){return view('licensing::distribution.admin',['releases'=>SonarDeskRelease::orderByDesc('created_at')->paginate(20),'products'=>DownloadProduct::orderBy('name')->orderByDesc('created_at')->get()]);}
private function upload($file,string $folder):array{$path=$file->store('sonardesk-distribution/'.$folder,'local');return ['file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize()];}
private function remove(?string $path):void {if($path && str_starts_with($path,'sonardesk-distribution/') && !str_contains($path,'..'))Storage::disk('local')->delete($path);}
private function productRules(bool $new):array{return ['name'=>'required|string|max:150','description'=>'nullable|string','version'=>'nullable|string|max:60','platform'=>'nullable|string|max:60','release_notes'=>'nullable|string','minimum_system'=>'nullable|string|max:2000','sort_order'=>'nullable|integer|between:-10000,10000','is_published'=>'nullable|boolean','file'=>[$new?'required':'nullable','file','max:512000']];}
private function releaseRules(bool $new):array{return ['version'=>'required|string|max:40','build_number'=>'required|integer|min:1','channel'=>['required',Rule::in(['stable','beta'])],'minimum_macos'=>'nullable|string|max:30','release_notes'=>'nullable|string','sparkle_signature'=>'nullable|string|max:2048','is_published'=>'nullable|boolean','is_featured'=>'nullable|boolean','file'=>[$new?'required':'nullable','file','max:512000','mimes:dmg,zip,pkg']];}
public function product(Request $request){$data=$request->validate($this->productRules(true));$file=$request->file('file');$attributes=$this->upload($file,'products');unset($data['file']);DownloadProduct::create(array_merge($data,$attributes,['is_published'=>$request->boolean('is_published'),'sort_order'=>(int)($data['sort_order']??0)]));return back()->with('status','Download added');}
public function updateProduct(Request $request,DownloadProduct $product){$data=$request->validate($this->productRules(false));unset($data['file']);$data['is_published']=$request->boolean('is_published');$data['sort_order']=(int)($data['sort_order']??0);if($request->hasFile('file')){$old=$product->file_path;$data=array_merge($data,$this->upload($request->file('file'),'products'));$product->update($data);$this->remove($old);}else{$product->update($data);}return back()->with('status','Download updated');}
public function deleteProduct(DownloadProduct $product){$path=$product->file_path;$product->delete();$this->remove($path);return back()->with('status','Download removed');}
public function release(Request $request){$data=$request->validate($this->releaseRules(true));$file=$request->file('file');unset($data['file']);$release=SonarDeskRelease::create(array_merge($data,$this->upload($file,'releases'),['sha256'=>hash_file('sha256',$file->getRealPath()),'is_published'=>$request->boolean('is_published'),'is_featured'=>$request->boolean('is_featured'),'published_at'=>$request->boolean('is_published')?now():null]));return back()->with('status','Release '.$release->version.' created');}
public function updateRelease(Request $request,SonarDeskRelease $release){$data=$request->validate($this->releaseRules(false));unset($data['file']);$data['is_published']=$request->boolean('is_published');$data['is_featured']=$request->boolean('is_featured');$data['published_at']=$data['is_published']?($release->published_at??now()):null;if($request->hasFile('file')){$file=$request->file('file');$old=$release->file_path;$data=array_merge($data,$this->upload($file,'releases'),['sha256'=>hash_file('sha256',$file->getRealPath())]);$release->update($data);$this->remove($old);}else{$release->update($data);}return back()->with('status','Release updated');}
public function deleteRelease(SonarDeskRelease $release){$path=$release->file_path;$release->delete();$this->remove($path);return back()->with('status','Release removed');}
public function publish(Request $request,SonarDeskRelease $release){$data=$request->validate(['is_published'=>'required|boolean']);$release->update(['is_published'=>(bool)$data['is_published'],'published_at'=>$data['is_published']?($release->published_at??now()):null]);return back()->with('status','Release status updated');}
}
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Storage;
use Modules\Licensing\Models\{SonarDeskRelease,DownloadProduct};
use Symfony\Component\HttpFoundation\BinaryFileResponse;
final class DistributionController extends Controller {
public function index(){ $latest=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->first(); $releases=SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);$products=DownloadProduct::where('is_published',true)->whereNotNull('file_path')->orderBy('name')->get();return view('licensing::downloads.index',compact('latest','releases','products')); }
public function index(){ $latest=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->first(); $releases=SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12);$products=DownloadProduct::where('is_published',true)->whereNotNull('file_path')->orderBy('name')->orderBy('sort_order')->orderByDesc('created_at')->get();return view('licensing::downloads.index',compact('latest','releases','products')); }
public function release(SonarDeskRelease $release){abort_unless($release->is_published && $release->published_at && $release->file_path,404);return view('licensing::downloads.release',compact('release'));}
public function product(DownloadProduct $product){abort_unless($product->is_published && $product->file_path,404);return view('licensing::downloads.product',compact('product'));}
public function latest():BinaryFileResponse{$r=SonarDeskRelease::published()->where('channel','stable')->orderByDesc('published_at')->firstOrFail();return $this->send($r);}
+2 -2
View File
@@ -3,6 +3,6 @@ namespace Modules\Licensing\Models;
use Illuminate\Database\Eloquent\Model;
final class DownloadProduct extends Model {
protected $table='download_products';
protected $fillable=['name','description','version','platform','file_path','original_filename','file_size','is_published'];
protected function casts():array{return ['is_published'=>'boolean','file_size'=>'integer'];}
protected $fillable=['name','description','version','platform','file_path','original_filename','file_size','is_published','release_notes','minimum_system','sort_order'];
protected function casts():array{return ['is_published'=>'boolean','file_size'=>'integer','sort_order'=>'integer'];}
}
@@ -3,13 +3,16 @@
<div class="space-y-6">
<div class="flex flex-wrap items-center justify-between gap-4 border-b border-zinc-200 pb-5 dark:border-zinc-700">
<div>
<p class="text-xs font-semibold uppercase tracking-widest text-amber-600 dark:text-amber-400">ABSDev / Products</p>
<p class="text-xs font-semibold uppercase tracking-widest text-orange-600 dark:text-orange-400">ABSDev / Products</p>
<h1 class="mt-1 text-3xl font-bold tracking-tight text-zinc-900 dark:text-white">SonarDesk</h1>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Licensing administration · Read only</p>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Manage licensing, releases and product downloads</p>
</div>
<div class="flex flex-wrap gap-2 text-sm">
<a class="rounded-lg border border-zinc-300 px-4 py-2 hover:bg-zinc-100 dark:border-zinc-600 dark:hover:bg-zinc-800" href="{{ route('administration.sonardesk.index') }}">Overview</a>
<a class="rounded-lg border border-zinc-300 px-4 py-2 hover:bg-zinc-100 dark:border-zinc-600 dark:hover:bg-zinc-800" href="{{ route('administration.sonardesk.licenses') }}">Licences</a>
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.distribution'))
<a class="rounded-lg border border-orange-500 bg-orange-500 px-4 py-2 font-semibold text-white hover:bg-orange-600" href="{{ route('administration.sonardesk.distribution') }}">Downloads &amp; releases</a>
@endif
</div>
</div>
<div class="sonardesk-admin space-y-6">@yield('content')</div>
@@ -17,11 +20,11 @@
<style>
.sonardesk-admin h1{font-size:1.65rem;font-weight:700;margin-bottom:1.25rem}
.sonardesk-admin h2{font-size:1.125rem;font-weight:650;margin:1.5rem 0 .85rem}
.sonardesk-admin a{color:#b7791f;text-decoration:underline;text-underline-offset:3px}
.sonardesk-admin a{color:#f97316;text-decoration:underline;text-underline-offset:3px}
.sonardesk-admin .cards,.sonardesk-admin .detail{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:1rem;margin-bottom:1.5rem}
.sonardesk-admin .card{border:1px solid #d4d4d8;border-radius:.85rem;padding:1.4rem;background:rgba(127,127,127,.035)}
.dark .sonardesk-admin .card{border-color:#52525b}
.sonardesk-admin .metric{font-size:2rem;font-weight:750;color:#b7791f}
.sonardesk-admin .metric{font-size:2rem;font-weight:750;color:#f97316}
.sonardesk-admin .muted,.sonardesk-admin dt{color:#71717a}
.sonardesk-admin dd{margin:.25rem 0 .8rem;overflow-wrap:anywhere}
.sonardesk-admin .table-wrap{width:100%;overflow-x:auto;border:1px solid #d4d4d8;border-radius:.85rem}
@@ -1,15 +1,3 @@
@extends('licensing::customer.layout')
@section('title', 'SonarDesk · Purchase')
@section('content')
<section class="card">
<h1>SonarDesk</h1>
<p>Native SonarQube project monitoring for macOS.</p>
<p>Choose your licence on our secure checkout service. Your subscription and licence are processed by the existing SonarDesk licensing platform.</p>
@if($enabled)
<p><a href="{{ route('sonardesk.checkout') }}" style="display:inline-block;background:#163b70;color:#fff;padding:12px 20px;border-radius:8px;text-decoration:none">View plans &amp; buy</a></p>
@else
<p role="status">Online checkout is not yet enabled on this site. No payment will be taken here.</p>
@endif
<p><a href="{{ route('sonardesk.downloads') }}">Downloads &amp; release notes</a> · <a href="{{ route('account.sonardesk.index') }}">My licences</a></p>
</section>
@endsection
@extends('sonardesk.layout')
@section('title','SonarDesk — Purchase')
@section('content')<section class="sd-pagehead"><div class="sd-container"><span class="sd-eyebrow">SonarDesk licensing</span><h1>Get SonarDesk</h1><p>Choose your licence using the existing secure SonarDesk checkout service.</p></div></section><section class="sd-section"><div class="sd-container sd-detail"><div class="sd-card"><h2>Purchase a licence</h2><p>Your subscription, billing and licence are managed by the established SonarDesk service.</p>@if($enabled)<a class="sd-btn sd-btn-dark" href="{{ route('sonardesk.checkout') }}">View plans &amp; buy →</a>@else<p class="sd-note">Online checkout is not enabled on this site yet. No payment will be taken here.</p>@endif<div class="sd-card-actions"><a class="sd-textlink" href="{{ route('sonardesk.downloads') }}">Downloads &amp; release notes →</a><a class="sd-textlink" href="{{ route('account.sonardesk.index') }}">My licences →</a></div></div></div></section>@endsection
@@ -1,9 +1,12 @@
@extends('licensing::admin.layout')
@section('title','Downloads & releases')
@section('content')
<h1>Downloads &amp; releases</h1>@if(session('status'))<p>{{ session('status') }}</p>@endif
<h2>Publish SonarDesk release</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.store') }}">@csrf
<p><input name="version" placeholder="Version (e.g. 1.2.0)" required> <input name="build_number" type="number" min="1" placeholder="Build" required> <select name="channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></p>
<p><input name="minimum_macos" placeholder="Minimum macOS version"><input type="file" name="file" accept=".dmg,.zip,.pkg" required></p><p><textarea name="release_notes" placeholder="Full release notes" rows="5" cols="70"></textarea></p><p><input name="sparkle_signature" placeholder="Sparkle EdDSA signature (optional)" size="65"></p><label><input type="checkbox" name="is_published" value="1"> Publish</label> <button type="submit">Create release</button></form>
<h2>Releases</h2><table><thead><tr><th>Version</th><th>Channel</th><th>Status</th><th>Downloads</th><th>Action</th></tr></thead><tbody>@foreach($releases as $r)<tr><td>{{ $r->version }} ({{ $r->build_number }})</td><td>{{ $r->channel }}</td><td>{{ $r->is_published ? 'Published' : 'Draft' }}</td><td>{{ $r->download_count }}</td><td><form method="POST" action="{{ route('administration.sonardesk.distribution.releases.publish',$r) }}">@csrf @method('PATCH')<input type="hidden" name="is_published" value="{{ $r->is_published ? 0 : 1 }}"><button>{{ $r->is_published ? 'Unpublish' : 'Publish' }}</button></form></td></tr>@endforeach</tbody></table>{{ $releases->links() }}
<h2>Add other download (e.g. SonarDisk)</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.store') }}">@csrf<p><input name="name" placeholder="Product name" required><input name="version" placeholder="Version"><input name="platform" placeholder="Platform"></p><p><textarea name="description" placeholder="Description"></textarea></p><p><input name="file" type="file" required></p><label><input type="checkbox" name="is_published" value="1"> Publish</label><button>Add download</button></form><h2>Other downloads</h2><ul>@foreach($products as $p)<li>{{ $p->name }} {{ $p->version }} — {{ $p->download_count }} downloads ({{ $p->is_published ? 'published' : 'draft' }})</li>@endforeach</ul>
@endsection
<div class="sd-admin"><div class="sd-admin-head"><div><p class="sd-eyebrow">PRODUCT DISTRIBUTION</p><h1>Downloads &amp; releases</h1><p>Manage SonarDesk releases, SonarDisk and alternative installers from one place.</p></div><a href="{{ route('sonardesk.downloads') }}" target="_blank" rel="noopener" class="sd-link">View public downloads ↗</a></div>
@if(session('status'))<div class="sd-notice">{{ session('status') }}</div>@endif
@if($errors->any())<div class="sd-errors"><strong>Unable to save:</strong><ul>@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
<div class="sd-two"><section class="sd-panel"><h2>Add SonarDesk release</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.store') }}">@csrf<div class="sd-grid"><label>Version<input name="version" required placeholder="1.2.0" value="{{ old('version') }}"></label><label>Build<input name="build_number" type="number" min="1" required value="{{ old('build_number') }}"></label><label>Channel<select name="channel"><option value="stable">Stable</option><option value="beta">Beta</option></select></label><label>Minimum macOS<input name="minimum_macos" placeholder="14.0"></label></div><label>Full release notes<textarea name="release_notes" rows="4"></textarea></label><label>Sparkle signature (optional)<input name="sparkle_signature"></label><label>Installer (.dmg, .zip, .pkg; up to 500 MB)<input type="file" name="file" required accept=".dmg,.zip,.pkg"></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1"> Publish now</label><button>Create release</button></div></form></section>
<section class="sd-panel"><h2>Add product / alternative download</h2><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.store') }}">@csrf<div class="sd-grid"><label>Product name<input name="name" required placeholder="SonarDisk"></label><label>Version<input name="version" placeholder="1.0"></label><label>Platform<input name="platform" placeholder="macOS · Apple Silicon"></label><label>Display order<input name="sort_order" type="number" value="0"></label></div><label>Short description<textarea name="description" rows="2"></textarea></label><label>Full release notes<textarea name="release_notes" rows="3"></textarea></label><label>System requirements<input name="minimum_system" placeholder="macOS 14 or later"></label><label>Download file (up to 500 MB)<input name="file" type="file" required></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1"> Publish now</label><button>Add download</button></div></form></section></div>
<section class="sd-panel"><h2>Manage products and alternative downloads <small>({{ $products->count() }})</small></h2><p class="sd-help">Add multiple files under the same product name to group them on the public page. Expand an entry to edit, replace its file or unpublish it.</p>@forelse($products as $p)<details class="sd-entry"><summary><strong>{{ $p->name }}</strong><span>{{ $p->version ?: 'Unversioned' }} · {{ $p->platform ?: 'Any platform' }}</span><span class="sd-status {{ $p->is_published ? 'live' : '' }}">{{ $p->is_published ? 'Published' : 'Draft' }}</span><span>{{ number_format($p->download_count) }} downloads</span></summary><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.products.update',$p) }}">@csrf @method('PATCH')<div class="sd-grid"><label>Name<input name="name" value="{{ $p->name }}" required></label><label>Version<input name="version" value="{{ $p->version }}"></label><label>Platform<input name="platform" value="{{ $p->platform }}"></label><label>Order<input type="number" name="sort_order" value="{{ $p->sort_order ?? 0 }}"></label></div><label>Description<textarea name="description" rows="2">{{ $p->description }}</textarea></label><label>Release notes<textarea name="release_notes" rows="4">{{ $p->release_notes }}</textarea></label><label>System requirements<input name="minimum_system" value="{{ $p->minimum_system }}"></label><label>Replace file (optional) — currently {{ $p->original_filename }}<input type="file" name="file"></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1" @checked($p->is_published)> Published</label><button>Save changes</button><a href="{{ route('sonardesk.products.show',$p) }}" target="_blank" rel="noopener">Public details ↗</a></div></form><form method="POST" action="{{ route('administration.sonardesk.distribution.products.delete',$p) }}" onsubmit="return confirm('Permanently delete this download and its file?')">@csrf @method('DELETE')<button class="sd-danger">Delete download</button></form></details>@empty<p>No additional downloads yet. Add SonarDisk or an alternative installer above.</p>@endforelse</section>
<section class="sd-panel"><h2>Manage SonarDesk releases</h2>@forelse($releases as $r)<details class="sd-entry"><summary><strong>SonarDesk {{ $r->version }}</strong><span>Build {{ $r->build_number }} · {{ ucfirst($r->channel) }}</span><span class="sd-status {{ $r->is_published ? 'live' : '' }}">{{ $r->is_published ? 'Published' : 'Draft' }}</span><span>{{ number_format($r->download_count) }} downloads</span></summary><form method="POST" enctype="multipart/form-data" action="{{ route('administration.sonardesk.distribution.releases.update',$r) }}">@csrf @method('PATCH')<div class="sd-grid"><label>Version<input name="version" required value="{{ $r->version }}"></label><label>Build<input type="number" min="1" name="build_number" required value="{{ $r->build_number }}"></label><label>Channel<select name="channel"><option value="stable" @selected($r->channel==='stable')>Stable</option><option value="beta" @selected($r->channel==='beta')>Beta</option></select></label><label>Minimum macOS<input name="minimum_macos" value="{{ $r->minimum_macos }}"></label></div><label>Release notes<textarea name="release_notes" rows="5">{{ $r->release_notes }}</textarea></label><label>Sparkle signature<input name="sparkle_signature" value="{{ $r->sparkle_signature }}"></label><label>Replace installer (optional) — {{ $r->original_filename }}<input type="file" name="file" accept=".dmg,.zip,.pkg"></label><div class="sd-actions"><label class="sd-check"><input type="checkbox" name="is_published" value="1" @checked($r->is_published)> Published</label><label class="sd-check"><input type="checkbox" name="is_featured" value="1" @checked($r->is_featured)> Featured</label><button>Save release</button></div></form><form method="POST" action="{{ route('administration.sonardesk.distribution.releases.delete',$r) }}" onsubmit="return confirm('Permanently delete this release and its installer?')">@csrf @method('DELETE')<button class="sd-danger">Delete release</button></form></details>@empty<p>No SonarDesk releases yet.</p>@endforelse<div class="sd-pager">{{ $releases->links() }}</div></section></div>
<style>.sd-admin{max-width:1480px;margin:auto}.sd-admin-head{display:flex;justify-content:space-between;gap:20px;align-items:center;margin-bottom:25px}.sd-admin-head h1{font-size:2rem;margin:4px 0}.sd-admin-head p{color:#71717a}.sd-eyebrow{font-size:11px;letter-spacing:.18em;color:#f97316!important;font-weight:800}.sd-two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.sd-panel{border:1px solid #a1a1aa55;border-radius:16px;padding:25px;margin-bottom:20px;background:rgba(127,127,127,.035)}.sd-panel h2{font-size:1.25rem;margin:0 0 20px}.sd-panel label{display:block;font-size:13px;font-weight:650;margin-bottom:14px}.sd-panel input:not([type=checkbox]),.sd-panel select,.sd-panel textarea{display:block;width:100%;margin-top:6px;padding:10px 12px;border:1px solid #a1a1aa88;border-radius:8px;background:transparent;color:inherit}.sd-panel select option{color:#111}.sd-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 14px}.sd-actions{display:flex;gap:15px;align-items:center;flex-wrap:wrap}.sd-panel .sd-check{display:flex;align-items:center;gap:8px;margin:0}.sd-panel button:not(.sd-danger){background:#f97316;color:#fff;border:0;padding:10px 18px;border-radius:8px;font-weight:750;cursor:pointer}.sd-panel .sd-danger{background:transparent;border:1px solid #dc2626;color:#dc2626;padding:8px 12px;border-radius:8px;margin-top:12px;cursor:pointer}.sd-entry{border-top:1px solid #a1a1aa55;padding:15px 0}.sd-entry summary{cursor:pointer;display:flex;gap:20px;align-items:center;flex-wrap:wrap}.sd-entry summary strong{min-width:170px}.sd-entry summary span{font-size:13px;color:#71717a}.sd-entry[open] summary{margin-bottom:20px}.sd-status{padding:4px 9px;border-radius:30px;background:#a1a1aa33}.sd-status.live{background:#16a34a22!important;color:#16a34a!important}.sd-notice,.sd-errors{padding:14px;border-radius:8px;margin-bottom:18px;background:#16a34a22}.sd-errors{background:#dc262622}.sd-help{color:#71717a;margin-bottom:18px}.sd-link{white-space:nowrap}.sd-pager{margin-top:20px}@media(max-width:900px){.sd-two{grid-template-columns:1fr}}@media(max-width:540px){.sd-grid{grid-template-columns:1fr}.sd-admin-head{display:block}}</style>
@endsection
@@ -1 +1,6 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SonarDesk Downloads</title><style>body{font:16px system-ui;background:#f6f8fc;color:#17243c;margin:0}.wrap{max-width:1120px;margin:48px auto;padding:0 24px}h1{font-size:42px;margin-bottom:6px}.card{background:white;border:1px solid #dce3ee;border-radius:14px;padding:22px;margin:18px 0}a.button{display:inline-block;background:#183b73;color:white;padding:10px 18px;border-radius:8px;text-decoration:none}small{color:#58657a}ul{line-height:2}</style></head><body><main class="wrap"><h1>SonarDesk Downloads</h1><p>Current applications and release history.</p>@if($latest)<section class="card"><h2>SonarDesk <small>{{ $latest->version }}</small></h2><p>{{ \Illuminate\Support\Str::limit(strip_tags($latest->release_notes ?? 'Native SonarQube dashboard for macOS.'),190) }}</p><a class="button" href="{{ route('sonardesk.releases.download',$latest) }}">Download for macOS</a> &nbsp;<a href="{{ route('sonardesk.releases.show',$latest) }}">Release notes &amp; requirements</a></section>@endif @if($products->isNotEmpty())<section class="card"><h2>Other downloads</h2><ul>@foreach($products as $product)<li><strong>{{ $product->name }}</strong> {{ $product->version }} ({{ $product->platform }}) — <a href="{{ route('sonardesk.products.download',$product) }}">Download</a> · <a href="{{ route('sonardesk.products.show',$product) }}">Details</a></li>@endforeach</ul></section>@endif<section class="card"><h2>Previous releases</h2><ul>@forelse($releases as $release)<li><strong>{{ $release->version }}</strong> — {{ $release->channel }} · <a href="{{ route('sonardesk.releases.show',$release) }}">Notes</a> · <a href="{{ route('sonardesk.releases.download',$release) }}">Download</a></li>@empty<li>No releases published.</li>@endforelse</ul>{{ $releases->links() }}</section></main></body></html>
@extends('sonardesk.layout')
@section('title','Software downloads and releases')
@section('content')<div class="product-shell"><header class="page-heading"><span class="eyebrow">ABSDEV Software Library</span><h1>Downloads &amp; releases.</h1><p>Find SonarDesk, SonarDisk and alternative installers, grouped by product. Release notes and system requirements are on the detail pages.</p></header>
@if($latest)<section class="download-group"><span class="eyebrow">Latest stable · SonarDesk</span><div class="download-group-head"><div><h2>SonarDesk {{ $latest->version }}</h2><p>{{ \Illuminate\Support\Str::limit(strip_tags($latest->release_notes ?: 'Native SonarQube monitoring for macOS.'), 180) }}</p></div><span class="badge">Latest release</span></div><div class="actions"><a class="button" href="{{ route('sonardesk.releases.download',$latest) }}">Download installer ↓</a><a class="button outline" href="{{ route('sonardesk.releases.show',$latest) }}">Full release notes →</a></div></section>@endif
@if($products->isNotEmpty())<section class="product-section" style="padding-top:40px"><div class="section-heading"><span class="eyebrow">Product catalogue</span><h2>Products &amp; alternative downloads</h2><p>Each product lists its available versions and platforms together.</p></div>@foreach($products->groupBy('name') as $name=>$files)<article class="download-group"><div class="download-group-head"><div><h2>{{ $name }}</h2><p>{{ $files->first()->description ?: 'Installers and supporting downloads.' }}</p></div><span class="badge">{{ $files->count() }} {{ $files->count() === 1 ? 'file' : 'files' }}</span></div>@foreach($files as $file)<div class="download-row"><div><strong>{{ $file->version ? 'Version '.$file->version : ($file->original_filename ?: 'Download') }}</strong><small>{{ $file->platform ?: 'All platforms' }} @if($file->file_size) · {{ number_format($file->file_size/1048576,1) }} MB @endif</small><p>{{ \Illuminate\Support\Str::limit(strip_tags($file->release_notes ?: $file->description ?: ''),120) }}</p></div><div class="row-actions"><a class="text-link" href="{{ route('sonardesk.products.show',$file) }}">Details →</a><a class="button small" href="{{ route('sonardesk.products.download',$file) }}">Download ↓</a></div></div>@endforeach</article>@endforeach</section>@endif
<section class="product-section" style="padding-top:20px"><div class="section-heading"><span class="eyebrow">Release archive</span><h2>SonarDesk release history</h2></div><div class="download-group">@forelse($releases as $release)<div class="download-row"><div><strong>SonarDesk {{ $release->version }}</strong><small>Build {{ $release->build_number }} · {{ ucfirst($release->channel) }}</small><p>{{ \Illuminate\Support\Str::limit(strip_tags($release->release_notes ?: 'Release information'),125) }}</p></div><div class="row-actions"><a class="text-link" href="{{ route('sonardesk.releases.show',$release) }}">Release notes →</a><a class="button small" href="{{ route('sonardesk.releases.download',$release) }}">Download ↓</a></div></div>@empty<p>No SonarDesk releases have been published yet.</p>@endforelse</div><div class="pagination">{{ $releases->links() }}</div></section></div>@endsection
@@ -1 +1,3 @@
<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>{{ $product->name }}</title></head><body style="font:16px system-ui;max-width:900px;margin:50px auto;padding:20px"><a href="{{ route('sonardesk.downloads') }}">← All downloads</a><h1>{{ $product->name }}</h1><p>Version {{ $product->version }} · {{ $product->platform }}</p><div style="white-space:pre-wrap">{{ $product->description }}</div><p><a href="{{ route('sonardesk.products.download',$product) }}">Download {{ $product->original_filename }}</a></p></body></html>
@extends('sonardesk.layout')
@section('title',$product->name.' download')
@section('content')<div class="product-shell"><header class="page-heading"><a class="text-link" href="{{ route('sonardesk.downloads') }}">← All downloads</a><p class="eyebrow">Product download</p><h1>{{ $product->name }}</h1><p>{{ $product->description }}</p></header><article class="detail-panel"><span class="badge">{{ $product->platform ?: 'All platforms' }}</span><p class="muted">Version {{ $product->version ?: 'Unspecified' }}</p><a class="button" href="{{ route('sonardesk.products.download',$product) }}">Download {{ $product->original_filename }} ↓</a>@if($product->release_notes)<h2>Full release notes</h2><div class="preformatted">{{ $product->release_notes }}</div>@endif@if($product->minimum_system)<h2>System requirements</h2><div class="preformatted">{{ $product->minimum_system }}</div>@endif</article></div>@endsection
@@ -1 +1,3 @@
<!doctype html><html><head><meta name="viewport" content="width=device-width,initial-scale=1"><title>SonarDesk {{ $release->version }}</title></head><body style="font:16px system-ui;max-width:900px;margin:50px auto;padding:20px"><a href="{{ route('sonardesk.downloads') }}">← All downloads</a><h1>SonarDesk {{ $release->version }}</h1><p>Build {{ $release->build_number }} · {{ $release->channel }}</p><h2>Release notes</h2><div style="white-space:pre-wrap">{{ $release->release_notes ?: 'No release notes provided.' }}</div><h2>System requirements</h2><p>macOS {{ $release->minimum_macos ?: 'See application documentation' }} or later.</p><p>SHA-256: <code>{{ $release->sha256 }}</code></p><a href="{{ route('sonardesk.releases.download',$release) }}">Download {{ $release->original_filename }}</a></body></html>
@extends('sonardesk.layout')
@section('title','SonarDesk '.$release->version.' release notes')
@section('content')<div class="product-shell"><header class="page-heading"><a class="text-link" href="{{ route('sonardesk.downloads') }}">← All downloads</a><p class="eyebrow">SonarDesk · {{ strtoupper($release->channel) }}</p><h1>SonarDesk {{ $release->version }}</h1><p>Build {{ $release->build_number }}</p></header><article class="detail-panel"><a class="button" href="{{ route('sonardesk.releases.download',$release) }}">Download {{ $release->original_filename }} ↓</a><h2>Full release notes</h2><div class="preformatted">{{ $release->release_notes ?: 'No release notes provided.' }}</div><h2>System requirements</h2><p>macOS {{ $release->minimum_macos ?: 'See application documentation' }} or later.</p>@if($release->sha256)<h2>File verification · SHA-256</h2><code class="hash">{{ $release->sha256 }}</code>@endif</article></div>@endsection
@@ -15,5 +15,9 @@ Route::middleware(['web','auth','verified','can:manage-sonardesk-licensing'])->p
Route::get('/distribution',[AdminDownloads::class,'index'])->name('distribution');
Route::post('/distribution/releases',[AdminDownloads::class,'release'])->name('distribution.releases.store');
Route::post('/distribution/products',[AdminDownloads::class,'product'])->name('distribution.products.store');
Route::patch('/distribution/products/{product}',[AdminDownloads::class,'updateProduct'])->name('distribution.products.update');
Route::delete('/distribution/products/{product}',[AdminDownloads::class,'deleteProduct'])->name('distribution.products.delete');
Route::patch('/distribution/releases/{release}',[AdminDownloads::class,'updateRelease'])->name('distribution.releases.update');
Route::delete('/distribution/releases/{release}',[AdminDownloads::class,'deleteRelease'])->name('distribution.releases.delete');
Route::patch('/distribution/releases/{release}/publish',[AdminDownloads::class,'publish'])->name('distribution.releases.publish');
});
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
*{box-sizing:border-box}.sd-site{margin:0;background:#0f0e0d;color:#f4f7fd;font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.sd-site a{color:inherit;text-decoration:none}.sd-top,.sd-footer{display:flex;justify-content:space-between;align-items:center;gap:20px;padding:22px max(5vw,24px);border-bottom:1px solid #ffffff1e}.sd-top nav{display:flex;gap:26px;flex-wrap:wrap;font-size:14px;color:#d7cbc2}.sd-top nav a:hover,.sd-top nav .selected{color:#f97316}.sd-brand{font-size:24px;font-weight:850;letter-spacing:-1px}.sd-brand span,.sd-gold{color:#f97316}.sd-main{width:min(1420px,calc(100% - 64px));margin:auto;padding:50px 0 95px}.sd-intro{max-width:900px;margin-bottom:46px}.sd-kicker{color:#f97316;font-size:11px;font-weight:800;letter-spacing:.19em}.sd-site h1{font-size:clamp(42px,5.6vw,72px);line-height:1.07;letter-spacing:-.045em;margin:13px 0 19px}.sd-intro p,.sd-lead{font-size:19px;color:#b9c8dc;max-width:740px}.sd-feature{display:grid;grid-template-columns:1.3fr 1fr;gap:40px;align-items:center;background:linear-gradient(125deg,#162f4d,#0e2239);border:1px solid #ffffff24;border-radius:18px;padding:clamp(28px,4vw,54px);margin-bottom:80px}.sd-feature h2{font-size:clamp(32px,4vw,52px);margin:12px 0}.sd-feature h2 span{color:#f97316}.sd-feature p,.sd-product p,.sd-section-title p{color:#b7c6d9}.sd-feature-art{min-height:230px;display:flex;flex-direction:column;justify-content:center;align-items:center;background:radial-gradient(circle at 50% 50%,#294c73,#11243d 70%);border:1px solid #ffffff20;border-radius:14px}.sd-feature-art>span{font-size:clamp(34px,4vw,58px);font-weight:850;letter-spacing:-.05em}.sd-feature-art small{color:#aabbd0}.sd-buttons{display:flex;flex-wrap:wrap;gap:12px;margin-top:25px}.sd-btn{display:inline-flex;align-items:center;justify-content:center;background:#f97316;color:#13233a!important;padding:12px 20px;border-radius:9px;font-weight:800;font-size:14px;white-space:nowrap}.sd-btn:hover{background:#f5cb77}.sd-btn.ghost{background:transparent;color:#f5f7fb!important;border:1px solid #ffffff60}.sd-btn.small{padding:9px 15px;font-size:13px}.sd-section{margin-top:65px}.sd-section-title{margin-bottom:28px}.sd-section-title h2{font-size:clamp(27px,3vw,39px);letter-spacing:-.03em;margin:8px 0}.sd-product{border:1px solid #ffffff25;border-radius:15px;background:#ffffff08;margin:15px 0;overflow:hidden}.sd-product-head{padding:25px 30px;display:flex;justify-content:space-between;align-items:center;gap:18px}.sd-product-head h3{font-size:25px;margin:0}.sd-product-head p{margin:6px 0 0}.sd-count{color:#f97316;font-size:13px;white-space:nowrap}.sd-file{padding:19px 30px;border-top:1px solid #ffffff1c;display:flex;align-items:center;justify-content:space-between;gap:20px}.sd-file strong{display:block;font-size:16px}.sd-file small{display:block;color:#a9bad1;margin-top:3px}.sd-file p{color:#a9bad1;font-size:13px;margin:6px 0 0}.sd-file-actions{display:flex;align-items:center;gap:20px;white-space:nowrap;font-size:14px}.sd-file-actions>a:not(.sd-btn){color:#f97316}.sd-history{border:1px solid #ffffff25;border-radius:15px;overflow:hidden}.sd-history .sd-file:first-child{border-top:0}.sd-pagination{margin-top:25px}.sd-detail{max-width:1000px;min-height:65vh}.sd-back{display:block;color:#f97316!important;margin-bottom:55px}.sd-meta{display:flex;gap:12px;flex-wrap:wrap;margin:25px 0}.sd-meta span{padding:6px 12px;background:#ffffff15;border-radius:30px}.sd-detail-section{margin-top:55px;padding-top:25px;border-top:1px solid #ffffff24}.sd-detail-section h2{font-size:26px}.sd-pre{white-space:pre-wrap;overflow-wrap:anywhere;color:#cad7e8}.sd-hash{overflow-wrap:anywhere;color:#aebfd5}.sd-footer{border-top:1px solid #ffffff1e;border-bottom:0;color:#a7b9cf;font-size:13px}@media(max-width:760px){.sd-main{width:calc(100% - 36px);padding-top:36px}.sd-feature{grid-template-columns:1fr}.sd-feature-art{min-height:150px}.sd-file{padding:17px;align-items:flex-start;flex-direction:column}.sd-product-head{padding:20px;align-items:flex-start;flex-direction:column}.sd-top{align-items:flex-start;flex-direction:column}.sd-top nav{gap:16px}}
/* SaaSland orange product downloads */
.sd-site{background:#0f0e0d;color:#f7f3ef}.sd-top,.sd-footer{background:#0f0e0d}.sd-top nav a:hover,.sd-top nav .selected,.sd-brand span,.sd-gold{color:#f97316}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -56,6 +56,9 @@
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.paddle-reconciliation'))
<flux:sidebar.item icon="arrow-path" :href="route('administration.sonardesk.paddle-reconciliation')" :current="request()->routeIs('administration.sonardesk.paddle-reconciliation')">{{ __('Paddle Reconciliation') }}</flux:sidebar.item>
@endif
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.distribution'))
<flux:sidebar.item icon="arrow-down-tray" :href="route('administration.sonardesk.distribution')" :current="request()->routeIs('administration.sonardesk.distribution*')">{{ __('Downloads & Releases') }}</flux:sidebar.item>
@endif
@if (\Illuminate\Support\Facades\Route::has('administration.sonardesk.subscriptions'))
<flux:sidebar.item icon="credit-card" :href="route('administration.sonardesk.subscriptions')" :current="request()->routeIs('administration.sonardesk.subscriptions')">{{ __('Subscriptions') }}</flux:sidebar.item>
@endif
@@ -23,4 +23,11 @@
: 'Streamline operations, boost productivity, and improve customer satisfaction with one connected platform built for modern teams.',
])
@endif
<section style="padding:78px 0;background:linear-gradient(120deg,#0f0e0d,#302219);color:#faf7f3">
<div class="wrap" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:36px;align-items:center">
<div><span style="color:#f97316;font-size:12px;font-weight:800;letter-spacing:.17em">ABSDEV SOFTWARE</span><h2 style="font-size:clamp(36px,4vw,55px);line-height:1.12;margin:15px 0">Meet <span style="color:#f97316">SonarDesk</span></h2><p style="color:#d7cbc2;font-size:18px;max-width:650px">Native SonarQube monitoring for macOS. Quality gates, vulnerabilities, bugs and issue details — without the browser clutter.</p><div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:26px"><a href="{{ url('/sonardesk') }}" style="padding:12px 20px;border-radius:8px;background:#f97316;color:#191714;font-weight:750">Explore SonarDesk →</a><a href="{{ route('sonardesk.downloads') }}" style="padding:12px 20px;border-radius:8px;border:1px solid #ffffff77;color:white;font-weight:750">Downloads ↓</a></div></div>
<div style="border:1px solid #ffffff28;border-radius:18px;background:#ffffff0c;padding:30px"><p style="color:#f97316;font-weight:750">SonarDesk for macOS</p><h3 style="font-size:25px;margin:10px 0">Know your code health.</h3><p style="color:#d7cbc2">One focused workspace for project quality, issues and server connections.</p><a href="{{ url('/sonardesk/features') }}" style="color:#f97316;font-weight:700">Explore features →</a></div>
</div>
</section>
@endsection
@@ -11,13 +11,23 @@
@if ($logoUrl)<img src="{{ $logoUrl }}" alt="{{ $siteTitle }}">@else{{ $siteTitle }}@endif
</a>
@if (filter_var($headerContent['show_navigation'] ?? true, FILTER_VALIDATE_BOOL) && $navigationLinks)
@if (filter_var($headerContent['show_navigation'] ?? true, FILTER_VALIDATE_BOOL))
<nav class="desktop-nav" aria-label="Primary navigation">
@foreach ($navigationLinks as $link)
<a href="{{ $link['url'] }}" @if($link['target']) target="{{ $link['target'] }}" rel="noopener" @endif>{{ $link['label'] }}</a>
@continue(in_array(strtolower(trim($link['label'])), ['products','sonardesk'], true))
<a href="{{ $link['url'] }}" @if($link['target'] ?? null) target="{{ $link['target'] }}" rel="noopener" @endif>{{ $link['label'] }}</a>
@endforeach
<div class="abs-products-menu"><a href="{{ url('/sonardesk') }}" aria-haspopup="true">Products <span aria-hidden="true">⌄</span></a><div class="abs-products-dropdown"><a href="{{ url('/sonardesk') }}">SonarDesk overview</a><a href="{{ url('/sonardesk/features') }}">Features</a><a href="{{ url('/sonardesk/pricing') }}">Pricing</a><a href="{{ route('sonardesk.downloads') }}">Downloads &amp; release notes</a></div></div>
</nav>
@endif
<style>
.saasland-theme .abs-products-menu{position:relative;display:flex;align-items:center;height:100%}
.saasland-theme .abs-products-menu>a{white-space:nowrap}
.saasland-theme .abs-products-dropdown{display:none;position:absolute;top:100%;right:0;min-width:235px;padding:10px;background:#241c16;border:1px solid #ffffff33;border-radius:12px;box-shadow:0 16px 36px #0005;z-index:100}
.saasland-theme .abs-products-menu:hover .abs-products-dropdown,.saasland-theme .abs-products-menu:focus-within .abs-products-dropdown{display:grid}
.saasland-theme .abs-products-dropdown a{padding:10px 12px;color:#f0f4fa;border-radius:6px;font-size:14px}
.saasland-theme .abs-products-dropdown a:hover{background:#ffffff16;color:#f97316}
</style>
<div class="nav-actions">
@auth
@@ -32,21 +42,19 @@
@if (filter_var($headerContent['show_registration'] ?? true, FILTER_VALIDATE_BOOL) && Route::has('register'))<a class="btn primary nav-register" href="{{ route('register') }}">{{ $siteSettings['registration_label'] }}</a>@endif
@endauth
@if (filter_var($headerContent['show_mobile_menu'] ?? true, FILTER_VALIDATE_BOOL))
<button
class="menu"
type="button"
aria-label="Open navigation"
aria-controls="{{ $mobileMenuId }}"
aria-expanded="false"
data-saasland-menu-toggle
><span></span><span></span><span></span></button>
<button class="menu" type="button" aria-label="Open navigation" aria-controls="{{ $mobileMenuId }}" aria-expanded="false" data-saasland-menu-toggle><span></span><span></span><span></span></button>
@endif
</div>
</header>
@if (filter_var($headerContent['show_mobile_menu'] ?? true, FILTER_VALIDATE_BOOL))
<div id="{{ $mobileMenuId }}" class="mobile-nav" data-saasland-mobile-nav hidden>
@foreach($navigationLinks as $link)
<a href="{{ $link['url'] }}" @if($link['target']) target="{{ $link['target'] }}" rel="noopener" @endif>{{ $link['label'] }}</a>
@continue(in_array(strtolower(trim($link['label'])), ['products','sonardesk'], true))
<a href="{{ $link['url'] }}" @if($link['target'] ?? null) target="{{ $link['target'] }}" rel="noopener" @endif>{{ $link['label'] }}</a>
@endforeach
<a href="{{ url('/sonardesk') }}">Products · SonarDesk</a>
<a href="{{ url('/sonardesk/features') }}">SonarDesk features</a>
<a href="{{ url('/sonardesk/pricing') }}">SonarDesk pricing</a>
<a href="{{ route('sonardesk.downloads') }}">Downloads &amp; releases</a>
</div>
@endif
+14 -2
View File
@@ -1,3 +1,15 @@
@extends('sonardesk.layout')
@section('title','Features')
@section('content')<section class="hero"><h1>Focus on what matters.</h1><p class="lead">SonarDesk brings SonarQube project health and issue exploration to a native macOS workspace.</p></section><div class="grid"><div class="card"><h2>Quality gates</h2><p>Understand project health quickly.</p></div><div class="card"><h2>Issue details</h2><p>Explore bugs, vulnerabilities and code smells.</p></div><div class="card"><h2>Multiple projects</h2><p>Organise and switch between your projects.</p></div></div>@endsection
@section('content')
<section class="hero-card marketing-hero compact-hero"><span class="eyebrow">Features</span><h1>A focused desktop companion for SonarQube.</h1><p class="lead">Built around the checks developers make every day: project status, quality gates, issues, security findings and measures.</p><div class="support-actions marketing-actions"><a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">Start free trial</a><a class="btn btn-secondary" href="{{ route('sonardesk.downloads') }}">Read the release notes</a></div></section>
<div class="feature-detail-grid">
<article class="feature-detail"><span>Projects</span><h2>Project dashboard</h2><p>Keep configured SonarQube projects together and scan project health without opening a collection of browser tabs.</p></article>
<article class="feature-detail"><span>Quality</span><h2>Quality gates and measures</h2><p>Surface the status and metrics that tell you whether a project needs attention, in a layout designed for quick review.</p></article>
<article class="feature-detail"><span>Issues</span><h2>Bugs, vulnerabilities and code smells</h2><p>Browse the major SonarQube issue categories, filter the list and open an issue to inspect its details and context.</p></article>
<article class="feature-detail"><span>Scale</span><h2>Server-side pagination</h2><p>Issue browsing is designed to remain practical on projects with larger result sets by paging data from the server.</p></article>
<article class="feature-detail"><span>Connections</span><h2>Multiple SonarQube servers</h2><p>Configure the environments you work with and keep them available from the same SonarDesk workspace.</p></article>
<article class="feature-detail"><span>Security</span><h2>macOS Keychain</h2><p>SonarQube tokens are stored using macOS Keychain rather than being treated as ordinary application preferences.</p></article>
<article class="feature-detail"><span>Licensing</span><h2>Email delivery and self-service portal</h2><p>After checkout, the licence key is emailed to the customer. The SonarDesk portal can resend the key and manage licence information and Mac activations.</p></article>
<article class="feature-detail"><span>Distribution</span><h2>Built for direct Mac distribution</h2><p>SonarDesk is distributed directly for macOS, giving the product a focused release and support channel outside the Mac App Store.</p></article>
</div>
<section class="cta-panel"><div><span class="eyebrow">7-day free trial</span><h2>Try the complete app.</h2><p>Choose Personal for one active Mac or Professional for up to three.</p></div><a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">View pricing</a></section>
@endsection
+46 -2
View File
@@ -1,3 +1,47 @@
@extends('sonardesk.layout')
@section('title','SonarDesk for macOS')
@section('content')<section class="hero"><p style="color:var(--gold);font-weight:700">NATIVE MACOS · SONARQUBE</p><h1>Code quality, without the clutter.</h1><p class="lead">Monitor projects, inspect issues and follow quality gates from a purpose-built macOS dashboard.</p><a class="button" href="/features">Explore features</a><a class="button secondary" href="/pricing">View pricing</a></section><div class="grid"><article class="card"><h2>At a glance</h2><p>Keep quality gates, bugs, vulnerabilities and code smells in view.</p></article><article class="card"><h2>Project focused</h2><p>Switch between projects and investigate issues with fewer distractions.</p></article><article class="card"><h2>Built for macOS</h2><p>A native interface designed for your Mac.</p></article></div>@endsection
@section('content')
<section class="hero-card marketing-hero">
<span class="eyebrow">SonarQube, without the browser tab sprawl</span>
<h1>Your code quality.<br>At home on the Mac.</h1>
<p class="lead">SonarDesk is a focused native macOS companion for developers and teams using SonarQube. See project health, quality gates, bugs, vulnerabilities, code smells and measures in one fast desktop workspace.</p>
<div class="support-actions marketing-actions">
<a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">Start 7-day free trial</a>
<a class="btn btn-secondary" href="{{ $base }}/features">Explore features</a>
</div>
<p class="microcopy">Direct download for macOS · Secure licence activation · Cancel anytime</p>
</section>
<section class="trust-strip" aria-label="SonarDesk highlights">
<span>Native macOS app</span><span>Multiple SonarQube servers</span><span>Keychain token storage</span><span>Issue detail workflow</span>
</section>
<section class="section-block">
<div class="section-heading"><span class="eyebrow">Built for daily engineering work</span><h2>See the signal. Open the issue. Get back to code.</h2><p>SonarDesk keeps the information you check most often close at hand, without replacing SonarQube or changing your existing workflow.</p></div>
<div class="feature-grid">
<article class="feature-card"><div class="feature-icon">01</div><h3>Project health at a glance</h3><p>Review configured projects, quality gate state and important measures from a clean Mac dashboard.</p></article>
<article class="feature-card"><div class="feature-icon">02</div><h3>Issue details that are useful</h3><p>Open bugs, vulnerabilities and code smells to see the context you need before switching back to the codebase.</p></article>
<article class="feature-card"><div class="feature-icon">03</div><h3>Focused filtering</h3><p>Work through issue lists with practical filters and server-side pagination rather than fighting a giant browser view.</p></article>
<article class="feature-card"><div class="feature-icon">04</div><h3>Multiple servers</h3><p>Keep the SonarQube environments you work with available from one desktop application.</p></article>
<article class="feature-card"><div class="feature-icon">05</div><h3>macOS security</h3><p>SonarQube access tokens are stored in the macOS Keychain. SonarDesk respects the permissions of the token you provide.</p></article>
<article class="feature-card"><div class="feature-icon">06</div><h3>Native workflow</h3><p>A purpose-built macOS interface with familiar navigation, desktop behaviour and a focused experience for engineering teams.</p></article>
</div>
</section>
<section class="section-block split-panel">
<div><span class="eyebrow">Designed to complement SonarQube</span><h2>Keep SonarQube as the source of truth.</h2><p class="lead compact">SonarDesk is a desktop companion, not a replacement. It connects to your SonarQube server using your credentials and presents the project and issue information you need for everyday monitoring.</p></div>
<div class="check-panel"><div>Quality gates and measures</div><div>Bugs, vulnerabilities and code smells</div><div>Project and issue navigation</div><div>Secure token storage in Keychain</div><div>Customer portal for licences and activations</div></div>
</section>
<section class="section-block pricing-preview">
<div class="section-heading"><span class="eyebrow">Straightforward pricing</span><h2>Start small. Upgrade when you need more Macs.</h2><p>Every plan includes the full SonarDesk application. Professional increases the activation allowance for people working across multiple Macs.</p></div>
<div class="pricing-preview-grid">
<article class="price-card"><span class="eyebrow">Personal</span><h3>For one Mac</h3><div class="marketing-price">£24.99 <small>/ year</small></div><p>or £2.99 monthly</p><ul class="features"><li>Full SonarDesk feature set</li><li>1 active Mac</li><li>7-day free trial</li><li>Licence key delivered by email</li><li>Customer licence portal</li></ul></article>
<article class="price-card featured"><span class="pill">Most popular</span><span class="eyebrow">Professional</span><h3>For a multi-Mac workflow</h3><div class="marketing-price">£44.99 <small>/ year</small></div><p>or £4.99 monthly</p><ul class="features"><li>Full SonarDesk feature set</li><li>Up to 3 active Macs</li><li>7-day free trial</li><li>Licence key delivered by email</li><li>Customer licence portal</li></ul></article>
</div>
<div class="center-action"><a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">See pricing and start trial</a></div>
</section>
<section class="home-download-showcase"><div class="home-download-copy"><span class="eyebrow">Desktop distribution</span><h2>Ready when you are.</h2><p>Download the latest signed SonarDesk release for macOS, review what changed, verify the package checksum and browse previous versions from one dedicated release centre.</p><div class="support-actions">@if(false)<a class="btn btn-primary" href="{{ route('sonardesk.latest') }}">Download SonarDesk </a>@else<a class="btn btn-primary" href="{{ route('sonardesk.downloads') }}">Visit download centre</a>@endif<a class="btn btn-secondary" href="{{ route('sonardesk.downloads') }}">Release notes</a></div></div><div class="home-download-meta">@if(false)<span>Latest stable</span><strong>Version </strong><small>Build · · macOS </small><div><b>SHA-256</b><code></code></div>@else<span>SonarDesk releases</span><strong>Distribution centre</strong><small>Published builds will appear here automatically.</small>@endif</div></section>
<section class="cta-panel"><div><span class="eyebrow">SonarDesk for macOS</span><h2>Make SonarQube easier to keep an eye on.</h2><p>Try SonarDesk for seven days and connect it to your existing SonarQube environment.</p></div><a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">Start free trial</a></section>
@endsection
+7 -1
View File
@@ -1 +1,7 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>@yield('title', 'SonarDesk') · ABSDev</title><meta name="description" content="SonarDesk, a native macOS dashboard for SonarQube."><style>:root{color-scheme:dark;--navy:#0d1c36;--gold:#e5b95b;--ink:#eef3fb}*{box-sizing:border-box}body{margin:0;background:linear-gradient(135deg,#0d1c36,#172d4e);color:var(--ink);font:16px/1.6 system-ui,-apple-system,sans-serif}a{color:inherit}header,main,footer{max-width:1200px;margin:auto;padding:24px 32px}header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #ffffff26}nav{display:flex;gap:24px;flex-wrap:wrap}.brand{font-weight:800;font-size:24px;letter-spacing:-.5px}.brand span{color:var(--gold)}.hero{padding:96px 0 64px;max-width:850px}h1{font-size:clamp(42px,6vw,76px);line-height:1.08;letter-spacing:-2px;margin:0 0 24px}h2{font-size:32px}.lead{font-size:21px;color:#c5d2e5}.button{display:inline-block;background:var(--gold);color:#0d1c36;padding:14px 25px;border-radius:9px;text-decoration:none;font-weight:750;margin:12px 12px 12px 0}.button.secondary{background:#ffffff16;color:white;border:1px solid #ffffff4d}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(245px,1fr));gap:20px;margin:36px 0 80px}.card{padding:28px;border:1px solid #ffffff28;background:#ffffff0b;border-radius:14px}footer{border-top:1px solid #ffffff26;color:#a9b8ce}@media(max-width:650px){header{display:block}nav{margin-top:16px}.hero{padding:56px 0}}</style></head><body><header><a class="brand" href="/">Sonar<span>Desk</span></a><nav><a href="/features">Features</a><a href="/pricing">Pricing</a><a href="https://absdev.net">ABSDev</a></nav></header><main>@yield('content')</main><footer>SonarDesk by ABSDev · macOS software</footer></body></html>
@php
$dedicated = in_array(request()->getHost(), ['sonardesk.absdev.net','sonardesk.co.uk','www.sonardesk.co.uk'], true);
$base = $dedicated ? '' : '/sonardesk';
@endphp
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#0b1220"><title>@yield('title','SonarDesk — Native SonarQube monitoring for macOS')</title><meta name="description" content="@yield('description','Native SonarQube monitoring for macOS.')"><link rel="stylesheet" href="{{ asset('css/sonardesk-original.css') }}"></head>
<body><header class="site-header"><nav class="nav-shell" aria-label="Primary navigation"><a class="brand" href="{{ $base ?: '/' }}"><img src="{{ asset('icons/sonardesk-icon-64.png') }}" alt="" width="34" height="34"><span>SonarDesk</span></a><div class="nav-links"><a href="{{ $base ?: '/' }}">Product</a><a href="{{ $base }}/features">Features</a><a href="{{ $base }}/pricing">Pricing</a><a href="{{ route('sonardesk.downloads') }}">Downloads</a><a href="{{ route('account.sonardesk.index') }}">My licences</a></div><div class="nav-account"><a class="nav-auth-link" href="{{ url('/') }}">ABSDEV</a><a class="nav-cta" href="{{ route('sonardesk.buy') }}">Start free trial</a></div></nav></header><main class="wrap">@yield('content')</main><footer class="site-footer"><div class="footer-shell"><div><div class="footer-brand"><img src="{{ asset('icons/sonardesk-icon-64.png') }}" alt="" width="28" height="28"><span>SonarDesk</span></div><div class="footer-copy">Native SonarQube monitoring for macOS.</div></div><div class="footer-links"><a href="{{ $base ?: '/' }}">Product</a><a href="{{ $base }}/features">Features</a><a href="{{ $base }}/pricing">Pricing</a><a href="{{ route('sonardesk.downloads') }}">Downloads</a><a href="{{ url('/') }}">ABSDEV</a></div></div></footer></body></html>
+6 -2
View File
@@ -1,3 +1,7 @@
@extends('sonardesk.layout')
@section('title','Pricing')
@section('content')<section class="hero"><h1>Simple plans for developers.</h1><p class="lead">Choose a plan that fits your workflow. Checkout will be available once the existing payment integration has passed migration verification.</p></section><div class="grid"><div class="card"><h2>Personal</h2><p>£4.99/month or £39.99/year</p></div><div class="card"><h2>Professional</h2><p>£8.99/month or £79.99/year</p></div></div>@endsection
@section('title','Pricing — SonarDesk')
@section('content')
<section class="hero-card marketing-hero compact-hero"><span class="eyebrow">Straightforward pricing</span><h1>SonarDesk for every Mac workflow.</h1><p class="lead">Choose Personal or Professional. Secure checkout and licence activation through ABSDEV.</p></section>
<section class="section-block"><div class="pricing-preview-grid"><article class="price-card"><span class="eyebrow">Personal</span><h3>For individual developers</h3><div class="marketing-price">£39.99 <small>/ year</small></div><p>or £4.99 monthly</p><ul class="features"><li>Native macOS SonarQube monitoring</li><li>Quality gates, issues and project health</li><li>Licence management</li></ul><a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">Choose Personal</a></article><article class="price-card featured"><span class="pill">Professional</span><span class="eyebrow">Professional</span><h3>For multi-Mac workflows</h3><div class="marketing-price">£79.99 <small>/ year</small></div><p>or £8.99 monthly</p><ul class="features"><li>All SonarDesk features</li><li>Professional licensing</li><li>Customer licence portal</li></ul><a class="btn btn-primary" href="{{ route('sonardesk.buy') }}">Choose Professional</a></article></div><p class="microcopy">Final billing terms and trial availability are confirmed during checkout.</p></section>
<section class="cta-panel"><div><span class="eyebrow">Get started</span><h2>Ready to try SonarDesk?</h2><p>Download the latest macOS release or view available plans.</p></div><a class="btn btn-secondary" href="{{ route('sonardesk.downloads') }}">View downloads</a></section>
@endsection
+7
View File
@@ -12,6 +12,13 @@ Route::get('/', function () {
return stylist_frontend_view(stylist()->frontend()->entryView());
})->name('home');
// Public SonarDesk product pages on ABSDEV.
Route::prefix('sonardesk')->name('absdev.sonardesk.')->group(function (): void {
Route::view('/', 'sonardesk.home')->name('home');
Route::view('/features', 'sonardesk.features')->name('features');
Route::view('/pricing', 'sonardesk.pricing')->name('pricing');
});
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', DashboardController::class)->name('dashboard');
});