Nexom 1.0.4__py3-none-any.whl → 1.0.5__py3-none-any.whl

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.
@@ -1,169 +1,125 @@
1
1
  <Extends base />
2
2
  <Insert main>
3
- <section id="install">
4
- <h2>インストール</h2>
5
- <pre><code>pip install nexom</code></pre>
6
- <div class="note">
7
- <strong>前提:</strong> Nexom は WSGI ベースです。WSGI サーバ(例: gunicorn)と組み合わせて動かす想定です。
8
- </div>
9
- </section>
10
-
11
- <section id="quickstart">
12
- <h2>最短スタート</h2>
13
- <p>まずは「HTML を返すルート」を 1 本作るのが最速です。</p>
14
- <h3>ルート定義(例)</h3>
15
- <pre><code>from nexom.web.path import Path, Pathlib
16
- from nexom.web.response import HtmlResponse
17
-
18
- def index(request, args):
19
- return HtmlResponse("&lt;h1&gt;Hello Nexom&lt;/h1&gt;")
20
-
21
- routing = Pathlib(
22
- Path("", index, "index"),
23
- )</code></pre>
24
-
25
- <p class="note">
26
- <strong>ポイント:</strong> Nexom handler は <code>(request, args)</code> を受け取って <code>Response</code>(または <code>HtmlResponse</code>/<code>JsonResponse</code>)を返すのが基本です。
27
- </p>
28
- </section>
29
-
30
- <section id="routing">
31
- <h2>ルーティング</h2>
32
-
33
- <h3>基本</h3>
34
- <pre><code>from nexom.web.path import Path, Pathlib
35
-
36
- routing = Pathlib(
37
- Path("", index, "index"),
38
- Path("doc/", docs, "docs"),
39
- )</code></pre>
40
-
41
- <h3>パスパラメータ({id})</h3>
42
- <p><code>user/{id}</code> のように書くと、URL の該当部分が <code>args</code> に入ります。</p>
43
- <pre><code>from nexom.web.response import HtmlResponse
44
-
45
- def user(request, args):
46
- return HtmlResponse(f"&lt;p&gt;User ID: {args['id']}&lt;/p&gt;")
47
-
48
- Path("user/{id}", user, "user")</code></pre>
49
-
50
- <h3>静的ファイル(Static)</h3>
51
- <p><code>Static</code> を使うと、ディレクトリ配下のファイルを配信できます。</p>
52
- <pre><code>from nexom.web.path import Static
53
-
54
- Static("static/", "./static", "static_files")</code></pre>
55
- <ul>
56
- <li><code>/static/xxx</code> <code>./static/xxx</code> にマップして配信します。</li>
57
- <li>ディレクトリトラバーサル対策(<code>../</code>)はフレームワーク側でブロックする設計です。</li>
58
- </ul>
59
- </section>
60
-
61
- <section id="responses">
62
- <h2>レスポンス</h2>
63
-
64
- <h3>HtmlResponse</h3>
65
- <pre><code>from nexom.web.response import HtmlResponse
66
-
67
- return HtmlResponse("&lt;h1&gt;OK&lt;/h1&gt;")</code></pre>
68
-
69
- <h3>JsonResponse</h3>
70
- <pre><code>from nexom.web.response import JsonResponse
71
-
72
- return JsonResponse({"ok": True, "message": "hello"})</code></pre>
73
-
74
- <h3>低レベル Response(自由度重視)</h3>
75
- <p>高度なことをしたい場合は <code>Response</code> を直接使います。</p>
76
- <pre><code>from nexom.web.response import Response
77
-
78
- # バイナリや任意 Content-Type を返す
79
- return Response(
80
- b"binary",
81
- content_type="application/octet-stream",
82
- )</code></pre>
83
-
84
- <h3>リダイレクト</h3>
85
- <pre><code>from nexom.web.response import Redirect
86
-
87
- return Redirect("/to")</code></pre>
88
-
89
- <h3>エラーレスポンス</h3>
90
- <p>標準のエラーページテンプレートから HTML を生成します。</p>
91
- <pre><code>from nexom.web.response import ErrorResponse
92
-
93
- return ErrorResponse(404, "Not Found")</code></pre>
94
-
95
- <div class="note">
96
- <strong>charset について:</strong>
97
- Nexom は <code>str</code> body を返すときに <code>charset</code> で <code>encode</code> します。<br>
98
- <code>HtmlResponse</code> / <code>JsonResponse</code> は “短く書ける” 方向で、UTF-8 を前提に使える設計です。
99
- </div>
100
- </section>
101
-
102
- <section id="templates">
103
- <h2>テンプレート</h2>
104
- <p>Nexom のテンプレートは「最小機能で予測可能」に寄せた独自構文です。</p>
105
-
106
- <h3>変数展開</h3>
107
- <pre><code>&lt;h1&gt;{{ title }}&lt;/h1&gt;</code></pre>
108
-
109
- <h3>継承(Extends)と差し込み(Insert)</h3>
110
- <pre><code>&lt;Extends base /&gt;
111
- &lt;Insert main&gt;
112
- &lt;h1&gt;Hello&lt;/h1&gt;
113
- &lt;/Insert&gt;</code></pre>
114
-
115
- <h3>部品読み込み(Import)</h3>
116
- <pre><code>&lt;Import header /&gt;</code></pre>
117
-
118
- <h3>Python 側から呼ぶ</h3>
119
- <pre><code>from nexom.web.template import Templates
120
-
121
- templates = Templates("./templates", "default", "document")
122
-
123
- html = templates.default(title="Nexom")
124
- return HtmlResponse(html)</code></pre>
125
-
126
- <div class="note">
127
- <strong>設計方針:</strong> テンプレートにロジックを入れず、「HTML と差し込み」に寄せることで、コード側をシンプルに保つ思想です。
128
- </div>
129
- </section>
130
-
131
- <section id="cli">
132
- <h2>CLI</h2>
133
-
134
- <h3>動作確認</h3>
135
- <pre><code>python -m nexom test</code></pre>
136
-
137
- <h3>サーバープロジェクト生成</h3>
138
- <pre><code>python -m nexom build-server myapp</code></pre>
139
-
140
- <p>生成されるプロジェクトには以下が含まれます(構成はバージョンにより変わる場合があります)。</p>
141
- <ul>
142
- <li>最小のルーティング定義</li>
143
- <li>テンプレート一式</li>
144
- <li>静的ファイル用ディレクトリ</li>
145
- <li>gunicorn / config の雛形</li>
146
- </ul>
147
- </section>
148
-
149
- <section id="tips">
150
- <h2>運用のコツ</h2>
151
-
152
- <h3>「短く書く層」と「脱出口」を分ける</h3>
153
- <ul>
154
- <li>普段: <code>HtmlResponse</code> / <code>JsonResponse</code> を使う</li>
155
- <li>詰めたい: <code>Response</code> で headers / content-type を直接制御</li>
156
- </ul>
157
-
158
- <h3>ルーティングは “薄く”</h3>
159
- <p>handler 内で複雑化しそうなら、関数を分けて「処理の塊」を小さくするのが Nexom と相性良いです。</p>
160
-
161
- <h3>テストのおすすめ</h3>
162
- <p>公開運用を想定するなら、最低限この3つのテストを用意すると安心です。</p>
163
- <ul>
164
- <li>Path の引数抽出</li>
165
- <li>Static の traversal ブロック</li>
166
- <li>Response の Content-Type / charset の期待値</li>
167
- </ul>
168
- </section>
3
+ <div class="article-container">
4
+ <h1 id="nexom">README</h1>
5
+ <p>Lightweight Python Web Framework (WSGI)</p>
6
+ <p>Nexomは短いコードで最低限動作し、シンプルで理解のしやすい設計・構造を目指しています。<br>
7
+ また細かい仕様も変更でき、多様な処理に対応します。</p>
8
+ <h2 id="はじめる">はじめる </h2>
9
+ <p>最初のサーバーを起動するには、3つの手順が必要です。</p>
10
+ <ol>
11
+ <li>プロジェクトディレクトリを作成</li>
12
+ <li>nexomをpipでインストール、プロジェクトのビルド</li>
13
+ <li>起動</li>
14
+ </ol>
15
+ <h3 id="1プロジェクトディレクトリの作成">1.プロジェクトディレクトリの作成 </h3>
16
+ <p><strong>準備</strong></p>
17
+ <p>用意していない場合はディレクトリを作成し、仮想環境も準備してください</p>
18
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>mkdir banana_project
19
+ cd banana_project
20
+
21
+ python -m venv venv
22
+ source venv/bin/activate
23
+ </code></pre><h3 id="2-pipでインストール-サーバーのビルド">2. pipでインストール、サーバーのビルド </h3>
24
+ <p><strong>インストール</strong></p>
25
+ <p>nexomをインストールします。</p>
26
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>pip install
27
+ </code></pre><p><strong>プロジェクトのビルド</strong></p>
28
+ <p>プロジェクトディレクトリ上で、以下のコマンドを実行してください(名前は自由)<br>
29
+ もしNginxもしくはApacheを使用する場合 --gateway オプションにどちらか入力してください</p>
30
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>$ python -m nexom start-project
31
+ </code></pre><p>以下の構成でプロジェクトが生成されます。</p>
32
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>banana_project/
33
+ ├─ app/
34
+ │ ├─ pages/
35
+ │ │ ├─ __init__.py
36
+ │ │ ├─ _templates.py
37
+ │ │ └─ * pages *
38
+ │ ├─ static/
39
+ │ │ └─ * static items *
40
+ │ ├─ templates/
41
+ │ │ └─ * html files *
42
+ │ ├─ __init__.py
43
+ │ ├─ config.py
44
+ │ ├─ gunicorn.conf.py
45
+ │ ├─ router.py
46
+ │ └─ wsgi.py
47
+ ├─ auth/
48
+ │ ├─ __init__.py
49
+ │ ├─ config.py
50
+ │ ├─ gunicorn.conf.py
51
+ │ └─ wsgi.py
52
+ └─ data/
53
+ ├─ log/
54
+ │ └─ * app logs *
55
+ ├─ db/
56
+ │ └─ * app db *
57
+ └─ gateway/
58
+ ├─ app.nginx.conf
59
+ └─ app.apache.conf
60
+ </code></pre><h3 id="3起動">3.起動 </h3>
61
+ <p>以下のコマンドを起動します。</p>
62
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>$ python -m nexom run
63
+ </code></pre><p>ブラウザからアクセスできるようになります。<br>
64
+ デフォルトのポートは8080です。</p>
65
+ <p><a href="https://localhost:8080">https://localhost:8080</a></p>
66
+ <p>ポートなどの設定は <code>config.py</code> から変更してください。</p>
67
+ <h2 id="nginx等使用して外部公開する">Nginx等使用して外部公開する </h2>
68
+ <p>gatewayディレクトリにある設定を読み込んでください</p>
69
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>http {
70
+ include /home/ubuntu/banana_project/gateway/*.conf;
71
+ }
72
+ </code></pre><h2 id="systemdに登録して自動起動する">Systemdに登録して自動起動する </h2>
73
+ <p><strong>Ubuntuの場合</strong></p>
74
+ <ol>
75
+ <li><code>/etc/systemd/system</code> に、 <code>banana_sample.service</code> を作成します。</li>
76
+ <li><code>banana_sample.service</code> に以下を書き込みます。(これは一例です。環境に合わせて設定してください。)</li>
77
+ </ol>
78
+ <p>サーバーのディレクトリが <code>/home/ubuntu/banana_project</code> にある場合</p>
79
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>[Unit]
80
+ Description=Nexom Web Freamework
81
+ After=network.target
82
+
83
+ [Service]
84
+ User=www-data
85
+ Group=www-data
86
+ WorkingDirectory=/home/ubuntu/banana_project
87
+ Environment="PYTHONPATH=/home/ubuntu/banana_project"
88
+ ExecStart=/home/ubuntu/banana_project/venv/bin/gunicorn sample.wsgi:app --config sample/gunicorn.conf.py
89
+ Restart=always
90
+ RestartSec=3
91
+ [Install]
92
+ WantedBy=multi-user.target
93
+ </code></pre><p>以下のコマンドを実行します</p>
94
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>sudo systemd daemon-reload
95
+ sudo systemd enable banana_sample
96
+ sudo systemd start banana_sample
97
+ </code></pre><h3 id="テンプレートユニットを活用して複数のアプリを効率的に管理">テンプレートユニットを活用して複数のアプリを効率的に管理 </h3>
98
+ <p>テンプレートユニットを活用し .service ファイルを一枚にまとめられます。</p>
99
+ <p><code>/etc/systemd/system/banana-project@.service</code></p>
100
+ <pre data-role="codeBlock" data-info="" class="language-text"><code>[Unit]
101
+ Description=Nexom Web Server (%i)
102
+ After=network.target
103
+
104
+ [Service]
105
+ User=www-data
106
+ Group=www-data
107
+ WorkingDirectory=/home/ubuntu/banana_project
108
+ Environment="PYTHONPATH=/home/ubuntu/banana_project"
109
+ ExecStart=/home/ubuntu/banana_project/venv/bin/gunicorn %iwsgi:app --config %i/gunicorn.conf.py
110
+ Restart=always
111
+ RestartSec=3
112
+ [Install]
113
+ WantedBy=multi-user.target
114
+ </code></pre><pre data-role="codeBlock" data-info="" class="language-text"><code>sudo systemd daemon-reload
115
+
116
+ sudo systemd enable banana-project@banana1
117
+ sudo systemd enable banana-project@banana2
118
+ sudo systemd enable banana-project@banana3
119
+
120
+ sudo systemd start banana-project@banana1
121
+ sudo systemd start banana-project@banana2
122
+ sudo systemd start banana-project@banana3
123
+ </code></pre><p>2026 1/27</p>
124
+ </div>
169
125
  </Insert>
@@ -1,3 +1,3 @@
1
- <footer>
2
- <div class="footer-text">Nexom Web Framework powerd by AidaTouri 2026</div>
3
- </footer>
1
+ <div class="footer-container">
2
+ <div class="footer-text">&copy; AidaTouri 2026</div>
3
+ </div>
@@ -1,3 +1,9 @@
1
- <header>
2
- <div class="header-title">Nexom Web Frameworks</div>
3
- </header>
1
+ <div class="header-wrapper">
2
+ <div class="header-container">
3
+ <a href="/" class="header-logo">Nexom</a>
4
+ <a href="https://github.com/ait913/Nexom" class="header-link", title="to GitHub Repository">
5
+ <img src="/static/github.png" alt="GitHub Link">
6
+ </a>
7
+ </div>
8
+ <div class="header-speacer"></div>
9
+ </div>
@@ -5,59 +5,188 @@
5
5
  <meta charset="utf-8" />
6
6
  <meta name="viewport" content="width=device-width,initial-scale=1" />
7
7
  <title>Nexom Login</title>
8
+
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Noto+Sans+JP:wght@400;600;700&display=swap" rel="stylesheet">
12
+
8
13
  <style>
9
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 24px; }
10
- .wrap { max-width: 720px; margin: 0 auto; }
11
- .card { border: 1px solid #eee; border-radius: 14px; padding: 16px; background: #fff; }
12
- label { display:block; font-size:12px; margin: 12px 0 6px; color:#333; }
13
- input { width:100%; padding:10px 12px; border:1px solid #ddd; border-radius:10px; font-size:14px; }
14
- button { margin-top: 14px; padding:10px 12px; border:1px solid #333; border-radius:10px; background:#333; color:#fff; cursor:pointer; }
15
- .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top: 10px; }
16
- a { color:#333; }
17
- .help { color:#666; font-size:12px; }
18
- .log { margin-top: 14px; padding: 12px; border-radius: 12px; background:#f7f7f7; border:1px solid #eee; white-space: pre-wrap;
19
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
20
- .ok { color:#0a7; }
21
- .ng { color:#c33; }
14
+ :root{
15
+ --bg:#0b0d12;
16
+ --card:#0f141c;
17
+ --line:rgba(255,255,255,.10);
18
+ --text:rgba(255,255,255,.92);
19
+ --muted:rgba(255,255,255,.62);
20
+ --danger:#ff5a6a;
21
+ --btn:#ffffff;
22
+ --btnText:#0b0d12;
23
+ --shadow:0 20px 60px rgba(0,0,0,.45);
24
+ }
25
+ *{ box-sizing:border-box; }
26
+ html,body{ height:100%; }
27
+ body{
28
+ margin:0;
29
+ background: radial-gradient(1200px 600px at 20% 10%, rgba(106,140,255,.18), transparent 60%),
30
+ radial-gradient(900px 500px at 85% 30%, rgba(39,211,155,.12), transparent 55%),
31
+ var(--bg);
32
+ color:var(--text);
33
+ font-family: Inter, "Noto Sans JP", "Hiragino Sans", "ヒラギノ角ゴシック", "Yu Gothic", "游ゴシック", system-ui, -apple-system, "Segoe UI", sans-serif;
34
+ overflow:hidden; /* スクロール禁止 */
35
+ }
36
+ .wrap{
37
+ height:100%;
38
+ display:grid;
39
+ place-items:center;
40
+ padding:24px;
41
+ }
42
+ .card{
43
+ width:min(560px, 100%);
44
+ background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.03));
45
+ border:1px solid var(--line);
46
+ border-radius:18px;
47
+ box-shadow: var(--shadow);
48
+ padding:18px;
49
+ backdrop-filter: blur(10px);
50
+ }
51
+ h1{
52
+ margin:0 0 10px;
53
+ font-size:18px;
54
+ letter-spacing:.2px;
55
+ }
56
+ .sub{
57
+ margin:0 0 14px;
58
+ color:var(--muted);
59
+ font-size:12px;
60
+ line-height:1.5;
61
+ }
62
+ label{
63
+ display:block;
64
+ font-size:11px;
65
+ color:var(--muted);
66
+ margin:0 0 6px;
67
+ }
68
+ input{
69
+ width:100%;
70
+ height:40px;
71
+ padding:0 12px;
72
+ border-radius:12px;
73
+ border:1px solid var(--line);
74
+ background: rgba(0,0,0,.25);
75
+ color:var(--text);
76
+ outline:none;
77
+ font-size:13px;
78
+ }
79
+ input:focus{
80
+ border-color: rgba(255,255,255,.18);
81
+ box-shadow: 0 0 0 3px rgba(106,140,255,.12);
82
+ }
83
+ .row{
84
+ display:flex;
85
+ gap:10px;
86
+ align-items:center;
87
+ justify-content:space-between;
88
+ margin-top:12px;
89
+ }
90
+ button{
91
+ height:40px;
92
+ padding:0 14px;
93
+ border-radius:12px;
94
+ border:1px solid rgba(255,255,255,.08);
95
+ background: var(--btn);
96
+ color: var(--btnText);
97
+ font-weight:800;
98
+ cursor:pointer;
99
+ white-space:nowrap;
100
+ }
101
+ button:disabled{ opacity:.55; cursor:not-allowed; }
102
+ .link{
103
+ color:var(--muted);
104
+ font-size:12px;
105
+ text-decoration:none;
106
+ border-bottom:1px dotted rgba(255,255,255,.25);
107
+ }
108
+ .link:hover{ color:var(--text); }
109
+
110
+ .msg{
111
+ display:none;
112
+ margin-top:10px;
113
+ padding:10px 12px;
114
+ border-radius:12px;
115
+ border:1px solid var(--line);
116
+ font-size:12px;
117
+ line-height:1.5;
118
+ white-space:pre-wrap;
119
+ background: rgba(0,0,0,.22);
120
+ }
121
+ .msg.is-error{ display:block; border-color: rgba(255,90,106,.35); }
22
122
  </style>
23
123
  </head>
124
+
24
125
  <body>
25
126
  <div class="wrap">
26
- <h1 style="margin:0 0 12px;font-size:20px;">Login</h1>
27
-
28
127
  <section class="card">
29
- <form id="formLogin">
30
- <label for="user_id">user_id</label>
128
+ <h1>Login</h1>
129
+ <p class="sub">アカウントにログイン</p>
130
+
131
+ <form id="form">
132
+ <label for="user_id">ユーザーID</label>
31
133
  <input id="user_id" name="user_id" autocomplete="username" required />
32
134
 
33
- <label for="password">password</label>
135
+ <label for="password" style="margin-top:10px;">パスワード</label>
34
136
  <input id="password" name="password" type="password" autocomplete="current-password" required />
35
137
 
36
- <button type="submit">login</button>
37
-
38
138
  <div class="row">
39
- <span class="help">送信先はこのアプリの <code>/login/</code>(アプリ → AuthClient → AuthService)</span>
40
- <a class="help" href="/signup/">signupへ</a>
139
+ <a class="link" href="./signup">まだアカウントを持っていない場合</a>
140
+ <button id="btn" type="submit">ログイン</button>
41
141
  </div>
42
- </form>
43
142
 
44
- <div id="log" class="log">ready</div>
143
+ <div id="msg" class="msg"></div>
144
+ </form>
45
145
  </section>
46
146
  </div>
47
147
 
48
148
  <script>
49
- const logEl = document.getElementById("log");
149
+ const AUTH_BASE = ""
150
+ const PAGE_PATH = "/{{ page_path }}"
151
+
152
+ const error_code_for_message = {
153
+ // ===== Auth errors =====
154
+ "A01": "フォームの入力内容に不足があります。",
155
+ "A02": "このユーザーIDはすでに使用されています。",
156
+ "A03": "ユーザーIDまたはパスワードが正しくありません。",
157
+ "A04": "このユーザーは無効化されています。",
158
+ "A05": "認証情報が見つかりません。再度ログインしてください。",
159
+ "A06": "認証情報が不正です。再度ログインしてください。",
160
+ "A07": "セッションの有効期限が切れています。再度ログインしてください。",
161
+ "A08": "このセッションは無効化されています。再度ログインしてください。",
162
+ "A09": "認証サービスに接続できません。しばらく時間をおいて再度お試しください。",
50
163
 
51
- function pretty(v) {
52
- try { return JSON.stringify(v, null, 2); } catch { return String(v); }
164
+ // fallback
165
+ "DEFAULT": "エラーが発生しました。もう一度お試しください。"
166
+ };
167
+ function getErrorMessage(code) {
168
+ if (typeof code !== "string") {
169
+ return error_code_for_message.DEFAULT;
170
+ }
171
+
172
+ return error_code_for_message[code] ?? error_code_for_message.DEFAULT;
173
+ }
174
+
175
+ const form = document.getElementById("form");
176
+ const btn = document.getElementById("btn");
177
+ const msg = document.getElementById("msg");
178
+
179
+ function clearMsg(){
180
+ msg.className = "msg";
181
+ msg.textContent = "";
53
182
  }
54
- function writeLog(ok, title, data) {
55
- const cls = ok ? "ok" : "ng";
56
- logEl.innerHTML = `<span class="${cls}">${title}</span>\n${pretty(data)}`;
183
+ function setError(text){
184
+ msg.className = "msg is-error";
185
+ msg.textContent = getErrorMessage(text);
57
186
  }
58
187
 
59
- async function postJSON(url, bodyObj) {
60
- const res = await fetch(url, {
188
+ async function postJSON(path, bodyObj){
189
+ const res = await fetch(path, {
61
190
  method: "POST",
62
191
  headers: {
63
192
  "Content-Type": "application/json; charset=utf-8",
@@ -69,26 +198,37 @@
69
198
  const text = await res.text();
70
199
  let data = {};
71
200
  try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
72
-
73
201
  return { ok: res.ok, status: res.status, data };
74
202
  }
75
203
 
76
- document.getElementById("formLogin").addEventListener("submit", async (e) => {
204
+ form.addEventListener("submit", async (e) => {
77
205
  e.preventDefault();
206
+ clearMsg();
78
207
 
79
208
  const user_id = document.getElementById("user_id").value.trim();
80
209
  const password = document.getElementById("password").value;
81
210
 
82
- if (!user_id || !password) {
83
- writeLog(false, "login: missing fields", { user_id });
211
+ if (!user_id || !password){
212
+ setError("T: missing fields");
84
213
  return;
85
214
  }
86
215
 
87
- const r = await postJSON("/login/", { user_id, password });
88
- writeLog(r.ok, `login: ${r.status}`, r.data);
216
+ btn.disabled = true;
217
+ try{
218
+ const r = await postJSON(PAGE_PATH, { user_id, password });
219
+
220
+ if (!r.ok || !r.data || r.data.ok !== true){
221
+ const code = (r.data && r.data.error) ? String(r.data.error) : "UnknownError";
222
+ setError(code);
223
+ return;
224
+ }
89
225
 
90
- // 例: token を localStorage に保存したいならここ(仕様次第)
91
- // if (r.ok && r.data && r.data.token) localStorage.setItem("nexom_token", r.data.token);
226
+ location.href = "/";
227
+ }catch{
228
+ setError("NetworkError");
229
+ }finally{
230
+ btn.disabled = false;
231
+ }
92
232
  });
93
233
  </script>
94
234
  </body>