feat: tiered costs feature + admin tier management + tag selection
Build & Push Docker Image / build (push) Successful in 6m52s

- costs: nullable notes (fix create without notes), drop renewalDate
  (schema + API + UI), gate POST/PATCH/DELETE to admin + costs feature
- feature middleware: admin-aware hasFeature + getEntitlements union;
  /auth/me and login return resolved entitlements
- users: tier enum (free/premium/enterprise) in create/update/list,
  admin UI tier select + tier badge
- tags: GET /tags/all, TagInput autocomplete in new/edit tool forms,
  feature suggestions on focus, query invalidation on create/update
- openapi: nullable ToolUpdate urls, ToolUpdate tier fields, listAllTags
- Dockerfile: push-force to drop renewal_date column
This commit is contained in:
opencode
2026-08-02 01:10:47 +02:00
parent cd4efd16f6
commit 01c70085db
27 changed files with 478 additions and 86 deletions
@@ -91,7 +91,6 @@ export default function ToolDetail() {
const [costBillingPeriod, setCostBillingPeriod] = useState("monthly");
const [costAmount, setCostAmount] = useState("");
const [costCurrency, setCostCurrency] = useState("EUR");
const [costRenewal, setCostRenewal] = useState("");
const [costNotes, setCostNotes] = useState("");
useEffect(() => {
@@ -150,7 +149,6 @@ export default function ToolDetail() {
notes: costNotes || null,
};
if (costAmount) body.cost = costAmount;
if (costRenewal) body.renewalDate = costRenewal;
try {
await customFetch(url, { method, body: JSON.stringify(body) });
toast({ title: editCost ? "Cost updated" : "Cost added" });
@@ -168,7 +166,6 @@ export default function ToolDetail() {
setCostBillingPeriod("monthly");
setCostAmount("");
setCostCurrency("EUR");
setCostRenewal("");
setCostNotes("");
}
@@ -178,7 +175,6 @@ export default function ToolDetail() {
setCostBillingPeriod(c.billingPeriod ?? "monthly");
setCostAmount(c.cost ?? "");
setCostCurrency(c.currency ?? "EUR");
setCostRenewal(c.renewalDate ?? "");
setCostNotes(c.notes ?? "");
setCostDialogOpen(true);
}
@@ -562,11 +558,6 @@ export default function ToolDetail() {
<div className="text-lg font-bold">
{c.cost != null ? `${c.cost} ${c.currency ?? ""}` : "Free"}
</div>
{c.renewalDate && (
<p className="text-xs text-muted-foreground mt-1">
Renews: {new Date(c.renewalDate).toLocaleDateString()}
</p>
)}
{c.notes && <p className="text-xs text-muted-foreground mt-1 italic">{c.notes}</p>}
</div>
{isAdmin && (
@@ -641,10 +632,6 @@ export default function ToolDetail() {
</Select>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Renewal Date</label>
<Input type="date" value={costRenewal} onChange={(e) => setCostRenewal(e.target.value)} />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Notes</label>
<Textarea placeholder="Billing details, contract info..." value={costNotes} onChange={(e) => setCostNotes(e.target.value)} />