@fui-org/fui-cli 2.2.1 → 2.2.3

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.
package/dist/fui.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-xg17j3f9.js");await e();
2
+ import{enableCompileCache as a}from"node:module";a?.();var{run:e}=await import("./fui-8tyckpke.js");await e();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fui-org/fui-cli",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Token-lean CLI for the FUI low-code platform (Node.js)",
5
5
  "keywords": [
6
6
  "ai",
@@ -72,13 +72,13 @@ A request to create, design, fix or update a module authorizes the agent to publ
72
72
 
73
73
  **Imports**
74
74
 
75
- - `f-echart`, `f-sheet`, `f-editor`, `f-pdfmake`, `f-file-upload`, `f-image-update`, `f-qrcode`, `f-qrcode-reader`, `f-editor-dialog` need external libs → **check imports on every add OR remove**. → [component-quickref.md](references/component-quickref.md)
75
+ - Before using an existing control, check `defaultControlAttr[control].import` in `scripts/component.js` (V2) or `scripts/component-3.0.js` (V3). Treat that array and its order as the source of truth for required JS/CSS; then check imports on every add or remove. → [component-quickref.md](references/component-quickref.md)
76
76
  - **NEVER** add CDN for bundled libs (jQuery, lodash, moment, numeral, Vuetify, toast, jquery-confirm...); list: [script-map.md](references/script-map.md) §0.
77
77
  - That ban is `HTMLOnly=false` only. `HTMLOnly: true` bundles **nothing** (no Vue, Vuetify, jQuery, lodash, MDI, `projectdefaultstyle.css`, `#fastProjectAPP`): CDNs are **required**, **per HTMLOnly module** (project level has no effect). → [module-structure.md](references/module-structure.md) §HTMLOnly=true
78
78
 
79
79
  **Database** (enforced in code; details under "Writes need the user")
80
80
 
81
- - **NEVER** `DROP TABLE` or `DROP DATABASE`, anywhere, incl. SP scripts.
81
+ - **NEVER** `DROP DATABASE` or drop a persistent table, anywhere, incl. SP scripts. `DROP TABLE #name` (one local temporary table) is allowed; `##name`, schema-qualified names and a mixed drop list remain blocked.
82
82
  - `ALTER TABLE` (structure OK, data loss not): **free** `ADD` (column/constraint), `DROP CONSTRAINT` · **needs `--confirm-write`** `ALTER COLUMN` · **hard-blocked** `DROP COLUMN`, `SWITCH`, all else (fail-closed). Checked **per statement**: an `ADD` doesn't cover a later `DROP COLUMN` in the batch; hard-block wins.
83
83
  - **NEVER** `DELETE`/`UPDATE` without `WHERE`; show the SQL for the user to run.
84
84
  - **Never test on the live system**; the line is **writes**, not network. Verify = read definition + reason + cross-check; **never run an SP that writes**, and never call a real API/SP/DML "to test". Levels:
@@ -225,7 +225,7 @@ Before writing `apiMocks` for an SP that exists, run `fui sp verify <name>`. It
225
225
  - Writes to real data exit `3` until you add `--confirm-write`. That covers `INSERT`, `UPDATE`, `DELETE`, `MERGE` and `TRUNCATE` in `query` or `exec` (also inside `EXEC('...')` or `sp_executesql`), `SELECT ... INTO` a real table, `DROP PROCEDURE`/`FUNCTION`/`VIEW`, and `ALTER TABLE ... ALTER COLUMN`.
226
226
  - Add `--confirm-write` **only after the user approved that exact write in chat**, never just to make it run. → [db-workflow.md](references/db-workflow.md)
227
227
  - Always blocked, with no flag to unlock them. Show the SQL to the user to run by hand:
228
- - `DROP TABLE` and `DROP DATABASE`
228
+ - `DROP TABLE` for a persistent/global table and `DROP DATABASE` (a single local `#name` temporary table is allowed)
229
229
  - `ALTER TABLE ... DROP COLUMN` or `SWITCH`
230
230
  - `xp_cmdshell`
231
231
  - `DELETE`/`UPDATE` without `WHERE`, in `query` and `exec`
@@ -257,6 +257,12 @@ fui query "SELECT TOP 5 ID, Name FROM tblX" --json | jq -r '.[].Name'
257
257
 
258
258
  ## Connections and secrets
259
259
 
260
+ For any DB exploration, schema, SQL, or API/SP design, start with `fui db list -p <project>` and use a saved connection with `--db`. These operations use its **dbToken**. A `userToken` is only for the `acc` management lookup and end-user API calls; it cannot authenticate SQL requests.
261
+
262
+ One `userToken` is reusable across projects in the same SSO zone (the parent domain, e.g. `*.lhu.edu.vn`). When a project has no token, the CLI looks for one on the exact API host first, then on another host in that zone. Do not ask for a new login when there is one unique token in the zone. A different zone needs its own token; if several distinct tokens exist in the zone, use `--user-token-from <project/connectionName>` for `db alias list` or `db add-by-name` to select an already saved token without exposing it. → [db-workflow.md](references/db-workflow.md) §6a.
263
+
264
+ If no connection exists, find the project's tAPI domain with `fui project sync -p <project>` when needed, then run `fui db alias list -p <project>` to see databases the account may manage. Connect the chosen alias with `fui db add-by-name <apiName> -p <project>`. This discovers SQLServer/DatabaseName through `acc`, reuses a SQL UID/PWD pair from a saved dbToken (same apiDomain preferred; another domain works when it is the only login), builds a new dbToken, tests it against the target DB, and fetches schema. With several source logins, select one with `--borrow-from <project/connectionName>`. If there is no saved SQL login, use `--uid <login> --pwd -` or `--pwd-env VAR` with `add-by-name`. A management userToken alone cannot reveal the SQL password; never treat the `Password` field from `acc` as one. Then use `--db <project/connectionName>` for DB work.
265
+
260
266
  ```bash
261
267
  printf %s "$TOKEN" | fui db add docs -p lhu-docs --token - --api-domain tapi.lhu.edu.vn --user-token-env UT
262
268
  fui db add local -p demo --conn-env SQLITE_URL # sqlite:// connects directly (MSSQL goes through tAPI)
@@ -268,11 +274,12 @@ fui token decode --db lhu-docs/docs # never shows the password
268
274
  Secrets are accepted only from stdin (`-`) or `--*-env VAR`. Never echo or paste token values.
269
275
 
270
276
  - Never `cat`, `jq` or otherwise print `_db/_connections.json` or `~/.config/fui/config.json`. Use `fui db list`, `fui token decode` or `fui config`; they never print secrets.
271
- - No token at hand? `fui db user-token` lists fingerprints of the tokens already in the workspace; copy one with `--from <project/alias>`. The value never appears.
272
- - Know the alias but not the connection string? `fui db alias list`, then `fui db add-by-name <apiName>`: it borrows the SQL login of a connection on the same apiDomain.
277
+ - Need a management userToken for the `acc` lookup? `fui db user-token` lists fingerprints of tokens already in the workspace; copy one with `--from <project/alias>`. This does not create a dbToken. The value never appears.
278
+ - Know the alias but not the connection string? `fui db alias list`, then `fui db add-by-name <apiName>`: it reuses a saved SQL UID/PWD pair and combines it with the target server/database returned by `acc`.
273
279
  - `db add` / `db add-by-name` take `--name <connectionName>`; omitted, it is suggested from apiName, then apiName_zone. The same server + database re-added keeps its name, and folders are never renamed.
280
+ - `fui db remove <project>/<connectionName>` removes one local connection from `_connections.json`. It does not change the server; cached schema and SQL files under `_db/<connectionName>/` remain available.
274
281
  - `right *` and `db alias new` write to the shared acc system. Ask the user first, especially for `delete`.
275
- - `_db/_connections.json` is version 2. An old v1 file is upgraded the first time fui reads it (the old file goes to `_history/`); `fui db migrate` does it for every project at once.
282
+ - `_db/_connections.json` has no `version` field. Existing files with `"version": 2` or connections missing `apiName` are normalized on first read (the old file goes to `_history/`); `fui db migrate` does this for every project at once.
276
283
 
277
284
  ## FUI conventions
278
285
 
@@ -49,7 +49,7 @@ Load: `fui skill ref references/a.md references/b.md` (several at once OK).
49
49
 
50
50
  - SP naming, params, URL, response, errors, wiring → `tapi-reference.md`
51
51
  - 200 but empty/wrong data, or changing/removing params of an SP a module calls: lenient binding (missing → `NULL`, extra → ignored, never errors) → `tapi-reference.md` §2
52
- - HTTP headers in SP (`User-Agent`, `Cookie`, `X-Forwarded-For`, client IP, device/browser): `@sys_header_*`, each `-` → `_` → `tapi-reference.md` §2
52
+ - Remote address in SP: `@sys_RemoteIP varchar(15)` from `REMOTE_ADDR`; HTTP headers (`User-Agent`, `Cookie`, `X-Forwarded-For`, device/browser): `@sys_header_*`, each `-` → `_`, except `Authorization` → `tapi-reference.md` §2
53
53
  - SP permission checks (5 patterns, `@sys_SystemRight`, `getSystemRight`) → `tapi-permission-patterns.md`
54
54
  - File upload/download/view (`spAPIFILE_`, `fileContent`, `tblFileData`) → `tapi-file-api.md`
55
55
  - SQL CLR: httpCall, sendMail, ImageResize, FileReader, RegexMatch… → `sql-clr-functions.md`
@@ -41,23 +41,25 @@ Prefer `f-*`; use `v-*` only when no FUI equivalent exists.
41
41
 
42
42
  ## Components needing JS/CSS imports
43
43
 
44
- **When adding or removing one of these, run `fui import list` and add/remove the matching imports.**
44
+ **Before using any built-in control, inspect `defaultControlAttr[control].import` in `scripts/component.js` for V2 or `scripts/component-3.0.js` for V3.** That ordered array is the source of truth for JS/CSS URLs. The table below is a quick reference, not an exhaustive list. When adding or removing a control with imports, run `fui import list` and add/remove the matching imports. Do not substitute a similar CDN URL without checking the selected framework's source.
45
45
 
46
46
  | Component | Required imports |
47
47
  | ----------------- | -------------------------------------------------------------------------------------------------------- |
48
48
  | `f-pdfmake` | `/include/pdfmake/pdfmake.min.js` · `/include/pdfmake/print.min.js` · `/include/pdfmake/vfs_fonts.js` |
49
49
  | `f-file-upload` | `/include/plupload-2.3.7/plupload.full.min.js` |
50
- | `f-image-update` | `/include/vue-advanced-cropper/index.umd.js` · `/include/vue-advanced-cropper/style.css` |
50
+ | `f-image-update` | V2: `/include/vue-advanced-cropper/index.umd.js` · `/include/vue-advanced-cropper/style.css`; V3: use the two `unpkg.com/vue-advanced-cropper` URLs in `component-3.0.js` |
51
51
  | `f-qrcode` | `/include/qrcode/qrcode.min.js` |
52
52
  | `f-qrcode-reader` | `/include/qrcode/qrcode-reader.min.js` |
53
53
  | `f-echart` | `/include/chart/echarts.min.js` · `/include/chart/fechart.js` (**in this order**) — Apache ECharts |
54
54
  | `f-sheet` | `/include/ag-grid/ag-grid-community.min.js` · `/include/ag-grid/fsheet.js` (**in this order**) — AG Grid |
55
55
  | `f-editor` | `/include/ckeditor5/build/ckeditor.js` |
56
56
  | `f-editor-dialog` | `/include/ckeditor5/build/ckeditor.js` |
57
+ | `f-audio`, `f-audio-recorder` | `/include/audio/vueaudio.js` |
58
+ | `f-q-and-a`, `f-etest-review` | MathJax, Q&A CSS, context menu, audio and QandA.js; `f-q-and-a` also needs CKEditor and Plupload. V3 `f-q-and-a` additionally loads Sortable and vuedraggable. Read the complete ordered array in the matching `component*.js`. |
57
59
 
58
60
  > **Bundled, NO import needed:** `v-*` (Vuetify) · `f-table` · `f-dialog` · `f-button` ·
59
61
  > `f-date` · `f-time` · `f-search` · `f-menu` · `f-radiobox` · `f-box` · `f-label` · `f-header` ·
60
- > `f-title` · `f-slider` · `f-excel-reader` — and any component not in the table above.
62
+ > `f-title` · `f-slider` · `f-excel-reader`. Other components may also declare `import` in `defaultControlAttr`; check the source before assuming none is needed.
61
63
 
62
64
  **Workflow when adding a component with imports** (see `fui import --help`):
63
65
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Owns: **table design rules: naming, PK/FK, audit columns, data types**. Connection/SP workflow: [db-workflow.md](db-workflow.md). Load when designing a new schema.
4
4
 
5
- > **DDL — structure changes OK, data loss not:** `ALTER TABLE ... ADD` (column/constraint) and `DROP CONSTRAINT` run directly via fui. `ALTER COLUMN` (type change) runs but **needs `--confirm-write`** — narrowing numeric/time precision silently loses data. `CREATE TABLE`, `DROP COLUMN`, `DROP TABLE` are **hard-blocked**; the developer runs them. Conventions below are documentation — present DDL to the user for approval before running.
5
+ > **DDL — structure changes OK, data loss not:** `ALTER TABLE ... ADD` (column/constraint), `DROP CONSTRAINT`, and `DROP TABLE #name` for one local temporary table run directly via fui. `ALTER COLUMN` (type change) runs but **needs `--confirm-write`** — narrowing numeric/time precision silently loses data. `CREATE TABLE`, `DROP COLUMN`, and dropping persistent or global (`##name`) tables are **hard-blocked**; the developer runs them. Conventions below are documentation — present DDL to the user for approval before running.
6
6
  >
7
7
  > DB connection, dbToken, schema, SP workflow: [db-workflow.md](db-workflow.md). Multi-table/multi-module app design: [system-design.md](system-design.md).
8
8
 
@@ -33,7 +33,6 @@ One `_db/` subfolder PER connection. The folder name is a short, readable `conne
33
33
 
34
34
  ```json
35
35
  {
36
- "version": 2,
37
36
  "default": "docs",
38
37
  "connections": {
39
38
  "docs": {
@@ -59,20 +58,19 @@ One `_db/` subfolder PER connection. The folder name is a short, readable `conne
59
58
 
60
59
  | Field | Req | Meaning |
61
60
  | ----------- | --- | ------------------------------------------------------------------ |
62
- | `version` | ✅ | Always `2` for this layout |
63
61
  | key | ✅ | `connectionName`, also the folder name (`docs`, `docs_lhbs`) |
64
62
  | `apiName` | ✅ | tAPI alias in `"API": "/{apiName}/Ten"`; may repeat across zones |
65
63
  | `apiDomain` | ✅ | Host only, NO alias, NO trailing `/` (`api.example3.vn`) |
66
64
  | `sqlServer` | ✅ | SQL Server host; with `database`, identifies the physical database |
67
65
  | `database` | ✅ | Database name; with `sqlServer`, prevents duplicate connections |
68
66
  | `dbToken` | ✅ | Base64 SQL Server connection string |
69
- | `userToken` | ❌ | Bearer to test `spAPI_*` after deploy |
67
+ | `userToken` | ❌ | Bearer for `acc` management lookup and end-user `spAPI_*` calls; never a SQL credential |
70
68
 
71
69
  Without `--sql-server`/`--database`, `fui db add` reads both from the connection string in the token.
72
70
 
73
71
  **Naming:** pass `--name <connectionName>` for a specific name. Omitted, fui suggests `apiName` first; if taken, adds the zone from `apiDomain` (`docs` → `docs_lhbs`); still taken, adds the database and a number. Names take letters, digits, `.`, `_`, `-` and are lowercased. The same server + database added again keeps its existing name.
74
72
 
75
- **v1 compatibility, no folder moves:** an old record without `apiName` reads as `apiName = connectionName = key`, and the file is rewritten as version 2 the first time fui reads it (old file kept in `_history/`). fui never renames old SQL folders. A new connection can take its own name, so an alias repeated across zones can sit next to the old record.
73
+ **Older files, no folder moves:** a record without `apiName` reads as `apiName = connectionName = key`. A legacy `"version": 2` marker is accepted. On first read, fui normalizes the file without a version field (old file kept in `_history/`). It never renames old SQL folders.
76
74
 
77
75
  > **CRITICAL — `apiDomain` has two meanings:** `_connections.json` → bare host (`api.example3.vn`); `project.json` `data.apiDomain` → full base URL with apiName + trailing `/` (`https://tapi.example.vn/acc/`). Top bug: bare host + apiName without `/` → `tapi.lhu.edu.vnts` instead of `tapi.lhu.edu.vn/ts`. See [tapi-reference.md](tapi-reference.md) §3. Don't build URLs; copy the `url (reference only, not called; copy verbatim)` line from `fui sp verify`/`fui sp help`.
78
76
 
@@ -107,11 +105,12 @@ Without `--sql-server`/`--database`, `fui db add` reads both from the connection
107
105
  Never extract UID/PWD by hand (PowerShell etc.) — exposes credentials.
108
106
 
109
107
  1. `fui db alias list` — no args: tAPI injects `@sys_UserID` from the Bearer token, so `SM_Modules_ManagerSelect` returns only DBs you're granted. Columns `APIName | SQLServer | DatabaseName | ModuleName | Enable`; `--search` filters client-side (APIName/DatabaseName/ModuleName, case-insensitive).
110
- 2. `fui db add-by-name <apiName>` — looks up SQLServer/DatabaseName on acc → borrows the UID/PWD pair of an existing connection → builds dbToken → tests `SELECT 1` before saving anything → saves + fetches schema to `_db/{connectionName}/`. Never prints dbToken/UID/PWD.
108
+ 2. `fui db add-by-name <apiName>` — looks up SQLServer/DatabaseName on acc → borrows the UID/PWD pair of an existing connection, or takes `--uid <login> --pwd -` / `--pwd-env VAR` → builds dbToken → tests `SELECT 1` before saving anything → saves + fetches schema to `_db/{connectionName}/`. Never prints dbToken/UID/PWD.
111
109
 
112
- - Several different logins in workspace → menu; pass `--borrow-from <projectId>/<connectionName>`. Same login across one group's connections is not ambiguous.
110
+ - Reuse the UID/PWD pair from a saved connection; target SQLServer and DatabaseName come from `acc` and may differ from the source. Prefer sources on the same apiDomain; if none exist, a unique login elsewhere in the workspace is tried against the target DB. Several different logins → menu; pass `--borrow-from <projectId>/<connectionName>`. Credentials are never printed.
113
111
  - "apiName not found" = alias doesn't exist OR you're not granted it. Don't assume a typo; run `fui db alias list`.
114
- - Project with no connections works: apiDomain from local `project.json`, userToken borrowed from another project on the same domain (auto only if exactly one candidate, else menu). Else pass `--api-domain` + `--user-token-env VAR`.
112
+ - A project with no connection can use apiDomain from local `project.json`. A project whose connection lacks userToken can use that connection's apiDomain. In either case fui looks for a token on the exact API host first, then another host in the same SSO zone (auto only if one distinct token; otherwise use `--user-token-from <project/connectionName>`). Else pass `--api-domain` + `--user-token-env VAR`.
113
+ - `userToken` authorizes only the `acc` lookup. It cannot become a `dbToken` or reveal a SQL password. If no saved dbToken contains a usable SQL login, supply the SQL login and password through `add-by-name`, or supply a dbToken through `fui db add`.
115
114
 
116
115
  > ⚠️ `Password` in `SM_Modules_ManagerSelect` is a tAPI internal hash, not the SQL password — unusable for dbToken. fui never prints it.
117
116
 
@@ -127,7 +126,7 @@ Never extract UID/PWD by hand (PowerShell etc.) — exposes credentials.
127
126
  5. Module uses other aliases? Repeat 4 per alias. Added connections do NOT become default (use --default).
128
127
  ```
129
128
 
130
- Already have a dbToken → skip 3–4, use `fui db add`.
129
+ Already have a dbToken → skip 3–4, use `fui db add`. Already connected → skip discovery and use `--db <project/connectionName>` for schema, SQL, SP and API design.
131
130
 
132
131
  ## 3. Building a dbToken
133
132
 
@@ -178,19 +177,19 @@ UID/PWD changed, Server/Database unchanged:
178
177
 
179
178
  ```
180
179
  dbToken → SQL Server connection to run schema/SP
181
- userToken → call tAPI REST API as an end-user (test spAPI_*)
180
+ userToken → discover managed aliases through acc, or call tAPI REST API as an end-user
182
181
  ```
183
182
 
184
- - Shareable across projects with the same `GroupName` in `_projectInfo.json` (`{ "GroupName": "GroupA" }`): `fui db user-token --db <connectionName> --from <project/connectionName>` — no re-login. `fui db user-token` with no source lists workspace tokens.
183
+ - Shareable across projects in the same SSO zone: `fui db user-token --db <connectionName> --from <project/connectionName>` copies a specific token without exposing its value. `fui db user-token` with no source lists workspace tokens. `GroupName` alone does not prove that two projects share an SSO zone.
185
184
  - Stored PER CONNECTION in `_connections.json`. Multi-DB → one token per connection; pass `--db <connectionName>` (omitted → default).
186
- - `fui right …` calls `acc` with the userToken of the connection matching the current apiName (else default); missing there → rights commands fail even if other connections have tokens.
185
+ - `fui right …` calls `acc` with the userToken of the connection matching the current apiName (else default); if absent, fui searches saved tokens in the same SSO zone.
187
186
  - Not set at `fui db add` → `fui db user-token --db <connectionName> --token -`.
188
187
 
189
188
  ## 6a. Domain SSO — same parent domain: token works, don't ask
190
189
 
191
190
  SSO is by parent domain: all APIs under e.g. `*.lhu.edu.vn` (`tapi.lhu.edu.vn`, `api.lhu.edu.vn`, `capnhatluong.lhu.edu.vn`…) accept the same `userToken`.
192
191
 
193
- - Have a token for that parent domain → use it for other projects/aliases there. Do NOT ask the user, do NOT test-call to "verify", do NOT make them log in.
192
+ - Have a token for that parent domain → use it for other projects/aliases there. Fui prefers an exact API host match, then a unique token from the zone. If several distinct tokens qualify, use `--user-token-from <project/connectionName>` for the `acc` lookup. Do not ask the user to log in again when the right token is already saved.
194
193
  - Scanning the workspace for tokens (`fui db user-token` without a token) is valid and recommended.
195
194
 
196
195
  | Case | Action |
@@ -248,7 +247,7 @@ Prints `📋 Archived orphaned local SP file(s)...` when archiving. Name match i
248
247
  - `--db` omitted → inferred from SP name (schema + local `.sql` per connection). One match → silent; zero or several (same name in two DBs) → connection menu, pick and re-run, don't guess.
249
248
  - `fui sp save` for a brand-new SP can't infer: multi-connection projects must pass `--db` on first save.
250
249
 
251
- > `fui sp verify` reads the real body, not the name; hard error if unreadable. Write-API testing with `fui module simulate`: [tools-registry.md](tools-registry.md). Guards on `DROP TABLE`/`DROP DATABASE`/`ALTER TABLE`, `xp_cmdshell`, `DELETE`/`UPDATE` without `WHERE` are enforced in code, no bypass. `sp deploy` also refuses a file that is not exactly one definition of the named object. Writes via `fui query`/`fui exec` need `--confirm-write`.
250
+ > `fui sp verify` reads the real body, not the name; hard error if unreadable. Write-API testing with `fui module simulate`: [tools-registry.md](tools-registry.md). Guards on persistent/global `DROP TABLE` (a single local `#name` temporary table is allowed), `DROP DATABASE`/`ALTER TABLE`, `xp_cmdshell`, `DELETE`/`UPDATE` without `WHERE` are enforced in code, no bypass. `sp deploy` also refuses a file that is not exactly one definition of the named object. Writes via `fui query`/`fui exec` need `--confirm-write`.
252
251
 
253
252
  **Rollback:** read `{workspace}/_history/{projectId}/_db/{connectionName}/{name}_{timestamp}.sql` → `fui sp save <name> -f <old file>` → `fui sp deploy <name>`.
254
253
 
@@ -285,6 +284,7 @@ EXEC XDATA23.Calendar2015.dbo.spIO_IN_DeleteMonHocInfo @MonHocID, @ErrorDKMH OUT
285
284
  | Alias only, no dbToken | `fui db alias list` → `fui db add-by-name` |
286
285
  | Which DBs am I granted | `fui db alias list` |
287
286
  | Another DB (new alias) | `fui db add-by-name` again |
287
+ | Remove one local connection | `fui db remove <project>/<connectionName>`; server alias and cached schema/SQL files remain |
288
288
  | Connections + default | `fui db list` |
289
289
  | UID/PWD changed | `fui db token-rebuild` (`--db` if several) |
290
290
  | Set userToken | `fui db user-token` (`--db` if several) |
@@ -26,19 +26,19 @@ Session for **one project** → `{workspaceRoot}/{projectId}`; general work →
26
26
  **Windows (PowerShell)** — most common:
27
27
 
28
28
  ```powershell
29
- powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd ''<THƯ-MỤC>''; claude --rc'"
29
+ powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'Remove-Item Env:\CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue; cd ''<THƯ-MỤC>''; claude --rc'"
30
30
  ```
31
31
 
32
- **macOS:**
32
+ **macOS** (by analogy, not yet verified on a Mac — see §4):
33
33
 
34
34
  ```bash
35
- osascript -e 'tell app "Terminal" to do script "cd \"<THƯ-MỤC>\" && claude --rc"'
35
+ osascript -e 'tell app "Terminal" to do script "unset CLAUDE_CODE_CHILD_SESSION; cd \"<THƯ-MỤC>\" && claude --rc"'
36
36
  ```
37
37
 
38
- **Linux** (swap `gnome-terminal` for the terminal in use):
38
+ **Linux** (swap `gnome-terminal` for the terminal in use; not yet verified):
39
39
 
40
40
  ```bash
41
- gnome-terminal -- bash -c 'cd "<THƯ-MỤC>" && claude --rc; exec bash'
41
+ gnome-terminal -- bash -c 'unset CLAUDE_CODE_CHILD_SESSION; cd "<THƯ-MỤC>" && claude --rc; exec bash'
42
42
  ```
43
43
 
44
44
  Team default is **`claude --rc`**. If the user gives other flags (`-c`, `-r`, `--resume <id>`…), substitute them.
@@ -46,7 +46,7 @@ Team default is **`claude --rc`**. If the user gives other flags (`-c`, `-r`, `-
46
46
  **With an opening prompt ("open a new session and do X") → the prompt goes BEFORE `--rc`, not after.** `--rc` consumes the token right after it as its own value (see the warning in §4), so placing it first opens an idle session instead of running the prompt. Correct order:
47
47
 
48
48
  ```powershell
49
- powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'cd ''<THƯ-MỤC>''; claude ''X'' --rc'"
49
+ powershell -Command "Start-Process powershell -ArgumentList '-NoExit', '-Command', 'Remove-Item Env:\CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue; cd ''<THƯ-MỤC>''; claude ''X'' --rc'"
50
50
  ```
51
51
 
52
52
  ---
@@ -80,6 +80,26 @@ Report **PID + folder**. No new process ⇒ say it failed; **never report succes
80
80
 
81
81
  **Windows: go through `powershell`.** `claude` is usually `claude.ps1` (not `.exe`), so `cmd` can't run it.
82
82
 
83
+ **🔴 Clear `CLAUDE_CODE_CHILD_SESSION` first, before `cd` — otherwise the new session can never be resumed.** The session running this command carries `CLAUDE_CODE_CHILD_SESSION=1`, and `Start-Process` / `osascript` / `bash -c` inherit the parent environment. The new `claude` sees the marker, treats itself as a child session and **turns transcript saving off**: it works normally but never appears in `claude --resume` (not even with Ctrl+A / all folders), however it is closed. A window the user opens by hand has no such variable, which is the only difference — not the folder, not how the window was closed. Verified on Windows (2026-10-17): the status line "Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker" disappears once `Remove-Item Env:\CLAUDE_CODE_CHILD_SESSION -ErrorAction SilentlyContinue;` runs first.
84
+
85
+ > In auto mode, the classifier **blocks this command the first time** ("Auto-Mode Bypass"). It goes through once the user has this allow rule in `~/.claude/settings.json` (verified — not a hard deny):
86
+ > ```json
87
+ > "permissions": {
88
+ > "allow": [
89
+ > "PowerShell(Start-Process powershell -ArgumentList '-NoExit', '-Command', 'Remove-Item Env:\\CLAUDE_CODE_CHILD_SESSION*)"
90
+ > ]
91
+ > }
92
+ > ```
93
+ > The rule is per machine (it does not ship with the skill). If it is missing and the command is blocked, tell the user to add it. **Do not route around the block, and do not drop the `Remove-Item` step to get past it** — that brings back the unresumable session.
94
+ >
95
+ > **Offer to add the rule yourself.** After the block, explain in one or two sentences why the rule is needed (so the new session can be resumed, not to bypass oversight), then ask the user whether you should add it. Only on an explicit yes:
96
+ > 1. Read `~/.claude/settings.json` first; create it only if it does not exist.
97
+ > 2. Append the rule to `permissions.allow`, keeping every existing entry (merge, never replace the array).
98
+ > 3. The rule must end with `*)` — a missing `)` fails settings validation ("Malformed Tool(content) rule").
99
+ > 4. Retry the open command once. If it is still blocked, stop and tell the user; do not try other variants.
100
+ >
101
+ > Never add the rule without the user's yes, and never add it pre-emptively before a block has actually happened.
102
+
83
103
  ---
84
104
 
85
105
  ## 5. The new session is an INDEPENDENT process
@@ -75,7 +75,7 @@ numeral(value).format("0,0.0"); // 1,234,567.8
75
75
  numeral(_.sumBy(items, key)).format("0,0"); // tổng cột trong bảng
76
76
  ```
77
77
 
78
- > `f-*` needing extra JS/CSS: [component-quickref.md](component-quickref.md) §Components needing JS/CSS imports.
78
+ > Before using any `f-*` control, check `defaultControlAttr[control].import` in the matching V2/V3 `component*.js` file. Its ordered URLs determine required JS/CSS; [component-quickref.md](component-quickref.md) is a quick reference.
79
79
 
80
80
  ## 1) scripts/fastproject.js — V3: fastproject-3.0.js
81
81
 
@@ -36,7 +36,7 @@ Before any module.json:
36
36
 
37
37
  1. Tables + PK + FK for **all** entities. Per [db-table-design.md](db-table-design.md): `tbl` + PascalCase, PK `[Entity]ID`, audit `CreateUser/CreateTime/UpdateUser/UpdateTime`.
38
38
  2. n-n → junction table (e.g. `tblUserRole`).
39
- 3. **`CREATE TABLE` is run by the developer** — fui has no guard for it, so do not run it yourself; present DDL for review. Existing tables, gated by data loss: `ALTER TABLE ... ADD` and `DROP CONSTRAINT` run directly; `ALTER COLUMN` needs `--confirm-write`; `DROP COLUMN`/`DROP TABLE`/`SWITCH` hard-blocked.
39
+ 3. **`CREATE TABLE` is run by the developer** — fui has no guard for it, so do not run it yourself; present DDL for review. Existing tables, gated by data loss: `ALTER TABLE ... ADD`, `DROP CONSTRAINT`, and `DROP TABLE #name` for a local temporary table run directly; `ALTER COLUMN` needs `--confirm-write`; `DROP COLUMN`/persistent or global `DROP TABLE`/`SWITCH` hard-blocked.
40
40
  4. Design lookup/catalog tables early (shared dropdowns).
41
41
 
42
42
  **Output:** tables + relations, user-confirmed.
@@ -65,10 +65,11 @@ https://api.domain.vn/app/FunctionName/value1/value2
65
65
  | `@sys_SystemRight` | int | Tiered right (1=low, 9=high) |
66
66
  | `@sys_FunctionRight` | varchar(2000) | Function rights as `[AD][RPT][MGR]` |
67
67
  | `@sys_SessionID` | varchar(50) | Auth session ID |
68
+ | `@sys_RemoteIP` | varchar(15) | `Request.ServerVariables("REMOTE_ADDR")`; the address seen by the server |
68
69
  | `@sys_RawData` | nvarchar(max) | Full client JSON body |
69
- | `@sys_header_{HeaderName}` | varchar(n) | **A family, not one fixed name** — reads any HTTP header; `NULL` if client didn't send it. Naming rule below |
70
+ | `@sys_header_{HeaderName}` | varchar(n) | Request header except `Authorization`; missing header becomes `NULL`. Naming rule below |
70
71
 
71
- > **CRITICAL — never put `@sys_` in module.json `IN`.** tAPI fills all `@sys_*` from the session; client values are **ignored or overwritten**.
72
+ > **CRITICAL — never put recognized `@sys_*` params in module.json `IN`.** tAPI assigns them from authenticated user data, the HTTP request, or the JSON body; a same-named client field is ignored.
72
73
  >
73
74
  > ```json
74
75
  > // ❌ SAI — không đưa sys_ vào IN
@@ -78,9 +79,21 @@ https://api.domain.vn/app/FunctionName/value1/value2
78
79
  > "IN": { "ModuleID": "sModuleID" }
79
80
  > ```
80
81
 
82
+ To read the remote address in an API procedure, declare the parameter with this exact name and type. tAPI reads `REMOTE_ADDR`; do not send it in `IN`. `varchar(15)` fits IPv4 addresses, so check the deployment's IPv6 and proxy behavior before using it for a wider address range:
83
+
84
+ ```sql
85
+ CREATE PROCEDURE spAPI_AuditVisit
86
+ @sys_UserID varchar(9),
87
+ @sys_RemoteIP varchar(15)
88
+ AS
89
+ BEGIN
90
+ SELECT @sys_UserID AS UserID, @sys_RemoteIP AS RemoteIP;
91
+ END
92
+ ```
93
+
81
94
  #### `@sys_header_*` — read HTTP headers: replace each `-` with `_`
82
95
 
83
- Works for **any** header. `-` is invalid in SQL Server param names, so the literal name won't compile:
96
+ Works for request headers **except `Authorization`**. The suffix after `@sys_header_` must be nonempty. tAPI lowercases the SQL parameter name, takes the suffix, replaces every `_` with `-`, then looks it up in `Request.Headers`. `-` is invalid in SQL Server param names, so the literal name won't compile:
84
97
 
85
98
  ```sql
86
99
  -- ❌ SAI — lỗi cú pháp ngay lúc CREATE PROCEDURE, không phải lỗi runtime
@@ -88,7 +101,7 @@ Works for **any** header. `-` is invalid in SQL Server param names, so the liter
88
101
  ```
89
102
 
90
103
  - **No quoting works**: `@[sys_header_User-Agent]` and `@"sys_header_User-Agent"` are both invalid. Renaming is the **only** way.
91
- - **Each `-` → one `_`.** 1-to-1: don't drop characters, change case, or merge segments.
104
+ - **Each `-` → one `_` in the SP declaration.** tAPI reverses every `_` in the suffix to `-`; a header whose actual name contains `_` cannot be represented exactly by this convention.
92
105
 
93
106
  | HTTP header | SP param |
94
107
  | ------------------ | ---------------------------------------- |
@@ -99,26 +112,28 @@ Works for **any** header. `-` is invalid in SQL Server param names, so the liter
99
112
  | `Cookie` | `@sys_header_Cookie` — no `-`, unchanged |
100
113
 
101
114
  - Reverse (reading an existing SP): drop `@sys_header_`, turn remaining `_` into `-` — `@sys_header_Accept_Language` → `Accept-Language`.
102
- - Matching is **case-insensitive** (prefix and name): `@sys_header_User_Agent`, `@sys_Header_User_Agent`, `@sys_header_USER_AGENT` are the same header (so `@sys_Header_RequestID` works). Still use standard casing (`User-Agent`, not `USER_AGENT`). Only `_` for `-` is mandatory.
115
+ - Matching is **case-insensitive** (prefix and header lookup): `@sys_header_User_Agent`, `@sys_Header_User_Agent`, `@sys_header_USER_AGENT` request the same header. Use `_` for each `-`; tAPI does not insert separators, so `@sys_Header_RequestID` requests `RequestID`, not `Request-ID`.
116
+ - `@sys_header_Authorization` is **not exposed**: the code skips assignment for that header. It does not explicitly set the SQL parameter to `NULL` in this branch; do not rely on its value.
117
+ - A missing header is assigned `DBNull.Value`; exceptions during header lookup are caught without a reported error, so diagnose a missing value against the actual incoming request.
103
118
 
104
119
  ```sql
105
120
  CREATE PROCEDURE spAPI_AuditLog_Insert
106
121
  @Action nvarchar(100),
107
122
  @sys_UserID varchar(9),
123
+ @sys_RemoteIP varchar(15),
108
124
  @sys_header_User_Agent nvarchar(500),
109
125
  @sys_header_X_Forwarded_For varchar(200)
110
126
  AS
111
127
  BEGIN
112
- -- ISNULL vì client không gửi header thì tham số là NULL, không phải chuỗi rỗng
113
- INSERT INTO tblAuditLog (Action, UserID, UserAgent, ClientIP, CreateTime)
114
- VALUES (@Action, @sys_UserID,
115
- ISNULL(@sys_header_User_Agent, N'(không có)'),
116
- ISNULL(@sys_header_X_Forwarded_For, ''),
117
- GETDATE());
128
+ SELECT @Action AS ActionName,
129
+ @sys_UserID AS UserID,
130
+ @sys_RemoteIP AS RemoteIP,
131
+ @sys_header_User_Agent AS UserAgent,
132
+ @sys_header_X_Forwarded_For AS ForwardedFor;
118
133
  END
119
134
  ```
120
135
 
121
- - **Still `@sys_*`:** auto-filled, **not** in `IN`, **no default** (`= null` is a hard error; `fui sp deploy` blocks it, see below).
136
+ - **Still recognized `@sys_*`:** auto-filled, **not** in `IN`, **no default** (`= null` is a hard error; `fui sp deploy` blocks it, see below).
122
137
  - **Missing header → `NULL`** (not `''`) — normal, e.g. script/server-to-server calls often lack `User-Agent`. Wrap in `ISNULL(...)` at use: comparisons give UNKNOWN (`WHERE @sys_header_User_Agent <> 'bot'` **drops** the row), and `+` concatenation gives `NULL` (blanks the whole log string).
123
138
  - **Untrusted (client-supplied, spoofable).** Log/stats only, never authorization; take rights from `@sys_UserID`/`@sys_SystemRight` (from the verified token).
124
139
 
@@ -53,7 +53,7 @@ Users name commands loosely, often in Vietnamese.
53
53
  | "let someone edit this project" / "cấp quyền admin project", "thêm người vào project" | `fui user search` → `fui user add` |
54
54
  | "who can edit this project" / "ai được sửa project này" | `fui user list` |
55
55
  | "update project.json", "edit the project menu" / "sửa menu project" | edit `{projectId}/project.json`, then `fui project publish-config` |
56
- | "connect the DB but I only know the alias" / "không có dbToken" | `fui db alias list` → `fui db add-by-name` |
56
+ | "connect the DB but I only know the alias" / "không có dbToken" | `fui db list` → `fui db alias list` → `fui db add-by-name`; if no saved SQL login, supply `--uid` and `--pwd -` / `--pwd-env` |
57
57
  | "sync / refresh the project" / "đồng bộ project", "lấy project.json" | `fui project sync` |
58
58
  | "get / pull / clone / refresh the module" / "lấy module về", "pull mới nhất" | `fui module get` |
59
59
  | "new module" / "tạo module mới" | `fui module new` |
@@ -79,7 +79,7 @@ Users name commands loosely, often in Vietnamese.
79
79
  | "DB password changed" / "đổi password DB", "rebuild dbToken" | `fui db token-rebuild` |
80
80
  | "which DBs does this project use" / "project này đang nối những DB nào" | `fui db list` |
81
81
  | "connect one more database" / "nối thêm một database nữa" | `fui db add` again with the new apiName |
82
- | **"sync the db", "refresh schema"** / **"đồng bộ db", "cập nhật schema", "làm mới schema"** | `fui schema pull`, **never `fui project sync`**. No connection yet → ask for the connection details; do not run something else instead |
82
+ | **"sync the db", "refresh schema"** / **"đồng bộ db", "cập nhật schema", "làm mới schema"** | `fui db list` → existing `--db`: `fui schema pull`. No connection: `fui db alias list` → `fui db add-by-name` (automatically fetches schema) |
83
83
  | "list designs" / "có style nào" · "use style X" / "dùng style X" | `fui design list` · `fui design read <brand>` |
84
84
 
85
85
  ## Commands
@@ -152,6 +152,7 @@ Scope, for every component command:
152
152
  | `db list`, `db test`, `token decode` | R | ✅ | never print secrets |
153
153
  | `db alias list` | R | ✅ | databases your token may manage on `acc`; run it when an alias is unknown or `db add-by-name` finds nothing |
154
154
  | `db add`, `db add-by-name` | M | ✅ | test, save, fetch the schema. `add-by-name` borrows a SQL login from a connection on the same `apiDomain`; "not found" means the alias does not exist **or** you may not manage it |
155
+ | `db remove <project>/<connectionName>` | M | ⚠️ | removes only the named local connection and updates the default; server alias and cached `_db/<connectionName>/` files remain |
155
156
  | `db user-token`, `db default`, `db migrate` | M | ✅ | local only |
156
157
  | `db token-rebuild` | M | ⚠️ | overwrites the stored token; `--db` is required when the project has several connections |
157
158
  | `db alias new` | M | ⚠️ | registers an existing database as a tAPI alias on `acc`; the login must be the **application** account (execute-only), never a `db_owner` dev login. Ask the user first |
@@ -199,7 +200,7 @@ Guarding with a right code that is not defined fails silently: `rightTest()` is
199
200
  ## Database rules (enforced in code)
200
201
 
201
202
  - `query` / `exec`: say what the SQL does and what data it touches, and wait for the user. Writes (INSERT, UPDATE…SET, DELETE, MERGE, TRUNCATE, `SELECT … INTO` a real table, a write inside `EXEC('…')` or `sp_executesql`, `DROP PROCEDURE`/`FUNCTION`/`VIEW`, ALTER COLUMN) exit 3 until `--confirm-write`, added only after the user approved that exact write. It is a confirmation gate, not an obstacle to work around.
202
- - Table DDL is judged by data loss, **per statement**: free `ALTER TABLE … ADD` and `DROP CONSTRAINT` · `--confirm-write` for `ALTER COLUMN` (narrowing numeric or date precision loses data silently) · always blocked `DROP TABLE`, `DROP DATABASE`, `DROP COLUMN`, `SWITCH`, `xp_cmdshell` and any action fui cannot parse. A blocked statement wins over a confirmable one. `DELETE`/`UPDATE` without `WHERE` is blocked too, in `query` and `exec` (a `MERGE` is scoped by its `ON`). A `--` or a quote inside a `[name]` or `"name"` does not hide the statements after it. Show blocked SQL for the user to run by hand; no flag unlocks it.
203
+ - Table DDL is judged by data loss, **per statement**: free `ALTER TABLE … ADD`, `DROP CONSTRAINT`, and `DROP TABLE #name` for one local temporary table · `--confirm-write` for `ALTER COLUMN` (narrowing numeric or date precision loses data silently) · always blocked `DROP TABLE` on persistent or global (`##name`) tables, mixed drop lists, `DROP DATABASE`, `DROP COLUMN`, `SWITCH`, `xp_cmdshell` and any action fui cannot parse. A blocked statement wins over a confirmable one. `DELETE`/`UPDATE` without `WHERE` is blocked too, in `query` and `exec` (a `MERGE` is scoped by its `ON`). A `--` or a quote inside a `[name]` or `"name"` does not hide the statements after it. Show blocked SQL for the user to run by hand; no flag unlocks it.
203
204
  - Never call a live API SP or run sub-queries "to test" it: read its body (`fui sp get`) and reason, or `fui sp verify <name|Fn>`. `sp verify` calls no endpoint; its only network reads are metadata (`OBJECT_DEFINITION` when no local `.sql`, permissions with `--check-grant`). An unreadable body is a hard error: it never guesses from the name.
204
205
  - `--mocks` for an SP that **exists** must be the snippet `fui sp verify` prints, never a guessed shape. On `🚨 UNVERIFIED MOCK SHAPE` stop, run `fui sp verify`, copy the snippet. Invented data is right only for an SP that does not exist yet.
205
206
  - `fui sp deploy` asks the server whether the object exists, so `CREATE PROCEDURE` for an existing SP is fine: do not edit the file to `ALTER` and redeploy. tAPI parameters with a default value are hard-blocked.
@@ -119,7 +119,8 @@ const LAYOUT = {
119
119
  lineHeight: 1.4, // hệ số quy cỡ chữ → chiều cao dòng thực tế
120
120
  legendRowHeight: 24, // một hàng legend (chữ + đệm của item); config.legendRows nếu nhiều hàng
121
121
  axisLabelBand: 26, // nhãn trục hoành vẽ NGOÀI grid, phải chừa thêm ở dưới
122
- axisTopBand: 10 // nửa chữ của nhãn trục tung cao nhất nhô lên trên mép grid
122
+ axisTopBand: 10, // nửa chữ của nhãn trục tung cao nhất nhô lên trên mép grid
123
+ seriesLabelBand: 18 // label của bar/line có thể nhô lên trên grid (font 12px × line-height 1.4)
123
124
  };
124
125
 
125
126
 
@@ -656,10 +657,34 @@ Vue.component('f-echart', {
656
657
  return (hasText ? line(fs) : 0) + (hasSub ? gap + line(sfs) : 0);
657
658
  },
658
659
 
660
+ // Đổi inset dạng px / '%' về px để có thể kiểm tra khoảng hở tối thiểu.
661
+ // Các từ khoá ngữ nghĩa như 'middle' không thể quy đổi an toàn nên trả null.
662
+ insetPx(value) {
663
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
664
+ if (typeof value === 'string') {
665
+ const s = value.trim();
666
+ if (/^-?\d+(\.\d+)?px$/.test(s)) return parseFloat(s);
667
+ if (/^-?\d+(\.\d+)?%$/.test(s)) {
668
+ const rawHeight = (this.$el && this.$el.clientHeight) || this.chartHeight || 0;
669
+ const h = typeof rawHeight === 'number' ? rawHeight : parseFloat(rawHeight);
670
+ return h ? h * parseFloat(s) / 100 : null;
671
+ }
672
+ }
673
+ return null;
674
+ },
675
+
676
+ // Đặt block không sớm hơn con trỏ hiện tại. Nhờ vậy top: 0 / '2%' của
677
+ // module không thể kéo legend hoặc vùng vẽ đè lên block đứng trước nó.
678
+ enforceInset(block, edge, minimum) {
679
+ const px = this.insetPx(block[edge]);
680
+ if (block[edge] == null || (px != null && px < minimum)) block[edge] = minimum;
681
+ return px == null ? minimum : Math.max(px, minimum);
682
+ },
683
+
659
684
  // Xếp chỗ theo chiều dọc: đi từ mép trên xuống và từ mép dưới lên, mỗi khối
660
685
  // hiện diện lấy đúng chiều cao của mình + một `gap`; phần chưa ai lấy là vùng vẽ.
661
- // Giá trị do module tự khai luôn được tôn trọng — engine chỉ điền chỗ còn trống,
662
- // và vẫn cộng dồn theo giá trị đó để khối sau không đè lên khối module đã đặt.
686
+ // Giá trị do module tự khai được giữ khi đủ rộng; nếu quá nhỏ thì được nâng lên
687
+ // mức tối thiểu để không block nào phá gap 12px với block đứng trước.
663
688
  applyLayout(opt, hints) {
664
689
  const P = LAYOUT.padding, G = LAYOUT.gap;
665
690
  const title = Array.isArray(opt.title) ? opt.title[0] : opt.title;
@@ -668,9 +693,14 @@ Vue.component('f-echart', {
668
693
 
669
694
  const titleH = this.measureTitle(title);
670
695
  if (titleH) {
671
- if (title.top == null) title.top = top;
672
- // top dạng '%' / 'middle' / 'bottom': không quy ra px được → chỉ giữ mức tối thiểu
673
- top = typeof title.top === 'number' ? title.top + titleH + G : Math.max(top, P + titleH + G);
696
+ const titleAnchor = title.bottom != null && title.top == null ? 'bottom' : 'top';
697
+ if (titleAnchor === 'bottom') {
698
+ const y = this.enforceInset(title, 'bottom', bottom);
699
+ bottom = y + titleH + G;
700
+ } else {
701
+ const y = this.enforceInset(title, 'top', top);
702
+ top = y + titleH + G;
703
+ }
674
704
  }
675
705
 
676
706
  if (legend && legend.show !== false) {
@@ -679,11 +709,11 @@ Vue.component('f-echart', {
679
709
  : legend.bottom != null ? 'bottom'
680
710
  : (hints.legendAnchor || 'top');
681
711
  if (anchor === 'top') {
682
- if (legend.top == null) legend.top = top;
683
- top = typeof legend.top === 'number' ? legend.top + legendH + G : Math.max(top, P + legendH + G);
712
+ const y = this.enforceInset(legend, 'top', top);
713
+ top = y + legendH + G;
684
714
  } else {
685
- if (legend.bottom == null) legend.bottom = bottom;
686
- bottom = typeof legend.bottom === 'number' ? legend.bottom + legendH + G : Math.max(bottom, P + legendH + G);
715
+ const y = this.enforceInset(legend, 'bottom', bottom);
716
+ bottom = y + legendH + G;
687
717
  }
688
718
  }
689
719
 
@@ -700,8 +730,22 @@ Vue.component('f-echart', {
700
730
  if (Array.isArray(opt.grid)) return; // multi-grid: module tự lo bố cục
701
731
  const dg = _.isPlainObject(declared.grid) ? declared.grid : {};
702
732
  const g = _.isPlainObject(opt.grid) ? opt.grid : (opt.grid = {});
703
- if (dg.top == null) g.top = Math.max(LAYOUT.gridTop, top + LAYOUT.axisTopBand);
704
- if (dg.bottom == null) g.bottom = Math.max(LAYOUT.gridBottom, bottom + LAYOUT.axisLabelBand);
733
+ const series = Array.isArray(opt.series) ? opt.series : (opt.series ? [opt.series] : []);
734
+ const hasOuterTopLabel = series.some(s => {
735
+ if (!s || !s.label || s.label.show !== true) return false;
736
+ const pos = s.label.position;
737
+ return pos == null || pos === 'top' || pos === 'outside';
738
+ });
739
+ const topBand = Math.max(LAYOUT.axisTopBand, hasOuterTopLabel ? LAYOUT.seriesLabelBand : 0);
740
+ const minTop = Math.max(LAYOUT.gridTop, top + topBand);
741
+ const minBottom = Math.max(LAYOUT.gridBottom, bottom + LAYOUT.axisLabelBand);
742
+
743
+ // Custom grid vẫn được giữ nếu rộng hơn; nếu quá sát thì nâng lên mức tối thiểu.
744
+ // Đây là điểm trước kia làm chart có grid.top nhỏ phá vỡ gap 12px.
745
+ const declaredTop = this.insetPx(dg.top);
746
+ const declaredBottom = this.insetPx(dg.bottom);
747
+ g.top = declaredTop == null ? minTop : Math.max(declaredTop, minTop);
748
+ g.bottom = declaredBottom == null ? minBottom : Math.max(declaredBottom, minBottom);
705
749
  if (dg.left == null) g.left = LAYOUT.gridLeft;
706
750
  if (dg.right == null) g.right = LAYOUT.gridRight;
707
751
  return;