diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5a006b9..3afc6da 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,9 +20,21 @@ jobs:
extensions: mbstring, sqlite3, pdo_sqlite
coverage: none
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist
+ - name: Install NPM dependencies
+ run: npm ci
+
+ - name: Build assets
+ run: npm run build
+
- name: Copy environment file
run: cp .env.example .env
diff --git a/app/Livewire/Concerns/HasVersionHistory.php b/app/Livewire/Concerns/HasVersionHistory.php
new file mode 100644
index 0000000..e814105
--- /dev/null
+++ b/app/Livewire/Concerns/HasVersionHistory.php
@@ -0,0 +1,99 @@
+showVersionHistory = true;
+ $this->previewVersionId = null;
+ }
+
+ public function closeVersionHistory(): void
+ {
+ $this->showVersionHistory = false;
+ $this->previewVersionId = null;
+ }
+
+ public function selectVersion(string $versionId): void
+ {
+ $this->previewVersionId = $versionId;
+ }
+
+ public function restoreVersion(string $versionId): void
+ {
+ $oldVersion = DocumentVersion::findOrFail($versionId);
+ $document = $this->document;
+
+ if (! $document) {
+ return;
+ }
+
+ // Verify version belongs to this document
+ if ($oldVersion->document_id !== $document->id) {
+ return;
+ }
+
+ // Verify user has permission to update this project
+ $this->authorize('update', $document->project);
+
+ // Wrap in transaction for atomicity
+ DB::transaction(function () use ($oldVersion, $document) {
+ // Create new version from old content
+ $newVersion = DocumentVersion::create([
+ 'document_id' => $document->id,
+ 'created_by' => auth()->id(),
+ 'content_md' => $oldVersion->content_md,
+ 'summary' => 'Restored from '.$oldVersion->created_at->format('M j, Y g:i A'),
+ ]);
+
+ // Update document to point to new version
+ $document->update(['current_version_id' => $newVersion->id]);
+ });
+
+ // Refresh UI state
+ $this->loadContent();
+ unset($this->document);
+ unset($this->versions);
+
+ $this->closeVersionHistory();
+
+ // Notify other components
+ $this->dispatch('docUpdated', type: $document->type->value);
+ $this->dispatch('version-restored', message: 'Version restored successfully');
+ }
+
+ #[Computed]
+ public function versions(): Collection
+ {
+ if (! $this->document) {
+ return collect();
+ }
+
+ return $this->document
+ ->versions()
+ ->with(['createdBy', 'planRun'])
+ ->orderByDesc('created_at')
+ ->get();
+ }
+
+ #[Computed]
+ public function selectedVersionForPreview(): ?DocumentVersion
+ {
+ if (! $this->previewVersionId) {
+ return null;
+ }
+
+ return $this->versions->firstWhere('id', $this->previewVersionId);
+ }
+}
diff --git a/app/Livewire/Projects/Tabs/KanbanBoard.php b/app/Livewire/Projects/Tabs/KanbanBoard.php
index 189d842..67552ba 100644
--- a/app/Livewire/Projects/Tabs/KanbanBoard.php
+++ b/app/Livewire/Projects/Tabs/KanbanBoard.php
@@ -7,7 +7,6 @@
use App\Models\Project;
use App\Models\Task;
use App\Models\TaskSet;
-use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Filament\Actions\Concerns\InteractsWithActions;
use Filament\Actions\Contracts\HasActions;
use Filament\Actions\CreateAction;
@@ -20,6 +19,7 @@
use Filament\Forms\Contracts\HasForms;
use Filament\Infolists\Components\TextEntry;
use Filament\Schemas\Schema;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
diff --git a/app/Livewire/Projects/Tabs/Prd.php b/app/Livewire/Projects/Tabs/Prd.php
index 3021aa8..13fd16a 100644
--- a/app/Livewire/Projects/Tabs/Prd.php
+++ b/app/Livewire/Projects/Tabs/Prd.php
@@ -3,6 +3,7 @@
namespace App\Livewire\Projects\Tabs;
use App\Enums\DocumentType;
+use App\Livewire\Concerns\HasVersionHistory;
use App\Models\Document;
use App\Models\DocumentVersion;
use App\Models\Project;
@@ -14,6 +15,7 @@
class Prd extends Component
{
use AuthorizesRequests;
+ use HasVersionHistory;
public string $projectId;
diff --git a/app/Livewire/Projects/Tabs/Tech.php b/app/Livewire/Projects/Tabs/Tech.php
index 207859e..add3232 100644
--- a/app/Livewire/Projects/Tabs/Tech.php
+++ b/app/Livewire/Projects/Tabs/Tech.php
@@ -5,11 +5,12 @@
use App\Actions\GenerateTasksFromTechSpec;
use App\Enums\DocumentType;
use App\Enums\PlanRunStepStatus;
+use App\Livewire\Concerns\HasVersionHistory;
use App\Models\Document;
use App\Models\DocumentVersion;
use App\Models\Project;
-use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use App\Models\TaskSet;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
@@ -17,6 +18,7 @@
class Tech extends Component
{
use AuthorizesRequests;
+ use HasVersionHistory;
public string $projectId;
diff --git a/resources/views/components/version-history-slide-over.blade.php b/resources/views/components/version-history-slide-over.blade.php
new file mode 100644
index 0000000..3577e71
--- /dev/null
+++ b/resources/views/components/version-history-slide-over.blade.php
@@ -0,0 +1,215 @@
+@props([
+ 'versions',
+ 'selectedVersion' => null,
+ 'previewVersionId' => null,
+ 'currentVersionId' => null,
+])
+
+
+ {{-- Backdrop --}}
+
+
+ {{-- Slide-over panel --}}
+
+
+
+ {{-- Header --}}
+
+
+
+
+
Version History
+
+
+
+
+
+ {{-- Content: Two-column layout --}}
+
+ {{-- Left: Version list --}}
+
+ @if($versions->isEmpty())
+
+
+
No versions yet
+
+ @else
+
+ @foreach($versions as $version)
+
+ @endforeach
+
+ @endif
+
+
+ {{-- Right: Preview & Restore --}}
+
+ @if($selectedVersion)
+ {{-- Preview Header --}}
+
+
+
+ Version from {{ $selectedVersion->created_at->format('M j, Y') }} at {{ $selectedVersion->created_at->format('g:i A') }}
+
+
+ @if($selectedVersion->created_by && $selectedVersion->createdBy)
+ By {{ $selectedVersion->createdBy->name }}
+ @elseif($selectedVersion->plan_run_id)
+ AI Generated
+ @endif
+
+
+
+ @if($selectedVersion->id !== $currentVersionId)
+
+ @else
+
+ Current Version
+
+ @endif
+
+
+ {{-- Preview Content --}}
+
+
{{ $selectedVersion->content_md }}
+
+ @else
+ {{-- Empty state --}}
+
+
+
+
Select a version to preview
+
+
+ @endif
+
+
+
+
+
+
+
+{{-- Toast notification --}}
+ show = false, 3000)"
+ x-show="show"
+ x-transition:enter="transition ease-out duration-300"
+ x-transition:enter-start="opacity-0 translate-y-2"
+ x-transition:enter-end="opacity-100 translate-y-0"
+ x-transition:leave="transition ease-in duration-200"
+ x-transition:leave-start="opacity-100 translate-y-0"
+ x-transition:leave-end="opacity-0 translate-y-2"
+ x-cloak
+ class="fixed bottom-4 right-4 flex items-center gap-2 bg-gray-900 text-white px-4 py-3 rounded-lg shadow-lg z-[60]"
+>
+
+
+
diff --git a/resources/views/livewire/projects/tabs/prd.blade.php b/resources/views/livewire/projects/tabs/prd.blade.php
index ecae9d7..66e1c7c 100644
--- a/resources/views/livewire/projects/tabs/prd.blade.php
+++ b/resources/views/livewire/projects/tabs/prd.blade.php
@@ -8,6 +8,20 @@
@if($isDirty)
Unsaved changes
@endif
+
+ {{-- Version History Button --}}
+ @if($this->document)
+
+ @endif
+