@feltdb/core 0.4.8 → 0.4.10

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.
@@ -72,7 +72,7 @@ export async function createProject(options) {
72
72
  const feltdbConfig = {
73
73
  namespace: applicationName,
74
74
  runtime,
75
- storage: runtime === 'browser' ? 'opfs' : 'durable',
75
+ storage: runtime === 'browser' ? 'indexeddb' : runtime === 'managed' ? 'managed' : 'durable',
76
76
  distributed,
77
77
  agents: {
78
78
  enabled: hasAgents,
@@ -156,16 +156,165 @@ export async function createProject(options) {
156
156
  // Create main application files
157
157
  const runtimeOptions = runtime === 'browser'
158
158
  ? "{ namespace: '" + applicationName + "', browser: true }"
159
- : runtime === 'self-hosted'
160
- ? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
161
- : "{ namespace: '" + applicationName + "', memory: true }";
159
+ : runtime === 'managed'
160
+ ? "{ namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || import.meta.env.VITE_FELTDB_URL || '', token: import.meta.env.VITE_FELTDB_MANAGED_API_KEY || import.meta.env.VITE_FELTDB_API_KEY || '' } }"
161
+ : runtime === 'self-hosted'
162
+ ? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
163
+ : "{ namespace: '" + applicationName + "', memory: true }";
162
164
  const feltdbTs = `import { createFeltDB } from '@feltdb/core';
163
165
 
164
166
  export const db = createFeltDB(${runtimeOptions});
165
167
 
166
168
  // Collections
167
- export const documents = db.collection('documents');
168
- export const reports = db.collection('reports');
169
+ export const projects = db.collection('projects');
170
+ export const tasks = db.collection('tasks');
171
+ export const activity = db.collection('activity');
172
+
173
+ // Types
174
+ export interface Project {
175
+ id: string;
176
+ name: string;
177
+ description: string;
178
+ status: 'active' | 'archived' | 'completed';
179
+ createdAt: string;
180
+ updatedAt: string;
181
+ metadata?: Record<string, any>;
182
+ }
183
+
184
+ export interface Task {
185
+ id: string;
186
+ projectId: string;
187
+ title: string;
188
+ description: string;
189
+ status: 'todo' | 'in-progress' | 'completed';
190
+ priority: 'low' | 'medium' | 'high';
191
+ assignee?: string;
192
+ createdAt: string;
193
+ updatedAt: string;
194
+ }
195
+
196
+ export interface ActivityEvent {
197
+ id: string;
198
+ timestamp: string;
199
+ type: string;
200
+ entityType: 'project' | 'task';
201
+ entityId: string;
202
+ entityName: string;
203
+ changes?: Record<string, any>;
204
+ userId?: string;
205
+ }
206
+
207
+ // Indexes
208
+ projects.createIndex({ name: 'projects_status', type: 'hash', field: 'status' });
209
+ tasks.createIndex({ name: 'tasks_project', type: 'hash', field: 'projectId' });
210
+ tasks.createIndex({ name: 'tasks_status', type: 'hash', field: 'status' });
211
+ tasks.createIndex({ name: 'tasks_priority', type: 'hash', field: 'priority' });
212
+ activity.createIndex({ name: 'activity_timestamp', type: 'sorted', field: 'timestamp' });
213
+
214
+ // Operations
215
+ export async function createProject(data: Omit<Project, 'id' | 'createdAt' | 'updatedAt'>): Promise<Project> {
216
+ const project: Project = {
217
+ id: 'proj_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
218
+ ...data,
219
+ createdAt: new Date().toISOString(),
220
+ updatedAt: new Date().toISOString(),
221
+ };
222
+ await projects.insert(project);
223
+ await logActivity({
224
+ type: 'project_created',
225
+ entityType: 'project',
226
+ entityId: project.id,
227
+ entityName: project.name,
228
+ });
229
+ return project;
230
+ }
231
+
232
+ export async function createTask(data: Omit<Task, 'id' | 'createdAt' | 'updatedAt'>): Promise<Task> {
233
+ const task: Task = {
234
+ id: 'task_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
235
+ ...data,
236
+ createdAt: new Date().toISOString(),
237
+ updatedAt: new Date().toISOString(),
238
+ };
239
+ await tasks.insert(task);
240
+ await logActivity({
241
+ type: 'task_created',
242
+ entityType: 'task',
243
+ entityId: task.id,
244
+ entityName: task.title,
245
+ });
246
+ return task;
247
+ }
248
+
249
+ export async function updateTask(id: string, updates: Partial<Task>): Promise<void> {
250
+ const task = await tasks.findOne({ id });
251
+ if (!task) throw new Error('Task not found');
252
+
253
+ const updated = {
254
+ ...task,
255
+ ...updates,
256
+ updatedAt: new Date().toISOString(),
257
+ };
258
+ await tasks.update({ id }, updated);
259
+ await logActivity({
260
+ type: 'task_updated',
261
+ entityType: 'task',
262
+ entityId: id,
263
+ entityName: updated.title,
264
+ changes: updates,
265
+ });
266
+ }
267
+
268
+ export async function updateProject(id: string, updates: Partial<Project>): Promise<void> {
269
+ const project = await projects.findOne({ id });
270
+ if (!project) throw new Error('Project not found');
271
+
272
+ const updated = {
273
+ ...project,
274
+ ...updates,
275
+ updatedAt: new Date().toISOString(),
276
+ };
277
+ await projects.update({ id }, updated);
278
+ await logActivity({
279
+ type: 'project_updated',
280
+ entityType: 'project',
281
+ entityId: id,
282
+ entityName: updated.name,
283
+ changes: updates,
284
+ });
285
+ }
286
+
287
+ export async function getTasksByProject(projectId: string): Promise<Task[]> {
288
+ return tasks.find({ projectId }) as Promise<Task[]>;
289
+ }
290
+
291
+ export async function getRecentActivity(limit = 50): Promise<ActivityEvent[]> {
292
+ const events = await activity.find({});
293
+ return (events as ActivityEvent[]).sort((a, b) =>
294
+ new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
295
+ ).slice(0, limit);
296
+ }
297
+
298
+ export async function getDashboardStats(): Promise<any> {
299
+ const allTasks = await tasks.find({});
300
+ const allProjects = await projects.find({});
301
+ return {
302
+ totalProjects: allProjects.length,
303
+ totalTasks: allTasks.length,
304
+ completedTasks: (allTasks as Task[]).filter(t => t.status === 'completed').length,
305
+ inProgressTasks: (allTasks as Task[]).filter(t => t.status === 'in-progress').length,
306
+ activeTasks: (allTasks as Task[]).filter(t => t.status === 'todo').length,
307
+ };
308
+ }
309
+
310
+ async function logActivity(event: Omit<ActivityEvent, 'id' | 'timestamp'>): Promise<void> {
311
+ const activityEvent: ActivityEvent = {
312
+ id: 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
313
+ timestamp: new Date().toISOString(),
314
+ ...event,
315
+ };
316
+ await activity.insert(activityEvent);
317
+ }
169
318
  `;
170
319
  fs.writeFileSync(path.join(srcDir, 'feltdb.ts'), feltdbTs);
171
320
  // Create a real local-inference agent for browser projects.
@@ -573,815 +722,387 @@ export const capabilities = {
573
722
  </section>`
574
723
  : '';
575
724
  const appTsx = `import React, { useState, useEffect } from 'react';
576
- import { db, documents } from './feltdb';
577
- ${agentImport}
725
+ import { useCollection } from '@feltdb/core/react';
726
+ import {
727
+ projects,
728
+ tasks,
729
+ activity,
730
+ createProject,
731
+ createTask,
732
+ updateTask,
733
+ getDashboardStats,
734
+ } from './feltdb';
735
+
736
+ type View = 'dashboard' | 'projects' | 'tasks' | 'activity' | 'inspector';
578
737
 
579
738
  export function App() {
580
- const [docs, setDocs] = useState<any[]>([]);
581
- const [loading, setLoading] = useState(true);
582
- const [error, setError] = useState<string | null>(null);
583
- const [addingDoc, setAddingDoc] = useState(false);
584
- const [searchTerm, setSearchTerm] = useState('');
585
- const [editingId, setEditingId] = useState<string | null>(null);
586
- const [editTitle, setEditTitle] = useState('');
587
- const [editContent, setEditContent] = useState('');
588
- const [deletingId, setDeletingId] = useState<string | null>(null);
589
- ${agentState}
590
-
591
- const loadDocs = async () => {
592
- try {
593
- setError(null);
594
- const allDocs = await documents.find({});
595
- setDocs(allDocs.sort((a: any, b: any) =>
596
- new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
597
- ));
598
- } catch (err) {
599
- const msg = err instanceof Error ? err.message : 'Failed to load documents';
600
- console.error('Error loading documents:', err);
601
- setError(msg);
602
- } finally {
603
- setLoading(false);
604
- }
605
- };
739
+ const [currentView, setCurrentView] = useState<View>('dashboard');
740
+ const [selectedProjectId, setSelectedProjectId] = useState<string>('');
741
+ const [dashboardStats, setDashboardStats] = useState<any>(null);
742
+ const [newProjectName, setNewProjectName] = useState('');
743
+ const [newTaskTitle, setNewTaskTitle] = useState('');
744
+ const [newTaskProject, setNewTaskProject] = useState('');
745
+
746
+ const { data: projectsList } = useCollection(projects);
747
+ const { data: tasksList } = useCollection(tasks);
748
+ const { data: activityList } = useCollection(activity);
606
749
 
607
750
  useEffect(() => {
608
- loadDocs();
609
- }, []);
751
+ getDashboardStats().then(setDashboardStats);
752
+ }, [projectsList, tasksList]);
753
+
754
+ const handleCreateProject = async (e: React.FormEvent) => {
755
+ e.preventDefault();
756
+ if (!newProjectName.trim()) return;
757
+ await createProject({
758
+ name: newProjectName,
759
+ description: 'Created via Workspace',
760
+ status: 'active',
761
+ metadata: {},
762
+ });
763
+ setNewProjectName('');
764
+ };
610
765
 
611
- const filteredDocs = docs.filter((doc: any) =>
612
- doc.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
613
- doc.content.toLowerCase().includes(searchTerm.toLowerCase())
614
- );
766
+ const handleCreateTask = async (e: React.FormEvent) => {
767
+ e.preventDefault();
768
+ if (!newTaskTitle.trim() || !newTaskProject) return;
769
+ await createTask({
770
+ projectId: newTaskProject,
771
+ title: newTaskTitle,
772
+ description: '',
773
+ status: 'todo',
774
+ priority: 'medium',
775
+ });
776
+ setNewTaskTitle('');
777
+ };
615
778
 
616
- const handleAddDocument = async () => {
617
- if (addingDoc) return;
618
- setAddingDoc(true);
619
- setError(null);
779
+ const containerStyle: React.CSSProperties = {
780
+ display: 'flex',
781
+ minHeight: '100vh',
782
+ fontFamily: 'system-ui, sans-serif',
783
+ backgroundColor: '#f5f7fa',
784
+ };
620
785
 
621
- try {
622
- const newDoc = {
623
- id: 'doc_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9),
624
- title: 'New Document',
625
- content: 'Enter your content here...',
626
- createdAt: new Date().toISOString(),
627
- };
628
- await documents.insert(newDoc);
629
- await loadDocs();
630
- } catch (err) {
631
- const msg = err instanceof Error ? err.message : 'Failed to add document';
632
- console.error('Error adding document:', err);
633
- setError(msg);
634
- } finally {
635
- setAddingDoc(false);
636
- }
786
+ const sidebarStyle: React.CSSProperties = {
787
+ width: '240px',
788
+ backgroundColor: '#1e293b',
789
+ color: '#e2e8f0',
790
+ padding: '20px',
791
+ borderRight: '1px solid #334155',
792
+ display: 'flex',
793
+ flexDirection: 'column',
794
+ gap: '20px',
637
795
  };
638
796
 
639
- const handleEditStart = (doc: any) => {
640
- setEditingId(doc.id);
641
- setEditTitle(doc.title);
642
- setEditContent(doc.content);
797
+ const mainStyle: React.CSSProperties = {
798
+ flex: 1,
799
+ display: 'flex',
800
+ flexDirection: 'column',
643
801
  };
644
802
 
645
- const handleEditSave = async () => {
646
- if (!editingId) return;
647
- setError(null);
803
+ const headerStyle: React.CSSProperties = {
804
+ backgroundColor: '#fff',
805
+ borderBottom: '1px solid #e2e8f0',
806
+ padding: '20px',
807
+ display: 'flex',
808
+ justifyContent: 'space-between',
809
+ alignItems: 'center',
810
+ };
648
811
 
649
- try {
650
- const doc = docs.find((d: any) => d.id === editingId);
651
- if (doc) {
652
- const updatedDoc = {
653
- ...doc,
654
- title: editTitle,
655
- content: editContent,
656
- updatedAt: new Date().toISOString(),
657
- };
658
- await documents.insert(updatedDoc);
659
- setEditingId(null);
660
- await loadDocs();
661
- }
662
- } catch (err) {
663
- const msg = err instanceof Error ? err.message : 'Failed to save document';
664
- console.error('Error saving document:', err);
665
- setError(msg);
666
- }
812
+ const contentStyle: React.CSSProperties = {
813
+ flex: 1,
814
+ padding: '20px',
815
+ overflowY: 'auto',
667
816
  };
668
817
 
669
- const handleDelete = async (docId: string) => {
670
- if (deletingId) return;
671
- setDeletingId(docId);
672
- setError(null);
818
+ const titleStyle: React.CSSProperties = {
819
+ fontSize: '24px',
820
+ fontWeight: 'bold',
821
+ color: '#1e293b',
822
+ };
673
823
 
674
- try {
675
- const doc = docs.find((d: any) => d.id === docId);
676
- if (doc) {
677
- await documents.delete(doc);
678
- await loadDocs();
679
- }
680
- } catch (err) {
681
- const msg = err instanceof Error ? err.message : 'Failed to delete document';
682
- setError(msg);
683
- } finally {
684
- setDeletingId(null);
685
- }
824
+ const navItemStyle = (active: boolean): React.CSSProperties => ({
825
+ padding: '10px 12px',
826
+ borderRadius: '6px',
827
+ cursor: 'pointer',
828
+ backgroundColor: active ? '#334155' : 'transparent',
829
+ color: active ? '#fff' : '#cbd5e1',
830
+ border: 'none',
831
+ textAlign: 'left',
832
+ width: '100%',
833
+ fontSize: '14px',
834
+ });
835
+
836
+ const statsStyle: React.CSSProperties = {
837
+ display: 'grid',
838
+ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
839
+ gap: '16px',
840
+ marginBottom: '24px',
841
+ };
842
+
843
+ const statCardStyle: React.CSSProperties = {
844
+ backgroundColor: '#fff',
845
+ padding: '16px',
846
+ borderRadius: '8px',
847
+ border: '1px solid #e2e8f0',
848
+ };
849
+
850
+ const formStyle: React.CSSProperties = {
851
+ display: 'flex',
852
+ gap: '8px',
853
+ marginBottom: '20px',
854
+ };
855
+
856
+ const inputStyle: React.CSSProperties = {
857
+ flex: 1,
858
+ padding: '8px 12px',
859
+ border: '1px solid #cbd5e1',
860
+ borderRadius: '6px',
861
+ fontSize: '14px',
862
+ };
863
+
864
+ const buttonStyle: React.CSSProperties = {
865
+ padding: '8px 16px',
866
+ backgroundColor: '#0284c7',
867
+ color: '#fff',
868
+ border: 'none',
869
+ borderRadius: '6px',
870
+ cursor: 'pointer',
871
+ fontSize: '14px',
872
+ };
873
+
874
+ const itemStyle: React.CSSProperties = {
875
+ backgroundColor: '#fff',
876
+ padding: '12px',
877
+ borderRadius: '6px',
878
+ marginBottom: '8px',
879
+ border: '1px solid #e2e8f0',
686
880
  };
687
- ${agentHandler}
688
881
 
689
882
  return (
690
- <div className="app">
691
- <header>
692
- <h1>🌊 FeltDB Research App</h1>
693
- <p>Distributed document management with agents and capabilities</p>
694
- </header>
695
-
696
- <main>
697
- <section className="stats">
698
- <div className="stat">
699
- <span className="label">Documents:</span>
700
- <span className="value">{docs.length}</span>
701
- </div>
702
- <div className="stat">
703
- <span className="label">Runtime:</span>
704
- <span className="value">${runtime}</span>
705
- </div>
706
- <div className="stat">
707
- <span className="label">Distributed:</span>
708
- <span className="value">${distributed ? '' : '✗'}</span>
883
+ <div style={containerStyle}>
884
+ <div style={sidebarStyle}>
885
+ <div style={{ fontSize: '16px', fontWeight: 'bold' }}>⚡ FeltDB Workspace</div>
886
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
887
+ <button
888
+ style={navItemStyle(currentView === 'dashboard')}
889
+ onClick={() => setCurrentView('dashboard')}
890
+ >
891
+ 📊 Dashboard
892
+ </button>
893
+ <button
894
+ style={navItemStyle(currentView === 'projects')}
895
+ onClick={() => setCurrentView('projects')}
896
+ >
897
+ 📁 Projects
898
+ </button>
899
+ <button
900
+ style={navItemStyle(currentView === 'tasks')}
901
+ onClick={() => setCurrentView('tasks')}
902
+ >
903
+ ✅ Tasks
904
+ </button>
905
+ <button
906
+ style={navItemStyle(currentView === 'activity')}
907
+ onClick={() => setCurrentView('activity')}
908
+ >
909
+ 📝 Activity
910
+ </button>
911
+ <button
912
+ style={navItemStyle(currentView === 'inspector')}
913
+ onClick={() => setCurrentView('inspector')}
914
+ >
915
+ 🔍 Inspector
916
+ </button>
917
+ </div>
918
+ <div style={{ marginTop: 'auto', fontSize: '12px', color: '#64748b' }}>
919
+ {projectsList.length} projects • {tasksList.length} tasks
920
+ </div>
921
+ </div>
922
+
923
+ <div style={mainStyle}>
924
+ <div style={headerStyle}>
925
+ <div style={titleStyle}>
926
+ {currentView === 'dashboard' && '📊 Dashboard'}
927
+ {currentView === 'projects' && '📁 Projects'}
928
+ {currentView === 'tasks' && '✅ Tasks'}
929
+ {currentView === 'activity' && '📝 Activity'}
930
+ {currentView === 'inspector' && '🔍 Data Inspector'}
709
931
  </div>
710
- </section>
711
-
712
- {error && (
713
- <section className="error-message">
714
- <strong>⚠ Error:</strong> {error}
715
- <button onClick={() => window.location.reload()} className="retry-btn">
716
- Retry
717
- </button>
718
- </section>
719
- )}
720
-
721
- <section className="documents">
722
- <div className="docs-header">
723
- <h2>📚 Documents</h2>
724
- <div className="docs-controls">
725
- <input
726
- type="text"
727
- placeholder="🔍 Search documents..."
728
- value={searchTerm}
729
- onChange={(e) => setSearchTerm(e.target.value)}
730
- className="search-input"
731
- />
732
- <button
733
- onClick={handleAddDocument}
734
- disabled={addingDoc || loading}
735
- className="primary-btn"
736
- >
737
- {addingDoc ? '⏳ Adding...' : '➕ New Document'}
738
- </button>
932
+ </div>
933
+
934
+ <div style={contentStyle}>
935
+ {currentView === 'dashboard' && (
936
+ <div>
937
+ <div style={statsStyle}>
938
+ <div style={statCardStyle}>
939
+ <div style={{ fontSize: '12px', color: '#64748b' }}>Projects</div>
940
+ <div style={{ fontSize: '28px', fontWeight: 'bold' }}>{projectsList.length}</div>
941
+ </div>
942
+ <div style={statCardStyle}>
943
+ <div style={{ fontSize: '12px', color: '#64748b' }}>Tasks</div>
944
+ <div style={{ fontSize: '28px', fontWeight: 'bold' }}>{tasksList.length}</div>
945
+ </div>
946
+ <div style={statCardStyle}>
947
+ <div style={{ fontSize: '12px', color: '#64748b' }}>Activity Events</div>
948
+ <div style={{ fontSize: '28px', fontWeight: 'bold' }}>{activityList.length}</div>
949
+ </div>
950
+ </div>
951
+ <h3>Welcome to FeltDB Workspace!</h3>
952
+ <p style={{ color: '#64748b', lineHeight: '1.6' }}>
953
+ This application demonstrates core FeltDB capabilities:<br/>
954
+ ✅ Collections and relationships (Projects → Tasks)<br/>
955
+ ✅ Indexed querying for efficient lookups<br/>
956
+ Activity logs for audit trails<br/>
957
+ ✅ Local-first persistence with IndexedDB<br/>
958
+ ✅ Reactive updates using React hooks<br/>
959
+ <br/>
960
+ Create a project to get started! 👇
961
+ </p>
739
962
  </div>
740
- </div>
963
+ )}
741
964
 
742
- {loading ? (
743
- <div className="loading">
744
- <div className="spinner"></div>
745
- <p>Loading documents...</p>
746
- </div>
747
- ) : docs.length === 0 ? (
748
- <div className="empty-state">
749
- <p>📭 No documents yet.</p>
750
- <p className="hint">Create one to get started!</p>
751
- </div>
752
- ) : filteredDocs.length === 0 ? (
753
- <div className="empty-state">
754
- <p>🔍 No documents match "{searchTerm}"</p>
755
- </div>
756
- ) : (
757
- <div className="docs-grid">
758
- {filteredDocs.map((doc: any) => (
759
- <div key={doc.id} className="doc-card">
760
- {editingId === doc.id ? (
761
- <div className="edit-mode">
762
- <input
763
- type="text"
764
- value={editTitle}
765
- onChange={(e) => setEditTitle(e.target.value)}
766
- className="edit-title"
767
- />
768
- <textarea
769
- value={editContent}
770
- onChange={(e) => setEditContent(e.target.value)}
771
- className="edit-content"
772
- rows={6}
773
- />
774
- <div className="edit-actions">
775
- <button onClick={handleEditSave} className="save-btn">
776
- ✓ Save
777
- </button>
778
- <button onClick={() => setEditingId(null)} className="cancel-btn">
779
- ✕ Cancel
780
- </button>
965
+ {currentView === 'projects' && (
966
+ <div>
967
+ <form style={formStyle} onSubmit={handleCreateProject}>
968
+ <input
969
+ style={inputStyle}
970
+ type="text"
971
+ placeholder="New project name..."
972
+ value={newProjectName}
973
+ onChange={(e) => setNewProjectName(e.target.value)}
974
+ />
975
+ <button style={buttonStyle} type="submit">Create</button>
976
+ </form>
977
+ <div>
978
+ {projectsList.map((p: any) => (
979
+ <div key={p.id} style={itemStyle}>
980
+ <div style={{ fontWeight: 'bold' }}>{p.name}</div>
981
+ {p.description && (
982
+ <div style={{ fontSize: '14px', color: '#64748b', margin: '4px 0' }}>
983
+ {p.description}
781
984
  </div>
985
+ )}
986
+ <div style={{ fontSize: '12px', color: '#0284c7', marginTop: '4px' }}>
987
+ Status: {p.status}
782
988
  </div>
783
- ) : (
784
- <>
785
- <h3>{doc.title}</h3>
786
- <p className="doc-preview">{doc.content.substring(0, 150)}...</p>
787
- <div className="doc-meta">
788
- <small>{new Date(doc.createdAt).toLocaleDateString()}</small>
789
- <span className="doc-size">{doc.content.length} chars</span>
989
+ </div>
990
+ ))}
991
+ {projectsList.length === 0 && (
992
+ <p style={{ color: '#64748b' }}>No projects yet. Create one above!</p>
993
+ )}
994
+ </div>
995
+ </div>
996
+ )}
997
+
998
+ {currentView === 'tasks' && (
999
+ <div>
1000
+ <form style={formStyle} onSubmit={handleCreateTask}>
1001
+ <select
1002
+ style={inputStyle}
1003
+ value={newTaskProject}
1004
+ onChange={(e) => setNewTaskProject(e.target.value)}
1005
+ >
1006
+ <option value="">Select a project...</option>
1007
+ {projectsList.map((p: any) => (
1008
+ <option key={p.id} value={p.id}>{p.name}</option>
1009
+ ))}
1010
+ </select>
1011
+ <input
1012
+ style={inputStyle}
1013
+ type="text"
1014
+ placeholder="Task title..."
1015
+ value={newTaskTitle}
1016
+ onChange={(e) => setNewTaskTitle(e.target.value)}
1017
+ />
1018
+ <button style={buttonStyle} type="submit">Create</button>
1019
+ </form>
1020
+ <div>
1021
+ {tasksList.map((t: any) => (
1022
+ <div key={t.id} style={itemStyle}>
1023
+ <div style={{ fontWeight: 'bold' }}>{t.title}</div>
1024
+ {t.description && (
1025
+ <div style={{ fontSize: '14px', color: '#64748b', margin: '4px 0' }}>
1026
+ {t.description}
790
1027
  </div>
791
- <div className="doc-actions">
792
- <button
793
- onClick={() => handleEditStart(doc)}
794
- className="edit-btn"
795
- >
796
- ✎ Edit
797
- </button>
798
- <button
799
- onClick={() => handleDelete(doc.id)}
800
- disabled={deletingId === doc.id}
801
- className="delete-btn"
802
- >
803
- {deletingId === doc.id ? '⏳ Deleting...' : '🗑 Delete'}
804
- </button>
1028
+ )}
1029
+ <div style={{ fontSize: '12px', color: '#64748b', marginTop: '4px' }}>
1030
+ Priority: <strong>{t.priority}</strong> | Status: <strong>{t.status}</strong>
1031
+ </div>
1032
+ </div>
1033
+ ))}
1034
+ {tasksList.length === 0 && (
1035
+ <p style={{ color: '#64748b' }}>No tasks yet. Create one above!</p>
1036
+ )}
1037
+ </div>
1038
+ </div>
1039
+ )}
1040
+
1041
+ {currentView === 'activity' && (
1042
+ <div>
1043
+ {activityList.length === 0 ? (
1044
+ <p style={{ color: '#64748b' }}>No activity yet</p>
1045
+ ) : (
1046
+ <div>
1047
+ {activityList.slice(-30).map((event: any, idx: number) => (
1048
+ <div key={idx} style={itemStyle}>
1049
+ <div style={{ fontSize: '12px', color: '#64748b' }}>
1050
+ {new Date(event.timestamp).toLocaleString()}
805
1051
  </div>
806
- </>
807
- )}
1052
+ <div style={{ fontWeight: 'bold' }}>{event.type}</div>
1053
+ {event.entityName && (
1054
+ <div style={{ fontSize: '14px', marginTop: '4px' }}>
1055
+ {event.entityName}
1056
+ </div>
1057
+ )}
1058
+ </div>
1059
+ ))}
1060
+ </div>
1061
+ )}
1062
+ </div>
1063
+ )}
1064
+
1065
+ {currentView === 'inspector' && (
1066
+ <div>
1067
+ <h3>Data Inspector</h3>
1068
+ <p style={{ color: '#64748b', marginBottom: '16px' }}>
1069
+ Developer panel showing FeltDB internals
1070
+ </p>
1071
+ <div style={statCardStyle}>
1072
+ <div style={{ fontWeight: 'bold', marginBottom: '8px' }}>Collections</div>
1073
+ <div style={{ fontSize: '14px', color: '#64748b', lineHeight: '1.6' }}>
1074
+ 📦 projects: {projectsList.length} records<br/>
1075
+ 📦 tasks: {tasksList.length} records<br/>
1076
+ 📦 activity: {activityList.length} events<br/>
808
1077
  </div>
809
- ))}
1078
+ </div>
1079
+ <div style={statCardStyle}>
1080
+ <div style={{ fontWeight: 'bold', marginBottom: '8px' }}>Storage</div>
1081
+ <div style={{ fontSize: '14px', color: '#64748b', lineHeight: '1.6' }}>
1082
+ 💾 Type: IndexedDB<br/>
1083
+ 🔑 Namespace: FeltDB<br/>
1084
+ 🔄 Sync: Automatic
1085
+ </div>
1086
+ </div>
1087
+ <div style={statCardStyle}>
1088
+ <div style={{ fontWeight: 'bold', marginBottom: '8px' }}>Indexes</div>
1089
+ <div style={{ fontSize: '14px', color: '#64748b', lineHeight: '1.6' }}>
1090
+ 📌 projects.status<br/>
1091
+ 📌 tasks.projectId<br/>
1092
+ 📌 tasks.status<br/>
1093
+ 📌 tasks.priority<br/>
1094
+ 📌 activity.timestamp
1095
+ </div>
1096
+ </div>
810
1097
  </div>
811
1098
  )}
812
- </section>
813
- ${agentMarkup}
814
- </main>
815
-
816
- <style>{\`
817
- * { box-sizing: border-box; }
818
-
819
- body {
820
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
821
- margin: 0;
822
- padding: 0;
823
- background: #f5f7fa;
824
- color: #333;
825
- }
826
-
827
- .app {
828
- max-width: 1400px;
829
- margin: 0 auto;
830
- padding: 20px;
831
- }
832
-
833
- header {
834
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
835
- color: white;
836
- padding: 40px 30px;
837
- border-radius: 12px;
838
- margin-bottom: 30px;
839
- box-shadow: 0 8px 24px rgba(102, 126, 234, 0.3);
840
- }
841
-
842
- header h1 {
843
- margin: 0 0 10px 0;
844
- font-size: 32px;
845
- font-weight: 700;
846
- }
847
-
848
- header p {
849
- margin: 0;
850
- opacity: 0.95;
851
- font-size: 16px;
852
- }
853
-
854
- main {
855
- background: white;
856
- padding: 30px;
857
- border-radius: 12px;
858
- box-shadow: 0 2px 12px rgba(0,0,0,0.08);
859
- }
860
-
861
- .stats {
862
- display: grid;
863
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
864
- gap: 20px;
865
- margin-bottom: 30px;
866
- }
867
-
868
- .stat {
869
- padding: 24px;
870
- background: linear-gradient(135deg, #f5f7fa 0%, #f9f9f9 100%);
871
- border-radius: 8px;
872
- border-left: 4px solid #667eea;
873
- box-shadow: 0 1px 3px rgba(0,0,0,0.05);
874
- }
875
-
876
- .stat .label {
877
- display: block;
878
- color: #999;
879
- font-size: 10px;
880
- text-transform: uppercase;
881
- letter-spacing: 1px;
882
- margin-bottom: 8px;
883
- font-weight: 700;
884
- }
885
-
886
- .stat .value {
887
- display: block;
888
- font-size: 32px;
889
- font-weight: 700;
890
- color: #667eea;
891
- }
892
-
893
- .error-message {
894
- padding: 16px 20px;
895
- background: #fff5f5;
896
- border: 1px solid #fca5a5;
897
- border-left: 4px solid #f56565;
898
- border-radius: 8px;
899
- margin-bottom: 20px;
900
- display: flex;
901
- justify-content: space-between;
902
- align-items: center;
903
- color: #c53030;
904
- }
905
-
906
- .error-message strong { font-weight: 600; }
907
-
908
- .retry-btn {
909
- background: #f56565;
910
- padding: 8px 16px;
911
- font-size: 12px;
912
- margin-left: 15px;
913
- border-radius: 4px;
914
- }
915
-
916
- .retry-btn:hover { background: #e53e3e; }
917
-
918
- .docs-header {
919
- display: flex;
920
- justify-content: space-between;
921
- align-items: center;
922
- margin-bottom: 24px;
923
- gap: 20px;
924
- }
925
-
926
- .docs-header h2 {
927
- margin: 0;
928
- color: #1a202c;
929
- font-size: 26px;
930
- flex: 1;
931
- }
932
-
933
- .docs-controls {
934
- display: flex;
935
- gap: 12px;
936
- flex: 1;
937
- max-width: 600px;
938
- }
939
-
940
- .search-input {
941
- flex: 1;
942
- padding: 10px 16px;
943
- border: 1px solid #e2e8f0;
944
- border-radius: 6px;
945
- font-size: 14px;
946
- transition: all 0.2s ease;
947
- }
948
-
949
- .search-input:focus {
950
- outline: none;
951
- border-color: #667eea;
952
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
953
- }
954
-
955
- .loading {
956
- text-align: center;
957
- padding: 60px 20px;
958
- color: #999;
959
- }
960
-
961
- .spinner {
962
- display: inline-block;
963
- width: 48px;
964
- height: 48px;
965
- border: 4px solid #e2e8f0;
966
- border-top-color: #667eea;
967
- border-radius: 50%;
968
- animation: spin 0.8s linear infinite;
969
- margin-bottom: 20px;
970
- }
971
-
972
- @keyframes spin { to { transform: rotate(360deg); } }
973
-
974
- .empty-state {
975
- text-align: center;
976
- padding: 60px 20px;
977
- color: #999;
978
- }
979
-
980
- .empty-state p { margin: 8px 0; font-size: 16px; }
981
- .empty-state .hint { font-size: 14px; opacity: 0.8; }
982
-
983
- .docs-grid {
984
- display: grid;
985
- grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
986
- gap: 20px;
987
- }
988
-
989
- .doc-card {
990
- background: #f9f9f9;
991
- border: 1px solid #e2e8f0;
992
- border-radius: 8px;
993
- padding: 20px;
994
- transition: all 0.3s ease;
995
- display: flex;
996
- flex-direction: column;
997
- }
998
-
999
- .doc-card:hover {
1000
- border-color: #667eea;
1001
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
1002
- transform: translateY(-2px);
1003
- }
1004
-
1005
- .doc-card h3 {
1006
- margin: 0 0 12px 0;
1007
- color: #1a202c;
1008
- font-size: 18px;
1009
- font-weight: 600;
1010
- word-break: break-word;
1011
- }
1012
-
1013
- .doc-preview {
1014
- flex: 1;
1015
- margin: 0 0 12px 0;
1016
- color: #666;
1017
- font-size: 14px;
1018
- line-height: 1.6;
1019
- display: -webkit-box;
1020
- -webkit-line-clamp: 3;
1021
- -webkit-box-orient: vertical;
1022
- overflow: hidden;
1023
- }
1024
-
1025
- .doc-meta {
1026
- display: flex;
1027
- justify-content: space-between;
1028
- align-items: center;
1029
- padding: 12px 0;
1030
- border-top: 1px solid #e2e8f0;
1031
- border-bottom: 1px solid #e2e8f0;
1032
- margin-bottom: 12px;
1033
- font-size: 12px;
1034
- color: #999;
1035
- }
1036
-
1037
- .doc-size {
1038
- background: #f0f4ff;
1039
- padding: 2px 8px;
1040
- border-radius: 4px;
1041
- color: #667eea;
1042
- font-weight: 600;
1043
- }
1044
-
1045
- .doc-actions {
1046
- display: flex;
1047
- gap: 8px;
1048
- }
1049
-
1050
- .edit-btn, .delete-btn, .save-btn, .cancel-btn {
1051
- flex: 1;
1052
- padding: 8px 12px;
1053
- font-size: 13px;
1054
- border: 1px solid;
1055
- border-radius: 6px;
1056
- cursor: pointer;
1057
- font-weight: 600;
1058
- transition: all 0.2s ease;
1059
- }
1060
-
1061
- .edit-btn {
1062
- background: #eef2ff;
1063
- border-color: #667eea;
1064
- color: #667eea;
1065
- }
1066
-
1067
- .edit-btn:hover { background: #e0e7ff; }
1068
-
1069
- .delete-btn {
1070
- background: #fee;
1071
- border-color: #fca5a5;
1072
- color: #c53030;
1073
- }
1074
-
1075
- .delete-btn:hover:not(:disabled) { background: #fdd; }
1076
- .delete-btn:disabled { opacity: 0.6; cursor: not-allowed; }
1077
-
1078
- .edit-mode {
1079
- display: flex;
1080
- flex-direction: column;
1081
- gap: 12px;
1082
- }
1083
-
1084
- .edit-title, .edit-content {
1085
- padding: 10px 12px;
1086
- border: 1px solid #ddd;
1087
- border-radius: 6px;
1088
- font-family: inherit;
1089
- font-size: 14px;
1090
- }
1091
-
1092
- .edit-title {
1093
- font-size: 16px;
1094
- font-weight: 600;
1095
- }
1096
-
1097
- .edit-title:focus, .edit-content:focus {
1098
- outline: none;
1099
- border-color: #667eea;
1100
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
1101
- }
1102
-
1103
- .edit-actions {
1104
- display: flex;
1105
- gap: 8px;
1106
- }
1107
-
1108
- .save-btn {
1109
- background: #667eea;
1110
- color: white;
1111
- border: none;
1112
- flex: 1;
1113
- }
1114
-
1115
- .save-btn:hover { background: #5568d3; }
1116
-
1117
- .cancel-btn {
1118
- background: #f0f0f0;
1119
- color: #666;
1120
- border: none;
1121
- flex: 1;
1122
- }
1123
-
1124
- .cancel-btn:hover { background: #e0e0e0; }
1125
-
1126
- .primary-btn {
1127
- background: #667eea;
1128
- color: white;
1129
- border: none;
1130
- padding: 10px 20px;
1131
- white-space: nowrap;
1132
- }
1133
-
1134
- .primary-btn:hover:not(:disabled) { background: #5568d3; }
1135
- .primary-btn:disabled { opacity: 0.6; cursor: not-allowed; }
1136
-
1137
- textarea, input {
1138
- display: block;
1139
- width: 100%;
1140
- margin: 12px 0;
1141
- padding: 12px;
1142
- font-family: inherit;
1143
- font-size: 14px;
1144
- border: 1px solid #ddd;
1145
- border-radius: 6px;
1146
- resize: vertical;
1147
- }
1148
-
1149
- textarea:focus, input:focus {
1150
- outline: none;
1151
- border-color: #667eea;
1152
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
1153
- }
1154
-
1155
- .status-badge {
1156
- display: inline-block;
1157
- padding: 8px 12px;
1158
- background: #f0f4ff;
1159
- color: #667eea;
1160
- border-radius: 6px;
1161
- font-size: 12px;
1162
- font-weight: 600;
1163
- margin-bottom: 16px;
1164
- }
1165
-
1166
- .researcher {
1167
- border-top: 2px solid #e2e8f0;
1168
- margin-top: 40px;
1169
- padding-top: 30px;
1170
- }
1171
-
1172
- .researcher h2 {
1173
- margin-top: 0;
1174
- color: #1a202c;
1175
- font-size: 20px;
1176
- }
1177
-
1178
- .research-btn {
1179
- background: #667eea;
1180
- color: white;
1181
- border: none;
1182
- padding: 12px 24px;
1183
- margin: 16px 0;
1184
- border-radius: 6px;
1185
- cursor: pointer;
1186
- font-weight: 600;
1187
- transition: all 0.2s ease;
1188
- }
1189
-
1190
- .research-btn:hover:not(:disabled) { background: #5568d3; }
1191
- .research-btn:disabled { opacity: 0.6; cursor: not-allowed; }
1192
-
1193
- .research-result {
1194
- background: #f5f8ff;
1195
- border: 1px solid #d4e0ff;
1196
- border-radius: 8px;
1197
- padding: 16px;
1198
- margin-top: 20px;
1199
- }
1200
-
1201
- .ai-modes {
1202
- display: flex;
1203
- gap: 16px;
1204
- margin-bottom: 16px;
1205
- flex-wrap: wrap;
1206
- }
1207
-
1208
- .mode-group {
1209
- display: flex;
1210
- flex-direction: column;
1211
- gap: 6px;
1212
- }
1213
-
1214
- .mode-group-label {
1215
- font-size: 11px;
1216
- font-weight: 700;
1217
- text-transform: uppercase;
1218
- color: #999;
1219
- letter-spacing: 0.5px;
1220
- }
1221
-
1222
- .mode-group {
1223
- display: flex;
1224
- gap: 6px;
1225
- flex-wrap: wrap;
1226
- }
1227
-
1228
- .mode-btn {
1229
- background: #f0f4ff;
1230
- color: #667eea;
1231
- border: 1px solid #d4e0ff;
1232
- padding: 8px 14px;
1233
- border-radius: 6px;
1234
- cursor: pointer;
1235
- font-size: 12px;
1236
- font-weight: 600;
1237
- transition: all 0.2s ease;
1238
- }
1239
-
1240
- .mode-btn:hover:not(:disabled) {
1241
- background: #e0e7ff;
1242
- border-color: #667eea;
1243
- transform: translateY(-1px);
1244
- }
1245
-
1246
- .mode-btn.active {
1247
- background: #667eea;
1248
- color: white;
1249
- border-color: #667eea;
1250
- }
1251
-
1252
- .mode-btn.code-mode {
1253
- background: #fef5e7;
1254
- color: #c87832;
1255
- border-color: #f4d29d;
1256
- }
1257
-
1258
- .mode-btn.code-mode:hover:not(:disabled) {
1259
- background: #fdebd0;
1260
- border-color: #c87832;
1261
- }
1262
-
1263
- .mode-btn.code-mode.active {
1264
- background: #c87832;
1265
- color: white;
1266
- border-color: #c87832;
1267
- }
1268
-
1269
- .mode-btn:disabled {
1270
- opacity: 0.5;
1271
- cursor: not-allowed;
1272
- }
1273
-
1274
- .doc-selector {
1275
- margin-bottom: 16px;
1276
- }
1277
-
1278
- .doc-selector label {
1279
- display: block;
1280
- font-size: 13px;
1281
- font-weight: 600;
1282
- color: #666;
1283
- margin-bottom: 8px;
1284
- text-transform: uppercase;
1285
- letter-spacing: 0.5px;
1286
- }
1287
-
1288
- .doc-selector select {
1289
- width: 100%;
1290
- padding: 10px 12px;
1291
- border: 1px solid #ddd;
1292
- border-radius: 6px;
1293
- font-family: inherit;
1294
- font-size: 14px;
1295
- background: white;
1296
- }
1297
-
1298
- .doc-selector select:focus {
1299
- outline: none;
1300
- border-color: #667eea;
1301
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
1302
- }
1303
-
1304
- .research-result h3 {
1305
- margin: 0 0 12px 0;
1306
- color: #667eea;
1307
- font-size: 16px;
1308
- }
1309
-
1310
- .result-content {
1311
- color: #555;
1312
- line-height: 1.6;
1313
- font-size: 14px;
1314
- margin-bottom: 16px;
1315
- max-height: 400px;
1316
- overflow-y: auto;
1317
- padding: 12px;
1318
- background: white;
1319
- border-radius: 4px;
1320
- }
1321
-
1322
- .result-actions {
1323
- display: flex;
1324
- gap: 8px;
1325
- flex-wrap: wrap;
1326
- }
1327
-
1328
- .save-result-btn, .copy-result-btn {
1329
- flex: 1;
1330
- min-width: 140px;
1331
- background: #48bb78;
1332
- color: white;
1333
- border: none;
1334
- padding: 10px 16px;
1335
- border-radius: 6px;
1336
- cursor: pointer;
1337
- font-weight: 600;
1338
- font-size: 13px;
1339
- transition: all 0.2s ease;
1340
- }
1341
-
1342
- .save-result-btn:hover {
1343
- background: #38a169;
1344
- transform: translateY(-1px);
1345
- }
1346
-
1347
- .copy-result-btn {
1348
- background: #4299e1;
1349
- }
1350
-
1351
- .copy-result-btn:hover {
1352
- background: #3182ce;
1353
- transform: translateY(-1px);
1354
- }
1355
-
1356
- .code-note {
1357
- font-size: 12px;
1358
- color: #999;
1359
- margin-top: 12px;
1360
- font-style: italic;
1361
- border-top: 1px solid #e2e8f0;
1362
- padding-top: 12px;
1363
- }
1364
-
1365
- @media (max-width: 768px) {
1366
- .docs-header {
1367
- flex-direction: column;
1368
- align-items: stretch;
1369
- }
1370
- .docs-controls {
1371
- flex-direction: column;
1372
- max-width: 100%;
1373
- }
1374
- .docs-grid {
1375
- grid-template-columns: 1fr;
1376
- }
1377
- header { padding: 24px 20px; }
1378
- main { padding: 20px; }
1379
- }
1380
- \`}</style>
1099
+ </div>
1100
+ </div>
1381
1101
  </div>
1382
1102
  );
1383
1103
  }
1384
1104
  `;
1105
+ ;
1385
1106
  fs.writeFileSync(path.join(srcDir, 'App.tsx'), appTsx);
1386
1107
  const indexTsx = `import React from 'react';
1387
1108
  import ReactDOM from 'react-dom/client';
@@ -1430,12 +1151,20 @@ main().catch(console.error);
1430
1151
  # Copy this file to .env.local and update the values
1431
1152
 
1432
1153
  # API Key for authenticating with FeltDB servers
1433
- # Leave empty for browser runtime, required for self-hosted
1154
+ # Leave empty for browser runtime; required for authenticated self-hosted and managed runtimes
1434
1155
  VITE_FELTDB_API_KEY=
1435
1156
 
1436
- # FeltDB Server URL (for self-hosted runtime)
1157
+ # FeltDB Server URL (self-hosted or managed runtime)
1437
1158
  VITE_FELTDB_URL=http://localhost:7700
1438
1159
 
1160
+ # Managed example: https://runtime.your-app.feltdb.com
1161
+ VITE_FELTDB_MANAGED_URL=
1162
+ VITE_FELTDB_MANAGED_API_KEY=
1163
+ VITE_FELTDB_MANAGED_TENANT_ID=
1164
+ VITE_FELTDB_MANAGED_APPLICATION_ID=
1165
+ VITE_FELTDB_MANAGED_NAMESPACE=
1166
+ VITE_FELTDB_MANAGED_ENVIRONMENT=production
1167
+
1439
1168
  # Override the default self-hosted container image
1440
1169
  # Example: ghcr.io/rkendel1/feltdb:latest
1441
1170
  FELTDB_IMAGE=
@@ -1544,6 +1273,17 @@ npm run dev
1544
1273
 
1545
1274
  The self-hosted instance runs on \`http://localhost:7700\` by default.
1546
1275
 
1276
+ ## Managed Runtime
1277
+
1278
+ \`\`\`json
1279
+ {
1280
+ "runtime": "managed",
1281
+ "storage": "managed"
1282
+ }
1283
+ \`\`\`
1284
+
1285
+ Managed mode uses the same application API with a FeltDB-hosted endpoint. The CLI creates \`.env.local\` with \`VITE_FELTDB_MANAGED_URL\` and \`VITE_FELTDB_MANAGED_API_KEY\` after account setup. Studio uses that same connection for state, health, operations, and API-key administration.
1286
+
1547
1287
  ## Vector Search Status
1548
1288
 
1549
1289
  ### Current Status
@@ -1700,8 +1440,10 @@ build/
1700
1440
  // Create README
1701
1441
  const readme = `# ${applicationName}
1702
1442
 
1703
- A FeltDB distributed application with agents and capabilities.
1704
- - **Runtime:** ${runtime}
1443
+ A FeltDB Workspace application demonstrating local-first database capabilities.
1444
+
1445
+ **Deployment Target:** \`${runtime}\`
1446
+ - **Runtime:** ${runtime === 'browser' ? 'Browser (IndexedDB)' : runtime === 'node' ? 'Node.js Server' : runtime === 'managed' ? 'Managed FeltDB' : 'Self-hosted Docker'}
1705
1447
  - **Framework:** ${framework}
1706
1448
  - **Distributed:** ${distributed ? 'Yes' : 'No'}
1707
1449
  - **Agents:** ${hasAgents ? 'Yes' : 'No'}
@@ -1714,21 +1456,42 @@ npm install
1714
1456
  npm run dev
1715
1457
  \`\`\`
1716
1458
 
1717
- The application will be available at http://localhost:5173.
1459
+ The application will be available at http://localhost:5173 (or your configured port).
1460
+
1461
+ ## FeltDB Workspace Showcase
1462
+
1463
+ This application demonstrates core FeltDB capabilities:
1464
+
1465
+ ✅ **Collections & Relationships** - Projects, Tasks, and Activity collections with foreign key relationships
1466
+ ✅ **Indexed Querying** - Efficient queries by projectId, status, priority, and timestamp
1467
+ ✅ **Activity/Audit Logs** - Append-only event collection demonstrating immutable history
1468
+ ✅ **Persistent Storage** - Data persists through page reloads (IndexedDB)
1469
+ ✅ **Reactive State** - Real-time updates using React hooks
1470
+ ✅ **Local-first** - Works offline with no server required
1471
+ ✅ **Data Inspector** - Built-in developer panel to inspect collections and indexes
1472
+
1473
+ ### Application Features
1474
+
1475
+ - **Dashboard** - Overview of projects, tasks, and runtime status
1476
+ - **Projects** - Create and manage projects with descriptions
1477
+ - **Tasks** - Create tasks within projects with priority and status
1478
+ - **Activity Log** - Append-only event history of all changes
1479
+ - **Search** - Indexed searching across task titles and descriptions
1480
+ - **Data Inspector** - Developer panel showing collections, indexes, and storage info
1718
1481
 
1719
1482
  ## Project Structure
1720
1483
 
1721
1484
  \`\`\`
1722
1485
  ${applicationName}/
1723
- ├── feltdb/ # FeltDB configuration and logic
1486
+ ├── feltdb/ # FeltDB configuration
1724
1487
  │ ├── agents/ # Agent definitions
1725
1488
  │ ├── capabilities/ # Capability implementations
1726
1489
  │ ├── workflows/ # Workflow definitions
1727
1490
  │ └── schema/ # Data schemas
1728
1491
  ├── src/ # Application source code
1729
- │ ├── App.${framework === 'react' ? 'tsx' : 'js'}
1730
- │ ├── feltdb.ts # FeltDB client initialization
1731
- │ └── index.${framework === 'react' ? 'tsx' : 'js'}
1492
+ │ ├── App.${framework === 'react' ? 'tsx' : 'js'} # Main workspace UI
1493
+ │ ├── feltdb.ts # FeltDB collections and operations
1494
+ │ └── index.${framework === 'react' ? 'tsx' : 'js'} # Entry point
1732
1495
  ├── public/ # Static assets
1733
1496
  ├── index.html
1734
1497
  ├── .feltdb/ # Local FeltDB configuration
@@ -1739,37 +1502,63 @@ ${applicationName}/
1739
1502
  └── README.md
1740
1503
  \`\`\`
1741
1504
 
1742
- ## Runtime Options
1743
-
1744
- ### Browser (\`browser\`)
1745
- - Local-first, client-side only
1746
- - Uses browser OPFS (Origin Private File System) for storage
1747
- - No server required
1748
- - Best for: Offline-first apps, privacy-focused applications
1749
- - Limitations: Single device scope (data stays local)
1750
-
1751
- ### Node.js (\`node\`)
1752
- - Server-side Node.js runtime
1753
- - In-memory or file-based storage
1754
- - Suitable for APIs and backend services
1755
- - Best for: Server-side applications, REST APIs
1756
- - Limitations: Memory-based by default
1757
-
1758
- ### Self-Hosted (\`self-hosted\`)
1759
- - Dedicated FeltDB server instance
1760
- - Durable storage with distributed capabilities
1761
- - Requires Docker (image: ghcr.io/rkendel1/feltdb)
1762
- - Best for: Production deployments, multi-user systems
1763
- - Setup: \`npm run dev\` starts the Docker container automatically
1505
+ ## FeltDB Runtime
1506
+
1507
+ This project was created with the **${runtime === 'browser' ? 'Browser' : runtime === 'node' ? 'Node.js Server' : runtime === 'managed' ? 'Managed' : 'Self-hosted'}** runtime.
1508
+
1509
+ ### Browser Runtime
1510
+
1511
+ FeltDB runs locally in the browser and persists through IndexedDB.
1512
+
1513
+ - ✅ Zero server/database infrastructure
1514
+ - Works completely offline
1515
+ - Data stays on your device (privacy-first)
1516
+ - ✅ Best for: Client-side apps, offline-first experiences, prototypes
1517
+
1518
+ **Data Persistence:** IndexedDB (OPFS fallback)
1519
+
1520
+ ### Node.js Runtime
1521
+
1522
+ FeltDB runs as a Node.js server with server-side authority.
1523
+
1524
+ - Server-side FeltDB authority
1525
+ - Persistent file-based storage
1526
+ - Multi-client capable
1527
+ - ✅ Best for: Server-side applications, APIs, backend services
1528
+
1529
+ **Data Persistence:** File-based storage
1530
+
1531
+ ### Self-hosted Runtime
1532
+
1533
+ FeltDB runs through a dedicated server with Docker Compose and persistent data volume.
1534
+
1535
+ - ✅ Production-ready deployment
1536
+ - ✅ Multi-node replication
1537
+ - ✅ Health checks and orchestration
1538
+ - ✅ Best for: Production deployments, multi-user systems
1539
+
1540
+ **Data Persistence:** Docker volume (persistent /data)
1541
+
1542
+ ### Managed Runtime
1543
+
1544
+ FeltDB provides the runtime endpoint, durable storage, synchronization, and background workload infrastructure. The CLI configures \`VITE_FELTDB_MANAGED_URL\` and \`VITE_FELTDB_MANAGED_API_KEY\` in \`.env.local\`.
1764
1545
 
1765
1546
  ## Configuration
1766
1547
 
1767
1548
  Configuration is in \`feltdb.config.json\`:
1768
- - \`runtime\`: ${runtime}
1769
- - \`storage\`: ${runtime === 'browser' ? 'opfs' : 'durable'}
1770
- - \`distributed\`: ${distributed}
1771
- - \`agents.enabled\`: ${hasAgents}
1772
- - \`capabilities\`: ${capabilities}
1549
+
1550
+ \`\`\`json
1551
+ {
1552
+ "namespace": "${applicationName}",
1553
+ "runtime": "${runtime}",
1554
+ "storage": "${runtime === 'browser' ? 'indexeddb' : runtime === 'managed' ? 'managed' : 'durable'}",
1555
+ "distributed": ${distributed},
1556
+ "agents": {
1557
+ "enabled": ${hasAgents}
1558
+ },
1559
+ "capabilities": ["${capabilities}"]
1560
+ }
1561
+ \`\`\`
1773
1562
 
1774
1563
  ### Environment Variables
1775
1564
 
@@ -1778,9 +1567,12 @@ Create a \`.env.local\` file (copy from \`.env.example\`):
1778
1567
  \`\`\`
1779
1568
  VITE_FELTDB_API_KEY=your_api_key_here
1780
1569
  VITE_FELTDB_URL=http://localhost:7700
1570
+ VITE_FELTDB_MANAGED_API_KEY=your_managed_api_key_here
1571
+ VITE_FELTDB_MANAGED_URL=https://api.feltdb.com
1572
+ VITE_FELTDB_WEBSOCKET_URL=ws://localhost:7700
1781
1573
  \`\`\`
1782
1574
 
1783
- These are used when connecting to a self-hosted FeltDB instance.
1575
+ These are required when connecting to an authenticated self-hosted or managed FeltDB instance.
1784
1576
 
1785
1577
  ## Development
1786
1578
 
@@ -1804,59 +1596,95 @@ npm run feltdb:validate
1804
1596
  npm run feltdb:status
1805
1597
  \`\`\`
1806
1598
 
1807
- ## Database Operations
1808
-
1809
- ### Collections
1599
+ ### Open FeltDB Studio
1600
+ \`\`\`bash
1601
+ npm run feltdb:studio
1602
+ \`\`\`
1810
1603
 
1811
- The application includes pre-configured collections:
1812
- - \`documents\`: Stores research documents
1813
- - \`reports\`: Stores generated reports
1604
+ ## Database Schema
1814
1605
 
1815
- ### Schema
1606
+ ### Collections
1816
1607
 
1817
- Review \`feltdb.flow\` for the complete schema and workflow definitions.
1608
+ #### projects
1609
+ - **id** (string): Unique project identifier
1610
+ - **name** (string): Project name
1611
+ - **description** (string): Project description
1612
+ - **status** (string): 'active' | 'archived' | 'completed'
1613
+ - **createdAt** (string): ISO timestamp
1614
+ - **updatedAt** (string): ISO timestamp
1615
+ - **metadata** (object, optional): Custom metadata
1616
+
1617
+ **Indexes:** status
1618
+
1619
+ #### tasks
1620
+ - **id** (string): Unique task identifier
1621
+ - **projectId** (string): Foreign key to projects
1622
+ - **title** (string): Task title
1623
+ - **description** (string): Task description
1624
+ - **status** (string): 'todo' | 'in-progress' | 'completed'
1625
+ - **priority** (string): 'low' | 'medium' | 'high'
1626
+ - **assignee** (string, optional): Assigned team member
1627
+ - **createdAt** (string): ISO timestamp
1628
+ - **updatedAt** (string): ISO timestamp
1629
+
1630
+ **Indexes:** projectId, status, priority
1631
+
1632
+ #### activity
1633
+ - **id** (string): Unique event identifier
1634
+ - **timestamp** (string): ISO timestamp
1635
+ - **type** (string): Event type
1636
+ - **entityType** (string): 'project' | 'task'
1637
+ - **entityId** (string): Reference to entity
1638
+ - **entityName** (string): Name of entity
1639
+ - **changes** (object, optional): Changed fields
1640
+ - **userId** (string, optional): User who made change
1641
+
1642
+ **Indexes:** timestamp, (entityType, entityId)
1818
1643
 
1819
1644
  ## Agents
1820
1645
 
1821
- ${hasAgents ? `### Researcher Agent
1822
- The \`researcher\` agent runs real private inference using \`@feltdb/webllm\`:
1823
- - Runs entirely in the browser (no data sent to servers)
1646
+ ${hasAgents ? `### Available Agents
1647
+ The application includes autonomous agents powered by \`@feltdb/webllm\`:
1648
+ - Runs entirely in the browser (private, no external APIs)
1824
1649
  - Model downloads on first use
1825
1650
  - Inference runs in a Web Worker
1826
- - Generated reports are stored in FeltDB
1827
- - Learn more: https://github.com/mlc-ai/web-llm` : 'No agents configured. Add agents by re-running create-feltdb.'}
1828
-
1829
- ## Vector Search
1830
-
1831
- ${capabilities.includes('vector') ? `Vector search is enabled. Configure your vector storage backend in \`feltdb.config.json\`.` : `Vector search is not enabled. To add it, update \`feltdb.config.json\` to include \`"vector-search": true\` in capabilities.`}
1651
+ - Learn more: https://github.com/mlc-ai/web-llm` : 'No agents configured. To enable agents, re-run create-feltdb or add them to feltdb.config.json.'}
1832
1652
 
1833
1653
  ## Troubleshooting
1834
1654
 
1835
- ### "Model not loaded" in Researcher
1836
- If the WebLLM Researcher shows "Model not loaded":
1655
+ ### Data not persisting
1837
1656
  1. Check browser console for errors
1838
- 2. Ensure sufficient disk space (models are ~2-3GB)
1839
- 3. Try in a private/incognito window if localStorage is full
1657
+ 2. Verify IndexedDB is enabled (not in private mode)
1658
+ 3. Check browser storage quota
1840
1659
  4. Clear browser cache and try again
1841
1660
 
1661
+ ### Slow performance
1662
+ 1. Check browser DevTools Performance tab
1663
+ 2. Verify indexes are being used (check Data Inspector)
1664
+ 3. Consider optimizing queries or collection size
1665
+
1842
1666
  ### Self-Hosted Connection Issues
1843
1667
  If using self-hosted mode and connection fails:
1844
1668
  1. Ensure Docker is installed and running
1845
- 2. Check FELTDB_URL and API key in .env.local
1669
+ 2. Check VITE_FELTDB_URL and API key in .env.local
1846
1670
  3. Run \`npm run feltdb:status\` to check server health
1847
- 4. View logs: \`docker logs feltdb\`
1671
+ 4. View Docker logs: \`docker logs feltdb\`
1848
1672
 
1849
- ### API Key Errors
1850
- If API key management fails in Studio:
1851
- 1. Verify VITE_FELTDB_URL is set correctly
1852
- 2. Ensure token has proper scopes
1853
- 3. Check CORS settings on self-hosted server
1673
+ ### Port conflicts
1674
+ If port 5173 is in use:
1675
+ 1. Change the port in \`vite.config.ts\`
1676
+ 2. Or: \`npm run dev -- --port 3000\`
1854
1677
 
1855
1678
  ## Learn More
1856
1679
 
1857
1680
  - [FeltDB Documentation](https://github.com/rkendel1/feltdb)
1858
- - [WebLLM Documentation](https://github.com/mlc-ai/web-llm)
1859
- - [Distribution & Capabilities](https://github.com/rkendel1/feltdb/docs/capabilities.md)
1681
+ - [FeltDB Architecture](https://github.com/rkendel1/feltdb/blob/main/ARCHITECTURE.md)
1682
+ - [WebLLM (In-browser LLM)](https://github.com/mlc-ai/web-llm)
1683
+ - [Getting Started Guide](https://github.com/rkendel1/feltdb/blob/main/GETTING_STARTED.md)
1684
+
1685
+ ## License
1686
+
1687
+ MIT
1860
1688
  `;
1861
1689
  fs.writeFileSync(path.join(projectDir, 'README.md'), readme);
1862
1690
  }