@feltdb/core 0.7.3 → 0.8.0
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.
- package/README.md +25 -0
- package/dist/application-development.d.ts +78 -0
- package/dist/application-development.d.ts.map +1 -0
- package/dist/application-development.js +153 -0
- package/dist/application-manifest.d.ts +6 -1
- package/dist/application-manifest.d.ts.map +1 -1
- package/dist/application-manifest.js +10 -3
- package/dist/canonical-application.d.ts +27 -0
- package/dist/canonical-application.d.ts.map +1 -0
- package/dist/canonical-application.js +46 -0
- package/dist/cli/ai-model-config.js +97 -0
- package/dist/cli/cli.js +1 -1
- package/dist/cli/commands.js +664 -120
- package/dist/cli/index.js +1 -1
- package/dist/cli/managed-environment.js +6 -0
- package/dist/cli/managed-project.js +6 -0
- package/dist/cli/proposal-client.js +63 -0
- package/dist/cli/publish-engine.js +165 -0
- package/dist/cli/source-sync.js +183 -0
- package/dist/create/cli.js +11 -2
- package/dist/create/create.js +38 -16
- package/dist/create/managed-account.js +82 -11
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
- package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
- package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
- package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
- package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
- package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
- package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
- package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
- package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
- package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
- package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
- package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
- package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
- package/dist/create/template/default-project/README.md +57 -0
- package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
- package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
- package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
- package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
- package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
- package/dist/create/template/default-project/feltdb.flow +174 -0
- package/dist/create/template/default-project/src/App.tsx +20 -0
- package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
- package/dist/create/template/default-project/src/feltdb.ts +87 -0
- package/dist/create/template/default-project/src/index.tsx +14 -0
- package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
- package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
- package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
- package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
- package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
- package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
- package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
- package/dist/create/template/default-project/src/styles-application.css +21 -0
- package/dist/create/template/default-project/src/styles.css +1 -0
- package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
- package/dist/create/template/default-project/workflows/invitation.ts +30 -0
- package/dist/create/template/default-project/workflows/project-created.ts +2 -0
- package/dist/db.d.ts +49 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +77 -0
- package/dist/error-codes.d.ts +20 -1
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +25 -0
- package/dist/flowspec.d.ts +1 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +66 -11
- package/dist/http-client.d.ts +135 -14
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +79 -31
- package/dist/http-db.d.ts +15 -2
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +113 -5
- package/dist/index-core.d.ts +5 -0
- package/dist/index-core.d.ts.map +1 -1
- package/dist/index-core.js +5 -0
- package/dist/module-intent.d.ts +37 -0
- package/dist/module-intent.d.ts.map +1 -0
- package/dist/module-intent.js +81 -0
- package/dist/module.d.ts +76 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +61 -0
- package/dist/proposal.d.ts +161 -0
- package/dist/proposal.d.ts.map +1 -0
- package/dist/proposal.js +232 -0
- package/dist/studio/app.d.ts +5 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/AskFeltDB.d.ts +21 -0
- package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
- package/dist/studio/components/ContractInspector.d.ts +7 -0
- package/dist/studio/components/ContractInspector.d.ts.map +1 -0
- package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
- package/dist/studio/components/ManagedInstancePanel.js +1 -5
- package/dist/studio/components/ModuleExplorer.d.ts +6 -0
- package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
- package/dist/studio/components/ProposalBrowser.d.ts +12 -0
- package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
- package/dist/studio/components/StateExplorer.d.ts +6 -7
- package/dist/studio/components/StateExplorer.d.ts.map +1 -1
- package/dist/studio/components/index.d.ts +4 -0
- package/dist/studio/components/index.d.ts.map +1 -1
- package/dist/studio/components/index.js +4 -4
- package/dist/studio/components-CNSNrmA9.js +2787 -0
- package/dist/studio/index.js +214 -157
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/index.d.ts +2 -0
- package/dist/studio/utils/index.d.ts.map +1 -1
- package/dist/studio/utils/index.js +14 -12
- package/dist/studio/utils/managed-instance.d.ts +0 -4
- package/dist/studio/utils/managed-instance.d.ts.map +1 -1
- package/dist/studio/utils/managed-instance.js +0 -4
- package/dist/studio/utils/proposal-api.d.ts +53 -0
- package/dist/studio/utils/proposal-api.d.ts.map +1 -0
- package/dist/studio/utils/proposal-api.js +91 -0
- package/dist/studio/utils/service-api.d.ts +81 -0
- package/dist/studio/utils/service-api.d.ts.map +1 -0
- package/dist/studio/utils/service-api.js +51 -0
- package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
- package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
- package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
- package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
- package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
- package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
- package/dist/studio-app/index.html +2 -2
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/local-development-authority.d.ts +3 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +124 -2
- package/package.json +1 -1
- package/dist/studio/components-Dxuhrv8_.js +0 -1832
- package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
- package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# My FeltDB App
|
|
2
|
+
|
|
3
|
+
This small application demonstrates FeltDB's complete programming model:
|
|
4
|
+
|
|
5
|
+
- Authentication and authorization
|
|
6
|
+
- Multi-tenant application state
|
|
7
|
+
- Atomic workflows
|
|
8
|
+
- Agents with bounded capabilities
|
|
9
|
+
- Reactive UI through `@feltdb/core/react`
|
|
10
|
+
- Shared inspection through FeltDB Studio
|
|
11
|
+
|
|
12
|
+
## Run it
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install
|
|
16
|
+
npm run feltdb:dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
In a second terminal:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm run dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then sign up, create a project, invite an existing user, and run an assistant. Open the same state in Studio:
|
|
26
|
+
|
|
27
|
+
## Choose an AI model
|
|
28
|
+
|
|
29
|
+
`npm run feltdb:ai` uses private browser-local WebLLM by default. Use
|
|
30
|
+
`feltdb ai model set --provider local-webllm --model <model-id>` for a larger
|
|
31
|
+
local model, or configure an OpenAI-compatible model with a credential supplied
|
|
32
|
+
through `--api-key-stdin`. `feltdb ai model show` displays the active choice.
|
|
33
|
+
Hosted credentials remain in the operating-system vault and are never exposed
|
|
34
|
+
to the application or Studio.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run feltdb:studio
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How the example fits together
|
|
41
|
+
|
|
42
|
+
- `feltdb.flow` defines state, authorization, workflows, capabilities, and agents.
|
|
43
|
+
- `workflows/` implements project creation, invitation lifecycle, and agent-assisted summary examples.
|
|
44
|
+
- `capabilities/` contains the narrow read and summary operations available to assistants.
|
|
45
|
+
- `agents/` registers the Project and Activity assistants and persists their execution lifecycle.
|
|
46
|
+
- `src/` contains the authenticated React application.
|
|
47
|
+
|
|
48
|
+
These folders make the concepts easy to find; they are not mandatory application architecture. Agents and workflows use the same FeltDB collections as the UI, so they do not bypass `authenticated`, `member`, `owner`, or `self(user)` policies.
|
|
49
|
+
|
|
50
|
+
For managed FeltDB, generate with `--runtime managed`; the model and application remain the same while the runtime configuration changes.
|
|
51
|
+
|
|
52
|
+
Preview and publish the same definition to its configured cloud environment:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm run feltdb:publish -- --dry-run
|
|
56
|
+
npm run feltdb:publish -- --yes
|
|
57
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { db, recordAgentActivity } from '../src/feltdb';
|
|
2
|
+
import { activitySummary, type ActivitySummary } from '../capabilities/activity-summary';
|
|
3
|
+
|
|
4
|
+
export const activityAssistant = db.defineAgent({ name: 'activity-assistant', version: 1, description: 'Explains authorized application activity.', capabilities: [activitySummary.id], goals: ['Summarize recent authorized audit events'], constraints: { maxIterations: 1 } });
|
|
5
|
+
|
|
6
|
+
export async function runActivityAssistant(input: { organization: string; user: string }): Promise<ActivitySummary> {
|
|
7
|
+
const execution = await db.createAgentExecution(activityAssistant, 'Summarize recent activity', []);
|
|
8
|
+
try {
|
|
9
|
+
await db.startAgentExecution(execution);
|
|
10
|
+
const result = await activitySummary.execute() as ActivitySummary;
|
|
11
|
+
await recordAgentActivity({ organization: input.organization, user: input.user, assistant: 'Activity Assistant', subject: execution.executionId });
|
|
12
|
+
await db.completeAgentExecution(execution, execution.executionId);
|
|
13
|
+
return result;
|
|
14
|
+
} catch (error) {
|
|
15
|
+
await db.failAgentExecution(execution, error instanceof Error ? error.message : String(error));
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { db, recordAgentActivity } from '../src/feltdb';
|
|
2
|
+
import { projectSearch } from '../capabilities/project-search';
|
|
3
|
+
import { projectSummary, type ProjectSummary } from '../capabilities/project-summary';
|
|
4
|
+
|
|
5
|
+
export const projectAssistant = db.defineAgent({ name: 'project-assistant', version: 1, description: 'Summarizes authorized project state.', capabilities: [projectSearch.id, projectSummary.id], goals: ['Explain project state without widening the actor authorization boundary'], constraints: { maxIterations: 1 } });
|
|
6
|
+
|
|
7
|
+
export async function runProjectAssistant(input: { projectId: string; organization: string; user: string }): Promise<ProjectSummary> {
|
|
8
|
+
const execution = await db.createAgentExecution(projectAssistant, 'Summarize the selected project', [input.projectId]);
|
|
9
|
+
try {
|
|
10
|
+
await db.startAgentExecution(execution);
|
|
11
|
+
const result = await projectSummary.execute(input.projectId) as ProjectSummary;
|
|
12
|
+
await recordAgentActivity({ organization: input.organization, user: input.user, assistant: 'Project Assistant', subject: input.projectId });
|
|
13
|
+
await db.completeAgentExecution(execution, input.projectId);
|
|
14
|
+
return result;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
await db.failAgentExecution(execution, error instanceof Error ? error.message : String(error));
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Capability } from '@feltdb/core';
|
|
2
|
+
import { auditEvents, type AuditEvent } from '../src/feltdb';
|
|
3
|
+
|
|
4
|
+
export interface ActivitySummary { summary: string; total: number; actions: string[] }
|
|
5
|
+
|
|
6
|
+
export const activitySummary: Capability = {
|
|
7
|
+
id: 'activity-summary',
|
|
8
|
+
name: 'Activity summary',
|
|
9
|
+
async execute(): Promise<ActivitySummary> {
|
|
10
|
+
const events = (await auditEvents.all() as AuditEvent[]).slice(-20);
|
|
11
|
+
const actions = [...new Set(events.map(event => event.action))];
|
|
12
|
+
return { total: events.length, actions, summary: events.length ? `${events.length} recent events across ${actions.length} action types: ${actions.join(', ')}.` : 'No application activity has been recorded yet.' };
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Capability } from '@feltdb/core';
|
|
2
|
+
import { projects, type Project } from '../src/feltdb';
|
|
3
|
+
|
|
4
|
+
export interface ProjectSearchResult { id: string; name: string; description: string }
|
|
5
|
+
|
|
6
|
+
export const projectSearch: Capability = {
|
|
7
|
+
id: 'project-search',
|
|
8
|
+
name: 'Project search',
|
|
9
|
+
async execute(query: string): Promise<ProjectSearchResult[]> {
|
|
10
|
+
// The collection read crosses FeltDB's normal authorization boundary. The
|
|
11
|
+
// capability narrows results; it never grants access to additional rows.
|
|
12
|
+
const words = query.trim().toLowerCase();
|
|
13
|
+
const authorized = await projects.all() as Project[];
|
|
14
|
+
return authorized.filter(project => `${project.name} ${project.description}`.toLowerCase().includes(words)).map(({ id, name, description }) => ({ id, name, description }));
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Capability } from '@feltdb/core';
|
|
2
|
+
import { auditEvents, projects, type AuditEvent, type Project } from '../src/feltdb';
|
|
3
|
+
|
|
4
|
+
export interface ProjectSummary { title: string; summary: string; activityCount: number }
|
|
5
|
+
|
|
6
|
+
export const projectSummary: Capability = {
|
|
7
|
+
id: 'project-summary',
|
|
8
|
+
name: 'Project summary',
|
|
9
|
+
async execute(projectId: string): Promise<ProjectSummary> {
|
|
10
|
+
const project = await projects.get(projectId) as Project | null;
|
|
11
|
+
if (!project) throw new Error('Project is unavailable in the current authorization context.');
|
|
12
|
+
const activity = (await auditEvents.all() as AuditEvent[]).filter(event => event.resource_id === project.id);
|
|
13
|
+
return { title: project.name, summary: project.description || `${project.name} is active and ready for its next state transition.`, activityCount: activity.length };
|
|
14
|
+
},
|
|
15
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
flow_version 1
|
|
2
|
+
|
|
3
|
+
app FeltDBStarter {
|
|
4
|
+
// Optional billing semantics. Uncomment to include the Stripe-compatible module:
|
|
5
|
+
// module stripe {
|
|
6
|
+
// provider stripe-compatible
|
|
7
|
+
// version 1.0.0
|
|
8
|
+
// }
|
|
9
|
+
collection User {
|
|
10
|
+
email: text
|
|
11
|
+
name: text
|
|
12
|
+
created_at: datetime
|
|
13
|
+
index email using hash(email)
|
|
14
|
+
}
|
|
15
|
+
collection Organization {
|
|
16
|
+
name: text
|
|
17
|
+
created_at: datetime
|
|
18
|
+
}
|
|
19
|
+
collection Membership {
|
|
20
|
+
user: ref User
|
|
21
|
+
organization: ref Organization
|
|
22
|
+
role: text
|
|
23
|
+
created_at: datetime
|
|
24
|
+
index user using hash(user)
|
|
25
|
+
index organization using hash(organization)
|
|
26
|
+
}
|
|
27
|
+
collection Session {
|
|
28
|
+
user: ref User
|
|
29
|
+
expires_at: datetime
|
|
30
|
+
created_at: datetime
|
|
31
|
+
index user using hash(user)
|
|
32
|
+
}
|
|
33
|
+
collection Project {
|
|
34
|
+
organization: ref Organization
|
|
35
|
+
name: text
|
|
36
|
+
description: text
|
|
37
|
+
owner_id: text
|
|
38
|
+
created_at: datetime
|
|
39
|
+
updated_at: datetime
|
|
40
|
+
index organization using hash(organization)
|
|
41
|
+
}
|
|
42
|
+
collection Invitation {
|
|
43
|
+
organization: ref Organization
|
|
44
|
+
user: ref User
|
|
45
|
+
email: text
|
|
46
|
+
role: text
|
|
47
|
+
status: text
|
|
48
|
+
created_at: datetime
|
|
49
|
+
updated_at: datetime
|
|
50
|
+
index organization using hash(organization)
|
|
51
|
+
}
|
|
52
|
+
collection AuditEvent {
|
|
53
|
+
organization: ref Organization
|
|
54
|
+
user: ref User
|
|
55
|
+
action: text
|
|
56
|
+
resource_type: text
|
|
57
|
+
resource_id: text
|
|
58
|
+
details: text
|
|
59
|
+
created_at: datetime
|
|
60
|
+
index organization using hash(organization)
|
|
61
|
+
}
|
|
62
|
+
collection Team {
|
|
63
|
+
organization: ref Organization
|
|
64
|
+
name: text
|
|
65
|
+
description: text
|
|
66
|
+
owner: ref User
|
|
67
|
+
created_at: datetime
|
|
68
|
+
updated_at: datetime
|
|
69
|
+
index organization using hash(organization)
|
|
70
|
+
}
|
|
71
|
+
collection TeamMembership {
|
|
72
|
+
organization: ref Organization
|
|
73
|
+
team: ref Team
|
|
74
|
+
user: ref User
|
|
75
|
+
role: text
|
|
76
|
+
created_at: datetime
|
|
77
|
+
updated_at: datetime
|
|
78
|
+
index organization using hash(organization)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
capability ProjectSearch {
|
|
82
|
+
visibility public
|
|
83
|
+
read Project
|
|
84
|
+
}
|
|
85
|
+
capability ProjectSummary {
|
|
86
|
+
visibility agent
|
|
87
|
+
read Project
|
|
88
|
+
read AuditEvent
|
|
89
|
+
}
|
|
90
|
+
capability ActivitySummary {
|
|
91
|
+
visibility agent
|
|
92
|
+
read AuditEvent
|
|
93
|
+
}
|
|
94
|
+
capability RecordAgentActivity {
|
|
95
|
+
visibility internal
|
|
96
|
+
write AuditEvent
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
workflow ProjectCreated {
|
|
100
|
+
step persist_project {
|
|
101
|
+
write Project
|
|
102
|
+
}
|
|
103
|
+
step record_activity {
|
|
104
|
+
write AuditEvent
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
workflow InvitationLifecycle {
|
|
108
|
+
step create_invitation {
|
|
109
|
+
write Invitation
|
|
110
|
+
}
|
|
111
|
+
step accept_invitation {
|
|
112
|
+
write Membership
|
|
113
|
+
}
|
|
114
|
+
step record_activity {
|
|
115
|
+
write AuditEvent
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
workflow AgentAssistedSummary {
|
|
119
|
+
step read_authorized_state {
|
|
120
|
+
capability ProjectSummary
|
|
121
|
+
}
|
|
122
|
+
step record_activity {
|
|
123
|
+
capability RecordAgentActivity
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
agent ProjectAssistant {
|
|
128
|
+
capability ProjectSearch
|
|
129
|
+
capability ProjectSummary
|
|
130
|
+
capability RecordAgentActivity
|
|
131
|
+
workflow AgentAssistedSummary
|
|
132
|
+
}
|
|
133
|
+
agent ActivityAssistant {
|
|
134
|
+
capability ActivitySummary
|
|
135
|
+
capability RecordAgentActivity
|
|
136
|
+
workflow AgentAssistedSummary
|
|
137
|
+
}
|
|
138
|
+
policy User {
|
|
139
|
+
read: authenticated
|
|
140
|
+
write: owner
|
|
141
|
+
}
|
|
142
|
+
policy Organization {
|
|
143
|
+
read: member
|
|
144
|
+
write: owner
|
|
145
|
+
}
|
|
146
|
+
policy Membership {
|
|
147
|
+
read: member
|
|
148
|
+
write: member
|
|
149
|
+
}
|
|
150
|
+
policy Session {
|
|
151
|
+
read: self(user)
|
|
152
|
+
write: self(user)
|
|
153
|
+
}
|
|
154
|
+
policy Project {
|
|
155
|
+
read: member
|
|
156
|
+
write: member
|
|
157
|
+
}
|
|
158
|
+
policy Invitation {
|
|
159
|
+
read: self(user) | member
|
|
160
|
+
write: member
|
|
161
|
+
}
|
|
162
|
+
policy AuditEvent {
|
|
163
|
+
read: member
|
|
164
|
+
write: authenticated
|
|
165
|
+
}
|
|
166
|
+
policy Team {
|
|
167
|
+
read: member
|
|
168
|
+
write: member
|
|
169
|
+
}
|
|
170
|
+
policy TeamMembership {
|
|
171
|
+
read: member
|
|
172
|
+
write: member
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthProvider, useAuth } from './context/AuthContext';
|
|
2
|
+
import { Dashboard } from './pages/Dashboard';
|
|
3
|
+
import { SignIn } from './pages/SignIn';
|
|
4
|
+
import { SignUp } from './pages/SignUp';
|
|
5
|
+
import './styles.css';
|
|
6
|
+
import './styles-application.css';
|
|
7
|
+
|
|
8
|
+
function Application() {
|
|
9
|
+
const { authenticated, loading } = useAuth();
|
|
10
|
+
if (loading) return <main className="center"><div className="loader" /><p>Resolving your FeltDB session…</p></main>;
|
|
11
|
+
if (!authenticated) return <AuthScreen />;
|
|
12
|
+
return <Dashboard />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function AuthScreen() {
|
|
16
|
+
const signIn = new URLSearchParams(window.location.search).get('view') === 'signin';
|
|
17
|
+
return <main className="auth-layout"><section className="intro"><a className="brand" href="https://feltdb.com">FeltDB</a><div><span className="eyebrow">State first</span><h1>A real application, from the first command.</h1><p>Sign up to create a durable user, organization, membership, and session—then inspect all of it in Studio.</p></div><div className="flow"><span>Identity</span><i>→</i><span>Tenant</span><i>→</i><span>State</span></div></section><section className="auth-card">{signIn ? <SignIn /> : <SignUp />}</section></main>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function App() { return <AuthProvider><Application /></AuthProvider>; }
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
import { useCollection } from '@feltdb/core/react';
|
|
3
|
+
import { createAccount, createSession, db, memberships, organizations, sessions, users, type Membership, type Organization, type Session, type User } from '../feltdb';
|
|
4
|
+
|
|
5
|
+
const SESSION_KEY = 'feltdb.example.session';
|
|
6
|
+
interface AuthValue { loading: boolean; authenticated: boolean; user?: User; session?: Session; organization?: Organization; membership?: Membership; signUp(input: { name: string; email: string; password: string; organization: string }): Promise<void>; signIn(email: string, password: string): Promise<void>; signOut(): Promise<void> }
|
|
7
|
+
const AuthContext = createContext<AuthValue | undefined>(undefined);
|
|
8
|
+
|
|
9
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
10
|
+
const sessionRecords = useCollection<Session>(sessions); const userRecords = useCollection<User>(users); const membershipRecords = useCollection<Membership>(memberships); const organizationRecords = useCollection<Organization>(organizations);
|
|
11
|
+
const [sessionId, setSessionId] = useState<string | null>(null);
|
|
12
|
+
useEffect(() => setSessionId(sessionStorage.getItem(SESSION_KEY)), []);
|
|
13
|
+
const session = sessionRecords.data.find(item => item.id === sessionId && Date.parse(item.expires_at) > Date.now());
|
|
14
|
+
const user = userRecords.data.find(item => item.id === session?.user);
|
|
15
|
+
const membership = membershipRecords.data.find(item => item.user === user?.id);
|
|
16
|
+
const organization = organizationRecords.data.find(item => item.id === membership?.organization);
|
|
17
|
+
const loading = sessionRecords.loading || userRecords.loading || membershipRecords.loading || organizationRecords.loading;
|
|
18
|
+
const establish = (next: Session) => { sessionStorage.setItem(SESSION_KEY, next.id); setSessionId(next.id); };
|
|
19
|
+
const value = useMemo<AuthValue>(() => ({ loading, authenticated: Boolean(session && user && membership && organization), session, user, membership, organization,
|
|
20
|
+
signUp: async input => { await db.auth.signUp({ email: input.email, password: input.password, display_name: input.name }); establish(await createAccount(input)); },
|
|
21
|
+
signIn: async (email, password) => { await db.auth.signIn({ email, password }); establish(await createSession(email)); },
|
|
22
|
+
signOut: async () => { if (session) await sessions.delete(session.id); await db.auth.signOut(); sessionStorage.removeItem(SESSION_KEY); setSessionId(null); },
|
|
23
|
+
}), [loading, session, user, membership, organization]);
|
|
24
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function useAuth() { const value = useContext(AuthContext); if (!value) throw new Error('useAuth must be used inside AuthProvider'); return value; }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { configureDevelopmentRuntimeBridge, createFeltDB } from '@feltdb/core';
|
|
2
|
+
|
|
3
|
+
configureDevelopmentRuntimeBridge({
|
|
4
|
+
sessionId: import.meta.env.VITE_FELTDB_DEV_SESSION_ID,
|
|
5
|
+
workspaceId: import.meta.env.VITE_FELTDB_WORKSPACE_ID,
|
|
6
|
+
namespace: import.meta.env.VITE_FELTDB_NAMESPACE,
|
|
7
|
+
runtime: import.meta.env.VITE_FELTDB_RUNTIME,
|
|
8
|
+
authorityUrl: import.meta.env.VITE_FELTDB_AUTHORITY_URL,
|
|
9
|
+
bridgeUrl: import.meta.env.VITE_FELTDB_DEV_BRIDGE_URL,
|
|
10
|
+
applicationUrl: import.meta.env.VITE_FELTDB_APPLICATION_URL,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const managedRuntime = Boolean(import.meta.env.VITE_FELTDB_MANAGED_URL);
|
|
14
|
+
export const db = createFeltDB(managedRuntime ? {
|
|
15
|
+
namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || 'feltdb-starter',
|
|
16
|
+
server: {
|
|
17
|
+
url: import.meta.env.VITE_FELTDB_MANAGED_URL,
|
|
18
|
+
applicationId: import.meta.env.VITE_FELTDB_MANAGED_APPLICATION_ID,
|
|
19
|
+
environment: import.meta.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
|
|
20
|
+
},
|
|
21
|
+
} : { namespace: import.meta.env.VITE_FELTDB_NAMESPACE || 'feltdb-starter', browser: true });
|
|
22
|
+
|
|
23
|
+
export interface User { id: string; email: string; name: string; created_at: string }
|
|
24
|
+
export interface Organization { id: string; name: string; created_at: string }
|
|
25
|
+
export interface Membership { id: string; user: string; organization: string; role: 'owner' | 'member' | 'viewer'; created_at: string }
|
|
26
|
+
export interface Session { id: string; user: string; expires_at: string; created_at: string }
|
|
27
|
+
export interface Project { id: string; organization: string; name: string; description: string; owner_id: string; created_at: string; updated_at: string }
|
|
28
|
+
export interface Invitation { id: string; organization: string; user: string; email: string; role: string; status: string; created_at: string; updated_at: string }
|
|
29
|
+
export interface AuditEvent { id: string; organization: string; user: string; action: string; resource_type: string; resource_id: string; details: string; created_at: string }
|
|
30
|
+
export interface Team { id: string; organization: string; name: string; description: string; owner: string; created_at: string; updated_at: string }
|
|
31
|
+
export interface TeamMembership { id: string; organization: string; team: string; user: string; role: string; created_at: string; updated_at: string }
|
|
32
|
+
|
|
33
|
+
export const users = db.collection<User>('User');
|
|
34
|
+
export const organizations = db.collection<Organization>('Organization');
|
|
35
|
+
export const memberships = db.collection<Membership>('Membership');
|
|
36
|
+
export const sessions = db.collection<Session>('Session');
|
|
37
|
+
export const projects = db.collection<Project>('Project');
|
|
38
|
+
export const invitations = db.collection<Invitation>('Invitation');
|
|
39
|
+
export const auditEvents = db.collection<AuditEvent>('AuditEvent');
|
|
40
|
+
export const teams = db.collection<Team>('Team');
|
|
41
|
+
export const teamMemberships = db.collection<TeamMembership>('TeamMembership');
|
|
42
|
+
|
|
43
|
+
const makeId = (prefix: string) => `${prefix}_${crypto.randomUUID()}`;
|
|
44
|
+
|
|
45
|
+
export async function createAccount(input: { name: string; email: string; organization: string }) {
|
|
46
|
+
const email = input.email.trim().toLowerCase();
|
|
47
|
+
if ((await users.find({ email })).length) throw new Error('An account already exists for that email.');
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
const user: User = { id: makeId('user'), email, name: input.name.trim(), created_at: now };
|
|
50
|
+
const organization: Organization = { id: makeId('org'), name: input.organization.trim(), created_at: now };
|
|
51
|
+
const membership: Membership = { id: makeId('membership'), user: user.id, organization: organization.id, role: 'owner', created_at: now };
|
|
52
|
+
const session: Session = { id: makeId('session'), user: user.id, created_at: now, expires_at: new Date(Date.now() + 86_400_000).toISOString() };
|
|
53
|
+
const event: AuditEvent = { id: makeId('event'), organization: organization.id, user: user.id, action: 'account.created', resource_type: 'User', resource_id: user.id, details: 'Initial organization and owner membership created', created_at: now };
|
|
54
|
+
await db.transaction(async transaction => {
|
|
55
|
+
transaction.collection<User>('User').insert(user, user.id, { requireAbsent: true });
|
|
56
|
+
transaction.collection<Organization>('Organization').insert(organization, organization.id, { requireAbsent: true });
|
|
57
|
+
transaction.collection<Membership>('Membership').insert(membership, membership.id, { requireAbsent: true });
|
|
58
|
+
transaction.collection<Session>('Session').insert(session, session.id, { requireAbsent: true });
|
|
59
|
+
transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
|
|
60
|
+
}, { transactionId: makeId('signup') });
|
|
61
|
+
return session;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function createSession(emailInput: string) {
|
|
65
|
+
const user = (await users.find({ email: emailInput.trim().toLowerCase() }))[0];
|
|
66
|
+
if (!user) throw new Error('Account not found.');
|
|
67
|
+
const now = new Date().toISOString();
|
|
68
|
+
const session: Session = { id: makeId('session'), user: user.id, created_at: now, expires_at: new Date(Date.now() + 86_400_000).toISOString() };
|
|
69
|
+
await sessions.insert(session, session.id);
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function createProject(input: { organization: string; owner: string; name: string }) {
|
|
74
|
+
const now = new Date().toISOString();
|
|
75
|
+
const project: Project = { id: makeId('project'), organization: input.organization, owner_id: input.owner, name: input.name.trim(), description: '', created_at: now, updated_at: now };
|
|
76
|
+
const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.owner, action: 'project.created', resource_type: 'Project', resource_id: project.id, details: project.name, created_at: now };
|
|
77
|
+
await db.transaction(async transaction => {
|
|
78
|
+
transaction.collection<Project>('Project').insert(project, project.id, { requireAbsent: true });
|
|
79
|
+
transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function recordAgentActivity(input: { organization: string; user: string; assistant: string; subject: string }) {
|
|
84
|
+
const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.user, action: 'agent.summary', resource_type: 'AgentExecution', resource_id: input.subject, details: `${input.assistant} summarized authorized application state`, created_at: new Date().toISOString() };
|
|
85
|
+
await auditEvents.insert(event, event.id);
|
|
86
|
+
return event;
|
|
87
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application entry point
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import ReactDOM from 'react-dom/client';
|
|
7
|
+
import { App } from './App';
|
|
8
|
+
|
|
9
|
+
const root = ReactDOM.createRoot(document.getElementById('root')!);
|
|
10
|
+
root.render(
|
|
11
|
+
<React.StrictMode>
|
|
12
|
+
<App />
|
|
13
|
+
</React.StrictMode>
|
|
14
|
+
);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useCollection } from '@feltdb/core/react';
|
|
2
|
+
import { auditEvents } from '../feltdb';
|
|
3
|
+
|
|
4
|
+
export function Activity() {
|
|
5
|
+
const state = useCollection(auditEvents);
|
|
6
|
+
return <section className="page"><div className="page-heading"><div><span className="eyebrow">Durable history</span><h1>Activity</h1><p>Workflow and agent actions land in the same application state.</p></div><span className="count">{state.data.length}</span></div><article className="panel records">{state.data.length ? state.data.slice().reverse().map(event => <div className="record" key={event.id}><span className="record-mark">↗</span><div><strong>{event.action}</strong><small>{event.details} · {new Date(event.created_at).toLocaleString()}</small></div></div>) : <div className="empty"><strong>No activity yet</strong><span>Create a project or run an assistant.</span></div>}</article></section>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useMemo, useState } from 'react';
|
|
2
|
+
import type { Capability } from '@feltdb/core';
|
|
3
|
+
import { useCapability, useCollection } from '@feltdb/core/react';
|
|
4
|
+
import { projects } from '../feltdb';
|
|
5
|
+
import { useAuth } from '../context/AuthContext';
|
|
6
|
+
import { runProjectAssistant } from '../../agents/project-assistant';
|
|
7
|
+
import { runActivityAssistant } from '../../agents/activity-assistant';
|
|
8
|
+
|
|
9
|
+
export function Agents() {
|
|
10
|
+
const { user, organization } = useAuth(); const projectState = useCollection(projects); const [projectId, setProjectId] = useState('');
|
|
11
|
+
const projectCapability = useMemo<Capability>(() => ({ id: 'run-project-assistant', name: 'Run Project Assistant', execute: () => runProjectAssistant({ projectId, organization: organization!.id, user: user!.id }) }), [projectId, organization, user]);
|
|
12
|
+
const activityCapability = useMemo<Capability>(() => ({ id: 'run-activity-assistant', name: 'Run Activity Assistant', execute: () => runActivityAssistant({ organization: organization!.id, user: user!.id }) }), [organization, user]);
|
|
13
|
+
const projectAgent = useCapability<[], { title: string; summary: string; activityCount: number }>(projectCapability); const activityAgent = useCapability<[], { summary: string; total: number }>(activityCapability);
|
|
14
|
+
return <section className="page"><div className="page-heading"><div><span className="eyebrow">Authorized agents</span><h1>Assistants</h1><p>Each assistant invokes bounded capabilities over the state you can already access.</p></div></div><div className="agent-grid"><article className="panel"><span className="agent-state">Project Assistant · project-summary</span><h2>Explain a project</h2><select value={projectId} onChange={event => setProjectId(event.target.value)}><option value="">Select a project</option>{projectState.data.map(project => <option key={project.id} value={project.id}>{project.name}</option>)}</select><button className="primary" disabled={!projectId || projectAgent.loading} onClick={() => void projectAgent.execute()}>{projectAgent.loading ? 'Running…' : 'Summarize project'}</button>{projectAgent.result && <div className="result"><strong>{projectAgent.result.title}</strong><p>{projectAgent.result.summary}</p><small>{projectAgent.result.activityCount} related activity events</small></div>}</article><article className="panel"><span className="agent-state">Activity Assistant · activity-summary</span><h2>Explain recent activity</h2><p className="muted">Reads authorized AuditEvent state and records its own execution.</p><button className="primary" disabled={activityAgent.loading} onClick={() => void activityAgent.execute()}>{activityAgent.loading ? 'Running…' : 'Summarize activity'}</button>{activityAgent.result && <div className="result"><p>{activityAgent.result.summary}</p></div>}</article></div></section>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useCollection } from '@feltdb/core/react';
|
|
3
|
+
import { auditEvents, invitations, projects, teams } from '../feltdb';
|
|
4
|
+
import { useAuth } from '../context/AuthContext';
|
|
5
|
+
import { Projects } from './Projects';
|
|
6
|
+
import { Invitations } from './Invitations';
|
|
7
|
+
import { Activity } from './Activity';
|
|
8
|
+
import { Agents } from './Agents';
|
|
9
|
+
|
|
10
|
+
type View = 'overview' | 'projects' | 'invitations' | 'activity' | 'agents';
|
|
11
|
+
|
|
12
|
+
export function Dashboard() {
|
|
13
|
+
const { user, organization, signOut } = useAuth(); const [view, setView] = useState<View>('overview');
|
|
14
|
+
const projectState = useCollection(projects); const teamState = useCollection(teams); const invitationState = useCollection(invitations); const activityState = useCollection(auditEvents);
|
|
15
|
+
const stats = [{ label: 'Projects', value: projectState.data.length }, { label: 'Teams', value: teamState.data.length }, { label: 'Invitations', value: invitationState.data.filter(item => item.status === 'pending').length }, { label: 'Activity', value: activityState.data.length }];
|
|
16
|
+
return <main className="dashboard"><header><a className="brand" href="https://feltdb.com">FeltDB</a><nav>{(['overview', 'projects', 'invitations', 'activity', 'agents'] as View[]).map(item => <button className={view === item ? 'nav-active' : 'quiet'} onClick={() => setView(item)} key={item}>{item}</button>)}</nav><div><span>{user?.name}</span><button className="quiet" onClick={() => void signOut()}>Sign out</button></div></header>{view === 'overview' && <><section className="hero"><div><span className="eyebrow">{organization?.name}</span><h1>Welcome, {user?.name.split(' ')[0]}.</h1><p>State, workflows, and agents share one authorization boundary.</p></div><code>member · {organization?.id.slice(0, 12)}…</code></section><section className="stats">{stats.map(stat => <article key={stat.label}><span>{stat.label}</span><strong>{stat.value}</strong></article>)}</section><section className="grid"><article className="panel"><span className="eyebrow">Start here</span><h2>One application, visible in Studio</h2><div className="journey"><button onClick={() => setView('projects')}>1 <span>Create a project<small>Atomic Project + AuditEvent workflow</small></span></button><button onClick={() => setView('invitations')}>2 <span>Invite a member<small>Identity and relational authorization</small></span></button><button onClick={() => setView('agents')}>3 <span>Run an assistant<small>Bounded capabilities over authorized state</small></span></button></div></article><aside className="panel"><span className="eyebrow">Runtime</span><h2>One state boundary</h2><div className="stack"><div><i className="online" /><span>Application connected</span></div><div><i /><span>Authorization in FeltDB</span></div><div><i /><span>Workflows and agents persisted</span></div></div><p className="hint">Run <code>npm run feltdb:studio</code> to inspect the same records.</p></aside></section></>}{view === 'projects' && <Projects />}{view === 'invitations' && <Invitations />}{view === 'activity' && <Activity />}{view === 'agents' && <Agents />}</main>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { useCollection } from '@feltdb/core/react';
|
|
3
|
+
import { invitations } from '../feltdb';
|
|
4
|
+
import { useAuth } from '../context/AuthContext';
|
|
5
|
+
import { runAcceptInvitationWorkflow, runInviteMemberWorkflow } from '../../workflows/invitation';
|
|
6
|
+
|
|
7
|
+
export function Invitations() {
|
|
8
|
+
const { user, organization } = useAuth(); const state = useCollection(invitations); const [message, setMessage] = useState('');
|
|
9
|
+
async function invite(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (!user || !organization) return; const form = event.currentTarget; setMessage(''); try { await runInviteMemberWorkflow({ organization: organization.id, actor: user.id, email: String(new FormData(form).get('email')), role: 'member' }); form.reset(); } catch (error) { setMessage(error instanceof Error ? error.message : 'Invitation failed.'); } }
|
|
10
|
+
return <section className="page"><div className="page-heading"><div><span className="eyebrow">Invitation workflow</span><h1>Invitations</h1><p>Invitation visibility combines <code>self(user) | member</code>.</p></div><span className="count">{state.data.filter(item => item.status === 'pending').length}</span></div><article className="panel"><form className="project-form" onSubmit={invite}><input name="email" type="email" required placeholder="Existing user email" aria-label="Invite email" /><button>Invite member</button></form>{message && <p className="error">{message}</p>}<div className="records">{state.data.length ? state.data.slice().reverse().map(invitation => <div className="record spread" key={invitation.id}><div><strong>{invitation.email}</strong><small>{invitation.role} · {invitation.status}</small></div>{invitation.user === user?.id && invitation.status === 'pending' && <button onClick={() => void runAcceptInvitationWorkflow(invitation, user.id)}>Accept</button>}</div>) : <div className="empty"><strong>No invitations yet</strong><span>For this starter, invite another user who has already signed up.</span></div>}</div></article></section>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { useCollection } from '@feltdb/core/react';
|
|
3
|
+
import { projects } from '../feltdb';
|
|
4
|
+
import { useAuth } from '../context/AuthContext';
|
|
5
|
+
import { runProjectCreatedWorkflow } from '../../workflows/project-created';
|
|
6
|
+
|
|
7
|
+
export function Projects() {
|
|
8
|
+
const { user, organization } = useAuth(); const state = useCollection(projects); const [busy, setBusy] = useState(false);
|
|
9
|
+
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (!user || !organization) return; const form = event.currentTarget; setBusy(true); await runProjectCreatedWorkflow({ organization: organization.id, owner: user.id, name: String(new FormData(form).get('name')) }); form.reset(); setBusy(false); }
|
|
10
|
+
return <section className="page"><div className="page-heading"><div><span className="eyebrow">Project workflow</span><h1>Projects</h1><p>Creating a project atomically persists both Project and AuditEvent state.</p></div><span className="count">{state.data.length}</span></div><article className="panel"><form className="project-form" onSubmit={submit}><input name="name" required placeholder="A new project" aria-label="Project name" /><button disabled={busy}>{busy ? 'Running workflow…' : 'Create project'}</button></form><div className="records">{state.data.length ? state.data.slice().reverse().map(project => <div className="record" key={project.id}><span className="record-mark">{project.name.charAt(0).toUpperCase()}</span><div><strong>{project.name}</strong><small>ProjectCreated workflow · {new Date(project.created_at).toLocaleDateString()}</small></div></div>) : <div className="empty"><strong>No projects yet</strong><span>Create one to produce your first workflow state.</span></div>}</div></article></section>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { useAuth } from '../context/AuthContext';
|
|
3
|
+
|
|
4
|
+
export function SignIn() {
|
|
5
|
+
const { signIn } = useAuth(); const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
|
|
6
|
+
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); setError(''); const form = new FormData(event.currentTarget); try { await signIn(String(form.get('email')), String(form.get('password'))); } catch { setError('Those credentials could not be verified.'); setBusy(false); } }
|
|
7
|
+
return <><span className="eyebrow">Welcome back</span><h2>Sign in to FeltDB.</h2><p className="muted">Managed apps exchange credentials for a short-lived, application-scoped actor session.</p><form onSubmit={submit}><label>Email<input name="email" type="email" autoComplete="email" required placeholder="ada@example.com" /></label><label>Password<input name="password" type="password" autoComplete="current-password" required /></label>{error && <p className="error">{error}</p>}<button disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button></form><p className="switch">New to this app? <a href="?view=signup">Create an account</a></p></>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useState, type FormEvent } from 'react';
|
|
2
|
+
import { useAuth } from '../context/AuthContext';
|
|
3
|
+
|
|
4
|
+
export function SignUp() {
|
|
5
|
+
const { signUp } = useAuth(); const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
|
|
6
|
+
async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); setError(''); const form = new FormData(event.currentTarget); try { await signUp({ name: String(form.get('name')), email: String(form.get('email')), password: String(form.get('password')), organization: String(form.get('organization')) }); } catch { setError('We could not create your account. Check the details and try again.'); setBusy(false); } }
|
|
7
|
+
return <><span className="eyebrow">Create your workspace</span><h2>Start with durable state.</h2><p className="muted">Your actor, account, and organization are established at the FeltDB boundary.</p><form onSubmit={submit}><label>Name<input name="name" autoComplete="name" required placeholder="Ada Lovelace" /></label><label>Work email<input name="email" type="email" autoComplete="email" required placeholder="ada@example.com" /></label><label>Password<input name="password" type="password" autoComplete="new-password" minLength={8} required /></label><label>Organization<input name="organization" required placeholder="Analytical Engines" /></label>{error && <p className="error">{error}</p>}<button disabled={busy}>{busy ? 'Creating…' : 'Create account'}</button></form><p className="switch">Already have an account? <a href="?view=signin">Sign in</a></p></>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.dashboard header nav { display: flex; gap: .15rem; }
|
|
2
|
+
.dashboard header nav button { text-transform: capitalize; }
|
|
3
|
+
.nav-active { background: #dff0e5; color: #17633b; border-radius: 6px; padding: .5rem .7rem; }
|
|
4
|
+
.page { padding: clamp(3rem, 7vw, 6rem) 0; }
|
|
5
|
+
.page-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 2rem; }
|
|
6
|
+
.page-heading h1 { font: 500 clamp(3rem, 6vw, 5rem)/1 Georgia, serif; letter-spacing: -.055em; margin: .6rem 0; }
|
|
7
|
+
.page-heading p { color: #68746d; }
|
|
8
|
+
.count { font: 500 3rem Georgia, serif; color: #268453; }
|
|
9
|
+
.journey { display: grid; margin-top: 1.5rem; }
|
|
10
|
+
.journey button { display: flex; gap: 1rem; text-align: left; background: transparent; border-top: 1px solid #dedfd9; padding: 1.1rem .2rem; color: #268453; font-weight: 800; }
|
|
11
|
+
.journey button span { display: grid; gap: .25rem; color: #102019; }
|
|
12
|
+
.journey small { font-weight: 400; }
|
|
13
|
+
.spread { justify-content: space-between; }
|
|
14
|
+
.spread button { background: #dff0e5; color: #17633b; border-radius: 6px; padding: .55rem .8rem; }
|
|
15
|
+
.agent-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
|
|
16
|
+
.agent-grid select { width: 100%; padding: .85rem; border: 1px solid #ccd2cd; border-radius: 8px; background: #fff; margin: 1rem 0; }
|
|
17
|
+
.primary { display: block; width: 100%; padding: .85rem; border-radius: 8px; background: #102019; color: #fff; font-weight: 700; }
|
|
18
|
+
.agent-state { font: 700 .68rem ui-monospace, monospace; color: #268453; text-transform: uppercase; }
|
|
19
|
+
.result { margin-top: 1rem; padding: 1rem; border-radius: 8px; background: #eef3ee; line-height: 1.5; }
|
|
20
|
+
@media (max-width: 900px) { .dashboard header nav { display: none; } }
|
|
21
|
+
@media (max-width: 800px) { .agent-grid { grid-template-columns: 1fr; } }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#102019;background:#f4f3ed;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit}button,input{font:inherit}button{border:0;cursor:pointer}.brand{font-weight:800;font-size:1.2rem;text-decoration:none;letter-spacing:-.04em}.auth-layout{min-height:100vh;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(380px,.75fr)}.intro{padding:clamp(2rem,6vw,6rem);background:#102019;color:#f8f7f1;display:flex;flex-direction:column;justify-content:space-between}.intro h1{max-width:730px;font:500 clamp(3rem,6vw,6.5rem)/.98 Georgia,serif;letter-spacing:-.055em;margin:.8rem 0 1.5rem}.intro p{max-width:590px;color:#bdc9c1;font-size:1.08rem;line-height:1.65}.flow{display:flex;gap:1rem;color:#9eb0a5;font:600 .73rem ui-monospace,monospace;text-transform:uppercase;letter-spacing:.12em}.flow i{color:#49c17d}.auth-card{background:#fbfaf5;padding:clamp(2rem,6vw,6rem);display:flex;flex-direction:column;justify-content:center}h2{font:500 2rem/1.1 Georgia,serif;letter-spacing:-.035em;margin:.7rem 0}.muted,.switch{color:#68746d;line-height:1.55}form{display:grid;gap:1rem;margin:2rem 0 1.5rem}label{display:grid;gap:.45rem;font-weight:650;font-size:.8rem}input{width:100%;border:1px solid #ccd2cd;border-radius:8px;padding:.85rem 1rem;background:#fff;color:#102019}input:focus{outline:2px solid #49c17d;outline-offset:1px}form button,.project-form button{border-radius:8px;padding:.9rem 1.1rem;background:#102019;color:#fff;font-weight:700}button:disabled{opacity:.55}.eyebrow{color:#268453;font:700 .7rem ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase}.intro .eyebrow{color:#64dd98}.error{color:#a42c2c;font-size:.85rem}.switch{font-size:.85rem}.center{min-height:100vh;display:grid;place-content:center;text-align:center;color:#68746d}.loader{width:24px;height:24px;margin:auto;border:2px solid #cbd2cc;border-top-color:#268453;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.dashboard{min-height:100vh;padding:0 clamp(1.25rem,5vw,5rem) 5rem}.dashboard header{height:80px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d5d7d1}.dashboard header>div{display:flex;gap:1.2rem;align-items:center;font-size:.85rem}.quiet{background:transparent;color:#68746d;padding:.5rem}.hero{padding:clamp(3rem,7vw,6.5rem) 0 3rem;display:flex;justify-content:space-between;align-items:end;gap:2rem}.hero h1{font:500 clamp(3rem,7vw,6rem)/1 Georgia,serif;letter-spacing:-.055em;margin:.7rem 0 1rem}.hero p{color:#68746d}code{font:.72rem ui-monospace,monospace;color:#26784e}.stats{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid #d5d7d1;border-radius:12px;overflow:hidden;background:#faf9f4}.stats article{padding:1.3rem;border-right:1px solid #d5d7d1;display:grid;gap:.45rem}.stats article:last-child{border:0}.stats span{color:#68746d;font-size:.75rem}.stats strong{font:500 2rem Georgia,serif}.grid{display:grid;grid-template-columns:1.6fr .8fr;gap:1rem;margin-top:1rem}.panel{background:#faf9f4;border:1px solid #d5d7d1;border-radius:12px;padding:1.5rem}.panel-title{display:flex;align-items:start;justify-content:space-between}.panel-title>span{color:#68746d;font-size:.75rem}.project-form{grid-template-columns:1fr auto;margin:1.4rem 0}.records{display:grid;gap:.6rem}.record{padding:.8rem;border-top:1px solid #e1e1db;display:flex;align-items:center;gap:.8rem}.record-mark{width:36px;height:36px;border-radius:8px;display:grid;place-items:center;background:#dff0e5;color:#26784e;font-weight:800}.record div{display:grid;gap:.2rem}small{color:#7b857f}.empty{min-height:110px;display:grid;place-content:center;text-align:center;gap:.35rem;color:#68746d}.empty strong{color:#102019}.stack{display:grid;gap:1rem;margin:2rem 0}.stack div{display:flex;gap:.7rem;font-size:.85rem}.stack i{width:8px;height:8px;margin-top:.3rem;border-radius:50%;background:#9aa49d}.stack .online{background:#39ad6b;box-shadow:0 0 0 4px #dff0e5}.hint{color:#68746d;font-size:.85rem;line-height:1.6}@media(max-width:800px){.auth-layout,.grid{grid-template-columns:1fr}.intro{min-height:52vh;gap:3rem}.stats{grid-template-columns:repeat(2,1fr)}.stats article:nth-child(2){border-right:0}.hero{align-items:start;flex-direction:column}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runProjectAssistant as runAgentAssistedSummaryWorkflow } from '../agents/project-assistant';
|