diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index c6c83a98bf..346a32a86f 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -148,14 +148,14 @@ id: 'aidx'
 # Job concurrency per worker
 # deliverJobConcurrency: 128
 # inboxJobConcurrency: 16
-# relashionshipJobConcurrency: 16
-#  What's relashionshipJob?:
+# relationshipJobConcurrency: 16
+#  What's relationshipJob?:
 #   Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
 
 # Job rate limiter
 # deliverJobPerSec: 128
 # inboxJobPerSec: 32
-# relashionshipJobPerSec: 64
+# relationshipJobPerSec: 64
 
 # Job attempts
 # deliverJobMaxAttempts: 12
diff --git a/.config/example.yml b/.config/example.yml
index 4aa7757c61..1e649deabb 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -160,14 +160,14 @@ id: 'aidx'
 # Job concurrency per worker
 #deliverJobConcurrency: 128
 #inboxJobConcurrency: 16
-#relashionshipJobConcurrency: 16
-# What's relashionshipJob?:
+#relationshipJobConcurrency: 16
+# What's relationshipJob?:
 #  Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations.
 
 # Job rate limiter
 #deliverJobPerSec: 128
 #inboxJobPerSec: 32
-#relashionshipJobPerSec: 64
+#relationshipJobPerSec: 64
 
 # Job attempts
 #deliverJobMaxAttempts: 12
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cff0fc07cd..8f6cf2071b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,6 +21,9 @@
 - Feat: [mCaptcha](https://github.com/mCaptcha/mCaptcha)のサポートを追加
 - Fix: リストライムラインの「リノートを表示」が正しく機能しない問題を修正
 - Feat: Add support for TrueMail
+- Fix: リモートユーザーのリアクション一覧がすべて見えてしまうのを修正
+  * すべてのリモートユーザーのリアクション一覧を見えないようにします
+- Enhance: モデレーターはすべてのユーザーのリアクション一覧を見られるように
 
 ### Client
 - Feat: 新しいゲームを追加
@@ -45,11 +48,20 @@
 - Enhance: ノート作成画面のファイル添付メニューから直接ファイルを削除できるように
 - Enhance: MFMの属性でオートコンプリートが使用できるように #12735
 - Enhance: 絵文字編集ダイアログをモーダルではなくウィンドウで表示するように
+- Enhance: リモートのユーザーはメニューから直接リモートで表示できるように
 - Fix: ネイティブモードの絵文字がモノクロにならないように
 - Fix: v2023.12.0で追加された「モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能」が管理画面上で正しく表示されていない問題を修正
 - Fix: AiScriptの`readline`関数が不正な値を返すことがある問題のv2023.12.0時点での修正がPlay以外に適用されていないのを修正
 - Fix: v2023.12.1で追加された`$[clickable ...]`および`onClickEv`が正しく機能していないのを修正
+- Fix: Renoteのキーボードショートカットが機能していなかった問題を修正
+- Fix: 投稿フォームでアンケートの日時指定をした状態で再読み込みをすると期日が復元されない問題を修正
+- Fix: アンケートを設定したノートを「削除して編集」をするとアンケートの期日が引き継がれず、リセットされてしまう問題を修正
+- Fix: デッキのプロファイル作成時に名前を空にできる問題を修正
+- Fix: テーマ作成時に名称が空欄でも作成できてしまう問題を修正
+- Fix: プラグインで`Plugin:register_note_post_interruptor`を使用すると、ノートが投稿できなくなる問題を修正
 - Enhance: ページ遷移時にPlayerを閉じるように
+- Fix: iOSで大きな画像を変換してアップロードできない問題を修正
+- Fix: 「アニメーション画像を再生しない」もしくは「データセーバー(アイコン)」を有効にしていても、アイコンデコレーションのアニメーションが停止されない問題を修正
 
 ### Server
 - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました
@@ -62,6 +74,8 @@
 - Fix: `notes/create`で、`text`が空白文字のみで構成されていてかつリノート、ファイルまたは投票を含んでいるリクエストに対するレスポンスの`text`が`""`から`null`になるように変更
 - Fix: ipv4とipv6の両方が利用可能な環境でallowedPrivateNetworksが設定されていた場合プライベートipの検証ができていなかった問題を修正
 - Fix: properly handle cc followers
+- Fix: ジョブに関する設定の名前を修正 relashionshipJobPerSec -> relationshipJobPerSec
+- Fix: コントロールパネル->モデレーション->「誰でも新規登録できるようにする」の初期値をONからOFFに変更 #13122
 
 ### Service Worker
 - Enhance: オフライン表示のデザインを改善・多言語対応
diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.js
new file mode 100644
index 0000000000..81f497b5b8
--- /dev/null
+++ b/cypress/e2e/router.cy.js
@@ -0,0 +1,30 @@
+describe('Router transition', () => {
+	describe('Redirect', () => {
+		// サーバの初期化。ルートのテストに関しては各describeごとに1度だけ実行で十分だと思う(使いまわした方が早い)
+		before(() => {
+			cy.resetState();
+
+			// インスタンス初期セットアップ
+			cy.registerUser('admin', 'pass', true);
+
+			// ユーザー作成
+			cy.registerUser('alice', 'alice1234');
+
+			cy.login('alice', 'alice1234');
+
+			// アカウント初期設定ウィザード
+			// 表示に時間がかかるのでデフォルト秒数だとタイムアウトする
+			cy.get('[data-cy-user-setup] [data-cy-modal-window-close]', { timeout: 12000 }).click();
+			cy.wait(500);
+			cy.get('[data-cy-modal-dialog-ok]').click();
+		});
+
+		it('redirect to user profile', () => {
+			// テストのためだけに用意されたリダイレクト用ルートに飛ぶ
+			cy.visit('/redirect-test');
+
+			// プロフィールページのURLであることを確認する
+			cy.url().should('include', '/@alice')
+		});
+	});
+});
diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml
index 59594f85fd..eeafcafed2 100644
--- a/locales/ca-ES.yml
+++ b/locales/ca-ES.yml
@@ -130,6 +130,7 @@ overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
 reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
 rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
 attachCancel: "Eliminar el fitxer adjunt"
+deleteFile: "Esborrar l'arxiu "
 markAsSensitive: "Marcar com a NSFW"
 unmarkAsSensitive: "Deixar de marcar com a sensible"
 enterFileName: "Defineix nom del fitxer"
@@ -1039,6 +1040,12 @@ rolesAssignedToMe: "Rols assignats "
 resetPasswordConfirm: "Vols canviar la teva contrasenya?"
 sensitiveWords: "Paraules sensibles"
 sensitiveWordsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves."
+sensitiveWordsDescription2: "Fent servir espais crearà expressions AND si l'expressió s'envolta amb barres inclinades es converteix en una expressió regular."
+hiddenTags: "Etiquetes ocultes"
+hiddenTagsDescription: "La visibilitat de totes les notes que continguin qualsevol de les paraules configurades seran, automàticament, afegides a \"Inici\". Pots llistar diferents paraules separant les per línies noves."
+notesSearchNotAvailable: "La cerca de notes no es troba disponible."
+license: "Llicència"
+unfavoriteConfirm: "Esborrar dels favorits?"
 myClips: "Els meus retalls"
 drivecleaner: "Netejador de Disc"
 retryAllQueuesNow: "Prova de nou d'executar totes les cues"
@@ -1048,6 +1055,14 @@ enableChartsForRemoteUser: "Generar gràfiques d'usuaris remots"
 enableChartsForFederatedInstances: "Generar gràfiques d'instàncies remotes"
 showClipButtonInNoteFooter: "Afegir \"Retall\" al menú d'acció de la nota"
 reactionsDisplaySize: "Mida de les reaccions"
+limitWidthOfReaction: "Limitar l'amplada màxima de la reacció i mostrar-les en una mida reduïda "
+noteIdOrUrl: "ID o URL de la nota"
+video: "Vídeo"
+videos: "Vídeos "
+audio: "So"
+audioFiles: "So"
+dataSaver: "Economitzador de dades"
+accountMigration: "Migració del compte"
 accountMoved: "Aquest usuari té un compte nou:"
 accountMovedShort: "Aquest compte ha sigut migrat"
 operationForbidden: "Operació no permesa "
@@ -1098,9 +1113,50 @@ branding: "Marca"
 enableServerMachineStats: "Publicar estadístiques del maquinari del servidor"
 enableIdenticonGeneration: "Activar la generació d'icones d'identificació "
 turnOffToImprovePerformance: "Desactivant aquesta opció es pot millorar el rendiment."
+createInviteCode: "Crear codi d'invitació "
+createWithOptions: "Crear invitació amb opcions"
+createCount: "Comptador d'invitacions "
+inviteCodeCreated: "Invitació creada"
+inviteLimitExceeded: "Has sobrepassat el límit d'invitacions que pots crear."
+createLimitRemaining: "Et queden {limit} invitacions restants"
+inviteLimitResetCycle: "Cada {time} {limit} invitacions."
+expirationDate: "Data de venciment"
+noExpirationDate: "Sense data de venciment"
+inviteCodeUsedAt: "Codi d'invitació fet servir el"
+registeredUserUsingInviteCode: "Codi d'invitació fet servir per l'usuari "
+waitingForMailAuth: "Esperant la verificació per correu electrònic "
+inviteCodeCreator: "Invitació creada per"
+usedAt: "Utilitzada el"
+unused: "Sense utilitzar"
+used: "Utilitzada"
+expired: "Caducat"
+doYouAgree: "Estàs d'acord?"
+beSureToReadThisAsItIsImportant: "Llegeix això perquè és molt important."
+iHaveReadXCarefullyAndAgree: "He llegit {x} i estic d'acord."
+dialog: "Diàleg "
 icon: "Icona"
+forYou: "Per a tu"
+currentAnnouncements: "Informes actuals"
+pastAnnouncements: "Informes passats"
+youHaveUnreadAnnouncements: "Tens informes per llegir."
+useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
 replies: "Respostes"
 renotes: "Impulsa"
+loadReplies: "Mostrar les respostes"
+loadConversation: "Mostrar la conversació "
+pinnedList: "Llista fixada"
+keepScreenOn: "Mantenir la pantalla encesa"
+verifiedLink: "La propietat de l'enllaç ha sigut verificada"
+notifyNotes: "Notificar quan hi hagi notes noves"
+unnotifyNotes: "Deixar de notificar quan hi hagi notes noves"
+authentication: "Autenticació "
+authenticationRequiredToContinue: "Si us plau autentificat per continuar"
+dateAndTime: "Data i hora"
+showRenotes: "Mostrar impulsos"
+edited: "Editat"
+notificationRecieveConfig: "Paràmetres de notificacions"
+mutualFollow: "Seguidor mutu"
+fileAttachedOnly: "Només notes amb adjunts"
 externalServices: "Serveis externs"
 impressum: "Impressum"
 impressumUrl: "Adreça URL impressum"
@@ -1153,6 +1209,149 @@ _initialAccountSetting:
   privacySetting: "Configuració de seguretat"
   theseSettingsCanEditLater: "Aquests ajustos es poden canviar més tard."
   youCanEditMoreSettingsInSettingsPageLater: "A més d'això, es poden fer diferents configuracions a través de la pàgina de configuració. Assegureu-vos de comprovar-ho més tard."
+_initialTutorial:
+  _reaction:
+    letsTryReacting: "Es poden afegir reaccions fent clic al botó '+'. Prova reaccionant a aquesta nota!"
+    reactToContinue: "Afegeix una reacció per continuar."
+    reactNotification: "Rebràs notificacions en temps real quan un usuari reaccioni a les teves notes."
+    reactDone: "Pots desfer una reacció fent clic al botó '-'."
+  _timeline:
+    title: "El concepte de les línies de temps"
+    description1: "Misskey mostra diferents línies de temps basades en l'ús (algunes poden no estar disponibles depenent de la política del servidor)"
+    home: "Pots veure notes dels comptes que segueixes"
+    local: "Pots veure les notes dels usuaris del servidor."
+    social: "Es mostren les notes de les línies de temps d'Inici i Local."
+    global: "Pots veure les notes de tots els servidors connectats."
+    description2: "Pots canviar la línia de temps en qualsevol moment fent servir la barra de la pantalla superior."
+    description3: "A més hi ha línies de temps per llistes i per canals. Si vols saber més {link}."
+  _postNote:
+    title: "Configuració de la publicació de les notes"
+    description1: "Quan públiques una nota a Misskey hi ha diferents opcions disponibles. El formulari de publicació es veu així"
+    _visibility:
+      description: "Pots limitar qui pot veure les teves notes."
+      public: "La teva nota serà visible per a tots els usuaris."
+      home: "Publicar només a línia de temps d'Inici. La gent que visiti el teu perfil o mitjançant les remotes també la podran veure."
+      followers: "Només visible per a seguidors. Només els teus seguidors la podran veure i ningú més. Ningú més podrà fer renotes."
+      direct: "Només visible per a alguns seguidors, el destinatari rebre una notificació. Es pot fer servir com una alternativa als missatges directes."
+      doNotSendConfidencialOnDirect1: "Tingues cura quan enviïs informació sensible."
+      doNotSendConfidencialOnDirect2: "Els administradors del servidor poden veure tot el que escrius. Ves compte quan enviïs informació sensible en enviar notes directes a altres usuaris en servidors de poca confiança."
+      localOnly: "Publicar amb aquesta opció activada farà que la nota no federi amb altres servidors. Els usuaris d'altres servidors no podran veure la nota directament, sense importar les opcions de visualització."
+    _cw:
+      title: "Avís de Contingut (CW)"
+      description: "En comptes del cos de la nota es mostrarà el que s'escrigui al camp de 'comentaris'. Fent clic a 'Llegir més' es mostrarà el cos."
+      _exampleNote:
+        cw: "Això et farà venir gana!"
+        note: "Acabo de menjar-me un donut de xocolata 🍩😋"
+      useCases: "Això es fa servir per seguir normes del servidor sobre certes notes o per ocultar contingut sensible O revelador."
+  _howToMakeAttachmentsSensitive:
+    title: "Com marcar adjunts com a contingut sensible?"
+    description: "Per adjunts que sigui requerit per les normes del servidor o que puguin contenir material sensible, s'ha d'afegir l'opció 'sensible'."
+    tryThisFile: "Prova de marcar la imatge adjunta en aquest formulari com a sensible!"
+    _exampleNote:
+      note: "Oops! L'he fet bona en obrir la tapa de Nocilla..."
+    method: "Per marcar un adjunt com a sensible, fes clic a la miniatura de l'adjunt, obre el menú i fes clic a 'Marcar com a sensible'."
+    sensitiveSucceeded: "Quan adjuntis fitxers si us plau marca la sensibilitat seguint les normes del servidor."
+    doItToContinue: "Marca el fitxer adjunt com a sensible per poder continuar."
+  _done:
+    title: "Has completat el tutorial 🎉"
+    description: "Les funcions explicades aquí és una petita mostra. Per una explicació més detallada de com fer servir MissKey consulta {link}."
+_timelineDescription:
+  home: "A la línia de temps d'Inici pots veure les notes dels usuaris que segueixes."
+  local: "A la línia de temps Local pots veure les notes de tots els usuaris d'aquest servidor."
+  social: "La línia de temps Social mostren les notes de les línies de temps d'Inici i Local."
+  global: "A la línia de temps Global pots veure les notes de tots els servidors connectats."
+_serverRules:
+  description: "Un conjunt de regles que seran mostrades abans de registrar-se. Es recomanable configurar un resum dels termes d'ús."
+_serverSettings:
+  iconUrl: "URL de la icona"
+  appIconDescription: "Especifica la icona que es mostrarà quan el {host} es mostri en una aplicació."
+  appIconUsageExample: "Per exemple com a PWA, o quan es mostri com un favorit a la pàgina d'inici del telèfon mòbil"
+  appIconStyleRecommendation: "Com la icona pot ser retallada com un cercle o un quadrat, es recomana fer servir una icona amb un marge acolorit que l'envolti."
+  appIconResolutionMustBe: "La resolució mínima és {resolution}."
+  manifestJsonOverride: "Sobreescriure manifest.json"
+  shortName: "Nom curt"
+  shortNameDescription: "Una abreviatura del nom de la instància que es poguí mostrar en cas que el nom oficial sigui massa llarg"
+  fanoutTimelineDescription: "Quan es troba activat millora bastant el rendiment quan es recuperen les línies de temps i redueix la carrega de la base de dades. Com a contrapunt, l'ús de memòria de Redis es veurà incrementada. Considera d'estabilitat aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes de inestabilitat."
+_achievements:
+  _types:
+    _notes100000:
+      flavor: "Segur que tens moltes coses a dir?"
+    _login3:
+      title: "Principiant I"
+      description: "Vas iniciar sessió fa tres dies"
+      flavor: "Des d'avui diguem Misskist"
+    _login7:
+      title: "Principiant II"
+      description: "Vas iniciar sessió fa set dies"
+      flavor: "Ja saps com va funcionant tot?"
+    _login15:
+      title: "Principiant III"
+      description: "Vas iniciar sessió fa quinze dies"
+    _login30:
+      title: "Misskist I"
+      description: "Vas iniciar sessió fa trenta dies"
+    _login60:
+      title: "Misskist II"
+      description: "Vas iniciar sessió fa seixanta dies"
+    _login100:
+      title: "Misskist III"
+      description: "Vas iniciar sessió fa cent dies"
+      flavor: "Misskist violent"
+    _login200:
+      title: "Regular I"
+      description: "Vas iniciar sessió fa dos-cents dies"
+    _login300:
+      title: "Regular II"
+      description: "Vas iniciar sessió fa tres-cents dies"
+    _login400:
+      title: "Regular III"
+      description: "Vas iniciar sessió fa quatre-cents dies"
+    _login500:
+      title: "Expert I"
+      description: "Vas iniciar sessió fa cinc-cents dies"
+      flavor: "Amics, he dit massa vegades que soc un amant de les notes"
+    _login600:
+      title: "Expert II"
+      description: "Vas iniciar sessió fa sis-cents dies"
+    _login700:
+      title: "Expert III"
+      description: "Vas iniciar sessió fa set-cents dies"
+    _login800:
+      title: "Mestre de les Notes I"
+      description: "Vas iniciar sessió fa vuit-cents dies "
+    _login900:
+      title: "Mestre de les Notes II"
+      description: "Vas iniciar sessió fa nou-cents dies"
+    _login1000:
+      title: "Mestre de les Notes III"
+      description: "Vas iniciar sessió fa mil dies"
+      flavor: "Gràcies per fer servir MissKey!"
+    _noteClipped1:
+      title: "He de retallar-te!"
+      description: "Retalla la teva primera nota"
+    _noteFavorited1:
+      title: "Quan miro les estrelles"
+      description: "La primera vegada que vaig registrar el meu favorit"
+    _myNoteFavorited1:
+      title: "Vull una estrella"
+      description: "La meva nota va ser registrada com favorita per una de les altres persones"
+    _profileFilled:
+      title: "Estic a punt"
+      description: "Vaig fer la configuració de perfil"
+    _markedAsCat:
+      title: "Soc un gat"
+      description: "He establert el meu compte com si fos un Gat"
+      flavor: "Encara no tinc nom"
+    _following1:
+      title: "És el meu primer seguiment"
+      description: "És la primera vegada que et segueixo"
+    _following10:
+      title: "Segueix-me... Segueix-me..."
+    _open3windows:
+      title: "Multi finestres"
+      description: "I va obrir més de tres finestres"
+    _driveFolderCircularReference:
+      title: "Consulteu la secció de bucle"
 _role:
   assignTarget: "Assignar "
   priority: "Prioritat"
@@ -1274,6 +1473,7 @@ _webhookSettings:
 _moderationLogTypes:
   suspend: "Suspèn"
   resetPassword: "Restableix la contrasenya"
+  createInvitation: "Crear codi d'invitació "
 _reversi:
   total: "Total"
 
diff --git a/locales/de-DE.yml b/locales/de-DE.yml
index 0cbd3db0aa..909ee5d45c 100644
--- a/locales/de-DE.yml
+++ b/locales/de-DE.yml
@@ -122,7 +122,10 @@ add: "Hinzufügen"
 reaction: "Reaktionen"
 reactions: "Reaktionen"
 emojiPicker: "Emoji auswählen"
-pinnedEmojisForReactionSettingDescription: "Wähle die Emojis aus, um sie an zu pinnen"
+pinnedEmojisForReactionSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie beim Reagieren als Erstes anzuzeigen."
+pinnedEmojisSettingDescription: "Lege Emojis fest, die angepinnt werden sollen, um sie in der Emoji-Auswahl als Erstes anzuzeigen"
+overwriteFromPinnedEmojisForReaction: "Überschreiben mit den Reaktions-Einstellungen"
+overwriteFromPinnedEmojis: "Überschreiben mit den allgemeinen Einstellungen"
 reactionSettingDescription2: "Ziehe um Anzuordnen, klicke um zu löschen, drücke „+“ um hinzuzufügen"
 rememberNoteVisibility: "Notizsichtbarkeit merken"
 attachCancel: "Anhang entfernen"
@@ -181,7 +184,7 @@ searchWith: "Suchen: {q}"
 youHaveNoLists: "Du hast keine Listen"
 followConfirm: "Möchtest du {name} wirklich folgen?"
 proxyAccount: "Proxy-Benutzerkonto"
-proxyAccountDescription: "Ein Proxy-Benutzerkonto ist ein Benutzerkonto, das sich für Nutzer unter bestimmten Konditionen wie ein Follower aus einer fremden Instanz verhält. Zum Beispiel wird die Aktivität eines Nutzers aus einer fremden Instanz nicht an diese Instanz übermittelt, falls es keinen Benutzer dieser Instanz gibt, der diesem Nutzer aus fremder Instanz folgt. In diesem Fall folgt stattdessen das Proxy-Benutzerkonto."
+proxyAccountDescription: "Ein Proxy-Konto ist ein Benutzerkonto, das unter bestimmten Bedingungen als Follower für Benutzer fremder Instanzen fungiert. Wenn zum Beispiel ein Benutzer einen Benutzer einer fremden Instanz zu einer Liste hinzufügt, werden die Aktivitäten des entfernten Benutzers nicht an die Instanz übermittelt, wenn kein lokaler Benutzer diesem Benutzer folgt; stattdessen folgt das Proxy-Konto."
 host: "Hostname"
 selectUser: "Benutzer auswählen"
 recipient: "Empfänger"
@@ -263,6 +266,7 @@ removed: "Erfolgreich gelöscht"
 removeAreYouSure: "Möchtest du „{x}“ wirklich entfernen?"
 deleteAreYouSure: "Möchtest du „{x}“ wirklich löschen?"
 resetAreYouSure: "Wirklich zurücksetzen?"
+areYouSure: "Bist du sicher?"
 saved: "Erfolgreich gespeichert"
 messaging: "Chat"
 upload: "Hochladen"
@@ -357,7 +361,7 @@ enableLocalTimeline: "Lokale Chronik aktivieren"
 enableGlobalTimeline: "Globale Chronik aktivieren"
 disablingTimelinesInfo: "Administratoren und Moderatoren haben immer Zugriff auf alle Chroniken, auch wenn diese deaktiviert sind."
 registration: "Registrieren"
-enableRegistration: "Registration neuer Benutzer erlauben"
+enableRegistration: "Registrierung neuer Benutzer erlauben"
 invite: "Einladen"
 driveCapacityPerLocalAccount: "Drive-Kapazität pro lokalem Benutzerkonto"
 driveCapacityPerRemoteAccount: "Drive-Kapazität pro Benutzer fremder Instanzen"
@@ -375,8 +379,11 @@ hcaptcha: "hCaptcha"
 enableHcaptcha: "hCaptcha aktivieren"
 hcaptchaSiteKey: "Site key"
 hcaptchaSecretKey: "Secret key"
+mcaptcha: "mCaptcha"
+enableMcaptcha: "mCaptcha aktivieren"
 mcaptchaSiteKey: "Site key"
 mcaptchaSecretKey: "Secret key"
+mcaptchaInstanceUrl: "mCaptcha Instanz-URL"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "reCAPTCHA aktivieren"
 recaptchaSiteKey: "Site key"
@@ -434,7 +441,7 @@ lastUsed: "Zuletzt benutzt"
 lastUsedAt: "Zuletzt verwendet: {t}"
 unregister: "Deaktivieren"
 passwordLessLogin: "Passwortloses Anmelden"
-passwordLessLoginDescription: "Ermöglicht passwortfreies Einloggen, nur via Security-Token oder Passkey"
+passwordLessLoginDescription: "Ermöglicht passwortloses Einloggen mit einem Security-Token oder Passkey"
 resetPassword: "Passwort zurücksetzen"
 newPasswordIs: "Das neue Passwort ist „{password}“"
 reduceUiAnimation: "Animationen der Benutzeroberfläche reduzieren"
@@ -1174,6 +1181,9 @@ signupPendingError: "Beim Überprüfen der Mailadresse ist etwas schiefgelaufen.
 cwNotationRequired: "Ist \"Inhaltswarnung verwenden\" aktiviert, muss eine Beschreibung gegeben werden."
 doReaction: "Reagieren"
 code: "Code"
+decorate: "Dekorieren"
+addMfmFunction: "MFM hinzufügen"
+sfx: "Soundeffekte"
 lastNDays: "Letzten {n} Tage"
 _announcement:
   forExistingUsers: "Nur für existierende Nutzer"
@@ -1184,6 +1194,7 @@ _announcement:
   tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren."
   readConfirmTitle: "Als gelesen markieren?"
   readConfirmText: "Dies markiert den Inhalt von \"{title}\" als gelesen."
+  shouldNotBeUsedToPresentPermanentInfo: "Es wird empfohlen, Ankündigungen für aktuelle und zeitlich begrenzte Neuigkeiten zu nutzen, statt für Informationen, die langfristig relevant sind."
   dialogAnnouncementUxWarn: "Bei der Verwendung von mehr als zwei Meldungen im Dialog-Format wird um Vorsicht geboten, da dies negative Auswirkungen auf die UX haben kann."
   silence: "Keine Benachrichtigung"
   silenceDescription: "Wenn aktiviert, gibt diese Meldung keine Nachricht aus und muss nicht als \"gelesen\" markiert werden."
@@ -1213,6 +1224,24 @@ _initialTutorial:
     description: "Hier kannst du sehen, wie Misskey funktioniert"
   _note:
     title: "Was sind Notizen?"
+    description: "Beiträge auf Misskey heißen \"Notizen\". Notizen werden chronologisch in der Chronik angeordnet und in Echtzeit aktualisiert."
+    reply: "Klicke auf diesen Button, um auf eine Nachricht zu antworten. Es ist auch möglich, auf Antworten zu antworten und die Unterhaltung wie einen Thread fortzusetzen."
+  _reaction:
+    title: "Was sind Reaktionen?"
+    reactToContinue: "Füge eine Reaktion hinzu, um fortzufahren."
+    reactNotification: "Du erhältst Echtzeit-Benachrichtigungen, wenn jemand auf deine Notiz reagiert."
+  _postNote:
+    _visibility:
+      description: "Du kannst einschränken, wer deine Notiz sehen kann."
+      public: "Deine Notiz wird für alle Nutzer sichtbar sein."
+      doNotSendConfidencialOnDirect1: "Sei vorsichtig, wenn du sensible Informationen verschickst!"
+    _cw:
+      title: "Inhaltswarnung"
+  _done:
+    title: "Du hast das Tutorial abgeschlossen! 🎉"
+_timelineDescription:
+  local: "In der lokalen Chronik siehst du Notizen von allen Benutzern auf diesem Server."
+  global: "In der globalen Chronik siehst du Notizen von allen föderierten Servern."
 _serverRules:
   description: "Eine Reihe von Regeln, die vor der Registrierung angezeigt werden. Eine Zusammenfassung der Nutzungsbedingungen anzuzeigen ist empfohlen."
 _serverSettings:
@@ -1225,6 +1254,8 @@ _serverSettings:
   shortName: "Abkürzung"
   shortNameDescription: "Ein Kürzel für den Namen der Instanz, der angezeigt werden kann, falls der volle Instanzname lang ist."
   fanoutTimelineDescription: "Ist diese Option aktiviert, kann eine erhebliche Verbesserung im Abrufen von Chroniken und eine Reduzierung der Datenbankbelastung erzielt werden, im Gegenzug zu einer Steigerung in der Speichernutzung von Redis. Bei geringem Serverspeicher oder Serverinstabilität kann diese Option deaktiviert werden."
+  fanoutTimelineDbFallback: "Auf die Datenbank zurückfallen"
+  fanoutTimelineDbFallbackDescription: "Ist diese Option aktiviert, wird die Chronik auf zusätzliche Abfragen in der Datenbank zurückgreifen, wenn sich die Chronik nicht im Cache befindet. Eine Deaktivierung führt zu geringerer Serverlast, aber schränkt den Zeitraum der abrufbaren Chronik ein. "
 _accountMigration:
   moveFrom: "Von einem anderen Konto zu diesem migrieren"
   moveFromSub: "Alias für ein anderes Konto erstellen"
@@ -1482,6 +1513,8 @@ _achievements:
     _smashTestNotificationButton:
       title: "Testüberfluss"
       description: "Betätige den Benachrichtigungstest mehrfach innerhalb einer extrem kurzen Zeitspanne"
+    _tutorialCompleted:
+      description: "Tutorial abgeschlossen"
 _role:
   new: "Rolle erstellen"
   edit: "Rolle bearbeiten"
@@ -1492,7 +1525,9 @@ _role:
   assignTarget: "Zuweisungsart"
   descriptionOfAssignTarget: "<b>Manuell</b> bedeutet, dass die Liste der Benutzer einer Rolle manuell verwaltet wird.\n<b>Konditional</b> bedeutet, dass die Liste der Benutzer einer Rolle durch eine Bedingung automatisch verwaltet wird."
   manual: "Manuell"
+  manualRoles: "Manuelle Rollen"
   conditional: "Konditional"
+  conditionalRoles: "Bedingte Rolle"
   condition: "Bedingung"
   isConditionalRole: "Dies ist eine konditionale Rolle."
   isPublic: "Öffentliche Rolle"
@@ -1534,13 +1569,14 @@ _role:
     webhookMax: "Maximale Anzahl an Webhooks"
     clipMax: "Maximale Anzahl an Clips"
     noteEachClipsMax: "Maximale Anzahl an Notizen innerhalb eines Clips"
-    userListMax: "Maximale Anzahl an Benutzern in einer Benutzerliste"
-    userEachUserListsMax: "Maximale Anzahl an Benutzerlisten"
+    userListMax: "Maximale Anzahl an Benutzerlisten"
+    userEachUserListsMax: "Maximale Anzahl an Benutzern in einer Benutzerliste"
     rateLimitFactor: "Versuchsanzahl"
     descriptionOfRateLimitFactor: "Je niedriger desto weniger restriktiv, je höher destro restriktiver."
     canHideAds: "Kann Werbung ausblenden"
     canSearchNotes: "Nutzung der Notizsuchfunktion"
     canUseTranslator: "Verwendung des Übersetzers"
+    avatarDecorationLimit: "Maximale Anzahl an Profilbilddekorationen, die angebracht werden können"
   _condition:
     isLocal: "Lokaler Benutzer"
     isRemote: "Benutzer fremder Instanz"
@@ -1569,6 +1605,7 @@ _emailUnavailable:
   disposable: "Wegwerf-Email-Adressen können nicht verwendet werden"
   mx: "Dieser Email-Server ist ungültig"
   smtp: "Dieser Email-Server antwortet nicht"
+  banned: "Du kannst dich mit dieser E-Mail-Adresse nicht registrieren"
 _ffVisibility:
   public: "Öffentlich"
   followers: "Nur für Follower sichtbar"
@@ -1897,6 +1934,7 @@ _widgets:
   _userList:
     chooseList: "Liste auswählen"
   clicker: "Klickzähler"
+  birthdayFollowings: "Nutzer, die heute Geburtstag haben"
 _cw:
   hide: "Inhalt verbergen"
   show: "Inhalt anzeigen"
@@ -2246,5 +2284,9 @@ _externalResourceInstaller:
       title: "Das Farbschema konnte nicht installiert werden"
       description: "Während der Installation des Farbschemas ist ein Problem aufgetreten. Bitte versuche es erneut. Detaillierte Fehlerinformationen können über die Javascript-Konsole abgerufen werden."
 _reversi:
+  blackOrWhite: "Schwarz/Weiß"
+  rules: "Regeln"
+  black: "Schwarz"
+  white: "Weiß"
   total: "Gesamt"
 
diff --git a/locales/en-US.yml b/locales/en-US.yml
index a97e0e69e9..4c859861f7 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -391,8 +391,11 @@ hcaptcha: "hCaptcha"
 enableHcaptcha: "Enable hCaptcha"
 hcaptchaSiteKey: "Site key"
 hcaptchaSecretKey: "Secret key"
+mcaptcha: "mCaptcha"
+enableMcaptcha: "Enable mCaptcha"
 mcaptchaSiteKey: "Site key"
 mcaptchaSecretKey: "Secret key"
+mcaptchaInstanceUrl: "mCaptcha instance URL"
 recaptcha: "reCAPTCHA"
 enableRecaptcha: "Enable reCAPTCHA"
 recaptchaSiteKey: "Site key"
@@ -645,6 +648,7 @@ medium: "Medium"
 small: "Small"
 generateAccessToken: "Generate access token"
 permission: "Permissions"
+adminPermission: "Admin Permissions"
 enableAll: "Enable all"
 disableAll: "Disable all"
 tokenRequested: "Grant access to account"
@@ -661,7 +665,7 @@ smtpHost: "Host"
 smtpPort: "Port"
 smtpUser: "Username"
 smtpPass: "Password"
-emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP verification"
+emptyToDisableSmtpAuth: "Leave username and password empty to disable SMTP authentication"
 smtpSecure: "Use implicit SSL/TLS for SMTP connections"
 smtpSecureInfo: "Turn this off when using STARTTLS"
 testEmail: "Test email delivery"
@@ -688,6 +692,7 @@ useGlobalSettingDesc: "If turned on, your account's notification settings will b
 other: "Other"
 regenerateLoginToken: "Regenerate login token"
 regenerateLoginTokenDescription: "Regenerates the token used internally during login. Normally this action is not necessary. If regenerated, all devices will be logged out."
+theKeywordWhenSearchingForCustomEmoji: "This is the keyword when searching for custom emojis."
 setMultipleBySeparatingWithSpace: "Separate multiple entries with spaces."
 fileIdOrUrl: "File ID or URL"
 behavior: "Behavior"
@@ -1091,6 +1096,8 @@ limitWidthOfReaction: "Limits the maximum width of reactions and display them in
 noteIdOrUrl: "Note ID or URL"
 video: "Video"
 videos: "Videos"
+audio: "Audio"
+audioFiles: "Audio"
 dataSaver: "Data Saver"
 accountMigration: "Account Migration"
 accountMoved: "This user has moved to a new account:"
@@ -1212,7 +1219,7 @@ donationUrl: "Donation URL"
 avatarDecorations: "Avatar decorations"
 attach: "Attach"
 detach: "Remove"
-detachAll: "Remove all"
+detachAll: "Remove All"
 angle: "Angle"
 flip: "Flip"
 showAvatarDecorations: "Show avatar decorations"
@@ -1234,8 +1241,23 @@ addMfmFunction: "Add MFM"
 enableQuickAddMfmFunction: "Show advanced MFM picker"
 bubbleGame: "Bubble Game"
 sfx: "Sound Effects"
+soundWillBePlayed: "Sound will be played"
+showReplay: "View Replay"
 replay: "Replay"
+replaying: "Showing replay"
+ranking: "Ranking"
 lastNDays: "Last {n} days"
+backToTitle: "Go back to title"
+hemisphere: "Where are you located"
+withSensitive: "Include notes with sensitive files"
+userSaysSomethingSensitive: "Post by {name} contains sensitive content"
+enableHorizontalSwipe: "Swipe to switch tabs"
+_bubbleGame:
+  howToPlay: "How to play"
+  _howToPlay:
+    section1: "Adjust the position and drop the object into the box."
+    section2: "When two objects of the same type touch each other, they will change into a different object and you score points."
+    section3: "The game is over when objects overflow from the box. Aim for a high score by fusing objects together while you avoid overflowing the box!"
 _announcement:
   forExistingUsers: "Existing users only"
   forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@@ -1245,7 +1267,7 @@ _announcement:
   tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete."
   readConfirmTitle: "Mark as read?"
   readConfirmText: "This will mark the contents of \"{title}\" as read."
-  shouldNotBeUsedToPresentPermanentInfo: "As it may significantly impact the user experience for new users, it is recommended to use notifications in the flow information rather than stock information."
+  shouldNotBeUsedToPresentPermanentInfo: "It's best to use announcements to publish fresh and time-bound information, not for information that will be relevant in the long term."
   dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully."
   silence: "No notification"
   silenceDescription: "Turning this on will skip the notification of this announcement and the user won't need to read it."
@@ -1608,8 +1630,11 @@ _achievements:
       description: "Tutorial completed"
     _bubbleGameExplodingHead:
       title: "🤯"
+      description: "The biggest object in the bubble game"
     _bubbleGameDoubleExplodingHead:
       title: "Double🤯"
+      description: "Two of the biggest objects in the bubble game at the same time"
+      flavor: "You can fill a lunch box like this 🤯 🤯 a bit."
 _role:
   new: "New role"
   edit: "Edit role"
@@ -2562,6 +2587,53 @@ _dataSaver:
   _code:
     title: "Code highlighting"
     description: "If code highlighting notations are used in MFM, etc., they will not load until tapped. Syntax highlighting requires downloading the highlight definition files for each programming language. Therefore, disabling the automatic loading of these files is expected to reduce the amount of communication data."
+_hemisphere:
+  N: "Northern Hemisphere"
+  S: "Southern Hemisphere"
+  caption: "Used in some client settings to determine season."
 _reversi:
+  reversi: "Reversi"
+  gameSettings: "Game settings"
+  chooseBoard: "Choose a board"
+  blackOrWhite: "Black/White"
+  blackIs: "{name} is playing Black"
+  rules: "Rules"
+  thisGameIsStartedSoon: "The game will begin shortly"
+  waitingForOther: "Waiting for opponent's turn"
+  waitingForMe: "Waiting for your turn"
+  waitingBoth: "Get ready"
+  ready: "Ready"
+  cancelReady: "Not ready"
+  opponentTurn: "Opponent's turn"
+  myTurn: "Your turn"
+  turnOf: "It's {name}'s turn"
+  pastTurnOf: "{name}'s turn"
+  surrender: "Surrender"
+  surrendered: "Surrendered"
+  timeout: "Out of time"
+  drawn: "Draw"
+  won: "{name} wins"
+  black: "Black"
+  white: "White"
   total: "Total"
+  turnCount: "Turn {count}"
+  myGames: "My rounds"
+  allGames: "All rounds"
+  ended: "Ended"
+  playing: "Currently playing"
+  isLlotheo: "The one with fewer stones wins (Llotheo)"
+  loopedMap: "Looping map"
+  canPutEverywhere: "Tiles are placeable everywhere"
+  timeLimitForEachTurn: "Time limit for turn"
+  freeMatch: "Free Match"
+  lookingForPlayer: "Finding opponent..."
+  gameCanceled: "The game has been cancelled."
+  shareToTlTheGameWhenStart: "Share Game to timeline when started"
+  iStartedAGame: "The game has begun! #MisskeyReversi"
+  opponentHasSettingsChanged: "The opponent has changed their settings."
+  allowIrregularRules: "Irregular rules (completely free)"
+  disallowIrregularRules: "No irregular rules"
+_offlineScreen:
+  title: "Offline - cannot connect to the server"
+  header: "Unable to connect to the server"
 
diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml
index c38616d221..ab6cb115d3 100644
--- a/locales/fr-FR.yml
+++ b/locales/fr-FR.yml
@@ -399,7 +399,7 @@ antennaKeywords: "Mots clés à recevoir"
 antennaExcludeKeywords: "Mots clés à exclure"
 antennaKeywordsDescription: "Séparer avec des espaces pour la condition AND. Séparer avec un saut de ligne pour une condition OR."
 notifyAntenna: "Me notifier pour les nouvelles notes"
-withFileAntenna: "Notes ayant des attachements uniquement"
+withFileAntenna: "Notes ayant des fichiers joints uniquement"
 enableServiceworker: "Activer ServiceWorker"
 antennaUsersDescription: "Saisissez un seul nom d’utilisateur·rice par ligne"
 caseSensitive: "Sensible à la casse"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index bb76d611ab..686fa57cd5 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5079,7 +5079,7 @@ export interface Locale extends ILocale {
          */
         "readConfirmText": ParameterizedString<"title">;
         /**
-         * 特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。
+         * 特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。
          */
         "shouldNotBeUsedToPresentPermanentInfo": string;
         /**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ca12b3cd54..f6879acda1 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1269,7 +1269,7 @@ _announcement:
   tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。"
   readConfirmTitle: "既読にしますか?"
   readConfirmText: "「{title}」の内容を読み、既読にします。"
-  shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、ストック情報ではなくフロー情報の掲示にお知らせを使用することを推奨します。"
+  shouldNotBeUsedToPresentPermanentInfo: "特に新規ユーザーのUXを損ねる可能性が高いため、常時掲示するための情報ではなく、即時性が求められる情報の掲示のためにお知らせを使用することを推奨します。"
   dialogAnnouncementUxWarn: "ダイアログ形式のお知らせが同時に2つ以上ある場合、UXに悪影響を及ぼす可能性が非常に高いため、使用は慎重に行うことを推奨します。"
   silence: "非通知"
   silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml
index 4b32ff359e..5d5b175e0c 100644
--- a/locales/ja-KS.yml
+++ b/locales/ja-KS.yml
@@ -2035,9 +2035,9 @@ _auth:
 _antennaSources:
   all: "みんなのノート"
   homeTimeline: "フォローしとるユーザーのノート"
-  users: "選らんだ一人か複数のユーザーのノート"
+  users: "選んだ一人か複数のユーザーのノート"
   userList: "選んだリストのユーザーのノート"
-  userBlacklist: "選んだ1人か複数のユーザーのノート"
+  userBlacklist: "選んだ一人か複数のユーザーを除いた全てのノート"
 _weekday:
   sunday: "日曜日"
   monday: "月曜日"
@@ -2487,6 +2487,8 @@ _reversi:
   shareToTlTheGameWhenStart: "初めの時に対局をタイムラインに投稿するで"
   iStartedAGame: "対局し始めたで! #MisskeyReversi"
   opponentHasSettingsChanged: "相手が設定変えたで"
+  allowIrregularRules: "変則許可 (完全フリー)"
+  disallowIrregularRules: "変則なし"
 _offlineScreen:
   title: "オフライン - サーバーに接続できひんで"
   header: "サーバーに接続できへんわ"
diff --git a/locales/ko-GS.yml b/locales/ko-GS.yml
index 1d6c6f7bbc..29bfe5394d 100644
--- a/locales/ko-GS.yml
+++ b/locales/ko-GS.yml
@@ -515,7 +515,7 @@ objectStoragePrefixDesc: "요 Prefix 디렉토리 안에다가 파일이 들어
 objectStorageEndpoint: "Endpoint"
 objectStorageEndpointDesc: "AWS S3을 쓸라멘 요는 비워두고, 아이멘은 그 서비스 가이드에 맞게 endpoint를 넣어 주이소. '<host>' 내지 '<host>:<port>'처럼 넣십니다."
 objectStorageRegion: "Region"
-objectStorageRegionDesc: "'xx-east-1' 같은 region 이름을 옇어 주이소. 써먹을 서비스에 region 개념 같은 게 읎다! 카면은 대신에 'us-east-1'을 옇어 놓으이소. AWS 설정 파일이나 환경 변수를 갖다 끌어다 쓸 거면은 요는 비워 두이소."
+objectStorageRegionDesc: "'xx-east-1' 같은 region 이름을 옇어 주이소. 만약에 내 서비스엔 region 같은 개념이 읎다, 카면은 대신에 'us-east-1'라고 해 두이소. AWS 설정 파일이나 환경 변수를 끌어다 쓰겠다믄 요는 비워 두이소."
 objectStorageUseSSL: "SSL 쓰기"
 objectStorageUseSSLDesc: "API 호출할 때 HTTPS 안 쓸거면은 꺼 두이소"
 objectStorageUseProxy: "연결에 프락시 사용"
@@ -538,7 +538,7 @@ volume: "음량"
 masterVolume: "대빵 음량"
 notUseSound: "음소거하기"
 useSoundOnlyWhenActive: "Misskey가 활성화되어 있을 때만 소리 내기"
-details: "좀 더"
+details: "자세히"
 chooseEmoji: "이모지 선택"
 unableToProcess: "작업 다 몬 했십니다"
 recentUsed: "최근 쓴 놈"
diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml
index de4cb3395b..718d04caae 100644
--- a/locales/ko-KR.yml
+++ b/locales/ko-KR.yml
@@ -2482,6 +2482,11 @@ _reversi:
   freeMatch: "프리매치"
   lookingForPlayer: "상대를 찾고 있습니다"
   gameCanceled: "대국이 취소되었습니다"
+  shareToTlTheGameWhenStart: "대국 시작 시 타임라인에 대국을 게시"
+  iStartedAGame: "대국이 시작되었습니다! #MisskeyReversi"
+  opponentHasSettingsChanged: "상대방이 설정을 변경했습니다"
+  allowIrregularRules: "규칙변경 허가 (완전 자유)"
+  disallowIrregularRules: "규칙변경 없음"
 _offlineScreen:
   title: "오프라인 - 서버에 접속할 수 없습니다"
   header: "서버에 접속할 수 없습니다"
diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml
index d14a77f5aa..09c210011c 100644
--- a/locales/zh-CN.yml
+++ b/locales/zh-CN.yml
@@ -1185,6 +1185,7 @@ useGroupedNotifications: "分组显示通知"
 signupPendingError: "确认电子邮件时出现错误。链接可能已过期。"
 cwNotationRequired: "在启用「隐藏内容」时必须输入注释"
 doReaction: "回应"
+code: "代码"
 reloadRequiredToApplySettings: "需要重新载入来使设置生效"
 remainingN: "剩余:{n}"
 overwriteContentConfirm: "将覆盖现有内容。确定吗?"
@@ -1202,9 +1203,14 @@ lastNDays: "最近 {n} 天"
 backToTitle: "返回标题"
 hemisphere: "居住地区"
 withSensitive: "显示包含敏感媒体的帖子"
+userSaysSomethingSensitive: "含 {name} 敏感文件的帖子"
 enableHorizontalSwipe: "滑动切换标签页"
 _bubbleGame:
   howToPlay: "游戏说明"
+  _howToPlay:
+    section1: "对准位置将Emoji投入盒子。"
+    section2: "相同的Emoji相互接触合成后会得到新的Emoji,以此获得分数。"
+    section3: "如果Emoji从箱子中溢出游戏将会结束。在防止Emoji溢出的同时,不断合成新的Emoji,来获取更高的分数吧!"
 _announcement:
   forExistingUsers: "仅限现有用户"
   forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
@@ -1577,6 +1583,10 @@ _achievements:
       description: "完成了教学"
     _bubbleGameExplodingHead:
       title: "🤯"
+      description: "你合成出了游戏里最大的Emoji"
+    _bubbleGameDoubleExplodingHead:
+      title: "两个🤯"
+      description: "你合成出了2个游戏里最大的Emoji"
 _role:
   new: "创建角色"
   edit: "编辑角色"
@@ -2435,7 +2445,41 @@ _hemisphere:
   caption: "在某些客户端设置中用来确定季节"
 _reversi:
   reversi: "黑白棋"
+  gameSettings: "对局设置"
+  blackOrWhite: "先手/后手"
+  blackIs: "{name}执黑(先手)"
+  rules: "规则"
+  thisGameIsStartedSoon: "对局即将开始"
+  waitingForOther: "等待对手准备"
+  waitingForMe: "等待你的准备"
+  waitingBoth: "请准备"
+  ready: "准备就绪"
+  cancelReady: "重新准备"
+  opponentTurn: "对手的回合"
+  myTurn: "你的回合"
+  turnOf: "{name}的回合"
+  pastTurnOf: "{name}的回合"
+  timeout: "超时"
+  drawn: "平局"
+  won: "{name}获胜"
+  black: "黑"
+  white: "白"
   total: "总计"
+  turnCount: "第{count}回合"
+  myGames: "我的对局"
+  allGames: "所有对局"
+  ended: "结束"
+  playing: "对局中"
+  canPutEverywhere: "无限制放置模式"
+  timeLimitForEachTurn: "1回合的时间限制"
+  freeMatch: "自由匹配"
+  lookingForPlayer: "正在寻找对手"
+  gameCanceled: "对局被取消了"
+  shareToTlTheGameWhenStart: "开始时在时间线发布对局"
+  iStartedAGame: "对局开始!#MisskeyReversi"
+  opponentHasSettingsChanged: "对手更改了设定"
+  allowIrregularRules: "允许非常规规则(完全自由)"
+  disallowIrregularRules: "禁止非常规规则"
 _offlineScreen:
   title: "离线——无法连接到服务器"
   header: "无法连接到服务器"
diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml
index 59f2ed6d2d..872a90bc6a 100644
--- a/locales/zh-TW.yml
+++ b/locales/zh-TW.yml
@@ -1221,7 +1221,7 @@ _announcement:
   tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。"
   readConfirmTitle: "標記為已讀嗎?"
   readConfirmText: "閱讀「{title}」的內容並標記為已讀。"
-  shouldNotBeUsedToPresentPermanentInfo: "由於可能會破壞使用者體驗,尤其是對於新使用者而言,我們建議使用公告來發布有時效性的資訊而不是固定不變的資訊。"
+  shouldNotBeUsedToPresentPermanentInfo: "為了避免損害新用戶的使用體驗,建議使用公告來發布即時性的訊息,而不是用於固定不變的資訊。"
   dialogAnnouncementUxWarn: "如果同時有 2 個以上對話方塊形式的公告存在,對於使用者體驗很可能會有不良的影響,因此建議謹慎使用。"
   silence: "不發送通知"
   silenceDescription: "啟用此選項後,將不會發送此公告的通知,並且無需將其標記為已讀。"
diff --git a/package.json b/package.json
index 487a963558..7af66b8e37 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "sharkey",
-	"version": "2024.2.0-beta2",
+	"version": "2024.2.0-beta.9",
 	"codename": "shonk",
 	"repository": {
 		"type": "git",
diff --git a/packages/backend/generate_api_json.js b/packages/backend/generate_api_json.js
index 5819c60a5f..4079b3bb0a 100644
--- a/packages/backend/generate_api_json.js
+++ b/packages/backend/generate_api_json.js
@@ -3,6 +3,6 @@ import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
 import { writeFileSync } from "node:fs";
 
 const config = loadConfig();
-const spec = genOpenapiSpec(config);
+const spec = genOpenapiSpec(config, true);
 
 writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
\ No newline at end of file
diff --git a/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js b/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js
new file mode 100644
index 0000000000..05469d5765
--- /dev/null
+++ b/packages/backend/migration/1706791962000-fix-meta-disableRegistration.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class FixMetaDisableRegistration1706791962000 {
+    name = 'FixMetaDisableRegistration1706791962000'
+
+    async up(queryRunner) {
+        await queryRunner.query(`alter table meta alter column "disableRegistration" set default true;`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`alter table meta alter column "disableRegistration" set default false;`);
+    }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index f4865b3d05..296eddfe3c 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -65,9 +65,9 @@
 	"dependencies": {
 		"@aws-sdk/client-s3": "3.412.0",
 		"@aws-sdk/lib-storage": "3.412.0",
-		"@bull-board/api": "5.10.2",
-		"@bull-board/fastify": "5.10.2",
-		"@bull-board/ui": "5.10.2",
+		"@bull-board/api": "5.14.0",
+		"@bull-board/fastify": "5.14.0",
+		"@bull-board/ui": "5.14.0",
 		"@discordapp/twemoji": "15.0.2",
 		"@fastify/accepts": "4.3.0",
 		"@fastify/cookie": "9.3.1",
@@ -84,11 +84,11 @@
 		"@nestjs/testing": "10.2.10",
 		"@peertube/http-signature": "1.7.0",
 		"@transfem-org/sfm-js": "0.24.4",
-		"@simplewebauthn/server": "9.0.0",
+		"@simplewebauthn/server": "9.0.1",
 		"@sinonjs/fake-timers": "11.2.2",
 		"@smithy/node-http-handler": "2.1.10",
 		"@swc/cli": "0.1.63",
-		"@swc/core": "1.3.105",
+		"@swc/core": "1.3.107",
 		"@twemoji/parser": "15.0.0",
 		"accepts": "1.3.8",
 		"ajv": "8.12.0",
@@ -98,7 +98,7 @@
 		"bcryptjs": "2.4.3",
 		"blurhash": "2.0.5",
 		"body-parser": "1.20.2",
-		"bullmq": "5.1.4",
+		"bullmq": "5.1.5",
 		"cacheable-lookup": "7.0.0",
 		"cbor": "9.0.1",
 		"chalk": "5.3.0",
@@ -117,7 +117,7 @@
 		"fluent-ffmpeg": "2.1.2",
 		"form-data": "4.0.0",
 		"glob": "10.3.10",
-		"got": "14.0.0",
+		"got": "14.1.0",
 		"happy-dom": "10.0.3",
 		"hpagent": "1.2.0",
 		"http-link-header": "1.1.1",
@@ -148,7 +148,7 @@
 		"otpauth": "9.2.2",
 		"parse5": "7.1.2",
 		"pg": "8.11.3",
-		"pkce-challenge": "4.0.1",
+		"pkce-challenge": "4.1.0",
 		"probe-image-size": "7.2.3",
 		"promise-limit": "2.7.0",
 		"pug": "3.0.2",
@@ -169,12 +169,12 @@
 		"slacc": "0.0.10",
 		"strict-event-emitter-types": "2.0.0",
 		"stringz": "2.1.0",
-		"systeminformation": "5.21.23",
+		"systeminformation": "5.21.24",
 		"tinycolor2": "1.6.0",
 		"tmp": "0.2.1",
 		"tsc-alias": "1.8.8",
 		"tsconfig-paths": "4.2.0",
-		"typeorm": "0.3.19",
+		"typeorm": "0.3.20",
 		"typescript": "5.3.3",
 		"ulid": "2.3.0",
 		"uuid": "^9.0.1",
@@ -186,7 +186,7 @@
 	"devDependencies": {
 		"@jest/globals": "29.7.0",
 		"@misskey-dev/eslint-plugin": "1.0.0",
-		"@nestjs/platform-express": "10.3.0",
+		"@nestjs/platform-express": "10.3.1",
 		"@simplewebauthn/typescript-types": "8.3.4",
 		"@swc/jest": "0.2.31",
 		"@types/accepts": "1.3.7",
@@ -205,13 +205,13 @@
 		"@types/jsrsasign": "10.5.12",
 		"@types/mime-types": "2.1.4",
 		"@types/ms": "0.7.34",
-		"@types/node": "20.11.5",
+		"@types/node": "20.11.10",
 		"@types/node-fetch": "3.0.3",
 		"@types/nodemailer": "6.4.14",
 		"@types/oauth": "0.9.4",
 		"@types/oauth2orize": "1.11.3",
 		"@types/oauth2orize-pkce": "0.1.2",
-		"@types/pg": "8.10.9",
+		"@types/pg": "8.11.0",
 		"@types/pug": "2.0.10",
 		"@types/punycode": "2.1.3",
 		"@types/qrcode": "1.5.5",
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 972d1be0ee..38003504bc 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -76,10 +76,10 @@ type Source = {
 
 	deliverJobConcurrency?: number;
 	inboxJobConcurrency?: number;
-	relashionshipJobConcurrency?: number;
+	relationshipJobConcurrency?: number;
 	deliverJobPerSec?: number;
 	inboxJobPerSec?: number;
-	relashionshipJobPerSec?: number;
+	relationshipJobPerSec?: number;
 	deliverJobMaxAttempts?: number;
 	inboxJobMaxAttempts?: number;
 
@@ -141,10 +141,10 @@ export type Config = {
 	outgoingAddressFamily: 'ipv4' | 'ipv6' | 'dual' | undefined;
 	deliverJobConcurrency: number | undefined;
 	inboxJobConcurrency: number | undefined;
-	relashionshipJobConcurrency: number | undefined;
+	relationshipJobConcurrency: number | undefined;
 	deliverJobPerSec: number | undefined;
 	inboxJobPerSec: number | undefined;
-	relashionshipJobPerSec: number | undefined;
+	relationshipJobPerSec: number | undefined;
 	deliverJobMaxAttempts: number | undefined;
 	inboxJobMaxAttempts: number | undefined;
 	proxyRemoteFiles: boolean | undefined;
@@ -257,10 +257,10 @@ export function loadConfig(): Config {
 		outgoingAddressFamily: config.outgoingAddressFamily,
 		deliverJobConcurrency: config.deliverJobConcurrency,
 		inboxJobConcurrency: config.inboxJobConcurrency,
-		relashionshipJobConcurrency: config.relashionshipJobConcurrency,
+		relationshipJobConcurrency: config.relationshipJobConcurrency,
 		deliverJobPerSec: config.deliverJobPerSec,
 		inboxJobPerSec: config.inboxJobPerSec,
-		relashionshipJobPerSec: config.relashionshipJobPerSec,
+		relationshipJobPerSec: config.relationshipJobPerSec,
 		deliverJobMaxAttempts: config.deliverJobMaxAttempts,
 		inboxJobMaxAttempts: config.inboxJobMaxAttempts,
 		proxyRemoteFiles: config.proxyRemoteFiles,
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 350aa6ba24..9d4d9219ef 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -96,7 +96,7 @@ export class AccountMoveService {
 		await this.apDeliverManagerService.deliverToFollowers(src, moveAct);
 
 		// Publish meUpdated event
-		const iObj = await this.userEntityService.pack<true, true>(src.id, src, { detail: true, includeSecrets: true });
+		const iObj = await this.userEntityService.pack(src.id, src, { schema: 'MeDetailed', includeSecrets: true });
 		this.globalEventService.publishMainStream(src.id, 'meUpdated', iObj);
 
 		// Unfollow after 24 hours
diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index dbbe5ecdc0..0b90e88182 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -54,15 +54,15 @@ export interface MainEventTypes {
 	reply: Packed<'Note'>;
 	renote: Packed<'Note'>;
 	follow: Packed<'UserDetailedNotMe'>;
-	followed: Packed<'User'>;
-	unfollow: Packed<'User'>;
-	meUpdated: Packed<'User'>;
+	followed: Packed<'UserLite'>;
+	unfollow: Packed<'UserDetailedNotMe'>;
+	meUpdated: Packed<'MeDetailed'>;
 	pageEvent: {
 		pageId: MiPage['id'];
 		event: string;
 		var: any;
 		userId: MiUser['id'];
-		user: Packed<'User'>;
+		user: Packed<'UserDetailed'>;
 	};
 	urlUploadFinished: {
 		marker?: string | null;
@@ -92,7 +92,7 @@ export interface MainEventTypes {
 	};
 	driveFileCreated: Packed<'DriveFile'>;
 	readAntenna: MiAntenna;
-	receiveFollowRequest: Packed<'User'>;
+	receiveFollowRequest: Packed<'UserLite'>;
 	announcementCreated: {
 		announcement: Packed<'Announcement'>;
 	};
@@ -143,8 +143,8 @@ type NoteStreamEventTypes = {
 };
 
 export interface UserListEventTypes {
-	userAdded: Packed<'User'>;
-	userRemoved: Packed<'User'>;
+	userAdded: Packed<'UserLite'>;
+	userRemoved: Packed<'UserLite'>;
 }
 
 export interface AntennaEventTypes {
diff --git a/packages/backend/src/core/UserBlockingService.ts b/packages/backend/src/core/UserBlockingService.ts
index 39b19325c3..c267849908 100644
--- a/packages/backend/src/core/UserBlockingService.ts
+++ b/packages/backend/src/core/UserBlockingService.ts
@@ -109,13 +109,13 @@ export class UserBlockingService implements OnModuleInit {
 
 		if (this.userEntityService.isLocalUser(followee)) {
 			this.userEntityService.pack(followee, followee, {
-				detail: true,
+				schema: 'MeDetailed',
 			}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
 		}
 
 		if (this.userEntityService.isLocalUser(follower) && !silent) {
 			this.userEntityService.pack(followee, follower, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}).then(async packed => {
 				this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
 
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index d600ffb9d9..93e9fbbd70 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -293,9 +293,9 @@ export class UserFollowingService implements OnModuleInit {
 		if (this.userEntityService.isLocalUser(follower) && !silent) {
 			// Publish follow event
 			this.userEntityService.pack(followee.id, follower, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}).then(async packed => {
-				this.globalEventService.publishMainStream(follower.id, 'follow', packed as Packed<'UserDetailedNotMe'>);
+				this.globalEventService.publishMainStream(follower.id, 'follow', packed);
 
 				const webhooks = (await this.webhookService.getActiveWebhooks()).filter(x => x.userId === follower.id && x.on.includes('follow'));
 				for (const webhook of webhooks) {
@@ -360,7 +360,7 @@ export class UserFollowingService implements OnModuleInit {
 		if (!silent && this.userEntityService.isLocalUser(follower)) {
 			// Publish unfollow event
 			this.userEntityService.pack(followee.id, follower, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}).then(async packed => {
 				this.globalEventService.publishMainStream(follower.id, 'unfollow', packed);
 
@@ -500,7 +500,7 @@ export class UserFollowingService implements OnModuleInit {
 			this.userEntityService.pack(follower.id, followee).then(packed => this.globalEventService.publishMainStream(followee.id, 'receiveFollowRequest', packed));
 
 			this.userEntityService.pack(followee.id, followee, {
-				detail: true,
+				schema: 'MeDetailed',
 			}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
 
 			// 通知を作成
@@ -548,7 +548,7 @@ export class UserFollowingService implements OnModuleInit {
 		});
 
 		this.userEntityService.pack(followee.id, followee, {
-			detail: true,
+			schema: 'MeDetailed',
 		}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
 	}
 
@@ -576,7 +576,7 @@ export class UserFollowingService implements OnModuleInit {
 		}
 
 		this.userEntityService.pack(followee.id, followee, {
-			detail: true,
+			schema: 'MeDetailed',
 		}).then(packed => this.globalEventService.publishMainStream(followee.id, 'meUpdated', packed));
 	}
 
@@ -696,7 +696,7 @@ export class UserFollowingService implements OnModuleInit {
 	@bindThis
 	private async publishUnfollow(followee: Both, follower: Local): Promise<void> {
 		const packedFollowee = await this.userEntityService.pack(followee.id, follower, {
-			detail: true,
+			schema: 'UserDetailedNotMe',
 		});
 
 		this.globalEventService.publishMainStream(follower.id, 'unfollow', packedFollowee);
diff --git a/packages/backend/src/core/chart/core.ts b/packages/backend/src/core/chart/core.ts
index 8d0a89f2d6..b1cde2f6e2 100644
--- a/packages/backend/src/core/chart/core.ts
+++ b/packages/backend/src/core/chart/core.ts
@@ -94,6 +94,29 @@ type ToJsonSchema<S> = {
 };
 
 export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
+	const unflatten = (str: string, parent: Record<string, any>) => {
+		const keys = str.split('.');
+		const key = keys.shift();
+		const nextKey = keys[0];
+
+		if (key == null) return;
+
+		if (parent.properties[key] == null) {
+			parent.properties[key] = nextKey ? {
+				type: 'object',
+				properties: {},
+				required: [],
+			} : {
+				type: 'array',
+				items: {
+					type: 'number',
+				},
+			};
+		}
+
+		if (nextKey) unflatten(keys.join('.'), parent.properties[key] as Record<string, any>);
+	};
+
 	const jsonSchema = {
 		type: 'object',
 		properties: {} as Record<string, unknown>,
@@ -101,10 +124,7 @@ export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatt
 	};
 
 	for (const k in schema) {
-		jsonSchema.properties[k] = {
-			type: 'array',
-			items: { type: 'number' },
-		};
+		unflatten(k, jsonSchema);
 	}
 
 	return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index 97de891ece..e7814161b8 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -38,13 +38,13 @@ export class AbuseUserReportEntityService {
 			targetUserId: report.targetUserId,
 			assigneeId: report.assigneeId,
 			reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}),
 			targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}),
 			assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}) : null,
 			forwarded: report.forwarded,
 		});
diff --git a/packages/backend/src/core/entities/BlockingEntityService.ts b/packages/backend/src/core/entities/BlockingEntityService.ts
index b4760346b7..f5abf67322 100644
--- a/packages/backend/src/core/entities/BlockingEntityService.ts
+++ b/packages/backend/src/core/entities/BlockingEntityService.ts
@@ -37,7 +37,7 @@ export class BlockingEntityService {
 			createdAt: this.idService.parse(blocking.id).date.toISOString(),
 			blockeeId: blocking.blockeeId,
 			blockee: this.userEntityService.pack(blocking.blockeeId, me, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}),
 		});
 	}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index dc335d9975..70faa2b380 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -42,7 +42,7 @@ export class FlashEntityService {
 			createdAt: this.idService.parse(flash.id).date.toISOString(),
 			updatedAt: flash.updatedAt.toISOString(),
 			userId: flash.userId,
-			user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { detail: true } すると無限ループするので注意
+			user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
 			title: flash.title,
 			summary: flash.summary,
 			script: flash.script,
diff --git a/packages/backend/src/core/entities/FollowingEntityService.ts b/packages/backend/src/core/entities/FollowingEntityService.ts
index 52aa979677..f9bc9fa1ab 100644
--- a/packages/backend/src/core/entities/FollowingEntityService.ts
+++ b/packages/backend/src/core/entities/FollowingEntityService.ts
@@ -89,10 +89,10 @@ export class FollowingEntityService {
 			followeeId: following.followeeId,
 			followerId: following.followerId,
 			followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}) : undefined,
 			follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}) : undefined,
 		});
 	}
diff --git a/packages/backend/src/core/entities/ModerationLogEntityService.ts b/packages/backend/src/core/entities/ModerationLogEntityService.ts
index 6729ca2671..3add9dbc74 100644
--- a/packages/backend/src/core/entities/ModerationLogEntityService.ts
+++ b/packages/backend/src/core/entities/ModerationLogEntityService.ts
@@ -37,7 +37,7 @@ export class ModerationLogEntityService {
 			info: log.info,
 			userId: log.userId,
 			user: this.userEntityService.pack(log.user ?? log.userId, null, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}),
 		});
 	}
diff --git a/packages/backend/src/core/entities/MutingEntityService.ts b/packages/backend/src/core/entities/MutingEntityService.ts
index 9d672169ba..5326955126 100644
--- a/packages/backend/src/core/entities/MutingEntityService.ts
+++ b/packages/backend/src/core/entities/MutingEntityService.ts
@@ -39,7 +39,7 @@ export class MutingEntityService {
 			expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
 			muteeId: muting.muteeId,
 			mutee: this.userEntityService.pack(muting.muteeId, me, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}),
 		});
 	}
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index a5d3054462..1cbd5cb70c 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -181,7 +181,7 @@ export class NoteEntityService implements OnModuleInit {
 
 		return {
 			multiple: poll.multiple,
-			expiresAt: poll.expiresAt,
+			expiresAt: poll.expiresAt?.toISOString() ?? null,
 			choices,
 		};
 	}
@@ -342,9 +342,7 @@ export class NoteEntityService implements OnModuleInit {
 			createdAt: this.idService.parse(note.id).date.toISOString(),
 			updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
 			userId: note.userId,
-			user: this.userEntityService.pack(note.user ?? note.userId, me, {
-				detail: false,
-			}),
+			user: this.userEntityService.pack(note.user ?? note.userId, me),
 			text: text,
 			cw: note.cw,
 			visibility: note.visibility,
diff --git a/packages/backend/src/core/entities/NotificationEntityService.ts b/packages/backend/src/core/entities/NotificationEntityService.ts
index 704081ed00..a620acc2dc 100644
--- a/packages/backend/src/core/entities/NotificationEntityService.ts
+++ b/packages/backend/src/core/entities/NotificationEntityService.ts
@@ -62,7 +62,7 @@ export class NotificationEntityService implements OnModuleInit {
 		},
 		hint?: {
 			packedNotes: Map<MiNote['id'], Packed<'Note'>>;
-			packedUsers: Map<MiUser['id'], Packed<'User'>>;
+			packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
 		},
 	): Promise<Packed<'Notification'>> {
 		const notification = src;
@@ -76,9 +76,7 @@ export class NotificationEntityService implements OnModuleInit {
 		const userIfNeed = 'notifierId' in notification ? (
 			hint?.packedUsers != null
 				? hint.packedUsers.get(notification.notifierId)
-				: this.userEntityService.pack(notification.notifierId, { id: meId }, {
-					detail: false,
-				})
+				: this.userEntityService.pack(notification.notifierId, { id: meId })
 		) : undefined;
 		const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
 
@@ -131,9 +129,7 @@ export class NotificationEntityService implements OnModuleInit {
 		const users = userIds.length > 0 ? await this.usersRepository.find({
 			where: { id: In(userIds) },
 		}) : [];
-		const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
-			detail: false,
-		});
+		const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
 		const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
 
 		// 既に解決されたフォローリクエストの通知を除外
@@ -161,7 +157,7 @@ export class NotificationEntityService implements OnModuleInit {
 		},
 		hint?: {
 			packedNotes: Map<MiNote['id'], Packed<'Note'>>;
-			packedUsers: Map<MiUser['id'], Packed<'User'>>;
+			packedUsers: Map<MiUser['id'], Packed<'UserLite'>>;
 		},
 	): Promise<Packed<'Notification'>> {
 		const notification = src;
@@ -175,18 +171,14 @@ export class NotificationEntityService implements OnModuleInit {
 		const userIfNeed = 'notifierId' in notification ? (
 			hint?.packedUsers != null
 				? hint.packedUsers.get(notification.notifierId)
-				: this.userEntityService.pack(notification.notifierId, { id: meId }, {
-					detail: false,
-				})
+				: this.userEntityService.pack(notification.notifierId, { id: meId })
 		) : undefined;
 
 		if (notification.type === 'reaction:grouped') {
 			const reactions = await Promise.all(notification.reactions.map(async reaction => {
 				const user = hint?.packedUsers != null
 					? hint.packedUsers.get(reaction.userId)!
-					: await this.userEntityService.pack(reaction.userId, { id: meId }, {
-						detail: false,
-					});
+					: await this.userEntityService.pack(reaction.userId, { id: meId });
 				return {
 					user,
 					reaction: reaction.reaction,
@@ -206,9 +198,7 @@ export class NotificationEntityService implements OnModuleInit {
 					return packedUser;
 				}
 
-				return this.userEntityService.pack(userId, { id: meId }, {
-					detail: false,
-				});
+				return this.userEntityService.pack(userId, { id: meId });
 			}));
 			return await awaitAll({
 				id: notification.id,
@@ -275,9 +265,7 @@ export class NotificationEntityService implements OnModuleInit {
 		const users = userIds.length > 0 ? await this.usersRepository.find({
 			where: { id: In(userIds) },
 		}) : [];
-		const packedUsersArray = await this.userEntityService.packMany(users, { id: meId }, {
-			detail: false,
-		});
+		const packedUsersArray = await this.userEntityService.packMany(users, { id: meId });
 		const packedUsers = new Map(packedUsersArray.map(p => [p.id, p]));
 
 		// 既に解決されたフォローリクエストの通知を除外
diff --git a/packages/backend/src/core/entities/PageEntityService.ts b/packages/backend/src/core/entities/PageEntityService.ts
index f39ef949db..bc26362aba 100644
--- a/packages/backend/src/core/entities/PageEntityService.ts
+++ b/packages/backend/src/core/entities/PageEntityService.ts
@@ -90,7 +90,7 @@ export class PageEntityService {
 			createdAt: this.idService.parse(page.id).date.toISOString(),
 			updatedAt: page.updatedAt.toISOString(),
 			userId: page.userId,
-			user: this.userEntityService.pack(page.user ?? page.userId, me), // { detail: true } すると無限ループするので注意
+			user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
 			content: page.content,
 			variables: page.variables,
 			title: page.title,
diff --git a/packages/backend/src/core/entities/RenoteMutingEntityService.ts b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
index 3f9dc9180a..5ad28f2e6a 100644
--- a/packages/backend/src/core/entities/RenoteMutingEntityService.ts
+++ b/packages/backend/src/core/entities/RenoteMutingEntityService.ts
@@ -38,7 +38,7 @@ export class RenoteMutingEntityService {
 			createdAt: this.idService.parse(muting.id).date.toISOString(),
 			muteeId: muting.muteeId,
 			mutee: this.userEntityService.pack(muting.muteeId, me, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			}),
 		});
 	}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index adb7dfbf86..6c1a02d9d8 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -30,14 +30,6 @@ import type { NoteEntityService } from './NoteEntityService.js';
 import type { DriveFileEntityService } from './DriveFileEntityService.js';
 import type { PageEntityService } from './PageEntityService.js';
 
-type IsUserDetailed<Detailed extends boolean> = Detailed extends true ? Packed<'UserDetailed'> : Packed<'UserLite'>;
-type IsMeAndIsUserDetailed<ExpectsMe extends boolean | null, Detailed extends boolean> =
-	Detailed extends true ?
-		ExpectsMe extends true ? Packed<'MeDetailed'> :
-		ExpectsMe extends false ? Packed<'UserDetailedNotMe'> :
-		Packed<'UserDetailed'> :
-	Packed<'UserLite'>;
-
 const Ajv = _Ajv.default;
 const ajv = new Ajv();
 
@@ -304,17 +296,17 @@ export class UserEntityService implements OnModuleInit {
 		return `${this.config.url}/users/${userId}`;
 	}
 
-	public async pack<ExpectsMe extends boolean | null = null, D extends boolean = false>(
+	public async pack<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
 		src: MiUser['id'] | MiUser,
 		me?: { id: MiUser['id']; } | null | undefined,
 		options?: {
-			detail?: D,
+			schema?: S,
 			includeSecrets?: boolean,
 			userProfile?: MiUserProfile,
 		},
-	): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
+	): Promise<Packed<S>> {
 		const opts = Object.assign({
-			detail: false,
+			schema: 'UserLite',
 			includeSecrets: false,
 		}, options);
 
@@ -346,19 +338,20 @@ export class UserEntityService implements OnModuleInit {
 			});
 		}
 
+		const isDetailed = opts.schema !== 'UserLite';
 		const meId = me ? me.id : null;
 		const isMe = meId === user.id;
 		const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
 
-		const relation = meId && !isMe && opts.detail ? await this.getRelation(meId, user.id) : null;
-		const pins = opts.detail ? await this.userNotePiningsRepository.createQueryBuilder('pin')
+		const relation = meId && !isMe && isDetailed ? await this.getRelation(meId, user.id) : null;
+		const pins = isDetailed ? await this.userNotePiningsRepository.createQueryBuilder('pin')
 			.where('pin.userId = :userId', { userId: user.id })
 			.innerJoinAndSelect('pin.note', 'note')
 			.orderBy('pin.id', 'DESC')
 			.getMany() : [];
-		const profile = opts.detail ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
+		const profile = isDetailed ? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id })) : null;
 
-		const mastoapi = !opts.detail ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
+		const mastoapi = !isDetailed ? opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }) : null;
 
 		const followingCount = profile == null ? null :
 			(profile.followingVisibility === 'public') || isMe ? user.followingCount :
@@ -370,17 +363,16 @@ export class UserEntityService implements OnModuleInit {
 			(profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
 			null;
 
-		const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
-		const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
-		const unreadAnnouncements = isMe && opts.detail ?
+		const isModerator = isMe && isDetailed ? this.roleService.isModerator(user) : null;
+		const isAdmin = isMe && isDetailed ? this.roleService.isAdministrator(user) : null;
+		const unreadAnnouncements = isMe && isDetailed ?
 			(await this.announcementService.getUnreadAnnouncements(user)).map((announcement) => ({
 				createdAt: this.idService.parse(announcement.id).date.toISOString(),
 				...announcement,
 			})) : null;
 
 		const checkHost = user.host == null ? this.config.host : user.host;
-		
-		const notificationsInfo = isMe && opts.detail ? await this.getNotificationsInfo(user.id) : null;
+		const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
 
 		const packed = {
 			id: user.id,
@@ -425,7 +417,7 @@ export class UserEntityService implements OnModuleInit {
 				displayOrder: r.displayOrder,
 			}))) : undefined,
 
-			...(opts.detail ? {
+			...(isDetailed ? {
 				url: profile!.url,
 				uri: user.uri,
 				movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
@@ -453,7 +445,7 @@ export class UserEntityService implements OnModuleInit {
 				}),
 				pinnedPageId: profile!.pinnedPageId,
 				pinnedPage: profile!.pinnedPageId ? this.pageEntityService.pack(profile!.pinnedPageId, me) : null,
-				publicReactions: profile!.publicReactions,
+				publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
 				followersVisibility: profile!.followersVisibility,
 				followingVisibility: profile!.followingVisibility,
 				twoFactorEnabled: profile!.twoFactorEnabled,
@@ -480,7 +472,7 @@ export class UserEntityService implements OnModuleInit {
 				moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
 			} : {}),
 
-			...(opts.detail && isMe ? {
+			...(isDetailed && isMe ? {
 				avatarId: user.avatarId,
 				bannerId: user.bannerId,
 				backgroundId: user.backgroundId,
@@ -554,19 +546,19 @@ export class UserEntityService implements OnModuleInit {
 				notify: relation.following?.notify ?? 'none',
 				withReplies: relation.following?.withReplies ?? false,
 			} : {}),
-		} as Promiseable<Packed<'User'>> as Promiseable<IsMeAndIsUserDetailed<ExpectsMe, D>>;
+		} as Promiseable<Packed<S>>;
 
 		return await awaitAll(packed);
 	}
 
-	public packMany<D extends boolean = false>(
+	public packMany<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
 		users: (MiUser['id'] | MiUser)[],
 		me?: { id: MiUser['id'] } | null | undefined,
 		options?: {
-			detail?: D,
+			schema?: S,
 			includeSecrets?: boolean,
 		},
-	): Promise<IsUserDetailed<D>[]> {
+	): Promise<Packed<S>[]> {
 		return Promise.all(users.map(u => this.pack(u, me, options)));
 	}
 }
diff --git a/packages/backend/src/misc/clone.ts b/packages/backend/src/misc/clone.ts
index 9d20deac3b..52e6c825f9 100644
--- a/packages/backend/src/misc/clone.ts
+++ b/packages/backend/src/misc/clone.ts
@@ -6,7 +6,7 @@
 // structredCloneが遅いため
 // SEE: http://var.blog.jp/archives/86038606.html
 
-type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
+type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
 
 export function deepClone<T extends Cloneable>(x: T): T {
 	if (typeof x === 'object') {
@@ -14,7 +14,7 @@ export function deepClone<T extends Cloneable>(x: T): T {
 		if (Array.isArray(x)) return x.map(deepClone) as T;
 		const obj = {} as Record<string, Cloneable>;
 		for (const [k, v] of Object.entries(x)) {
-			obj[k] = deepClone(v);
+			obj[k] = v === undefined ? undefined : deepClone(v);
 		}
 		return obj as T;
 	} else {
diff --git a/packages/backend/src/misc/json-schema.ts b/packages/backend/src/misc/json-schema.ts
index b4f0541712..dc29003681 100644
--- a/packages/backend/src/misc/json-schema.ts
+++ b/packages/backend/src/misc/json-schema.ts
@@ -25,7 +25,7 @@ import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
 import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
 import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
 import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
-import { packedPageSchema } from '@/models/json-schema/page.js';
+import { packedPageSchema, packedPageBlockSchema } from '@/models/json-schema/page.js';
 import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
 import { packedChannelSchema } from '@/models/json-schema/channel.js';
 import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
@@ -37,7 +37,7 @@ import { packedEmojiDetailedSchema, packedEmojiSimpleSchema } from '@/models/jso
 import { packedFlashSchema } from '@/models/json-schema/flash.js';
 import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
 import { packedSigninSchema } from '@/models/json-schema/signin.js';
-import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
+import { packedRoleLiteSchema, packedRoleSchema, packedRolePoliciesSchema } from '@/models/json-schema/role.js';
 import { packedAdSchema } from '@/models/json-schema/ad.js';
 import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
 
@@ -67,6 +67,7 @@ export const refs = {
 	Hashtag: packedHashtagSchema,
 	InviteCode: packedInviteCodeSchema,
 	Page: packedPageSchema,
+	PageBlock: packedPageBlockSchema,
 	Channel: packedChannelSchema,
 	QueueCount: packedQueueCountSchema,
 	Antenna: packedAntennaSchema,
@@ -79,12 +80,16 @@ export const refs = {
 	Signin: packedSigninSchema,
 	RoleLite: packedRoleLiteSchema,
 	Role: packedRoleSchema,
+	RolePolicies: packedRolePoliciesSchema,
 	ReversiGameLite: packedReversiGameLiteSchema,
 	ReversiGameDetailed: packedReversiGameDetailedSchema,
 };
 
 export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
 
+export type KeyOf<x extends keyof typeof refs> = PropertiesToUnion<typeof refs[x]>;
+type PropertiesToUnion<p extends Schema> = p['properties'] extends NonNullable<Obj> ? keyof p['properties'] : never;
+
 type TypeStringef = 'null' | 'boolean' | 'integer' | 'number' | 'string' | 'array' | 'object' | 'any';
 type StringDefToType<T extends TypeStringef> =
 	T extends 'null' ? null :
@@ -114,6 +119,7 @@ export interface Schema extends OfSchema {
 	readonly example?: any;
 	readonly format?: string;
 	readonly ref?: keyof typeof refs;
+	readonly selfRef?: boolean;
 	readonly enum?: ReadonlyArray<string | null>;
 	readonly default?: (this['type'] extends TypeStringef ? StringDefToType<this['type']> : any) | null;
 	readonly maxLength?: number;
diff --git a/packages/backend/src/models/Announcement.ts b/packages/backend/src/models/Announcement.ts
index 8f8be88fed..c2d9e9878c 100644
--- a/packages/backend/src/models/Announcement.ts
+++ b/packages/backend/src/models/Announcement.ts
@@ -38,7 +38,7 @@ export class MiAnnouncement {
 		length: 256, nullable: false,
 		default: 'info',
 	})
-	public icon: string;
+	public icon: 'info' | 'warning' | 'error' | 'success';
 
 	// normal ... お知らせページ掲載
 	// banner ... お知らせページ掲載 + バナー表示
@@ -47,7 +47,7 @@ export class MiAnnouncement {
 		length: 256, nullable: false,
 		default: 'normal',
 	})
-	public display: string;
+	public display: 'normal' | 'banner' | 'dialog';
 
 	@Column('boolean', {
 		default: false,
diff --git a/packages/backend/src/models/json-schema/announcement.ts b/packages/backend/src/models/json-schema/announcement.ts
index 78a98872b2..57fd7d605d 100644
--- a/packages/backend/src/models/json-schema/announcement.ts
+++ b/packages/backend/src/models/json-schema/announcement.ts
@@ -37,10 +37,12 @@ export const packedAnnouncementSchema = {
 		icon: {
 			type: 'string',
 			optional: false, nullable: false,
+			enum: ['info', 'warning', 'error', 'success'],
 		},
 		display: {
 			type: 'string',
 			optional: false, nullable: false,
+			enum: ['dialog', 'normal', 'banner'],
 		},
 		needConfirmationToRead: {
 			type: 'boolean',
diff --git a/packages/backend/src/models/json-schema/blocking.ts b/packages/backend/src/models/json-schema/blocking.ts
index 0b58f1f8d7..1b3227d455 100644
--- a/packages/backend/src/models/json-schema/blocking.ts
+++ b/packages/backend/src/models/json-schema/blocking.ts
@@ -25,7 +25,7 @@ export const packedBlockingSchema = {
 		blockee: {
 			type: 'object',
 			optional: false, nullable: false,
-			ref: 'UserDetailed',
+			ref: 'UserDetailedNotMe',
 		},
 	},
 } as const;
diff --git a/packages/backend/src/models/json-schema/following.ts b/packages/backend/src/models/json-schema/following.ts
index e92cff20a1..dd3234ee5a 100644
--- a/packages/backend/src/models/json-schema/following.ts
+++ b/packages/backend/src/models/json-schema/following.ts
@@ -30,12 +30,12 @@ export const packedFollowingSchema = {
 		followee: {
 			type: 'object',
 			optional: true, nullable: false,
-			ref: 'UserDetailed',
+			ref: 'UserDetailedNotMe',
 		},
 		follower: {
 			type: 'object',
 			optional: true, nullable: false,
-			ref: 'UserDetailed',
+			ref: 'UserDetailedNotMe',
 		},
 	},
 } as const;
diff --git a/packages/backend/src/models/json-schema/muting.ts b/packages/backend/src/models/json-schema/muting.ts
index dde9dc0288..c3d0bb603c 100644
--- a/packages/backend/src/models/json-schema/muting.ts
+++ b/packages/backend/src/models/json-schema/muting.ts
@@ -30,7 +30,7 @@ export const packedMutingSchema = {
 		mutee: {
 			type: 'object',
 			optional: false, nullable: false,
-			ref: 'UserDetailed',
+			ref: 'UserDetailedNotMe',
 		},
 	},
 } as const;
diff --git a/packages/backend/src/models/json-schema/note.ts b/packages/backend/src/models/json-schema/note.ts
index 2b7722129b..929f697e8a 100644
--- a/packages/backend/src/models/json-schema/note.ts
+++ b/packages/backend/src/models/json-schema/note.ts
@@ -69,6 +69,7 @@ export const packedNoteSchema = {
 		visibility: {
 			type: 'string',
 			optional: false, nullable: false,
+			enum: ['public', 'home', 'followers', 'specified'],
 		},
 		mentions: {
 			type: 'array',
@@ -117,6 +118,48 @@ export const packedNoteSchema = {
 		poll: {
 			type: 'object',
 			optional: true, nullable: true,
+			properties: {
+				expiresAt: {
+					type: 'string',
+					optional: true, nullable: true,
+					format: 'date-time',
+				},
+				multiple: {
+					type: 'boolean',
+					optional: false, nullable: false,
+				},
+				choices: {
+					type: 'array',
+					optional: false, nullable: false,
+					items: {
+						type: 'object',
+						optional: false, nullable: false,
+						properties: {
+							isVoted: {
+								type: 'boolean',
+								optional: false, nullable: false,
+							},
+							text: {
+								type: 'string',
+								optional: false, nullable: false,
+							},
+							votes: {
+								type: 'number',
+								optional: false, nullable: false,
+							},
+						},
+					},
+				},
+			},
+		},
+		emojis: {
+			type: 'object',
+			optional: true, nullable: false,
+			additionalProperties: {
+				anyOf: [{
+					type: 'string',
+				}],
+			},
 		},
 		channelId: {
 			type: 'string',
@@ -162,9 +205,23 @@ export const packedNoteSchema = {
 			type: 'string',
 			optional: false, nullable: true,
 		},
+		reactionEmojis: {
+			type: 'object',
+			optional: false, nullable: false,
+			additionalProperties: {
+				anyOf: [{
+					type: 'string',
+				}],
+			},
+		},
 		reactions: {
 			type: 'object',
 			optional: false, nullable: false,
+			additionalProperties: {
+				anyOf: [{
+					type: 'number',
+				}],
+			},
 		},
 		renoteCount: {
 			type: 'number',
@@ -196,7 +253,7 @@ export const packedNoteSchema = {
 		},
 
 		myReaction: {
-			type: 'object',
+			type: 'string',
 			optional: true, nullable: true,
 		},
 	},
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index c6d6e84317..6286950de5 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -5,7 +5,7 @@
 
 import { notificationTypes } from '@/types.js';
 
-export const packedNotificationSchema = {
+const baseSchema = {
 	type: 'object',
 	properties: {
 		id: {
@@ -23,68 +23,368 @@ export const packedNotificationSchema = {
 			optional: false, nullable: false,
 			enum: [...notificationTypes, 'reaction:grouped', 'renote:grouped'],
 		},
-		user: {
-			type: 'object',
-			ref: 'UserLite',
-			optional: true, nullable: true,
-		},
-		userId: {
-			type: 'string',
-			optional: true, nullable: true,
-			format: 'id',
-		},
-		note: {
-			type: 'object',
-			ref: 'Note',
-			optional: true, nullable: true,
-		},
-		reaction: {
-			type: 'string',
-			optional: true, nullable: true,
-		},
-		achievement: {
-			type: 'string',
-			optional: true, nullable: false,
-		},
-		body: {
-			type: 'string',
-			optional: true, nullable: true,
-		},
-		header: {
-			type: 'string',
-			optional: true, nullable: true,
-		},
-		icon: {
-			type: 'string',
-			optional: true, nullable: true,
-		},
-		reactions: {
-			type: 'array',
-			optional: true, nullable: true,
-			items: {
-				type: 'object',
-				properties: {
-					user: {
-						type: 'object',
-						ref: 'UserLite',
-						optional: false, nullable: false,
-					},
-					reaction: {
-						type: 'string',
-						optional: false, nullable: false,
-					},
-				},
-				required: ['user', 'reaction'],
+	},
+} as const;
+
+export const packedNotificationSchema = {
+	type: 'object',
+	oneOf: [{
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['note'],
 			},
-		},
-		users: {
-			type: 'array',
-			optional: true, nullable: true,
-			items: {
+			user: {
 				type: 'object',
 				ref: 'UserLite',
 				optional: false, nullable: false,
 			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
 		},
-	},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['mention'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['reply'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['renote'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['quote'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['reaction'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+			reaction: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['pollEnded'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['follow'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['receiveFollowRequest'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['followRequestAccepted'],
+			},
+			user: {
+				type: 'object',
+				ref: 'UserLite',
+				optional: false, nullable: false,
+			},
+			userId: {
+				type: 'string',
+				optional: false, nullable: false,
+				format: 'id',
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['roleAssigned'],
+			},
+			role: {
+				type: 'object',
+				ref: 'Role',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['achievementEarned'],
+			},
+			achievement: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['app'],
+			},
+			body: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			header: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+			icon: {
+				type: 'string',
+				optional: false, nullable: false,
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['reaction:grouped'],
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+			reactions: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'object',
+					properties: {
+						user: {
+							type: 'object',
+							ref: 'UserLite',
+							optional: false, nullable: false,
+						},
+						reaction: {
+							type: 'string',
+							optional: false, nullable: false,
+						},
+					},
+					required: ['user', 'reaction'],
+				},
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['renote:grouped'],
+			},
+			note: {
+				type: 'object',
+				ref: 'Note',
+				optional: false, nullable: false,
+			},
+			users: {
+				type: 'array',
+				optional: false, nullable: false,
+				items: {
+					type: 'object',
+					ref: 'UserLite',
+					optional: false, nullable: false,
+				},
+			},
+		},
+	}, {
+		type: 'object',
+		properties: {
+			...baseSchema.properties,
+			type: {
+				type: 'string',
+				optional: false, nullable: false,
+				enum: ['test'],
+			},
+		},
+	}],
 } as const;
diff --git a/packages/backend/src/models/json-schema/page.ts b/packages/backend/src/models/json-schema/page.ts
index 9baacd6884..e4d844645e 100644
--- a/packages/backend/src/models/json-schema/page.ts
+++ b/packages/backend/src/models/json-schema/page.ts
@@ -3,6 +3,108 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
+const blockBaseSchema = {
+	type: 'object',
+	properties: {
+		id: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
+
+const textBlockSchema = {
+	type: 'object',
+	properties: {
+		...blockBaseSchema.properties,
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['text'],
+		},
+		text: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+	},
+} as const;
+
+const sectionBlockSchema = {
+	type: 'object',
+	properties: {
+		...blockBaseSchema.properties,
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['section'],
+		},
+		title: {
+			type: 'string',
+			optional: false, nullable: false,
+		},
+		children: {
+			type: 'array',
+			optional: false, nullable: false,
+			items: {
+				type: 'object',
+				optional: false, nullable: false,
+				ref: 'PageBlock',
+				selfRef: true,
+			},
+		},
+	},
+} as const;
+
+const imageBlockSchema = {
+	type: 'object',
+	properties: {
+		...blockBaseSchema.properties,
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['image'],
+		},
+		fileId: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+	},
+} as const;
+
+const noteBlockSchema = {
+	type: 'object',
+	properties: {
+		...blockBaseSchema.properties,
+		type: {
+			type: 'string',
+			optional: false, nullable: false,
+			enum: ['note'],
+		},
+		detailed: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		note: {
+			type: 'string',
+			optional: false, nullable: true,
+		},
+	},
+} as const;
+
+export const packedPageBlockSchema = {
+	type: 'object',
+	oneOf: [
+		textBlockSchema,
+		sectionBlockSchema,
+		imageBlockSchema,
+		noteBlockSchema,
+	],
+} as const;
+
 export const packedPageSchema = {
 	type: 'object',
 	properties: {
@@ -38,6 +140,7 @@ export const packedPageSchema = {
 			items: {
 				type: 'object',
 				optional: false, nullable: false,
+				ref: 'PageBlock',
 			},
 		},
 		variables: {
diff --git a/packages/backend/src/models/json-schema/renote-muting.ts b/packages/backend/src/models/json-schema/renote-muting.ts
index feed1ceb09..769b33f515 100644
--- a/packages/backend/src/models/json-schema/renote-muting.ts
+++ b/packages/backend/src/models/json-schema/renote-muting.ts
@@ -25,7 +25,7 @@ export const packedRenoteMutingSchema = {
 		mutee: {
 			type: 'object',
 			optional: false, nullable: false,
-			ref: 'UserDetailed',
+			ref: 'UserDetailedNotMe',
 		},
 	},
 } as const;
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index b0c6804bb8..55348d4f3d 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -1,26 +1,103 @@
-const rolePolicyValue = {
+export const packedRolePoliciesSchema = {
 	type: 'object',
+	optional: false, nullable: false,
 	properties: {
-		value: {
-			oneOf: [
-				{
-					type: 'integer',
-					optional: false, nullable: false,
-				},
-				{
-					type: 'boolean',
-					optional: false, nullable: false,
-				},
-			],
+		gtlAvailable: {
+			type: 'boolean',
+			optional: false, nullable: false,
 		},
-		priority: {
+		ltlAvailable: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canPublicNote: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canInvite: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		inviteLimit: {
 			type: 'integer',
 			optional: false, nullable: false,
 		},
-		useDefault: {
+		inviteLimitCycle: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		inviteExpirationTime: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		canManageCustomEmojis: {
 			type: 'boolean',
 			optional: false, nullable: false,
 		},
+		canManageAvatarDecorations: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canSearchNotes: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canUseTranslator: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		canHideAds: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		driveCapacityMb: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		alwaysMarkNsfw: {
+			type: 'boolean',
+			optional: false, nullable: false,
+		},
+		pinLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		antennaLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		wordMuteLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		webhookLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		clipLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		noteEachClipsLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		userListLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		userEachUserListsLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		rateLimitFactor: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
+		avatarDecorationLimit: {
+			type: 'integer',
+			optional: false, nullable: false,
+		},
 	},
 } as const;
 
@@ -121,31 +198,28 @@ export const packedRoleSchema = {
 				policies: {
 					type: 'object',
 					optional: false, nullable: false,
-					properties: {
-						pinLimit: rolePolicyValue,
-						canInvite: rolePolicyValue,
-						clipLimit: rolePolicyValue,
-						canHideAds: rolePolicyValue,
-						inviteLimit: rolePolicyValue,
-						antennaLimit: rolePolicyValue,
-						gtlAvailable: rolePolicyValue,
-						ltlAvailable: rolePolicyValue,
-						webhookLimit: rolePolicyValue,
-						canPublicNote: rolePolicyValue,
-						userListLimit: rolePolicyValue,
-						wordMuteLimit: rolePolicyValue,
-						alwaysMarkNsfw: rolePolicyValue,
-						canSearchNotes: rolePolicyValue,
-						driveCapacityMb: rolePolicyValue,
-						rateLimitFactor: rolePolicyValue,
-						inviteLimitCycle: rolePolicyValue,
-						noteEachClipsLimit: rolePolicyValue,
-						inviteExpirationTime: rolePolicyValue,
-						canManageCustomEmojis: rolePolicyValue,
-						userEachUserListsLimit: rolePolicyValue,
-						canManageAvatarDecorations: rolePolicyValue,
-						canUseTranslator: rolePolicyValue,
-						avatarDecorationLimit: rolePolicyValue,
+					additionalProperties: {
+						anyOf: [{
+							type: 'object',
+							properties: {
+								value: {
+									oneOf: [
+										{
+											type: 'integer',
+										},
+										{
+											type: 'boolean',
+										},
+									],
+								},
+								priority: {
+									type: 'integer',
+								},
+								useDefault: {
+									type: 'boolean',
+								},
+							},
+						}],
 					},
 				},
 				usersCount: {
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index af67e62afa..c343178fa2 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -628,104 +628,7 @@ export const packedMeDetailedOnlySchema = {
 		policies: {
 			type: 'object',
 			nullable: false, optional: false,
-			properties: {
-				gtlAvailable: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				ltlAvailable: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				canPublicNote: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				canInvite: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				inviteLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				inviteLimitCycle: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				inviteExpirationTime: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				canManageCustomEmojis: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				canManageAvatarDecorations: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				canSearchNotes: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				canUseTranslator: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				canHideAds: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				driveCapacityMb: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				alwaysMarkNsfw: {
-					type: 'boolean',
-					nullable: false, optional: false,
-				},
-				pinLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				antennaLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				wordMuteLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				webhookLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				clipLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				noteEachClipsLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				userListLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				userEachUserListsLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				rateLimitFactor: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-				avatarDecorationLimit: {
-					type: 'number',
-					nullable: false, optional: false,
-				},
-			},
+			ref: 'RolePolicies',
 		},
 		//#region secrets
 		email: {
@@ -820,13 +723,5 @@ export const packedUserSchema = {
 			type: 'object',
 			ref: 'UserDetailed',
 		},
-		{
-			type: 'object',
-			ref: 'UserDetailedNotMe',
-		},
-		{
-			type: 'object',
-			ref: 'MeDetailed',
-		},
 	],
 } as const;
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index cdca744b88..d9923ade13 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -295,9 +295,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
 		}, {
 			...baseQueueOptions(this.config, QUEUE.RELATIONSHIP),
 			autorun: false,
-			concurrency: this.config.relashionshipJobConcurrency ?? 16,
+			concurrency: this.config.relationshipJobConcurrency ?? 16,
 			limiter: {
-				max: this.config.relashionshipJobPerSec ?? 64,
+				max: this.config.relationshipJobPerSec ?? 64,
 				duration: 1000,
 			},
 		});
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 3b43b931ae..4a0f109a08 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -206,7 +206,7 @@ export class ServerService implements OnApplicationShutdown {
 				});
 
 				this.globalEventService.publishMainStream(profile.userId, 'meUpdated', await this.userEntityService.pack(profile.userId, { id: profile.userId }, {
-					detail: true,
+					schema: 'MeDetailed',
 					includeSecrets: true,
 				}));
 
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 1758c03aca..386e593636 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -157,7 +157,7 @@ export class ApiServerService {
 				return {
 					ok: true,
 					token: token.token,
-					user: await this.userEntityService.pack(token.userId, null, { detail: true }),
+					user: await this.userEntityService.pack(token.userId, null, { schema: 'UserDetailedNotMe' }),
 				};
 			} else {
 				return {
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 8788a1fd64..d2e6185aa3 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -260,7 +260,7 @@ export class SignupApiService {
 				});
 
 				const res = await this.userEntityService.pack(account, account, {
-					detail: true,
+					schema: 'MeDetailed',
 					includeSecrets: true,
 				});
 
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 8de5171475..9a4e45522d 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -4,8 +4,7 @@
  */
 
 import { permissions } from 'misskey-js';
-import type { Schema } from '@/misc/json-schema.js';
-import { RolePolicies } from '@/core/RoleService.js';
+import type { KeyOf, Schema } from '@/misc/json-schema.js';
 
 import * as ep___admin_meta from './endpoints/admin/meta.js';
 import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
@@ -802,7 +801,7 @@ interface IEndpointMetaBase {
 	 */
 	readonly requireAdmin?: boolean;
 
-	readonly requireRolePolicy?: keyof RolePolicies;
+	readonly requireRolePolicy?: KeyOf<'RolePolicies'>;
 
 	/**
 	 * 引っ越し済みのユーザーによるリクエストを禁止するか
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index 3484d6707a..4ffefa05b7 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -62,17 +62,17 @@ export const meta = {
 				reporter: {
 					type: 'object',
 					nullable: false, optional: false,
-					ref: 'User',
+					ref: 'UserDetailedNotMe',
 				},
 				targetUser: {
 					type: 'object',
 					nullable: false, optional: false,
-					ref: 'User',
+					ref: 'UserDetailedNotMe',
 				},
 				assignee: {
 					type: 'object',
 					nullable: true, optional: true,
-					ref: 'User',
+					ref: 'UserDetailedNotMe',
 				},
 			},
 		},
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index f54d567fff..b18a7e0e41 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -11,6 +11,7 @@ import { SignupService } from '@/core/SignupService.js';
 import { UserEntityService } from '@/core/entities/UserEntityService.js';
 import { localUsernameSchema, passwordSchema } from '@/models/User.js';
 import { DI } from '@/di-symbols.js';
+import { Packed } from '@/misc/json-schema.js';
 
 export const meta = {
 	tags: ['admin'],
@@ -18,7 +19,7 @@ export const meta = {
 	res: {
 		type: 'object',
 		optional: false, nullable: false,
-		ref: 'User',
+		ref: 'MeDetailed',
 		properties: {
 			token: {
 				type: 'string',
@@ -60,11 +61,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			});
 
 			const res = await this.userEntityService.pack(account, account, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
-			});
+			}) as Packed<'MeDetailed'> & { token: string };
 
-			(res as any).token = secret;
+			res.token = secret;
 
 			return res;
 		});
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts
index 93673453d6..80b198eb80 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/find-by-email.ts
@@ -27,7 +27,7 @@ export const meta = {
 	res: {
 		type: 'object',
 		optional: false, nullable: false,
-		ref: 'User',
+		ref: 'UserDetailedNotMe',
 	},
 } as const;
 
@@ -58,7 +58,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			}
 
 			const res = await this.userEntityService.pack(profile.user!, null, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			});
 
 			return res;
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/users.ts b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
index 66f4d9d26b..1e05685991 100644
--- a/packages/backend/src/server/api/endpoints/admin/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/users.ts
@@ -40,7 +40,7 @@ export const meta = {
 			},
 			required: ['id', 'createdAt', 'user'],
 		},
-	}
+	},
 } as const;
 
 export const paramDef = {
@@ -92,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			return await Promise.all(assigns.map(async assign => ({
 				id: assign.id,
 				createdAt: this.idService.parse(assign.id).date.toISOString(),
-				user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
+				user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
 				expiresAt: assign.expiresAt?.toISOString() ?? null,
 			})));
 		});
diff --git a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
index f3601be9bb..51b5a02600 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-moderation-logs.ts
@@ -50,7 +50,7 @@ export const meta = {
 				user: {
 					type: 'object',
 					optional: false, nullable: false,
-					ref: 'UserDetailed',
+					ref: 'UserDetailedNotMe',
 				},
 			},
 		},
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 6267fb97b2..710ca69e55 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -115,7 +115,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const users = await query.getMany();
 
-			return await this.userEntityService.packMany(users, me, { detail: true });
+			return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index 8ab16880fa..eb3ce40313 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		if (user != null) {
 			return {
 				type: 'User',
-				object: await this.userEntityService.pack(user, me, { detail: true }),
+				object: await this.userEntityService.pack(user, me, { schema: 'UserDetailedNotMe' }),
 			};
 		} else if (note != null) {
 			try {
diff --git a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
index ffddda090b..eeb580cead 100644
--- a/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
+++ b/packages/backend/src/server/api/endpoints/auth/session/userkey.ts
@@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			return {
 				accessToken: accessToken.token,
 				user: await this.userEntityService.pack(session.userId, null, {
-					detail: true,
+					schema: 'UserDetailedNotMe',
 				}),
 			};
 		});
diff --git a/packages/backend/src/server/api/endpoints/blocking/create.ts b/packages/backend/src/server/api/endpoints/blocking/create.ts
index 3c7d7ac8cd..1dc4563180 100644
--- a/packages/backend/src/server/api/endpoints/blocking/create.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/create.ts
@@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			await this.userBlockingService.block(blocker, blockee);
 
 			return await this.userEntityService.pack(blockee.id, blocker, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			});
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/blocking/delete.ts b/packages/backend/src/server/api/endpoints/blocking/delete.ts
index 0ce334d559..a6e6bcb5b3 100644
--- a/packages/backend/src/server/api/endpoints/blocking/delete.ts
+++ b/packages/backend/src/server/api/endpoints/blocking/delete.ts
@@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			await this.userBlockingService.unblock(blocker, blockee);
 
 			return await this.userEntityService.pack(blockee.id, blocker, {
-				detail: true,
+				schema: 'UserDetailedNotMe',
 			});
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts
index 9c057760ca..731bcc5b65 100644
--- a/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts
+++ b/packages/backend/src/server/api/endpoints/bubble-game/ranking.ts
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				relations: ['user'],
 			});
 
-			const users = await this.userEntityService.packMany(records.map(r => r.user!), null, { detail: false });
+			const users = await this.userEntityService.packMany(records.map(r => r.user!), null);
 
 			return records.map(r => ({
 				id: r.id,
diff --git a/packages/backend/src/server/api/endpoints/federation/users.ts b/packages/backend/src/server/api/endpoints/federation/users.ts
index d97171865a..df8b66ab44 100644
--- a/packages/backend/src/server/api/endpoints/federation/users.ts
+++ b/packages/backend/src/server/api/endpoints/federation/users.ts
@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				.limit(ps.limit)
 				.getMany();
 
-			return await this.userEntityService.packMany(users, me, { detail: true });
+			return await this.userEntityService.packMany(users, me, { schema: 'UserDetailedNotMe' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/hashtags/users.ts b/packages/backend/src/server/api/endpoints/hashtags/users.ts
index 8302d2380f..5071dd22b7 100644
--- a/packages/backend/src/server/api/endpoints/hashtags/users.ts
+++ b/packages/backend/src/server/api/endpoints/hashtags/users.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const users = await query.limit(ps.limit).getMany();
 
-			return await this.userEntityService.packMany(users, me, { detail: true });
+			return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/i.ts b/packages/backend/src/server/api/endpoints/i.ts
index c24e049180..c613794589 100644
--- a/packages/backend/src/server/api/endpoints/i.ts
+++ b/packages/backend/src/server/api/endpoints/i.ts
@@ -71,8 +71,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				userProfile.loggedInDates = [...userProfile.loggedInDates, today];
 			}
 
-			return await this.userEntityService.pack<true, true>(userProfile.user!, userProfile.user!, {
-				detail: true,
+			return await this.userEntityService.pack(userProfile.user!, userProfile.user!, {
+				schema: 'MeDetailed',
 				includeSecrets: isSecure,
 				userProfile,
 			});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index 9f8e2894b8..7aaf3982d1 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -64,7 +64,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Publish meUpdated event
 			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			}));
 
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 4161553d28..0494af2767 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 
 			// Publish meUpdated event
 			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			}));
 
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index 2ed701014d..b68f23bf8a 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -74,7 +74,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Publish meUpdated event
 			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			}));
 		});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 21e848fb5c..159a311d37 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -99,7 +99,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Publish meUpdated event
 			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			}));
 
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 8dd880c9fa..8503c72a4a 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -77,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Publish meUpdated event
 			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			}));
 		});
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
index 7056ec5a58..e491cf59e1 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -69,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Publish meUpdated event
 			this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			}));
 
diff --git a/packages/backend/src/server/api/endpoints/i/pin.ts b/packages/backend/src/server/api/endpoints/i/pin.ts
index c89cdfa3a4..71182cc29a 100644
--- a/packages/backend/src/server/api/endpoints/i/pin.ts
+++ b/packages/backend/src/server/api/endpoints/i/pin.ts
@@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw err;
 			});
 
-			return await this.userEntityService.pack<true, true>(me.id, me, {
-				detail: true,
+			return await this.userEntityService.pack(me.id, me, {
+				schema: 'MeDetailed',
 			});
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/i/unpin.ts b/packages/backend/src/server/api/endpoints/i/unpin.ts
index b59c0e954f..1e5f66f4a8 100644
--- a/packages/backend/src/server/api/endpoints/i/unpin.ts
+++ b/packages/backend/src/server/api/endpoints/i/unpin.ts
@@ -51,8 +51,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				throw err;
 			});
 
-			return await this.userEntityService.pack<true, true>(me.id, me, {
-				detail: true,
+			return await this.userEntityService.pack(me.id, me, {
+				schema: 'MeDetailed',
 			});
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index 090b07be3c..58809871c7 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -44,7 +44,7 @@ export const meta = {
 
 	res: {
 		type: 'object',
-		ref: 'UserDetailed',
+		ref: 'MeDetailed',
 	},
 } as const;
 
@@ -107,7 +107,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 			});
 
 			const iObj = await this.userEntityService.pack(me.id, me, {
-				detail: true,
+				schema: 'MeDetailed',
 				includeSecrets: true,
 			});
 
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index bfb3fffdc6..b0b9b09e31 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -478,8 +478,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				verifiedLinks: [],
 			});
 
-			const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
-				detail: true,
+			const iObj = await this.userEntityService.pack(user.id, user, {
+				schema: 'MeDetailed',
 				includeSecrets: isSecure,
 			});
 
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index 8367536ad9..cb52bf6b51 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -314,6 +314,11 @@ export const meta = {
 				type: 'string',
 				optional: false, nullable: true,
 			},
+			policies: {
+				type: 'object',
+				optional: false, nullable: false,
+				ref: 'RolePolicies',
+			},
 		},
 	},
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/page-push.ts b/packages/backend/src/server/api/endpoints/page-push.ts
index 0a68516586..49fcaf061b 100644
--- a/packages/backend/src/server/api/endpoints/page-push.ts
+++ b/packages/backend/src/server/api/endpoints/page-push.ts
@@ -55,7 +55,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				var: ps.var,
 				userId: me.id,
 				user: await this.userEntityService.pack(me.id, { id: page.userId }, {
-					detail: true,
+					schema: 'UserDetailed',
 				}),
 			});
 		});
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 390042c815..415633e8b6 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -52,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				host: acct.host ?? IsNull(),
 			})));
 
-			return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { detail: true });
+			return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { schema: 'UserDetailed' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/retention.ts b/packages/backend/src/server/api/endpoints/retention.ts
index dac6d65407..2631693139 100644
--- a/packages/backend/src/server/api/endpoints/retention.ts
+++ b/packages/backend/src/server/api/endpoints/retention.ts
@@ -14,6 +14,32 @@ export const meta = {
 	requireCredential: false,
 
 	res: {
+		type: 'array',
+		items: {
+			type: 'object',
+			properties: {
+				createdAt: {
+					type: 'string',
+					format: 'date-time',
+				},
+				users: {
+					type: 'number',
+				},
+				data: {
+					type: 'object',
+					additionalProperties: {
+						anyOf: [{
+							type: 'number',
+						}],
+					},
+				},
+			},
+			required: [
+				'createdAt',
+				'users',
+				'data',
+			],
+		},
 	},
 
 	allowGet: true,
diff --git a/packages/backend/src/server/api/endpoints/roles/users.ts b/packages/backend/src/server/api/endpoints/roles/users.ts
index d304d075b2..2e43af5c52 100644
--- a/packages/backend/src/server/api/endpoints/roles/users.ts
+++ b/packages/backend/src/server/api/endpoints/roles/users.ts
@@ -33,11 +33,11 @@ export const meta = {
 			properties: {
 				id: {
 					type: 'string',
-					format: 'misskey:id'
+					format: 'misskey:id',
 				},
 				user: {
 					type: 'object',
-					ref: 'User'
+					ref: 'UserDetailed',
 				},
 			},
 			required: ['id', 'user'],
@@ -94,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			return await Promise.all(assigns.map(async assign => ({
 				id: assign.id,
-				user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
+				user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
 			})));
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
index 8dc5841314..b886dd4a60 100644
--- a/packages/backend/src/server/api/endpoints/users.ts
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const users = await query.getMany();
 
-			return await this.userEntityService.packMany(users, me, { detail: true });
+			return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
index d6fb65cecb..6c04a06cbc 100644
--- a/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
+++ b/packages/backend/src/server/api/endpoints/users/get-frequently-replied-users.ts
@@ -122,7 +122,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			// Make replies object (includes weights)
 			const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
-				user: await this.userEntityService.pack(user, me, { detail: true }),
+				user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
 				weight: repliedUsers[user] / peak,
 			})));
 
diff --git a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
index 985141515e..1fd3232de6 100644
--- a/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
+++ b/packages/backend/src/server/api/endpoints/users/lists/get-memberships.ts
@@ -46,7 +46,7 @@ export const meta = {
 				},
 				user: {
 					type: 'object',
-					ref: 'User',
+					ref: 'UserLite',
 				},
 				withReplies: {
 					type: 'boolean',
diff --git a/packages/backend/src/server/api/endpoints/users/reactions.ts b/packages/backend/src/server/api/endpoints/users/reactions.ts
index 372ab80c4c..69a842dbfb 100644
--- a/packages/backend/src/server/api/endpoints/users/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/users/reactions.ts
@@ -9,6 +9,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
 import { QueryService } from '@/core/QueryService.js';
 import { NoteReactionEntityService } from '@/core/entities/NoteReactionEntityService.js';
 import { DI } from '@/di-symbols.js';
+import { CacheService } from '@/core/CacheService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { RoleService } from '@/core/RoleService.js';
 import { ApiError } from '../../error.js';
 
 export const meta = {
@@ -34,6 +37,11 @@ export const meta = {
 			code: 'REACTIONS_NOT_PUBLIC',
 			id: '673a7dd2-6924-1093-e0c0-e68456ceae5c',
 		},
+		isRemoteUser: {
+			message: 'Currently unavailable to display reactions of remote users. See https://github.com/misskey-dev/misskey/issues/12964',
+			code: 'IS_REMOTE_USER',
+			id: '6b95fa98-8cf9-2350-e284-f0ffdb54a805',
+		},
 	},
 } as const;
 
@@ -59,14 +67,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 		@Inject(DI.noteReactionsRepository)
 		private noteReactionsRepository: NoteReactionsRepository,
 
+		private cacheService: CacheService,
+		private userEntityService: UserEntityService,
 		private noteReactionEntityService: NoteReactionEntityService,
 		private queryService: QueryService,
+		private roleService: RoleService,
 	) {
 		super(meta, paramDef, async (ps, me) => {
-			const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
+			const iAmModerator = me ? await this.roleService.isModerator(me) : false; // Moderators can see reactions of all users
+			if (!iAmModerator) {
+				const user = await this.cacheService.findUserById(ps.userId);
+				if (this.userEntityService.isRemoteUser(user)) {
+					throw new ApiError(meta.errors.isRemoteUser);
+				}
 
-			if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
-				throw new ApiError(meta.errors.reactionsNotPublic);
+				const profile = await this.userProfilesRepository.findOneByOrFail({ userId: ps.userId });
+				if ((me == null || me.id !== ps.userId) && !profile.publicReactions) {
+					throw new ApiError(meta.errors.reactionsNotPublic);
+				}
 			}
 
 			const query = this.queryService.makePaginationQuery(this.noteReactionsRepository.createQueryBuilder('reaction'),
diff --git a/packages/backend/src/server/api/endpoints/users/recommendation.ts b/packages/backend/src/server/api/endpoints/users/recommendation.ts
index 1b30e99b15..c73495e3c7 100644
--- a/packages/backend/src/server/api/endpoints/users/recommendation.ts
+++ b/packages/backend/src/server/api/endpoints/users/recommendation.ts
@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 
 			const users = await query.limit(ps.limit).offset(ps.offset).getMany();
 
-			return await this.userEntityService.packMany(users, me, { detail: true });
+			return await this.userEntityService.packMany(users, me, { schema: 'UserDetailed' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
index 4bf25d9fbb..7d36f6f932 100644
--- a/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
+++ b/packages/backend/src/server/api/endpoints/users/search-by-username-and-host.ts
@@ -131,7 +131,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 					.getMany();
 			}
 
-			return await this.userEntityService.packMany(users, me, { detail: !!ps.detail });
+			return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/search.ts b/packages/backend/src/server/api/endpoints/users/search.ts
index 32b5c12372..bc86136592 100644
--- a/packages/backend/src/server/api/endpoints/users/search.ts
+++ b/packages/backend/src/server/api/endpoints/users/search.ts
@@ -141,7 +141,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 			}
 
-			return await this.userEntityService.packMany(users, me, { detail: ps.detail });
+			return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });
 		});
 	}
 }
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index 389497301d..4cf3858494 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 
 				return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
-					detail: true,
+					schema: 'UserDetailed',
 				})));
 			} else {
 				// Lookup user
@@ -146,7 +146,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 				}
 
 				return await this.userEntityService.pack(user, me, {
-					detail: true,
+					schema: 'UserDetailed',
 				});
 			}
 		});
diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts
index 971a6116bf..5dbd4a01e1 100644
--- a/packages/backend/src/server/api/openapi/gen-spec.ts
+++ b/packages/backend/src/server/api/openapi/gen-spec.ts
@@ -6,9 +6,9 @@
 import type { Config } from '@/config.js';
 import endpoints, { IEndpoint } from '../endpoints.js';
 import { errors as basicErrors } from './errors.js';
-import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
+import { getSchemas, convertSchemaToOpenApiSchema } from './schemas.js';
 
-export function genOpenapiSpec(config: Config) {
+export function genOpenapiSpec(config: Config, includeSelfRef = false) {
 	const spec = {
 		openapi: '3.1.0',
 
@@ -30,7 +30,7 @@ export function genOpenapiSpec(config: Config) {
 		paths: {} as any,
 
 		components: {
-			schemas: schemas,
+			schemas: getSchemas(includeSelfRef),
 
 			securitySchemes: {
 				bearerAuth: {
@@ -56,7 +56,7 @@ export function genOpenapiSpec(config: Config) {
 			}
 		}
 
-		const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res') : {};
+		const resSchema = endpoint.meta.res ? convertSchemaToOpenApiSchema(endpoint.meta.res, 'res', includeSelfRef) : {};
 
 		let desc = (endpoint.meta.description ? endpoint.meta.description : 'No description provided.') + '\n\n';
 
@@ -71,7 +71,7 @@ export function genOpenapiSpec(config: Config) {
 		}
 
 		const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
-		const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param') };
+		const schema = { ...convertSchemaToOpenApiSchema(endpoint.params, 'param', false) };
 
 		if (endpoint.meta.requireFile) {
 			schema.properties = {
diff --git a/packages/backend/src/server/api/openapi/schemas.ts b/packages/backend/src/server/api/openapi/schemas.ts
index a862a7b742..61ce913b74 100644
--- a/packages/backend/src/server/api/openapi/schemas.ts
+++ b/packages/backend/src/server/api/openapi/schemas.ts
@@ -6,10 +6,10 @@
 import type { Schema } from '@/misc/json-schema.js';
 import { refs } from '@/misc/json-schema.js';
 
-export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res') {
+export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any {
 	// optional, nullable, refはスキーマ定義に含まれないので分離しておく
 	// eslint-disable-next-line @typescript-eslint/no-unused-vars
-	const { optional, nullable, ref, ...res }: any = schema;
+	const { optional, nullable, ref, selfRef, ...res }: any = schema;
 
 	if (schema.type === 'object' && schema.properties) {
 		if (type === 'res') {
@@ -21,20 +21,20 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're
 		}
 
 		for (const k of Object.keys(schema.properties)) {
-			res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type);
+			res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k], type, includeSelfRef);
 		}
 	}
 
 	if (schema.type === 'array' && schema.items) {
-		res.items = convertSchemaToOpenApiSchema(schema.items, type);
+		res.items = convertSchemaToOpenApiSchema(schema.items, type, includeSelfRef);
 	}
 
 	for (const o of ['anyOf', 'oneOf', 'allOf'] as const) {
 		// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-		if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type));
+		if (o in schema) res[o] = schema[o]!.map(schema => convertSchemaToOpenApiSchema(schema, type, includeSelfRef));
 	}
 
-	if (type === 'res' && schema.ref) {
+	if (type === 'res' && schema.ref && (!schema.selfRef || includeSelfRef)) {
 		const $ref = `#/components/schemas/${schema.ref}`;
 		if (schema.nullable || schema.optional) {
 			res.allOf = [{ $ref }];
@@ -54,35 +54,37 @@ export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 're
 	return res;
 }
 
-export const schemas = {
-	Error: {
-		type: 'object',
-		properties: {
-			error: {
-				type: 'object',
-				description: 'An error object.',
-				properties: {
-					code: {
-						type: 'string',
-						description: 'An error code. Unique within the endpoint.',
-					},
-					message: {
-						type: 'string',
-						description: 'An error message.',
-					},
-					id: {
-						type: 'string',
-						format: 'uuid',
-						description: 'An error ID. This ID is static.',
+export function getSchemas(includeSelfRef: boolean) {
+	return {
+		Error: {
+			type: 'object',
+			properties: {
+				error: {
+					type: 'object',
+					description: 'An error object.',
+					properties: {
+						code: {
+							type: 'string',
+							description: 'An error code. Unique within the endpoint.',
+						},
+						message: {
+							type: 'string',
+							description: 'An error message.',
+						},
+						id: {
+							type: 'string',
+							format: 'uuid',
+							description: 'An error ID. This ID is static.',
+						},
 					},
+					required: ['code', 'id', 'message'],
 				},
-				required: ['code', 'id', 'message'],
 			},
+			required: ['error'],
 		},
-		required: ['error'],
-	},
 
-	...Object.fromEntries(
-		Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res')]),
-	),
-};
+		...Object.fromEntries(
+			Object.entries(refs).map(([key, schema]) => [key, convertSchemaToOpenApiSchema(schema, 'res', includeSelfRef)]),
+		),
+	};
+}
diff --git a/packages/frontend/@types/global.d.ts b/packages/frontend/@types/global.d.ts
index 7d9335cc52..936e74decf 100644
--- a/packages/frontend/@types/global.d.ts
+++ b/packages/frontend/@types/global.d.ts
@@ -16,3 +16,8 @@ declare const _DATA_TRANSFER_DECK_COLUMN_: string;
 
 // for dev-mode
 declare const _LANGS_FULL_: string[][];
+
+// TagCanvas
+interface Window {
+	TagCanvas: any;
+}
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index f0bb1bb769..707c15f5c7 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -20,7 +20,7 @@
 		"@discordapp/twemoji": "15.0.2",
 		"@github/webauthn-json": "2.1.1",
 		"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
-		"@misskey-dev/browser-image-resizer": "2.2.1-misskey.10",
+		"@misskey-dev/browser-image-resizer": "2024.1.0",
 		"@rollup/plugin-json": "6.1.0",
 		"@rollup/plugin-replace": "5.0.5",
 		"@rollup/pluginutils": "5.1.0",
@@ -28,8 +28,8 @@
 		"@syuilo/aiscript": "0.17.0",
 		"@phosphor-icons/web": "^2.0.3",
 		"@twemoji/parser": "15.0.0",
-		"@vitejs/plugin-vue": "5.0.2",
-		"@vue/compiler-sfc": "3.4.3",
+		"@vitejs/plugin-vue": "5.0.3",
+		"@vue/compiler-sfc": "3.4.15",
 		"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.0.6",
 		"astring": "1.8.6",
 		"broadcast-channel": "7.0.0",
@@ -40,7 +40,7 @@
 		"chartjs-chart-matrix": "2.0.1",
 		"chartjs-plugin-gradient": "0.6.1",
 		"chartjs-plugin-zoom": "2.0.1",
-		"chromatic": "10.3.1",
+		"chromatic": "10.6.1",
 		"compare-versions": "6.1.0",
 		"cropperjs": "2.0.0-beta.4",
 		"date-fns": "2.30.0",
@@ -61,10 +61,10 @@
 		"rollup": "4.9.6",
 		"sanitize-html": "2.11.0",
 		"sass": "1.70.0",
-		"shiki": "0.14.7",
+		"shiki": "1.0.0-beta.3",
 		"strict-event-emitter-types": "2.0.0",
 		"textarea-caret": "3.1.0",
-		"three": "0.160.0",
+		"three": "0.160.1",
 		"throttle-debounce": "5.0.0",
 		"tinycolor2": "1.6.0",
 		"tsc-alias": "1.8.8",
@@ -77,8 +77,8 @@
 		"vuedraggable": "next"
 	},
 	"devDependencies": {
-		"@misskey-dev/eslint-plugin": "^1.0.0",
-		"@misskey-dev/summaly": "^5.0.3",
+		"@misskey-dev/eslint-plugin": "1.0.0",
+		"@misskey-dev/summaly": "5.0.3",
 		"@storybook/addon-actions": "7.6.10",
 		"@storybook/addon-essentials": "7.6.10",
 		"@storybook/addon-interactions": "7.6.10",
@@ -102,12 +102,12 @@
 		"@types/estree": "1.0.5",
 		"@types/matter-js": "0.19.6",
 		"@types/micromatch": "4.0.6",
-		"@types/node": "20.11.5",
+		"@types/node": "20.11.10",
 		"@types/punycode": "2.1.3",
 		"@types/sanitize-html": "2.9.5",
 		"@types/throttle-debounce": "5.0.2",
 		"@types/tinycolor2": "1.4.6",
-		"@types/uuid": "9.0.7",
+		"@types/uuid": "9.0.8",
 		"@types/ws": "8.5.10",
 		"@typescript-eslint/eslint-plugin": "6.18.1",
 		"@typescript-eslint/parser": "6.18.1",
@@ -135,7 +135,8 @@
 		"vite-plugin-turbosnap": "1.0.3",
 		"vitest": "0.34.6",
 		"vitest-fetch-mock": "0.2.2",
-		"vue-eslint-parser": "9.4.0",
+		"vue-component-type-helpers": "^1.8.27",
+		"vue-eslint-parser": "9.4.2",
 		"vue-tsc": "1.8.27"
 	}
 }
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 2d1d0b8011..f6a3e40305 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -22,7 +22,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { fetchCustomEmojis } from '@/custom-emojis.js';
-import { setupRouter } from '@/global/router/definition.js';
+import { setupRouter } from '@/router/definition.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Sharkey v${version}`);
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 8f74e927a4..3d72c04b3d 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -19,7 +19,7 @@ import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js
 import { initializeSw } from '@/scripts/initialize-sw.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { emojiPicker } from '@/scripts/emoji-picker.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => createApp(
@@ -79,13 +79,18 @@ export async function mainBoot() {
 			// ▼南半球
 			if (month === 7 || month === 8) {
 				const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
-				new SnowfallEffect().render();
+				new SnowfallEffect({}).render();
 			}
 		} else {
 			// ▼北半球
 			if (month === 12 || month === 1) {
 				const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
-				new SnowfallEffect().render();
+				new SnowfallEffect({}).render();
+			} else if (month === 3 || month === 4) {
+				const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+				new SakuraEffect({
+					sakura: true,
+				}).render();
 			}
 		}
 	}
diff --git a/packages/frontend/src/components/MkAbuseReportWindow.vue b/packages/frontend/src/components/MkAbuseReportWindow.vue
index 6819630b74..2dfb49d450 100644
--- a/packages/frontend/src/components/MkAbuseReportWindow.vue
+++ b/packages/frontend/src/components/MkAbuseReportWindow.vue
@@ -39,7 +39,7 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
-	user: Misskey.entities.User;
+	user: Misskey.entities.UserDetailed;
 	initialComment?: string;
 }>();
 
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index 9a7078c2f9..08256de5e6 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<div v-if="achievements" :class="$style.root">
-		<div v-for="achievement in achievements" :key="achievement" :class="$style.achievement" class="_panel">
+		<div v-for="achievement in achievements" :key="achievement.name" :class="$style.achievement" class="_panel">
 			<div :class="$style.icon">
 				<div
 					:class="[$style.iconFrame, {
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 8f550cf255..0cb996e4df 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -49,7 +49,7 @@ async function ok() {
 		if (confirm.canceled) return;
 	}
 
-	modal.value.close();
+	modal.value?.close();
 	misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
 	updateAccount({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
@@ -57,7 +57,7 @@ async function ok() {
 }
 
 function onBgClick() {
-	rootEl.value.animate([{
+	rootEl.value?.animate([{
 		offset: 0,
 		transform: 'scale(1)',
 	}, {
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 8475233dfa..c0c6bd4fcb 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
 		</template>
 	</div>
-	<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }">{{ c.text }}</span>
-	<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text" @clickEv="c.onClickEv"/>
+	<span v-else-if="c.type === 'text'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : undefined, fontWeight: c.bold ? 'bold' : undefined, color: c.color }">{{ c.text }}</span>
+	<Mfm v-else-if="c.type === 'mfm'" :class="{ [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }" :style="{ fontSize: c.size ? `${c.size * 100}%` : null, fontWeight: c.bold ? 'bold' : null, color: c.color ?? null }" :text="c.text ?? ''" @clickEv="c.onClickEv"/>
 	<MkButton v-else-if="c.type === 'button'" :primary="c.primary" :rounded="c.rounded" :disabled="c.disabled" :small="size === 'small'" inline @click="c.onClick">{{ c.text }}</MkButton>
 	<div v-else-if="c.type === 'buttons'" class="_buttons" :style="{ justifyContent: align }">
 		<MkButton v-for="button in c.buttons" :primary="button.primary" :rounded="button.rounded" :disabled="button.disabled" inline :small="size === 'small'" @click="button.onClick">{{ button.text }}</MkButton>
@@ -20,19 +20,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template v-if="c.label" #label>{{ c.label }}</template>
 		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 	</MkSwitch>
-	<MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default" @update:modelValue="c.onInput">
+	<MkTextarea v-else-if="c.type === 'textarea'" :modelValue="c.default ?? null" @update:modelValue="c.onInput">
 		<template v-if="c.label" #label>{{ c.label }}</template>
 		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 	</MkTextarea>
-	<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onInput">
+	<MkInput v-else-if="c.type === 'textInput'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onInput">
 		<template v-if="c.label" #label>{{ c.label }}</template>
 		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 	</MkInput>
-	<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default" type="number" @update:modelValue="c.onInput">
+	<MkInput v-else-if="c.type === 'numberInput'" :small="size === 'small'" :modelValue="c.default ?? null" type="number" @update:modelValue="c.onInput">
 		<template v-if="c.label" #label>{{ c.label }}</template>
 		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 	</MkInput>
-	<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default" @update:modelValue="c.onChange">
+	<MkSelect v-else-if="c.type === 'select'" :small="size === 'small'" :modelValue="c.default ?? null" @update:modelValue="c.onChange">
 		<template v-if="c.label" #label>{{ c.label }}</template>
 		<template v-if="c.caption" #caption>{{ c.caption }}</template>
 		<option v-for="item in c.items" :key="item.value" :value="item.value">{{ item.text }}</option>
@@ -42,8 +42,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPostForm
 			fixed
 			:instant="true"
-			:initialText="c.form.text"
-			:initialCw="c.form.cw"
+			:initialText="c.form?.text"
+			:initialCw="c.form?.cw"
 		/>
 	</div>
 	<MkFolder v-else-if="c.type === 'folder'" :defaultOpen="c.opened">
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
 		</template>
 	</MkFolder>
-	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align ?? null, backgroundColor: c.bgColor ?? null, color: c.fgColor ?? null, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
+	<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
 		<template v-for="child in c.children" :key="child">
 			<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
 		</template>
@@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import { AsUiComponent } from '@/scripts/aiscript/ui.js';
+import { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 
@@ -85,20 +85,32 @@ const props = withDefaults(defineProps<{
 const c = props.component;
 
 function g(id) {
-	return props.components.find(x => x.value.id === id).value;
+	const v = props.components.find(x => x.value.id === id)?.value;
+	if (v) return v;
+
+	return {
+		id: 'dummy',
+		type: 'root',
+		children: [],
+	} as AsUiRoot;
 }
 
-const valueForSwitch = ref(c.default ?? false);
+const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
 
 function onSwitchUpdate(v) {
 	valueForSwitch.value = v;
-	if (c.onChange) c.onChange(v);
+	if ('onChange' in c && c.onChange) {
+		c.onChange(v as never);
+	}
 }
 
 function openPostForm() {
+	const form = (c as AsUiPostFormButton).form;
+	if (!form) return;
+
 	os.post({
-		initialText: c.form.text,
-		initialCw: c.form.cw,
+		initialText: form.text,
+		initialCw: form.cw,
 		instant: true,
 	});
 }
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 976ba190ff..0b10605307 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -431,7 +431,7 @@ function applySelect() {
 
 function chooseUser() {
 	props.close();
-	os.selectUser().then(user => {
+	os.selectUser({ includeSelf: true }).then(user => {
 		complete('user', user);
 		props.textarea.focus();
 	});
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index da13f8a067..c0ef8a89a0 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkA
 	v-else class="_button"
 	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
-	:to="to"
+	:to="to ?? '#'"
 	@mousedown="onMousedown"
 >
 	<div ref="ripples" :class="$style.ripples" :data-children-class="$style.ripple"></div>
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index f60c721eae..7aa08cf51f 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<span v-if="!available">{{ i18n.ts.waiting }}<MkEllipsis/></span>
+	<span v-if="!available">Loading<MkEllipsis/></span>
 	<div v-if="props.provider == 'mcaptcha'">
 		<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
 		<div ref="captchaEl"></div>
@@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
 import { defaultStore } from '@/store.js';
-import { i18n } from '@/i18n.js';
 
 // APIs provided by Captcha services
 export type Captcha = {
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 82605123c5..06cdc37977 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 */
 import { onMounted, ref, shallowRef, watch, PropType } from 'vue';
 import { Chart } from 'chart.js';
-import gradient from 'chartjs-plugin-gradient';
 import { misskeyApiGet } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
 import { chartVLine } from '@/scripts/chart-vline.js';
 import { alpha } from '@/scripts/color.js';
 import date from '@/filters/date.js';
+import bytes from '@/filters/bytes.js';
 import { initChart } from '@/scripts/init-chart.js';
 import { chartLegend } from '@/scripts/chart-legend.js';
 import MkChartLegend from '@/components/MkChartLegend.vue';
@@ -95,7 +95,7 @@ const getColor = (i) => {
 };
 
 const now = new Date();
-let chartInstance: Chart = null;
+let chartInstance: Chart | null = null;
 let chartData: {
 	series: {
 		name: string;
@@ -108,9 +108,10 @@ let chartData: {
 			y: number;
 		}[];
 	}[];
-} = null;
+	bytes?: boolean;
+} | null = null;
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
 const fetching = ref(true);
 
 const getDate = (ago: number) => {
@@ -132,6 +133,7 @@ const format = (arr) => {
 const { handler: externalTooltipHandler } = useChartTooltip();
 
 const render = () => {
+	if (chartData == null || chartEl.value == null) return;
 	if (chartInstance) {
 		chartInstance.destroy();
 	}
@@ -188,7 +190,6 @@ const render = () => {
 					stacked: props.stacked,
 					offset: false,
 					time: {
-						stepSize: 1,
 						unit: props.span === 'day' ? 'month' : 'day',
 						displayFormats: {
 							day: 'M/d',
@@ -198,6 +199,7 @@ const render = () => {
 					grid: {
 					},
 					ticks: {
+						stepSize: 1,
 						display: props.detailed,
 						maxRotation: 0,
 						autoSkipPadding: 16,
@@ -237,6 +239,9 @@ const render = () => {
 						duration: 0,
 					},
 					external: externalTooltipHandler,
+					callbacks: {
+						label: (item) => chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString(),
+					},
 				},
 				zoom: props.detailed ? {
 					pan: {
@@ -265,10 +270,9 @@ const render = () => {
 						},
 					},
 				} : undefined,
-				gradient,
 			},
 		},
-		plugins: [chartVLine(vLineColor), ...(props.detailed ? [chartLegend(legendEl.value)] : [])],
+		plugins: [chartVLine(vLineColor), ...(props.detailed && legendEl.value ? [chartLegend(legendEl.value)] : [])],
 	});
 };
 
@@ -566,7 +570,7 @@ const fetchDriveFilesChart = async (): Promise<typeof chartData> => {
 };
 
 const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'In',
@@ -588,7 +592,7 @@ const fetchInstanceRequestsChart = async (): Promise<typeof chartData> => {
 };
 
 const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Users',
@@ -603,7 +607,7 @@ const fetchInstanceUsersChart = async (total: boolean): Promise<typeof chartData
 };
 
 const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Notes',
@@ -618,7 +622,7 @@ const fetchInstanceNotesChart = async (total: boolean): Promise<typeof chartData
 };
 
 const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Following',
@@ -641,7 +645,7 @@ const fetchInstanceFfChart = async (total: boolean): Promise<typeof chartData> =
 };
 
 const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
 	return {
 		bytes: true,
 		series: [{
@@ -649,7 +653,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
 			type: 'area',
 			color: '#008FFB',
 			data: format(total
-				? raw.drive.totalUsage
+				? sum(raw.drive.incUsage)
 				: sum(raw.drive.incUsage, negate(raw.drive.decUsage)),
 			),
 		}],
@@ -657,7 +661,7 @@ const fetchInstanceDriveUsageChart = async (total: boolean): Promise<typeof char
 };
 
 const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/instance', { host: props.args.host, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/instance', { host: props.args?.host, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Drive files',
@@ -672,11 +676,11 @@ const fetchInstanceDriveFilesChart = async (total: boolean): Promise<typeof char
 };
 
 const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/user/notes', { userId: props.args.user.id, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/user/notes', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
 	return {
-		series: [...(props.args.withoutAll ? [] : [{
+		series: [...(props.args?.withoutAll ? [] : [{
 			name: 'All',
-			type: 'line',
+			type: 'line' as const,
 			data: format(sum(raw.inc, negate(raw.dec))),
 			color: '#888888',
 		}]), {
@@ -704,7 +708,7 @@ const fetchPerUserNotesChart = async (): Promise<typeof chartData> => {
 };
 
 const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/user/pv', { userId: props.args.user.id, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/user/pv', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Unique PV (user)',
@@ -731,7 +735,7 @@ const fetchPerUserPvChart = async (): Promise<typeof chartData> => {
 };
 
 const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Local',
@@ -746,7 +750,7 @@ const fetchPerUserFollowingChart = async (): Promise<typeof chartData> => {
 };
 
 const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/user/following', { userId: props.args.user.id, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/user/following', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
 	return {
 		series: [{
 			name: 'Local',
@@ -761,8 +765,9 @@ const fetchPerUserFollowersChart = async (): Promise<typeof chartData> => {
 };
 
 const fetchPerUserDriveChart = async (): Promise<typeof chartData> => {
-	const raw = await misskeyApiGet('charts/user/drive', { userId: props.args.user.id, limit: props.limit, span: props.span });
+	const raw = await misskeyApiGet('charts/user/drive', { userId: props.args?.user?.id, limit: props.limit, span: props.span });
 	return {
+		bytes: true,
 		series: [{
 			name: 'Inc',
 			type: 'area',
@@ -806,6 +811,8 @@ const fetchAndRender = async () => {
 			case 'per-user-following': return fetchPerUserFollowingChart();
 			case 'per-user-followers': return fetchPerUserFollowersChart();
 			case 'per-user-drive': return fetchPerUserDriveChart();
+
+			default: return null;
 		}
 	};
 	fetching.value = true;
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index c265fe6e97..e77cea569b 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<button v-for="item in items" class="_button item" :class="{ disabled: item.hidden }" @click="onClick(item)">
-		<span class="box" :style="{ background: chart.config.type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
+		<span class="box" :style="{ background: type === 'line' ? item.strokeStyle?.toString() : item.fillStyle?.toString() }"></span>
 		{{ item.text }}
 	</button>
 </div>
@@ -16,25 +16,23 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { shallowRef } from 'vue';
 import { Chart, LegendItem } from 'chart.js';
 
-const props = defineProps({
-});
-
 const chart = shallowRef<Chart>();
+const type = shallowRef<string>();
 const items = shallowRef<LegendItem[]>([]);
 
 function update(_chart: Chart, _items: LegendItem[]) {
 	chart.value = _chart,
 	items.value = _items;
+	if ('type' in _chart.config) type.value = _chart.config.type;
 }
 
 function onClick(item: LegendItem) {
 	if (chart.value == null) return;
-	const { type } = chart.value.config;
-	if (type === 'pie' || type === 'doughnut') {
+	if (type.value === 'pie' || type.value === 'doughnut') {
 		// Pie and doughnut charts only have a single dataset and visibility is per item
-		chart.value.toggleDataVisibility(item.index);
+		if (item.index != null) chart.value.toggleDataVisibility(item.index);
 	} else {
-		chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
+		if (item.datasetIndex != null) chart.value.setDatasetVisibility(item.datasetIndex, !chart.value.isDatasetVisible(item.datasetIndex));
 	}
 	chart.value.update();
 }
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 77a01627ba..b06bb70e99 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -5,13 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <!-- eslint-disable vue/no-v-html -->
 <template>
-<div :class="['codeBlockRoot', { 'codeEditor': codeEditor }]" v-html="html"></div>
+<div :class="[$style.codeBlockRoot, { [$style.codeEditor]: codeEditor }]" v-html="html"></div>
 </template>
 
 <script lang="ts" setup>
 import { ref, computed, watch } from 'vue';
-import { BUNDLED_LANGUAGES } from 'shiki';
-import type { Lang as ShikiLang } from 'shiki';
+import { bundledLanguagesInfo } from 'shiki';
+import type { BuiltinLanguage } from 'shiki';
 import { getHighlighter } from '@/scripts/code-highlighter.js';
 
 const props = defineProps<{
@@ -22,24 +22,25 @@ const props = defineProps<{
 
 const highlighter = await getHighlighter();
 
-const codeLang = ref<ShikiLang | 'aiscript'>('js');
+const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
 const html = computed(() => highlighter.codeToHtml(props.code, {
 	lang: codeLang.value,
 	theme: 'dark-plus',
 }));
 
 async function fetchLanguage(to: string): Promise<void> {
-	const language = to as ShikiLang;
+	const language = to as BuiltinLanguage;
 
 	// Check for the loaded languages, and load the language if it's not loaded yet.
 	if (!highlighter.getLoadedLanguages().includes(language)) {
 		// Check if the language is supported by Shiki
-		const bundles = BUNDLED_LANGUAGES.filter((bundle) => {
+		const bundles = bundledLanguagesInfo.filter((bundle) => {
 			// Languages are specified by their id, they can also have aliases (i. e. "js" and "javascript")
 			return bundle.id === language || bundle.aliases?.includes(language);
 		});
 		if (bundles.length > 0) {
-			await highlighter.loadLanguage(language);
+			console.log(`Loading language: ${language}`);
+			await highlighter.loadLanguage(bundles[0].import);
 			codeLang.value = language;
 		} else {
 			codeLang.value = 'js';
@@ -57,13 +58,13 @@ watch(() => props.lang, (to) => {
 }, { immediate: true });
 </script>
 
-<style scoped lang="scss">
-.codeBlockRoot :deep(.shiki) > code {
+<style module lang="scss">
+.codeBlockRoot :global(.shiki) > code {
   counter-reset: step;
   counter-increment: step 0;
 }
 
-.codeBlockRoot :deep(.shiki) > code > .line::before {
+.codeBlockRoot :global(.shiki) > code > .line::before {
   content: counter(step);
   counter-increment: step;
   width: 1rem;
@@ -73,7 +74,7 @@ watch(() => props.lang, (to) => {
   color: rgba(115,138,148,.4)
 }
 
-.codeBlockRoot :deep(.shiki) {
+.codeBlockRoot :global(.shiki) {
 	padding: 1em;
 	margin: .5em 0;
 	overflow: auto;
@@ -89,7 +90,7 @@ watch(() => props.lang, (to) => {
 	min-width: 100%;
 	height: 100%;
 
-	& :deep(.shiki) {
+	& :global(.shiki) {
 		padding: 12px;
 		margin: 0;
 		border-radius: var(--radius-sm);
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index 4f15e88951..a48628993d 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -41,8 +41,8 @@ const { modelValue } = toRefs(props);
 const v = ref(modelValue.value);
 const inputEl = shallowRef<HTMLElement>();
 
-const onInput = (ev: KeyboardEvent) => {
-	emit('update:modelValue', v.value);
+const onInput = () => {
+	emit('update:modelValue', v.value ?? '');
 };
 </script>
 
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index e29cf472f7..d330d66b28 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -44,8 +44,8 @@ onMounted(() => {
 	let left = props.ev.pageX + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 	let top = props.ev.pageY + 1; // 間違って右ダブルクリックした場合に意図せずアイテムがクリックされるのを防ぐため + 1
 
-	const width = rootEl.value.offsetWidth;
-	const height = rootEl.value.offsetHeight;
+	const width = rootEl.value!.offsetWidth;
+	const height = rootEl.value!.offsetHeight;
 
 	if (left + width - window.pageXOffset >= (window.innerWidth - SCROLLBAR_THICKNESS)) {
 		left = (window.innerWidth - SCROLLBAR_THICKNESS) - width + window.pageXOffset;
@@ -63,8 +63,10 @@ onMounted(() => {
 		left = 0;
 	}
 
-	rootEl.value.style.top = `${top}px`;
-	rootEl.value.style.left = `${left}px`;
+	if (rootEl.value) {
+		rootEl.value.style.top = `${top}px`;
+		rootEl.value.style.left = `${left}px`;
+	}
 
 	document.body.addEventListener('mousedown', onMousedown);
 });
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index ca19a2122d..c7395ad3d5 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
 import { concat } from '@/scripts/array.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
@@ -17,22 +18,9 @@ import MkButton from '@/components/MkButton.vue';
 const props = defineProps<{
 	modelValue: boolean;
 	text: string | null;
-	renote: Misskey.entities.Note | null;
-	files: Misskey.entities.DriveFile[];
-	poll?: {
-		expiresAt: string | null;
-		multiple: boolean;
-		choices: {
-			isVoted: boolean;
-			text: string;
-			votes: number;
-		}[];
-	} | {
-		choices: string[];
-		multiple: boolean;
-		expiresAt: string | null;
-		expiredAfter: string | null;
-	};
+	renote?: Misskey.entities.Note | null;
+	files?: Misskey.entities.DriveFile[];
+	poll?: Misskey.entities.Note['poll'] | PollEditorModelValue | null;
 }>();
 
 const emit = defineEmits<{
@@ -43,7 +31,7 @@ const label = computed(() => {
 	return concat([
 		props.text ? [i18n.tsx._cw.chars({ count: props.text.length })] : [],
 		props.renote ? [i18n.ts.quote] : [],
-		props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
+		props.files && props.files.length !== 0 ? [i18n.tsx._cw.files({ count: props.files.length })] : [],
 		props.poll != null ? [i18n.ts.poll] : [],
 	] as string[][]).join(' / ');
 });
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index f6bd85bd1d..0f936dca01 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -118,34 +118,36 @@ export default defineComponent({
 			return children;
 		};
 
-		function onBeforeLeave(el: HTMLElement) {
+		function onBeforeLeave(element: Element) {
+			const el = element as HTMLElement;
 			el.style.top = `${el.offsetTop}px`;
 			el.style.left = `${el.offsetLeft}px`;
 		}
 
-		function onLeaveCanceled(el: HTMLElement) {
+		function onLeaveCancelled(element: Element) {
+			const el = element as HTMLElement;
 			el.style.top = '';
 			el.style.left = '';
 		}
 
-		return () => h(
-			defaultStore.state.animation ? TransitionGroup : 'div',
-			{
-				class: {
-					[$style['date-separated-list']]: true,
-					[$style['date-separated-list-nogap']]: props.noGap,
-					[$style['reversed']]: props.reversed,
-					[$style['direction-down']]: props.direction === 'down',
-					[$style['direction-up']]: props.direction === 'up',
-				},
-				...(defaultStore.state.animation ? {
-					name: 'list',
-					tag: 'div',
-					onBeforeLeave,
-					onLeaveCanceled,
-				} : {}),
-			},
-			{ default: renderChildren });
+		// eslint-disable-next-line vue/no-setup-props-destructure
+		const classes = {
+			[$style['date-separated-list']]: true,
+			[$style['date-separated-list-nogap']]: props.noGap,
+			[$style['reversed']]: props.reversed,
+			[$style['direction-down']]: props.direction === 'down',
+			[$style['direction-up']]: props.direction === 'up',
+		};
+
+		return () => defaultStore.state.animation ? h(TransitionGroup, {
+			class: classes,
+			name: 'list',
+			tag: 'div',
+			onBeforeLeave,
+			onLeaveCancelled,
+		}, { default: renderChildren }) : h('div', {
+			class: classes,
+		}, { default: renderChildren });
 	},
 });
 </script>
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 83f5041a46..a3b430a72c 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" :autocomplete="input.autocomplete" @keydown="onInputKeydown">
 			<template v-if="input.type === 'password'" #prefix><i class="ph-lock ph-bold ph-lg"></i></template>
 			<template #caption>
-				<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string).length, max: input.maxLength ?? 'NaN' })"/>
-				<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string).length, min: input.minLength ?? 'NaN' })"/>
+				<span v-if="okButtonDisabledReason === 'charactersExceeded'" v-text="i18n.tsx._dialog.charactersExceeded({ current: (inputValue as string)?.length ?? 0, max: input.maxLength ?? 'NaN' })"/>
+				<span v-else-if="okButtonDisabledReason === 'charactersBelow'" v-text="i18n.tsx._dialog.charactersBelow({ current: (inputValue as string)?.length ?? 0, min: input.minLength ?? 'NaN' })"/>
 			</template>
 		</MkInput>
 		<MkSelect v-if="select" v-model="selectedValue" autofocus>
@@ -125,7 +125,7 @@ const selectedValue = ref(props.select?.default ?? null);
 const okButtonDisabledReason = computed<null | 'charactersExceeded' | 'charactersBelow'>(() => {
 	if (props.input) {
 		if (props.input.minLength) {
-			if ((inputValue.value || inputValue.value === '') && (inputValue.value as string).length < props.input.minLength) {
+			if (inputValue.value == null || (inputValue.value as string).length < props.input.minLength) {
 				return 'charactersBelow';
 			}
 		}
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 7d9b0c603f..ce0800374e 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -51,7 +51,7 @@ import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
 import { deviceKind } from '@/scripts/device-kind.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index a2c8fd4fa5..8f2a803ddd 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -205,7 +205,7 @@ function onDragend() {
 }
 
 function go() {
-	emit('move', props.folder.id);
+	emit('move', props.folder);
 }
 
 function rename() {
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index e68b69c140..b6fffa5e7a 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -98,6 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from './MkButton.vue';
+import type { MenuItem } from '@/types/menu.js';
 import XNavFolder from '@/components/MkDrive.navFolder.vue';
 import XFolder from '@/components/MkDrive.folder.vue';
 import XFile from '@/components/MkDrive.file.vue';
@@ -427,7 +428,7 @@ function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
 	}
 }
 
-function move(target?: Misskey.entities.DriveFolder) {
+function move(target?: Misskey.entities.DriveFolder | Misskey.entities.DriveFolder['id' | 'parentId']) {
 	if (!target) {
 		goRoot();
 		return;
@@ -613,7 +614,7 @@ function fetchMoreFiles() {
 }
 
 function getMenu() {
-	return [{
+	const menu: MenuItem[] = [{
 		type: 'switch',
 		text: i18n.ts.keepOriginalUploading,
 		ref: keepOriginal,
@@ -634,7 +635,7 @@ function getMenu() {
 	}, folder.value ? {
 		text: i18n.ts.renameFolder,
 		icon: 'ph-textbox ph-bold ph-lg',
-		action: () => { renameFolder(folder.value); },
+		action: () => { if (folder.value) renameFolder(folder.value); },
 	} : undefined, folder.value ? {
 		text: i18n.ts.deleteFolder,
 		icon: 'ph-trash ph-bold ph-lg',
@@ -644,6 +645,8 @@ function getMenu() {
 		icon: 'ph-folder ph-bold ph-lg-plus',
 		action: () => { createFolder(); },
 	}];
+
+	return menu;
 }
 
 function showMenu(ev: MouseEvent) {
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index dabc12237a..ebfefceb18 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
 <section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
 	<header class="_acrylic" @click="shown = !shown">
-		<i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }})
+		<i class="toggle ti-fw" :class="shown ? 'ph-caret-down ph-bold ph-lg' : 'ph-caret-up ph-bold ph-lg'"></i> <slot></slot> (<i class="ph-folder ph-bold ph-lg"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }})
 	</header>
 	<div v-if="shown" style="padding-left: 9px;">
 		<MkEmojiPickerSection
@@ -60,8 +60,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed, Ref } from 'vue';
-import { i18n } from '../i18n.js';
 import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
+import { i18n } from '@/i18n.js';
 import { customEmojis } from '@/custom-emojis.js';
 import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
 
@@ -87,7 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 	elm.title = getEmojiName(emoji) ?? emoji;
 }
 
-function nestedChosen(emoji: any, ev?: MouseEvent) {
+function nestedChosen(emoji: any, ev: MouseEvent) {
 	emit('chosen', emoji, ev);
 }
 </script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 7f5fcfcdbc..1db03a5eb9 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</section>
 
 		<div v-if="tab === 'index'" class="group index">
-			<section v-if="showPinned && pinned.length > 0">
+			<section v-if="showPinned && (pinned && pinned.length > 0)">
 				<div class="body">
 					<button
 						v-for="emoji in pinned"
@@ -340,7 +340,7 @@ watch(q, () => {
 });
 
 function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
-	return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
+	return ((emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction?.includes(r.id)))) ?? false;
 }
 
 function focus() {
diff --git a/packages/frontend/src/components/MkFileCaptionEditWindow.vue b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
index b799fb9447..b13eea70bb 100644
--- a/packages/frontend/src/components/MkFileCaptionEditWindow.vue
+++ b/packages/frontend/src/components/MkFileCaptionEditWindow.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:withOkButton="true"
 	:okButtonDisabled="false"
 	@ok="ok()"
-	@close="dialog.close()"
+	@close="dialog?.close()"
 	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.describeFile }}</template>
@@ -48,6 +48,6 @@ const caption = ref(props.default);
 
 async function ok() {
 	emit('done', caption.value);
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index eb0d4d61ac..579ca31b70 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div>
 	<MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }">
 		<MkA
-			v-for="file in items"
+			v-for="file in (items as Misskey.entities.DriveFile[])"
 			:key="file.id"
 			v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${dateString(file.createdAt)}\nby ${file.user ? '@' + Misskey.acct.toString(file.user) : 'system'}`"
 			:to="`/admin/file/${file.id}`"
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 65afc48f06..0edc51ff1c 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div ref="el" :class="$style.root">
+<div ref="rootEl" :class="$style.root">
 	<header :class="$style.header" class="_button" :style="{ background: bg }" @click="showBody = !showBody">
 		<div :class="$style.title"><div><slot name="header"></slot></div></div>
 		<div :class="$style.divider"></div>
@@ -14,7 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</button>
 	</header>
 	<Transition
-		:name="defaultStore.state.animation ? 'folder-toggle' : ''"
+		:enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''"
+		:leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''"
+		:enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''"
+		:leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''"
 		@enter="enter"
 		@afterEnter="afterEnter"
 		@leave="leave"
@@ -42,8 +45,8 @@ const props = withDefaults(defineProps<{
 	expanded: true,
 });
 
-const el = shallowRef<HTMLDivElement>();
-const bg = ref<string | null>(null);
+const rootEl = shallowRef<HTMLDivElement>();
+const bg = ref<string>();
 const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
 
 watch(showBody, () => {
@@ -52,40 +55,44 @@ watch(showBody, () => {
 	}
 });
 
-function enter(el: Element) {
+function enter(element: Element) {
+	const el = element as HTMLElement;
 	const elementHeight = el.getBoundingClientRect().height;
-	el.style.height = 0;
+	el.style.height = '0';
 	el.offsetHeight; // reflow
 	el.style.height = elementHeight + 'px';
 }
 
-function afterEnter(el: Element) {
-	el.style.height = null;
+function afterEnter(element: Element) {
+	const el = element as HTMLElement;
+	el.style.height = 'unset';
 }
 
-function leave(el: Element) {
+function leave(element: Element) {
+	const el = element as HTMLElement;
 	const elementHeight = el.getBoundingClientRect().height;
 	el.style.height = elementHeight + 'px';
 	el.offsetHeight; // reflow
-	el.style.height = 0;
+	el.style.height = '0';
 }
 
-function afterLeave(el: Element) {
-	el.style.height = null;
+function afterLeave(element: Element) {
+	const el = element as HTMLElement;
+	el.style.height = 'unset';
 }
 
 onMounted(() => {
-	function getParentBg(el: HTMLElement | null): string {
+	function getParentBg(el?: HTMLElement | null): string {
 		if (el == null || el.tagName === 'BODY') return 'var(--bg)';
-		const bg = el.style.background || el.style.backgroundColor;
-		if (bg) {
-			return bg;
+		const background = el.style.background || el.style.backgroundColor;
+		if (background) {
+			return background;
 		} else {
 			return getParentBg(el.parentElement);
 		}
 	}
 
-	const rawBg = getParentBg(el.value);
+	const rawBg = getParentBg(rootEl.value);
 	const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 	_bg.setAlpha(0.85);
 	bg.value = _bg.toRgbString();
@@ -93,14 +100,12 @@ onMounted(() => {
 </script>
 
 <style lang="scss" module>
-.folder-toggle-enter-active, .folder-toggle-leave-active {
+.folderToggleEnterActive, .folderToggleLeaveActive {
 	overflow-y: clip;
 	transition: opacity 0.5s, height 0.5s !important;
 }
-.folder-toggle-enter-from {
-	opacity: 0;
-}
-.folder-toggle-leave-to {
+
+.folderToggleEnterFrom, .folderToggleLeaveTo {
 	opacity: 0;
 }
 
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 03621a4255..9c62c37ee8 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</template>
 
-		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
+		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
 			<Transition
 				:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
 				:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
@@ -109,7 +109,7 @@ function toggle() {
 
 onMounted(() => {
 	const computedStyle = getComputedStyle(document.documentElement);
-	const parentBg = getBgColor(rootEl.value.parentElement);
+	const parentBg = getBgColor(rootEl.value!.parentElement!);
 	const myBg = computedStyle.getPropertyValue('--panel');
 	bgSame.value = parentBg === myBg;
 });
diff --git a/packages/frontend/src/components/MkForgotPassword.vue b/packages/frontend/src/components/MkForgotPassword.vue
index 9b57688a02..4a0a35b4cf 100644
--- a/packages/frontend/src/components/MkForgotPassword.vue
+++ b/packages/frontend/src/components/MkForgotPassword.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="dialog"
 	:width="370"
 	:height="400"
-	@close="dialog.close()"
+	@close="dialog?.close()"
 	@closed="emit('closed')"
 >
 	<template #header>{{ i18n.ts.forgotPassword }}</template>
@@ -66,6 +66,6 @@ async function onSubmit() {
 		email: email.value,
 	});
 	emit('done');
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 2095a1dcea..61e23a0a42 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -40,11 +40,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkSwitch>
 				<MkSelect v-else-if="form[item].type === 'enum'" v-model="values[item]">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
-					<option v-for="item in form[item].enum" :key="item.value" :value="item.value">{{ item.label }}</option>
+					<option v-for="option in form[item].enum" :key="option.value" :value="option.value">{{ option.label }}</option>
 				</MkSelect>
 				<MkRadios v-else-if="form[item].type === 'radio'" v-model="values[item]">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
-					<option v-for="item in form[item].options" :key="item.value" :value="item.value">{{ item.label }}</option>
+					<option v-for="option in form[item].options" :key="option.value" :value="option.value">{{ option.label }}</option>
 				</MkRadios>
 				<MkRange v-else-if="form[item].type === 'range'" v-model="values[item]" :min="form[item].min" :max="form[item].max" :step="form[item].step" :textConverter="form[item].textConverter">
 					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ i18n.ts.optional }})</span></template>
@@ -86,6 +86,7 @@ const emit = defineEmits<{
 		canceled?: boolean;
 		result?: any;
 	}): void;
+	(ev: 'closed'): void;
 }>();
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
@@ -99,13 +100,13 @@ function ok() {
 	emit('done', {
 		result: values,
 	});
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 function cancel() {
 	emit('done', {
 		canceled: true,
 	});
-	dialog.value.close();
+	dialog.value?.close();
 }
 </script>
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 316632b1a6..0d8612fe26 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 					leaveActiveClass: $style.transition_toggle_leaveActive,
 					leaveToClass: $style.transition_toggle_leaveTo,
 				}"
-				:src="post.files[0].thumbnailUrl"
-				:hash="post.files[0].blurhash"
+				:src="post.files?.[0]?.thumbnailUrl"
+				:hash="post.files?.[0]?.blurhash"
 				:forceBlurhash="!show"
 			/>
 		</Transition>
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index a77f3627f9..336e127725 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -35,10 +35,10 @@ const props = withDefaults(defineProps<{
 	label: '',
 });
 
-const rootEl = shallowRef<HTMLDivElement>(null);
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const rootEl = shallowRef<HTMLDivElement | null>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
 const now = new Date();
-let chartInstance: Chart = null;
+let chartInstance: Chart | null = null;
 const fetching = ref(true);
 
 const { handler: externalTooltipHandler } = useChartTooltip({
@@ -46,6 +46,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
 });
 
 async function renderChart() {
+	if (rootEl.value == null) return;
 	if (chartInstance) {
 		chartInstance.destroy();
 	}
@@ -64,7 +65,7 @@ async function renderChart() {
 		return new Date(y, m, d - ago);
 	};
 
-	const format = (arr) => {
+	const format = (arr: number[]) => {
 		return arr.map((v, i) => {
 			const dt = getDate(i);
 			const iso = `${dt.getFullYear()}-${(dt.getMonth() + 1).toString().padStart(2, '0')}-${dt.getDate().toString().padStart(2, '0')}`;
@@ -77,7 +78,7 @@ async function renderChart() {
 		});
 	};
 
-	let values;
+	let values: number[] = [];
 
 	if (props.src === 'active-users') {
 		const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
@@ -114,25 +115,25 @@ async function renderChart() {
 
 	const marginEachCell = 4;
 
+	if (chartEl.value == null) return;
+
 	chartInstance = new Chart(chartEl.value, {
 		type: 'matrix',
 		data: {
 			datasets: [{
 				label: props.label,
-				data: format(values),
-				pointRadius: 0,
+				data: format(values) as any,
 				borderWidth: 0,
-				borderJoinStyle: 'round',
 				borderRadius: 3,
 				backgroundColor(c) {
-					const value = c.dataset.data[c.dataIndex].v;
+					// @ts-expect-error TS(2339)
+					const value = c.dataset.data[c.dataIndex].v as number;
 					let a = (value - min) / max;
 					if (value !== 0) { // 0でない限りは完全に不可視にはしない
 						a = Math.max(a, 0.05);
 					}
 					return alpha(color, a);
 				},
-				fill: true,
 				width(c) {
 					const a = c.chart.chartArea ?? {};
 					return (a.right - a.left) / weeks - marginEachCell;
@@ -206,11 +207,13 @@ async function renderChart() {
 					enabled: false,
 					callbacks: {
 						title(context) {
-							const v = context[0].dataset.data[context[0].dataIndex];
-							return v.d;
+							// @ts-expect-error TS(2339)
+							return context[0].dataset.data[context[0].dataIndex].d;
 						},
 						label(context) {
 							const v = context.dataset.data[context.dataIndex];
+
+							// @ts-expect-error TS(2339)
 							return [v.v];
 						},
 					},
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index 942861e1f4..03f5106aa3 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -73,7 +73,7 @@ const props = withDefaults(defineProps<{
 		leaveFromClass?: string;
 	} | null;
 	src?: string | null;
-	hash?: string;
+	hash?: string | null;
 	alt?: string | null;
 	title?: string | null;
 	height?: number;
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index b4b4e1b0b7..aff08673e3 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -88,17 +88,18 @@ const focused = ref(false);
 const changed = ref(false);
 const invalid = ref(false);
 const filled = computed(() => v.value !== '' && v.value != null);
-const inputEl = shallowRef<HTMLElement>();
+const inputEl = shallowRef<HTMLInputElement>();
 const prefixEl = shallowRef<HTMLElement>();
 const suffixEl = shallowRef<HTMLElement>();
 const height =
 	props.small ? 33 :
 	props.large ? 39 :
 	36;
-let autocomplete: Autocomplete;
+let autocompleteWorker: Autocomplete | null = null;
 
-const focus = () => inputEl.value.focus();
-const onInput = (ev: KeyboardEvent) => {
+const focus = () => inputEl.value?.focus();
+const onInput = (event: Event) => {
+	const ev = event as KeyboardEvent;
 	changed.value = true;
 	emit('change', ev);
 };
@@ -115,9 +116,9 @@ const onKeydown = (ev: KeyboardEvent) => {
 const updated = () => {
 	changed.value = false;
 	if (type.value === 'number') {
-		emit('update:modelValue', parseFloat(v.value));
+		emit('update:modelValue', typeof v.value === 'number' ? v.value : parseFloat(v.value ?? '0'));
 	} else {
-		emit('update:modelValue', v.value);
+		emit('update:modelValue', v.value ?? '');
 	}
 };
 
@@ -127,7 +128,7 @@ watch(modelValue, newValue => {
 	v.value = newValue;
 });
 
-watch(v, newValue => {
+watch(v, () => {
 	if (!props.manualSave) {
 		if (props.debounce) {
 			debouncedUpdated();
@@ -136,12 +137,14 @@ watch(v, newValue => {
 		}
 	}
 
-	invalid.value = inputEl.value.validity.badInput;
+	invalid.value = inputEl.value?.validity.badInput ?? true;
 });
 
 // このコンポーネントが作成された時、非表示状態である場合がある
 // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
 useInterval(() => {
+	if (inputEl.value == null) return;
+
 	if (prefixEl.value) {
 		if (prefixEl.value.offsetWidth) {
 			inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -163,15 +166,15 @@ onMounted(() => {
 			focus();
 		}
 	});
-	
-	if (props.mfmAutocomplete) {
-		autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+
+	if (props.mfmAutocomplete && inputEl.value) {
+		autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
 	}
 });
 
 onUnmounted(() => {
-	if (autocomplete) {
-		autocomplete.detach();
+	if (autocompleteWorker) {
+		autocompleteWorker.detach();
 	}
 });
 
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 00f5e96286..6c24156c2f 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -138,7 +138,8 @@ function createDoughnut(chartEl, tooltip, data) {
 				},
 			},
 			onClick: (ev) => {
-				const hit = chartInstance.getElementsAtEventForMode(ev, 'nearest', { intersect: true }, false)[0];
+				if (ev.native == null) return;
+				const hit = chartInstance.getElementsAtEventForMode(ev.native, 'nearest', { intersect: true }, false)[0];
 				if (hit && data[hit.index].onClick) {
 					data[hit.index].onClick();
 				}
@@ -164,23 +165,46 @@ function createDoughnut(chartEl, tooltip, data) {
 
 onMounted(() => {
 	misskeyApiGet('federation/stats', { limit: 30 }).then(fedStats => {
-		createDoughnut(subDoughnutEl.value, externalTooltipHandler1, fedStats.topSubInstances.map(x => ({
+		type ChartData = {
+			name: string,
+			color: string | null,
+			value: number,
+			onClick?: () => void,
+		}[];
+
+		const subs: ChartData = fedStats.topSubInstances.map(x => ({
 			name: x.host,
 			color: x.themeColor,
 			value: x.followersCount,
 			onClick: () => {
 				os.pageWindow(`/instance-info/${x.host}`);
 			},
-		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowersCount }]));
+		}));
 
-		createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, fedStats.topPubInstances.map(x => ({
+		subs.push({
+			name: '(other)',
+			color: '#80808080',
+			value: fedStats.otherFollowersCount,
+		});
+
+		createDoughnut(subDoughnutEl.value, externalTooltipHandler1, subs);
+
+		const pubs: ChartData = fedStats.topPubInstances.map(x => ({
 			name: x.host,
 			color: x.themeColor,
 			value: x.followingCount,
 			onClick: () => {
 				os.pageWindow(`/instance-info/${x.host}`);
 			},
-		})).concat([{ name: '(other)', color: '#80808080', value: fedStats.otherFollowingCount }]));
+		}));
+
+		pubs.push({
+			name: '(other)',
+			color: '#80808080',
+			value: fedStats.otherFollowingCount,
+		});
+
+		createDoughnut(pubDoughnutEl.value, externalTooltipHandler2, pubs);
 	});
 });
 </script>
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index e358a1c549..88a85cde57 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -18,9 +18,9 @@ import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 
 const props = defineProps<{
 	instance?: {
-		faviconUrl?: string
-		name: string
-		themeColor?: string
+		faviconUrl?: string | null
+		name?: string | null
+		themeColor?: string | null
 	}
 }>();
 
@@ -30,7 +30,7 @@ const instance = props.instance ?? {
 	themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
 };
 
-const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
+const faviconUrl = computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? '/favicon.ico');
 
 const themeColor = instance.themeColor ?? '#777777';
 
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 0a2385ff25..79b2b91154 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')">
 	<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
 		<div class="main">
 			<template v-for="item in items" :key="item.text">
@@ -63,7 +63,7 @@ const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k =>
 }));
 
 function close() {
-	modal.value.close();
+	modal.value?.close();
 }
 </script>
 
diff --git a/packages/frontend/src/components/MkMarquee.vue b/packages/frontend/src/components/MkMarquee.vue
index 145b60c8e7..8b4066fb68 100644
--- a/packages/frontend/src/components/MkMarquee.vue
+++ b/packages/frontend/src/components/MkMarquee.vue
@@ -30,6 +30,7 @@ export default {
 		const contentEl = ref<HTMLElement>();
 
 		function calc() {
+			if (contentEl.value == null) return;
 			const eachLength = contentEl.value.offsetWidth / props.repeat;
 			const factor = 3000;
 			const duration = props.duration / ((1 / eachLength) * factor);
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 39352014b0..a52f2a0e05 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -273,7 +273,7 @@ onDeactivated(() => {
 
 .hidden {
 	width: 100%;
-	background: none;
+	background: #000;
 	border: none;
 	outline: none;
 	font: inherit;
@@ -283,7 +283,6 @@ onDeactivated(() => {
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	background: #000;
 }
 
 .hiddenTextWrapper {
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 8f73018734..942da91c5c 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -54,7 +54,7 @@ const count = computed(() => props.mediaList.filter(media => previewable(media))
 let lightbox: PhotoSwipeLightbox | null;
 
 const popstateHandler = (): void => {
-	if (lightbox.pswp && lightbox.pswp.isOpen === true) {
+	if (lightbox?.pswp && lightbox.pswp.isOpen === true) {
 		lightbox.pswp.close();
 	}
 };
@@ -69,7 +69,10 @@ async function calcAspectRatio() {
 		return;
 	}
 
-	const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
+	const ratioMax = (ratio: number) => {
+		if (img.properties.width == null || img.properties.height == null) return '';
+		return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
+	};
 
 	switch (defaultStore.state.mediaListWithOneImageAppearance) {
 		case '16_9':
@@ -145,7 +148,7 @@ onMounted(() => {
 		// element is children
 		const { element } = itemData;
 
-		const id = element.dataset.id;
+		const id = element?.dataset.id;
 		const file = props.mediaList.find(media => media.id === id);
 		if (!file) return;
 
@@ -155,14 +158,14 @@ onMounted(() => {
 		if (file.properties.orientation != null && file.properties.orientation >= 5) {
 			[itemData.w, itemData.h] = [itemData.h, itemData.w];
 		}
-		itemData.msrc = file.thumbnailUrl;
+		itemData.msrc = file.thumbnailUrl ?? undefined;
 		itemData.alt = file.comment ?? undefined;
 		itemData.comment = file.comment;
 		itemData.thumbCropped = true;
 	});
 
 	lightbox.on('uiRegister', () => {
-		lightbox.pswp.ui.registerElement({
+		lightbox?.pswp?.ui?.registerElement({
 			name: 'altText',
 			className: 'pwsp__alt-text-container',
 			appendTo: 'wrapper',
@@ -178,7 +181,7 @@ onMounted(() => {
 						textBox.style.display = 'none';
 					}
 
-					textBox.textContent = pwsp.currSlide.data.comment;
+					textBox.textContent = pwsp.currSlide?.data.comment;
 				});
 			},
 		});
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 91a5ddcbe7..3b8d43c85b 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -396,7 +396,8 @@ onDeactivated(() => {
 
 .hidden {
 	width: 100%;
-	background: none;
+	height: 100%;
+	background: #000;
 	border: none;
 	outline: none;
 	font: inherit;
@@ -406,7 +407,6 @@ onDeactivated(() => {
 	display: flex;
 	align-items: center;
 	justify-content: center;
-	background: #000;
 }
 
 .hiddenTextWrapper {
@@ -466,7 +466,6 @@ onDeactivated(() => {
 	grid-template-columns: auto auto 1fr auto auto;
 	align-items: center;
 	gap: 4px 8px;
-	pointer-events: none;
 
 	padding: 35px 10px 10px 10px;
 	background: linear-gradient(rgba(0, 0, 0, 0),rgba(0, 0, 0, .75));
diff --git a/packages/frontend/src/components/MkMenu.child.vue b/packages/frontend/src/components/MkMenu.child.vue
index 962dcd91eb..929d39519b 100644
--- a/packages/frontend/src/components/MkMenu.child.vue
+++ b/packages/frontend/src/components/MkMenu.child.vue
@@ -33,6 +33,7 @@ const align = 'left';
 const SCROLLBAR_THICKNESS = 16;
 
 function setPosition() {
+	if (el.value == null) return;
 	const rootRect = props.rootElement.getBoundingClientRect();
 	const parentRect = props.targetElement.getBoundingClientRect();
 	const myRect = el.value.getBoundingClientRect();
@@ -66,7 +67,7 @@ const ro = new ResizeObserver((entries, observer) => {
 });
 
 onMounted(() => {
-	ro.observe(el.value);
+	if (el.value) ro.observe(el.value);
 	setPosition();
 	nextTick(() => {
 		setPosition();
@@ -79,7 +80,7 @@ onUnmounted(() => {
 
 defineExpose({
 	checkHit: (ev: MouseEvent) => {
-		return (ev.target === el.value || el.value.contains(ev.target));
+		return (ev.target === el.value || el.value?.contains(ev.target as Node));
 	},
 });
 </script>
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index eb6e7935ed..ad3162bad5 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
 		@contextmenu.self="e => e.preventDefault()"
 	>
-		<template v-for="(item, i) in items2">
+		<template v-for="(item, i) in (items2 ?? [])">
 			<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
 			<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
 				<span style="opacity: 0.7;">{{ item.text }}</span>
@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<span :class="$style.caret" style="pointer-events: none;"><i class="ph-caret-right ph-bold ph-lg ti-fw"></i></span>
 				</div>
 			</button>
-			<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
+			<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: getValue(item.active) }]" :disabled="getValue(item.active)" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
 				<div :class="$style.item_content">
@@ -63,18 +63,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</button>
 		</template>
-		<span v-if="items2.length === 0" :class="[$style.none, $style.item]">
+		<span v-if="items2 == null || items2.length === 0" :class="[$style.none, $style.item]">
 			<span>{{ i18n.ts.none }}</span>
 		</span>
 	</div>
 	<div v-if="childMenu">
-		<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/>
+		<XChild ref="child" :items="childMenu" :targetElement="childTarget!" :rootElement="itemsEl!" showing @actioned="childActioned" @close="close(false)"/>
 	</div>
 </div>
 </template>
 
 <script lang="ts">
-import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
+import { ComputedRef, computed, defineAsyncComponent, isRef, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
 import { focusPrev, focusNext } from '@/scripts/focus.js';
 import MkSwitchButton from '@/components/MkSwitch.button.vue';
 import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu.js';
@@ -104,7 +104,7 @@ const emit = defineEmits<{
 
 const itemsEl = shallowRef<HTMLDivElement>();
 
-const items2 = ref<InnerMenuItem[]>([]);
+const items2 = ref<InnerMenuItem[]>();
 
 const child = shallowRef<InstanceType<typeof XChild>>();
 
@@ -119,15 +119,15 @@ const childShowingItem = ref<MenuItem | null>();
 let preferClick = isTouchUsing || props.asDrawer;
 
 watch(() => props.items, () => {
-	const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
+	const items = [...props.items].filter(item => item !== undefined) as (NonNullable<MenuItem> | MenuPending)[];
 
 	for (let i = 0; i < items.length; i++) {
 		const item = items[i];
 
-		if (item && 'then' in item) { // if item is Promise
+		if ('then' in item) { // if item is Promise
 			items[i] = { type: 'pending' };
 			item.then(actualItem => {
-				items2.value[i] = actualItem;
+				if (items2.value?.[i]) items2.value[i] = actualItem;
 			});
 		}
 	}
@@ -151,7 +151,7 @@ function childActioned() {
 }
 
 const onGlobalMousedown = (event: MouseEvent) => {
-	if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target))) return;
+	if (childTarget.value && (event.target === childTarget.value || childTarget.value.contains(event.target as Node))) return;
 	if (child.value && child.value.checkHit(event)) return;
 	closeChild();
 };
@@ -169,7 +169,7 @@ function onItemMouseLeave(item) {
 }
 
 async function showChildren(item: MenuParent, ev: MouseEvent) {
-	const children = await (async () => {
+	const children: MenuItem[] = await (async () => {
 		if (childrenCache.has(item)) {
 			return childrenCache.get(item)!;
 		} else {
@@ -189,7 +189,7 @@ async function showChildren(item: MenuParent, ev: MouseEvent) {
 		});
 		emit('hide');
 	} else {
-		childTarget.value = ev.currentTarget ?? ev.target;
+		childTarget.value = (ev.currentTarget ?? ev.target) as HTMLElement;
 		// これでもリアクティビティは保たれる
 		childMenu.value = children;
 		childShowingItem.value = item;
@@ -218,6 +218,10 @@ function switchItem(item: MenuSwitch & { ref: any }) {
 	item.ref = !item.ref;
 }
 
+function getValue<T>(item?: ComputedRef<T> | T) {
+	return isRef(item) ? item.value : item;
+}
+
 onMounted(() => {
 	if (props.viaKeyboard) {
 		nextTick(() => {
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index f0a2c232bd..bf36c230c9 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 		stroke-width="2"
 	/>
 	<circle
-		:cx="headX"
-		:cy="headY"
+		:cx="headX ?? undefined"
+		:cy="headY ?? undefined"
 		r="3"
 		:fill="color"
 	/>
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index b91988304d..cbac469151 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -51,7 +51,7 @@ const bodyWidth = ref(0);
 const bodyHeight = ref(0);
 
 const close = () => {
-	modal.value.close();
+	modal.value?.close();
 };
 
 const onBgClick = () => {
@@ -67,11 +67,13 @@ const onKeydown = (evt) => {
 };
 
 const ro = new ResizeObserver((entries, observer) => {
+	if (rootEl.value == null || headerEl.value == null) return;
 	bodyWidth.value = rootEl.value.offsetWidth;
 	bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
 });
 
 onMounted(() => {
+	if (rootEl.value == null || headerEl.value == null) return;
 	bodyWidth.value = rootEl.value.offsetWidth;
 	bodyHeight.value = rootEl.value.offsetHeight - headerEl.value.offsetHeight;
 	ro.observe(rootEl.value);
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 824818756c..bdad1fc1fe 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div
 	v-if="!hardMuted && muted === false"
 	v-show="!isDeleted"
-	ref="el"
+	ref="rootEl"
 	v-hotkey="keymap"
 	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
 	:tabindex="!isDeleted ? '-1' : undefined"
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						/>
 						<div v-if="translating || translation" :class="$style.translation">
 							<MkLoading v-if="translating" mini/>
-							<div v-else>
+							<div v-else-if="translation">
 								<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
 								<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
 							</div>
@@ -82,10 +82,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
 						<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
 					</div>
-					<div v-if="appearNote.files.length > 0">
+					<div v-if="appearNote.files && appearNote.files.length > 0">
 						<MkMediaList :mediaList="appearNote.files" @click.stop/>
 					</div>
-					<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
+					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" @click.stop/>
 					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/>
 					<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
 					<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
@@ -145,7 +145,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
 					<i class="ph-paperclip ph-bold ph-lg"></i>
 				</button>
-				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
+				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
 					<i class="ph-dots-three ph-bold ph-lg"></i>
 				</button>
 			</footer>
@@ -215,7 +215,7 @@ import { MenuItem } from '@/types/menu.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -254,7 +254,7 @@ if (noteViewInterruptors.length > 0) {
 		let result: Misskey.entities.Note | null = deepClone(note.value);
 		for (const interruptor of noteViewInterruptors) {
 			try {
-				result = await interruptor.handler(result);
+				result = await interruptor.handler(result!) as Misskey.entities.Note | null;
 				if (result === null) {
 					isDeleted.value = true;
 					return;
@@ -263,7 +263,7 @@ if (noteViewInterruptors.length > 0) {
 				console.error(err);
 			}
 		}
-		note.value = result;
+		note.value = result as Misskey.entities.Note;
 	});
 }
 
@@ -271,11 +271,11 @@ const isRenote = (
 	note.value.renote != null &&
 	note.value.text == null &&
 	note.value.cw == null &&
-	note.value.fileIds.length === 0 &&
+	note.value.fileIds && note.value.fileIds.length === 0 &&
 	note.value.poll == null
 );
 
-const el = shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
 const menuButton = shallowRef<HTMLElement>();
 const menuVersionsButton = shallowRef<HTMLElement>();
 const renoteButton = shallowRef<HTMLElement>();
@@ -301,8 +301,13 @@ const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hard
 const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
 const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
-const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
+const renoteCollapsed = ref(
+	defaultStore.state.collapseRenotes && isRenote && (
+		($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
+		(appearNote.value.myReaction != null)
+	)
+);
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
 const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
 const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
@@ -327,11 +332,11 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renoteButton.value.renote(true),
+	'q': () => renote(appearNote.value.visibility),
 	'up|k|shift+tab': focusBefore,
 	'down|j|tab': focusAfter,
 	'esc': blur,
-	'm|o': () => menu(true),
+	'm|o': () => showMenu(true),
 	's': () => showContent.value !== showContent.value,
 };
 
@@ -348,7 +353,7 @@ if (props.mock) {
 	}, { deep: true });
 } else {
 	useNoteCapture({
-		rootEl: el,
+		rootEl: rootEl,
 		note: appearNote,
 		pureNote: note,
 		isDeletedRef: isDeleted,
@@ -521,7 +526,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -543,7 +548,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -571,7 +576,7 @@ function reply(viaKeyboard = false): void {
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
 		animation: !viaKeyboard,
-	}, () => {
+	}).then(() => {
 		focus();
 	});
 }
@@ -610,7 +615,7 @@ function react(viaKeyboard = false): void {
 			noteId: appearNote.value.id,
 			override: defaultLike.value,
 		});
-		const el = reactButton.value as HTMLElement | null | undefined;
+		const el = reactButton.value;
 		if (el) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
@@ -619,7 +624,7 @@ function react(viaKeyboard = false): void {
 		}
 	} else {
 		blur();
-		reactionPicker.show(reactButton.value, reaction => {
+		reactionPicker.show(reactButton.value ?? null, reaction => {
 			sound.playMisskeySfx('reaction');
 
 			if (props.mock) {
@@ -640,8 +645,8 @@ function react(viaKeyboard = false): void {
 	}
 }
 
-function undoReact(note): void {
-	const oldReaction = note.myReaction;
+function undoReact(targetNote: Misskey.entities.Note): void {
+	const oldReaction = targetNote.myReaction;
 	if (!oldReaction) return;
 
 	if (props.mock) {
@@ -650,7 +655,7 @@ function undoReact(note): void {
 	}
 
 	misskeyApi('notes/reactions/delete', {
-		noteId: note.id,
+		noteId: targetNote.id,
 	});
 }
 
@@ -678,32 +683,34 @@ function onContextmenu(ev: MouseEvent): void {
 		return;
 	}
 
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === 'A') return true;
 		// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
 		if (el.tagName === 'AUDIO') return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== '') return;
+
+	if (ev.target && isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== '') return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
-		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
 		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
-function menu(viaKeyboard = false): void {
+function showMenu(viaKeyboard = false): void {
 	if (props.mock) {
 		return;
 	}
 
-	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
 	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
 	}).then(focus).finally(cleanup);
@@ -757,7 +764,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 			getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
 			{ type: 'divider' },
 			getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
-			$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
+			($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
 		], renoteTime.value, {
 			viaKeyboard: viaKeyboard,
 		});
@@ -777,19 +784,19 @@ function animatedMFM() {
 }
 
 function focus() {
-	el.value.focus();
+	rootEl.value?.focus();
 }
 
 function blur() {
-	el.value.blur();
+	rootEl.value?.blur();
 }
 
 function focusBefore() {
-	focusPrev(el.value);
+	focusPrev(rootEl.value ?? null);
 }
 
 function focusAfter() {
-	focusNext(el.value);
+	focusNext(rootEl.value ?? null);
 }
 
 function readPromo() {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 137b38b4d6..de028f4c34 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div
 	v-if="!muted"
 	v-show="!isDeleted"
-	ref="el"
+	ref="rootEl"
 	v-hotkey="keymap"
 	:class="$style.root"
 >
@@ -88,17 +88,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
 				<div v-if="translating || translation" :class="$style.translation">
 					<MkLoading v-if="translating" mini/>
-					<div v-else>
+					<div v-else-if="translation">
 						<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
 						<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
 					</div>
 				</div>
 				<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
 				<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
-				<div v-if="appearNote.files.length > 0">
+				<div v-if="appearNote.files && appearNote.files.length > 0">
 					<MkMediaList :mediaList="appearNote.files"/>
 				</div>
-				<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
+				<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
 				<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
 				<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
 			</div>
@@ -154,7 +154,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
 				<i class="ph-paperclip ph-bold ph-lg"></i>
 			</button>
-			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
+			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
 				<i class="ph-dots-three ph-bold ph-lg"></i>
 			</button>
 		</footer>
@@ -254,7 +254,7 @@ import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination from '@/components/MkPagination.vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 
@@ -273,7 +273,7 @@ if (noteViewInterruptors.length > 0) {
 		let result: Misskey.entities.Note | null = deepClone(note.value);
 		for (const interruptor of noteViewInterruptors) {
 			try {
-				result = await interruptor.handler(result);
+				result = await interruptor.handler(result!) as Misskey.entities.Note | null;
 				if (result === null) {
 					isDeleted.value = true;
 					return;
@@ -282,18 +282,18 @@ if (noteViewInterruptors.length > 0) {
 				console.error(err);
 			}
 		}
-		note.value = result;
+		note.value = result as Misskey.entities.Note;
 	});
 }
 
 const isRenote = (
 	note.value.renote != null &&
 	note.value.text == null &&
-	note.value.fileIds.length === 0 &&
+	note.value.fileIds && note.value.fileIds.length === 0 &&
 	note.value.poll == null
 );
 
-const el = shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
 const menuButton = shallowRef<HTMLElement>();
 const menuVersionsButton = shallowRef<HTMLElement>();
 const renoteButton = shallowRef<HTMLElement>();
@@ -320,7 +320,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
 const conversation = ref<Misskey.entities.Note[]>([]);
 const replies = ref<Misskey.entities.Note[]>([]);
 const quotes = ref<Misskey.entities.Note[]>([]);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
 
 watch(() => props.expandAllCws, (expandAllCws) => {
@@ -340,9 +340,9 @@ if ($i) {
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renoteButton.value.renote(true),
+	'q': () => renote(appearNote.value.visibility),
 	'esc': blur,
-	'm|o': () => menu(true),
+	'm|o': () => showMenu(true),
 	's': () => showContent.value !== showContent.value,
 };
 
@@ -356,7 +356,7 @@ provide('react', (reaction: string) => {
 const tab = ref('replies');
 const reactionTabType = ref<string | null>(null);
 
-const renotesPagination = computed(() => ({
+const renotesPagination = computed<Paging>(() => ({
 	endpoint: 'notes/renotes',
 	limit: 10,
 	params: {
@@ -364,7 +364,7 @@ const renotesPagination = computed(() => ({
 	},
 }));
 
-const reactionsPagination = computed(() => ({
+const reactionsPagination = computed<Paging>(() => ({
 	endpoint: 'notes/reactions',
 	limit: 10,
 	params: {
@@ -387,7 +387,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
 }
 
 useNoteCapture({
-	rootEl: el,
+	rootEl: rootEl,
 	note: appearNote,
 	pureNote: note,
 	isDeletedRef: isDeleted,
@@ -540,7 +540,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -562,7 +562,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -588,7 +588,7 @@ function reply(viaKeyboard = false): void {
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
 		animation: !viaKeyboard,
-	}, () => {
+	}).then(() => {
 		focus();
 	});
 }
@@ -612,7 +612,7 @@ function react(viaKeyboard = false): void {
 		}
 	} else {
 		blur();
-		reactionPicker.show(reactButton.value, reaction => {
+		reactionPicker.show(reactButton.value ?? null, reaction => {
 			sound.playMisskeySfx('reaction');
 
 			misskeyApi('notes/reactions/create', {
@@ -671,26 +671,28 @@ function undoRenote() : void {
 }
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === 'A') return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== '') return;
+
+	if (ev.target && isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== '') return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
-		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
+		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
 		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
-function menu(viaKeyboard = false): void {
-	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
+function showMenu(viaKeyboard = false): void {
+	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
 	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
 	}).then(focus).finally(cleanup);
@@ -726,11 +728,11 @@ function showRenoteMenu(viaKeyboard = false): void {
 }
 
 function focus() {
-	el.value.focus();
+	rootEl.value?.focus();
 }
 
 function blur() {
-	el.value.blur();
+	rootEl.value?.blur();
 }
 
 const repliesLoaded = ref(false);
@@ -767,6 +769,7 @@ const conversationLoaded = ref(false);
 
 function loadConversation() {
 	conversationLoaded.value = true;
+	if (appearNote.value.replyId == null) return;
 	misskeyApi('notes/conversation', {
 		noteId: appearNote.value.replyId,
 	}).then(res => {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index 6121db3f8f..d103ddf0ae 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
 	<div :class="$style.username"><MkAcct :user="note.user"/></div>
 	<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
-		<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
+		<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
 	</div>
 	<div :class="$style.info">
 		<div v-if="mock">
diff --git a/packages/frontend/src/components/MkNotePreview.vue b/packages/frontend/src/components/MkNotePreview.vue
index c517bc6800..cc658c35ae 100644
--- a/packages/frontend/src/components/MkNotePreview.vue
+++ b/packages/frontend/src/components/MkNotePreview.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 		<div>
 			<p v-if="useCw" :class="$style.cw">
-				<Mfm v-if="cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
+				<Mfm v-if="cw != null && cw != ''" :text="cw" :author="user" :nyaize="'respect'" :i="user" style="margin-right: 8px;"/>
 				<MkCwButton v-model="showContent" :text="text.trim()" :files="files" :poll="poll" style="margin: 4px 0;"/>
 			</p>
 			<div v-show="!useCw || showContent">
@@ -26,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 
 const showContent = ref(false);
@@ -33,12 +34,7 @@ const showContent = ref(false);
 const props = defineProps<{
 	text: string;
 	files: Misskey.entities.DriveFile[];
-	poll?: {
-		choices: string[];
-		multiple: boolean;
-		expiresAt: string | null;
-		expiredAfter: string | null;
-	};
+	poll?: PollEditorModelValue;
 	useCw: boolean;
 	cw: string | null;
 	user: Misskey.entities.User;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 0018924804..fbb8d34646 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -6,10 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<div :class="$style.head">
-		<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
-		<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
-		<MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
-		<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
+		<MkAvatar v-if="['pollEnded', 'note'].includes(notification.type) && notification.note" :class="$style.icon" :user="notification.note.user" link preview/>
+		<MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
 		<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
 		<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ph-rocket-launch ph-bold ph-lg" style="line-height: 1;"></i></div>
 		<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
@@ -26,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				[$style.t_quote]: notification.type === 'quote',
 				[$style.t_pollEnded]: notification.type === 'pollEnded',
 				[$style.t_achievementEarned]: notification.type === 'achievementEarned',
+				[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
 			}]"
 		>
 			<i v-if="notification.type === 'follow'" class="ph-plus ph-bold ph-lg"></i>
@@ -37,12 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<i v-else-if="notification.type === 'quote'" class="ph-quotes ph-bold ph-lg"></i>
 			<i v-else-if="notification.type === 'pollEnded'" class="ph-chart-bar-horizontal ph-bold ph-lg"></i>
 			<i v-else-if="notification.type === 'achievementEarned'" class="ph-trophy ph-bold ph-lg"></i>
-			<img v-else-if="notification.type === 'roleAssigned'" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
+			<template v-else-if="notification.type === 'roleAssigned'">
+				<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
+				<i v-else class="ph-seal-check ph-bold ph-lg"></i>
+			</template>
 			<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
 			<MkReactionIcon
 				v-else-if="notification.type === 'reaction'"
 				:withTooltip="true"
-				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
+				:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
 				:noStyle="true"
 				style="width: 100%; height: 100%;"
 			/>
@@ -55,10 +57,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
 			<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
 			<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
-			<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
+			<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
 			<span v-else-if="notification.type === 'reaction:grouped'">{{ i18n.tsx._notification.reactedBySomeUsers({ n: notification.reactions.length }) }}</span>
 			<span v-else-if="notification.type === 'renote:grouped'">{{ i18n.tsx._notification.renotedBySomeUsers({ n: notification.users.length }) }}</span>
-			<span v-else>{{ notification.header }}</span>
+			<span v-else-if="notification.type === 'app'">{{ notification.header }}</span>
 			<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
 		</header>
 		<div>
@@ -97,7 +99,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 			<template v-else-if="notification.type === 'follow'">
 				<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
-				<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div>
 			</template>
 			<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
 			<template v-else-if="notification.type === 'receiveFollowRequest'">
@@ -113,12 +114,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</span>
 
 			<div v-if="notification.type === 'reaction:grouped'">
-				<div v-for="reaction of notification.reactions" :class="$style.reactionsItem">
+				<div v-for="reaction of notification.reactions" :key="reaction.user.id + reaction.reaction" :class="$style.reactionsItem">
 					<MkAvatar :class="$style.reactionsItemAvatar" :user="reaction.user" link preview/>
 					<div :class="$style.reactionsItemReaction">
 						<MkReactionIcon
 							:withTooltip="true"
-							:reaction="reaction.reaction ? reaction.reaction.replace(/^:(\w+):$/, ':$1@.:') : reaction.reaction"
+							:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
 							:noStyle="true"
 							style="width: 100%; height: 100%;"
 						/>
@@ -126,7 +127,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</div>
 			</div>
 			<div v-else-if="notification.type === 'renote:grouped'">
-				<div v-for="user of notification.users" :class="$style.reactionsItem">
+				<div v-for="user of notification.users" :key="user.id" :class="$style.reactionsItem">
 					<MkAvatar :class="$style.reactionsItemAvatar" :user="user" link preview/>
 				</div>
 			</div>
@@ -139,16 +140,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
-import MkFollowButton from '@/components/MkFollowButton.vue';
 import MkButton from '@/components/MkButton.vue';
 import { getNoteSummary } from '@/scripts/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
 import { infoImageUrl } from '@/instance.js';
 
+const $i = signinRequired();
+
 const props = withDefaults(defineProps<{
 	notification: Misskey.entities.Notification;
 	withTime?: boolean;
@@ -161,11 +163,13 @@ const props = withDefaults(defineProps<{
 const followRequestDone = ref(false);
 
 const acceptFollowRequest = () => {
+	if (props.notification.user == null) return;
 	followRequestDone.value = true;
 	misskeyApi('following/requests/accept', { userId: props.notification.user.id });
 };
 
 const rejectFollowRequest = () => {
+	if (props.notification.user == null) return;
 	followRequestDone.value = true;
 	misskeyApi('following/requests/reject', { userId: props.notification.user.id });
 };
@@ -283,6 +287,12 @@ const rejectFollowRequest = () => {
 	pointer-events: none;
 }
 
+.t_roleAssigned {
+	padding: 3px;
+	background: #88a6b7;
+	pointer-events: none;
+}
+
 .tail {
 	flex: 1;
 	min-width: 0;
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index a157820d56..cc1f9296fb 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<template #default="{ items: notifications }">
 			<MkDateSeparatedList v-if="defaultStore.state.noteDesign === 'misskey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
+				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
 				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
 			</MkDateSeparatedList>
 			<MkDateSeparatedList v-else-if="defaultStore.state.noteDesign === 'sharkey'" v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-				<SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
+				<SkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id + ':note'" :note="notification.note" :withHardMute="true"/>
 				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
 			</MkDateSeparatedList>
 		</template>
@@ -68,7 +68,7 @@ function onNotification(notification) {
 	}
 
 	if (!isMuted) {
-		pagingComponent.value.prepend(notification);
+		pagingComponent.value?.prepend(notification);
 	}
 }
 
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 702bb95dc7..50271d9481 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -27,7 +27,7 @@ const omitted = ref(false);
 const ignoreOmit = ref(false);
 
 const calcOmit = () => {
-	if (omitted.value || ignoreOmit.value) return;
+	if (omitted.value || ignoreOmit.value || content.value == null) return;
 	omitted.value = content.value.offsetHeight > props.maxHeight;
 };
 
@@ -37,7 +37,7 @@ const omitObserver = new ResizeObserver((entries, observer) => {
 
 onMounted(() => {
 	calcOmit();
-	omitObserver.observe(content.value);
+	omitObserver.observe(content.value as HTMLElement);
 });
 
 onUnmounted(() => {
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 6c8a0e56a6..f7fdf30322 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</header>
 		<p v-if="page.summary" :title="page.summary">{{ page.summary.length > 85 ? page.summary.slice(0, 85) + '…' : page.summary }}</p>
 		<footer>
-			<img class="icon" :src="page.user.avatarUrl"/>
+			<img v-if="page.user.avatarUrl" class="icon" :src="page.user.avatarUrl"/>
 			<p>{{ userName(page.user) }}</p>
 		</footer>
 	</article>
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 2696baa0d6..cb2db0b6a5 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -41,8 +41,8 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.j
 import { openingWindowsCount } from '@/os.js';
 import { claimAchievement } from '@/scripts/achievements.js';
 import { getScrollContainer } from '@/scripts/scroll.js';
-import { useRouterFactory } from '@/global/router/supplier.js';
-import { mainRouter } from '@/global/router/main.js';
+import { useRouterFactory } from '@/router/supplier.js';
+import { mainRouter } from '@/router/main.js';
 
 const props = defineProps<{
 	initialPath: string;
@@ -55,7 +55,7 @@ defineEmits<{
 const routerFactory = useRouterFactory();
 const windowRouter = routerFactory(props.initialPath);
 
-const contents = shallowRef<HTMLElement>();
+const contents = shallowRef<HTMLElement | null>(null);
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
 const history = ref<{ path: string; key: any; }[]>([{
@@ -63,7 +63,7 @@ const history = ref<{ path: string; key: any; }[]>([{
 	key: windowRouter.getCurrentKey(),
 }]);
 const buttonsLeft = computed(() => {
-	const buttons = [];
+	const buttons: Record<string, unknown>[] = [];
 
 	if (history.value.length > 1) {
 		buttons.push({
@@ -93,6 +93,13 @@ windowRouter.addListener('push', ctx => {
 	history.value.push({ path: ctx.path, key: ctx.key });
 });
 
+windowRouter.addListener('replace', ctx => {
+	history.value.pop();
+	history.value.push({ path: ctx.path, key: ctx.key });
+});
+
+windowRouter.init();
+
 provide('router', windowRouter);
 provideMetadataReceiver((info) => {
 	pageMetadata.value = info;
@@ -115,7 +122,7 @@ const contextmenu = computed(() => ([{
 	text: i18n.ts.openInNewTab,
 	action: () => {
 		window.open(url + windowRouter.getCurrentPath(), '_blank', 'noopener');
-		windowEl.value.close();
+		windowEl.value?.close();
 	},
 }, {
 	icon: 'ph-link ph-bold ph-lg',
@@ -135,17 +142,17 @@ function reload() {
 }
 
 function close() {
-	windowEl.value.close();
+	windowEl.value?.close();
 }
 
 function expand() {
 	mainRouter.push(windowRouter.getCurrentPath(), 'forcePage');
-	windowEl.value.close();
+	windowEl.value?.close();
 }
 
 function popout() {
-	_popout(windowRouter.getCurrentPath(), windowEl.value.$el);
-	windowEl.value.close();
+	_popout(windowRouter.getCurrentPath(), windowEl.value?.$el);
+	windowEl.value?.close();
 }
 
 useScrollPositionManager(() => getScrollContainer(contents.value), windowRouter);
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index f5b238046a..4553156c25 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -204,7 +204,7 @@ async function init(): Promise<void> {
 	queue.value = new Map();
 	fetching.value = true;
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
-	await misskeyApi(props.pagination.endpoint, {
+	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: props.pagination.limit ?? 10,
 		allowPartial: true,
@@ -240,7 +240,7 @@ const fetchMore = async (): Promise<void> => {
 	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 	moreFetching.value = true;
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
-	await misskeyApi(props.pagination.endpoint, {
+	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
 		...(props.pagination.offsetMode ? {
@@ -304,7 +304,7 @@ const fetchMoreAhead = async (): Promise<void> => {
 	if (!more.value || fetching.value || moreFetching.value || items.value.size === 0) return;
 	moreFetching.value = true;
 	const params = props.pagination.params ? isRef(props.pagination.params) ? props.pagination.params.value : props.pagination.params : {};
-	await misskeyApi(props.pagination.endpoint, {
+	await misskeyApi<MisskeyEntity[]>(props.pagination.endpoint, {
 		...params,
 		limit: SECOND_FETCH_LIMIT,
 		...(props.pagination.offsetMode ? {
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 3779fa47ff..8fdfa25d93 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="{ [$style.done]: closed || isVoted }">
 	<ul :class="$style.choices">
-		<li v-for="(choice, i) in note.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
+		<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
 			<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
 			<span :class="$style.fg">
 				<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg" style="margin-right: 4px; color: var(--accent);"></i></template>
@@ -17,13 +17,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</ul>
 	<p v-if="!readOnly" :class="$style.info">
 		<span>{{ i18n.tsx._poll.totalVotes({ n: total }) }}</span>
-		<span v-if="note.poll.multiple"> · </span>
-		<span v-if="note.poll.multiple" style="color: var(--accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span>
+		<span v-if="poll.multiple"> · </span>
+		<span v-if="poll.multiple" style="color: var(--accent); font-weight: bolder;">{{ i18n.ts._poll.multiple }}</span>
 		<span> · </span>
 		<a v-if="!closed && !isVoted" style="color: inherit;" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
 		<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
 		<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
-		<span v-if="!isLocal"><span> · </span><a @click.stop="refresh">{{ i18n.ts.reload }}</a></span>
 		<span v-if="remaining > 0"> · {{ timer }}</span>
 	</p>
 </div>
@@ -38,36 +37,35 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { useInterval } from '@/scripts/use-interval.js';
-import { WithNonNullable } from '@/type.js';
 
 const props = defineProps<{
-	note: WithNonNullable<Misskey.entities.Note, 'poll'>;
+	noteId: string;
+	poll: NonNullable<Misskey.entities.Note['poll']>;
 	readOnly?: boolean;
 }>();
 
 const remaining = ref(-1);
 
-const total = computed(() => sum(props.note.poll.choices.map(x => x.votes)));
+const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
 const closed = computed(() => remaining.value === 0);
-const isLocal = computed(() => !props.note.uri);
-const isVoted = computed(() => !props.note.poll.multiple && props.note.poll.choices.some(c => c.isVoted));
+const isVoted = computed(() => !props.poll.multiple && props.poll.choices.some(c => c.isVoted));
 const timer = computed(() => i18n.tsx._poll[
-		remaining.value >= 86400 ? 'remainingDays' :
-		remaining.value >= 3600 ? 'remainingHours' :
-		remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
-	]({
-		s: Math.floor(remaining.value % 60),
-		m: Math.floor(remaining.value / 60) % 60,
-		h: Math.floor(remaining.value / 3600) % 24,
-		d: Math.floor(remaining.value / 86400),
-	}));
+	remaining.value >= 86400 ? 'remainingDays' :
+	remaining.value >= 3600 ? 'remainingHours' :
+	remaining.value >= 60 ? 'remainingMinutes' : 'remainingSeconds'
+]({
+	s: Math.floor(remaining.value % 60),
+	m: Math.floor(remaining.value / 60) % 60,
+	h: Math.floor(remaining.value / 3600) % 24,
+	d: Math.floor(remaining.value / 86400),
+}));
 
 const showResult = ref(props.readOnly || isVoted.value);
 
 // 期限付きアンケート
-if (props.note.poll.expiresAt) {
+if (props.poll.expiresAt) {
 	const tick = () => {
-		remaining.value = Math.floor(Math.max(new Date(props.note.poll.expiresAt).getTime() - Date.now(), 0) / 1000);
+		remaining.value = Math.floor(Math.max(new Date(props.poll.expiresAt!).getTime() - Date.now(), 0) / 1000);
 		if (remaining.value === 0) {
 			showResult.value = true;
 		}
@@ -83,34 +81,26 @@ const vote = async (id) => {
 	pleaseLogin();
 
 	if (props.readOnly || closed.value || isVoted.value) return;
-	if (!props.note.poll.multiple) {
+	if (!props.poll.multiple) {
 		const { canceled } = await os.confirm({
 			type: 'question',
-			text: i18n.tsx.voteConfirm({ choice: props.note.poll.choices[id].text }),
+			text: i18n.tsx.voteConfirm({ choice: props.poll.choices[id].text }),
 		});
 		if (canceled) return;
 	} else {
 		const { canceled } = await os.confirm({
 			type: 'question',
-			text: i18n.tsx.voteConfirmMulti({ choice: props.note.poll.choices[id].text }),
+			text: i18n.tsx.voteConfirmMulti({ choice: props.poll.choices[id].text }),
 		});
 		if (canceled) return;
 	}
 
 	await misskeyApi('notes/polls/vote', {
-		noteId: props.note.id,
+		noteId: props.noteId,
 		choice: id,
 	});
-	if (!showResult.value) showResult.value = !props.note.poll.multiple;
+	if (!showResult.value) showResult.value = !props.poll.multiple;
 };
-
-async function refresh() {
-	if (!props.note.uri) return;
-	const obj = await os.apiWithDialog('ap/show', { uri: props.note.uri });
-	if (obj.type === 'Note' && obj.object.poll) {
-		props.note.poll = obj.object.poll; // eslint-disable-line vue/no-mutating-props
-	}
-}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 47557e1225..ff6246917f 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -62,21 +62,18 @@ import { formatDateTimeString } from '@/scripts/format-time-string.js';
 import { addTime } from '@/scripts/time.js';
 import { i18n } from '@/i18n.js';
 
+export type PollEditorModelValue = {
+	expiresAt: number | null;
+	expiredAfter: number | null;
+	choices: string[];
+	multiple: boolean;
+};
+
 const props = defineProps<{
-	modelValue: {
-		expiresAt: string;
-		expiredAfter: number;
-		choices: string[];
-		multiple: boolean;
-	};
+	modelValue: PollEditorModelValue;
 }>();
 const emit = defineEmits<{
-	(ev: 'update:modelValue', v: {
-		expiresAt: string;
-		expiredAfter: number;
-		choices: string[];
-		multiple: boolean;
-	}): void;
+	(ev: 'update:modelValue', v: PollEditorModelValue): void;
 }>();
 
 const choices = ref(props.modelValue.choices);
@@ -89,7 +86,9 @@ const unit = ref('second');
 
 if (props.modelValue.expiresAt) {
 	expiration.value = 'at';
-	atDate.value = atTime.value = props.modelValue.expiresAt;
+	const expiresAt = new Date(props.modelValue.expiresAt);
+	atDate.value = formatDateTimeString(expiresAt, 'yyyy-MM-dd');
+	atTime.value = formatDateTimeString(expiresAt, 'HH:mm');
 } else if (typeof props.modelValue.expiredAfter === 'number') {
 	expiration.value = 'after';
 	after.value = props.modelValue.expiredAfter / 1000;
@@ -113,20 +112,21 @@ function remove(i) {
 	choices.value = choices.value.filter((_, _i) => _i !== i);
 }
 
-function get() {
+function get(): PollEditorModelValue {
 	const calcAt = () => {
 		return new Date(`${atDate.value} ${atTime.value}`).getTime();
 	};
 
 	const calcAfter = () => {
-		let base = parseInt(after.value);
+		let base = parseInt(after.value.toString());
 		switch (unit.value) {
+			// @ts-expect-error fallthrough
 			case 'day': base *= 24;
-				// fallthrough
+			// @ts-expect-error fallthrough
 			case 'hour': base *= 60;
-				// fallthrough
+			// @ts-expect-error fallthrough
 			case 'minute': base *= 60;
-				// fallthrough
+			// eslint-disable-next-line no-fallthrough
 			case 'second': return base *= 1000;
 			default: return null;
 		}
@@ -135,10 +135,8 @@ function get() {
 	return {
 		choices: choices.value,
 		multiple: multiple.value,
-		...(
-			expiration.value === 'at' ? { expiresAt: calcAt() } :
-			expiration.value === 'after' ? { expiredAfter: calcAfter() } : {}
-		),
+		expiresAt: expiration.value === 'at' ? calcAt() : null,
+		expiredAfter: expiration.value === 'after' ? calcAfter() : null,
 	};
 }
 
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index e3d8a6c0d4..65ffb7b7a5 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<button v-tooltip="i18n.ts.useCw" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: useCw }]" @click="useCw = !useCw"><i class="ph-eye-slash ph-bold ph-lg"></i></button>
 			<button v-tooltip="i18n.ts.mention" class="_button" :class="$style.footerButton" @click="insertMention"><i class="ph-at ph-bold ph-lg"></i></button>
 			<button v-tooltip="i18n.ts.hashtags" class="_button" :class="[$style.footerButton, { [$style.footerButtonActive]: withHashtags }]" @click="withHashtags = !withHashtags"><i class="ph-hash ph-bold ph-lg"></i></button>
-			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugin" class="_button" :class="$style.footerButton" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button>
+			<button v-if="postFormActions.length > 0" v-tooltip="i18n.ts.plugins" class="_button" :class="$style.footerButton" @click="showActions"><i class="ph-plug ph-bold ph-lg"></i></button>
 			<button v-tooltip="i18n.ts.emoji" :class="['_button', $style.footerButton]" @click="insertEmoji"><i class="ph-smiley ph-bold ph-lg"></i></button>
 			<button v-if="showAddMfmFunction" v-tooltip="i18n.ts.addMfmFunction" :class="['_button', $style.footerButton]" @click="insertMfmFunction"><i class="ph-palette ph-bold ph-lg"></i></button>
 		</div>
@@ -109,7 +109,7 @@ import { toASCII } from 'punycode/';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkNotePreview from '@/components/MkNotePreview.vue';
 import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
-import MkPollEditor from '@/components/MkPollEditor.vue';
+import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
 import { host, url } from '@/config.js';
 import { erase, unique } from '@/scripts/array.js';
 import { extractMentions } from '@/scripts/extract-mentions.js';
@@ -140,13 +140,13 @@ const props = withDefaults(defineProps<{
 	renote?: Misskey.entities.Note;
 	channel?: Misskey.entities.Channel; // TODO
 	mention?: Misskey.entities.User;
-	specified?: Misskey.entities.User;
+	specified?: Misskey.entities.UserDetailed;
 	initialText?: string;
 	initialCw?: string;
 	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
 	initialFiles?: Misskey.entities.DriveFile[];
 	initialLocalOnly?: boolean;
-	initialVisibleUsers?: Misskey.entities.User[];
+	initialVisibleUsers?: Misskey.entities.UserDetailed[];
 	initialNote?: Misskey.entities.Note;
 	instant?: boolean;
 	fixed?: boolean;
@@ -180,12 +180,7 @@ const posting = ref(false);
 const posted = ref(false);
 const text = ref(props.initialText ?? '');
 const files = ref(props.initialFiles ?? []);
-const poll = ref<{
-	choices: string[];
-	multiple: boolean;
-	expiresAt: string | null;
-	expiredAfter: string | null;
-} | null>(null);
+const poll = ref<PollEditorModelValue | null>(null);
 const useCw = ref<boolean>(!!props.initialCw);
 const showPreview = ref(defaultStore.state.showPreview);
 watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
@@ -334,7 +329,7 @@ if (props.reply && ['home', 'followers', 'specified'].includes(props.reply.visib
 	if (visibility.value === 'specified') {
 		if (props.reply.visibleUserIds) {
 			misskeyApi('users/show', {
-				userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply.userId),
+				userIds: props.reply.visibleUserIds.filter(uid => uid !== $i.id && uid !== props.reply?.userId),
 			}).then(users => {
 				users.forEach(pushVisibleUser);
 			});
@@ -540,7 +535,7 @@ async function toggleReactionAcceptance() {
 	reactionAcceptance.value = select.result;
 }
 
-function pushVisibleUser(user) {
+function pushVisibleUser(user: Misskey.entities.UserDetailed) {
 	if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
 		visibleUsers.value.push(user);
 	}
@@ -582,10 +577,12 @@ function onCompositionEnd(ev: CompositionEvent) {
 
 async function onPaste(ev: ClipboardEvent) {
 	if (props.mock) return;
+	if (!ev.clipboardData) return;
 
-	for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) {
+	for (const { item, i } of Array.from(ev.clipboardData.items, (data, x) => ({ item: data, i: x }))) {
 		if (item.kind === 'file') {
 			const file = item.getAsFile();
+			if (!file) continue;
 			const lio = file.name.lastIndexOf('.');
 			const ext = lio >= 0 ? file.name.slice(lio) : '';
 			const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
@@ -607,7 +604,7 @@ async function onPaste(ev: ClipboardEvent) {
 				return;
 			}
 
-			quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)[1];
+			quoteId.value = paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ?? null;
 		});
 	}
 }
@@ -638,26 +635,26 @@ function onDragover(ev) {
 	}
 }
 
-function onDragenter(ev) {
+function onDragenter() {
 	draghover.value = true;
 }
 
-function onDragleave(ev) {
+function onDragleave() {
 	draghover.value = false;
 }
 
-function onDrop(ev): void {
+function onDrop(ev: DragEvent): void {
 	draghover.value = false;
 
 	// ファイルだったら
-	if (ev.dataTransfer.files.length > 0) {
+	if (ev.dataTransfer && ev.dataTransfer.files.length > 0) {
 		ev.preventDefault();
 		for (const x of Array.from(ev.dataTransfer.files)) upload(x);
 		return;
 	}
 
 	//#region ドライブのファイル
-	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
+	const driveFile = ev.dataTransfer?.getData(_DATA_TRANSFER_DRIVE_FILE_);
 	if (driveFile != null && driveFile !== '') {
 		const file = JSON.parse(driveFile);
 		files.value.push(file);
@@ -705,11 +702,14 @@ async function post(ev?: MouseEvent) {
 	}
 
 	if (ev) {
-		const el = ev.currentTarget ?? ev.target;
-		const rect = el.getBoundingClientRect();
-		const x = rect.left + (el.offsetWidth / 2);
-		const y = rect.top + (el.offsetHeight / 2);
-		os.popup(MkRippleEffect, { x, y }, {}, 'end');
+		const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
+
+		if (el) {
+			const rect = el.getBoundingClientRect();
+			const x = rect.left + (el.offsetWidth / 2);
+			const y = rect.top + (el.offsetHeight / 2);
+			os.popup(MkRippleEffect, { x, y }, {}, 'end');
+		}
 	}
 
 	if (props.mock) return;
@@ -779,18 +779,18 @@ async function post(ev?: MouseEvent) {
 	if (notePostInterruptors.length > 0) {
 		for (const interruptor of notePostInterruptors) {
 			try {
-				postData = await interruptor.handler(deepClone(postData));
+				postData = await interruptor.handler(deepClone(postData)) as typeof postData;
 			} catch (err) {
 				console.error(err);
 			}
 		}
 	}
 
-	let token = undefined;
+	let token: string | undefined = undefined;
 
 	if (postAccount.value) {
 		const storedAccounts = await getAccounts();
-		token = storedAccounts.find(x => x.id === postAccount.value.id)?.token;
+		token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
 	}
 
 	posting.value = true;
@@ -804,7 +804,7 @@ async function post(ev?: MouseEvent) {
 			deleteDraft();
 			emit('posted');
 			if (postData.text && postData.text !== '') {
-				const hashtags_ = mfm.parse(postData.text).filter(x => x.type === 'hashtag').map(x => x.props.hashtag);
+				const hashtags_ = mfm.parse(postData.text).map(x => x.type === 'hashtag' && x.props.hashtag).filter(x => x) as string[];
 				const history = JSON.parse(miLocalStorage.getItem('hashtags') ?? '[]') as string[];
 				miLocalStorage.setItem('hashtags', JSON.stringify(unique(hashtags_.concat(history))));
 			}
@@ -867,16 +867,17 @@ function cancel() {
 }
 
 function insertMention() {
-	os.selectUser().then(user => {
+	os.selectUser({ localOnly: localOnly.value, includeSelf: true }).then(user => {
 		insertTextAtCursor(textareaEl.value, '@' + Misskey.acct.toString(user) + ' ');
 	});
 }
 
 async function insertEmoji(ev: MouseEvent) {
 	textAreaReadOnly.value = true;
-
+	const target = ev.currentTarget ?? ev.target;
+	if (target == null) return;
 	emojiPicker.show(
-		ev.currentTarget ?? ev.target,
+		target as HTMLElement,
 		emoji => {
 			insertTextAtCursor(textareaEl.value, emoji);
 		},
@@ -888,6 +889,7 @@ async function insertEmoji(ev: MouseEvent) {
 }
 
 async function insertMfmFunction(ev: MouseEvent) {
+	if (textareaEl.value == null) return;
 	mfmFunctionPicker(
 		ev.currentTarget ?? ev.target,
 		textareaEl.value,
@@ -895,14 +897,15 @@ async function insertMfmFunction(ev: MouseEvent) {
 	);
 }
 
-function showActions(ev) {
+function showActions(ev: MouseEvent) {
 	os.popupMenu(postFormActions.map(action => ({
 		text: action.title,
 		action: () => {
 			action.handler({
 				text: text.value,
 				cw: cw.value,
-			}, (key, value) => {
+			}, (key, value: any) => {
+				if (typeof key !== 'string') return;
 				if (key === 'text') { text.value = value; }
 				if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
 			});
@@ -939,9 +942,9 @@ onMounted(() => {
 	}
 
 	// TODO: detach when unmount
-	new Autocomplete(textareaEl.value, text);
-	new Autocomplete(cwInputEl.value, cw);
-	new Autocomplete(hashtagsInputEl.value, hashtags);
+	if (textareaEl.value) new Autocomplete(textareaEl.value, text);
+	if (cwInputEl.value) new Autocomplete(cwInputEl.value, cw);
+	if (hashtagsInputEl.value) new Autocomplete(hashtagsInputEl.value, hashtags);
 
 	nextTick(() => {
 		// 書きかけの投稿を復元
@@ -971,19 +974,19 @@ onMounted(() => {
 		if (props.initialNote) {
 			const init = props.initialNote;
 			text.value = init.text ? init.text : '';
-			files.value = init.files;
-			cw.value = init.cw;
+			files.value = init.files ?? [];
+			cw.value = init.cw ?? null;
 			useCw.value = init.cw != null;
 			if (init.poll) {
 				poll.value = {
 					choices: init.poll.choices.map(x => x.text),
 					multiple: init.poll.multiple,
-					expiresAt: init.poll.expiresAt ? new Date(init.poll.expiresAt).getTime().toString() : null,
-					expiredAfter: init.poll.expiredAfter ? new Date(init.poll.expiredAfter).getTime().toString() : null,
+					expiresAt: init.poll.expiresAt ? (new Date(init.poll.expiresAt)).getTime() : null,
+					expiredAfter: null,
 				};
 			}
 			visibility.value = init.visibility;
-			localOnly.value = init.localOnly;
+			localOnly.value = init.localOnly ?? false;
 			quoteId.value = init.renote ? init.renote.id : null;
 		}
 
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index c94eaeef42..aaca63da4c 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -93,7 +93,7 @@ async function rename(file) {
 	const { canceled, result } = await os.inputText({
 		title: i18n.ts.enterFileName,
 		default: file.name,
-		allowEmpty: false,
+		minLength: 1,
 	});
 	if (canceled) return;
 	misskeyApi('drive/files/update', {
diff --git a/packages/frontend/src/components/MkPostFormDialog.vue b/packages/frontend/src/components/MkPostFormDialog.vue
index cd25077bfb..f4e1777ae1 100644
--- a/packages/frontend/src/components/MkPostFormDialog.vue
+++ b/packages/frontend/src/components/MkPostFormDialog.vue
@@ -4,8 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" :preferType="'dialog'" @click="modal.close()" @closed="onModalClosed()">
-	<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal.close()" @esc="modal.close()"/>
+<MkModal ref="modal" :preferType="'dialog'" @click="modal?.close()" @closed="onModalClosed()">
+	<MkPostForm ref="form" :class="$style.form" v-bind="props" autofocus freezeAfterPosted @posted="onPosted" @cancel="modal?.close()" @esc="modal?.close()"/>
 </MkModal>
 </template>
 
@@ -20,13 +20,13 @@ const props = defineProps<{
 	renote?: Misskey.entities.Note;
 	channel?: any; // TODO
 	mention?: Misskey.entities.User;
-	specified?: Misskey.entities.User;
+	specified?: Misskey.entities.UserDetailed;
 	initialText?: string;
 	initialCw?: string;
-	initialVisibility?: typeof Misskey.noteVisibilities;
+	initialVisibility?: (typeof Misskey.noteVisibilities)[number];
 	initialFiles?: Misskey.entities.DriveFile[];
 	initialLocalOnly?: boolean;
-	initialVisibleUsers?: Misskey.entities.User[];
+	initialVisibleUsers?: Misskey.entities.UserDetailed[];
 	initialNote?: Misskey.entities.Note;
 	instant?: boolean;
 	fixed?: boolean;
@@ -42,7 +42,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
 const form = shallowRef<InstanceType<typeof MkPostForm>>();
 
 function onPosted() {
-	modal.value.close({
+	modal.value?.close({
 		useSendAnimation: true,
 	});
 }
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 1b8263ae67..de9f752c34 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -126,7 +126,7 @@ async function unsubscribe() {
 }
 
 function encode(buffer: ArrayBuffer | null) {
-	return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+	return btoa(String.fromCharCode.apply(null, buffer ? new Uint8Array(buffer) as any : []));
 }
 
 /**
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 22e7ed1ef7..01bc517057 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -38,7 +38,7 @@ export default defineComponent({
 			h('div', {
 				class: 'body',
 			}, options.map(option => h(MkRadio, {
-				key: option.key,
+				key: option.key as string,
 				value: option.props?.value,
 				modelValue: value.value,
 				'onUpdate:modelValue': _v => value.value = _v,
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index e8760e1946..4c588d8edc 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -86,7 +86,7 @@ onMounted(() => {
 	ro = new ResizeObserver((entries, observer) => {
 		calcThumbPosition();
 	});
-	ro.observe(containerEl.value);
+	if (containerEl.value) ro.observe(containerEl.value);
 });
 
 onUnmounted(() => {
@@ -122,7 +122,7 @@ const onMousedown = (ev: MouseEvent | TouchEvent) => {
 	const onDrag = (ev: MouseEvent | TouchEvent) => {
 		ev.preventDefault();
 		const containerRect = containerEl.value!.getBoundingClientRect();
-		const pointerX = ev.touches && ev.touches.length > 0 ? ev.touches[0].clientX : ev.clientX;
+		const pointerX = 'touches' in ev && ev.touches.length > 0 ? ev.touches[0].clientX : 'clientX' in ev ? ev.clientX : 0;
 		const pointerPositionOnContainer = pointerX - (containerRect.left + (thumbWidth / 2));
 		rawValue.value = Math.min(1, Math.max(0, pointerPositionOnContainer / (containerEl.value!.offsetWidth - thumbWidth)));
 
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index afb7d9bd46..e43841e5c9 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, inject, onMounted, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
 import XDetails from '@/components/MkReactionsViewer.details.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
-import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
 import * as os from '@/os.js';
 import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
 import { useTooltip } from '@/scripts/use-tooltip.js';
@@ -102,7 +102,7 @@ async function toggleReaction() {
 
 async function menu(ev) {
 	if (!canToggle.value) return;
-	if (!props.reaction.includes(":")) return;
+	if (!props.reaction.includes(':')) return;
 	os.popupMenu([{
 		text: i18n.ts.info,
 		icon: 'ph-info ph-bold ph-lg',
@@ -117,8 +117,7 @@ async function menu(ev) {
 }
 
 function anime() {
-	if (document.hidden) return;
-	if (!defaultStore.state.animation) return;
+	if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return;
 
 	const rect = buttonEl.value.getBoundingClientRect();
 	const x = rect.left + 16;
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index ef497e0e82..c9cf79015a 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -23,10 +23,9 @@ import { initChart } from '@/scripts/init-chart.js';
 
 initChart();
 
-const rootEl = shallowRef<HTMLDivElement>(null);
-const chartEl = shallowRef<HTMLCanvasElement>(null);
-const now = new Date();
-let chartInstance: Chart = null;
+const rootEl = shallowRef<HTMLDivElement | null>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
+let chartInstance: Chart | null = null;
 const fetching = ref(true);
 
 const { handler: externalTooltipHandler } = useChartTooltip({
@@ -34,6 +33,7 @@ const { handler: externalTooltipHandler } = useChartTooltip({
 });
 
 async function renderChart() {
+	if (rootEl.value == null) return;
 	if (chartInstance) {
 		chartInstance.destroy();
 	}
@@ -47,7 +47,12 @@ async function renderChart() {
 
 	raw = raw.slice(0, maxDays + 1);
 
-	const data = [];
+	const data: {
+		x: number;
+		y: string;
+		v: number;
+	}[] = [];
+
 	for (const record of raw) {
 		data.push({
 			x: 0,
@@ -83,19 +88,20 @@ async function renderChart() {
 
 	const marginEachCell = 12;
 
+	if (chartEl.value == null) return;
+
 	chartInstance = new Chart(chartEl.value, {
 		type: 'matrix',
 		data: {
 			datasets: [{
 				label: 'Active',
-				data: data,
-				pointRadius: 0,
+				data: data as any,
 				borderWidth: 0,
-				borderJoinStyle: 'round',
 				borderRadius: 3,
 				backgroundColor(c) {
-					const value = c.dataset.data[c.dataIndex].v;
-					const m = max(c.dataset.data[c.dataIndex].y);
+					const v = c.dataset.data[c.dataIndex] as unknown as typeof data[0];
+					const value = v.v;
+					const m = max(v.y);
 					if (m === 0) {
 						return alpha(color, 0);
 					} else {
@@ -103,7 +109,6 @@ async function renderChart() {
 						return alpha(color, a);
 					}
 				},
-				fill: true,
 				width(c) {
 					const a = c.chart.chartArea ?? {};
 					return (a.right - a.left) / maxDays - marginEachCell;
@@ -146,7 +151,6 @@ async function renderChart() {
 				},
 				y: {
 					type: 'time',
-					min: new Date(new Date().getFullYear(), new Date().getMonth(), new Date().getDate() - maxDays),
 					offset: true,
 					reverse: true,
 					position: 'left',
@@ -179,7 +183,7 @@ async function renderChart() {
 							return getYYYYMMDD(new Date(new Date(v.y).getTime() + (v.x * 86400000)));
 						},
 						label(context) {
-							const v = context.dataset.data[context.dataIndex];
+							const v = context.dataset.data[context.dataIndex] as unknown as typeof data[0];
 							const m = max(v.y);
 							if (m === 0) {
 								return [`Active: ${v.v} (-%)`];
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index eb05878ae8..2d8b0714ed 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -20,11 +20,11 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 
 initChart();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
 
 const { handler: externalTooltipHandler } = useChartTooltip();
 
-let chartInstance: Chart;
+let chartInstance: Chart | null = null;
 
 const getYYYYMMDD = (date: Date) => {
 	const y = date.getFullYear().toString().padStart(2, '0');
@@ -47,6 +47,8 @@ onMounted(async () => {
 	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
 	const color = accent.toHex();
 
+	if (chartEl.value == null) return;
+
 	chartInstance = new Chart(chartEl.value, {
 		type: 'line',
 		data: {
@@ -67,7 +69,7 @@ onMounted(async () => {
 					x: (i + 1).toString(),
 					y: (v / record.users) * 100,
 					d: getYYYYMMDD(new Date(record.createdAt)),
-				}))],
+				}))] as any,
 			})),
 		},
 		options: {
@@ -109,11 +111,11 @@ onMounted(async () => {
 					enabled: false,
 					callbacks: {
 						title(context) {
-							const v = context[0].dataset.data[context[0].dataIndex];
+							const v = context[0].dataset.data[context[0].dataIndex] as unknown as { x: string, y: number, d: string };
 							return `${v.x} days later`;
 						},
 						label(context) {
-							const v = context.dataset.data[context.dataIndex];
+							const v = context.dataset.data[context.dataIndex] as unknown as { x: string, y: number, d: string };
 							const p = Math.round(v.y) + '%';
 							return `${v.d} ${p}`;
 						},
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 38c6bf8449..6ac8297c02 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -32,11 +32,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots } from 'vue';
+import { onMounted, nextTick, ref, watch, computed, toRefs, VNode, useSlots, VNodeChild } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { useInterval } from '@/scripts/use-interval.js';
 import { i18n } from '@/i18n.js';
+import { MenuItem } from '@/types/menu.js';
 
 const props = defineProps<{
 	modelValue: string | null;
@@ -52,7 +53,7 @@ const props = defineProps<{
 }>();
 
 const emit = defineEmits<{
-	(ev: 'changeByUser'): void;
+	(ev: 'changeByUser', value: string | null): void;
 	(ev: 'update:modelValue', value: string | null): void;
 }>();
 
@@ -74,7 +75,7 @@ const height =
 	props.large ? 39 :
 	36;
 
-const focus = () => inputEl.value.focus();
+const focus = () => inputEl.value?.focus();
 const onInput = (ev) => {
 	changed.value = true;
 };
@@ -88,17 +89,19 @@ watch(modelValue, newValue => {
 	v.value = newValue;
 });
 
-watch(v, newValue => {
+watch(v, () => {
 	if (!props.manualSave) {
 		updated();
 	}
 
-	invalid.value = inputEl.value.validity.badInput;
+	invalid.value = inputEl.value?.validity.badInput ?? true;
 });
 
 // このコンポーネントが作成された時、非表示状態である場合がある
 // 非表示状態だと要素の幅などは0になってしまうので、定期的に計算する
 useInterval(() => {
+	if (inputEl.value == null) return;
+
 	if (prefixEl.value) {
 		if (prefixEl.value.offsetWidth) {
 			inputEl.value.style.paddingLeft = prefixEl.value.offsetWidth + 'px';
@@ -122,36 +125,37 @@ onMounted(() => {
 	});
 });
 
-function show(ev: MouseEvent) {
+function show() {
 	focused.value = true;
 	opening.value = true;
 
-	const menu = [];
+	const menu: MenuItem[] = [];
 	let options = slots.default!();
 
 	const pushOption = (option: VNode) => {
 		menu.push({
-			text: option.children,
-			active: computed(() => v.value === option.props.value),
+			text: option.children as string,
+			active: computed(() => v.value === option.props?.value),
 			action: () => {
-				v.value = option.props.value;
+				v.value = option.props?.value;
 				emit('changeByUser', v.value);
 			},
 		});
 	};
 
-	const scanOptions = (options: VNode[]) => {
+	const scanOptions = (options: VNodeChild[]) => {
 		for (const vnode of options) {
+			if (typeof vnode !== 'object' || vnode === null || Array.isArray(vnode)) continue;
 			if (vnode.type === 'optgroup') {
 				const optgroup = vnode;
 				menu.push({
 					type: 'label',
-					text: optgroup.props.label,
+					text: optgroup.props?.label,
 				});
-				scanOptions(optgroup.children);
+				if (Array.isArray(optgroup.children)) scanOptions(optgroup.children);
 			} else if (Array.isArray(vnode.children)) { // 何故かフラグメントになってくることがある
 				const fragment = vnode;
-				scanOptions(fragment.children);
+				if (Array.isArray(fragment.children)) scanOptions(fragment.children);
 			} else if (vnode.props == null) { // v-if で条件が false のときにこうなる
 				// nop?
 			} else {
@@ -164,7 +168,7 @@ function show(ev: MouseEvent) {
 	scanOptions(options);
 
 	os.popupMenu(menu, container.value, {
-		width: container.value.offsetWidth,
+		width: container.value?.offsetWidth,
 		onClosing: () => {
 			opening.value = false;
 		},
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 278e2d6054..efbda4a357 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -112,6 +112,7 @@ function onLogin(res: any): Promise<void> | void {
 }
 
 async function queryKey(): Promise<void> {
+	if (credentialRequest.value == null) return;
 	queryingKey.value = true;
 	await webAuthnRequest(credentialRequest.value)
 		.catch(() => {
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 4fba0f184b..7de212f05b 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -84,6 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
 import { toUnicode } from 'punycode/';
+import * as Misskey from 'misskey-js';
 import MkButton from './MkButton.vue';
 import MkInput from './MkInput.vue';
 import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
@@ -101,7 +102,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'signup', user: Record<string, any>): void;
+	(ev: 'signup', user: Misskey.entities.SigninResponse): void;
 	(ev: 'signupEmailPending'): void;
 	(ev: 'approvalPending'): void;
 }>();
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index fa4ce648b1..9e2d7821d5 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -34,8 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<template #label>{{ tosPrivacyPolicyLabel }}</template>
 				<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ph-check ph-bold ph-lg" style="color: var(--success)"></i></template>
 				<div class="_gaps_s">
-					<div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div>
-					<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div>
+					<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div>
+					<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ph-arrow-square-out ph-bold ph-lg"></i></a></div>
 				</div>
 
 				<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 6efdced69f..56af20b04c 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="dialog"
 	:width="500"
 	:height="600"
-	@close="dialog.close()"
+	@close="dialog?.close()"
 	@closed="$emit('closed')"
 >
 	<template #header>{{ i18n.ts.signup }}</template>
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:leaveToClass="$style.transition_x_leaveTo"
 		>
 			<template v-if="!isAcceptedServerRule">
-				<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog.close()"/>
+				<XServerRules @done="isAcceptedServerRule = true" @cancel="dialog?.close()"/>
 			</template>
 			<template v-else>
 				<XSignup :autoSet="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending" @approvalPending="onApprovalPending"/>
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { shallowRef, ref } from 'vue';
-
+import * as Misskey from 'misskey-js';
 import XSignup from '@/components/MkSignupDialog.form.vue';
 import XServerRules from '@/components/MkSignupDialog.rules.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
 });
 
 const emit = defineEmits<{
-	(ev: 'done'): void;
+	(ev: 'done', res: Misskey.entities.SigninResponse): void;
 	(ev: 'closed'): void;
 }>();
 
@@ -55,13 +55,13 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 const isAcceptedServerRule = ref(false);
 
-function onSignup(res) {
+function onSignup(res: Misskey.entities.SigninResponse) {
 	emit('done', res);
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 function onSignupEmailPending() {
-	dialog.value.close();
+	dialog.value?.close();
 }
 
 function onApprovalPending() {
diff --git a/packages/frontend/src/components/MkSparkle.vue b/packages/frontend/src/components/MkSparkle.vue
index 269825e25e..fa7bd96b16 100644
--- a/packages/frontend/src/components/MkSparkle.vue
+++ b/packages/frontend/src/components/MkSparkle.vue
@@ -89,10 +89,11 @@ let ro: ResizeObserver | undefined;
 
 onMounted(() => {
 	ro = new ResizeObserver((entries, observer) => {
-		width.value = el.value?.offsetWidth + 64;
-		height.value = el.value?.offsetHeight + 64;
+		if (el.value == null) return;
+		width.value = el.value.offsetWidth + 64;
+		height.value = el.value.offsetHeight + 64;
 	});
-	ro.observe(el.value);
+	if (el.value) ro.observe(el.value);
 	const add = () => {
 		if (stop) return;
 		const x = (Math.random() * (width.value - 64));
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 51e3ed0613..038f5aa30d 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div :class="[$style.root, { [$style.collapsed]: collapsed }]">
 	<div :class="{ [$style.clickToOpen]: defaultStore.state.clickToOpen }" @click="defaultStore.state.clickToOpen ? noteclick(note.id) : undefined">
 		<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
-		<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
+		<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deletedNote }})</span>
 		<MkA v-if="note.replyId" :class="$style.reply" :to="`/notes/${note.replyId}`" @click.stop><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
 		<Mfm v-if="note.text" :text="note.text" :author="note.user" :nyaize="'respect'" :isAnim="allowAnim" :emojiUrls="note.emojis"/>
 		<MkButton v-if="!allowAnim && animated && !hideFiles" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
@@ -21,13 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 		<MkA v-if="note.renoteId" :class="$style.rp" :to="`/notes/${note.renoteId}`" @click.stop>RN: ...</MkA>
 	</div>
-	<details v-if="note.files.length > 0" :open="!defaultStore.state.collapseFiles && !hideFiles">
+	<details v-if="note.files && note.files.length > 0" :open="!defaultStore.state.collapseFiles && !hideFiles">
 		<summary>({{ i18n.tsx.withNFiles({ n: note.files.length }) }})</summary>
 		<MkMediaList :mediaList="note.files"/>
 	</details>
 	<details v-if="note.poll">
 		<summary>{{ i18n.ts.poll }}</summary>
-		<MkPoll :note="note"/>
+		<MkPoll :noteId="note.id" :poll="note.poll"/>
 	</details>
 	<button v-if="isLong && collapsed" :class="$style.fade" class="_button" @click="collapsed = false">
 		<span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span>
@@ -48,7 +48,7 @@ import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
 import { defaultStore } from '@/store.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
 
diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue
index b82f36cdd3..8338a4a047 100644
--- a/packages/frontend/src/components/MkSwitch.button.vue
+++ b/packages/frontend/src/components/MkSwitch.button.vue
@@ -24,7 +24,7 @@ import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
 	checked: boolean | Ref<boolean>;
-	disabled?: boolean;
+	disabled?: boolean | Ref<boolean>;
 }>(), {
 	disabled: false,
 });
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 2b56b946d2..c58c8d97f5 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -13,18 +13,18 @@ export default defineComponent({
 		},
 	},
 	setup(props, { emit, slots }) {
-		const options = slots.default();
+		const options = slots.default?.() ?? [];
 
 		return () => h('div', {
 			class: 'pxhvhrfw',
 		}, options.map(option => withDirectives(h('button', {
-			class: ['_button', { active: props.modelValue === option.props.value }],
-			key: option.key,
-			disabled: props.modelValue === option.props.value,
+			class: ['_button', { active: props.modelValue === option.props?.value }],
+			key: option.key as string,
+			disabled: props.modelValue === option.props?.value,
 			onClick: () => {
-				emit('update:modelValue', option.props.value);
+				emit('update:modelValue', option.props?.value);
 			},
-		}, option.children), [
+		}, option.children ?? []), [
 			[resolveDirective('click-anime')],
 		])));
 	},
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 083c34906f..cae286058e 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -52,7 +52,7 @@ watch(available, () => {
 });
 
 onMounted(() => {
-	width.value = rootEl.value.offsetWidth;
+	if (rootEl.value) width.value = rootEl.value.offsetWidth;
 
 	if (loaded) {
 		available.value = true;
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 5c70adde11..263ed22bd6 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:readonly="readonly"
 			:placeholder="placeholder"
 			:pattern="pattern"
-			:autocomplete="props.autocomplete"
+			:autocomplete="autocomplete"
 			:spellcheck="spellcheck"
 			@focus="focused = true"
 			@blur="focused = false"
@@ -76,9 +76,9 @@ const invalid = ref(false);
 const filled = computed(() => v.value !== '' && v.value != null);
 const inputEl = shallowRef<HTMLTextAreaElement>();
 const preview = ref(false);
-let autocomplete: Autocomplete;
+let autocompleteWorker: Autocomplete | null = null;
 
-const focus = () => inputEl.value.focus();
+const focus = () => inputEl.value?.focus();
 const onInput = (ev) => {
 	changed.value = true;
 	emit('change', ev);
@@ -111,10 +111,10 @@ const updated = () => {
 const debouncedUpdated = debounce(1000, updated);
 
 watch(modelValue, newValue => {
-	v.value = newValue;
+	v.value = newValue ?? '';
 });
 
-watch(v, newValue => {
+watch(v, () => {
 	if (!props.manualSave) {
 		if (props.debounce) {
 			debouncedUpdated();
@@ -123,7 +123,7 @@ watch(v, newValue => {
 		}
 	}
 
-	invalid.value = inputEl.value.validity.badInput;
+	invalid.value = inputEl.value?.validity.badInput ?? true;
 });
 
 onMounted(() => {
@@ -133,14 +133,14 @@ onMounted(() => {
 		}
 	});
 
-	if (props.mfmAutocomplete) {
-		autocomplete = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? null : props.mfmAutocomplete);
+	if (props.mfmAutocomplete && inputEl.value) {
+		autocompleteWorker = new Autocomplete(inputEl.value, v, props.mfmAutocomplete === true ? undefined : props.mfmAutocomplete);
 	}
 });
 
 onUnmounted(() => {
-	if (autocomplete) {
-		autocomplete.detach();
+	if (autocompleteWorker) {
+		autocompleteWorker.detach();
 	}
 });
 </script>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index f41cf09c29..95bbe71f96 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -18,6 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
+import * as Misskey from 'misskey-js';
 import { ChannelConnection as Connection } from 'misskey-js';
 import MkNotes from '@/components/MkNotes.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
@@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js';
 import { Paging } from '@/components/MkPagination.vue';
 
 const props = withDefaults(defineProps<{
-	src: string;
+	src: 'home' | 'local' | 'social' | 'bubble' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
 	list?: string;
 	antenna?: string;
 	channel?: string;
@@ -97,6 +98,7 @@ const stream = useStream();
 
 function connectChannel() {
 	if (props.src === 'antenna') {
+		if (props.antenna == null) return;
 		connection = stream.useChannel('antenna', {
 			antennaId: props.antenna,
 		});
@@ -144,21 +146,24 @@ function connectChannel() {
 		connection = stream.useChannel('main');
 		connection.on('mention', onNote);
 	} else if (props.src === 'list') {
+		if (props.list == null) return;
 		connection = stream.useChannel('userList', {
 			withRenotes: props.withRenotes,
 			withFiles: props.onlyFiles ? true : undefined,
 			listId: props.list,
 		});
 	} else if (props.src === 'channel') {
+		if (props.channel == null) return;
 		connection = stream.useChannel('channel', {
 			channelId: props.channel,
 		});
 	} else if (props.src === 'role') {
+		if (props.role == null) return;
 		connection = stream.useChannel('roleTimeline', {
 			roleId: props.role,
 		});
 	}
-	if (props.src !== 'directs' || props.src !== 'mentions') connection.on('note', prepend);
+	if (props.src !== 'directs' && props.src !== 'mentions') connection.on('note', prepend);
 }
 
 function disconnectChannel() {
@@ -167,7 +172,7 @@ function disconnectChannel() {
 }
 
 function updatePaginationQuery() {
-	let endpoint: string | null;
+	let endpoint: keyof Misskey.Endpoints | null;
 	let query: TimelineQueryType | null;
 
 	if (props.src === 'antenna') {
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 27e5bb1b84..59d3c62b1c 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -55,7 +55,7 @@ const el = shallowRef<HTMLElement>();
 const zIndex = os.claimZIndex('high');
 
 function setPosition() {
-	if (!el.value || !props.targetElement) return;
+	if (el.value == null) return;
 	const data = calcPopupPosition(el.value, {
 		anchorElement: props.targetElement,
 		direction: props.direction,
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index 3fca958055..f3a046c55c 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div v-else-if="phase === 'howToReact'" class="_gaps">
 	<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
 	<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
-	<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/>
+	<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
 	<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ph-check ph-bold ph-lg"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>
 </div>
 </template>
@@ -53,7 +53,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
 		isBot: false,
 		isCat: true,
 		emojis: {},
-		onlineStatus: null,
+		onlineStatus: 'unknown',
 		badgeRoles: [],
 	},
 	text: 'just setting up my shonk',
@@ -86,7 +86,6 @@ function doNotification(emoji: string): void {
 	const notification: Misskey.entities.Notification = {
 		id: Math.random().toString(),
 		createdAt: new Date().toUTCString(),
-		isRead: false,
 		type: 'reaction',
 		reaction: emoji,
 		user: $i,
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index f093d6d9ef..3c9a94060f 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -58,7 +58,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
 		isBot: false,
 		isCat: true,
 		emojis: {},
-		onlineStatus: null,
+		onlineStatus: 'unknown',
 		badgeRoles: [],
 	},
 	text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note,
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index dd255a2214..2394085b19 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -40,7 +40,7 @@ const emit = defineEmits<{
 const onceSucceeded = ref<boolean>(false);
 
 function doSucceeded(fileId: string, to: boolean) {
-	if (fileId === exampleNote.fileIds[0] && to) {
+	if (fileId === exampleNote.fileIds?.[0] && to) {
 		onceSucceeded.value = true;
 		emit('succeeded');
 	}
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 788c373ccf..21c83c5992 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkModalWindow
 	ref="dialog"
 	:width="400"
-	@close="dialog.close()"
+	@close="dialog?.close()"
 	@closed="$emit('closed')"
 >
 	<template v-if="announcement" #header>:{{ announcement.title }}:</template>
@@ -64,14 +64,14 @@ import MkRadios from '@/components/MkRadios.vue';
 
 const props = defineProps<{
 	user: Misskey.entities.User,
-	announcement?: any,
+	announcement?: Misskey.entities.Announcement,
 }>();
 
 const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
-const title = ref<string>(props.announcement ? props.announcement.title : '');
-const text = ref<string>(props.announcement ? props.announcement.text : '');
-const icon = ref<string>(props.announcement ? props.announcement.icon : 'info');
-const display = ref<string>(props.announcement ? props.announcement.display : 'dialog');
+const title = ref(props.announcement ? props.announcement.title : '');
+const text = ref(props.announcement ? props.announcement.text : '');
+const icon = ref(props.announcement ? props.announcement.icon : 'info');
+const display = ref(props.announcement ? props.announcement.display : 'dialog');
 const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
 
 const emit = defineEmits<{
@@ -92,18 +92,18 @@ async function done() {
 
 	if (props.announcement) {
 		await os.apiWithDialog('admin/announcements/update', {
-			id: props.announcement.id,
 			...params,
+			id: props.announcement.id,
 		});
 
 		emit('done', {
 			updated: {
-				id: props.announcement.id,
 				...params,
+				id: props.announcement.id,
 			},
 		});
 
-		dialog.value.close();
+		dialog.value?.close();
 	} else {
 		const created = await os.apiWithDialog('admin/announcements/create', params);
 
@@ -111,7 +111,7 @@ async function done() {
 			created: created,
 		});
 
-		dialog.value.close();
+		dialog.value?.close();
 	}
 }
 
@@ -122,14 +122,16 @@ async function del() {
 	});
 	if (canceled) return;
 
-	misskeyApi('admin/announcements/delete', {
-		id: props.announcement.id,
-	}).then(() => {
-		emit('done', {
-			deleted: true,
+	if (props.announcement) {
+		await misskeyApi('admin/announcements/delete', {
+			id: props.announcement.id,
 		});
-		dialog.value.close();
+	}
+
+	emit('done', {
+		deleted: true,
 	});
+	dialog.value?.close();
 }
 </script>
 
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 6068d5d13b..3b91125902 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, blue: !user.approved, red: user.isSuspended, gray: false }]">
-	<MkAvatar class="avatar" :user="user" indicator/>
-	<div class="body">
-		<span class="name"><MkUserName class="name" :user="user"/></span>
-		<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
+<div v-adaptive-bg :class="[$style.root]">
+	<MkAvatar :class="$style.avatar" :user="user" indicator/>
+	<div :class="$style.body">
+		<span :class="$style.name"><MkUserName :user="user"/></span>
+		<span :class="$style.sub"><span class="_monospace">@{{ acct(user) }}</span></span>
 	</div>
-	<MkMiniChart v-if="chartValues" class="chart" :src="chartValues"/>
+	<MkMiniChart v-if="chartValues" :class="$style.chart" :src="chartValues"/>
 </div>
 </template>
 
@@ -42,77 +42,53 @@ onMounted(() => {
 </script>
 
 <style lang="scss" module>
-.root {
-	$bodyTitleHieght: 18px;
-	$bodyInfoHieght: 16px;
+$bodyTitleHieght: 18px;
+$bodyInfoHieght: 16px;
 
+.root {
 	display: flex;
 	align-items: center;
 	padding: 16px;
 	background: var(--panel);
 	border-radius: var(--radius-sm);
+}
 
-	> :global(.avatar) {
-		display: block;
-		width: ($bodyTitleHieght + $bodyInfoHieght);
-		height: ($bodyTitleHieght + $bodyInfoHieght);
-		margin-right: 12px;
-	}
+.avatar {
+	display: block;
+	width: ($bodyTitleHieght + $bodyInfoHieght);
+	height: ($bodyTitleHieght + $bodyInfoHieght);
+	margin-right: 12px;
+}
 
-	> :global(.body) {
-		flex: 1;
-		overflow: hidden;
-		font-size: 0.9em;
-		color: var(--fg);
-		padding-right: 8px;
+.body {
+	flex: 1;
+	overflow: hidden;
+	font-size: 0.9em;
+	color: var(--fg);
+	padding-right: 8px;
+}
 
-		> :global(.name) {
-			display: block;
-			width: 100%;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-			line-height: $bodyTitleHieght;
-		}
+.name {
+	display: block;
+	width: 100%;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	line-height: $bodyTitleHieght;
+}
 
-		> :global(.sub) {
-			display: block;
-			width: 100%;
-			font-size: 95%;
-			opacity: 0.7;
-			line-height: $bodyInfoHieght;
-			white-space: nowrap;
-			overflow: hidden;
-			text-overflow: ellipsis;
-		}
-	}
+.sub {
+	display: block;
+	width: 100%;
+	font-size: 95%;
+	opacity: 0.7;
+	line-height: $bodyInfoHieght;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
 
-	> :global(.chart) {
-		height: 30px;
-	}
-
-	&:global(.yellow) {
-		--c: rgb(255 196 0 / 15%);
-		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
-		background-size: 16px 16px;
-	}
-
-	&:global(.blue) {
-		--c: rgba(0 153 255 / 15%);
-		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
-		background-size: 16px 16px;
-	}
-
-	&:global(.red) {
-		--c: rgb(255 0 0 / 15%);
-		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
-		background-size: 16px 16px;
-	}
-
-	&:global(.gray) {
-		--c: var(--bg);
-		background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
-		background-size: 16px 16px;
-	}
+.chart {
+	height: 30px;
 }
 </style>
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 0795b43bfd..211989cf7e 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -98,6 +98,7 @@ const top = ref(0);
 const left = ref(0);
 
 function showMenu(ev: MouseEvent) {
+	if (user.value == null) return;
 	const { menu, cleanup } = getUserMenu(user.value);
 	os.popupMenu(menu, ev.currentTarget ?? ev.target).finally(cleanup);
 }
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 2c941fcfdd..b2538b344b 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -16,7 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header>{{ i18n.ts.selectUser }}</template>
 	<div>
 		<div :class="$style.form">
-			<FormSplit :minWidth="170">
+			<MkInput v-if="localOnly" v-model="username" :autofocus="true" @update:modelValue="search">
+				<template #label>{{ i18n.ts.username }}</template>
+				<template #prefix>@</template>
+			</MkInput>
+			<FormSplit v-else :minWidth="170">
 				<MkInput v-model="username" :autofocus="true" @update:modelValue="search">
 					<template #label>{{ i18n.ts.username }}</template>
 					<template #prefix>@</template>
@@ -66,7 +70,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { hostname } from '@/config.js';
+import { host as currentHost, hostname } from '@/config.js';
 
 const emit = defineEmits<{
 	(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
@@ -74,16 +78,19 @@ const emit = defineEmits<{
 	(ev: 'closed'): void;
 }>();
 
-const props = defineProps<{
+const props = withDefaults(defineProps<{
 	includeSelf?: boolean;
-	local?: boolean;
-}>();
+	localOnly?: boolean;
+}>(), {
+	includeSelf: false,
+	localOnly: false,
+});
 
 const username = ref('');
 const host = ref('');
-const users = ref<Misskey.entities.UserDetailed[]>([]);
+const users = ref<Misskey.entities.UserLite[]>([]);
 const recentUsers = ref<Misskey.entities.UserDetailed[]>([]);
-const selected = ref<Misskey.entities.UserDetailed | null>(null);
+const selected = ref<Misskey.entities.UserLite | null>(null);
 const dialogEl = ref();
 
 function search() {
@@ -91,25 +98,36 @@ function search() {
 		users.value = [];
 		return;
 	}
-	if (props.local) host.value = '.';
+
 	misskeyApi('users/search-by-username-and-host', {
 		username: username.value,
-		host: host.value,
+		host: props.localOnly ? '.' : host.value,
 		limit: 10,
 		detail: false,
 	}).then(_users => {
-		users.value = _users;
+		users.value = _users.filter((u) => {
+			if (props.includeSelf) {
+				return true;
+			} else {
+				return u.id !== $i?.id;
+			}
+		});
 	});
 }
 
-function ok() {
+async function ok() {
 	if (selected.value == null) return;
-	emit('ok', selected.value);
+
+	const user = await misskeyApi('users/show', {
+		userId: selected.value.id,
+	});
+	emit('ok', user);
+
 	dialogEl.value.close();
 
 	// 最近使ったユーザー更新
 	let recents = defaultStore.state.recentlyUsedUsers;
-	recents = recents.filter(x => x !== selected.value.id);
+	recents = recents.filter(x => x !== selected.value?.id);
 	recents.unshift(selected.value.id);
 	defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
 }
@@ -122,12 +140,23 @@ function cancel() {
 onMounted(() => {
 	misskeyApi('users/show', {
 		userIds: defaultStore.state.recentlyUsedUsers,
-	}).then(users => {
-		if (props.includeSelf && users.find(x => $i ? x.id === $i.id : true) == null) {
-			recentUsers.value = [$i, ...users];
-		} else {
-			recentUsers.value = users;
-		}
+	}).then(foundUsers => {
+		let _users = foundUsers;
+		_users = _users.filter((u) => {
+			if (props.localOnly) {
+				return u.host == null;
+			} else {
+				return true;
+			}
+		});
+		_users = _users.filter((u) => {
+			if (props.includeSelf) {
+				return true;
+			} else {
+				return u.id !== $i?.id;
+			}
+		});
+		recentUsers.value = _users;
 	});
 });
 </script>
@@ -135,7 +164,7 @@ onMounted(() => {
 <style lang="scss" module>
 
 .form {
-	padding: 0 var(--root-margin);
+	padding: calc(var(--root-margin) / 2) var(--root-margin);
 }
 
 .result,
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 5f3f5b81dd..46459df6a6 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="pinnedUsers">
 			<template #default="{ items }">
 				<div :class="$style.users">
-					<XUser v-for="item in items" :key="item.id" :user="item"/>
+					<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
 				</div>
 			</template>
 		</MkPagination>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkPagination :pagination="popularUsers">
 			<template #default="{ items }">
 				<div :class="$style.users">
-					<XUser v-for="item in items" :key="item.id" :user="item"/>
+					<XUser v-for="item in (items as Misskey.entities.UserDetailed[])" :key="item.id" :user="item"/>
 				</div>
 			</template>
 		</MkPagination>
@@ -34,18 +34,28 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
+import Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
 import XUser from '@/components/MkUserSetupDialog.User.vue';
-import MkPagination from '@/components/MkPagination.vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 
-const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
+const pinnedUsers: Paging = {
+	endpoint: 'pinned-users',
+	noPaging: true,
+	limit: 10,
+};
 
-const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
-	state: 'alive',
-	origin: 'local',
-	sort: '+follower',
-} };
+const popularUsers: Paging = {
+	endpoint: 'users',
+	limit: 10,
+	noPaging: true,
+	params: {
+		state: 'alive',
+		origin: 'local',
+		sort: '+follower',
+	},
+};
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index f082833838..3242d59eda 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -39,7 +39,9 @@ import FormSlot from '@/components/form/slot.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { chooseFileFromPc } from '@/scripts/select-file.js';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { signinRequired } from '@/account.js';
+
+const $i = signinRequired();
 
 const name = ref($i.name ?? '');
 const description = ref($i.description ?? '');
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 61edc345a9..ff1e4274ab 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal.close()" @closed="emit('closed')">
+<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')">
 	<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
 		<div :class="[$style.label, $style.item]">
 			{{ i18n.ts.visibility }}
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index e45d594f12..46d75da839 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, shallowRef, ref } from 'vue';
+import { onMounted, shallowRef, ref, nextTick } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import tinycolor from 'tinycolor2';
@@ -25,9 +25,9 @@ import { initChart } from '@/scripts/init-chart.js';
 
 initChart();
 
-const chartEl = shallowRef<HTMLCanvasElement>(null);
+const chartEl = shallowRef<HTMLCanvasElement | null>(null);
 const now = new Date();
-let chartInstance: Chart = null;
+let chartInstance: Chart | null = null;
 const chartLimit = 30;
 const fetching = ref(true);
 
@@ -55,6 +55,10 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
 
+	fetching.value = false;
+
+	await nextTick();
+
 	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const computedStyle = getComputedStyle(document.documentElement);
@@ -65,6 +69,8 @@ async function renderChart() {
 
 	const max = Math.max(...raw.read);
 
+	if (chartEl.value == null) return;
+
 	chartInstance = new Chart(chartEl.value, {
 		type: 'bar',
 		data: {
@@ -97,7 +103,6 @@ async function renderChart() {
 					type: 'time',
 					offset: true,
 					time: {
-						stepSize: 1,
 						unit: 'day',
 						displayFormats: {
 							day: 'M/d',
@@ -108,6 +113,7 @@ async function renderChart() {
 						display: false,
 					},
 					ticks: {
+						stepSize: 1,
 						display: true,
 						maxRotation: 0,
 						autoSkipPadding: 8,
@@ -141,13 +147,10 @@ async function renderChart() {
 					},
 					external: externalTooltipHandler,
 				},
-				gradient,
 			},
 		},
 		plugins: [chartVLine(vLineColor)],
 	});
-
-	fetching.value = false;
 }
 
 onMounted(async () => {
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 8f15fa93a6..11707b07f3 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div v-if="meta" :class="$style.root">
 	<div :class="[$style.main, $style.panel]">
-		<img :src="instance.iconUrl || instance.faviconUrl || '/apple-touch-icon.png'" alt="" :class="$style.mainIcon"/>
+		<img :src="instance.iconUrl || '/apple-touch-icon.png'" alt="" :class="$style.mainIcon"/>
 		<button class="_button _acrylic" :class="$style.mainMenu" @click="showMenu"><i class="ph-dots-three ph-bold ph-lg"></i></button>
 		<div :class="$style.mainFg">
 			<h1 :class="$style.mainTitle">
@@ -109,19 +109,19 @@ function showMenu(ev) {
 		text: i18n.ts.impressum,
 		icon: 'ph-newspaper-clipping ph-bold ph-lg',
 		action: () => {
-			window.open(instance.impressumUrl, '_blank', 'noopener');
+			window.open(instance.impressumUrl!, '_blank', 'noopener');
 		},
 	} : undefined, (instance.tosUrl) ? {
 		text: i18n.ts.termsOfService,
 		icon: 'ph-notebook ph-bold ph-lg',
 		action: () => {
-			window.open(instance.tosUrl, '_blank', 'noopener');
+			window.open(instance.tosUrl!, '_blank', 'noopener');
 		},
 	} : undefined, (instance.privacyPolicyUrl) ? {
 		text: i18n.ts.privacyPolicy,
 		icon: 'ph-shield ph-bold ph-lg',
 		action: () => {
-			window.open(instance.privacyPolicyUrl, '_blank', 'noopener');
+			window.open(instance.privacyPolicyUrl!, '_blank', 'noopener');
 		},
 	} : undefined, (instance.donationUrl) ? {
 		text: i18n.ts.donation,
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 28943efd1a..f59bbfb425 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -32,7 +32,7 @@ const emit = defineEmits<{
 
 function done() {
 	emit('done');
-	modal.value.close();
+	modal.value?.close();
 }
 
 watch(() => props.showing, () => {
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 100683cc54..810cbb108e 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -104,14 +104,16 @@ const updateWidget = (id, data) => {
 };
 
 function onContextmenu(widget: Widget, ev: MouseEvent) {
-	const isLink = (el: HTMLElement) => {
+	const element = ev.target as HTMLElement | null;
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === 'A') return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
+	if (element && isLink(element)) return;
+	if (element && (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(element.tagName) || element.attributes['contenteditable'])) return;
 	if (window.getSelection()?.toString() !== '') return;
 
 	os.contextMenu([{
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index e897a0b7a6..7774b5b566 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -63,7 +63,7 @@ import { defaultStore } from '@/store.js';
 const minHeight = 50;
 const minWidth = 250;
 
-function dragListen(fn: (ev: MouseEvent) => void) {
+function dragListen(fn: (ev: MouseEvent | TouchEvent) => void) {
 	window.addEventListener('mousemove', fn);
 	window.addEventListener('touchmove', fn);
 	window.addEventListener('mouseleave', dragClear.bind(null, fn));
@@ -138,7 +138,7 @@ function onContextmenu(ev: MouseEvent) {
 // 最前面へ移動
 function top() {
 	if (rootEl.value) {
-		rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low');
+		rootEl.value.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low').toString();
 	}
 }
 
@@ -202,9 +202,17 @@ function onDblClick() {
 	}
 }
 
-function onHeaderMousedown(evt: MouseEvent) {
+function getPositionX(event: MouseEvent | TouchEvent) {
+	return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientX : 'clientX' in event ? event.clientX : 0;
+}
+
+function getPositionY(event: MouseEvent | TouchEvent) {
+	return 'touches' in event && event.touches.length > 0 ? event.touches[0].clientY : 'clientY' in event ? event.clientY : 0;
+}
+
+function onHeaderMousedown(evt: MouseEvent | TouchEvent) {
 	// 右クリックはコンテキストメニューを開こうとした可能性が高いため無視
-	if (evt.button === 2) return;
+	if ('button' in evt && evt.button === 2) return;
 
 	let beforeMaximized = false;
 
@@ -229,8 +237,8 @@ function onHeaderMousedown(evt: MouseEvent) {
 
 	const position = main.getBoundingClientRect();
 
-	const clickX = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientX : evt.clientX;
-	const clickY = evt.touches && evt.touches.length > 0 ? evt.touches[0].clientY : evt.clientY;
+	const clickX = getPositionX(evt);
+	const clickY = getPositionY(evt);
 	const moveBaseX = beforeMaximized ? parseInt(unResizedWidth, 10) / 2 : clickX - position.left; // TODO: parseIntやめる
 	const moveBaseY = beforeMaximized ? 20 : clickY - position.top;
 	const browserWidth = window.innerWidth;
@@ -254,8 +262,10 @@ function onHeaderMousedown(evt: MouseEvent) {
 		// 右はみ出し
 		if (moveLeft + windowWidth > browserWidth) moveLeft = browserWidth - windowWidth;
 
-		rootEl.value.style.left = moveLeft + 'px';
-		rootEl.value.style.top = moveTop + 'px';
+		if (rootEl.value) {
+			rootEl.value.style.left = moveLeft + 'px';
+			rootEl.value.style.top = moveTop + 'px';
+		}
 	}
 
 	if (beforeMaximized) {
@@ -264,26 +274,26 @@ function onHeaderMousedown(evt: MouseEvent) {
 
 	// 動かした時
 	dragListen(me => {
-		const x = me.touches && me.touches.length > 0 ? me.touches[0].clientX : me.clientX;
-		const y = me.touches && me.touches.length > 0 ? me.touches[0].clientY : me.clientY;
+		const x = getPositionX(me);
+		const y = getPositionY(me);
 
 		move(x, y);
 	});
 }
 
 // 上ハンドル掴み時
-function onTopHandleMousedown(evt) {
+function onTopHandleMousedown(evt: MouseEvent | TouchEvent) {
 	const main = rootEl.value;
 	// どういうわけかnullになることがある
 	if (main == null) return;
 
-	const base = evt.clientY;
+	const base = getPositionY(evt);
 	const height = parseInt(getComputedStyle(main, '').height, 10);
 	const top = parseInt(getComputedStyle(main, '').top, 10);
 
 	// 動かした時
 	dragListen(me => {
-		const move = me.clientY - base;
+		const move = getPositionY(me) - base;
 		if (top + move > 0) {
 			if (height + -move > minHeight) {
 				applyTransformHeight(height + -move);
@@ -300,18 +310,18 @@ function onTopHandleMousedown(evt) {
 }
 
 // 右ハンドル掴み時
-function onRightHandleMousedown(evt) {
+function onRightHandleMousedown(evt: MouseEvent | TouchEvent) {
 	const main = rootEl.value;
 	if (main == null) return;
 
-	const base = evt.clientX;
+	const base = getPositionX(evt);
 	const width = parseInt(getComputedStyle(main, '').width, 10);
 	const left = parseInt(getComputedStyle(main, '').left, 10);
 	const browserWidth = window.innerWidth;
 
 	// 動かした時
 	dragListen(me => {
-		const move = me.clientX - base;
+		const move = getPositionX(me) - base;
 		if (left + width + move < browserWidth) {
 			if (width + move > minWidth) {
 				applyTransformWidth(width + move);
@@ -325,18 +335,18 @@ function onRightHandleMousedown(evt) {
 }
 
 // 下ハンドル掴み時
-function onBottomHandleMousedown(evt) {
+function onBottomHandleMousedown(evt: MouseEvent | TouchEvent) {
 	const main = rootEl.value;
 	if (main == null) return;
 
-	const base = evt.clientY;
+	const base = getPositionY(evt);
 	const height = parseInt(getComputedStyle(main, '').height, 10);
 	const top = parseInt(getComputedStyle(main, '').top, 10);
 	const browserHeight = window.innerHeight;
 
 	// 動かした時
 	dragListen(me => {
-		const move = me.clientY - base;
+		const move = getPositionY(me) - base;
 		if (top + height + move < browserHeight) {
 			if (height + move > minHeight) {
 				applyTransformHeight(height + move);
@@ -350,17 +360,17 @@ function onBottomHandleMousedown(evt) {
 }
 
 // 左ハンドル掴み時
-function onLeftHandleMousedown(evt) {
+function onLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
 	const main = rootEl.value;
 	if (main == null) return;
 
-	const base = evt.clientX;
+	const base = getPositionX(evt);
 	const width = parseInt(getComputedStyle(main, '').width, 10);
 	const left = parseInt(getComputedStyle(main, '').left, 10);
 
 	// 動かした時
 	dragListen(me => {
-		const move = me.clientX - base;
+		const move = getPositionX(me) - base;
 		if (left + move > 0) {
 			if (width + -move > minWidth) {
 				applyTransformWidth(width + -move);
@@ -377,25 +387,25 @@ function onLeftHandleMousedown(evt) {
 }
 
 // 左上ハンドル掴み時
-function onTopLeftHandleMousedown(evt) {
+function onTopLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
 	onTopHandleMousedown(evt);
 	onLeftHandleMousedown(evt);
 }
 
 // 右上ハンドル掴み時
-function onTopRightHandleMousedown(evt) {
+function onTopRightHandleMousedown(evt: MouseEvent | TouchEvent) {
 	onTopHandleMousedown(evt);
 	onRightHandleMousedown(evt);
 }
 
 // 右下ハンドル掴み時
-function onBottomRightHandleMousedown(evt) {
+function onBottomRightHandleMousedown(evt: MouseEvent | TouchEvent) {
 	onBottomHandleMousedown(evt);
 	onRightHandleMousedown(evt);
 }
 
 // 左下ハンドル掴み時
-function onBottomLeftHandleMousedown(evt) {
+function onBottomLeftHandleMousedown(evt: MouseEvent | TouchEvent) {
 	onBottomHandleMousedown(evt);
 	onLeftHandleMousedown(evt);
 }
@@ -403,23 +413,23 @@ function onBottomLeftHandleMousedown(evt) {
 // 高さを適用
 function applyTransformHeight(height) {
 	if (height > window.innerHeight) height = window.innerHeight;
-	rootEl.value.style.height = height + 'px';
+	if (rootEl.value) rootEl.value.style.height = height + 'px';
 }
 
 // 幅を適用
 function applyTransformWidth(width) {
 	if (width > window.innerWidth) width = window.innerWidth;
-	rootEl.value.style.width = width + 'px';
+	if (rootEl.value) rootEl.value.style.width = width + 'px';
 }
 
 // Y座標を適用
 function applyTransformTop(top) {
-	rootEl.value.style.top = top + 'px';
+	if (rootEl.value) rootEl.value.style.top = top + 'px';
 }
 
 // X座標を適用
 function applyTransformLeft(left) {
-	rootEl.value.style.left = left + 'px';
+	if (rootEl.value) rootEl.value.style.left = left + 'px';
 }
 
 function onBrowserResize() {
@@ -441,8 +451,10 @@ onMounted(() => {
 	applyTransformWidth(props.initialWidth);
 	if (props.initialHeight) applyTransformHeight(props.initialHeight);
 
-	applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2));
-	applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2));
+	if (rootEl.value) {
+		applyTransformTop((window.innerHeight / 2) - (rootEl.value.offsetHeight / 2));
+		applyTransformLeft((window.innerWidth / 2) - (rootEl.value.offsetWidth / 2));
+	}
 
 	// 他のウィンドウ内のボタンなどを押してこのウィンドウが開かれた場合、親が最前面になろうとするのでそれに隠されないようにする
 	top();
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index a9b2e8a00d..a44ee962d0 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -39,7 +39,7 @@ if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid
 const fetching = ref(true);
 const title = ref<string | null>(null);
 const player = ref({
-	url: null,
+	url: null as string | null,
 	width: null,
 	height: null,
 });
diff --git a/packages/frontend/src/components/SkNote.vue b/packages/frontend/src/components/SkNote.vue
index c5faf3dbe1..4e224d2e6d 100644
--- a/packages/frontend/src/components/SkNote.vue
+++ b/packages/frontend/src/components/SkNote.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div
 	v-if="!hardMuted && muted === false"
 	v-show="!isDeleted"
-	ref="el"
+	ref="rootEl"
 	v-hotkey="keymap"
 	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
 	:tabindex="!isDeleted ? '-1' : undefined"
@@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						/>
 						<div v-if="translating || translation" :class="$style.translation">
 							<MkLoading v-if="translating" mini/>
-							<div v-else>
+							<div v-else-if="translation">
 								<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
 								<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
 							</div>
@@ -84,10 +84,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
 						<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
 					</div>
-					<div v-if="appearNote.files.length > 0">
+					<div v-if="appearNote.files && appearNote.files.length > 0">
 						<MkMediaList :mediaList="appearNote.files" @click.stop/>
 					</div>
-					<MkPoll v-if="appearNote.poll" :note="appearNote" :class="$style.poll" @click.stop/>
+					<MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll" @click.stop/>
 					<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview" @click.stop/>
 					<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
 					<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click.stop @click="collapsed = false">
@@ -147,7 +147,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown="clip()">
 					<i class="ph-paperclip ph-bold ph-lg"></i>
 				</button>
-				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
+				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="showMenu()">
 					<i class="ph-dots-three ph-bold ph-lg"></i>
 				</button>
 			</footer>
@@ -216,7 +216,7 @@ import { MenuItem } from '@/types/menu.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import { shouldCollapsed } from '@/scripts/collapsed.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -255,7 +255,7 @@ if (noteViewInterruptors.length > 0) {
 		let result: Misskey.entities.Note | null = deepClone(note.value);
 		for (const interruptor of noteViewInterruptors) {
 			try {
-				result = await interruptor.handler(result);
+				result = await interruptor.handler(result!) as Misskey.entities.Note | null;
 				if (result === null) {
 					isDeleted.value = true;
 					return;
@@ -264,7 +264,7 @@ if (noteViewInterruptors.length > 0) {
 				console.error(err);
 			}
 		}
-		note.value = result;
+		note.value = result as Misskey.entities.Note;
 	});
 }
 
@@ -272,11 +272,11 @@ const isRenote = (
 	note.value.renote != null &&
 	note.value.text == null &&
 	note.value.cw == null &&
-	note.value.fileIds.length === 0 &&
+	note.value.fileIds && note.value.fileIds.length === 0 &&
 	note.value.poll == null
 );
 
-const el = shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
 const menuButton = shallowRef<HTMLElement>();
 const menuVersionsButton = shallowRef<HTMLElement>();
 const renoteButton = shallowRef<HTMLElement>();
@@ -302,8 +302,13 @@ const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hard
 const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
 const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
-const renoteCollapsed = ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || (appearNote.value.myReaction != null)));
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
+const renoteCollapsed = ref(
+	defaultStore.state.collapseRenotes && isRenote && (
+		($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
+		(appearNote.value.myReaction != null)
+	)
+);
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
 const animated = computed(() => parsed.value ? checkAnimationFromMfm(parsed.value) : null);
 const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : false);
@@ -328,11 +333,11 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renoteButton.value.renote(true),
+	'q': () => renote(appearNote.value.visibility),
 	'up|k|shift+tab': focusBefore,
 	'down|j|tab': focusAfter,
 	'esc': blur,
-	'm|o': () => menu(true),
+	'm|o': () => showMenu(true),
 	's': () => showContent.value !== showContent.value,
 };
 
@@ -349,7 +354,7 @@ if (props.mock) {
 	}, { deep: true });
 } else {
 	useNoteCapture({
-		rootEl: el,
+		rootEl: rootEl,
 		note: appearNote,
 		pureNote: note,
 		isDeletedRef: isDeleted,
@@ -522,7 +527,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -544,7 +549,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -572,7 +577,7 @@ function reply(viaKeyboard = false): void {
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
 		animation: !viaKeyboard,
-	}, () => {
+	}).then(() => {
 		focus();
 	});
 }
@@ -611,7 +616,7 @@ function react(viaKeyboard = false): void {
 			noteId: appearNote.value.id,
 			override: defaultLike.value,
 		});
-		const el = reactButton.value as HTMLElement | null | undefined;
+		const el = reactButton.value;
 		if (el) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
@@ -620,7 +625,7 @@ function react(viaKeyboard = false): void {
 		}
 	} else {
 		blur();
-		reactionPicker.show(reactButton.value, reaction => {
+		reactionPicker.show(reactButton.value ?? null, reaction => {
 			sound.playMisskeySfx('reaction');
 
 			if (props.mock) {
@@ -641,8 +646,8 @@ function react(viaKeyboard = false): void {
 	}
 }
 
-function undoReact(note): void {
-	const oldReaction = note.myReaction;
+function undoReact(targetNote: Misskey.entities.Note): void {
+	const oldReaction = targetNote.myReaction;
 	if (!oldReaction) return;
 
 	if (props.mock) {
@@ -651,7 +656,7 @@ function undoReact(note): void {
 	}
 
 	misskeyApi('notes/reactions/delete', {
-		noteId: note.id,
+		noteId: targetNote.id,
 	});
 }
 
@@ -679,32 +684,34 @@ function onContextmenu(ev: MouseEvent): void {
 		return;
 	}
 
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === 'A') return true;
 		// 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。
 		if (el.tagName === 'AUDIO') return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== '') return;
+
+	if (ev.target && isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== '') return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
-		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
 		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
-function menu(viaKeyboard = false): void {
+function showMenu(viaKeyboard = false): void {
 	if (props.mock) {
 		return;
 	}
 
-	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value });
+	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value });
 	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
 	}).then(focus).finally(cleanup);
@@ -758,7 +765,7 @@ function showRenoteMenu(viaKeyboard = false): void {
 			getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote),
 			{ type: 'divider' },
 			getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote),
-			$i.isModerator || $i.isAdmin ? getUnrenote() : undefined,
+			($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined,
 		], renoteTime.value, {
 			viaKeyboard: viaKeyboard,
 		});
@@ -778,23 +785,19 @@ function animatedMFM() {
 }
 
 function focus() {
-	el.value.focus();
+	rootEl.value?.focus();
 }
 
 function blur() {
-	el.value.blur();
+	rootEl.value?.blur();
 }
 
 function focusBefore() {
-	focusPrev(el.value);
+	focusPrev(rootEl.value ?? null);
 }
 
 function focusAfter() {
-	focusNext(el.value);
-}
-
-function scrollIntoView() {
-	el.value.scrollIntoView();
+	focusNext(rootEl.value ?? null);
 }
 
 function readPromo() {
@@ -811,12 +814,6 @@ function emitUpdReaction(emoji: string, delta: number) {
 		emit('reaction', emoji);
 	}
 }
-
-defineExpose({
-	focus,
-	blur,
-	scrollIntoView,
-});
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 43cf87a11e..d26e431c95 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div
 	v-if="!muted"
 	v-show="!isDeleted"
-	ref="el"
+	ref="rootEl"
 	v-hotkey="keymap"
 	:class="$style.root"
 >
@@ -96,17 +96,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<a v-if="appearNote.renote != null" :class="$style.rn">RN:</a>
 				<div v-if="translating || translation" :class="$style.translation">
 					<MkLoading v-if="translating" mini/>
-					<div v-else>
+					<div v-else-if="translation">
 						<b>{{ i18n.tsx.translatedFrom({ x: translation.sourceLang }) }}: </b>
 						<Mfm :text="translation.text" :author="appearNote.user" :nyaize="'respect'" :emojiUrls="appearNote.emojis"/>
 					</div>
 				</div>
 				<MkButton v-if="!allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-play ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.play }}</MkButton>
 				<MkButton v-else-if="!defaultStore.state.animatedMfm && allowAnim && animated" :class="$style.playMFMButton" :small="true" @click="animatedMFM()" @click.stop><i class="ph-stop ph-bold ph-lg "></i> {{ i18n.ts._animatedMFM.stop }}</MkButton>
-				<div v-if="appearNote.files.length > 0">
+				<div v-if="appearNote.files && appearNote.files.length > 0">
 					<MkMediaList :mediaList="appearNote.files"/>
 				</div>
-				<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
+				<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
 				<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" style="margin-top: 6px;"/>
 				<div v-if="appearNote.renote" :class="$style.quote"><SkNoteSimple :note="appearNote.renote" :class="$style.quoteNote" :expandAllCws="props.expandAllCws"/></div>
 			</div>
@@ -162,7 +162,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
 				<i class="ph-paperclip ph-bold ph-lg"></i>
 			</button>
-			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
+			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
 				<i class="ph-dots-three ph-bold ph-lg"></i>
 			</button>
 		</footer>
@@ -262,7 +262,7 @@ import { checkAnimationFromMfm } from '@/scripts/check-animated-mfm.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import MkPagination from '@/components/MkPagination.vue';
+import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 
@@ -281,7 +281,7 @@ if (noteViewInterruptors.length > 0) {
 		let result: Misskey.entities.Note | null = deepClone(note.value);
 		for (const interruptor of noteViewInterruptors) {
 			try {
-				result = await interruptor.handler(result);
+				result = await interruptor.handler(result!) as Misskey.entities.Note | null;
 				if (result === null) {
 					isDeleted.value = true;
 					return;
@@ -290,18 +290,18 @@ if (noteViewInterruptors.length > 0) {
 				console.error(err);
 			}
 		}
-		note.value = result;
+		note.value = result as Misskey.entities.Note;
 	});
 }
 
 const isRenote = (
 	note.value.renote != null &&
 	note.value.text == null &&
-	note.value.fileIds.length === 0 &&
+	note.value.fileIds && note.value.fileIds.length === 0 &&
 	note.value.poll == null
 );
 
-const el = shallowRef<HTMLElement>();
+const rootEl = shallowRef<HTMLElement>();
 const noteEl = shallowRef<HTMLElement>();
 const menuButton = shallowRef<HTMLElement>();
 const menuVersionsButton = shallowRef<HTMLElement>();
@@ -329,7 +329,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
 const conversation = ref<Misskey.entities.Note[]>([]);
 const replies = ref<Misskey.entities.Note[]>([]);
 const quotes = ref<Misskey.entities.Note[]>([]);
-const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i.id));
+const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
 
 watch(() => props.expandAllCws, (expandAllCws) => {
@@ -349,9 +349,9 @@ if ($i) {
 const keymap = {
 	'r': () => reply(true),
 	'e|a|plus': () => react(true),
-	'q': () => renoteButton.value.renote(true),
+	'q': () => renote(appearNote.value.visibility),
 	'esc': blur,
-	'm|o': () => menu(true),
+	'm|o': () => showMenu(true),
 	's': () => showContent.value !== showContent.value,
 };
 
@@ -365,7 +365,7 @@ provide('react', (reaction: string) => {
 const tab = ref('replies');
 const reactionTabType = ref<string | null>(null);
 
-const renotesPagination = computed(() => ({
+const renotesPagination = computed<Paging>(() => ({
 	endpoint: 'notes/renotes',
 	limit: 10,
 	params: {
@@ -373,7 +373,7 @@ const renotesPagination = computed(() => ({
 	},
 }));
 
-const reactionsPagination = computed(() => ({
+const reactionsPagination = computed<Paging>(() => ({
 	endpoint: 'notes/reactions',
 	limit: 10,
 	params: {
@@ -396,7 +396,7 @@ async function removeReply(id: Misskey.entities.Note['id']) {
 }
 
 useNoteCapture({
-	rootEl: el,
+	rootEl: rootEl,
 	note: appearNote,
 	pureNote: note,
 	isDeletedRef: isDeleted,
@@ -549,7 +549,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -571,7 +571,7 @@ function quote() {
 		}).then(() => {
 			misskeyApi('notes/renotes', {
 				noteId: appearNote.value.id,
-				userId: $i.id,
+				userId: $i?.id,
 				limit: 1,
 				quote: true,
 			}).then((res) => {
@@ -597,7 +597,7 @@ function reply(viaKeyboard = false): void {
 		reply: appearNote.value,
 		channel: appearNote.value.channel,
 		animation: !viaKeyboard,
-	}, () => {
+	}).then(() => {
 		focus();
 	});
 }
@@ -621,7 +621,7 @@ function react(viaKeyboard = false): void {
 		}
 	} else {
 		blur();
-		reactionPicker.show(reactButton.value, reaction => {
+		reactionPicker.show(reactButton.value ?? null, reaction => {
 			sound.playMisskeySfx('reaction');
 
 			misskeyApi('notes/reactions/create', {
@@ -680,26 +680,28 @@ function undoRenote() : void {
 }
 
 function onContextmenu(ev: MouseEvent): void {
-	const isLink = (el: HTMLElement) => {
+	const isLink = (el: HTMLElement): boolean => {
 		if (el.tagName === 'A') return true;
 		if (el.parentElement) {
 			return isLink(el.parentElement);
 		}
+		return false;
 	};
-	if (isLink(ev.target)) return;
-	if (window.getSelection().toString() !== '') return;
+
+	if (ev.target && isLink(ev.target as HTMLElement)) return;
+	if (window.getSelection()?.toString() !== '') return;
 
 	if (defaultStore.state.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
-		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
+		const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
 		os.contextMenu(menu, ev).then(focus).finally(cleanup);
 	}
 }
 
-function menu(viaKeyboard = false): void {
-	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, menuButton, isDeleted });
+function showMenu(viaKeyboard = false): void {
+	const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted });
 	os.popupMenu(menu, menuButton.value, {
 		viaKeyboard,
 	}).then(focus).finally(cleanup);
@@ -776,6 +778,7 @@ const conversationLoaded = ref(false);
 
 function loadConversation() {
 	conversationLoaded.value = true;
+	if (appearNote.value.replyId == null) return;
 	misskeyApi('notes/conversation', {
 		noteId: appearNote.value.replyId,
 	}).then(res => {
diff --git a/packages/frontend/src/components/SkNoteHeader.vue b/packages/frontend/src/components/SkNoteHeader.vue
index d471c2e6df..26ebfc6e8b 100644
--- a/packages/frontend/src/components/SkNoteHeader.vue
+++ b/packages/frontend/src/components/SkNoteHeader.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</MkA>
 			<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
 			<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
-				<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
+				<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
 			</div>
 		</div>
 		<div :class="$style.username"><MkAcct :user="note.user"/></div>
@@ -82,7 +82,7 @@ import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
 import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
 import { popupMenu } from '@/os.js';
 import { defaultStore } from '@/store.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 import { deviceKind } from '@/scripts/device-kind.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 2119b33d07..c669f257a2 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -15,7 +15,7 @@ import * as os from '@/os.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import { url } from '@/config.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const props = withDefaults(defineProps<{
 	to: string;
diff --git a/packages/frontend/src/components/global/MkAcct.vue b/packages/frontend/src/components/global/MkAcct.vue
index 594494f3c8..0171e22c79 100644
--- a/packages/frontend/src/components/global/MkAcct.vue
+++ b/packages/frontend/src/components/global/MkAcct.vue
@@ -21,7 +21,7 @@ import { host as hostRaw } from '@/config.js';
 import { defaultStore } from '@/store.js';
 
 defineProps<{
-	user: Misskey.entities.UserDetailed;
+	user: Misskey.entities.User;
 	detail?: boolean;
 }>();
 
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 4a876931c3..303ecc7cfd 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<img
 			v-for="decoration in decorations ?? user.avatarDecorations"
 			:class="[$style.decoration]"
-			:src="decoration.url"
+			:src="getDecorationUrl(decoration)"
 			:style="{
 				rotate: getDecorationAngle(decoration),
 				scale: getDecorationScale(decoration),
@@ -81,15 +81,22 @@ const bound = computed(() => props.link
 	? { to: userPage(props.user), target: props.target }
 	: {});
 
-const url = computed(() => (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar)
-	? getStaticImageUrl(props.user.avatarUrl)
-	: props.user.avatarUrl);
+const url = computed(() => {
+	if (props.user.avatarUrl == null) return null;
+	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
+	return props.user.avatarUrl;
+});
 
 function onClick(ev: MouseEvent): void {
 	if (props.link) return;
 	emit('click', ev);
 }
 
+function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
+	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url);
+	return decoration.url;
+}
+
 function getDecorationAngle(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
 	const angle = decoration.angle ?? 0;
 	return angle === 0 ? undefined : `${angle * 360}deg`;
@@ -109,6 +116,7 @@ function getDecorationOffset(decoration: Omit<Misskey.entities.UserDetailed['ava
 const color = ref<string | undefined>();
 
 watch(() => props.user.avatarBlurhash, () => {
+	if (props.user.avatarBlurhash == null) return;
 	color.value = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
 }, {
 	immediate: true,
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 5ba2ffb8b9..fbfb2cdf34 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -57,7 +57,7 @@ const rawUrl = computed(() => {
 });
 
 const url = computed(() => {
-	if (rawUrl.value == null) return null;
+	if (rawUrl.value == null) return undefined;
 
 	const proxied =
 		(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
index 00c19790e8..6914fdcc3c 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
+++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
@@ -20,6 +20,7 @@ import MkA from '@/components/global/MkA.vue';
 import { host } from '@/config.js';
 import { defaultStore } from '@/store.js';
 import { nyaize as doNyaize } from '@/scripts/nyaize.js';
+import { safeParseFloat } from '@/scripts/safe-parse.js';
 
 const QUOTE_STYLE = `
 display: block;
@@ -36,7 +37,7 @@ type MfmProps = {
 	nowrap?: boolean;
 	author?: Misskey.entities.UserLite;
 	isNote?: boolean;
-	emojiUrls?: string[];
+	emojiUrls?: Record<string, string>;
 	rootScale?: number;
 	nyaize?: boolean | 'respect';
 	parsedNodes?: mfm.MfmNode[] | null;
@@ -50,7 +51,7 @@ type MfmEvents = {
 };
 
 // eslint-disable-next-line import/no-default-export
-export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
+export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
 	const isNote = props.isNote ?? true;
 	const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat ? props.author.speakAsCat : false : false : false;
 
@@ -59,15 +60,16 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 
 	const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
 
-	const validTime = (t: string | null | undefined) => {
+	const validTime = (t: string | boolean | null | undefined) => {
 		if (t == null) return null;
+		if (typeof t === 'boolean') return null;
 		return t.match(/^[0-9.]+s$/) ? t : null;
 	};
 
 	const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm ? true : props.isAnim ? true : false;
 
-	const validColor = (c: string | null | undefined): string | null => {
-		if (c == null) return null;
+	const validColor = (c: unknown): string | null => {
+		if (typeof c !== 'string') return null;
 		return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
 	};
 
@@ -226,14 +228,14 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 						return h(MkSparkle, {}, genEl(token.children, scale));
 					}
 					case 'rotate': {
-						const degrees = parseFloat(token.props.args.deg ?? '90');
+						const degrees = safeParseFloat(token.props.args.deg) ?? 90;
 						style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
 						break;
 					}
 					case 'position': {
 						if (!defaultStore.state.advancedMfm) break;
-						const x = parseFloat(token.props.args.x ?? '0');
-						const y = parseFloat(token.props.args.y ?? '0');
+						const x = safeParseFloat(token.props.args.x) ?? 0;
+						const y = safeParseFloat(token.props.args.y) ?? 0;
 						style = `transform: translateX(${x}em) translateY(${y}em);`;
 						break;
 					}
@@ -242,8 +244,8 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 							style = '';
 							break;
 						}
-						const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
-						const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
+						const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5);
+						const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5);
 						style = `transform: scale(${x}, ${y});`;
 						scale = scale * Math.max(x, y);
 						break;
@@ -265,11 +267,12 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 						color = color ? `#${color}` : 'var(--accent)';
 						let b_style = token.props.args.style;
 						if (
+							typeof b_style !== 'string' ||
 							!['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
 								.includes(b_style)
 						) b_style = 'solid';
-						const width = parseFloat(token.props.args.width ?? '1');
-						const radius = parseFloat(token.props.args.radius ?? '0');
+						const width = safeParseFloat(token.props.args.width) ?? 1;
+						const radius = safeParseFloat(token.props.args.radius) ?? 0;
 						style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`;
 						break;
 					}
@@ -311,7 +314,8 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 						return h('span', { onClick(ev: MouseEvent): void {
 							ev.stopPropagation();
 							ev.preventDefault();
-							context.emit('clickEv', token.props.args.ev ?? '');
+							const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : '';
+							emit('clickEv', clickEv);
 						} }, genEl(token.children, scale));
 					}
 				}
@@ -372,7 +376,7 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 				return [h(MkCode, {
 					key: Math.random(),
 					code: token.props.code,
-					lang: token.props.lang,
+					lang: token.props.lang ?? undefined,
 				})];
 			}
 
@@ -415,8 +419,7 @@ export default function(props: MfmProps, context: SetupContext<MfmEvents>) {
 						return [h(MkCustomEmoji, {
 							key: Math.random(),
 							name: token.props.name,
-							// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-							url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
+							url: props.emojiUrls && props.emojiUrls[token.props.name],
 							normal: props.plain,
 							host: props.author.host,
 							useOriginalSize: scale >= 2.5,
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 75c8e73582..0b69e885c8 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -38,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 export type Tab = {
 	key: string;
+	title: string;
 	onClick?: (ev: MouseEvent) => void;
 } & (
 	| {
@@ -120,8 +121,9 @@ function onTabWheel(ev: WheelEvent) {
 
 let entering = false;
 
-async function enter(el: HTMLElement) {
+async function enter(element: Element) {
 	entering = true;
+	const el = element as HTMLElement;
 	const elementWidth = el.getBoundingClientRect().width;
 	el.style.width = '0';
 	el.style.paddingLeft = '0';
@@ -135,11 +137,12 @@ async function enter(el: HTMLElement) {
 	setTimeout(renderTab, 170);
 }
 
-function afterEnter(el: HTMLElement) {
+function afterEnter(element: Element) {
 	//el.style.width = '';
 }
 
-async function leave(el: HTMLElement) {
+async function leave(element: Element) {
+	const el = element as HTMLElement;
 	const elementWidth = el.getBoundingClientRect().width;
 	el.style.width = elementWidth + 'px';
 	el.style.paddingLeft = '';
@@ -148,7 +151,8 @@ async function leave(el: HTMLElement) {
 	el.style.paddingLeft = '0';
 }
 
-function afterLeave(el: HTMLElement) {
+function afterLeave(element: Element) {
+	const el = element as HTMLElement;
 	el.style.width = '';
 }
 
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 70cc68b14c..c528b80285 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -63,27 +63,32 @@ onMounted(() => {
 	watch([parentStickyTop, parentStickyBottom], calc);
 
 	watch(childStickyTop, () => {
+		if (bodyEl.value == null) return;
 		bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
 	}, {
 		immediate: true,
 	});
 
 	watch(childStickyBottom, () => {
+		if (bodyEl.value == null) return;
 		bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
 	}, {
 		immediate: true,
 	});
 
-	headerEl.value.style.position = 'sticky';
-	headerEl.value.style.top = 'var(--stickyTop, 0)';
-	headerEl.value.style.zIndex = '1000';
+	if (headerEl.value != null) {
+		headerEl.value.style.position = 'sticky';
+		headerEl.value.style.top = 'var(--stickyTop, 0)';
+		headerEl.value.style.zIndex = '1000';
+		observer.observe(headerEl.value);
+	}
 
-	footerEl.value.style.position = 'sticky';
-	footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
-	footerEl.value.style.zIndex = '1000';
-
-	observer.observe(headerEl.value);
-	observer.observe(footerEl.value);
+	if (footerEl.value != null) {
+		footerEl.value.style.position = 'sticky';
+		footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
+		footerEl.value.style.zIndex = '1000';
+		observer.observe(footerEl.value);
+	}
 });
 
 onUnmounted(() => {
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 2b0bf246ad..a81ba42a5b 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -24,7 +24,7 @@ const props = withDefaults(defineProps<{
 	mode?: 'relative' | 'absolute' | 'detail';
 	colored?: boolean;
 }>(), {
-	origin: isChromatic() ? new Date('2023-04-01T00:00:00Z') : null,
+	origin: isChromatic() ? () => new Date('2023-04-01T00:00:00Z') : null,
 	mode: 'relative',
 });
 
diff --git a/packages/frontend/src/components/page/block.type.ts b/packages/frontend/src/components/page/block.type.ts
deleted file mode 100644
index cdd39339e6..0000000000
--- a/packages/frontend/src/components/page/block.type.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export type BlockBase = {
-	id: string;
-	type: string;
-};
-
-export type TextBlock = BlockBase & {
-	type: 'text';
-	text: string;
-};
-
-export type SectionBlock = BlockBase & {
-	type: 'section';
-	title: string;
-	children: Block[];
-};
-
-export type ImageBlock = BlockBase & {
-	type: 'image';
-	fileId: string | null;
-};
-
-export type NoteBlock = BlockBase & {
-	type: 'note';
-	detailed: boolean;
-	note: string | null;
-};
-
-export type Block =
-	TextBlock | SectionBlock | ImageBlock | NoteBlock;
diff --git a/packages/frontend/src/components/page/page.block.vue b/packages/frontend/src/components/page/page.block.vue
index 7dbbaa03b4..c53ca6519d 100644
--- a/packages/frontend/src/components/page/page.block.vue
+++ b/packages/frontend/src/components/page/page.block.vue
@@ -14,7 +14,6 @@ import XText from './page.text.vue';
 import XSection from './page.section.vue';
 import XImage from './page.image.vue';
 import XNote from './page.note.vue';
-import { Block } from './block.type.js';
 
 function getComponent(type: string) {
 	switch (type) {
@@ -27,7 +26,7 @@ function getComponent(type: string) {
 }
 
 defineProps<{
-	block: Block,
+	block: Misskey.entities.PageBlock,
 	h: number,
 	page: Misskey.entities.Page,
 }>();
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index 29aebf63e5..af37c7b1b3 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -14,15 +14,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref } from 'vue';
+import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { ImageBlock } from './block.type.js';
 import MediaImage from '@/components/MkMediaImage.vue';
 
 const props = defineProps<{
-	block: ImageBlock,
+	block: Misskey.entities.PageBlock,
 	page: Misskey.entities.Page,
 }>();
 
-const image = ref<Misskey.entities.DriveFile>(props.page.attachedFiles.find(x => x.id === props.block.fileId));
+const image = ref<Misskey.entities.DriveFile | null>(null);
+
+onMounted(() => {
+	image.value = props.page.attachedFiles.find(x => x.id === props.block.fileId) ?? null;
+});
+
 </script>
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index 83fdf24deb..5093ee9b79 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -13,19 +13,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { NoteBlock } from './block.type.js';
 import MkNote from '@/components/MkNote.vue';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 
 const props = defineProps<{
-	block: NoteBlock,
+	block: Misskey.entities.PageBlock,
 	page: Misskey.entities.Page,
 }>();
 
 const note = ref<Misskey.entities.Note | null>(null);
 
 onMounted(() => {
+	if (props.block.note == null) return;
 	misskeyApi('notes/show', { noteId: props.block.note })
 		.then(result => {
 			note.value = result;
diff --git a/packages/frontend/src/components/page/page.section.vue b/packages/frontend/src/components/page/page.section.vue
index e4e5a43b59..63c155ada6 100644
--- a/packages/frontend/src/components/page/page.section.vue
+++ b/packages/frontend/src/components/page/page.section.vue
@@ -25,12 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
-import { SectionBlock } from './block.type.js';
 
 const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
 
 defineProps<{
-	block: SectionBlock,
+	block: Misskey.entities.PageBlock,
 	h: number,
 	page: Misskey.entities.Page,
 }>();
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index 2cb49d9a8f..b66f0a23ec 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_gaps">
-	<Mfm :text="block.text" :isNote="false"/>
+	<Mfm :text="block.text ?? ''" :isNote="false"/>
 	<MkUrlPreview v-for="url in urls" :key="url" :url="url"/>
 </div>
 </template>
@@ -14,13 +14,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent } from 'vue';
 import * as mfm from '@transfem-org/sfm-js';
 import * as Misskey from 'misskey-js';
-import { TextBlock } from './block.type.js';
 import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
 
 const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
 
 const props = defineProps<{
-	block: TextBlock,
+	block: Misskey.entities.PageBlock,
 	page: Misskey.entities.Page,
 }>();
 
diff --git a/packages/frontend/src/nirax.ts b/packages/frontend/src/nirax.ts
index 587f88412d..878335b9f3 100644
--- a/packages/frontend/src/nirax.ts
+++ b/packages/frontend/src/nirax.ts
@@ -9,16 +9,25 @@ import { Component, onMounted, shallowRef, ShallowRef } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import { safeURIDecode } from '@/scripts/safe-uri-decode.js';
 
-export type RouteDef = {
+interface RouteDefBase {
 	path: string;
-	component: Component;
 	query?: Record<string, string>;
 	loginRequired?: boolean;
 	name?: string;
 	hash?: string;
 	globalCacheKey?: string;
 	children?: RouteDef[];
-};
+}
+
+interface RouteDefWithComponent extends RouteDefBase {
+	component: Component,
+}
+
+interface RouteDefWithRedirect extends RouteDefBase {
+	redirect: string | ((props: Map<string, string | boolean>) => string);
+}
+
+export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
 
 type ParsedPath = (string | {
 	name: string;
@@ -48,7 +57,19 @@ export type RouterEvent = {
 	same: () => void;
 }
 
-export type Resolved = { route: RouteDef; props: Map<string, string | boolean>; child?: Resolved; };
+export type Resolved = {
+	route: RouteDef;
+	props: Map<string, string | boolean>;
+	child?: Resolved;
+	redirected?: boolean;
+
+	/** @internal */
+	_parsedRoute: {
+		fullPath: string;
+		queryString: string | null;
+		hash: string | null;
+	};
+};
 
 function parsePath(path: string): ParsedPath {
 	const res = [] as ParsedPath;
@@ -81,6 +102,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 	currentRoute: ShallowRef<RouteDef>;
 	navHook: ((path: string, flag?: any) => boolean) | null;
 
+	/**
+	 * ルートの初期化(eventListenerの定義後に必ず呼び出すこと)
+	 */
+	init(): void;
+
 	resolve(path: string): Resolved | null;
 
 	getCurrentPath(): any;
@@ -156,12 +182,13 @@ export interface IRouter extends EventEmitter<RouterEvent> {
 export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	private routes: RouteDef[];
 	public current: Resolved;
-	public currentRef: ShallowRef<Resolved> = shallowRef();
-	public currentRoute: ShallowRef<RouteDef> = shallowRef();
-	private currentPath = '';
+	public currentRef: ShallowRef<Resolved>;
+	public currentRoute: ShallowRef<RouteDef>;
+	private currentPath: string;
 	private isLoggedIn: boolean;
 	private notFoundPageComponent: Component;
 	private currentKey = Date.now().toString();
+	private redirectCount = 0;
 
 	public navHook: ((path: string, flag?: any) => boolean) | null = null;
 
@@ -169,13 +196,24 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		super();
 
 		this.routes = routes;
-		//this.currentPath = currentPath;
+		this.current = this.resolve(currentPath)!;
+		this.currentRef = shallowRef(this.current);
+		this.currentRoute = shallowRef(this.current.route);
+		this.currentPath = currentPath;
 		this.isLoggedIn = isLoggedIn;
 		this.notFoundPageComponent = notFoundPageComponent;
-		this.navigate(currentPath, null, false);
+	}
+
+	public init() {
+		const res = this.navigate(this.currentPath, null, false);
+		this.emit('replace', {
+			path: res._parsedRoute.fullPath,
+			key: this.currentKey,
+		});
 	}
 
 	public resolve(path: string): Resolved | null {
+		const fullPath = path;
 		let queryString: string | null = null;
 		let hash: string | null = null;
 		if (path[0] === '/') path = path.substring(1);
@@ -188,6 +226,12 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 			path = path.substring(0, path.indexOf('?'));
 		}
 
+		const _parsedRoute = {
+			fullPath,
+			queryString,
+			hash,
+		};
+
 		if (_DEV_) console.log('Routing: ', path, queryString);
 
 		function check(routes: RouteDef[], _parts: string[]): Resolved | null {
@@ -238,6 +282,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 								route,
 								props,
 								child,
+								_parsedRoute,
 							};
 						} else {
 							continue forEachRouteLoop;
@@ -263,6 +308,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 					return {
 						route,
 						props,
+						_parsedRoute,
 					};
 				} else {
 					if (route.children) {
@@ -272,6 +318,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 								route,
 								props,
 								child,
+								_parsedRoute,
 							};
 						} else {
 							continue forEachRouteLoop;
@@ -290,7 +337,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		return check(this.routes, _parts);
 	}
 
-	private navigate(path: string, key: string | null | undefined, emitChange = true) {
+	private navigate(path: string, key: string | null | undefined, emitChange = true, _redirected = false): Resolved {
 		const beforePath = this.currentPath;
 		this.currentPath = path;
 
@@ -300,6 +347,20 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 			throw new Error('no route found for: ' + path);
 		}
 
+		if ('redirect' in res.route) {
+			let redirectPath: string;
+			if (typeof res.route.redirect === 'function') {
+				redirectPath = res.route.redirect(res.props);
+			} else {
+				redirectPath = res.route.redirect + (res._parsedRoute.queryString ? '?' + res._parsedRoute.queryString : '') + (res._parsedRoute.hash ? '#' + res._parsedRoute.hash : '');
+			}
+			if (_DEV_) console.log('Redirecting to: ', redirectPath);
+			if (_redirected && this.redirectCount++ > 10) {
+				throw new Error('redirect loop detected');
+			}
+			return this.navigate(redirectPath, null, emitChange, true);
+		}
+
 		if (res.route.loginRequired && !this.isLoggedIn) {
 			res.route.component = this.notFoundPageComponent;
 			res.props.set('showLoginPopup', true);
@@ -321,7 +382,11 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 			});
 		}
 
-		return res;
+		this.redirectCount = 0;
+		return {
+			...res,
+			redirected: _redirected,
+		};
 	}
 
 	public getCurrentPath() {
@@ -345,7 +410,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 		const res = this.navigate(path, null);
 		this.emit('push', {
 			beforePath,
-			path,
+			path: res._parsedRoute.fullPath,
 			route: res.route,
 			props: res.props,
 			key: this.currentKey,
@@ -353,15 +418,20 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
 	}
 
 	public replace(path: string, key?: string | null) {
-		this.navigate(path, key);
+		const res = this.navigate(path, key);
+		this.emit('replace', {
+			path: res._parsedRoute.fullPath,
+			key: this.currentKey,
+		});
 	}
 }
 
-export function useScrollPositionManager(getScrollContainer: () => HTMLElement, router: IRouter) {
+export function useScrollPositionManager(getScrollContainer: () => HTMLElement | null, router: IRouter) {
 	const scrollPosStore = new Map<string, number>();
 
 	onMounted(() => {
 		const scrollContainer = getScrollContainer();
+		if (scrollContainer == null) return;
 
 		scrollContainer.addEventListener('scroll', () => {
 			scrollPosStore.set(router.getCurrentKey(), scrollContainer.scrollTop);
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 1d42c7e4d5..7cce77cdf7 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -9,6 +9,7 @@ import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import insertTextAtCursor from 'insert-text-at-cursor';
 import * as Misskey from 'misskey-js';
+import type { ComponentProps } from 'vue-component-type-helpers';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
@@ -143,7 +144,7 @@ export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
 	return zIndexes[priority];
 }
 
-export async function popup(component: Component, props: Record<string, any>, events = {}, disposeEvent?: string) {
+export async function popup<T extends Component>(component: T, props: ComponentProps<T>, events = {}, disposeEvent?: string) {
 	markRaw(component);
 
 	const id = ++popupIdCount;
@@ -419,11 +420,11 @@ export function form(title, form) {
 	});
 }
 
-export async function selectUser(opts: { includeSelf?: boolean, local?: boolean } = {}): Promise<Misskey.entities.UserLite> {
+export async function selectUser(opts: { includeSelf?: boolean; localOnly?: boolean; } = {}): Promise<Misskey.entities.UserDetailed> {
 	return new Promise((resolve, reject) => {
 		popup(defineAsyncComponent(() => import('@/components/MkUserSelectDialog.vue')), {
 			includeSelf: opts.includeSelf,
-			local: opts.local,
+			localOnly: opts.localOnly,
 		}, {
 			ok: user => {
 				resolve(user);
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index bcdd2c50a7..3d36b3bfa5 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -80,7 +80,7 @@ function show(file) {
 async function find() {
 	const { canceled, result: q } = await os.inputText({
 		title: i18n.ts.fileIdOrUrl,
-		allowEmpty: false,
+		minLength: 1,
 	});
 	if (canceled) return;
 
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index ab6f3007c0..b174b3f64d 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -38,7 +38,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
 import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const isEmpty = (x: string | null) => x == null || x === '';
 
diff --git a/packages/frontend/src/pages/admin/proxy-account.vue b/packages/frontend/src/pages/admin/proxy-account.vue
index 860f4f4b2c..32e2bc7cc2 100644
--- a/packages/frontend/src/pages/admin/proxy-account.vue
+++ b/packages/frontend/src/pages/admin/proxy-account.vue
@@ -45,7 +45,7 @@ async function init() {
 }
 
 function chooseProxyAccount() {
-	os.selectUser().then(user => {
+	os.selectUser({ localOnly: true }).then(user => {
 		proxyAccount.value = user;
 		proxyAccountId.value = user.id;
 		save();
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 9729d80eaa..ff6fb360f4 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -33,7 +33,7 @@ import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import MkButton from '@/components/MkButton.vue';
 import { rolesCache } from '@/cache.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index fc7055df43..d0806b4dd6 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -75,7 +75,7 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { infoImageUrl } from '@/instance.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
@@ -116,9 +116,7 @@ async function del() {
 }
 
 async function assign() {
-	const user = await os.selectUser({
-		includeSelf: true,
-	});
+	const user = await os.selectUser({ includeSelf: true });
 
 	const { canceled: canceled2, result: period } = await os.select({
 		title: i18n.ts.period,
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index ca918faf8a..29935974c5 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -259,7 +259,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { instance } from '@/instance.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { ROLE_POLICIES } from '@/const.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 const baseRoleQ = ref('');
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index 1bc4eb4089..51edfd5263 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -91,7 +91,7 @@ const pagination = {
 };
 
 function searchUser() {
-	os.selectUser().then(user => {
+	os.selectUser({ includeSelf: true }).then(user => {
 		show(user);
 	});
 }
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index febc77136b..a4816921d5 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -32,7 +32,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index ef7d258389..23d2027ab7 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -82,7 +82,7 @@ import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index f9bbe831a3..657da54050 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -94,7 +94,7 @@ import { PageHeaderItem } from '@/types/page-header.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index 143c9414ca..cf3c6b9e02 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -63,7 +63,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 7e8c6f9309..3f26834a24 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -80,7 +80,7 @@ import { infoImageUrl } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index 5fd527a59c..2e5c2036ee 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -45,7 +45,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const PRESET_DEFAULT = `/// @ 0.16.0
 
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index c846d40dd9..00d1bace23 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -47,7 +47,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/follow.vue b/packages/frontend/src/pages/follow.vue
index 44364bb0f2..42e60fe657 100644
--- a/packages/frontend/src/pages/follow.vue
+++ b/packages/frontend/src/pages/follow.vue
@@ -15,7 +15,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { defaultStore } from '@/store.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 async function follow(user): Promise<void> {
 	const { canceled } = await os.confirm({
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index 7dfa2b592e..5b9355f446 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -50,7 +50,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index 0401b7340a..23196fc02b 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -54,7 +54,7 @@ import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index e84959dd5a..b190ab9c30 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -78,7 +78,7 @@ import { defaultStore } from '@/store.js';
 import { $i } from '@/account.js';
 import { isSupportShare } from '@/scripts/navigator.js';
 import copyToClipboard from '@/scripts/copy-to-clipboard.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index c1f8064c23..0794597505 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -15,7 +15,7 @@ import XAntenna from './editor.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache } from '@/cache.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 38bfa9f0cb..5b4db5e530 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -17,7 +17,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { antennasCache } from '@/cache.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/my-antennas/editor.vue b/packages/frontend/src/pages/my-antennas/editor.vue
index 4f78964ade..5799762352 100644
--- a/packages/frontend/src/pages/my-antennas/editor.vue
+++ b/packages/frontend/src/pages/my-antennas/editor.vue
@@ -129,7 +129,7 @@ async function deleteAntenna() {
 }
 
 function addUser() {
-	os.selectUser().then(user => {
+	os.selectUser({ includeSelf: true }).then(user => {
 		users.value = users.value.trim();
 		users.value += '\n@' + Misskey.acct.toString(user as any);
 		users.value = users.value.trim();
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index f4cc3bad02..672981c500 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -69,7 +69,7 @@ import { userListsCache } from '@/cache.js';
 import { signinRequired } from '@/account.js';
 import { defaultStore } from '@/store.js';
 import MkPagination from '@/components/MkPagination.vue';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index 403d73e380..21977114b9 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -75,7 +75,7 @@ import { selectFile } from '@/scripts/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { $i } from '@/account.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const props = defineProps<{
 	initPageId?: string;
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index f8f3a8bb32..14f3fa5b41 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -45,7 +45,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 6263c9a460..805754c327 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -26,7 +26,7 @@ import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const props = defineProps<{
 	token?: string;
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index f67d177ac3..33eb7f86a8 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -37,11 +37,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.board">
 			<div :class="$style.boardInner">
 				<div v-if="showBoardLabels" :class="$style.labelsX">
-					<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
+					<span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
 				</div>
 				<div style="display: flex;">
 					<div v-if="showBoardLabels" :class="$style.labelsY">
-						<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
+						<div v-for="i in game.map.length" :key="i" :class="$style.labelsYLabel">{{ i }}</div>
 					</div>
 					<div :class="$style.boardCells" :style="cellsStyle">
 						<div
@@ -66,8 +66,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 								mode="default"
 							>
 								<template v-if="useAvatarAsStone">
-									<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl"/>
-									<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl"/>
+									<img v-if="stone === true" :class="$style.boardCellStone" :src="blackUser.avatarUrl ?? undefined"/>
+									<img v-else-if="stone === false" :class="$style.boardCellStone" :src="whiteUser.avatarUrl ?? undefined"/>
 								</template>
 								<template v-else>
 									<img v-if="stone === true" :class="$style.boardCellStone" src="/client-assets/reversi/stone_b.png"/>
@@ -77,11 +77,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</div>
 					</div>
 					<div v-if="showBoardLabels" :class="$style.labelsY">
-						<div v-for="i in game.map.length" :class="$style.labelsYLabel">{{ i }}</div>
+						<div v-for="i in game.map.length" :key="i" :class="$style.labelsYLabel">{{ i }}</div>
 					</div>
 				</div>
 				<div v-if="showBoardLabels" :class="$style.labelsX">
-					<span v-for="i in game.map[0].length" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
+					<span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
 				</div>
 			</div>
 		</div>
@@ -162,13 +162,14 @@ const $i = signinRequired();
 
 const props = defineProps<{
 	game: Misskey.entities.ReversiGameDetailed;
-	connection?: Misskey.ChannelConnection | null;
+	connection?: Misskey.ChannelConnection<Misskey.Channels['reversiGame']> | null;
 }>();
 
 const showBoardLabels = ref<boolean>(false);
 const useAvatarAsStone = ref<boolean>(true);
 const autoplaying = ref<boolean>(false);
-const game = ref<Misskey.entities.ReversiGameDetailed>(deepClone(props.game));
+// eslint-disable-next-line vue/no-setup-props-destructure
+const game = ref<Misskey.entities.ReversiGameDetailed & { logs: Reversi.Serializer.SerializedLog[] }>(deepClone(props.game));
 const logPos = ref<number>(game.value.logs.length);
 const engine = shallowRef<Reversi.Game>(Reversi.Serializer.restoreGame({
 	map: game.value.map,
@@ -256,7 +257,7 @@ if (game.value.isStarted && !game.value.isEnded) {
 
 const appliedOps: string[] = [];
 
-function putStone(pos) {
+function putStone(pos: number) {
 	if (game.value.isEnded) return;
 	if (!iAmPlayer.value) return;
 	if (!isMyTurn.value) return;
@@ -305,7 +306,7 @@ if (!props.game.isEnded) {
 	}, TIMER_INTERVAL_SEC * 1000, { immediate: false, afterMounted: true });
 }
 
-async function onStreamLog(log: Reversi.Serializer.Log & { id: string | null }) {
+async function onStreamLog(log) {
 	game.value.logs = Reversi.Serializer.serializeLogs([
 		...Reversi.Serializer.deserializeLogs(game.value.logs),
 		log,
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index f64c98d0f3..4b92eaeb9b 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -122,7 +122,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
 import { MenuItem } from '@/types/menu.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index ea96609532..876cf048e6 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -18,7 +18,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { useStream } from '@/stream.js';
 import { signinRequired } from '@/account.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { useInterval } from '@/scripts/use-interval.js';
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index c9a6012e75..fb812589b4 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -115,9 +115,10 @@ import MkFolder from '@/components/MkFolder.vue';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import MkPagination from '@/components/MkPagination.vue';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { useInterval } from '@/scripts/use-interval.js';
+import { pleaseLogin } from '@/scripts/please-login.js';
 import * as sound from '@/scripts/sound.js';
 
 const myGamesPagination = {
@@ -193,7 +194,9 @@ async function matchHeatbeat() {
 }
 
 async function matchUser() {
-	const user = await os.selectUser({ local: true, includeSelf: false });
+	pleaseLogin();
+
+	const user = await os.selectUser({ includeSelf: false, localOnly: true });
 	if (user == null) return;
 
 	matchingUser.value = user;
@@ -202,6 +205,8 @@ async function matchUser() {
 }
 
 function matchAny(ev: MouseEvent) {
+	pleaseLogin();
+
 	os.popupMenu([{
 		text: i18n.ts._reversi.allowIrregularRules,
 		action: () => {
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 8ce84de06c..18c8746fa0 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<option value="audio">Audio</option>
 				</MkSelect>
 
-				<MkFolder>
+				<MkFolder :defaultOpen="true">
 					<template #label>{{ i18n.ts.specifyUser }}</template>
 					<template v-if="user" #suffix>@{{ user.username }}</template>
 
@@ -61,7 +61,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
@@ -75,7 +75,7 @@ const order = ref(false);
 const filetype = ref(null);
 
 function selectUser() {
-	os.selectUser().then(_user => {
+	os.selectUser({ includeSelf: true }).then(_user => {
 		user.value = _user;
 	});
 }
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index ac3380bc5f..80e8172d4d 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -34,7 +34,7 @@ import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index bcdce7bb68..401c2aa32f 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -36,7 +36,7 @@ import { clearCache } from '@/scripts/clear-cache.js';
 import { instance } from '@/instance.js';
 import { PageMetadata, definePageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import * as os from '@/os.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const indexInfo = {
 	title: i18n.ts.settings,
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 2324208964..b5d0894c71 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -51,7 +51,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index e2d3362fb8..d70cb1f687 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -56,7 +56,7 @@ const renote = ref<Misskey.entities.Note | undefined>();
 const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visibilityQuery : undefined);
 const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined);
 const files = ref([] as Misskey.entities.DriveFile[]);
-const visibleUsers = ref([] as Misskey.entities.User[]);
+const visibleUsers = ref([] as Misskey.entities.UserDetailed[]);
 
 async function init() {
 	let noteText = '';
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 1969ec8613..7d3e90f0c3 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -186,7 +186,7 @@ function applyThemeCode() {
 async function saveAs() {
 	const { canceled, result: name } = await os.inputText({
 		title: i18n.ts.name,
-		allowEmpty: false,
+		minLength: 1,
 	});
 	if (canceled) return;
 
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index fcff94e10f..3a1c3738f0 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -31,7 +31,7 @@ import { scroll } from '@/scripts/scroll.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 876ab8b208..40e117b1ca 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -192,7 +192,7 @@ import { confetti } from '@/scripts/confetti.js';
 import { defaultStore } from '@/store.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index 4880126698..4021f04ad8 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -96,7 +96,7 @@ const headerTabs = computed(() => user.value ? [{
 	key: 'achievements',
 	title: i18n.ts.achievements,
 	icon: 'ph-trophy ph-bold ph-lg',
-}] : []), ...($i && ($i.id === user.value.id)) || user.value.publicReactions ? [{
+}] : []), ...($i && ($i.id === user.value.id || $i.isAdmin || $i.isModerator)) || user.value.publicReactions ? [{
 	key: 'reactions',
 	title: i18n.ts.reaction,
 	icon: 'ph-smiley ph-bold ph-lg',
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 228f5a13b7..f9d7f8c9e2 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkMediaList :mediaList="note.files"/>
 				</div>
 				<div v-if="note.poll">
-					<MkPoll :note="note" :readOnly="true"/>
+					<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
 				</div>
 			</div>
 			<MkReactionsViewer ref="reactionsViewer" :note="note"/>
diff --git a/packages/frontend/src/global/router/definition.ts b/packages/frontend/src/router/definition.ts
similarity index 92%
rename from packages/frontend/src/global/router/definition.ts
rename to packages/frontend/src/router/definition.ts
index a24d34f67b..f2b3eacb13 100644
--- a/packages/frontend/src/global/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -4,11 +4,12 @@
  */
 
 import { App, AsyncComponentLoader, defineAsyncComponent, provide } from 'vue';
+import type { RouteDef } from '@/nirax.js';
 import { IRouter, Router } from '@/nirax.js';
 import { $i, iAmModerator } from '@/account.js';
 import MkLoading from '@/pages/_loading_.vue';
 import MkError from '@/pages/_error_.vue';
-import { setMainRouter } from '@/global/router/main.js';
+import { setMainRouter } from '@/router/main.js';
 
 const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
 	loader: loader,
@@ -16,7 +17,7 @@ const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({
 	errorComponent: MkError,
 });
 
-const routes = [{
+const routes: RouteDef[] = [{
 	path: '/@:initUser/pages/:initPageName/view-source',
 	component: page(() => import('@/pages/page-editor/page-editor.vue')),
 }, {
@@ -333,8 +334,7 @@ const routes = [{
 	component: page(() => import('@/pages/registry.vue')),
 }, {
 	path: '/install-extentions',
-	// Note: This path is kept for compatibility. It may be deleted.
-	component: page(() => import('@/pages/install-extensions.vue')),
+	redirect: '/install-extensions',
 	loginRequired: true,
 }, {
 	path: '/install-extensions',
@@ -561,6 +561,11 @@ const routes = [{
 	path: '/',
 	component: $i ? page(() => import('@/pages/timeline.vue')) : page(() => import('@/pages/welcome.vue')),
 	globalCacheKey: 'index',
+}, {
+	// テスト用リダイレクト設定。ログイン中ユーザのプロフィールにリダイレクトする
+	path: '/redirect-test',
+	redirect: $i ? `@${$i.username}` : '/',
+	loginRequired: true,
 }, {
 	path: '/:(*)',
 	component: page(() => import('@/pages/not-found.vue')),
@@ -579,51 +584,23 @@ export function setupRouter(app: App) {
 
 	const mainRouter = createRouterImpl(location.pathname + location.search + location.hash);
 
-	window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
-
-	const scrollPosStore = new Map<string, number>();
-	let restoring = false;
-
-	window.setInterval(() => {
-		if (!restoring) {
-			scrollPosStore.set(window.history.state?.key, window.scrollY);
-		}
-	}, 1000);
-
 	window.addEventListener('popstate', (event) => {
 		mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
-
-		restoring = true;
-		const scrollPos = scrollPosStore.get(event.state?.key) ?? 0;
-		window.scroll({ top: scrollPos, behavior: 'instant' });
-		window.setTimeout(() => {
-			// 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
-			window.scroll({ top: scrollPos, behavior: 'instant' });
-			restoring = false;
-		}, 100);
 	});
 
 	mainRouter.addListener('push', ctx => {
 		window.history.pushState({ key: ctx.key }, '', ctx.path);
-
-		restoring = true;
-		const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
-		window.scroll({ top: scrollPos, behavior: 'instant' });
-
-		if (scrollPos !== 0) {
-			window.setTimeout(() => {
-				// 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
-				window.scroll({ top: scrollPos, behavior: 'instant' });
-			}, 100);
-			restoring = false;
-		} else {
-			restoring = false;
-		}
 	});
 
 	mainRouter.addListener('same', () => {
 		window.scroll({ top: 0, behavior: 'smooth' });
 	});
 
+	mainRouter.addListener('replace', ctx => {
+		window.history.replaceState({ key: ctx.key }, '', ctx.path);
+	});
+
+	mainRouter.init();
+
 	setMainRouter(mainRouter);
 }
diff --git a/packages/frontend/src/global/router/main.ts b/packages/frontend/src/router/main.ts
similarity index 100%
rename from packages/frontend/src/global/router/main.ts
rename to packages/frontend/src/router/main.ts
diff --git a/packages/frontend/src/global/router/supplier.ts b/packages/frontend/src/router/supplier.ts
similarity index 94%
rename from packages/frontend/src/global/router/supplier.ts
rename to packages/frontend/src/router/supplier.ts
index 1e321ef21f..cac6b32585 100644
--- a/packages/frontend/src/global/router/supplier.ts
+++ b/packages/frontend/src/router/supplier.ts
@@ -5,7 +5,7 @@
 
 import { inject } from 'vue';
 import { IRouter, Router } from '@/nirax.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 /**
  * メインの{@link Router}を取得する。
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts
index b0c36cb927..36264fc459 100644
--- a/packages/frontend/src/scripts/autocomplete.ts
+++ b/packages/frontend/src/scripts/autocomplete.ts
@@ -19,7 +19,7 @@ export class Autocomplete {
 	} | null;
 	private textarea: HTMLInputElement | HTMLTextAreaElement;
 	private currentType: string;
-	private textRef: Ref<string>;
+	private textRef: Ref<string | number | null>;
 	private opening: boolean;
 	private onlyType: SuggestionType[];
 
@@ -38,7 +38,7 @@ export class Autocomplete {
 	/**
 	 * 対象のテキストエリアを与えてインスタンスを初期化します。
 	 */
-	constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) {
+	constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string | number | null>, onlyType?: SuggestionType[]) {
 		//#region BIND
 		this.onInput = this.onInput.bind(this);
 		this.complete = this.complete.bind(this);
diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts
index 96b53684f3..ac38faefaa 100644
--- a/packages/frontend/src/scripts/clone.ts
+++ b/packages/frontend/src/scripts/clone.ts
@@ -8,7 +8,7 @@
 // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった
 // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045
 
-type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[];
+type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | Cloneable[];
 
 export function deepClone<T extends Cloneable>(x: T): T {
 	if (typeof x === 'object') {
@@ -16,7 +16,7 @@ export function deepClone<T extends Cloneable>(x: T): T {
 		if (Array.isArray(x)) return x.map(deepClone) as T;
 		const obj = {} as Record<string, Cloneable>;
 		for (const [k, v] of Object.entries(x)) {
-			obj[k] = deepClone(v);
+			obj[k] = v === undefined ? undefined : deepClone(v);
 		}
 		return obj as T;
 	} else {
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 957669122e..bc05ec94d5 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -1,7 +1,6 @@
-import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki';
-
-setWasm('/assets/shiki/dist/onig.wasm');
-setCDN('/assets/shiki/');
+import { getHighlighterCore, loadWasm } from 'shiki/core';
+import darkPlus from 'shiki/themes/dark-plus.mjs';
+import type { Highlighter, LanguageRegistration } from 'shiki';
 
 let _highlighter: Highlighter | null = null;
 
@@ -13,16 +12,19 @@ export async function getHighlighter(): Promise<Highlighter> {
 }
 
 export async function initHighlighter() {
-	const highlighter = await _getHighlighter({
-		theme: 'dark-plus',
-		langs: ['js'],
-	});
+	const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
 
-	await highlighter.loadLanguage({
-		path: 'languages/aiscript.tmLanguage.json',
-		id: 'aiscript',
-		scopeName: 'source.aiscript',
-		aliases: ['is', 'ais'],
+	await loadWasm(import('shiki/onig.wasm?init'));
+
+	const highlighter = await getHighlighterCore({
+		themes: [darkPlus],
+		langs: [
+			import('shiki/langs/javascript.mjs'),
+			{
+				aliases: ['is', 'ais'],
+				...aiScriptGrammar.default,
+			} as unknown as LanguageRegistration,
+		],
 	});
 
 	_highlighter = highlighter;
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 445c601800..b58755fe58 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { defineAsyncComponent, Ref } from 'vue';
+import { defineAsyncComponent, Ref, ShallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { claimAchievement } from './achievements.js';
 import { $i } from '@/account.js';
@@ -36,7 +36,7 @@ export async function getNoteClipMenu(props: {
 	const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note;
 
 	const clips = await clipsCache.fetch();
-	return [...clips.map(clip => ({
+	const menu: MenuItem[] = [...clips.map(clip => ({
 		text: clip.name,
 		action: () => {
 			claimAchievement('noteClipped1');
@@ -93,6 +93,8 @@ export async function getNoteClipMenu(props: {
 			os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
 		},
 	}];
+
+	return menu;
 }
 
 export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem {
@@ -133,7 +135,6 @@ export function getCopyNoteOriginLinkMenu(note: misskey.entities.Note, text: str
 
 export function getNoteMenu(props: {
 	note: Misskey.entities.Note;
-	menuButton: Ref<HTMLElement>;
 	translation: Ref<Misskey.entities.NotesTranslateResponse | null>;
 	translating: Ref<boolean>;
 	isDeleted: Ref<boolean>;
@@ -505,7 +506,7 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi
 
 export function getRenoteMenu(props: {
 	note: Misskey.entities.Note;
-	renoteButton: Ref<HTMLElement>;
+	renoteButton: ShallowRef<HTMLElement | undefined>;
 	mock?: boolean;
 }) {
 	const isRenote = (
@@ -525,7 +526,7 @@ export function getRenoteMenu(props: {
 			text: i18n.ts.inChannelRenote,
 			icon: 'ti ti-repeat',
 			action: () => {
-				const el = props.renoteButton.value as HTMLElement | null | undefined;
+				const el = props.renoteButton.value;
 				if (el) {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
@@ -561,7 +562,7 @@ export function getRenoteMenu(props: {
 			text: i18n.ts.renote,
 			icon: 'ti ti-repeat',
 			action: () => {
-				const el = props.renoteButton.value as HTMLElement | null | undefined;
+				const el = props.renoteButton.value;
 				if (el) {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
@@ -601,7 +602,7 @@ export function getRenoteMenu(props: {
 
 	const renoteItems = [
 		...normalRenoteItems,
-		...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [],
+		...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [],
 		...channelRenoteItems,
 	];
 
diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts
index 2007e0ea97..72153ceb75 100644
--- a/packages/frontend/src/scripts/get-note-summary.ts
+++ b/packages/frontend/src/scripts/get-note-summary.ts
@@ -10,7 +10,11 @@ import { i18n } from '@/i18n.js';
  * 投稿を表す文字列を取得します。
  * @param {*} note (packされた)投稿
  */
-export const getNoteSummary = (note: Misskey.entities.Note): string => {
+export const getNoteSummary = (note?: Misskey.entities.Note | null): string => {
+	if (note == null) {
+		return '';
+	}
+
 	if (note.deletedAt) {
 		return `(${i18n.ts.deletedNote})`;
 	}
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 35eeded7e7..0c7cf7e0e6 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -15,7 +15,7 @@ import { defaultStore, userActions } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
 import { IRouter } from '@/nirax.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -170,20 +170,21 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		action: () => {
 			copyToClipboard(`${user.host ?? host}/@${user.username}.atom`);
 		},
-	}, {
+	}, ...(user.host != null && user.url != null ? [{
+		icon: 'ph-share ph-bold ph-lg',
+		text: i18n.ts.showOnRemote,
+		action: () => {
+			if (user.url == null) return;
+			window.open(user.url, '_blank', 'noopener');
+		},
+	}] : []), {
 		icon: 'ph-share-network ph-bold ph-lg',
 		text: i18n.ts.copyProfileUrl,
 		action: () => {
 			const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`;
 			copyToClipboard(`${url}/${canonical}`);
 		},
-	}, ...(user.host ? [{
-		icon: 'ph-share ph-bold ph-lg',
-		text: i18n.ts.openRemoteProfile,
-		action: () => {
-			open(`${user.uri}`, '_blank');
-		},
-	}] : []), {
+	}, {
 		icon: 'ph-envelope ph-bold ph-lg',
 		text: i18n.ts.sendMessage,
 		action: () => {
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts
index ddcfd8852e..69c8d9cbb7 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/scripts/lookup.ts
@@ -7,7 +7,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { Router } from '@/nirax.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 export async function lookup(router?: Router) {
 	const _router = router ?? mainRouter;
diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts
index 0a799c5665..f36388b8f1 100644
--- a/packages/frontend/src/scripts/popup-position.ts
+++ b/packages/frontend/src/scripts/popup-position.ts
@@ -4,7 +4,7 @@
  */
 
 export function calcPopupPosition(el: HTMLElement, props: {
-	anchorElement: HTMLElement | null;
+	anchorElement?: HTMLElement | null;
 	innerMargin: number;
 	direction: 'top' | 'bottom' | 'left' | 'right';
 	align: 'top' | 'bottom' | 'left' | 'right' | 'center';
diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts
index 9b13e794f5..a13351b536 100644
--- a/packages/frontend/src/scripts/reaction-picker.ts
+++ b/packages/frontend/src/scripts/reaction-picker.ts
@@ -38,7 +38,7 @@ class ReactionPicker {
 		});
 	}
 
-	public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
+	public show(src: HTMLElement | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
 		this.src.value = src;
 		this.manualShowing.value = true;
 		this.onChosen = onChosen;
diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts
new file mode 100644
index 0000000000..7bce1f79ca
--- /dev/null
+++ b/packages/frontend/src/scripts/safe-parse.ts
@@ -0,0 +1,11 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function safeParseFloat(str: unknown): number | null {
+	if (typeof str !== 'string' || str === '') return null;
+	const num = parseFloat(str);
+	if (isNaN(num)) return null;
+	return num;
+}
diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts
index a09f02cec0..b506b21907 100644
--- a/packages/frontend/src/scripts/snowfall-effect.ts
+++ b/packages/frontend/src/scripts/snowfall-effect.ts
@@ -17,20 +17,20 @@ export class SnowfallEffect {
 		uniform vec3 u_worldSize;
 		uniform float u_gravity;
 		uniform float u_wind;
+		uniform float u_spin_factor;
+		uniform float u_turbulence;
 
 		void main() {
 			v_color = a_color;
-			v_rotation = a_rotation.x + u_time * a_rotation.y;
+			v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y;
 
 			vec3 pos = a_position.xyz;
 
-			float turbulence = 1.0;
-
 			pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x;
 			pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y;
 
-			pos.x += sin(u_time * a_speed.z * turbulence) * a_rotation.z;
-			pos.z += cos(u_time * a_speed.z * turbulence) * a_rotation.z;
+			pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z;
+			pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z;
 
 			gl_Position = u_projection * vec4(pos.xyz, a_position.w);
 			gl_PointSize = (a_size / gl_Position.w) * 100.0;
@@ -105,6 +105,7 @@ export class SnowfallEffect {
 	private opacity = 1;
 	private size = 4;
 	private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg==';
+	private mode = 'snow';
 
 	private INITIAL_BUFFERS = () => ({
 		position: { size: 3, value: [] },
@@ -119,6 +120,8 @@ export class SnowfallEffect {
 		worldSize: { type: 'vec3', value: [0, 0, 0] },
 		gravity: { type: 'float', value: this.gravity },
 		wind: { type: 'float', value: 0 },
+		spin_factor: { type: 'float', value: this.mode === 'sakura' ? 8 : 1 },
+		turbulence: { type: 'float', value: this.mode === 'sakura' ? 2 : 1 },
 		projection: {
 			type: 'mat4',
 			value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
@@ -153,7 +156,16 @@ export class SnowfallEffect {
 		easing: 0.0005,
 	};
 
-	constructor() {
+	constructor(options: {
+		sakura?: boolean;
+	}) {
+		if (options.sakura) {
+			this.mode = 'sakura';
+			this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1lbnNpb249IjY0IgogICBleGlmOkNvbG9yU3BhY2U9IjEiCiAgIHRpZmY6SW1hZ2VXaWR0aD0iNjQiCiAgIHRpZmY6SW1hZ2VMZW5ndGg9IjY0IgogICB0aWZmOlJlc29sdXRpb25Vbml0PSIyIgogICB0aWZmOlhSZXNvbHV0aW9uPSI3Mi8xIgogICB0aWZmOllSZXNvbHV0aW9uPSI3Mi8xIj4KICAgPHhtcE1NOkhpc3Rvcnk+CiAgICA8cmRmOlNlcT4KICAgICA8cmRmOmxpCiAgICAgIHN0RXZ0OmFjdGlvbj0icHJvZHVjZWQiCiAgICAgIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFmZmluaXR5IFBob3RvIDIgMi4zLjEiCiAgICAgIHN0RXZ0OndoZW49IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiLz4KICAgIDwvcmRmOlNlcT4KICAgPC94bXBNTTpIaXN0b3J5PgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KPD94cGFja2V0IGVuZD0iciI/PhldI30AAAGBaUNDUHNSR0IgSUVDNjE5NjYtMi4xAAAokXWRu0sDQRCHP6Mh4oOIWlhYBPHRJBIjiDYWEV+gFjGCr+ZyuUuEJB53JyK2gq2gINr4KvQv0FawFgRFEcTaWtFG5ZwzgQQxs+zst7+dGXZnwRPPqFmrKgzZnG3GRqOB2bn5gO8FDxV46aJRUS1jcnokTln7uJdYsduQW6t83L9Wm9QsFSqqhQdVw7SFx4QnVm3D5R3hZjWtJIXPhIOmXFD4ztUTeX5xOZXnL5fNeGwIPA3CgVQJJ0pYTZtZYXk57dnMilq4j/uSOi03My1rm8xWLGKMEiXAOMMM0UcPA+L7CBGhW3aUyQ//5k+xLLmqeIM1TJZIkcYmKOqKVNdk1UXXZGRYc/v/t6+W3hvJV6+LgvfZcd46wLcN31uO83nkON/HUPkEl7li/vIh9L+LvlXU2g/AvwHnV0UtsQsXm9DyaCim8itVyvToOryeQv0cNN1AzUK+Z4VzTh4gvi5fdQ17+9Ap8f7FHyc6Z8kcDq1+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADwElEQVR4nO2bT4hWVRjGf75TkhoEkhSa/9ocRIIwCsrE1pVnLbkYdFdGgQRS6caVm3CVy2oRuqmQ2yJXKTJh4GqCGs/CJCcLccAJ/yDpnGnxHYeZ4TrNfOc55y78nuWdc3/ve57v+b65f86BgQaqotiE5bEJKxYx7onYhOU1egKwGkViE/YCN4Cx2ITNC4xbDVwAJmMT9tXobVnpArEJe4CvZx0aB7aZdxPzxhkwArw66/Ae8+5Eyf6KJiA2YRPw+bzD64EjLcP3MXfyAMdjEzYWaG1GxRIQmzAEnAVeb/nzFPCSeTeaxj4FBOCZlrEjwBvm3VSJPksm4BPaJw8wBHwXm/BibMIW4HvaJ09ifFygP6BQAtKkfgEeEyHvAy+YdxdFvBmVSsBBdJMnsQ4KeTOSJyA2YT1wCXhcjL4HPG/e/amElkjAAfSTJzEPqKHSBKQLmSvAKiV3lm4BG8y7GyqgOgHvU27yAE+mGjLJEhCbsBL4A3haxXyIJoCN5t0dBUyZgF2UnzypxtsqmNKAt4SsarUkX4F0I3ONOgkAuA48a97FXJAqAa9Qb/IAa4CXFSCVATXjL635yBuQ/RsQm7AWuCroZamaBtaZd3/nQBQJeFPA6EfLFLUVBrwmYPSr7bkAhQHPCRj9al0uQGHAWgGjs9oKA7I/hS5rZ/0XSC86JDclGVph3t3t9+TcBHT56T9QVg+5BnT5/X+grB4GCcgs/sgnYCjzfIWyesg14Hrm+Qpl9ZBrwMT/DymurB4GCeiyuEidGnCN3n15V5pOPfStLAPMu1vAWA4jU7+Zd7dzAIqboREBo7PaCgN+EjA6qz1IQDbAu9/prQeorUvm3eVciOqx+JcizlL0hQKiMuAreiu/amkq1cyWxADz7ipwWsFapH4w7/5SgJRvh+cviCyp4yqQeonMOWCHktmic+bdThVMvUSmyFK2kjWkBph354FTSuY8nTLvflYCSyyT+xD4pwB3EvhADZUbYN5dAfarucB+825cDS25WvwksFuEO2nevSNizVHJ1eLvAoplrePAewJOq4oZYN5NAsPkPTCZBoYTq4iK7hgx734EjmUgjpl3Z1T9tKnGpqlP6e+p0Vg6t6iKG5De3A6ztJul+/Si3/db38WqyrY58+4CcHQJpxxN5xRXFQOSjgCjixg3SvuusiKqZoB59y+964KbCwy7Cew27+7V6apuAkibnhbaEbq3xMaohVTVAADz7hvgMHN/FKeAQ+bdt7X7Kb519mGKTdgKfEbvYucj8+7XLvr4DxAA134c0w/5AAAAAElFTkSuQmCC';
+			this.size = 10;
+			this.density = 1 / 280;
+		}
+
 		const canvas = this.initCanvas();
 		const gl = canvas.getContext('webgl2', { antialias: true });
 		if (gl == null) throw new Error('Failed to get WebGL context');
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index fb31fce1de..a020b9b10b 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { onUnmounted, Ref } from 'vue';
+import { onUnmounted, Ref, ShallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useStream } from '@/stream.js';
 import { $i } from '@/account.js';
@@ -11,7 +11,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from './misskey-api.js';
 
 export function useNoteCapture(props: {
-	rootEl: Ref<HTMLElement>;
+	rootEl: ShallowRef<HTMLElement | undefined>;
 	note: Ref<Misskey.entities.Note>;
 	pureNote: Ref<Misskey.entities.Note>;
 	isDeletedRef: Ref<boolean>;
@@ -122,7 +122,7 @@ export function useNoteCapture(props: {
 	function capture(withHandler = false): void {
 		if (connection) {
 			// TODO: このノートがストリーミング経由で流れてきた場合のみ sr する
-			connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id });
+			connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id });
 			if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id });
 			if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated);
 		}
diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts
index f4516bbe5b..d5bea5c01c 100644
--- a/packages/frontend/src/types/menu.ts
+++ b/packages/frontend/src/types/menu.ts
@@ -4,7 +4,7 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { Ref } from 'vue';
+import { ComputedRef, Ref } from 'vue';
 
 export type MenuAction = (ev: MouseEvent) => void;
 
@@ -15,7 +15,7 @@ export type MenuLink = { type: 'link', to: string, text: string, icon?: string,
 export type MenuA = { type: 'a', href: string, target?: string, download?: string, text: string, icon?: string, indicate?: boolean };
 export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
 export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean | Ref<boolean> };
-export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
+export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean | ComputedRef<boolean>, avatar?: Misskey.entities.User; action: MenuAction };
 export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
 
 export type MenuPending = { type: 'pending' };
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index 4c77465eb1..ad4107ab3b 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -8,7 +8,7 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
 import { $i, login } from '@/account.js';
 import { getAccountFromId } from '@/scripts/get-account-from-id.js';
 import { deepClone } from '@/scripts/clone.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 export function swInject() {
 	navigator.serviceWorker.addEventListener('message', async ev => {
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 959a135cc6..9072e9a984 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -56,7 +56,7 @@ import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.j
 import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index f21a25b64a..53a05ec3d1 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -116,7 +116,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue';
 import XMentionsColumn from '@/ui/deck/mentions-column.vue';
 import XDirectColumn from '@/ui/deck/direct-column.vue';
 import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
 
@@ -241,7 +241,7 @@ function changeProfile(ev: MouseEvent) {
 			action: async () => {
 				const { canceled, result: name } = await os.inputText({
 					title: i18n.ts._deck.profile,
-					allowEmpty: false,
+					minLength: 1,
 				});
 				if (canceled) return;
 
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 0ea6a7f23b..84f818bcc5 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -27,7 +27,7 @@ import { i18n } from '@/i18n.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { getScrollContainer } from '@/scripts/scroll.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index b0a2aa35f9..a0036c5695 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -18,7 +18,7 @@ import { provide, ComputedRef, ref } from 'vue';
 import XCommon from './_common_/common.vue';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { instanceName } from '@/config.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index cb62012d09..8c8522e1ab 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -110,7 +110,7 @@ import { deviceKind } from '@/scripts/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { CURRENT_STICKY_BOTTOM } from '@/const.js';
 import { useScrollPositionManager } from '@/nirax.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index da725292fd..ea39bed0db 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -80,7 +80,7 @@ import { ColdDeviceStorage, defaultStore } from '@/store.js';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { i18n } from '@/i18n.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const DESKTOP_THRESHOLD = 1100;
 
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index 59549c781d..57390156f8 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -27,7 +27,7 @@ import XCommon from './_common_/common.vue';
 import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata.js';
 import { instanceName, ui } from '@/config.js';
 import { i18n } from '@/i18n.js';
-import { mainRouter } from '@/global/router/main.js';
+import { mainRouter } from '@/router/main.js';
 
 const pageMetadata = ref<null | ComputedRef<PageMetadata>>();
 
diff --git a/packages/frontend/src/widgets/WidgetSearch.vue b/packages/frontend/src/widgets/WidgetSearch.vue
index 91f4b58912..cf91a8f089 100644
--- a/packages/frontend/src/widgets/WidgetSearch.vue
+++ b/packages/frontend/src/widgets/WidgetSearch.vue
@@ -22,7 +22,7 @@ import MkContainer from '@/components/MkContainer.vue';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as os from '@/os.js';
-import { useRouter } from '@/global/router/supplier.js';
+import { useRouter } from '@/router/supplier.js';
 import { GetFormResultType } from '@/scripts/form.js';
 
 const name = 'search';
diff --git a/packages/frontend/vite.config.local-dev.ts b/packages/frontend/vite.config.local-dev.ts
index 41b4561e28..4c19dfbc66 100644
--- a/packages/frontend/vite.config.local-dev.ts
+++ b/packages/frontend/vite.config.local-dev.ts
@@ -1,5 +1,7 @@
 import dns from 'dns';
+import { readFile } from 'node:fs/promises';
 import { defineConfig } from 'vite';
+import * as yaml from 'js-yaml';
 import locales from '../../locales/index.js';
 import { getConfig } from './vite.config.js';
 
@@ -7,6 +9,11 @@ dns.setDefaultResultOrder('ipv4first');
 
 const defaultConfig = getConfig();
 
+const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
+
+const httpUrl = `http://localhost:${port}/`;
+const websocketUrl = `ws://localhost:${port}/`;
+
 const devConfig = {
 	// 基本の設定は vite.config.js から引き継ぐ
 	...defaultConfig,
@@ -19,29 +26,29 @@ const devConfig = {
 		proxy: {
 			'/api': {
 				changeOrigin: true,
-				target: 'http://localhost:3000/',
+				target: httpUrl,
 			},
-			'/assets': 'http://localhost:3000/',
-			'/static-assets': 'http://localhost:3000/',
-			'/client-assets': 'http://localhost:3000/',
-			'/files': 'http://localhost:3000/',
-			'/twemoji': 'http://localhost:3000/',
-			'/fluent-emoji': 'http://localhost:3000/',
-			'/tossface': 'http://localhost:3000/',
-			'/sw.js': 'http://localhost:3000/',
+			'/assets': httpUrl,
+			'/static-assets': httpUrl,
+			'/client-assets': httpUrl,
+			'/files': httpUrl,
+			'/twemoji': httpUrl,
+			'/fluent-emoji': httpUrl,
+			'/tossface': httpUrl,
+			'/sw.js': httpUrl,
 			'/streaming': {
-				target: 'ws://localhost:3000/',
+				target: websocketUrl,
 				ws: true,
 			},
-			'/favicon.ico': 'http://localhost:3000/',
+			'/favicon.ico': httpUrl,
 			'/identicon': {
-				target: 'http://localhost:3000/',
+				target: httpUrl,
 				rewrite(path) {
 					return path.replace('@localhost:5173', '');
 				},
 			},
-			'/url': 'http://localhost:3000',
-			'/proxy': 'http://localhost:3000',
+			'/url': httpUrl,
+			'/proxy': httpUrl,
 		},
 	},
 	build: {
diff --git a/packages/misskey-bubble-game/package.json b/packages/misskey-bubble-game/package.json
index 5a6b952e07..9de7ba005f 100644
--- a/packages/misskey-bubble-game/package.json
+++ b/packages/misskey-bubble-game/package.json
@@ -2,6 +2,7 @@
 	"type": "module",
 	"name": "misskey-bubble-game",
 	"version": "0.0.1",
+	"types": "./built/dts/index.d.ts",
 	"exports": {
 		".": {
 			"import": "./built/esm/index.js",
@@ -15,7 +16,7 @@
 	"scripts": {
 		"build": "node ./build.js",
 		"build:tsc": "npm run tsc",
-		"tsc": "npm run ts-esm && npm run ts-dts",
+		"tsc": "npm run tsc-esm && npm run tsc-dts",
 		"tsc-esm": "tsc --outDir built/esm",
 		"tsc-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
 		"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build:tsc\"",
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 57436d8e5b..7d18a40f22 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -535,10 +535,10 @@ export type Channels = {
             mention: (payload: Note) => void;
             reply: (payload: Note) => void;
             renote: (payload: Note) => void;
-            follow: (payload: User) => void;
-            followed: (payload: User) => void;
-            unfollow: (payload: User) => void;
-            meUpdated: (payload: MeDetailed) => void;
+            follow: (payload: UserDetailedNotMe) => void;
+            followed: (payload: UserDetailed | UserLite) => void;
+            unfollow: (payload: UserDetailed) => void;
+            meUpdated: (payload: UserDetailed) => void;
             pageEvent: (payload: PageEvent) => void;
             urlUploadFinished: (payload: {
                 marker: string;
@@ -571,6 +571,7 @@ export type Channels = {
         params: {
             withRenotes?: boolean;
             withFiles?: boolean;
+            withBots?: boolean;
         };
         events: {
             note: (payload: Note) => void;
@@ -582,6 +583,7 @@ export type Channels = {
             withRenotes?: boolean;
             withReplies?: boolean;
             withFiles?: boolean;
+            withBots?: boolean;
         };
         events: {
             note: (payload: Note) => void;
@@ -593,6 +595,7 @@ export type Channels = {
             withRenotes?: boolean;
             withReplies?: boolean;
             withFiles?: boolean;
+            withBots?: boolean;
         };
         events: {
             note: (payload: Note) => void;
@@ -603,6 +606,7 @@ export type Channels = {
         params: {
             withRenotes?: boolean;
             withFiles?: boolean;
+            withBots?: boolean;
         };
         events: {
             note: (payload: Note) => void;
@@ -610,7 +614,11 @@ export type Channels = {
         receives: null;
     };
     bubbleTimeline: {
-        params: null;
+        params: {
+            withRenotes?: boolean;
+            withFiles?: boolean;
+            withBots?: boolean;
+        };
         events: {
             note: (payload: Note) => void;
         };
@@ -620,6 +628,7 @@ export type Channels = {
         params: {
             listId: string;
             withFiles?: boolean;
+            withRenotes?: boolean;
         };
         events: {
             note: (payload: Note) => void;
@@ -670,7 +679,7 @@ export type Channels = {
             fileUpdated: (payload: DriveFile) => void;
             folderCreated: (payload: DriveFolder) => void;
             folderDeleted: (payload: DriveFolder['id']) => void;
-            folderUpdated: (payload: DriveFile) => void;
+            folderUpdated: (payload: DriveFolder) => void;
         };
         receives: null;
     };
@@ -712,6 +721,46 @@ export type Channels = {
         };
         receives: null;
     };
+    reversiGame: {
+        params: {
+            gameId: string;
+        };
+        events: {
+            started: (payload: {
+                game: ReversiGameDetailed;
+            }) => void;
+            ended: (payload: {
+                winnerId: User['id'] | null;
+                game: ReversiGameDetailed;
+            }) => void;
+            canceled: (payload: {
+                userId: User['id'];
+            }) => void;
+            changeReadyStates: (payload: {
+                user1: boolean;
+                user2: boolean;
+            }) => void;
+            updateSettings: (payload: {
+                userId: User['id'];
+                key: string;
+                value: any;
+            }) => void;
+            log: (payload: Record<string, any>) => void;
+        };
+        receives: {
+            putStone: {
+                pos: number;
+                id: string;
+            };
+            ready: boolean;
+            cancel: null | Record<string, never>;
+            updateSettings: {
+                key: string;
+                value: any;
+            };
+            claimTimeIsUp: null | Record<string, never>;
+        };
+    };
 };
 
 // @public (undocumented)
@@ -1697,6 +1746,7 @@ declare namespace entities {
         Hashtag,
         InviteCode,
         Page,
+        PageBlock,
         Channel,
         QueueCount,
         Antenna,
@@ -1709,6 +1759,7 @@ declare namespace entities {
         Signin,
         RoleLite,
         Role,
+        RolePolicies,
         ReversiGameLite,
         ReversiGameDetailed
     }
@@ -2235,7 +2286,7 @@ type ModerationLog = {
     id: ID;
     createdAt: DateString;
     userId: User['id'];
-    user: UserDetailed | null;
+    user: UserDetailedNotMe | null;
 } & ({
     type: 'updateServerSettings';
     info: ModerationLogPayloads['updateServerSettings'];
@@ -2577,6 +2628,9 @@ export const notificationTypes: readonly ["note", "follow", "mention", "reply",
 // @public (undocumented)
 type Page = components['schemas']['Page'];
 
+// @public (undocumented)
+type PageBlock = components['schemas']['PageBlock'];
+
 // @public (undocumented)
 type PageEvent = {
     pageId: Page['id'];
@@ -2725,6 +2779,9 @@ type Role = components['schemas']['Role'];
 // @public (undocumented)
 type RoleLite = components['schemas']['RoleLite'];
 
+// @public (undocumented)
+type RolePolicies = components['schemas']['RolePolicies'];
+
 // @public (undocumented)
 type RolesListResponse = operations['roles/list']['responses']['200']['content']['application/json'];
 
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 06c3ce6b54..03952c7ac1 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2024.2.0-beta.3",
+	"version": "2024.2.0-beta.8",
 	"description": "Misskey SDK for JavaScript",
 	"types": "./built/dts/index.d.ts",
 	"exports": {
@@ -19,7 +19,7 @@
 		"ts": "npm run ts-esm && npm run ts-dts",
 		"ts-esm": "tsc --outDir built/esm",
 		"ts-dts": "tsc --outDir built/dts --declaration true --emitDeclarationOnly true --declarationMap true",
-		"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run build\"",
+		"watch": "nodemon -w src -e ts,js,cjs,mjs,json --exec \"pnpm run ts\"",
 		"tsd": "tsd",
 		"api": "pnpm api-extractor run --local --verbose",
 		"api-prod": "pnpm api-extractor run --verbose",
@@ -39,7 +39,7 @@
 		"@misskey-dev/eslint-plugin": "1.0.0",
 		"@swc/jest": "0.2.31",
 		"@types/jest": "29.5.11",
-		"@types/node": "20.11.5",
+		"@types/node": "20.11.10",
 		"@typescript-eslint/eslint-plugin": "6.18.1",
 		"@typescript-eslint/parser": "6.18.1",
 		"eslint": "8.56.0",
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 23f5cec2e5..205fba69a7 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -1,6 +1,6 @@
 /*
  * version: 2024.2.0-beta2
- * generatedAt: 2024-01-26T20:30:18.423Z
+ * generatedAt: 2024-02-03T19:17:05.681Z
  */
 
 import type { SwitchCaseResponseType } from '../api.js';
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 79449a32c6..28fc82c609 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -1,6 +1,6 @@
 /*
  * version: 2024.2.0-beta2
- * generatedAt: 2024-01-26T20:30:18.421Z
+ * generatedAt: 2024-02-03T19:17:05.679Z
  */
 
 import type {
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index c976ff9560..17471a27f0 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -1,6 +1,6 @@
 /*
  * version: 2024.2.0-beta2
- * generatedAt: 2024-01-26T20:30:18.419Z
+ * generatedAt: 2024-02-03T19:17:05.678Z
  */
 
 import { operations } from './types.js';
diff --git a/packages/misskey-js/src/autogen/models.ts b/packages/misskey-js/src/autogen/models.ts
index 3c400fb342..5d87df588c 100644
--- a/packages/misskey-js/src/autogen/models.ts
+++ b/packages/misskey-js/src/autogen/models.ts
@@ -1,6 +1,6 @@
 /*
  * version: 2024.2.0-beta2
- * generatedAt: 2024-01-26T20:30:18.418Z
+ * generatedAt: 2024-02-03T19:17:05.676Z
  */
 
 import { components } from './types.js';
@@ -29,6 +29,7 @@ export type Blocking = components['schemas']['Blocking'];
 export type Hashtag = components['schemas']['Hashtag'];
 export type InviteCode = components['schemas']['InviteCode'];
 export type Page = components['schemas']['Page'];
+export type PageBlock = components['schemas']['PageBlock'];
 export type Channel = components['schemas']['Channel'];
 export type QueueCount = components['schemas']['QueueCount'];
 export type Antenna = components['schemas']['Antenna'];
@@ -41,5 +42,6 @@ export type Flash = components['schemas']['Flash'];
 export type Signin = components['schemas']['Signin'];
 export type RoleLite = components['schemas']['RoleLite'];
 export type Role = components['schemas']['Role'];
+export type RolePolicies = components['schemas']['RolePolicies'];
 export type ReversiGameLite = components['schemas']['ReversiGameLite'];
 export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index b812d0033b..54765e86f2 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -3,7 +3,7 @@
 
 /*
  * version: 2024.2.0-beta2
- * generatedAt: 2024-01-26T20:30:18.319Z
+ * generatedAt: 2024-02-03T19:17:05.578Z
  */
 
 /**
@@ -3876,32 +3876,7 @@ export type components = {
           unlockedAt: number;
         }[];
       loggedInDays: number;
-      policies: {
-        gtlAvailable: boolean;
-        ltlAvailable: boolean;
-        canPublicNote: boolean;
-        canInvite: boolean;
-        inviteLimit: number;
-        inviteLimitCycle: number;
-        inviteExpirationTime: number;
-        canManageCustomEmojis: boolean;
-        canManageAvatarDecorations: boolean;
-        canSearchNotes: boolean;
-        canUseTranslator: boolean;
-        canHideAds: boolean;
-        driveCapacityMb: number;
-        alwaysMarkNsfw: boolean;
-        pinLimit: number;
-        antennaLimit: number;
-        wordMuteLimit: number;
-        webhookLimit: number;
-        clipLimit: number;
-        noteEachClipsLimit: number;
-        userListLimit: number;
-        userEachUserListsLimit: number;
-        rateLimitFactor: number;
-        avatarDecorationLimit: number;
-      };
+      policies: components['schemas']['RolePolicies'];
       email?: string | null;
       emailVerified?: boolean | null;
       securityKeysList?: {
@@ -3918,7 +3893,7 @@ export type components = {
     UserDetailedNotMe: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'];
     MeDetailed: components['schemas']['UserLite'] & components['schemas']['UserDetailedNotMeOnly'] & components['schemas']['MeDetailedOnly'];
     UserDetailed: components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
-    User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'] | components['schemas']['UserDetailedNotMe'] | components['schemas']['MeDetailed'];
+    User: components['schemas']['UserLite'] | components['schemas']['UserDetailed'];
     UserList: {
       /**
        * Format: id
@@ -3962,8 +3937,10 @@ export type components = {
       text: string;
       title: string;
       imageUrl: string | null;
-      icon: string;
-      display: string;
+      /** @enum {string} */
+      icon: 'info' | 'warning' | 'error' | 'success';
+      /** @enum {string} */
+      display: 'dialog' | 'normal' | 'banner';
       needConfirmationToRead: boolean;
       silence: boolean;
       forYou: boolean;
@@ -4005,13 +3982,26 @@ export type components = {
       reply?: components['schemas']['Note'] | null;
       renote?: components['schemas']['Note'] | null;
       isHidden?: boolean;
-      visibility: string;
+      /** @enum {string} */
+      visibility: 'public' | 'home' | 'followers' | 'specified';
       mentions?: string[];
       visibleUserIds?: string[];
       fileIds?: string[];
       files?: components['schemas']['DriveFile'][];
       tags?: string[];
-      poll?: Record<string, never> | null;
+      poll?: ({
+        /** Format: date-time */
+        expiresAt?: string | null;
+        multiple: boolean;
+        choices: {
+            isVoted: boolean;
+            text: string;
+            votes: number;
+          }[];
+      }) | null;
+      emojis?: {
+        [key: string]: string;
+      };
       /**
        * Format: id
        * @example xxxxxxxxxx
@@ -4027,14 +4017,19 @@ export type components = {
       }) | null;
       localOnly?: boolean;
       reactionAcceptance: string | null;
-      reactions: Record<string, never>;
+      reactionEmojis: {
+        [key: string]: string;
+      };
+      reactions: {
+        [key: string]: number;
+      };
       renoteCount: number;
       repliesCount: number;
       uri?: string;
       url?: string;
       reactionAndUserPairCache?: string[];
       clippedCount?: number;
-      myReaction?: Record<string, never> | null;
+      myReaction?: string | null;
     };
     NoteReaction: {
       /**
@@ -4065,21 +4060,162 @@ export type components = {
       /** Format: date-time */
       createdAt: string;
       /** @enum {string} */
-      type: 'note' | 'follow' | 'mention' | 'reply' | 'renote' | 'quote' | 'reaction' | 'pollEnded' | 'receiveFollowRequest' | 'followRequestAccepted' | 'roleAssigned' | 'achievementEarned' | 'app' | 'test' | 'reaction:grouped' | 'renote:grouped';
-      user?: components['schemas']['UserLite'] | null;
+      type: 'note';
+      user: components['schemas']['UserLite'];
       /** Format: id */
-      userId?: string | null;
-      note?: components['schemas']['Note'] | null;
-      reaction?: string | null;
-      achievement?: string;
-      body?: string | null;
-      header?: string | null;
-      icon?: string | null;
-      reactions?: {
+      userId: string;
+      note: components['schemas']['Note'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'mention';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+      note: components['schemas']['Note'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'reply';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+      note: components['schemas']['Note'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'renote';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+      note: components['schemas']['Note'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'quote';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+      note: components['schemas']['Note'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'reaction';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+      note: components['schemas']['Note'];
+      reaction: string;
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'pollEnded';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+      note: components['schemas']['Note'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'follow';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'receiveFollowRequest';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'followRequestAccepted';
+      user: components['schemas']['UserLite'];
+      /** Format: id */
+      userId: string;
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'roleAssigned';
+      role: components['schemas']['Role'];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'achievementEarned';
+      achievement: string;
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'app';
+      body: string;
+      header: string;
+      icon: string;
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'reaction:grouped';
+      note: components['schemas']['Note'];
+      reactions: {
           user: components['schemas']['UserLite'];
           reaction: string;
-        }[] | null;
-      users?: components['schemas']['UserLite'][] | null;
+        }[];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'renote:grouped';
+      note: components['schemas']['Note'];
+      users: components['schemas']['UserLite'][];
+    } | {
+      /** Format: id */
+      id: string;
+      /** Format: date-time */
+      createdAt: string;
+      /** @enum {string} */
+      type: 'test';
     };
     DriveFile: {
       /**
@@ -4160,8 +4296,8 @@ export type components = {
       followeeId: string;
       /** Format: id */
       followerId: string;
-      followee?: components['schemas']['UserDetailed'];
-      follower?: components['schemas']['UserDetailed'];
+      followee?: components['schemas']['UserDetailedNotMe'];
+      follower?: components['schemas']['UserDetailedNotMe'];
     };
     Muting: {
       /**
@@ -4175,7 +4311,7 @@ export type components = {
       expiresAt: string | null;
       /** Format: id */
       muteeId: string;
-      mutee: components['schemas']['UserDetailed'];
+      mutee: components['schemas']['UserDetailedNotMe'];
     };
     RenoteMuting: {
       /**
@@ -4187,7 +4323,7 @@ export type components = {
       createdAt: string;
       /** Format: id */
       muteeId: string;
-      mutee: components['schemas']['UserDetailed'];
+      mutee: components['schemas']['UserDetailedNotMe'];
     };
     Blocking: {
       /**
@@ -4199,7 +4335,7 @@ export type components = {
       createdAt: string;
       /** Format: id */
       blockeeId: string;
-      blockee: components['schemas']['UserDetailed'];
+      blockee: components['schemas']['UserDetailedNotMe'];
     };
     Hashtag: {
       /** @example misskey */
@@ -4242,7 +4378,7 @@ export type components = {
       /** Format: id */
       userId: string;
       user: components['schemas']['UserLite'];
-      content: Record<string, never>[];
+      content: components['schemas']['PageBlock'][];
       variables: Record<string, never>[];
       title: string;
       name: string;
@@ -4257,6 +4393,29 @@ export type components = {
       likedCount: number;
       isLiked?: boolean;
     };
+    PageBlock: OneOf<[{
+      id: string;
+      /** @enum {string} */
+      type: 'text';
+      text: string;
+    }, {
+      id: string;
+      /** @enum {string} */
+      type: 'section';
+      title: string;
+      children: components['schemas']['PageBlock'][];
+    }, {
+      id: string;
+      /** @enum {string} */
+      type: 'image';
+      fileId: string | null;
+    }, {
+      id: string;
+      /** @enum {string} */
+      type: 'note';
+      detailed: boolean;
+      note: string | null;
+    }]>;
     Channel: {
       /**
        * Format: id
@@ -4477,129 +4636,40 @@ export type components = {
       /** @example false */
       canEditMembersByModerator: boolean;
       policies: {
-        pinLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canInvite: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        clipLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canHideAds: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        inviteLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        antennaLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        gtlAvailable: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        ltlAvailable: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        webhookLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canPublicNote: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        userListLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        wordMuteLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        alwaysMarkNsfw: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canSearchNotes: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        driveCapacityMb: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        rateLimitFactor: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        inviteLimitCycle: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        noteEachClipsLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        inviteExpirationTime: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canManageCustomEmojis: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        userEachUserListsLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canManageAvatarDecorations: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        canUseTranslator: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
-        };
-        avatarDecorationLimit: {
-          value: number | boolean;
-          priority: number;
-          useDefault: boolean;
+        [key: string]: {
+          value?: number | boolean;
+          priority?: number;
+          useDefault?: boolean;
         };
       };
       usersCount: number;
     });
+    RolePolicies: {
+      gtlAvailable: boolean;
+      ltlAvailable: boolean;
+      canPublicNote: boolean;
+      canInvite: boolean;
+      inviteLimit: number;
+      inviteLimitCycle: number;
+      inviteExpirationTime: number;
+      canManageCustomEmojis: boolean;
+      canManageAvatarDecorations: boolean;
+      canSearchNotes: boolean;
+      canUseTranslator: boolean;
+      canHideAds: boolean;
+      driveCapacityMb: number;
+      alwaysMarkNsfw: boolean;
+      pinLimit: number;
+      antennaLimit: number;
+      wordMuteLimit: number;
+      webhookLimit: number;
+      clipLimit: number;
+      noteEachClipsLimit: number;
+      userListLimit: number;
+      userEachUserListsLimit: number;
+      rateLimitFactor: number;
+      avatarDecorationLimit: number;
+    };
     ReversiGameLite: {
       /** Format: id */
       id: string;
@@ -4893,9 +4963,9 @@ export type operations = {
               targetUserId: string;
               /** Format: id */
               assigneeId: string | null;
-              reporter: components['schemas']['User'];
-              targetUser: components['schemas']['User'];
-              assignee?: components['schemas']['User'] | null;
+              reporter: components['schemas']['UserDetailedNotMe'];
+              targetUser: components['schemas']['UserDetailedNotMe'];
+              assignee?: components['schemas']['UserDetailedNotMe'] | null;
             })[];
         };
       };
@@ -4950,7 +5020,7 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': components['schemas']['User'];
+          'application/json': components['schemas']['MeDetailed'];
         };
       };
       /** @description Client error */
@@ -5055,7 +5125,7 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': components['schemas']['User'];
+          'application/json': components['schemas']['UserDetailedNotMe'];
         };
       };
       /** @description Client error */
@@ -8204,7 +8274,7 @@ export type operations = {
               info: Record<string, never>;
               /** Format: id */
               userId: string;
-              user: components['schemas']['UserDetailed'];
+              user: components['schemas']['UserDetailedNotMe'];
             }[];
         };
       };
@@ -11439,14 +11509,18 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'local.incCount': number[];
-            'local.incSize': number[];
-            'local.decCount': number[];
-            'local.decSize': number[];
-            'remote.incCount': number[];
-            'remote.incSize': number[];
-            'remote.decCount': number[];
-            'remote.decSize': number[];
+            local: {
+              incCount: number[];
+              incSize: number[];
+              decCount: number[];
+              decSize: number[];
+            };
+            remote: {
+              incCount: number[];
+              incSize: number[];
+              decCount: number[];
+              decSize: number[];
+            };
           };
         };
       };
@@ -11574,30 +11648,44 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'requests.failed': number[];
-            'requests.succeeded': number[];
-            'requests.received': number[];
-            'notes.total': number[];
-            'notes.inc': number[];
-            'notes.dec': number[];
-            'notes.diffs.normal': number[];
-            'notes.diffs.reply': number[];
-            'notes.diffs.renote': number[];
-            'notes.diffs.withFile': number[];
-            'users.total': number[];
-            'users.inc': number[];
-            'users.dec': number[];
-            'following.total': number[];
-            'following.inc': number[];
-            'following.dec': number[];
-            'followers.total': number[];
-            'followers.inc': number[];
-            'followers.dec': number[];
-            'drive.totalFiles': number[];
-            'drive.incFiles': number[];
-            'drive.decFiles': number[];
-            'drive.incUsage': number[];
-            'drive.decUsage': number[];
+            requests: {
+              failed: number[];
+              succeeded: number[];
+              received: number[];
+            };
+            notes: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+              diffs: {
+                normal: number[];
+                reply: number[];
+                renote: number[];
+                withFile: number[];
+              };
+            };
+            users: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+            };
+            following: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+            };
+            followers: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+            };
+            drive: {
+              totalFiles: number[];
+              incFiles: number[];
+              decFiles: number[];
+              incUsage: number[];
+              decUsage: number[];
+            };
           };
         };
       };
@@ -11657,20 +11745,28 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'local.total': number[];
-            'local.inc': number[];
-            'local.dec': number[];
-            'local.diffs.normal': number[];
-            'local.diffs.reply': number[];
-            'local.diffs.renote': number[];
-            'local.diffs.withFile': number[];
-            'remote.total': number[];
-            'remote.inc': number[];
-            'remote.dec': number[];
-            'remote.diffs.normal': number[];
-            'remote.diffs.reply': number[];
-            'remote.diffs.renote': number[];
-            'remote.diffs.withFile': number[];
+            local: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+              diffs: {
+                normal: number[];
+                reply: number[];
+                renote: number[];
+                withFile: number[];
+              };
+            };
+            remote: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+              diffs: {
+                normal: number[];
+                reply: number[];
+                renote: number[];
+                withFile: number[];
+              };
+            };
           };
         };
       };
@@ -11799,18 +11895,30 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'local.followings.total': number[];
-            'local.followings.inc': number[];
-            'local.followings.dec': number[];
-            'local.followers.total': number[];
-            'local.followers.inc': number[];
-            'local.followers.dec': number[];
-            'remote.followings.total': number[];
-            'remote.followings.inc': number[];
-            'remote.followings.dec': number[];
-            'remote.followers.total': number[];
-            'remote.followers.inc': number[];
-            'remote.followers.dec': number[];
+            local: {
+              followings: {
+                total: number[];
+                inc: number[];
+                dec: number[];
+              };
+              followers: {
+                total: number[];
+                inc: number[];
+                dec: number[];
+              };
+            };
+            remote: {
+              followings: {
+                total: number[];
+                inc: number[];
+                dec: number[];
+              };
+              followers: {
+                total: number[];
+                inc: number[];
+                dec: number[];
+              };
+            };
           };
         };
       };
@@ -11875,10 +11983,12 @@ export type operations = {
             total: number[];
             inc: number[];
             dec: number[];
-            'diffs.normal': number[];
-            'diffs.reply': number[];
-            'diffs.renote': number[];
-            'diffs.withFile': number[];
+            diffs: {
+              normal: number[];
+              reply: number[];
+              renote: number[];
+              withFile: number[];
+            };
           };
         };
       };
@@ -11940,10 +12050,14 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'upv.user': number[];
-            'pv.user': number[];
-            'upv.visitor': number[];
-            'pv.visitor': number[];
+            upv: {
+              user: number[];
+              visitor: number[];
+            };
+            pv: {
+              user: number[];
+              visitor: number[];
+            };
           };
         };
       };
@@ -12005,8 +12119,12 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'local.count': number[];
-            'remote.count': number[];
+            local: {
+              count: number[];
+            };
+            remote: {
+              count: number[];
+            };
           };
         };
       };
@@ -12066,12 +12184,16 @@ export type operations = {
       200: {
         content: {
           'application/json': {
-            'local.total': number[];
-            'local.inc': number[];
-            'local.dec': number[];
-            'remote.total': number[];
-            'remote.inc': number[];
-            'remote.dec': number[];
+            local: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+            };
+            remote: {
+              total: number[];
+              inc: number[];
+              dec: number[];
+            };
           };
         };
       };
@@ -18744,7 +18866,7 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': components['schemas']['UserDetailed'];
+          'application/json': components['schemas']['MeDetailed'];
         };
       };
       /** @description Client error */
@@ -19570,6 +19692,7 @@ export type operations = {
             privacyPolicyUrl: string | null;
             serverRules: string[];
             themeColor: string | null;
+            policies: components['schemas']['RolePolicies'];
           };
         };
       };
@@ -23699,7 +23822,7 @@ export type operations = {
           'application/json': {
               /** Format: misskey:id */
               id: string;
-              user: components['schemas']['User'];
+              user: components['schemas']['UserDetailed'];
             }[];
         };
       };
@@ -25494,7 +25617,7 @@ export type operations = {
               createdAt: string;
               /** Format: misskey:id */
               userId: string;
-              user: components['schemas']['User'];
+              user: components['schemas']['UserLite'];
               withReplies: boolean;
             }[];
         };
@@ -26390,7 +26513,14 @@ export type operations = {
       /** @description OK (with results) */
       200: {
         content: {
-          'application/json': unknown;
+          'application/json': {
+              /** Format: date-time */
+              createdAt: string;
+              users: number;
+              data: {
+                [key: string]: number;
+              };
+            }[];
         };
       };
       /** @description Client error */
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index d435d2268f..54517c3845 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -1,5 +1,5 @@
 import { ModerationLogPayloads } from './consts.js';
-import { Announcement, EmojiDetailed, MeDetailed, MeDetailedOnly, Page, User, UserDetailed } from './autogen/models.js';
+import { Announcement, EmojiDetailed, MeDetailed, Page, User, UserDetailedNotMe } from './autogen/models.js';
 
 export * from './autogen/entities.js';
 export * from './autogen/models.js';
@@ -19,7 +19,7 @@ export type ModerationLog = {
 	id: ID;
 	createdAt: DateString;
 	userId: User['id'];
-	user: UserDetailed | null;
+	user: UserDetailedNotMe | null;
 } & ({
 	type: 'updateServerSettings';
 	info: ModerationLogPayloads['updateServerSettings'];
diff --git a/packages/misskey-js/src/streaming.types.ts b/packages/misskey-js/src/streaming.types.ts
index e2b550ab7a..9a2b814d20 100644
--- a/packages/misskey-js/src/streaming.types.ts
+++ b/packages/misskey-js/src/streaming.types.ts
@@ -2,11 +2,13 @@ import {
 	Antenna,
 	DriveFile,
 	DriveFolder,
-	MeDetailed,
 	Note,
 	Notification,
 	Signin,
 	User,
+	UserDetailed,
+	UserDetailedNotMe,
+	UserLite,
 } from './autogen/models.js';
 import {
 	AnnouncementCreated,
@@ -17,6 +19,7 @@ import {
 	QueueStatsLog,
 	ServerStats,
 	ServerStatsLog,
+	ReversiGameDetailed,
 } from './entities.js';
 
 export type Channels = {
@@ -27,10 +30,10 @@ export type Channels = {
 			mention: (payload: Note) => void;
 			reply: (payload: Note) => void;
 			renote: (payload: Note) => void;
-			follow: (payload: User) => void; // 自分が他人をフォローしたとき
-			followed: (payload: User) => void; // 他人が自分をフォローしたとき
-			unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき
-			meUpdated: (payload: MeDetailed) => void;
+			follow: (payload: UserDetailedNotMe) => void; // 自分が他人をフォローしたとき
+			followed: (payload: UserDetailed | UserLite) => void; // 他人が自分をフォローしたとき
+			unfollow: (payload: UserDetailed) => void; // 自分が他人をフォロー解除したとき
+			meUpdated: (payload: UserDetailed) => void;
 			pageEvent: (payload: PageEvent) => void;
 			urlUploadFinished: (payload: { marker: string; file: DriveFile; }) => void;
 			readAllNotifications: () => void;
@@ -60,6 +63,7 @@ export type Channels = {
 		params: {
 			withRenotes?: boolean;
 			withFiles?: boolean;
+			withBots?: boolean;
 		};
 		events: {
 			note: (payload: Note) => void;
@@ -71,6 +75,7 @@ export type Channels = {
 			withRenotes?: boolean;
 			withReplies?: boolean;
 			withFiles?: boolean;
+			withBots?: boolean;
 		};
 		events: {
 			note: (payload: Note) => void;
@@ -82,6 +87,7 @@ export type Channels = {
 			withRenotes?: boolean;
 			withReplies?: boolean;
 			withFiles?: boolean;
+			withBots?: boolean;
 		};
 		events: {
 			note: (payload: Note) => void;
@@ -92,6 +98,7 @@ export type Channels = {
 		params: {
 			withRenotes?: boolean;
 			withFiles?: boolean;
+			withBots?: boolean;
 		};
 		events: {
 			note: (payload: Note) => void;
@@ -99,7 +106,11 @@ export type Channels = {
 		receives: null;
 	};
 	bubbleTimeline: {
-		params: null;
+		params: {
+			withRenotes?: boolean;
+			withFiles?: boolean;
+			withBots?: boolean;
+		};
 		events: {
 			note: (payload: Note) => void;
 		};
@@ -109,6 +120,7 @@ export type Channels = {
 		params: {
 			listId: string;
 			withFiles?: boolean;
+			withRenotes?: boolean;
 		};
 		events: {
 			note: (payload: Note) => void;
@@ -159,7 +171,7 @@ export type Channels = {
 			fileUpdated: (payload: DriveFile) => void;
 			folderCreated: (payload: DriveFolder) => void;
 			folderDeleted: (payload: DriveFolder['id']) => void;
-			folderUpdated: (payload: DriveFile) => void;
+			folderUpdated: (payload: DriveFolder) => void;
 		};
 		receives: null;
 	};
@@ -200,6 +212,32 @@ export type Channels = {
 			}
 		};
 		receives: null;
+	};
+	reversiGame: {
+		params: {
+			gameId: string;
+		};
+		events: {
+			started: (payload: { game: ReversiGameDetailed; }) => void;
+			ended: (payload: { winnerId: User['id'] | null; game: ReversiGameDetailed; }) => void;
+			canceled: (payload: { userId: User['id']; }) => void;
+			changeReadyStates: (payload: { user1: boolean; user2: boolean; }) => void;
+			updateSettings: (payload: { userId: User['id']; key: string; value: any; }) => void;
+			log: (payload: Record<string, any>) => void;
+		};
+		receives: {
+			putStone: {
+				pos: number;
+				id: string;
+			};
+			ready: boolean;
+			cancel: null | Record<string, never>;
+			updateSettings: {
+				key: string;
+				value: any;
+			};
+			claimTimeIsUp: null | Record<string, never>;
+		}
 	}
 };
 
diff --git a/packages/misskey-reversi/package.json b/packages/misskey-reversi/package.json
index bd8d4b498c..52d497d3f8 100644
--- a/packages/misskey-reversi/package.json
+++ b/packages/misskey-reversi/package.json
@@ -2,6 +2,7 @@
 	"type": "module",
 	"name": "misskey-reversi",
 	"version": "0.0.1",
+	"types": "./built/dts/index.d.ts",
 	"exports": {
 		".": {
 			"import": "./built/esm/index.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5e63a16209..913f0ec2e1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -62,14 +62,14 @@ importers:
         specifier: 3.412.0
         version: 3.412.0(@aws-sdk/client-s3@3.412.0)
       '@bull-board/api':
-        specifier: 5.10.2
-        version: 5.10.2(@bull-board/ui@5.10.2)
+        specifier: 5.14.0
+        version: 5.14.0(@bull-board/ui@5.14.0)
       '@bull-board/fastify':
-        specifier: 5.10.2
-        version: 5.10.2
+        specifier: 5.14.0
+        version: 5.14.0
       '@bull-board/ui':
-        specifier: 5.10.2
-        version: 5.10.2
+        specifier: 5.14.0
+        version: 5.14.0
       '@discordapp/twemoji':
         specifier: 15.0.2
         version: 15.0.2
@@ -108,16 +108,16 @@ importers:
         version: 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
       '@nestjs/core':
         specifier: 10.2.10
-        version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1)
+        version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.1)(reflect-metadata@0.1.14)(rxjs@7.8.1)
       '@nestjs/testing':
         specifier: 10.2.10
-        version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.0)
+        version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.1)
       '@peertube/http-signature':
         specifier: 1.7.0
         version: 1.7.0
       '@simplewebauthn/server':
-        specifier: 9.0.0
-        version: 9.0.0
+        specifier: 9.0.1
+        version: 9.0.1
       '@sinonjs/fake-timers':
         specifier: 11.2.2
         version: 11.2.2
@@ -126,10 +126,10 @@ importers:
         version: 2.1.10
       '@swc/cli':
         specifier: 0.1.63
-        version: 0.1.63(@swc/core@1.3.105)(chokidar@3.5.3)
+        version: 0.1.63(@swc/core@1.3.107)(chokidar@3.5.3)
       '@swc/core':
-        specifier: 1.3.105
-        version: 1.3.105
+        specifier: 1.3.107
+        version: 1.3.107
       '@transfem-org/sfm-js':
         specifier: 0.24.4
         version: 0.24.4
@@ -161,8 +161,8 @@ importers:
         specifier: 1.20.2
         version: 1.20.2
       bullmq:
-        specifier: 5.1.4
-        version: 5.1.4
+        specifier: 5.1.5
+        version: 5.1.5
       cacheable-lookup:
         specifier: 7.0.0
         version: 7.0.0
@@ -218,8 +218,8 @@ importers:
         specifier: 10.3.10
         version: 10.3.10
       got:
-        specifier: 14.0.0
-        version: 14.0.0
+        specifier: 14.1.0
+        version: 14.1.0
       happy-dom:
         specifier: 10.0.3
         version: 10.0.3
@@ -311,8 +311,8 @@ importers:
         specifier: 8.11.3
         version: 8.11.3
       pkce-challenge:
-        specifier: 4.0.1
-        version: 4.0.1
+        specifier: 4.1.0
+        version: 4.1.0
       probe-image-size:
         specifier: 7.2.3
         version: 7.2.3
@@ -374,8 +374,8 @@ importers:
         specifier: 2.1.0
         version: 2.1.0
       systeminformation:
-        specifier: 5.21.23
-        version: 5.21.23
+        specifier: 5.21.24
+        version: 5.21.24
       tinycolor2:
         specifier: 1.6.0
         version: 1.6.0
@@ -389,8 +389,8 @@ importers:
         specifier: 4.2.0
         version: 4.2.0
       typeorm:
-        specifier: 0.3.19
-        version: 0.3.19(ioredis@5.3.2)(pg@8.11.3)
+        specifier: 0.3.20
+        version: 0.3.20(ioredis@5.3.2)(pg@8.11.3)
       typescript:
         specifier: 5.3.3
         version: 5.3.3
@@ -502,14 +502,14 @@ importers:
         specifier: 1.0.0
         version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
       '@nestjs/platform-express':
-        specifier: 10.3.0
-        version: 10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
+        specifier: 10.3.1
+        version: 10.3.1(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
       '@simplewebauthn/typescript-types':
         specifier: 8.3.4
         version: 8.3.4
       '@swc/jest':
         specifier: 0.2.31
-        version: 0.2.31(@swc/core@1.3.105)
+        version: 0.2.31(@swc/core@1.3.107)
       '@types/accepts':
         specifier: 1.3.7
         version: 1.3.7
@@ -559,8 +559,8 @@ importers:
         specifier: 0.7.34
         version: 0.7.34
       '@types/node':
-        specifier: 20.11.5
-        version: 20.11.5
+        specifier: 20.11.10
+        version: 20.11.10
       '@types/node-fetch':
         specifier: 3.0.3
         version: 3.0.3
@@ -577,8 +577,8 @@ importers:
         specifier: 0.1.2
         version: 0.1.2
       '@types/pg':
-        specifier: 8.10.9
-        version: 8.10.9
+        specifier: 8.11.0
+        version: 8.11.0
       '@types/pug':
         specifier: 2.0.10
         version: 2.0.10
@@ -656,7 +656,7 @@ importers:
         version: 9.0.0
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@20.11.5)
+        version: 29.7.0(@types/node@20.11.10)
       jest-mock:
         specifier: 29.7.0
         version: 29.7.0
@@ -682,8 +682,8 @@ importers:
         specifier: 0.1.0-alpha-3
         version: 0.1.0-alpha-3
       '@misskey-dev/browser-image-resizer':
-        specifier: 2.2.1-misskey.10
-        version: 2.2.1-misskey.10
+        specifier: 2024.1.0
+        version: 2024.1.0
       '@phosphor-icons/web':
         specifier: ^2.0.3
         version: 2.0.3
@@ -706,11 +706,11 @@ importers:
         specifier: 15.0.0
         version: 15.0.0
       '@vitejs/plugin-vue':
-        specifier: 5.0.2
-        version: 5.0.2(vite@5.0.12)(vue@3.4.15)
+        specifier: 5.0.3
+        version: 5.0.3(vite@5.0.12)(vue@3.4.15)
       '@vue/compiler-sfc':
-        specifier: 3.4.3
-        version: 3.4.3
+        specifier: 3.4.15
+        version: 3.4.15
       aiscript-vscode:
         specifier: github:aiscript-dev/aiscript-vscode#v0.0.6
         version: github.com/aiscript-dev/aiscript-vscode/b5a8aa0ad927831a0b867d1c183460a14e6c48cd
@@ -742,8 +742,8 @@ importers:
         specifier: 2.0.1
         version: 2.0.1(chart.js@4.4.1)
       chromatic:
-        specifier: 10.3.1
-        version: 10.3.1
+        specifier: 10.6.1
+        version: 10.6.1
       compare-versions:
         specifier: 6.1.0
         version: 6.1.0
@@ -805,8 +805,8 @@ importers:
         specifier: 1.70.0
         version: 1.70.0
       shiki:
-        specifier: 0.14.7
-        version: 0.14.7
+        specifier: 1.0.0-beta.3
+        version: 1.0.0-beta.3
       strict-event-emitter-types:
         specifier: 2.0.0
         version: 2.0.0
@@ -814,8 +814,8 @@ importers:
         specifier: 3.1.0
         version: 3.1.0
       three:
-        specifier: 0.160.0
-        version: 0.160.0
+        specifier: 0.160.1
+        version: 0.160.1
       throttle-debounce:
         specifier: 5.0.0
         version: 5.0.0
@@ -839,7 +839,7 @@ importers:
         version: 1.7.2(vue@3.4.15)
       vite:
         specifier: 5.0.12
-        version: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+        version: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
       vue:
         specifier: 3.4.15
         version: 3.4.15(typescript@5.3.3)
@@ -848,10 +848,10 @@ importers:
         version: 4.1.0(vue@3.4.15)
     devDependencies:
       '@misskey-dev/eslint-plugin':
-        specifier: ^1.0.0
+        specifier: 1.0.0
         version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
       '@misskey-dev/summaly':
-        specifier: ^5.0.3
+        specifier: 5.0.3
         version: 5.0.3
       '@storybook/addon-actions':
         specifier: 7.6.10
@@ -909,7 +909,7 @@ importers:
         version: 7.6.10(typescript@5.3.3)(vite@5.0.12)(vue@3.4.15)
       '@testing-library/vue':
         specifier: 8.0.1
-        version: 8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.15)
+        version: 8.0.1(@vue/compiler-sfc@3.4.15)(vue@3.4.15)
       '@types/escape-regexp':
         specifier: 0.0.3
         version: 0.0.3
@@ -923,8 +923,8 @@ importers:
         specifier: 4.0.6
         version: 4.0.6
       '@types/node':
-        specifier: 20.11.5
-        version: 20.11.5
+        specifier: 20.11.10
+        version: 20.11.10
       '@types/punycode':
         specifier: 2.1.3
         version: 2.1.3
@@ -938,8 +938,8 @@ importers:
         specifier: 1.4.6
         version: 1.4.6
       '@types/uuid':
-        specifier: 9.0.7
-        version: 9.0.7
+        specifier: 9.0.8
+        version: 9.0.8
       '@types/ws':
         specifier: 8.5.10
         version: 8.5.10
@@ -1021,9 +1021,12 @@ importers:
       vitest-fetch-mock:
         specifier: 0.2.2
         version: 0.2.2(vitest@0.34.6)
+      vue-component-type-helpers:
+        specifier: ^1.8.27
+        version: 1.8.27
       vue-eslint-parser:
-        specifier: 9.4.0
-        version: 9.4.0(eslint@8.56.0)
+        specifier: 9.4.2
+        version: 9.4.2(eslint@8.56.0)
       vue-tsc:
         specifier: 1.8.27
         version: 1.8.27(typescript@5.3.3)
@@ -1102,7 +1105,7 @@ importers:
         version: 9.0.0(eslint@8.54.0)
       jest:
         specifier: ^29.7.0
-        version: 29.7.0(@types/node@20.11.5)
+        version: 29.7.0(@types/node@20.11.10)
       jest-worker:
         specifier: ^29.7.0
         version: 29.7.0
@@ -1169,7 +1172,7 @@ importers:
     dependencies:
       '@swc/cli':
         specifier: 0.1.63
-        version: 0.1.63(@swc/core@1.3.105)(chokidar@3.5.3)
+        version: 0.1.63(@swc/core@1.3.105)
       '@swc/core':
         specifier: 1.3.105
         version: 1.3.105
@@ -1182,7 +1185,7 @@ importers:
     devDependencies:
       '@microsoft/api-extractor':
         specifier: 7.39.1
-        version: 7.39.1(@types/node@20.11.5)
+        version: 7.39.1(@types/node@20.11.10)
       '@misskey-dev/eslint-plugin':
         specifier: 1.0.0
         version: 1.0.0(@typescript-eslint/eslint-plugin@6.18.1)(@typescript-eslint/parser@6.18.1)(eslint-plugin-import@2.29.1)(eslint@8.56.0)
@@ -1193,8 +1196,8 @@ importers:
         specifier: 29.5.11
         version: 29.5.11
       '@types/node':
-        specifier: 20.11.5
-        version: 20.11.5
+        specifier: 20.11.10
+        version: 20.11.10
       '@typescript-eslint/eslint-plugin':
         specifier: 6.18.1
         version: 6.18.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0)(typescript@5.3.3)
@@ -1206,7 +1209,7 @@ importers:
         version: 8.56.0
       jest:
         specifier: 29.7.0
-        version: 29.7.0(@types/node@20.11.5)
+        version: 29.7.0(@types/node@20.11.10)
       jest-fetch-mock:
         specifier: 3.0.3
         version: 3.0.3
@@ -3322,29 +3325,29 @@ packages:
     resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
     dev: true
 
-  /@bull-board/api@5.10.2(@bull-board/ui@5.10.2):
-    resolution: {integrity: sha512-Gx98cqN0cryJB35mVKjYsEnD3NxArWY3Xi2E5Wrr17QTVOzWEP4jyDQ/riiapVdnYqc9RSsxCCmdIaNdNPcXlQ==}
+  /@bull-board/api@5.14.0(@bull-board/ui@5.14.0):
+    resolution: {integrity: sha512-ppN9GeCH8QmCzs47CpDFwVb4Q5W2nK2QvcnbxKpjktCTonZ+5PnoWyXQvLStbcKU9SbMKAM0/OXhj4xOcSRllQ==}
     peerDependencies:
-      '@bull-board/ui': 5.10.2
+      '@bull-board/ui': 5.14.0
     dependencies:
-      '@bull-board/ui': 5.10.2
+      '@bull-board/ui': 5.14.0
       redis-info: 3.1.0
     dev: false
 
-  /@bull-board/fastify@5.10.2:
-    resolution: {integrity: sha512-NrV1PBu1jwXMBnLslxWLjmt4Qb0oPDSngcUXRll5B8Lvm6E8jtecmnVuNb2X1EtpIGVqhgwlGZ+Q7AC+3ZBMFg==}
+  /@bull-board/fastify@5.14.0:
+    resolution: {integrity: sha512-MEZbfUY74wL2dc9OJZGgYABZADlohp62MP1ZMOlC+6ZF4i7X95yxTQ9DmtIV6kkva7+abJgFGNUhtKi7Mq15Fg==}
     dependencies:
-      '@bull-board/api': 5.10.2(@bull-board/ui@5.10.2)
-      '@bull-board/ui': 5.10.2
+      '@bull-board/api': 5.14.0(@bull-board/ui@5.14.0)
+      '@bull-board/ui': 5.14.0
       '@fastify/static': 6.12.0
       '@fastify/view': 8.2.0
       ejs: 3.1.9
     dev: false
 
-  /@bull-board/ui@5.10.2:
-    resolution: {integrity: sha512-wU9XmrX/COISZ3+sn3VEDB1UtPt7szu4QSKTw1O0q+U1JLM4Kxfs3tH9ZAIulzMrY+CQtkJXd+dKZPuRqy4rfQ==}
+  /@bull-board/ui@5.14.0:
+    resolution: {integrity: sha512-quustWmLsLbqdbCQd4Mud9Eo/2BQzfJSNSiyJt9OrtYT4AXHMgGtbFUy2Ycyda7iQjC4ScKl8f+WdFs4y+KUJA==}
     dependencies:
-      '@bull-board/api': 5.10.2(@bull-board/ui@5.10.2)
+      '@bull-board/api': 5.14.0(@bull-board/ui@5.14.0)
     dev: false
 
   /@bundled-es-modules/cookie@2.0.0:
@@ -4314,7 +4317,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -4335,14 +4338,14 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.9.0
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.7.0
-      jest-config: 29.7.0(@types/node@20.11.5)
+      jest-config: 29.7.0(@types/node@20.11.10)
       jest-haste-map: 29.7.0
       jest-message-util: 29.7.0
       jest-regex-util: 29.6.3
@@ -4377,7 +4380,7 @@ packages:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       jest-mock: 29.7.0
     dev: true
 
@@ -4403,7 +4406,7 @@ packages:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -4436,7 +4439,7 @@ packages:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.20
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       chalk: 4.1.2
       collect-v8-coverage: 1.0.2
       exit: 0.1.2
@@ -4529,7 +4532,7 @@ packages:
     dependencies:
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       '@types/yargs': 16.0.5
       chalk: 4.1.2
     dev: true
@@ -4541,7 +4544,7 @@ packages:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.6
       '@types/istanbul-reports': 3.0.4
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       '@types/yargs': 17.0.32
       chalk: 4.1.2
 
@@ -4559,7 +4562,7 @@ packages:
       magic-string: 0.27.0
       react-docgen-typescript: 2.2.2(typescript@5.3.3)
       typescript: 5.3.3
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
     dev: true
 
   /@jridgewell/gen-mapping@0.3.3:
@@ -4652,24 +4655,24 @@ packages:
       react: 18.2.0
     dev: true
 
-  /@microsoft/api-extractor-model@7.28.4(@types/node@20.11.5):
+  /@microsoft/api-extractor-model@7.28.4(@types/node@20.11.10):
     resolution: {integrity: sha512-vucgyPmgHrJ/D4/xQywAmjTmSfxAx2/aDmD6TkIoLu51FdsAfuWRbijWA48AePy60OO+l+mmy9p2P/CEeBZqig==}
     dependencies:
       '@microsoft/tsdoc': 0.14.2
       '@microsoft/tsdoc-config': 0.16.2
-      '@rushstack/node-core-library': 3.63.0(@types/node@20.11.5)
+      '@rushstack/node-core-library': 3.63.0(@types/node@20.11.10)
     transitivePeerDependencies:
       - '@types/node'
     dev: true
 
-  /@microsoft/api-extractor@7.39.1(@types/node@20.11.5):
+  /@microsoft/api-extractor@7.39.1(@types/node@20.11.10):
     resolution: {integrity: sha512-V0HtCufWa8hZZvSmlEzQZfINcJkHAU/bmpyJQj6w+zpI87EkR8DuBOW6RWrO9c7mUYFZoDaNgUTyKo83ytv+QQ==}
     hasBin: true
     dependencies:
-      '@microsoft/api-extractor-model': 7.28.4(@types/node@20.11.5)
+      '@microsoft/api-extractor-model': 7.28.4(@types/node@20.11.10)
       '@microsoft/tsdoc': 0.14.2
       '@microsoft/tsdoc-config': 0.16.2
-      '@rushstack/node-core-library': 3.63.0(@types/node@20.11.5)
+      '@rushstack/node-core-library': 3.63.0(@types/node@20.11.10)
       '@rushstack/rig-package': 0.5.1
       '@rushstack/ts-command-line': 4.17.1
       colors: 1.2.5
@@ -4695,8 +4698,8 @@ packages:
     resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==}
     dev: true
 
-  /@misskey-dev/browser-image-resizer@2.2.1-misskey.10:
-    resolution: {integrity: sha512-Spjiwa8brffhz4FiYrZ8VoPRyPPRzcdaIzLVb8oMnD9YGU3uzcX/CcZ08okFhrUR/N6IlQM86r5dNH/yY5Uyjg==}
+  /@misskey-dev/browser-image-resizer@2024.1.0:
+    resolution: {integrity: sha512-4EnO0zLW5NDtng3Gaz5MuT761uiuoOuplwX18wBqgj8w56LTU5BjLn/vbHwDIIe0j2gwqDYhMb7bDjmr1/Fomg==}
     dev: false
 
   /@misskey-dev/eslint-plugin@1.0.0(@typescript-eslint/eslint-plugin@6.11.0)(@typescript-eslint/parser@6.11.0)(eslint-plugin-import@2.29.1)(eslint@8.53.0):
@@ -4853,7 +4856,7 @@ packages:
       tslib: 2.6.2
       uid: 2.0.2
 
-  /@nestjs/core@10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1):
+  /@nestjs/core@10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.1)(reflect-metadata@0.1.14)(rxjs@7.8.1):
     resolution: {integrity: sha512-+ckOI6BPi2ZMHikT9MCG4ctHDc4OnjhoIytrn7f2AYMMXI4bnutJhqyQKc30VDka5x3Wq6QAD57pgSP7y+JjJg==}
     requiresBuild: true
     peerDependencies:
@@ -4872,7 +4875,7 @@ packages:
         optional: true
     dependencies:
       '@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
-      '@nestjs/platform-express': 10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
+      '@nestjs/platform-express': 10.3.1(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
       '@nuxtjs/opencollective': 0.3.2
       fast-safe-stringify: 2.1.1
       iterare: 1.2.1
@@ -4884,14 +4887,14 @@ packages:
     transitivePeerDependencies:
       - encoding
 
-  /@nestjs/platform-express@10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10):
-    resolution: {integrity: sha512-E4hUW48bYv8OHbP9XQg6deefmXb0pDSSuE38SdhA0mJ37zGY7C5EqqBUdlQk4ttfD+OdnbIgJ1zOokT6dd2d7A==}
+  /@nestjs/platform-express@10.3.1(@nestjs/common@10.2.10)(@nestjs/core@10.2.10):
+    resolution: {integrity: sha512-Rj21quI5h4Lry7q9an+nO4ADQiQUy9A6XK74o5aTUHo3Ysm25ujqh2NgU4XbT3M2oXU9qzhE59OfhkQ7ZUvTAg==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
       '@nestjs/core': ^10.0.0
     dependencies:
       '@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
-      '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1)
+      '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.1)(reflect-metadata@0.1.14)(rxjs@7.8.1)
       body-parser: 1.20.2
       cors: 2.8.5
       express: 4.18.2
@@ -4900,7 +4903,7 @@ packages:
     transitivePeerDependencies:
       - supports-color
 
-  /@nestjs/testing@10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.0):
+  /@nestjs/testing@10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.1):
     resolution: {integrity: sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==}
     peerDependencies:
       '@nestjs/common': ^10.0.0
@@ -4914,8 +4917,8 @@ packages:
         optional: true
     dependencies:
       '@nestjs/common': 10.2.10(reflect-metadata@0.1.14)(rxjs@7.8.1)
-      '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.0)(reflect-metadata@0.1.14)(rxjs@7.8.1)
-      '@nestjs/platform-express': 10.3.0(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
+      '@nestjs/core': 10.2.10(@nestjs/common@10.2.10)(@nestjs/platform-express@10.3.1)(reflect-metadata@0.1.14)(rxjs@7.8.1)
+      '@nestjs/platform-express': 10.3.1(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)
       tslib: 2.6.2
     dev: false
 
@@ -5748,7 +5751,7 @@ packages:
     requiresBuild: true
     optional: true
 
-  /@rushstack/node-core-library@3.63.0(@types/node@20.11.5):
+  /@rushstack/node-core-library@3.63.0(@types/node@20.11.10):
     resolution: {integrity: sha512-Q7B3dVpBQF1v+mUfxNcNZh5uHVR8ntcnkN5GYjbBLrxUYHBGKbnCM+OdcN+hzCpFlLBH6Ob0dEHhZ0spQwf24A==}
     peerDependencies:
       '@types/node': '*'
@@ -5756,7 +5759,7 @@ packages:
       '@types/node':
         optional: true
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       colors: 1.2.5
       fs-extra: 7.0.1
       import-lazy: 4.0.0
@@ -5782,6 +5785,10 @@ packages:
       string-argv: 0.3.1
     dev: true
 
+  /@shikijs/core@1.0.0-beta.3:
+    resolution: {integrity: sha512-SCwPom2Wn8XxNlEeqdzycU93SKgzYeVsedjqDsgZaz4XiiPpZUzlHt2NAEQTwTnPcHNZapZ6vbkwJ8P11ggL3Q==}
+    dev: false
+
   /@sideway/address@4.1.4:
     resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
     dependencies:
@@ -5796,8 +5803,8 @@ packages:
     resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
     dev: true
 
-  /@simplewebauthn/server@9.0.0:
-    resolution: {integrity: sha512-pm3UWhZrneBoSDQmtYTScZeOhcKbPch/zmMgfJZJY3sE1l0VAQsAKlIjoLGRrw2vXYSL7Eslhh0Qdb301IPxTQ==}
+  /@simplewebauthn/server@9.0.1:
+    resolution: {integrity: sha512-XnilMoBygy2BOZjIHPxby+7ENx5ChN2wXfhd14mOgO/XitYMqdphTo/kwgxEI4/Je3lELK1h/eLDJqM2fIKS1w==}
     engines: {node: '>=16.0.0'}
     dependencies:
       '@hexagon/base64': 1.1.27
@@ -6345,7 +6352,7 @@ packages:
     dependencies:
       '@storybook/core-events': 7.6.10
       '@storybook/global': 5.0.0
-      '@types/uuid': 9.0.7
+      '@types/uuid': 9.0.8
       dequal: 2.0.3
       polished: 4.2.2
       uuid: 9.0.1
@@ -6604,7 +6611,7 @@ packages:
       magic-string: 0.30.5
       rollup: 3.29.4
       typescript: 5.3.3
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
     transitivePeerDependencies:
       - encoding
       - supports-color
@@ -6978,7 +6985,7 @@ packages:
       react: 18.2.0
       react-docgen: 7.0.1
       react-dom: 18.2.0(react@18.2.0)
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
     transitivePeerDependencies:
       - '@preact/preset-vite'
       - encoding
@@ -7104,7 +7111,7 @@ packages:
       '@storybook/vue3': 7.6.10(vue@3.4.15)
       '@vitejs/plugin-vue': 4.5.2(vite@5.0.12)(vue@3.4.15)
       magic-string: 0.30.5
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
       vue-docgen-api: 4.64.1(vue@3.4.15)
     transitivePeerDependencies:
       - '@preact/preset-vite'
@@ -7139,7 +7146,7 @@ packages:
       - supports-color
     dev: true
 
-  /@swc/cli@0.1.63(@swc/core@1.3.105)(chokidar@3.5.3):
+  /@swc/cli@0.1.63(@swc/core@1.3.105):
     resolution: {integrity: sha512-EM9oxxHzmmsprYRbGqsS2M4M/Gr5Gkcl0ROYYIdlUyTkhOiX822EQiRCpPCwdutdnzH2GyaTN7wc6i0Y+CKd3A==}
     engines: {node: '>= 12.13'}
     hasBin: true
@@ -7152,6 +7159,26 @@ packages:
     dependencies:
       '@mole-inc/bin-wrapper': 8.0.1
       '@swc/core': 1.3.105
+      commander: 7.2.0
+      fast-glob: 3.3.2
+      semver: 7.5.4
+      slash: 3.0.0
+      source-map: 0.7.4
+    dev: false
+
+  /@swc/cli@0.1.63(@swc/core@1.3.107)(chokidar@3.5.3):
+    resolution: {integrity: sha512-EM9oxxHzmmsprYRbGqsS2M4M/Gr5Gkcl0ROYYIdlUyTkhOiX822EQiRCpPCwdutdnzH2GyaTN7wc6i0Y+CKd3A==}
+    engines: {node: '>= 12.13'}
+    hasBin: true
+    peerDependencies:
+      '@swc/core': ^1.2.66
+      chokidar: 3.5.3
+    peerDependenciesMeta:
+      chokidar:
+        optional: true
+    dependencies:
+      '@mole-inc/bin-wrapper': 8.0.1
+      '@swc/core': 1.3.107
       chokidar: 3.5.3
       commander: 7.2.0
       fast-glob: 3.3.2
@@ -7179,6 +7206,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-darwin-arm64@1.3.107:
+    resolution: {integrity: sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [darwin]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-darwin-arm64@1.3.56:
     resolution: {integrity: sha512-DZcu7BzDaLEdWHabz9DRTP0yEBLqkrWmskFcD5BX0lGAvoIvE4duMnAqi5F2B3X7630QioHRCYFoRw2WkeE3Cw==}
     engines: {node: '>=10'}
@@ -7196,6 +7231,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-darwin-x64@1.3.107:
+    resolution: {integrity: sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [darwin]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-darwin-x64@1.3.56:
     resolution: {integrity: sha512-VH5saqYFasdRXJy6RAT+MXm0+IjkMZvOkohJwUei+oA65cKJofQwrJ1jZro8yOJFYvUSI3jgNRGsdBkmo/4hMw==}
     engines: {node: '>=10'}
@@ -7224,6 +7267,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-linux-arm-gnueabihf@1.3.107:
+    resolution: {integrity: sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==}
+    engines: {node: '>=10'}
+    cpu: [arm]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-linux-arm-gnueabihf@1.3.56:
     resolution: {integrity: sha512-LWwPo6NnJkH01+ukqvkoNIOpMdw+Zundm4vBeicwyVrkP+mC3kwVfi03TUFpQUz3kRKdw/QEnxGTj+MouCPbtw==}
     engines: {node: '>=10'}
@@ -7241,6 +7292,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-linux-arm64-gnu@1.3.107:
+    resolution: {integrity: sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-linux-arm64-gnu@1.3.56:
     resolution: {integrity: sha512-GzsUy/4egJ4cMlxbM+Ub7AMi5CKAc+pxBxrh8MUPQbyStW8jGgnQsJouTnGy0LHawtdEnsCOl6PcO6OgvktXuQ==}
     engines: {node: '>=10'}
@@ -7258,6 +7317,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-linux-arm64-musl@1.3.107:
+    resolution: {integrity: sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-linux-arm64-musl@1.3.56:
     resolution: {integrity: sha512-9gxL09BIiAv8zY0DjfnFf19bo8+P4T9tdhzPwcm+1yPJcY5yr1+YFWLNFzz01agtOj6VlZ2/wUJTaOfdjjtc+A==}
     engines: {node: '>=10'}
@@ -7275,6 +7342,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-linux-x64-gnu@1.3.107:
+    resolution: {integrity: sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-linux-x64-gnu@1.3.56:
     resolution: {integrity: sha512-n0ORNknl50vMRkll3BDO1E4WOqY6iISlPV1ZQCRLWQ6YQ2q8/WAryBxc2OAybcGHBUFkxyACpJukeU1QZ/9tNw==}
     engines: {node: '>=10'}
@@ -7292,6 +7367,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-linux-x64-musl@1.3.107:
+    resolution: {integrity: sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-linux-x64-musl@1.3.56:
     resolution: {integrity: sha512-r+D34WLAOAlJtfw1gaVWpHRwCncU9nzW9i7w9kSw4HpWYnHJOz54jLGSEmNsrhdTCz1VK2ar+V2ktFUsrlGlDA==}
     engines: {node: '>=10'}
@@ -7309,6 +7392,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-win32-arm64-msvc@1.3.107:
+    resolution: {integrity: sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [win32]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-win32-arm64-msvc@1.3.56:
     resolution: {integrity: sha512-29Yt75Is6X24z3x8h/xZC1HnDPkPpyLH9mDQiM6Cuc0I9mVr1XSriPEUB2N/awf5IE4SA8c+3IVq1DtKWbkJIw==}
     engines: {node: '>=10'}
@@ -7326,6 +7417,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-win32-ia32-msvc@1.3.107:
+    resolution: {integrity: sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==}
+    engines: {node: '>=10'}
+    cpu: [ia32]
+    os: [win32]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-win32-ia32-msvc@1.3.56:
     resolution: {integrity: sha512-mplp0zbYDrcHtfvkniXlXdB04e2qIjz2Gq/XHKr4Rnc6xVORJjjXF91IemXKpavx2oZYJws+LNJL7UFQ8jyCdQ==}
     engines: {node: '>=10'}
@@ -7343,6 +7442,14 @@ packages:
     requiresBuild: true
     optional: true
 
+  /@swc/core-win32-x64-msvc@1.3.107:
+    resolution: {integrity: sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [win32]
+    requiresBuild: true
+    optional: true
+
   /@swc/core-win32-x64-msvc@1.3.56:
     resolution: {integrity: sha512-zp8MBnrw/bjdLenO/ifYzHrImSjKunqL0C2IF4LXYNRfcbYFh2NwobsVQMZ20IT0474lKRdlP8Oxdt+bHuXrzA==}
     engines: {node: '>=10'}
@@ -7376,6 +7483,30 @@ packages:
       '@swc/core-win32-ia32-msvc': 1.3.105
       '@swc/core-win32-x64-msvc': 1.3.105
 
+  /@swc/core@1.3.107:
+    resolution: {integrity: sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==}
+    engines: {node: '>=10'}
+    requiresBuild: true
+    peerDependencies:
+      '@swc/helpers': ^0.5.0
+    peerDependenciesMeta:
+      '@swc/helpers':
+        optional: true
+    dependencies:
+      '@swc/counter': 0.1.2
+      '@swc/types': 0.1.5
+    optionalDependencies:
+      '@swc/core-darwin-arm64': 1.3.107
+      '@swc/core-darwin-x64': 1.3.107
+      '@swc/core-linux-arm-gnueabihf': 1.3.107
+      '@swc/core-linux-arm64-gnu': 1.3.107
+      '@swc/core-linux-arm64-musl': 1.3.107
+      '@swc/core-linux-x64-gnu': 1.3.107
+      '@swc/core-linux-x64-musl': 1.3.107
+      '@swc/core-win32-arm64-msvc': 1.3.107
+      '@swc/core-win32-ia32-msvc': 1.3.107
+      '@swc/core-win32-x64-msvc': 1.3.107
+
   /@swc/counter@0.1.2:
     resolution: {integrity: sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==}
 
@@ -7390,6 +7521,17 @@ packages:
       jsonc-parser: 3.2.0
     dev: true
 
+  /@swc/jest@0.2.31(@swc/core@1.3.107):
+    resolution: {integrity: sha512-Gh0Ste380O8KUY1IqsKr+aOvqqs2Loa+WcWWVNwl+lhXqOWK1iTFAP1K0IDfLqAuFP68+D/PxcpBJn21e6Quvw==}
+    engines: {npm: '>= 7.0.0'}
+    peerDependencies:
+      '@swc/core': '*'
+    dependencies:
+      '@jest/create-cache-key-function': 29.7.0
+      '@swc/core': 1.3.107
+      jsonc-parser: 3.2.0
+    dev: true
+
   /@swc/types@0.1.5:
     resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==}
 
@@ -7487,7 +7629,7 @@ packages:
       '@testing-library/dom': 9.2.0
     dev: true
 
-  /@testing-library/vue@8.0.1(@vue/compiler-sfc@3.4.3)(vue@3.4.15):
+  /@testing-library/vue@8.0.1(@vue/compiler-sfc@3.4.15)(vue@3.4.15):
     resolution: {integrity: sha512-l51ZEpjTQ6glq3wM+asQ1GbKJMGcxwgHEygETx0aCRN4TjFEGvMZy4YdWKs/y7bu4bmLrxcxhbEPP7iPSW/2OQ==}
     engines: {node: '>=14'}
     peerDependencies:
@@ -7496,7 +7638,7 @@ packages:
     dependencies:
       '@babel/runtime': 7.23.2
       '@testing-library/dom': 9.3.3
-      '@vue/compiler-sfc': 3.4.3
+      '@vue/compiler-sfc': 3.4.15
       '@vue/test-utils': 2.4.1(vue@3.4.15)
       vue: 3.4.15(typescript@5.3.3)
     transitivePeerDependencies:
@@ -7530,7 +7672,7 @@ packages:
   /@types/accepts@1.3.7:
     resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/archiver@6.0.2:
@@ -7584,7 +7726,7 @@ packages:
     resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
     dependencies:
       '@types/connect': 3.4.35
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/braces@3.0.1:
@@ -7596,7 +7738,7 @@ packages:
     dependencies:
       '@types/http-cache-semantics': 4.0.4
       '@types/keyv': 3.1.4
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       '@types/responselike': 1.0.0
     dev: false
 
@@ -7629,7 +7771,7 @@ packages:
   /@types/connect@3.4.35:
     resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/content-disposition@0.5.8:
@@ -7647,7 +7789,7 @@ packages:
   /@types/cross-spawn@6.0.2:
     resolution: {integrity: sha512-KuwNhp3eza+Rhu8IFI5HUXRP0LIhqH5cAjubUvGXXthh4YYBuP2ntwEX+Cz8GJoZUHlKo247wPWOfA9LYEq4cw==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/detect-port@1.3.2:
@@ -7699,7 +7841,7 @@ packages:
   /@types/express-serve-static-core@4.17.33:
     resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       '@types/qs': 6.9.7
       '@types/range-parser': 1.2.4
     dev: true
@@ -7720,7 +7862,7 @@ packages:
   /@types/fluent-ffmpeg@2.1.24:
     resolution: {integrity: sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/form-data@2.5.0:
@@ -7734,13 +7876,13 @@ packages:
     resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
     dependencies:
       '@types/minimatch': 5.1.2
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/graceful-fs@4.1.9:
     resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/http-cache-semantics@4.0.4:
@@ -7749,7 +7891,7 @@ packages:
   /@types/http-link-header@1.0.5:
     resolution: {integrity: sha512-AxhIKR8UbyoqCTNp9rRepkktHuUOw3DjfOfDCaO9kwI8AYzjhxyrvZq4+mRw/2daD3hYDknrtSeV6SsPwmc71w==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/istanbul-lib-coverage@2.0.4:
@@ -7813,7 +7955,7 @@ packages:
   /@types/jsdom@21.1.6:
     resolution: {integrity: sha512-/7kkMsC+/kMs7gAYmmBR9P0vGTnOoLhQhyhQJSlXGI5bzTHp6xdo0TtKWQAsz6pmSAeVqKSbqeyP6hytqr9FDw==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       '@types/tough-cookie': 4.0.2
       parse5: 7.1.2
     dev: true
@@ -7837,7 +7979,7 @@ packages:
   /@types/keyv@3.1.4:
     resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: false
 
   /@types/lodash@4.14.191:
@@ -7881,7 +8023,7 @@ packages:
   /@types/node-fetch@2.6.4:
     resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       form-data: 3.0.1
     dev: true
 
@@ -7895,10 +8037,16 @@ packages:
     resolution: {integrity: sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==}
     dev: true
 
+  /@types/node@20.11.10:
+    resolution: {integrity: sha512-rZEfe/hJSGYmdfX9tvcPMYeYPW2sNl50nsw4jZmRcaG0HIAb0WYEpsB05GOb53vjqpyE9GUhlDQ4jLSoB5q9kg==}
+    dependencies:
+      undici-types: 5.26.5
+
   /@types/node@20.11.5:
     resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==}
     dependencies:
       undici-types: 5.26.5
+    dev: true
 
   /@types/node@20.9.1:
     resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==}
@@ -7909,7 +8057,7 @@ packages:
   /@types/nodemailer@6.4.14:
     resolution: {integrity: sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/normalize-package-data@2.4.1:
@@ -7926,13 +8074,13 @@ packages:
     resolution: {integrity: sha512-Ali0fUUn+zgr4Yy/pCTFbuiaiJpq7l7OQwFnxYVchNbNGIx0c4Wkcdje6WO89I91RAaYF+gVc1pOaizA4YKZmA==}
     dependencies:
       '@types/express': 4.17.17
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/oauth@0.9.4:
     resolution: {integrity: sha512-qk9orhti499fq5XxKCCEbd0OzdPZuancneyse3KtR+vgMiHRbh+mn8M4G6t64ob/Fg+GZGpa565MF/2dKWY32A==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
 
   /@types/object-assign-deep@0.4.3:
     resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
@@ -7942,10 +8090,10 @@ packages:
     resolution: {integrity: sha512-ffLAxD6Xqcf2gSbtEJehj8yJ5R/2OZqD4liodQvQQ+hhO4kg1mk9ToEZQPMtNTm/zIQj2GNleQbsjPp9+UQm4Q==}
     dev: false
 
-  /@types/pg@8.10.9:
-    resolution: {integrity: sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==}
+  /@types/pg@8.11.0:
+    resolution: {integrity: sha512-sDAlRiBNthGjNFfvt0k6mtotoVYVQ63pA8R4EMWka7crawSR60waVYR0HAgmPRs/e2YaeJTD/43OoZ3PFw80pw==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       pg-protocol: 1.6.0
       pg-types: 4.0.1
     dev: true
@@ -7969,7 +8117,7 @@ packages:
   /@types/qrcode@1.5.5:
     resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/qs@6.9.7:
@@ -7999,7 +8147,7 @@ packages:
   /@types/readdir-glob@1.1.1:
     resolution: {integrity: sha512-ImM6TmoF8bgOwvehGviEj3tRdRBbQujr1N+0ypaln/GWjaerOB26jb93vsRHmdMtvVQZQebOlqt2HROark87mQ==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/rename@1.0.7:
@@ -8013,7 +8161,7 @@ packages:
   /@types/responselike@1.0.0:
     resolution: {integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: false
 
   /@types/sanitize-html@2.9.5:
@@ -8038,7 +8186,7 @@ packages:
     resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==}
     dependencies:
       '@types/mime': 3.0.1
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/serviceworker@0.0.67:
@@ -8107,23 +8255,28 @@ packages:
 
   /@types/uuid@9.0.7:
     resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==}
+    dev: false
+
+  /@types/uuid@9.0.8:
+    resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
+    dev: true
 
   /@types/vary@1.1.3:
     resolution: {integrity: sha512-XJT8/ZQCL7NUut9QDLf6l24JfAEl7bnNdgxfj50cHIpEPRJLHHDDFOAq6i+GsEmeFfH7NamhBE4c4Thtb2egWg==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/web-push@3.6.3:
     resolution: {integrity: sha512-v3oT4mMJsHeJ/rraliZ+7TbZtr5bQQuxcgD7C3/1q/zkAj29c8RE0F9lVZVu3hiQe5Z9fYcBreV7TLnfKR+4mg==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /@types/ws@8.5.10:
     resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
 
   /@types/yargs-parser@21.0.0:
     resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
@@ -8147,7 +8300,7 @@ packages:
     resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
     requiresBuild: true
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
     optional: true
 
@@ -8560,7 +8713,7 @@ packages:
       '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.23.3)
       magic-string: 0.27.0
       react-refresh: 0.14.0
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
     transitivePeerDependencies:
       - supports-color
     dev: true
@@ -8572,18 +8725,18 @@ packages:
       vite: ^4.0.0 || ^5.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
       vue: 3.4.15(typescript@5.3.3)
     dev: true
 
-  /@vitejs/plugin-vue@5.0.2(vite@5.0.12)(vue@3.4.15):
-    resolution: {integrity: sha512-kEjJHrLb5ePBvjD0SPZwJlw1QTRcjjCA9sB5VyfonoXVBxTS7TMnqL6EkLt1Eu61RDeiuZ/WN9Hf6PxXhPI2uA==}
+  /@vitejs/plugin-vue@5.0.3(vite@5.0.12)(vue@3.4.15):
+    resolution: {integrity: sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==}
     engines: {node: ^18.0.0 || >=20.0.0}
     peerDependencies:
       vite: ^5.0.0
       vue: ^3.2.25
     dependencies:
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
       vue: 3.4.15(typescript@5.3.3)
     dev: false
 
@@ -8691,6 +8844,7 @@ packages:
       entities: 4.5.0
       estree-walker: 2.0.2
       source-map-js: 1.0.2
+    dev: true
 
   /@vue/compiler-dom@3.3.12:
     resolution: {integrity: sha512-RdJU9oEYaoPKUdGXCy0l+i4clesdDeLmbvRlszoc9iagsnBnMmQtYfCPVQ5BHB6o7K4SCucDdJM2Dh3oXB0D6g==}
@@ -8705,12 +8859,6 @@ packages:
       '@vue/compiler-core': 3.4.15
       '@vue/shared': 3.4.15
 
-  /@vue/compiler-dom@3.4.3:
-    resolution: {integrity: sha512-oGF1E9/htI6JWj/lTJgr6UgxNCtNHbM6xKVreBWeZL9QhRGABRVoWGAzxmtBfSOd+w0Zi5BY0Es/tlJrN6WgEg==}
-    dependencies:
-      '@vue/compiler-core': 3.4.3
-      '@vue/shared': 3.4.3
-
   /@vue/compiler-sfc@3.4.15:
     resolution: {integrity: sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==}
     dependencies:
@@ -8724,31 +8872,12 @@ packages:
       postcss: 8.4.33
       source-map-js: 1.0.2
 
-  /@vue/compiler-sfc@3.4.3:
-    resolution: {integrity: sha512-NuJqb5is9I4uzv316VRUDYgIlPZCG8D+ARt5P4t5UDShIHKL25J3TGZAUryY/Aiy0DsY7srJnZL5ryB6DD63Zw==}
-    dependencies:
-      '@babel/parser': 7.23.6
-      '@vue/compiler-core': 3.4.3
-      '@vue/compiler-dom': 3.4.3
-      '@vue/compiler-ssr': 3.4.3
-      '@vue/shared': 3.4.3
-      estree-walker: 2.0.2
-      magic-string: 0.30.5
-      postcss: 8.4.32
-      source-map-js: 1.0.2
-
   /@vue/compiler-ssr@3.4.15:
     resolution: {integrity: sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==}
     dependencies:
       '@vue/compiler-dom': 3.4.15
       '@vue/shared': 3.4.15
 
-  /@vue/compiler-ssr@3.4.3:
-    resolution: {integrity: sha512-wnYQtMBkeFSxgSSQbYGQeXPhQacQiog2c6AlvMldQH6DB+gSXK/0F6DVXAJfEiuBSgBhUc8dwrrG5JQcqwalsA==}
-    dependencies:
-      '@vue/compiler-dom': 3.4.3
-      '@vue/shared': 3.4.3
-
   /@vue/language-core@1.8.27(typescript@5.3.3):
     resolution: {integrity: sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==}
     peerDependencies:
@@ -8805,6 +8934,7 @@ packages:
 
   /@vue/shared@3.4.3:
     resolution: {integrity: sha512-rIwlkkP1n4uKrRzivAKPZIEkHiuwY5mmhMJ2nZKCBLz8lTUlE73rQh4n1OnnMurXt1vcUNyH4ZPfdh8QweTjpQ==}
+    dev: true
 
   /@vue/test-utils@2.4.1(vue@3.4.15):
     resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
@@ -9011,6 +9141,7 @@ packages:
 
   /ansi-sequence-parser@1.1.1:
     resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
+    dev: true
 
   /ansi-styles@3.2.1:
     resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
@@ -9749,8 +9880,8 @@ packages:
     dependencies:
       node-gyp-build: 4.6.0
 
-  /bullmq@5.1.4:
-    resolution: {integrity: sha512-j/AjaPc8BhyrH7b2MyZpi4cUtGH8TJTxonZUmXEefmKU8z5DcldzmlXPief0P4+qvN0A7qwWZH3n0F+GsWgQkg==}
+  /bullmq@5.1.5:
+    resolution: {integrity: sha512-Rc9QGHrj/wJ8RMENKa839o1pJmdicg7KBTfmVU8YqYuEK2JcMSJaKMg2XrAi7sdYSawgOJgC/kiW9fCGYEj6Yg==}
     dependencies:
       cron-parser: 4.8.1
       glob: 8.1.0
@@ -10093,9 +10224,17 @@ packages:
     engines: {node: '>=10'}
     requiresBuild: true
 
-  /chromatic@10.3.1:
-    resolution: {integrity: sha512-IHczKH3K3vVeZGE3XyCy/T8EQH2mGUEyQ9QUuULrWlYCfo760cnzehdTjrpuIUetkHtv7noA5Hmn6joQlz3Ufw==}
+  /chromatic@10.6.1:
+    resolution: {integrity: sha512-bd4C5sEEtN83uUmbc4Fu+x7+lJIPdMUdu4D6HRDQEIDl/Tatc8+By4bZluH1pzg/MbP9vllkL6Ua9vF4EEA7VA==}
     hasBin: true
+    peerDependencies:
+      chromatic-cypress: ^0.4.0 || ^1.0.0
+      chromatic-playwright: ^0.4.0 || ^1.0.0
+    peerDependenciesMeta:
+      chromatic-cypress:
+        optional: true
+      chromatic-playwright:
+        optional: true
     dev: false
 
   /ci-info@3.9.0:
@@ -10461,7 +10600,7 @@ packages:
       readable-stream: 3.6.2
     dev: false
 
-  /create-jest@29.7.0(@types/node@20.11.5):
+  /create-jest@29.7.0(@types/node@20.11.10):
     resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -10470,7 +10609,7 @@ packages:
       chalk: 4.1.2
       exit: 0.1.2
       graceful-fs: 4.2.11
-      jest-config: 29.7.0(@types/node@20.11.5)
+      jest-config: 29.7.0(@types/node@20.11.10)
       jest-util: 29.7.0
       prompts: 2.4.2
     transitivePeerDependencies:
@@ -11600,7 +11739,7 @@ packages:
       nth-check: 2.1.1
       postcss-selector-parser: 6.0.13
       semver: 7.5.4
-      vue-eslint-parser: 9.4.0(eslint@8.56.0)
+      vue-eslint-parser: 9.4.2(eslint@8.56.0)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - supports-color
@@ -12807,8 +12946,8 @@ packages:
       p-cancelable: 3.0.0
       responselike: 3.0.0
 
-  /got@14.0.0:
-    resolution: {integrity: sha512-X01vTgaX9SwaMq5DfImvS+3GMQFFs5HtrrlS9CuzUSzkxAf/tWGEyynuI+Qy7BjciMczZGjyVSmawYbP4eYhYA==}
+  /got@14.1.0:
+    resolution: {integrity: sha512-jGmSBfxa7jOGg464azcsf/cUlJBZldU8edFpiVebIJrVBE4vqVx0t3Z2f1kz1WrcMvLgQREoC/l2ttDmSHwyRg==}
     engines: {node: '>=20'}
     dependencies:
       '@sindresorhus/is': 6.1.0
@@ -13730,7 +13869,7 @@ packages:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.5.1
@@ -13751,7 +13890,7 @@ packages:
       - supports-color
     dev: true
 
-  /jest-cli@29.7.0(@types/node@20.11.5):
+  /jest-cli@29.7.0(@types/node@20.11.10):
     resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -13765,10 +13904,10 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
       chalk: 4.1.2
-      create-jest: 29.7.0(@types/node@20.11.5)
+      create-jest: 29.7.0(@types/node@20.11.10)
       exit: 0.1.2
       import-local: 3.1.0
-      jest-config: 29.7.0(@types/node@20.11.5)
+      jest-config: 29.7.0(@types/node@20.11.10)
       jest-util: 29.7.0
       jest-validate: 29.7.0
       yargs: 17.6.2
@@ -13779,7 +13918,7 @@ packages:
       - ts-node
     dev: true
 
-  /jest-config@29.7.0(@types/node@20.11.5):
+  /jest-config@29.7.0(@types/node@20.11.10):
     resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     peerDependencies:
@@ -13794,7 +13933,7 @@ packages:
       '@babel/core': 7.23.3
       '@jest/test-sequencer': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       babel-jest: 29.7.0(@babel/core@7.23.3)
       chalk: 4.1.2
       ci-info: 3.9.0
@@ -13873,7 +14012,7 @@ packages:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       jest-mock: 29.7.0
       jest-util: 29.7.0
     dev: true
@@ -13902,7 +14041,7 @@ packages:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.9
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -13961,7 +14100,7 @@ packages:
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
     dependencies:
       '@jest/types': 27.5.1
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
     dev: true
 
   /jest-mock@29.7.0:
@@ -13969,7 +14108,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       jest-util: 29.7.0
     dev: true
 
@@ -14024,7 +14163,7 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -14055,7 +14194,7 @@ packages:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       chalk: 4.1.2
       cjs-module-lexer: 1.2.2
       collect-v8-coverage: 1.0.1
@@ -14107,7 +14246,7 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       chalk: 4.1.2
       ci-info: 3.9.0
       graceful-fs: 4.2.11
@@ -14131,7 +14270,7 @@ packages:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -14150,13 +14289,13 @@ packages:
     resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
     dev: true
 
-  /jest@29.7.0(@types/node@20.11.5):
+  /jest@29.7.0(@types/node@20.11.10):
     resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     hasBin: true
@@ -14169,7 +14308,7 @@ packages:
       '@jest/core': 29.7.0
       '@jest/types': 29.6.3
       import-local: 3.1.0
-      jest-cli: 29.7.0(@types/node@20.11.5)
+      jest-cli: 29.7.0(@types/node@20.11.10)
     transitivePeerDependencies:
       - '@types/node'
       - babel-plugin-macros
@@ -14377,6 +14516,7 @@ packages:
 
   /jsonc-parser@3.2.0:
     resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
+    dev: true
 
   /jsonfile@4.0.0:
     resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
@@ -16158,8 +16298,8 @@ packages:
     engines: {node: '>= 6'}
     dev: true
 
-  /pkce-challenge@4.0.1:
-    resolution: {integrity: sha512-WGmtS1stcStsvRwNXix3iR1ujFcDaJR+sEODRa2ZFruT0lM4lhPAFTL5SUpqD5vTJdRlgtuMQhcp1kIEJx4LUw==}
+  /pkce-challenge@4.1.0:
+    resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==}
     engines: {node: '>=16.20.0'}
     dev: false
 
@@ -16532,14 +16672,6 @@ packages:
       source-map-js: 1.0.2
     dev: false
 
-  /postcss@8.4.32:
-    resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==}
-    engines: {node: ^10 || ^12 || >=14}
-    dependencies:
-      nanoid: 3.3.7
-      picocolors: 1.0.0
-      source-map-js: 1.0.2
-
   /postcss@8.4.33:
     resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==}
     engines: {node: ^10 || ^12 || >=14}
@@ -17348,6 +17480,10 @@ packages:
   /reflect-metadata@0.1.14:
     resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==}
 
+  /reflect-metadata@0.2.1:
+    resolution: {integrity: sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==}
+    dev: false
+
   /regenerate-unicode-properties@10.1.0:
     resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
     engines: {node: '>=4'}
@@ -17836,6 +17972,13 @@ packages:
       jsonc-parser: 3.2.0
       vscode-oniguruma: 1.7.0
       vscode-textmate: 8.0.0
+    dev: true
+
+  /shiki@1.0.0-beta.3:
+    resolution: {integrity: sha512-z7cHTNSSvwGx2DfeLwjSNLo+HcVxifgNIzLm6Ye52eXcIwNHXT0wHbhy7FDOKSKveuEHBwt9opfj3Hoc8LE1Yg==}
+    dependencies:
+      '@shikijs/core': 1.0.0-beta.3
+    dev: false
 
   /side-channel@1.0.4:
     resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
@@ -18529,8 +18672,8 @@ packages:
     resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==}
     dev: true
 
-  /systeminformation@5.21.23:
-    resolution: {integrity: sha512-hI8W9WoPKgRAahVsfqfqg7H6JyIQXoA5RKOohUSOeyZwG+D0vnLOdXaKAUvdaMaL6CCsfN4x3mQYUIY4Qaalcg==}
+  /systeminformation@5.21.24:
+    resolution: {integrity: sha512-xQada8ByGGFoRXJaUptGgddn3i7IjtSdqNdCKzB8xkzsM7pHnfLYBWxkPdGzhZ0Z/l+W1yo+aZQZ74d2isj8kw==}
     engines: {node: '>=8.0.0'}
     os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
     hasBin: true
@@ -18665,8 +18808,8 @@ packages:
       real-require: 0.2.0
     dev: false
 
-  /three@0.160.0:
-    resolution: {integrity: sha512-DLU8lc0zNIPkM7rH5/e1Ks1Z8tWCGRq6g8mPowdDJpw1CFBJMU7UoJjC6PefXW7z//SSl0b2+GCw14LB+uDhng==}
+  /three@0.160.1:
+    resolution: {integrity: sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==}
     dev: false
 
   /throttle-debounce@5.0.0:
@@ -18865,7 +19008,7 @@ packages:
       '@babel/core': 7.23.3
       bs-logger: 0.2.6
       fast-json-stable-stringify: 2.1.0
-      jest: 29.7.0(@types/node@20.11.5)
+      jest: 29.7.0(@types/node@20.11.10)
       jest-util: 29.7.0
       json5: 2.2.3
       lodash.memoize: 4.1.2
@@ -19064,9 +19207,9 @@ packages:
       typescript: 5.1.6
     dev: true
 
-  /typeorm@0.3.19(ioredis@5.3.2)(pg@8.11.3):
-    resolution: {integrity: sha512-OGelrY5qEoAU80mR1iyvmUHiKCPUydL6xp6bebXzS7jyv/X70Gp/jBWRAfF4qGOfy2A7orMiGRfwsBUNbEL65g==}
-    engines: {node: '>= 12.9.0'}
+  /typeorm@0.3.20(ioredis@5.3.2)(pg@8.11.3):
+    resolution: {integrity: sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==}
+    engines: {node: '>=16.13.0'}
     hasBin: true
     peerDependencies:
       '@google-cloud/spanner': ^5.18.0
@@ -19134,11 +19277,11 @@ packages:
       ioredis: 5.3.2
       mkdirp: 2.1.6
       pg: 8.11.3
-      reflect-metadata: 0.1.14
+      reflect-metadata: 0.2.1
       sha.js: 2.4.11
       tslib: 2.6.2
       uuid: 9.0.1
-      yargs: 17.6.2
+      yargs: 17.7.2
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -19462,7 +19605,7 @@ packages:
       core-util-is: 1.0.2
       extsprintf: 1.3.0
 
-  /vite-node@0.34.6(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0):
+  /vite-node@0.34.6(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0):
     resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -19472,7 +19615,7 @@ packages:
       mlly: 1.5.0
       pathe: 1.1.2
       picocolors: 1.0.0
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -19488,7 +19631,7 @@ packages:
     resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==}
     dev: true
 
-  /vite@5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0):
+  /vite@5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0):
     resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==}
     engines: {node: ^18.0.0 || >=20.0.0}
     hasBin: true
@@ -19516,7 +19659,7 @@ packages:
       terser:
         optional: true
     dependencies:
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       esbuild: 0.19.11
       postcss: 8.4.33
       rollup: 4.9.6
@@ -19570,7 +19713,7 @@ packages:
     dependencies:
       '@types/chai': 4.3.11
       '@types/chai-subset': 1.3.5
-      '@types/node': 20.11.5
+      '@types/node': 20.11.10
       '@vitest/expect': 0.34.6
       '@vitest/runner': 0.34.6
       '@vitest/snapshot': 0.34.6
@@ -19590,8 +19733,8 @@ packages:
       strip-literal: 1.3.0
       tinybench: 2.6.0
       tinypool: 0.7.0
-      vite: 5.0.12(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
-      vite-node: 0.34.6(@types/node@20.11.5)(sass@1.70.0)(terser@5.27.0)
+      vite: 5.0.12(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
+      vite-node: 0.34.6(@types/node@20.11.10)(sass@1.70.0)(terser@5.27.0)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - less
@@ -19609,9 +19752,11 @@ packages:
 
   /vscode-oniguruma@1.7.0:
     resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
+    dev: true
 
   /vscode-textmate@8.0.0:
     resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
+    dev: true
 
   /vue-component-type-helpers@1.8.27:
     resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
@@ -19641,8 +19786,8 @@ packages:
     dependencies:
       '@babel/parser': 7.23.6
       '@babel/types': 7.23.4
-      '@vue/compiler-dom': 3.4.3
-      '@vue/compiler-sfc': 3.4.3
+      '@vue/compiler-dom': 3.4.15
+      '@vue/compiler-sfc': 3.4.15
       ast-types: 0.14.2
       hash-sum: 2.0.0
       lru-cache: 8.0.4
@@ -19654,8 +19799,8 @@ packages:
       - vue
     dev: true
 
-  /vue-eslint-parser@9.4.0(eslint@8.56.0):
-    resolution: {integrity: sha512-7KsNBb6gHFA75BtneJsoK/dbZ281whUIwFYdQxA68QrCrGMXYzUMbPDHGcOQ0OocIVKrWSKWXZ4mL7tonCXoUw==}
+  /vue-eslint-parser@9.4.2(eslint@8.56.0):
+    resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
     engines: {node: ^14.17.0 || >=16.0.0}
     peerDependencies:
       eslint: '>=6.0.0'
@@ -20133,6 +20278,7 @@ packages:
       string-width: 4.2.3
       y18n: 5.0.8
       yargs-parser: 21.1.1
+    dev: true
 
   /yargs@17.7.2:
     resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
@@ -20145,7 +20291,6 @@ packages:
       string-width: 4.2.3
       y18n: 5.0.8
       yargs-parser: 21.1.1
-    dev: true
 
   /yauzl@2.10.0:
     resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
diff --git a/scripts/build-assets.mjs b/scripts/build-assets.mjs
index 22f0871caa..6267e5c963 100644
--- a/scripts/build-assets.mjs
+++ b/scripts/build-assets.mjs
@@ -35,13 +35,6 @@ async function copyFrontendLocales() {
   }
 }
 
-async function copyFrontendShikiAssets() {
-  await fs.cp('./packages/frontend/node_modules/shiki/dist', './built/_frontend_dist_/shiki/dist', { dereference: true, recursive: true });
-  await fs.cp('./packages/frontend/node_modules/shiki/languages', './built/_frontend_dist_/shiki/languages', { dereference: true, recursive: true });
-  await fs.cp('./packages/frontend/node_modules/aiscript-vscode/aiscript/syntaxes', './built/_frontend_dist_/shiki/languages', { dereference: true, recursive: true });
-  await fs.cp('./packages/frontend/node_modules/shiki/themes', './built/_frontend_dist_/shiki/themes', { dereference: true, recursive: true });
-}
-
 async function copyBackendViews() {
   await fs.cp('./packages/backend/src/server/web/views', './packages/backend/built/server/web/views', { recursive: true });
 }
@@ -81,7 +74,6 @@ async function build() {
     copyFrontendFonts(),
     copyFrontendTablerIcons(),
     copyFrontendLocales(),
-    copyFrontendShikiAssets(),
     copyBackendViews(),
     buildBackendScript(),
     buildBackendStyle(),
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index f4ba51b7db..c5c56ce739 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -28,7 +28,7 @@ await execa('pnpm', ['build-assets'], {
 	stderr: process.stderr,
 });
 
-await execa('pnpm', ['--filter', 'misskey-js', 'build'], {
+await execa('pnpm', ['--filter', 'misskey-js', 'ts'], {
 	cwd: _dirname + '/../',
 	stdout: process.stdout,
 	stderr: process.stderr,
@@ -40,13 +40,13 @@ await execa("pnpm", ['--filter', 'megalodon', 'build'], {
 	stderr: process.stderr,
 });
 
-await execa('pnpm', ['--filter', 'misskey-reversi', 'build'], {
+await execa('pnpm', ['--filter', 'misskey-reversi', 'build:tsc'], {
 	cwd: _dirname + '/../',
 	stdout: process.stdout,
 	stderr: process.stderr,
 });
 
-await execa('pnpm', ['--filter', 'misskey-bubble-game', 'build'], {
+await execa('pnpm', ['--filter', 'misskey-bubble-game', 'build:tsc'], {
 	cwd: _dirname + '/../',
 	stdout: process.stdout,
 	stderr: process.stderr,
@@ -82,6 +82,12 @@ execa('pnpm', ['--filter', 'sw', 'watch'], {
 	stderr: process.stderr,
 });
 
+execa('pnpm', ['--filter', 'misskey-js', 'watch'], {
+	cwd: _dirname + '/../',
+	stdout: process.stdout,
+	stderr: process.stderr,
+});
+
 execa('pnpm', ['--filter', 'misskey-reversi', 'watch'], {
 	cwd: _dirname + '/../',
 	stdout: process.stdout,