Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b292db
fix: declutter frontpage — remove stats row and recent plans list
neoneye Apr 13, 2026
d5cfd15
feat: add MCP recommendation below the Create a New Plan form
neoneye Apr 13, 2026
57d4d59
fix: link to MCP docs at docs.planexe.org
neoneye Apr 13, 2026
ad8d68f
fix: remove welcome greeting from frontpage
neoneye Apr 13, 2026
d8ff8ee
fix: replace "This form works" with "You can type a prompt above"
neoneye Apr 13, 2026
bdcef0e
fix: update tip wording per user feedback
neoneye Apr 13, 2026
eaf40c8
feat: show example plans link for new users with no plans
neoneye Apr 13, 2026
0608ffe
fix: simplify example plans description
neoneye Apr 13, 2026
8ebfa81
refactor: move Create a New Plan to /plan/create page
neoneye Apr 13, 2026
97f3b33
feat: onboarding dashboard on frontpage + move form to /plan/create
neoneye Apr 13, 2026
dd9472f
feat: add Discord support link below onboarding steps
neoneye Apr 13, 2026
88daf84
feat: add ?debug=1 to show onboarding data on frontpage
neoneye Apr 13, 2026
88b4606
feat: add per-step debug overrides for onboarding
neoneye Apr 13, 2026
c776e15
docs: add usage examples to onboarding debug panel
neoneye Apr 13, 2026
62a5d8e
fix: rename "Getting Started" to "Onboarding"
neoneye Apr 13, 2026
e01c0e0
feat: add descriptions to onboarding steps
neoneye Apr 13, 2026
56f1435
fix: center onboarding content with equal margins
neoneye Apr 13, 2026
2ca90ab
fix: add links to Account page in onboarding step descriptions
neoneye Apr 13, 2026
6ce214f
fix: make Account links visible in onboarding descriptions
neoneye Apr 13, 2026
3cb1e82
fix: resolve pyright errors for admin_account possibly unbound
neoneye Apr 13, 2026
923a6db
fix: initialize admin_account to None to satisfy pyright
neoneye Apr 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 98 additions & 85 deletions frontend_multi_user/src/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1038,113 +1038,126 @@ def inject_current_user_name():
@self.app.route('/')
def index():
user = None
recent_tasks: list[SimpleNamespace] = []
total_tasks_count = 0
admin_account = None
is_admin = False
nonce = None
user_id = None
can_create_plan = False
example_prompts: list[str] = []
credits_balance_display = "0"
onboarding_steps: list[dict] = []

if current_user.is_authenticated:
is_admin = current_user.is_admin
try:
if is_admin:
admin_account = _get_current_user_account()
user_id = str(admin_account.id) if admin_account else self.admin_username
user = SimpleNamespace(name="Admin", given_name=None)
credits_balance_display = "Full access"
can_create_plan = True
user_id = str(admin_account.id) if admin_account else self.admin_username
else:
user_uuid = uuid.UUID(str(current_user.id))
user = self.db.session.get(UserAccount, user_uuid)
if user:
user_id = str(user.id)
user_id = str(user.id) if user else None

if user and user_id:
account_id = getattr(admin_account, 'id', None) if is_admin else getattr(user, 'id', None)

# Step 1: Account created (always done if logged in)
onboarding_steps.append({
"title": "Create account",
"description": "Sign up for PlanExe to get started.",
"done": True,
"detail": "Signed in",
"link": None,
})

# Step 2: Deposit credits
if is_admin:
has_credits = True
credit_detail = "Full access"
else:
credits_balance = to_credit_decimal(user.credits_balance)
credits_balance_display = format_credit_display(user.credits_balance)
min_credits = Decimal(os.environ.get("PLANEXE_MIN_CREDITS_TO_CREATE_PLAN", "2"))
can_create_plan = credits_balance >= min_credits

if user_id:
# Generate a nonce so the user can start a plan from the dashboard
nonce = 'DASH_' + str(uuid.uuid4())
tx_count = CreditHistory.query.filter_by(user_id=user.id).count()
has_credits = credits_balance > 0 or tx_count > 1
credit_detail = format_credit_display(user.credits_balance) if has_credits else "No credits yet"
onboarding_steps.append({
"title": "Deposit credits",
"description": 'Credits pay for the AI models that generate your plan. Go to <a href="' + url_for('account') + '">Account</a> to add credits.',
"done": has_credits,
"detail": credit_detail,
"link": url_for('account') if not has_credits else None,
})

# Step 3: Create API key
key_count = UserApiKey.query.filter_by(user_id=account_id, revoked_at=None).count() if account_id else 0
has_key = key_count >= 1
if key_count == 0:
key_detail = "No API keys yet"
elif key_count == 1:
key_detail = "1 API key"
else:
key_detail = f"{key_count} API keys"
onboarding_steps.append({
"title": "Create API key",
"description": 'Your AI assistant uses an API key to connect to PlanExe. Create one on the <a href="' + url_for('account') + '">Account</a> page.',
"done": has_key,
"detail": key_detail,
"link": url_for('account') if not has_key else None,
})

# Step 4: Use MCP (check if any API key has LLM calls)
total_llm_calls = 0
if has_key:
user_key_ids = [
str(k.id) for k in UserApiKey.query
.filter_by(user_id=account_id, revoked_at=None)
.all()
] if account_id else []
if user_key_ids:
try:
total_llm_calls = (
self.db.session.query(func.count(TokenMetrics.id))
.filter(TokenMetrics.api_key_id.in_(user_key_ids))
.scalar() or 0
)
except Exception:
self.db.session.rollback()
used_mcp = total_llm_calls >= 1
onboarding_steps.append({
"title": "Connect via MCP",
"description": "Add PlanExe to your AI tool (Claude, Cursor, Windsurf, etc.) using your API key. Your AI will then be able to create plans for you.",
"done": used_mcp,
"detail": f"{total_llm_calls} LLM calls" if used_mcp else "Not connected yet",
"link": "https://docs.planexe.org/mcp/mcp_welcome/" if not used_mcp else None,
})

# Step 5: Create 5+ plans
uid_filter = (
PlanItem.user_id.in_(_admin_user_ids())
if is_admin
else PlanItem.user_id == str(user_id)
)
try:
recent_task_rows = (
self.db.session.query(
PlanItem.id,
PlanItem.state,
PlanItem.stop_requested,
func.substr(PlanItem.prompt, 1, 240).label("prompt_preview"),
)
.filter(uid_filter)
.order_by(PlanItem.timestamp_created.desc())
.limit(10)
.all()
)
except DataError:
self.db.session.rollback()
logger.warning(
"Detected invalid UTF-8 in task_item.prompt for user_id=%s while loading dashboard; "
"falling back without prompt previews.",
user_id,
exc_info=True,
)
recent_task_rows = (
self.db.session.query(
PlanItem.id,
PlanItem.state,
PlanItem.stop_requested,
)
.filter(uid_filter)
.order_by(PlanItem.timestamp_created.desc())
.limit(10)
.all()
)
recent_tasks = []
for task in recent_task_rows:
prompt_preview = getattr(task, "prompt_preview", None)
if prompt_preview is None:
from src.plan_routes import _load_prompt_preview_safe
prompt_text = _load_prompt_preview_safe(task.id)
else:
prompt_text = (prompt_preview or "").strip() or "[Prompt unavailable]"
state = task.state if isinstance(task.state, PlanState) else None
recent_tasks.append(
SimpleNamespace(
id=str(task.id),
state=state,
prompt=prompt_text,
)
)
total_tasks_count = (
PlanItem.query
.filter(uid_filter)
.count()
)
# Load example prompts for the "Start New Plan" form
for prompt_uuid in DEMO_FORM_RUN_PROMPT_UUIDS:
prompt_item = self.prompt_catalog.find(prompt_uuid)
if prompt_item:
example_prompts.append(prompt_item.prompt)
total_plans = PlanItem.query.filter(uid_filter).count()
is_superuser = total_plans >= 5
onboarding_steps.append({
"title": "Superuser",
"description": "Create 5 or more plans to earn the Superuser badge.",
"done": is_superuser,
"detail": f"{total_plans} plans created" if is_superuser else f"{total_plans}/5 plans",
"link": None,
})
except Exception:
logger.debug("Could not load dashboard data", exc_info=True)

# Debug overrides: /?debug=1&step1=0&step2=1&step3=0&step4=1&step5=0
if request.args.get("debug") == "1" and onboarding_steps:
step_keys = ["step1", "step2", "step3", "step4", "step5"]
for i, key in enumerate(step_keys):
val = request.args.get(key)
if val is not None and i < len(onboarding_steps):
onboarding_steps[i]["done"] = val == "1"

return render_template(
'index.html',
user=user,
credits_balance_display=credits_balance_display,
can_create_plan=can_create_plan,
total_tasks_count=total_tasks_count,
recent_tasks=recent_tasks,
is_admin=is_admin,
nonce=nonce,
user_id=user_id,
example_prompts=example_prompts,
model_profile_options=_model_profile_options(),
onboarding_steps=onboarding_steps,
onboarding_debug=request.args.get("debug") == "1",
)

@self.app.route('/models')
Expand Down
28 changes: 28 additions & 0 deletions frontend_multi_user/src/plan_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,34 @@ def run():
return render_template("run_via_database.html", run_id=task_id)


@plan_routes_bp.route("/plan/create", methods=["GET"])
@login_required
def plan_create_page():
from src.app import _model_profile_options, DEMO_FORM_RUN_PROMPT_UUIDS
prompt_catalog = current_app.config["PROMPT_CATALOG"]
example_prompts: list[str] = []
for prompt_uuid in DEMO_FORM_RUN_PROMPT_UUIDS:
prompt_item = prompt_catalog.find(prompt_uuid)
if prompt_item:
example_prompts.append(prompt_item.prompt)

can_create_plan = True
if not current_user.is_admin:
user = _get_current_user_account()
if user:
min_credits = Decimal(os.environ.get("PLANEXE_MIN_CREDITS_TO_CREATE_PLAN", "2"))
can_create_plan = to_credit_decimal(user.credits_balance) >= min_credits

nonce = "CREATE_" + str(uuid.uuid4())
return render_template(
"plan_create.html",
can_create_plan=can_create_plan,
nonce=nonce,
example_prompts=example_prompts,
model_profile_options=_model_profile_options(),
)


@plan_routes_bp.route("/create_plan", methods=["POST"])
@login_required
@_nocache
Expand Down
1 change: 1 addition & 0 deletions frontend_multi_user/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@
<span class="site-brand-label">Home</span>
</a>
{% if current_user_name %}
<a href="{{ url_for('plan_routes.plan_create_page') }}" class="{% if request.endpoint == 'plan_routes.plan_create_page' %}nav-active{% endif %}">Create</a>
<a href="{{ url_for('plan_routes.plan') }}" class="{% if request.endpoint == 'plan_routes.plan' and not request.args.get('id') %}nav-active{% endif %}">Plans</a>
<a href="{{ url_for('models') }}" class="{% if request.endpoint == 'models' %}nav-active{% endif %}">Models</a>
{% endif %}
Expand Down
Loading