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
+12 -4
View File
@@ -10,6 +10,7 @@ import {
getListToolsQueryKey,
getListCategoriesQueryKey,
getListAllFeaturesQueryKey,
getListAllTagsQueryKey,
getGetTopToolsQueryKey,
getGetAnalyticsSummaryQueryKey,
} from "@workspace/api-client-react";
@@ -27,6 +28,7 @@ import { Pencil, Plus, X, ArrowLeft } from "lucide-react";
import { Link } from "wouter";
import { CategoryCombobox } from "@/components/category-combobox";
import { FeatureInput } from "@/components/feature-input";
import { TagInput } from "@/components/tag-input";
import { useAuth } from "@/hooks/use-auth";
const toolSchema = z.object({
@@ -109,6 +111,7 @@ export default function ToolEdit() {
queryClient.invalidateQueries({ queryKey: getListToolsQueryKey() });
queryClient.invalidateQueries({ queryKey: getListCategoriesQueryKey() });
queryClient.invalidateQueries({ queryKey: getListAllFeaturesQueryKey() });
queryClient.invalidateQueries({ queryKey: getListAllTagsQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetTopToolsQueryKey() });
queryClient.invalidateQueries({ queryKey: getGetAnalyticsSummaryQueryKey() });
setLocation(`/tools/${id}`);
@@ -260,7 +263,7 @@ export default function ToolEdit() {
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Features</h3>
<p className="text-sm text-muted-foreground">Key capabilities of this tool.</p>
<p className="text-sm text-muted-foreground">Key capabilities of this tool. Existing features from other tools are selectable.</p>
</div>
<Button type="button" variant="outline" size="sm" onClick={() => appendFeature({ value: "" })}>
<Plus className="w-4 h-4 mr-2" /> Add Feature
@@ -304,7 +307,7 @@ export default function ToolEdit() {
<div className="flex justify-between items-center">
<div>
<h3 className="text-lg font-medium">Tags</h3>
<p className="text-sm text-muted-foreground">Keywords for this tool.</p>
<p className="text-sm text-muted-foreground">Keywords for this tool. Existing tags from other tools are selectable.</p>
</div>
<Button type="button" variant="outline" size="sm" onClick={() => appendTag({ value: "" })}>
<Plus className="w-4 h-4 mr-2" /> Add Tag
@@ -317,9 +320,14 @@ export default function ToolEdit() {
control={form.control}
name={`tags.${index}.value`}
render={({ field }) => (
<FormItem className="flex items-center space-y-0 relative w-[150px]">
<FormItem className="flex items-center space-y-0 relative w-[200px]">
<FormControl>
<Input placeholder="Tag" className="pr-8 h-9 text-sm" {...field} />
<TagInput
placeholder="Tag"
className="pr-8 h-9 text-sm"
onChange={field.onChange}
value={field.value ?? ""}
/>
</FormControl>
<Button
type="button"