decision_agent 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,6 +30,15 @@ class RuleBuilder {
30
30
  document.getElementById('downloadBtn').addEventListener('click', () => this.downloadJSON());
31
31
  document.getElementById('importFile').addEventListener('change', (e) => this.importJSON(e));
32
32
 
33
+ // Versioning
34
+ document.getElementById('saveVersionBtn').addEventListener('click', () => this.openSaveVersionModal());
35
+ document.getElementById('refreshVersionsBtn').addEventListener('click', () => this.loadVersionHistory());
36
+ document.getElementById('closeSaveVersionBtn').addEventListener('click', () => this.closeSaveVersionModal());
37
+ document.getElementById('cancelSaveVersionBtn').addEventListener('click', () => this.closeSaveVersionModal());
38
+ document.getElementById('confirmSaveVersionBtn').addEventListener('click', () => this.confirmSaveVersion());
39
+ document.getElementById('closeCompareBtn').addEventListener('click', () => this.closeCompareModal());
40
+ document.getElementById('closeCompareModalBtn').addEventListener('click', () => this.closeCompareModal());
41
+
33
42
  // Modal close on outside click
34
43
  document.getElementById('ruleModal').addEventListener('click', (e) => {
35
44
  if (e.target.id === 'ruleModal') {
@@ -37,6 +46,18 @@ class RuleBuilder {
37
46
  }
38
47
  });
39
48
 
49
+ document.getElementById('saveVersionModal').addEventListener('click', (e) => {
50
+ if (e.target.id === 'saveVersionModal') {
51
+ this.closeSaveVersionModal();
52
+ }
53
+ });
54
+
55
+ document.getElementById('compareVersionsModal').addEventListener('click', (e) => {
56
+ if (e.target.id === 'compareVersionsModal') {
57
+ this.closeCompareModal();
58
+ }
59
+ });
60
+
40
61
  // Operator change - hide/show value input
41
62
  document.addEventListener('change', (e) => {
42
63
  if (e.target.classList.contains('operator-select')) {
@@ -565,6 +586,303 @@ class RuleBuilder {
565
586
  div.textContent = String(text);
566
587
  return div.innerHTML;
567
588
  }
589
+
590
+ // ===== VERSION MANAGEMENT METHODS =====
591
+
592
+ getRuleData() {
593
+ const version = document.getElementById('rulesetVersion').value || '1.0';
594
+ const ruleset = document.getElementById('rulesetName').value || 'my_ruleset';
595
+
596
+ return {
597
+ version: version,
598
+ ruleset: ruleset,
599
+ rules: this.rules
600
+ };
601
+ }
602
+
603
+ openSaveVersionModal() {
604
+ const ruleset = document.getElementById('rulesetName').value;
605
+ if (!ruleset) {
606
+ alert('Please enter a ruleset name before saving a version');
607
+ return;
608
+ }
609
+
610
+ document.getElementById('saveVersionModal').classList.remove('hidden');
611
+ }
612
+
613
+ closeSaveVersionModal() {
614
+ document.getElementById('saveVersionModal').classList.add('hidden');
615
+ document.getElementById('versionChangelog').value = '';
616
+ }
617
+
618
+ async confirmSaveVersion() {
619
+ const ruleset = document.getElementById('rulesetName').value;
620
+ const createdBy = document.getElementById('versionCreatedBy').value;
621
+ const changelog = document.getElementById('versionChangelog').value;
622
+
623
+ const ruleData = this.getRuleData();
624
+
625
+ try {
626
+ const response = await fetch('/api/versions', {
627
+ method: 'POST',
628
+ headers: { 'Content-Type': 'application/json' },
629
+ body: JSON.stringify({
630
+ rule_id: ruleset,
631
+ content: ruleData,
632
+ created_by: createdBy,
633
+ changelog: changelog
634
+ })
635
+ });
636
+
637
+ if (response.ok) {
638
+ const version = await response.json();
639
+ alert(`Version ${version.version_number} saved successfully!`);
640
+ this.closeSaveVersionModal();
641
+ this.loadVersionHistory();
642
+ } else {
643
+ const error = await response.json();
644
+ alert(`Error saving version: ${error.error}`);
645
+ }
646
+ } catch (error) {
647
+ alert(`Error saving version: ${error.message}`);
648
+ }
649
+ }
650
+
651
+ async loadVersionHistory() {
652
+ const ruleset = document.getElementById('rulesetName').value;
653
+ if (!ruleset) {
654
+ document.getElementById('versionHistoryContainer').innerHTML =
655
+ '<p class="empty-state">Enter a ruleset name to view version history</p>';
656
+ return;
657
+ }
658
+
659
+ try {
660
+ const response = await fetch(`/api/rules/${encodeURIComponent(ruleset)}/history`);
661
+ if (response.ok) {
662
+ const history = await response.json();
663
+ this.displayVersionHistory(history);
664
+ } else {
665
+ document.getElementById('versionHistoryContainer').innerHTML =
666
+ '<p class="empty-state">No versions found for this ruleset</p>';
667
+ }
668
+ } catch (error) {
669
+ document.getElementById('versionHistoryContainer').innerHTML =
670
+ `<p class="error-message">Error loading versions: ${error.message}</p>`;
671
+ }
672
+ }
673
+
674
+ displayVersionHistory(history) {
675
+ const container = document.getElementById('versionHistoryContainer');
676
+
677
+ if (!history.versions || history.versions.length === 0) {
678
+ container.innerHTML = '<p class="empty-state">No versions yet. Save a version to get started.</p>';
679
+ return;
680
+ }
681
+
682
+ let html = `
683
+ <div class="version-stats">
684
+ <div class="stat">
685
+ <span class="stat-label">Total Versions:</span>
686
+ <span class="stat-value">${history.total_versions}</span>
687
+ </div>
688
+ <div class="stat">
689
+ <span class="stat-label">Active Version:</span>
690
+ <span class="stat-value">${history.active_version ? history.active_version.version_number : 'None'}</span>
691
+ </div>
692
+ </div>
693
+ <table class="version-table">
694
+ <thead>
695
+ <tr>
696
+ <th>Version</th>
697
+ <th>Created By</th>
698
+ <th>Created At</th>
699
+ <th>Status</th>
700
+ <th>Changelog</th>
701
+ <th>Actions</th>
702
+ </tr>
703
+ </thead>
704
+ <tbody>
705
+ `;
706
+
707
+ history.versions.forEach((version, index) => {
708
+ const createdAt = new Date(version.created_at).toLocaleString();
709
+ const statusClass = version.status === 'active' ? 'status-active' :
710
+ version.status === 'draft' ? 'status-draft' : 'status-archived';
711
+
712
+ html += `
713
+ <tr class="${version.status === 'active' ? 'active-version' : ''}">
714
+ <td><strong>v${version.version_number}</strong></td>
715
+ <td>${this.escapeHtml(version.created_by)}</td>
716
+ <td>${createdAt}</td>
717
+ <td><span class="status-badge ${statusClass}">${version.status}</span></td>
718
+ <td>${this.escapeHtml(version.changelog || '')}</td>
719
+ <td class="action-buttons">
720
+ <button class="btn btn-xs btn-secondary" onclick="ruleBuilder.loadVersion('${version.id}')">Load</button>
721
+ ${version.status !== 'active' ?
722
+ `<button class="btn btn-xs btn-primary" onclick="ruleBuilder.rollbackToVersion('${version.id}')">Rollback</button>` :
723
+ '<span class="active-badge">Active</span>'}
724
+ ${index < history.versions.length - 1 ?
725
+ `<button class="btn btn-xs btn-secondary" onclick="ruleBuilder.compareVersions('${version.id}', '${history.versions[index + 1].id}')">Compare</button>` : ''}
726
+ ${version.status !== 'active' ?
727
+ `<button class="btn btn-xs btn-danger" onclick="ruleBuilder.deleteVersion('${version.id}')">Delete</button>` : ''}
728
+ </td>
729
+ </tr>
730
+ `;
731
+ });
732
+
733
+ html += `
734
+ </tbody>
735
+ </table>
736
+ `;
737
+
738
+ container.innerHTML = html;
739
+ }
740
+
741
+ async loadVersion(versionId) {
742
+ try {
743
+ const response = await fetch(`/api/versions/${encodeURIComponent(versionId)}`);
744
+ if (response.ok) {
745
+ const version = await response.json();
746
+ this.loadRulesFromVersion(version.content);
747
+ alert(`Loaded version ${version.version_number}`);
748
+ } else {
749
+ alert('Error loading version');
750
+ }
751
+ } catch (error) {
752
+ alert(`Error loading version: ${error.message}`);
753
+ }
754
+ }
755
+
756
+ loadRulesFromVersion(content) {
757
+ if (content.version) {
758
+ document.getElementById('rulesetVersion').value = content.version;
759
+ }
760
+ if (content.ruleset) {
761
+ document.getElementById('rulesetName').value = content.ruleset;
762
+ }
763
+ if (content.rules) {
764
+ this.rules = content.rules;
765
+ this.renderRules();
766
+ this.updateJSONPreview();
767
+ }
768
+ }
769
+
770
+ async rollbackToVersion(versionId) {
771
+ if (!confirm('Are you sure you want to rollback to this version? This will create a new version.')) {
772
+ return;
773
+ }
774
+
775
+ try {
776
+ const performedBy = prompt('Enter your name:', 'system');
777
+ if (!performedBy) return;
778
+
779
+ const response = await fetch(`/api/versions/${encodeURIComponent(versionId)}/activate`, {
780
+ method: 'POST',
781
+ headers: { 'Content-Type': 'application/json' },
782
+ body: JSON.stringify({ performed_by: performedBy })
783
+ });
784
+
785
+ if (response.ok) {
786
+ const version = await response.json();
787
+ alert(`Rolled back to version ${version.version_number}`);
788
+ this.loadVersionHistory();
789
+ this.loadVersion(versionId);
790
+ } else {
791
+ const error = await response.json();
792
+ alert(`Error during rollback: ${error.error}`);
793
+ }
794
+ } catch (error) {
795
+ alert(`Error during rollback: ${error.message}`);
796
+ }
797
+ }
798
+
799
+ async compareVersions(versionId1, versionId2) {
800
+ try {
801
+ const response = await fetch(
802
+ `/api/versions/${encodeURIComponent(versionId1)}/compare/${encodeURIComponent(versionId2)}`
803
+ );
804
+
805
+ if (response.ok) {
806
+ const comparison = await response.json();
807
+ this.displayComparison(comparison);
808
+ document.getElementById('compareVersionsModal').classList.remove('hidden');
809
+ } else {
810
+ alert('Error comparing versions');
811
+ }
812
+ } catch (error) {
813
+ alert(`Error comparing versions: ${error.message}`);
814
+ }
815
+ }
816
+
817
+ displayComparison(comparison) {
818
+ const container = document.getElementById('comparisonResult');
819
+ const v1 = comparison.version_1;
820
+ const v2 = comparison.version_2;
821
+ const diff = comparison.differences;
822
+
823
+ let html = `
824
+ <div class="comparison-header">
825
+ <div class="version-info">
826
+ <h3>Version ${v1.version_number}</h3>
827
+ <p>By ${this.escapeHtml(v1.created_by)} on ${new Date(v1.created_at).toLocaleString()}</p>
828
+ </div>
829
+ <div class="comparison-arrow">→</div>
830
+ <div class="version-info">
831
+ <h3>Version ${v2.version_number}</h3>
832
+ <p>By ${this.escapeHtml(v2.created_by)} on ${new Date(v2.created_at).toLocaleString()}</p>
833
+ </div>
834
+ </div>
835
+
836
+ <div class="comparison-content">
837
+ <div class="comparison-section">
838
+ <h4>Version ${v1.version_number} Content:</h4>
839
+ <pre class="json-code">${JSON.stringify(v1.content, null, 2)}</pre>
840
+ </div>
841
+ <div class="comparison-section">
842
+ <h4>Version ${v2.version_number} Content:</h4>
843
+ <pre class="json-code">${JSON.stringify(v2.content, null, 2)}</pre>
844
+ </div>
845
+ </div>
846
+
847
+ <div class="diff-summary">
848
+ <h4>Changes Summary:</h4>
849
+ <div class="diff-stats">
850
+ <span class="diff-added">+${diff.added.length} added</span>
851
+ <span class="diff-removed">-${diff.removed.length} removed</span>
852
+ <span class="diff-changed">${Object.keys(diff.changed).length} changed</span>
853
+ </div>
854
+ </div>
855
+ `;
856
+
857
+ container.innerHTML = html;
858
+ }
859
+
860
+ closeCompareModal() {
861
+ document.getElementById('compareVersionsModal').classList.add('hidden');
862
+ }
863
+
864
+ async deleteVersion(versionId) {
865
+ if (!confirm('Are you sure you want to delete this version? This action cannot be undone.')) {
866
+ return;
867
+ }
868
+
869
+ try {
870
+ const response = await fetch(`/api/versions/${encodeURIComponent(versionId)}`, {
871
+ method: 'DELETE',
872
+ headers: { 'Content-Type': 'application/json' }
873
+ });
874
+
875
+ if (response.ok) {
876
+ alert('Version deleted successfully');
877
+ this.loadVersionHistory();
878
+ } else {
879
+ const error = await response.json();
880
+ alert(`Error deleting version: ${error.error}`);
881
+ }
882
+ } catch (error) {
883
+ alert(`Error deleting version: ${error.message}`);
884
+ }
885
+ }
568
886
  }
569
887
 
570
888
  // Initialize app when DOM is ready
@@ -48,6 +48,7 @@
48
48
  <!-- Action Buttons -->
49
49
  <div class="actions">
50
50
  <button class="btn btn-success" id="validateBtn">✓ Validate Rules</button>
51
+ <button class="btn btn-primary" id="saveVersionBtn">💾 Save Version</button>
51
52
  <button class="btn btn-secondary" id="clearBtn">Clear All</button>
52
53
  </div>
53
54
  </div>
@@ -84,6 +85,18 @@
84
85
  </div>
85
86
  </div>
86
87
 
88
+ <!-- Version History Section -->
89
+ <div class="version-section">
90
+ <div class="panel-header">
91
+ <h2>📝 Version History</h2>
92
+ <button class="btn btn-secondary" id="refreshVersionsBtn">🔄 Refresh</button>
93
+ </div>
94
+
95
+ <div id="versionHistoryContainer" class="version-history-container">
96
+ <p class="empty-state">No versions yet. Save a version to get started.</p>
97
+ </div>
98
+ </div>
99
+
87
100
  <!-- Rule Editor Modal -->
88
101
  <div id="ruleModal" class="modal hidden">
89
102
  <div class="modal-content">
@@ -179,6 +192,48 @@
179
192
  </div>
180
193
  </div>
181
194
  </template>
195
+
196
+ <!-- Save Version Modal -->
197
+ <div id="saveVersionModal" class="modal hidden">
198
+ <div class="modal-content">
199
+ <div class="modal-header">
200
+ <h2>Save New Version</h2>
201
+ <button class="close-btn" id="closeSaveVersionBtn">&times;</button>
202
+ </div>
203
+ <div class="modal-body">
204
+ <div class="form-group">
205
+ <label for="versionCreatedBy">Created By:</label>
206
+ <input type="text" id="versionCreatedBy" class="input" placeholder="system" value="system">
207
+ </div>
208
+ <div class="form-group">
209
+ <label for="versionChangelog">Changelog:</label>
210
+ <textarea id="versionChangelog" class="input" rows="3" placeholder="Describe the changes in this version..."></textarea>
211
+ </div>
212
+ </div>
213
+ <div class="modal-footer">
214
+ <button class="btn btn-secondary" id="cancelSaveVersionBtn">Cancel</button>
215
+ <button class="btn btn-primary" id="confirmSaveVersionBtn">Save Version</button>
216
+ </div>
217
+ </div>
218
+ </div>
219
+
220
+ <!-- Compare Versions Modal -->
221
+ <div id="compareVersionsModal" class="modal hidden">
222
+ <div class="modal-content modal-large">
223
+ <div class="modal-header">
224
+ <h2>Compare Versions</h2>
225
+ <button class="close-btn" id="closeCompareBtn">&times;</button>
226
+ </div>
227
+ <div class="modal-body">
228
+ <div id="comparisonResult" class="comparison-container">
229
+ <!-- Comparison will be displayed here -->
230
+ </div>
231
+ </div>
232
+ <div class="modal-footer">
233
+ <button class="btn btn-secondary" id="closeCompareModalBtn">Close</button>
234
+ </div>
235
+ </div>
236
+ </div>
182
237
  </div>
183
238
 
184
239
  <footer class="footer">
@@ -229,6 +229,15 @@ textarea.input {
229
229
  background: #059669;
230
230
  }
231
231
 
232
+ .btn-danger {
233
+ background: var(--danger-color);
234
+ color: white;
235
+ }
236
+
237
+ .btn-danger:hover {
238
+ background: #dc2626;
239
+ }
240
+
232
241
  .btn-sm {
233
242
  padding: 5px 10px;
234
243
  font-size: 12px;
@@ -556,3 +565,213 @@ textarea.input {
556
565
  ::-webkit-scrollbar-thumb:hover {
557
566
  background: var(--secondary-color);
558
567
  }
568
+
569
+ /* ===== VERSION HISTORY STYLES ===== */
570
+ .version-section {
571
+ margin-top: 30px;
572
+ padding: 20px;
573
+ background: white;
574
+ border-radius: 8px;
575
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
576
+ }
577
+
578
+ .version-history-container {
579
+ margin-top: 15px;
580
+ }
581
+
582
+ .empty-state {
583
+ text-align: center;
584
+ padding: 40px;
585
+ color: #999;
586
+ font-style: italic;
587
+ }
588
+
589
+ .version-stats {
590
+ display: flex;
591
+ gap: 30px;
592
+ margin-bottom: 20px;
593
+ padding: 15px;
594
+ background: var(--hover-bg);
595
+ border-radius: 6px;
596
+ }
597
+
598
+ .stat {
599
+ display: flex;
600
+ align-items: center;
601
+ gap: 10px;
602
+ }
603
+
604
+ .stat-label {
605
+ font-weight: 600;
606
+ color: #666;
607
+ }
608
+
609
+ .stat-value {
610
+ font-size: 1.2em;
611
+ font-weight: bold;
612
+ color: var(--primary-color);
613
+ }
614
+
615
+ .version-table {
616
+ width: 100%;
617
+ border-collapse: collapse;
618
+ margin-top: 15px;
619
+ }
620
+
621
+ .version-table th {
622
+ background: var(--primary-color);
623
+ color: white;
624
+ padding: 12px;
625
+ text-align: left;
626
+ font-weight: 600;
627
+ }
628
+
629
+ .version-table td {
630
+ padding: 12px;
631
+ border-bottom: 1px solid var(--border-color);
632
+ }
633
+
634
+ .version-table tr:hover {
635
+ background: var(--hover-bg);
636
+ }
637
+
638
+ .version-table tr.active-version {
639
+ background: #e8f5e9;
640
+ }
641
+
642
+ .status-badge {
643
+ display: inline-block;
644
+ padding: 4px 12px;
645
+ border-radius: 12px;
646
+ font-size: 0.85em;
647
+ font-weight: 600;
648
+ }
649
+
650
+ .status-active {
651
+ background: #4caf50;
652
+ color: white;
653
+ }
654
+
655
+ .status-draft {
656
+ background: #ff9800;
657
+ color: white;
658
+ }
659
+
660
+ .status-archived {
661
+ background: #757575;
662
+ color: white;
663
+ }
664
+
665
+ .active-badge {
666
+ color: #4caf50;
667
+ font-weight: 600;
668
+ }
669
+
670
+ .action-buttons {
671
+ display: flex;
672
+ gap: 8px;
673
+ flex-wrap: wrap;
674
+ }
675
+
676
+ .btn-xs {
677
+ padding: 4px 10px;
678
+ font-size: 0.85em;
679
+ }
680
+
681
+ /* Version Modals */
682
+ .modal-large .modal-content {
683
+ max-width: 900px;
684
+ width: 90%;
685
+ }
686
+
687
+ .comparison-header {
688
+ display: flex;
689
+ align-items: center;
690
+ justify-content: space-between;
691
+ gap: 20px;
692
+ padding: 20px;
693
+ background: var(--hover-bg);
694
+ border-radius: 6px;
695
+ margin-bottom: 20px;
696
+ }
697
+
698
+ .version-info {
699
+ flex: 1;
700
+ }
701
+
702
+ .version-info h3 {
703
+ margin: 0 0 8px 0;
704
+ color: var(--primary-color);
705
+ }
706
+
707
+ .version-info p {
708
+ margin: 0;
709
+ color: #666;
710
+ font-size: 0.9em;
711
+ }
712
+
713
+ .comparison-arrow {
714
+ font-size: 2em;
715
+ color: var(--primary-color);
716
+ font-weight: bold;
717
+ }
718
+
719
+ .comparison-content {
720
+ display: grid;
721
+ grid-template-columns: 1fr 1fr;
722
+ gap: 20px;
723
+ margin-bottom: 20px;
724
+ }
725
+
726
+ .comparison-section {
727
+ border: 1px solid var(--border-color);
728
+ border-radius: 6px;
729
+ padding: 15px;
730
+ }
731
+
732
+ .comparison-section h4 {
733
+ margin-top: 0;
734
+ color: var(--primary-color);
735
+ }
736
+
737
+ .comparison-section .json-code {
738
+ max-height: 400px;
739
+ overflow-y: auto;
740
+ }
741
+
742
+ .diff-summary {
743
+ padding: 15px;
744
+ background: var(--hover-bg);
745
+ border-radius: 6px;
746
+ }
747
+
748
+ .diff-summary h4 {
749
+ margin-top: 0;
750
+ }
751
+
752
+ .diff-stats {
753
+ display: flex;
754
+ gap: 20px;
755
+ margin-top: 10px;
756
+ }
757
+
758
+ .diff-added {
759
+ color: #4caf50;
760
+ font-weight: 600;
761
+ }
762
+
763
+ .diff-removed {
764
+ color: #f44336;
765
+ font-weight: 600;
766
+ }
767
+
768
+ .diff-changed {
769
+ color: #ff9800;
770
+ font-weight: 600;
771
+ }
772
+
773
+ .error-message {
774
+ color: var(--danger-color);
775
+ text-align: center;
776
+ padding: 20px;
777
+ }