pgkiln 0.69.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/db/migrations/136_vector_search.sql +252 -0
  2. package/db/migrations/137_create_page_types.sql +904 -0
  3. package/db/migrations/138_app_utilities.sql +153 -0
  4. package/docs/apex-feature-parity.md +10 -8
  5. package/docs/guide/03-builder.md +176 -4
  6. package/docs/guide/04-pages-and-regions.md +45 -3
  7. package/docs/guide/05-items.md +1 -1
  8. package/docs/guide/06-processing.md +18 -0
  9. package/docs/guide/09-reference.md +34 -1
  10. package/docs/guide/12-development.md +53 -3
  11. package/docs/guide/15-extensions.md +1 -1
  12. package/docs/guide/16-files.md +1 -1
  13. package/examples/hr/README.md +1 -0
  14. package/examples/hr/hr_72_vector_search.sql +158 -0
  15. package/examples/hr/hr_73_configuration_options.sql +17 -0
  16. package/examples/hr/hr_74_app_utilities.sql +22 -0
  17. package/package.json +1 -1
  18. package/public/app.css +9 -0
  19. package/public/builder.css +48 -0
  20. package/src/ai/embeddings.ts +197 -0
  21. package/src/ai/service.ts +13 -9
  22. package/src/app.ts +13 -0
  23. package/src/builder/advisor.ts +43 -3
  24. package/src/builder/ai-agents.ts +13 -5
  25. package/src/builder/app-updates.ts +3 -0
  26. package/src/builder/components.ts +24 -4
  27. package/src/builder/create-page.ts +259 -0
  28. package/src/builder/dependencies.ts +311 -0
  29. package/src/builder/features.ts +313 -0
  30. package/src/builder/home.ts +3 -0
  31. package/src/builder/i18n/de.ts +38 -0
  32. package/src/builder/i18n/en.ts +38 -0
  33. package/src/builder/i18n/es.ts +38 -0
  34. package/src/builder/i18n/fr.ts +38 -0
  35. package/src/builder/i18n/it.ts +38 -0
  36. package/src/builder/i18n/ja.ts +38 -0
  37. package/src/builder/i18n/ko.ts +38 -0
  38. package/src/builder/i18n/nl.ts +38 -0
  39. package/src/builder/i18n/pt.ts +38 -0
  40. package/src/builder/i18n/zh-cn.ts +38 -0
  41. package/src/builder/i18n/zh-tw.ts +38 -0
  42. package/src/builder/login-page.ts +17 -14
  43. package/src/builder/newapp.ts +41 -23
  44. package/src/builder/object-actions.ts +1 -0
  45. package/src/builder/routes.ts +13 -2
  46. package/src/builder/shared.ts +1 -1
  47. package/src/builder/sql.ts +3 -0
  48. package/src/builder/table-editor.ts +1 -0
  49. package/src/builder/ui-defaults.ts +161 -0
  50. package/src/builder/ui.ts +2 -1
  51. package/src/builder/upgrade.ts +154 -0
  52. package/src/builder/utilities.ts +181 -0
  53. package/src/builder/vector-column.ts +229 -0
  54. package/src/builder/vector-providers.ts +286 -0
  55. package/src/builder/wizards.ts +211 -26
  56. package/src/cli/replace.ts +11 -1
  57. package/src/history.ts +155 -0
  58. package/src/runtime/ai-agents.ts +53 -2
  59. package/src/runtime/builtin-components.ts +23 -2
  60. package/src/runtime/items.ts +4 -2
  61. package/src/runtime/search.ts +116 -12
  62. package/src/server.ts +3 -0
  63. package/src/vector.ts +288 -0
@@ -285,6 +285,7 @@ export const ja: BuilderMessages = {
285
285
  'app.tab.api': 'REST API',
286
286
  'app.tab.settings': '設定',
287
287
  'app.tab.advisor': 'アドバイザ',
288
+ 'app.tab.utilities': 'ユーティリティ',
288
289
  'app.lock_refused': 'ロックが解除されるまで、変更は拒否されます。',
289
290
  'app.search_pages': 'ページの検索',
290
291
  'app.search_placeholder': 'ページ名または番号',
@@ -318,6 +319,9 @@ export const ja: BuilderMessages = {
318
319
  'app.table_or_view': '表またはビュー(マスター詳細の場合はマスター表)',
319
320
  'app.no_access': '{table}({role} にはアクセス権なし)',
320
321
  'app.ai_pages': 'AI でページを作成',
322
+ 'app.all_page_types': "すべてのページタイプ、機能、コピー",
323
+ 'app.task.add_feature': "機能ページを追加",
324
+ 'app.task.copy_page': "コピーとしてページを作成",
321
325
  'app.ai_pages_note': '(自分の言葉で説明してください)',
322
326
  'app.blank': '空白ページの作成',
323
327
  'app.page_number': 'ページ番号',
@@ -1065,6 +1069,40 @@ export const ja: BuilderMessages = {
1065
1069
  'fts.gin_yes': "GIN 索引",
1066
1070
  'fts.gin_no': "GIN 索引なし",
1067
1071
  'fts.added': "{table} に全文検索を追加しました:列 {column}、索引 {index}。",
1072
+ // (sprint 136, AP-1) Table Properties › Add vector column
1073
+ 'vec.add': "ベクトル列を追加",
1074
+ 'vec.title': "{table} のベクトル列",
1075
+ 'vec.intro': "埋め込み列 vector(n)(n はベクトル・プロバイダの次元数)、距離用の HNSW 索引、およびソース列が変わったときに行を再埋め込みするトリガーを追加します。その後、検索タイプ vector または hybrid の検索構成や AI エージェントの Retrieve ツールで、意味によって行を検索できます。既存の行はすぐに埋め込みのキューに入れられます。",
1076
+ 'vec.existing': "現在のベクトル列",
1077
+ 'vec.provider': "ベクトル・プロバイダ",
1078
+ 'vec.provider_help': "このワークスペースのアプリケーションの共有コンポーネント › ベクトル・プロバイダ。検索にも同じプロバイダを使用します。",
1079
+ 'vec.keep': "最新に保つ方法",
1080
+ 'vec.keep_help': "関数プロバイダは、トリガー内ですぐに埋め込みを設定できます。AI サービスはサーバーが呼び出します。トリガーが行をキューに入れ、サーバーがプロバイダの 1 分あたりのリクエスト数とアプリケーションの 1 日の AI 制限の範囲内で、キューの行をまとめて埋め込みます。",
1081
+ 'vec.mode_set': "トリガーがすぐに設定",
1082
+ 'vec.mode_queue': "サーバー用のキューに入れる",
1083
+ 'vec.sources': "ソース列",
1084
+ 'vec.sources_help': "埋め込むテキスト:これらの列を 1 行に 1 つずつ。いずれかが変わると行が再埋め込みされます。",
1085
+ 'vec.distance': "距離",
1086
+ 'vec.distance_help': "ほとんどの埋め込みモデルでは cosine。検索構成も同じ距離を使用します。",
1087
+ 'vec.column': "ベクトル列",
1088
+ 'vec.no_key': "表には 1 列の主キーが必要です。キューはそれで行を見つけます。",
1089
+ 'vec.pick_provider': "ベクトル・プロバイダを選択してください。",
1090
+ 'vec.no_providers': "このワークスペースのアプリケーションにはまだベクトル・プロバイダがありません。共有コンポーネント › ベクトル・プロバイダで追加してください。",
1091
+ 'vec.missing': "このデータベースには pgvector 拡張機能がインストールされていないため、まだベクトル列を追加できません。",
1092
+ 'vec.missing_create': "データベース・サーバーには pgvector {version} があります:ワークスペース・ユーティリティ › ベクトル・プロバイダで拡張機能を作成してください。",
1093
+ 'vec.missing_admin': "データベース・サーバーには pgvector {version} があります:管理者が拡張機能を作成します(ワークスペース・ユーティリティ › ベクトル・プロバイダ)。",
1094
+ 'vec.missing_package': "データベース・サーバーに pgvector パッケージがありません:インストールしてから(例:apt install postgresql-17-pgvector、または Docker イメージ pgvector/pgvector)、拡張機能を作成してください。",
1095
+ 'vec.utilities': "ワークスペース・ユーティリティ › ベクトル・プロバイダ",
1096
+ 'vec.kept_by': "{provider} が {columns} から維持",
1097
+ 'vec.not_kept': "最新に保つトリガーはありません",
1098
+ 'vec.progress': "{column} の埋め込み済みの行",
1099
+ 'vec.counts': "{total} 行中 {done} 行を埋め込み済み、{queued} 行がキュー内",
1100
+ 'vec.failing': "{n} 件失敗",
1101
+ 'vec.reembed_all': "埋め込みのない行だけでなく、すべての行",
1102
+ 'vec.backfill': "埋め戻し",
1103
+ 'vec.refresh': "更新",
1104
+ 'vec.backfill_queued': "{column} の {n} 行を埋め込みのキューに入れました。",
1105
+ 'vec.added': "{table} にベクトル列 {column} を追加しました。{n} 行を埋め込みのキューに入れました。",
1068
1106
  // ------------------------------------------------------------ Shared Components (sprint 124)
1069
1107
  'comp.nav_entry': 'ナビゲーション・エントリ',
1070
1108
  'comp.nav_entry.plural': 'ナビゲーション・メニュー',
@@ -285,6 +285,7 @@ export const ko: BuilderMessages = {
285
285
  'app.tab.api': 'REST API',
286
286
  'app.tab.settings': '설정',
287
287
  'app.tab.advisor': '어드바이저',
288
+ 'app.tab.utilities': '유틸리티',
288
289
  'app.lock_refused': '잠금이 해제될 때까지 변경이 거부됩니다.',
289
290
  'app.search_pages': '페이지 검색',
290
291
  'app.search_placeholder': '페이지 이름 또는 번호',
@@ -318,6 +319,9 @@ export const ko: BuilderMessages = {
318
319
  'app.table_or_view': '테이블 또는 뷰(마스터 세부의 경우 마스터 테이블)',
319
320
  'app.no_access': '{table}({role}에 대한 액세스 권한 없음)',
320
321
  'app.ai_pages': 'AI로 페이지 생성',
322
+ 'app.all_page_types': "모든 페이지 유형, 기능 및 복사",
323
+ 'app.task.add_feature': "기능 페이지 추가",
324
+ 'app.task.copy_page': "복사하여 페이지 생성",
321
325
  'app.ai_pages_note': '(원하는 내용을 직접 설명하세요)',
322
326
  'app.blank': '빈 페이지 생성',
323
327
  'app.page_number': '페이지 번호',
@@ -1065,6 +1069,40 @@ export const ko: BuilderMessages = {
1065
1069
  'fts.gin_yes': "GIN 인덱스",
1066
1070
  'fts.gin_no': "GIN 인덱스 없음",
1067
1071
  'fts.added': "{table}에 전체 텍스트 검색을 추가했습니다: 열 {column}, 인덱스 {index}.",
1072
+ // (sprint 136, AP-1) Table Properties › Add vector column
1073
+ 'vec.add': "벡터 열 추가",
1074
+ 'vec.title': "{table}의 벡터 열",
1075
+ 'vec.intro': "임베딩 열 vector(n)(n: 벡터 공급자의 차원 수), 거리용 HNSW 인덱스, 그리고 소스 열이 바뀌면 행을 다시 임베딩하는 트리거를 추가합니다. 그러면 검색 유형 vector 또는 hybrid의 검색 구성이나 AI 에이전트의 Retrieve 도구가 의미로 행을 찾습니다. 지금 있는 행은 바로 임베딩 대기열에 들어갑니다.",
1076
+ 'vec.existing': "현재 벡터 열",
1077
+ 'vec.provider': "벡터 공급자",
1078
+ 'vec.provider_help': "이 작업 영역 애플리케이션의 공유 구성 요소 › 벡터 공급자. 검색할 때도 같은 공급자를 사용하십시오.",
1079
+ 'vec.keep': "최신 상태 유지",
1080
+ 'vec.keep_help': "함수 공급자는 트리거에서 바로 임베딩을 설정할 수 있습니다. AI 서비스는 서버가 호출합니다: 트리거가 행을 대기열에 넣고, 서버가 공급자의 분당 요청 수와 애플리케이션의 일일 AI 한도 안에서 대기열의 행을 일괄로 임베딩합니다.",
1081
+ 'vec.mode_set': "트리거가 바로 설정",
1082
+ 'vec.mode_queue': "서버용 대기열에 넣음",
1083
+ 'vec.sources': "소스 열",
1084
+ 'vec.sources_help': "임베딩할 텍스트: 이 열들, 한 줄에 하나씩. 그중 하나가 바뀌면 행을 다시 임베딩합니다.",
1085
+ 'vec.distance': "거리",
1086
+ 'vec.distance_help': "대부분의 임베딩 모델에는 cosine; 검색 구성도 같은 거리를 사용합니다.",
1087
+ 'vec.column': "벡터 열",
1088
+ 'vec.no_key': "테이블에 열 하나로 된 기본 키가 필요합니다: 대기열은 그것으로 행을 찾습니다.",
1089
+ 'vec.pick_provider': "벡터 공급자를 선택하십시오.",
1090
+ 'vec.no_providers': "이 작업 영역의 애플리케이션에는 아직 벡터 공급자가 없습니다: 공유 구성 요소 › 벡터 공급자에서 추가하십시오.",
1091
+ 'vec.missing': "이 데이터베이스에 pgvector 확장이 설치되어 있지 않아 아직 벡터 열을 추가할 수 없습니다.",
1092
+ 'vec.missing_create': "데이터베이스 서버에 pgvector {version}이(가) 있습니다: 작업 영역 유틸리티 › 벡터 공급자에서 확장을 만드십시오.",
1093
+ 'vec.missing_admin': "데이터베이스 서버에 pgvector {version}이(가) 있습니다: 관리자가 확장을 만듭니다(작업 영역 유틸리티 › 벡터 공급자).",
1094
+ 'vec.missing_package': "데이터베이스 서버에 pgvector 패키지가 없습니다: 설치한 다음(예: apt install postgresql-17-pgvector 또는 Docker 이미지 pgvector/pgvector) 확장을 만드십시오.",
1095
+ 'vec.utilities': "작업 영역 유틸리티 › 벡터 공급자",
1096
+ 'vec.kept_by': "{provider}이(가) {columns}에서 유지",
1097
+ 'vec.not_kept': "최신 상태로 유지하는 트리거가 없음",
1098
+ 'vec.progress': "{column}의 임베딩된 행",
1099
+ 'vec.counts': "{total}행 중 {done}행 임베딩됨, {queued}행 대기 중",
1100
+ 'vec.failing': "{n}건 실패",
1101
+ 'vec.reembed_all': "임베딩이 없는 행만이 아니라 모든 행",
1102
+ 'vec.backfill': "채우기",
1103
+ 'vec.refresh': "새로 고침",
1104
+ 'vec.backfill_queued': "{column}의 {n}행을 임베딩 대기열에 넣었습니다.",
1105
+ 'vec.added': "{table}에 벡터 열 {column}을(를) 추가했습니다. {n}행이 임베딩 대기열에 있습니다.",
1068
1106
  // ------------------------------------------------------------ Shared Components (sprint 124)
1069
1107
  'comp.nav_entry': '탐색 항목',
1070
1108
  'comp.nav_entry.plural': '탐색 메뉴',
@@ -285,6 +285,7 @@ export const nl: BuilderMessages = {
285
285
  'app.tab.api': 'REST-API',
286
286
  'app.tab.settings': 'Instellingen',
287
287
  'app.tab.advisor': 'Adviseur',
288
+ 'app.tab.utilities': 'Hulpmiddelen',
288
289
  'app.lock_refused': 'Wijzigingen worden geweigerd tot de vergrendeling is opgeheven.',
289
290
  'app.search_pages': "Pagina's zoeken",
290
291
  'app.search_placeholder': 'Paginanaam of -nummer',
@@ -318,6 +319,9 @@ export const nl: BuilderMessages = {
318
319
  'app.table_or_view': 'Tabel of view (mastertabel voor master-detail)',
319
320
  'app.no_access': '{table} (geen toegang voor {role})',
320
321
  'app.ai_pages': "Pagina's maken met AI",
322
+ 'app.all_page_types': "Alle paginatypen, functies en kopieën",
323
+ 'app.task.add_feature': "Functiepagina toevoegen",
324
+ 'app.task.copy_page': "Pagina als kopie maken",
321
325
  'app.ai_pages_note': '(beschrijf ze in je eigen woorden)',
322
326
  'app.blank': 'Lege pagina maken',
323
327
  'app.page_number': 'Paginanummer',
@@ -1065,6 +1069,40 @@ export const nl: BuilderMessages = {
1065
1069
  'fts.gin_yes': "GIN-index",
1066
1070
  'fts.gin_no': "geen GIN-index",
1067
1071
  'fts.added': "Volledige-tekstzoeken toegevoegd aan {table}: kolom {column} met index {index}.",
1072
+ // (sprint 136, AP-1) Table Properties › Add vector column
1073
+ 'vec.add': "Vectorkolom toevoegen",
1074
+ 'vec.title': "Vectorkolom op {table}",
1075
+ 'vec.intro': "Voegt een embeddingkolom vector(n) toe (n: de dimensies van de vectorprovider), een HNSW-index voor de afstand, en een trigger die een rij opnieuw embedt als de bronkolommen veranderen. Een zoekconfiguratie van het type vector of hybrid, of de Retrieve-tool van een AI-agent, vindt rijen dan op betekenis. De rijen die er nu zijn, komen meteen in de wachtrij voor hun embeddings.",
1076
+ 'vec.existing': "Vectorkolommen nu",
1077
+ 'vec.provider': "Vectorprovider",
1078
+ 'vec.provider_help': "Gedeelde componenten › Vectorproviders van de applicaties in deze werkruimte. Zoek met dezelfde provider.",
1079
+ 'vec.keep': "Actueel houden",
1080
+ 'vec.keep_help': "Een functieprovider kan de embedding meteen in de trigger zetten. Een AI-service wordt door de server aangeroepen: de trigger zet de rij in de wachtrij en de server embedt rijen in batches, binnen de verzoeken per minuut van de provider en de dagelijkse AI-limieten van de applicatie.",
1081
+ 'vec.mode_set': "door de trigger gezet, meteen",
1082
+ 'vec.mode_queue': "in de wachtrij voor de server",
1083
+ 'vec.sources': "Bronkolommen",
1084
+ 'vec.sources_help': "De tekst die wordt ge-embed: deze kolommen, één per regel. Een wijziging in een ervan embedt de rij opnieuw.",
1085
+ 'vec.distance': "Afstand",
1086
+ 'vec.distance_help': "cosine voor de meeste embeddingmodellen; de zoekconfiguratie gebruikt dezelfde afstand.",
1087
+ 'vec.column': "Vectorkolom",
1088
+ 'vec.no_key': "De tabel heeft een primaire sleutel van één kolom nodig: de wachtrij vindt rijen daarmee.",
1089
+ 'vec.pick_provider': "Kies een vectorprovider.",
1090
+ 'vec.no_providers': "Nog geen applicatie in deze werkruimte heeft een vectorprovider: voeg er een toe onder Gedeelde componenten › Vectorproviders.",
1091
+ 'vec.missing': "De extensie pgvector is niet geïnstalleerd in deze database, dus een vectorkolom kan nog niet worden toegevoegd.",
1092
+ 'vec.missing_create': "De databaseserver heeft pgvector {version}: maak de extensie aan onder Werkruimtehulpmiddelen › Vectorproviders.",
1093
+ 'vec.missing_admin': "De databaseserver heeft pgvector {version}: een beheerder maakt de extensie aan (Werkruimtehulpmiddelen › Vectorproviders).",
1094
+ 'vec.missing_package': "De databaseserver heeft het pakket pgvector niet: installeer het (bijv. apt install postgresql-17-pgvector, of het Docker-image pgvector/pgvector) en maak dan de extensie aan.",
1095
+ 'vec.utilities': "Werkruimtehulpmiddelen › Vectorproviders",
1096
+ 'vec.kept_by': "bijgehouden door {provider} uit {columns}",
1097
+ 'vec.not_kept': "geen trigger houdt hem actueel",
1098
+ 'vec.progress': "Ge-embede rijen van {column}",
1099
+ 'vec.counts': "{done} van {total} rijen ge-embed, {queued} in de wachtrij",
1100
+ 'vec.failing': "{n} mislukt",
1101
+ 'vec.reembed_all': "alle rijen, niet alleen die zonder embedding",
1102
+ 'vec.backfill': "Aanvullen",
1103
+ 'vec.refresh': "Vernieuwen",
1104
+ 'vec.backfill_queued': "{n} rijen van {column} in de wachtrij voor hun embeddings.",
1105
+ 'vec.added': "Vectorkolom {column} toegevoegd aan {table}; {n} rijen in de wachtrij voor hun embeddings.",
1068
1106
  // ------------------------------------------------------------ Shared Components (sprint 124)
1069
1107
  'comp.nav_entry': 'Navigatie-item',
1070
1108
  'comp.nav_entry.plural': 'Navigatiemenu',
@@ -285,6 +285,7 @@ export const pt: BuilderMessages = {
285
285
  'app.tab.api': 'API REST',
286
286
  'app.tab.settings': 'Configurações',
287
287
  'app.tab.advisor': 'Consultor',
288
+ 'app.tab.utilities': 'Utilitários',
288
289
  'app.lock_refused': 'As alterações são recusadas até que seja desbloqueado.',
289
290
  'app.search_pages': 'Pesquisar páginas',
290
291
  'app.search_placeholder': 'Nome ou número da página',
@@ -318,6 +319,9 @@ export const pt: BuilderMessages = {
318
319
  'app.table_or_view': 'Tabela ou view (tabela mestre para mestre-detalhe)',
319
320
  'app.no_access': '{table} (sem acesso para {role})',
320
321
  'app.ai_pages': 'Criar páginas com IA',
322
+ 'app.all_page_types': "Todos os tipos de página, recursos e cópias",
323
+ 'app.task.add_feature': "Adicionar página de recurso",
324
+ 'app.task.copy_page': "Criar página como cópia",
321
325
  'app.ai_pages_note': '(descreva-as com suas próprias palavras)',
322
326
  'app.blank': 'Criar página em branco',
323
327
  'app.page_number': 'Número da página',
@@ -1065,6 +1069,40 @@ export const pt: BuilderMessages = {
1065
1069
  'fts.gin_yes': "índice GIN",
1066
1070
  'fts.gin_no': "sem índice GIN",
1067
1071
  'fts.added': "Pesquisa de texto integral adicionada a {table}: coluna {column} com o índice {index}.",
1072
+ // (sprint 136, AP-1) Table Properties › Add vector column
1073
+ 'vec.add': "Adicionar coluna vetorial",
1074
+ 'vec.title': "Coluna vetorial em {table}",
1075
+ 'vec.intro': "Adiciona uma coluna de embedding vector(n) (n: as dimensões do fornecedor de vetores), um índice HNSW para a distância e um gatilho que recalcula o embedding de uma linha quando as colunas de origem mudam. Uma configuração de pesquisa do tipo vector ou hybrid, ou a ferramenta Retrieve de um agente de IA, encontra então as linhas pelo significado. As linhas que já existem entram logo na fila para os seus embeddings.",
1076
+ 'vec.existing': "Colunas vetoriais atuais",
1077
+ 'vec.provider': "Fornecedor de vetores",
1078
+ 'vec.provider_help': "Componentes partilhados › Fornecedores de vetores das aplicações deste espaço de trabalho. Pesquise com o mesmo fornecedor.",
1079
+ 'vec.keep': "Manter atualizada",
1080
+ 'vec.keep_help': "Um fornecedor do tipo função pode definir o embedding no gatilho, de imediato. Um serviço de IA é chamado pelo servidor: o gatilho põe a linha na fila e o servidor calcula os embeddings em lotes, dentro dos pedidos por minuto do fornecedor e dos limites diários de IA da aplicação.",
1081
+ 'vec.mode_set': "definido pelo gatilho, de imediato",
1082
+ 'vec.mode_queue': "na fila para o servidor",
1083
+ 'vec.sources': "Colunas de origem",
1084
+ 'vec.sources_help': "O texto de que se calcula o embedding: estas colunas, uma por linha. Uma alteração numa delas recalcula o embedding da linha.",
1085
+ 'vec.distance': "Distância",
1086
+ 'vec.distance_help': "cosine para a maioria dos modelos de embedding; a configuração de pesquisa usa a mesma distância.",
1087
+ 'vec.column': "Coluna vetorial",
1088
+ 'vec.no_key': "A tabela precisa de uma chave primária de uma só coluna: a fila encontra as linhas por ela.",
1089
+ 'vec.pick_provider': "Escolha um fornecedor de vetores.",
1090
+ 'vec.no_providers': "Nenhuma aplicação deste espaço de trabalho tem ainda um fornecedor de vetores: adicione um em Componentes partilhados › Fornecedores de vetores.",
1091
+ 'vec.missing': "A extensão pgvector não está instalada nesta base de dados, por isso ainda não é possível adicionar uma coluna vetorial.",
1092
+ 'vec.missing_create': "O servidor de base de dados tem o pgvector {version}: crie a extensão em Utilitários do espaço de trabalho › Fornecedores de vetores.",
1093
+ 'vec.missing_admin': "O servidor de base de dados tem o pgvector {version}: um administrador cria a extensão (Utilitários do espaço de trabalho › Fornecedores de vetores).",
1094
+ 'vec.missing_package': "O servidor de base de dados não tem o pacote pgvector: instale-o (p. ex. apt install postgresql-17-pgvector, ou a imagem Docker pgvector/pgvector) e depois crie a extensão.",
1095
+ 'vec.utilities': "Utilitários do espaço de trabalho › Fornecedores de vetores",
1096
+ 'vec.kept_by': "mantida por {provider} a partir de {columns}",
1097
+ 'vec.not_kept': "nenhum gatilho a mantém atualizada",
1098
+ 'vec.progress': "Linhas de {column} com embedding",
1099
+ 'vec.counts': "{done} de {total} linhas com embedding, {queued} na fila",
1100
+ 'vec.failing': "{n} com erro",
1101
+ 'vec.reembed_all': "todas as linhas, não só as que não têm embedding",
1102
+ 'vec.backfill': "Completar",
1103
+ 'vec.refresh': "Atualizar",
1104
+ 'vec.backfill_queued': "{n} linhas de {column} na fila para os seus embeddings.",
1105
+ 'vec.added': "Coluna vetorial {column} adicionada a {table}; {n} linhas na fila para os seus embeddings.",
1068
1106
  // ------------------------------------------------------------ Shared Components (sprint 124)
1069
1107
  'comp.nav_entry': 'Entrada de navegação',
1070
1108
  'comp.nav_entry.plural': 'Menu de navegação',
@@ -285,6 +285,7 @@ export const zhCN: BuilderMessages = {
285
285
  'app.tab.api': 'REST API',
286
286
  'app.tab.settings': '设置',
287
287
  'app.tab.advisor': '顾问',
288
+ 'app.tab.utilities': '实用工具',
288
289
  'app.lock_refused': '解锁前将拒绝所有更改。',
289
290
  'app.search_pages': '搜索页面',
290
291
  'app.search_placeholder': '页面名称或编号',
@@ -318,6 +319,9 @@ export const zhCN: BuilderMessages = {
318
319
  'app.table_or_view': '表或视图(主从页面的主表)',
319
320
  'app.no_access': '{table}({role} 无访问权限)',
320
321
  'app.ai_pages': '使用 AI 创建页面',
322
+ 'app.all_page_types': "所有页面类型、功能和副本",
323
+ 'app.task.add_feature': "添加功能页",
324
+ 'app.task.copy_page': "以副本方式创建页面",
321
325
  'app.ai_pages_note': '(用您自己的话描述它们)',
322
326
  'app.blank': '创建空白页面',
323
327
  'app.page_number': '页码',
@@ -1065,6 +1069,40 @@ export const zhCN: BuilderMessages = {
1065
1069
  'fts.gin_yes': "GIN 索引",
1066
1070
  'fts.gin_no': "无 GIN 索引",
1067
1071
  'fts.added': "已为 {table} 添加全文搜索:列 {column},索引 {index}。",
1072
+ // (sprint 136, AP-1) Table Properties › Add vector column
1073
+ 'vec.add': "添加向量列",
1074
+ 'vec.title': "{table} 的向量列",
1075
+ 'vec.intro': "添加一个嵌入列 vector(n)(n:向量提供者的维数)、一个用于该距离的 HNSW 索引,以及在源列变化时重新嵌入该行的触发器。之后,搜索类型为 vector 或 hybrid 的搜索配置,或 AI 代理的 Retrieve 工具,就能按含义找到行。现有的行会立即进入嵌入队列。",
1076
+ 'vec.existing': "当前的向量列",
1077
+ 'vec.provider': "向量提供者",
1078
+ 'vec.provider_help': "此工作区应用程序的共享组件 › 向量提供者。搜索时使用同一个提供者。",
1079
+ 'vec.keep': "保持最新",
1080
+ 'vec.keep_help': "函数提供者可以在触发器中立即设置嵌入。AI 服务由服务器调用:触发器把行放入队列,服务器在提供者的每分钟请求数和应用程序的每日 AI 限额内批量嵌入队列中的行。",
1081
+ 'vec.mode_set': "由触发器立即设置",
1082
+ 'vec.mode_queue': "放入服务器的队列",
1083
+ 'vec.sources': "源列",
1084
+ 'vec.sources_help': "要嵌入的文本:这些列,每行一个。其中任一列变化时会重新嵌入该行。",
1085
+ 'vec.distance': "距离",
1086
+ 'vec.distance_help': "大多数嵌入模型使用 cosine;搜索配置使用相同的距离。",
1087
+ 'vec.column': "向量列",
1088
+ 'vec.no_key': "该表需要一个单列主键:队列据此查找行。",
1089
+ 'vec.pick_provider': "请选择一个向量提供者。",
1090
+ 'vec.no_providers': "此工作区的应用程序还没有向量提供者:请在共享组件 › 向量提供者中添加。",
1091
+ 'vec.missing': "此数据库未安装 pgvector 扩展,因此暂时无法添加向量列。",
1092
+ 'vec.missing_create': "数据库服务器有 pgvector {version}:请在工作区实用工具 › 向量提供者中创建该扩展。",
1093
+ 'vec.missing_admin': "数据库服务器有 pgvector {version}:由管理员创建该扩展(工作区实用工具 › 向量提供者)。",
1094
+ 'vec.missing_package': "数据库服务器没有 pgvector 软件包:请先安装(例如 apt install postgresql-17-pgvector,或 Docker 镜像 pgvector/pgvector),再创建该扩展。",
1095
+ 'vec.utilities': "工作区实用工具 › 向量提供者",
1096
+ 'vec.kept_by': "由 {provider} 根据 {columns} 维护",
1097
+ 'vec.not_kept': "没有触发器保持其最新",
1098
+ 'vec.progress': "{column} 已嵌入的行",
1099
+ 'vec.counts': "{total} 行中已嵌入 {done} 行,{queued} 行在队列中",
1100
+ 'vec.failing': "{n} 行失败",
1101
+ 'vec.reembed_all': "所有行,而不仅是没有嵌入的行",
1102
+ 'vec.backfill': "回填",
1103
+ 'vec.refresh': "刷新",
1104
+ 'vec.backfill_queued': "已将 {column} 的 {n} 行放入嵌入队列。",
1105
+ 'vec.added': "已为 {table} 添加向量列 {column};{n} 行已进入嵌入队列。",
1068
1106
  // ------------------------------------------------------------ Shared Components (sprint 124)
1069
1107
  'comp.nav_entry': '导航条目',
1070
1108
  'comp.nav_entry.plural': '导航菜单',
@@ -285,6 +285,7 @@ export const zhTW: BuilderMessages = {
285
285
  'app.tab.api': 'REST API',
286
286
  'app.tab.settings': '設定',
287
287
  'app.tab.advisor': '顧問',
288
+ 'app.tab.utilities': '公用程式',
288
289
  'app.lock_refused': '在解除鎖定前,所有變更都會被拒絕。',
289
290
  'app.search_pages': '搜尋頁面',
290
291
  'app.search_placeholder': '頁面名稱或編號',
@@ -318,6 +319,9 @@ export const zhTW: BuilderMessages = {
318
319
  'app.table_or_view': '表格或檢視(主從式的主表格)',
319
320
  'app.no_access': '{table}({role} 無存取權)',
320
321
  'app.ai_pages': '以 AI 建立頁面',
322
+ 'app.all_page_types': "所有頁面類型、功能與副本",
323
+ 'app.task.add_feature': "新增功能頁",
324
+ 'app.task.copy_page': "以副本建立頁面",
321
325
  'app.ai_pages_note': '(用您自己的話描述)',
322
326
  'app.blank': '建立空白頁面',
323
327
  'app.page_number': '頁面編號',
@@ -1065,6 +1069,40 @@ export const zhTW: BuilderMessages = {
1065
1069
  'fts.gin_yes': "GIN 索引",
1066
1070
  'fts.gin_no': "無 GIN 索引",
1067
1071
  'fts.added': "已為 {table} 新增全文檢索:欄位 {column},索引 {index}。",
1072
+ // (sprint 136, AP-1) Table Properties › Add vector column
1073
+ 'vec.add': "新增向量欄位",
1074
+ 'vec.title': "{table} 的向量欄位",
1075
+ 'vec.intro': "新增一個嵌入欄位 vector(n)(n:向量提供者的維度數)、一個用於該距離的 HNSW 索引,以及在來源欄位變更時重新嵌入該列的觸發程序。之後,搜尋類型為 vector 或 hybrid 的搜尋組態,或 AI 代理的 Retrieve 工具,就能依含意找到資料列。現有的資料列會立即進入嵌入佇列。",
1076
+ 'vec.existing': "目前的向量欄位",
1077
+ 'vec.provider': "向量提供者",
1078
+ 'vec.provider_help': "此工作區應用程式的共用元件 › 向量提供者。搜尋時使用同一個提供者。",
1079
+ 'vec.keep': "保持最新",
1080
+ 'vec.keep_help': "函式提供者可以在觸發程序中立即設定嵌入。AI 服務由伺服器呼叫:觸發程序將資料列放入佇列,伺服器在提供者的每分鐘請求數與應用程式的每日 AI 上限內,批次嵌入佇列中的資料列。",
1081
+ 'vec.mode_set': "由觸發程序立即設定",
1082
+ 'vec.mode_queue': "放入伺服器的佇列",
1083
+ 'vec.sources': "來源欄位",
1084
+ 'vec.sources_help': "要嵌入的文字:這些欄位,每行一個。其中任一欄位變更時會重新嵌入該列。",
1085
+ 'vec.distance': "距離",
1086
+ 'vec.distance_help': "大多數嵌入模型使用 cosine;搜尋組態使用相同的距離。",
1087
+ 'vec.column': "向量欄位",
1088
+ 'vec.no_key': "此表格需要單一欄位的主索引鍵:佇列以此找出資料列。",
1089
+ 'vec.pick_provider': "請選擇一個向量提供者。",
1090
+ 'vec.no_providers': "此工作區的應用程式還沒有向量提供者:請在共用元件 › 向量提供者中新增。",
1091
+ 'vec.missing': "此資料庫未安裝 pgvector 擴充功能,因此暫時無法新增向量欄位。",
1092
+ 'vec.missing_create': "資料庫伺服器有 pgvector {version}:請在工作區公用程式 › 向量提供者中建立該擴充功能。",
1093
+ 'vec.missing_admin': "資料庫伺服器有 pgvector {version}:由管理員建立該擴充功能(工作區公用程式 › 向量提供者)。",
1094
+ 'vec.missing_package': "資料庫伺服器沒有 pgvector 套件:請先安裝(例如 apt install postgresql-17-pgvector,或 Docker 映像檔 pgvector/pgvector),再建立該擴充功能。",
1095
+ 'vec.utilities': "工作區公用程式 › 向量提供者",
1096
+ 'vec.kept_by': "由 {provider} 依 {columns} 維護",
1097
+ 'vec.not_kept': "沒有觸發程序讓它保持最新",
1098
+ 'vec.progress': "{column} 已嵌入的資料列",
1099
+ 'vec.counts': "{total} 列中已嵌入 {done} 列,{queued} 列在佇列中",
1100
+ 'vec.failing': "{n} 列失敗",
1101
+ 'vec.reembed_all': "所有資料列,而不只是沒有嵌入的資料列",
1102
+ 'vec.backfill': "回填",
1103
+ 'vec.refresh': "重新整理",
1104
+ 'vec.backfill_queued': "已將 {column} 的 {n} 列放入嵌入佇列。",
1105
+ 'vec.added': "已為 {table} 新增向量欄位 {column};{n} 列已進入嵌入佇列。",
1068
1106
  // ------------------------------------------------------------ Shared Components (sprint 124)
1069
1107
  'comp.nav_entry': '導覽項目',
1070
1108
  'comp.nav_entry.plural': '導覽選單',
@@ -1,5 +1,5 @@
1
1
  import type { FastifyInstance } from 'fastify';
2
- import { owner } from '../db.ts';
2
+ import { owner, type Client } from '../db.ts';
3
3
  import { APP_MFA, EMAIL_CODE_MINUTES, EMAIL_CODES_PER_HOUR, type AppMfa } from '../account-mfa.ts';
4
4
  import { html, raw } from '../html.ts';
5
5
  import { secretKeyConfigured } from '../secrets.ts';
@@ -78,6 +78,21 @@ export async function saveLoginSettings(appId: string, b: Record<string, string
78
78
  [appId, page, b.password_reset === 'true', mfa, roles, b.mfa_email === 'true']);
79
79
  }
80
80
 
81
+ /** Page 9999 (or the next free number), public, minimal, with a login region, made the login page. (137) Also Add feature → Login page. */
82
+ export async function createLoginPage(c: Client, a: { id: number; name: string }) {
83
+ const n = (await c.query<{ n: number }>(
84
+ `select case when not exists (select 1 from meta.page where app_id = $1 and page_no = 9999) then 9999
85
+ else (select max(page_no) + 1 from meta.page where app_id = $1) end as n`, [a.id],
86
+ )).rows[0].n;
87
+ const id = (await c.query<{ id: number }>(
88
+ `insert into meta.page (app_id, page_no, name, title, requires_auth, page_template) values ($1, $2, 'Login', $3, false, 'minimal') returning id`,
89
+ [a.id, n, a.name],
90
+ )).rows[0].id;
91
+ await c.query(`insert into meta.region (page_id, seq, title, type, template) values ($1, 10, $2, 'login', 'standard')`, [id, a.name]);
92
+ await c.query('update meta.app set login_page = $2, updated_at = now() where id = $1', [a.id, n]);
93
+ return { page: id, no: n };
94
+ }
95
+
81
96
  export async function loginPageRoutes(app: FastifyInstance) {
82
97
  // Create a login page: page 9999 (or the next free number), public, minimal, with a login region
83
98
  app.post(`${BASE}/apps/:id/login-page`, async (req: Req, reply) => {
@@ -86,19 +101,7 @@ export async function loginPageRoutes(app: FastifyInstance) {
86
101
  const a = await owner.one<{ id: number; name: string }>('select id, name from meta.app where id = $1', [req.params.id]);
87
102
  if (!a) return reply.code(404).send('Not found');
88
103
  try {
89
- const { page, no } = await owner.tx(async (c) => {
90
- const n = (await c.query<{ n: number }>(
91
- `select case when not exists (select 1 from meta.page where app_id = $1 and page_no = 9999) then 9999
92
- else (select max(page_no) + 1 from meta.page where app_id = $1) end as n`, [a.id],
93
- )).rows[0].n;
94
- const id = (await c.query<{ id: number }>(
95
- `insert into meta.page (app_id, page_no, name, title, requires_auth, page_template) values ($1, $2, 'Login', $3, false, 'minimal') returning id`,
96
- [a.id, n, a.name],
97
- )).rows[0].id;
98
- await c.query(`insert into meta.region (page_id, seq, title, type, template) values ($1, 10, $2, 'login', 'standard')`, [id, a.name]);
99
- await c.query('update meta.app set login_page = $2, updated_at = now() where id = $1', [a.id, n]);
100
- return { page: id, no: n };
101
- });
104
+ const { page, no } = await owner.tx((c) => createLoginPage(c, a));
102
105
  flash(s, `Page ${no} "Login" created and made the login page. Add regions around the sign-in form in the Page Designer.`);
103
106
  return back(reply, s, `${BASE}/pages/${page}`);
104
107
  } catch (e) {
@@ -71,34 +71,52 @@ export const featuresFrom = (b: Record<string, unknown>, posted: boolean): Featu
71
71
  /** (sprint 73) The Activity page of a generated application. */
72
72
  export const ACTIVITY_PAGE = 9010;
73
73
 
74
- async function applyFeatures(c: Client, appId: number, f: Partial<Features>, notes: string[]) {
75
- if (f.access) {
76
- await c.query(
77
- `insert into meta.authz_scheme (app_id, name, type, value, error_message) values
78
- ($1, 'CONTRIBUTOR', 'sql', 'meta.has_role(''admin'') or meta.has_role(''contributor'')', 'Only contributors can do this.'),
79
- ($1, 'READER', 'sql', 'meta.has_role(''admin'') or meta.has_role(''contributor'') or meta.has_role(''reader'')', 'Only readers can open this page.')
80
- on conflict do nothing`,
81
- [appId],
82
- );
83
- }
84
- if (f.activity && !(await c.query('select 1 from meta.page where app_id = $1 and page_no = $2', [appId, ACTIVITY_PAGE])).rowCount) {
85
- const home = (await c.query('select home_page from meta.app where id = $1', [appId])).rows[0].home_page;
86
- const page = (await c.query(`insert into meta.page (app_id, page_no, name, title, parent_page, authz) values ($1, $2, 'Activity', 'Activity', $3, 'ADMIN') returning id`, [appId, ACTIVITY_PAGE, home])).rows[0].id;
87
- await c.query(
88
- `insert into meta.region (page_id, seq, title, type, source, config) values
89
- ($1, 10, 'Page views per day', 'chart', $2, '{"kind": "column"}'),
90
- ($1, 20, 'Latest activity', 'report', $3, '{"headings": {"at": "Time", "username": "User", "event": "Event", "page_no": "Page", "elapsed_ms": "Time taken (ms)"}}')`,
91
- [page,
92
- `select to_char(date_trunc('day', at), 'YYYY-MM-DD') as "Day", count(*) as "Page views"
74
+ /** (sprint 73; exported in 137 for Add feature) The authorization schemes CONTRIBUTOR and READER (and ADMIN when missing). */
75
+ export async function addAccessSchemes(c: Client, appId: number) {
76
+ await ensureAdminScheme(c, appId);
77
+ await c.query(
78
+ `insert into meta.authz_scheme (app_id, name, type, value, error_message) values
79
+ ($1, 'CONTRIBUTOR', 'sql', 'meta.has_role(''admin'') or meta.has_role(''contributor'')', 'Only contributors can do this.'),
80
+ ($1, 'READER', 'sql', 'meta.has_role(''admin'') or meta.has_role(''contributor'') or meta.has_role(''reader'')', 'Only readers can open this page.')
81
+ on conflict do nothing`,
82
+ [appId],
83
+ );
84
+ }
85
+
86
+ /** (137) The authorization scheme ADMIN (role admin) that administrators' pages use, unless the application has one. */
87
+ export async function ensureAdminScheme(c: Client, appId: number) {
88
+ await c.query(
89
+ `insert into meta.authz_scheme (app_id, name, type, value, error_message) values ($1, 'ADMIN', 'role', 'admin', 'Only administrators can access this page.')
90
+ on conflict do nothing`,
91
+ [appId],
92
+ );
93
+ }
94
+
95
+ /** (sprint 73; exported in 137 for Add feature) The Activity page for administrators and its navigation entry; nothing when the page number is taken. */
96
+ export async function addActivityPage(c: Client, appId: number) {
97
+ if ((await c.query('select 1 from meta.page where app_id = $1 and page_no = $2', [appId, ACTIVITY_PAGE])).rowCount) return;
98
+ await ensureAdminScheme(c, appId);
99
+ const home = (await c.query('select home_page from meta.app where id = $1', [appId])).rows[0].home_page;
100
+ const page = (await c.query(`insert into meta.page (app_id, page_no, name, title, parent_page, authz) values ($1, $2, 'Activity', 'Activity', $3, 'ADMIN') returning id`, [appId, ACTIVITY_PAGE, home])).rows[0].id;
101
+ await c.query(
102
+ `insert into meta.region (page_id, seq, title, type, source, config) values
103
+ ($1, 10, 'Page views per day', 'chart', $2, '{"kind": "column"}'),
104
+ ($1, 20, 'Latest activity', 'report', $3, '{"headings": {"at": "Time", "username": "User", "event": "Event", "page_no": "Page", "elapsed_ms": "Time taken (ms)"}}')`,
105
+ [page,
106
+ `select to_char(date_trunc('day', at), 'YYYY-MM-DD') as "Day", count(*) as "Page views"
93
107
  from meta.app_activity(30)
94
108
  where event = 'page_view'
95
109
  group by 1
96
110
  order by 1`,
97
- `select at, username, event, page_no, elapsed_ms
111
+ `select at, username, event, page_no, elapsed_ms
98
112
  from meta.app_activity(30)`],
99
- );
100
- await c.query(`insert into meta.nav_entry (app_id, seq, label, icon, target_page, authz) values ($1, 9010, 'Activity', 'activity', $2, 'ADMIN')`, [appId, ACTIVITY_PAGE]);
101
- }
113
+ );
114
+ await c.query(`insert into meta.nav_entry (app_id, seq, label, icon, target_page, authz) values ($1, 9010, 'Activity', 'activity', $2, 'ADMIN')`, [appId, ACTIVITY_PAGE]);
115
+ }
116
+
117
+ async function applyFeatures(c: Client, appId: number, f: Partial<Features>, notes: string[]) {
118
+ if (f.access) await addAccessSchemes(c, appId);
119
+ if (f.activity) await addActivityPage(c, appId);
102
120
  if (f.feedback) await c.query('update meta.app set feedback = true where id = $1', [appId]);
103
121
  if (f.theme === false) await c.query(`update meta.app set theme = theme || '{"user_choice": false}' where id = $1`, [appId]);
104
122
  if (f.pwa || f.push) await c.query('update meta.app set pwa = true where id = $1', [appId]);
@@ -33,6 +33,7 @@ export function objectMenu(schema: string, o: MenuObj): Raw {
33
33
  ${table ? item(`${BASE}/sql/objects/security?${new URLSearchParams({ o: o.ref })}`, 'shield', 'Security…') : ''}
34
34
  ${table ? item(`${BASE}/sql/load`, 'upload', 'Load data') : ''}
35
35
  ${table ? item(`${BASE}/sql/objects/fulltext?${new URLSearchParams({ o: o.ref })}`, 'search', 'Add full-text search…') : ''}
36
+ ${table ? item(`${BASE}/sql/objects/vector?${new URLSearchParams({ o: o.ref })}`, 'search', 'Add vector column…') : ''}
36
37
  ${table ? html`${item(q('index'), 'list', 'Add index')}${item(q('trigger'), 'bolt', 'Add trigger')}${item(q('policy'), 'shield', 'Add policy')}` : ''}
37
38
  ${RELATIONS.has(o.type) ? item(`${BASE}/sql/rest/object?${new URLSearchParams({ schema, name: o.name })}`, 'globe', 'REST › Enable / Disable') : ''}
38
39
  ${item(q('ddl'), 'code', 'Quick DDL to the worksheet')}
@@ -1,4 +1,5 @@
1
1
  import { advise } from './advisor.ts';
2
+ import { appChecksum } from '../history.ts';
2
3
  import { appLocks, isAdmin, lockPanel, lockText } from './locks.ts';
3
4
  import { parseRoleList, validRoleName } from '../dbauth.ts';
4
5
  import { DEFAULT_HEADER, headerProxiesConfigured } from '../headerauth.ts';
@@ -23,6 +24,8 @@ import { securityRoutes } from './security.ts';
23
24
  import { instanceRoutes } from './instance.ts';
24
25
  import { saveTimeZoneSettings, timeZoneSettings } from './globalization.ts';
25
26
  import { WIZARD_KINDS, wizardRoutes, wizardTables } from './wizards.ts';
27
+ import { createPageRoutes } from './create-page.ts';
28
+ import { featureRoutes } from './features.ts';
26
29
  import { appFromFileRoutes } from './appfromfile.ts';
27
30
  import { clearDevbar } from '../runtime/devbar.ts';
28
31
  import { EMAIL } from './find-workspace.ts';
@@ -564,6 +567,9 @@ export async function builderRoutes(app: FastifyInstance) {
564
567
  await securityRoutes(app);
565
568
  await builderSsoRoutes(app);
566
569
  await wizardRoutes(app);
570
+ // (137) Create page: the grid of page types, features and copy (create-page.ts, features.ts)
571
+ await createPageRoutes(app);
572
+ await featureRoutes(app);
567
573
  await appFromFileRoutes(app);
568
574
 
569
575
  // (sprint 73) POST /apps, creating an application, is in appwizard.ts
@@ -653,7 +659,7 @@ export async function builderRoutes(app: FastifyInstance) {
653
659
  <button class="btn">${icon('search')}<span class="sr-only">${tx('common.search')}</span></button></form>
654
660
  <div class="seg view-toggle" role="group" aria-label="${tx('ab.view')}">
655
661
  <a href="${keep({})}"${icons ? '' : raw(' aria-current="true"')}>${icon('list')} ${tx('ab.view_report')}</a><a href="${keep({ view: 'icons' })}"${icons ? raw(' aria-current="true"') : ''}>${icon('grid')} ${tx('app.view_icons')}</a></div>
656
- <a class="btn btn-hot" href="#create-page">${icon('plus')} ${tx('app.create_page')}</a>
662
+ <a class="btn btn-hot" href="${BASE}/apps/${a.id}/create-page">${icon('plus')} ${tx('app.create_page')}</a>
657
663
  </div>
658
664
  ${region(tx('app.tab.pages'), html`${grouped
659
665
  ? html`<form method="get" class="search u-mwnone u-mb1"><div class="field"><label class="label" for="f_group">${tx('app.page_group')}</label>
@@ -681,6 +687,7 @@ export async function builderRoutes(app: FastifyInstance) {
681
687
  </div>
682
688
  <div class="buttons"><button class="btn btn-hot">${tx('common.next')}</button></div>
683
689
  </form>
690
+ <p><a href="${BASE}/apps/${a.id}/create-page">${tx('app.all_page_types')} ▸</a></p>
684
691
  <p><a href="${BASE}/apps/${a.id}/ai-pages">${tx('app.ai_pages')} ▸</a> <span class="muted">${tx('app.ai_pages_note')}</span></p>`)}
685
692
  ${region(tx('app.blank'), html`
686
693
  <form method="post" action="${BASE}/apps/${a.id}/pages">${csrf(s)}
@@ -697,7 +704,9 @@ export async function builderRoutes(app: FastifyInstance) {
697
704
  <aside class="app-tasks region region-standard" aria-labelledby="app-tasks-h"><header class="region-header"><h2 id="app-tasks-h">${tx('common.tasks')}</h2></header>
698
705
  <ul class="sc-links">
699
706
  <li><a href="/a/${a.alias}" target="_blank" rel="noopener">${icon('play')} ${tx('app.task.run')}</a></li>
700
- <li><a href="#create-page">${icon('plus')} ${tx('app.create_page')}</a></li>
707
+ <li><a href="${BASE}/apps/${a.id}/create-page">${icon('plus')} ${tx('app.create_page')}</a></li>
708
+ <li><a href="${BASE}/apps/${a.id}/create-page?tab=feature">${icon('puzzle')} ${tx('app.task.add_feature')}</a></li>
709
+ <li><a href="${BASE}/apps/${a.id}/create-page?tab=copy">${icon('copy')} ${tx('app.task.copy_page')}</a></li>
701
710
  <li><a href="${BASE}/apps/${a.id}/ai-pages">${icon('lightbulb')} ${tx('app.ai_pages')}</a></li>
702
711
  <li><a href="${BASE}/apps/${a.id}/shared">${icon('layers')} ${tx('app.tab.shared')}</a></li>
703
712
  <li><a href="${BASE}/apps/${a.id}/shared?type=page_group">${icon('folder')} ${tx('app.task.page_groups')}</a></li>
@@ -743,6 +752,8 @@ export async function builderRoutes(app: FastifyInstance) {
743
752
  const a = await appOr404(req.params.id);
744
753
  if (!a) return reply.code(404).send('Not found');
745
754
  const r = await owner.one('select meta.export_app($1) as doc', [a.alias]);
755
+ // (sprint 138) the application checksum (src/history.ts), as on Utilities
756
+ reply.header('x-pgkiln-checksum', appChecksum(r.doc));
746
757
  // (sprint 106) the change log marks the change sets made before an application's latest export
747
758
  await logActivity({ appId: a.id, username: s.username, event: 'app_export', ip: clientIp(req), detail: String(req.query?.format ?? 'json') });
748
759
  // ?format=dir|text: one file per component, as `pgkiln export --format dir|text` writes it (docs/guide/18-cli.md)
@@ -76,7 +76,7 @@ export function sharedGroups(appId: number): { key: string; title: string; icon:
76
76
  { key: 'data', title: 'Data Sources', icon: 'database', items: [t('rest_source', 'REST Data Sources'), t('web_credential', 'Web Credentials'), t('rest_module', 'RESTful Services'), t('data_load_def', 'Data Load Definitions')] },
77
77
  { key: 'workflows', title: 'Workflows and Automations', icon: 'activity', items: [t('workflow_definition', 'Workflows'), t('task_definition', 'Task Definitions'), t('automation', 'Automations')] },
78
78
  { key: 'globalization', title: 'Globalization', icon: 'globe', items: [page('globalization', 'Translations and Text Messages'), t('dynamic_translation', 'Dynamic Translations'), settings('globalization', 'Globalization Attributes')] },
79
- { key: 'ai', title: 'Generative AI', icon: 'lightbulb', items: [page('ai', 'AI Services'), page('ai-agents', 'AI Agents')] },
79
+ { key: 'ai', title: 'Generative AI', icon: 'lightbulb', items: [page('ai', 'AI Services'), page('ai-agents', 'AI Agents'), page('vector-providers', 'Vector Providers')] },
80
80
  ];
81
81
  }
82
82
 
@@ -20,6 +20,7 @@ import { createTemplate, objectBrowserRoutes } from './object-browser.ts';
20
20
  import { objectActionRoutes } from './object-actions.ts';
21
21
  import { tableEditorRoutes } from './table-editor.ts';
22
22
  import { fulltextRoutes } from './fulltext.ts';
23
+ import { vectorColumnRoutes } from './vector-column.ts';
23
24
  import { dataSecurityRoutes } from './data-security.ts';
24
25
  import { dataModelerRoutes } from './data-modeler.ts';
25
26
  import { autoRestBuilderRoutes } from './autorest.ts';
@@ -137,6 +138,8 @@ export async function sqlRoutes(app: FastifyInstance) {
137
138
  await app.register(tableEditorRoutes);
138
139
  // (sprint 135, AP-2) Table Properties › Add full-text search
139
140
  await app.register(fulltextRoutes);
141
+ // (sprint 136, AP-1) Table Properties › Add vector column
142
+ await app.register(vectorColumnRoutes);
140
143
  await app.register(dataSecurityRoutes);
141
144
  // (sprint 98) the Data Modeler: relational diagrams, their DDL and data dictionary
142
145
  await app.register(dataModelerRoutes);
@@ -143,6 +143,7 @@ function form(s: Session, def: TableDef, opts: { editing: TableDef | null; schem
143
143
  <button class="btn btn-hot" name="mode" value="apply">${icon('check')} ${tx('te.apply')}</button>
144
144
  <button class="btn" name="mode" value="worksheet">${icon('code')} ${tx('te.open_ws')}</button>
145
145
  ${editing ? html`<a class="btn" href="${BASE}/sql/objects/fulltext?${new URLSearchParams({ o: opts.ref })}">${icon('search')} ${tx('fts.add')}…</a>` : ''}
146
+ ${editing ? html`<a class="btn" href="${BASE}/sql/objects/vector?${new URLSearchParams({ o: opts.ref })}">${icon('search')} ${tx('vec.add')}…</a>` : ''}
146
147
  <a class="btn" href="${editing ? `${BASE}/sql/objects?${new URLSearchParams({ schema: def.schema, k: 'table', o: opts.ref })}` : `${BASE}/sql/objects?${new URLSearchParams({ schema: def.schema })}`}">${tx('common.cancel')}</a>
147
148
  </div></form>`;
148
149
  }