Hexa's Blog

Nhật ký tích hợp Nexus Dashboard (Web Template) vào Phoenix 1.8

23/04/2025 @ Saigon Elixir

23/4/2025

Trước tiên, khi nhìn vào mix.exs, phần alias/0:

defp aliases do
  [
    ...
    "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
    "assets.build": ["tailwind mining_rig_monitor", "esbuild mining_rig_monitor"],
    "assets.deploy": [
      "tailwind mining_rig_monitor --minify",
      "esbuild mining_rig_monitor --minify",
      "phx.digest"
    ]
  ]
end

config.exs:

 # Configure esbuild (the version is required)
config :esbuild,
  version: "0.17.11",
  mining_rig_monitor: [
    args:
      ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
    cd: Path.expand("../assets", __DIR__),
    env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
  ]

 # Configure tailwind (the version is required)
config :tailwind,
  version: "3.4.3",
  mining_rig_monitor: [
    args: ~w(
      --config=tailwind.config.js
      --input=css/app.css
      --output=../priv/static/assets/app.css
    ),
    cd: Path.expand("../assets", __DIR__)
  ]

Chúng ta có thể hiểu như sau:

  • Những cái liên quan đến css, app.css, tailwind sẽ nhận trách nhiệm.
  • Những cái liên quan đến js, app.js, esbuild sẽ nhận trách nhiệm.

Tiếp theo là về việc làm sao kéo css của Nexus Dashboard vào Phoenix. Có 2 cách:

  • Cách dễ nhất: Lấy file app.css trong thư mục html của Nexus Dashboard và copy vào Phoenix
  • Cách tốn công nhất: tái hiện lại build-step, tôn trọng tailwind, lợi thế là sẽ optimized dung lượng file bao gồm css, icon.

Tôi chọn cách đầu tiên để thử nghiệm. Page login đã hiện ra, khá đẹp.

[1] static login page
[1] static login page

24/4/2025

Hôm qua, tôi đã làm xong cái static page login. Bây giờ, tôi cần nó chạy được:

  • chuyển trang khi login thành công
  • báo lỗi khi đăng nhập thất bại.

Để làm cái này, tôi quyết định không dùng .form‌/1, .input/2 trong core_component.ex. Khi dùng những cái này, khả năng tùy biến rất khó. Chơi thuần túy luôn, học cho được nhiều. form(assigns)

<.form for={@form} action={~p"/users/log_in"} phx-update="ignore">
 <fieldset class="fieldset">
   <legend class="fieldset-legend">Email Address</legend>
   <label class="input w-full focus:outline-0">
     <span class="iconify lucide--mail text-base-content/80 size-5"></span>
     <input class="grow focus:outline-0" placeholder="Email Address" type="email" name={@form[:email].name}  id={@form[:email].id} value={@form[:email].value} />
   </label>
 </fieldset>

 <fieldset class="fieldset">
   <legend class="fieldset-legend">Password</legend>
   <label class="input w-full focus:outline-0">
     <span class="iconify lucide--key-round text-base-content/80 size-5"></span>
     <input class="grow focus:outline-0" placeholder="Password" type="password" name={@form[:password].name} id={@form[:password].id} />
     <label class="swap btn btn-xs btn-ghost btn-circle text-base-content/60">
       <input type="checkbox" aria-label="Show password" data-password="password" />
       <span class="iconify lucide--eye swap-off size-4"></span>
       <span class="iconify lucide--eye-off swap-on size-4"></span>
     </label>
   </label>
 </fieldset>

 <div class="mt-4 flex items-center gap-3 md:mt-6">
   <input class="checkbox checkbox-sm checkbox-primary" aria-label="Checkbox example" type="checkbox" name={@form[:remember_me].name} id={@form[:remember_me].id} />
     <label for="agreement" class="text-sm">
       Remember me
     </label>
   </div>

 <button type="submit" class="btn btn-primary btn-wide mt-4 max-w-full gap-3 md:mt-6">
   <span class="iconify lucide--log-in size-4"></span>
   Login
 </button>
</.form>

Tiếp theo, tôi cần báo lỗi khi đăng nhập thất bại.

Nếu mà lượng icon dùng y hệt Nexus Dashboard thì sẽ chả có gì để nói. Tuy nhiên, nếu mà dùng icon, class css mà trong app.css không có thì ngập hành. Đừng quên nhé, tôi copy nguyên cái app.css của Nexus vào Phoenix.

Cái thời mà tôi dùng fontawesome, sau đó cứ điềm nhiên gán font trong span đã hết rồi sao. Phiền quá trời.

Nhân tiện, Nexus Dashboard dùng icon chủ yếu là từ https://lucide.dev/.

Để khắc phục, một giải pháp khác, đó là sử dụng thuần <svg> tag:

<!-- https://lucide.dev/icons/a-arrow-down -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
     stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
     class="lucide lucide-aarrow-down-icon lucide-a-arrow-down">
    <path d="M3.5 13h6"/>
    <path d="m2 16 4.5-9 4.5 9"/>
    <path d="M18 7v9"/>
    <path d="m14 12 4 4 4-4"/>
</svg>

Còn không, cách bài bản hơn đó là:

  • load javascript của lucide về
  • tạo script nhận diện class-name , nếu class-name bắt đầu bằng lucide thì sẽ trả về svg tương ứng.
  • thiết lập để script này tương thích với tailwind. Tailwind khi chạy, sẽ tạo ra các class tương ứng.

Tôi sẽ thử nghiệm với phương án copy trực tiếp <svg>.

Tôi sẽ so sánh css class mà lấy từ Nexus dashboard và svg lấy từ Lucide

Nexus Dashboard

<span class="iconify lucide--info size-5"></span>
.lucide--info {
  --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24'
             height='24'%3E%3Cg fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round'
              stroke-width='2'%3E%3Ccircle cx='12' cy='12' r='10'/%3E%3Cpath d='M12 16v-4m0-4h.01'/%3E%3C/g%3E%3C/svg%3E");
}

.size-5 {
    width: calc(var(--spacing)* 5);
    height: calc(var(--spacing)* 5);
}

.iconify {
    width: 1em;
    height: 1em;
    -webkit-mask-image: var(--svg);
    mask-image: var(--svg);
    background-color: currentColor;
    display: inline-block;
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
}

SVG thay thế Tôi dùng trực tiếp luôn, không cần bọc với <span>. Kết quả vẫn tốt.

<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
     stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info-icon lucide-info">
  <circle cx="12" cy="12" r="10"/>
  <path d="M12 16v-4"/>
  <path d="M12 8h.01"/>
</svg>
[2] Lucide svg config.
[2] Lucide svg config.

Nhật ký migrate Phoenix Web Framework 1.7.14 lên 1.8

22/04/2025 Elixir

Bài viết này liệt kê lại quá trình cũng như dòng suy nghĩ của tôi khi migrate phoenix 1.7 lên 1.8.

Thay đổi mix.exs phần dependencíe deps/0:

  • {:phoenix, "~> 1.7.14"} -> {:phoenix, "1.8.0-rc.1", override: true}
  • {:phoenix_live_view, "~> 1.0.0", override: true} -> {:phoenix_live_view, "~> 1.0.9"}

Thay đổi mining_rig_monitor_web.ex , phần live_view/0:

  • use Phoenix.LiveView, layout: {MiningRigMonitorWeb.Layouts, :app} -> use Phoenix.LiveView

Sau cùng, nó sẽ trông như thế này.

  # Phiên bản cũ
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MiningRigMonitorWeb.Layouts, :app}

      unquote(html_helpers())
    end
  end
  # Phiên bản mới
  def live_view do
    quote do
      use Phoenix.LiveView

      unquote(html_helpers())
    end
  end

Tôi đã nghĩ rằng sẽ phải có lỗi xảy ra ở lần chạy iex -S mix phx.server đầu tiên, tôi sẽ cần phải thay đổi các live_view liên quan, nhưng mà không, khả năng tương thích ngược là khá tốt.


Tôi trích dẫn một cái quan trọng liên quan đến root.html.heexapp.html.heex (version 1.7) , app/1 (version 1.8).

defmodule DevAppWeb.Layouts do
  @moduledoc """
  This module holds different layouts used by your application.

  See the `layouts` directory for all templates available.
  The "root" layout is a skeleton rendered as part of the
  application router. The "app" layout is rendered as component
  in regular views and live views.
  """
end

cái app/1 này phải được gọi thì nó mới render, không có chuyện chạy mặc định. Có thể ở 1.7 có file app.html.heex, tuy nhiên, nó ko liên quan đến func app/1

Tôi cần kiểm tra chéo 1 chút, thực sự là app.html.heex có được chạy mặc định hay không, sau khi tôi đã bỏ option layout: {MiningRigMonitorWeb.Layouts, :app}

Để test, tôi đã thêm 1 cái tag <h1> cho file app.html.heex để đánh dấu.

 <!-- app.html.heex -->
<main class="bg-gray-50 dark:bg-gray-900">
  <.flash_group flash={@flash} />
  <%= @inner_content %>
  <h1> APP.HTML.HEEX</h1>
</main>

Đệt, nó ko chạy qua app.html.heex sau khi bỏ option layout: {MiningRigMonitorWeb.Layouts, :app} trong _web.ex, live_view/0 nhé.

Điều này nghĩa là tôi sẽ phải:

  • Kéo nội dung của app.html.heex vào module Layouts, function app
  • Thêm alias MiningRigMonitorWeb.Layouts trong mining_rig_monitor_web.ex, function html_heler/0
  • Các liveview module, ví dụ module MiningRigMonitorWeb.AsicMinerLive.Index, sau khi tôi tách function render/1 thành file index.html.heex. nếu tôi mà muốn sử dụng liveview layout có tên là app, tôi sẽ cần phải bọc nó lại với tag <Layout.app> <‌/Layout.app>

Ví dụ file mining_rig_monitor/lib/mining_rig_monitor_web/live/asic_miner_live/index.html.heex

<Layouts.app flash={@flash} >
  <._index_top />

  <._index_overall_figures aggregated_coin_hashrate_map={@aggregated_coin_hashrate_map}
    aggregated_total_power={@aggregated_total_power}
    aggregated_total_power_uom={@aggregated_total_power_uom}
    aggregated_asic_miner_alive={@aggregated_asic_miner_alive} />

  <._index_activated_asic_miner_table streams={@streams} />

  <._index_not_activated_asic_miner_table streams={@streams} />

</Layouts.app>

Tiếp theo là về cái vụ Scope. Tôi tính toán là sẽ mix phx.gen.auth ở một dự án test khác, sau đó sẽ copy quá. Tuy nhiên, bị vướng field :authenticated_at, :utc_datetime trong Accounts.UserToken, ở phiên bản 1.7, không có field authenticated_at.

Tôi có xem kỹ hơn cái field này, có vẻ là nó liên quan đến sudo mode. Thế tính năng sudo mode là gì?

Tính năng cực kỳ thích hợp cho những tác vụ nhạy cảm. Nó sẽ yêu cầu người dùng phải đăng nhập lại trước khi đưa ra hành động nào đó.

Tôi không có nhu cầu dùng sudo mode. Cụ thể là trong dự án Mining Rig Monitor. Thực tế, app này chỉ có 1 user role là admin. Chả có nhu cầu đụng đến Scope luôn, thôi dẹp cho khỏe.

Hahaha

Các câu hỏi tôi gặp phải khi làm việc với Phoenix Web Framework

22/04/2025 @ Saigon Elixir

Bài post này tổng hợp các câu hỏi tôi gặp phải khi làm việc với Phoenix Web Framework, có những câu hỏi tôi không có câu trả lời, nhưng tôi sẽ vẫn ghi lại, khi nào rảnh tôi sẽ quay lại nghiên cứu.

Trên con đường tập trung vào tính năng thay vì sự hoàn hảo, luôn luôn có những lúc tôi hoàn toàn bỏ qua vẫn đề kỹ thuật mà tập trung vào nghiệp vụ nhằm khai thác tối đa feedback loop. Bài post này sẽ giúp tôi quay lại, xử lý những vấn đề ngu ngốc mà tôi chủ động tạo ra trong quá trình phát triển phần mềm.

001. Tại sao ở phiên bản Phoenix Web Framework cũ, phần layout chỉ cần quan tâm đến root.html.heex, nhưng bây giờ nó lại có thêm cả app.html.heex Phiên bản 1.7. Tuy nhiên ở phiên bản 1.8, nó lại bỏ đi, nhồi vào file layout.ex.

Ở thời điểm hiện tại, 21/4/2025, tôi đang có nhu cầu migrate phoenix từ 1.7 sang 1.8 Official changelog. Rồi lại còn đổi từ dashboard template Flowbite Dashboard qua Nexus Dashboard của DaisyUI. Cái này mất não thật sự. Vấn đề lớn nhất của FlowBite là giá tiền 299 USD, tiếp theo là class name. Tôi không phải là fan của việc nhìn 1 cái div có hơn 10 cái class, tôi mua Nexus Dashboard với giá 69 USD với hi vọng giải quyết vấn đề này.

  • root.html.heex, cái này mục đích là để render những cái html tĩnh mà thôi.
  • app.html.heex, cái này sử dụng kèm và đi xuyên suốt vòng đời của LiveView.

Đây là file _web.ex , phiên bản phoenix 1.7, phần live_view.

  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MiningRigMonitorWeb.Layouts, :app}

      unquote(html_helpers())
    end
  end

Khi sử dụng live_view ở ver 1.7, layout :app đã được gài cứng, điều này gây khó khăn khi webapp có nhiều liveview layout. Ví dụ nhé:

  • bạn dev phoenix web app, hoàn toàn chơi live_view, người dùng đã đăng nhập sẽ có liveview layout khác với người dùng chưa đăng nhập.
  • sau khi đăng nhập, user role-admin sẽ có giao diện khác với role-thường dân.

Trong file routes.ex, nếu mà login thì tôi sẽ dùng là root-no-nav.html.heex, ngược lại, nếu đăng nhập thành công, tôi sẽ dùng root.html.heex.

Đừng ngủ nhé, đây là file _web.ex, phiên bản phoenix 1.8, phần live_view.

  def live_view do
    quote do
      use Phoenix.LiveView

      unquote(html_helpers())
    end
  end

Bạn nhìn phần layout nhé, macro không hề chỉ định :app là layout mặc định của live_view. Điều này nghĩa là ở trong các live_view, cụ thể là render, phải tường minh ghi rõ là live_view đang muốn dùng layout nào, ví dụ như:

  • app_admin
  • app_login

002. Khi làm việc với phoenix, tôi thấy có từ khóa @inner_content@inner_block, chúng được sử dụng như thế nào.

  • @inner_content tìm thấy trong root.html.‌heex
  • @inner_block tìm thấy trong layout.ex, function app/1

… Vẫn còn tiếp

003. Khi khai thác conn.assigns hay socket.assigns, kỹ thuật nào có thể giúp sử dụng @tên_var thay cho Map.get(@conn.assigns, :tên_var)

Tôi chưa có câu trả lời cho câu hỏi này, tuy nhiên khi nào có thời gian tôi sẽ xem các bài sau:

004. Khi viết controller test, tôi hay thấy test không được viết trực tiếp mà được gói lại trong describe, lợi thế của nó là gì?

Lợi thế của nó là khi nhóm test này cùng cần cách setup giống nhau. Ví dụ rõ nhất là khi test update entity nào đó. Từ entity này tôi lấy từ Java Spring.

Dịch qua tiếng việt là thực thể, nhưng bạn cứ hiểu là record trong database. Khi ta muốn làm test liên quan đến update record trong database, chúng ta cần có record đó được tạo từ trước.

Lúc này có 2 test chúng ta quan tâm:

  • test update với các tham số hợp lệ
  • test update với tham số không hợp lệ

Cả 2 test này sẽ cùng cần được tạo trước record CpuGpuMinerLog. Dưới dây là ví dụ test cho Controller của CpuGpuLog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
defmodule MiningRigMonitorWeb.CpuGpuMinerLogControllerTest do
  use MiningRigMonitorWeb.ConnCase
  import MiningRigMonitor.CpuGpuMinerLogsFixtures
  alias MiningRigMonitor.CpuGpuMinerLogs.CpuGpuMinerLog

  @update_attrs %{}
  @invalid_attrs %{}

  setup %{conn: conn} do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  describe "update cpu_gpu_miner_log" do
    setup [:create_cpu_gpu_miner_log]

    test "renders cpu_gpu_miner_log when data is valid", %{conn: conn, cpu_gpu_miner_log: %CpuGpuMinerLog{id: id} = cpu_gpu_miner_log} do
      conn = put(conn, ~p"/api/cpu_gpu_miner_logs/#{cpu_gpu_miner_log}", cpu_gpu_miner_log: @update_attrs)
      assert %{"id" => ^id} = json_response(conn, 200)["data"]

      conn = get(conn, ~p"/api/cpu_gpu_miner_logs/#{id}")

      assert %{
               "id" => ^id
             } = json_response(conn, 200)["data"]
    end

    test "renders errors when data is invalid", %{conn: conn, cpu_gpu_miner_log: cpu_gpu_miner_log} do
      conn = put(conn, ~p"/api/cpu_gpu_miner_logs/#{cpu_gpu_miner_log}", cpu_gpu_miner_log: @invalid_attrs)
      assert json_response(conn, 422)["errors"] != %{}
    end
  end

  defp create_cpu_gpu_miner_log(_) do
    cpu_gpu_miner_log = cpu_gpu_miner_log_fixture()
    %{cpu_gpu_miner_log: cpu_gpu_miner_log}
  end
end

Sau khi create_cpu_gpu_miner_log/1 được kích hoạt, nó trả 1 lại cái map %{}. Cái map này sẽ được nhồi tiếp vào test "xxx", %{map} do end. Hãy chú ý dòng số 14, 1627.

005. Ở trong form, tôi hay thấy :let={f}, cái này là gì thế, kỹ thuật alias từ @form thành f với let là như thế nào?

006. Luồng chạy khi tạo ``form với core_component.ex` generate mặc định là như thế nào?

007. @from[field] hoạt động như thế nào?

008. phx-update="ignore" dùng để làm gì, thấy nó sử dụng ở login form.

009. Khi chạy function trong html.heex thì cần dấu . trước tên funciton, tuy nhiên tại sao điều này lại không cần khi chạy function với module.

Cụ thể ở đây là Layout.app/1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Layouts.app flash={@flash}>
  <div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-2 xl:gap-4 dark:bg-gray-900">
    <div class="mb-4 col-span-full xl:mb-2">
      <._breadcrumb />
    </div>
    <!-- Right Content -->
    <div class="col-span-1">
      <._email_information email_form={ @email_form } email_form_current_password={@email_form_current_password} />
    </div>

    <div class="col-span-1">
      <._password_information password_form={ @password_form} trigger_submit={ @trigger_submit }
        current_email={@current_email} current_password={@current_password}/>
    </div>
  </div>
</Layouts.app>

010. Tại sao UserLoginLive.mount/3 (mix phx.gen.auth) , function này return {} tuple 3 phần tử. Có cả temporary_assigns.

defmodule MiningRigMonitorWeb.UserLoginLive do
  use MiningRigMonitorWeb, :live_view

  def mount(_params, _session, socket) do
    email = Phoenix.Flash.get(socket.assigns.flash, :email)
    form = to_form(%{"email" => email}, as: "user")
    {:ok, assign(socket, form: form), temporary_assigns: [form: form]}
  end
end

011. Để tiện trong việc migrate lên version Phoenix mới, tôi không muốn dụng vào core_component.ex, tôi tạo ra file mới nexus_component.ex. Trong _web.ex, sẽ xuất hiện tình trạng import 2 module , và cả 2 module đó có function tên giống nhau. Bên cạnh việc tôi có thể đổi tên function, ngoài ra, tôi có thể gài quyền ưu tiên như thế nào.

chú ý dòng 67.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  defp html_helpers do
    quote do
      # HTML escaping functionality
      import Phoenix.HTML
      # Core UI components and translation
      import MiningRigMonitorWeb.CoreComponents
      import MiningRigMonitorWeb.NexusComponents
      import MiningRigMonitorWeb.Gettext

      # Shortcut for generating JS commands
      alias Phoenix.LiveView.JS
      alias MiningRigMonitorWeb.Layouts

      # Routes generation with the ~p sigil
      unquote(verified_routes())
    end
  end

012. Nguyên lý thay đổi theme light/dark là gì

Cách tách dấu thanh và chữ cái có dấu trong tiếng Việt thành chữ Latin với Elixir

18/04/2025 @ Saigon Elixir

Xin chào, ở bài viết này, trước tiên tôi muốn nói đến vấn đề của mình, Khi phát triển phần mềm Mining Rig Monitor, tôi muốn sử dụng các tên tiếng Việt để đặt tên cho dàn đào. Ví dụ:

  • Thanh Long
  • Bạch Hổ
  • Huyền Vũ
  • Chu Tước

Khi gài vào phần mềm khai thác tiền mã hóa, tôi muốn những cái tên này như sau, dấu gạch ngang tôi sẽ đề cập sau:

  • Thanh-Long (giữ nguyên)
  • Bach-Ho (mất dấu nặng dưới chữ a, mũ và dấu hỏi của chữ )
  • Huyen-Vu (chữ thành chữ e, chữ ũ thành chữ u )
  • Chu-Tuoc (ước thành uoc)

Giải pháp như sau, với function remove_diacritical_marks/1:

def remove_diacritical_marks(string) when is_binary(string) do
  # á à ã ạ ả: dấu sắc, huyền, ngã, nặng, hỏi
  list_1 = [769, 768, 771, 803, 777]
  # â, ă, ư
  list_2 = [770, 774, 795]

  string
  |> String.normalize(:nfd)
  |> String.to_charlist()
  |> Enum.filter(fn(e) ->
    Enum.member?(list_1 ++ list_2, e) == false
  end)
  |> Kernel.to_string()
end

Còn đây là test case:

defmodule MiningRigMonitor.UtilityTest do
  use ExUnit.Case
  alias  MiningRigMonitor.Utility

  test "remove_diacritical_marks 1" do
    string = """
    a á à ã ạ ả
    â ấ ầ ẫ ậ ẩ
    ă ắ ằ ẳ ặ ẳ
    e é è ẽ ẹ ẻ
    ê ế ề ễ ệ ể
    u ú ù ũ ụ ủ
    ư ứ ừ ữ ự ử
    o ó ò õ ọ ỏ
    ơ ớ ờ ỡ ợ ở
    """
    test_result = Utility.remove_diacritical_marks(string)
    expected_result = """
    a a a a a a
    a a a a a a
    a a a a a a
    e e e e e e
    e e e e e e
    u u u u u u
    u u u u u u
    o o o o o o
    o o o o o o
    """
    assert(test_result == expected_result)
  end

  test "remove_diacritical_marks 2" do
    string = """
    A Á À Ã Ạ Ả
    Â Ấ Ầ Ẫ Ậ Ẩ
    Ă Ắ Ằ Ẳ Ặ Ẳ
    E É È Ẽ Ẹ Ẻ
    Ê Ế Ề Ễ Ệ Ể
    U Ú Ù Ũ Ụ Ủ
    Ư Ứ Ừ Ữ Ự Ử
    O Ó Ò Õ Ọ Ỏ
    Ơ Ớ Ờ Ỡ Ợ Ở
    """
    test_result = Utility.remove_diacritical_marks(string)
    expected_result = """
    A A A A A A
    A A A A A A
    A A A A A A
    E E E E E E
    E E E E E E
    U U U U U U
    U U U U U U
    O O O O O O
    O O O O O O
    """
    assert(test_result == expected_result)
  end
end

Phương pháp của tôi là tách chữ có dấu thành một danh sách chữ + dấu liên quan. (Trong Elixir, module String, nó gọi là Normalization Form Canonical Decomposition - nfd). Nguyên văn tiếng anh như sau:

:nfd - Normalization Form Canonical Decomposition. Characters are decomposed by canonical equivalence,
and multiple combining characters are arranged in a specific order.

Ví dụ:

  • chứ áa + dấu sắc.
  • chữ a + mũ + dấu sắc.
iex(3)> String.normalize("á", :nfd) |> String.to_charlist
[97, 769]
iex(4)> String.normalize("ấ", :nfd) |> String.to_charlist
[97, 770, 769]

Dưới đây là danh sách thanh sắc, ký hiệu mà tôi mò được.

# á à ã ạ ả: dấu sắc, huyền, ngã, nặng, hỏi
list_1 = [769, 768, 771, 803, 777]
# â, ă, ư
list_2 = [770, 774, 795]

Bạn thấy đấy, sau khi có danh sách này, việc cần làm chỉ là dùng Enum.filter/2, nếu mà char nào nằm trong nhóm list_1 & list_2 thì chúng ta loại bỏ. Kết quả lúc này là 1 charlist []. Để biến nó thành String, tôi dùng String.to_string/1.

Còn về cái dấu gạch ngang -. Tôi sử dụng regular expression |> String.replace(~r([^a-zA-Z0-9]),"-") sau khi đã chạy qua remove_diacritical_marks/1.

Hi vọng tôi đã có thể giúp tiết kiệm 2 phút cuộc đời với cái này!

Reference:

How to block suspend/hibernate/sleep on Fedora?

16/04/2025 @ Saigon Linux

Execute the following command to block Fedora going to suspend/hibernate/sleep mode

Part 1. Systemd

systemctl mask sleep.target;
systemctl mask hibernate.target;
systemctl mask sleep.target;
systemctl mask hybrid-sleep.target;

Part 2. Gnome Desktop Management

How to find which config to update?

sudo -u gdm dbus-run-session gsettings list-recursively org.gnome.settings-daemon.plugins.power | grep sleep

org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout 900
org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type 'suspend'
org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout 900
org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type 'suspend'

Change sleep-inactive-ac-timeout & sleep-inactive-battery-timeout to 0

sudo -u gdm dbus-run-session gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-ac-timeout 0
sudo -u gdm dbus-run-session gsettings set org.gnome.settings-daemon.plugins.power sleep-inactive-battery-timeout 0

Các vấn đề và cách khắc phục trên máy in 3D Creality K1 Max?

10/04/2025 @ Saigon 3D Print

Xin chào, tôi đã sở hữu chiếc máy in 3D nhãn hiệu K1 Max được gần một năm. Đây là một chiếc máy in tuyệt vời, tuy nhiên tin tôi đi, nếu bạn không may mắn, chiếc máy này sẽ hành bạn ra trò đấy. Tôi bị nó hành!

Vấn đề 1. Bàn cong vênh và nghiêng (bed mesh)

Nếu bạn chỉ in mỗi cái thuyền - 3DBenchy đi kèm theo máy thì sẽ chả sao đâu. Các vấn đề tôi gặp là khi in với bề mặt sàn lớn.

Nói một cách đơn giản hơn, việc có một lớp nhựa đầu tiên mới diện tích 25cm x 25cm là điều không khả thi. Thực sự tệ hại! Đây chính là vấn đề đầu tiên!

Tôi thực sự ghét điều này! Tôi đã kỳ vọng rất nhiều ở K1 Max!

[1] Nhìn mà xem, chán đời!
[1] Nhìn mà xem, chán đời!

Để giải quyết vấn đề này, chúng ta cần hiểu cơ cấu một chút. Có 2 bộ phần:

  • Bàn gia nhiệt, nó đơn giản là tấm kim loại có dây mai xo bên dưới, tấm này có gắn lớp nam châm để gắn với bàn in. Tấm này bị cong vênh khi gia nhiệt, thậm chí, độ cong vênh còn thay đổi theo thời gian.
  • Giá đỡ tấm kim loại mà tôi đề cập bên trên. Cái giá đỡ này khi lắp ráp vào ty ren trục Z, nó được lắp không đều khiến khi bàn bị nghiêng.
[4] Cơ cấu bàn gia nhiệt - heat bed
[4] Cơ cấu bàn gia nhiệt - heat bed

Cách giải quyết như sau:

  • Sử dụng bàn kính Glass Bed 310x320x4mm . Thứ này sẽ giúp khắc phục sự cong vênh của bàn gia nhiệt.
  • Đệm cân bàn silicon giảm chấn - cao bằng nhau 16mm
  • Bộ lò xo + ốc cân bàn M4. Thứ này kết hợp với đệm silicon giảm chấn giúp khắc phục độ chênh ở ty ren trục Z.
[5] Bàn kính - 310x320x4mm
[5] Bàn kính - 310x320x4mm
[6] Đệm cân bàn Silicone giảm chấn cho máy In 3D - cao 16mm
[6] Đệm cân bàn Silicone giảm chấn cho máy In 3D - cao 16mm
[7] Bộ lò xo ốc cân bàn M4 núm hoa
[7] Bộ lò xo ốc cân bàn M4 núm hoa

Vấn đề 2. Tắc nhựa trong bộ phận kéo nhựa (extruder)

Vấn đề thứ hai là tắc nhựa, Trong quá trình sử dụng bộ phận phun nhựa(extruder) có hiện tượng bị tắc nhựa, nhựa không thể đùn xuống vòi phun được. Anh em trên reddit hay gọi là heat creep, jams, clogs. Nhiệt năng từ động cơ, truyền vào bánh răng đang kéo nhựa bên trong, hệ quả là chỗ nhựa tiếp xúc với bánh răng kéo nhựa bị mềm ra, sau cùng không đùn nhựa xuống được.

[2] Động cơ kéo nhựa không được làm mát tốt.
[2] Động cơ kéo nhựa không được làm mát tốt.
[3] Bên trong bộ đùn nhựa.
[3] Bên trong bộ đùn nhựa.

Cách giải quyết:

  • Mở nắp máy in 3D khi in chất liệu PLA
  • Giảm dòng điện cấp cho động cơ kéo nhựa, từ 0.55 amp xuống 0.45 amp. file printer.cfg
  • Lắp lá tản nhiệt để làm mát động cơ
[9] Giảm dòng điện của động cơ.
[9] Giảm dòng điện của động cơ.
[8] Tản nhiệt cho động cơ
[8] Tản nhiệt cho động cơ

Vấn đề 3. Tắc nhựa trong mũi (nozzle)

Ở thời điểm tôi mua máy, vòi phun của tôi là unicorn nozzle, loại mới nhất của Creality.

Vấn đề này sau khi vấn đề số tắc nhựa trong bộ phần kéo nhựa xảy ra. Có khả năng là nhựa trong đầu phun khi làm nóng quá lâu, biến chất, làm thô bề mặt trong đầu phun nhựa.

Để khắc phục vấn đề này, tôi hiện tại đang dùng đầu phun 0.6mm thay cho mũi bán kèm theo máy là 0.4mm

[10] Vòi phun 0.6mm
[10] Vòi phun 0.6mm

Vấn đề 4. Ánh sáng yếu

Vấn đề thứ ba là ánh sáng, hệ thống ánh sáng của K1 Max phục vụ cho cái camera của nó chứ không phục vụ cho người xem trực tiếp. Ánh sáng khá là tối so với nhu cầu của tôi.

Giải pháp cho vấn đề này xem ở đây: A new lightning system for K1 Max

[11] Thêm đèn LED 12V
[11] Thêm đèn LED 12V

Trích Dẫn và Nguồn gốc

XMRig: FAILED TO APPLY MSR MOD, HASHRATE WILL BE LOW

08/04/2025 @ Saigon Mining Rig

This post is all about resolve MSR error while running XMRig on Linux and Window.

Error: FAILED TO APPLY MSR MOD, HASHRATE WILL BE LOW

 * ABOUT        XMRig/6.22.2 gcc/13.2.1 (built for Linux x86-64, 64 bit)
 * LIBS         libuv/1.49.2 OpenSSL/3.0.15 hwloc/2.11.2
 * HUGE PAGES   supported
 * 1GB PAGES    supported
 * CPU          AMD Ryzen 9 7950X3D 16-Core Processor (1) 64-bit AES
                L2:16.0 MB L3:128.0 MB 16C/32T NUMA:1
 * MEMORY       6.8/30.5 GB (22%)
                DIMMA1: <empty>
                DIMMA2: 16 GB DDR5 @ 6000 MHz F5-6000J3038F16G
                DIMMB1: <empty>
                DIMMB2: 16 GB DDR5 @ 6000 MHz F5-6000J3038F16G
 * MOTHERBOARD  Micro-Star International Co., Ltd. - MAG B650 TOMAHAWK WIFI (MS-7D75)
 * DONATE       1%
 * ASSEMBLY     auto:ryzen
 * POOL #1      pool.hashvault.pro:443 coin Monero
 * COMMANDS     hashrate, pause, resume, results, connection
 * HTTP API     0.0.0.0:8080
[2025-04-08 00:19:57.673]  net      use pool pool.hashvault.pro:443 TLSv1.3 157.20.104.252
[2025-04-08 00:19:57.673]  net      fingerprint (SHA-256): "420c7850e09b7c0bdcf748a7da9eb3647daf8515718f36d9ccfdd6b9ff834b14"
[2025-04-08 00:19:57.673]  net      new job from pool.hashvault.pro:443 diff 72000 algo rx/0 height 3384982 (124 tx)
[2025-04-08 00:19:57.673]  cpu      use argon2 implementation AVX-512F
[2025-04-08 00:19:57.673]  msr      cannot set MSR 0xc0011020 to 0x0004400000000000
[2025-04-08 00:19:57.673]  msr      FAILED TO APPLY MSR MOD, HASHRATE WILL BE LOW   <<-------------------- ERROR HERE
[2025-04-08 00:19:57.673]  randomx  init dataset algo rx/0 (32 threads) seed fbd882390916fe90...
[2025-04-08 00:19:57.782]  randomx  allocated 3072 MB (2080+256) huge pages 100% 3/3 +JIT (109 ms)
[2025-04-08 00:19:58.896]  randomx  dataset ready (1114 ms)

For Linux, I test it with Fedora 41.

  • Run as sudo
  • Disable Secure Boot in BIOS
  • Turn off Intel Virtualization Technology, on MSI Motherboard, Bios Click 5 dashboard, its name is SVM (Overlocking >> Advanced CPU Configuration >> SVM mode)

For Window, I did not test, but the concept is the same.

  • Run as administrator (Window)
  • Disable Secure Boot in BIOS
  • Turn off Intel Virtualization Technology in BIOS, on MSI Motherboard, Bios Click 5 dashboard, its name is SVM (Overlocking >> Advanced CPU Configuration >> SVM mode)
  • Turn off core isolation (Window)
  • Turn off memory integrety (Window)

What is screen size of BPhone B86

05/04/2025 @ Saigon etc

The screen size of Bphone B86 is 424 x 800px (width x height).

You can check any device with this web tool - viewportsizer.com-What is my screen size?

BPhone B86 - 424 x 800px
BPhone B86 - 424 x 800px

Personal note: Installing a Beam fullnode

30/03/2025 @ Saigon Cryptocurrency Node

I. Systemctl service /etc/systemd/system/beam.service

[Unit]
Description=Beam Node
Requires=network.target

[Service]
WorkingDirectory=/opt/beam-node-7.5.13882/
ExecStart=/opt/beam-node-7.5.13882/beam-node --config_file=/opt/beam-node-7.5.13882/beam-node.cfg
User=nguyenvinhlinh
RemainAfterExit=yes
Restart=on-failure
RestartSec=10
TimeoutStopSec=180

[Install]
WantedBy=multi-user.target#

II. Firewall-cmd service - /etc/firewalld/services/beam.xml

<?xml version="1.0" encoding="utf-8"?>
<service>
  <short>Beam</short>
  <description>
    P2P: 10000
  </description>
  <port protocol="tcp" port="10000"/>
  <port protocol="udp" port="10000"/>
</service>

III. Beam node config - /opt/beam-node-7.5.13882/beam-node.cfg

################################################################################
# General options:
################################################################################

# port to start server on
port=10000

# log level [info|debug|verbose]
log_level=debug

# file log level [info|debug|verbose]
file_log_level=debug

# old logs cleanup period (days)
log_cleanup_days=5

################################################################################
# Node options:
################################################################################

# node storage path
storage=/opt/beam-node-7.5.13882/data/node.db
network=mainnet

# nodes to connect to
peer=eu-nodes.mainnet.beam.mw:8100
peer=us-nodes.mainnet.beam.mw:8100

# port to start stratum server on
# stratum_port=0

# path to stratum server api keys file, and tls certificate and private key
# stratum_secrets_path=.

# Enforce re-synchronization (soft reset)
# resync=0

# Owner viewer key
# owner_key=

# Standalone miner key
# miner_key=

# password for keys
# pass

# Fork1 height
# Fork1=

# Path to treasury for testing
# treasury_path=

Lắp ráp hệ thống xịt rửa cho sân vườn.

28/03/2025 @ Saigon Projects

Hey hey hey, chào buổi sáng Việt Nam.

Tôi đã lên kế hoạch lắp ráp hệ thống xịt rửa cho sân thượng được một thời gian rồi. Tuy nhiên, mọi việc cứ trôi đi do bận rộn với dự án quản lý dàn đào tiền mã hóa. Đến hôm nay, 27/3/2025 tôi mới có thể thực hiện được với sự giúp đỡ của hai người em trai.

Cảm nhận đầu tiền sau khi sử dụng là nước hơi hơi yếu, cơ mà dùng một lúc thì nó cũng quen, bây giờ thì tôi thấy khá là ổn, vừa phải. Thực ra vệ sinh sân vườn với cái máy bơm tăng áp này là vừa phải, bên cạnh đó nó cũng góp phần tiết kiệm nước. Tôi thích điều này.

Còn gì thích hơn việc mình làm điều mình yêu, và yêu điều mình làm.

[1] Lắp ráp thử nghiệm
[1] Lắp ráp thử nghiệm

Chi phí cho dự án nhỏ này như sau:

Miêu tả Giá tiền
[1] Bộ bơm tăng áp 560,000
++ Bơm tăng áp 12V Sinleader TH2203 96W  
++ Nguồn 12V  
++ Lọc rác  
++ Vòi xịt rửa xe tăng áp  
++ Ống đây áp lực PU 12mm 6m  
++ Tiền ship bên boba.vn 25,000
[2] Thêm 3m ống dây 36,000
[3] Tủ điện ngoài trời 231,200
++ Tiền ship shopee 20,700
[4] Vít nở 6mm x 10 2,000
[5] Dây điện đôi 0.75mm, 3m dây 24,000
[6] Phích cắm 10,000
[7] Mũi khoan tháp 175,000
Tổng cộng 1,083,900

Để khoan 2 cái lỗ tròn cho dây nước vào và ra, tôi buộc lòng phải mua mũi khoan tháp của shop MR.DIY. Họ bán một bộ 3 mũi khoan tháp là 175,000 VND. Tính ra, mỗi lỗ khoan, có giá trị là hơn 85,000. Đây là những cái lỗ đắt nhất mà tôi từng phải khoan.

Lúc đang chưa có việc làm, mấy thứ tốn tiền hay xảy ra!

Trò đời nó hay thế lắm, nhưng tội cái, tôi lại yêu một cái lỗ hoàn hảo.

[2] 3 mũi khoan tháp với giá 175,000
[2] 3 mũi khoan tháp với giá 175,000

Và còn đây là kết quả, thực sự rất hài lòng.

[3] Thùng điện gắn ngoài trời
[3] Thùng điện gắn ngoài trời
[4] Vòi nước tăng áp này ổn phết. 7/10 điểm
[4] Vòi nước tăng áp này ổn phết. 7/10 điểm

Đặc biệt là cái máy bơm này nó có cảm biến hay sao ý, khi mà tôi khóa nước ở vòi tăng áp, động cơ nó tự động ngắt. Khá là thú vị. Tôi chắc chắn sẽ tìm hiểu cái này.

Lời kết: Cá nhân tôi, tôi nghĩ rằng sẽ tuyệt hơn nếu mà có hệ thống vệ sinh thụ động, ví dụ là hệ thống nước bao xung quanh sân.

Ấn nút là nó xả nước đẩy sân. Tôi chỉ cần gạt nước đi là xong. Kết thúc câu chuyện.

Và, con đường OVER ENGINEER chả bao giờ kết thúc cả.