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:

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

Decision

Implement hybrid approach:

  1. REST API wizard endpoints for modern web UI (React/Shadcn)
  2. WebSocket terminal for power users who want full CLI
  3. 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

Negative

Neutral

Implementation Priority

  1. Phase 1: Add wizard REST endpoints to CliApiResource.java
  2. Phase 2: Add SSE streaming for progress
  3. Phase 3: Create React frontend (could be separate repo)
  4. Phase 4: Add WebSocket terminal for power users

References

Path: /docs/developers/architecture/idempiere-hub/032-web-wizard-architecture