ADR-032: Web Wizard Architecture
Status
Proposed
Date
2025-12-07
Context
The iDempiere CLI has interactive wizards (e.g., TableWizardCommand) that guide users through complex multi-step processes. These currently run in terminal only. Users want web-based access for:
- Browser-based development workflows
- Mobile/tablet access
- Integration with web admin panels
- Non-technical users who prefer GUI over CLI
Research Summary
Technologies Evaluated
| Technology | Use Case | Mobile-Friendly | Difficulty |
|---|---|---|---|
| xterm.js | Terminal emulator in browser | No | Medium |
| ttyd | Quick CLI sharing (zero code) | No | Easy |
| Shadcn UI | Modern React wizard forms | Yes | Medium |
| Quarkus SSE | Real-time progress streaming | Yes | Medium |
| WebContainers | Browser-only runtime | No | Hard (JS only) |
Key Sources
- xterm.js: https://xtermjs.org/
- ttyd: https://tsl0922.github.io/ttyd/
- Shadcn UI Multi-Form: https://github.com/Remy349/shadcn-ui-multi-form
- Quarkus SSE: https://quarkus.io/guides/rest
Decision
Implement hybrid approach:
- REST API wizard endpoints for modern web UI (React/Shadcn)
- WebSocket terminal for power users who want full CLI
- SSE streaming for real-time progress updates
Implementation
Architecture
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Wizard Mode │ │ Terminal Mode │ │
│ │ (React/Shadcn) │ │ (xterm.js) │ │
│ └────────┬────────┘ └────────┬────────┘ │
└───────────│─────────────────────│───────────────────────┘
│ HTTP/SSE │ WebSocket
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Quarkus Backend │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ WizardResource │ │ TerminalEndpoint│ │
│ │ /api/wizard/* │ │ /terminal │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Shared Tool Logic │ │
│ │ TableToolLogic, RegistryToolLogic... │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
REST Wizard Endpoints
@Path("/api/wizard")
public class WizardResource {
@Inject TableToolLogic tableToolLogic;
// Step 1: Get wizard metadata
@GET
@Path("/table/schema")
public Response getTableWizardSchema() {
return Response.ok("""
{
"steps": [
{"id": "name", "title": "Table Name", "fields": ["tableName", "description"]},
{"id": "columns", "title": "Columns", "fields": ["columns", "templates"]},
{"id": "config", "title": "Configuration", "fields": ["entityType", "accessLevel"]},
{"id": "review", "title": "Review & Create"}
]
}
""").build();
}
// Step 2: Validate inputs
@POST
@Path("/table/validate")
public Response validateTable(TableDefinition def) {
ValidationResult result = tableToolLogic.validate(def);
return Response.ok(result).build();
}
// Step 3: Preview execution plan
@POST
@Path("/table/plan")
public Response previewTable(TableDefinition def) {
ExecutionPlan plan = tableToolLogic.createPlan(def);
// Returns: what will be created (like terraform plan)
return Response.ok(plan).build();
}
// Step 4: Execute with SSE streaming
@POST
@Path("/table/apply")
@Produces(MediaType.SERVER_SENT_EVENTS)
@RestStreamElementType(MediaType.APPLICATION_JSON)
public Multi<WizardProgress> applyTable(TableDefinition def) {
return Multi.createFrom().emitter(emitter -> {
emitter.emit(new WizardProgress("validate", "Validating...", 10));
emitter.emit(new WizardProgress("create_table", "Creating AD_Table...", 30));
emitter.emit(new WizardProgress("create_columns", "Creating columns...", 60));
emitter.emit(new WizardProgress("sync_db", "Syncing database...", 80));
emitter.emit(new WizardProgress("complete", "Done!", 100));
emitter.complete();
});
}
}
record WizardProgress(String step, String message, int progress) {}
record TableDefinition(String tableName, String description, List<ColumnDef> columns, String entityType, String accessLevel) {}
WebSocket Terminal Endpoint
@ServerEndpoint("/terminal")
@ApplicationScoped
public class TerminalEndpoint {
@Inject IFactory factory;
@OnMessage
public void onMessage(String input, Session session) throws IOException {
// Activate request context for CDI beans
ManagedContext ctx = Arc.container().requestContext();
ctx.activate();
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
CommandLine cmd = new CommandLine(IdempiereCli.class, factory);
cmd.setOut(new PrintStream(out));
cmd.execute(input.split(" "));
session.getBasicRemote().sendText(out.toString());
} finally {
ctx.terminate();
}
}
}
Frontend: React Wizard Component
// TableWizard.tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Progress } from '@/components/ui/progress';
export function TableWizard() {
const [step, setStep] = useState(0);
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState('');
const form = useForm({
defaultValues: {
tableName: '',
description: '',
columns: [],
entityType: 'U',
accessLevel: '3'
}
});
async function onSubmit(data) {
// Preview plan first
const plan = await fetch('/api/wizard/table/plan', {
method: 'POST',
body: JSON.stringify(data)
}).then(r => r.json());
if (confirm(`Create table with ${plan.columnCount} columns?`)) {
// Execute with SSE streaming
const eventSource = new EventSource(
`/api/wizard/table/apply?${new URLSearchParams(data)}`
);
eventSource.onmessage = (event) => {
const { step, message, progress } = JSON.parse(event.data);
setStatus(message);
setProgress(progress);
if (progress === 100) {
eventSource.close();
}
};
}
}
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{step === 0 && <TableNameStep form={form} />}
{step === 1 && <ColumnsStep form={form} />}
{step === 2 && <ConfigStep form={form} />}
{step === 3 && <ReviewStep form={form} />}
<Progress value={progress} />
<p>{status}</p>
<div className="flex gap-2">
{step > 0 && <Button onClick={() => setStep(s => s - 1)}>Back</Button>}
{step < 3 && <Button onClick={() => setStep(s => s + 1)}>Next</Button>}
{step === 3 && <Button type="submit">Create Table</Button>}
</div>
</form>
);
}
Quick Demo with ttyd
For instant web access without code changes:
# Install ttyd (macOS)
brew install ttyd
# Run CLI via ttyd
ttyd -p 7681 java -jar target/idempiere-hub-runner.jar shell
# Access at http://localhost:7681
Consequences
Positive
- Accessibility: Non-technical users can use wizards via web UI
- Mobile: React wizard works on tablets/phones
- Integration: Can embed in existing web admin panels
- Real-time: SSE streaming shows progress like CI/CD pipelines
Negative
- Complexity: More code to maintain (frontend + backend)
- Security: Need authentication for web endpoints
- Deployment: Additional frontend build step
Neutral
- Existing CLI commands unchanged
- Shared tool logic reused
Implementation Priority
- Phase 1: Add wizard REST endpoints to
CliApiResource.java - Phase 2: Add SSE streaming for progress
- Phase 3: Create React frontend (could be separate repo)
- Phase 4: Add WebSocket terminal for power users
References
- ADR-026: CLI Execution Modes - API server mode
- xterm.js - Terminal emulator
- Shadcn UI - React components
- ttyd - Share terminal over web